diff --git a/AGENTS.md b/AGENTS.md index 9811018b2..e5b7e513c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -198,20 +198,6 @@ 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 b3b365564..711114d60 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: -- **concurrency.md** - Unified concurrency design (supersedes multiplicity.md, fork.md, threads.md). Includes multiplicity demo and progress tracking. +- **multiplicity.md** - Multiple independent Perl runtimes (enables fork/threads/web concurrency) - **jsr223-perlonjava-web.md** - JSR-223 compliance and web server integration -- **multiplicity.md** / **fork.md** / **threads.md** - Superseded by concurrency.md +- **fork.md** / **threads.md** - Concurrency model and limitations These represent major architectural directions for the project. diff --git a/dev/design/concurrency.md b/dev/design/concurrency.md index 4e151a548..a4c26181a 100644 --- a/dev/design/concurrency.md +++ b/dev/design/concurrency.md @@ -892,683 +892,3 @@ 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 deleted file mode 100644 index c50bba828..000000000 --- a/dev/sandbox/multiplicity/MultiplicityDemo.java +++ /dev/null @@ -1,136 +0,0 @@ -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 deleted file mode 100644 index 33d18dcc4..000000000 --- a/dev/sandbox/multiplicity/README.md +++ /dev/null @@ -1,45 +0,0 @@ -# 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 deleted file mode 100644 index e81dadfa8..000000000 --- a/dev/sandbox/multiplicity/multiplicity_script1.pl +++ /dev/null @@ -1,26 +0,0 @@ -# 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 deleted file mode 100644 index 3ddda4079..000000000 --- a/dev/sandbox/multiplicity/multiplicity_script2.pl +++ /dev/null @@ -1,26 +0,0 @@ -# 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 deleted file mode 100644 index a34b0e7ae..000000000 --- a/dev/sandbox/multiplicity/multiplicity_script3.pl +++ /dev/null @@ -1,26 +0,0 @@ -# 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 deleted file mode 100755 index 2c6084d19..000000000 --- a/dev/sandbox/multiplicity/run_multiplicity_demo.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/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 66dc86e8a..9600e299f 100644 --- a/src/main/java/org/perlonjava/app/cli/Main.java +++ b/src/main/java/org/perlonjava/app/cli/Main.java @@ -4,7 +4,6 @@ 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; @@ -27,9 +26,6 @@ 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 cae9c06df..82fde36f4 100644 --- a/src/main/java/org/perlonjava/app/scriptengine/PerlLanguageProvider.java +++ b/src/main/java/org/perlonjava/app/scriptengine/PerlLanguageProvider.java @@ -26,7 +26,6 @@ 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.*; @@ -53,33 +52,10 @@ */ public class PerlLanguageProvider { - /** - * 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(); - } - } + private static boolean globalInitialized = false; public static void resetAll() { - ensureRuntimeInitialized(); - PerlRuntime.current().globalInitialized = false; + globalInitialized = false; resetAllGlobals(); DataSection.reset(); } @@ -109,8 +85,9 @@ public static RuntimeList executePerlCode(CompilerOptions compilerOptions, boolean isTopLevelScript, int callerContext) throws Exception { - ensureRuntimeInitialized(); - PerlRuntime runtime = PerlRuntime.current(); + // 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(); // 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. @@ -123,141 +100,125 @@ public static RuntimeList executePerlCode(CompilerOptions compilerOptions, // Store the isMainProgram flag in CompilerOptions for use during code generation compilerOptions.isMainProgram = isTopLevelScript; - // ---- 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; + 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 - 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); - } + 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); - - // 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() - ); + // Use caller's context if specified, otherwise default based on script type + int contextType = callerContext >= 0 ? callerContext : + (isTopLevelScript ? RuntimeContextType.VOID : RuntimeContextType.SCALAR); - if (!runtime.globalInitialized) { - GlobalContext.initializeGlobals(compilerOptions); - runtime.globalInitialized = true; - } + // 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() + ); + + if (!globalInitialized) { + GlobalContext.initializeGlobals(compilerOptions); + 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); - } - RuntimeIO.closeAllHandles(); - return null; // success + // 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); } - compilerOptions.code = null; // Throw away the source code to spare memory + RuntimeIO.closeAllHandles(); + return null; // success + } + 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; + 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 { 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 { - 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(); - } + // 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"); - - // 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); + // 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()); - // Compile to executable (compiler or interpreter based on flag) - runtimeCode = compileToExecutable(ast, ctx); - } finally { - COMPILE_LOCK.unlock(); + 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); - // ---- Execution phase (no lock — compiled code is thread-safe) ---- try { + // Compile to executable (compiler or interpreter based on flag) + RuntimeCode runtimeCode = compileToExecutable(ast, ctx); + + // Execute (unified path for both backends) return executeCode(runtimeCode, ctx, isTopLevelScript, callerContext); } finally { // Restore the caller's scope so require/do doesn't leak its scope to the caller. @@ -300,8 +261,6 @@ 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(); @@ -346,9 +305,9 @@ public static RuntimeList executePerlAST(Node ast, new RuntimeArray() ); - if (!PerlRuntime.current().globalInitialized) { + if (!globalInitialized) { GlobalContext.initializeGlobals(compilerOptions); - PerlRuntime.current().globalInitialized = true; + globalInitialized = true; } if (CompilerOptions.DEBUG_ENABLED) ctx.logDebug("Using provided AST"); @@ -618,58 +577,51 @@ private static boolean needsInterpreterFallback(Throwable e) { * @throws Exception if compilation fails */ public static Object compilePerlCode(CompilerOptions compilerOptions) throws Exception { - ensureRuntimeInitialized(); + 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 - 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); - } + 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 (!PerlRuntime.current().globalInitialized) { - GlobalContext.initializeGlobals(compilerOptions); - PerlRuntime.current().globalInitialized = true; - } + if (!globalInitialized) { + GlobalContext.initializeGlobals(compilerOptions); + 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); - } finally { - COMPILE_LOCK.unlock(); - } + // Use unified compilation path (works for JSR 223 too!) + return compileToExecutable(ast, ctx); } } diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index 04b1d247b..59dcac611 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -3,9 +3,6 @@ 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; @@ -79,8 +76,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, thread-safe) - private static final java.util.concurrent.atomic.AtomicInteger nextCallsiteId = new java.util.concurrent.atomic.AtomicInteger(1); + // Callsite ID counter for /o modifier support (unique across all compilations) + private static int nextCallsiteId = 1; // Track last result register for expression chaining int lastResultReg = -1; // Target output register for ALIAS elimination (same save/restore pattern as currentCallContext). @@ -2429,7 +2426,7 @@ void compileVariableDeclaration(OperatorNode node, String op) { boolean isDeclaredReference = node.annotations != null && Boolean.TRUE.equals(node.annotations.get("isDeclaredReference")); - Integer beginId = RuntimeCode.getEvalBeginIds().get(sigilOp); + Integer beginId = RuntimeCode.evalBeginIds.get(sigilOp); if (beginId != null) { // BEGIN-captured variable: use RETRIEVE_BEGIN_* (destructive removal from global storage) int persistId = beginId; @@ -2834,7 +2831,7 @@ void compileVariableDeclaration(OperatorNode node, String op) { continue; } - Integer beginId2 = RuntimeCode.getEvalBeginIds().get(sigilOp); + Integer beginId2 = RuntimeCode.evalBeginIds.get(sigilOp); if (beginId2 != null || op.equals("state")) { int persistId = beginId2 != null ? beginId2 : sigilOp.id; int reg = allocateRegister(); @@ -4464,7 +4461,7 @@ int allocateRegister() { * Each callsite with /o gets a unique ID so the pattern is compiled only once per callsite. */ int allocateCallsiteId() { - return nextCallsiteId.getAndIncrement(); + return nextCallsiteId++; } int allocateOutputRegister() { @@ -4878,7 +4875,7 @@ private void visitNamedSubroutine(SubroutineNode node) { int beginId = 0; if (!closureVarIndices.isEmpty()) { - beginId = EmitterMethodCreator.classCounter.getAndIncrement(); + beginId = EmitterMethodCreator.classCounter++; // Store each closure variable in PersistentVariable globals for (int i = 0; i < closureVarNames.size(); i++) { @@ -5002,10 +4999,6 @@ 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. @@ -5021,141 +5014,7 @@ private void visitAnonymousSubroutine(SubroutineNode node) { closureCapturedVarNames.addAll(closureVarNames); - // 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) { + // Step 3: Create a new BytecodeCompiler for the subroutine body // 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 52f002a9d..a5d166f7e 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.getGlobalCodeRefsMap().put(name, codeRef) + // Store global code: GlobalVariable.globalCodeRefs.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.getGlobalCodeRefsMap().put(name, codeRef); + GlobalVariable.globalCodeRefs.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.decrementEvalDepth(); + RuntimeCode.evalDepth--; break; } return result; @@ -1175,7 +1175,7 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c DynamicVariableManager.popToLocalLevel(savedLevel); } pc = evalCatchStack.pop(); - RuntimeCode.decrementEvalDepth(); + RuntimeCode.evalDepth--; 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.incrementEvalDepth(); + RuntimeCode.evalDepth++; // 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.decrementEvalDepth(); + RuntimeCode.evalDepth--; } 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.decrementEvalDepth(); + RuntimeCode.evalDepth--; 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.decrementEvalDepth(); + RuntimeCode.evalDepth--; // 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 b9a98db35..1153cf356 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.getEvalBeginIds().get(sigilOp); + Integer beginIdObj = RuntimeCode.evalBeginIds.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.getEvalBeginIds().get(sigilOp); + Integer beginIdArr = RuntimeCode.evalBeginIds.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.getEvalBeginIds().get(sigilOp); + Integer beginIdHash = RuntimeCode.evalBeginIds.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.getEvalBeginIds().get(sigilOp); + Integer beginIdList = RuntimeCode.evalBeginIds.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 e9064cbe7..0172c88b7 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java @@ -1,7 +1,6 @@ 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; @@ -110,182 +109,174 @@ public static RuntimeList evalStringList(String perlCode, // Step 1: Clear $@ at start of eval GlobalVariable.getGlobalVariable("main::@").set(""); - // 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()); - } + // 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 + // 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() ); + 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; + } - 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; - } - - 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 + 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; } - - capturedList.add(value); - // Map to new register index starting at 3 - adjustedRegistry.put(varName, 3 + captureIndex); - captureIndex++; + } 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 - ); - 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 + ); + 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.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 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 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 6: Execute the compiled code (outside the lock — execution is thread-safe). + // Step 6: Execute the compiled code. // 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 @@ -299,11 +290,11 @@ public static RuntimeList evalStringList(String perlCode, InterpreterState.currentPackage.get().set(savedPkg); RuntimeArray args = new RuntimeArray(); // Empty @_ RuntimeList result; - RuntimeCode.incrementEvalDepth(); + RuntimeCode.evalDepth++; try { result = evalCode.apply(args, callContext); } finally { - RuntimeCode.decrementEvalDepth(); + RuntimeCode.evalDepth--; DynamicVariableManager.popToLocalLevel(pkgLevel); } evalTrace("EvalStringHandler exec ok ctx=" + callContext + @@ -337,71 +328,63 @@ public static RuntimeScalar evalString(String perlCode, // Clear $@ at start GlobalVariable.getGlobalVariable("main::@").set(""); - // 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 - ); - - 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)); - } - - // 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); - } + // 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)); + } - // Attach captured variables - evalCode = evalCode.withCapturedVars(capturedVars); - } finally { - PerlLanguageProvider.COMPILE_LOCK.unlock(); + // 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); } - // Execute outside the lock — execution is thread-safe + // Attach captured variables + evalCode = evalCode.withCapturedVars(capturedVars); + // Scope currentPackage around eval — see Step 6 comment in evalStringHelper above. int pkgLevel = DynamicVariableManager.getLocalLevel(); String savedPkg = InterpreterState.currentPackage.get().toString(); @@ -409,11 +392,11 @@ public static RuntimeScalar evalString(String perlCode, InterpreterState.currentPackage.get().set(savedPkg); RuntimeArray args = new RuntimeArray(); RuntimeList result; - RuntimeCode.incrementEvalDepth(); + RuntimeCode.evalDepth++; try { result = evalCode.apply(args, RuntimeContextType.SCALAR); } finally { - RuntimeCode.decrementEvalDepth(); + RuntimeCode.evalDepth--; 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 05463683e..c218df163 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java +++ b/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java @@ -1,6 +1,5 @@ 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; @@ -889,15 +888,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++) { @@ -905,43 +904,35 @@ public static int executeCreateClosure(int[] bytecode, int pc, RuntimeBase[] reg capturedVars[i] = registers[captureReg]; } - 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; + // 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++; } + } + 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 d28451abc..8ecdcc620 100644 --- a/src/main/java/org/perlonjava/backend/jvm/ByteCodeSourceMapper.java +++ b/src/main/java/org/perlonjava/backend/jvm/ByteCodeSourceMapper.java @@ -4,7 +4,6 @@ import java.util.ArrayList; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.TreeMap; @@ -14,45 +13,32 @@ * resolution for stack traces at runtime. */ public class ByteCodeSourceMapper { + // Maps source files to their debug information + private static final Map sourceFiles = 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 package names to optimize memory usage + private static final ArrayList packageNamePool = new ArrayList<>(); + private static final Map packageNameToId = new HashMap<>(); - /** Returns the current PerlRuntime's source-mapper state. */ - private static State state() { - return org.perlonjava.runtime.runtimetypes.PerlRuntime.current().sourceMapperState; - } + // Pool of file names to optimize memory usage + private static final ArrayList fileNamePool = new ArrayList<>(); + private static final Map fileNameToId = new HashMap<>(); + + // Pool of subroutine names to optimize memory usage + private static final ArrayList subroutineNamePool = new ArrayList<>(); + private static final Map subroutineNameToId = new HashMap<>(); public static void resetAll() { - state().resetAll(); + sourceFiles.clear(); + + packageNamePool.clear(); + packageNameToId.clear(); + + fileNamePool.clear(); + fileNameToId.clear(); + + subroutineNamePool.clear(); + subroutineNameToId.clear(); } /** @@ -62,10 +48,9 @@ public static void resetAll() { * @return The unique identifier for the package */ private static int getOrCreatePackageId(String packageName) { - State s = state(); - return s.packageNameToId.computeIfAbsent(packageName, name -> { - s.packageNamePool.add(name); - return s.packageNamePool.size() - 1; + return packageNameToId.computeIfAbsent(packageName, name -> { + packageNamePool.add(name); + return packageNamePool.size() - 1; }); } @@ -76,10 +61,9 @@ private static int getOrCreatePackageId(String packageName) { * @return The unique identifier for the file */ private static int getOrCreateFileId(String fileName) { - State s = state(); - return s.fileNameToId.computeIfAbsent(fileName, name -> { - s.fileNamePool.add(name); - return s.fileNamePool.size() - 1; + return fileNameToId.computeIfAbsent(fileName, name -> { + fileNamePool.add(name); + return fileNamePool.size() - 1; }); } @@ -90,10 +74,9 @@ private static int getOrCreateFileId(String fileName) { * @return The unique identifier for the subroutine name */ private static int getOrCreateSubroutineId(String subroutineName) { - State s = state(); - return s.subroutineNameToId.computeIfAbsent(subroutineName, name -> { - s.subroutineNamePool.add(name); - return s.subroutineNamePool.size() - 1; + return subroutineNameToId.computeIfAbsent(subroutineName, name -> { + subroutineNamePool.add(name); + return subroutineNamePool.size() - 1; }); } @@ -104,7 +87,7 @@ private static int getOrCreateSubroutineId(String subroutineName) { */ static void setDebugInfoFileName(EmitterContext ctx) { int fileId = getOrCreateFileId(ctx.compilerOptions.fileName); - state().sourceFiles.computeIfAbsent(fileId, SourceFileInfo::new); + sourceFiles.computeIfAbsent(fileId, SourceFileInfo::new); ctx.cw.visitSource(ctx.compilerOptions.fileName, null); } @@ -143,14 +126,13 @@ 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 = s.sourceFiles.computeIfAbsent(fileId, SourceFileInfo::new); + SourceFileInfo info = sourceFiles.computeIfAbsent(fileId, SourceFileInfo::new); // Get current subroutine name (empty string for main code) String subroutineName = ctx.symbolTable.getCurrentSubroutine(); @@ -171,12 +153,6 @@ 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; } @@ -198,7 +174,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 = s.fileNamePool.get(nearbyEntry.getValue().sourceFileNameId()); + String nearbySourceFile = fileNamePool.get(nearbyEntry.getValue().sourceFileNameId()); if (!nearbySourceFile.equals(ctx.compilerOptions.fileName)) { // Nearby entry has #line-adjusted filename - inherit it sourceFileName = nearbySourceFile; @@ -232,13 +208,12 @@ 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) { - State s = state(); - int fileId = s.fileNameToId.getOrDefault(fileName, -1); + int fileId = fileNameToId.getOrDefault(fileName, -1); if (fileId == -1) { return null; } - SourceFileInfo info = s.sourceFiles.get(fileId); + SourceFileInfo info = sourceFiles.get(fileId); if (info == null) { return null; } @@ -248,11 +223,7 @@ public static String getPackageAtLocation(String fileName, int tokenIndex) { return null; } - 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); - } + String pkg = packageNamePool.get(entry.getValue().packageNameId()); return pkg; } @@ -263,11 +234,10 @@ public static String getPackageAtLocation(String fileName, int tokenIndex) { * @return The corresponding source code location */ public static SourceLocation parseStackTraceElement(StackTraceElement element, HashMap locationToClassName) { - State s = state(); - int fileId = s.fileNameToId.getOrDefault(element.getFileName(), -1); + int fileId = fileNameToId.getOrDefault(element.getFileName(), -1); int tokenIndex = element.getLineNumber(); - SourceFileInfo info = s.sourceFiles.get(fileId); + SourceFileInfo info = sourceFiles.get(fileId); if (info == null) { return new SourceLocation(element.getFileName(), "", tokenIndex, null); } @@ -281,9 +251,9 @@ public static SourceLocation parseStackTraceElement(StackTraceElement element, H LineInfo lineInfo = entry.getValue(); // Get the #line directive-adjusted source filename for caller() reporting - String sourceFileName = s.fileNamePool.get(lineInfo.sourceFileNameId()); + String sourceFileName = fileNamePool.get(lineInfo.sourceFileNameId()); int lineNumber = lineInfo.lineNumber(); - String packageName = s.packageNamePool.get(lineInfo.packageNameId()); + String packageName = 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. @@ -296,10 +266,7 @@ public static SourceLocation parseStackTraceElement(StackTraceElement element, H boolean foundLineDirective = false; while (lowerEntry != null && (entry.getKey() - lowerEntry.getKey()) < 300) { - 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()); - } + String lowerSourceFile = fileNamePool.get(lowerEntry.getValue().sourceFileNameId()); 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 @@ -326,10 +293,7 @@ public static SourceLocation parseStackTraceElement(StackTraceElement element, H int estimatedExtraLines = tokenDistFromLineDirective / 6; lineNumber = lowerEntry.getValue().lineNumber() + estimatedExtraLines; - 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); - } + packageName = packageNamePool.get(lowerEntry.getValue().packageNameId()); break; } // This lower entry still has the original file, keep looking @@ -341,20 +305,14 @@ 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 = 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); - } + String higherSourceFile = fileNamePool.get(higherEntry.getValue().sourceFileNameId()); 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 = s.packageNamePool.get(higherEntry.getValue().packageNameId()); - if (System.getenv("DEBUG_CALLER") != null) { - System.err.println("DEBUG parseStackTraceElement: APPLYING higherEntry sourceFile=" + sourceFileName + " adjustedLine=" + lineNumber); - } + packageName = packageNamePool.get(higherEntry.getValue().packageNameId()); break; } // This higher entry still has the original file, keep looking @@ -366,7 +324,7 @@ public static SourceLocation parseStackTraceElement(StackTraceElement element, H // Retrieve subroutine name - String subroutineName = s.subroutineNamePool.get(lineInfo.subroutineNameId()); + String subroutineName = 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 8594d3c99..e614ded16 100644 --- a/src/main/java/org/perlonjava/backend/jvm/Dereference.java +++ b/src/main/java/org/perlonjava/backend/jvm/Dereference.java @@ -15,9 +15,8 @@ import static org.perlonjava.runtime.perlmodule.Strict.HINT_STRICT_REFS; public class Dereference { - // 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); + // Callsite ID counter for inline method caching (unique across all compilations) + private static int nextMethodCallsiteId = 0; /** * Handles the postfix `[]` operator. @@ -966,7 +965,7 @@ static void handleArrowOperator(EmitterVisitor emitterVisitor, BinaryOperatorNod mv.visitVarInsn(Opcodes.ISTORE, callContextSlot); // Allocate a unique callsite ID for inline method caching - int callsiteId = nextMethodCallsiteId.getAndIncrement(); + int callsiteId = nextMethodCallsiteId++; 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 77b7e6ab0..aff8c7eec 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.getGlobalClassLoader() + *
  • Global ClassLoader: All eval classes use GlobalVariable.globalClassLoader * 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.getAndIncrement(); + int counter = EmitterMethodCreator.classCounter++; // 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.getEvalContext().put(evalTag, evalCtx); + RuntimeCode.evalContext.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 db6682c91..eaccb8cf3 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.visitMethodInsn(Opcodes.INVOKESTATIC, + emitterVisitor.ctx.mv.visitFieldInsn(Opcodes.PUTSTATIC, "org/perlonjava/runtime/runtimetypes/RuntimeIO", - "setLastReadlineHandleName", "(Ljava/lang/String;)V", false); + "lastReadlineHandleName", "Ljava/lang/String;"); // 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 2f9212822..e5e833e90 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitRegex.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitRegex.java @@ -17,9 +17,8 @@ * transliteration and replacement. */ public class EmitRegex { - // 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 + // 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 /** * Handles the binding regex operation where a variable is bound to a regex operation. @@ -281,7 +280,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.getAndIncrement(); + int callsiteId = nextCallsiteId++; 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 a94e25611..0c1e14fbd 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.getAnonSubs().put(subCtx.javaClassInfo.javaClassName, generatedClass); // Cache the class + RuntimeCode.anonSubs.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,15 +291,14 @@ 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.getInterpretedSubs().put(fallbackKey, fallback.interpretedCode); + RuntimeCode.interpretedSubs.put(fallbackKey, fallback.interpretedCode); // Generate bytecode to retrieve and configure the InterpretedCode // 1. Load the InterpretedCode from the map - mv.visitMethodInsn(Opcodes.INVOKESTATIC, + mv.visitFieldInsn(Opcodes.GETSTATIC, "org/perlonjava/runtime/runtimetypes/RuntimeCode", - "getInterpretedSubs", - "()Ljava/util/HashMap;", - false); + "interpretedSubs", + "Ljava/util/HashMap;"); 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 4b728e259..0ecce00c2 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.getEvalBeginIds().get(sigilNode); + Integer beginId = RuntimeCode.evalBeginIds.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 944a9eb56..b1470199b 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java @@ -14,7 +14,6 @@ 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; @@ -25,7 +24,6 @@ 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; @@ -49,13 +47,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 final int skipVariables = 3; - // Counter for generating unique class names (thread-safe) - public static final AtomicInteger classCounter = new AtomicInteger(0); + public static int skipVariables = 3; + // Counter for generating unique class names + public static int classCounter = 0; // Generate a unique internal class name public static String generateClassName() { - return "org/perlonjava/anon" + classCounter.getAndIncrement(); + return "org/perlonjava/anon" + classCounter++; } private static String insnToString(AbstractInsnNode n) { @@ -630,14 +628,8 @@ 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; - // 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); - } + 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 @@ -1243,7 +1235,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.getGlobalClassLoader()) { + ClassLoader verifyLoader = new ClassLoader(GlobalVariable.globalClassLoader) { @Override protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { if (name.equals(thisClassNameDot)) { @@ -1285,7 +1277,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.getGlobalClassLoader()) { + ClassLoader verifyLoader = new ClassLoader(GlobalVariable.globalClassLoader) { @Override protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { if (name.equals(thisClassNameDot)) { @@ -1335,7 +1327,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.getGlobalClassLoader()) { + ClassLoader verifyLoader = new ClassLoader(GlobalVariable.globalClassLoader) { @Override protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { if (name.equals(thisClassNameDot)) { @@ -1489,7 +1481,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.getGlobalClassLoader(); + CustomClassLoader loader = GlobalVariable.globalClassLoader; // Create a "Java" class name with dots instead of slashes String javaClassNameDot = ctx.javaClassInfo.javaClassName.replace('/', '.'); @@ -1830,21 +1822,27 @@ private static void applyCompilerFlagNodes(EmitterContext ctx, Node ast) { /** * Emits bytecode to increment RuntimeCode.evalDepth (for $^S support). - * Calls RuntimeCode.incrementEvalDepth() static method. - * Stack effect: net 0. + * Stack effect: net 0 (pushes 2, pops 2). */ private static void emitEvalDepthIncrement(MethodVisitor mv) { - mv.visitMethodInsn(Opcodes.INVOKESTATIC, - "org/perlonjava/runtime/runtimetypes/RuntimeCode", "incrementEvalDepth", "()V", false); + 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"); } /** * Emits bytecode to decrement RuntimeCode.evalDepth (for $^S support). - * Calls RuntimeCode.decrementEvalDepth() static method. - * Stack effect: net 0. + * Stack effect: net 0 (pushes 2, pops 2). */ private static void emitEvalDepthDecrement(MethodVisitor mv) { - mv.visitMethodInsn(Opcodes.INVOKESTATIC, - "org/perlonjava/runtime/runtimetypes/RuntimeCode", "decrementEvalDepth", "()V", false); + 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"); } } diff --git a/src/main/java/org/perlonjava/backend/jvm/JvmClosureTemplate.java b/src/main/java/org/perlonjava/backend/jvm/JvmClosureTemplate.java deleted file mode 100644 index bdb3c134b..000000000 --- a/src/main/java/org/perlonjava/backend/jvm/JvmClosureTemplate.java +++ /dev/null @@ -1,131 +0,0 @@ -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 fc2fe3d9e..2f019ac46 100644 --- a/src/main/java/org/perlonjava/backend/jvm/astrefactor/LargeBlockRefactorer.java +++ b/src/main/java/org/perlonjava/backend/jvm/astrefactor/LargeBlockRefactorer.java @@ -24,6 +24,9 @@ */ 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) { @@ -100,10 +103,9 @@ 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 - // 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()) { + controlFlowDetector.reset(); + controlFlowDetector.scan(node); + if (controlFlowDetector.hasUnsafeControlFlow()) { return false; } diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index 27b2a98b9..ae081404f 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 = "d7b8bc7ee"; + public static final String gitCommitId = "ffc466124"; /** * 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-11"; + public static final String gitCommitDate = "2026-04-10"; /** * 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 11 2026 08:21:54"; + public static final String buildTimestamp = "Apr 10 2026 22:16:43"; // 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 7b7868380..1b8c44e41 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.getGlobalCodeRefsMap().get(fullName); + RuntimeScalar codeRef = GlobalVariable.globalCodeRefs.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.getGlobalCodeRefsMap().get(fullName); + RuntimeScalar codeRef = GlobalVariable.globalCodeRefs.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 e9227b528..dd7814604 100644 --- a/src/main/java/org/perlonjava/frontend/analysis/RegexUsageDetector.java +++ b/src/main/java/org/perlonjava/frontend/analysis/RegexUsageDetector.java @@ -21,11 +21,10 @@ public class RegexUsageDetector { /** - * Unary operators that perform regex matching/substitution, - * or that may dynamically introduce regex operations (eval STRING). + * Unary operators that perform regex matching/substitution. */ private static final java.util.Set REGEX_OPERATORS = - java.util.Set.of("matchRegex", "replaceRegex", "eval"); + java.util.Set.of("matchRegex", "replaceRegex"); /** * 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 53c740bdd..1167eaa03 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.getAndIncrement(); + operandNode.id = EmitterMethodCreator.classCounter++; } } @@ -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.setAutoloadEnabled(false); + InheritanceResolver.autoloadEnabled = false; RuntimeList codeList; try { codeList = Universal.can(canArgs, RuntimeContextType.SCALAR); } finally { - InheritanceResolver.setAutoloadEnabled(true); + InheritanceResolver.autoloadEnabled = 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 651c575e3..cd31f8070 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.getIsSubsMap().getOrDefault(fullName, false)) { + if (GlobalVariable.isSubs.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 452a2f3b5..2f6ce3e20 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.getEvalBeginIds().containsKey(ast); - int beginId = RuntimeCode.getEvalBeginIds().computeIfAbsent( + isFromOuterScope = RuntimeCode.evalBeginIds.containsKey(ast); + int beginId = RuntimeCode.evalBeginIds.computeIfAbsent( ast, - k -> EmitterMethodCreator.classCounter.getAndIncrement()); + k -> EmitterMethodCreator.classCounter++); 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.getGlobalArraysMap().put(fullName, (RuntimeArray) runtimeValue); + GlobalVariable.globalArrays.put(fullName, (RuntimeArray) runtimeValue); if (CompilerOptions.DEBUG_ENABLED) parser.ctx.logDebug("BEGIN block: Aliased array " + fullName); } else if (runtimeValue instanceof RuntimeHash) { - GlobalVariable.getGlobalHashesMap().put(fullName, (RuntimeHash) runtimeValue); + GlobalVariable.globalHashes.put(fullName, (RuntimeHash) runtimeValue); if (CompilerOptions.DEBUG_ENABLED) parser.ctx.logDebug("BEGIN block: Aliased hash " + fullName); } else if (runtimeValue instanceof RuntimeScalar) { - GlobalVariable.getGlobalVariablesMap().put(fullName, (RuntimeScalar) runtimeValue); + GlobalVariable.globalVariables.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 5b430c7e8..60ddc0cd6 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.setAutoloadEnabled(false); + InheritanceResolver.autoloadEnabled = false; try { codeList = Universal.can(canArgs, RuntimeContextType.SCALAR); } finally { - InheritanceResolver.setAutoloadEnabled(true); + InheritanceResolver.autoloadEnabled = 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 - GlobalVariable.getPackageExistsCacheMap().put(packageName, true); + packageExistsCache.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 8961249d5..2b281678f 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.getAndIncrement(); + innerVarNode.id = EmitterMethodCreator.classCounter++; } // 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 161febe31..cd4c45fd5 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.getPackageExistsCacheMap().get(packageName); + Boolean isPackage = GlobalVariable.packageExistsCache.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.getPackageExistsCacheMap().get(qualifiedName); + Boolean qualifiedResult = GlobalVariable.packageExistsCache.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.getPackageExistsCacheMap().put(qualifiedSubName, false); + GlobalVariable.packageExistsCache.put(qualifiedSubName, false); } else if (subName != null) { - GlobalVariable.getPackageExistsCacheMap().put(subName, false); + GlobalVariable.packageExistsCache.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.getEvalBeginIds().computeIfAbsent( + int beginId = RuntimeCode.evalBeginIds.computeIfAbsent( ast, - k -> EmitterMethodCreator.classCounter.getAndIncrement()); + k -> EmitterMethodCreator.classCounter++); 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.setAutoloadEnabled(false); + InheritanceResolver.autoloadEnabled = false; RuntimeList codeList; try { codeList = Universal.can(canArgs, RuntimeContextType.SCALAR); } finally { - InheritanceResolver.setAutoloadEnabled(true); + InheritanceResolver.autoloadEnabled = 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 c69d2d247..aae32cf96 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.getGlobalCodeRefsMap(). + * Install a RuntimeCode wrapper into GlobalVariable.globalCodeRefs. */ 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 8d5106b1f..df21571dc 100644 --- a/src/main/java/org/perlonjava/runtime/HintHashRegistry.java +++ b/src/main/java/org/perlonjava/runtime/HintHashRegistry.java @@ -2,7 +2,6 @@ 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; @@ -23,9 +22,6 @@ * 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 { @@ -42,6 +38,15 @@ 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 ---- /** @@ -105,7 +110,7 @@ public static int snapshotCurrentHintHash() { * @param id the snapshot ID (0 = empty/no hints) */ public static void setCallSiteHintHashId(int id) { - PerlRuntime.current().hintCallSiteSnapshotId = id; + callSiteSnapshotId.set(id); } /** @@ -115,11 +120,11 @@ public static void setCallSiteHintHashId(int id) { * Called by RuntimeCode.apply() before entering a subroutine. */ public static void pushCallerHintHash() { - PerlRuntime rt = PerlRuntime.current(); - rt.hintCallerSnapshotIdStack.push(rt.hintCallSiteSnapshotId); + int currentId = callSiteSnapshotId.get(); + callerSnapshotIdStack.get().push(currentId); // 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. - rt.hintCallSiteSnapshotId = 0; + callSiteSnapshotId.set(0); } /** @@ -128,10 +133,12 @@ public static void pushCallerHintHash() { * Called by RuntimeCode.apply() after a subroutine returns. */ public static void popCallerHintHash() { - PerlRuntime rt = PerlRuntime.current(); - Deque stack = rt.hintCallerSnapshotIdStack; + Deque stack = callerSnapshotIdStack.get(); if (!stack.isEmpty()) { - rt.hintCallSiteSnapshotId = stack.pop(); + 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); } } @@ -143,7 +150,7 @@ public static void popCallerHintHash() { * @return The hint hash map, or null if not available */ public static Map getCallerHintHashAtFrame(int frame) { - Deque stack = PerlRuntime.current().hintCallerSnapshotIdStack; + Deque stack = callerSnapshotIdStack.get(); if (stack.isEmpty()) { return null; } @@ -165,7 +172,7 @@ public static Map getCallerHintHashAtFrame(int frame) { * @return the hint hash map, or null if empty/not set */ public static Map getCurrentCallSiteHintHash() { - int id = PerlRuntime.current().hintCallSiteSnapshotId; + int id = callSiteSnapshotId.get(); if (id == 0) return null; return snapshotRegistry.get(id); } @@ -178,8 +185,7 @@ public static void clear() { compileTimeStack.clear(); snapshotRegistry.clear(); nextSnapshotId.set(0); - PerlRuntime rt = PerlRuntime.current(); - rt.hintCallSiteSnapshotId = 0; - rt.hintCallerSnapshotIdStack.clear(); + callSiteSnapshotId.set(0); + callerSnapshotIdStack.get().clear(); } } diff --git a/src/main/java/org/perlonjava/runtime/WarningBitsRegistry.java b/src/main/java/org/perlonjava/runtime/WarningBitsRegistry.java index 02c259e19..7e6a4898f 100644 --- a/src/main/java/org/perlonjava/runtime/WarningBitsRegistry.java +++ b/src/main/java/org/perlonjava/runtime/WarningBitsRegistry.java @@ -1,12 +1,11 @@ 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; @@ -21,17 +20,54 @@ * * At runtime, caller() looks up warning bits by class name. * - * 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. + * Additionally, a ThreadLocal stack tracks the "current" warning bits + * for runtime code that needs to check FATAL warnings. */ 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, @@ -68,7 +104,7 @@ public static String get(String className) { */ public static void pushCurrent(String bits) { if (bits != null) { - PerlRuntime.current().warningCurrentBitsStack.push(bits); + currentBitsStack.get().push(bits); } } @@ -77,7 +113,7 @@ public static void pushCurrent(String bits) { * Called when exiting a subroutine or code block. */ public static void popCurrent() { - Deque stack = PerlRuntime.current().warningCurrentBitsStack; + Deque stack = currentBitsStack.get(); if (!stack.isEmpty()) { stack.pop(); } @@ -90,7 +126,7 @@ public static void popCurrent() { * @return The current warning bits string, or null if stack is empty */ public static String getCurrent() { - Deque stack = PerlRuntime.current().warningCurrentBitsStack; + Deque stack = currentBitsStack.get(); return stack.isEmpty() ? null : stack.peek(); } @@ -100,14 +136,13 @@ public static String getCurrent() { */ public static void clear() { registry.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(); + currentBitsStack.get().clear(); + callSiteBits.remove(); + callerBitsStack.get().clear(); + callSiteHints.remove(); + callerHintsStack.get().clear(); + callSiteHintHash.get().clear(); + callerHintHashStack.get().clear(); } /** @@ -118,7 +153,7 @@ public static void clear() { * @param bits The warning bits string for the current call site */ public static void setCallSiteBits(String bits) { - PerlRuntime.current().warningCallSiteBits = bits; + callSiteBits.set(bits); } /** @@ -127,7 +162,7 @@ public static void setCallSiteBits(String bits) { * @return The current call-site warning bits, or null if not set */ public static String getCallSiteBits() { - return PerlRuntime.current().warningCallSiteBits; + return callSiteBits.get(); } /** @@ -136,9 +171,8 @@ public static String getCallSiteBits() { * This preserves the caller's warning bits so caller()[9] can retrieve them. */ public static void pushCallerBits() { - PerlRuntime rt = PerlRuntime.current(); - String bits = rt.warningCallSiteBits; - rt.warningCallerBitsStack.push(bits != null ? bits : ""); + String bits = callSiteBits.get(); + callerBitsStack.get().push(bits != null ? bits : ""); } /** @@ -146,7 +180,7 @@ public static void pushCallerBits() { * Called by RuntimeCode.apply() after a subroutine returns. */ public static void popCallerBits() { - Deque stack = PerlRuntime.current().warningCallerBitsStack; + Deque stack = callerBitsStack.get(); if (!stack.isEmpty()) { stack.pop(); } @@ -161,7 +195,7 @@ public static void popCallerBits() { * @return The warning bits string, or null if not available */ public static String getCallerBitsAtFrame(int frame) { - Deque stack = PerlRuntime.current().warningCallerBitsStack; + Deque stack = callerBitsStack.get(); if (stack.isEmpty()) { return null; } @@ -180,7 +214,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(); @@ -195,7 +229,7 @@ public static int size() { * @param hints The $^H bitmask */ public static void setCallSiteHints(int hints) { - PerlRuntime.current().warningCallSiteHints = hints; + callSiteHints.set(hints); } /** @@ -204,7 +238,7 @@ public static void setCallSiteHints(int hints) { * @return The current call-site $^H value */ public static int getCallSiteHints() { - return PerlRuntime.current().warningCallSiteHints; + return callSiteHints.get(); } /** @@ -212,8 +246,7 @@ public static int getCallSiteHints() { * Called by RuntimeCode.apply() before entering a subroutine. */ public static void pushCallerHints() { - PerlRuntime rt = PerlRuntime.current(); - rt.warningCallerHintsStack.push(rt.warningCallSiteHints); + callerHintsStack.get().push(callSiteHints.get()); } /** @@ -221,7 +254,7 @@ public static void pushCallerHints() { * Called by RuntimeCode.apply() after a subroutine returns. */ public static void popCallerHints() { - Deque stack = PerlRuntime.current().warningCallerHintsStack; + Deque stack = callerHintsStack.get(); if (!stack.isEmpty()) { stack.pop(); } @@ -236,7 +269,7 @@ public static void popCallerHints() { * @return The $^H value, or -1 if not available */ public static int getCallerHintsAtFrame(int frame) { - Deque stack = PerlRuntime.current().warningCallerHintsStack; + Deque stack = callerHintsStack.get(); if (stack.isEmpty()) { return -1; } @@ -259,7 +292,7 @@ public static int getCallerHintsAtFrame(int frame) { * @param hintHash A snapshot of the %^H hash elements */ public static void setCallSiteHintHash(java.util.Map hintHash) { - PerlRuntime.current().warningCallSiteHintHash = hintHash != null ? new java.util.HashMap<>(hintHash) : new java.util.HashMap<>(); + callSiteHintHash.set(hintHash != null ? new java.util.HashMap<>(hintHash) : new java.util.HashMap<>()); } /** @@ -276,8 +309,7 @@ public static void snapshotCurrentHintHash() { * Called by RuntimeCode.apply() before entering a subroutine. */ public static void pushCallerHintHash() { - PerlRuntime rt = PerlRuntime.current(); - rt.warningCallerHintHashStack.push(new java.util.HashMap<>(rt.warningCallSiteHintHash)); + callerHintHashStack.get().push(new java.util.HashMap<>(callSiteHintHash.get())); } /** @@ -285,7 +317,7 @@ public static void pushCallerHintHash() { * Called by RuntimeCode.apply() after a subroutine returns. */ public static void popCallerHintHash() { - Deque> stack = PerlRuntime.current().warningCallerHintHashStack; + Deque> stack = callerHintHashStack.get(); if (!stack.isEmpty()) { stack.pop(); } @@ -300,7 +332,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 = PerlRuntime.current().warningCallerHintHashStack; + Deque> stack = callerHintHashStack.get(); 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 6c0d842ae..ca99a0812 100644 --- a/src/main/java/org/perlonjava/runtime/io/DirectoryIO.java +++ b/src/main/java/org/perlonjava/runtime/io/DirectoryIO.java @@ -1,7 +1,6 @@ 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; @@ -42,7 +41,7 @@ public DirectoryIO(DirectoryStream directoryStream, String directoryPath) // Resolve and store absolute path Path path = Paths.get(directoryPath); if (!path.isAbsolute()) { - path = Paths.get(PerlRuntime.getCwd(), directoryPath); + path = Paths.get(System.getProperty("user.dir"), 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 43550f1e9..38fcd7720 100644 --- a/src/main/java/org/perlonjava/runtime/io/PipeInputChannel.java +++ b/src/main/java/org/perlonjava/runtime/io/PipeInputChannel.java @@ -4,7 +4,6 @@ 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.*; @@ -92,7 +91,7 @@ public PipeInputChannel(List commandArgs) throws IOException { */ private void setupProcess(ProcessBuilder processBuilder) throws IOException { // Set working directory to current directory - String userDir = PerlRuntime.getCwd(); + String userDir = System.getProperty("user.dir"); processBuilder.directory(new File(userDir)); // Copy %ENV to the subprocess environment @@ -107,17 +106,9 @@ 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 265edee6f..f6976261b 100644 --- a/src/main/java/org/perlonjava/runtime/io/PipeOutputChannel.java +++ b/src/main/java/org/perlonjava/runtime/io/PipeOutputChannel.java @@ -4,7 +4,6 @@ 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.*; @@ -151,7 +150,7 @@ private void startProcessDirect(List commandArgs) throws IOException { */ private void setupProcess(ProcessBuilder processBuilder) throws IOException { // Set working directory to current directory - String userDir = PerlRuntime.getCwd(); + String userDir = System.getProperty("user.dir"); processBuilder.directory(new File(userDir)); // Copy %ENV to the subprocess environment @@ -167,17 +166,9 @@ 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) { @@ -200,10 +191,6 @@ 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 eaeba2685..00ca9f93a 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.getLinearizedClassesCache().get(cacheKey); + List result = InheritanceResolver.linearizedClassesCache.get(cacheKey); if (result == null) { Map> isaMap = new HashMap<>(); InheritanceResolver.populateIsaMap(className, isaMap); @@ -34,7 +34,7 @@ public static List linearizeC3(String className) { } } - InheritanceResolver.getLinearizedClassesCache().put(cacheKey, result); + InheritanceResolver.linearizedClassesCache.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 924fd526e..b5f8c4cc8 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.getLinearizedClassesCache().get(cacheKey); + List cached = InheritanceResolver.linearizedClassesCache.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.getLinearizedClassesCache().put(cacheKey, new ArrayList<>(result)); + InheritanceResolver.linearizedClassesCache.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 b3c63357d..81d79db45 100644 --- a/src/main/java/org/perlonjava/runtime/mro/InheritanceResolver.java +++ b/src/main/java/org/perlonjava/runtime/mro/InheritanceResolver.java @@ -10,50 +10,20 @@ * 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 - - // ---- 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; - } + // 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; /** * Sets the default MRO algorithm. @@ -61,7 +31,7 @@ private static MROAlgorithm getCurrentMRO() { * @param algorithm The MRO algorithm to use as default. */ public static void setDefaultMRO(MROAlgorithm algorithm) { - PerlRuntime.current().currentMRO = algorithm; + currentMRO = algorithm; invalidateCache(); } @@ -72,7 +42,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) { - getPackageMROMap().put(packageName, algorithm); + packageMRO.put(packageName, algorithm); invalidateCache(); } @@ -83,7 +53,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 getPackageMROMap().getOrDefault(packageName, getCurrentMRO()); + return packageMRO.getOrDefault(packageName, currentMRO); } /** @@ -93,21 +63,19 @@ 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, rt)) { - invalidateCacheForClass(className, rt); + if (hasIsaChanged(className)) { + invalidateCacheForClass(className); } - Map> cache = rt.linearizedClassesCache; // Check cache first - List cached = cache.get(className); + List cached = linearizedClassesCache.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 = rt.packageMRO.getOrDefault(className, rt.currentMRO); + MROAlgorithm mro = getPackageMRO(className); List result; switch (mro) { @@ -122,7 +90,7 @@ public static List linearizeHierarchy(String className) { } // Cache the result (store a copy to prevent external modifications) - cache.put(className, new ArrayList<>(result)); + linearizedClassesCache.put(className, new ArrayList<>(result)); return result; } @@ -130,10 +98,6 @@ 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 @@ -145,12 +109,11 @@ private static boolean hasIsaChanged(String className, PerlRuntime rt) { } } - Map> isCache = rt.isaStateCache; - List cachedIsa = isCache.get(className); + List cachedIsa = isaStateCache.get(className); // If ISA changed, update cache and return true if (!currentIsa.equals(cachedIsa)) { - isCache.put(className, currentIsa); + isaStateCache.put(className, currentIsa); return true; } @@ -161,19 +124,12 @@ private static boolean hasIsaChanged(String className, PerlRuntime rt) { * 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 - linCache.remove(className); - linCache.entrySet().removeIf(entry -> entry.getKey().startsWith(className + "::")); + linearizedClassesCache.remove(className); + linearizedClassesCache.entrySet().removeIf(entry -> entry.getKey().startsWith(className + "::")); // Remove from method cache (entries for this class and subclasses) - mCache.entrySet().removeIf(entry -> + methodCache.entrySet().removeIf(entry -> entry.getKey().startsWith(className + "::") || entry.getKey().contains("::" + className + "::")); // Could also notify dependents here if we had that information @@ -184,11 +140,10 @@ private static void invalidateCacheForClass(String className, PerlRuntime rt) { * This should be called whenever the class hierarchy or method definitions change. */ public static void invalidateCache() { - PerlRuntime rt = PerlRuntime.current(); - rt.methodCache.clear(); - rt.linearizedClassesCache.clear(); - rt.overloadContextCache.clear(); - rt.isaStateCache.clear(); + methodCache.clear(); + linearizedClassesCache.clear(); + overloadContextCache.clear(); + isaStateCache.clear(); // Also clear the inline method cache in RuntimeCode RuntimeCode.clearInlineMethodCache(); // Clear DESTROY-related caches (destroyClasses BitSet and destroyMethodCache) @@ -202,7 +157,7 @@ public static void invalidateCache() { * @return The cached OverloadContext, or null if not found. */ public static OverloadContext getCachedOverloadContext(int blessId) { - return getOverloadContextCache().get(blessId); + return overloadContextCache.get(blessId); } /** @@ -212,7 +167,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) { - getOverloadContextCache().put(blessId, context); + overloadContextCache.put(blessId, context); } /** @@ -222,7 +177,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 getMethodCache().get(normalizedMethodName); + return methodCache.get(normalizedMethodName); } /** @@ -232,7 +187,7 @@ public static RuntimeScalar getCachedMethod(String normalizedMethodName) { * @param method The RuntimeScalar representing the method to cache. */ public static void cacheMethod(String normalizedMethodName, RuntimeScalar method) { - getMethodCache().put(normalizedMethodName, method); + methodCache.put(normalizedMethodName, method); } /** @@ -314,8 +269,6 @@ 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 + "'"); @@ -335,18 +288,17 @@ public static RuntimeScalar findMethodInHierarchy(String methodName, String perl } // Check if ISA changed for this class - if so, invalidate relevant caches - if (hasIsaChanged(perlClassName, rt)) { - invalidateCacheForClass(perlClassName, rt); + if (hasIsaChanged(perlClassName)) { + invalidateCacheForClass(perlClassName); } // Check the method cache - handles both found and not-found cases - Map mCache = rt.methodCache; - if (mCache.containsKey(cacheKey)) { + if (methodCache.containsKey(cacheKey)) { if (TRACE_METHOD_RESOLUTION) { - System.err.println(" Found in cache: " + (mCache.get(cacheKey) != null ? "YES" : "NULL")); + System.err.println(" Found in cache: " + (methodCache.get(cacheKey) != null ? "YES" : "NULL")); System.err.flush(); } - return mCache.get(cacheKey); + return methodCache.get(cacheKey); } // Get the linearized inheritance hierarchy using the appropriate MRO @@ -376,7 +328,7 @@ public static RuntimeScalar findMethodInHierarchy(String methodName, String perl if (!codeRef.getDefinedBoolean()) { continue; } - mCache.put(cacheKey, codeRef); + cacheMethod(cacheKey, codeRef); if (TRACE_METHOD_RESOLUTION) { System.err.println(" FOUND method!"); System.err.flush(); @@ -388,7 +340,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 (rt.autoloadEnabled && !methodName.startsWith("(")) { + if (autoloadEnabled && !methodName.startsWith("(")) { for (int i = startFromIndex; i < linearizedClasses.size(); i++) { String className = linearizedClasses.get(i); String effectiveClassName = GlobalVariable.resolveStashAlias(className); @@ -407,7 +359,7 @@ public static RuntimeScalar findMethodInHierarchy(String methodName, String perl } else { autoloadCode.autoloadVariableName = autoloadName; } - mCache.put(cacheKey, autoload); + cacheMethod(cacheKey, autoload); return autoload; } } @@ -415,7 +367,7 @@ public static RuntimeScalar findMethodInHierarchy(String methodName, String perl } // Cache the fact that method was not found (using null) - mCache.put(cacheKey, null); + methodCache.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 80e56d6d3..abe071f92 100644 --- a/src/main/java/org/perlonjava/runtime/operators/Directory.java +++ b/src/main/java/org/perlonjava/runtime/operators/Directory.java @@ -93,8 +93,7 @@ public static RuntimeScalar chdir(RuntimeScalar runtimeScalar) { if (absoluteDir.exists() && absoluteDir.isDirectory()) { // Normalize the path to remove redundant . and .. components - // Update per-runtime CWD (not the JVM-global user.dir property) - PerlRuntime.current().cwd = absoluteDir.toPath().normalize().toString(); + System.setProperty("user.dir", 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 b08b96ef3..1f751551b 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.getSelectedHandle()); + return new RuntimeScalar(RuntimeIO.selectedHandle); } 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.getSelectedHandle()); - RuntimeIO.setSelectedHandle(runtimeList.getFirst().getRuntimeIO()); - RuntimeIO.setLastAccessedHandle(RuntimeIO.getSelectedHandle()); + RuntimeScalar fh = new RuntimeScalar(RuntimeIO.selectedHandle); + RuntimeIO.selectedHandle = runtimeList.getFirst().getRuntimeIO(); + RuntimeIO.lastAccesseddHandle = RuntimeIO.selectedHandle; return fh; } @@ -405,7 +405,7 @@ public static RuntimeScalar seek(RuntimeScalar fileHandle, RuntimeList runtimeLi whence = runtimeList.elements.get(1).scalar().getInt(); } - RuntimeIO.setLastAccessedHandle(runtimeIO); + RuntimeIO.lastAccesseddHandle = 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.getLastAccessedHandle(); + RuntimeIO last = RuntimeIO.lastAccesseddHandle; if (last != null) { return last.tell(); } @@ -455,7 +455,7 @@ public static RuntimeScalar tell(RuntimeScalar fileHandle) { } // Update the last accessed filehandle - RuntimeIO.setLastAccessedHandle(fh); + RuntimeIO.lastAccesseddHandle = 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.getLastAccessedHandle(); + RuntimeIO last = RuntimeIO.lastAccesseddHandle; 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.getLastAccessedHandle(); + RuntimeIO last = RuntimeIO.lastAccesseddHandle; 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.getStdout(); // Default output handle + RuntimeIO fh = RuntimeIO.stdout; // 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.getStdout()) { + if (fh == RuntimeIO.stdout) { formatName = "STDOUT"; - } else if (fh == RuntimeIO.getStderr()) { + } else if (fh == RuntimeIO.stderr) { formatName = "STDERR"; - } else if (fh == RuntimeIO.getStdin()) { + } else if (fh == RuntimeIO.stdin) { 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.getStdin(); + return RuntimeIO.stdin; case 1: // STDOUT - return RuntimeIO.getStdout(); + return RuntimeIO.stdout; case 2: // STDERR - return RuntimeIO.getStderr(); + return RuntimeIO.stderr; 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.getStdin(); break; - case "STDOUT": sourceHandle = RuntimeIO.getStdout(); break; - case "STDERR": sourceHandle = RuntimeIO.getStderr(); break; + case "STDIN": sourceHandle = RuntimeIO.stdin; break; + case "STDOUT": sourceHandle = RuntimeIO.stdout; break; + case "STDERR": sourceHandle = RuntimeIO.stderr; 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 1f48a23dc..a4b02a21a 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.setLastAccessedHandle(runtimeIO); + RuntimeIO.lastAccesseddHandle = 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 b6f5551c9..24e0c3173 100644 --- a/src/main/java/org/perlonjava/runtime/operators/ScalarGlobOperator.java +++ b/src/main/java/org/perlonjava/runtime/operators/ScalarGlobOperator.java @@ -1,7 +1,6 @@ package org.perlonjava.runtime.operators; import org.perlonjava.runtime.runtimetypes.*; -import org.perlonjava.runtime.runtimetypes.PerlRuntime; import java.io.File; import java.util.*; @@ -226,7 +225,7 @@ private static void globRecursive(ScalarGlobOperator scalarGlobOperator, String startSegment = 1; } } else { - startDir = new File(PerlRuntime.getCwd()); + startDir = new File(System.getProperty("user.dir")); prefix = ""; startSegment = 0; } @@ -347,7 +346,7 @@ private PathComponents extractPathComponents(String normalizedPattern, boolean i filePattern = normalizedPattern.substring(lastSep + 1); } else { // No directory separator - use current directory - baseDir = new File(PerlRuntime.getCwd()); + baseDir = new File(System.getProperty("user.dir")); } 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 4a9817542..e9f1c7f4d 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 = PerlRuntime.getCwd(); + String userDir = System.getProperty("user.dir"); 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 = PerlRuntime.getCwd(); + String userDir = System.getProperty("user.dir"); 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 = PerlRuntime.getCwd(); + String userDir = System.getProperty("user.dir"); 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(PerlRuntime.getCwd())); + processBuilder.directory(new File(System.getProperty("user.dir"))); 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 = PerlRuntime.getCwd(); + String userDir = System.getProperty("user.dir"); 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 = PerlRuntime.getCwd(); + String userDir = System.getProperty("user.dir"); 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 8f8694311..62acc9bd0 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.getSelectedHandle()) { - RuntimeIO.setSelectedHandle(tieHandle); + if (previousValue == RuntimeIO.selectedHandle) { + RuntimeIO.selectedHandle = 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.getSelectedHandle()) { - RuntimeIO.setSelectedHandle(previousValue); + if (currentTieHandle == RuntimeIO.selectedHandle) { + RuntimeIO.selectedHandle = 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 1ce8b3a49..7e80418e3 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.incrementEvalDepth(); + RuntimeCode.evalDepth++; 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.decrementEvalDepth(); + RuntimeCode.evalDepth--; // 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.getLastAccessedHandle() != null && RuntimeIO.getLastAccessedHandle().currentLineNumber > 0) { - String handleName = findFilehandleName(RuntimeIO.getLastAccessedHandle()); + if (RuntimeIO.lastAccesseddHandle != null && RuntimeIO.lastAccesseddHandle.currentLineNumber > 0) { + String handleName = findFilehandleName(RuntimeIO.lastAccesseddHandle); 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.getLastAccessedHandle().currentLineNumber; + return ", <" + handleName + "> " + unit + " " + RuntimeIO.lastAccesseddHandle.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.getLastReadlineHandleName(); + return RuntimeIO.lastReadlineHandleName; } } \ 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 60e088941..57c105eaa 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.setAutoloadEnabled(false); + InheritanceResolver.autoloadEnabled = false; RuntimeList codeList; try { codeList = Universal.can(canArgs, RuntimeContextType.SCALAR); } finally { - InheritanceResolver.setAutoloadEnabled(true); + InheritanceResolver.autoloadEnabled = 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.setAutoloadEnabled(false); + InheritanceResolver.autoloadEnabled = false; RuntimeList codeList; try { codeList = Universal.can(canArgs, RuntimeContextType.SCALAR); } finally { - InheritanceResolver.setAutoloadEnabled(true); + InheritanceResolver.autoloadEnabled = 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 cb875c479..82863be92 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/FileSpec.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/FileSpec.java @@ -1,6 +1,5 @@ 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; @@ -512,14 +511,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() : PerlRuntime.getCwd(); + String base = args.size() == 3 ? args.get(2).toString() : System.getProperty("user.dir"); // Ensure both paths are absolute before relativizing (like Perl does) - // Note: We use PerlRuntime.getCwd() explicitly because Java's Path.toAbsolutePath() - // doesn't respect per-runtime cwd set by chdir() + // Note: We use user.dir explicitly because Java's Path.toAbsolutePath() + // doesn't respect System.setProperty("user.dir", ...) set by chdir() Path pathObj = Paths.get(path); Path baseObj = Paths.get(base); - String userDir = PerlRuntime.getCwd(); + String userDir = System.getProperty("user.dir"); if (!pathObj.isAbsolute()) { pathObj = Paths.get(userDir).resolve(pathObj).normalize(); @@ -544,7 +543,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() : PerlRuntime.getCwd(); + String base = args.size() == 3 ? args.get(2).toString() : System.getProperty("user.dir"); // PerlOnJava: jar: paths are already absolute, return as-is if (path.startsWith("jar:")) { @@ -560,7 +559,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(PerlRuntime.getCwd()).resolve(basePath); + basePath = Paths.get(System.getProperty("user.dir")).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 dd8a96e21..b55237d3d 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/IPCOpen3.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/IPCOpen3.java @@ -5,7 +5,6 @@ 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; @@ -122,7 +121,7 @@ public static RuntimeList _open3(RuntimeArray args, int ctx) { } ProcessBuilder processBuilder = new ProcessBuilder(command); - String userDir = PerlRuntime.getCwd(); + String userDir = System.getProperty("user.dir"); processBuilder.directory(new File(userDir)); // Copy %ENV to the subprocess @@ -350,7 +349,7 @@ public static RuntimeList _open2(RuntimeArray args, int ctx) { } ProcessBuilder processBuilder = new ProcessBuilder(command); - String userDir = PerlRuntime.getCwd(); + String userDir = System.getProperty("user.dir"); 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 538c1735e..094f1d2e7 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(PerlRuntime.getCwd()).getList(); + return new RuntimeScalar(System.getProperty("user.dir")).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(PerlRuntime.getCwd(), path); + file = new java.io.File(System.getProperty("user.dir"), 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 a536b24c8..b516c2d0d 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/Mro.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/Mro.java @@ -12,11 +12,15 @@ * 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". @@ -229,28 +233,27 @@ 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() { - Map> cache = PerlRuntime.current().mroIsaRevCache; - cache.clear(); + isaRevCache.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, cache); + buildIsaRevForClass(className); } } /** * Build reverse ISA relationships for a specific class. */ - private static void buildIsaRevForClass(String className, Map> cache) { + private static void buildIsaRevForClass(String className) { 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()) { - cache.computeIfAbsent(parentName, k -> new HashSet<>()).add(className); + isaRevCache.computeIfAbsent(parentName, k -> new HashSet<>()).add(className); } } } @@ -269,15 +272,14 @@ 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 (cache.isEmpty()) { + if (isaRevCache.isEmpty()) { buildIsaRevCache(); } RuntimeArray result = new RuntimeArray(); - Set inheritors = cache.getOrDefault(className, new HashSet<>()); + Set inheritors = isaRevCache.getOrDefault(className, new HashSet<>()); // Add all classes that inherit from this one, including indirectly Set allInheritors = new HashSet<>(); @@ -299,8 +301,7 @@ private static void collectAllInheritors(String className, Set result, S } visited.add(className); - Map> cache = PerlRuntime.current().mroIsaRevCache; - Set directInheritors = cache.getOrDefault(className, new HashSet<>()); + Set directInheritors = isaRevCache.getOrDefault(className, new HashSet<>()); for (String inheritor : directInheritors) { result.add(inheritor); collectAllInheritors(inheritor, result, visited); @@ -346,12 +347,11 @@ 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(); - rt.mroIsaRevCache.clear(); + isaRevCache.clear(); // Increment all package generations - for (String pkg : new HashSet<>(rt.mroPackageGenerations.keySet())) { + for (String pkg : new HashSet<>(packageGenerations.keySet())) { incrementPackageGeneration(pkg); } @@ -371,17 +371,16 @@ 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 (cache.isEmpty()) { + if (isaRevCache.isEmpty()) { buildIsaRevCache(); } - Set dependents = cache.getOrDefault(className, new HashSet<>()); + Set dependents = isaRevCache.getOrDefault(className, new HashSet<>()); dependents.add(className); // Include the class itself // Increment package generation for all dependent classes @@ -392,6 +391,9 @@ 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. * @@ -405,7 +407,6 @@ 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")) { @@ -417,15 +418,15 @@ public static RuntimeList get_pkg_gen(RuntimeArray args, int ctx) { currentIsa.add(parentName); } } - List cachedIsa = rt.mroPkgGenIsaState.get(className); + List cachedIsa = pkgGenIsaState.get(className); if (cachedIsa != null && !currentIsa.equals(cachedIsa)) { incrementPackageGeneration(className); } - rt.mroPkgGenIsaState.put(className, currentIsa); + pkgGenIsaState.put(className, currentIsa); } // Return current generation, starting from 1 - Integer gen = rt.mroPackageGenerations.getOrDefault(className, 1); + Integer gen = packageGenerations.getOrDefault(className, 1); return new RuntimeScalar(gen).getList(); } @@ -436,8 +437,7 @@ public static RuntimeList get_pkg_gen(RuntimeArray args, int ctx) { * @param packageName The name of the package. */ public static void incrementPackageGeneration(String packageName) { - Map generations = PerlRuntime.current().mroPackageGenerations; - Integer current = generations.getOrDefault(packageName, 1); - generations.put(packageName, current + 1); + Integer current = packageGenerations.getOrDefault(packageName, 1); + packageGenerations.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 19bfa77ef..d19decafe 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(PerlRuntime.getCwd()).getList(); + return new RuntimeScalar(System.getProperty("user.dir")).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.setStdin(target); - case 1 -> RuntimeIO.setStdout(target); - case 2 -> RuntimeIO.setStderr(target); + case 0 -> RuntimeIO.stdin = target; + case 1 -> RuntimeIO.stdout = target; + case 2 -> RuntimeIO.stderr = 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.getStdin(); - case 1 -> RuntimeIO.getStdout(); - case 2 -> RuntimeIO.getStderr(); + case 0 -> RuntimeIO.stdin; + case 1 -> RuntimeIO.stdout; + case 2 -> RuntimeIO.stderr; 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 1c226303d..c676ff5d2 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.getIsSubsMap().put(fullName, true); + GlobalVariable.isSubs.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.getIsSubsMap().put(fullName, true); + GlobalVariable.isSubs.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 9c2a232d5..82d28cfe9 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.getAndIncrement(); + String globName = "Symbol::GEN" + EmitterMethodCreator.classCounter++; 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.getAndIncrement(); + String globName = "Symbol::GEN" + EmitterMethodCreator.classCounter++; 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 b9f0d3729..4988c951a 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.getStdin(); + RuntimeIO fh = RuntimeIO.stdin; 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.getStdin(); + RuntimeIO fh = RuntimeIO.stdin; 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.getStdin(); + RuntimeIO fh = RuntimeIO.stdin; 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.getStdout(); + RuntimeIO fh = RuntimeIO.stdout; 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.getStdout(); + RuntimeIO fh = RuntimeIO.stdout; 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.getStdin(); + RuntimeIO fh = RuntimeIO.stdin; 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.getStdin(); + RuntimeIO fh = RuntimeIO.stdin; 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.getStdin(); + RuntimeIO fh = RuntimeIO.stdin; 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 eb31badd1..94e03df5b 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.getStdout().write(prompt); - RuntimeIO.getStdout().flush(); + RuntimeIO.stdout.write(prompt); + RuntimeIO.stdout.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.getStdin())); + return new RuntimeList(new RuntimeScalar(RuntimeIO.stdin)); } /** @@ -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.getStdout())); + return new RuntimeList(new RuntimeScalar(RuntimeIO.stdout)); } /** diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/XMLParserExpat.java b/src/main/java/org/perlonjava/runtime/perlmodule/XMLParserExpat.java index 84dcef8fa..5a4ae3e5d 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/XMLParserExpat.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/XMLParserExpat.java @@ -3,7 +3,6 @@ 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.*; @@ -1063,7 +1062,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 = PerlRuntime.getCwd(); + String cwd = System.getProperty("user.dir"); String baseUri = new java.io.File(cwd, "dummy").toURI().toString(); baseUri = baseUri.substring(0, baseUri.lastIndexOf('/') + 1); inputSource.setSystemId(baseUri); @@ -2133,7 +2132,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 = PerlRuntime.getCwd(); + String cwd = System.getProperty("user.dir"); 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 0a73d96b9..f8503ce77 100644 --- a/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java +++ b/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java @@ -5,7 +5,10 @@ import org.perlonjava.runtime.perlmodule.Utf8; import org.perlonjava.runtime.runtimetypes.*; -import java.util.*; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -33,87 +36,38 @@ 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 (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; } + // 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; // Compiled regex pattern (for byte strings - ASCII-only \w, \d) public Pattern pattern; // Compiled regex pattern for Unicode strings (Unicode \w, \d) @@ -521,16 +475,15 @@ 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 = cache.get(callsiteId); + RuntimeScalar cached = optimizedRegexCache.get(callsiteId); if (cached != null) { return cached; } // Compile the regex and cache it RuntimeScalar result = getQuotedRegex(patternString, modifiers); - cache.put(callsiteId, result); + optimizedRegexCache.put(callsiteId, result); return result; } @@ -664,25 +617,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 (PerlRuntime.current().regexLastSuccessfulPattern != null) { + if (lastSuccessfulPattern != null) { // Use the pattern from last successful match // But keep the current flags (especially /g and /i) - Pattern pattern = PerlRuntime.current().regexLastSuccessfulPattern.pattern; + Pattern pattern = lastSuccessfulPattern.pattern; // Re-apply current flags if they differ - if (originalFlags != null && !originalFlags.equals(PerlRuntime.current().regexLastSuccessfulPattern.regexFlags)) { + if (originalFlags != null && !originalFlags.equals(lastSuccessfulPattern.regexFlags)) { // Need to recompile with current flags using preprocessed pattern int newFlags = originalFlags.toPatternFlags(); - String recompilePattern = PerlRuntime.current().regexLastSuccessfulPattern.javaPatternString != null - ? PerlRuntime.current().regexLastSuccessfulPattern.javaPatternString : PerlRuntime.current().regexLastSuccessfulPattern.patternString; + String recompilePattern = lastSuccessfulPattern.javaPatternString != null + ? lastSuccessfulPattern.javaPatternString : lastSuccessfulPattern.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 = 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.patternUnicode = lastSuccessfulPattern.patternUnicode; + tempRegex.patternString = lastSuccessfulPattern.patternString; + tempRegex.javaPatternString = lastSuccessfulPattern.javaPatternString; + tempRegex.hasPreservesMatch = lastSuccessfulPattern.hasPreservesMatch || (originalFlags != null && originalFlags.preservesMatch()); tempRegex.regexFlags = originalFlags; tempRegex.useGAssertion = originalFlags != null && originalFlags.useGAssertion(); regex = tempRegex; @@ -843,52 +796,52 @@ private static RuntimeBase matchRegexDirect(RuntimeScalar quotedRegex, RuntimeSc } found = true; - PerlRuntime.current().regexLastMatchWasByteString = (string.type == RuntimeScalarType.BYTE_STRING); + lastMatchWasByteString = (string.type == RuntimeScalarType.BYTE_STRING); int captureCount = matcher.groupCount(); // Always initialize $1, $2, @+, @-, $`, $&, $' for every successful match - PerlRuntime.current().regexGlobalMatcher = matcher; - PerlRuntime.current().regexGlobalMatchString = inputStr; - PerlRuntime.current().regexLastMatchUsedBackslashK = regex.hasBackslashK; + globalMatcher = matcher; + globalMatchString = inputStr; + lastMatchUsedBackslashK = 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) { - PerlRuntime.current().regexLastCaptureGroups = new String[userGroupCount]; + lastCaptureGroups = new String[userGroupCount]; int destIdx = 0; for (int i = 1; i <= captureCount; i++) { if (i == perlKGroup) continue; - PerlRuntime.current().regexLastCaptureGroups[destIdx++] = matcher.group(i); + lastCaptureGroups[destIdx++] = matcher.group(i); } } else { - PerlRuntime.current().regexLastCaptureGroups = null; + lastCaptureGroups = null; } } else { - PerlRuntime.current().regexLastCaptureGroups = new String[captureCount]; + lastCaptureGroups = new String[captureCount]; for (int i = 0; i < captureCount; i++) { - PerlRuntime.current().regexLastCaptureGroups[i] = matcher.group(i + 1); + lastCaptureGroups[i] = matcher.group(i + 1); } } } else { - PerlRuntime.current().regexLastCaptureGroups = null; + lastCaptureGroups = null; } // For \K, adjust match start/string so $& is only the post-\K portion if (regex.hasBackslashK) { int keepEnd = matcher.end("perlK"); - PerlRuntime.current().regexLastMatchedString = inputStr.substring(keepEnd, matcher.end()); - PerlRuntime.current().regexLastMatchStart = keepEnd; + lastMatchedString = inputStr.substring(keepEnd, matcher.end()); + lastMatchStart = keepEnd; } else { - PerlRuntime.current().regexLastMatchedString = matcher.group(0); - PerlRuntime.current().regexLastMatchStart = matcher.start(); + lastMatchedString = matcher.group(0); + lastMatchStart = matcher.start(); } - PerlRuntime.current().regexLastMatchEnd = matcher.end(); + lastMatchEnd = 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 ? PerlRuntime.current().regexLastMatchedString : matcher.group(0); + String matchedStr = regex.hasBackslashK ? lastMatchedString : matcher.group(0); matchedGroups.add(makeMatchResultScalar(matchedStr)); } else { // save captures in return list if needed @@ -978,24 +931,24 @@ private static RuntimeBase matchRegexDirect(RuntimeScalar quotedRegex, RuntimeSc if (!found) { // No match: scalar match vars ($`, $&, $') should become undef. - // Keep lastSuccessful* and the previous PerlRuntime.current().regexGlobalMatcher intact so @-/@+ do not get clobbered + // Keep lastSuccessful* and the previous globalMatcher intact so @-/@+ do not get clobbered // by internal regex checks that fail (e.g. in test libraries). - 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 + globalMatchString = null; + lastMatchedString = null; + lastMatchStart = -1; + lastMatchEnd = -1; + // Don't clear lastCaptureGroups - Perl preserves $1 across failed matches } if (found) { regex.matched = true; // Counter for m?PAT? - PerlRuntime.current().regexLastMatchUsedPFlag = regex.hasPreservesMatch; - PerlRuntime.current().regexLastSuccessfulPattern = regex; + lastMatchUsedPFlag = regex.hasPreservesMatch; + lastSuccessfulPattern = regex; // Store last successful match information (persists across failed matches) - PerlRuntime.current().regexLastSuccessfulMatchedString = PerlRuntime.current().regexLastMatchedString; - PerlRuntime.current().regexLastSuccessfulMatchStart = PerlRuntime.current().regexLastMatchStart; - PerlRuntime.current().regexLastSuccessfulMatchEnd = PerlRuntime.current().regexLastMatchEnd; - PerlRuntime.current().regexLastSuccessfulMatchString = PerlRuntime.current().regexGlobalMatchString; + lastSuccessfulMatchedString = lastMatchedString; + lastSuccessfulMatchStart = lastMatchStart; + lastSuccessfulMatchEnd = lastMatchEnd; + lastSuccessfulMatchString = globalMatchString; // Update $^R if this regex has code block captures (performance optimization) if (regex.hasCodeBlockCaptures) { @@ -1009,9 +962,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, PerlRuntime.current().regexGlobalMatcher is " + (PerlRuntime.current().regexGlobalMatcher == null ? "null" : "set")); + // System.err.println("DEBUG: Match completed, globalMatcher is " + (globalMatcher == null ? "null" : "set")); } else { - // System.err.println("DEBUG: No match found, PerlRuntime.current().regexGlobalMatcher is " + (PerlRuntime.current().regexGlobalMatcher == null ? "null" : "set")); + // System.err.println("DEBUG: No match found, globalMatcher is " + (globalMatcher == null ? "null" : "set")); } if (ctx == RuntimeContextType.LIST) { @@ -1105,25 +1058,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 (PerlRuntime.current().regexLastSuccessfulPattern != null) { + if (lastSuccessfulPattern != null) { // Use the pattern from last successful match // But keep the current replacement and flags (especially /g and /i) - Pattern pattern = PerlRuntime.current().regexLastSuccessfulPattern.pattern; + Pattern pattern = lastSuccessfulPattern.pattern; // Re-apply current flags if they differ - if (originalFlags != null && !originalFlags.equals(PerlRuntime.current().regexLastSuccessfulPattern.regexFlags)) { + if (originalFlags != null && !originalFlags.equals(lastSuccessfulPattern.regexFlags)) { // Need to recompile with current flags using preprocessed pattern int newFlags = originalFlags.toPatternFlags(); - String recompilePattern = PerlRuntime.current().regexLastSuccessfulPattern.javaPatternString != null - ? PerlRuntime.current().regexLastSuccessfulPattern.javaPatternString : PerlRuntime.current().regexLastSuccessfulPattern.patternString; + String recompilePattern = lastSuccessfulPattern.javaPatternString != null + ? lastSuccessfulPattern.javaPatternString : lastSuccessfulPattern.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 = 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.patternUnicode = lastSuccessfulPattern.patternUnicode; + tempRegex.patternString = lastSuccessfulPattern.patternString; + tempRegex.javaPatternString = lastSuccessfulPattern.javaPatternString; + tempRegex.hasPreservesMatch = lastSuccessfulPattern.hasPreservesMatch || (originalFlags != null && originalFlags.preservesMatch()); tempRegex.regexFlags = originalFlags; tempRegex.useGAssertion = originalFlags != null && originalFlags.useGAssertion(); tempRegex.replacement = replacement; @@ -1178,7 +1131,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 PerlRuntime.current().regexGlobalMatcher here - only reset it if we actually find a match + // Don't reset globalMatcher 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 @@ -1188,47 +1141,47 @@ public static RuntimeBase replaceRegex(RuntimeScalar quotedRegex, RuntimeScalar try { while (matcher.find()) { found++; - PerlRuntime.current().regexLastMatchWasByteString = (string.type == RuntimeScalarType.BYTE_STRING); + lastMatchWasByteString = (string.type == RuntimeScalarType.BYTE_STRING); // Initialize $1, $2, @+, @- only when we have a match - PerlRuntime.current().regexGlobalMatcher = matcher; - PerlRuntime.current().regexGlobalMatchString = inputStr; - PerlRuntime.current().regexLastMatchUsedBackslashK = regex.hasBackslashK; + globalMatcher = matcher; + globalMatchString = inputStr; + lastMatchUsedBackslashK = 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) { - PerlRuntime.current().regexLastCaptureGroups = new String[userGroupCount]; + lastCaptureGroups = new String[userGroupCount]; int destIdx = 0; for (int i = 1; i <= matcher.groupCount(); i++) { if (i == perlKGroup) continue; - PerlRuntime.current().regexLastCaptureGroups[destIdx++] = matcher.group(i); + lastCaptureGroups[destIdx++] = matcher.group(i); } } else { - PerlRuntime.current().regexLastCaptureGroups = null; + lastCaptureGroups = null; } } else { - PerlRuntime.current().regexLastCaptureGroups = new String[matcher.groupCount()]; + lastCaptureGroups = new String[matcher.groupCount()]; for (int i = 0; i < matcher.groupCount(); i++) { - PerlRuntime.current().regexLastCaptureGroups[i] = matcher.group(i + 1); + lastCaptureGroups[i] = matcher.group(i + 1); } } } else { - PerlRuntime.current().regexLastCaptureGroups = null; + lastCaptureGroups = null; } // For \K, adjust match start so $& is only the post-\K portion if (regex.hasBackslashK) { int keepEnd = matcher.end("perlK"); - PerlRuntime.current().regexLastMatchStart = keepEnd; - PerlRuntime.current().regexLastMatchedString = inputStr.substring(keepEnd, matcher.end()); + lastMatchStart = keepEnd; + lastMatchedString = inputStr.substring(keepEnd, matcher.end()); } else { - PerlRuntime.current().regexLastMatchStart = matcher.start(); - PerlRuntime.current().regexLastMatchedString = matcher.group(0); + lastMatchStart = matcher.start(); + lastMatchedString = matcher.group(0); } - PerlRuntime.current().regexLastMatchEnd = matcher.end(); + lastMatchEnd = matcher.end(); String replacementStr; if (replacementIsCode) { @@ -1276,8 +1229,8 @@ public static RuntimeBase replaceRegex(RuntimeScalar quotedRegex, RuntimeScalar boolean wasByteString = (string.type == RuntimeScalarType.BYTE_STRING); // Store as last successful pattern for empty pattern reuse - PerlRuntime.current().regexLastMatchUsedPFlag = regex.hasPreservesMatch; - PerlRuntime.current().regexLastSuccessfulPattern = regex; + lastMatchUsedPFlag = regex.hasPreservesMatch; + lastSuccessfulPattern = regex; if (regex.regexFlags.isNonDestructive()) { // /r modifier: return the modified string @@ -1312,15 +1265,12 @@ public static RuntimeBase replaceRegex(RuntimeScalar quotedRegex, RuntimeScalar */ public static void reset() { // Iterate over the regexCache and reset the `matched` flag for each cached regex - // 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 - } + 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 regexOptimizedCache - for (Map.Entry entry : PerlRuntime.current().regexOptimizedCache.entrySet()) { + // Also reset m?PAT? patterns cached per-callsite in optimizedRegexCache + for (Map.Entry entry : optimizedRegexCache.entrySet()) { RuntimeScalar scalar = entry.getValue(); if (scalar.value instanceof RuntimeRegex regex) { regex.matched = false; @@ -1334,48 +1284,48 @@ public static void reset() { */ public static void initialize() { // Reset all match state - PerlRuntime.current().regexGlobalMatcher = null; - PerlRuntime.current().regexGlobalMatchString = null; + globalMatcher = null; + globalMatchString = null; // Reset current match information - PerlRuntime.current().regexLastMatchedString = null; - PerlRuntime.current().regexLastMatchStart = -1; - PerlRuntime.current().regexLastMatchEnd = -1; + lastMatchedString = null; + lastMatchStart = -1; + lastMatchEnd = -1; // Reset last successful match information - 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; + lastSuccessfulPattern = null; + lastSuccessfulMatchedString = null; + lastSuccessfulMatchStart = -1; + lastSuccessfulMatchEnd = -1; + lastSuccessfulMatchString = null; + lastMatchUsedPFlag = false; + lastCaptureGroups = null; // Reset regex cache matched flags reset(); } public static String matchString() { - if (PerlRuntime.current().regexGlobalMatcher != null && PerlRuntime.current().regexLastMatchedString != null) { + if (globalMatcher != null && lastMatchedString != null) { // Current match data available - return PerlRuntime.current().regexLastMatchedString; + return lastMatchedString; } return null; } public static String preMatchString() { - if (PerlRuntime.current().regexGlobalMatcher != null && PerlRuntime.current().regexGlobalMatchString != null && PerlRuntime.current().regexLastMatchStart != -1) { + if (globalMatcher != null && globalMatchString != null && lastMatchStart != -1) { // Current match data available - String result = PerlRuntime.current().regexGlobalMatchString.substring(0, PerlRuntime.current().regexLastMatchStart); + String result = globalMatchString.substring(0, lastMatchStart); return result; } return null; } public static String postMatchString() { - if (PerlRuntime.current().regexGlobalMatcher != null && PerlRuntime.current().regexGlobalMatchString != null && PerlRuntime.current().regexLastMatchEnd != -1) { + if (globalMatcher != null && globalMatchString != null && lastMatchEnd != -1) { // Current match data available - String result = PerlRuntime.current().regexGlobalMatchString.substring(PerlRuntime.current().regexLastMatchEnd); + String result = globalMatchString.substring(lastMatchEnd); return result; } return null; @@ -1383,24 +1333,24 @@ public static String postMatchString() { public static String captureString(int group) { if (group <= 0) { - return PerlRuntime.current().regexLastMatchedString; + return lastMatchedString; } - if (PerlRuntime.current().regexLastCaptureGroups == null || group > PerlRuntime.current().regexLastCaptureGroups.length) { + if (lastCaptureGroups == null || group > lastCaptureGroups.length) { return null; } - return PerlRuntime.current().regexLastCaptureGroups[group - 1]; + return lastCaptureGroups[group - 1]; } public static String lastCaptureString() { - if (PerlRuntime.current().regexLastCaptureGroups == null || PerlRuntime.current().regexLastCaptureGroups.length == 0) { + if (lastCaptureGroups == null || lastCaptureGroups.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 = PerlRuntime.current().regexLastCaptureGroups.length - 1; i >= 0; i--) { - if (PerlRuntime.current().regexLastCaptureGroups[i] != null) { - return PerlRuntime.current().regexLastCaptureGroups[i]; + for (int i = lastCaptureGroups.length - 1; i >= 0; i--) { + if (lastCaptureGroups[i] != null) { + return lastCaptureGroups[i]; } } return null; @@ -1415,7 +1365,7 @@ public static RuntimeScalar makeMatchResultScalar(String value) { return RuntimeScalarCache.scalarUndef; } RuntimeScalar scalar = new RuntimeScalar(value); - if (PerlRuntime.current().regexLastMatchWasByteString) { + if (lastMatchWasByteString) { scalar.type = RuntimeScalarType.BYTE_STRING; } return scalar; @@ -1423,18 +1373,18 @@ public static RuntimeScalar makeMatchResultScalar(String value) { public static RuntimeScalar matcherStart(int group) { if (group == 0) { - return PerlRuntime.current().regexLastMatchStart >= 0 ? getScalarInt(PerlRuntime.current().regexLastMatchStart) : scalarUndef; + return lastMatchStart >= 0 ? getScalarInt(lastMatchStart) : scalarUndef; } - if (PerlRuntime.current().regexGlobalMatcher == null) { + if (globalMatcher == null) { return scalarUndef; } try { // Adjust group number to skip the internal perlK group int javaGroup = adjustGroupForBackslashK(group); - if (javaGroup < 0 || javaGroup > PerlRuntime.current().regexGlobalMatcher.groupCount()) { + if (javaGroup < 0 || javaGroup > globalMatcher.groupCount()) { return scalarUndef; } - int start = PerlRuntime.current().regexGlobalMatcher.start(javaGroup); + int start = globalMatcher.start(javaGroup); if (start == -1) { return scalarUndef; } @@ -1446,18 +1396,18 @@ public static RuntimeScalar matcherStart(int group) { public static RuntimeScalar matcherEnd(int group) { if (group == 0) { - return PerlRuntime.current().regexLastMatchEnd >= 0 ? getScalarInt(PerlRuntime.current().regexLastMatchEnd) : scalarUndef; + return lastMatchEnd >= 0 ? getScalarInt(lastMatchEnd) : scalarUndef; } - if (PerlRuntime.current().regexGlobalMatcher == null) { + if (globalMatcher == null) { return scalarUndef; } try { // Adjust group number to skip the internal perlK group int javaGroup = adjustGroupForBackslashK(group); - if (javaGroup < 0 || javaGroup > PerlRuntime.current().regexGlobalMatcher.groupCount()) { + if (javaGroup < 0 || javaGroup > globalMatcher.groupCount()) { return scalarUndef; } - int end = PerlRuntime.current().regexGlobalMatcher.end(javaGroup); + int end = globalMatcher.end(javaGroup); if (end == -1) { return scalarUndef; } @@ -1468,12 +1418,12 @@ public static RuntimeScalar matcherEnd(int group) { } public static int matcherSize() { - if (PerlRuntime.current().regexGlobalMatcher == null) { + if (globalMatcher == null) { return 0; } - int size = PerlRuntime.current().regexGlobalMatcher.groupCount(); + int size = globalMatcher.groupCount(); // Subtract the internal perlK group if \K was used - if (PerlRuntime.current().regexLastMatchUsedBackslashK) { + if (lastMatchUsedBackslashK) { size--; } // +1 because groupCount is zero-based, and we include the entire match @@ -1485,10 +1435,10 @@ public static int matcherSize() { * skipping the internal perlK named group when \K is active. */ private static int adjustGroupForBackslashK(int perlGroup) { - if (!PerlRuntime.current().regexLastMatchUsedBackslashK || PerlRuntime.current().regexGlobalMatcher == null) { + if (!lastMatchUsedBackslashK || globalMatcher == null) { return perlGroup; } - int perlKGroup = getPerlKGroup(PerlRuntime.current().regexGlobalMatcher); + int perlKGroup = getPerlKGroup(globalMatcher); if (perlKGroup < 0) return perlGroup; // Perl groups before perlK: same number. At or after: add 1. return perlGroup >= perlKGroup ? perlGroup + 1 : perlGroup; @@ -1797,7 +1747,7 @@ public void dynamicRestoreState() { * @return The constant value for $^R, or null if no code block was matched */ public RuntimeScalar getLastCodeBlockResult() { - Matcher matcher = PerlRuntime.current().regexGlobalMatcher; + Matcher matcher = globalMatcher; 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 e0cf3d9b7..7857cd39d 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/CallerStack.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/CallerStack.java @@ -10,10 +10,8 @@ * for implementing the caller() function during operations like import() and unimport(). */ public class CallerStack { - // State is now held per-PerlRuntime. This accessor delegates to the current runtime. - private static List callerStack() { - return PerlRuntime.current().callerStack; - } + // Store either CallerInfo (resolved) or LazyCallerInfo (deferred) + private static final List callerStack = new ArrayList<>(); /** * Pushes a new CallerInfo object onto the stack, representing a new entry in the calling sequence. @@ -23,11 +21,7 @@ private static List callerStack() { * @param line The line number in the file where the call originated. */ public static void push(String packageName, String filename, int 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)); + callerStack.add(new CallerInfo(packageName, filename, line)); } /** @@ -39,11 +33,7 @@ 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) { - 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)); + callerStack.add(new LazyCallerInfo(packageName, resolver)); } /** @@ -54,20 +44,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) { - List stack = callerStack(); - if (stack.isEmpty()) { + if (callerStack.isEmpty()) { return null; } - int index = stack.size() - 1 - callFrame; + int index = callerStack.size() - 1 - callFrame; if (index < 0) { return null; } - Object entry = stack.get(index); + Object entry = callerStack.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(); - stack.set(index, resolved); + callerStack.set(index, resolved); return resolved; } return null; @@ -80,14 +70,14 @@ public static CallerInfo peek(int callFrame) { * @return The most recent CallerInfo object, or null if the stack is empty. */ public static CallerInfo pop() { - List stack = callerStack(); - if (stack.isEmpty()) { + if (callerStack.isEmpty()) { return null; } - Object entry = stack.removeLast(); + Object entry = callerStack.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; @@ -100,15 +90,14 @@ 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 < stack.size(); i++) { - Object entry = stack.get(i); + for (int i = 0; i < callerStack.size(); i++) { + Object entry = callerStack.get(i); if (entry instanceof CallerInfo ci) { result.add(ci); } else if (entry instanceof LazyCallerInfo lazy) { CallerInfo resolved = lazy.resolve(); - stack.set(i, resolved); + callerStack.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 12df0c29d..b38ab3e5b 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.setLastAccessedHandle(currentWriter); + RuntimeIO.lastAccesseddHandle = currentWriter; // CRITICAL: Update selectedHandle so print statements without explicit filehandle // write to the original file during in-place editing - RuntimeIO.setSelectedHandle(currentWriter); + RuntimeIO.selectedHandle = 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 8796e6c84..66471b4fc 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/DynamicVariableManager.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/DynamicVariableManager.java @@ -10,10 +10,9 @@ * to their original states. */ public class DynamicVariableManager { - // State is now held per-PerlRuntime. This accessor delegates to the current runtime. - private static Deque variableStack() { - return PerlRuntime.current().dynamicVariableStack; - } + // 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<>(); /** * Returns the current local level, which is the size of the variable stack. @@ -22,7 +21,7 @@ private static Deque variableStack() { * @return the number of dynamic states in the stack. */ public static int getLocalLevel() { - return variableStack().size(); + return variableStack.size(); } /** @@ -34,14 +33,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; } @@ -49,7 +48,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). @@ -58,7 +57,7 @@ public static RuntimeGlob pushLocalVariable(RuntimeGlob variable) { public static void pushLocalVariable(DynamicState variable) { variable.dynamicSaveState(); - variableStack().addLast(variable); + variableStack.addLast(variable); } /** @@ -73,9 +72,8 @@ 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 > stack.size()) { + if (targetLocalLevel < 0 || targetLocalLevel > variableStack.size()) { throw new IllegalArgumentException("Invalid target local level: " + targetLocalLevel); } @@ -83,8 +81,8 @@ public static void popToLocalLevel(int targetLocalLevel) { Throwable pendingException = null; // Pop variables until the stack size matches the target local level - while (stack.size() > targetLocalLevel) { - DynamicState variable = stack.removeLast(); + while (variableStack.size() > targetLocalLevel) { + DynamicState variable = variableStack.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 cfb775d21..7945626c6 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.getGlobalVariablesMap().get("main::!"); + RuntimeScalar errnoVar = GlobalVariable.globalVariables.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 54fd51467..97e7911af 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/ErrnoVariable.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/ErrnoVariable.java @@ -331,29 +331,25 @@ public void clear() { set(0); } - // 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; - } + // 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<>(); @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 fca481886..f25e674a2 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.getGlobalVariablesMap().put(encodeSpecialVar("N"), new RuntimeScalarReadOnly()); + GlobalVariable.globalVariables.put(encodeSpecialVar("N"), new RuntimeScalarReadOnly()); // $^S - current state of the interpreter (undef=compiling, 0=not in eval, 1=in eval) - GlobalVariable.getGlobalVariablesMap().put("main::" + Character.toString('S' - 'A' + 1), + GlobalVariable.globalVariables.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.getGlobalVariablesMap().put("main::!", new ErrnoVariable()); // initialize $! with dualvar support + GlobalVariable.globalVariables.put("main::!", new ErrnoVariable()); // initialize $! with dualvar support // Initialize $, (output field separator) with special variable class - if (!GlobalVariable.getGlobalVariablesMap().containsKey("main::,")) { + if (!GlobalVariable.globalVariables.containsKey("main::,")) { var ofs = new OutputFieldSeparator(); ofs.set(""); - GlobalVariable.getGlobalVariablesMap().put("main::,", ofs); + GlobalVariable.globalVariables.put("main::,", ofs); } - GlobalVariable.getGlobalVariablesMap().put("main::|", new OutputAutoFlushVariable()); + GlobalVariable.globalVariables.put("main::|", new OutputAutoFlushVariable()); // Only set $\ if it hasn't been set yet - prevents overwriting during re-entrant calls - if (!GlobalVariable.getGlobalVariablesMap().containsKey("main::\\")) { + if (!GlobalVariable.globalVariables.containsKey("main::\\")) { var ors = new OutputRecordSeparator(); ors.set(compilerOptions.outputRecordSeparator); // initialize $\ - GlobalVariable.getGlobalVariablesMap().put("main::\\", ors); + GlobalVariable.globalVariables.put("main::\\", ors); } - GlobalVariable.getGlobalVariable("main::$").set(PerlRuntime.current().pid); // initialize `$$` to per-runtime unique pid + GlobalVariable.getGlobalVariable("main::$").set(ProcessHandle.current().pid()); // initialize `$$` to process id 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.getGlobalVariablesMap().containsKey("main::0")) { + if (!GlobalVariable.globalVariables.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.getGlobalVariablesMap().containsKey(taintVarName) || - (compilerOptions.taintMode && GlobalVariable.getGlobalVariablesMap().get(taintVarName) == RuntimeScalarCache.scalarZero)) { - GlobalVariable.getGlobalVariablesMap().put(taintVarName, + if (!GlobalVariable.globalVariables.containsKey(taintVarName) || + (compilerOptions.taintMode && GlobalVariable.globalVariables.get(taintVarName) == RuntimeScalarCache.scalarZero)) { + GlobalVariable.globalVariables.put(taintVarName, compilerOptions.taintMode ? RuntimeScalarCache.scalarOne : RuntimeScalarCache.scalarZero); } - 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.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.getGlobalVariable("main::;").set("\034"); // initialize $; (SUBSEP) to \034 - 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.globalVariables.put("main::(", new ScalarSpecialVariable(ScalarSpecialVariable.Id.REAL_GID)); // $( - real GID (lazy) + GlobalVariable.globalVariables.put("main::)", new ScalarSpecialVariable(ScalarSpecialVariable.Id.EFFECTIVE_GID)); // $) - effective GID (lazy) GlobalVariable.getGlobalVariable("main::="); // TODO GlobalVariable.getGlobalVariable("main::^"); // TODO GlobalVariable.getGlobalVariable("main:::"); // TODO // Only set $/ if it hasn't been set yet - prevents overwriting during re-entrant calls - if (!GlobalVariable.getGlobalVariablesMap().containsKey("main::/")) { - GlobalVariable.getGlobalVariablesMap().put("main::/", new InputRecordSeparator(compilerOptions.inputRecordSeparator)); // initialize $/ + if (!GlobalVariable.globalVariables.containsKey("main::/")) { + GlobalVariable.globalVariables.put("main::/", new InputRecordSeparator(compilerOptions.inputRecordSeparator)); // initialize $/ } - 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 + 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 // $^R is writable, not read-only - initialize as regular variable instead of ScalarSpecialVariable - // GlobalVariable.getGlobalVariablesMap().put(encodeSpecialVar("R"), new ScalarSpecialVariable(ScalarSpecialVariable.Id.LAST_REGEXP_CODE_RESULT)); // $^R + // GlobalVariable.globalVariables.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.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.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.getGlobalVariable(encodeSpecialVar("UNICODE")).set(0); // initialize $^UNICODE to 0 - `-C` unicode flags - 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.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.getGlobalVariable(encodeSpecialVar("SAFE_LOCALES")); // TODO // Initialize additional magic scalar variables that tests expect to exist at startup GlobalVariable.getGlobalVariable(encodeSpecialVar("UTF8LOCALE")); // ${^UTF8LOCALE} - GlobalVariable.getGlobalVariablesMap().put(encodeSpecialVar("WARNING_BITS"), new ScalarSpecialVariable(ScalarSpecialVariable.Id.WARNING_BITS)); // ${^WARNING_BITS} + GlobalVariable.globalVariables.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.getGlobalHashesMap().put("main::SIG", new RuntimeSigHash()); + GlobalVariable.globalHashes.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 25863cc65..40fd6ca79 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.getGlobalVariablesMap().values()) { + for (RuntimeScalar val : GlobalVariable.globalVariables.values()) { destroyIfTracked(val); } // Walk global arrays for blessed ref elements - for (RuntimeArray arr : GlobalVariable.getGlobalArraysMap().values()) { + for (RuntimeArray arr : GlobalVariable.globalArrays.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.getGlobalHashesMap().values()) { + for (RuntimeHash hash : GlobalVariable.globalHashes.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 02a0a0bb0..96918b359 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalRuntimeArray.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalRuntimeArray.java @@ -16,11 +16,7 @@ * and {@link GlobalRuntimeScalar} for scalars. */ public class GlobalRuntimeArray implements DynamicState { - // Localized stack is now held per-PerlRuntime. - @SuppressWarnings("unchecked") - private static Stack localizedStack() { - return (Stack) (Stack) PerlRuntime.current().globalArrayLocalizedStack; - } + private static final Stack localizedStack = new Stack<>(); private final String fullName; public GlobalRuntimeArray(String fullName) { @@ -44,37 +40,37 @@ public static RuntimeArray makeLocal(String fullName) { @Override public void dynamicSaveState() { // Save the current array reference from the global map - RuntimeArray original = GlobalVariable.getGlobalArraysMap().get(fullName); - localizedStack().push(new SavedGlobalArrayState(fullName, original)); + RuntimeArray original = GlobalVariable.globalArrays.get(fullName); + localizedStack.push(new SavedGlobalArrayState(fullName, original)); // Install a fresh empty array in the global map RuntimeArray newLocal = new RuntimeArray(); - GlobalVariable.getGlobalArraysMap().put(fullName, newLocal); + GlobalVariable.globalArrays.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.getGlobalArraysMap().put(alias, newLocal); + GlobalVariable.globalArrays.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.getGlobalArraysMap().put(saved.fullName, saved.originalArray); + GlobalVariable.globalArrays.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.getGlobalArraysMap().put(alias, saved.originalArray); + GlobalVariable.globalArrays.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 6d37165a8..40df14826 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalRuntimeHash.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalRuntimeHash.java @@ -12,11 +12,7 @@ *

    Follows the same pattern as {@link GlobalRuntimeScalar} for scalars. */ public class GlobalRuntimeHash implements DynamicState { - // Localized stack is now held per-PerlRuntime. - @SuppressWarnings("unchecked") - private static Stack localizedStack() { - return (Stack) (Stack) PerlRuntime.current().globalHashLocalizedStack; - } + private static final Stack localizedStack = new Stack<>(); private final String fullName; public GlobalRuntimeHash(String fullName) { @@ -40,37 +36,37 @@ public static RuntimeHash makeLocal(String fullName) { @Override public void dynamicSaveState() { // Save the current hash reference from the global map - RuntimeHash original = GlobalVariable.getGlobalHashesMap().get(fullName); - localizedStack().push(new SavedGlobalHashState(fullName, original)); + RuntimeHash original = GlobalVariable.globalHashes.get(fullName); + localizedStack.push(new SavedGlobalHashState(fullName, original)); // Install a fresh empty hash in the global map RuntimeHash newLocal = new RuntimeHash(); - GlobalVariable.getGlobalHashesMap().put(fullName, newLocal); + GlobalVariable.globalHashes.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.getGlobalHashesMap().put(alias, newLocal); + GlobalVariable.globalHashes.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.getGlobalHashesMap().put(saved.fullName, saved.originalHash); + GlobalVariable.globalHashes.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.getGlobalHashesMap().put(alias, saved.originalHash); + GlobalVariable.globalHashes.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 6a129873f..e4f505b65 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalRuntimeScalar.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalRuntimeScalar.java @@ -8,11 +8,8 @@ * global symbol table and restoring it when the context exits. */ public class GlobalRuntimeScalar extends RuntimeScalar { - // Localized stack is now held per-PerlRuntime. - @SuppressWarnings("unchecked") - private static Stack localizedStack() { - return (Stack) (Stack) PerlRuntime.current().globalScalarLocalizedStack; - } + // Stack to store the previous values when localized + private static final Stack localizedStack = new Stack<>(); private final String fullName; public GlobalRuntimeScalar(String fullName) { @@ -47,9 +44,9 @@ public static RuntimeScalar makeLocal(String fullName) { @Override public void dynamicSaveState() { // Save the current global reference - var originalVariable = GlobalVariable.getGlobalVariablesMap().get(fullName); + var originalVariable = GlobalVariable.globalVariables.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 @@ -67,7 +64,7 @@ public void dynamicSaveState() { } // Replace this variable in the global symbol table with the new one - GlobalVariable.getGlobalVariablesMap().put(fullName, newLocal); + GlobalVariable.globalVariables.put(fullName, newLocal); // Also update all glob aliases to point to the new local variable. // This implements Perl 5 semantics where after `*verbose = *Verbose`, @@ -75,23 +72,22 @@ public void dynamicSaveState() { java.util.List aliasGroup = GlobalVariable.getGlobAliasGroup(fullName); for (String alias : aliasGroup) { if (!alias.equals(fullName)) { - GlobalVariable.getGlobalVariablesMap().put(alias, newLocal); + GlobalVariable.globalVariables.put(alias, newLocal); } } } @Override public void dynamicRestoreState() { - Stack stack = localizedStack(); - if (!stack.isEmpty()) { - SavedGlobalState saved = stack.peek(); + if (!localizedStack.isEmpty()) { + SavedGlobalState saved = localizedStack.peek(); if (saved.fullName.equals(this.fullName)) { - stack.pop(); + localizedStack.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.getGlobalVariablesMap().get(saved.fullName); + RuntimeScalar currentVar = GlobalVariable.globalVariables.get(saved.fullName); if (currentVar != null && (currentVar.type & RuntimeScalarType.REFERENCE_BIT) != 0 && currentVar.value instanceof RuntimeBase displacedBase @@ -108,13 +104,13 @@ public void dynamicRestoreState() { } // Restore the original variable in the global symbol table - GlobalVariable.getGlobalVariablesMap().put(saved.fullName, saved.originalVariable); + GlobalVariable.globalVariables.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.getGlobalVariablesMap().put(alias, saved.originalVariable); + GlobalVariable.globalVariables.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 dc69a365e..6fdcb25ab 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java @@ -6,6 +6,7 @@ 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; @@ -20,94 +21,81 @@ * the existence of these global entities, initializing them as necessary. */ public class GlobalVariable { - // ---- 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; - } - - 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) + // 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+)$"); + // 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<>(); + /** * Marks a global variable as explicitly declared (e.g., via use vars, Exporter import). */ public static void declareGlobalVariable(String key) { - PerlRuntime.current().declaredGlobalVariables.add(key); + declaredGlobalVariables.add(key); } /** * Marks a global array as explicitly declared. */ public static void declareGlobalArray(String key) { - PerlRuntime.current().declaredGlobalArrays.add(key); + declaredGlobalArrays.add(key); } /** * Marks a global hash as explicitly declared. */ public static void declareGlobalHash(String key) { - PerlRuntime.current().declaredGlobalHashes.add(key); + declaredGlobalHashes.add(key); } /** * Checks if a global variable was explicitly declared (not just auto-vivified). */ public static boolean isDeclaredGlobalVariable(String key) { - return PerlRuntime.current().declaredGlobalVariables.contains(key) + return declaredGlobalVariables.contains(key) || key.endsWith("::a") || key.endsWith("::b"); } @@ -115,14 +103,14 @@ public static boolean isDeclaredGlobalVariable(String key) { * Checks if a global array was explicitly declared. */ public static boolean isDeclaredGlobalArray(String key) { - return PerlRuntime.current().declaredGlobalArrays.contains(key); + return declaredGlobalArrays.contains(key); } /** * Checks if a global hash was explicitly declared. */ public static boolean isDeclaredGlobalHash(String key) { - return PerlRuntime.current().declaredGlobalHashes.contains(key); + return declaredGlobalHashes.contains(key); } /** @@ -130,22 +118,21 @@ 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 - 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(); + 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(); clearPackageCache(); RuntimeCode.clearCaches(); @@ -153,9 +140,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.getInitBlocks().elements.clear(); - SpecialBlock.getEndBlocks().elements.clear(); - SpecialBlock.getCheckBlocks().elements.clear(); + SpecialBlock.initBlocks.elements.clear(); + SpecialBlock.endBlocks.elements.clear(); + SpecialBlock.checkBlocks.elements.clear(); // Method resolution caches can grow across test scripts. InheritanceResolver.invalidateCache(); @@ -175,24 +162,23 @@ public static void resetAllGlobals() { // Destroy the old classloader and create a new one // This allows the old generated classes to be garbage collected - rt.globalClassLoader = new CustomClassLoader(GlobalVariable.class.getClassLoader()); + 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 + "::"; - PerlRuntime.current().stashAliases.put(dst, src); + stashAliases.put(dst, src); } public static void clearStashAlias(String namespace) { String key = namespace.endsWith("::") ? namespace : namespace + "::"; - PerlRuntime.current().stashAliases.remove(key); + stashAliases.remove(key); } public static String resolveStashAlias(String namespace) { - PerlRuntime rt = PerlRuntime.current(); String key = namespace.endsWith("::") ? namespace : namespace + "::"; - String aliased = rt.stashAliases.get(key); + String aliased = stashAliases.get(key); if (aliased == null) { return namespace; } @@ -210,14 +196,13 @@ 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)) { - rt.globAliases.put(fromGlob, canonical); + 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)) { - rt.globAliases.put(toGlob, canonical); + globAliases.put(toGlob, canonical); } } @@ -226,7 +211,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 = PerlRuntime.current().globAliases.get(globName); + String aliased = globAliases.get(globName); if (aliased != null && !aliased.equals(globName)) { // Follow the chain in case of multiple aliases return resolveGlobAlias(aliased); @@ -242,7 +227,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 : PerlRuntime.current().globAliases.entrySet()) { + for (Map.Entry entry : globAliases.entrySet()) { if (resolveGlobAlias(entry.getKey()).equals(canonical) && !group.contains(entry.getKey())) { group.add(entry.getKey()); } @@ -258,8 +243,7 @@ public static java.util.List getGlobAliasGroup(String globName) { * @return The RuntimeScalar representing the global variable. */ public static RuntimeScalar getGlobalVariable(String key) { - PerlRuntime rt = PerlRuntime.current(); - RuntimeScalar var = rt.globalVariables.get(key); + RuntimeScalar var = globalVariables.get(key); if (var == null) { // Need to initialize global variable Matcher matcher = regexVariablePattern.matcher(key); @@ -275,20 +259,19 @@ public static RuntimeScalar getGlobalVariable(String key) { // Normal "non-magic" global variable var = new RuntimeScalar(); } - rt.globalVariables.put(key, var); + globalVariables.put(key, var); } return var; } public static RuntimeScalar aliasGlobalVariable(String key, String to) { - PerlRuntime rt = PerlRuntime.current(); - RuntimeScalar var = rt.globalVariables.get(to); - rt.globalVariables.put(key, var); + RuntimeScalar var = globalVariables.get(to); + globalVariables.put(key, var); return var; } public static void aliasGlobalVariable(String key, RuntimeScalar var) { - PerlRuntime.current().globalVariables.put(key, var); + globalVariables.put(key, var); } /** @@ -308,7 +291,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 PerlRuntime.current().globalVariables.containsKey(key) + return globalVariables.containsKey(key) || key.endsWith("::a") // $a, $b always exist || key.endsWith("::b"); } @@ -320,7 +303,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 = PerlRuntime.current().globalVariables.get(key); + RuntimeScalar var = globalVariables.get(key); return var != null && var.getDefinedBoolean(); } @@ -331,7 +314,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 PerlRuntime.current().globalVariables.remove(key); + return globalVariables.remove(key); } /** @@ -341,11 +324,10 @@ public static RuntimeScalar removeGlobalVariable(String key) { * @return The RuntimeArray representing the global array. */ public static RuntimeArray getGlobalArray(String key) { - PerlRuntime rt = PerlRuntime.current(); - RuntimeArray var = rt.globalArrays.get(key); + RuntimeArray var = globalArrays.get(key); if (var == null) { var = new RuntimeArray(); - rt.globalArrays.put(key, var); + globalArrays.put(key, var); } return var; } @@ -357,7 +339,7 @@ public static RuntimeArray getGlobalArray(String key) { * @return True if the global array exists, false otherwise. */ public static boolean existsGlobalArray(String key) { - return PerlRuntime.current().globalArrays.containsKey(key); + return globalArrays.containsKey(key); } /** @@ -367,7 +349,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 PerlRuntime.current().globalArrays.remove(key); + return globalArrays.remove(key); } /** @@ -377,8 +359,7 @@ public static RuntimeArray removeGlobalArray(String key) { * @return The RuntimeHash representing the global hash. */ public static RuntimeHash getGlobalHash(String key) { - PerlRuntime rt = PerlRuntime.current(); - RuntimeHash var = rt.globalHashes.get(key); + RuntimeHash var = globalHashes.get(key); if (var == null) { // Check if this is a package stash (ends with ::) if (key.endsWith("::")) { @@ -386,7 +367,7 @@ public static RuntimeHash getGlobalHash(String key) { } else { var = new RuntimeHash(); } - rt.globalHashes.put(key, var); + globalHashes.put(key, var); } return var; } @@ -398,7 +379,7 @@ public static RuntimeHash getGlobalHash(String key) { * @return True if the global hash exists, false otherwise. */ public static boolean existsGlobalHash(String key) { - return PerlRuntime.current().globalHashes.containsKey(key); + return globalHashes.containsKey(key); } /** @@ -408,7 +389,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 PerlRuntime.current().globalHashes.remove(key); + return globalHashes.remove(key); } /** @@ -423,18 +404,17 @@ 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 = rt.pinnedCodeRefs.get(key); + RuntimeScalar pinned = pinnedCodeRefs.get(key); if (pinned != null) { // Return the pinned ref so compiled code keeps working, but do NOT - // re-add to rt.globalCodeRefs. If it was deleted from the stash (e.g., by + // re-add to 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 = rt.globalCodeRefs.get(key); + RuntimeScalar var = globalCodeRefs.get(key); if (var == null) { var = new RuntimeScalar(); var.type = RuntimeScalarType.CODE; // value is null @@ -455,11 +435,11 @@ public static RuntimeScalar getGlobalCodeRef(String key) { // It will be set specifically for \&{string} patterns in createCodeReference var.value = runtimeCode; - rt.globalCodeRefs.put(key, var); + globalCodeRefs.put(key, var); } // Pin the RuntimeScalar so it survives stash deletion - rt.pinnedCodeRefs.put(key, var); + pinnedCodeRefs.put(key, var); return var; } @@ -467,7 +447,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 - * PerlRuntime.current().globalCodeRefs for method resolution via can() and the inheritance hierarchy. + * 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. @@ -475,10 +455,9 @@ public static RuntimeScalar getGlobalCodeRef(String key) { */ public static RuntimeScalar defineGlobalCodeRef(String key) { RuntimeScalar ref = getGlobalCodeRef(key); - 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); + // Ensure it's in globalCodeRefs so method resolution finds it + if (!globalCodeRefs.containsKey(key)) { + globalCodeRefs.put(key, ref); } return ref; } @@ -490,7 +469,7 @@ public static RuntimeScalar defineGlobalCodeRef(String key) { * @return True if the global code reference exists, false otherwise. */ public static boolean existsGlobalCodeRef(String key) { - return PerlRuntime.current().globalCodeRefs.containsKey(key); + return globalCodeRefs.containsKey(key); } /** @@ -502,9 +481,8 @@ 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) { - Map pinned = PerlRuntime.current().pinnedCodeRefs; - if (pinned.containsKey(key)) { - pinned.put(key, codeRef); + if (pinnedCodeRefs.containsKey(key)) { + pinnedCodeRefs.put(key, codeRef); } } @@ -516,7 +494,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 = PerlRuntime.current().globalCodeRefs.get(key); + RuntimeScalar var = globalCodeRefs.get(key); if (var != null && var.type == RuntimeScalarType.CODE && var.value instanceof RuntimeCode runtimeCode) { return runtimeCode.defined(); } @@ -524,7 +502,7 @@ public static boolean isGlobalCodeRefDefined(String key) { } public static RuntimeScalar existsGlobalCodeRefAsScalar(String key) { - RuntimeScalar var = PerlRuntime.current().globalCodeRefs.get(key); + RuntimeScalar var = 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 @@ -569,7 +547,7 @@ public static RuntimeScalar definedGlobalCodeRefAsScalar(String key) { } } - RuntimeScalar var = PerlRuntime.current().globalCodeRefs.get(key); + RuntimeScalar var = globalCodeRefs.get(key); if (var != null && var.type == RuntimeScalarType.CODE && var.value instanceof RuntimeCode runtimeCode) { return runtimeCode.defined() ? scalarTrue : scalarFalse; } @@ -601,7 +579,7 @@ public static RuntimeScalar definedGlobalCodeRefAsScalar(RuntimeScalar key, Stri public static RuntimeScalar deleteGlobalCodeRefAsScalar(String key) { - RuntimeScalar deleted = PerlRuntime.current().globalCodeRefs.remove(key); + RuntimeScalar deleted = globalCodeRefs.remove(key); return deleted != null ? deleted : scalarFalse; } @@ -632,7 +610,7 @@ public static RuntimeScalar deleteGlobalCodeRefAsScalar(RuntimeScalar key, Strin * @param prefix The namespace prefix (e.g., "Foo::") to clear. */ public static void clearPinnedCodeRefsForNamespace(String prefix) { - PerlRuntime.current().pinnedCodeRefs.keySet().removeIf(k -> k.startsWith(prefix)); + pinnedCodeRefs.keySet().removeIf(k -> k.startsWith(prefix)); } /** @@ -640,7 +618,7 @@ public static void clearPinnedCodeRefsForNamespace(String prefix) { * Should be called when new packages are loaded or code refs are modified. */ public static void clearPackageCache() { - PerlRuntime.current().packageExistsCache.clear(); + packageExistsCache.clear(); } /** @@ -650,9 +628,8 @@ 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 = rt.packageExistsCache.get(className); + Boolean cached = packageExistsCache.get(className); if (cached != null) { return cached; } @@ -664,11 +641,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 = rt.globalCodeRefs.keySet().stream() + boolean exists = globalCodeRefs.keySet().stream() .anyMatch(key -> key.startsWith(prefix) && !key.substring(prefix.length()).contains("::")); // Cache the result - rt.packageExistsCache.put(className, exists); + packageExistsCache.put(className, exists); return exists; } @@ -692,7 +669,7 @@ public static String resolveStashHashRedirect(String fullName) { int lastDoubleColon = fullName.lastIndexOf("::"); if (lastDoubleColon >= 0) { String pkgPart = fullName.substring(0, lastDoubleColon + 2); - RuntimeHash stashHash = PerlRuntime.current().globalHashes.get(pkgPart); + RuntimeHash stashHash = globalHashes.get(pkgPart); if (stashHash instanceof RuntimeStash stash && !stash.namespace.equals(pkgPart)) { String shortName = fullName.substring(lastDoubleColon + 2); return stash.namespace + shortName; @@ -713,11 +690,10 @@ public static String resolveStashHashRedirect(String fullName) { */ public static RuntimeGlob getGlobalIO(String key) { String resolvedKey = resolveStashHashRedirect(key); - PerlRuntime rt = PerlRuntime.current(); - RuntimeGlob glob = rt.globalIORefs.get(resolvedKey); + RuntimeGlob glob = globalIORefs.get(resolvedKey); if (glob == null) { glob = new RuntimeGlob(resolvedKey); - rt.globalIORefs.put(resolvedKey, glob); + globalIORefs.put(resolvedKey, glob); } return glob; } @@ -747,7 +723,7 @@ public static RuntimeScalar getGlobalIOCopy(String key) { * @return True if the global IO reference exists, false otherwise. */ public static boolean existsGlobalIO(String key) { - return PerlRuntime.current().globalIORefs.containsKey(key); + return globalIORefs.containsKey(key); } /** @@ -758,7 +734,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 = PerlRuntime.current().globalIORefs.get(key); + RuntimeGlob glob = 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(); @@ -775,7 +751,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 PerlRuntime.current().globalIORefs.get(key); + return globalIORefs.get(key); } /** @@ -804,41 +780,39 @@ public static RuntimeScalar definedGlob(RuntimeScalar scalar, String packageName return RuntimeScalarCache.scalarTrue; } - PerlRuntime rt = PerlRuntime.current(); - // Check if glob was explicitly assigned - if (rt.globalGlobs.getOrDefault(varName, false)) { + if (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 (rt.globalVariables.containsKey(varName)) { + if (globalVariables.containsKey(varName)) { return RuntimeScalarCache.scalarTrue; } // Check array slot - exists = defined (even if empty) - if (rt.globalArrays.containsKey(varName)) { + if (globalArrays.containsKey(varName)) { return RuntimeScalarCache.scalarTrue; } // Check hash slot - exists = defined (even if empty) - if (rt.globalHashes.containsKey(varName)) { + if (globalHashes.containsKey(varName)) { return RuntimeScalarCache.scalarTrue; } // Check code slot - slot existence makes glob defined - if (rt.globalCodeRefs.containsKey(varName)) { + if (globalCodeRefs.containsKey(varName)) { return RuntimeScalarCache.scalarTrue; } - // Check IO slot (via rt.globalIORefs) - if (rt.globalIORefs.containsKey(varName)) { + // Check IO slot (via globalIORefs) + if (globalIORefs.containsKey(varName)) { return RuntimeScalarCache.scalarTrue; } // Check format slot - if (rt.globalFormatRefs.containsKey(varName)) { + if (globalFormatRefs.containsKey(varName)) { return RuntimeScalarCache.scalarTrue; } @@ -852,11 +826,10 @@ public static RuntimeScalar definedGlob(RuntimeScalar scalar, String packageName * @return The RuntimeFormat representing the global format reference. */ public static RuntimeFormat getGlobalFormatRef(String key) { - PerlRuntime rt = PerlRuntime.current(); - RuntimeFormat format = rt.globalFormatRefs.get(key); + RuntimeFormat format = globalFormatRefs.get(key); if (format == null) { format = new RuntimeFormat(key); - rt.globalFormatRefs.put(key, format); + globalFormatRefs.put(key, format); } return format; } @@ -869,7 +842,7 @@ public static RuntimeFormat getGlobalFormatRef(String key) { * @param format The RuntimeFormat object to set. */ public static void setGlobalFormatRef(String key, RuntimeFormat format) { - PerlRuntime.current().globalFormatRefs.put(key, format); + globalFormatRefs.put(key, format); } /** @@ -879,11 +852,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 PerlRuntime.current().globalFormatRefs.containsKey(key); + return globalFormatRefs.containsKey(key); } public static RuntimeScalar existsGlobalFormatAsScalar(String key) { - return PerlRuntime.current().globalFormatRefs.containsKey(key) ? scalarTrue : scalarFalse; + return globalFormatRefs.containsKey(key) ? scalarTrue : scalarFalse; } public static RuntimeScalar existsGlobalFormatAsScalar(RuntimeScalar key) { @@ -897,14 +870,13 @@ 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 = PerlRuntime.current().globalFormatRefs.get(key); + RuntimeFormat format = globalFormatRefs.get(key); return format != null && format.isFormatDefined(); } public static RuntimeScalar definedGlobalFormatAsScalar(String key) { - PerlRuntime rt = PerlRuntime.current(); - return rt.globalFormatRefs.containsKey(key) ? - (rt.globalFormatRefs.get(key).isFormatDefined() ? scalarTrue : scalarFalse) : scalarFalse; + return globalFormatRefs.containsKey(key) ? + (globalFormatRefs.get(key).isFormatDefined() ? scalarTrue : scalarFalse) : scalarFalse; } public static RuntimeScalar definedGlobalFormatAsScalar(RuntimeScalar key) { @@ -918,9 +890,8 @@ 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 : rt.globalVariables.entrySet()) { + for (Map.Entry entry : globalVariables.entrySet()) { String key = entry.getKey(); if (key.startsWith(currentPackage) && shouldResetVariable(key, currentPackage, resetChars)) { @@ -930,7 +901,7 @@ public static void resetGlobalVariables(Set resetChars, String curren } // Reset array variables - for (Map.Entry entry : rt.globalArrays.entrySet()) { + for (Map.Entry entry : globalArrays.entrySet()) { String key = entry.getKey(); if (key.startsWith(currentPackage) && shouldResetVariable(key, currentPackage, resetChars)) { @@ -940,7 +911,7 @@ public static void resetGlobalVariables(Set resetChars, String curren } // Reset hash variables - for (Map.Entry entry : rt.globalHashes.entrySet()) { + for (Map.Entry entry : globalHashes.entrySet()) { String key = entry.getKey(); if (key.startsWith(currentPackage) && shouldResetVariable(key, currentPackage, resetChars)) { @@ -992,7 +963,7 @@ private static boolean shouldResetVariable(String fullKey, String packagePrefix, */ public static Map getAllIsaArrays() { Map result = new HashMap<>(); - for (Map.Entry entry : PerlRuntime.current().globalArrays.entrySet()) { + for (Map.Entry entry : 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 ddd9c92ee..f4f69d721 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.getGlobalMatcher(); + Matcher matcher = RuntimeRegex.globalMatcher; 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.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()); + 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()); // 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.getGlobalMatcher(); + Matcher matcher = RuntimeRegex.globalMatcher; 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.getGlobalVariablesMap(), prefix) || - containsNamespace(GlobalVariable.getGlobalArraysMap(), prefix) || - containsNamespace(GlobalVariable.getGlobalHashesMap(), prefix) || - containsNamespace(GlobalVariable.getGlobalCodeRefsMap(), prefix) || - containsNamespace(GlobalVariable.getGlobalIORefsMap(), prefix) || - containsNamespace(GlobalVariable.getGlobalFormatRefsMap(), 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)) { 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.getGlobalMatcher(); + Matcher matcher = RuntimeRegex.globalMatcher; 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.getGlobalMatcher(); + Matcher matcher = RuntimeRegex.globalMatcher; 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.getGlobalVariablesMap(), fullKey) || - containsNamespace(GlobalVariable.getGlobalArraysMap(), fullKey) || - containsNamespace(GlobalVariable.getGlobalHashesMap(), fullKey) || - containsNamespace(GlobalVariable.getGlobalCodeRefsMap(), fullKey) || - containsNamespace(GlobalVariable.getGlobalIORefsMap(), fullKey) || - containsNamespace(GlobalVariable.getGlobalFormatRefsMap(), fullKey); + 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); 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.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); + 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); // 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.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)); + 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)); 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 8a1445f93..eeef675e5 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/OutputAutoFlushVariable.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/OutputAutoFlushVariable.java @@ -7,15 +7,11 @@ */ public class OutputAutoFlushVariable extends RuntimeScalar { - // State stack is now held per-PerlRuntime. - @SuppressWarnings("unchecked") - private static Stack stateStack() { - return (Stack) (Stack) PerlRuntime.current().autoFlushStateStack; - } + private static final Stack stateStack = new Stack<>(); private static RuntimeIO currentHandle() { - RuntimeIO handle = RuntimeIO.getSelectedHandle(); - return handle != null ? handle : RuntimeIO.getStdout(); + RuntimeIO handle = RuntimeIO.selectedHandle; + return handle != null ? handle : RuntimeIO.stdout; } @Override @@ -85,14 +81,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 bc40c7ef5..747982fee 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/OutputFieldSeparator.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/OutputFieldSeparator.java @@ -15,13 +15,16 @@ */ 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 Stack ofsStack() { - return PerlRuntime.current().ofsStack; - } + private static final Stack ofsStack = new Stack<>(); public OutputFieldSeparator() { super(); @@ -29,10 +32,9 @@ public OutputFieldSeparator() { /** * Returns the internal OFS value for use by print. - * Now per-PerlRuntime for multiplicity thread-safety. */ public static String getInternalOFS() { - return PerlRuntime.current().internalOFS; + return internalOFS; } /** @@ -40,8 +42,7 @@ public static String getInternalOFS() { * Called from GlobalRuntimeScalar.dynamicSaveState() when localizing $,. */ public static void saveInternalOFS() { - PerlRuntime rt = PerlRuntime.current(); - ofsStack().push(rt.internalOFS); + ofsStack.push(internalOFS); } /** @@ -49,50 +50,50 @@ public static void saveInternalOFS() { * Called from GlobalRuntimeScalar.dynamicRestoreState() when restoring $,. */ public static void restoreInternalOFS() { - if (!ofsStack().isEmpty()) { - PerlRuntime.current().internalOFS = ofsStack().pop(); + if (!ofsStack.isEmpty()) { + internalOFS = ofsStack.pop(); } } @Override public RuntimeScalar set(RuntimeScalar value) { super.set(value); - PerlRuntime.current().internalOFS = this.toString(); + internalOFS = this.toString(); return this; } @Override public RuntimeScalar set(String value) { super.set(value); - PerlRuntime.current().internalOFS = this.toString(); + internalOFS = this.toString(); return this; } @Override public RuntimeScalar set(int value) { super.set(value); - PerlRuntime.current().internalOFS = this.toString(); + internalOFS = this.toString(); return this; } @Override public RuntimeScalar set(long value) { super.set(value); - PerlRuntime.current().internalOFS = this.toString(); + internalOFS = this.toString(); return this; } @Override public RuntimeScalar set(boolean value) { super.set(value); - PerlRuntime.current().internalOFS = this.toString(); + internalOFS = this.toString(); return this; } @Override public RuntimeScalar set(Object value) { super.set(value); - PerlRuntime.current().internalOFS = this.toString(); + 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 e8cd691ed..f35fe8991 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/OutputRecordSeparator.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/OutputRecordSeparator.java @@ -20,13 +20,16 @@ */ 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 Stack orsStack() { - return PerlRuntime.current().orsStack; - } + private static final Stack orsStack = new Stack<>(); public OutputRecordSeparator() { super(); @@ -34,10 +37,9 @@ public OutputRecordSeparator() { /** * Returns the internal ORS value for use by print. - * Now per-PerlRuntime for multiplicity thread-safety. */ public static String getInternalORS() { - return PerlRuntime.current().internalORS; + return internalORS; } /** @@ -45,8 +47,7 @@ public static String getInternalORS() { * Called from GlobalRuntimeScalar.dynamicSaveState() when localizing $\. */ public static void saveInternalORS() { - PerlRuntime rt = PerlRuntime.current(); - orsStack().push(rt.internalORS); + orsStack.push(internalORS); } /** @@ -54,50 +55,50 @@ public static void saveInternalORS() { * Called from GlobalRuntimeScalar.dynamicRestoreState() when restoring $\. */ public static void restoreInternalORS() { - if (!orsStack().isEmpty()) { - PerlRuntime.current().internalORS = orsStack().pop(); + if (!orsStack.isEmpty()) { + internalORS = orsStack.pop(); } } @Override public RuntimeScalar set(RuntimeScalar value) { super.set(value); - PerlRuntime.current().internalORS = this.toString(); + internalORS = this.toString(); return this; } @Override public RuntimeScalar set(String value) { super.set(value); - PerlRuntime.current().internalORS = this.toString(); + internalORS = this.toString(); return this; } @Override public RuntimeScalar set(int value) { super.set(value); - PerlRuntime.current().internalORS = this.toString(); + internalORS = this.toString(); return this; } @Override public RuntimeScalar set(long value) { super.set(value); - PerlRuntime.current().internalORS = this.toString(); + internalORS = this.toString(); return this; } @Override public RuntimeScalar set(boolean value) { super.set(value); - PerlRuntime.current().internalORS = this.toString(); + internalORS = this.toString(); return this; } @Override public RuntimeScalar set(Object value) { super.set(value); - PerlRuntime.current().internalORS = this.toString(); + 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 deleted file mode 100644 index 3fa4fa437..000000000 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/PerlRuntime.java +++ /dev/null @@ -1,644 +0,0 @@ -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 fe3783b5f..e4e9a4455 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RegexState.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RegexState.java @@ -4,7 +4,6 @@ import java.util.regex.Matcher; - /** * Snapshot of regex-related global state (Perl's $1, $&, $`, $', etc.). * @@ -29,21 +28,19 @@ public class RegexState implements DynamicState { private final boolean lastMatchWasByteString; public RegexState() { - // 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; + 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; } public static void save() { @@ -60,20 +57,18 @@ public void restore() { @Override public void dynamicRestoreState() { - // 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; + 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; } } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeArray.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeArray.java index c0a7fa977..081e60e4b 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeArray.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeArray.java @@ -20,10 +20,8 @@ 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; - // Dynamic state stack is now held per-PerlRuntime. - private static Stack dynamicStateStack() { - return PerlRuntime.current().arrayDynamicStateStack; - } + // Static stack to store saved "local" states of RuntimeArray instances + private static final Stack dynamicStateStack = new Stack<>(); // Internal type of array - PLAIN_ARRAY, AUTOVIVIFY_ARRAY, TIED_ARRAY, or READONLY_ARRAY public int type; public boolean strictAutovivify; @@ -1174,7 +1172,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); @@ -1193,9 +1191,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 584ef53dd..196d0dd75 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeArrayProxyEntry.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeArrayProxyEntry.java @@ -9,13 +9,8 @@ * when they are accessed. */ public class RuntimeArrayProxyEntry extends RuntimeBaseProxy { - // Dynamic state stacks are now held per-PerlRuntime. - private static Stack dynamicStateStackInt() { - return PerlRuntime.current().arrayProxyDynamicStateStackInt; - } - private static Stack dynamicStateStack() { - return PerlRuntime.current().arrayProxyDynamicStateStack; - } + private static final Stack dynamicStateStackInt = new Stack<>(); + private static final Stack dynamicStateStack = new Stack<>(); // Reference to the parent RuntimeArray private final RuntimeArray parent; @@ -96,10 +91,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(); @@ -107,7 +102,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(); } @@ -121,9 +116,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. @@ -144,7 +139,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 f57fc5d34..387a73ff6 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java @@ -1,7 +1,6 @@ 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; @@ -53,10 +52,7 @@ public class RuntimeCode extends RuntimeBase implements RuntimeScalarReference { // Lookup object for performing method handle operations public static final MethodHandles.Lookup lookup = MethodHandles.lookup(); - // evalBeginIds migrated to PerlRuntime; access via getEvalBeginIds() - public static IdentityHashMap getEvalBeginIds() { - return PerlRuntime.current().evalBeginIds; - } + public static final IdentityHashMap evalBeginIds = new IdentityHashMap<>(); /** * Flag to control whether eval STRING should use the interpreter backend. @@ -101,18 +97,26 @@ public static IdentityHashMap getEvalBeginIds() { * - 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 PerlRuntime storage, so parallel + * Thread-safety: Each thread's eval compilation uses its own ThreadLocal storage, so parallel * eval compilations don't interfere with each other. */ - // 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; - } + 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; + } + }; /** * Flag to enable disassembly of eval STRING bytecode. * When set, prints the interpreter bytecode for each eval STRING compilation. @@ -131,17 +135,9 @@ private static Map, MethodHandle> getMethodHandleCache() { /** * Tracks the current eval nesting depth for $^S support. * 0 = not inside any eval, >0 = inside eval (eval STRING or eval BLOCK). - * Migrated to PerlRuntime; access via getEvalDepth()/incrementEvalDepth()/decrementEvalDepth(). + * Incremented on eval entry, decremented on eval exit (success or failure). */ - public static int getEvalDepth() { - return PerlRuntime.current().evalDepth; - } - public static void incrementEvalDepth() { - PerlRuntime.current().evalDepth++; - } - public static void decrementEvalDepth() { - PerlRuntime.current().evalDepth--; - } + public static int evalDepth = 0; /** * Thread-local stack of @_ arrays for each active subroutine call. @@ -150,9 +146,9 @@ public static void decrementEvalDepth() { * * 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. @@ -162,7 +158,7 @@ public static void decrementEvalDepth() { * @return The current @_ array, or null if not in a subroutine */ public static RuntimeArray getCurrentArgs() { - Deque stack = PerlRuntime.current().argsStack; + Deque stack = argsStack.get(); return stack.isEmpty() ? null : stack.peek(); } @@ -178,7 +174,7 @@ public static RuntimeArray getCurrentArgs() { * @return The caller's @_ array, or null if not available */ public static RuntimeArray getCallerArgs() { - Deque stack = PerlRuntime.current().argsStack; + Deque stack = argsStack.get(); if (stack.size() < 2) { return null; } @@ -192,7 +188,7 @@ public static RuntimeArray getCallerArgs() { * Public so BytecodeInterpreter can use it when calling InterpretedCode directly. */ public static void pushArgs(RuntimeArray args) { - PerlRuntime.current().argsStack.push(args); + argsStack.get().push(args); } /** @@ -200,7 +196,7 @@ public static void pushArgs(RuntimeArray args) { * Public so BytecodeInterpreter can use it when calling InterpretedCode directly. */ public static void popArgs() { - Deque stack = PerlRuntime.current().argsStack; + Deque stack = argsStack.get(); if (!stack.isEmpty()) { stack.pop(); } @@ -233,28 +229,29 @@ public static void popArgs() { * This optimization provides ~50% speedup for method-heavy code like: * while ($i < 10000) { $obj->method($arg); $i++ } */ - // Inline cache arrays migrated to PerlRuntime; access via PerlRuntime.current() + 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]; private static int nextCallsiteId = 0; public static int allocateMethodCallsiteId() { - return nextCallsiteId++ % PerlRuntime.METHOD_CALL_CACHE_SIZE; + return nextCallsiteId++ % METHOD_CALL_CACHE_SIZE; } /** * Clear the inline method cache. Should be called when method definitions change. */ public static void clearInlineMethodCache() { - 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); + java.util.Arrays.fill(inlineCacheBlessId, 0); + java.util.Arrays.fill(inlineCacheMethodHash, 0); + java.util.Arrays.fill(inlineCacheCode, null); } - // 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; } + // 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 // 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) @@ -474,7 +471,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) PerlRuntime.current().evalRuntimeContext; + return evalRuntimeContext.get(); } /** @@ -485,10 +482,9 @@ public static EvalRuntimeContext getEvalRuntimeContext() { * @return The saved eval runtime context (may be null) */ public static EvalRuntimeContext saveAndClearEvalRuntimeContext() { - PerlRuntime rt = PerlRuntime.current(); - EvalRuntimeContext saved = (EvalRuntimeContext) rt.evalRuntimeContext; + EvalRuntimeContext saved = evalRuntimeContext.get(); if (saved != null) { - rt.evalRuntimeContext = null; + evalRuntimeContext.remove(); } return saved; } @@ -500,7 +496,7 @@ public static EvalRuntimeContext saveAndClearEvalRuntimeContext() { */ public static void restoreEvalRuntimeContext(EvalRuntimeContext saved) { if (saved != null) { - PerlRuntime.current().evalRuntimeContext = saved; + evalRuntimeContext.set(saved); } } @@ -516,13 +512,12 @@ public static synchronized String getNextEvalFilename() { // Add a method to clear caches when globals are reset public static void clearCaches() { - PerlRuntime rt = PerlRuntime.current(); - rt.evalCache.clear(); - rt.methodHandleCache.clear(); - rt.anonSubs.clear(); - rt.interpretedSubs.clear(); - rt.evalContext.clear(); - rt.evalRuntimeContext = null; + evalCache.clear(); + methodHandleCache.clear(); + anonSubs.clear(); + interpretedSubs.clear(); + evalContext.clear(); + evalRuntimeContext.remove(); } public static void copy(RuntimeCode code, RuntimeCode codeFrom) { @@ -576,13 +571,8 @@ 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.getEvalContext().get(evalTag); + EmitterContext ctx = RuntimeCode.evalContext.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, @@ -615,7 +605,7 @@ public static Class evalStringHelper(RuntimeScalar code, String evalTag, Obje ctx.capturedEnv, // Variable names in same order as runtimeValues evalTag ); - PerlRuntime.current().evalRuntimeContext = runtimeCtx; + evalRuntimeContext.set(runtimeCtx); try { // Check if the eval string contains non-ASCII characters @@ -671,10 +661,9 @@ 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) { - Map> cache = getEvalCache(); - synchronized (cache) { - if (cache.containsKey(cacheKey)) { - cachedClass = cache.get(cacheKey); + synchronized (evalCache) { + if (evalCache.containsKey(cacheKey)) { + cachedClass = evalCache.get(cacheKey); } } @@ -741,19 +730,19 @@ public static Class evalStringHelper(RuntimeScalar code, String evalTag, Obje // variable reinitialization in loops. OperatorNode ast = entry.ast(); if (ast != null) { - int beginId = getEvalBeginIds().computeIfAbsent( + int beginId = evalBeginIds.computeIfAbsent( ast, - k -> EmitterMethodCreator.classCounter.getAndIncrement()); + k -> EmitterMethodCreator.classCounter++); String packageName = PersistentVariable.beginPackage(beginId); String varNameWithoutSigil = entry.name().substring(1); String fullName = packageName + "::" + varNameWithoutSigil; if (runtimeValue instanceof RuntimeArray) { - GlobalVariable.getGlobalArraysMap().put(fullName, (RuntimeArray) runtimeValue); + GlobalVariable.globalArrays.put(fullName, (RuntimeArray) runtimeValue); } else if (runtimeValue instanceof RuntimeHash) { - GlobalVariable.getGlobalHashesMap().put(fullName, (RuntimeHash) runtimeValue); + GlobalVariable.globalHashes.put(fullName, (RuntimeHash) runtimeValue); } else if (runtimeValue instanceof RuntimeScalar) { - GlobalVariable.getGlobalVariablesMap().put(fullName, (RuntimeScalar) runtimeValue); + GlobalVariable.globalVariables.put(fullName, (RuntimeScalar) runtimeValue); } evalAliasKeys.add(entry.name().charAt(0) + fullName); } @@ -896,9 +885,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.getGlobalVariablesMap().remove(fullName); - case '@' -> GlobalVariable.getGlobalArraysMap().remove(fullName); - case '%' -> GlobalVariable.getGlobalHashesMap().remove(fullName); + case '$' -> GlobalVariable.globalVariables.remove(fullName); + case '@' -> GlobalVariable.globalArrays.remove(fullName); + case '%' -> GlobalVariable.globalHashes.remove(fullName); } } @@ -910,9 +899,8 @@ public static Class evalStringHelper(RuntimeScalar code, String evalTag, Obje // Cache the result (unless debugging is enabled) if (!isDebugging) { - Map> cache = getEvalCache(); - synchronized (cache) { - cache.put(cacheKey, generatedClass); + synchronized (evalCache) { + evalCache.put(cacheKey, generatedClass); } } @@ -927,11 +915,7 @@ 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. - PerlRuntime.current().evalRuntimeContext = null; - } - - } finally { - PerlLanguageProvider.COMPILE_LOCK.unlock(); + evalRuntimeContext.remove(); } } @@ -1071,7 +1055,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.getEvalContext().get(evalTag); + EmitterContext ctx = RuntimeCode.evalContext.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, @@ -1086,11 +1070,6 @@ 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. @@ -1102,7 +1081,7 @@ public static RuntimeList evalStringWithInterpreter( ctx.capturedEnv, evalTag ); - PerlRuntime.current().evalRuntimeContext = runtimeCtx; + evalRuntimeContext.set(runtimeCtx); InterpretedCode interpretedCode = null; RuntimeList result; @@ -1169,19 +1148,19 @@ public static RuntimeList evalStringWithInterpreter( if (runtimeValue != null) { OperatorNode operatorAst = entry.ast(); if (operatorAst != null) { - int beginId = getEvalBeginIds().computeIfAbsent( + int beginId = evalBeginIds.computeIfAbsent( operatorAst, - k -> EmitterMethodCreator.classCounter.getAndIncrement()); + k -> EmitterMethodCreator.classCounter++); String packageName = PersistentVariable.beginPackage(beginId); String varNameWithoutSigil = entry.name().substring(1); String fullName = packageName + "::" + varNameWithoutSigil; if (runtimeValue instanceof RuntimeArray) { - GlobalVariable.getGlobalArraysMap().put(fullName, (RuntimeArray) runtimeValue); + GlobalVariable.globalArrays.put(fullName, (RuntimeArray) runtimeValue); } else if (runtimeValue instanceof RuntimeHash) { - GlobalVariable.getGlobalHashesMap().put(fullName, (RuntimeHash) runtimeValue); + GlobalVariable.globalHashes.put(fullName, (RuntimeHash) runtimeValue); } else if (runtimeValue instanceof RuntimeScalar) { - GlobalVariable.getGlobalVariablesMap().put(fullName, (RuntimeScalar) runtimeValue); + GlobalVariable.globalVariables.put(fullName, (RuntimeScalar) runtimeValue); } evalAliasKeys.add(entry.name().charAt(0) + fullName); } @@ -1344,24 +1323,16 @@ public static RuntimeList evalStringWithInterpreter( for (String key : evalAliasKeys) { String fullName = key.substring(1); switch (key.charAt(0)) { - case '$' -> GlobalVariable.getGlobalVariablesMap().remove(fullName); - case '@' -> GlobalVariable.getGlobalArraysMap().remove(fullName); - case '%' -> GlobalVariable.getGlobalHashesMap().remove(fullName); + case '$' -> GlobalVariable.globalVariables.remove(fullName); + case '@' -> GlobalVariable.globalArrays.remove(fullName); + case '%' -> GlobalVariable.globalHashes.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 - incrementEvalDepth(); + evalDepth++; try { result = interpretedCode.apply(args, callContext); @@ -1419,8 +1390,9 @@ public static RuntimeList evalStringWithInterpreter( return new RuntimeList(new RuntimeScalar()); } } finally { - decrementEvalDepth(); + evalDepth--; } + } finally { evalTrace("evalStringWithInterpreter exit tag=" + evalTag + " ctx=" + callContext + " $@=" + GlobalVariable.getGlobalVariable("main::@")); @@ -1429,9 +1401,6 @@ 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 @@ -1443,16 +1412,8 @@ public static RuntimeList evalStringWithInterpreter( storeSourceLines(code.toString(), evalFilename, ast, tokens); } - // 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(); - } + // Clean up ThreadLocal + evalRuntimeContext.remove(); } } @@ -1602,13 +1563,12 @@ public static RuntimeList callCached(int callsiteId, int blessId = ((RuntimeBase) invocant.value).blessId; if (blessId != 0) { int methodHash = System.identityHashCode(method.value); - int cacheIndex = callsiteId & (PerlRuntime.METHOD_CALL_CACHE_SIZE - 1); - PerlRuntime rt = PerlRuntime.current(); + int cacheIndex = callsiteId & (METHOD_CALL_CACHE_SIZE - 1); // Check if cache hit - if (rt.inlineCacheBlessId[cacheIndex] == blessId && - rt.inlineCacheMethodHash[cacheIndex] == methodHash) { - RuntimeCode cachedCode = rt.inlineCacheCode[cacheIndex]; + if (inlineCacheBlessId[cacheIndex] == blessId && + inlineCacheMethodHash[cacheIndex] == methodHash) { + RuntimeCode cachedCode = inlineCacheCode[cacheIndex]; if (cachedCode != null && (cachedCode.subroutine != null || cachedCode.methodHandle != null)) { // Cache hit - ultra fast path: directly invoke method try { @@ -1667,9 +1627,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 - rt.inlineCacheBlessId[cacheIndex] = blessId; - rt.inlineCacheMethodHash[cacheIndex] = methodHash; - rt.inlineCacheCode[cacheIndex] = code; + inlineCacheBlessId[cacheIndex] = blessId; + inlineCacheMethodHash[cacheIndex] = methodHash; + inlineCacheCode[cacheIndex] = code; } // Call the method @@ -2262,9 +2222,15 @@ 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); - // Batch push: caller bits, hints, hint hash, and warning bits in one PerlRuntime.current() call - PerlRuntime rt = PerlRuntime.current(); - rt.pushCallerState(warningBits); + 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(); try { // Cast the value to RuntimeCode and call apply() RuntimeList result = code.apply(a, callContext); @@ -2291,7 +2257,12 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, RuntimeArray a, int // Consume at normal subroutine boundary return e.returnValue != null ? e.returnValue.getList() : new RuntimeList(); } finally { - rt.popCallerState(warningBits != null); + HintHashRegistry.popCallerHintHash(); + WarningBitsRegistry.popCallerHints(); + WarningBitsRegistry.popCallerBits(); + if (warningBits != null) { + WarningBitsRegistry.popCurrent(); + } // 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, @@ -2346,7 +2317,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) { - incrementEvalDepth(); + evalDepth++; try { RuntimeList result = apply(runtimeScalar, a, callContext); // Perl clears $@ on successful eval (even if nested evals previously set it). @@ -2379,7 +2350,7 @@ public static RuntimeList applyEval(RuntimeScalar runtimeScalar, RuntimeArray a, } return new RuntimeList(new RuntimeScalar()); } finally { - decrementEvalDepth(); + evalDepth--; // 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) { @@ -2505,9 +2476,15 @@ 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); - // Batch push: caller bits, hints, hint hash, and warning bits in one PerlRuntime.current() call - PerlRuntime rt = PerlRuntime.current(); - rt.pushCallerState(warningBits); + 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(); try { // Cast the value to RuntimeCode and call apply() return code.apply(subroutineName, a, callContext); @@ -2519,7 +2496,12 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, String subroutineNa // Consume at normal subroutine boundary return e.returnValue != null ? e.returnValue.getList() : new RuntimeList(); } finally { - rt.popCallerState(warningBits != null); + HintHashRegistry.popCallerHintHash(); + WarningBitsRegistry.popCallerHints(); + WarningBitsRegistry.popCallerBits(); + if (warningBits != null) { + WarningBitsRegistry.popCurrent(); + } } } @@ -2660,9 +2642,15 @@ 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); - // Batch push: caller bits, hints, hint hash, and warning bits in one PerlRuntime.current() call - PerlRuntime rt = PerlRuntime.current(); - rt.pushCallerState(warningBits); + 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(); try { // Cast the value to RuntimeCode and call apply() return code.apply(subroutineName, a, callContext); @@ -2674,7 +2662,12 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, String subroutineNa // Consume at normal subroutine boundary return e.returnValue != null ? e.returnValue.getList() : new RuntimeList(); } finally { - rt.popCallerState(warningBits != null); + HintHashRegistry.popCallerHintHash(); + WarningBitsRegistry.popCallerHints(); + WarningBitsRegistry.popCallerBits(); + if (warningBits != null) { + WarningBitsRegistry.popCurrent(); + } } } @@ -3070,11 +3063,13 @@ 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); - PerlRuntime rt = PerlRuntime.current(); - rt.pushSubState(a, warningBits); + if (warningBits != null) { + WarningBitsRegistry.pushCurrent(warningBits); + } try { RuntimeList result; // Prefer functional interface over MethodHandle for better performance @@ -3087,7 +3082,10 @@ public RuntimeList apply(RuntimeArray a, int callContext) { } return result; } finally { - rt.popSubState(warningBits != null); + if (warningBits != null) { + WarningBitsRegistry.popCurrent(); + } + popArgs(); if (DebugState.debugMode) { DebugHooks.exitSubroutine(); DebugState.popArgs(); @@ -3162,11 +3160,13 @@ 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); - PerlRuntime rt = PerlRuntime.current(); - rt.pushSubState(a, warningBits); + if (warningBits != null) { + WarningBitsRegistry.pushCurrent(warningBits); + } try { RuntimeList result; // Prefer functional interface over MethodHandle for better performance @@ -3179,7 +3179,10 @@ public RuntimeList apply(String subroutineName, RuntimeArray a, int callContext) } return result; } finally { - rt.popSubState(warningBits != null); + if (warningBits != null) { + WarningBitsRegistry.popCurrent(); + } + popArgs(); 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 0945d2182..9accc7559 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java @@ -15,11 +15,7 @@ */ public class RuntimeGlob extends RuntimeScalar implements RuntimeScalarReference { - // Glob slot stack is now held per-PerlRuntime. - @SuppressWarnings("unchecked") - private static Stack globSlotStack() { - return (Stack) (Stack) PerlRuntime.current().globSlotStack; - } + private static final Stack globSlotStack = new Stack<>(); // The name of the typeglob public String globName; public RuntimeScalar IO; @@ -155,7 +151,7 @@ public boolean equals(Object obj) { } public static boolean isGlobAssigned(String globName) { - return GlobalVariable.getGlobalGlobsMap().getOrDefault(globName, false); + return GlobalVariable.globalGlobs.getOrDefault(globName, false); } /** @@ -167,27 +163,27 @@ public static boolean isGlobAssigned(String globName) { */ public RuntimeScalar defined() { // Check if the glob has been assigned (any slot has content) - if (GlobalVariable.getGlobalGlobsMap().getOrDefault(this.globName, false)) { + if (GlobalVariable.globalGlobs.getOrDefault(this.globName, false)) { return RuntimeScalarCache.scalarTrue; } // Check scalar slot - must have defined value - if (GlobalVariable.getGlobalVariablesMap().containsKey(this.globName)) { - RuntimeScalar scalar = GlobalVariable.getGlobalVariablesMap().get(this.globName); + if (GlobalVariable.globalVariables.containsKey(this.globName)) { + RuntimeScalar scalar = GlobalVariable.globalVariables.get(this.globName); if (scalar != null && scalar.getDefinedBoolean()) { return RuntimeScalarCache.scalarTrue; } } // Check array slot - exists = defined (even if empty) - if (GlobalVariable.getGlobalArraysMap().containsKey(this.globName)) { + if (GlobalVariable.globalArrays.containsKey(this.globName)) { return RuntimeScalarCache.scalarTrue; } // Check hash slot - exists = defined (even if empty) - if (GlobalVariable.getGlobalHashesMap().containsKey(this.globName)) { + if (GlobalVariable.globalHashes.containsKey(this.globName)) { return RuntimeScalarCache.scalarTrue; } // Check code slot - must have defined value - if (GlobalVariable.getGlobalCodeRefsMap().containsKey(this.globName)) { - RuntimeScalar code = GlobalVariable.getGlobalCodeRefsMap().get(this.globName); + if (GlobalVariable.globalCodeRefs.containsKey(this.globName)) { + RuntimeScalar code = GlobalVariable.globalCodeRefs.get(this.globName); if (code != null && code.getDefinedBoolean()) { return RuntimeScalarCache.scalarTrue; } @@ -239,7 +235,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.getIsSubsMap().put(this.globName, true); + GlobalVariable.isSubs.put(this.globName, true); // Increment package generation counter for mro::get_pkg_gen int lastColonIdx = this.globName.lastIndexOf("::"); @@ -264,7 +260,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.getGlobalArraysMap().put(aliasedName, arr); + GlobalVariable.globalArrays.put(aliasedName, arr); } // Mark as explicitly declared for strict vars (e.g., Exporter imports) GlobalVariable.declareGlobalArray(this.globName); @@ -275,7 +271,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.getGlobalHashesMap().put(aliasedName, hash); + GlobalVariable.globalHashes.put(aliasedName, hash); } // Mark as explicitly declared for strict vars (e.g., Exporter imports) GlobalVariable.declareGlobalHash(this.globName); @@ -365,9 +361,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.getSelectedHandle() + if (oldRuntimeIO != null && oldRuntimeIO == RuntimeIO.selectedHandle && value.IO != null && value.IO.value instanceof RuntimeIO newRIO) { - RuntimeIO.setSelectedHandle(newRIO); + RuntimeIO.selectedHandle = newRIO; } return value.scalar(); @@ -418,22 +414,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.getSelectedHandle() + if (oldRuntimeIO != null && oldRuntimeIO == RuntimeIO.selectedHandle && sourceIO.IO != null && sourceIO.IO.value instanceof RuntimeIO newRIO) { - RuntimeIO.setSelectedHandle(newRIO); + RuntimeIO.selectedHandle = newRIO; } // Alias the ARRAY slot: both names point to the same RuntimeArray object RuntimeArray sourceArray = GlobalVariable.getGlobalArray(globName); - GlobalVariable.getGlobalArraysMap().put(this.globName, sourceArray); + GlobalVariable.globalArrays.put(this.globName, sourceArray); // Alias the HASH slot: both names point to the same RuntimeHash object RuntimeHash sourceHash = GlobalVariable.getGlobalHash(globName); - GlobalVariable.getGlobalHashesMap().put(this.globName, sourceHash); + GlobalVariable.globalHashes.put(this.globName, sourceHash); // Alias the SCALAR slot: both names point to the same RuntimeScalar object RuntimeScalar sourceScalar = GlobalVariable.getGlobalVariable(globName); - GlobalVariable.getGlobalVariablesMap().put(this.globName, sourceScalar); + GlobalVariable.globalVariables.put(this.globName, sourceScalar); // Alias the FORMAT slot: both names point to the same RuntimeFormat object RuntimeFormat sourceFormat = GlobalVariable.getGlobalFormatRef(globName); @@ -456,7 +452,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.getGlobalGlobsMap().put(globName, true); + GlobalVariable.globalGlobs.put(globName, true); } /** @@ -482,7 +478,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.getGlobalCodeRefsMap().get(this.globName); + RuntimeScalar codeRef = GlobalVariable.globalCodeRefs.get(this.globName); if (codeRef != null && codeRef.type == RuntimeScalarType.CODE && codeRef.value instanceof RuntimeCode code) { if (code.defined() || code.isDeclared) { yield codeRef; @@ -618,8 +614,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.getSelectedHandle()) { - RuntimeIO.setSelectedHandle(runtimeIO); + if (oldIO != null && oldIO == RuntimeIO.selectedHandle) { + RuntimeIO.selectedHandle = runtimeIO; } } return this; @@ -644,8 +640,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.getSelectedHandle()) { - RuntimeIO.setSelectedHandle(io); + if (oldIO != null && oldIO == RuntimeIO.selectedHandle) { + RuntimeIO.selectedHandle = io; } return this; } @@ -871,10 +867,10 @@ public RuntimeGlob undefine() { GlobalVariable.getGlobalVariable(this.globName).set(new RuntimeScalar()); // Undefine ARRAY - create empty array - GlobalVariable.getGlobalArraysMap().put(this.globName, new RuntimeArray()); + GlobalVariable.globalArrays.put(this.globName, new RuntimeArray()); // Undefine HASH - create empty hash - GlobalVariable.getGlobalHashesMap().put(this.globName, new RuntimeHash()); + GlobalVariable.globalHashes.put(this.globName, new RuntimeHash()); return this; } @@ -893,24 +889,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.getSelectedHandle()) { - savedSelectedHandle = RuntimeIO.getSelectedHandle(); + if (this.IO != null && this.IO.value instanceof RuntimeIO rio && rio == RuntimeIO.selectedHandle) { + savedSelectedHandle = RuntimeIO.selectedHandle; isSelectedHandle = true; - } else if (this.IO != null && this.IO.type == TIED_SCALAR && this.IO.value == RuntimeIO.getSelectedHandle()) { - savedSelectedHandle = RuntimeIO.getSelectedHandle(); + } else if (this.IO != null && this.IO.type == TIED_SCALAR && this.IO.value == RuntimeIO.selectedHandle) { + savedSelectedHandle = RuntimeIO.selectedHandle; 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.getGlobalVariablesMap().put(this.globName, new RuntimeScalar()); - GlobalVariable.getGlobalArraysMap().put(this.globName, new RuntimeArray()); - GlobalVariable.getGlobalHashesMap().put(this.globName, new RuntimeHash()); + GlobalVariable.globalVariables.put(this.globName, new RuntimeScalar()); + GlobalVariable.globalArrays.put(this.globName, new RuntimeArray()); + GlobalVariable.globalHashes.put(this.globName, new RuntimeHash()); RuntimeScalar newCode = new RuntimeScalar(); - GlobalVariable.getGlobalCodeRefsMap().put(this.globName, newCode); + GlobalVariable.globalCodeRefs.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 @@ -938,15 +934,15 @@ public void dynamicSaveState() { RuntimeIO stubIO = new RuntimeIO(); stubIO.globName = this.globName; newGlob.IO = new RuntimeScalar(stubIO); - RuntimeIO.setSelectedHandle(stubIO); + RuntimeIO.selectedHandle = stubIO; } - GlobalVariable.getGlobalIORefsMap().put(this.globName, newGlob); + GlobalVariable.globalIORefs.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; @@ -955,19 +951,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.setSelectedHandle(snap.savedSelectedHandle); + RuntimeIO.selectedHandle = 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.getGlobalIORefsMap().put(snap.globName, this); + GlobalVariable.globalIORefs.put(snap.globName, this); // Restore saved objects directly - they were never mutated, so no // dynamicRestoreState() call is needed. - GlobalVariable.getGlobalVariablesMap().put(snap.globName, snap.scalar); - GlobalVariable.getGlobalHashesMap().put(snap.globName, snap.hash); - GlobalVariable.getGlobalArraysMap().put(snap.globName, snap.array); + GlobalVariable.globalVariables.put(snap.globName, snap.scalar); + GlobalVariable.globalHashes.put(snap.globName, snap.hash); + GlobalVariable.globalArrays.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 @@ -977,14 +973,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.getGlobalCodeRefsMap().get(snap.globName); + RuntimeScalar localCode = GlobalVariable.globalCodeRefs.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.getGlobalCodeRefsMap().put(snap.globName, snap.code); + GlobalVariable.globalCodeRefs.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 b15d59a11..df1085a3a 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeHash.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeHash.java @@ -18,10 +18,8 @@ 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; - // Dynamic state stack is now held per-PerlRuntime. - private static Stack dynamicStateStack() { - return PerlRuntime.current().hashDynamicStateStack; - } + // Static stack to store saved "local" states of RuntimeHash instances + private static final Stack dynamicStateStack = new Stack<>(); private static final RuntimeArray EMPTY_KEYS = new RuntimeArray(); static { @@ -1032,7 +1030,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; @@ -1045,9 +1043,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 1df693096..14ac502d9 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeHashProxyEntry.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeHashProxyEntry.java @@ -8,10 +8,7 @@ * when they are accessed. */ public class RuntimeHashProxyEntry extends RuntimeBaseProxy { - // Dynamic state stack is now held per-PerlRuntime. - private static Stack dynamicStateStack() { - return PerlRuntime.current().hashProxyDynamicStateStack; - } + private static final Stack dynamicStateStack = new Stack<>(); // Reference to the parent RuntimeHash private final RuntimeHash parent; @@ -90,7 +87,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(); @@ -98,7 +95,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(); } @@ -112,9 +109,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 b6d6d0201..609fde661 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeIO.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeIO.java @@ -91,51 +91,71 @@ public class RuntimeIO extends RuntimeScalar { private static final Map> MODE_OPTIONS = new HashMap<>(); /** - * Returns the per-runtime LRU cache of open file handles. - * Migrated from a static field for multiplicity thread-safety. + * Maximum number of file handles to keep in the LRU cache. + * Older handles are flushed (not closed) when this limit is exceeded. */ - private static Map openHandles() { - return PerlRuntime.current().openHandles; - } - - private static final Map childProcesses = new java.util.concurrent.ConcurrentHashMap<>(); - - // ---- 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; } + private static final int MAX_OPEN_HANDLES = 100; - /** 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; } + /** + * 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; + } + }; - /** 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; } + 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; /** * Fileno registry for select() support. @@ -439,7 +459,7 @@ public static Process removeChildProcess(long pid) { * @param out the OutputStream to wrap */ public static void setCustomOutputStream(OutputStream out) { - setLastWrittenHandle(new RuntimeIO(new CustomOutputStreamHandle(out))); + lastWrittenHandle = new RuntimeIO(new CustomOutputStreamHandle(out)); } /** @@ -525,12 +545,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(getStdout()); - getGlobalIO("main::STDERR").setIO(getStderr()); - getGlobalIO("main::STDIN").setIO(getStdin()); - setLastAccessedHandle(null); - setLastWrittenHandle(getStdout()); - setSelectedHandle(getStdout()); + getGlobalIO("main::STDOUT").setIO(stdout); + getGlobalIO("main::STDERR").setIO(stderr); + getGlobalIO("main::STDIN").setIO(stdin); + lastAccesseddHandle = null; + lastWrittenHandle = stdout; + selectedHandle = stdout; } /** @@ -936,8 +956,8 @@ public static Path resolvePath(String fileName, String opName) { return path.toAbsolutePath(); } - // For relative paths, resolve against per-runtime current directory - return Paths.get(PerlRuntime.getCwd()).resolve(sanitized).toAbsolutePath(); + // For relative paths, resolve against current directory + return Paths.get(System.getProperty("user.dir")).resolve(sanitized).toAbsolutePath(); } /** @@ -947,13 +967,11 @@ 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 - RuntimeIO out = getStdout(); - RuntimeIO err = getStderr(); - if (out.needFlush) { - out.flush(); + if (stdout.needFlush) { + stdout.flush(); } - if (err.needFlush) { - err.flush(); + if (stderr.needFlush) { + stderr.flush(); } } @@ -963,9 +981,8 @@ public static void flushFileHandles() { * @param handle the IOHandle to cache */ public static void addHandle(IOHandle handle) { - Map handles = openHandles(); - synchronized (handles) { - handles.put(handle, Boolean.TRUE); + synchronized (openHandles) { + openHandles.put(handle, Boolean.TRUE); } } @@ -975,9 +992,8 @@ public static void addHandle(IOHandle handle) { * @param handle the IOHandle to remove */ public static void removeHandle(IOHandle handle) { - Map handles = openHandles(); - synchronized (handles) { - handles.remove(handle); + synchronized (openHandles) { + openHandles.remove(handle); } } @@ -986,9 +1002,8 @@ public static void removeHandle(IOHandle handle) { * This ensures all buffered data is written without closing files. */ public static void flushAllHandles() { - Map handles = openHandles(); - synchronized (handles) { - for (IOHandle handle : handles.keySet()) { + synchronized (openHandles) { + for (IOHandle handle : openHandles.keySet()) { handle.flush(); } } @@ -1001,9 +1016,8 @@ public static void flushAllHandles() { */ public static void closeAllHandles() { flushAllHandles(); - Map handles = openHandles(); - synchronized (handles) { - for (IOHandle handle : handles.keySet()) { + synchronized (openHandles) { + for (IOHandle handle : openHandles.keySet()) { try { handle.close(); handle = new ClosedIOHandle(); @@ -1011,7 +1025,7 @@ public static void closeAllHandles() { // Handle exception if needed } } - handles.clear(); // Clear the cache after closing all handles + openHandles.clear(); // Clear the cache after closing all handles } } @@ -1386,7 +1400,7 @@ public RuntimeScalar close() { * @return RuntimeScalar with true if at EOF */ public RuntimeScalar eof() { - setLastAccessedHandle(this); + lastAccesseddHandle = this; return ioHandle.eof(); } @@ -1397,7 +1411,7 @@ public RuntimeScalar eof() { * @return RuntimeScalar with the current position */ public RuntimeScalar tell() { - setLastAccessedHandle(this); + lastAccesseddHandle = this; return ioHandle.tell(); } @@ -1409,7 +1423,7 @@ public RuntimeScalar tell() { * @return RuntimeScalar indicating success/failure */ public RuntimeScalar seek(long pos) { - setLastAccessedHandle(this); + lastAccesseddHandle = this; return ioHandle.seek(pos); } @@ -1446,15 +1460,14 @@ 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) - RuntimeIO lastWritten = getLastWrittenHandle(); - if (lastWritten != null && - lastWritten != this && - lastWritten.needFlush && - lastWritten.ioHandle != this.ioHandle) { + if (lastWrittenHandle != null && + lastWrittenHandle != this && + lastWrittenHandle.needFlush && + lastWrittenHandle.ioHandle != this.ioHandle) { // Synchronize terminal output for stdout and stderr - lastWritten.flush(); + lastWrittenHandle.flush(); } - setLastWrittenHandle(this); + lastWrittenHandle = 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 8f86afef4..88c7ef55a 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java @@ -31,10 +31,8 @@ */ public class RuntimeScalar extends RuntimeBase implements RuntimeScalarReference, DynamicState { - // Dynamic state stack is now held per-PerlRuntime. - private static Stack dynamicStateStack() { - return PerlRuntime.current().dynamicStateStack; - } + // Static stack to store saved "local" states of RuntimeScalar instances + private static final Stack dynamicStateStack = new Stack<>(); // Pre-compiled regex pattern for decimal numification fast-path // INTEGER_PATTERN replaced with isIntegerString() for better performance @@ -2703,7 +2701,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; @@ -2718,10 +2716,9 @@ public void dynamicSaveState() { */ @Override public void dynamicRestoreState() { - Stack stack = dynamicStateStack(); - if (!stack.isEmpty()) { + if (!dynamicStateStack.isEmpty()) { // Pop the most recent saved state from the stack - RuntimeScalar previousState = stack.pop(); + RuntimeScalar previousState = dynamicStateStack.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 afa527f03..d7162184c 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeStash.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeStash.java @@ -8,10 +8,8 @@ * The RuntimeStash class simulates Perl stash hashes. */ public class RuntimeStash extends RuntimeHash { - // Dynamic state stack is now held per-PerlRuntime. - private static Stack dynamicStateStack() { - return PerlRuntime.current().stashDynamicStateStack; - } + // Static stack to store saved "local" states of RuntimeStash instances + private static final Stack dynamicStateStack = new Stack<>(); // Map to store the elements of the hash public Map elements; public String namespace; @@ -165,12 +163,12 @@ private RuntimeScalar deleteGlob(String k) { String fullKey = namespace + k; // Check if the glob exists - 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); + 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); if (!exists) { return new RuntimeScalar(); @@ -178,21 +176,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.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); + 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); // 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.getGlobalCodeRefsMap().remove(fullKey); - GlobalVariable.getGlobalVariablesMap().remove(fullKey); - GlobalVariable.getGlobalArraysMap().remove(fullKey); - GlobalVariable.getGlobalHashesMap().remove(fullKey); - GlobalVariable.getGlobalIORefsMap().remove(fullKey); - GlobalVariable.getGlobalFormatRefsMap().remove(fullKey); + GlobalVariable.globalCodeRefs.remove(fullKey); + GlobalVariable.globalVariables.remove(fullKey); + GlobalVariable.globalArrays.remove(fullKey); + GlobalVariable.globalHashes.remove(fullKey); + GlobalVariable.globalIORefs.remove(fullKey); + GlobalVariable.globalFormatRefs.remove(fullKey); // Removing symbols from a stash can affect method lookup. InheritanceResolver.invalidateCache(); @@ -221,12 +219,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.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)); + 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)); // Clear pinned code refs so deleted subs don't get resurrected // by getGlobalCodeRef() lookups (e.g., in SubroutineParser redefinition check) @@ -391,12 +389,12 @@ public RuntimeStash undefine() { GlobalVariable.clearStashAlias(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)); + 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)); this.elements.clear(); @@ -451,7 +449,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; @@ -464,9 +462,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 6ac898338..e65bcbeda 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.getGlobalArraysMap().put(this.globName, targetArray); + GlobalVariable.globalArrays.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.getGlobalArraysMap().put(this.globName, new RuntimeArray()); + GlobalVariable.globalArrays.put(this.globName, new RuntimeArray()); // Undefine HASH - create empty hash - GlobalVariable.getGlobalHashesMap().put(this.globName, new RuntimeHash()); + GlobalVariable.globalHashes.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 749cf5457..b6bd44db2 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarSpecialVariable.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarSpecialVariable.java @@ -23,11 +23,7 @@ */ public class ScalarSpecialVariable extends RuntimeBaseProxy { - // Input line state stack is now held per-PerlRuntime. - @SuppressWarnings("unchecked") - private static Stack inputLineStateStack() { - return (Stack) (Stack) PerlRuntime.current().inputLineStateStack; - } + private static final Stack inputLineStateStack = new Stack<>(); // The type of special variable, represented by an enum. final Id variableId; // The position of the capture group, used only for CAPTURE type variables. @@ -88,9 +84,9 @@ void vivify() { public RuntimeScalar set(RuntimeScalar value) { if (variableId == Id.INPUT_LINE_NUMBER) { vivify(); - if (RuntimeIO.getLastAccessedHandle() != null) { - RuntimeIO.getLastAccessedHandle().currentLineNumber = value.getInt(); - lvalue.set(RuntimeIO.getLastAccessedHandle().currentLineNumber); + if (RuntimeIO.lastAccesseddHandle != null) { + RuntimeIO.lastAccesseddHandle.currentLineNumber = value.getInt(); + lvalue.set(RuntimeIO.lastAccesseddHandle.currentLineNumber); } else { lvalue.set(value); } @@ -162,25 +158,25 @@ public RuntimeScalar getValueAsScalar() { yield postmatch != null ? makeRegexResultScalar(postmatch) : scalarUndef; } case P_PREMATCH -> { - if (!RuntimeRegex.getLastMatchUsedPFlag()) yield scalarUndef; + if (!RuntimeRegex.lastMatchUsedPFlag) yield scalarUndef; String prematch = RuntimeRegex.preMatchString(); yield prematch != null ? makeRegexResultScalar(prematch) : scalarUndef; } case P_MATCH -> { - if (!RuntimeRegex.getLastMatchUsedPFlag()) yield scalarUndef; + if (!RuntimeRegex.lastMatchUsedPFlag) yield scalarUndef; String match = RuntimeRegex.matchString(); yield match != null ? makeRegexResultScalar(match) : scalarUndef; } case P_POSTMATCH -> { - if (!RuntimeRegex.getLastMatchUsedPFlag()) yield scalarUndef; + if (!RuntimeRegex.lastMatchUsedPFlag) yield scalarUndef; String postmatch = RuntimeRegex.postMatchString(); yield postmatch != null ? makeRegexResultScalar(postmatch) : scalarUndef; } case LAST_FH -> { - if (RuntimeIO.getLastAccessedHandle() == null) { + if (RuntimeIO.lastAccesseddHandle == null) { yield scalarUndef; } - String globName = RuntimeIO.getLastAccessedHandle().globName; + String globName = RuntimeIO.lastAccesseddHandle.globName; if (globName != null) { // Extract package and name from the glob name String packageName; @@ -206,28 +202,28 @@ public RuntimeScalar getValueAsScalar() { } } // Fallback to the RuntimeIO object if no glob name is available - yield new RuntimeScalar(RuntimeIO.getLastAccessedHandle()); + yield new RuntimeScalar(RuntimeIO.lastAccesseddHandle); } case INPUT_LINE_NUMBER -> { - if (RuntimeIO.getLastAccessedHandle() == null) { + if (RuntimeIO.lastAccesseddHandle == null) { if (lvalue != null) { yield lvalue; } yield scalarUndef; } - yield getScalarInt(RuntimeIO.getLastAccessedHandle().currentLineNumber); + yield getScalarInt(RuntimeIO.lastAccesseddHandle.currentLineNumber); } case LAST_PAREN_MATCH -> { String lastCapture = RuntimeRegex.lastCaptureString(); yield lastCapture != null ? new RuntimeScalar(lastCapture) : scalarUndef; } - case LAST_SUCCESSFUL_PATTERN -> RuntimeRegex.getLastSuccessfulPattern() != null - ? new RuntimeScalar(RuntimeRegex.getLastSuccessfulPattern()) : scalarUndef; + case LAST_SUCCESSFUL_PATTERN -> RuntimeRegex.lastSuccessfulPattern != null + ? new RuntimeScalar(RuntimeRegex.lastSuccessfulPattern) : 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.getLastSuccessfulPattern() != null) { - RuntimeScalar codeBlockResult = RuntimeRegex.getLastSuccessfulPattern().getLastCodeBlockResult(); + if (RuntimeRegex.lastSuccessfulPattern != null) { + RuntimeScalar codeBlockResult = RuntimeRegex.lastSuccessfulPattern.getLastCodeBlockResult(); yield codeBlockResult != null ? codeBlockResult : scalarUndef; } yield scalarUndef; @@ -277,7 +273,7 @@ public RuntimeScalar getValueAsScalar() { // During BEGIN/UNITCHECK blocks = compilation phase yield scalarUndef; } - yield getScalarInt(RuntimeCode.getEvalDepth() > 0 ? 1 : 0); + yield getScalarInt(RuntimeCode.evalDepth > 0 ? 1 : 0); } }; return result; @@ -428,10 +424,10 @@ public RuntimeList getList() { @Override public void dynamicSaveState() { if (variableId == Id.INPUT_LINE_NUMBER) { - RuntimeIO handle = RuntimeIO.getLastAccessedHandle(); + RuntimeIO handle = RuntimeIO.lastAccesseddHandle; 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(); @@ -446,9 +442,9 @@ public void dynamicSaveState() { @Override public void dynamicRestoreState() { if (variableId == Id.INPUT_LINE_NUMBER) { - if (!inputLineStateStack().isEmpty()) { - InputLineState previous = inputLineStateStack().pop(); - RuntimeIO.setLastAccessedHandle(previous.lastHandle); + if (!inputLineStateStack.isEmpty()) { + InputLineState previous = inputLineStateStack.pop(); + RuntimeIO.lastAccesseddHandle = previous.lastHandle; if (previous.lastHandle != null) { previous.lastHandle.currentLineNumber = previous.lastLineNumber; } @@ -469,7 +465,7 @@ public void dynamicRestoreState() { */ private static RuntimeScalar makeRegexResultScalar(String value) { RuntimeScalar scalar = new RuntimeScalar(value); - if (RuntimeRegex.getLastMatchWasByteString()) { + if (RuntimeRegex.lastMatchWasByteString) { 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 36c8f40ec..ee3bf5df0 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/SpecialBlock.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/SpecialBlock.java @@ -13,11 +13,10 @@ */ public class SpecialBlock { - // 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; } + // 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(); /** * Saves a code reference to the endBlocks array. @@ -26,7 +25,7 @@ public class SpecialBlock { * @param codeRef the code reference to be saved */ public static void saveEndBlock(RuntimeScalar codeRef) { - RuntimeArray.push(getEndBlocks(), codeRef); + RuntimeArray.push(endBlocks, codeRef); } /** @@ -36,7 +35,7 @@ public static void saveEndBlock(RuntimeScalar codeRef) { * @param codeRef the code reference to be saved */ public static void saveInitBlock(RuntimeScalar codeRef) { - RuntimeArray.unshift(getInitBlocks(), codeRef); + RuntimeArray.unshift(initBlocks, codeRef); } /** @@ -46,7 +45,7 @@ public static void saveInitBlock(RuntimeScalar codeRef) { * @param codeRef the code reference to be saved */ public static void saveCheckBlock(RuntimeScalar codeRef) { - RuntimeArray.push(getCheckBlocks(), codeRef); + RuntimeArray.push(checkBlocks, codeRef); } /** @@ -57,11 +56,13 @@ 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); } - RuntimeArray blocks = getEndBlocks(); - while (!blocks.isEmpty()) { - RuntimeScalar block = RuntimeArray.pop(blocks); + + while (!endBlocks.isEmpty()) { + RuntimeScalar block = RuntimeArray.pop(endBlocks); if (block.getDefinedBoolean()) { RuntimeCode.apply(block, new RuntimeArray(), RuntimeContextType.VOID); } @@ -80,9 +81,8 @@ public static void runEndBlocks() { * Executes all code blocks stored in the initBlocks array in FIFO order. */ public static void runInitBlocks() { - RuntimeArray blocks = getInitBlocks(); - while (!blocks.isEmpty()) { - RuntimeScalar block = RuntimeArray.pop(blocks); + while (!initBlocks.isEmpty()) { + RuntimeScalar block = RuntimeArray.pop(initBlocks); if (block.getDefinedBoolean()) { RuntimeCode.apply(block, new RuntimeArray(), RuntimeContextType.VOID); } @@ -93,9 +93,8 @@ public static void runInitBlocks() { * Executes all code blocks stored in the checkBlocks array in LIFO order. */ public static void runCheckBlocks() { - RuntimeArray blocks = getCheckBlocks(); - while (!blocks.isEmpty()) { - RuntimeScalar block = RuntimeArray.pop(blocks); + while (!checkBlocks.isEmpty()) { + RuntimeScalar block = RuntimeArray.pop(checkBlocks); 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 6b202d990..5757e31c9 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.getStdin()) { + if (fh != RuntimeIO.stdin) { 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.getStdin() ? System.in : new ByteArrayInputStream(new byte[0]))); + fh == RuntimeIO.stdin ? 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 4c959f6f3..feb8addc5 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.getStdin()) { + if (fh != RuntimeIO.stdin) { 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 02637c2e7..3b6a012ad 100644 --- a/src/test/java/org/perlonjava/ModuleTestExecutionTest.java +++ b/src/test/java/org/perlonjava/ModuleTestExecutionTest.java @@ -7,7 +7,6 @@ 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; @@ -98,28 +97,23 @@ 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.setStdout(new RuntimeIO(newStdout)); - GlobalVariable.getGlobalIO("main::STDOUT").setIO(RuntimeIO.getStdout()); - GlobalVariable.getGlobalIO("main::STDERR").setIO(RuntimeIO.getStderr()); + RuntimeIO.stdout = new RuntimeIO(newStdout); + GlobalVariable.getGlobalIO("main::STDOUT").setIO(RuntimeIO.stdout); + GlobalVariable.getGlobalIO("main::STDERR").setIO(RuntimeIO.stderr); System.setOut(new PrintStream(outputStream)); } @AfterEach void tearDown() { // Restore original stdout - RuntimeIO.setStdout(new RuntimeIO(new StandardIO(originalOut, true))); - GlobalVariable.getGlobalIO("main::STDOUT").setIO(RuntimeIO.getStdout()); - GlobalVariable.getGlobalIO("main::STDERR").setIO(RuntimeIO.getStderr()); + RuntimeIO.stdout = new RuntimeIO(new StandardIO(originalOut, true)); + GlobalVariable.getGlobalIO("main::STDOUT").setIO(RuntimeIO.stdout); + GlobalVariable.getGlobalIO("main::STDERR").setIO(RuntimeIO.stderr); 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 9f596b3ad..7e3d6fd82 100644 --- a/src/test/java/org/perlonjava/PerlScriptExecutionTest.java +++ b/src/test/java/org/perlonjava/PerlScriptExecutionTest.java @@ -8,7 +8,6 @@ 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; @@ -159,11 +158,6 @@ 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(); @@ -171,11 +165,11 @@ void setUp() { StandardIO newStdout = new StandardIO(outputStream, true); // Replace RuntimeIO.stdout with a new instance - RuntimeIO.setStdout(new RuntimeIO(newStdout)); + RuntimeIO.stdout = 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.getStdout()); - GlobalVariable.getGlobalIO("main::STDERR").setIO(RuntimeIO.getStderr()); + GlobalVariable.getGlobalIO("main::STDOUT").setIO(RuntimeIO.stdout); + GlobalVariable.getGlobalIO("main::STDERR").setIO(RuntimeIO.stderr); // Also update System.out for any direct Java calls System.setOut(new PrintStream(outputStream)); @@ -187,9 +181,9 @@ void setUp() { @AfterEach void tearDown() { // Restore original stdout - RuntimeIO.setStdout(new RuntimeIO(new StandardIO(originalOut, true))); - GlobalVariable.getGlobalIO("main::STDOUT").setIO(RuntimeIO.getStdout()); - GlobalVariable.getGlobalIO("main::STDERR").setIO(RuntimeIO.getStderr()); + RuntimeIO.stdout = new RuntimeIO(new StandardIO(originalOut, true)); + GlobalVariable.getGlobalIO("main::STDOUT").setIO(RuntimeIO.stdout); + GlobalVariable.getGlobalIO("main::STDERR").setIO(RuntimeIO.stderr); System.setOut(originalOut); }