diff --git a/.cognition/skills/fix-pat-sprintf/SKILL.md b/.cognition/skills/fix-pat-sprintf/SKILL.md new file mode 100644 index 000000000..afaf76269 --- /dev/null +++ b/.cognition/skills/fix-pat-sprintf/SKILL.md @@ -0,0 +1,178 @@ +--- +name: fix-pat-sprintf +description: Fix re/pat.t and op/sprintf2.t test regressions on fix-exiftool-cli branch +argument-hint: "[test-name or specific failure]" +triggers: + - user + - model +--- + +# Fix pat.t and sprintf2.t Regressions + +You are fixing test regressions in `re/pat.t` (-17 tests) and `op/sprintf2.t` (-3 tests) on the `fix-exiftool-cli` branch of PerlOnJava. + +## Hard Constraints + +1. **No AST refactoring fallback.** The `LargeBlockRefactorer` / AST splitter must NOT be restored. This is non-negotiable. +2. **Fix the interpreter.** The bytecode interpreter must achieve feature parity with the JVM compiler. Both backends must produce identical results for all Perl constructs. +3. **Match the baseline exactly.** Target is the master baseline scores — no more, no less: + - `re/pat.t`: 1056/1296 + - `op/sprintf2.t`: 1652/1655 +4. **Do NOT modify shared runtime** (`RuntimeRegex.java`, `RegexFlags.java`, `RegexPreprocessor.java`, etc.). The runtime is shared between both backends. Fixes must be in the interpreter code. + +## Why the Interpreter Is Involved + +Large subroutines that exceed the JVM 64KB method limit fall back to the bytecode interpreter via `EmitterMethodCreator.createRuntimeCode()`. + +- **pat.t**: The `run_tests` subroutine (lines 38-2652, ~2614 lines) falls back to interpreter. All 1296 tests run through it. Confirmed with `JPERL_SHOW_FALLBACK=1`. +- **sprintf2.t**: Same mechanism — large test body falls back to interpreter. + +## Baseline vs Branch + +| Test | Master baseline (397ba45d) | Branch HEAD | Delta | +|------|---------------------------|-------------|-------| +| re/pat.t | 1056/1296 | 1039/1296 | -17 | +| op/sprintf2.t | 1652/1655 | 1649/1655 | -3 | + +## Methodology + +For each failing test: + +1. **Extract** the specific Perl code from the test file +2. **Compare** JVM vs interpreter output: + ```bash + ./jperl -E 'extracted code' # JVM backend (correct behavior) + ./jperl --interpreter -E 'extracted code' # Interpreter (may differ) + ``` +3. **When they differ**: identify the root cause in the interpreter code (BytecodeCompiler, BytecodeInterpreter, etc.) and fix it +4. **When they don't differ standalone**: the failure depends on context from earlier tests in the same large function. Investigate what prior state affects the result — look at regex state, variable scoping, match variables, pos(), etc. +5. **Verify** the fix doesn't break other tests + +## Running the Tests + +```bash +# Build +make build + +# Run individual tests via test runner (sets correct ENV vars) +perl dev/tools/perl_test_runner.pl perl5_t/t/re/pat.t +perl dev/tools/perl_test_runner.pl perl5_t/t/op/sprintf2.t + +# Run manually with correct ENV +cd perl5_t/t +PERL_SKIP_BIG_MEM_TESTS=1 JPERL_UNIMPLEMENTED=warn JPERL_OPTS="-Xss256m" ../../jperl re/pat.t +PERL_SKIP_BIG_MEM_TESTS=1 JPERL_UNIMPLEMENTED=warn ../../jperl op/sprintf2.t + +# Compare JVM vs interpreter for a specific construct +./jperl -E 'code' +./jperl --interpreter -E 'code' + +# Check if a test file uses interpreter fallback +cd perl5_t/t && JPERL_SHOW_FALLBACK=1 ../../jperl re/pat.t 2>&1 | grep 'interpreter backend' + +# Get interpreter bytecodes for a construct +./jperl --interpreter --disassemble -E 'code' 2>&1 +``` + +## pat.t: Exact Regressions (18 PASS->FAIL, 1 FAIL->PASS, net -17) + +### Tests that went from PASS to FAIL + +| # | Test Description | pat.t Line | Category | +|---|-----------------|------------|----------| +| 1 | Stack may be bad | 508 | regex match | +| 2 | $^N, @- and @+ are read-only | 845-851 | eval STRING special vars | +| 3-4 | \G testing (x2) | 858, 866 | \G anchor | +| 5 | \b is not special | 1089 | word boundary | +| 6-8 | \s, [[:space:]] and [[:blank:]] (x3) | 1223-1225 | POSIX classes | +| 9 | got a latin string - rt75680 | 1252 | latin/unicode | +| 10-11 | RT #3516 A, B | 1329, 1335 | \G loop | +| 12 | Qr3 bare | ~1490 | qr// overload | +| 13 | Qr3 bare - with use re eval | ~1498 | qr// eval | +| 14 | Eval-group not allowed at runtime | 524 | regex eval | +| 15-18 | Branch reset pattern 1-4 | 2392-2409 | branch reset | + +### Test that went from FAIL to PASS + +| Test Description | Category | +|-----------------|----------| +| 1 '', '1', '12' (Eval-group) | regex eval | + +## Interpreter Architecture + +``` +Source -> Lexer -> Parser -> AST --+--> JVM Compiler (EmitterMethodCreator) -> JVM bytecode + \--> BytecodeCompiler -> InterpretedCode -> BytecodeInterpreter +``` + +Both backends share the same runtime (RuntimeRegex, RuntimeScalar, etc.). The difference is ONLY in how the AST is lowered to executable form. The interpreter must handle every construct identically to the JVM compiler. + +### Key interpreter files + +| File | Role | +|------|------| +| `backend/bytecode/BytecodeCompiler.java` | AST -> interpreter bytecodes | +| `backend/bytecode/BytecodeInterpreter.java` | Main dispatch loop | +| `backend/bytecode/InterpretedCode.java` | Code object + disassembler | +| `backend/bytecode/Opcodes.java` | Opcode constants | +| `backend/bytecode/CompileAssignment.java` | Assignment compilation | +| `backend/bytecode/CompileBinaryOperator.java` | Binary ops compilation | +| `backend/bytecode/CompileOperator.java` | Unary/misc ops compilation | +| `backend/bytecode/SlowOpcodeHandler.java` | Rarely-used op handlers | +| `backend/bytecode/OpcodeHandlerExtended.java` | CREATE_CLOSURE, STORE_GLOB, etc. | +| `backend/bytecode/MiscOpcodeHandler.java` | Misc operations | +| `backend/bytecode/EvalStringHandler.java` | eval STRING compilation for interpreter | + +All paths relative to `src/main/java/org/perlonjava/`. + +### Key source files (do NOT modify) + +| Area | File | Notes | +|------|------|-------| +| Regex runtime | `runtime/regex/RuntimeRegex.java` | DO NOT MODIFY | +| Regex flags | `runtime/regex/RegexFlags.java` | DO NOT MODIFY | +| Regex preprocessor | `runtime/regex/RegexPreprocessor.java` | DO NOT MODIFY | + +All paths relative to `src/main/java/org/perlonjava/`. + +## Verification Steps + +After any fix: + +```bash +# 1. Build must pass +make build + +# 2. Unit tests must pass +make test-unit + +# 3. Check pat.t — must match baseline (1056/1296) +perl dev/tools/perl_test_runner.pl perl5_t/t/re/pat.t + +# 4. Check sprintf2.t — must match baseline (1652/1655) +perl dev/tools/perl_test_runner.pl perl5_t/t/op/sprintf2.t + +# 5. No regressions in other key tests +perl dev/tools/perl_test_runner.pl perl5_t/t/op/pack.t +perl dev/tools/perl_test_runner.pl perl5_t/t/re/pat_rt_report.t +``` + +## Debugging Tips + +### Compare raw output between baseline and branch +```bash +# Save branch output +cd perl5_t/t && PERL_SKIP_BIG_MEM_TESTS=1 JPERL_UNIMPLEMENTED=warn JPERL_OPTS="-Xss256m" ../../jperl re/pat.t > /tmp/pat_branch.txt 2>&1 + +# Compare by test name against saved baseline +LC_ALL=C diff \ + <(LC_ALL=C grep -E '^(ok|not ok)' /tmp/pat_base_raw.txt | LC_ALL=C sed 's/^ok [0-9]* - /PASS: /;s/^not ok [0-9]* - /FAIL: /' | LC_ALL=C sort) \ + <(LC_ALL=C grep -E '^(ok|not ok)' /tmp/pat_branch.txt | LC_ALL=C sed 's/^ok [0-9]* - /PASS: /;s/^not ok [0-9]* - /FAIL: /' | LC_ALL=C sort) \ + | grep '^[<>]' +``` + +### Test specific construct through both backends +```bash +./jperl -E 'my $s="abcde"; pos $s=2; say $s =~ /^\G/ ? "match" : "no"' +./jperl --interpreter -E 'my $s="abcde"; pos $s=2; say $s =~ /^\G/ ? "match" : "no"' +``` diff --git a/.cognition/skills/interpreter-parity/SKILL.md b/.cognition/skills/interpreter-parity/SKILL.md index 519dcc5e8..b989b4a80 100644 --- a/.cognition/skills/interpreter-parity/SKILL.md +++ b/.cognition/skills/interpreter-parity/SKILL.md @@ -200,6 +200,48 @@ Test::More → Test::Builder → Test::Builder::Formatter → Test2::Formatter:: ``` The failure is a ClassCastException in `Test/Builder/Formatter.pm` BEGIN block where `*OUT_STD = Test2::Formatter::TAP->can('OUT_STD')` — method call result (RuntimeList) is stored to glob (expects RuntimeScalar). +## Design Decision: JVM Emitter Must Not Mutate the AST + +When the JVM backend fails with `MethodTooLargeException` (or `VerifyError`, etc.), `createRuntimeCode()` in `EmitterMethodCreator.java` falls back to the interpreter via `compileToInterpreter(ast, ...)`. The same fallback exists in `PerlLanguageProvider.compileToExecutable()`. + +**Problem**: The JVM emitter (EmitterVisitor and helpers) mutates the AST during code generation. If JVM compilation fails partway through, the interpreter receives a corrupted AST, producing wrong results. This is the root cause of mixed-mode failures (e.g., pack.t gets 45 extra failures when the main script falls back to interpreter after partial JVM emission). + +**Rule**: The JVM emitter must NEVER permanently mutate AST nodes. All mutations must either: +1. Be avoided entirely (work on local copies), OR +2. Use save/restore in try/finally (already done in `EmitLogicalOperator.java`) + +### Known AST mutation sites + +| File | Line(s) | What it mutates | Status | +|------|---------|-----------------|--------| +| `EmitOperator.java` | ~373 | `operand.elements.addFirst(operand.handle)` in `handleSystemBuiltin` — adds handle to elements list, never removed | **DANGEROUS** | +| `Dereference.java` | ~347,442,511,579,911 | `nodeRight.elements.set(0, new StringNode(...))` — converts IdentifierNode to StringNode for hash autoquoting. `nodeRight` comes from `asListNode()` which creates a new ListNode but shares the same `elements` list | **DANGEROUS** — mutates shared elements list | +| `EmitLogicalOperator.java` | ~188,300,340 | Temporarily rewrites `declaration.operator`/`.operand` | **SAFE** — uses save/restore in try/finally | +| `EmitControlFlow.java` | ~280 | `argsNode.elements.add(atUnderscore)` | **SAFE** — `argsNode` is a freshly created ListNode | +| `EmitOperator.java` | ~398,410 | `handleSpliceBuiltin` removes/restores first element | **SAFE** — uses try/finally restore | +| Annotations (`setAnnotation`) | various | Sets `blockIsSubroutine`, `skipRegexSaveRestore`, `isDeclaredReference` | **Likely safe** — annotations are additive hints, but verify interpreter handles them | + +### How to fix dangerous sites + +**`handleSystemBuiltin` (EmitOperator.java:373)**: Wrap in try/finally to remove the added element after accept(): +```java +if (operand.handle != null) { + hasHandle = true; + operand.elements.addFirst(operand.handle); +} +try { + operand.accept(emitterVisitor.with(RuntimeContextType.LIST)); +} finally { + if (hasHandle) { + operand.elements.removeFirst(); + } +} +``` + +**Dereference.java autoquoting**: `asListNode()` creates a new ListNode but passes the SAME `elements` list reference. The `elements.set(0, ...)` call mutates the original HashLiteralNode's elements. Fix by either: +- Making `asListNode()` copy the elements list: `new ListNode(new ArrayList<>(elements), tokenIndex)` +- Or saving/restoring the original element in try/finally + ## Lessons Learned ### InterpretedCode constructor drops metadata diff --git a/.cognition/skills/migrate-jna/SKILL.md b/.cognition/skills/migrate-jna/SKILL.md new file mode 100644 index 000000000..8d63e09aa --- /dev/null +++ b/.cognition/skills/migrate-jna/SKILL.md @@ -0,0 +1,122 @@ +--- +name: migrate-jna +description: Migrate from JNA to a modern native access library (eliminate sun.misc.Unsafe warnings) +argument-hint: "[library choice or file to migrate]" +triggers: + - user +--- + +# Migrate JNA to Modern Native Access Library + +## Problem + +JNA 5.18.1 uses `sun.misc.Unsafe::staticFieldBase` internally, which produces deprecation warnings on Java 21+ and will break in future JDK releases. The project needs to migrate to a library that uses supported APIs. + +## Candidate Replacement Libraries + +The choice of replacement library is TBD. Evaluate these options: + +### Option A: jnr-posix +- **Maven**: `com.github.jnr:jnr-posix` +- **Pros**: Purpose-built for POSIX ops, used by JRuby (production-proven), clean high-level API (`FileStat`, `kill()`, `waitpid()`, `umask()`, `utime()`), built on jnr-ffi (no `sun.misc.Unsafe`) +- **Cons**: Third-party dependency, may not cover Windows-specific calls + +### Option B: Java Foreign Function & Memory API (FFM) +- **Module**: `java.lang.foreign` (JDK built-in) +- **Pros**: No third-party dependency, official JDK solution, no deprecated APIs +- **Cons**: Stable only since Java 22 (preview in 21), verbose low-level API, requires manual struct layout definitions +- **Note**: If the project bumps minimum to Java 22, this becomes viable without preview flags + +### Option C: jnr-ffi (without jnr-posix) +- **Maven**: `com.github.jnr:jnr-ffi` +- **Pros**: Modern JNA alternative, no `sun.misc.Unsafe`, flexible +- **Cons**: Lower-level than jnr-posix, requires manual bindings (similar effort to FFM) + +## Current JNA Usage + +10 files use JNA. All paths relative to `src/main/java/org/perlonjava/`. + +### Native interface definitions + +| File | JNA Usage | +|------|-----------| +| `runtime/nativ/PosixLibrary.java` | POSIX C library bindings: `stat`, `lstat`, `chmod`, `chown`, `getpid`, `getppid`, `setpgid`, `getpgid`, `setsid`, `tcsetpgrp`, `tcgetpgrp`, `getpgrp`, `setpgrp` | +| `runtime/nativ/WindowsLibrary.java` | Windows kernel32 bindings: `GetCurrentProcessId`, `_getpid` | +| `runtime/nativ/NativeUtils.java` | JNA Platform utilities: `getpid()`, `getuid()`, `geteuid()`, `getgid()`, `getegid()`, plus `CLibrary` for `getpriority`/`setpriority`/`alarm`/`getlogin` | +| `runtime/nativ/ExtendedNativeUtils.java` | Additional POSIX: `getpwuid`, `getpwnam`, `getgrnam`, `getgrgid` (passwd/group lookups) | + +### Consumers (files that call native operations) + +| File | Operations Used | +|------|----------------| +| `runtime/operators/Stat.java` | `PosixLibrary.stat()`, `PosixLibrary.lstat()` — all 13 stat fields (dev, ino, mode, nlink, uid, gid, rdev, size, atime, mtime, ctime, blksize, blocks) | +| `runtime/operators/Operator.java` | `PosixLibrary.chmod()`, `PosixLibrary.chown()`, `NativeUtils` for pid/uid/gid | +| `runtime/operators/KillOperator.java` | `PosixLibrary.kill()` for sending signals, `NativeUtils.getpid()` | +| `runtime/operators/WaitpidOperator.java` | JNA `CLibrary.waitpid()` with `WNOHANG`/`WUNTRACED` flags, macros `WIFEXITED`/`WEXITSTATUS`/`WIFSIGNALED`/`WTERMSIG`/`WIFSTOPPED`/`WSTOPSIG` | +| `runtime/operators/UmaskOperator.java` | JNA `CLibrary.umask()` | +| `runtime/operators/UtimeOperator.java` | JNA `CLibrary.utimes()` with `timeval` struct | + +## Migration Strategy + +### Phase 1: Replace native interface definitions +1. Create new interface files using the chosen library +2. Keep the same method signatures where possible +3. Ensure struct mappings (stat, timeval, passwd, group) are complete + +### Phase 2: Update consumers one by one +Migrate in this order (least to most complex): +1. `UmaskOperator.java` — single `umask()` call +2. `KillOperator.java` — `kill()` + `getpid()` +3. `UtimeOperator.java` — `utimes()` with struct +4. `Operator.java` — `chmod()`, `chown()`, pid/uid/gid +5. `WaitpidOperator.java` — `waitpid()` with flag macros +6. `Stat.java` — `stat()`/`lstat()` with 13-field struct +7. `NativeUtils.java` / `ExtendedNativeUtils.java` — passwd/group lookups + +### Phase 3: Remove JNA dependency +1. Remove JNA imports from all files +2. Remove JNA from `build.gradle` and `pom.xml` +3. Remove `--enable-native-access=ALL-UNNAMED` from `jperl` launcher (if no longer needed) +4. Verify the `sun.misc.Unsafe` warning is gone + +## Testing + +After each file migration: +```bash +make # Must pass +make test-all # Check for regressions +``` + +Key tests that exercise native operations: +- `perl5_t/t/op/stat.t` — stat/lstat fields +- `perl5_t/t/io/fs.t` — chmod, chown, utime +- `perl5_t/t/op/fork.t` — kill, waitpid +- `src/test/resources/unit/glob.t` — readdir (uses stat internally) + +## Build Configuration + +### Current JNA in gradle +``` +# gradle/libs.versions.toml +jna = "5.18.1" +jna = { module = "net.java.dev.jna:jna", version.ref = "jna" } +jna-platform = { module = "net.java.dev.jna:jna-platform", version.ref = "jna" } +``` + +### Current JNA in pom.xml +```xml + + net.java.dev.jna + jna + + + net.java.dev.jna + jna-platform + +``` + +## Platform Considerations + +- **macOS/Linux**: Full POSIX support required (stat, lstat, kill, waitpid, chmod, chown, umask, utime, passwd/group lookups) +- **Windows**: Limited support via `kernel32` (`GetCurrentProcessId`), `msvcrt` (`_getpid`, stat) +- The replacement must handle both platforms, or gracefully degrade on Windows (as JNA currently does) diff --git a/jperl b/jperl index c3d4f4880..56d5ca96f 100755 --- a/jperl +++ b/jperl @@ -24,5 +24,5 @@ else fi # Launch Java -java ${JPERL_OPTS} -cp "$CLASSPATH:$JAR_PATH" org.perlonjava.app.cli.Main "$@" +java --enable-native-access=ALL-UNNAMED ${JPERL_OPTS} -cp "$CLASSPATH:$JAR_PATH" org.perlonjava.app.cli.Main "$@" diff --git a/src/main/java/org/perlonjava/app/scriptengine/PerlLanguageProvider.java b/src/main/java/org/perlonjava/app/scriptengine/PerlLanguageProvider.java index 3e74ff8f5..c6f30266a 100644 --- a/src/main/java/org/perlonjava/app/scriptengine/PerlLanguageProvider.java +++ b/src/main/java/org/perlonjava/app/scriptengine/PerlLanguageProvider.java @@ -238,7 +238,7 @@ public static RuntimeList executePerlAST(Node ast, // Compile to executable (compiler or interpreter based on flag) RuntimeCode runtimeCode = compileToExecutable(ast, ctx); - // executePerlAST is always called from BEGIN blocks which use VOID context + // executePerlAST is always called from special blocks which use VOID context return executeCode(runtimeCode, ctx, false, RuntimeContextType.VOID); } @@ -322,7 +322,7 @@ private static RuntimeCode compileToExecutable(Node ast, EmitterContext ctx) thr 1, // sourceLine (legacy parameter) ctx.errorUtil // Pass errorUtil for proper error formatting with line numbers ); - InterpretedCode interpretedCode = compiler.compile(ast); + InterpretedCode interpretedCode = compiler.compile(ast, ctx); // If --disassemble is enabled, print the bytecode if (ctx.compilerOptions.disassembleEnabled) { @@ -362,26 +362,33 @@ private static RuntimeCode compileToExecutable(Node ast, EmitterContext ctx) thr ); return compiled; - } catch (RuntimeException e) { + } catch (Throwable e) { // Check if this is a recoverable compilation error that can use interpreter fallback + // Catch Throwable (not just RuntimeException) because ClassFormatError + // ("Too many arguments in method signature") extends Error, not Exception if (needsInterpreterFallback(e)) { - // Interpreter fallback is enabled by default and can be disabled with JPERL_DISABLE_INTERPRETER_FALLBACK - // automatically fall back to the interpreter backend boolean showFallback = System.getenv("JPERL_SHOW_FALLBACK") != null; if (showFallback) { System.err.println("Note: Method too large after AST splitting, using interpreter backend."); } - // Fall back to interpreter path ctx.logDebug("Falling back to bytecode interpreter due to method size"); + // Reset strict/feature/warning flags before fallback compilation. + // The JVM compiler already processed BEGIN blocks (use strict, etc.) + // which set these flags on ctx.symbolTable. But the interpreter will + // re-process those pragmas during execution, so inheriting them causes + // false strict violations (e.g. bareword filehandles rejected). + if (ctx.symbolTable != null) { + ctx.symbolTable.strictOptionsStack.pop(); + ctx.symbolTable.strictOptionsStack.push(0); + } BytecodeCompiler compiler = new BytecodeCompiler( ctx.compilerOptions.fileName, - 1, // sourceLine (legacy parameter) - ctx.errorUtil // Pass errorUtil for proper error formatting with line numbers + 1, + ctx.errorUtil ); - InterpretedCode interpretedCode = compiler.compile(ast); + InterpretedCode interpretedCode = compiler.compile(ast, ctx); - // If --disassemble is enabled, print the bytecode if (ctx.compilerOptions.disassembleEnabled) { System.out.println("=== Interpreter Bytecode ==="); System.out.println(interpretedCode.disassemble()); @@ -389,9 +396,10 @@ private static RuntimeCode compileToExecutable(Node ast, EmitterContext ctx) thr } return interpretedCode; + } else if (e instanceof RuntimeException) { + throw (RuntimeException) e; } else { - // Not a size error, rethrow - throw e; + throw new RuntimeException(e); } } } @@ -402,6 +410,7 @@ private static boolean needsInterpreterFallback(Throwable e) { String msg = t.getMessage(); if (msg != null && ( msg.contains("Method too large") || + msg.contains("Too many arguments in method signature") || msg.contains("ASM frame computation failed") || msg.contains("Unexpected runtime error during bytecode generation") || msg.contains("dstFrame") || diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index b0009c72e..c99706e8d 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -1,6 +1,7 @@ package org.perlonjava.backend.bytecode; +import org.perlonjava.frontend.analysis.FindDeclarationVisitor; import org.perlonjava.frontend.analysis.RegexUsageDetector; import org.perlonjava.frontend.analysis.Visitor; import org.perlonjava.backend.jvm.EmitterMethodCreator; @@ -98,6 +99,9 @@ private static class LoopInfo { // Track current calling context for subroutine calls int currentCallContext = RuntimeContextType.LIST; // Default to LIST + // True when this compiler was constructed for eval STRING (has parentRegistry) + private boolean isEvalString; + // Closure support private RuntimeBase[] capturedVars; // Captured variable values private String[] capturedVarNames; // Parallel array of names @@ -106,6 +110,7 @@ private static class LoopInfo { // Per-eval-site variable registries: each eval STRING emission snapshots the // currently visible variables so at runtime the correct registers are captured. final List> evalSiteRegistries = new ArrayList<>(); + final List evalSitePragmaFlags = new ArrayList<>(); // BEGIN support for named subroutine closures int currentSubroutineBeginId = 0; // BEGIN ID for current named subroutine (0 = not in named sub) @@ -157,6 +162,8 @@ public BytecodeCompiler(String sourceName, int sourceLine, ErrorMessageUtil erro symbolTable.addVariableWithIndex("@_", 1, "reserved"); symbolTable.addVariableWithIndex("wantarray", 2, "reserved"); + this.isEvalString = true; + if (parentRegistry != null) { // Add parent scope variables to symbolTable (for eval STRING variable capture) for (Map.Entry entry : parentRegistry.entrySet()) { @@ -483,6 +490,12 @@ public InterpretedCode compile(Node node, EmitterContext ctx) { if (ctx.symbolTable != null) { symbolTable.setCurrentPackage(ctx.symbolTable.getCurrentPackage(), ctx.symbolTable.currentPackageIsClass()); + symbolTable.strictOptionsStack.pop(); + symbolTable.strictOptionsStack.push(ctx.symbolTable.strictOptionsStack.peek()); + symbolTable.featureFlagsStack.pop(); + symbolTable.featureFlagsStack.push(ctx.symbolTable.featureFlagsStack.peek()); + symbolTable.warningFlagsStack.pop(); + symbolTable.warningFlagsStack.push((java.util.BitSet) ctx.symbolTable.warningFlagsStack.peek().clone()); } } @@ -496,6 +509,13 @@ public InterpretedCode compile(Node node, EmitterContext ctx) { nextRegister = 3 + capturedVars.length; } + // For non-eval-STRING compilations (special blocks, top-level scripts), + // override VOID→LIST so the block tracks its result. eval STRING must + // preserve the caller's context so wantarray() works correctly inside. + if (!isEvalString && currentCallContext == RuntimeContextType.VOID) { + currentCallContext = RuntimeContextType.LIST; + } + // Visit the node to generate bytecode node.accept(this); @@ -546,7 +566,8 @@ public InterpretedCode compile(Node node, EmitterContext ctx) { featureFlags, warningFlags, symbolTable.getCurrentPackage(), - evalSiteRegistries.isEmpty() ? null : evalSiteRegistries + evalSiteRegistries.isEmpty() ? null : evalSiteRegistries, + evalSitePragmaFlags.isEmpty() ? null : evalSitePragmaFlags ); } @@ -726,16 +747,23 @@ public void visit(BlockNode node) { && node.elements.get(0) instanceof OperatorNode localOp && localOp.operator.equals("local"); - // If the first statement is a scoped package (package Foo { }), - // save the DynamicVariableManager level before the block body so PUSH_PACKAGE is restored. - int scopedPackageLevelReg = -1; - if (!node.elements.isEmpty() - && node.elements.get(0) instanceof OperatorNode firstOp - && (firstOp.operator.equals("package") || firstOp.operator.equals("class")) - && Boolean.TRUE.equals(firstOp.getAnnotation("isScoped"))) { - scopedPackageLevelReg = allocateRegister(); - emit(Opcodes.GET_LOCAL_LEVEL); - emitReg(scopedPackageLevelReg); + // Save DynamicVariableManager level before the block body when the block contains + // `local` operators or a scoped package declaration, so locals are restored on block exit. + // This matches the JVM compiler's Local.localSetup/localTeardown pattern. + int localLevelReg = -1; + boolean needsLocalRestore = false; + if (!node.getBooleanAnnotation("blockIsSubroutine")) { + boolean hasScopedPackage = !node.elements.isEmpty() + && node.elements.get(0) instanceof OperatorNode firstOp + && (firstOp.operator.equals("package") || firstOp.operator.equals("class")) + && Boolean.TRUE.equals(firstOp.getAnnotation("isScoped")); + boolean hasLocal = FindDeclarationVisitor.findOperator(node, "local") != null; + if (hasScopedPackage || hasLocal) { + needsLocalRestore = true; + localLevelReg = allocateRegister(); + emit(Opcodes.GET_LOCAL_LEVEL); + emitReg(localLevelReg); + } } enterScope(); @@ -809,11 +837,9 @@ public void visit(BlockNode node) { // Exit scope restores register state exitScope(); - // Restore DynamicVariableManager level after scoped package block - // (undoes PUSH_PACKAGE emitted by the package operator inside the block) - if (scopedPackageLevelReg >= 0) { + if (needsLocalRestore) { emit(Opcodes.POP_LOCAL_LEVEL); - emitReg(scopedPackageLevelReg); + emitReg(localLevelReg); } // Set lastResultReg to the outer register (or -1 if VOID context) @@ -948,12 +974,20 @@ void handleArrayKeyValueSlice(BinaryOperatorNode node, OperatorNode leftOp) { @Override public void visit(StringNode node) { - // Emit LOAD_STRING or LOAD_VSTRING depending on whether this is a v-string literal. - // LOAD_VSTRING sets type=VSTRING so that ModuleOperators.require() recognises version strings. int rd = allocateRegister(); int strIndex = addToStringPool(node.value); - emit(node.isVString ? Opcodes.LOAD_VSTRING : Opcodes.LOAD_STRING); + short opcode; + if (node.isVString) { + opcode = Opcodes.LOAD_VSTRING; + } else if (emitterContext != null && emitterContext.symbolTable != null + && !emitterContext.symbolTable.isStrictOptionEnabled(Strict.HINT_UTF8) + && !emitterContext.compilerOptions.isUnicodeSource) { + opcode = Opcodes.LOAD_BYTE_STRING; + } else { + opcode = Opcodes.LOAD_STRING; + } + emit(opcode); emitReg(rd); emit(strIndex); @@ -1612,7 +1646,12 @@ void handleCompoundAssignment(BinaryOperatorNode node) { */ void handleGeneralArrayAccess(BinaryOperatorNode node) { // Compile the left side (the expression that should yield an array or arrayref) + // Force LIST context so comma expressions like (0,0,1,1) create a list, + // not just return the last value (which happens in scalar context) + int savedContext = currentCallContext; + currentCallContext = RuntimeContextType.LIST; node.left.accept(this); + currentCallContext = savedContext; int baseReg = lastResultReg; // Compile the index expression (right side) @@ -4098,6 +4137,7 @@ private void visitAnonymousSubroutine(SubroutineNode node) { InterpretedCode subCode = subCompiler.compile(node.block); subCode.prototype = node.prototype; subCode.attributes = node.attributes; + subCode.packageName = getCurrentPackage(); if (RuntimeCode.DISASSEMBLE) { System.out.println(subCode.disassemble()); @@ -4705,21 +4745,21 @@ public void visit(LabelNode node) { @Override public void visit(CompilerFlagNode node) { - // Process compiler flags - they modify the symbolTable's pragma stacks - // This is critical for handling `use strict`, `no strict`, etc. during compilation if (emitterContext != null && emitterContext.symbolTable != null) { - ScopedSymbolTable symbolTable = emitterContext.symbolTable; - - // Pop and push new flags - this updates the current scope's pragmas - symbolTable.warningFlagsStack.pop(); - symbolTable.warningFlagsStack.push((java.util.BitSet) node.getWarningFlags().clone()); - - symbolTable.featureFlagsStack.pop(); - symbolTable.featureFlagsStack.push(node.getFeatureFlags()); - - symbolTable.strictOptionsStack.pop(); - symbolTable.strictOptionsStack.push(node.getStrictOptions()); - } + ScopedSymbolTable st = emitterContext.symbolTable; + st.warningFlagsStack.pop(); + st.warningFlagsStack.push((java.util.BitSet) node.getWarningFlags().clone()); + st.featureFlagsStack.pop(); + st.featureFlagsStack.push(node.getFeatureFlags()); + st.strictOptionsStack.pop(); + st.strictOptionsStack.push(node.getStrictOptions()); + } + symbolTable.featureFlagsStack.pop(); + symbolTable.featureFlagsStack.push(node.getFeatureFlags()); + symbolTable.strictOptionsStack.pop(); + symbolTable.strictOptionsStack.push(node.getStrictOptions()); + symbolTable.warningFlagsStack.pop(); + symbolTable.warningFlagsStack.push((java.util.BitSet) node.getWarningFlags().clone()); lastResultReg = -1; } diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index 393701236..2281d18c2 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -101,6 +101,8 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c // Record DVM level so the finally block can clean up everything pushed // by this subroutine (local variables AND regex state snapshot). int savedLocalLevel = DynamicVariableManager.getLocalLevel(); + String savedPackage = InterpreterState.currentPackage.get().toString(); + InterpreterState.currentPackage.get().set(framePackageName); RegexState.save(); try { outer: @@ -223,16 +225,35 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c } case Opcodes.LOAD_STRING: { - // Load string: rd = new RuntimeScalar(stringPool[index]) int rd = bytecode[pc++]; int strIndex = bytecode[pc++]; - registers[rd] = new RuntimeScalar(code.stringPool[strIndex]); + String s = code.stringPool[strIndex]; + RuntimeBase existing = registers[rd]; + if (!(existing instanceof RuntimeScalar rs + && rs.type == RuntimeScalarType.STRING + && s.equals(rs.value))) { + registers[rd] = new RuntimeScalar(s); + } + break; + } + + case Opcodes.LOAD_BYTE_STRING: { + int rd = bytecode[pc++]; + int strIndex = bytecode[pc++]; + String s = code.stringPool[strIndex]; + RuntimeBase existing = registers[rd]; + if (existing instanceof RuntimeScalar rs + && rs.type == RuntimeScalarType.BYTE_STRING + && s.equals(rs.value)) { + break; + } + RuntimeScalar bs = new RuntimeScalar(s); + bs.type = RuntimeScalarType.BYTE_STRING; + registers[rd] = bs; break; } case Opcodes.LOAD_VSTRING: { - // Load v-string literal with VSTRING type (e.g. v5.5.640) - // Mirrors JVM EmitLiteral isVString handling. int rd = bytecode[pc++]; int strIndex = bytecode[pc++]; RuntimeScalar vs = new RuntimeScalar(code.stringPool[strIndex]); @@ -640,7 +661,9 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c RuntimeScalar count = (countVal instanceof RuntimeScalar) ? (RuntimeScalar) countVal : ((RuntimeList) countVal).scalar(); - registers[rd] = Operator.repeat(registers[rs1], count, 1); + int repeatCtx = (registers[rs1] instanceof RuntimeScalar) + ? RuntimeContextType.SCALAR : RuntimeContextType.LIST; + registers[rd] = Operator.repeat(registers[rs1], count, repeatCtx); break; } @@ -1082,6 +1105,26 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c break; } + case Opcodes.ARRAY_EXISTS: { + int rd = bytecode[pc++]; + int arrayReg = bytecode[pc++]; + int indexReg = bytecode[pc++]; + RuntimeArray array = (RuntimeArray) registers[arrayReg]; + RuntimeScalar index = (RuntimeScalar) registers[indexReg]; + registers[rd] = array.exists(index); + break; + } + + case Opcodes.ARRAY_DELETE: { + int rd = bytecode[pc++]; + int arrayReg = bytecode[pc++]; + int indexReg = bytecode[pc++]; + RuntimeArray array = (RuntimeArray) registers[arrayReg]; + RuntimeScalar index = (RuntimeScalar) registers[indexReg]; + registers[rd] = array.delete(index); + break; + } + case Opcodes.HASH_KEYS: { // Get hash keys: rd = keys %hash // Call .keys() on RuntimeBase so that scalars/undef throw the proper @@ -1779,49 +1822,22 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c } case Opcodes.SCALAR_TO_LIST: { - // Convert scalar to RuntimeList (flattened) + // Convert value to RuntimeList, preserving aggregate types (PerlRange, RuntimeArray) + // so that consumers like Pack.pack() can iterate them via RuntimeList's iterator. + // List assignment flattening is handled by SET_FROM_LIST (setFromList method). int rd = bytecode[pc++]; int rs = bytecode[pc++]; RuntimeBase val = registers[rs]; if (val instanceof RuntimeList) { - RuntimeList srcList = (RuntimeList) val; - boolean needsFlatten = false; - for (RuntimeBase elem : srcList.elements) { - if (!(elem instanceof RuntimeScalar)) { - needsFlatten = true; - break; - } - } - if (needsFlatten) { - RuntimeList flat = new RuntimeList(); - for (RuntimeBase elem : srcList.elements) { - if (elem instanceof RuntimeScalar) { - flat.elements.add(elem); - } else if (elem instanceof RuntimeArray) { - for (RuntimeScalar s : (RuntimeArray) elem) { - flat.elements.add(s); - } - } else if (elem instanceof RuntimeList) { - flat.elements.addAll(((RuntimeList) elem).elements); - } else { - flat.elements.add(elem.scalar()); - } - } - registers[rd] = flat; - } else { - registers[rd] = val; - } - } else if (val instanceof RuntimeArray) { - // Convert array to list + registers[rd] = val; + } else if (val instanceof RuntimeScalar) { RuntimeList list = new RuntimeList(); - for (RuntimeScalar elem : (RuntimeArray) val) { - list.elements.add(elem); - } + list.elements.add(val); registers[rd] = list; } else { - // Scalar to list - wrap in a list + // RuntimeArray, PerlRange, etc. - wrap in list, preserving type RuntimeList list = new RuntimeList(); - list.elements.add(val.scalar()); + list.elements.add(val); registers[rd] = list; } break; @@ -2018,6 +2034,20 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c break; } + case Opcodes.SET_FROM_LIST: { + // List assignment: rd = lhsList.setFromList(rhsList) + // Matches JVM backend's RuntimeBase.setFromList() call + int rd = bytecode[pc++]; + int lhsReg = bytecode[pc++]; + int rhsReg = bytecode[pc++]; + RuntimeList lhsList = (RuntimeList) registers[lhsReg]; + RuntimeBase rhsBase = registers[rhsReg]; + RuntimeList rhsList = (rhsBase instanceof RuntimeList rl) ? rl : rhsBase.getList(); + RuntimeArray result = lhsList.setFromList(rhsList); + registers[rd] = result; + break; + } + case Opcodes.HASH_SET_FROM_LIST: { // Set hash content from list: hash_reg = RuntimeHash.createHash(list_reg) // Format: [HASH_SET_FROM_LIST] [hash_reg] [list_reg] @@ -2499,7 +2529,8 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c // Check if we're running inside an eval STRING context // (sourceName starts with "(eval " when code is from eval STRING) // In this case, don't wrap the exception - let the outer eval handler catch it - boolean insideEvalString = code.sourceName != null && code.sourceName.startsWith("(eval "); + boolean insideEvalString = code.sourceName != null + && (code.sourceName.startsWith("(eval ") || code.sourceName.endsWith("(eval)")); if (insideEvalString) { // Re-throw as-is - will be caught by EvalStringHandler.evalString() throw e; @@ -2520,6 +2551,7 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c } // end outer while } finally { DynamicVariableManager.popToLocalLevel(savedLocalLevel); + InterpreterState.currentPackage.get().set(savedPackage); InterpreterState.pop(); } } @@ -2614,10 +2646,12 @@ private static int executeArithmetic(int opcode, int[] bytecode, int pc, int rd = bytecode[pc++]; int rs1 = bytecode[pc++]; int rs2 = bytecode[pc++]; + int repeatCtx = (registers[rs1] instanceof RuntimeScalar) + ? RuntimeContextType.SCALAR : RuntimeContextType.LIST; registers[rd] = Operator.repeat( registers[rs1], (RuntimeScalar) registers[rs2], - 1 // scalar context + repeatCtx ); return pc; } diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java index 6249a9a05..3c7fc133a 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java @@ -81,14 +81,14 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, } // Regular lexical variable (not captured) - // Allocate register for new lexical variable and add to symbol table - int reg = bytecodeCompiler.addVariable(varName, "my"); - - // Compile RHS in the appropriate context - // @ operator will check currentCallContext and emit ARRAY_SIZE if needed + // Compile RHS first, before adding variable to scope, + // so that `my $x = $x` reads the outer $x on the RHS node.right.accept(bytecodeCompiler); int valueReg = bytecodeCompiler.lastResultReg; + // Now allocate register for new lexical variable and add to symbol table + int reg = bytecodeCompiler.addVariable(varName, "my"); + bytecodeCompiler.emit(Opcodes.MY_SCALAR); bytecodeCompiler.emitReg(reg); bytecodeCompiler.emitReg(valueReg); @@ -136,17 +136,19 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, } // Regular lexical array (not captured) - // Allocate register for new lexical array and add to symbol table - int arrayReg = bytecodeCompiler.addVariable(varName, "my"); - - // Create empty array - bytecodeCompiler.emit(Opcodes.NEW_ARRAY); - bytecodeCompiler.emitReg(arrayReg); + // Allocate register but don't add to scope yet, + // so that `my @a = @a` reads the outer @a on the RHS + int arrayReg = bytecodeCompiler.allocateRegister(); - // Compile RHS (should evaluate to a list) + // Compile RHS first, before adding variable to scope node.right.accept(bytecodeCompiler); int listReg = bytecodeCompiler.lastResultReg; + // Now add to symbol table and create array + bytecodeCompiler.registerVariable(varName, arrayReg); + bytecodeCompiler.emit(Opcodes.NEW_ARRAY); + bytecodeCompiler.emitReg(arrayReg); + // Populate array from list using setFromList bytecodeCompiler.emit(Opcodes.ARRAY_SET_FROM_LIST); bytecodeCompiler.emitReg(arrayReg); @@ -194,17 +196,19 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, } // Regular lexical hash (not captured) - // Allocate register for new lexical hash and add to symbol table - int hashReg = bytecodeCompiler.addVariable(varName, "my"); - - // Create empty hash - bytecodeCompiler.emit(Opcodes.NEW_HASH); - bytecodeCompiler.emitReg(hashReg); + // Allocate register but don't add to scope yet, + // so that `my %h = %h` reads the outer %h on the RHS + int hashReg = bytecodeCompiler.allocateRegister(); - // Compile RHS (should evaluate to a list) + // Compile RHS first, before adding variable to scope node.right.accept(bytecodeCompiler); int listReg = bytecodeCompiler.lastResultReg; + // Now add to symbol table and create hash + bytecodeCompiler.registerVariable(varName, hashReg); + bytecodeCompiler.emit(Opcodes.NEW_HASH); + bytecodeCompiler.emitReg(hashReg); + // Populate hash from list bytecodeCompiler.emit(Opcodes.HASH_SET_FROM_LIST); bytecodeCompiler.emitReg(hashReg); @@ -219,13 +223,13 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, if (myOperand instanceof IdentifierNode) { String varName = ((IdentifierNode) myOperand).name; - // Allocate register for new lexical variable and add to symbol table - int reg = bytecodeCompiler.addVariable(varName, "my"); - - // Compile RHS + // Compile RHS first, before adding variable to scope node.right.accept(bytecodeCompiler); int valueReg = bytecodeCompiler.lastResultReg; + // Now allocate register and add to symbol table + int reg = bytecodeCompiler.addVariable(varName, "my"); + bytecodeCompiler.emit(Opcodes.MY_SCALAR); bytecodeCompiler.emitReg(reg); bytecodeCompiler.emitReg(valueReg); @@ -235,6 +239,7 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, } // Handle my ($x, $y, @rest) = ... - list declaration with assignment + // Uses SET_FROM_LIST to match JVM backend's setFromList() semantics if (myOperand instanceof ListNode) { ListNode listNode = (ListNode) myOperand; @@ -248,16 +253,15 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, bytecodeCompiler.emitReg(rhsListReg); bytecodeCompiler.emitReg(listReg); - // Declare and assign each variable + // Declare all variables and collect their registers + List varRegs = new ArrayList<>(); for (int i = 0; i < listNode.elements.size(); i++) { Node element = listNode.elements.get(i); - if (element instanceof OperatorNode) { - OperatorNode sigilOp = (OperatorNode) element; + if (element instanceof OperatorNode sigilOp) { String sigil = sigilOp.operator; if (sigilOp.operand instanceof IdentifierNode) { String varName = sigil + ((IdentifierNode) sigilOp.operand).name; - int varReg; Integer beginIdList = RuntimeCode.evalBeginIds.get(sigilOp); @@ -286,14 +290,9 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, bytecodeCompiler.emit(beginId); } } - bytecodeCompiler.registerVariable(varName, varReg); } else { - // Regular lexical variable (not captured) - // Declare the variable varReg = bytecodeCompiler.addVariable(varName, "my"); - - // Initialize based on sigil switch (sigil) { case "$" -> { bytecodeCompiler.emit(Opcodes.LOAD_UNDEF); @@ -309,54 +308,27 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, } } } - - if (sigil.equals("$")) { - int indexReg = bytecodeCompiler.allocateRegister(); - bytecodeCompiler.emit(Opcodes.LOAD_INT); - bytecodeCompiler.emitReg(indexReg); - bytecodeCompiler.emitInt(i); - - int elemReg = bytecodeCompiler.allocateRegister(); - bytecodeCompiler.emit(Opcodes.ARRAY_GET); - bytecodeCompiler.emitReg(elemReg); - bytecodeCompiler.emitReg(rhsListReg); - bytecodeCompiler.emitReg(indexReg); - - if (beginIdList != null) { - bytecodeCompiler.emit(Opcodes.SET_SCALAR); - bytecodeCompiler.emitReg(varReg); - bytecodeCompiler.emitReg(elemReg); - } else { - bytecodeCompiler.emit(Opcodes.MY_SCALAR); - bytecodeCompiler.emitReg(varReg); - bytecodeCompiler.emitReg(elemReg); - } - } else if (sigil.equals("@")) { - int remainingListReg = bytecodeCompiler.allocateRegister(); - bytecodeCompiler.emit(Opcodes.LIST_SLICE_FROM); - bytecodeCompiler.emitReg(remainingListReg); - bytecodeCompiler.emitReg(rhsListReg); - bytecodeCompiler.emitInt(i); - - bytecodeCompiler.emit(Opcodes.ARRAY_SET_FROM_LIST); - bytecodeCompiler.emitReg(varReg); - bytecodeCompiler.emitReg(remainingListReg); - } else if (sigil.equals("%")) { - int remainingListReg = bytecodeCompiler.allocateRegister(); - bytecodeCompiler.emit(Opcodes.LIST_SLICE_FROM); - bytecodeCompiler.emitReg(remainingListReg); - bytecodeCompiler.emitReg(rhsListReg); - bytecodeCompiler.emitInt(i); - - bytecodeCompiler.emit(Opcodes.HASH_SET_FROM_LIST); - bytecodeCompiler.emitReg(varReg); - bytecodeCompiler.emitReg(remainingListReg); - } + varRegs.add(varReg); } } } - bytecodeCompiler.lastResultReg = rhsListReg; + // Build LHS list and assign via SET_FROM_LIST + int lhsListReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.CREATE_LIST); + bytecodeCompiler.emitReg(lhsListReg); + bytecodeCompiler.emit(varRegs.size()); + for (int reg : varRegs) { + bytecodeCompiler.emitReg(reg); + } + + int resultReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.SET_FROM_LIST); + bytecodeCompiler.emitReg(resultReg); + bytecodeCompiler.emitReg(lhsListReg); + bytecodeCompiler.emitReg(rhsListReg); + + bytecodeCompiler.lastResultReg = resultReg; return; } } @@ -538,6 +510,11 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, bytecodeCompiler.emit(Opcodes.SET_SCALAR); bytecodeCompiler.emitReg(localReg); bytecodeCompiler.emitReg(valueReg); + // After localization, reload ourReg so subsequent accesses + // to the `our` variable see the new localized scalar. + bytecodeCompiler.emit(Opcodes.LOAD_GLOBAL_SCALAR); + bytecodeCompiler.emitReg(ourReg); + bytecodeCompiler.emit(nameIdx); bytecodeCompiler.lastResultReg = localReg; } case "@" -> { @@ -944,8 +921,7 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, } } else if (leftOp.operand instanceof ListNode) { // our ($a, $b) = ... - list declaration with assignment - // The our statement already declared the variables and returned a list - // We need to assign the RHS values to each variable + // Uses SET_FROM_LIST to match JVM backend's setFromList() semantics ListNode listNode = (ListNode) leftOp.operand; // Convert RHS to list @@ -954,46 +930,35 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, bytecodeCompiler.emitReg(rhsListReg); bytecodeCompiler.emitReg(valueReg); - // Assign each element + // Collect variable registers (already declared by our visitor) + List varRegs = new ArrayList<>(); for (int i = 0; i < listNode.elements.size(); i++) { Node element = listNode.elements.get(i); - if (element instanceof OperatorNode) { - OperatorNode sigilOp = (OperatorNode) element; + if (element instanceof OperatorNode sigilOp) { String sigil = sigilOp.operator; - if (sigilOp.operand instanceof IdentifierNode) { String varName = sigil + ((IdentifierNode) sigilOp.operand).name; - int varReg = bytecodeCompiler.getVariableRegister(varName); - - // Get i-th element from RHS - int indexReg = bytecodeCompiler.allocateRegister(); - bytecodeCompiler.emit(Opcodes.LOAD_INT); - bytecodeCompiler.emitReg(indexReg); - bytecodeCompiler.emitInt(i); - - int elemReg = bytecodeCompiler.allocateRegister(); - bytecodeCompiler.emit(Opcodes.ARRAY_GET); - bytecodeCompiler.emitReg(elemReg); - bytecodeCompiler.emitReg(rhsListReg); - bytecodeCompiler.emitReg(indexReg); - - if (sigil.equals("$")) { - bytecodeCompiler.emit(Opcodes.SET_SCALAR); - bytecodeCompiler.emitReg(varReg); - bytecodeCompiler.emitReg(elemReg); - } else if (sigil.equals("@")) { - bytecodeCompiler.emit(Opcodes.ARRAY_SET_FROM_LIST); - bytecodeCompiler.emitReg(varReg); - bytecodeCompiler.emitReg(elemReg); - } else if (sigil.equals("%")) { - bytecodeCompiler.emit(Opcodes.HASH_SET_FROM_LIST); - bytecodeCompiler.emitReg(varReg); - bytecodeCompiler.emitReg(elemReg); - } + varRegs.add(bytecodeCompiler.getVariableRegister(varName)); } } } - bytecodeCompiler.lastResultReg = valueReg; + + // Build LHS list and assign via SET_FROM_LIST + int lhsListReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.CREATE_LIST); + bytecodeCompiler.emitReg(lhsListReg); + bytecodeCompiler.emit(varRegs.size()); + for (int reg : varRegs) { + bytecodeCompiler.emitReg(reg); + } + + int resultReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.SET_FROM_LIST); + bytecodeCompiler.emitReg(resultReg); + bytecodeCompiler.emitReg(lhsListReg); + bytecodeCompiler.emitReg(rhsListReg); + + bytecodeCompiler.lastResultReg = resultReg; bytecodeCompiler.currentCallContext = savedContext; return; } @@ -1681,17 +1646,12 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, // List assignment: ($a, $b) = ... or () = ... // In scalar context, returns the number of elements on RHS // In list context, returns the RHS list - // Validate lvalue context - throws PerlCompilerException for invalid LHS - // (e.g. "($a ? $x : ($y)) = 5" -> "Assignment to both a list and a scalar") LValueVisitor.getContext(node.left); ListNode listNode = (ListNode) node.left; - // Compile RHS in LIST context to get all elements - int savedRhsContext = bytecodeCompiler.currentCallContext; - bytecodeCompiler.currentCallContext = RuntimeContextType.LIST; - node.right.accept(bytecodeCompiler); - int rhsReg = bytecodeCompiler.lastResultReg; - bytecodeCompiler.currentCallContext = savedRhsContext; + // RHS was already compiled at the "regular assignment" fallthrough above (valueReg). + // Reuse it instead of compiling again. + int rhsReg = valueReg; // Convert RHS to RuntimeList if needed int rhsListReg = bytecodeCompiler.allocateRegister(); @@ -1699,157 +1659,108 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, bytecodeCompiler.emitReg(rhsListReg); bytecodeCompiler.emitReg(rhsReg); - // If the list is not empty, perform the assignment - if (!listNode.elements.isEmpty()) { - // Assign each RHS element to corresponding LHS variable - for (int i = 0; i < listNode.elements.size(); i++) { - Node lhsElement = listNode.elements.get(i); - - // Get the i-th element from RHS list - int indexReg = bytecodeCompiler.allocateRegister(); - bytecodeCompiler.emit(Opcodes.LOAD_INT); - bytecodeCompiler.emitReg(indexReg); - bytecodeCompiler.emitInt(i); - - int elementReg = bytecodeCompiler.allocateRegister(); - bytecodeCompiler.emit(Opcodes.ARRAY_GET); - bytecodeCompiler.emitReg(elementReg); - bytecodeCompiler.emitReg(rhsListReg); - bytecodeCompiler.emitReg(indexReg); - - // Assign to LHS element - if (lhsElement instanceof OperatorNode) { - OperatorNode lhsOp = (OperatorNode) lhsElement; - if (lhsOp.operator.equals("$") && lhsOp.operand instanceof IdentifierNode) { - String varName = "$" + ((IdentifierNode) lhsOp.operand).name; - - if (bytecodeCompiler.hasVariable(varName)) { - int targetReg = bytecodeCompiler.getVariableRegister(varName); - if ((bytecodeCompiler.capturedVarIndices != null && bytecodeCompiler.capturedVarIndices.containsKey(varName)) - || bytecodeCompiler.closureCapturedVarNames.contains(varName)) { - bytecodeCompiler.emit(Opcodes.SET_SCALAR); - bytecodeCompiler.emitReg(targetReg); - bytecodeCompiler.emitReg(elementReg); - } else { - bytecodeCompiler.emit(Opcodes.LOAD_UNDEF); - bytecodeCompiler.emitReg(targetReg); - bytecodeCompiler.emit(Opcodes.SET_SCALAR); - bytecodeCompiler.emitReg(targetReg); - bytecodeCompiler.emitReg(elementReg); - } - } else { - // Normalize global variable name (remove sigil, add package) - // Check strict vars before list assignment - if (bytecodeCompiler.shouldBlockGlobalUnderStrictVars(varName)) { - bytecodeCompiler.throwCompilerException("Global symbol \"" + varName + "\" requires explicit package name"); - } + // Resolve all LHS variables and collect their registers + List varRegs = new ArrayList<>(); + for (Node lhsElement : listNode.elements) { + if (lhsElement instanceof OperatorNode lhsOp && lhsOp.operand instanceof IdentifierNode idNode) { + String sigil = lhsOp.operator; + String varName = sigil + idNode.name; - String bareVarName = varName.substring(1); // Remove "$" - String normalizedName = NameNormalizer.normalizeVariableName(bareVarName, bytecodeCompiler.getCurrentPackage()); - int nameIdx = bytecodeCompiler.addToStringPool(normalizedName); - bytecodeCompiler.emit(Opcodes.STORE_GLOBAL_SCALAR); - bytecodeCompiler.emit(nameIdx); - bytecodeCompiler.emitReg(elementReg); + if (sigil.equals("$")) { + if (bytecodeCompiler.hasVariable(varName)) { + int targetReg = bytecodeCompiler.getVariableRegister(varName); + if (!((bytecodeCompiler.capturedVarIndices != null && bytecodeCompiler.capturedVarIndices.containsKey(varName)) + || bytecodeCompiler.closureCapturedVarNames.contains(varName))) { + bytecodeCompiler.emit(Opcodes.LOAD_UNDEF); + bytecodeCompiler.emitReg(targetReg); } - } else if (lhsOp.operator.equals("@") && lhsOp.operand instanceof IdentifierNode) { - // Array slurp: ($a, @rest) = ... - // Collect remaining elements into a RuntimeList - String varName = "@" + ((IdentifierNode) lhsOp.operand).name; - - int arrayReg; - if (bytecodeCompiler.currentSubroutineBeginId != 0 && bytecodeCompiler.currentSubroutineClosureVars != null - && bytecodeCompiler.currentSubroutineClosureVars.contains(varName)) { - arrayReg = bytecodeCompiler.allocateRegister(); - int nameIdx = bytecodeCompiler.addToStringPool(varName); - bytecodeCompiler.emitWithToken(Opcodes.RETRIEVE_BEGIN_ARRAY, node.getIndex()); - bytecodeCompiler.emitReg(arrayReg); - bytecodeCompiler.emit(nameIdx); - bytecodeCompiler.emit(bytecodeCompiler.currentSubroutineBeginId); - } else if (bytecodeCompiler.hasVariable(varName)) { - arrayReg = bytecodeCompiler.getVariableRegister(varName); - } else { - arrayReg = bytecodeCompiler.allocateRegister(); - String globalArrayName = NameNormalizer.normalizeVariableName( - ((IdentifierNode) lhsOp.operand).name, - bytecodeCompiler.getCurrentPackage() - ); - int nameIdx = bytecodeCompiler.addToStringPool(globalArrayName); - bytecodeCompiler.emit(Opcodes.LOAD_GLOBAL_ARRAY); - bytecodeCompiler.emitReg(arrayReg); - bytecodeCompiler.emit(nameIdx); + varRegs.add(targetReg); + } else { + if (bytecodeCompiler.shouldBlockGlobalUnderStrictVars(varName)) { + bytecodeCompiler.throwCompilerException("Global symbol \"" + varName + "\" requires explicit package name"); } - - // Create a list of remaining indices - // Use SLOWOP_LIST_SLICE_FROM to get list[i..] - int remainingListReg = bytecodeCompiler.allocateRegister(); - bytecodeCompiler.emit(Opcodes.LIST_SLICE_FROM); - bytecodeCompiler.emitReg(remainingListReg); - bytecodeCompiler.emitReg(rhsListReg); - bytecodeCompiler.emitInt(i); // Start index - - // Populate array from remaining elements - bytecodeCompiler.emit(Opcodes.ARRAY_SET_FROM_LIST); + String normalizedName = NameNormalizer.normalizeVariableName(idNode.name, bytecodeCompiler.getCurrentPackage()); + int nameIdx = bytecodeCompiler.addToStringPool(normalizedName); + int globalReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.LOAD_GLOBAL_SCALAR); + bytecodeCompiler.emitReg(globalReg); + bytecodeCompiler.emit(nameIdx); + varRegs.add(globalReg); + } + } else if (sigil.equals("@")) { + int arrayReg; + if (bytecodeCompiler.currentSubroutineBeginId != 0 && bytecodeCompiler.currentSubroutineClosureVars != null + && bytecodeCompiler.currentSubroutineClosureVars.contains(varName)) { + arrayReg = bytecodeCompiler.allocateRegister(); + int nameIdx = bytecodeCompiler.addToStringPool(varName); + bytecodeCompiler.emitWithToken(Opcodes.RETRIEVE_BEGIN_ARRAY, node.getIndex()); bytecodeCompiler.emitReg(arrayReg); - bytecodeCompiler.emitReg(remainingListReg); - - // Array slurp consumes all remaining elements - break; - } else if (lhsOp.operator.equals("%") && lhsOp.operand instanceof IdentifierNode) { - // Hash slurp: ($a, %rest) = ... - String varName = "%" + ((IdentifierNode) lhsOp.operand).name; - - int hashReg; - if (bytecodeCompiler.currentSubroutineBeginId != 0 && bytecodeCompiler.currentSubroutineClosureVars != null - && bytecodeCompiler.currentSubroutineClosureVars.contains(varName)) { - hashReg = bytecodeCompiler.allocateRegister(); - int nameIdx = bytecodeCompiler.addToStringPool(varName); - bytecodeCompiler.emitWithToken(Opcodes.RETRIEVE_BEGIN_HASH, node.getIndex()); - bytecodeCompiler.emitReg(hashReg); - bytecodeCompiler.emit(nameIdx); - bytecodeCompiler.emit(bytecodeCompiler.currentSubroutineBeginId); - } else if (bytecodeCompiler.hasVariable(varName)) { - hashReg = bytecodeCompiler.getVariableRegister(varName); - } else { - hashReg = bytecodeCompiler.allocateRegister(); - String globalHashName = NameNormalizer.normalizeVariableName( - ((IdentifierNode) lhsOp.operand).name, - bytecodeCompiler.getCurrentPackage() - ); - int nameIdx = bytecodeCompiler.addToStringPool(globalHashName); - bytecodeCompiler.emit(Opcodes.LOAD_GLOBAL_HASH); - bytecodeCompiler.emitReg(hashReg); - bytecodeCompiler.emit(nameIdx); - } - - // Get remaining elements from list - int remainingListReg = bytecodeCompiler.allocateRegister(); - bytecodeCompiler.emit(Opcodes.LIST_SLICE_FROM); - bytecodeCompiler.emitReg(remainingListReg); - bytecodeCompiler.emitReg(rhsListReg); - bytecodeCompiler.emitInt(i); // Start index - - // Populate hash from remaining elements - bytecodeCompiler.emit(Opcodes.HASH_SET_FROM_LIST); + bytecodeCompiler.emit(nameIdx); + bytecodeCompiler.emit(bytecodeCompiler.currentSubroutineBeginId); + } else if (bytecodeCompiler.hasVariable(varName)) { + arrayReg = bytecodeCompiler.getVariableRegister(varName); + } else { + arrayReg = bytecodeCompiler.allocateRegister(); + String globalName = NameNormalizer.normalizeVariableName(idNode.name, bytecodeCompiler.getCurrentPackage()); + int nameIdx = bytecodeCompiler.addToStringPool(globalName); + bytecodeCompiler.emit(Opcodes.LOAD_GLOBAL_ARRAY); + bytecodeCompiler.emitReg(arrayReg); + bytecodeCompiler.emit(nameIdx); + } + varRegs.add(arrayReg); + } else if (sigil.equals("%")) { + int hashReg; + if (bytecodeCompiler.currentSubroutineBeginId != 0 && bytecodeCompiler.currentSubroutineClosureVars != null + && bytecodeCompiler.currentSubroutineClosureVars.contains(varName)) { + hashReg = bytecodeCompiler.allocateRegister(); + int nameIdx = bytecodeCompiler.addToStringPool(varName); + bytecodeCompiler.emitWithToken(Opcodes.RETRIEVE_BEGIN_HASH, node.getIndex()); bytecodeCompiler.emitReg(hashReg); - bytecodeCompiler.emitReg(remainingListReg); - - // Hash slurp consumes all remaining elements - break; + bytecodeCompiler.emit(nameIdx); + bytecodeCompiler.emit(bytecodeCompiler.currentSubroutineBeginId); + } else if (bytecodeCompiler.hasVariable(varName)) { + hashReg = bytecodeCompiler.getVariableRegister(varName); + } else { + hashReg = bytecodeCompiler.allocateRegister(); + String globalName = NameNormalizer.normalizeVariableName(idNode.name, bytecodeCompiler.getCurrentPackage()); + int nameIdx = bytecodeCompiler.addToStringPool(globalName); + bytecodeCompiler.emit(Opcodes.LOAD_GLOBAL_HASH); + bytecodeCompiler.emitReg(hashReg); + bytecodeCompiler.emit(nameIdx); } + varRegs.add(hashReg); } } } - // Return value depends on savedContext (the context this assignment was called in) + int countReg = -1; if (savedContext == RuntimeContextType.SCALAR) { - // In scalar context, list assignment returns the count of RHS elements - int countReg = bytecodeCompiler.allocateRegister(); + countReg = bytecodeCompiler.allocateRegister(); bytecodeCompiler.emit(Opcodes.ARRAY_SIZE); bytecodeCompiler.emitReg(countReg); bytecodeCompiler.emitReg(rhsListReg); + } + + // Build LHS list and assign via SET_FROM_LIST + if (!varRegs.isEmpty()) { + int lhsListReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.CREATE_LIST); + bytecodeCompiler.emitReg(lhsListReg); + bytecodeCompiler.emit(varRegs.size()); + for (int reg : varRegs) { + bytecodeCompiler.emitReg(reg); + } + + int resultReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.SET_FROM_LIST); + bytecodeCompiler.emitReg(resultReg); + bytecodeCompiler.emitReg(lhsListReg); + bytecodeCompiler.emitReg(rhsListReg); + } + + if (countReg >= 0) { bytecodeCompiler.lastResultReg = countReg; } else { - // In list context, return the RHS value bytecodeCompiler.lastResultReg = rhsListReg; } diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperator.java index 584f38310..d514f28a7 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperator.java @@ -1,6 +1,7 @@ package org.perlonjava.backend.bytecode; import org.perlonjava.frontend.astnode.*; +import org.perlonjava.runtime.runtimetypes.NameNormalizer; import org.perlonjava.runtime.runtimetypes.RuntimeContextType; public class CompileBinaryOperator { @@ -697,7 +698,17 @@ else if (node.right instanceof BinaryOperatorNode) { } private static void compileBinaryAsListOp(BytecodeCompiler bytecodeCompiler, BinaryOperatorNode node) { - node.left.accept(bytecodeCompiler); + if (node.left instanceof IdentifierNode idNode) { + String name = NameNormalizer.normalizeVariableName(idNode.name, bytecodeCompiler.getCurrentPackage()); + int fhReg = bytecodeCompiler.allocateRegister(); + int nameIdx = bytecodeCompiler.addToStringPool(name); + bytecodeCompiler.emit(Opcodes.LOAD_GLOB); + bytecodeCompiler.emitReg(fhReg); + bytecodeCompiler.emit(nameIdx); + bytecodeCompiler.lastResultReg = fhReg; + } else { + node.left.accept(bytecodeCompiler); + } int fhReg = bytecodeCompiler.lastResultReg; java.util.List argRegs = new java.util.ArrayList<>(); diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java index 2057ee310..471938a19 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java @@ -30,6 +30,45 @@ private static void compileScalarOperand(BytecodeCompiler bc, OperatorNode node, } } + private static int compileArrayForExistsDelete(BytecodeCompiler bc, BinaryOperatorNode arrayAccess, int tokenIndex) { + if (!(arrayAccess.left instanceof OperatorNode leftOp) || !leftOp.operator.equals("$") + || !(leftOp.operand instanceof IdentifierNode)) { + bc.throwCompilerException("Array exists/delete requires simple array variable"); + return -1; + } + String varName = ((IdentifierNode) leftOp.operand).name; + String arrayVarName = "@" + varName; + if (bc.currentSubroutineBeginId != 0 && bc.currentSubroutineClosureVars != null + && bc.currentSubroutineClosureVars.contains(arrayVarName)) { + int arrayReg = bc.allocateRegister(); + int nameIdx = bc.addToStringPool(arrayVarName); + bc.emitWithToken(Opcodes.RETRIEVE_BEGIN_ARRAY, tokenIndex); + bc.emitReg(arrayReg); + bc.emit(nameIdx); + bc.emit(bc.currentSubroutineBeginId); + return arrayReg; + } else if (bc.hasVariable(arrayVarName)) { + return bc.getVariableRegister(arrayVarName); + } else { + int arrayReg = bc.allocateRegister(); + String globalArrayName = NameNormalizer.normalizeVariableName(varName, bc.getCurrentPackage()); + int nameIdx = bc.addToStringPool(globalArrayName); + bc.emit(Opcodes.LOAD_GLOBAL_ARRAY); + bc.emitReg(arrayReg); + bc.emit(nameIdx); + return arrayReg; + } + } + + private static int compileArrayIndex(BytecodeCompiler bc, BinaryOperatorNode arrayAccess) { + if (!(arrayAccess.right instanceof ArrayLiteralNode indexNode) || indexNode.elements.isEmpty()) { + bc.throwCompilerException("Array exists/delete requires index"); + return -1; + } + indexNode.elements.get(0).accept(bc); + return bc.lastResultReg; + } + public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode node) { // Track token index for error reporting bytecodeCompiler.currentTokenIndex = node.getIndex(); @@ -879,10 +918,14 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode int stringReg = bytecodeCompiler.lastResultReg; int rd = bytecodeCompiler.allocateRegister(); - // Snapshot visible variables for this eval site + // Snapshot visible variables and pragma flags for this eval site int evalSiteIndex = bytecodeCompiler.evalSiteRegistries.size(); bytecodeCompiler.evalSiteRegistries.add( bytecodeCompiler.symbolTable.getVisibleVariableRegistry()); + bytecodeCompiler.evalSitePragmaFlags.add(new int[]{ + bytecodeCompiler.symbolTable.strictOptionsStack.peek(), + bytecodeCompiler.symbolTable.featureFlagsStack.peek() + }); bytecodeCompiler.emitWithToken(Opcodes.EVAL_STRING, node.getIndex()); bytecodeCompiler.emitReg(rd); @@ -1229,7 +1272,7 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode bytecodeCompiler.emit(Opcodes.REVERSE); bytecodeCompiler.emitReg(rd); bytecodeCompiler.emitReg(argsListReg); - bytecodeCompiler.emit(RuntimeContextType.LIST); // Context + bytecodeCompiler.emit(bytecodeCompiler.currentCallContext); bytecodeCompiler.lastResultReg = rd; } else if (op.equals("exists")) { @@ -1349,9 +1392,20 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode bytecodeCompiler.emitReg(hashReg); bytecodeCompiler.emitReg(keyReg); + bytecodeCompiler.lastResultReg = rd; + } else if (arg instanceof BinaryOperatorNode && ((BinaryOperatorNode) arg).operator.equals("[")) { + BinaryOperatorNode arrayAccess = (BinaryOperatorNode) arg; + int arrayReg = compileArrayForExistsDelete(bytecodeCompiler, arrayAccess, node.getIndex()); + int indexReg = compileArrayIndex(bytecodeCompiler, arrayAccess); + + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.ARRAY_EXISTS); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(arrayReg); + bytecodeCompiler.emitReg(indexReg); + bytecodeCompiler.lastResultReg = rd; } else { - // For now, use SLOW_OP for other cases (array exists, etc.) arg.accept(bytecodeCompiler); int argReg = bytecodeCompiler.lastResultReg; @@ -1602,9 +1656,20 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode bytecodeCompiler.emitReg(hashReg); bytecodeCompiler.emitReg(keyReg); + bytecodeCompiler.lastResultReg = rd; + } else if (arg instanceof BinaryOperatorNode && ((BinaryOperatorNode) arg).operator.equals("[")) { + BinaryOperatorNode arrayAccess = (BinaryOperatorNode) arg; + int arrayReg = compileArrayForExistsDelete(bytecodeCompiler, arrayAccess, node.getIndex()); + int indexReg = compileArrayIndex(bytecodeCompiler, arrayAccess); + + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.ARRAY_DELETE); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(arrayReg); + bytecodeCompiler.emitReg(indexReg); + bytecodeCompiler.lastResultReg = rd; } else { - // For now, use SLOW_OP for other cases (hash slice delete, array delete, etc.) arg.accept(bytecodeCompiler); int argReg = bytecodeCompiler.lastResultReg; diff --git a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java index 503ca4c81..917d21b0c 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java @@ -90,6 +90,19 @@ public static RuntimeList evalStringList(String perlCode, int sourceLine, int callContext, Map siteRegistry) { + return evalStringList(perlCode, currentCode, registers, sourceName, sourceLine, + callContext, siteRegistry, -1, -1); + } + + public static RuntimeList evalStringList(String perlCode, + InterpretedCode currentCode, + RuntimeBase[] registers, + String sourceName, + int sourceLine, + int callContext, + Map siteRegistry, + int siteStrictOptions, + int siteFeatureFlags) { try { evalTrace("EvalStringHandler enter ctx=" + callContext + " srcName=" + sourceName + " srcLine=" + sourceLine + " codeLen=" + (perlCode != null ? perlCode.length() : -1)); @@ -109,11 +122,12 @@ public static RuntimeList evalStringList(String perlCode, // Inherit lexical pragma flags from parent if available if (currentCode != null) { - // Replace default values with parent's flags + int strictOpts = (siteStrictOptions >= 0) ? siteStrictOptions : currentCode.strictOptions; + int featFlags = (siteFeatureFlags >= 0) ? siteFeatureFlags : currentCode.featureFlags; symbolTable.strictOptionsStack.pop(); - symbolTable.strictOptionsStack.push(currentCode.strictOptions); + symbolTable.strictOptionsStack.push(strictOpts); symbolTable.featureFlagsStack.pop(); - symbolTable.featureFlagsStack.push(currentCode.featureFlags); + symbolTable.featureFlagsStack.push(featFlags); symbolTable.warningFlagsStack.pop(); symbolTable.warningFlagsStack.push((java.util.BitSet) currentCode.warningFlags.clone()); } diff --git a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java index d0b6f8dc3..101585144 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java +++ b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java @@ -30,6 +30,7 @@ public class InterpretedCode extends RuntimeCode { public final RuntimeBase[] capturedVars; // Closure support (captured from outer scope) public final Map variableRegistry; // Variable name → register index (for eval STRING) public final List> evalSiteRegistries; // Per-eval-site variable registries + public final List evalSitePragmaFlags; // Per-eval-site [strictOptions, featureFlags] // Lexical pragma state (for eval STRING to inherit) public final int strictOptions; // Strict flags at compile time @@ -69,7 +70,7 @@ public InterpretedCode(int[] bytecode, Object[] constants, String[] stringPool, int strictOptions, int featureFlags, BitSet warningFlags) { this(bytecode, constants, stringPool, maxRegisters, capturedVars, sourceName, sourceLine, pcToTokenIndex, variableRegistry, errorUtil, - strictOptions, featureFlags, warningFlags, "main", null); + strictOptions, featureFlags, warningFlags, "main", null, null); } public InterpretedCode(int[] bytecode, Object[] constants, String[] stringPool, @@ -82,7 +83,7 @@ public InterpretedCode(int[] bytecode, Object[] constants, String[] stringPool, String compilePackage) { this(bytecode, constants, stringPool, maxRegisters, capturedVars, sourceName, sourceLine, pcToTokenIndex, variableRegistry, errorUtil, - strictOptions, featureFlags, warningFlags, compilePackage, null); + strictOptions, featureFlags, warningFlags, compilePackage, null, null); } public InterpretedCode(int[] bytecode, Object[] constants, String[] stringPool, @@ -93,7 +94,8 @@ public InterpretedCode(int[] bytecode, Object[] constants, String[] stringPool, ErrorMessageUtil errorUtil, int strictOptions, int featureFlags, BitSet warningFlags, String compilePackage, - List> evalSiteRegistries) { + List> evalSiteRegistries, + List evalSitePragmaFlags) { super(null, new java.util.ArrayList<>()); this.bytecode = bytecode; this.constants = constants; @@ -105,11 +107,15 @@ public InterpretedCode(int[] bytecode, Object[] constants, String[] stringPool, this.pcToTokenIndex = pcToTokenIndex; this.variableRegistry = variableRegistry; this.evalSiteRegistries = evalSiteRegistries; + this.evalSitePragmaFlags = evalSitePragmaFlags; this.errorUtil = errorUtil; this.strictOptions = strictOptions; this.featureFlags = featureFlags; this.warningFlags = warningFlags; this.compilePackage = compilePackage; + if (this.packageName == null && compilePackage != null) { + this.packageName = compilePackage; + } } // Legacy constructor for backward compatibility @@ -190,7 +196,8 @@ public InterpretedCode withCapturedVars(RuntimeBase[] capturedVars) { this.featureFlags, this.warningFlags, this.compilePackage, - this.evalSiteRegistries + this.evalSiteRegistries, + this.evalSitePragmaFlags ); copy.prototype = this.prototype; copy.attributes = this.attributes; @@ -332,10 +339,12 @@ public String disassemble() { pc += 1; sb.append("LOAD_INT r").append(rd).append(" = ").append(value).append("\n"); break; + case Opcodes.LOAD_BYTE_STRING: case Opcodes.LOAD_STRING: rd = bytecode[pc++]; int strIdx = bytecode[pc++]; - sb.append("LOAD_STRING r").append(rd).append(" = \""); + sb.append(opcode == Opcodes.LOAD_BYTE_STRING ? "LOAD_BYTE_STRING r" : "LOAD_STRING r") + .append(rd).append(" = \""); if (stringPool != null && strIdx < stringPool.length) { String str = stringPool[strIdx]; // Escape special characters for readability @@ -1058,6 +1067,18 @@ public String disassemble() { int keyDeleteReg = bytecode[pc++]; sb.append("HASH_DELETE r").append(rd).append(" = delete r").append(hashDeleteReg).append("{r").append(keyDeleteReg).append("}\n"); break; + case Opcodes.ARRAY_EXISTS: + rd = bytecode[pc++]; + int arrExistsReg = bytecode[pc++]; + int idxExistsReg = bytecode[pc++]; + sb.append("ARRAY_EXISTS r").append(rd).append(" = exists r").append(arrExistsReg).append("[r").append(idxExistsReg).append("]\n"); + break; + case Opcodes.ARRAY_DELETE: + rd = bytecode[pc++]; + int arrDeleteReg = bytecode[pc++]; + int idxDeleteReg = bytecode[pc++]; + sb.append("ARRAY_DELETE r").append(rd).append(" = delete r").append(arrDeleteReg).append("[r").append(idxDeleteReg).append("]\n"); + break; case Opcodes.HASH_KEYS: rd = bytecode[pc++]; int hashKeysReg = bytecode[pc++]; @@ -1166,6 +1187,12 @@ public String disassemble() { rs2 = bytecode[pc++]; // list register sb.append("ARRAY_SET_FROM_LIST r").append(rs1).append(".setFromList(r").append(rs2).append(")\n"); break; + case Opcodes.SET_FROM_LIST: + rd = bytecode[pc++]; + rs1 = bytecode[pc++]; // lhs list + rs2 = bytecode[pc++]; // rhs list + sb.append("SET_FROM_LIST r").append(rd).append(" = r").append(rs1).append(".setFromList(r").append(rs2).append(")\n"); + break; case Opcodes.HASH_SET_FROM_LIST: rs1 = bytecode[pc++]; // hash register rs2 = bytecode[pc++]; // list register diff --git a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java index 7e00862b4..5507fee91 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java +++ b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java @@ -1211,5 +1211,18 @@ public class Opcodes { * Format: DEREF_GLOB_NONSTRICT rd rs pkgIdx */ public static final short DEREF_GLOB_NONSTRICT = 370; + /** Array exists: rd = array_reg.exists(index_reg) */ + public static final short ARRAY_EXISTS = 371; + /** Array delete: rd = array_reg.delete(index_reg) */ + public static final short ARRAY_DELETE = 372; + /** List assignment: rd = lhs_list_reg.setFromList(rhs_list_reg) + * Format: SET_FROM_LIST rd lhsListReg rhsListReg */ + public static final short SET_FROM_LIST = 373; + + /** Load byte string: rd = new RuntimeScalar(stringPool[index]) with BYTE_STRING type. + * Used for string literals under `no utf8` (the default). + * Format: LOAD_BYTE_STRING rd strIndex */ + public static final short LOAD_BYTE_STRING = 374; + private Opcodes() {} // Utility class - no instantiation } diff --git a/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java b/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java index 84211be1c..53fb2cfae 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java @@ -288,6 +288,16 @@ public static int executeEvalString( siteRegistry = code.evalSiteRegistries.get(evalSiteIndex); } + // Look up per-eval-site pragma flags (strict/feature at compile time of eval site) + int siteStrictOptions = -1; + int siteFeatureFlags = -1; + if (evalSiteIndex >= 0 && code.evalSitePragmaFlags != null + && evalSiteIndex < code.evalSitePragmaFlags.size()) { + int[] pragmaFlags = code.evalSitePragmaFlags.get(evalSiteIndex); + siteStrictOptions = pragmaFlags[0]; + siteFeatureFlags = pragmaFlags[1]; + } + RuntimeBase codeValue = registers[stringReg]; RuntimeScalar codeScalar; if (codeValue instanceof RuntimeScalar) { @@ -315,21 +325,25 @@ public static int executeEvalString( code.sourceName, code.sourceLine, callContext, - siteRegistry + siteRegistry, + siteStrictOptions, + siteFeatureFlags ); registers[rd] = result; evalTrace("EVAL_STRING opcode exit LIST stored=" + (registers[rd] != null ? registers[rd].getClass().getSimpleName() : "null") + " scalar=" + result.scalar().toString()); } else { - RuntimeScalar result = EvalStringHandler.evalString( + RuntimeScalar result = EvalStringHandler.evalStringList( perlCode, code, registers, code.sourceName, code.sourceLine, callContext, - siteRegistry - ); + siteRegistry, + siteStrictOptions, + siteFeatureFlags + ).scalar(); registers[rd] = result; evalTrace("EVAL_STRING opcode exit SCALAR/VOID stored=" + (registers[rd] != null ? registers[rd].getClass().getSimpleName() : "null") + " val=" + result.toString() + " bool=" + result.getBoolean()); @@ -545,6 +559,12 @@ public static int executeDerefArray( registers[rd] = scalarBase; return pc; } + if (scalarBase instanceof RuntimeList) { + RuntimeArray arr = new RuntimeArray(); + ((RuntimeList) scalarBase).addToArray(arr); + registers[rd] = arr; + return pc; + } // Otherwise, dereference as array reference RuntimeScalar scalar = scalarBase.scalar(); @@ -873,6 +893,12 @@ public static int executeDerefArrayNonStrict(int[] bytecode, int pc, RuntimeBase registers[rd] = scalarBase; return pc; } + if (scalarBase instanceof RuntimeList) { + RuntimeArray arr = new RuntimeArray(); + ((RuntimeList) scalarBase).addToArray(arr); + registers[rd] = arr; + return pc; + } RuntimeScalar scalar = scalarBase.scalar(); registers[rd] = scalar.arrayDerefNonStrict(pkg); return pc; @@ -1053,7 +1079,8 @@ public static int executeLength( RuntimeBase stringBase = registers[stringReg]; RuntimeScalar stringScalar = stringBase.scalar(); - int length = stringScalar.toString().length(); + String str = stringScalar.toString(); + int length = str.codePointCount(0, str.length()); registers[rd] = new RuntimeScalar(length); return pc; diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java b/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java index 0fc54c4d7..187f05a86 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java @@ -373,8 +373,14 @@ static void handleSystemBuiltin(EmitterVisitor emitterVisitor, OperatorNode node operand.elements.addFirst(operand.handle); } - // Accept the operand in LIST context. - operand.accept(emitterVisitor.with(RuntimeContextType.LIST)); + try { + // Accept the operand in LIST context. + operand.accept(emitterVisitor.with(RuntimeContextType.LIST)); + } finally { + if (hasHandle) { + operand.elements.removeFirst(); + } + } // Push the boolean value of hasHandle to the stack MethodVisitor mv = emitterVisitor.ctx.mv; diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitOperatorDeleteExists.java b/src/main/java/org/perlonjava/backend/jvm/EmitOperatorDeleteExists.java index 593ecb5b7..a4781e17a 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitOperatorDeleteExists.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitOperatorDeleteExists.java @@ -23,11 +23,21 @@ static void handleDeleteExists(EmitterVisitor emitterVisitor, OperatorNode node) if (node.operand instanceof ListNode listNode && listNode.elements.size() == 1) { Node operand2 = listNode.elements.getFirst(); if (operand2 instanceof OperatorNode operatorNode && operatorNode.operator.equals("+")) { - // Unwrap the `+` operation + Node saved = operand2; listNode.elements.set(0, operatorNode.operand); + try { + handleDeleteExistsInner(node, emitterVisitor); + } finally { + listNode.elements.set(0, saved); + } + return; } } + handleDeleteExistsInner(node, emitterVisitor); + } + + private static void handleDeleteExistsInner(OperatorNode node, EmitterVisitor emitterVisitor) { String operator = node.operator; if (node.operand instanceof ListNode operand) { if (operand.elements.size() == 1) { @@ -166,8 +176,14 @@ static void handleDefined(OperatorNode node, String operator, if (node.operand instanceof ListNode listNode && listNode.elements.size() == 1) { Node operand2 = listNode.elements.getFirst(); if (operand2 instanceof OperatorNode operatorNode && operatorNode.operator.equals("+")) { - // Unwrap the `+` operation + Node saved = operand2; listNode.elements.set(0, operatorNode.operand); + try { + handleDefined(node, operator, emitterVisitor); + } finally { + listNode.elements.set(0, saved); + } + return; } } diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java b/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java index 9e0f56dba..919750bfd 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java @@ -14,10 +14,8 @@ import org.objectweb.asm.util.Printer; import org.objectweb.asm.util.TraceClassVisitor; import org.perlonjava.frontend.analysis.EmitterVisitor; -import org.perlonjava.backend.jvm.astrefactor.LargeBlockRefactorer; import org.perlonjava.backend.bytecode.BytecodeCompiler; import org.perlonjava.backend.bytecode.InterpretedCode; -import org.perlonjava.frontend.analysis.DepthFirstLiteralRefactorVisitor; import org.perlonjava.frontend.analysis.TempLocalCountVisitor; import org.perlonjava.frontend.astnode.BlockNode; import org.perlonjava.frontend.astnode.Node; @@ -351,38 +349,10 @@ public static Class createClassWithMethod(EmitterContext ctx, Node ast, boole public static byte[] getBytecode(EmitterContext ctx, Node ast, boolean useTryCatch) { boolean asmDebug = System.getenv("JPERL_ASM_DEBUG") != null; - boolean showFallback = System.getenv("JPERL_SHOW_FALLBACK") != null; try { return getBytecodeInternal(ctx, ast, useTryCatch, false); } catch (MethodTooLargeException tooLarge) { - try { - if (showFallback) { - System.err.println("Note: Method too large, retrying with AST splitter (automatic refactoring)."); - } - DepthFirstLiteralRefactorVisitor.refactor(ast); - if (ast instanceof BlockNode blockAst) { - LargeBlockRefactorer.forceRefactorForCodegen(blockAst); - } - if (ctx != null && ctx.javaClassInfo != null) { - String previousName = ctx.javaClassInfo.javaClassName; - ctx.javaClassInfo = new JavaClassInfo(); - ctx.javaClassInfo.javaClassName = previousName; - ctx.clearContextCache(); - } - byte[] result = getBytecodeInternal(ctx, ast, useTryCatch, false); - if (showFallback) { - System.err.println("Note: AST splitter succeeded."); - } - return result; - } catch (MethodTooLargeException retryTooLarge) { - if (showFallback) { - System.err.println("Note: AST splitter failed, propagating exception."); - } - throw retryTooLarge; - } catch (Throwable retryError) { - System.err.println("Warning: Automatic refactoring failed: " + retryError.getMessage()); - } throw tooLarge; } catch (ArrayIndexOutOfBoundsException frameComputeCrash) { // In normal operation we MUST NOT fall back to no-frames output, as that will fail diff --git a/src/main/java/org/perlonjava/frontend/astnode/ArrayLiteralNode.java b/src/main/java/org/perlonjava/frontend/astnode/ArrayLiteralNode.java index c8cf8dc5c..54b6fed57 100644 --- a/src/main/java/org/perlonjava/frontend/astnode/ArrayLiteralNode.java +++ b/src/main/java/org/perlonjava/frontend/astnode/ArrayLiteralNode.java @@ -71,7 +71,7 @@ public ArrayLiteralNode(List elements, int tokenIndex, Parser parser) { * @return a new ListNode containing this array's elements */ public ListNode asListNode() { - return new ListNode(elements, tokenIndex); + return new ListNode(new java.util.ArrayList<>(elements), tokenIndex); } /** diff --git a/src/main/java/org/perlonjava/frontend/astnode/HashLiteralNode.java b/src/main/java/org/perlonjava/frontend/astnode/HashLiteralNode.java index 810425e2e..9f4999f9d 100644 --- a/src/main/java/org/perlonjava/frontend/astnode/HashLiteralNode.java +++ b/src/main/java/org/perlonjava/frontend/astnode/HashLiteralNode.java @@ -72,7 +72,7 @@ public HashLiteralNode(List elements, int tokenIndex, Parser parser) { * @return a new ListNode containing this hash's key-value pairs */ public ListNode asListNode() { - return new ListNode(elements, tokenIndex); + return new ListNode(new java.util.ArrayList<>(elements), tokenIndex); } /** diff --git a/src/main/java/org/perlonjava/runtime/io/DirectoryIO.java b/src/main/java/org/perlonjava/runtime/io/DirectoryIO.java index 7b800359d..7610c1c62 100644 --- a/src/main/java/org/perlonjava/runtime/io/DirectoryIO.java +++ b/src/main/java/org/perlonjava/runtime/io/DirectoryIO.java @@ -1,6 +1,7 @@ package org.perlonjava.runtime.io; import org.perlonjava.runtime.runtimetypes.PerlCompilerException; +import org.perlonjava.runtime.runtimetypes.RuntimeBase; import org.perlonjava.runtime.runtimetypes.RuntimeContextType; import org.perlonjava.runtime.runtimetypes.RuntimeList; import org.perlonjava.runtime.runtimetypes.RuntimeScalar; @@ -113,31 +114,23 @@ public void rewinddir() { * @param ctx the context type, either scalar or list * @return a {@code RuntimeScalar} representing the directory entry or entries */ - public RuntimeScalar readdir(int ctx) { - // Make sure all entries are loaded + public RuntimeBase readdir(int ctx) { loadAllEntries(); if (ctx == RuntimeContextType.SCALAR) { - // Check if we're at a valid position if (currentPosition < 0 || currentPosition >= allEntries.size()) { return scalarUndef; } - - // Get the entry at current position and advance String entry = allEntries.get(currentPosition); currentPosition++; return new RuntimeScalar(entry); - } else { - // List context - return all remaining entries RuntimeList result = new RuntimeList(); - while (currentPosition >= 0 && currentPosition < allEntries.size()) { result.elements.add(new RuntimeScalar(allEntries.get(currentPosition))); currentPosition++; } - - return result.scalar(); + return result; } } } \ No newline at end of file diff --git a/src/main/java/org/perlonjava/runtime/operators/FileTestOperator.java b/src/main/java/org/perlonjava/runtime/operators/FileTestOperator.java index ef534d4ee..6c5077620 100644 --- a/src/main/java/org/perlonjava/runtime/operators/FileTestOperator.java +++ b/src/main/java/org/perlonjava/runtime/operators/FileTestOperator.java @@ -132,21 +132,19 @@ private static boolean statForFileTest(RuntimeScalar arg, Path path, boolean lst ? Files.readAttributes(path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS) : Files.readAttributes(path, BasicFileAttributes.class); - // POSIX attributes are not available on all platforms (e.g. Windows). - // Perl filetest operators like -e/-f/-d only need the basic attributes. PosixFileAttributes posixAttr = null; try { posixAttr = lstat ? Files.readAttributes(path, PosixFileAttributes.class, LinkOption.NOFOLLOW_LINKS) : Files.readAttributes(path, PosixFileAttributes.class); } catch (UnsupportedOperationException | IOException ignored) { - // Leave posixAttr as null. } lastBasicAttr = basicAttr; lastPosixAttr = posixAttr; getGlobalVariable("main::!").set(0); updateLastStat(arg, true, 0, lstat); + Stat.lastNativeStatFields = Stat.nativeStat(path.toString(), !lstat); return true; } catch (NoSuchFileException e) { getGlobalVariable("main::!").set(2); diff --git a/src/main/java/org/perlonjava/runtime/operators/Stat.java b/src/main/java/org/perlonjava/runtime/operators/Stat.java index 30c98faf3..ecc22f44c 100644 --- a/src/main/java/org/perlonjava/runtime/operators/Stat.java +++ b/src/main/java/org/perlonjava/runtime/operators/Stat.java @@ -63,7 +63,7 @@ private interface MsvcrtLib extends Library { MSVCRT = lib; } - private static NativeStatFields nativeStat(String path, boolean followLinks) { + static NativeStatFields nativeStat(String path, boolean followLinks) { try { if (Platform.isWindows()) return nativeStatWindows(path); return nativeStatUnix(path, followLinks); diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java index 62ae70122..5d45ce999 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java @@ -336,12 +336,9 @@ public static RuntimeScalar definedGlobalCodeRefAsScalar(String key) { } } - // Check if it's a user-defined subroutine RuntimeScalar var = globalCodeRefs.get(key); if (var != null && var.type == RuntimeScalarType.CODE && var.value instanceof RuntimeCode runtimeCode) { - // Check if the subroutine has actual implementation (not just a placeholder) - boolean result = (runtimeCode.methodHandle != null || runtimeCode.compilerSupplier != null || runtimeCode.isBuiltin); - return result ? scalarTrue : scalarFalse; + return runtimeCode.defined() ? scalarTrue : scalarFalse; } return scalarFalse; } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java index 89d6833ca..775d7c4d8 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java @@ -47,7 +47,6 @@ public static boolean isGlobAssigned(String globName) { public RuntimeScalar set(RuntimeScalar value) { markGlobAsAssigned(); - // System.out.println("glob set " + this.globName + " to " + value.type); switch (value.type) { case CODE: GlobalVariable.getGlobalCodeRef(this.globName).set(value); diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeList.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeList.java index d2b754a1a..e3c9d0fb5 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeList.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeList.java @@ -375,8 +375,9 @@ public RuntimeList flattenElements() { result.elements.add(array.get(i)); } } else if (element instanceof RuntimeHash hash) { - for (RuntimeScalar val : hash.values()) { - result.elements.add(val); + for (Map.Entry entry : hash.elements.entrySet()) { + result.elements.add(new RuntimeScalar(entry.getKey())); + result.elements.add(entry.getValue()); } } else { result.elements.add(element); diff --git a/src/test/resources/unit/glob.t b/src/test/resources/unit/glob.t index 7e4e44e72..9b10a073b 100644 --- a/src/test/resources/unit/glob.t +++ b/src/test/resources/unit/glob.t @@ -6,6 +6,8 @@ use File::Temp qw(tempdir); use File::Spec; # Create a temporary directory for testing +use Cwd qw(getcwd); +my $orig_cwd = getcwd(); my $tmpdir = tempdir(CLEANUP => 1); chdir $tmpdir or die "Cannot chdir to $tmpdir: $!"; @@ -241,4 +243,5 @@ subtest 'glob state per source location' => sub { is($count, 1, 'While loop with glob exhausts the iterator (found Readme.md)'); }; +chdir $orig_cwd; done_testing();