diff --git a/dev/modules/moose_support.md b/dev/modules/moose_support.md index 95f7d2b00..07b1b9da8 100644 --- a/dev/modules/moose_support.md +++ b/dev/modules/moose_support.md @@ -315,28 +315,62 @@ install" scenario — define a distroprefs entry that overrides `pl` / ### Quick-path baseline (Moose 2.4000) -Snapshot from `./jcpan -t Moose` against the current shim: - -| Metric | Value | -|---|---| -| Test files executed | 478 | -| Individual assertions executed | 616 | -| Fully passing files | ~29 | -| Partially passing files | ~44 | -| Compile/load fail (missing `Class::MOP::*`, `Moose::Meta::*`) | ~405 | -| Assertions ok | 370 | -| Assertions fail | 246 | - -The 29 fully-passing files cover BUILDARGS / BUILD chains, immutable -round-trips, anonymous role creation, several Moo↔Moose bug regressions, -the cookbook recipes for basic attribute / inheritance / subtype use, -and the Type::Tiny integration test. The 44 partials include -high-value chunks such as `basics/import_unimport.t` (31/48), +Snapshot history from `./jcpan -t Moose` against the current shim: + +| Metric | Initial shim | After refcount/DESTROY (Apr 2026) | After Phase A + C-mini (Apr 2026) | +|---|---|---|---| +| Test files executed | 478 | 478 | 478 | +| Individual assertions executed | 616 | 616 | **667** | +| Fully passing files | ~29 | 35 | **36** | +| Partially passing files | ~44 | 94 | **98** | +| Compile/load fail (missing `Class::MOP::*`, `Moose::Meta::*`) | ~405 | ~349 | **~344** | +| Assertions ok | 370 | 372 | **419** | +| Assertions fail | 246 | 244 | **248** | + +The initial 29 fully-passing files covered BUILDARGS / BUILD chains, +immutable round-trips, anonymous role creation, several Moo↔Moose bug +regressions, the cookbook recipes for basic attribute / inheritance / +subtype use, and the Type::Tiny integration test. The 44 partials +included high-value chunks such as `basics/import_unimport.t` (31/48), `basics/wrapped_method_cxt_propagation.t` (6/7), and `recipes/basics_point_attributesandsubclassing.t` (28/31). -Phases C/D (real `Class::MOP` and `Moose` ports) should move these -numbers; record the new totals here whenever they shift. +The refcount/DESTROY merge (PRs #565, #566, plus weaken/destroy work) +moved the structural picture meaningfully even though the assertion +total only nudged: ~56 files that previously failed at compile/load +time now run subtests. Most ended up partial rather than fully green +(partials roughly doubled, 44 → 94), but six more files are fully +passing (29 → 35). The shim's per-test infrastructure (BUILD chains, +DEMOLISH ordering, weak refs) is now solid; the remaining failures +are dominated by missing `Class::MOP::*` and `Moose::Meta::*` +introspection APIs. + +**Phase A + Phase C-mini** (this PR) added two pieces: + +- `ExtUtils::HasCompiler` deterministic stub + (`src/main/perl/lib/ExtUtils/HasCompiler.pm`) — always reports "no + compiler", instead of relying on `$Config{usedl}` happening to be + empty. +- `Class::MOP` shim (`src/main/perl/lib/Class/MOP.pm`) — provides + `class_of`, `get_metaclass_by_name`, `store_metaclass_by_name`, + `remove_metaclass_by_name`, `does_metaclass_exist`, + `get_all_metaclasses` (and friends), `get_code_info`, + `is_class_loaded`, `load_class`, `load_first_existing_class`. Returns + "no metaclass" everywhere, which is the correct answer under the + Moose-as-Moo shim. The previous behavior was a hard "Undefined + subroutine &Class::MOP::class_of called" the moment Moo's + `_Utils::_load_module` hit a not-installed dependency on a class + that already had `Moose.pm` loaded. + +Net effect of Phase A + C-mini: **+51 individual assertions now +execute** (616 → 667), **+47 newly pass** (372 → 419), and one more +file goes fully green (35 → 36). The four extra failures are +upstream tests that previously bailed before reaching their assertion +phase and now reach it; none are real regressions. + +Phases C-full / D (real `Class::MOP::Class` instances and a pure-Perl +`Moose` port) should move these numbers further; record the new +totals here whenever they shift. --- @@ -427,17 +461,21 @@ isn't `Class::MOP` itself loads cleanly today. ### Current Status - **Phase 1 — DONE.** B-module subroutine name/stash introspection works. -- **Quick path — not started.** Highest leverage: ships `Moose.pm` shim, immediately unblocks ANSI::Unicode-class modules. -- **Phase A — not started.** Trivial; replace upstream `ExtUtils::HasCompiler` with deterministic stub. -- **Phase B — not started.** Strip XS keys in `WriteMakefile`. -- **Phase C — not started.** Java `Class::MOP::get_code_info` + helpers. -- **Phase D — not started.** Bundle pure-Perl `Class::MOP` and `Moose`. +- **Quick path — DONE.** `Moose.pm` shim ships, ANSI::Unicode-class modules unblocked. +- **Phase A — DONE.** `ExtUtils::HasCompiler` deterministic stub ships at `src/main/perl/lib/ExtUtils/HasCompiler.pm`. +- **Phase B — not started.** Strip XS keys in `WriteMakefile`. (Lower priority while we're not yet trying to install upstream Moose.) +- **Phase C-mini — DONE.** `Class::MOP` shim with `class_of` / `get_metaclass_by_name` / `get_code_info` / `is_class_loaded` and friends; ships at `src/main/perl/lib/Class/MOP.pm`. +- **Phase C-full — not started.** Real `Class::MOP::Class` instances backed by Java helpers (`org.perlonjava.runtime.perlmodule.ClassMOP`). +- **Phase D — not started.** Bundle pure-Perl `Class::MOP::*` and `Moose::*` distributions. - **Phase E — deferred.** Export-flag MAGIC. ### Completed - [x] Phase 1: B-module subroutine name introspection - [x] Verified working dependency tree (Apr 2026) +- [x] Quick path: `Moose.pm` / `Moose::Role` / `Moose::Object` / `Moose::Util::TypeConstraints` shims +- [x] Phase A: `ExtUtils::HasCompiler` deterministic stub +- [x] Phase C-mini: `Class::MOP` shim (no metaclass instances; just enough surface to keep Moo happy) ### Decision needed diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index 48a80f664..ceddafa3e 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -33,14 +33,14 @@ public final class Configuration { * Automatically populated by Gradle/Maven during build. * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String gitCommitId = "ba8021aed"; + public static final String gitCommitId = "12fb0165d"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). * Automatically populated by Gradle/Maven during build. * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String gitCommitDate = "2026-04-26"; + public static final String gitCommitDate = "2026-04-27"; /** * Build timestamp in Perl 5 "Compiled at" format (e.g., "Apr 7 2026 11:20:00"). @@ -48,7 +48,7 @@ public final class Configuration { * Parsed by App::perlbrew and other tools via: perl -V | grep "Compiled at" * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String buildTimestamp = "Apr 26 2026 23:11:17"; + public static final String buildTimestamp = "Apr 27 2026 10:20:21"; // Prevent instantiation private Configuration() { diff --git a/src/main/perl/lib/Class/MOP.pm b/src/main/perl/lib/Class/MOP.pm new file mode 100644 index 000000000..eb8743812 --- /dev/null +++ b/src/main/perl/lib/Class/MOP.pm @@ -0,0 +1,179 @@ +package Class::MOP; + +# PerlOnJava minimal Class::MOP stub. +# +# This is NOT the real Class::MOP. PerlOnJava cannot run Moose's XS +# meta-object protocol, and a full pure-Perl port is a separate, much +# larger effort (Phase D in dev/modules/moose_support.md). +# +# What this stub provides is just enough surface area for Moo's "is +# Moose loaded?" probes to answer truthfully ("no metaclass for that +# class") instead of dying with "Undefined subroutine" the moment our +# Moose shim sets $INC{"Moose.pm"}. That single change unblocks dozens +# of Moo-delegating Moose tests at compile time. +# +# Functions: +# - class_of($name_or_obj) -> undef (no Moose metaclass) +# - get_metaclass_by_name($name) -> undef +# - store_metaclass_by_name($name, $m) -> no-op (returns $m) +# - remove_metaclass_by_name($name) -> no-op +# - get_all_metaclasses() -> () +# - get_all_metaclass_names() -> () +# - get_all_metaclass_instances() -> () +# - get_code_info($cv) -> ($pkg, $name) via B +# - is_class_loaded($name) -> bool, mirrors Class::Load logic +# +# See dev/modules/moose_support.md for the broader plan. + +use strict; +use warnings; + +our $VERSION = '2.2207'; # Match a recent upstream version. + +use Scalar::Util (); + +# --------------------------------------------------------------------------- +# Metaclass registry. Stays empty under the shim — we never construct real +# Class::MOP::Class instances — but accept stores so consumers that try to +# register a metaclass don't blow up. +# --------------------------------------------------------------------------- + +my %METAS; + +sub class_of { + my $thing = shift; + return undef unless defined $thing; + my $name = ref($thing) ? Scalar::Util::blessed($thing) : $thing; + return undef unless defined $name; + return $METAS{$name}; +} + +sub get_metaclass_by_name { + my ($name) = @_; + return undef unless defined $name; + return $METAS{$name}; +} + +sub store_metaclass_by_name { + my ($name, $meta) = @_; + return unless defined $name; + $METAS{$name} = $meta; + return $meta; +} + +sub remove_metaclass_by_name { + my ($name) = @_; + return unless defined $name; + delete $METAS{$name}; + return; +} + +sub does_metaclass_exist { + my ($name) = @_; + return defined $name && exists $METAS{$name}; +} + +sub get_all_metaclasses { %METAS } +sub get_all_metaclass_names { keys %METAS } +sub get_all_metaclass_instances { values %METAS } + +# --------------------------------------------------------------------------- +# get_code_info($cv) — used by Moose, Sub::Identify, and some role-composition +# code to ask "where did this coderef come from?". Answered via B, which on +# PerlOnJava reads packageName/subName off RuntimeCode (see Phase 1). +# --------------------------------------------------------------------------- + +sub get_code_info { + my ($cv) = @_; + return unless ref($cv) eq 'CODE'; + + require B; + my $cvobj = B::svref_2object($cv); + return unless $cvobj; + + my $gv = eval { $cvobj->GV }; + return unless $gv && ref $gv; + + my $stash = eval { $gv->STASH->NAME }; + my $name = eval { $gv->NAME }; + return unless defined $stash && defined $name; + + return ($stash, $name); +} + +# --------------------------------------------------------------------------- +# is_class_loaded — borrowed from Class::Load's logic. Some Moose code asks +# this directly via Class::MOP. We answer based on the package's symbol +# table rather than dragging in Class::Load. +# --------------------------------------------------------------------------- + +sub is_class_loaded { + my ($class) = @_; + return 0 unless defined $class && length $class; + return 0 if $class =~ /(?:\A|::)\z/; + return 0 unless $class =~ /\A[A-Za-z_][\w:]*\z/; + + no strict 'refs'; + my $stash = \%{"${class}::"}; + return 0 unless %$stash; + + # A package is "loaded" if it has $VERSION, @ISA, or any subroutine. + return 1 if defined ${"${class}::VERSION"}; + return 1 if @{"${class}::ISA"}; + for my $sym (keys %$stash) { + next if $sym =~ /::\z/; + my $glob = $stash->{$sym}; + next unless ref \$glob eq 'GLOB'; + return 1 if defined *{$glob}{CODE}; + } + return 0; +} + +# --------------------------------------------------------------------------- +# load_class / load_first_existing_class — minimal pass-throughs to require. +# Some Moose code reaches for these directly. +# --------------------------------------------------------------------------- + +sub load_class { + my ($class) = @_; + return 1 if is_class_loaded($class); + (my $file = "$class.pm") =~ s{::}{/}g; + require $file; + return 1; +} + +sub load_first_existing_class { + my @classes = @_; + for my $class (@classes) { + my $ok = eval { load_class($class); 1 }; + return $class if $ok; + } + require Carp; + Carp::croak("Can't locate any of: @classes"); +} + +1; + +__END__ + +=head1 NAME + +Class::MOP - PerlOnJava minimal shim (no real meta-object protocol) + +=head1 DESCRIPTION + +PerlOnJava ships a small subset of the Class::MOP API to keep Moo's +"is Moose loaded?" probes happy when our Moose shim sets +C<$INC{"Moose.pm"}>. The functions here intentionally answer "no +metaclass" for every class, because under the shim Moose classes are +really Moo classes with no MOP introspection layer. + +For the full meta-object protocol, run on system Perl with the real +Moose / Class::MOP installed. See C for +the longer-term plan. + +=head1 SEE ALSO + +L, L, L + +=cut diff --git a/src/main/perl/lib/ExtUtils/HasCompiler.pm b/src/main/perl/lib/ExtUtils/HasCompiler.pm new file mode 100644 index 000000000..993dbe76b --- /dev/null +++ b/src/main/perl/lib/ExtUtils/HasCompiler.pm @@ -0,0 +1,47 @@ +package ExtUtils::HasCompiler; + +# PerlOnJava deterministic stub for ExtUtils::HasCompiler. +# +# Upstream this module probes the system for a working C compiler / linker +# and reports whether XS code can be built. PerlOnJava cannot build or load +# .so/.dll files (the JVM has no dlopen for native libraries we control), so +# we always answer "no". This is preferable to relying on the upstream +# probe, which on PerlOnJava just happens to return false because +# `$Config{usedl}` is empty — a fragile coincidence. +# +# See dev/modules/moose_support.md (Phase A) for the rationale. + +use strict; +use warnings; + +our $VERSION = '0.025'; + +use Exporter 'import'; + +our @EXPORT_OK = qw( + can_compile_loadable_object + can_compile_static_library + can_compile_extension +); + +our %EXPORT_TAGS = ( all => [@EXPORT_OK] ); + +sub can_compile_loadable_object { 0 } +sub can_compile_static_library { 0 } +sub can_compile_extension { 0 } + +1; + +__END__ + +=head1 NAME + +ExtUtils::HasCompiler - PerlOnJava stub; reports no compiler available. + +=head1 DESCRIPTION + +PerlOnJava cannot build or load XS extensions. This stub answers C<0> for +all probes so distributions that conditionally fall back to pure-Perl +implementations choose that path. + +=cut