diff --git a/.agents/skills/create-release/SKILL.md b/.agents/skills/create-release/SKILL.md new file mode 100644 index 0000000000..5adac1f487 --- /dev/null +++ b/.agents/skills/create-release/SKILL.md @@ -0,0 +1,72 @@ +--- +name: create-release +description: Create a PerlOnJava release, including the project-wide version bump, changelog promotion, validation, release PR, exact merged-commit tag, and GitHub release. Use for PerlOnJava version bumps, release preparation, release tags, or GitHub release publication. +--- + +# Create a PerlOnJava release + +Follow `AGENTS.md`, especially its dirty-tree preflight, testing, branch, commit-attribution, and no-direct-push-to-master rules. + +## Prepare + +1. Fetch `origin` and create a clean `release/` branch from current `origin/master` in a separate worktree. +2. Confirm the tag and GitHub release do not already exist. +3. Inspect the previous tag and GitHub release for naming, notes, and tag style. +4. Record the starting version from `src/main/java/org/perlonjava/core/Configuration.java.in`. + +## Update the version + +Run from the repository root: + +```bash +perl Configure.pl -D version= +``` + +Review every changed file. Search the entire tracked tree for both the old and new versions, including regex-escaped forms such as `5\\.44\\.0`. Update current product-version references, generated artifact names, launchers, packaging checks, examples, tests, and active documentation. Preserve references that are explicitly historical, such as prior changelog entries, upstream Perl history/delta documentation, and design discussions about older releases. + +Do not commit the generated, ignored `Configuration.java`. + +## Promote the changelog + +In `docs/about/changelog.md`: + +1. Leave a new, empty `## Work in progress` section at the top. +2. Promote the previous work-in-progress content to `## v: `. +3. Consolidate implementation history into short user-facing bullets. Keep important features, compatibility improvements, performance changes, and bug fixes; omit PR chronology, internal evidence mechanics, and superseded intermediate details. + +Use the promoted changelog section as the source for GitHub release notes. + +## Validate and integrate + +1. Validate this skill when it changed: + + ```bash + python3 -m venv /tmp/perlonjava-release-skill-validator + /tmp/perlonjava-release-skill-validator/bin/pip install PyYAML + /tmp/perlonjava-release-skill-validator/bin/python /Users/fglock/.codex/skills/.system/skill-creator/scripts/quick_validate.py .agents/skills/create-release + ``` + + Reuse an existing validation virtual environment when available. Do not + install PyYAML into an externally managed system Python. + +2. Immediately before final validation, fetch `origin` and rebase the release + branch onto the latest `origin/master` so concurrent fixes are included. + Preserve and verify the release commits after the rebase. If `master` + advances again before publication, repeat the integration and required + validation rather than tagging a stale candidate. +3. Run `make`, capture its complete output, and stop if it fails. +4. Run `make test-bundled-modules`, capture its complete output, and require every bundled-module test to pass. +5. Audit the main claims in the promoted changelog and draft release notes. Map every headline feature and compatibility metric to recent evidence or a focused test; rerun representative tests for the release's primary advertised features. Stop when a main claim is stale, unverified, or failing. +6. Commit with the required AI attribution, push the release branch, and open a PR using `--body-file`. +7. Monitor all required CI checks. Merge only after local validation and CI pass. + +## Tag and publish + +1. Fetch `origin/master` after the release PR merges. +2. Verify the release changes are present and identify the exact merged `origin/master` commit. +3. Create `v` using the same annotated/lightweight convention as the preceding release, targeting that exact commit. Verify the local tag target before pushing it. +4. Push only the release tag, then verify the remote tag resolves to the intended commit. +5. Create the GitHub release from a notes file, matching the previous release's title and concise Markdown style. Release notes are plain documentation and must not include AI attribution. Mark it latest unless this is explicitly a prerelease. +6. Verify the published release URL, title, tag, release status, and target commit. + +Stop rather than overwrite an existing tag/release, publish from an unmerged branch, tag an unexpected commit, or continue after a failed required check. diff --git a/AGENTS.md b/AGENTS.md index fc864296dc..85afa67a76 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -190,31 +190,6 @@ ╚══════════════════════════════════════════════════════════════════════════════╝ ``` -## Incident Log (do not delete — this is why the rules above exist) - -| Date | What was lost | Root cause | -|------------|------------------------------------------------|---------------------------------------------------| -| 2026-04-28 | ~600 cpan-tester module results (4736 → 4139) | Agent ran `git checkout dev/cpan-reports/` on an unstaged refresh; concurrent `cpan_random_tester.pl` instances also race on `.dat` files (separate bug). | -| 2026-04-29 | cpan-reports refresh commit (briefly, on a feature branch — recovered from reflog) | Agent resolved a rebase conflict with `git checkout --ours` thinking it would keep the branch's version. During rebase, `--ours` means UPSTREAM, so the upstream files were taken, the replayed commit became empty, and rebase silently dropped it. Recovery: `git reset --hard ` from `git reflog`, then re-rebase using `--theirs`. | -| 2026-04-30 | (no work lost — recovered) Working tree on `fix/class-trait-tests` was overwritten with master content | Agent ran `git checkout master -- .` to A/B test failures vs master without first snapshotting and without switching branches. Recovery only worked because the changes had already been committed to HEAD: `git restore .` (also a forbidden command on a dirty tree, but safe here because "dirty" was master content, not user work) brought the tree back from HEAD. Correct workflow would have been: stash via `git diff > /tmp/wip.patch`, or use `git worktree add` for the master comparison instead of mutating the current tree. | -| 2026-04-30 | A full afternoon chasing a phantom "DBIx::Class regression" in `t/76joins.t` / `t/96_is_deteministic_value.t` | Investigative agent launched the test repeatedly under `/usr/bin/time -p ./jperl …` (no `timeout` wrapper). Each hung JVM survived past the agent's lifetime, accumulated as ~14 orphans at 100% CPU each, and starved the active `jcpan` harness — which then SIGKILLed innocent tests after 300 s of no TAP output. Symptom looked exactly like a real perf regression. Fix: always `timeout N ./jperl …` for any potentially-hanging run. | -| 2026-08-06 | (no source work lost — build recovered) A process cleanup killed the active Gradle test workers, producing exit 137 failures in two shards. | Agent selected Java PIDs from a broad CPU list without first constraining them to stale processes. Recovery: rerun `make` without killing workers; subsequent build completed successfully. Fix: never kill by CPU list alone; identify the exact command and build ownership first. | -| 2026-08-17 | (no source work lost — stale workers removed) Failed `make` runs were interrupted after their known Joni failures, but their Gradle unit-shard workers survived and competed with later builds. | Agent sent Ctrl-C to the parent build session before all parallel workers had exited. Recovery: identified stale workers by PID, start time, and shard work directory, terminated only those exact PIDs, and left the current build and sibling repositories untouched. Fix: let failed parallel `make` runs finish naturally, or verify and clean up their exact child PIDs before starting another build. | -| 2026-08-17 | (no source work lost — CPAN run rerun) A concurrent `make` replaced the development shadow JAR while an active `jcpan` process was spawning a child JVM, causing a transient `ClassNotFoundException`. | Agent waited for another worktree's build but did not wait for the same worktree's bounded CPAN runs before rebuilding `target/perlonjava-5.44.0.jar`. Recovery: let `make` finish and rerun the affected CPAN target. Fix: never rebuild a worktree's development JAR while that worktree has active `jperl` or `jcpan` processes. | -| 2026-08-18 | (no source work lost — wrong local WIP ref recovered) A test-snapshot cherry-pick landed in the original checkout instead of its newly created continuation worktree. | Agent chained `git worktree add` and `git cherry-pick` while the shell remained in the original working directory. Recovery: preserved the mistaken commit on a recovery branch, restored the original WIP ref to its exact prior commit without reset, then cherry-picked in the intended worktree. Fix: run post-creation Git commands with the new worktree as the explicit working directory and verify `git branch --show-current` before committing. | -| 2026-08-20 | (no source work lost — green build evidence discarded) A coordinator cherry-picked an integrated worker commit into a checkout while `make` was still validating an earlier supposedly immutable commit. | Integration and validation shared one worktree, and the coordinator continued integration before the build session drained. Recovery: let the exact build processes finish untouched, mark the result invalid regardless of exit status, and rerun from an immutable barrier. Fix: never mutate, cherry-pick, rebase, or regenerate a checkout with an active build/test gate; integrate in a separate worktree or wait for the gate to finish. | -| 2026-08-22 | (no source work lost — focused tests rerun) Two A165 focused JVMs opened an incomplete shadow JAR and exited immediately with `ClassNotFoundException`. | Agent treated Gradle's `> Task :shadowJar` console line as task completion and started `jperl` while the same worktree's build was still writing the JAR. Recovery: waited for the build process itself to exit, verified the stable JAR hash, and reran the bounded tests. Fix: a task-start line is not a completion fence; never launch `jperl` until the owning build process has exited successfully. | -| 2026-08-22 | (no source work lost — focused result discarded) An agent edited a direct-Joni test while its focused `make test-joni` gate was active. | The running build compiled a mutable source identity, so its failure could not distinguish the old test from the corrected test. Recovery: discarded the result and reran from a frozen diff. Fix: do not edit any file in a worktree from build launch until that gate drains, even when implementation can otherwise continue in parallel. | -| 2026-08-22 | (no source work lost — four valid builds drained naturally) Two workers each launched what appeared to be the third permitted regex implementation build. | The first worker released the atomic launch mutex when its `timeout` wrapper was visible but before the owned `make` executable appeared; the second worker could not yet count it and launched concurrently. Recovery: preserved both valid runs, launched no fifth job, and let them drain. Fix: under the mutex, count accepted launch intents and active owner roots, then release only after the exact payload executable is visible with its intended cwd; a shell or timeout ancestor is not a visibility fence. | -| 2026-08-22 | (no source work lost — String::Random evidence discarded) Two CPAN gates used a JAR in the integration checkout's `target/` directory while the coordinator rebuilt that same path. | Workers treated a matching embedded source SHA and an initial file hash as immutable identity, but did not copy the artifact out of the shared build tree. Recovery: stopped only the exact affected process, retained its log as invalid evidence, and reran from a private copy after the build drained. Fix: acceptance gates must use hashed task-owned copies of JARs and launchers; any overlap with a writer invalidates the result. | -| 2026-08-24 | (no source work lost — focused probes rerun) A Text::CSV `jperl` probe twice observed a transient missing `Main.class` while another agent's focused bundled-module build replaced the same worktree's development JAR. | The coordinator allowed readers and writers of the shared development JAR to run concurrently despite the existing 2026-08-17 warning. Recovery: stopped new launches, let the active Net::SSLeay build finish naturally, verified no build or `jperl` process remained, and reopened a single-writer/readers-after-build fence. Fix: coordinate one explicit shared-JAR build fence per worktree; no `jperl`/`jcpan` reader may start while a build can replace the JAR, and no build may start until all readers finish. | -| 2026-08-24 | (no source work lost — core regex evidence discarded and rerun) A core `pat.t`/`pat_thr.t`/`anyof.t` reader was launched beside `make test-bundled-modules` on the same candidate JAR. | The coordinator incorrectly classified the bundled-module target as read-only, but it runs `shadowJar` before its module tests and can replace the development JAR. Recovery: let both bounded processes drain naturally, retained the bundled result, discarded the overlapped core result, and reran the core files after the writer exited. Fix: classify every Make target by its full dependency graph; `make test-bundled-modules` is a shared-JAR writer and must never overlap `jperl` or `jcpan` readers of that worktree. | - -When you cause a new incident, append a row here in the same commit -that fixes it. Future agents need to see that these warnings are real. - ---- - ## Project Rules ### Progress Tracking for Multi-Phase Work diff --git a/Configure.pl b/Configure.pl index 63f22b8c70..b7cc77b79b 100755 --- a/Configure.pl +++ b/Configure.pl @@ -16,7 +16,7 @@ # USAGE: # # ./Configure.pl # Show current configuration -# ./Configure.pl -D version=5.44.0 # Update version everywhere +# ./Configure.pl -D version=5.44.1 # Update version everywhere # ./Configure.pl --upgrade # Upgrade dependencies to latest versions # # VERSION UPDATE BEHAVIOR: @@ -88,7 +88,7 @@ sub show_help { -D key=value Set configuration value Supported configuration keys: - version - PerlOnJava version (e.g., 5.44.0) + version - PerlOnJava version (e.g., 5.44.1) Updates Configuration.java.in, build files, and all JAR references Read-only keys (managed by build system): @@ -103,7 +103,7 @@ sub show_help { Examples: ./Configure.pl # Show current configuration - ./Configure.pl -D version=5.44.0 # Update version everywhere + ./Configure.pl -D version=5.44.1 # Update version everywhere ./Configure.pl --search org.h2.Driver # Search for JDBC driver ./Configure.pl --direct com.h2database:h2:2.2.224 ./Configure.pl --upgrade # Upgrade all dependencies @@ -285,8 +285,8 @@ sub update_version_everywhere { # Update version in README.md (feature support line) if ($file =~ /README\.md$/) { - (my $old_feature_version = $old_version) =~ s/\.0$//; - (my $new_feature_version = $new_version) =~ s/\.0$//; + my ($old_feature_version) = $old_version =~ /^(\d+\.\d+)/; + my ($new_feature_version) = $new_version =~ /^(\d+\.\d+)/; if ($file_content =~ s/(Perl )\Q$old_feature_version\E( language compatibility)/$1$new_feature_version$2/g) { $updated = 1; print "Updated version in $file\n"; diff --git a/Dockerfile b/Dockerfile index 7f1c8f5a97..6726ac68d2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,7 +24,7 @@ FROM eclipse-temurin:24-jdk WORKDIR /app # Copy the built JAR file from the Maven container -COPY --from=build /app/target/perlonjava-5.44.0.jar /app/perlonjava-5.44.0.jar +COPY --from=build /app/target/perlonjava-5.44.1.jar /app/perlonjava-5.44.1.jar # Copy the wrapper scripts COPY --from=build /app/jperl /app/jperl diff --git a/build.gradle b/build.gradle index 65a96d405a..36623e44ea 100644 --- a/build.gradle +++ b/build.gradle @@ -69,7 +69,7 @@ tasks.buildDeb { // Project metadata group = 'org.perlonjava' -version = '5.44.0' +version = '5.44.1' // CycloneDX SBOM generation configuration cyclonedxBom { diff --git a/dev/design/bytecode_debugging.md b/dev/design/bytecode_debugging.md index 220114db3c..2cd8090024 100644 --- a/dev/design/bytecode_debugging.md +++ b/dev/design/bytecode_debugging.md @@ -139,5 +139,5 @@ Fix strategy: ## Notes -- `jperl` runs `target/perlonjava-5.44.0.jar`. Rebuild after changes, otherwise you may be debugging stale code. +- `jperl` runs `target/perlonjava-5.44.1.jar`. Rebuild after changes, otherwise you may be debugging stale code. - `JPERL_ASM_DEBUG_CLASS` is useful to avoid massive logs during large tests. diff --git a/dev/design/debugger.md b/dev/design/debugger.md index 82759c3ff3..cb16b6f5a0 100644 --- a/dev/design/debugger.md +++ b/dev/design/debugger.md @@ -28,7 +28,7 @@ This client would use JPDA to communicate with the JVM, but would present the de ## Run PerlOnJava with Debug Flags ```bash -java -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005 -jar target/perlonjava-5.44.0.jar myscript.pl +java -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005 -jar target/perlonjava-5.44.1.jar myscript.pl ``` ## Start Debugging diff --git a/dev/design/getting_started.md b/dev/design/getting_started.md index 73aae3a7cf..f79e912389 100644 --- a/dev/design/getting_started.md +++ b/dev/design/getting_started.md @@ -14,12 +14,12 @@ This guide helps you start using PerlOnJava to run Perl code on the Java Virtual 1. Download the JAR file: ```bash -java -jar target/perlonjava-5.44.0.jar +java -jar target/perlonjava-5.44.1.jar ``` 2. Run your first Perl script: ```bash -java -jar target/perlonjava-5.44.0.jar -E 'print "Hello from Perl on JVM!\n"' +java -jar target/perlonjava-5.44.1.jar -E 'print "Hello from Perl on JVM!\n"' ``` ## Basic Usage Examples @@ -28,12 +28,12 @@ java -jar target/perlonjava-5.44.0.jar -E 'print "Hello from Perl on JVM!\n"' Run a Perl file: ```bash -java -jar target/perlonjava-5.44.0.jar script.pl +java -jar target/perlonjava-5.44.1.jar script.pl ``` Run Perl code directly: ```bash -java -jar target/perlonjava-5.44.0.jar -E 'for (1..3) { print "$_\n" }' +java -jar target/perlonjava-5.44.1.jar -E 'for (1..3) { print "$_\n" }' ``` ### 2. Using Modules @@ -96,12 +96,12 @@ Common switches: Enable debugging output: ```bash -java -jar target/perlonjava-5.44.0.jar --debug script.pl +java -jar target/perlonjava-5.44.1.jar --debug script.pl ``` View generated bytecode: ```bash -java -jar target/perlonjava-5.44.0.jar --disassemble script.pl +java -jar target/perlonjava-5.44.1.jar --disassemble script.pl ``` ## Next Steps diff --git a/dev/design/graalvm.md b/dev/design/graalvm.md index 7850e9e3cb..01b9c69e8b 100644 --- a/dev/design/graalvm.md +++ b/dev/design/graalvm.md @@ -42,7 +42,7 @@ Added GraalVM support to pom.xml in a dedicated profile: Used GraalVM tracing agent to capture required runtime methods: ```bash -java -agentlib:native-image-agent=config-output-dir=src/main/resources/META-INF/native-image -jar target/perlonjava-5.44.0.jar examples/life.pl +java -agentlib:native-image-agent=config-output-dir=src/main/resources/META-INF/native-image -jar target/perlonjava-5.44.1.jar examples/life.pl ``` ## Results diff --git a/dev/design/maven-central-publishing.md b/dev/design/maven-central-publishing.md index 2e59b1b98c..0d368a6242 100644 --- a/dev/design/maven-central-publishing.md +++ b/dev/design/maven-central-publishing.md @@ -54,7 +54,7 @@ a polished automated release including review and first-Portal validation. | Maven build description | `pom.xml` exists and can build the project independently | | Group ID | `org.perlonjava` in Gradle and Maven | | Artifact ID | `perlonjava` | -| Version | `5.44.0` | +| Version | `5.44.1` | | Runtime Java version | Java 24+ | | Standalone artifact | Shaded executable JAR; currently replaces the unclassified main JAR | | POM project metadata | Incomplete; URL is still the Maven example URL | @@ -150,14 +150,14 @@ temporary coordinate with the intention of moving later. ### 3. Versioning The current project version mirrors the supported Perl language version. Before -publishing `5.44.0`, decide how subsequent PerlOnJava-only fixes are numbered. +publishing `5.44.1`, decide how subsequent PerlOnJava-only fixes are numbered. The scheme must allow multiple runtime releases against the same Perl version without overwriting an immutable Central coordinate. Recommended candidates: - SemVer with Perl compatibility documented separately, for example `1.0.0`; -- a fourth numeric component such as `5.44.0.1`; or +- a fourth numeric component such as `5.44.1.1`; or - a SemVer-compatible qualifier whose ordering is documented. The Git tag, Gradle version, POM version, generated runtime version, and GitHub @@ -388,7 +388,7 @@ After Portal validation and publication: - Is `perlonjava.org` controlled and available for Central DNS verification? - Should the standalone distribution use an `all` classifier or a separate `perlonjava-cli` artifact ID? -- What version follows `5.44.0` when runtime fixes ship without a Perl language +- What version follows `5.44.1` when runtime fixes ship without a Perl language version change? - Which Java packages constitute the supported public embedding API? - Who owns the long-term GPG key and Central Portal account? diff --git a/dev/design/regex-implementation.md b/dev/design/regex-implementation.md index d2b692ef10..487c09672e 100644 --- a/dev/design/regex-implementation.md +++ b/dev/design/regex-implementation.md @@ -198,8 +198,7 @@ Preserve the candidate SHA and complete logs. ## Progress Tracking -### Current Status: implementation and local validation complete; CI and UAT -active on PR 1095 +### Current Status: PR 1101 UAT regressions fixed; CI and UAT rerun pending ### Completed @@ -242,6 +241,7 @@ active on PR 1095 corpus omits `op/hash-rt85026.t`, an inherited 0/0 row in PRs 1091 and 1093. - [x] Consolidated PR 1095 published; reviewed disposition comments posted and legacy PRs 1089, 1065, 1062, and 1061 closed. +- [x] PR 1095 passed CI and UAT and was merged (2026-08-24). ### Remaining @@ -258,9 +258,35 @@ active on PR 1095 three documented non-semantic corpus/environment exclusions above. - [x] Warmed performance and bounded stress pass on both backends. -- [ ] Ubuntu/Windows CI passes; local packaging and provenance checks already +- [x] Ubuntu/Windows CI passes; local packaging and provenance checks already pass. -- [ ] Final UAT passes and the implementation is merged. +- [x] Final UAT passes and the implementation is merged. +- [x] Before the final release, the release branch was rebased onto + `origin/master` at `c7b1a560d`, including the user's parallel CPAN fixes and + report refresh; post-rebase `make` and `make test-bundled-modules` passed. +- [x] Release acceptance regressions have permanent system-Perl-validated + coverage where reducible. The focused Catalyst gate passes 568/568, the + upload lifecycle gate passes 105/105 at baseline speed, and scalar-context + `sort` plus the JAPH example pass on both backends. +- [x] PR 1101 UAT follow-up restored `op/sort.t` from 180/206 to the 188/206 + reference count and `op/for.t` from 137/149 to 141/149. Permanent reducers + pass on system Perl and both PerlOnJava backends; `make` and + `make test-bundled-modules` pass after the fixes. +- [x] The `gh7094-speed-up-keys-on-empty-hash.t` 5/6 result was classified as + benchmark variance: the hash implementation is unchanged, historical runs + fluctuate on multiple timing assertions, and the same candidate passed 6/6 + in two immediate bounded reruns. +- [x] The follow-up `op/ref.t` regression is covered by permanent tests for + read-only numeric, string, and canonical-undef foreach references. The fix + preserves reference identity while guarding interpreter dereference + assignment; `op/ref.t` is restored to 423/481 and `op/for.t` remains 141/149. +- [ ] Push the PR 1101 UAT fixes and require Ubuntu and Windows CI to pass. +- [ ] While CI runs, execute the complete local Perl suite with 10 jobs and a + 300-second per-file timeout, then compare its JSON-backed log to PR 1093 and + treat every negative delta as release-blocking. +- [ ] Rerun release UAT on the resulting unchanged candidate SHA. +- [ ] After approval, rebase the latest `master` if needed and complete the + 5.44.1 release workflow. ## Related Documents and Skills diff --git a/dev/design/sbom.md b/dev/design/sbom.md index 81f9d8d270..dd661f42a1 100644 --- a/dev/design/sbom.md +++ b/dev/design/sbom.md @@ -203,7 +203,7 @@ my $bom = SBOM::CycloneDX->new(spec_version => '1.6'); my $root = SBOM::CycloneDX::Component->new( type => COMPONENT_TYPE_APPLICATION, name => 'perlonjava-perl-modules', - version => $ENV{VERSION} // '5.44.0', + version => $ENV{VERSION} // '5.44.1', licenses => [SBOM::CycloneDX::License->new(id => 'Artistic-2.0')], bom_ref => 'perlonjava-perl' ); @@ -302,7 +302,7 @@ During build, SBOMs are generated to: SBOMs can be embedded inside the JAR for easy discovery: ``` -perlonjava-5.44.0.jar +perlonjava-5.44.1.jar ├── META-INF/ │ ├── MANIFEST.MF │ └── sbom/ @@ -335,7 +335,7 @@ For Debian packages, SBOMs go in the standard documentation directory: ├── bin/ │ └── jperl ├── lib/ -│ └── perlonjava-5.44.0.jar +│ └── perlonjava-5.44.1.jar └── share/ └── sbom/ ├── bom.json @@ -370,11 +370,11 @@ ospackage { SBOMs should also be attached as separate release artifacts: ``` -Release v5.44.0 -├── perlonjava-5.44.0.jar -├── perlonjava_5.44.0_amd64.deb -├── perlonjava-5.44.0-sbom.json # Standalone SBOM -└── perlonjava-5.44.0-sbom.xml # Standalone SBOM (XML) +Release v5.44.1 +├── perlonjava-5.44.1.jar +├── perlonjava_5.44.1_amd64.deb +├── perlonjava-5.44.1-sbom.json # Standalone SBOM +└── perlonjava-5.44.1-sbom.xml # Standalone SBOM (XML) ``` This allows consumers to inspect the SBOM without downloading/extracting the full package. @@ -542,7 +542,7 @@ The generated SBOM must include: PerlOnJava ships as a **shaded/uber JAR** containing everything: ``` -perlonjava-5.44.0.jar +perlonjava-5.44.1.jar ├── org/perlonjava/... (PerlOnJava Java classes) ├── org/ow2/asm/... (shaded ASM library) ├── com/ibm/icu/... (shaded ICU4J library) @@ -590,17 +590,17 @@ The Perl SBOM should include: The final distribution artifacts should have accompanying hash files: ``` -Release v5.44.0/ -├── perlonjava-5.44.0.jar -├── perlonjava-5.44.0.jar.sha256 # echo "abc123... perlonjava-5.44.0.jar" -├── perlonjava_5.44.0_amd64.deb -├── perlonjava_5.44.0_amd64.deb.sha256 -└── perlonjava-5.44.0-sbom.json # SBOM (contains component hashes) +Release v5.44.1/ +├── perlonjava-5.44.1.jar +├── perlonjava-5.44.1.jar.sha256 # echo "abc123... perlonjava-5.44.1.jar" +├── perlonjava_5.44.1_amd64.deb +├── perlonjava_5.44.1_amd64.deb.sha256 +└── perlonjava-5.44.1-sbom.json # SBOM (contains component hashes) ``` Generate with: ```bash -sha256sum perlonjava-5.44.0.jar > perlonjava-5.44.0.jar.sha256 +sha256sum perlonjava-5.44.1.jar > perlonjava-5.44.1.jar.sha256 ``` ### Supported Hash Algorithms diff --git a/dev/design/scriptingapi.md b/dev/design/scriptingapi.md index 974c5effc4..a14ac67173 100644 --- a/dev/design/scriptingapi.md +++ b/dev/design/scriptingapi.md @@ -7,7 +7,7 @@ - Note that `jrunscript` creates a new scope every time, so it doesn't keep lexical variables from one line to the next. ```sh - $ jrunscript -cp target/perlonjava-5.44.0.jar -l perl + $ jrunscript -cp target/perlonjava-5.44.1.jar -l perl Perl5> my $sub = sub { say $_[0] }; $sub->($_) for 4,5,6; 4 5 diff --git a/dev/design/windows_installer.md b/dev/design/windows_installer.md index f83cf08123..0c19758c8d 100644 --- a/dev/design/windows_installer.md +++ b/dev/design/windows_installer.md @@ -84,7 +84,7 @@ C:\Program Files\PerlOnJava\ ├── bin\ │ └── jperl.exe ├── lib\ -│ └── perlonjava-5.44.0.jar +│ └── perlonjava-5.44.1.jar └── runtime\ └── [JRE files] ``` \ No newline at end of file diff --git a/dev/modules/dynamic_loading.md b/dev/modules/dynamic_loading.md index 4b880e6a5c..c325c2f114 100644 --- a/dev/modules/dynamic_loading.md +++ b/dev/modules/dynamic_loading.md @@ -325,7 +325,7 @@ The build creates multiple JAR files in target/: ``` target/ - perlonjava-5.44.0.jar # Core runtime + perlonjava-5.44.1.jar # Core runtime perlonjava-module-dbi-3.0.0.jar # DBI module ``` diff --git a/dev/modules/gtk2.md b/dev/modules/gtk2.md index cbeab6adfa..abedb7c6b6 100644 --- a/dev/modules/gtk2.md +++ b/dev/modules/gtk2.md @@ -392,7 +392,7 @@ set FX_LIBS=%SCRIPT_DIR%lib\javafx java %JVM_OPTS% %JPERL_OPTS% ^ --module-path "%FX_LIBS%" ^ --add-modules javafx.controls,javafx.graphics,javafx.base ^ - -cp "%SCRIPT_DIR%target\perlonjava-5.44.0.jar" ^ + -cp "%SCRIPT_DIR%target\perlonjava-5.44.1.jar" ^ org.perlonjava.app.cli.Main %* ``` diff --git a/dev/presentations/German_Perl_Raku_Workshop_2026/slide-deck-plan.md b/dev/presentations/German_Perl_Raku_Workshop_2026/slide-deck-plan.md index 56141ca244..8a2c2a11a9 100644 --- a/dev/presentations/German_Perl_Raku_Workshop_2026/slide-deck-plan.md +++ b/dev/presentations/German_Perl_Raku_Workshop_2026/slide-deck-plan.md @@ -63,7 +63,7 @@ - Interactive debugger (`-d`) **Slide 6 — One JAR, Everything Included** -- `perlonjava-5.44.0.jar` — 25 MB, zero external dependencies +- `perlonjava-5.44.1.jar` — 25 MB, zero external dependencies - Diagram: 392 compiled classes + 341 Perl modules + bundled Java libs - `java -jar perlonjava.jar script.pl` — that's it - _Establishes simplicity before going deeper_ diff --git a/dev/presentations/German_Perl_Raku_Workshop_2026/slides-part1-intro.md b/dev/presentations/German_Perl_Raku_Workshop_2026/slides-part1-intro.md index e23fe1a2ad..99d5a2cefc 100644 --- a/dev/presentations/German_Perl_Raku_Workshop_2026/slides-part1-intro.md +++ b/dev/presentations/German_Perl_Raku_Workshop_2026/slides-part1-intro.md @@ -68,7 +68,7 @@ JSR-223 is the standard Java scripting API, available since Java 6. It allows bi ## One JAR, Everything Included -**`perlonjava-5.44.0.jar`** — 25 MB, zero external dependencies +**`perlonjava-5.44.1.jar`** — 25 MB, zero external dependencies ```text perlonjava.jar diff --git a/dev/presentations/German_Perl_Raku_Workshop_2026/slides.md b/dev/presentations/German_Perl_Raku_Workshop_2026/slides.md index add49f7dc4..a33a54e7fb 100644 --- a/dev/presentations/German_Perl_Raku_Workshop_2026/slides.md +++ b/dev/presentations/German_Perl_Raku_Workshop_2026/slides.md @@ -69,7 +69,7 @@ JSR-223 is the standard Java scripting API, available since Java 6. Bidirectiona ## One JAR, Everything Included -**`perlonjava-5.44.0.jar`** — 25 MB, zero external dependencies +**`perlonjava-5.44.1.jar`** — 25 MB, zero external dependencies ```text perlonjava.jar diff --git a/dev/regex/tools/run_package_evidence.pl b/dev/regex/tools/run_package_evidence.pl index 3f78c186f3..0e470e8dfe 100755 --- a/dev/regex/tools/run_package_evidence.pl +++ b/dev/regex/tools/run_package_evidence.pl @@ -186,9 +186,9 @@ my $install = safe_existing_directory($install_parent, 'perlonjava'); my $target = safe_existing_directory($source, 'target'); my @target_jars = grep { /\.jar\z/i } directory_entries($target); -die "Expected exact standalone JAR perlonjava-5.44.0.jar and no other JARs\n" - unless @target_jars == 1 && $target_jars[0] eq 'perlonjava-5.44.0.jar'; -my $jar = safe_existing_file($target, 'perlonjava-5.44.0.jar'); +die "Expected exact standalone JAR perlonjava-5.44.1.jar and no other JARs\n" + unless @target_jars == 1 && $target_jars[0] eq 'perlonjava-5.44.1.jar'; +my $jar = safe_existing_file($target, 'perlonjava-5.44.1.jar'); my $reports = safe_existing_directory($source, 'build', 'reports'); my $java_bom_path = File::Spec->catfile($reports, 'bom.json'); my $perl_bom_path = File::Spec->catfile($reports, 'perl-bom.json'); @@ -939,8 +939,8 @@ sub parse_package_contract { die "Production package configuration has unsupported architecture metadata\n" unless @architectures == 0 || (@architectures == 1 && $architectures[0] eq 'NOARCH'); - die "Production package name/version must be exactly perlonjava/5.44.0\n" - unless $packages[0] eq 'perlonjava' && $versions[0] eq '5.44.0'; + die "Production package name/version must be exactly perlonjava/5.44.1\n" + unless $packages[0] eq 'perlonjava' && $versions[0] eq '5.44.1'; return { package => $packages[0], version => $versions[0], architecture => 'all', maintainer => $maintainers[0] }; } @@ -1196,7 +1196,7 @@ sub publish_evidence_bundle { mkdir $stage_logs, 0700 or die "Cannot create staged evidence log directory: $!\n"; my (%stage_files, %descriptors); my %names = ( - jar => 'perlonjava-5.44.0.jar', java_bom => 'bom.json', + jar => 'perlonjava-5.44.1.jar', java_bom => 'bom.json', perl_bom => 'perl-bom.json', sbom => 'sbom.json', deb => basename($sources->{deb}), notice_license => 'notice-license.json', ); diff --git a/dev/regex/tools/tests/b23_archive_sbom_boundary_hardening.t b/dev/regex/tools/tests/b23_archive_sbom_boundary_hardening.t index 877de200f8..5f2e5a9260 100644 --- a/dev/regex/tools/tests/b23_archive_sbom_boundary_hardening.t +++ b/dev/regex/tools/tests/b23_archive_sbom_boundary_hardening.t @@ -132,8 +132,8 @@ sub sbom_fixture { version => 1, metadata => { component => { type => 'application', 'bom-ref' => 'perlonjava', - name => 'perlonjava', version => '5.44.0', - purl => 'pkg:generic/perlonjava@5.44.0', + name => 'perlonjava', version => '5.44.1', + purl => 'pkg:generic/perlonjava@5.44.1', licenses => [{ license => { id => 'Artistic-2.0' } }], } }, components => [ diff --git a/dev/regex/tools/tests/joni_distribution_launcher_boundaries.t b/dev/regex/tools/tests/joni_distribution_launcher_boundaries.t index 4097df61d8..8419feef74 100644 --- a/dev/regex/tools/tests/joni_distribution_launcher_boundaries.t +++ b/dev/regex/tools/tests/joni_distribution_launcher_boundaries.t @@ -12,7 +12,7 @@ use Test::More; my $root = File::Spec->rel2abs(File::Spec->catdir($FindBin::Bin, '..', '..', '..', '..')); my $tool = File::Spec->catfile($root, 'dev', 'regex', 'tools', 'verify-joni-distribution.pl'); my $temporary = tempdir(CLEANUP => 1); -my $jar_name = 'perlonjava-5.44.0.jar'; +my $jar_name = 'perlonjava-5.44.1.jar'; subtest 'generated Unix and Windows launcher forms are accepted' => sub { my $distribution = fixture('valid'); diff --git a/dev/regex/tools/tests/joni_distribution_relocation.t b/dev/regex/tools/tests/joni_distribution_relocation.t index 42fa3a1065..f34f347ef8 100644 --- a/dev/regex/tools/tests/joni_distribution_relocation.t +++ b/dev/regex/tools/tests/joni_distribution_relocation.t @@ -47,10 +47,10 @@ subtest 'installed notices are fail-closed and byte-exact' => sub { subtest 'launch scripts cannot restore a thin dependency classpath' => sub { rejected(fixture('extra-launch-jar', extra_launch_jar => 1), - qr/launcher CLASSPATH must select only perlonjava-5\.44\.0\.jar/, + qr/launcher CLASSPATH must select only perlonjava-5\.44\.1\.jar/, 'additional launcher classpath entry'); rejected(fixture('wrong-launch-jar', wrong_launch_jar => 1), - qr/launcher CLASSPATH must select only perlonjava-5\.44\.0\.jar/, + qr/launcher CLASSPATH must select only perlonjava-5\.44\.1\.jar/, 'launcher missing standalone artifact'); }; @@ -76,7 +76,7 @@ sub fixture { my $licenses = File::Spec->catdir($distribution, 'share', 'licenses'); make_path($lib, $bin, $licenses); - my $jar_name = 'perlonjava-5.44.0.jar'; + my $jar_name = 'perlonjava-5.44.1.jar'; unless ($option{missing_jar}) { my $tree = File::Spec->catdir($temporary, "$name-jar"); make_path( diff --git a/dev/regex/tools/tests/joni_fork_sbom_embedded_contract.t b/dev/regex/tools/tests/joni_fork_sbom_embedded_contract.t index 2eec480957..42b1c193aa 100644 --- a/dev/regex/tools/tests/joni_fork_sbom_embedded_contract.t +++ b/dev/regex/tools/tests/joni_fork_sbom_embedded_contract.t @@ -148,7 +148,7 @@ done_testing; sub merged_sbom { my $java = { - metadata => { component => { version => '5.44.0' } }, + metadata => { component => { version => '5.44.1' } }, components => [ { type => 'library', group => 'org.jruby.jcodings', diff --git a/dev/regex/tools/tests/run_package_evidence.t b/dev/regex/tools/tests/run_package_evidence.t index 5626663e6b..b664f9f274 100644 --- a/dev/regex/tools/tests/run_package_evidence.t +++ b/dev/regex/tools/tests/run_package_evidence.t @@ -49,7 +49,7 @@ subtest 'happy path publishes one complete atomic artifact' => sub { ok(grep($_ eq 'jar-version', @names), 'trusted Java executes the JAR version path'); ok(grep($_ eq 'jar-commit-resolve', @names), 'JAR commit is resolved to a full SHA'); is($evidence->{package}{package}, 'perlonjava', 'exact package name is retained'); - is($evidence->{package}{version}, '5.44.0', 'exact package version is retained'); + is($evidence->{package}{version}, '5.44.1', 'exact package version is retained'); is($evidence->{package}{architecture}, 'all', 'exact package architecture is retained'); ok($evidence->{notice_license_artifact}{verified}, 'durable notice/license record is retained'); @@ -89,8 +89,8 @@ subtest 'stale package output is rejected before make executes' => sub { for my $case ( ['JAR full-commit binding', 'jar-wrong-commit', qr/jar-commit-resolve exited nonzero/], - ['configured package name', 'config-package', qr/exactly perlonjava\/5\.44\.0/], - ['configured package version', 'config-version', qr/exactly perlonjava\/5\.44\.0/], + ['configured package name', 'config-package', qr/exactly perlonjava\/5\.44\.1/], + ['configured package version', 'config-version', qr/exactly perlonjava\/5\.44\.1/], ['configured architecture', 'config-architecture', qr/unsupported architecture/], ['Make package target', 'make-contract', qr/Makefile does not expose/], ['configured maintainer', 'config-maintainer', qr/control Maintainer mismatch/], @@ -135,7 +135,7 @@ subtest 'every stale SBOM and package-output spelling is rejected pre-build' => ['build', 'reports', 'nested', 'BOM.JSON'], ['build', 'reports', 'nested', 'perl-bom.json'], ['build', 'reports', 'nested', 'sbom.json'], - ['target', 'perlonjava-5.44.0.jar'], + ['target', 'perlonjava-5.44.1.jar'], ['build', 'other', 'stale.deb'], ); for my $parts (@stale) { @@ -228,7 +228,7 @@ sub fixture { $makefile =~ s/buildDeb/wrongTask/g if $scenario eq 'make-contract'; write_file(File::Spec->catfile($source, 'Makefile'), $makefile); write_file(File::Spec->catfile($source, 'build.gradle'), <<'GRADLE'); -version = '5.44.0' +version = '5.44.1' ospackage { packageName = 'perlonjava' version = project.version @@ -240,7 +240,7 @@ GRADLE my $text = read_file($path); $text =~ s/packageName = 'perlonjava'/packageName = 'other'/ if $scenario eq 'config-package'; - $text =~ s/version = '5\.44\.0'/version = '5.44.1'/ + $text =~ s/version = '5\.44\.1'/version = '5.44.2'/ if $scenario eq 'config-version'; $text =~ s/maintainer = '[^']+'/maintainer = 'Other '/ if $scenario eq 'config-maintainer'; @@ -286,11 +286,11 @@ my $sbom = JSON::PP->new->canonical->encode({ bomFormat => 'CycloneDX', componen sub put { my ($path, $bytes) = @_; open my $fh, '>:raw', $path or die $!; print {$fh} $bytes; close $fh or die $! } my $jar_name = $scenario eq 'jar-name' ? 'perlonjava-5.44.jar' - : 'perlonjava-5.44.0.jar'; + : 'perlonjava-5.44.1.jar'; put("$root/target/$jar_name", $jar); put("$root/build/reports/sbom.json", $sbom); for my $dir ($install, $package) { - put("$dir/lib/perlonjava-5.44.0.jar", $jar); + put("$dir/lib/perlonjava-5.44.1.jar", $jar); put("$dir/bin/perlonjava", "launcher\n"); put("$dir/bin/perlonjava.bat", "launcher\n"); put("$dir/share/sbom/sbom.json", $sbom); @@ -310,8 +310,8 @@ for my $name (qw(jperl jcpan jperldoc jprove)) { symlink($target, "$root/.fake-package-tree/usr/local/bin/$name") or die $!; } -my $deb_name = $scenario eq 'deb-name' ? 'PerlOnJava_5.44.0_all.deb' - : 'perlonjava_5.44.0_all.deb'; +my $deb_name = $scenario eq 'deb-name' ? 'PerlOnJava_5.44.1_all.deb' + : 'perlonjava_5.44.1_all.deb'; put("$root/build/distributions/$deb_name", "DEB\n"); if ($scenario eq 'tool-mutation') { open my $fh, '>>', "$root/../tools/java-bin/java" or die $!; @@ -358,7 +358,7 @@ open my $sf, '<', "$root/SCENARIO" or die $!; chomp(my $scenario = <$sf>); close exit 9 if $mode eq '--field' && $scenario eq 'malformed'; if ($mode eq '--field') { my $package = $scenario eq 'control-package' ? 'other' : 'perlonjava'; - my $version = $scenario eq 'control-version' ? '5.44.1' : '5.44.0'; + my $version = $scenario eq 'control-version' ? '5.44.2' : '5.44.1'; my $architecture = $scenario eq 'control-architecture' ? 'amd64' : 'all'; my $maintainer = $scenario eq 'control-maintainer' ? 'Other ' : 'Flavio Soibelmann Glock '; @@ -380,7 +380,7 @@ if ($mode eq '--contents') { print "-rw-r--r-- root/root 4 2026-01-01 00:00 ./opt/perlonjava/lib/x\n" x 2; exit 0; } - print "-rw-r--r-- root/root 4 2026-01-01 00:00 ./opt/perlonjava/lib/perlonjava-5.44.0.jar\n"; + print "-rw-r--r-- root/root 4 2026-01-01 00:00 ./opt/perlonjava/lib/perlonjava-5.44.1.jar\n"; exit 0; } die "bad dpkg mode" unless $mode eq '--extract'; diff --git a/dev/regex/tools/tests/run_package_evidence_correction.t b/dev/regex/tools/tests/run_package_evidence_correction.t index 71c478a728..c492a1476b 100644 --- a/dev/regex/tools/tests/run_package_evidence_correction.t +++ b/dev/regex/tools/tests/run_package_evidence_correction.t @@ -237,7 +237,7 @@ sub fixture { write_file(File::Spec->catfile($source, 'Makefile'), "deb: check-java-gradle\nifeq (\$(OS),Windows_NT)\n\tgradlew.bat buildDeb\nelse\n\t./gradlew buildDeb\nendif\n"); write_file(File::Spec->catfile($source, 'build.gradle'), <<'GRADLE'); -version = '5.44.0' +version = '5.44.1' ospackage { packageName = 'perlonjava' version = project.version @@ -287,7 +287,7 @@ if ($scenario eq 'legacy-both-missing' || $scenario eq 'legacy-evidence-strict') $merged =~ s/\A\{/\{"bomFormat":"CycloneDX",/ if $scenario eq 'duplicate-sbom-key'; if ($scenario eq 'extra-sbom-field') { my $d=$json->decode($merged); $d->{unexpected}=1; $merged=$json->encode($d) } if ($scenario eq 'bad-relation') { my $d=$json->decode($merged); pop @{$d->{components}}; $merged=$json->encode($d) } -put("$root/target/perlonjava-5.44.0.jar",$jar); +put("$root/target/perlonjava-5.44.1.jar",$jar); put("$root/build/reports/bom.json",$java) unless $scenario eq 'missing-java-bom' || $scenario eq 'report-final-mutation' || $scenario eq 'legacy-both-missing' || $scenario eq 'legacy-evidence-strict'; @@ -296,12 +296,12 @@ put("$root/build/reports/perl-bom.json",$perl) || $scenario eq 'legacy-evidence-strict'; put("$root/build/reports/sbom.json",$merged); for my $dir ($install,$package) { - put("$dir/bin/perlonjava","launcher\n"); put("$dir/lib/perlonjava-5.44.0.jar",$jar); + put("$dir/bin/perlonjava","launcher\n"); put("$dir/lib/perlonjava-5.44.1.jar",$jar); put("$dir/share/sbom/sbom.json",$merged); for my $n (qw(joni-LICENSE.txt joni-PERLONJAVA-NOTICE.md jcodings-LICENSE.txt)) { put("$dir/share/licenses/$n","$n\n") } } for my $n (qw(jperl jcpan jperldoc jprove)) { symlink("/opt/perlonjava/bin/$n","$root/.package/usr/local/bin/$n") or die $! } -put("$root/build/distributions/perlonjava_5.44.0_all.deb","DEB\n"); +put("$root/build/distributions/perlonjava_5.44.1_all.deb","DEB\n"); MAKE my $git = write_executable(File::Spec->catfile($tools, 'git-bin', 'git'), <<'GIT'); #!/usr/bin/perl @@ -323,8 +323,8 @@ GIT #!/usr/bin/perl use strict; use warnings; use File::Find qw(find); use File::Path qw(make_path); use File::Basename qw(dirname); my($mode,$deb,$dest)=@ARGV; my $root=dirname(dirname(dirname($deb))); -if ($mode eq '--field') { print "Package: perlonjava\nVersion: 5.44.0\nArchitecture: all\nMaintainer: Flavio Soibelmann Glock \n"; exit 0 } -if ($mode eq '--contents') { print "-rw-r--r-- root/root 4 2026-01-01 00:00 ./opt/perlonjava/lib/perlonjava-5.44.0.jar\n"; exit 0 } +if ($mode eq '--field') { print "Package: perlonjava\nVersion: 5.44.1\nArchitecture: all\nMaintainer: Flavio Soibelmann Glock \n"; exit 0 } +if ($mode eq '--contents') { print "-rw-r--r-- root/root 4 2026-01-01 00:00 ./opt/perlonjava/lib/perlonjava-5.44.1.jar\n"; exit 0 } die "bad mode" unless $mode eq '--extract'; my $tree="$root/.package"; find({no_chdir=>1,wanted=>sub{return if $_ eq $tree; my $rel=substr($_,length($tree)+1); my $to="$dest/$rel"; if(-d $_){make_path($to);return} make_path(dirname($to)); if(-l $_){symlink(readlink($_),$to) or die $!;return} diff --git a/dev/regex/tools/tests/verify_joni_packaging.t b/dev/regex/tools/tests/verify_joni_packaging.t index 780208cdef..b426500d75 100644 --- a/dev/regex/tools/tests/verify_joni_packaging.t +++ b/dev/regex/tools/tests/verify_joni_packaging.t @@ -120,8 +120,8 @@ sub fixture { metadata => { component => { type => 'application', 'bom-ref' => 'perlonjava', - name => 'perlonjava', version => '5.44.0', - purl => 'pkg:generic/perlonjava@5.44.0', + name => 'perlonjava', version => '5.44.1', + purl => 'pkg:generic/perlonjava@5.44.1', licenses => [{ license => { id => 'Artistic-2.0' } }], }, }, @@ -143,9 +143,9 @@ sub fixture { if ($option{dependency_only}) { $document->{metadata}{component} = { type => 'application', - 'bom-ref' => 'pkg:maven/org.perlonjava/perlonjava@5.44.0', - group => 'org.perlonjava', name => 'perlonjava', version => '5.44.0', - purl => 'pkg:maven/org.perlonjava/perlonjava@5.44.0', + 'bom-ref' => 'pkg:maven/org.perlonjava/perlonjava@5.44.1', + group => 'org.perlonjava', name => 'perlonjava', version => '5.44.1', + purl => 'pkg:maven/org.perlonjava/perlonjava@5.44.1', }; pop @{$document->{components}}; pop @{$document->{dependencies}}; diff --git a/dev/sandbox/command_line.pl b/dev/sandbox/command_line.pl index e7e7bb6235..3500b1a6e7 100644 --- a/dev/sandbox/command_line.pl +++ b/dev/sandbox/command_line.pl @@ -8,7 +8,7 @@ my $perl; $perl = 'perl'; -$perl = 'java -jar target/perlonjava-5.44.0.jar'; +$perl = 'java -jar target/perlonjava-5.44.1.jar'; # Test -c (compile only) { diff --git a/dev/tools/generate-perl-sbom.pl b/dev/tools/generate-perl-sbom.pl index 11febae3d9..731469f34d 100755 --- a/dev/tools/generate-perl-sbom.pl +++ b/dev/tools/generate-perl-sbom.pl @@ -9,7 +9,7 @@ # perl dev/tools/generate-perl-sbom.pl > build/reports/perl-bom.json # # Environment variables: -# VERSION - Override the version string (default: 5.44.0) +# VERSION - Override the version string (default: 5.44.1) use strict; use warnings; @@ -26,7 +26,7 @@ die "Error: Perl library directory not found: $lib_dir\n" unless -d $lib_dir; # Configuration -my $version = $ENV{VERSION} // '5.44.0'; +my $version = $ENV{VERSION} // '5.44.1'; my $timestamp = strftime('%Y-%m-%dT%H:%M:%SZ', gmtime()); my $serial_number = generate_uuid(); diff --git a/dev/tools/merge-sbom.pl b/dev/tools/merge-sbom.pl index bc07baec29..ae4fe4639a 100755 --- a/dev/tools/merge-sbom.pl +++ b/dev/tools/merge-sbom.pl @@ -31,7 +31,7 @@ my $serial_number = generate_uuid(); # Get version from Java BOM metadata -my $version = $java_bom->{metadata}{component}{version} // '5.44.0'; +my $version = $java_bom->{metadata}{component}{version} // '5.44.1'; # Build merged SBOM my $merged = { diff --git a/docs/about/changelog.md b/docs/about/changelog.md index 91c7299882..cd8c99e59f 100644 --- a/docs/about/changelog.md +++ b/docs/about/changelog.md @@ -4,201 +4,48 @@ Release history of PerlOnJava. See [Roadmap](roadmap.md) for future plans. ## Work in progress -- CPAN/compiler tooling: index token source-line offsets once per parser so - quote-heavy generated sources such as multi-megabyte CPAN `CHECKSUMS` files - parse linearly instead of quadratically. Complete bundled `Want::want` - varargs and chained-object context handling. This unblocks - Data::OpenStruct::Deep, App::column::run, and - Dist::Zilla::Plugin::Rinci::AddPrereqs without distribution preferences. -- CPAN/compiler tooling: permit exact-identity literal argument assignment, - parse indirect constructors after trailing package separators, create nested - MakeMaker `.PL` targets safely, and finish fixed test plans with explicit - skips when they reach unsupported process `fork`. Add a commonmark-java-backed - `Text::Markdown::Hoedown` implementation. Preserve installed closure - captures during interpreter eval cleanup, Perl's runtime-regex `\u` - behavior, and encoding-layer unmappable-character warnings. This unblocks - MetaStore, Story::Interact::WWW, Cantella::Worker, - Parallel::Fork::BossWorker, and the strict XML::Writer dependency suite - without distribution preferences. -- Regex: complete the regex implementation's Joni compatibility slice on both execution - backends. Closure-bearing patterns support matcher-owned callback unwind, - dynamic `(??{ ... })` programs, bounded recursion, variable-length - lookbehind, grapheme clusters, advanced Unicode properties, and Perl's - `ACCEPT`, `FAIL`, `PRUNE`, `SKIP`, `THEN`, and `COMMIT` control verbs. Runtime - source preserves lexical warning and package context. The full 80-file core - regex gate improves the PR 958 baseline by 729 passing assertions with no - per-file pass-count regressions. -- CPAN/compiler tooling: preserve tied-container ownership through weak - references, make plain-hash `each` tolerant of deleting the current key, - release global tie handlers during final destruction, and resolve top-level - `SUPER::method` calls in dynamically required modules from their lexical - package. Also distinguish anonymous hashes at the start of `map` blocks. Add - a Java-backed `Time::UTC::Now` clock using `java.time.Instant`. This unblocks - Ceph::RadosGW::Admin, Tie::Config, DBIx::Class::Sims::REST, and Time::TAI::Now - without distribution preferences. -- CPAN/compiler tooling: run MakeMaker `CONFIGURE` callbacks and prefer their - generated root modules over auxiliary metadata stubs; enforce known - subroutine lvalue errors at compile time; preserve blessed anonymous-glob - dereferencing and `isa('GLOB')`; and route XML parsing through tied-handle - `READ`. Add an honest `Authen::PAM` Java boundary that reports - `PAM_SYSTEM_ERR` until native conversations are implemented. This unblocks - Types::Namespace, Catalyst::Plugin::Unicode, Search::Sitemap's compressed - input, Authen::SimplePam, and Text::RecordParser without distribution - preferences. Search::Sitemap retains one upstream speed-sensitive `'now'` - assertion on the slower JVM runtime. -- CPAN/compiler tooling: restore YAML::PP's object `dump` API, recognize - TAP-indented missing prerequisites, report gzip stream completion for CPAN - single-file distributions, and route Object::Pad's core syntax through the - native class compiler. This unblocks Pegex::JSON, Music::Factory, - App::Chained, and Queue without distribution preferences. -- CPAN/compiler tooling: add transitive prerequisites to the bundled-provider - manifest, provide a JAXP-backed `XML::LibXSLT`, and preserve descriptors for - anonymous handles stored in container lvalues. This unblocks - `Catmandu::CrossRef` and `AnyEvent::SMTP` without distribution preferences. -- Add Perl interpreter multiplicity and full ithread support across - the JVM and interpreter backends. Mutable execution state is owned by - independent `PerlRuntime` instances; child threads receive identity-aware - snapshots, while `threads::shared` preserves explicitly shared - scalar/array/hash storage. Implement create/join/detach and lifecycle/error - inspection, nested threads, child-only exit, `CLONE`/`CLONE_SKIP`, recursive - locks, condition variables, and compatible imports/stringification. - `Config` now reports `useithreads`, `usethreads`, and `usemultiplicity` as - `define`. Java 24 virtual threads are the default and platform carriers remain - an explicit compatibility mode. Live attached children support targeted signals, - `object`/`wantarray`, and platform-thread stack sizing. Native-style callback - registrations retain their owning runtime, internal pipes have an explicit - inherited-handle policy, and nested plain shared graphs are validated before - publication. General resource inheritance, DBI ownership, lexical regex - diagnostics, blessed roots, tied shared-value conversion, nested shared - proxy views, and global final destruction are implemented. The unchanged - upstream `threads`, `threads::shared`, `Thread::Queue`, and - `Thread::Semaphore` distributions pass on both backends and both Java carrier - policies. The five non-regex Perl core thread files additionally pass 849/849 - in all four backend/carrier modes. Add strict core-wrapper and focused Windows - gates, plus system-Perl differential coverage for DBI thread ownership; see - the [Perl threads reference](../reference/threads.md). -- CPAN/tooling: expose tested dependency scripts through `PATH`, deduplicate - repeated `PERL5LIB` setup, and resolve test prerequisites against tested - `blib` trees before launching tests. Add `JSON::DWIW`, `Taint::Runtime`, and - `String::Similarity` compatibility ports; preserve open-file identity across - rename and Archive::Zip scalar/subclass behavior. Preserve the active `@_` - across `goto &sub` when an outer scope localized `*_`, and avoid synthetic - stash traversal in reachability checks used by large Moose/Dist::Zilla loads. -- CPAN/compiler tooling: unblock `Template::Lace`, `Sledge::Plugin::JSONRPC`, - and `Catmandu::Exporter::MAB2` without distribution preferences. Add - Java-backed `Data::Util` scalar inspection, a `YAML::Syck` compatibility - layer, XML reader/BOM support, POSIX math defaults, and fixes for try/catch - control flow, exception values, named-character regexes, bareword `isa`, - cloned weak references, and temporary reference ownership. -- Bugfix: overloaded mutators skip copy constructors for unshared hash/array - objects, preserving subclass-only fields in `Math::BigInt`, `Math::BigFloat`, - and `Math::BigRat` subclasses. -- CPAN: add Java-backed `Tie::Array::Packed`, BouncyCastle-backed - `Crypt::Blowfish`, and ProcessHandle-backed `Proc::ProcessTable` XS - replacements. Fix MakeMaker test-helper staging, H/h pack semantics, and raw - MD5 byte flags so `Tie::Array::Packed` and `Git::Crypt` pass without new - distribution preferences. -- CPAN: add a Java-backed `Digest::JHash` XS replacement for CHI and - `TimeZone::TimeZoneDB` dependency chains, and make CPAN's generated - Makefile fallback work when a distribution ships a read-only Makefile.PL. -- CPAN tooling: avoid launching AutoSplit for POD-only modules, materialize - `CORE/keywords.h` for build-time probes, and expose only real control-letter - globals through `%main::` stash enumeration. -- Runtime: avoid redundant global reachability walks while releasing weak - references in large object trees, substantially reducing PPI teardown cost. -- Runtime: honor explicit custom-warning mask bits and prevent stale recycled - descriptors from replacing live borrowed-handle mappings. -- CPAN: add a Java XS replacement for `Tie::Hash::Indexed`, including its - ordered tied-hash/object APIs, iterators, and Storable integration; nested - tied containers now thaw without acquiring an extra scalar-reference layer. -- CPAN/tooling: bootstrap bundled distroprefs and patches for every `jcpan` - entry path, preserve substitution `pos` inside replacement code, honor - `CORE::GLOBAL::rand`, and fix scalar-context coderef assignment returns. -- Add Perl taint mode with `-T` on both JVM and interpreter backends, including - external-input provenance, scalar and regex propagation, capture-based - untainting, and security checks for process execution, code loading, file - mutation, and other sensitive operations. -- Bugfix: targeted weak-reference sweeps preserve objects rescued by `DESTROY` - until rescue-specific reachability cleanup runs, keeping live DBIx::Class - storage callbacks valid after a schema self-rescue. -- CPAN: add a BouncyCastle-backed `Crypt::Twofish2` XS replacement, portable - `B::Flags`, bounded balanced-pattern support for `Text::Markdown`, and a - PerlOnJava-aware `Char::Latin7` launcher guard; `Text::Markdown::Slidy`, - `Text::Fold`, and their dependency suites now pass under `jcpan`. -- CPAN: add BouncyCastle-backed `Crypt::Blowfish` block-cipher support and - reusable noninteractive configure retry, library-staging, and SDBM - writeback fixes for legacy distributions. -- Add a Java-backed `Scalar::Type` port, replacing its native XS scalar-flag probe. -- Index physical source line numbers so very large generated Perl modules do - not repeatedly rescan their complete token streams while parsing strings. -- Add Java-backed `PadWalker`, `Devel::Caller`, and `Devel::LexAlias` - compatibility, including anonymous-sub pad metadata and lexical rebinding - across JVM and interpreter closures; `Lexical::Persistence` and - `Test::Cookbook` now pass their installation suites. -- CPAN: supply POD::Tested's omitted `Pod::Parser` prerequisite and tolerate - Test::Block diagnostics that also fail under current system Perl. -- Bugfix: `UNIVERSAL::DOES` honors classes that override `isa`. -- Add `Catalyst::Runtime` 5.90132 support for single-process PSGI - applications through `Plack::Handler::Netty`, including action dispatch, - parameters, uploads, responses, UTF-8, logging, and exception handling. -- Add localized `CORE::GLOBAL::exit` trapping and non-forking `Test::Trap` - compatibility; the exit-dependent `MooseX::Getopt` tests now pass. -- Bugfix: byte-mode regex substitution preserves the original scalar's lvalue - identity and `/g` position on both the JVM and interpreter backends. -- Bugfix: Catalyst class-data scalar-reference assignments retain normal stash - aliasing without turning literal or object references into pseudo-constants. -- Bugfix: `Plack::Handler::Netty` preserves PSGI byte-string response bodies - without double UTF-8 encoding. -- Bugfix: dynamic `Encode` aliases, `POSIX::tzset`, `utf-8-strict` PerlIO - layers, CJK display width, and `env perl5` shebang routing work correctly. -- CPAN: add isolated-home Catalyst policies, distribution-scoped recommendation - handling, and structured failure reporting without false status 8 results - from informational messages. +## v5.44.1: Regex, Threads, Async/Await, and CPAN Compatibility + +- Reach 686,288 of 696,597 passing assertions in the Perl standard test suite + (98.5%), and 7,522 of 16,311 randomly selected CPAN modules passing all tests + (46.1%). +- Bundle `Moose` and `Future::AsyncAwait`. Applications and libraries + including `WWW::Mechanize`, `Moo`, Template Toolkit, `DBIx::Class`, and + `Catalyst::Runtime` can be installed from CPAN through `jcpan`. +- Complete the regex implementation's Joni compatibility slice on both + execution backends, including dynamic regex programs, bounded recursion, + variable-length lookbehind, grapheme clusters, advanced Unicode properties, + and Perl control verbs. The 80-file core regex gate gains 729 passing + assertions over the PR 958 baseline with no per-file regressions. +- Add full ithread support and interpreter multiplicity across both execution + backends. The unchanged `threads`, `threads::shared`, `Thread::Queue`, and + `Thread::Semaphore` distributions pass with virtual and platform threads; + the five non-regex Perl core thread files pass all 849 assertions in all four + backend/carrier combinations. - Add native `Future::AsyncAwait` syntax and runtime support, including - suspended Future resumption, cancellation, async signatures and attributes, - `defer`/`CANCEL` integration, and the Awaitable role contract. -- Add the compatibility needed to run the single-process `PAGI::Server` - reference stack with HTTP, WebSocket, and Server-Sent Events; add a verified - runnable HTTP example under `examples/pagi/`. Process-forking server modes - remain unsupported. -- CPAN: load large metadata caches lazily and avoid full-catalog scans during - dependency installation and command summaries, so `jcpan -T PAGI::Server` - installs the reference server and its dependency chain successfully. -- Bugfix: scope interpreter warning state across JVM-compiled Perl calls and - honor explicit `local $^W = 0` suppression; this restores the full - 14,726-assertion `op/pack.t` run and related regex/substitution baselines. -- Bugfix: constrain async callback-aggregate lifecycle bookkeeping to active - async frames, preserve DESTROY-rescued graphs during targeted weak sweeps, - and assign collision-free async bytecode opcodes after integer operations. -- Bugfix: async multi-value `foreach my (...)` preserves every grouped lexical - across `await`, and the legacy `experimental::signatures` warning category - remains accepted for compatible signature-enabled code. -- CPAN: test the bundled `Future::AsyncAwait` implementation directly without - its replaced XS parser prerequisites; all 52 upstream files and 221 - assertions pass with `jcpan -t Future::AsyncAwait`. -- Bugfix: the unit-test harness accepts successful `plan skip_all` exits as - clean TAP completion, allowing Unix-only socket tests to skip correctly on - Windows CI. -- Add compatibility modules for `Socket6`, `Email::Address::XS`, and the - JSONP-used subset of `Want`; add a Java `Net::Gen` XS bridge for Net-ext. -- Bugfix: subroutine return values are rvalue copies instead of aliases to - reusable scalar containers. -- Bugfix: interpreter `map` now inherits the caller's scalar/list context. -- Bugfix: valid UTF-8 octets in interpolated source strings compile without - `use utf8` while retaining byte-string semantics. -- Bugfix: inherited AutoSplit forward declarations now participate in method - resolution, allowing parent `.al` methods to load before child `AUTOLOAD` - fallbacks. -- Bugfix: nested typeglob hash/code dereferences and numeric or byte-string - `AUTOLOAD` method names retain their Perl symbol-table semantics. -- Bugfix: IPv6 bind/listen/send/receive, socket-name packing, and - `getnameinfo` work through Java NIO; UDP sends to wildcard local addresses - are routed consistently. -- Bugfix: generated `jperl` shebang commands work in piped opens without being - misclassified as shell syntax. -- CPAN: `HTML::Diff`, `Graph::PetriNet`, `MIME::Lite`, - `File::Path::Tiny`, `Net::UDP`, `IO::Socket::INET6`, - `Email::Sender`, and `JSONP` pass their installation test suites. + suspension, resumption, cancellation, async signatures, `defer`, `CANCEL`, + and the Awaitable role. All 52 upstream files and 225 assertions pass on + both execution backends. +- Add Perl taint mode with `-T` on both execution backends. +- Add single-process Catalyst applications through `Plack::Handler::Netty`, + and support the PAGI HTTP, WebSocket, and Server-Sent Events reference stack. +- Add or expand Java-backed compatibility for `XML::LibXSLT`, + `Text::Markdown::Hoedown`, `PadWalker`, `Devel::Caller`, + `Devel::LexAlias`, `Data::Util`, `YAML::Syck`, `Scalar::Type`, + `Tie::Hash::Indexed`, `Tie::Array::Packed`, `Digest::JHash`, + `Crypt::Blowfish`, `Crypt::Twofish2`, `Proc::ProcessTable`, and other + modules. +- Improve `jcpan`, MakeMaker, distribution preferences, prerequisite + resolution, generated-module handling, and test-plan compatibility, + unblocking a broad set of CPAN distributions without source preferences. +- Make quote-heavy generated Perl sources parse linearly, load CPAN metadata + lazily, and reduce redundant reachability work in large object graphs. +- Fix weak-reference, destruction, closure, lvalue, warning-scope, encoding, + networking, symbol-table, and interpreter-context behavior. +- Improve IPv6 and UDP behavior, dynamic Encode aliases, PerlIO encoding + layers, CJK display width, and `env perl5` shebang routing. +- Preserve the single-JAR distribution model with strengthened packaging, + SBOM, license, and cross-platform validation. ## v5.44.0: Named Parameters in Signatures diff --git a/docs/about/roadmap.md b/docs/about/roadmap.md index 3da3cd5f45..6f5441348c 100644 --- a/docs/about/roadmap.md +++ b/docs/about/roadmap.md @@ -50,6 +50,14 @@ These capabilities are implemented and available in the current release: and `Thread::Semaphore` distributions pass unchanged on both backends and both Java carrier policies. See the [Perl threads reference](../reference/threads.md). +- **Native regex implementation** — The maintained Joni fork is the sole + production matcher across both backends, including dynamic regex programs, + bounded recursion, variable-length lookbehind, grapheme clusters, advanced + Unicode properties, control verbs, and lexical `re` policy. See the + [regex feature matrix](../reference/feature-matrix.md#regular-expressions). +- **Native Async/Await** — The bundled `Future::AsyncAwait` implementation + supports suspension, resumption, cancellation, signatures, `defer`, and + `CANCEL` blocks; all 52 upstream files and 221 assertions pass. - **Pack/Unpack** — Full template support for binary data manipulation. See `dev/design/pack_unpack_architecture.md`. - **Subroutine Prototypes and Signatures** — All prototype characters supported; formal parameter signatures implemented. - **`format`/`write`** — Report generation with `formline` and `$^A` accumulator. @@ -68,13 +76,7 @@ These capabilities are implemented and available in the current release: Work currently in progress: - **Warnings Subsystem** — Improving lexical `warnings` pragma scope handling and warning message formatting for Perl5 compatibility. See `dev/design/warnings-scope.md`. -- **Regex implementation delivery** — The native Joni direct/thread semantic - projection is complete. Remaining work is code cleanup and the unchanged-SHA - build, bundled-module, parity, performance/stress, packaging, platform-CI, - and UAT gates tracked in the - [regex implementation plan](../../dev/design/regex-implementation.md). - **Overload Completeness** — Adding remaining overload operators: `--`, bitwise, string repeat, and their compound forms. `++`, copy-constructor `=`, and concatenation are verified. See [Feature Matrix — overload](../reference/feature-matrix.md#pragmas). -- **`caller` Extended Information** — Implementing `(caller($level))[3..11]` for subroutine names, `wantarray`, `evaltext`, hints. Required for better error messages and Carp compatibility. - **Compiler Hardening** — Automatic fallback to interpreter mode when JVM "Method too large" errors occur. Fix remaining global variable aliasing edge cases in `for` loops. - **perl5 Test Suite** — Expanding pass rates across `perl5_t/t/` categories (op, re, uni, mro, io, lib). @@ -97,18 +99,16 @@ Work currently in progress: The maintained Joni fork is the sole production matcher. Current supported capability families and their narrow diagnostic or representation boundaries are recorded in the -[Feature Matrix](../reference/feature-matrix.md#regular-expressions); cleanup -and final delivery validation are tracked in the +[Feature Matrix](../reference/feature-matrix.md#regular-expressions); the +implementation and delivery record is preserved in the [regex implementation plan](../../dev/design/regex-implementation.md). ### Missing Pragmas and Features - **`no strict refs`** — Extend to work with lexical (`my`) variables, not just globals. -- **`bignum`/`bigint`/`bigrat`** — Transparent arbitrary-precision arithmetic. +- **`bignum`/`bigint`** — Complete transparent arbitrary-precision arithmetic + on both backends. `bigrat` is implemented. - **`locale`** — Locale-aware string operations. -- **`integer`** — Force integer arithmetic. -- **`encoding`** — Source encoding pragma. -- **`re` Pragma** — `use re 'eval'`, `use re '/flags'`, `use re 'debug'`. - **`attributes`** — Variable and subroutine attributes beyond `:lvalue` and `prototype`. - **`overloading`** — Fine-grained overload control pragma. - **`CORE` Operator References** — `\&CORE::push` and similar. diff --git a/docs/about/support.md b/docs/about/support.md index a1732769b0..6f21549095 100644 --- a/docs/about/support.md +++ b/docs/about/support.md @@ -16,7 +16,7 @@ ## Version Support PerlOnJava version numbers track the compatible Perl language version. For -example, PerlOnJava 5.44.0 targets Perl 5.44, and later patch releases use the +example, PerlOnJava 5.44.1 targets Perl 5.44, and later patch releases use the final component for PerlOnJava fixes. The project does not currently publish a formal LTS or fixed-duration support diff --git a/docs/getting-started/docker.md b/docs/getting-started/docker.md index ce2e9f09af..f5df17e05e 100644 --- a/docs/getting-started/docker.md +++ b/docs/getting-started/docker.md @@ -86,7 +86,7 @@ Modify the `Dockerfile` to include additional dependencies: ```dockerfile # Add JDBC driver FROM eclipse-temurin:24-jdk -COPY --from=build /app/target/perlonjava-5.44.0.jar /app/perlonjava.jar +COPY --from=build /app/target/perlonjava-5.44.1.jar /app/perlonjava.jar COPY path/to/driver.jar /app/drivers/ ENV CLASSPATH=/app/drivers/driver.jar ENTRYPOINT ["java", "-jar", "/app/perlonjava.jar"] diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index d2f3f8cacb..31e720bd17 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -177,7 +177,7 @@ make # Rebuild to include driver **Update configuration:** ```bash -./Configure.pl -D version=5.44.0 +./Configure.pl -D version=5.44.1 ``` **Upgrade all dependencies:** diff --git a/docs/guides/database-access.md b/docs/guides/database-access.md index 51ba3f0a84..89d0203bdf 100644 --- a/docs/guides/database-access.md +++ b/docs/guides/database-access.md @@ -50,7 +50,7 @@ make Calling java directly with the classpath is also possible: ```bash - java --enable-native-access=ALL-UNNAMED -cp "jdbc-drivers/mysql-connector-j-8.2.0.jar:target/perlonjava-5.44.0.jar" org.perlonjava.app.cli.Main myscript.pl + java --enable-native-access=ALL-UNNAMED -cp "jdbc-drivers/mysql-connector-j-8.2.0.jar:target/perlonjava-5.44.1.jar" org.perlonjava.app.cli.Main myscript.pl ``` ## Database Connection Examples diff --git a/docs/guides/java-integration.md b/docs/guides/java-integration.md index b3ea17c917..6aab9a61ce 100644 --- a/docs/guides/java-integration.md +++ b/docs/guides/java-integration.md @@ -127,8 +127,8 @@ make 3. Add to your classpath: ```bash - javac -cp target/perlonjava-5.44.0.jar YourApp.java - java --enable-native-access=ALL-UNNAMED -cp .:target/perlonjava-5.44.0.jar YourApp + javac -cp target/perlonjava-5.44.1.jar YourApp.java + java --enable-native-access=ALL-UNNAMED -cp .:target/perlonjava-5.44.1.jar YourApp ``` ## Use Cases diff --git a/docs/reference/bundled-modules.md b/docs/reference/bundled-modules.md index e2b260662f..79a79b8b91 100644 --- a/docs/reference/bundled-modules.md +++ b/docs/reference/bundled-modules.md @@ -11,6 +11,7 @@ Recent CPAN compatibility additions include: | Module | Implementation | Notes | |--------|----------------|-------| +| `Future::AsyncAwait` | Bundled pure Perl module plus PerlOnJava async runtime | Native `async`/`await` syntax and upstream-compatible Future integration | | `JSON::DWIW` | Pure Perl over bundled `JSON::PP` | Relaxed legacy JSON API, file conversion, booleans, and compatibility helpers | | `Taint::Runtime` | Perl + Java XS bridge | Runtime taint toggling and scalar taint inspection | | `String::Similarity` | Java XS bridge | Unicode-aware `fstrcmp`/`similarity` implementation | diff --git a/docs/reference/configure.md b/docs/reference/configure.md index 5c38c559e9..4d797c9ed1 100644 --- a/docs/reference/configure.md +++ b/docs/reference/configure.md @@ -55,7 +55,7 @@ the default `0` retains the single-runtime handler. - String values are automatically quoted ```bash -./Configure.pl -D version=5.44.0 +./Configure.pl -D version=5.44.1 ``` **Special behavior for `version`:** @@ -218,13 +218,13 @@ Output: ``` Current configuration: -version = "5.44.0" +version = "5.44.1" ``` ### Update Configuration ```bash -./Configure.pl -D version=5.44.0 +./Configure.pl -D version=5.44.1 ``` ### Search and Add JDBC Driver @@ -293,7 +293,7 @@ CLASSPATH=/path/to/mysql-connector.jar ./jperl script.pl ### Updating Project Version ```bash -./Configure.pl -D version=5.44.0 +./Configure.pl -D version=5.44.1 # This updates Configuration.java.in and all references to perlonjava-*.jar ``` diff --git a/docs/reference/feature-matrix.md b/docs/reference/feature-matrix.md index 154baf2261..41f3820198 100644 --- a/docs/reference/feature-matrix.md +++ b/docs/reference/feature-matrix.md @@ -407,7 +407,7 @@ The current source-level architecture is described in [`dev/implementation/regex.md`](../../dev/implementation/regex.md) and the runtime-neutral callback contract in [`docs/design/joni-callout-fork.md`](../design/joni-callout-fork.md). Delivery -validation is tracked in the regex implementation plan rather than duplicated +validation is recorded in the regex implementation plan rather than duplicated in this capability matrix. @@ -670,8 +670,8 @@ The `:encoding()` layer supports all encodings provided by Java's `Charset.forNa - ❌ **ops** pragma - ✅ **re** pragma: `is_regexp`, `regexp_pattern`, `optimization`, `strict`, `eval`, `taint`, debug modes, and complete lexical regex defaults/cancellation. - See [Regular Expressions](#regular-expressions) for supported flags and the - still-open final corpus/release boundary. + See [Regular Expressions](#regular-expressions) for supported flags and + documented boundaries. - 🚧 **vmsish** pragma. - ✅ **subs** pragma. - 🚧 **builtin** pragma: diff --git a/examples/ExifToolExample.java b/examples/ExifToolExample.java index e6965164e8..70ffb596a9 100644 --- a/examples/ExifToolExample.java +++ b/examples/ExifToolExample.java @@ -24,63 +24,63 @@ * Or compile and run the Java version: * 1. Build the fat jar: * make - * or: - * ./gradlew shadowJar * 2. Compile this example: - * javac -cp target/perlonjava-5.44.0.jar examples/ExifToolExample.java + * javac -cp target/perlonjava-5.44.1.jar examples/ExifToolExample.java * 3. Run: - * java --enable-native-access=ALL-UNNAMED -cp target/perlonjava-5.44.0.jar:. examples.ExifToolExample + * java --enable-native-access=ALL-UNNAMED -cp target/perlonjava-5.44.1.jar:. examples.ExifToolExample */ public class ExifToolExample { public static void main(String[] args) throws Exception { // Initialize PerlOnJava PerlLanguageProvider.resetAll(); - - // Add ExifTool lib to @INC - RuntimeArray inc = GlobalVariable.getGlobalArray("main::INC"); - RuntimeArray.push(inc, new RuntimeScalar("Image-ExifTool-13.44/lib")); - - // Load Image::ExifTool and define helper subroutine - String initScript = """ - use strict; - use warnings; - use Image::ExifTool; - - our $exif = Image::ExifTool->new(); - - sub process_image { - my ($file) = @_; - my $info = $exif->ImageInfo($file, qw(Make Model DateTimeOriginal)); - print "File: $file\\n"; - for my $tag (sort keys %$info) { - print " $tag: $info->{$tag}\\n"; + PerlRuntime runtime = new PerlRuntime(); + try (PerlRuntime.Binding ignored = runtime.bind()) { + // Add ExifTool lib to @INC + RuntimeArray inc = GlobalVariable.getGlobalArray("main::INC"); + RuntimeArray.push(inc, new RuntimeScalar("Image-ExifTool-13.44/lib")); + + // Load Image::ExifTool and define helper subroutine + String initScript = """ + use strict; + use warnings; + use Image::ExifTool; + + our $exif = Image::ExifTool->new(); + + sub process_image { + my ($file) = @_; + my $info = $exif->ImageInfo($file, qw(Make Model DateTimeOriginal)); + print "File: $file\\n"; + for my $tag (sort keys %$info) { + print " $tag: $info->{$tag}\\n"; + } + print "\\n"; } - print "\\n"; + 1; + """; + + CompilerOptions options = new CompilerOptions(); + options.fileName = ""; + options.code = initScript; + + System.out.println("Loading Image::ExifTool..."); + PerlLanguageProvider.executePerlCode(options, true); + System.out.println("Ready.\n"); + + // Process multiple images by calling the Perl subroutine + String[] images = { + "Image-ExifTool-13.44/t/images/Canon.jpg", + "Image-ExifTool-13.44/t/images/Nikon.jpg" + }; + + RuntimeScalar processImage = GlobalVariable.getGlobalCodeRef("main::process_image"); + + for (String image : images) { + RuntimeArray callArgs = new RuntimeArray(); + RuntimeArray.push(callArgs, new RuntimeScalar(image)); + RuntimeCode.apply(processImage, callArgs, RuntimeContextType.VOID); } - 1; - """; - - CompilerOptions options = new CompilerOptions(); - options.fileName = ""; - options.code = initScript; - - System.out.println("Loading Image::ExifTool..."); - PerlLanguageProvider.executePerlCode(options, true); - System.out.println("Ready.\n"); - - // Process multiple images by calling the Perl subroutine - String[] images = { - "Image-ExifTool-13.44/t/images/Canon.jpg", - "Image-ExifTool-13.44/t/images/Nikon.jpg" - }; - - RuntimeScalar processImage = GlobalVariable.getGlobalCodeRef("main::process_image"); - - for (String image : images) { - RuntimeArray callArgs = new RuntimeArray(); - RuntimeArray.push(callArgs, new RuntimeScalar(image)); - RuntimeCode.apply(processImage, callArgs, RuntimeContextType.VOID); } } } diff --git a/examples/ExifToolExample.pl b/examples/ExifToolExample.pl index a9c613b779..8213787edf 100644 --- a/examples/ExifToolExample.pl +++ b/examples/ExifToolExample.pl @@ -8,7 +8,7 @@ # tar xzf Image-ExifTool-13.44.tar.gz # # Run with: -# ./gradlew run --args='examples/ExifToolExample.pl' +# ./jperl examples/ExifToolExample.pl use strict; use warnings; diff --git a/examples/README.md b/examples/README.md index a7a7b1815c..32d0e98712 100644 --- a/examples/README.md +++ b/examples/README.md @@ -17,13 +17,11 @@ Note: They are provided for educational and illustrative purposes. - Automated tests for Perl scripts are located in the src/test/resources directory. - These test files are executed during the build process by Maven/Gradle to ensure the correctness of the Perl code. + These test files are executed during `make` to ensure the correctness of the Perl code. -- To run the automated tests manually, you can use the following commands: - - For Maven: `mvn test` - - For Gradle: `gradle test` +- To run the automated tests manually, use `make`. - These commands will compile the Java code, run the Java and Perl tests, and generate test reports. + This compiles the Java code, runs the Java and Perl tests, and generates test reports. - Ensure that any new Perl scripts added to the src/test/resources directory follow the project's testing conventions. diff --git a/examples/catalyst_netty/README.md b/examples/catalyst_netty/README.md index e9228d2dc8..f8786446ef 100644 --- a/examples/catalyst_netty/README.md +++ b/examples/catalyst_netty/README.md @@ -1,26 +1,32 @@ # Catalyst on Plack::Handler::Netty -This fixture is an unmodified Catalyst application used by the Catalyst support -acceptance tests. It covers Catalyst action discovery, request parsing, response -handling, exception conversion, logging, PSGI adaptation, and the Netty server. +This example is an unmodified Catalyst application for the Netty PSGI server. +It demonstrates Catalyst action discovery, request parsing, response handling, +exception conversion, logging, PSGI adaptation, and the Netty server. Install the runtime into an [isolated PerlOnJava home](../../docs/guides/using-cpan-modules.md#isolated-installations), -run the direct dispatcher suite, then start the server with a hard timeout: +then start the server with a hard timeout: ```bash export PERLONJAVA_HOME=/path/to/isolated-home -timeout 180 ./jperl examples/catalyst_netty/t/dispatch.t CATALYST_NETTY_PORT=5099 timeout 180 \ ./jperl examples/catalyst_netty/server.pl -PERLONJAVA_HOME="$PERLONJAVA_HOME" \ - examples/catalyst_netty/t/netty_e2e.sh +``` + +While the server is running, exercise its routes from another terminal: + +```bash +curl http://127.0.0.1:5099/ +curl http://127.0.0.1:5099/local +curl http://127.0.0.1:5099/path/example +curl http://127.0.0.1:5099/api/item/example ``` The supported deployment mode is a single PerlOnJava process through `Plack::Handler::Netty`. Catalyst development reloaders, prefork servers, daemonization, Perl threads, and Catalyst::Devel are outside this fixture's scope. JDBC-backed models can use PerlOnJava's DBI/JDBC support independently; -no database model is required by the runtime acceptance gate. See the +no database model is required by this example. See the [database access guide](../../docs/guides/database-access.md) when adding a Catalyst model backed by a JDBC driver. diff --git a/examples/demo.pl b/examples/demo.pl index 37ee902a86..68808adbaf 100644 --- a/examples/demo.pl +++ b/examples/demo.pl @@ -8,9 +8,7 @@ # ./jperl examples/demo.pl # # Note: The actual test suite is located in src/test/resources -# and is executed during the build process via: -# - Maven: mvn test -# - Gradle: gradle test +# and is executed during the build process with `make`. # # Features demonstrated: # - Variable and list assignments diff --git a/examples/http_server_plack/Makefile b/examples/http_server_plack/Makefile index 5ac189143b..0364af86c2 100644 --- a/examples/http_server_plack/Makefile +++ b/examples/http_server_plack/Makefile @@ -1,4 +1,4 @@ -.PHONY: help run https streaming certs test clean benchmark +.PHONY: help run streaming certs test clean benchmark # Default target help: @@ -6,7 +6,6 @@ help: @echo "" @echo "Targets:" @echo " make run - Start HTTP server on port 5000" - @echo " make https - Start HTTPS server on port 8443 (generates certs if needed)" @echo " make streaming - Start streaming response test server" @echo " make certs - Generate test SSL certificates" @echo " make test - Test all endpoints" @@ -15,7 +14,6 @@ help: @echo "" @echo "Examples:" @echo " make run # Start server, then: curl http://localhost:5000/" - @echo " make https # Start HTTPS, then: curl -k https://localhost:8443/" # Start HTTP server run: @@ -24,13 +22,6 @@ run: @echo "" ../../jperl test.pl -# Start HTTPS server (generate certs if needed) -https: certs - @echo "Starting HTTPS server on https://localhost:8443" - @echo "Press Ctrl+C to stop" - @echo "" - ../../jperl test_https.pl - # Start streaming test server streaming: @echo "Starting streaming response test server on http://localhost:5000" diff --git a/examples/http_server_plack/README.md b/examples/http_server_plack/README.md index 7d47e90652..c81443c436 100644 --- a/examples/http_server_plack/README.md +++ b/examples/http_server_plack/README.md @@ -36,7 +36,7 @@ Response ← [status, headers, body] ← Netty From the project root: ```bash -./gradlew shadowJar # or: mvn package +make ``` ### 2. Run the Example @@ -157,12 +157,9 @@ cd examples/http_server_plack/certs ./generate_test_cert.sh ``` -Then run the HTTPS test server: - -```bash -./jperl examples/http_server_plack/test_https.pl -curl -k https://localhost:8443/ # -k skips cert verification -``` +Use the generated `server-cert.pem` and `server-key.pem` paths as the +`ssl_cert` and `ssl_key` options in your own PSGI application. When testing a +self-signed endpoint with curl, pass `-k` to skip certificate verification. ### Production Certificates diff --git a/jcpan b/jcpan index 26a27ff817..2b82b298c0 100755 --- a/jcpan +++ b/jcpan @@ -116,7 +116,7 @@ export JPERL_ORPHAN_EXIT=1 # CPAN build tools may install copies of their own implementation modules into # the user library. Keep PerlOnJava's narrow compatibility overlays ahead of # those copies while jcpan and its child build processes run. -export PERLONJAVA_PREFER_BUNDLED_MODULES="Module/Build/Base.pm,Module/Build/Tiny.pm,Object/Pad.pm${PERLONJAVA_PREFER_BUNDLED_MODULES:+,$PERLONJAVA_PREFER_BUNDLED_MODULES}" +export PERLONJAVA_PREFER_BUNDLED_MODULES="Module/Build/Base.pm,Module/Build/Tiny.pm,Object/Pad.pm,IO/Handle.pm,IO/File.pm${PERLONJAVA_PREFER_BUNDLED_MODULES:+,$PERLONJAVA_PREFER_BUNDLED_MODULES}" # CPAN test suites should run with deterministic semantics. User-interface # color preferences such as NO_COLOR can change module behavior under test diff --git a/jperl b/jperl index 4c564f7f87..27e095e52f 100755 --- a/jperl +++ b/jperl @@ -40,26 +40,30 @@ if [ -n "${PERLONJAVA_JAR+x}" ]; then # Check development environment first (target directory). During Maven's test # phase the packaged JAR does not exist yet, so use compiled classes plus the # runtime dependency classpath generated by maven-dependency-plugin. -elif [ -f "$SCRIPT_DIR/target/perlonjava-5.44.0.jar" ]; then - PERLONJAVA_CP="$SCRIPT_DIR/target/perlonjava-5.44.0.jar" +elif [ -f "$SCRIPT_DIR/target/perlonjava-5.44.1.jar" ]; then + PERLONJAVA_CP="$SCRIPT_DIR/target/perlonjava-5.44.1.jar" elif [ -d "$SCRIPT_DIR/target/classes" ] && [ -f "$SCRIPT_DIR/target/jperl-test-classpath.txt" ]; then MAVEN_RUNTIME_CP=$(tr -d '\r\n' < "$SCRIPT_DIR/target/jperl-test-classpath.txt") PERLONJAVA_CP="$SCRIPT_DIR/target/classes" if [ -n "$MAVEN_RUNTIME_CP" ]; then PERLONJAVA_CP="$PERLONJAVA_CP:$MAVEN_RUNTIME_CP" fi -elif [ -f "$SCRIPT_DIR/perlonjava-5.44.0.jar" ]; then +elif [ -f "$SCRIPT_DIR/perlonjava-5.44.1.jar" ]; then # Docker or local installation with jar in same directory - PERLONJAVA_CP="$SCRIPT_DIR/perlonjava-5.44.0.jar" + PERLONJAVA_CP="$SCRIPT_DIR/perlonjava-5.44.1.jar" else # Use installed package path (when installed via deb package) - PERLONJAVA_CP="$SCRIPT_DIR/../lib/perlonjava-5.44.0.jar" + PERLONJAVA_CP="$SCRIPT_DIR/../lib/perlonjava-5.44.1.jar" fi # Determine JVM options based on Java version # --enable-native-access=ALL-UNNAMED: Required by FFM (Foreign Function & Memory) API # for native system calls (file operations, process management). -JVM_OPTS="--enable-native-access=ALL-UNNAMED" +# Perl call frames currently use the Java stack. A 16 MiB stack lets ordinary +# Perl recursion reach ecosystem guard limits (Catalyst defaults to 1000 calls) +# before HotSpot raises StackOverflowError. A later -Xss in JPERL_OPTS still +# overrides this default for callers with different constraints. +JVM_OPTS="-Xss16m --enable-native-access=ALL-UNNAMED" # Note on JVM heap settings: do NOT set -XX:SoftMaxHeapSize below -Xmx. # That combination triggers an aggressive G1 GC cadence that interacts diff --git a/jperl.bat b/jperl.bat index 355f3bda75..aeed1e82e5 100755 --- a/jperl.bat +++ b/jperl.bat @@ -20,7 +20,10 @@ if not defined JPERL_THREAD_MODE set JPERL_THREAD_MODE=virtual rem Determine JVM options based on Java version rem --enable-native-access=ALL-UNNAMED: Required by FFM (Foreign Function & Memory) API rem for native system calls (file operations, process management). -set JVM_OPTS=--enable-native-access=ALL-UNNAMED +rem Perl call frames currently use the Java stack. Keep enough stack for +rem ecosystem recursion guards such as Catalyst's default 1000-call limit. +rem A later -Xss value in JPERL_OPTS overrides this default. +set JVM_OPTS=-Xss16m --enable-native-access=ALL-UNNAMED rem Note on JVM heap settings: do NOT set -XX:SoftMaxHeapSize below -Xmx. rem That combination triggers an aggressive G1 GC cadence that interacts @@ -59,9 +62,9 @@ if %JAVA_VERSION% LSS 24 ( rem During Maven tests the packaged JAR does not exist yet. Use compiled rem classes plus the runtime classpath generated by maven-dependency-plugin. -set "PERLONJAVA_CP=%SCRIPT_DIR%..\lib\perlonjava-5.44.0.jar" -if exist "%SCRIPT_DIR%target\perlonjava-5.44.0.jar" ( - set "PERLONJAVA_CP=%SCRIPT_DIR%target\perlonjava-5.44.0.jar" +set "PERLONJAVA_CP=%SCRIPT_DIR%..\lib\perlonjava-5.44.1.jar" +if exist "%SCRIPT_DIR%target\perlonjava-5.44.1.jar" ( + set "PERLONJAVA_CP=%SCRIPT_DIR%target\perlonjava-5.44.1.jar" ) else ( if exist "%SCRIPT_DIR%target\classes" ( if exist "%SCRIPT_DIR%target\jperl-test-classpath.txt" ( diff --git a/pom.xml b/pom.xml index eb7a5fcca6..629c44ad8e 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.perlonjava perlonjava - 5.44.0 + 5.44.1 jar perlonjava diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index 45c2ef3363..23c58dae96 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -530,10 +530,13 @@ private void emitScopeCleanup(int scopeIdx, boolean flush) { } private void emitLoopControlScopeCleanup(LoopInfo loopInfo) { - if (loopInfo.cleanupScopeIndex < 0) { - return; + if (loopInfo.dynamicLocalLevelReg >= 0) { + emit(Opcodes.POP_LOCAL_LEVEL); + emitReg(loopInfo.dynamicLocalLevelReg); + } + if (loopInfo.cleanupScopeIndex >= 0) { + emitScopeCleanup(loopInfo.cleanupScopeIndex, true); } - emitScopeCleanup(loopInfo.cleanupScopeIndex, true); } private Set myVariableIndexSet() { @@ -1313,6 +1316,7 @@ public void visit(BlockNode node) { // EmitBlock's pushLoopLabels(... isBareBlock, isBareBlock)). blockLoopInfo = new LoopInfo(node.labelName, blockLoopStartPc, true); blockLoopInfo.resultReg = outerResultReg; + blockLoopInfo.dynamicLocalLevelReg = localLevelReg; loopStack.push(blockLoopInfo); } @@ -6521,6 +6525,11 @@ public void visit(For1Node node) { LoopInfo loopInfo = new LoopInfo(node.labelName, bodyStartPc, true); loopStack.push(loopInfo); loopInfo.cleanupScopeIndex = symbolTable.currentScopeIndex() + 1; + if (node.body != null && FindDeclarationVisitor.containsLocalOrDefer(node.body)) { + loopInfo.dynamicLocalLevelReg = allocateRegister(); + emit(Opcodes.GET_LOCAL_LEVEL); + emitReg(loopInfo.dynamicLocalLevelReg); + } // Step 8: Execute body if (lexicalLoopVarName != null) { @@ -6745,6 +6754,7 @@ public void visit(For3Node node) { LoopInfo loopInfo = new LoopInfo( isUnlabeledTarget ? null : node.labelName, bodyStartPc, true); + loopInfo.dynamicLocalLevelReg = blockLocalLevelReg; loopStack.push(loopInfo); int nonLocalExitPc = -1; @@ -6844,6 +6854,7 @@ public void visit(For3Node node) { int loopStartPc = bytecode.size(); // do-while is NOT a true loop (can't use last/next/redo); while/for are true loops LoopInfo loopInfo = new LoopInfo(node.labelName, loopStartPc, !node.isDoWhile); + loopInfo.dynamicLocalLevelReg = for3LocalLevelReg; loopStack.push(loopInfo); int loopEndJumpPc = -1; @@ -7581,6 +7592,7 @@ private static class LoopInfo { final boolean isTrueLoop; // True for for/while/foreach; false for do-while/bare blocks int continuePc; // PC for next (continue block or increment) int cleanupScopeIndex; // Lower bound for scopes bypassed by local loop control + int dynamicLocalLevelReg; // Saved DVM level for locals bypassed by loop control int resultReg; // Result register for value-producing synthetic blocks LoopInfo(String label, int startPc, boolean isTrueLoop) { @@ -7589,6 +7601,7 @@ private static class LoopInfo { this.isTrueLoop = isTrueLoop; this.continuePc = -1; // Will be set later this.cleanupScopeIndex = -1; + this.dynamicLocalLevelReg = -1; this.resultReg = -1; this.breakPcs = new ArrayList<>(); this.nextPcs = new ArrayList<>(); diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index 317b1ad33c..40d575c2bd 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -945,8 +945,7 @@ private static RuntimeList execute(SuspendedInterpreterFrame frame) { if (iterator.hasNext()) { // See FOREACH_NEXT_OR_EXIT above for the rationale. RuntimeScalar element = iterator.next(); - if (element instanceof RuntimeScalarReadOnly - && element != RuntimeScalarCache.scalarUndef) { + if (element instanceof RuntimeScalarReadOnly) { element = new ReadOnlyAlias(element); } registers[rd] = element; @@ -1346,8 +1345,7 @@ private static RuntimeList execute(SuspendedInterpreterFrame frame) { // to match variables such as $' must observe later matches, // just like the JVM backend and Perl do. RuntimeScalar elem = iterator.next(); - if (elem instanceof RuntimeScalarReadOnly - && elem != RuntimeScalarCache.scalarUndef) { + if (elem instanceof RuntimeScalarReadOnly) { elem = new ReadOnlyAlias(elem); } registers[rd] = elem; diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperatorHelper.java b/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperatorHelper.java index 334f868327..8d5df1a820 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperatorHelper.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperatorHelper.java @@ -313,12 +313,20 @@ private static int compileBinaryOperatorSwitch(BytecodeCompiler bytecodeCompiler // rs1 = closure (SubroutineNode compiled to code reference) // rs2 = list expression - // Emit SORT opcode - bytecodeCompiler.emit(Opcodes.SORT); - bytecodeCompiler.emitReg(rd); - bytecodeCompiler.emitReg(rs2); // List register - bytecodeCompiler.emitReg(rs1); // Closure register - bytecodeCompiler.emitInt(bytecodeCompiler.addToStringPool(bytecodeCompiler.getCurrentPackage())); // Package name for sort + // Perl evaluates the input expression but returns undef and + // does not invoke the comparator when sort is used in scalar + // context. In particular, the result must not remain a + // RuntimeList for a surrounding scalar unary operator. + if (bytecodeCompiler.currentCallContext == RuntimeContextType.SCALAR) { + bytecodeCompiler.emit(Opcodes.LOAD_UNDEF); + bytecodeCompiler.emitReg(rd); + } else { + bytecodeCompiler.emit(Opcodes.SORT); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs2); // List register + bytecodeCompiler.emitReg(rs1); // Closure register + bytecodeCompiler.emitInt(bytecodeCompiler.addToStringPool(bytecodeCompiler.getCurrentPackage())); // Package name for sort + } } case "split" -> { // Split operator: split pattern, string diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java index bf11182663..1a386bdacd 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java @@ -1598,7 +1598,13 @@ private static void visitReverse(BytecodeCompiler bc, OperatorNode node) { if (node.operand == null || !(node.operand instanceof ListNode)) bc.throwCompilerException("reverse requires arguments"); ListNode list = (ListNode) node.operand; List argRegs = new ArrayList<>(); - for (Node arg : list.elements) { arg.accept(bc); argRegs.add(bc.lastResultReg); } + // reverse has prototype (@): its operands are always evaluated in list + // context, even when reverse itself is in scalar/string context. This + // is observable for list operators such as `reverse sort LIST`. + for (Node arg : list.elements) { + bc.compileNode(arg, -1, RuntimeContextType.LIST); + argRegs.add(bc.lastResultReg); + } int argsListReg = bc.allocateRegister(); bc.emit(Opcodes.CREATE_LIST); bc.emitReg(argsListReg); bc.emit(argRegs.size()); for (int argReg : argRegs) bc.emitReg(argReg); diff --git a/src/main/java/org/perlonjava/backend/bytecode/FutureAsyncAwaitRuntime.java b/src/main/java/org/perlonjava/backend/bytecode/FutureAsyncAwaitRuntime.java index 2aa5520f33..4161e505cc 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/FutureAsyncAwaitRuntime.java +++ b/src/main/java/org/perlonjava/backend/bytecode/FutureAsyncAwaitRuntime.java @@ -87,9 +87,15 @@ private static void attach(InterpreterSuspension suspension, AwaitState state) { RuntimeScalar outer = state.outer(); if (outer == null) { state.terminal.set(true); - warnLostReturningFuture(suspension.frame); - suspension.releaseAwaitedOwner(); - cleanupAbandonedFrame(suspension.frame); + try { + warnLostReturningFuture(suspension.frame); + } finally { + try { + suspension.releaseAwaitedOwner(); + } finally { + cleanupAbandonedFrame(suspension.frame); + } + } return; } // Future::AsyncAwait requires cancellation to flow from the Future returned by @@ -100,14 +106,31 @@ private static void attach(InterpreterSuspension suspension, AwaitState state) { PerlRuntime runtime = PerlRuntime.current(); RuntimeCode callback = new RuntimeCode((callbackArgs, callbackContext) -> { - enqueue(() -> resume(suspension, state)); + // Capture the callback's dynamic warning handler before enqueueing. + // Nested Future completions can defer this resume until after the + // readiness callback (and its localized %SIG scope) has returned. + RuntimeScalar callbackWarningHandler = retainCurrentWarningHandler(); + enqueue(() -> { + try { + resume(suspension, state, callbackWarningHandler); + } finally { + if (!state.terminal.get()) { + state.callbackActive.set(false); + } + if (callbackWarningHandler != null) { + callbackWarningHandler.releaseClosureCapture(); + RuntimeScalar.scopeExitCleanup(callbackWarningHandler); + } + } + }); return new RuntimeList(); }, null).bindCallbackTo(runtime); call(suspension.awaited, "AWAIT_ON_READY", new RuntimeArray(new RuntimeScalar(callback)), RuntimeContextType.VOID); } - private static void resume(InterpreterSuspension suspension, AwaitState state) { + private static void resume(InterpreterSuspension suspension, AwaitState state, + RuntimeScalar callbackWarningHandler) { // A Future implementation may invoke a readiness callback more than once, // and cancellation may race with that callback. The returned Future owns // the terminal transition; the first terminal path wins. @@ -120,10 +143,16 @@ private static void resume(InterpreterSuspension suspension, AwaitState state) { RuntimeScalar outer = state.outer(); if (outer == null) { state.terminal.set(true); - warnLostReturningFuture(suspension.frame); - suspension.releaseAwaitedOwner(); - cleanupAbandonedFrame(suspension.frame); - state.callbackActive.set(false); + try { + warnLostReturningFuture(suspension.frame, callbackWarningHandler); + } finally { + state.callbackActive.set(false); + try { + suspension.releaseAwaitedOwner(); + } finally { + cleanupAbandonedFrame(suspension.frame); + } + } return; } if (isCancelled(outer)) { @@ -134,6 +163,7 @@ private static void resume(InterpreterSuspension suspension, AwaitState state) { state.callbackActive.set(false); return; } + RuntimeList result; try { if (isCancelled(outer)) { state.terminal.set(true); @@ -150,20 +180,48 @@ private static void resume(InterpreterSuspension suspension, AwaitState state) { suspension.releaseAwaitedOwner(); } - RuntimeList result = resumeWithCallState(suspension.frame); - if (result instanceof InterpreterSuspension next) { - attach(next, state); - return; - } - outer = state.outer(); - if (outer == null) { + result = resumeWithCallState(suspension.frame); + } catch (Throwable error) { + completeFailure(state, suspension.frame, error, callbackWarningHandler); + return; + } + + if (result instanceof InterpreterSuspension next) { + if (state.outerAfterResume() == null) { state.terminal.set(true); - warnLostReturningFuture(suspension.frame); + try { + warnLostReturningFuture(suspension.frame, callbackWarningHandler); + } finally { + try { + next.releaseAwaitedOwner(); + } finally { + cleanupAbandonedFrame(next.frame); + } + } return; } - if (!state.terminal.compareAndSet(false, true)) { - return; + try { + attach(next, state); + } catch (Throwable error) { + completeFailure(state, suspension.frame, error, callbackWarningHandler); + } finally { + if (!state.terminal.get()) { + state.callbackActive.set(false); + } } + return; + } + + outer = state.outerAfterResume(); + if (outer == null) { + state.terminal.set(true); + warnLostReturningFuture(suspension.frame, callbackWarningHandler); + return; + } + if (!state.terminal.compareAndSet(false, true)) { + return; + } + try { if (isCancelled(outer)) { state.clearOuterProbe(); return; @@ -171,27 +229,28 @@ private static void resume(InterpreterSuspension suspension, AwaitState state) { call(outer, "AWAIT_DONE", new RuntimeArray(result), RuntimeContextType.VOID); state.clearOuterProbe(); } catch (Throwable error) { - outer = state.outer(); - if (outer == null) { - state.terminal.set(true); - warnAbandonedFailure(suspension.frame, error); - return; - } - if (!state.terminal.compareAndSet(false, true)) { - return; - } - if (isCancelled(outer)) { - state.clearOuterProbe(); - return; - } - call(outer, "AWAIT_FAIL", new RuntimeArray(exceptionValue(error)), - RuntimeContextType.VOID); + completeFailure(state, suspension.frame, error, callbackWarningHandler); + } + } + + private static void completeFailure(AwaitState state, SuspendedInterpreterFrame frame, + Throwable error, RuntimeScalar callbackWarningHandler) { + RuntimeScalar outer = state.outerAfterResume(); + if (outer == null) { + state.terminal.set(true); + warnAbandonedFailure(frame, error, callbackWarningHandler); + return; + } + if (!state.terminal.compareAndSet(false, true)) { + return; + } + if (isCancelled(outer)) { state.clearOuterProbe(); - } finally { - if (!state.terminal.get()) { - state.callbackActive.set(false); - } + return; } + call(outer, "AWAIT_FAIL", new RuntimeArray(exceptionValue(error)), + RuntimeContextType.VOID); + state.clearOuterProbe(); } private static boolean isCancelled(RuntimeScalar future) { @@ -258,6 +317,20 @@ RuntimeScalar outer() { return outerWeak.getDefinedBoolean() ? outerWeak : null; } + RuntimeScalar outerAfterResume() { + if (!outerWeak.getDefinedBoolean()) { + return null; + } + if (outerWeak.value instanceof RuntimeBase referent + && !ReachabilityWalker.isReachableFromRoots(referent) + && !ReachabilityWalker.isReachableFromLiveScalarRegistry(referent) + && !ReachabilityWalker.isReachableFromLiveCodeCaptures(referent)) { + WeakRefRegistry.clearWeakRefsTo(referent); + return null; + } + return outerWeak; + } + void clearOuterProbe() { if (outerWeak.getDefinedBoolean()) { outerWeak.set(new RuntimeScalar()); @@ -266,16 +339,24 @@ void clearOuterProbe() { } private static void warnLostReturningFuture(SuspendedInterpreterFrame frame) { + warnLostReturningFuture(frame, null); + } + + private static void warnLostReturningFuture( + SuspendedInterpreterFrame frame, RuntimeScalar warningHandler) { warn("Suspended async sub " + asyncSubDisplayName(frame.code) + " lost its returning future at " + frame.code.sourceName - + " line " + Math.max(1, frame.code.cvStartLine) + ".\n"); + + " line " + Math.max(1, frame.code.cvStartLine) + ".\n", + warningHandler); } - private static void warnAbandonedFailure(SuspendedInterpreterFrame frame, Throwable error) { + private static void warnAbandonedFailure( + SuspendedInterpreterFrame frame, Throwable error, + RuntimeScalar warningHandler) { String message = exceptionValue(error).toString(); warn("Abandoned async sub " + asyncSubDisplayName(frame.code) + " failed: " + message - + (message.endsWith("\n") ? "" : "\n")); + + (message.endsWith("\n") ? "" : "\n"), warningHandler); } private static String asyncSubDisplayName(InterpretedCode code) { @@ -292,10 +373,93 @@ private static String asyncSubDisplayName(InterpretedCode code) { } private static void warn(String message) { + warn(message, null); + } + + private static void warn(String message, RuntimeScalar retainedHandler) { + // A non-null retainedHandler is a complete callback-time policy snapshot, + // including an undefined handler (which means Perl's default warning + // behavior). A queued resume can run after that dynamic %SIG scope has + // unwound, so the then-current handler must not override the snapshot. + if (retainedHandler != null) { + if (!retainedHandler.getDefinedBoolean()) { + warnWithDefaultHandler(message); + return; + } + if (isPlainWarningDirective(retainedHandler, "IGNORE")) { + return; + } + if (isPlainWarningDirective(retainedHandler, "DEFAULT")) { + warnWithDefaultHandler(message); + return; + } + RuntimeScalar warningSlot = GlobalVariable.getGlobalHash("main::SIG") + .get("__WARN__"); + int level = DynamicVariableManager.getLocalLevel(); + DynamicVariableManager.pushLocalVariable(warningSlot); + try { + // The resumed async frame can detach and restore dynamic hash + // state, replacing the concrete %SIG slot that was active at + // callback entry. Invoke the retained handler directly instead + // of routing back through that potentially replaced slot. The + // localized undef still provides Perl's recursion guard while + // the warning handler runs. + RuntimeCode.apply(retainedHandler, + new RuntimeArray(new RuntimeScalar(message)), + RuntimeContextType.SCALAR); + } finally { + DynamicVariableManager.popToLocalLevel(level); + } + return; + } + RuntimeScalar currentHandler = GlobalVariable.getGlobalHash("main::SIG") + .get("__WARN__"); + if (currentHandler != null && currentHandler.getDefinedBoolean()) { + org.perlonjava.runtime.operators.WarnDie.warn( + new RuntimeScalar(message), new RuntimeScalar("misc")); + return; + } org.perlonjava.runtime.operators.WarnDie.warn( new RuntimeScalar(message), new RuntimeScalar("misc")); } + private static void warnWithDefaultHandler(String message) { + RuntimeScalar warningSlot = GlobalVariable.getGlobalHash("main::SIG") + .get("__WARN__"); + int level = DynamicVariableManager.getLocalLevel(); + DynamicVariableManager.pushLocalVariable(warningSlot); + try { + org.perlonjava.runtime.operators.WarnDie.warn( + new RuntimeScalar(message), new RuntimeScalar("misc")); + } finally { + DynamicVariableManager.popToLocalLevel(level); + } + } + + private static RuntimeScalar retainCurrentWarningHandler() { + RuntimeScalar handler = GlobalVariable.getGlobalHash("main::SIG").get("__WARN__"); + RuntimeScalar retained = new RuntimeScalar(); + if (handler != null && handler.getDefinedBoolean()) { + retained.set(handler); + } + // The resumed frame may discard the final Perl-visible owner of this + // anonymous handler and its lexical captures before it emits the + // self-abandonment warning. Keep the closure graph alive until the + // queued resume has completed. + retained.retainClosureCapture(); + return retained; + } + + private static boolean isPlainWarningDirective(RuntimeScalar handler, String directive) { + return handler != null + && handler.getDefinedBoolean() + && !RuntimeScalarType.isReference(handler) + && handler.type != RuntimeScalarType.CODE + && handler.type != RuntimeScalarType.GLOB + && handler.type != RuntimeScalarType.GLOBREFERENCE + && directive.equals(handler.toString()); + } + private static RuntimeList resumeWithCallState(SuspendedInterpreterFrame frame) { InterpretedCode code = frame.code; RuntimeArray args = (RuntimeArray) frame.registers[1]; diff --git a/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java b/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java index cb96f3a61d..c3958084a6 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java @@ -477,7 +477,8 @@ public static int executeDerefScalarStrict( int rd = bytecode[pc++]; int rs = bytecode[pc++]; - registers[rd] = registers[rs].scalar().scalarDeref(); + registers[rd] = guardReadOnlyScalarDeref( + registers[rs].scalar().scalarDeref()); return pc; } @@ -496,10 +497,19 @@ public static int executeDerefScalarNonStrict( int rs = bytecode[pc++]; int pkgIdx = bytecode[pc++]; String pkg = code.stringPool[pkgIdx]; - registers[rd] = registers[rs].scalar().scalarDerefNonStrict(pkg); + registers[rd] = guardReadOnlyScalarDeref( + registers[rs].scalar().scalarDerefNonStrict(pkg)); return pc; } + private static RuntimeScalar guardReadOnlyScalarDeref(RuntimeScalar target) { + if (target instanceof RuntimeScalarReadOnly + && !(target instanceof ReadOnlyAlias)) { + return new ReadOnlyAlias(target); + } + return target; + } + /** * DEREF_GLOB: rd = rs.globDeref() * Format: DEREF_GLOB rd rs pkgIdx diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitBlock.java b/src/main/java/org/perlonjava/backend/jvm/EmitBlock.java index ebcbd7c1f8..9ff8248d77 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitBlock.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitBlock.java @@ -238,6 +238,10 @@ public static void emitBlock(EmitterVisitor emitterVisitor, BlockNode node) { emitterVisitor.ctx.contextType, isBareBlock, isBareBlock); + LoopLabels loopLabels = emitterVisitor.ctx.javaClassInfo.getInnermostLoopLabels(); + if (localRecord.needsCleanup()) { + loopLabels.dynamicLocalLevelSlot = localRecord.dynamicIndex(); + } } // Special case: detect pattern of `local $_` followed by `For1Node` with needsArrayOfAlias diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitControlFlow.java b/src/main/java/org/perlonjava/backend/jvm/EmitControlFlow.java index 018a82ab11..9762a3cf4a 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitControlFlow.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitControlFlow.java @@ -255,13 +255,18 @@ static void handleNextOperator(EmitterVisitor emitterVisitor, OperatorNode node) private static void emitLoopControlScopeCleanup( EmitterContext ctx, LoopLabels loopLabels, boolean exitsLoop) { + if (loopLabels.dynamicLocalLevelSlot >= 0) { + ctx.mv.visitVarInsn(Opcodes.ILOAD, loopLabels.dynamicLocalLevelSlot); + ctx.mv.visitMethodInsn(Opcodes.INVOKESTATIC, + "org/perlonjava/runtime/runtimetypes/DynamicVariableManager", + "popToLocalLevel", "(I)V", false); + } int cleanupScopeIndex = exitsLoop && loopLabels.lastCleanupScopeIndex >= 0 ? loopLabels.lastCleanupScopeIndex : loopLabels.cleanupScopeIndex; - if (cleanupScopeIndex < 0) { - return; + if (cleanupScopeIndex >= 0) { + EmitStatement.emitLoopControlScopeExit(ctx, cleanupScopeIndex); } - EmitStatement.emitLoopControlScopeExit(ctx, cleanupScopeIndex); } static void emitLoopControlScopeCleanupForDispatcher( diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitForeach.java b/src/main/java/org/perlonjava/backend/jvm/EmitForeach.java index 76e4718feb..fd83df1886 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitForeach.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitForeach.java @@ -613,6 +613,9 @@ public static void emitFor1(EmitterVisitor emitterVisitor, For1Node node) { int bodyScopeIndex = emitterVisitor.ctx.symbolTable.enterScope(); currentLoopLabels.cleanupScopeIndex = bodyScopeIndex; Local.localRecord bodyLocalRecord = Local.localSetup(emitterVisitor.ctx, blockNode, mv, true); + if (bodyLocalRecord.needsCleanup()) { + currentLoopLabels.dynamicLocalLevelSlot = bodyLocalRecord.dynamicIndex(); + } pushGotoLabelsForBlock(emitterVisitor, blockNode); @@ -1034,6 +1037,8 @@ private static void emitFor1AsWhileLoop(EmitterVisitor emitterVisitor, For1Node redoLabel, loopEnd, RuntimeContextType.VOID); + LoopLabels loopLabels = emitterVisitor.ctx.javaClassInfo.getInnermostLoopLabels(); + loopLabels.dynamicLocalLevelSlot = Local.saveLocalLevel(emitterVisitor.ctx, mv); node.body.accept(emitterVisitor.with(RuntimeContextType.VOID)); diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java b/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java index 1944911583..df465a627a 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java @@ -629,6 +629,18 @@ static void handleMapOperator(EmitterVisitor emitterVisitor, BinaryOperatorNode // Accept the right operand in LIST context and the left operand in SCALAR context. node.right.accept(emitterVisitor.with(RuntimeContextType.LIST)); // list node.left.accept(emitterVisitor.with(RuntimeContextType.SCALAR)); // subroutine + if (operator.equals("sort") && + emitterVisitor.ctx.contextType == RuntimeContextType.SCALAR) { + // Both operands have been evaluated and are now on the stack. + // Perl returns undef here without invoking the comparator. + mv.visitInsn(Opcodes.POP); // comparator + mv.visitInsn(Opcodes.POP); // input list + mv.visitFieldInsn(Opcodes.GETSTATIC, + "org/perlonjava/runtime/runtimetypes/RuntimeScalarCache", + "scalarUndef", + "Lorg/perlonjava/runtime/runtimetypes/RuntimeScalarReadOnly;"); + return; + } if (operator.equals("sort")) { // Push outer @_ so sort blocks can access $_[0], $_[1], etc. // (real Perl's sort BLOCK shares the surrounding sub's @_). diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitStatement.java b/src/main/java/org/perlonjava/backend/jvm/EmitStatement.java index 86ac5daa75..481a507d78 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitStatement.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitStatement.java @@ -667,6 +667,7 @@ public static void emitFor3(EmitterVisitor emitterVisitor, For3Node node) { LoopLabels loopLabels = emitterVisitor.ctx.javaClassInfo.getInnermostLoopLabels(); loopLabels.cleanupScopeIndex = scopeIndex + 1; loopLabels.lastCleanupScopeIndex = scopeIndex + 1; + loopLabels.dynamicLocalLevelSlot = Local.saveLocalLevel(emitterVisitor.ctx, mv); loopLabels.cleanupMarkSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); mv.visitMethodInsn(Opcodes.INVOKESTATIC, "org/perlonjava/runtime/runtimetypes/MyVarCleanupStack", diff --git a/src/main/java/org/perlonjava/backend/jvm/Local.java b/src/main/java/org/perlonjava/backend/jvm/Local.java index 1d27f368c3..617d2a1cb5 100644 --- a/src/main/java/org/perlonjava/backend/jvm/Local.java +++ b/src/main/java/org/perlonjava/backend/jvm/Local.java @@ -7,7 +7,7 @@ public class Local { - static int localSetup(EmitterContext ctx, Node ast, MethodVisitor mv) { + static int saveLocalLevel(EmitterContext ctx, MethodVisitor mv) { int dynamicIndex = ctx.symbolTable.allocateLocalVariable(); mv.visitMethodInsn(Opcodes.INVOKESTATIC, "org/perlonjava/runtime/runtimetypes/DynamicVariableManager", @@ -18,6 +18,10 @@ static int localSetup(EmitterContext ctx, Node ast, MethodVisitor mv) { return dynamicIndex; } + static int localSetup(EmitterContext ctx, Node ast, MethodVisitor mv) { + return saveLocalLevel(ctx, mv); + } + static void localTeardown(int dynamicIndex, MethodVisitor mv) { mv.visitVarInsn(Opcodes.ILOAD, dynamicIndex); mv.visitMethodInsn(Opcodes.INVOKESTATIC, @@ -32,13 +36,7 @@ static localRecord localSetup(EmitterContext ctx, Node ast, MethodVisitor mv, bo boolean needsCleanup = FindDeclarationVisitor.containsLocalOrDefer(ast); int dynamicIndex = -1; if (needsCleanup) { - dynamicIndex = ctx.symbolTable.allocateLocalVariable(); - mv.visitMethodInsn(Opcodes.INVOKESTATIC, - "org/perlonjava/runtime/runtimetypes/DynamicVariableManager", - "getLocalLevel", - "()I", - false); - mv.visitVarInsn(Opcodes.ISTORE, dynamicIndex); + dynamicIndex = saveLocalLevel(ctx, mv); } return new localRecord(needsCleanup, dynamicIndex); } diff --git a/src/main/java/org/perlonjava/backend/jvm/LoopLabels.java b/src/main/java/org/perlonjava/backend/jvm/LoopLabels.java index 982d69bc6b..a5b0f84aa9 100644 --- a/src/main/java/org/perlonjava/backend/jvm/LoopLabels.java +++ b/src/main/java/org/perlonjava/backend/jvm/LoopLabels.java @@ -52,6 +52,9 @@ public class LoopLabels { /** JVM local containing the MyVarCleanupStack mark for this loop body. */ public int cleanupMarkSlot = -1; + /** JVM local containing the dynamic-local level at loop-body entry. */ + public int dynamicLocalLevelSlot = -1; + /** * The context type in which this loop operates */ diff --git a/src/main/java/org/perlonjava/core/Configuration.java.in b/src/main/java/org/perlonjava/core/Configuration.java.in index 58ec4ef160..cfd12abc03 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java.in +++ b/src/main/java/org/perlonjava/core/Configuration.java.in @@ -9,7 +9,7 @@ import org.perlonjava.runtime.runtimetypes.RuntimeScalarType; *

* Configuration values are managed using the Configure.pl script: *

- * ./Configure.pl -D version=5.44.0 # Update version everywhere + * ./Configure.pl -D version=5.44.1 # Update version everywhere * ./Configure.pl # Show current configuration *

* This will update the version constant in this class and replace all @@ -33,7 +33,7 @@ public final class Configuration { * This version is used for both the JAR artifact and Perl compatibility version. * Updated via: ./Configure.pl -D version=X.Y.Z */ - public static final String version = "5.44.0"; + public static final String version = "5.44.1"; /** * Git commit ID (short hash) of the build. @@ -63,7 +63,7 @@ public final class Configuration { /** * Returns the version for use with "use VERSION" feature bundles. - * For version 5.44.0, returns ":5.44" + * For version 5.44.1, returns ":5.44" */ public static String getPerlVersionBundle() { int lastDot = version.lastIndexOf('.'); @@ -79,7 +79,7 @@ public final class Configuration { } /** - * Returns the version in old Perl $] format (e.g., "5.044000" for 5.44.0). + * Returns the version in old Perl $] format (e.g., "5.044001" for 5.44.1). */ public static String getPerlVersionOld() { String versionNoV = getPerlVersionNoV(); @@ -99,7 +99,7 @@ public final class Configuration { /** * Returns the Perl version as a vstring RuntimeScalar. - * For example, 5.44.0 becomes a vstring with bytes \u0005,\u0000 + * For example, 5.44.1 becomes a vstring with components 5, 44, and 1, * where each version component is represented as a character. * * @return RuntimeScalar with type VSTRING containing the version diff --git a/src/main/java/org/perlonjava/frontend/parser/FutureAsyncAwaitParser.java b/src/main/java/org/perlonjava/frontend/parser/FutureAsyncAwaitParser.java index dca991a25d..4ba329c00c 100644 --- a/src/main/java/org/perlonjava/frontend/parser/FutureAsyncAwaitParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/FutureAsyncAwaitParser.java @@ -93,7 +93,9 @@ static void markAsync(Node result, int asyncIndex) { } static OperatorNode parseAwait(Parser parser, int awaitIndex) { - if (parser.parsingEvalString && !parser.parsingFutureAsyncAwaitSub) { + if (parser.parsingEvalString + && !parser.parsingFutureAsyncAwaitSub + && !parser.ctx.symbolTable.isInSubroutineBody()) { parser.throwError(awaitIndex, "await is not allowed inside string eval"); } if (parser.futureAsyncAwaitForbiddenContext != null) { diff --git a/src/main/java/org/perlonjava/frontend/parser/SpecialBlockParser.java b/src/main/java/org/perlonjava/frontend/parser/SpecialBlockParser.java index 29d1b66f29..219a45a410 100644 --- a/src/main/java/org/perlonjava/frontend/parser/SpecialBlockParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/SpecialBlockParser.java @@ -447,7 +447,25 @@ static RuntimeList runSpecialBlock(Parser parser, String blockPhase, Node block, System.out.println(); } - String message = t.getMessage(); + String message = null; + Throwable current = t; + while (current != null) { + if (current instanceof PerlDieException die && die.getPayload() != null) { + RuntimeScalar payload = die.getPayload().getFirst(); + if (payload != null && RuntimeScalarType.isReference(payload)) { + message = payload.toString(); + } + break; + } + Throwable cause = current.getCause(); + if (cause == null || cause == current) { + break; + } + current = cause; + } + if (message == null) { + message = t.getMessage(); + } if (message == null) { message = t.getClass().getSimpleName() + " during " + blockPhase; } diff --git a/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java b/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java index 27abeec12b..33d03982b5 100644 --- a/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java @@ -1543,9 +1543,12 @@ public static ListNode handleNamedSubWithFilter(Parser parser, String subName, S ast, k -> EmitterMethodCreator.classCounter.getAndIncrement()); } - variableName = NameNormalizer.normalizeVariableName( - entry.name().substring(1), - PersistentVariable.beginPackage(beginId)); + // This is an internal lexical-storage key, not a Perl package + // variable lookup. NameNormalizer intentionally forces names such as + // STDERR and ARGV into main::, which would make `my $STDERR` captured + // by a named sub alias the package glob instead of its lexical cell. + variableName = PersistentVariable.beginPackage(beginId) + + "::" + entry.name().substring(1); } // Determine the class type based on the sigil classList.add( diff --git a/src/main/java/org/perlonjava/runtime/operators/ModuleOperators.java b/src/main/java/org/perlonjava/runtime/operators/ModuleOperators.java index 03f952206b..e94ab6f679 100644 --- a/src/main/java/org/perlonjava/runtime/operators/ModuleOperators.java +++ b/src/main/java/org/perlonjava/runtime/operators/ModuleOperators.java @@ -751,7 +751,7 @@ else if (code == null) { if (isRequire && setINC) { getGlobalHash("main::INC").elements.put(fileName, new RuntimeScalar()); } - GlobalVariable.setGlobalVariable("main::@", findInnermostCause(t).getMessage()); + GlobalVariable.setGlobalVariable("main::@", ErrorMessageUtil.stringifyException(t)); return new RuntimeScalar(); // return undef } finally { // Fire any on_scope_end callbacks registered during this file's loading diff --git a/src/main/java/org/perlonjava/runtime/operators/ReferenceOperators.java b/src/main/java/org/perlonjava/runtime/operators/ReferenceOperators.java index 92adf64be3..ff445d9d7a 100644 --- a/src/main/java/org/perlonjava/runtime/operators/ReferenceOperators.java +++ b/src/main/java/org/perlonjava/runtime/operators/ReferenceOperators.java @@ -3,7 +3,6 @@ import org.perlonjava.runtime.perlmodule.Universal; import org.perlonjava.runtime.runtimetypes.*; -import static org.perlonjava.runtime.runtimetypes.RuntimeScalarCache.scalarEmptyString; import static org.perlonjava.runtime.runtimetypes.RuntimeScalarType.*; /** @@ -252,7 +251,7 @@ public static RuntimeScalar ref(RuntimeScalar runtimeScalar) { // and caused Params::Validate::PP::_get_type() to misclassify globs // (e.g. *HANDLE with a CODE slot was reported as "CODE" instead of // falling through to the UNIVERSAL::isa(\$val,'GLOB') path). - return scalarEmptyString; + return refResult(""); case REGEX: if (runtimeScalar.value == null) { str = "Regexp"; @@ -325,9 +324,22 @@ public static RuntimeScalar ref(RuntimeScalar runtimeScalar) { case READONLY_SCALAR: return ref((RuntimeScalar) runtimeScalar.value); default: - return scalarEmptyString; + return refResult(""); } - return new RuntimeScalar(str); + return refResult(str); + } + + /** + * Perl's built-in reference names and ordinary ASCII package names are + * SvUTF8-off. Keeping them as byte strings prevents an ASCII class name + * from upgrading adjacent raw UTF-8 octets during concatenation. + */ + private static RuntimeScalar refResult(String value) { + RuntimeScalar result = new RuntimeScalar(value); + if (value.codePoints().allMatch(codePoint -> codePoint <= 0x7f)) { + result.type = BYTE_STRING; + } + return result; } /** diff --git a/src/main/java/org/perlonjava/runtime/operators/StringOperators.java b/src/main/java/org/perlonjava/runtime/operators/StringOperators.java index 3e9aebe97d..d6e09e37d5 100644 --- a/src/main/java/org/perlonjava/runtime/operators/StringOperators.java +++ b/src/main/java/org/perlonjava/runtime/operators/StringOperators.java @@ -244,6 +244,12 @@ public static RuntimeScalar lc(RuntimeScalar runtimeScalar) { } private static RuntimeScalar lcUnpropagated(RuntimeScalar runtimeScalar) { + // Regex captures such as $1 are live special-variable scalars. Inspect + // their current value rather than the placeholder object's own type so + // byte captures remain byte strings through lc. + if (runtimeScalar instanceof ScalarSpecialVariable) { + runtimeScalar = new RuntimeScalar(runtimeScalar); + } if (runtimeScalar.type == RuntimeScalarType.BYTE_STRING) { return caseFoldBytesAsciiOnly(runtimeScalar); } diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/Encode.java b/src/main/java/org/perlonjava/runtime/perlmodule/Encode.java index 338eeaf904..f67de28a99 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/Encode.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/Encode.java @@ -910,7 +910,10 @@ private static RuntimeScalar createEncodingObject(Charset charset) { encObj.put("Name", new RuntimeScalar(perlCanonicalName(charset.name()))); encObj.put("MimeName", new RuntimeScalar(charset.name())); RuntimeScalar ref = encObj.createReference(); - ReferenceOperators.bless(ref, new RuntimeScalar("Encode::Encoding")); + String packageName = charset.equals(StandardCharsets.UTF_8) + ? "Encode::utf8" + : "Encode::Encoding"; + ReferenceOperators.bless(ref, new RuntimeScalar(packageName)); return ref; } diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/ScalarUtil.java b/src/main/java/org/perlonjava/runtime/perlmodule/ScalarUtil.java index 036ff84aa2..6be936711f 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/ScalarUtil.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/ScalarUtil.java @@ -85,17 +85,26 @@ public static RuntimeList blessed(RuntimeArray args, int ctx) { if (blessId == 0) { // In Perl, qr// objects are implicitly blessed into "Regexp" if (scalar.type == RuntimeScalarType.REGEX) { - return new RuntimeScalar("Regexp").getList(); + return blessedResult("Regexp").getList(); } // IO slots such as *STDOUT{IO} are internally represented as // RuntimeIO-backed GLOBREFERENCE values, but Perl treats them as // blessed IO objects. if (scalar.type == RuntimeScalarType.GLOBREFERENCE && scalar.value instanceof RuntimeIO) { - return new RuntimeScalar("IO::Handle").getList(); + return blessedResult("IO::Handle").getList(); } return new RuntimeScalar().getList(); // undef } - return new RuntimeScalar(NameNormalizer.getBlessStr(blessId)).getList(); + return blessedResult(NameNormalizer.getBlessStr(blessId)).getList(); + } + + /** Match Perl's SvUTF8 flag on class names returned by blessed(). */ + private static RuntimeScalar blessedResult(String value) { + RuntimeScalar result = new RuntimeScalar(value); + if (value.codePoints().allMatch(codePoint -> codePoint <= 0x7f)) { + result.type = BYTE_STRING; + } + return result; } /** diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/DestroyDispatch.java b/src/main/java/org/perlonjava/runtime/runtimetypes/DestroyDispatch.java index 4865f8695d..b9a0c0f67b 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/DestroyDispatch.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/DestroyDispatch.java @@ -44,18 +44,26 @@ static void requestSweepAfterOuterDestroy() { public static void registerIfDestroyable(RuntimeBase referent, int blessId) { if (referent == null || blessId == 0) return; String className = NameNormalizer.getBlessStr(blessId); - if (className != null - && className.endsWith("::Cursor") - && classHasDestroy(blessId, className)) { + boolean hasDestroy = className != null && classHasDestroy(blessId, className); + boolean statementBoundaryCleanup = hasDestroy + && (className.equals("File::Temp") || className.equals("File::Temp::Dir")); + if (hasDestroy && (className.endsWith("::Cursor") || statementBoundaryCleanup)) { state().destroyableObjects.add(referent); } else { state().destroyableObjects.remove(referent); } + if (statementBoundaryCleanup) { + state().statementBoundaryDestroyableObjects.add(referent); + MortalList.noteBoundaryWork(); + } else { + state().statementBoundaryDestroyableObjects.remove(referent); + } } public static void unregisterDestroyable(RuntimeBase referent) { if (referent != null) { state().destroyableObjects.remove(referent); + state().statementBoundaryDestroyableObjects.remove(referent); } } @@ -70,6 +78,17 @@ public static boolean hasDestroyableObjects() { return !state().destroyableObjects.isEmpty(); } + static boolean hasStatementBoundaryDestroyableObjects() { + return !state().statementBoundaryDestroyableObjects.isEmpty(); + } + + static ArrayList snapshotStatementBoundaryDestroyableObjects() { + Set objects = state().statementBoundaryDestroyableObjects; + synchronized (objects) { + return new ArrayList<>(objects); + } + } + // Rescued objects whose weak refs need deferred clearing. // We cannot clear weak refs immediately after rescue because that would also // clear back-references from sibling objects (e.g., $source->{schema}) that diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/LifecycleRuntimeState.java b/src/main/java/org/perlonjava/runtime/runtimetypes/LifecycleRuntimeState.java index 9d8db4290f..53a89a885f 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/LifecycleRuntimeState.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/LifecycleRuntimeState.java @@ -45,6 +45,8 @@ final class LifecycleRuntimeState { final ConcurrentHashMap destroyMethodCache = new ConcurrentHashMap<>(); final Set destroyableObjects = Collections.synchronizedSet(Collections.newSetFromMap(new IdentityHashMap<>())); + final Set statementBoundaryDestroyableObjects = + Collections.synchronizedSet(Collections.newSetFromMap(new IdentityHashMap<>())); RuntimeBase currentDestroyTarget; boolean destroyTargetRescued; boolean sweepPendingAfterOuterDestroy; @@ -86,6 +88,7 @@ void clear() { destroyClassesChecked.clear(); destroyMethodCache.clear(); destroyableObjects.clear(); + statementBoundaryDestroyableObjects.clear(); currentDestroyTarget = null; destroyTargetRescued = false; sweepPendingAfterOuterDestroy = false; diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/MortalList.java b/src/main/java/org/perlonjava/runtime/runtimetypes/MortalList.java index ebf4181e6f..a141d5ef22 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/MortalList.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/MortalList.java @@ -45,6 +45,7 @@ private static void refreshBoundaryWork(LifecycleRuntimeState state) { || state.deferredCapturesMayBeReady || state.immediateWeakSweepRequested || !state.targetedWeakSweepReferents.isEmpty() + || !state.statementBoundaryDestroyableObjects.isEmpty() || state.weakRefsExist; if (!needed && state.boundaryWorkRegistered.compareAndSet(true, false)) { runtimesWithBoundaryWork.decrementAndGet(); @@ -1433,6 +1434,15 @@ private static void maybeAutoSweepAtStatementBoundary( } } + private static void maybeSweepStatementBoundaryDestroyables() { + LifecycleRuntimeState state = state(); + if (!ModuleInitGuard.inModuleInit() + && DestroyDispatch.hasStatementBoundaryDestroyableObjects()) { + ReachabilityWalker.sweepStatementBoundaryDestroyableObjects(); + } + refreshBoundaryWork(state); + } + /** * Phase 3 (refcount_alignment_plan.md): Return the current pending-queue * size. Used by {@link DestroyDispatch#doCallDestroy} to snapshot the @@ -1578,6 +1588,7 @@ public static void popAndFlush() { // that may have become ready (captureCount reached 0) during // scope cleanup. processReadyDeferredCaptures(state); + maybeSweepStatementBoundaryDestroyables(); maybeAutoSweepIfRequested(state); return; } @@ -1595,6 +1606,7 @@ public static void popAndFlush() { // After processing mortals (which may have triggered releaseCaptures // via callDestroy), check if any deferred captures are now ready. processReadyDeferredCaptures(state); + maybeSweepStatementBoundaryDestroyables(); maybeAutoSweepIfRequested(state); } } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/PerlRuntime.java b/src/main/java/org/perlonjava/runtime/runtimetypes/PerlRuntime.java index 945aefb9b4..e95c897cbc 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/PerlRuntime.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/PerlRuntime.java @@ -449,38 +449,66 @@ private RootSnapshot snapshotCloneInternal( } } + java.util.List cloneSkipHooks; + // Lazy compilation mutates package and compiler metadata. A cloned + // CV may materialize its source definition from a descendant thread, + // so the source execution lock cannot protect this transaction: the + // source may be blocked in join while still owning that lock. Use the + // process-wide reentrant compiler lock shared by every compilation + // path. Capture CLONE_SKIP callbacks here, but invoke that user code + // only after releasing the compiler lock. + org.perlonjava.app.scriptengine.PerlLanguageProvider.COMPILE_LOCK.lock(); + try { + try (Binding ignored = bind()) { + materializeLazyCodeDefinitions(); + cloneSkipHooks = captureCloneSkipHooks(); + } + } finally { + org.perlonjava.app.scriptengine.PerlLanguageProvider.COMPILE_LOCK.unlock(); + } + Set skipped; try (Binding ignored = bind()) { - materializeLazyCodeDefinitions(); - skipped = preflightCloneSkip(); + skipped = invokeCloneSkipHooks(cloneSkipHooks); } - PerlRuntime child = new PerlRuntime(registry, threadId); - nameNormalizerState.snapshotInto(child.nameNormalizerState); - RuntimeGraphCloner cloner = new RuntimeGraphCloner(this, child, skipped); - try (Binding ignored = bind()) { - globalState.snapshotInto(child.globalState, cloner); - cloner.cloneCompilationHints(compilationState, child.compilationState); - cloner.cloneCompilationWarnings(compilationState, child.compilationState); - runtimeCodeState.snapshotCompiledMetadataInto(child.runtimeCodeState); - sourceMapperState.snapshotInto(child.sourceMapperState); - regexState.snapshotInto(child.regexState); + RootSnapshot snapshot; + org.perlonjava.app.scriptengine.PerlLanguageProvider.COMPILE_LOCK.lock(); + try { + PerlRuntime child = new PerlRuntime(registry, threadId); + nameNormalizerState.snapshotInto(child.nameNormalizerState); + RuntimeGraphCloner cloner = new RuntimeGraphCloner(this, child, skipped); + try (Binding ignored = bind()) { + globalState.snapshotInto(child.globalState, cloner); + cloner.cloneCompilationHints(compilationState, child.compilationState); + cloner.cloneCompilationWarnings(compilationState, child.compilationState); + runtimeCodeState.snapshotCompiledMetadataInto(child.runtimeCodeState); + sourceMapperState.snapshotInto(child.sourceMapperState); + regexState.snapshotInto(child.regexState); + } + java.util.List clonedRoots = cloner.cloneSnapshotRoots(roots); + cloner.finishSnapshot(); + child.currentDirectory = currentDirectory; + child.initialized = true; + snapshot = new RootSnapshot(child, clonedRoots); + } finally { + org.perlonjava.app.scriptengine.PerlLanguageProvider.COMPILE_LOCK.unlock(); } - java.util.List clonedRoots = cloner.cloneSnapshotRoots(roots); - cloner.finishSnapshot(); - child.currentDirectory = currentDirectory; - child.initialized = true; - try (Binding ignored = child.bind()) { - child.runCloneHooks(); + + try (Binding ignored = snapshot.runtime().bind()) { + snapshot.runtime().runCloneHooks(); } - return new RootSnapshot(child, clonedRoots); + return snapshot; } finally { executionLock.unlock(); } } - private Set preflightCloneSkip() { - Set skipped = new HashSet<>(); + private record CloneSkipHook(String packageName, RuntimeScalar hook) {} + + /** Capture effective CLONE_SKIP callbacks while compiler metadata is stable. */ + private java.util.List captureCloneSkipHooks() { + java.util.List hooks = new java.util.ArrayList<>(); // Perl calls the effective CLONE_SKIP method once for each class that // has live blessed values in the snapshot. Walking every defined hook // invoked callbacks long before any object of that class existed; only @@ -492,8 +520,19 @@ private Set preflightCloneSkip() { RuntimeScalar hook = InheritanceResolver.findMethodInHierarchy( "CLONE_SKIP", packageName, null, 0, false); if (hook == null || !(hook.value instanceof RuntimeCode code) || !code.defined()) continue; + hooks.add(new CloneSkipHook(packageName, hook)); + } + return java.util.List.copyOf(hooks); + } + + /** Invoke captured user callbacks without retaining the global compiler lock. */ + private Set invokeCloneSkipHooks(java.util.List hooks) { + Set skipped = new HashSet<>(); + for (CloneSkipHook candidate : hooks) { + String packageName = candidate.packageName(); RuntimeArray args = new RuntimeArray(new RuntimeScalar(packageName)); - if (RuntimeCode.apply(hook, args, RuntimeContextType.SCALAR).scalar().getBoolean()) { + if (RuntimeCode.apply(candidate.hook(), args, RuntimeContextType.SCALAR) + .scalar().getBoolean()) { skipped.add(packageName); } } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/ReachabilityWalker.java b/src/main/java/org/perlonjava/runtime/runtimetypes/ReachabilityWalker.java index f3d1732684..8067336193 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/ReachabilityWalker.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/ReachabilityWalker.java @@ -1925,9 +1925,24 @@ public static int sweepDestroyableObjects(boolean forceJvmGc) { return destroyUnreachableDestroyables(w.walk()); } + /** Reconcile File::Temp-style external resources at a safe statement boundary. */ + static int sweepStatementBoundaryDestroyableObjects() { + java.util.List candidates = + DestroyDispatch.snapshotStatementBoundaryDestroyableObjects(); + if (candidates.isEmpty()) return 0; + Set live = new ReachabilityWalker().walk(); + return destroyUnreachableDestroyables(candidates, live); + } + private static int destroyUnreachableDestroyables(Set live) { + return destroyUnreachableDestroyables( + DestroyDispatch.snapshotDestroyableObjects(), live); + } + + private static int destroyUnreachableDestroyables( + java.util.List candidates, Set live) { int destroyed = 0; - for (RuntimeBase referent : DestroyDispatch.snapshotDestroyableObjects()) { + for (RuntimeBase referent : candidates) { if (referent == null || referent.destroyFired || referent.currentlyDestroying diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/ReadOnlyAlias.java b/src/main/java/org/perlonjava/runtime/runtimetypes/ReadOnlyAlias.java index 3047fa36ee..f7c8732370 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/ReadOnlyAlias.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/ReadOnlyAlias.java @@ -66,4 +66,10 @@ public long getLong() { public double getDouble() { return src.getDouble(); } + + /** A foreach alias references the original scalar cell, not this guard wrapper. */ + @Override + public RuntimeScalar createReference() { + return src.createReference(); + } } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGraphCloner.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGraphCloner.java index 591a141f92..8a02b3f2aa 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGraphCloner.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGraphCloner.java @@ -313,34 +313,45 @@ private void copyCodeMetadata(RuntimeCode source, RuntimeCode target) { // the source materializer under its owning runtime and then clone the // completed definition into this runtime. target.compilerSupplier = () -> { - synchronized (source) { - if (source.compilerSupplier != null) { - try (PerlRuntime.Binding ignored = sourceRuntime.bind()) { - source.compilerSupplier.get(); + // A descendant may reach this supplier while the source runtime + // is snapshotting another ithread. Compilation mutates package and + // compiler metadata, so serialize the complete materialize/copy + // transaction with snapshots. Do not acquire the source runtime's + // execution lock: it may be blocked in join while this descendant + // owns its own runtime lock, which would deadlock nested ithreads. + org.perlonjava.app.scriptengine.PerlLanguageProvider.COMPILE_LOCK.lock(); + try { + synchronized (source) { + if (source.compilerSupplier != null) { + try (PerlRuntime.Binding ignored = sourceRuntime.bind()) { + source.compilerSupplier.get(); + } } } - } - // Lazy compilation may have registered eval STRING descriptors - // after the thread's initial runtime snapshot. Copy only that - // compiled metadata before installing the completed child CODE. - sourceRuntime.runtimeCodeState().snapshotCompiledMetadataInto( - targetRuntime.runtimeCodeState()); - sourceRuntime.globalState().snapshotCompiledCodeRefsInto( - targetRuntime.globalState(), this); - RuntimeCode completed; - try (PerlRuntime.Binding ignored = targetRuntime.bind()) { - // Reuse this snapshot's identity map. A fresh cloner would - // duplicate lexicals that were already copied as runtime - // roots before this lazy CV was materialized. - clones.remove(source); - try { - completed = (RuntimeCode) cloneCode(source); - } finally { - clones.put(source, target); + // Lazy compilation may have registered eval STRING descriptors + // after the thread's initial runtime snapshot. Copy only that + // compiled metadata before installing the completed child CODE. + sourceRuntime.runtimeCodeState().snapshotCompiledMetadataInto( + targetRuntime.runtimeCodeState()); + sourceRuntime.globalState().snapshotCompiledCodeRefsInto( + targetRuntime.globalState(), this); + RuntimeCode completed; + try (PerlRuntime.Binding ignored = targetRuntime.bind()) { + // Reuse this snapshot's identity map. A fresh cloner would + // duplicate lexicals that were already copied as runtime + // roots before this lazy CV was materialized. + clones.remove(source); + try { + completed = (RuntimeCode) cloneCode(source); + } finally { + clones.put(source, target); + } } + target.adoptDefinitionFrom(completed); + target.compilerSupplier = null; + } finally { + org.perlonjava.app.scriptengine.PerlLanguageProvider.COMPILE_LOCK.unlock(); } - target.adoptDefinitionFrom(completed); - target.compilerSupplier = null; return null; }; } diff --git a/src/main/perl/lib/Config.pm b/src/main/perl/lib/Config.pm index 8d99923cd2..a727fad9a9 100644 --- a/src/main/perl/lib/Config.pm +++ b/src/main/perl/lib/Config.pm @@ -16,7 +16,7 @@ use Java::System qw(getProperty getenv); our ( %Config, $VERSION ); -$VERSION = "5.044000"; +$VERSION = "5.044001"; # Skip @Config::EXPORT because it only contains %Config, which we special # case below as it's not a function. @Config::EXPORT won't change in the @@ -49,11 +49,11 @@ sub import { return; } -die "$0: Perl lib version (5.44.0) doesn't match executable '$^X' version ($])" +die "$0: Perl lib version (5.44.1) doesn't match executable '$^X' version ($])" unless $^V; -$^V eq 5.44.0 - or die sprintf "%s: Perl lib version (5.44.0) doesn't match executable '$^X' version (%vd)", $0, $^V; +$^V eq 5.44.1 + or die sprintf "%s: Perl lib version (5.44.1) doesn't match executable '$^X' version (%vd)", $0, $^V; # Get Java system properties using Java::System module @@ -74,7 +74,7 @@ my $perlonjava_home = defined($perlonjava_override) && length($perlonjava_overri : ($user_home ? _catdir($file_separator, $user_home, '.perlonjava') : '.perlonjava'); -my $core_privlib = _catdir($file_separator, $perlonjava_home, 'core', 'lib', 'perl5', '5.44.0'); +my $core_privlib = _catdir($file_separator, $perlonjava_home, 'core', 'lib', 'perl5', '5.44.1'); my $core_archlib = _catdir($file_separator, $core_privlib, "java-$java_version-$os_arch"); _ensure_dir(_catdir($file_separator, $core_archlib, 'CORE')); _ensure_core_probe_file( @@ -199,7 +199,7 @@ my $startperl = $is_windows osvers => $os_version, # PerlOnJava specific - perlonjava => '5.44.0', + perlonjava => '5.44.1', java_version => $java_version, java_vendor => $java_vendor, java_home => $java_home, @@ -243,10 +243,10 @@ my $startperl = $is_windows # CPAN build helpers such as ExtUtils::CBuilder probe $archlibexp/CORE. archlibexp => $core_archlib, privlibexp => $core_privlib, - sitearchexp => 'perlonjava/lib/perl5/site_perl/5.44.0/' . "java-$java_version-$os_arch", - sitelibexp => 'perlonjava/lib/perl5/site_perl/5.44.0', - vendorarchexp => 'perlonjava/lib/perl5/vendor_perl/5.44.0/' . "java-$java_version-$os_arch", - vendorlibexp => 'perlonjava/lib/perl5/vendor_perl/5.44.0', + sitearchexp => 'perlonjava/lib/perl5/site_perl/5.44.1/' . "java-$java_version-$os_arch", + sitelibexp => 'perlonjava/lib/perl5/site_perl/5.44.1', + vendorarchexp => 'perlonjava/lib/perl5/vendor_perl/5.44.1/' . "java-$java_version-$os_arch", + vendorlibexp => 'perlonjava/lib/perl5/vendor_perl/5.44.1', # Script directory (JAR-embedded scripts at /bin/) scriptdir => 'jar:PERL5BIN', @@ -363,11 +363,11 @@ my $startperl = $is_windows eunicefix => ':', # No-op fixer (only used on EUNICE) # Version info - version => '5.44.0', - version_patchlevel_string => 'version 44 patchlevel 0', + version => '5.44.1', + version_patchlevel_string => 'version 44 patchlevel 1', api_version => '44', - api_subversion => '0', - api_versionstring => '5.44.0', + api_subversion => '1', + api_versionstring => '5.44.1', # Build configuration dont_use_nlink => undef, @@ -483,7 +483,7 @@ sub _ensure_core_probe_file { # Return a string describing the perl configuration (like perl -V) sub myconfig { - my $config = "Summary of my perl5 (revision 5 version 44 subversion 0) configuration:\n"; + my $config = "Summary of my perl5 (revision 5 version 44 subversion 1) configuration:\n"; $config .= " \n"; # Blank line with leading spaces (matches Perl format) $config .= " Platform:\n"; $config .= " osname=$Config{osname}\n"; diff --git a/src/main/perl/lib/DBI.pm b/src/main/perl/lib/DBI.pm index 114e15db1c..92aeb4610a 100644 --- a/src/main/perl/lib/DBI.pm +++ b/src/main/perl/lib/DBI.pm @@ -841,7 +841,7 @@ sub parse_dsn { # Example: # -# java -cp "h2-2.2.224.jar:target/perlonjava-5.44.0.jar" org.perlonjava.app.cli.Main dbi.pl +# java -cp "h2-2.2.224.jar:target/perlonjava-5.44.1.jar" org.perlonjava.app.cli.Main dbi.pl # # # Connect to H2 database # my $dbh = DBI->connect( diff --git a/src/main/perl/lib/Devel/Cycle.pm b/src/main/perl/lib/Devel/Cycle.pm index 720a3f3705..b1ea3279db 100644 --- a/src/main/perl/lib/Devel/Cycle.pm +++ b/src/main/perl/lib/Devel/Cycle.pm @@ -1,35 +1,459 @@ package Devel::Cycle; +# $Id: Cycle.pm,v 1.15 2009/08/24 12:51:02 lstein Exp $ + +use 5.006001; use strict; +use Carp 'croak','carp'; use warnings; -our $VERSION = '1.12'; +use Scalar::Util qw(isweak blessed refaddr reftype); + +my $SHORT_NAME = 'A'; +my %SHORT_NAMES; -# No-op stub for PerlOnJava. -# The JVM uses tracing GC which handles circular references natively. -# Cycles are never a problem, so find_cycle always reports zero cycles. -use Exporter qw(import); +require Exporter; +our @ISA = qw(Exporter); our @EXPORT = qw(find_cycle find_weakened_cycle); -our @EXPORT_OK = @EXPORT; +our @EXPORT_OK = qw($FORMATTING); +our $VERSION = '1.12'; +our $FORMATTING = 'roasted'; +our $QUIET = 0; -# Never calls callback = no cycles found -sub find_cycle { } -sub find_weakened_cycle { } +my %import_args = (-quiet =>1, + -raw =>1, + -cooked =>1, + -roasted=>1); -1; +BEGIN { + require constant; + constant->import( HAVE_PADWALKER => + eval { + require PadWalker; + $PadWalker::VERSION >= 1.0; + }); +} + +sub import { + my $self = shift; + my @args = @_; + my %args = map {$_=>1} @args; + $QUIET++ if exists $args{-quiet}; + $FORMATTING = 'roasted' if exists $args{-roasted}; + $FORMATTING = 'raw' if exists $args{-raw}; + $FORMATTING = 'cooked' if exists $args{-cooked}; + $self->export_to_level(1,$self,grep {!exists $import_args{$_}} @_); +} + +sub find_weakened_cycle { + my $ref = shift; + my $callback = shift; + unless ($callback) { + my $counter = 0; + $callback = sub { + _do_report(++$counter,shift) + } + } + _find_cycle($ref,{},$callback,1,{},()); +} + +sub find_cycle { + my $ref = shift; + my $callback = shift; + unless ($callback) { + my $counter = 0; + $callback = sub { + _do_report(++$counter,shift) + } + } + _find_cycle($ref,{},$callback,0,{},()); +} + +sub _find_cycle { + my $current = shift; + my $seenit = shift; + my $callback = shift; + my $inc_weak_refs = shift; + my $complain = shift; + my @report = @_; + + return unless ref $current; + + # note: it seems like you could just do: + # + # return if isweak($current); + # + # but strangely the weak flag doesn't seem to survive the copying, + # so the test has to happen directly on the reference in the data + # structure being scanned. + + if ($seenit->{refaddr $current}) { + $callback->(\@report); + return; + } + $seenit->{refaddr $current}++; + + _find_cycle_dispatch($current,{%$seenit},$callback,$inc_weak_refs,$complain,@report); +} + +sub _find_cycle_dispatch { + my $type = _get_type($_[0]); + + if (!defined $type) { + my $ref = reftype $_[0]; + our %already_warned; + if (!$already_warned{$ref}++) { + warn "Unhandled type: $ref"; + } + return; + } + my $sub = do { no strict 'refs'; \&{"_find_cycle_$type"} }; + $sub->(@_); +} + +sub _find_cycle_SCALAR { + my $current = shift; + my $seenit = shift; + my $callback = shift; + my $inc_weak_refs = shift; + my $complain = shift; + my @report = @_; + + return if !$inc_weak_refs && isweak($$current); + _find_cycle($$current,{%$seenit},$callback,$inc_weak_refs,$complain, + (@report,['SCALAR',undef,$current => $$current,$inc_weak_refs?isweak($$current):()])); +} + +sub _find_cycle_ARRAY { + my $current = shift; + my $seenit = shift; + my $callback = shift; + my $inc_weak_refs = shift; + my $complain = shift; + my @report = @_; + + for (my $i=0; $i<@$current; $i++) { + next if !$inc_weak_refs && isweak($current->[$i]); + _find_cycle($current->[$i],{%$seenit},$callback,$inc_weak_refs,$complain, + (@report,['ARRAY',$i,$current => $current->[$i],$inc_weak_refs?isweak($current->[$i]):()])); + } +} + +sub _find_cycle_HASH { + my $current = shift; + my $seenit = shift; + my $callback = shift; + my $inc_weak_refs = shift; + my $complain = shift; + my @report = @_; + + for my $key (sort keys %$current) { + next if !$inc_weak_refs && isweak($current->{$key}); + _find_cycle($current->{$key},{%$seenit},$callback,$inc_weak_refs,$complain, + (@report,['HASH',$key,$current => $current->{$key},$inc_weak_refs?isweak($current->{$key}):()])); + } +} + +sub _find_cycle_CODE { + my $current = shift; + my $seenit = shift; + my $callback = shift; + my $inc_weak_refs = shift; + my $complain = shift; + my @report = @_; + + unless (HAVE_PADWALKER) { + if (!$complain->{$current} && !$QUIET) { + carp "A code closure was detected in but we cannot check it unless the PadWalker module is installed"; + } + return; + } + + my $closed_vars = PadWalker::closed_over( $current ); + foreach my $varname ( sort keys %$closed_vars ) { + my $value = $closed_vars->{$varname}; + _find_cycle_dispatch($value,{%$seenit},$callback,$inc_weak_refs,$complain, + (@report,['CODE',$varname,$current => $value])); + } +} + +sub _do_report { + my $counter = shift; + my $path = shift; + print "Cycle ($counter):\n"; + foreach (@$path) { + my ($type,$index,$ref,$value,$is_weak) = @$_; + printf("\t%30s => %-30s\n",($is_weak ? 'w-> ' : '')._format_reference($type,$index,$ref,0),_format_reference(undef,undef,$value,1)); + } + print "\n"; +} + +sub _format_reference { + my ($type,$index,$ref,$deref) = @_; + $type ||= _get_type($ref); + return $ref unless $type; + my $suffix = defined $index ? _format_index($type,$index) : ''; + if ($FORMATTING eq 'raw') { + return $ref.$suffix; + } + + else { + my $package = blessed($ref); + my $prefix = $package ? ($FORMATTING eq 'roasted' ? "${package}::" : "${package}=" ) : ''; + my $sygil = $deref ? '\\' : ''; + my $shortname = ($SHORT_NAMES{$ref} ||= $SHORT_NAME++); + return $sygil . ($sygil ? '$' : '$$'). $prefix . $shortname . $suffix if $type eq 'SCALAR'; + return $sygil . ($sygil ? '@' : '$') . $prefix . $shortname . $suffix if $type eq 'ARRAY'; + return $sygil . ($sygil ? '%' : '$') . $prefix . $shortname . $suffix if $type eq 'HASH'; + return $sygil . ($sygil ? '&' : '$') . $prefix . $shortname . $suffix if $type eq 'CODE'; + } +} + +# why not Scalar::Util::reftype? +sub _get_type { + my $thingy = shift; + return unless ref $thingy; + return 'SCALAR' if UNIVERSAL::isa($thingy,'SCALAR') || UNIVERSAL::isa($thingy,'REF'); + return 'ARRAY' if UNIVERSAL::isa($thingy,'ARRAY'); + return 'HASH' if UNIVERSAL::isa($thingy,'HASH'); + return 'CODE' if UNIVERSAL::isa($thingy,'CODE'); + undef; +} + +sub _format_index { + my ($type,$index) = @_; + return "->[$index]" if $type eq 'ARRAY'; + return "->{'$index'}" if $type eq 'HASH'; + return " variable $index" if $type eq 'CODE'; + return; +} + +1; __END__ =head1 NAME -Devel::Cycle - No-op stub for PerlOnJava +Devel::Cycle - Find memory cycles in objects + +=head1 SYNOPSIS + + #!/usr/bin/perl + use Devel::Cycle; + my $test = {fred => [qw(a b c d e)], + ethel => [qw(1 2 3 4 5)], + george => {martha => 23, + agnes => 19} + }; + $test->{george}{phyllis} = $test; + $test->{fred}[3] = $test->{george}; + $test->{george}{mary} = $test->{fred}; + find_cycle($test); + exit 0; + + # output: + + Cycle (1): + $A->{'george'} => \%B + $B->{'phyllis'} => \%A + + Cycle (2): + $A->{'george'} => \%B + $B->{'mary'} => \@A + $A->[3] => \%B + + Cycle (3): + $A->{'fred'} => \@A + $A->[3] => \%B + $B->{'phyllis'} => \%A + + Cycle (4): + $A->{'fred'} => \@A + $A->[3] => \%B + $B->{'mary'} => \@A + + # you can also check weakened references + weaken($test->{george}->{phyllis}); + find_weakened_cycle($test); + exit 0; + + # output: + + Cycle (1): + $A->{'george'} => \%B + $B->{'mary'} => \@C + $C->[3] => \%B + + Cycle (2): + $A->{'george'} => \%B + w-> $B->{'phyllis'} => \%A + + Cycle (3): + $A->{'fred'} => \@C + $C->[3] => \%B + $B->{'mary'} => \@C + + Cycle (4): + $A->{'fred'} => \@C + $C->[3] => \%B + w-> $B->{'phyllis'} => \%A =head1 DESCRIPTION -This is a no-op implementation of Devel::Cycle for PerlOnJava. -The JVM uses tracing garbage collection which handles circular -references natively, so cycle detection is unnecessary. -C and C always report zero cycles. +This is a simple developer's tool for finding circular references in +objects and other types of references. Because of Perl's +reference-count based memory management, circular references will +cause memory leaks. + +=head2 EXPORT + +The find_cycle() and find_weakened_cycle() subroutine are exported by default. + +=over 4 + +=item find_cycle($object_reference,[$callback]) + +The find_cycle() function will traverse the object reference and print +a report to STDOUT identifying any memory cycles it finds. + +If an optional callback code reference is provided, then this callback +will be invoked on each cycle that is found. The callback will be +passed an array reference pointing to a list of lists with the +following format: + + $arg = [ ['REFTYPE',$index,$reference,$reference_value], + ['REFTYPE',$index,$reference,$reference_value], + ['REFTYPE',$index,$reference,$reference_value], + ... + ] + +Each element in the array reference describes one edge in the memory +cycle. 'REFTYPE' describes the type of the reference and is one of +'SCALAR','ARRAY' or 'HASH'. $index is the index affected by the +reference, and is undef for a scalar, an integer for an array +reference, or a hash key for a hash. $reference is the memory +reference, and $reference_value is its dereferenced value. For +example, if the edge is an ARRAY, then the following relationship +holds: + + $reference->[$index] eq $reference_value + +The first element of the array reference is the $object_reference that +you pased to find_cycle() and may not be directly involved in the +cycle. + +If a reference is a weak ref produced using Scalar::Util's weaken() +function then it won't contribute to cycles. + +=item find_weakened_cycle($object_reference,[$callback]) + +The find_weakened_cycle() function will traverse the object reference and print +a report to STDOUT identifying any memory cycles it finds, I any weakened +cycles produced using Scalar::Util's weaken(). + +If an optional callback code reference is provided, then this callback +will be invoked on each cycle that is found. The callback will be +passed an array reference pointing to a list of lists with the +following format: + + $arg = [ ['REFTYPE',$index,$reference,$reference_value,$is_weakened], + ['REFTYPE',$index,$reference,$reference_value,$is_weakened], + ['REFTYPE',$index,$reference,$reference_value,$is_weakened], + ... + ] + +Each element in the array reference describes one edge in the memory +cycle. 'REFTYPE' describes the type of the reference and is one of +'SCALAR','ARRAY' or 'HASH'. $index is the index affected by the +reference, and is undef for a scalar, an integer for an array +reference, or a hash key for a hash. $reference is the memory +reference, and $reference_value is its dereferenced value. $is_weakened +is a boolean specifying if the reference is weakened or not. For +example, if the edge is an ARRAY, then the following relationship +holds: + + $reference->[$index] eq $reference_value + +The first element of the array reference is the $object_reference that +you pased to find_cycle() and may not be directly involved in the +cycle. + +=back + +=head2 Cycle Report Formats + +The default callback prints out a trace of each cycle it finds. You +can control the format of the trace by setting the package variable +$Devel::Cycle::FORMATTING to one of "raw," "cooked," or "roasted". + +The "raw" format prints out anonymous memory references using standard +Perl memory location nomenclature. For example, a "Foo::Bar" object +that points to an ordinary hash will appear in the trace like this: + + Foo::Bar=HASH(0x8124394)->{'phyllis'} => HASH(0x81b4a90) + +The "cooked" format (the default), uses short names for anonymous +memory locations, beginning with "A" and moving upward with the magic +++ operator. This leads to a much more readable display: + + $Foo::Bar=B->{'phyllis'} => \%A + +The "roasted" format is similar to the "cooked" format, except that +object references are formatted slightly differently: + + $Foo::Bar::B->{'phyllis'} => \%A + +If a reference is a weakened ref, then it will have a 'w->' prepended to +it, like this: + + w-> $Foo::Bar::B->{'phyllis'} => \%A + +For your convenience, $Devel::Cycle::FORMATTING can be imported: + + use Devel::Cycle qw(:DEFAULT $FORMATTING); + $FORMATTING = 'raw'; + +Alternatively, you can control the formatting at compile time by +passing one of the options -raw, -cooked, or -roasted to "use" as +illustrated here: + + use Devel::Cycle -raw; + +=head2 Code references (closures) + +If the PadWalker module is installed, Devel::Cycle will also report +cycles in code closures. If PadWalker is not installed and +Devel::Cycle detects a CODE reference in one of the data structures, +it will warn (once per data structure) that it cannot inspect the CODE +unless PadWalker is available. You can turn this warning off by +passing -quiet to Devel::Cycle at compile time: + + use Devel::Cycle -quiet; + +=head1 SEE ALSO + +L +L +L + +=head1 DEVELOPING + +https://github.com/lstein/Devel-Cycle. Please contribute to the code +base by sending pull requests. Use GitHub for bug reports and feature +requests. + +=head1 AUTHOR + +Lincoln Stein, Elincoln.stein@gmail.comE + +=head1 COPYRIGHT AND LICENSE + +Copyright (C) 2003-2014 by Lincoln Stein + +This library is free software; you can redistribute it and/or modify +it under the same terms as Perl itself, either Perl version 5.8.2 or, +at your option, any later version of Perl 5 you may have available. + =cut diff --git a/src/main/perl/lib/Encode.pm b/src/main/perl/lib/Encode.pm index e89f2b77ce..1558fee88c 100644 --- a/src/main/perl/lib/Encode.pm +++ b/src/main/perl/lib/Encode.pm @@ -17,6 +17,11 @@ our @EXPORT_OK = qw( use XSLoader; XSLoader::load('Encode', $VERSION); +{ + package Encode::utf8; + our @ISA = qw(Encode::Encoding); +} + # Override find_encoding to add Encode::Alias support. # The Java backend only recognises hardcoded charset names. This wrapper # consults Encode::Alias (loaded by modules like Encode::Locale) dynamically. @@ -46,6 +51,8 @@ XSLoader::load('Encode', $VERSION); *find_encoding = sub { my ($name, $skip_external) = @_; return undef unless defined $name; + return $name + if ref($name) && eval { $name->isa('Encode::Encoding') }; # Guard against circular alias chains for the same name return undef if $_resolving{$name}; diff --git a/src/main/perl/lib/Moose/Util/TypeConstraints/Builtins.pm b/src/main/perl/lib/Moose/Util/TypeConstraints/Builtins.pm index 15593a7b3a..220498c1fa 100644 --- a/src/main/perl/lib/Moose/Util/TypeConstraints/Builtins.pm +++ b/src/main/perl/lib/Moose/Util/TypeConstraints/Builtins.pm @@ -14,6 +14,11 @@ sub as { goto &Moose::Util::TypeConstraints::as } sub where (&) { goto &Moose::Util::TypeConstraints::where } sub inline_as (&) { goto &Moose::Util::TypeConstraints::inline_as } +sub _RegexpRef { + my $type = Scalar::Util::reftype($_[0]); + return defined($type) && $type eq 'REGEXP'; +} + sub define_builtins { my $registry = shift; diff --git a/src/test/java/org/perlonjava/frontend/parser/FutureAsyncAwaitRuntimeTest.java b/src/test/java/org/perlonjava/frontend/parser/FutureAsyncAwaitRuntimeTest.java index 136be2dde4..191227e7e6 100644 --- a/src/test/java/org/perlonjava/frontend/parser/FutureAsyncAwaitRuntimeTest.java +++ b/src/test/java/org/perlonjava/frontend/parser/FutureAsyncAwaitRuntimeTest.java @@ -65,6 +65,8 @@ class FutureAsyncAwaitRuntimeTest { private static final String PROGRAM = """ use strict; use warnings; + use feature 'defer'; + no warnings 'experimental::defer'; BEGIN { package Future; @@ -195,6 +197,12 @@ unless join(',', @{$pair_result->AWAIT_GET}) eq die "await at string-eval level was not rejected\n" unless $string_eval_error =~ /^await is not allowed inside string eval /; + eval q{ sub { await $_[0] } }; + my $ordinary_sub_eval_error = $@; + die "await in ordinary sub inside string eval used wrong diagnostic\n" + unless $ordinary_sub_eval_error =~ + /^Cannot 'await' outside of an 'async sub' /; + async sub add_one { my $value = await $_[0]; return $value + 1; @@ -245,6 +253,253 @@ unless join(',', @{$pair_result->AWAIT_GET}) eq unless $abandonment_warning =~ /^Suspended async sub main::abandonment_outer lost its returning future /; + my $self_abandoned_result; + async sub self_abandon { + my ($first_pending, $second_pending) = @_; + await $first_pending; + undef $self_abandoned_result; + await $second_pending; + } + my $self_abandon_first = Future->new; + my $self_abandon_second = Future->new; + $self_abandoned_result = self_abandon( + $self_abandon_first, $self_abandon_second); + my $self_abandon_warning = ''; + { + local $SIG{__WARN__} = sub { $self_abandon_warning .= join '', @_ }; + $self_abandon_first->AWAIT_DONE(1); + } + die "self-abandoned async warning bypassed localized warning handler\n" + unless $self_abandon_warning =~ + /^Suspended async sub main::self_abandon lost its returning future /; + $self_abandon_second->AWAIT_CANCEL; + + my $self_failed_result; + async sub self_abandon_and_die { + await $_[0]; + undef $self_failed_result; + die "Oopsie\n"; + } + my $self_fail_pending = Future->new; + $self_failed_result = self_abandon_and_die($self_fail_pending); + my $self_fail_warning = ''; + { + local $SIG{__WARN__} = sub { $self_fail_warning .= join '', @_ }; + $self_fail_pending->AWAIT_DONE(1); + } + die "self-abandoned async failure bypassed localized warning handler\n" + unless $self_fail_warning =~ + /^Abandoned async sub main::self_abandon_and_die failed: Oopsie$/m; + + my $queued_abandon_warning = ''; + my $queued_victim_result; + async sub queued_abandon_victim { + await $_[0]; + return 7; + } + async sub queued_abandon_driver { + my ($driver_pending, $victim_pending) = @_; + local $SIG{__WARN__} = sub { + $queued_abandon_warning .= join '', @_; + }; + await $driver_pending; + $victim_pending->AWAIT_DONE(1); + undef $queued_victim_result; + return 1; + } + my $queued_driver_pending = Future->new; + my $queued_victim_pending = Future->new; + $queued_victim_result = queued_abandon_victim($queued_victim_pending); + my $queued_driver_result = queued_abandon_driver( + $queued_driver_pending, $queued_victim_pending); + $queued_driver_pending->AWAIT_DONE(1); + my @queued_abandon_warnings = + ($queued_abandon_warning =~ + /Suspended async sub main::queued_abandon_victim lost its returning future /g); + die "queued pre-resume abandonment did not use its localized warning handler exactly once\n" + unless @queued_abandon_warnings == 1 + && $queued_driver_result->AWAIT_GET == 1; + + async sub queued_warning_policy_driver { + my ($driver_pending, $victim_pending, $handler, $abandon) = @_; + local $SIG{__WARN__} = $handler; + await $driver_pending; + $victim_pending->AWAIT_DONE(1); + $abandon->(); + return 1; + } + + my $ignore_result; + async sub retained_ignore_victim { + await $_[0]; + return 1; + } + my $ignore_driver_pending = Future->new; + my $ignore_victim_pending = Future->new; + $ignore_result = retained_ignore_victim($ignore_victim_pending); + my $ignore_driver_result = queued_warning_policy_driver( + $ignore_driver_pending, $ignore_victim_pending, 'IGNORE', + sub { undef $ignore_result }); + my $ignore_stderr = ''; + my $ignore_outer_calls = 0; + { + open my $capture, '>', \\$ignore_stderr + or die "cannot capture retained IGNORE stderr: $!\n"; + local *STDERR = $capture; + local $SIG{__WARN__} = sub { ++$ignore_outer_calls }; + $ignore_driver_pending->AWAIT_DONE(1); + } + die "retained IGNORE did not suppress abandonment warning\n" + unless $ignore_stderr eq '' + && $ignore_outer_calls == 0 + && $ignore_driver_result->AWAIT_GET == 1; + + my $default_result; + async sub retained_default_victim { + await $_[0]; + return 1; + } + my $default_driver_pending = Future->new; + my $default_victim_pending = Future->new; + $default_result = retained_default_victim($default_victim_pending); + my $default_driver_result = queued_warning_policy_driver( + $default_driver_pending, $default_victim_pending, 'DEFAULT', + sub { undef $default_result }); + my $default_stderr = ''; + my $default_outer_calls = 0; + my $default_ok = eval { + open my $capture, '>', \\$default_stderr + or die "cannot capture retained DEFAULT stderr: $!\n"; + local *STDERR = $capture; + local $SIG{__WARN__} = sub { ++$default_outer_calls }; + $default_driver_pending->AWAIT_DONE(1); + 1; + }; + my $default_error = $@; + my @default_warnings = + ($default_stderr =~ + /Suspended async sub main::retained_default_victim lost its returning future /g); + die "retained DEFAULT was called as a handler or did not warn once on stderr: $default_error\n" + unless $default_ok && @default_warnings == 1 + && $default_outer_calls == 0 + && $default_driver_result->AWAIT_GET == 1; + + my $undef_result; + async sub retained_undef_victim { + await $_[0]; + return 1; + } + my $undef_driver_pending = Future->new; + my $undef_victim_pending = Future->new; + $undef_result = retained_undef_victim($undef_victim_pending); + my $undef_driver_result = queued_warning_policy_driver( + $undef_driver_pending, $undef_victim_pending, undef, + sub { undef $undef_result }); + my $undef_stderr = ''; + my $undef_outer_calls = 0; + { + open my $capture, '>', \\$undef_stderr + or die "cannot capture retained undef stderr: $!\n"; + local *STDERR = $capture; + local $SIG{__WARN__} = sub { ++$undef_outer_calls }; + $undef_driver_pending->AWAIT_DONE(1); + } + my @undef_warnings = + ($undef_stderr =~ + /Suspended async sub main::retained_undef_victim lost its returning future /g); + die "retained undef warning policy was overridden\n" + unless @undef_warnings == 1 + && $undef_outer_calls == 0 + && $undef_driver_result->AWAIT_GET == 1; + + my $dying_handler_result; + my $dying_handler_calls = 0; + my $dying_handler_cleanups = 0; + my $dying_outer_calls = 0; + # Keep this CODE-policy case after IGNORE, DEFAULT, and undef: the + # sequence verifies that repeated suspended localizations do not + # resurrect a borrowed warning-handler token with released captures. + async sub retained_dying_handler_victim { + my ($first_pending, $second_pending) = @_; + defer { ++$dying_handler_cleanups; } + await $first_pending; + undef $dying_handler_result; + await $second_pending; + } + my $dying_driver_pending = Future->new; + my $dying_first_pending = Future->new; + my $dying_second_pending = Future->new; + $dying_handler_result = retained_dying_handler_victim( + $dying_first_pending, $dying_second_pending); + my $dying_driver_result = queued_warning_policy_driver( + $dying_driver_pending, $dying_first_pending, + sub { + ++$dying_handler_calls; + die "retained warning handler died\n"; + }, + sub { }); + my $dying_ok = eval { + local $SIG{__WARN__} = sub { ++$dying_outer_calls }; + $dying_driver_pending->AWAIT_DONE(1); + 1; + }; + my $dying_error = $@; + die "dying retained warning handler policy mismatch: " + . "ok=" . (defined($dying_ok) ? $dying_ok : 'undef') + . ", error=<$dying_error>, calls=$dying_handler_calls" + . ", outer_calls=$dying_outer_calls" + . ", cleanups=$dying_handler_cleanups\n" + unless !$dying_ok + && $dying_error eq "retained warning handler died\n" + && $dying_handler_calls == 1 + && $dying_outer_calls == 0 + && $dying_handler_cleanups == 1 + && $dying_driver_result->AWAIT_GET == 1; + + async sub suspended_global_policy_driver { + my ($pending, $policy, $invoke) = @_; + local $AsyncPolicy::handler = $policy; + await $pending; + $AsyncPolicy::handler->() if $invoke; + return 1; + } + for my $plain_policy ('IGNORE', 'DEFAULT', undef) { + my $plain_pending = Future->new; + my $plain_result = suspended_global_policy_driver( + $plain_pending, $plain_policy, 0); + $plain_pending->AWAIT_DONE(1); + die "suspended localized package scalar lost plain policy\n" + unless $plain_result->AWAIT_GET == 1; + } + my $global_handler_calls = 0; + my $global_code_pending = Future->new; + my $global_code_result = suspended_global_policy_driver( + $global_code_pending, sub { ++$global_handler_calls }, 1); + $global_code_pending->AWAIT_DONE(1); + die "suspended localized package scalar lost temporary CODE policy\n" + unless $global_handler_calls == 1 + && $global_code_result->AWAIT_GET == 1; + + async sub closure_owned_result { + return await $_[0]; + } + my $closure_owned_pending = Future->new; + my $closure_owner = do { + my $captured_result = closure_owned_result($closure_owned_pending); + sub { $captured_result }; + }; + my $closure_owned_warning = ''; + { + local $SIG{__WARN__} = sub { + $closure_owned_warning .= join '', @_; + }; + $closure_owned_pending->AWAIT_DONE(42); + } + die "closure-owned returning future was falsely abandoned\n" + if $closure_owned_warning ne ''; + die "closure-owned returning future did not complete\n" + unless $closure_owner->()->AWAIT_GET == 42; + my $failed = Future->new; my $failed_result = add_one($failed); $failed->AWAIT_FAIL("await failed\n"); diff --git a/src/test/resources/unit/cli_warning_overrides.t b/src/test/resources/unit/cli_warning_overrides.t index 0f43ec6433..c8098b3b96 100644 --- a/src/test/resources/unit/cli_warning_overrides.t +++ b/src/test/resources/unit/cli_warning_overrides.t @@ -4,7 +4,7 @@ use Test::More; use File::Temp qw(tempdir); use IPC::Open3; -my $skip_launcher = $^X eq 'jperl' && !-f 'target/perlonjava-5.44.0.jar'; +my $skip_launcher = $^X eq 'jperl' && !-f 'target/perlonjava-5.44.1.jar'; my $tmpdir = tempdir(CLEANUP => 1); my $seq = 0; diff --git a/src/test/resources/unit/cpan_bundled_io_shadowing.t b/src/test/resources/unit/cpan_bundled_io_shadowing.t new file mode 100644 index 0000000000..25c8323429 --- /dev/null +++ b/src/test/resources/unit/cpan_bundled_io_shadowing.t @@ -0,0 +1,51 @@ +use strict; +use warnings; + +use Config; +use File::Path qw(make_path); +use File::Temp qw(tempdir); +use Test::More; + +plan skip_all => 'tests PerlOnJava bundled IO precedence during CPAN runs' + unless $Config{archname} =~ /^java-/; + +require PerlOnJava::Process; + +my $shadow = tempdir(CLEANUP => 1); +make_path("$shadow/IO"); + +for my $module (qw(Handle File)) { + open my $fh, '>', "$shadow/IO/$module.pm" + or die "Cannot create shadow IO::$module module: $!"; + print {$fh} "package IO::$module; 1;\n" + or die "Cannot write shadow IO::$module module: $!"; + close $fh or die "Cannot close shadow IO::$module module: $!"; +} + +my $probe = "$shadow/probe.pl"; +open my $probe_fh, '>', $probe or die "Cannot create IO shadow probe: $!"; +print {$probe_fh} <<'PROBE'; +use IO::Handle; +use IO::File; +print "handle=$INC{'IO/Handle.pm'}\n"; +print "file=$INC{'IO/File.pm'}\n"; +exit(IO::File->can('new_tmpfile') ? 0 : 1); +PROBE +close $probe_fh or die "Cannot close IO shadow probe: $!"; + +my $jperl = $ENV{PERLONJAVA_EXECUTABLE}; +ok(defined($jperl) && -x $jperl, 'test runner exposes the jperl launcher'); + +local $ENV{PERL5LIB} = $shadow; +local $ENV{PERLONJAVA_PREFER_BUNDLED_MODULES} = 'IO/Handle.pm,IO/File.pm'; +my $result = PerlOnJava::Process::run_process( + argv => [ $jperl, $probe ], + timeout => 60, +); +ok(!$result->{timed_out} && $result->{exit_code} == 0, + 'bounded child jperl keeps bundled IO modules ahead of CPAN blib shadows'); + +is($result->{output}, "handle=jar:PERL5LIB/IO/Handle.pm\nfile=jar:PERL5LIB/IO/File.pm\n", + 'IO::Handle and IO::File both resolve to bundled sources'); + +done_testing; diff --git a/src/test/resources/unit/devel_cycle.t b/src/test/resources/unit/devel_cycle.t new file mode 100644 index 0000000000..0cfcc2fc11 --- /dev/null +++ b/src/test/resources/unit/devel_cycle.t @@ -0,0 +1,54 @@ +use strict; +use warnings; +use Test::More; +use Scalar::Util qw(weaken); +use Devel::Cycle qw(find_cycle find_weakened_cycle); + +my $self_cycle = {}; +$self_cycle->{self} = $self_cycle; +my @cycles; +find_cycle($self_cycle, sub { push @cycles, shift }); +is(scalar @cycles, 1, 'find_cycle detects a hash self-cycle'); +is_deeply( + [ map { [ $_->[0], $_->[1] ] } @{ $cycles[0] } ], + [ [ 'HASH', 'self' ] ], + 'callback describes the hash edge in the cycle', +); + +my ($left, $right); +$left = \$right; +$right = \$left; +@cycles = (); +find_cycle($left, sub { push @cycles, shift }); +is(scalar @cycles, 1, 'find_cycle detects a scalar-reference cycle'); +is_deeply( + [ map { $_->[0] } @{ $cycles[0] } ], + [ 'SCALAR', 'SCALAR' ], + 'scalar-reference cycle reports both scalar edges', +); + +my $weak_cycle = {}; +$weak_cycle->{self} = $weak_cycle; +weaken($weak_cycle->{self}); +@cycles = (); +find_cycle($weak_cycle, sub { push @cycles, shift }); +is(scalar @cycles, 0, 'find_cycle excludes weak edges'); +find_weakened_cycle($weak_cycle, sub { push @cycles, shift }); +is(scalar @cycles, 1, 'find_weakened_cycle includes weak edges'); +ok($cycles[0][0][4], 'weakened cycle marks the weak edge'); + +ok(Devel::Cycle::HAVE_PADWALKER(), + 'PadWalker is available for closure inspection'); +my $closure_cycle = {}; +my $closure = sub { return $closure_cycle }; +$closure_cycle->{closure} = $closure; +@cycles = (); +find_cycle($closure_cycle, sub { push @cycles, shift }); +is(scalar @cycles, 1, 'find_cycle detects a captured lexical cycle'); +is_deeply( + [ map { $_->[0] } @{ $cycles[0] } ], + [ 'HASH', 'CODE', 'SCALAR' ], + 'closure cycle reports hash, captured-code, and lexical-scalar edges', +); + +done_testing; diff --git a/src/test/resources/unit/encode_find_encoding_class.t b/src/test/resources/unit/encode_find_encoding_class.t new file mode 100644 index 0000000000..7ca16ad7d2 --- /dev/null +++ b/src/test/resources/unit/encode_find_encoding_class.t @@ -0,0 +1,21 @@ +use strict; +use warnings; +use utf8; + +use Encode (); +use Scalar::Util qw(refaddr); +use Test::More; + +my $encoding = Encode::find_encoding('UTF-8'); +isa_ok($encoding, 'Encode::utf8', 'UTF-8 lookup returns the standard encoding subclass'); +is($encoding->name, 'utf-8-strict', 'UTF-8 encoding has the standard canonical name'); +is($encoding->mime_name, 'UTF-8', 'UTF-8 encoding has the standard MIME name'); + +my $alias = Encode::find_encoding('utf8'); +isa_ok($alias, 'Encode::utf8', 'UTF-8 alias returns the same standard subclass'); +is(refaddr(Encode::find_encoding($encoding)), refaddr($encoding), + 'looking up an existing encoding object preserves its identity'); +is($alias->decode("\xC3\xA9"), "é", 'UTF-8 subclass inherits decoding behavior'); +is($alias->encode("é"), "\xC3\xA9", 'UTF-8 subclass inherits encoding behavior'); + +done_testing; diff --git a/src/test/resources/unit/foreach_undef_readonly_alias.t b/src/test/resources/unit/foreach_undef_readonly_alias.t new file mode 100644 index 0000000000..a829bc4894 --- /dev/null +++ b/src/test/resources/unit/foreach_undef_readonly_alias.t @@ -0,0 +1,49 @@ +use strict; +use warnings; +use Test::More; + +my @numbers = 2 .. 4; +my @letters = qw(b c d); + +my $got = eval { + for my $item (@letters, undef, @numbers) { + ++$item; + } + 1; +}; + +is($got, undef, 'modifying an undef rvalue through a foreach alias fails'); +like($@, qr/^Modification of a read-only value attempted/, + 'foreach alias reports the standard read-only error'); +is("@letters", 'c d e', 'values before the undef rvalue were modified'); +is("@numbers", '2 3 4', 'values after the undef rvalue were not modified'); + +for my $item (@numbers[0, 1, 0]) { + ++$item; +} +is("@numbers", '4 4 4', 'array slices remain foreach lvalues'); + +for (3) { + eval { ${\$_} = 4 }; + like($@, qr/^Modification of a read-only value attempted/, + 'a reference to a numeric literal foreach alias remains read-only'); + is($_, 3, 'failed reference assignment preserves the numeric literal'); +} + +for ('literal') { + eval { ${\$_} = 'changed' }; + like($@, qr/^Modification of a read-only value attempted/, + 'a reference to a string literal foreach alias remains read-only'); + is($_, 'literal', 'failed reference assignment preserves the string literal'); +} + +for (undef) { + is(\$_, \undef, + 'a canonical undef foreach alias retains reference identity'); + eval { ${\$_} = 'changed' }; + like($@, qr/^Modification of a read-only value attempted/, + 'a reference to an undef foreach alias remains read-only'); + ok(!defined($_), 'failed reference assignment preserves undef'); +} + +done_testing; diff --git a/src/test/resources/unit/interpreter_dbic_regressions.t b/src/test/resources/unit/interpreter_dbic_regressions.t index bf2de74c1b..c1ef37ad2b 100644 --- a/src/test/resources/unit/interpreter_dbic_regressions.t +++ b/src/test/resources/unit/interpreter_dbic_regressions.t @@ -6,7 +6,7 @@ use File::Temp qw(tempfile); my $is_jperl = $^X eq 'jperl' || $^X =~ m{(?:^|[\\/])jperl(?:\.bat)?$}; my $skip_launcher = !$is_jperl || $^O eq 'MSWin32' - || !-f 'target/perlonjava-5.44.0.jar'; + || !-f 'target/perlonjava-5.44.1.jar'; sub run_interpreter_child { my ($code) = @_; diff --git a/src/test/resources/unit/local_nested_loop_control_restore.t b/src/test/resources/unit/local_nested_loop_control_restore.t new file mode 100644 index 0000000000..28e157390f --- /dev/null +++ b/src/test/resources/unit/local_nested_loop_control_restore.t @@ -0,0 +1,61 @@ +use strict; +use warnings; +use Test::More tests => 8; + +our $value = 'outer'; +my @seen; + +OUTER: for my $iteration (2, 1) { + { + local $value = "$value-next"; + push @seen, $value; + next OUTER unless $iteration == 1; + } + push @seen, "after:$value"; +} + +is_deeply( + \@seen, + [ 'outer-next', 'outer-next', 'after:outer' ], + 'next unwinds a nested local before the next iteration', +); +is($value, 'outer', 'next leaves the package variable restored'); + +my $redo_count = 0; +my @redo_seen; +REDO_LOOP: while (1) { + { + local $value = "$value-redo"; + push @redo_seen, $value; + redo REDO_LOOP if $redo_count++ == 0; + } + last REDO_LOOP; +} + +is_deeply( + \@redo_seen, + [ 'outer-redo', 'outer-redo' ], + 'redo unwinds a nested local before restarting the body', +); +is($value, 'outer', 'redo leaves the package variable restored'); + +LAST_LOOP: while (1) { + { + local $value = 'inner-last'; + last LAST_LOOP; + } +} + +is($value, 'outer', 'last unwinds a nested local while exiting the loop'); + +my $postfix_count = 0; +POSTFIX_LOOP: while ($postfix_count++ < 2) { + { + local $value = "$value-postfix"; + next POSTFIX_LOOP unless $postfix_count == 2; + } +} + +is($value, 'outer', 'postfix labeled next unwinds a nested local'); +is($postfix_count, 3, 'postfix labeled next retains normal loop progress'); +is(scalar(@redo_seen), 2, 'redo executes exactly two iterations'); diff --git a/src/test/resources/unit/moose_regexpref_constraint.t b/src/test/resources/unit/moose_regexpref_constraint.t new file mode 100644 index 0000000000..3718d310f8 --- /dev/null +++ b/src/test/resources/unit/moose_regexpref_constraint.t @@ -0,0 +1,43 @@ +use strict; +use warnings; + +use Config; +use Scalar::Util qw(reftype); +use Test::More; + +my $regexp = qr/foo/; +is(reftype($regexp), 'REGEXP', 'compiled regex has the standard REGEXP reference type'); + +my $blessed_regexp = qr/bar/; +bless $blessed_regexp, 'Local::BlessedRegexp'; +is(reftype($blessed_regexp), 'REGEXP', 'blessing preserves the underlying REGEXP type'); + +SKIP: { + skip 'bundled Moose constraint is available under PerlOnJava', 6 + unless $Config{archname} =~ /^java-/; + + require Moose::Util::TypeConstraints; + my $type = Moose::Util::TypeConstraints::find_type_constraint('RegexpRef'); + ok($type, 'bundled Moose registers RegexpRef'); + ok($type->check($regexp), 'RegexpRef accepts a compiled regex'); + ok($type->check($blessed_regexp), 'RegexpRef accepts a blessed compiled regex'); + ok(!$type->check('foo'), 'RegexpRef rejects a string'); + + my $compiled = eval q{ + package Local::RegexpHolder; + use Moose; + use Moose::Util::TypeConstraints qw(as subtype); + subtype 'Local::RegexpRef' => as 'RegexpRef'; + has pattern => (is => 'ro', isa => 'Local::RegexpRef'); + __PACKAGE__->meta->make_immutable; + 1; + }; + ok($compiled, 'Moose compiles an immutable accessor using a RegexpRef subtype') + or diag($@); + + my $holder = eval { Local::RegexpHolder->new(pattern => qr/baz/) }; + ok($holder && reftype($holder->pattern) eq 'REGEXP', + 'generated constructor executes the inlined RegexpRef helper'); +} + +done_testing; diff --git a/src/test/resources/unit/named_sub_lexical_capture.t b/src/test/resources/unit/named_sub_lexical_capture.t new file mode 100644 index 0000000000..462765b71d --- /dev/null +++ b/src/test/resources/unit/named_sub_lexical_capture.t @@ -0,0 +1,46 @@ +use strict; +use warnings; +use Test::More; + +my $counter = 40; +sub increment_captured_counter { ++$counter } + +is(increment_captured_counter(), 41, + 'named package sub captures a surrounding lexical'); +is($counter, 41, + 'named package sub updates the original lexical cell'); + +my $STDERR; +sub duplicate_stderr_lexically { + $STDERR ||= do { + open my $copy, '>&', STDERR or die "Cannot duplicate STDERR: $!"; + $copy; + }; +} + +my $stderr_copy = duplicate_stderr_lexically(); +ok($stderr_copy, 'named package sub initializes a special-named lexical'); +is($STDERR, $stderr_copy, + 'named package sub updates the original special-named lexical cell'); +is(duplicate_stderr_lexically(), $stderr_copy, + 'named package sub reuses the captured lexical value'); + +my $stderr_scalar_slot = *main::STDERR{SCALAR}; +ok(!defined $$stderr_scalar_slot, + 'captured lexical does not populate the STDERR glob scalar slot'); + +package NamedSubCapture::Globals; +our $value = 'global'; + +package main; +sub read_lexically_scoped_our { $value } + +is(read_lexically_scoped_our(), 'global', + 'named package sub retains the package associated with our'); +{ + local $NamedSubCapture::Globals::value = 'localized'; + is(read_lexically_scoped_our(), 'localized', + 'our remains a dynamic package lookup inside named package sub'); +} + +done_testing; diff --git a/src/test/resources/unit/recursion_depth_1000.t b/src/test/resources/unit/recursion_depth_1000.t new file mode 100644 index 0000000000..805ec7b5ef --- /dev/null +++ b/src/test/resources/unit/recursion_depth_1000.t @@ -0,0 +1,14 @@ +use strict; +use warnings; +use Test::More tests => 1; + +no warnings 'recursion'; + +sub descend { + my ($remaining) = @_; + return 0 unless $remaining; + return 1 + descend($remaining - 1); +} + +is(descend(1000), 1000, + 'ordinary Perl recursion reaches an ecosystem guard depth of 1000'); diff --git a/src/test/resources/unit/ref_utf8_flag.t b/src/test/resources/unit/ref_utf8_flag.t new file mode 100644 index 0000000000..30a0490b13 --- /dev/null +++ b/src/test/resources/unit/ref_utf8_flag.t @@ -0,0 +1,64 @@ +use strict; +use warnings; +use Test::More; +use Scalar::Util qw(blessed); + +ok(!utf8::is_utf8(ref 1), 'ref of a non-reference is a byte string'); +ok(!utf8::is_utf8(ref []), 'built-in reference type is a byte string'); + +my $object = bless {}, 'TestApp::Controller::Action::Path'; +my $class = ref $object; +ok(!utf8::is_utf8($class), 'ASCII blessed class name is a byte string'); +my $blessed_class = blessed $object; +ok(!utf8::is_utf8($blessed_class), + 'Scalar::Util::blessed returns an ASCII class name as a byte string'); + +my $prefix = lc $class; +$prefix =~ s/^testapp::controller:://; +$prefix =~ s/::/\//g; +my $raw_utf8 = pack('C*', 0xC3, 0xA5, 0xC3, 0xA4, 0xC3, 0xB6); +my $path = join '/', $prefix, $raw_utf8; +ok(!utf8::is_utf8($path), 'joining ref-derived prefix preserves byte semantics'); +is(unpack('H*', $path), + '616374696f6e2f706174682fc3a5c3a4c3b6', + 'ref-derived path retains the original UTF-8 octets'); + +my $blessed_prefix = lc $blessed_class; +$blessed_prefix =~ s/^testapp::controller:://; +$blessed_prefix =~ s/::/\//g; +my $blessed_path = join '/', $blessed_prefix, $raw_utf8; +ok(!utf8::is_utf8($blessed_path), + 'joining blessed-derived prefix preserves byte semantics'); +is(unpack('H*', $blessed_path), + '616374696f6e2f706174682fc3a5c3a4c3b6', + 'blessed-derived path retains the original UTF-8 octets'); + +my $component = 'TestApp::Controller::Action::Path'; +if ($component =~ /^.+?::([MVC]|Model|View|Controller)::(.+)$/) { + ok(!utf8::is_utf8($2), + 'regex capture from a byte string remains a byte string'); + my $captured_prefix = lc $2; + $captured_prefix =~ s{::}{/}g; + ok(!utf8::is_utf8($captured_prefix), + 'lowercasing a byte regex capture preserves byte semantics'); + my $captured_path = join '/', $captured_prefix, $raw_utf8; + ok(!utf8::is_utf8($captured_path), + 'joining a capture-derived prefix preserves byte semantics'); + is(unpack('H*', $captured_path), + '616374696f6e2f706174682fc3a5c3a4c3b6', + 'capture-derived path retains the original UTF-8 octets'); +} else { + fail('component class matched the expected shape') for 1 .. 4; +} + +my $unicode_class; +{ + use utf8; + $unicode_class = ref bless {}, "Ångström"; +} +ok(utf8::is_utf8($unicode_class), + 'Unicode blessed class name retains its UTF-8 flag'); +ok(utf8::is_utf8(blessed bless {}, $unicode_class), + 'blessed retains the UTF-8 flag on a Unicode class name'); + +done_testing; diff --git a/src/test/resources/unit/regex/re_debug_fatal_free_order.t b/src/test/resources/unit/regex/re_debug_fatal_free_order.t index 51e19c302c..8f2e0de133 100644 --- a/src/test/resources/unit/regex/re_debug_fatal_free_order.t +++ b/src/test/resources/unit/regex/re_debug_fatal_free_order.t @@ -5,7 +5,7 @@ use IPC::Open3; use Symbol qw(gensym); my $skip_launcher = $^X eq 'jperl' - && !-f 'target/perlonjava-5.44.0.jar'; + && !-f 'target/perlonjava-5.44.1.jar'; sub run_child { my ($code) = @_; diff --git a/src/test/resources/unit/require_overloaded_die_stringification.t b/src/test/resources/unit/require_overloaded_die_stringification.t new file mode 100644 index 0000000000..46c3c59a26 --- /dev/null +++ b/src/test/resources/unit/require_overloaded_die_stringification.t @@ -0,0 +1,26 @@ +use strict; +use warnings; +use File::Spec; +use File::Temp qw(tempdir); +use Test::More tests => 5; + +my $dir = tempdir(CLEANUP => 1); +my $module = File::Spec->catfile($dir, 'OverloadedRequireDie.pm'); + +open my $fh, '>', $module or die "open $module: $!"; +print {$fh} <<'MODULE'; +package OverloadedRequireDie; +use overload '""' => sub { 'OVERLOADED-REQUIRE-DIE' }, fallback => 1; +BEGIN { die bless {}, __PACKAGE__ } +1; +MODULE +close $fh or die "close $module: $!"; + +local @INC = ($dir, @INC); +my $loaded = eval { require OverloadedRequireDie; 1 }; + +ok(!$loaded, 'require fails when a BEGIN block dies'); +like($@, qr/OVERLOADED-REQUIRE-DIE/, 'require stringifies an overloaded exception object'); +unlike($@, qr/=HASH\(/, 'require does not expose the exception reference identity'); +like($@, qr/BEGIN failed--compilation aborted/, 'require keeps the compilation diagnostic'); +like($@, qr/Compilation failed in require/, 'require keeps the require failure suffix'); diff --git a/src/test/resources/unit/sort_scalar_context.t b/src/test/resources/unit/sort_scalar_context.t new file mode 100644 index 0000000000..0d98f28ea3 --- /dev/null +++ b/src/test/resources/unit/sort_scalar_context.t @@ -0,0 +1,39 @@ +use strict; +use warnings; +no warnings 'void'; +use Test::More; + +my $comparisons = 0; +my $sorted = sort { + $comparisons++; + $a cmp $b; +} qw(c b a); +ok(!defined($sorted), 'sort returns undef in scalar context'); +is($comparisons, 0, 'sort comparator is not called in scalar context'); + +my $evaluations = 0; +my $side_effect_result = sort ($evaluations++, 3, 2); +ok(!defined($side_effect_result), + 'scalar sort remains undef with an evaluated input expression'); +is($evaluations, 1, 'scalar sort still evaluates its input expression'); + +my $sin_result; +{ + no warnings; + local $_ = '3 2 1'; + $sin_result = sin sort split; +} +is($sin_result, 0, + 'scalar unary operator accepts the undef result of scalar sort'); + +my $reverse_comparisons = 0; +my $reversed = reverse sort { + $reverse_comparisons++; + $a cmp $b; +} qw(c b a); +is($reversed, 'cba', + 'scalar reverse evaluates its sort operand in list context'); +is($reverse_comparisons, 2, + 'sort comparator runs when nested under scalar reverse'); + +done_testing; diff --git a/src/test/resources/unit/threads_lazy_compile_snapshot_race.t b/src/test/resources/unit/threads_lazy_compile_snapshot_race.t new file mode 100644 index 0000000000..dfbfb9cd9d --- /dev/null +++ b/src/test/resources/unit/threads_lazy_compile_snapshot_race.t @@ -0,0 +1,84 @@ +use strict; +use warnings; + +use Test::More tests => 8; +use threads; +use threads::shared; + +# Keep a sizeable set of named subs lazy until the first child runs them. The +# parent immediately snapshots more children at the same time; compiling a +# child CV must never mutate metadata underneath that parent snapshot. +my @helpers; +for my $number (0 .. 127) { + my $name = "lazy_snapshot_helper_$number"; + my $definition = "sub $name { $number + shift }"; + eval $definition; + die $@ if $@; + no strict 'refs'; + push @helpers, \&{$name}; +} + +my $race_ready :shared = 0; +my $race_go :shared = 0; + +sub run_lazy_helpers { + my ($seed) = @_; + my $sum = 0; + $sum += $_->($seed) for @helpers; + return $sum; +} + +sub run_lazy_helpers_after_barrier { + my ($seed) = @_; + { + lock($race_ready); + $race_ready = 1; + cond_signal($race_ready); + } + { + lock($race_go); + cond_wait($race_go) until $race_go; + } + return run_lazy_helpers($seed); +} + +# Hold the first worker immediately before its lazy calls, then release it just +# before the parent snapshots five more children. This makes compiler mutation +# and snapshot traversal overlap reliably instead of depending on startup luck. +my @workers = (threads->create(\&run_lazy_helpers_after_barrier, 0)); +{ + lock($race_ready); + cond_wait($race_ready) until $race_ready; +} +{ + lock($race_go); + $race_go = 1; + cond_broadcast($race_go); +} +push @workers, map { threads->create(\&run_lazy_helpers, $_) } 1 .. 5; +my @results = map { $_->join } @workers; +my $base = 127 * 128 / 2; + +is(scalar @results, 6, 'all workers join after concurrent snapshots'); +is($results[$_], $base + 128 * $_, "worker $_ runs every lazy helper") + for 0 .. 5; + +# A child owns its managed-runtime execution lock until its body returns. Keep +# this helper lazy through the child's snapshot so the grandchild has to +# materialize its child-owned source CV while the child waits in join. Lazy +# compilation must not try to acquire that execution lock. +sub nested_lazy_helper { + return 40 + shift; +} + +sub nested_launcher { + return nested_lazy_helper(shift); +} + +sub launch_nested_thread { + my $nested = threads->create(\&nested_launcher, 2); + return $nested->join; +} + +my $outer = threads->create(\&launch_nested_thread); +is($outer->join, 42, 'nested child materializes a lazy source CV without deadlock');