Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 62 additions & 24 deletions dev/modules/moose_support.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---

Expand Down Expand Up @@ -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

Expand Down
6 changes: 3 additions & 3 deletions src/main/java/org/perlonjava/core/Configuration.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,22 +33,22 @@ 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").
* Automatically populated by Gradle during build.
* 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() {
Expand Down
179 changes: 179 additions & 0 deletions src/main/perl/lib/Class/MOP.pm
Original file line number Diff line number Diff line change
@@ -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<dev/modules/moose_support.md> for
the longer-term plan.

=head1 SEE ALSO

L<Moose>, L<Class::MOP>, L<Moo>

=cut
47 changes: 47 additions & 0 deletions src/main/perl/lib/ExtUtils/HasCompiler.pm
Original file line number Diff line number Diff line change
@@ -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