diff --git a/dev/cpan-reports/cpan-compatibility-fail.dat b/dev/cpan-reports/cpan-compatibility-fail.dat index 5ef0c8541..adfab6519 100644 --- a/dev/cpan-reports/cpan-compatibility-fail.dat +++ b/dev/cpan-reports/cpan-compatibility-fail.dat @@ -1906,7 +1906,6 @@ Data::Object::Try FAIL 2026-04-21 Data::Object::Types FAIL 2026-04-21 Data::Object::Vars FAIL 2026-04-21 Data::ObjectDriver FAIL 79 0 682/79 subtests failed 2026-05-15 -Data::Perl FAIL 194 193 1/194 subtests failed 2026-04-12 Data::Phrasebook::Loader::Ini FAIL 12 1 11/12 subtests failed 2026-05-09 Data::Printer FAIL 647 621 26/647 subtests failed 2026-05-21 Data::Properties::JSON FAIL 2026-05-18 @@ -1989,7 +1988,6 @@ DateTime::Format::Intl FAIL 2 0 2/2 subtests failed 2026-05-16 DateTime::Format::Japanese FAIL 12 0 293/12 subtests failed 2026-05-10 DateTime::Format::Lite FAIL 1 0 1/1 subtests failed 2026-05-10 DateTime::Format::RelativeTime FAIL 2 0 2/2 subtests failed 2026-05-10 -DateTime::Format::SQLite FAIL 69 68 1/69 subtests failed 2026-05-16 DateTime::Format::Strftimeq FAIL 1 0 1/1 subtests failed 2026-05-10 DateTime::Format::Unicode FAIL 1 0 1/1 subtests failed 2026-05-18 DateTime::Format::Variant FAIL 1 0 3/1 subtests failed 2026-05-17 @@ -2380,7 +2378,6 @@ File::Temp FAIL Configure failed 2026-04-21 File::UserConfig FAIL 2 0 28/2 subtests failed 2026-05-19 File::Util FAIL 395 353 42/395 subtests failed 2026-05-19 File::chmod FAIL 39 30 9/39 subtests failed 2026-04-12 -File::pushd FAIL 47 41 6/47 subtests failed 2026-05-21 FileHandle::Unget FAIL 76 53 23/76 subtests failed 2026-05-17 FileHash::Base FAIL Missing: Fault/Logger.pm 2026-05-11 FileSlurp_12 FAIL 280 258 22/280 subtests failed 2026-05-18 @@ -3786,7 +3783,6 @@ Module::Build::Compat FAIL Unknown test outcome 2026-05-20 Module::Build::Tiny FAIL 5 0 11/5 subtests failed 2026-05-21 Module::Build::XSUtil FAIL 3 1 2/3 subtests failed; 1/2 test programs failed 2026-06-03 Module::CGI::Install FAIL 3 0 71/3 subtests failed 2026-05-11 -Module::CPANfile FAIL Missing: File/pushd.pm 2026-05-20 Module::Collect FAIL 43 42 1/43 subtests failed 2026-05-19 Module::Data FAIL 54 54 2026-05-17 Module::Extract::Namespaces FAIL 14 10 4/14 subtests failed 2026-04-12 @@ -3902,7 +3898,6 @@ MooX::ClassAttribute FAIL 26 24 2/26 subtests failed 2026-05-18 MooX::Commander FAIL Missing: Test/Compile.pm 2026-05-11 MooX::Const FAIL 65 57 8/65 subtests failed 2026-05-12 MooX::File::ConfigDir FAIL 1 0 1/1 subtests failed 2026-05-19 -MooX::HandlesVia FAIL 799 797 2/799 subtests failed 2026-05-10 MooX::Keyword FAIL 1 0 1/1 subtests failed 2026-05-19 MooX::Keyword::Factory FAIL 1 0 1/1 subtests failed 2026-05-17 MooX::Keyword::Field FAIL 1 0 1/1 subtests failed 2026-05-19 @@ -5491,7 +5486,6 @@ SparkX::Form::BasicFields FAIL 38 0 52/38 subtests failed 2026-05-15 SpecioX::XS FAIL 1 0 1/1 subtests failed 2026-05-16 Sphinx::Search FAIL 32 22 10/32 subtests failed 2026-05-17 Spica FAIL 13 2 11/13 subtests failed 2026-05-17 -Spiffy FAIL 198 193 5/198 subtests failed; 3/32 test programs failed 2026-06-03 Spoon FAIL 76 47 29/76 subtests failed 2026-05-18 SpoorForwardHook FAIL 86 86 Missing: Test/SetupTeardown.pm 2026-05-13 Spp FAIL 10 2 8/10 subtests failed 2026-05-18 @@ -5698,7 +5692,6 @@ Test::Assert FAIL Missing: Exception/Base.pm 2026-05-19 Test::Auto FAIL 2026-04-21 Test::BDD::Cucumber FAIL 357 356 1/357 subtests failed 2026-05-17 Test::BDD::Cucumber::StepFile FAIL 357 356 1/357 subtests failed 2026-05-17 -Test::Base FAIL 105/106 test programs failed; 0/0 subtests failed 2026-06-03 Test::Base::Less FAIL 30 26 4/30 subtests failed 2026-05-12 Test::Block FAIL 26 22 4/26 subtests failed; 2/6 test programs failed 2026-06-03 Test::CGI::Multipart FAIL 7 3 4/7 subtests failed 2026-05-12 @@ -5718,7 +5711,6 @@ Test::Database::Temp FAIL 2026-05-15 Test::DatabaseRow FAIL 320 299 21/320 subtests failed 2026-05-17 Test::Deep FAIL 1268 1267 1/1268 subtests failed 2026-05-20 Test::Dependencies FAIL 2 0 4/2 subtests failed 2026-05-11 -Test::Differences FAIL 49 44 5/49 subtests failed; 4/17 test programs failed 2026-06-03 Test::Directory FAIL 50 27 23/50 subtests failed 2026-04-22 Test::DistManifest FAIL 12 4 8/12 subtests failed 2026-05-17 Test::Exception FAIL 9 0 45/9 subtests failed 2026-05-20 @@ -6931,7 +6923,6 @@ re::engine::RE2 FAIL 2 0 125/2 subtests failed 2026-05-17 routines FAIL 2026-04-21 sanity FAIL 22 9 13/22 subtests failed 2026-04-22 self FAIL Missing: B/Hooks/Parser.pm 2026-04-22 -strictures FAIL 5 4 1/5 subtests failed 2026-04-12 subs FAIL 2 0 2/2 subtests failed 2026-05-07 superclass FAIL 49 48 1/49 subtests failed 2026-05-13 threads FAIL Unknown test outcome 2026-05-20 diff --git a/dev/cpan-reports/cpan-compatibility-pass.dat b/dev/cpan-reports/cpan-compatibility-pass.dat index 954f5325d..3cdb15de2 100644 --- a/dev/cpan-reports/cpan-compatibility-pass.dat +++ b/dev/cpan-reports/cpan-compatibility-pass.dat @@ -59,7 +59,7 @@ Algorithm::Backoff PASS 22 22 2026-05-12 b5c01ce1c Algorithm::Backoff::Exponential PASS 22 22 2026-05-18 bd327bb57 Algorithm::C3 PASS 66 66 2026-05-19 b5536d190 Algorithm::Dependency::Source::DBI PASS 3 3 2026-05-17 d8a8a9f34 -Algorithm::Diff PASS 1004 1004 2026-06-03 d0f827461 +Algorithm::Diff PASS 1004 1004 2026-06-09 505082700 Algorithm::Merge PASS 66 66 2026-05-19 b5536d190 Algorithm::NaiveBayes PASS 23 23 2026-05-19 b5536d190 Algorithm::NeedlemanWunsch PASS 71 71 2026-05-17 bd327bb57 @@ -398,7 +398,7 @@ CPAN::Nearest PASS 2 2 2026-05-15 3fe76ed3b CPAN::Perl::Releases::MetaCPAN PASS 1 1 2026-05-12 312b4f902 CPAN::Releases::Latest PASS 9 9 2026-05-10 1e95a0902 CPAN::Repo PASS 2 2 2026-05-17 d8a8a9f34 -CPAN::Requirements::Dynamic PASS 1 1 2026-06-04 d0f827461 +CPAN::Requirements::Dynamic PASS 1 1 2026-06-09 505082700 CPAN::Search::Tester PASS 7 7 2026-05-17 d8a8a9f34 CPAN::Static PASS 32 32 2026-05-12 b5c01ce1c CPAN::Tarball::Patch PASS 1 1 2026-05-12 312b4f902 @@ -467,6 +467,7 @@ Canary::Stability PASS 1 1 2026-06-04 d0f827461 Canella PASS 1 1 2026-05-10 1e95a0902 Captcha::noCAPTCHA PASS 37 37 2026-05-11 b5c01ce1c Captcha::reCAPTCHA PASS 77 77 2026-05-12 b5c01ce1c +Carp::Always PASS 17 17 2026-06-09 505082700 Carp::Clan PASS 116 116 2026-06-04 d0f827461 Carp::POE PASS 2 2 2026-04-21 73edc8aba Catalyst::Authentication::Store::FromSub PASS 1 1 2026-05-08 454bddc34 @@ -484,7 +485,7 @@ CharsetDetector PASS 11 11 2026-05-15 3fe76ed3b Chart::Gnuplot PASS 64 64 2026-05-08 454bddc34 Check::Term::Color PASS 13 13 2026-05-17 bd327bb57 Checkster PASS 12 12 2026-05-17 bd327bb57 -Class::Accessor PASS 139 139 2026-06-04 d0f827461 +Class::Accessor PASS 139 139 2026-06-09 505082700 Class::Accessor::Array PASS 5 5 2026-04-22 3f3336774 Class::Accessor::Array::Glob PASS 3 3 2026-04-22 3f3336774 Class::Accessor::Chained PASS 8 8 2026-04-22 e228c2529 @@ -604,6 +605,7 @@ ConfigReader::Simple PASS 205 205 2026-05-11 b5c01ce1c Const::Fast PASS 26 26 2026-05-18 b5536d190 ConstantCalculus::CircleConstant PASS 1 1 2026-05-10 1e95a0902 Contize PASS 1 1 2026-05-18 b5536d190 +Convert::ASN1 PASS 595 595 2026-06-09 505082700 Convert::Base64 PASS 1 1 2026-05-20 b5536d190 Convert::Moji PASS 16 16 2026-05-11 b5c01ce1c Convert::NLS_DATE_FORMAT PASS 30 30 2026-05-12 b5c01ce1c @@ -654,7 +656,7 @@ DBIx::Class::Bootstrap::Simple PASS 2 2 2026-05-17 d8a8a9f34 DBIx::Class::CryptColumn PASS 31 31 2026-05-10 a0cb33710 DBIx::Class::CustomPrefetch PASS 95 95 2026-05-15 3fe76ed3b DBIx::Class::DeleteAction PASS 68 68 2026-05-11 b5c01ce1c -DBIx::Class::EasyFixture PASS 106 106 2026-05-11 b5c01ce1c +DBIx::Class::EasyFixture PASS 106 106 2026-06-09 505082700 DBIx::Class::ElasticSync PASS 5 5 2026-05-10 1e95a0902 DBIx::Class::EncodeColumns PASS 1 1 2026-05-18 bd327bb57 DBIx::Class::FormTools PASS 95 95 2026-05-18 bd327bb57 @@ -667,7 +669,7 @@ DBIx::Class::InflateColumn::Geo PASS 3 3 2026-05-10 1e95a0902 DBIx::Class::InflateColumn::Math::Currency PASS 15 15 2026-05-10 1e95a0902 DBIx::Class::InflateColumn::Time PASS 15 15 2026-05-10 1e95a0902 DBIx::Class::IntrospectableM2M PASS 3 3 2026-05-10 1e95a0902 -DBIx::Class::Objects PASS 62 62 2026-05-11 b5c01ce1c +DBIx::Class::Objects PASS 62 62 2026-06-09 505082700 DBIx::Class::QueriesTime PASS 1 1 2026-05-10 a0cb33710 DBIx::Class::QueryLog PASS 43 43 2026-05-12 b5c01ce1c DBIx::Class::QueryLog::Conditional PASS 3 3 2026-05-10 1e95a0902 @@ -800,6 +802,7 @@ Data::Monad PASS 71 71 2026-05-12 b5c01ce1c Data::Munge PASS 89 89 2026-05-17 bd327bb57 Data::Page::NoTotalEntries PASS 5 5 2026-04-21 3f3336774 Data::Pairs PASS 179 179 2026-05-19 b5536d190 +Data::Perl PASS 194 194 2026-06-09 505082700 Data::Phrasebook PASS 110 110 2026-05-09 d7916b01c Data::Properties::YAML PASS 10 10 2026-04-21 73edc8aba Data::Record PASS 32 32 2026-05-07 24f4fb162 @@ -824,7 +827,7 @@ Date::Format PASS 1215 1215 2026-06-03 d0f827461 Date::Hijri PASS 3 3 2026-05-15 3fe76ed3b Date::ISO8601 PASS 456 456 2026-05-16 75a25eb21 Date::Leapyear PASS 764 764 2026-05-20 b5536d190 -Date::Manip PASS 4533 4533 2026-06-04 d0f827461 +Date::Manip PASS 4533 4533 2026-06-09 505082700 Date::Parse PASS 1215 1215 2026-06-03 d0f827461 Date::Tolkien::Shire::Data PASS 5558 5558 2026-05-10 1e95a0902 DateStamp PASS 46 46 2026-05-12 93dfa497b @@ -845,7 +848,7 @@ DateTime::Event::WarwickUniversity PASS 67 67 2026-05-17 bd327bb57 DateTime::Format::Atom PASS 4 4 2026-05-12 b5c01ce1c DateTime::Format::Baby PASS 87 87 2026-05-10 1e95a0902 DateTime::Format::Bork PASS 10 10 2026-05-18 bd327bb57 -DateTime::Format::Builder PASS 218 218 2026-05-21 474c0f049 +DateTime::Format::Builder PASS 226 226 2026-06-09 505082700 DateTime::Format::Czech PASS 14 14 2026-05-17 d8a8a9f34 DateTime::Format::DB2 PASS 25 25 2026-05-16 75a25eb21 DateTime::Format::DateManip PASS 7 7 2026-05-19 b5536d190 @@ -874,7 +877,8 @@ DateTime::Format::RFC3339 PASS 33 33 2026-05-16 75a25eb21 DateTime::Format::RFC3501 PASS 12 12 2026-05-18 b5536d190 DateTime::Format::RSS PASS 128 128 2026-05-09 a0cb33710 DateTime::Format::Roman PASS 23 23 2026-05-11 1e95a0902 -DateTime::Format::Strptime PASS 143 143 2026-05-21 474c0f049 +DateTime::Format::SQLite PASS 68 68 2026-06-09 505082700 +DateTime::Format::Strptime PASS 143 143 2026-06-09 505082700 DateTime::Format::Sybase PASS 12 12 2026-05-17 d8a8a9f34 DateTime::Format::W3CDTF PASS 43 43 2026-05-20 b5536d190 DateTime::Format::WindowsFileTime PASS 2 2 2026-05-19 b5536d190 @@ -979,15 +983,15 @@ Exporter::Rinci PASS 1 1 2026-04-22 3f3336774 Exporter::Tidy PASS 44 44 2026-04-12 c1942aad0 ExtJS::Generator::DBIC PASS 7 7 2026-05-14 3fe76ed3b ExtUtils::AutoInstall PASS 6 6 2026-05-18 bd327bb57 -ExtUtils::CBuilder PASS 72 72 2026-06-04 d0f827461 -ExtUtils::Config PASS 16 16 2026-06-04 d0f827461 +ExtUtils::CBuilder PASS 72 72 2026-06-09 505082700 +ExtUtils::Config PASS 16 16 2026-06-09 505082700 ExtUtils::FindFunctions PASS 11 11 2026-05-10 1e95a0902 ExtUtils::HasCompiler PASS 4 4 2026-04-29 82e5e452d -ExtUtils::Helpers PASS 30 30 2026-06-04 d0f827461 +ExtUtils::Helpers PASS 30 30 2026-06-09 505082700 ExtUtils::InstallPAR PASS 3 3 2026-05-13 3fe76ed3b -ExtUtils::InstallPaths PASS 111 111 2026-06-04 d0f827461 +ExtUtils::InstallPaths PASS 111 111 2026-06-09 505082700 ExtUtils::MakeMaker PASS 26 26 2026-04-22 e228c2529 -ExtUtils::MakeMaker::CPANfile PASS 5 5 2026-05-10 1e95a0902 +ExtUtils::MakeMaker::CPANfile PASS 5 5 2026-06-09 505082700 ExtUtils::MakeMaker::META_MERGE::GitHub PASS 57 57 2026-05-11 2b8886eec ExtUtils::ModuleMaker PASS 1589 1589 2026-05-14 3fe76ed3b ExtUtils::PL2Bat PASS 3 3 2026-05-16 75a25eb21 @@ -1052,6 +1056,7 @@ File::Which PASS 21 21 2026-06-04 d0f827461 File::XDG PASS 9 9 2026-05-17 d8a8a9f34 File::chdir PASS 94 94 2026-06-03 d0f827461 File::chmod::Recursive PASS 1 1 2026-05-15 3fe76ed3b +File::pushd PASS 47 47 2026-06-09 505082700 FileCache::Appender PASS 20 20 2026-05-07 24f4fb162 FileSystem::LL::FAT PASS 1 1 2026-05-17 d8a8a9f34 Filesys::Cap PASS 1 1 2026-05-17 d8a8a9f34 @@ -1545,7 +1550,7 @@ List::BinarySearch PASS 28 28 2026-04-28 e3750ee00 List::Categorize PASS 20 20 2026-04-22 3f3336774 List::Cycle PASS 34 34 2026-05-12 312b4f902 List::GroupingPriorityQueue PASS 25 25 2026-05-10 a0cb33710 -List::MoreUtils PASS 4533 4533 2026-06-03 d0f827461 +List::MoreUtils PASS 4533 4533 2026-06-09 505082700 List::PowerSet PASS 14 14 2026-05-06 9bda7d2ed List::Uniq PASS 38 38 2026-05-19 b5536d190 List::Util::WeightedChoice PASS 5 5 2026-05-15 3fe76ed3b @@ -1698,6 +1703,7 @@ Module::Build::Pluggable::ReadmeMarkdownFromPod PASS 1 1 2026-04-30 1766659c7 Module::Build::Prereqs::FromCPANfile PASS 11 11 2026-05-19 b5536d190 Module::Build::Using::PkgConfig PASS 2 2 2026-05-17 d8a8a9f34 Module::Build::WithXSpp PASS 1 1 2026-05-18 b5536d190 +Module::CPANfile PASS 41 41 2026-06-09 505082700 Module::CoreList PASS 102 102 2026-05-20 b5536d190 Module::Depends PASS 20 20 2026-05-10 1e95a0902 Module::Depends::Intrusive PASS 20 20 2026-05-13 3fe76ed3b @@ -1734,6 +1740,7 @@ MooX::BuildArgs PASS 11 11 2026-05-11 b5c01ce1c MooX::ChainedAttributes PASS 9 9 2026-05-11 2b8886eec MooX::Cmd PASS 1 1 2026-05-15 3fe76ed3b MooX::Enumeration PASS 44 44 2026-05-17 d8a8a9f34 +MooX::HandlesVia PASS 799 799 2026-06-09 505082700 MooX::HasEnv PASS 11 11 2026-05-11 b5c01ce1c MooX::LazierAttributes PASS 103 103 2026-05-18 bd327bb57 MooX::LazyRequire PASS 12 12 2026-05-19 b5536d190 @@ -1755,7 +1762,7 @@ MooX::StrictHas PASS 7 7 2026-05-15 3fe76ed3b MooX::Thunking PASS 16 16 2026-05-11 1e95a0902 MooX::TypeTiny PASS 43 43 2026-05-10 a0cb33710 MooX::Types::MooseLike PASS 196 196 2026-05-21 474c0f049 -MooX::Types::MooseLike::Base PASS 169 169 2026-04-12 c1942aad0 +MooX::Types::MooseLike::Base PASS 196 196 2026-06-09 505082700 MooX::Types::MooseLike::DateTime PASS 7 7 2026-05-11 b5c01ce1c MooX::Types::MooseLike::Numeric PASS 43 43 2026-05-16 d8a8a9f34 Mooish PASS 2 2 2026-05-16 75a25eb21 @@ -1835,7 +1842,7 @@ MooseX::Role::DBIC PASS 10 10 2026-05-16 d8a8a9f34 MooseX::Role::Logger PASS 7 7 2026-05-12 312b4f902 MooseX::Role::Matcher PASS 42 42 2026-05-11 2b8886eec MooseX::Role::Nameable PASS 7 7 2026-05-18 bd327bb57 -MooseX::Role::Parameterized PASS 153 153 2026-05-19 b5536d190 +MooseX::Role::Parameterized PASS 153 153 2026-06-09 505082700 MooseX::Role::Pluggable PASS 29 29 2026-05-11 2b8886eec MooseX::Role::WarnOnConflict PASS 7 7 2026-05-11 b5c01ce1c MooseX::Role::WithOverloading PASS 72 72 2026-05-18 bd327bb57 @@ -1934,6 +1941,7 @@ Net::IMAP::Simple PASS 143 143 2026-05-15 3fe76ed3b Net::IP PASS 36069 36069 2026-06-03 d0f827461 Net::IP::Route::Reject PASS 2 2 2026-04-26 1d5def215 Net::IPv6Addr PASS 799 799 2026-05-12 b5c01ce1c +Net::LDAP PASS 789 789 2026-06-09 505082700 Net::LDAP::Makepath PASS 5 5 2026-04-22 3f3336774 Net::MAC PASS 965 965 2026-05-12 93dfa497b Net::MQTT::Constants PASS 157 157 2026-05-18 bd327bb57 @@ -2308,6 +2316,7 @@ RateLimitations::Pluggable PASS 12 12 2026-05-11 2b8886eec RePrec PASS 1 1 2026-05-19 b5536d190 Reddit PASS 1 1 2026-05-12 312b4f902 Redis PASS 4 4 2026-05-19 b5536d190 +Ref::Util PASS 468 468 2026-06-09 505082700 Regexp::Common::Email::Address PASS 7 7 2026-05-12 b5c01ce1c Regexp::Common::time PASS 7202 7202 2026-05-12 b5c01ce1c Regexp::IPv4 PASS 5 5 2026-05-16 75a25eb21 @@ -2477,6 +2486,7 @@ SortKey::date_in_text PASS 1 1 2026-05-18 bd327bb57 SortSpec PASS 1 1 2026-05-10 1e95a0902 SortSpec::Perl::CPAN::ChangesGroup::PERLANCAR PASS 1 1 2026-05-12 b5c01ce1c Spellunker PASS 161 161 2026-05-11 2b8886eec +Spiffy PASS 198 198 2026-06-09 505082700 Spike PASS 46 46 2026-05-18 bd327bb57 Splunklib PASS 6 6 2026-05-10 1e95a0902 Spreadsheet::WriteExcel PASS 1254 1254 2026-06-03 d0f827461 @@ -2614,6 +2624,7 @@ Test2::Util::DistFiles PASS 4 4 2026-05-17 d8a8a9f34 Test::API PASS 32 32 2026-04-12 c1942aad0 Test::AllModules PASS 24 24 2026-05-13 3fe76ed3b Test::Arrow PASS 113 113 2026-05-07 24f4fb162 +Test::Base PASS 435 435 2026-06-09 505082700 Test::Benchmark PASS 36 36 2026-05-11 b5c01ce1c Test::CPAN::Meta PASS 196 196 2026-04-12 c1942aad0 Test::CPAN::Meta::JSON PASS 815 815 2026-05-19 b5536d190 @@ -2632,6 +2643,7 @@ Test::Deep::Fuzzy PASS 12 12 2026-05-17 d8a8a9f34 Test::Deep::JSON PASS 7 7 2026-05-12 312b4f902 Test::Deep::UnorderedPairs PASS 15 15 2026-05-21 474c0f049 Test::DescribeMe PASS 12 12 2026-05-21 474c0f049 +Test::Differences PASS 49 49 2026-06-09 505082700 Test::Dist::VersionSync PASS 47 47 2026-04-26 1d5def215 Test::Distribution PASS 42 42 2026-05-19 b5536d190 Test::ExpectAndCheck PASS 30 30 2026-05-12 93dfa497b @@ -2648,7 +2660,7 @@ Test::HexDifferences PASS 75 75 2026-04-22 3f3336774 Test::HexString PASS 7 7 2026-05-12 b5c01ce1c Test::Identity PASS 10 10 2026-05-10 a0cb33710 Test::InDistDir PASS 6 6 2026-04-12 c1942aad0 -Test::Inter PASS 90 90 2026-06-04 d0f827461 +Test::Inter PASS 90 90 2026-06-09 505082700 Test::JSON PASS 39 39 2026-05-18 bd327bb57 Test::Lives PASS 3 3 2026-05-19 b5536d190 Test::LoadAllModules PASS 5 5 2026-04-22 3f3336774 @@ -2661,7 +2673,7 @@ Test::MockTime::HiRes PASS 216 216 2026-05-15 3fe76ed3b Test::MonkeyMock PASS 34 34 2026-05-16 d8a8a9f34 Test::More::Diagnostic PASS 2 2 2026-05-15 3fe76ed3b Test::More::UTF8 PASS 13 13 2026-05-21 b5536d190 -Test::Most PASS 103 103 2026-06-03 d0f827461 +Test::Most PASS 103 103 2026-06-09 505082700 Test::Name::FromLine PASS 12 12 2026-05-07 24f4fb162 Test::Needs PASS 227 227 2026-05-20 b5536d190 Test::NeedsDisplay PASS 2 2 2026-05-18 bd327bb57 @@ -2670,7 +2682,7 @@ Test::NoWarnings PASS 46 46 2026-06-04 d0f827461 Test::Number::Delta PASS 72 72 2026-04-12 c1942aad0 Test::Object PASS 5 5 2026-06-03 d0f827461 Test::OpenTracing::Interface PASS 31 31 2026-05-08 348368253 -Test::Output PASS 1217 1217 2026-06-03 d0f827461 +Test::Output PASS 1217 1217 2026-06-09 505082700 Test::PAUSE::Permissions PASS 1 1 2026-05-17 d8a8a9f34 Test::PerlTidy PASS 25 25 2026-04-30 1766659c7 Test::Pod PASS 19 19 2026-05-21 474c0f049 @@ -2711,7 +2723,7 @@ Text::Context PASS 30 30 2026-05-10 1e95a0902 Text::Context::EitherSide PASS 12 12 2026-05-10 1e95a0902 Text::Control PASS 166 166 2026-05-08 24f4fb162 Text::DSV PASS 29 29 2026-05-10 a0cb33710 -Text::Diff PASS 33 33 2026-06-03 d0f827461 +Text::Diff PASS 33 33 2026-06-09 505082700 Text::Format PASS 15 15 2026-05-16 75a25eb21 Text::German PASS 34 34 2026-04-12 c1942aad0 Text::Gitignore PASS 42 42 2026-05-18 bd327bb57 @@ -2727,7 +2739,7 @@ Text::Patch PASS 1 1 2026-04-12 c1942aad0 Text::Quote PASS 53 53 2026-04-12 c1942aad0 Text::SimpleTable PASS 10 10 2026-04-12 c1942aad0 Text::SimpleTable::AutoWidth PASS 6 6 2026-04-12 c1942aad0 -Text::Soundex PASS 18 18 2026-04-12 c1942aad0 +Text::Soundex PASS 18 18 2026-06-09 505082700 Text::SpanningTable PASS 20 20 2026-04-21 73edc8aba Text::Table PASS 175 175 2026-05-21 b5536d190 Text::Tags PASS 89 89 2026-04-22 3f3336774 @@ -3280,6 +3292,7 @@ namespace::sweep PASS 42 42 2026-05-12 b5c01ce1c parent PASS 37 37 2026-04-21 73edc8aba rlib PASS 7 7 2026-04-12 c1942aad0 smallnum PASS 72 72 2026-06-03 d0f827461 +strictures PASS 42 42 2026-06-09 505082700 subroutines PASS 1 1 2026-05-08 454bddc34 syntax PASS 4 4 2026-04-12 c1942aad0 threads::shared PASS 80 80 2026-05-20 b5536d190 diff --git a/dev/cpan-reports/cpan-compatibility-skip.dat b/dev/cpan-reports/cpan-compatibility-skip.dat index 3784b6b51..8aca0de17 100644 --- a/dev/cpan-reports/cpan-compatibility-skip.dat +++ b/dev/cpan-reports/cpan-compatibility-skip.dat @@ -1,5 +1,6 @@ AnyDBM_File SKIP 2026-06-04 distroprefs CGI SKIP 2026-06-04 distroprefs +Digest::MD2 SKIP 2026-06-09 bundled File::Slurp SKIP 2026-06-04 distroprefs HTTP::CookieJar::LWP SKIP 2026-06-04 bundled HTTP::Daemon SKIP 2026-06-03 distroprefs diff --git a/dev/cpan-reports/cpan-compatibility.md b/dev/cpan-reports/cpan-compatibility.md index 99b1e3a8e..587436653 100644 --- a/dev/cpan-reports/cpan-compatibility.md +++ b/dev/cpan-reports/cpan-compatibility.md @@ -1,6 +1,6 @@ # CPAN Module Compatibility Report for PerlOnJava -> Auto-generated by `dev/tools/cpan_random_tester.pl` on 2026-06-04 01:42:58 +> Auto-generated by `dev/tools/cpan_random_tester.pl` on 2026-06-09 23:14:00 > > Modules are randomly selected from the full CPAN index and tested > with `./jcpan -t`. Dependencies are tested too; every module that @@ -10,10 +10,10 @@ | Metric | Count | |--------|-------| -| **Modules Tested** | 10242 | -| **Pass** | 3285 (32.1%) | -| **Fail** | 6941 | -| **Skipped** | 16 | +| **Modules Tested** | 10247 | +| **Pass** | 3298 (32.2%) | +| **Fail** | 6932 | +| **Skipped** | 17 | ## Modules That Pass All Tests @@ -80,7 +80,7 @@ | Algorithm::Backoff::Exponential | 22 | 2026-05-18 | bd327bb57 | | Algorithm::C3 | 66 | 2026-05-19 | b5536d190 | | Algorithm::Dependency::Source::DBI | 3 | 2026-05-17 | d8a8a9f34 | -| Algorithm::Diff | 1004 | 2026-06-03 | d0f827461 | +| Algorithm::Diff | 1004 | 2026-06-09 | 505082700 | | Algorithm::Merge | 66 | 2026-05-19 | b5536d190 | | Algorithm::NaiveBayes | 23 | 2026-05-19 | b5536d190 | | Algorithm::NeedlemanWunsch | 71 | 2026-05-17 | bd327bb57 | @@ -419,7 +419,7 @@ | CPAN::Perl::Releases::MetaCPAN | 1 | 2026-05-12 | 312b4f902 | | CPAN::Releases::Latest | 9 | 2026-05-10 | 1e95a0902 | | CPAN::Repo | 2 | 2026-05-17 | d8a8a9f34 | -| CPAN::Requirements::Dynamic | 1 | 2026-06-04 | d0f827461 | +| CPAN::Requirements::Dynamic | 1 | 2026-06-09 | 505082700 | | CPAN::Search::Tester | 7 | 2026-05-17 | d8a8a9f34 | | CPAN::Static | 32 | 2026-05-12 | b5c01ce1c | | CPAN::Tarball::Patch | 1 | 2026-05-12 | 312b4f902 | @@ -488,6 +488,7 @@ | Canella | 1 | 2026-05-10 | 1e95a0902 | | Captcha::noCAPTCHA | 37 | 2026-05-11 | b5c01ce1c | | Captcha::reCAPTCHA | 77 | 2026-05-12 | b5c01ce1c | +| Carp::Always | 17 | 2026-06-09 | 505082700 | | Carp::Clan | 116 | 2026-06-04 | d0f827461 | | Carp::POE | 2 | 2026-04-21 | 73edc8aba | | Catalyst::Authentication::Store::FromSub | 1 | 2026-05-08 | 454bddc34 | @@ -505,7 +506,7 @@ | Chart::Gnuplot | 64 | 2026-05-08 | 454bddc34 | | Check::Term::Color | 13 | 2026-05-17 | bd327bb57 | | Checkster | 12 | 2026-05-17 | bd327bb57 | -| Class::Accessor | 139 | 2026-06-04 | d0f827461 | +| Class::Accessor | 139 | 2026-06-09 | 505082700 | | Class::Accessor::Array | 5 | 2026-04-22 | 3f3336774 | | Class::Accessor::Array::Glob | 3 | 2026-04-22 | 3f3336774 | | Class::Accessor::Chained | 8 | 2026-04-22 | e228c2529 | @@ -625,6 +626,7 @@ | Const::Fast | 26 | 2026-05-18 | b5536d190 | | ConstantCalculus::CircleConstant | 1 | 2026-05-10 | 1e95a0902 | | Contize | 1 | 2026-05-18 | b5536d190 | +| Convert::ASN1 | 595 | 2026-06-09 | 505082700 | | Convert::Base64 | 1 | 2026-05-20 | b5536d190 | | Convert::Moji | 16 | 2026-05-11 | b5c01ce1c | | Convert::NLS_DATE_FORMAT | 30 | 2026-05-12 | b5c01ce1c | @@ -675,7 +677,7 @@ | DBIx::Class::CryptColumn | 31 | 2026-05-10 | a0cb33710 | | DBIx::Class::CustomPrefetch | 95 | 2026-05-15 | 3fe76ed3b | | DBIx::Class::DeleteAction | 68 | 2026-05-11 | b5c01ce1c | -| DBIx::Class::EasyFixture | 106 | 2026-05-11 | b5c01ce1c | +| DBIx::Class::EasyFixture | 106 | 2026-06-09 | 505082700 | | DBIx::Class::ElasticSync | 5 | 2026-05-10 | 1e95a0902 | | DBIx::Class::EncodeColumns | 1 | 2026-05-18 | bd327bb57 | | DBIx::Class::FormTools | 95 | 2026-05-18 | bd327bb57 | @@ -688,7 +690,7 @@ | DBIx::Class::InflateColumn::Math::Currency | 15 | 2026-05-10 | 1e95a0902 | | DBIx::Class::InflateColumn::Time | 15 | 2026-05-10 | 1e95a0902 | | DBIx::Class::IntrospectableM2M | 3 | 2026-05-10 | 1e95a0902 | -| DBIx::Class::Objects | 62 | 2026-05-11 | b5c01ce1c | +| DBIx::Class::Objects | 62 | 2026-06-09 | 505082700 | | DBIx::Class::QueriesTime | 1 | 2026-05-10 | a0cb33710 | | DBIx::Class::QueryLog | 43 | 2026-05-12 | b5c01ce1c | | DBIx::Class::QueryLog::Conditional | 3 | 2026-05-10 | 1e95a0902 | @@ -821,6 +823,7 @@ | Data::Munge | 89 | 2026-05-17 | bd327bb57 | | Data::Page::NoTotalEntries | 5 | 2026-04-21 | 3f3336774 | | Data::Pairs | 179 | 2026-05-19 | b5536d190 | +| Data::Perl | 194 | 2026-06-09 | 505082700 | | Data::Phrasebook | 110 | 2026-05-09 | d7916b01c | | Data::Properties::YAML | 10 | 2026-04-21 | 73edc8aba | | Data::Record | 32 | 2026-05-07 | 24f4fb162 | @@ -845,7 +848,7 @@ | Date::Hijri | 3 | 2026-05-15 | 3fe76ed3b | | Date::ISO8601 | 456 | 2026-05-16 | 75a25eb21 | | Date::Leapyear | 764 | 2026-05-20 | b5536d190 | -| Date::Manip | 4533 | 2026-06-04 | d0f827461 | +| Date::Manip | 4533 | 2026-06-09 | 505082700 | | Date::Parse | 1215 | 2026-06-03 | d0f827461 | | Date::Tolkien::Shire::Data | 5558 | 2026-05-10 | 1e95a0902 | | DateStamp | 46 | 2026-05-12 | 93dfa497b | @@ -866,7 +869,7 @@ | DateTime::Format::Atom | 4 | 2026-05-12 | b5c01ce1c | | DateTime::Format::Baby | 87 | 2026-05-10 | 1e95a0902 | | DateTime::Format::Bork | 10 | 2026-05-18 | bd327bb57 | -| DateTime::Format::Builder | 218 | 2026-05-21 | 474c0f049 | +| DateTime::Format::Builder | 226 | 2026-06-09 | 505082700 | | DateTime::Format::Czech | 14 | 2026-05-17 | d8a8a9f34 | | DateTime::Format::DB2 | 25 | 2026-05-16 | 75a25eb21 | | DateTime::Format::DateManip | 7 | 2026-05-19 | b5536d190 | @@ -895,7 +898,8 @@ | DateTime::Format::RFC3501 | 12 | 2026-05-18 | b5536d190 | | DateTime::Format::RSS | 128 | 2026-05-09 | a0cb33710 | | DateTime::Format::Roman | 23 | 2026-05-11 | 1e95a0902 | -| DateTime::Format::Strptime | 143 | 2026-05-21 | 474c0f049 | +| DateTime::Format::SQLite | 68 | 2026-06-09 | 505082700 | +| DateTime::Format::Strptime | 143 | 2026-06-09 | 505082700 | | DateTime::Format::Sybase | 12 | 2026-05-17 | d8a8a9f34 | | DateTime::Format::W3CDTF | 43 | 2026-05-20 | b5536d190 | | DateTime::Format::WindowsFileTime | 2 | 2026-05-19 | b5536d190 | @@ -1000,15 +1004,15 @@ | Exporter::Tidy | 44 | 2026-04-12 | c1942aad0 | | ExtJS::Generator::DBIC | 7 | 2026-05-14 | 3fe76ed3b | | ExtUtils::AutoInstall | 6 | 2026-05-18 | bd327bb57 | -| ExtUtils::CBuilder | 72 | 2026-06-04 | d0f827461 | -| ExtUtils::Config | 16 | 2026-06-04 | d0f827461 | +| ExtUtils::CBuilder | 72 | 2026-06-09 | 505082700 | +| ExtUtils::Config | 16 | 2026-06-09 | 505082700 | | ExtUtils::FindFunctions | 11 | 2026-05-10 | 1e95a0902 | | ExtUtils::HasCompiler | 4 | 2026-04-29 | 82e5e452d | -| ExtUtils::Helpers | 30 | 2026-06-04 | d0f827461 | +| ExtUtils::Helpers | 30 | 2026-06-09 | 505082700 | | ExtUtils::InstallPAR | 3 | 2026-05-13 | 3fe76ed3b | -| ExtUtils::InstallPaths | 111 | 2026-06-04 | d0f827461 | +| ExtUtils::InstallPaths | 111 | 2026-06-09 | 505082700 | | ExtUtils::MakeMaker | 26 | 2026-04-22 | e228c2529 | -| ExtUtils::MakeMaker::CPANfile | 5 | 2026-05-10 | 1e95a0902 | +| ExtUtils::MakeMaker::CPANfile | 5 | 2026-06-09 | 505082700 | | ExtUtils::MakeMaker::META_MERGE::GitHub | 57 | 2026-05-11 | 2b8886eec | | ExtUtils::ModuleMaker | 1589 | 2026-05-14 | 3fe76ed3b | | ExtUtils::PL2Bat | 3 | 2026-05-16 | 75a25eb21 | @@ -1073,6 +1077,7 @@ | File::XDG | 9 | 2026-05-17 | d8a8a9f34 | | File::chdir | 94 | 2026-06-03 | d0f827461 | | File::chmod::Recursive | 1 | 2026-05-15 | 3fe76ed3b | +| File::pushd | 47 | 2026-06-09 | 505082700 | | FileCache::Appender | 20 | 2026-05-07 | 24f4fb162 | | FileSystem::LL::FAT | 1 | 2026-05-17 | d8a8a9f34 | | Filesys::Cap | 1 | 2026-05-17 | d8a8a9f34 | @@ -1566,7 +1571,7 @@ | List::Categorize | 20 | 2026-04-22 | 3f3336774 | | List::Cycle | 34 | 2026-05-12 | 312b4f902 | | List::GroupingPriorityQueue | 25 | 2026-05-10 | a0cb33710 | -| List::MoreUtils | 4533 | 2026-06-03 | d0f827461 | +| List::MoreUtils | 4533 | 2026-06-09 | 505082700 | | List::PowerSet | 14 | 2026-05-06 | 9bda7d2ed | | List::Uniq | 38 | 2026-05-19 | b5536d190 | | List::Util::WeightedChoice | 5 | 2026-05-15 | 3fe76ed3b | @@ -1719,6 +1724,7 @@ | Module::Build::Prereqs::FromCPANfile | 11 | 2026-05-19 | b5536d190 | | Module::Build::Using::PkgConfig | 2 | 2026-05-17 | d8a8a9f34 | | Module::Build::WithXSpp | 1 | 2026-05-18 | b5536d190 | +| Module::CPANfile | 41 | 2026-06-09 | 505082700 | | Module::CoreList | 102 | 2026-05-20 | b5536d190 | | Module::Depends | 20 | 2026-05-10 | 1e95a0902 | | Module::Depends::Intrusive | 20 | 2026-05-13 | 3fe76ed3b | @@ -1755,6 +1761,7 @@ | MooX::ChainedAttributes | 9 | 2026-05-11 | 2b8886eec | | MooX::Cmd | 1 | 2026-05-15 | 3fe76ed3b | | MooX::Enumeration | 44 | 2026-05-17 | d8a8a9f34 | +| MooX::HandlesVia | 799 | 2026-06-09 | 505082700 | | MooX::HasEnv | 11 | 2026-05-11 | b5c01ce1c | | MooX::LazierAttributes | 103 | 2026-05-18 | bd327bb57 | | MooX::LazyRequire | 12 | 2026-05-19 | b5536d190 | @@ -1776,7 +1783,7 @@ | MooX::Thunking | 16 | 2026-05-11 | 1e95a0902 | | MooX::TypeTiny | 43 | 2026-05-10 | a0cb33710 | | MooX::Types::MooseLike | 196 | 2026-05-21 | 474c0f049 | -| MooX::Types::MooseLike::Base | 169 | 2026-04-12 | c1942aad0 | +| MooX::Types::MooseLike::Base | 196 | 2026-06-09 | 505082700 | | MooX::Types::MooseLike::DateTime | 7 | 2026-05-11 | b5c01ce1c | | MooX::Types::MooseLike::Numeric | 43 | 2026-05-16 | d8a8a9f34 | | Mooish | 2 | 2026-05-16 | 75a25eb21 | @@ -1856,7 +1863,7 @@ | MooseX::Role::Logger | 7 | 2026-05-12 | 312b4f902 | | MooseX::Role::Matcher | 42 | 2026-05-11 | 2b8886eec | | MooseX::Role::Nameable | 7 | 2026-05-18 | bd327bb57 | -| MooseX::Role::Parameterized | 153 | 2026-05-19 | b5536d190 | +| MooseX::Role::Parameterized | 153 | 2026-06-09 | 505082700 | | MooseX::Role::Pluggable | 29 | 2026-05-11 | 2b8886eec | | MooseX::Role::WarnOnConflict | 7 | 2026-05-11 | b5c01ce1c | | MooseX::Role::WithOverloading | 72 | 2026-05-18 | bd327bb57 | @@ -1955,6 +1962,7 @@ | Net::IP | 36069 | 2026-06-03 | d0f827461 | | Net::IP::Route::Reject | 2 | 2026-04-26 | 1d5def215 | | Net::IPv6Addr | 799 | 2026-05-12 | b5c01ce1c | +| Net::LDAP | 789 | 2026-06-09 | 505082700 | | Net::LDAP::Makepath | 5 | 2026-04-22 | 3f3336774 | | Net::MAC | 965 | 2026-05-12 | 93dfa497b | | Net::MQTT::Constants | 157 | 2026-05-18 | bd327bb57 | @@ -2329,6 +2337,7 @@ | RePrec | 1 | 2026-05-19 | b5536d190 | | Reddit | 1 | 2026-05-12 | 312b4f902 | | Redis | 4 | 2026-05-19 | b5536d190 | +| Ref::Util | 468 | 2026-06-09 | 505082700 | | Regexp::Common::Email::Address | 7 | 2026-05-12 | b5c01ce1c | | Regexp::Common::time | 7202 | 2026-05-12 | b5c01ce1c | | Regexp::IPv4 | 5 | 2026-05-16 | 75a25eb21 | @@ -2498,6 +2507,7 @@ | SortSpec | 1 | 2026-05-10 | 1e95a0902 | | SortSpec::Perl::CPAN::ChangesGroup::PERLANCAR | 1 | 2026-05-12 | b5c01ce1c | | Spellunker | 161 | 2026-05-11 | 2b8886eec | +| Spiffy | 198 | 2026-06-09 | 505082700 | | Spike | 46 | 2026-05-18 | bd327bb57 | | Splunklib | 6 | 2026-05-10 | 1e95a0902 | | Spreadsheet::WriteExcel | 1254 | 2026-06-03 | d0f827461 | @@ -2635,6 +2645,7 @@ | Test::API | 32 | 2026-04-12 | c1942aad0 | | Test::AllModules | 24 | 2026-05-13 | 3fe76ed3b | | Test::Arrow | 113 | 2026-05-07 | 24f4fb162 | +| Test::Base | 435 | 2026-06-09 | 505082700 | | Test::Benchmark | 36 | 2026-05-11 | b5c01ce1c | | Test::CPAN::Meta | 196 | 2026-04-12 | c1942aad0 | | Test::CPAN::Meta::JSON | 815 | 2026-05-19 | b5536d190 | @@ -2653,6 +2664,7 @@ | Test::Deep::JSON | 7 | 2026-05-12 | 312b4f902 | | Test::Deep::UnorderedPairs | 15 | 2026-05-21 | 474c0f049 | | Test::DescribeMe | 12 | 2026-05-21 | 474c0f049 | +| Test::Differences | 49 | 2026-06-09 | 505082700 | | Test::Dist::VersionSync | 47 | 2026-04-26 | 1d5def215 | | Test::Distribution | 42 | 2026-05-19 | b5536d190 | | Test::ExpectAndCheck | 30 | 2026-05-12 | 93dfa497b | @@ -2669,7 +2681,7 @@ | Test::HexString | 7 | 2026-05-12 | b5c01ce1c | | Test::Identity | 10 | 2026-05-10 | a0cb33710 | | Test::InDistDir | 6 | 2026-04-12 | c1942aad0 | -| Test::Inter | 90 | 2026-06-04 | d0f827461 | +| Test::Inter | 90 | 2026-06-09 | 505082700 | | Test::JSON | 39 | 2026-05-18 | bd327bb57 | | Test::Lives | 3 | 2026-05-19 | b5536d190 | | Test::LoadAllModules | 5 | 2026-04-22 | 3f3336774 | @@ -2682,7 +2694,7 @@ | Test::MonkeyMock | 34 | 2026-05-16 | d8a8a9f34 | | Test::More::Diagnostic | 2 | 2026-05-15 | 3fe76ed3b | | Test::More::UTF8 | 13 | 2026-05-21 | b5536d190 | -| Test::Most | 103 | 2026-06-03 | d0f827461 | +| Test::Most | 103 | 2026-06-09 | 505082700 | | Test::Name::FromLine | 12 | 2026-05-07 | 24f4fb162 | | Test::Needs | 227 | 2026-05-20 | b5536d190 | | Test::NeedsDisplay | 2 | 2026-05-18 | bd327bb57 | @@ -2691,7 +2703,7 @@ | Test::Number::Delta | 72 | 2026-04-12 | c1942aad0 | | Test::Object | 5 | 2026-06-03 | d0f827461 | | Test::OpenTracing::Interface | 31 | 2026-05-08 | 348368253 | -| Test::Output | 1217 | 2026-06-03 | d0f827461 | +| Test::Output | 1217 | 2026-06-09 | 505082700 | | Test::PAUSE::Permissions | 1 | 2026-05-17 | d8a8a9f34 | | Test::PerlTidy | 25 | 2026-04-30 | 1766659c7 | | Test::Pod | 19 | 2026-05-21 | 474c0f049 | @@ -2732,7 +2744,7 @@ | Text::Context::EitherSide | 12 | 2026-05-10 | 1e95a0902 | | Text::Control | 166 | 2026-05-08 | 24f4fb162 | | Text::DSV | 29 | 2026-05-10 | a0cb33710 | -| Text::Diff | 33 | 2026-06-03 | d0f827461 | +| Text::Diff | 33 | 2026-06-09 | 505082700 | | Text::Format | 15 | 2026-05-16 | 75a25eb21 | | Text::German | 34 | 2026-04-12 | c1942aad0 | | Text::Gitignore | 42 | 2026-05-18 | bd327bb57 | @@ -2748,7 +2760,7 @@ | Text::Quote | 53 | 2026-04-12 | c1942aad0 | | Text::SimpleTable | 10 | 2026-04-12 | c1942aad0 | | Text::SimpleTable::AutoWidth | 6 | 2026-04-12 | c1942aad0 | -| Text::Soundex | 18 | 2026-04-12 | c1942aad0 | +| Text::Soundex | 18 | 2026-06-09 | 505082700 | | Text::SpanningTable | 20 | 2026-04-21 | 73edc8aba | | Text::Table | 175 | 2026-05-21 | b5536d190 | | Text::Tags | 89 | 2026-04-22 | 3f3336774 | @@ -3301,6 +3313,7 @@ | parent | 37 | 2026-04-21 | 73edc8aba | | rlib | 7 | 2026-04-12 | c1942aad0 | | smallnum | 72 | 2026-06-03 | d0f827461 | +| strictures | 42 | 2026-06-09 | 505082700 | | subroutines | 1 | 2026-05-08 | 454bddc34 | | syntax | 4 | 2026-04-12 | c1942aad0 | | threads::shared | 80 | 2026-05-20 | b5536d190 | @@ -3480,7 +3493,7 @@ | mod_perl | | Configure failed | 2026-04-29 | | version | | Configure failed | 2026-05-01 | -### Missing Dependencies (623 modules) +### Missing Dependencies (622 modules) | Module | Pass/Total | Error | Date | |--------|-----------|-------|------| @@ -3871,7 +3884,6 @@ | MemcacheDBI::TieDBH | | Missing: Test/Deep.pm | 2026-05-20 | | Memphis | | Missing: Glib.pm | 2026-05-20 | | Minion::Backend::SQLite | 1/1 | Missing: Minion.pm | 2026-05-17 | -| Module::CPANfile | | Missing: File/pushd.pm | 2026-05-20 | | Module::Install::AuthorTests | | Missing: Module/Install/Base.pm | 2026-04-22 | | Module::Loaded | | Missing: less.pm | 2026-04-21 | | Module::Signature | 2/2 | Missing: IPC/Run.pm | 2026-04-12 | @@ -5933,7 +5945,7 @@ | YAMLTest | 1/1 | Syntax error | 2026-05-10 | | constant::lexical | | Syntax error | 2026-05-17 | -### Test Failures (4311 modules) +### Test Failures (4303 modules) | Module | Pass/Total | Error | Date | |--------|-----------|-------|------| @@ -7069,7 +7081,6 @@ | Data::ModeMerge | 0/9 | 463/9 subtests failed | 2026-05-10 | | Data::MultiValuedHash | 0/7 | 214/7 subtests failed | 2026-05-20 | | Data::ObjectDriver | 0/79 | 682/79 subtests failed | 2026-05-15 | -| Data::Perl | 193/194 | 1/194 subtests failed | 2026-04-12 | | Data::Phrasebook::Loader::Ini | 1/12 | 11/12 subtests failed | 2026-05-09 | | Data::Printer | 621/647 | 26/647 subtests failed | 2026-05-21 | | Data::Recursive::Encode | 27/28 | 1/28 subtests failed | 2026-05-17 | @@ -7129,7 +7140,6 @@ | DateTime::Format::Japanese | 0/12 | 293/12 subtests failed | 2026-05-10 | | DateTime::Format::Lite | 0/1 | 1/1 subtests failed | 2026-05-10 | | DateTime::Format::RelativeTime | 0/2 | 2/2 subtests failed | 2026-05-10 | -| DateTime::Format::SQLite | 68/69 | 1/69 subtests failed | 2026-05-16 | | DateTime::Format::Strftimeq | 0/1 | 1/1 subtests failed | 2026-05-10 | | DateTime::Format::Unicode | 0/1 | 1/1 subtests failed | 2026-05-18 | | DateTime::Format::Variant | 0/1 | 3/1 subtests failed | 2026-05-17 | @@ -7372,7 +7382,6 @@ | File::UserConfig | 0/2 | 28/2 subtests failed | 2026-05-19 | | File::Util | 353/395 | 42/395 subtests failed | 2026-05-19 | | File::chmod | 30/39 | 9/39 subtests failed | 2026-04-12 | -| File::pushd | 41/47 | 6/47 subtests failed | 2026-05-21 | | FileHandle::Unget | 53/76 | 23/76 subtests failed | 2026-05-17 | | FileSlurp_12 | 258/280 | 22/280 subtests failed | 2026-05-18 | | Filesys::SmbClient | 0/1 | 18/1 subtests failed | 2026-05-15 | @@ -8348,7 +8357,6 @@ | MooX::ClassAttribute | 24/26 | 2/26 subtests failed | 2026-05-18 | | MooX::Const | 57/65 | 8/65 subtests failed | 2026-05-12 | | MooX::File::ConfigDir | 0/1 | 1/1 subtests failed | 2026-05-19 | -| MooX::HandlesVia | 797/799 | 2/799 subtests failed | 2026-05-10 | | MooX::Keyword | 0/1 | 1/1 subtests failed | 2026-05-19 | | MooX::Keyword::Factory | 0/1 | 1/1 subtests failed | 2026-05-17 | | MooX::Keyword::Field | 0/1 | 1/1 subtests failed | 2026-05-19 | @@ -9298,7 +9306,6 @@ | SpecioX::XS | 0/1 | 1/1 subtests failed | 2026-05-16 | | Sphinx::Search | 22/32 | 10/32 subtests failed | 2026-05-17 | | Spica | 2/13 | 11/13 subtests failed | 2026-05-17 | -| Spiffy | 193/198 | 5/198 subtests failed; 3/32 test programs failed | 2026-06-03 | | Spoon | 47/76 | 29/76 subtests failed | 2026-05-18 | | Spp | 2/10 | 8/10 subtests failed | 2026-05-18 | | Spreadsheet::HTML | 0/1 | 9/1 subtests failed | 2026-05-18 | @@ -9425,7 +9432,6 @@ | Test::Approx | 0/1 | 35/1 subtests failed | 2026-05-14 | | Test::BDD::Cucumber | 356/357 | 1/357 subtests failed | 2026-05-17 | | Test::BDD::Cucumber::StepFile | 356/357 | 1/357 subtests failed | 2026-05-17 | -| Test::Base | | 105/106 test programs failed; 0/0 subtests failed | 2026-06-03 | | Test::Base::Less | 26/30 | 4/30 subtests failed | 2026-05-12 | | Test::Block | 22/26 | 4/26 subtests failed; 2/6 test programs failed | 2026-06-03 | | Test::CGI::Multipart | 3/7 | 4/7 subtests failed | 2026-05-12 | @@ -9441,7 +9447,6 @@ | Test::DatabaseRow | 299/320 | 21/320 subtests failed | 2026-05-17 | | Test::Deep | 1267/1268 | 1/1268 subtests failed | 2026-05-20 | | Test::Dependencies | 0/2 | 4/2 subtests failed | 2026-05-11 | -| Test::Differences | 44/49 | 5/49 subtests failed; 4/17 test programs failed | 2026-06-03 | | Test::Directory | 27/50 | 23/50 subtests failed | 2026-04-22 | | Test::DistManifest | 4/12 | 8/12 subtests failed | 2026-05-17 | | Test::Exception | 0/9 | 45/9 subtests failed | 2026-05-20 | @@ -10243,7 +10248,6 @@ | next::XS | 0/1 | 1/1 subtests failed | 2026-05-09 | | re::engine::RE2 | 0/2 | 125/2 subtests failed | 2026-05-17 | | sanity | 9/22 | 13/22 subtests failed | 2026-04-22 | -| strictures | 4/5 | 1/5 subtests failed | 2026-04-12 | | subs | 0/2 | 2/2 subtests failed | 2026-05-07 | | superclass | 48/49 | 1/49 subtests failed | 2026-05-13 | | utf8::all | 0/4 | 40/4 subtests failed | 2026-04-22 | @@ -10296,6 +10300,7 @@ These modules were recognized as intentionally skipped by the tester. |--------|--------|------| | AnyDBM_File | distroprefs | 2026-06-04 | | CGI | distroprefs | 2026-06-04 | +| Digest::MD2 | bundled | 2026-06-09 | | File::Slurp | distroprefs | 2026-06-04 | | HTTP::CookieJar::LWP | bundled | 2026-06-04 | | HTTP::Daemon | distroprefs | 2026-06-03 | diff --git a/src/main/java/org/perlonjava/app/scriptengine/PerlLanguageProvider.java b/src/main/java/org/perlonjava/app/scriptengine/PerlLanguageProvider.java index ac5349c4d..09d026902 100644 --- a/src/main/java/org/perlonjava/app/scriptengine/PerlLanguageProvider.java +++ b/src/main/java/org/perlonjava/app/scriptengine/PerlLanguageProvider.java @@ -606,21 +606,36 @@ private static RuntimeCode compileToExecutable(Node ast, EmitterContext ctx) thr } if (CompilerOptions.DEBUG_ENABLED) ctx.logDebug("Falling back to bytecode interpreter due to method size"); - // Reset strict/feature/warning flags before fallback compilation. - // The JVM compiler already processed BEGIN blocks (use strict, etc.) - // which set these flags on ctx.symbolTable. But the interpreter will - // re-process those pragmas during execution, so inheriting them causes - // false strict violations (e.g. bareword filehandles rejected). + EmitterContext fallbackCtx = ctx; + // The interpreter replays CompilerFlagNodes, so it must not start from + // the parser's final strict state. Do that on a private snapshot: the + // live parser context is still used by lazy sub definitions registered + // while fallback bytecode is compiled. if (ctx.symbolTable != null) { - ctx.symbolTable.strictOptionsStack.pop(); - ctx.symbolTable.strictOptionsStack.push(0); + ScopedSymbolTable fallbackSymbolTable = ctx.symbolTable.snapShot(); + fallbackSymbolTable.strictOptionsStack.pop(); + fallbackSymbolTable.strictOptionsStack.push(0); + fallbackCtx = new EmitterContext( + ctx.javaClassInfo, + fallbackSymbolTable, + ctx.mv, + ctx.cw, + ctx.contextType, + ctx.isBoxed, + ctx.errorUtil, + ctx.compilerOptions, + ctx.unitcheckBlocks + ); } BytecodeCompiler compiler = new BytecodeCompiler( ctx.compilerOptions.fileName, 1, ctx.errorUtil ); - InterpretedCode interpretedCode = compiler.compile(ast, ctx); + InterpretedCode interpretedCode = compiler.compile(ast, fallbackCtx); + if (ctx.symbolTable != null && fallbackCtx.symbolTable != null) { + ctx.symbolTable.copyFlagsFrom(fallbackCtx.symbolTable); + } if (ctx.compilerOptions.disassembleEnabled) { System.out.println("=== Interpreter Bytecode ==="); diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index 06f032a2e..7a7e52040 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -815,13 +815,6 @@ public InterpretedCode compile(Node node, EmitterContext ctx) { nextRegister = 3 + capturedVars.length; } - // For non-eval-STRING compilations (special blocks, top-level scripts), - // override VOID→LIST so the block tracks its result. eval STRING must - // preserve the caller's context so wantarray() works correctly inside. - if (!isEvalString && currentCallContext == RuntimeContextType.VOID) { - currentCallContext = RuntimeContextType.LIST; - } - int returnTargetReg = allocateRegister(); targetOutputReg = returnTargetReg; @@ -2254,13 +2247,16 @@ void handleCompoundAssignment(BinaryOperatorNode node) { // Get the left operand register (the variable or expression being assigned to) int targetReg = compileLhsForCompoundAssignment(node); + Object useIntegerAnnotation = node.getAnnotation("useInteger"); + boolean useInteger = useIntegerAnnotation instanceof Boolean value ? value : isIntegerEnabled(); + // Emit the appropriate compound assignment opcode switch (op) { case "+=" -> emit(Opcodes.ADD_ASSIGN); case "-=" -> emit(Opcodes.SUBTRACT_ASSIGN); case "*=" -> emit(Opcodes.MULTIPLY_ASSIGN); - case "/=" -> emit(isIntegerEnabled() ? Opcodes.INTEGER_DIV_ASSIGN : Opcodes.DIVIDE_ASSIGN); - case "%=" -> emit(isIntegerEnabled() ? Opcodes.INTEGER_MOD_ASSIGN : Opcodes.MODULUS_ASSIGN); + case "/=" -> emit(useInteger ? Opcodes.INTEGER_DIV_ASSIGN : Opcodes.DIVIDE_ASSIGN); + case "%=" -> emit(useInteger ? Opcodes.INTEGER_MOD_ASSIGN : Opcodes.MODULUS_ASSIGN); case ".=" -> emit(Opcodes.STRING_CONCAT_ASSIGN); case "&=" -> emit(Opcodes.BITWISE_AND_ASSIGN); // Bitwise AND (dispatch) case "|=" -> emit(Opcodes.BITWISE_OR_ASSIGN); // Bitwise OR (dispatch) @@ -2273,8 +2269,8 @@ void handleCompoundAssignment(BinaryOperatorNode node) { case "^.=" -> emit(Opcodes.STRING_BITWISE_XOR_ASSIGN); // String bitwise XOR case "x=" -> emit(Opcodes.REPEAT_ASSIGN); // String repetition case "**=" -> emit(Opcodes.POW_ASSIGN); // Exponentiation - case "<<=" -> emit(isIntegerEnabled() ? Opcodes.INTEGER_LEFT_SHIFT_ASSIGN : Opcodes.LEFT_SHIFT_ASSIGN); - case ">>=" -> emit(isIntegerEnabled() ? Opcodes.INTEGER_RIGHT_SHIFT_ASSIGN : Opcodes.RIGHT_SHIFT_ASSIGN); + case "<<=" -> emit(useInteger ? Opcodes.INTEGER_LEFT_SHIFT_ASSIGN : Opcodes.LEFT_SHIFT_ASSIGN); + case ">>=" -> emit(useInteger ? Opcodes.INTEGER_RIGHT_SHIFT_ASSIGN : Opcodes.RIGHT_SHIFT_ASSIGN); default -> { throwCompilerException("Unknown compound assignment operator: " + op); return; diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index a8d07debe..871b1caa0 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -62,6 +62,30 @@ private static boolean lexicalAssignmentMustPreserveSlot(RuntimeBase val) { || scalar.type == RuntimeScalarType.READONLY_SCALAR; } + private static boolean returnListContainsTrackedReference(RuntimeList retList) { + if (retList == null || retList instanceof RuntimeControlFlowList) return true; + for (RuntimeBase elem : retList.elements) { + if (runtimeBaseIsTrackedReference(elem)) { + return true; + } + } + return false; + } + + private static boolean runtimeBaseIsTrackedReference(RuntimeBase elem) { + if (elem == null) return false; + if (elem instanceof RuntimeScalar scalar) { + if (!RuntimeScalarType.isReference(scalar)) { + return false; + } + if (!(scalar.value instanceof RuntimeBase base)) { + return false; + } + return base.blessId != 0 || base.refCount >= 0; + } + return elem.blessId != 0 || elem.refCount >= 0; + } + private static void releaseConsumedTemp(RuntimeBase temp, RuntimeBase target) { if (!(temp instanceof RuntimeScalar tempScalar) || !(target instanceof RuntimeScalar targetScalar) @@ -447,6 +471,9 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c RuntimeList retList = RuntimeCode.returnList(retVal, callContext); RuntimeCode.materializeSpecialVarsInResult(retList, callContext); returnedClosures = collectReturnedClosures(retList); + if (!returnListContainsTrackedReference(retList)) { + MortalList.flushAboveMark(); + } return retList; } @@ -1305,28 +1332,14 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c CallerStack.pushLazy(lazyPkg, () -> getCallSiteInfo(code, lazyPc, lazyPkg)); RuntimeList result; try { - // Fast path for InterpretedCode: call execute() directly, - // bypassing RuntimeCode.apply() indirection chain - if (codeRef.type == RuntimeScalarType.CODE && codeRef.value instanceof InterpretedCode interpCode) { - RuntimeCode.requireLvalueCallable(interpCode, context, null); - int effectiveContext = RuntimeCode.effectiveCallContext(interpCode, context); - // Direct call to interpreter - skip RuntimeCode.apply overhead - // Push args to argsStack for getCallerArgs() support (used by List::Util::any/all/etc.) - RuntimeCode.pushArgs(callArgs); - try { - // Pass null for subroutineName to enable frame caching - result = BytecodeInterpreter.execute(interpCode, callArgs, effectiveContext, null); - } finally { - RuntimeCode.popArgs(); - } + // Route interpreted code through RuntimeCode.apply too. Its wrapper + // establishes mortal marks, warning/hint stacks, args-stack state, + // and void-result cleanup. Bypassing it keeps scope temporaries alive + // in large-code interpreter fallbacks (Net::LDAP ref-loop cleanup). + if (shareArgs) { + result = RuntimeCode.apply(codeRef, callArgs, context); } else { - // Slow path for JVM-compiled code, symbolic references, etc. - // For &func (shareArgs), use the apply overload that shares @_ - if (shareArgs) { - result = RuntimeCode.apply(codeRef, callArgs, context); - } else { - result = RuntimeCode.apply(codeRef, "", callArgs, context); - } + result = RuntimeCode.apply(codeRef, "", callArgs, context); } // Handle TAILCALL with trampoline loop (same as JVM backend) @@ -1337,20 +1350,7 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c codeRef = flow.getTailCallCodeRef(); callArgs = flow.getTailCallArgs(); try { - // Use fast path for InterpretedCode - if (codeRef.type == RuntimeScalarType.CODE && codeRef.value instanceof InterpretedCode interpCode) { - RuntimeCode.requireLvalueCallable(interpCode, context, "tailcall"); - int effectiveContext = RuntimeCode.effectiveCallContext(interpCode, context); - // Push args for tail call too - RuntimeCode.pushArgs(callArgs); - try { - result = BytecodeInterpreter.execute(interpCode, callArgs, effectiveContext, null); - } finally { - RuntimeCode.popArgs(); - } - } else { - result = RuntimeCode.apply(codeRef, "tailcall", callArgs, context); - } + result = RuntimeCode.apply(codeRef, "tailcall", callArgs, context); } finally { RuntimeCode.cleanupTailCallArgs(callArgs); RuntimeCode.cleanupTailCallCodeRef(codeRef); @@ -2611,10 +2611,12 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c // Not in eval block - propagate exception // Re-throw RuntimeExceptions as-is (includes PerlDieException) - propagatingException = e; if (e instanceof RuntimeException re) { + re = WarnDie.maybeInvokeUnhandledDieHandler(re); + propagatingException = re; throw re; } + propagatingException = e; // Check if we're running inside an eval STRING context // (sourceName starts with "(eval " when code is from eval STRING) diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperator.java index 64158dd49..c34f91e99 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperator.java @@ -422,7 +422,7 @@ else if (node.right instanceof BinaryOperatorNode rightCall) { // sub/block arguments and &-prototype calls report the block/arg line. int callSiteToken = callerLineCallSiteToken(node); int rd = CompileBinaryOperatorHelper.compileBinaryOperatorSwitch( - bytecodeCompiler, node.operator, rs1, rs2, callSiteToken, + bytecodeCompiler, node, rs1, rs2, callSiteToken, shareCallerArgs); bytecodeCompiler.lastResultReg = rd; return; @@ -635,7 +635,7 @@ else if (node.right instanceof BinaryOperatorNode rightCall) { } int rd = CompileBinaryOperatorHelper.compileBinaryOperatorSwitch( - bytecodeCompiler, node.operator, rs1, rs2, node.getIndex()); + bytecodeCompiler, node, rs1, rs2, node.getIndex()); bytecodeCompiler.lastResultReg = rd; return; } @@ -647,7 +647,8 @@ else if (node.right instanceof BinaryOperatorNode rightCall) { case "+", "-", "*", "/", "%", "**", "&", "|", "^", "<<", ">>", "binary&", "binary|", "binary^", - "&.", "|.", "^." -> true; + "&.", "|.", "^.", + ".." -> true; default -> false; }; // For grep/map/sort/all/any, the right operand (list) must always be in LIST context @@ -673,7 +674,7 @@ else if (node.right instanceof BinaryOperatorNode rightCall) { int rs2 = bytecodeCompiler.lastResultReg; // Emit opcode based on operator (delegated to helper method) - int rd = CompileBinaryOperatorHelper.compileBinaryOperatorSwitch(bytecodeCompiler, node.operator, rs1, rs2, node.getIndex()); + int rd = CompileBinaryOperatorHelper.compileBinaryOperatorSwitch(bytecodeCompiler, node, rs1, rs2, node.getIndex()); bytecodeCompiler.lastResultReg = rd; diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperatorHelper.java b/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperatorHelper.java index 3d58514e4..1362923d6 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperatorHelper.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperatorHelper.java @@ -1,5 +1,6 @@ package org.perlonjava.backend.bytecode; +import org.perlonjava.frontend.astnode.BinaryOperatorNode; import org.perlonjava.runtime.runtimetypes.RuntimeContextType; public class CompileBinaryOperatorHelper { @@ -20,11 +21,33 @@ public static int compileBinaryOperatorSwitch(BytecodeCompiler bytecodeCompiler, } public static int compileBinaryOperatorSwitch(BytecodeCompiler bytecodeCompiler, String operator, int rs1, int rs2, int tokenIndex, boolean shareCallerArgs) { + return compileBinaryOperatorSwitch(bytecodeCompiler, operator, rs1, rs2, tokenIndex, shareCallerArgs, null); + } + + public static int compileBinaryOperatorSwitch(BytecodeCompiler bytecodeCompiler, BinaryOperatorNode node, int rs1, int rs2, int tokenIndex) { + return compileBinaryOperatorSwitch(bytecodeCompiler, node.operator, rs1, rs2, tokenIndex, false, integerOverride(node)); + } + + public static int compileBinaryOperatorSwitch(BytecodeCompiler bytecodeCompiler, BinaryOperatorNode node, int rs1, int rs2, int tokenIndex, boolean shareCallerArgs) { + return compileBinaryOperatorSwitch(bytecodeCompiler, node.operator, rs1, rs2, tokenIndex, shareCallerArgs, integerOverride(node)); + } + + private static Boolean integerOverride(BinaryOperatorNode node) { + Object useInteger = node.getAnnotation("useInteger"); + return useInteger instanceof Boolean value ? value : null; + } + + private static boolean isIntegerEnabled(BytecodeCompiler bytecodeCompiler, Boolean useIntegerOverride) { + return useIntegerOverride != null ? useIntegerOverride : bytecodeCompiler.isIntegerEnabled(); + } + + private static int compileBinaryOperatorSwitch(BytecodeCompiler bytecodeCompiler, String operator, int rs1, int rs2, int tokenIndex, boolean shareCallerArgs, Boolean useIntegerOverride) { // Allocate result register int rd = bytecodeCompiler.allocateOutputRegister(); // Emit opcode based on operator boolean noOverload = bytecodeCompiler.isNoOverloadingEnabled(); + boolean useInteger = isIntegerEnabled(bytecodeCompiler, useIntegerOverride); switch (operator) { case "+" -> { bytecodeCompiler.emit(noOverload ? Opcodes.ADD_NO_OVERLOAD : Opcodes.ADD_SCALAR); @@ -46,14 +69,14 @@ public static int compileBinaryOperatorSwitch(BytecodeCompiler bytecodeCompiler, } case "%" -> { bytecodeCompiler.emit(noOverload ? Opcodes.MOD_NO_OVERLOAD - : (bytecodeCompiler.isIntegerEnabled() ? Opcodes.INTEGER_MOD : Opcodes.MOD_SCALAR)); + : (useInteger ? Opcodes.INTEGER_MOD : Opcodes.MOD_SCALAR)); bytecodeCompiler.emitReg(rd); bytecodeCompiler.emitReg(rs1); bytecodeCompiler.emitReg(rs2); } case "/" -> { bytecodeCompiler.emit(noOverload ? Opcodes.DIV_NO_OVERLOAD - : (bytecodeCompiler.isIntegerEnabled() ? Opcodes.INTEGER_DIV : Opcodes.DIV_SCALAR)); + : (useInteger ? Opcodes.INTEGER_DIV : Opcodes.DIV_SCALAR)); bytecodeCompiler.emitReg(rd); bytecodeCompiler.emitReg(rs1); bytecodeCompiler.emitReg(rs2); @@ -427,13 +450,13 @@ public static int compileBinaryOperatorSwitch(BytecodeCompiler bytecodeCompiler, bytecodeCompiler.emitReg(rs2); } case "<<" -> { - bytecodeCompiler.emit(bytecodeCompiler.isIntegerEnabled() ? Opcodes.INTEGER_LEFT_SHIFT : Opcodes.LEFT_SHIFT); + bytecodeCompiler.emit(useInteger ? Opcodes.INTEGER_LEFT_SHIFT : Opcodes.LEFT_SHIFT); bytecodeCompiler.emitReg(rd); bytecodeCompiler.emitReg(rs1); bytecodeCompiler.emitReg(rs2); } case ">>" -> { - bytecodeCompiler.emit(bytecodeCompiler.isIntegerEnabled() ? Opcodes.INTEGER_RIGHT_SHIFT : Opcodes.RIGHT_SHIFT); + bytecodeCompiler.emit(useInteger ? Opcodes.INTEGER_RIGHT_SHIFT : Opcodes.RIGHT_SHIFT); bytecodeCompiler.emitReg(rd); bytecodeCompiler.emitReg(rs1); bytecodeCompiler.emitReg(rs2); diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitBinaryOperator.java b/src/main/java/org/perlonjava/backend/jvm/EmitBinaryOperator.java index 9e6b08af1..a3831e6da 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitBinaryOperator.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitBinaryOperator.java @@ -19,6 +19,14 @@ public class EmitBinaryOperator { static final boolean ENABLE_SPILL_BINARY_LHS = true; + private static boolean isIntegerEnabled(EmitterVisitor emitterVisitor, BinaryOperatorNode node) { + Object useInteger = node.getAnnotation("useInteger"); + if (useInteger instanceof Boolean value) { + return value; + } + return emitterVisitor.ctx.symbolTable.isStrictOptionEnabled(Strict.HINT_INTEGER); + } + static void handleBinaryOperator(EmitterVisitor emitterVisitor, BinaryOperatorNode node, OperatorHandler operatorHandler) { EmitterVisitor scalarVisitor = emitterVisitor.with(RuntimeContextType.SCALAR); // execute operands in scalar context @@ -54,7 +62,7 @@ static void handleBinaryOperator(EmitterVisitor emitterVisitor, BinaryOperatorNo } // Special case for modulus, division, and shift operators under "use integer" - if (emitterVisitor.ctx.symbolTable.isStrictOptionEnabled(Strict.HINT_INTEGER)) { + if (isIntegerEnabled(emitterVisitor, node)) { if (node.operator.equals("%")) { // Use integer modulus when "use integer" is in effect MethodVisitor mv = emitterVisitor.ctx.mv; @@ -215,7 +223,7 @@ static void handleCompoundAssignment(EmitterVisitor emitterVisitor, BinaryOperat // Check if we have an operator handler for this compound operator // Under "use integer", use the integer warn variant for /= - boolean isInteger = emitterVisitor.ctx.symbolTable.isStrictOptionEnabled(Strict.HINT_INTEGER); + boolean isInteger = isIntegerEnabled(emitterVisitor, node); OperatorHandler operatorHandler; if (shouldUseWarnVariant && isInteger && node.operator.equals("/=")) { operatorHandler = OperatorHandler.get("/=_int_warn"); diff --git a/src/main/java/org/perlonjava/frontend/parser/CoreOperatorResolver.java b/src/main/java/org/perlonjava/frontend/parser/CoreOperatorResolver.java index f0ce13fa8..8b3236f38 100644 --- a/src/main/java/org/perlonjava/frontend/parser/CoreOperatorResolver.java +++ b/src/main/java/org/perlonjava/frontend/parser/CoreOperatorResolver.java @@ -46,6 +46,10 @@ public static Node parseCoreOperator(Parser parser, LexerToken token, int startI */ public static Node parseCoreOperator(Parser parser, LexerToken token, int startIndex, boolean coreQualified) { int currentIndex = parser.tokenIndex; + // ParsePrimary has already consumed the operator token. Some parsers use + // currentIndex as a post-consume cursor, but die/warn need the actual + // operator token for source locations. + int sourceIndex = consumedTokenIndex(parser, token, startIndex); String operatorName = token.text; return switch (operatorName) { @@ -105,7 +109,7 @@ public static Node parseCoreOperator(Parser parser, LexerToken token, int startI case "pack" -> OperatorParser.parsePack(parser, token, currentIndex); case "chomp", "chop" -> OperatorParser.parseChompChop(parser, token, currentIndex); case "splice", "mkdir" -> OperatorParser.parseReverse(parser, token, currentIndex); - case "die", "warn" -> OperatorParser.parseDieWarn(parser, token, currentIndex); + case "die", "warn" -> OperatorParser.parseDieWarn(parser, token, sourceIndex); case "system", "exec" -> OperatorParser.parseSystem(parser, token, currentIndex); case "readline", "eof", "tell" -> OperatorParser.parseReadline(parser, token, currentIndex); case "binmode" -> OperatorParser.parseBinmodeOperator(parser, token, currentIndex); @@ -143,6 +147,21 @@ public static Node parseCoreOperator(Parser parser, LexerToken token, int startI }; } + private static int consumedTokenIndex(Parser parser, LexerToken token, int startIndex) { + int upper = Math.min(parser.tokenIndex - 1, parser.tokens.size() - 1); + if (upper < 0) { + return 0; + } + int lower = Math.max(0, Math.min(startIndex, upper)); + for (int i = upper; i >= lower; i--) { + LexerToken candidate = parser.tokens.get(i); + if (candidate.type == token.type && candidate.text.equals(token.text)) { + return i; + } + } + return upper; + } + private static Node parseWithPrototype( Parser parser, LexerToken token, int currentIndex, boolean coreQualified) { String operator = token.text; diff --git a/src/main/java/org/perlonjava/frontend/parser/FileHandle.java b/src/main/java/org/perlonjava/frontend/parser/FileHandle.java index 5820f2db2..4437f6941 100644 --- a/src/main/java/org/perlonjava/frontend/parser/FileHandle.java +++ b/src/main/java/org/perlonjava/frontend/parser/FileHandle.java @@ -272,7 +272,7 @@ public static Node parseBarewordHandle(Parser parser, String name) { // Check if this is a known file handle in the global I/O table // This helps distinguish between file handles and other barewords if (GlobalVariable.existsGlobalIO(name) || isStandardFilehandle(name) - || isAllDigitGlobName(name)) { + || isAllDigitGlobName(name) || isPackageDataHandle(name)) { // Create a GLOB reference for the file handle, like `\*FH` return new OperatorNode("\\", new OperatorNode("*", @@ -299,6 +299,11 @@ private static boolean isAllDigitGlobName(String normalizedName) { return true; } + private static boolean isPackageDataHandle(String normalizedName) { + return normalizedName.endsWith("::DATA") + && normalizedName.length() > "::DATA".length(); + } + /** * Checks if a normalized name represents a standard filehandle. * diff --git a/src/main/java/org/perlonjava/frontend/parser/ParseInfix.java b/src/main/java/org/perlonjava/frontend/parser/ParseInfix.java index 809509719..ef00e4783 100644 --- a/src/main/java/org/perlonjava/frontend/parser/ParseInfix.java +++ b/src/main/java/org/perlonjava/frontend/parser/ParseInfix.java @@ -148,11 +148,15 @@ public static Node parseInfixOperation(Parser parser, Node left, int precedence) BinaryOperatorNode node = new BinaryOperatorNode(operator, left, right, parser.tokenIndex); - // Annotate arithmetic nodes with 'use integer' state so constant folding - // can skip folding when integer semantics are in effect (division truncation, - // overflow wrapping, etc.) - if (parser.ctx.symbolTable.isStrictOptionEnabled(Strict.HINT_INTEGER)) { - node.setAnnotation("useInteger", true); + // Annotate integer-sensitive operators with the parse-time `use integer` + // state. Lazy sub compilation and interpreter fallback may emit code long + // after the parser's lexical hint stack has moved on. + switch (operator) { + case "/", "%", "<<", ">>", "/=", "%=", "<<=", ">>=" -> + node.setAnnotation("useInteger", + parser.ctx.symbolTable.isStrictOptionEnabled(Strict.HINT_INTEGER)); + default -> { + } } return node; diff --git a/src/main/java/org/perlonjava/frontend/parser/SpecialBlockParser.java b/src/main/java/org/perlonjava/frontend/parser/SpecialBlockParser.java index a423ee3f6..fc3db2f59 100644 --- a/src/main/java/org/perlonjava/frontend/parser/SpecialBlockParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/SpecialBlockParser.java @@ -11,8 +11,10 @@ import org.perlonjava.runtime.runtimetypes.*; import java.util.ArrayList; +import java.util.BitSet; import java.util.List; import java.util.Map; +import java.util.Stack; import static org.perlonjava.runtime.runtimetypes.GlobalContext.GLOBAL_PHASE; import static org.perlonjava.runtime.runtimetypes.SpecialBlock.*; @@ -25,6 +27,25 @@ public class SpecialBlockParser { private static ScopedSymbolTable symbolTable = new ScopedSymbolTable(); + private static Stack cloneBitSetStack(Stack source) { + Stack copy = new Stack<>(); + for (BitSet flags : source) { + copy.push((BitSet) flags.clone()); + } + return copy; + } + + private static Stack cloneIntegerStack(Stack source) { + Stack copy = new Stack<>(); + copy.addAll(source); + return copy; + } + + private static void restoreStack(Stack target, Stack source) { + target.clear(); + target.addAll(source); + } + public static ScopedSymbolTable getCurrentScope() { return symbolTable; } @@ -325,6 +346,19 @@ static RuntimeList runSpecialBlock(Parser parser, String blockPhase, Node block, parsedArgs.compileOnly = false; // Special blocks are always run if (CompilerOptions.DEBUG_ENABLED) parser.ctx.logDebug("Special block captures " + parser.ctx.symbolTable.getAllVisibleVariables()); RuntimeList result; + boolean preserveCallerPragmas = !blockPhase.equals("BEGIN"); + Stack savedWarningFlagsStack = null; + Stack savedWarningFatalStack = null; + Stack savedWarningDisabledStack = null; + Stack savedFeatureFlagsStack = null; + Stack savedStrictOptionsStack = null; + if (preserveCallerPragmas) { + savedWarningFlagsStack = cloneBitSetStack(parser.ctx.symbolTable.warningFlagsStack); + savedWarningFatalStack = cloneBitSetStack(parser.ctx.symbolTable.warningFatalStack); + savedWarningDisabledStack = cloneBitSetStack(parser.ctx.symbolTable.warningDisabledStack); + savedFeatureFlagsStack = cloneIntegerStack(parser.ctx.symbolTable.featureFlagsStack); + savedStrictOptionsStack = cloneIntegerStack(parser.ctx.symbolTable.strictOptionsStack); + } try { setCurrentScope(parser.ctx.symbolTable); // Mark wrapper infrastructure nodes to skip DEBUG opcodes and source location mapping. @@ -374,6 +408,14 @@ static RuntimeList runSpecialBlock(Parser parser, String blockPhase, Node block, } message += blockPhase + " failed--compilation aborted"; throw new PerlCompilerException(parser.tokenIndex, message, parser.ctx.errorUtil); + } finally { + if (preserveCallerPragmas) { + restoreStack(parser.ctx.symbolTable.warningFlagsStack, savedWarningFlagsStack); + restoreStack(parser.ctx.symbolTable.warningFatalStack, savedWarningFatalStack); + restoreStack(parser.ctx.symbolTable.warningDisabledStack, savedWarningDisabledStack); + restoreStack(parser.ctx.symbolTable.featureFlagsStack, savedFeatureFlagsStack); + restoreStack(parser.ctx.symbolTable.strictOptionsStack, savedStrictOptionsStack); + } } GlobalVariable.getGlobalVariable("main::@").set(""); // Reset error variable diff --git a/src/main/java/org/perlonjava/frontend/parser/StatementParser.java b/src/main/java/org/perlonjava/frontend/parser/StatementParser.java index 2e1b26ed7..33388e1d5 100644 --- a/src/main/java/org/perlonjava/frontend/parser/StatementParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/StatementParser.java @@ -19,7 +19,9 @@ import org.perlonjava.runtime.runtimetypes.*; import java.util.ArrayList; +import java.util.BitSet; import java.util.List; +import java.util.Stack; import static org.perlonjava.frontend.parser.NumberParser.parseNumber; import static org.perlonjava.frontend.parser.ParserNodeUtils.atUnderscoreArgs; @@ -43,6 +45,24 @@ * use declarations, and package declarations. */ public class StatementParser { + private static Stack cloneBitSetStack(Stack source) { + Stack copy = new Stack<>(); + for (BitSet flags : source) { + copy.push((BitSet) flags.clone()); + } + return copy; + } + + private static Stack cloneIntegerStack(Stack source) { + Stack copy = new Stack<>(); + copy.addAll(source); + return copy; + } + + private static void restoreStack(Stack target, Stack source) { + target.clear(); + target.addAll(source); + } /** * Parses a while or until statement. @@ -867,10 +887,29 @@ public static Node parseUseDeclaration(Parser parser, LexerToken token) { } } - // Execute the argument list immediately in LIST context + // Execute the argument list immediately in LIST context. // This is necessary for expressions like: use lib ($path =~ /^(.*)$/); - // where the regex match must return captured groups, not just success/failure - RuntimeList args = runSpecialBlock(parser, "BEGIN", list, RuntimeContextType.LIST); + // where the regex match must return captured groups, not just success/failure. + // + // The synthetic BEGIN wrapper used for list evaluation has its own compiler + // context. Its final lexical pragma state must not be copied back over the + // surrounding file scope; the actual pragma effect of a `use` statement happens + // below when import()/unimport() is invoked against parser.ctx.symbolTable. + Stack savedWarningFlagsStack = cloneBitSetStack(ctx.symbolTable.warningFlagsStack); + Stack savedWarningFatalStack = cloneBitSetStack(ctx.symbolTable.warningFatalStack); + Stack savedWarningDisabledStack = cloneBitSetStack(ctx.symbolTable.warningDisabledStack); + Stack savedFeatureFlagsStack = cloneIntegerStack(ctx.symbolTable.featureFlagsStack); + Stack savedStrictOptionsStack = cloneIntegerStack(ctx.symbolTable.strictOptionsStack); + RuntimeList args; + try { + args = runSpecialBlock(parser, "BEGIN", list, RuntimeContextType.LIST); + } finally { + restoreStack(ctx.symbolTable.warningFlagsStack, savedWarningFlagsStack); + restoreStack(ctx.symbolTable.warningFatalStack, savedWarningFatalStack); + restoreStack(ctx.symbolTable.warningDisabledStack, savedWarningDisabledStack); + restoreStack(ctx.symbolTable.featureFlagsStack, savedFeatureFlagsStack); + restoreStack(ctx.symbolTable.strictOptionsStack, savedStrictOptionsStack); + } if (CompilerOptions.DEBUG_ENABLED) ctx.logDebug("Use statement list: " + args); if ((hasParentheses || hasEmptyLiteralList) && args.isEmpty()) { @@ -1352,6 +1391,14 @@ private static void applySourceFilterToRemainingTokens(Parser parser) { } } String remainingSource = sb.toString(); + boolean consumedLeadingLineBreak = false; + if (remainingSource.startsWith("\r\n")) { + remainingSource = remainingSource.substring(2); + consumedLeadingLineBreak = true; + } else if (remainingSource.startsWith("\n") || remainingSource.startsWith("\r")) { + remainingSource = remainingSource.substring(1); + consumedLeadingLineBreak = true; + } // Step 2: Apply the installed filters String filteredSource = FilterUtilCall.applyFilters(remainingSource); @@ -1368,6 +1415,9 @@ private static void applySourceFilterToRemainingTokens(Parser parser) { // Step 3: Re-tokenize the filtered source Lexer lexer = new Lexer(filteredSource); List newTokens = lexer.tokenize(); + if (consumedLeadingLineBreak) { + newTokens.add(0, new LexerToken(LexerTokenType.NEWLINE, "\n")); + } // Step 4: Replace remaining tokens with filtered tokens // Keep tokens[0..currentPos-1], replace tokens from currentPos onwards with newTokens diff --git a/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java b/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java index 92aa657fa..c323e70d5 100644 --- a/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java @@ -526,16 +526,27 @@ && isValidIndirectMethod(subName, parser) // Consume the closing brace TokenUtils.consume(parser, LexerTokenType.OPERATOR, "}"); - // Parse any additional arguments after the block - // These become arguments to the method call + // Parse any additional arguments after the block. ListNode arguments = consumeArgsWithPrototype(parser, "@"); - - // Create method call: (block_result)->method(args) + + Node invocant = blockExpr; + if (blockExpr instanceof BlockNode block + && block.elements.size() == 1 + && block.elements.getFirst() instanceof ListNode list + && !list.elements.isEmpty()) { + invocant = list.elements.getFirst(); + List methodArgs = new ArrayList<>(); + methodArgs.addAll(list.elements.subList(1, list.elements.size())); + methodArgs.addAll(arguments.elements); + arguments = new ListNode(methodArgs, currentIndex); + } + + // Create method call: invocant->method(args) Node methodCall = new BinaryOperatorNode("(", new OperatorNode("&", nameNode, currentIndex), arguments, currentIndex); - return new BinaryOperatorNode("->", blockExpr, methodCall, currentIndex); + return new BinaryOperatorNode("->", invocant, methodCall, currentIndex); } ListNode arguments = consumeArgsWithPrototype(parser, "@"); @@ -892,10 +903,23 @@ public static Node parseSubroutineDefinition(Parser parser, boolean wantName, St parser.ctx.symbolTable.setCurrentSubroutine(qualifiedSubName); // We are now parsing inside a subroutine body (named or anonymous) parser.ctx.symbolTable.setInSubroutineBody(true); + java.util.BitSet definitionWarningFlags = + (java.util.BitSet) parser.ctx.symbolTable.warningFlagsStack.peek().clone(); + java.util.BitSet definitionWarningFatalFlags = + (java.util.BitSet) parser.ctx.symbolTable.warningFatalStack.peek().clone(); + java.util.BitSet definitionWarningDisabledFlags = + (java.util.BitSet) parser.ctx.symbolTable.warningDisabledStack.peek().clone(); + int definitionFeatureFlags = parser.ctx.symbolTable.featureFlagsStack.peek(); + int definitionStrictOptions = parser.ctx.symbolTable.strictOptionsStack.peek(); try { // Parse the block of the subroutine, which contains the actual code. BlockNode block = ParseBlock.parseBlock(parser); + block.setAnnotation("definitionWarningFlags", definitionWarningFlags); + block.setAnnotation("definitionWarningFatalFlags", definitionWarningFatalFlags); + block.setAnnotation("definitionWarningDisabledFlags", definitionWarningDisabledFlags); + block.setAnnotation("definitionFeatureFlags", definitionFeatureFlags); + block.setAnnotation("definitionStrictOptions", definitionStrictOptions); // After the block, we expect a closing curly brace '}' to denote the end of the subroutine. // Check if we reached EOF instead of finding the closing brace @@ -1447,25 +1471,46 @@ public static ListNode handleNamedSubWithFilter(Parser parser, String subName, S // Clone the current subroutine filteredSnapshot.setCurrentSubroutine(parser.ctx.symbolTable.getCurrentSubroutine()); + java.util.BitSet definitionWarningFlags = + block.getAnnotation("definitionWarningFlags") instanceof java.util.BitSet bits + ? (java.util.BitSet) bits.clone() + : (java.util.BitSet) parser.ctx.symbolTable.warningFlagsStack.peek().clone(); + java.util.BitSet definitionWarningFatalFlags = + block.getAnnotation("definitionWarningFatalFlags") instanceof java.util.BitSet bits + ? (java.util.BitSet) bits.clone() + : (java.util.BitSet) parser.ctx.symbolTable.warningFatalStack.peek().clone(); + java.util.BitSet definitionWarningDisabledFlags = + block.getAnnotation("definitionWarningDisabledFlags") instanceof java.util.BitSet bits + ? (java.util.BitSet) bits.clone() + : (java.util.BitSet) parser.ctx.symbolTable.warningDisabledStack.peek().clone(); + int definitionFeatureFlags = + block.getAnnotation("definitionFeatureFlags") instanceof Integer value + ? value + : parser.ctx.symbolTable.featureFlagsStack.peek(); + int definitionStrictOptions = + block.getAnnotation("definitionStrictOptions") instanceof Integer value + ? value + : parser.ctx.symbolTable.strictOptionsStack.peek(); + // Clone warning flags (critical for 'no warnings' pragmas) filteredSnapshot.warningFlagsStack.pop(); // Remove the initial value pushed by enterScope - filteredSnapshot.warningFlagsStack.push((java.util.BitSet) parser.ctx.symbolTable.warningFlagsStack.peek().clone()); + filteredSnapshot.warningFlagsStack.push(definitionWarningFlags); // Clone fatal warning flags (critical for 'use warnings FATAL' pragmas) filteredSnapshot.warningFatalStack.pop(); - filteredSnapshot.warningFatalStack.push((java.util.BitSet) parser.ctx.symbolTable.warningFatalStack.peek().clone()); + filteredSnapshot.warningFatalStack.push(definitionWarningFatalFlags); // Clone disabled warning flags (critical for 'no warnings' pragmas) filteredSnapshot.warningDisabledStack.pop(); - filteredSnapshot.warningDisabledStack.push((java.util.BitSet) parser.ctx.symbolTable.warningDisabledStack.peek().clone()); + filteredSnapshot.warningDisabledStack.push(definitionWarningDisabledFlags); // Clone feature flags (critical for 'use feature' pragmas like refaliasing) filteredSnapshot.featureFlagsStack.pop(); // Remove the initial value pushed by enterScope - filteredSnapshot.featureFlagsStack.push(parser.ctx.symbolTable.featureFlagsStack.peek()); + filteredSnapshot.featureFlagsStack.push(definitionFeatureFlags); // Clone strict options (critical for 'use strict' pragma) filteredSnapshot.strictOptionsStack.pop(); // Remove the initial value pushed by enterScope - filteredSnapshot.strictOptionsStack.push(parser.ctx.symbolTable.strictOptionsStack.peek()); + filteredSnapshot.strictOptionsStack.push(definitionStrictOptions); EmitterContext newCtx = new EmitterContext( new JavaClassInfo(), diff --git a/src/main/java/org/perlonjava/runtime/io/SocketIO.java b/src/main/java/org/perlonjava/runtime/io/SocketIO.java index 0209394f7..daf62c12f 100644 --- a/src/main/java/org/perlonjava/runtime/io/SocketIO.java +++ b/src/main/java/org/perlonjava/runtime/io/SocketIO.java @@ -48,6 +48,8 @@ public class SocketIO implements IOHandle { private DatagramChannel datagramChannel; // Last received sender address for recv() return value private SocketAddress lastReceivedFrom; + private byte[] peekedDatagram; + private SocketAddress peekedDatagramFrom; // Datagram socketpair peer tracking. Java UDP channels do not report the // local peer-closed error that Perl's poll/select tests rely on. private SocketIO datagramPeer; @@ -1116,6 +1118,10 @@ public int sendDatagram(byte[] data) throws IOException { * @return the received data as a byte array, or null on error */ public byte[] recvFrom(int maxLength) throws IOException { + return recvFrom(maxLength, false); + } + + public byte[] recvFrom(int maxLength, boolean peek) throws IOException { if (datagramChannel == null) { throw new IllegalStateException("Not a datagram socket"); } @@ -1123,6 +1129,15 @@ public byte[] recvFrom(int maxLength) throws IOException { consumeDatagramConnectionRefused(); return null; } + if (peekedDatagram != null) { + lastReceivedFrom = peekedDatagramFrom; + byte[] data = copyDatagramPrefix(peekedDatagram, maxLength); + if (!peek) { + peekedDatagram = null; + peekedDatagramFrom = null; + } + return data; + } java.nio.ByteBuffer buf = java.nio.ByteBuffer.allocate(maxLength); try { lastReceivedFrom = datagramChannel.receive(buf); @@ -1137,9 +1152,20 @@ public byte[] recvFrom(int maxLength) throws IOException { buf.flip(); byte[] data = new byte[buf.remaining()]; buf.get(data); + if (peek) { + peekedDatagram = data; + peekedDatagramFrom = lastReceivedFrom; + } return data; } + private byte[] copyDatagramPrefix(byte[] data, int maxLength) { + int length = Math.min(maxLength, data.length); + byte[] result = new byte[length]; + System.arraycopy(data, 0, result, 0, length); + return result; + } + /** * Get the sender address from the last recvFrom() call. * Returns a packed sockaddr_in structure suitable for Perl. diff --git a/src/main/java/org/perlonjava/runtime/operators/IOOperator.java b/src/main/java/org/perlonjava/runtime/operators/IOOperator.java index e6e2badd8..0324851ec 100644 --- a/src/main/java/org/perlonjava/runtime/operators/IOOperator.java +++ b/src/main/java/org/perlonjava/runtime/operators/IOOperator.java @@ -2612,7 +2612,7 @@ public static RuntimeScalar recv(int ctx, RuntimeBase... args) { // Check if this is a UDP socket if (socketIO.ioHandle instanceof SocketIO sio && sio.isDatagramSocket()) { - byte[] data = sio.recvFrom(length); + byte[] data = sio.recvFrom(length, (flags & Socket.MSG_PEEK) != 0); if (data != null) { buffer.set(new String(data, java.nio.charset.StandardCharsets.ISO_8859_1)); // Return the sender's packed sockaddr (Perl recv() returns this) diff --git a/src/main/java/org/perlonjava/runtime/operators/ModuleOperators.java b/src/main/java/org/perlonjava/runtime/operators/ModuleOperators.java index c33a6d2c4..5edcb6fba 100644 --- a/src/main/java/org/perlonjava/runtime/operators/ModuleOperators.java +++ b/src/main/java/org/perlonjava/runtime/operators/ModuleOperators.java @@ -227,11 +227,12 @@ else if (firstElem.type == RuntimeScalarType.GLOB || } if (filterRef != null) { - // Apply filter to the content + // Apply filter to the content. The filter uses + // $_ as scratch space, so swap the slot rather + // than mutating the caller's scalar/alias. RuntimeScalar savedDefaultVar = GlobalVariable.getGlobalVariable("main::_"); try { - // Set $_ to the content - GlobalVariable.getGlobalVariable("main::_").set(code); + GlobalVariable.aliasGlobalVariable("main::_", new RuntimeScalar(code)); // Build filter args: $_[0] = undef, $_[1..N] = state RuntimeArray filterArgs = new RuntimeArray(); @@ -246,8 +247,7 @@ else if (firstElem.type == RuntimeScalarType.GLOB || // Get modified content from $_ code = GlobalVariable.getGlobalVariable("main::_").toString(); } finally { - // Restore $_ - GlobalVariable.getGlobalVariable("main::_").set(savedDefaultVar.toString()); + GlobalVariable.aliasGlobalVariable("main::_", savedDefaultVar); } } } @@ -307,7 +307,7 @@ else if (elem.type == RuntimeScalarType.GLOB || elem.type == RuntimeScalarType.G if (filterRef != null) { RuntimeScalar savedDefaultVar = GlobalVariable.getGlobalVariable("main::_"); try { - GlobalVariable.getGlobalVariable("main::_").set(filterableContent.toString()); + GlobalVariable.aliasGlobalVariable("main::_", new RuntimeScalar(filterableContent.toString())); // Build filter args with remaining elements as state RuntimeArray filterArgs = new RuntimeArray(); @@ -319,7 +319,7 @@ else if (elem.type == RuntimeScalarType.GLOB || elem.type == RuntimeScalarType.G filterRef.apply(filterArgs, RuntimeContextType.SCALAR); filterableContent = new StringBuilder(GlobalVariable.getGlobalVariable("main::_").toString()); } finally { - GlobalVariable.getGlobalVariable("main::_").set(savedDefaultVar.toString()); + GlobalVariable.aliasGlobalVariable("main::_", savedDefaultVar); } } diff --git a/src/main/java/org/perlonjava/runtime/operators/ReferenceOperators.java b/src/main/java/org/perlonjava/runtime/operators/ReferenceOperators.java index 4fb38968d..5295fb84e 100644 --- a/src/main/java/org/perlonjava/runtime/operators/ReferenceOperators.java +++ b/src/main/java/org/perlonjava/runtime/operators/ReferenceOperators.java @@ -298,7 +298,12 @@ public static RuntimeScalar ref(RuntimeScalar runtimeScalar) { } break; case FORMAT: - str = "FORMAT"; + if (runtimeScalar.value == null) { + str = "FORMAT"; + } else { + blessId = ((RuntimeBase) runtimeScalar.value).blessId; + str = blessId == 0 ? "FORMAT" : NameNormalizer.getBlessStr(blessId); + } break; case READONLY_SCALAR: return ref((RuntimeScalar) runtimeScalar.value); diff --git a/src/main/java/org/perlonjava/runtime/operators/WarnDie.java b/src/main/java/org/perlonjava/runtime/operators/WarnDie.java index 43f1367a5..061ba02d5 100644 --- a/src/main/java/org/perlonjava/runtime/operators/WarnDie.java +++ b/src/main/java/org/perlonjava/runtime/operators/WarnDie.java @@ -18,6 +18,17 @@ * respectively. These operations can trigger custom signal handlers if defined. */ public class WarnDie { + private static final ThreadLocal> unhandledDieHandlerSeen = + ThreadLocal.withInitial(java.util.IdentityHashMap::new); + private static final ThreadLocal insideUnhandledDieHandler = + ThreadLocal.withInitial(() -> Boolean.FALSE); + + public static boolean isInsideUnhandledDieHandler() { + return insideUnhandledDieHandler.get(); + } + + private record SyntheticDieCallerFrame(String packageName, String filename, int line) { + } /** * Returns true when a %SIG slot holds one of Perl 5's reserved string @@ -65,6 +76,97 @@ private static Throwable unwrapException(Throwable throwable) { return throwable; } + private static SyntheticDieCallerFrame firstPerlFrame(Throwable throwable) { + try { + ExceptionFormatter.StackTraceResult result = ExceptionFormatter.formatExceptionDetailed(throwable); + if (result.frames().isEmpty()) { + return null; + } + var frame = result.frames().getFirst(); + if (frame.size() < 3 || frame.get(1) == null || frame.get(1).isEmpty()) { + return null; + } + int line = Integer.parseInt(frame.get(2)); + return new SyntheticDieCallerFrame(frame.get(0), frame.get(1), line); + } catch (Throwable ignored) { + return null; + } + } + + private static String signalHandlerSubName(RuntimeScalar sigHandler) { + if (sigHandler == null || sigHandler.type != RuntimeScalarType.CODE + || !(sigHandler.value instanceof RuntimeCode code)) { + return null; + } + if (code.subName == null || code.subName.isEmpty()) { + return null; + } + if (code.subName.contains("::")) { + return code.subName; + } + String pkg = code.packageName != null && !code.packageName.isEmpty() + ? code.packageName + : "main"; + return pkg + "::" + code.subName; + } + + public static RuntimeException maybeInvokeUnhandledDieHandler(RuntimeException e) { + Throwable unwrapped = unwrapException(e); + if (unwrapped instanceof PerlDieException || unwrapped instanceof PerlExitException) { + return e; + } + if (RuntimeCode.evalDepth > 0) { + return e; + } + + RuntimeScalar sig = getGlobalHash("main::SIG").get("__DIE__"); + if (!sig.getDefinedBoolean() || isReservedSigString(sig)) { + return e; + } + + var seen = unhandledDieHandlerSeen.get(); + if (seen.containsKey(unwrapped)) { + return e; + } + seen.put(unwrapped, Boolean.TRUE); + + RuntimeArray args = new RuntimeArray(); + RuntimeArray.push(args, new RuntimeScalar(ErrorMessageUtil.stringifyException(unwrapped))); + RuntimeScalar sigHandler = new RuntimeScalar(sig); + SyntheticDieCallerFrame syntheticFrame = firstPerlFrame(unwrapped); + String handlerSubName = signalHandlerSubName(sigHandler); + + int level = DynamicVariableManager.getLocalLevel(); + DynamicVariableManager.pushLocalVariable(sig); + boolean wasInsideUnhandledDieHandler = insideUnhandledDieHandler.get(); + insideUnhandledDieHandler.set(Boolean.TRUE); + boolean pushedSyntheticFrame = false; + try { + if (syntheticFrame != null) { + RuntimeCode.pushSyntheticCallerFrame( + syntheticFrame.packageName(), + syntheticFrame.filename(), + syntheticFrame.line(), + handlerSubName); + pushedSyntheticFrame = true; + } + RuntimeCode.apply(sigHandler, args, RuntimeContextType.SCALAR); + } catch (Throwable handlerException) { + Throwable handled = unwrapException(handlerException); + if (handled instanceof RuntimeException re) { + return re; + } + return new RuntimeException(handled); + } finally { + if (pushedSyntheticFrame) { + RuntimeCode.popSyntheticCallerFrame(); + } + insideUnhandledDieHandler.set(wasInsideUnhandledDieHandler); + DynamicVariableManager.popToLocalLevel(level); + } + return e; + } + /** * Catches the exception in an eval-block. * Note: PerlExitException should NEVER be caught by eval{} - it always propagates. @@ -162,20 +264,24 @@ public static RuntimeBase warn(RuntimeBase message, RuntimeScalar where, String if (err.type == RuntimeScalarType.TIED_SCALAR) { err = err.tiedFetch(); } - if (err.getDefinedBoolean()) { + if (RuntimeScalarType.isReference(err)) { // If $@ is a reference, pass it directly to the signal handler - if (RuntimeScalarType.isReference(err)) { - finalMessage = new RuntimeScalar(err); - } else { - // String in $@, append "...caught" with location - String errStr = err.toString(); - if (!errStr.endsWith("\n")) { - errStr += "\n"; - } - finalMessage = new RuntimeScalar(errStr + "\t...caught" + where.toString()); + finalMessage = new RuntimeScalar(err); + } else if (err.getDefinedBoolean() && !err.toString().isEmpty()) { + // String in $@, append "...caught" with location. If $@ has no + // trailing newline, Perl appends directly after the message. + String errStr = err.toString(); + String caught = errStr.endsWith("\n") ? "\t...caught" : "\t...caught"; + String out = errStr + caught + where; + if (!out.endsWith("\n")) { + out += ".\n"; } + finalMessage = new RuntimeScalar(out); } else { finalMessage = new RuntimeScalar("Warning: something's wrong" + where.toString()); + if (!finalMessage.toString().endsWith("\n")) { + finalMessage.set(finalMessage + ".\n"); + } } } else { // Handle non-empty message diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/DigestMD2.java b/src/main/java/org/perlonjava/runtime/perlmodule/DigestMD2.java new file mode 100644 index 000000000..4264545d0 --- /dev/null +++ b/src/main/java/org/perlonjava/runtime/perlmodule/DigestMD2.java @@ -0,0 +1,342 @@ +package org.perlonjava.runtime.perlmodule; + +import org.perlonjava.frontend.parser.StringParser; +import org.perlonjava.runtime.io.ClosedIOHandle; +import org.perlonjava.runtime.runtimetypes.*; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Base64; + +import static org.perlonjava.runtime.runtimetypes.RuntimeScalarCache.scalarFalse; +import static org.perlonjava.runtime.runtimetypes.RuntimeScalarCache.scalarUndef; +import static org.perlonjava.runtime.runtimetypes.RuntimeScalarType.JAVAOBJECT; + +/** + * Digest::MD2 XS-compatible implementation for PerlOnJava. + */ +public class DigestMD2 extends PerlModuleBase { + + private static final String CLASS_NAME = "Digest::MD2"; + private static final String STATE_KEY = "_md2_state"; + + private static final int[] S = { + 41, 46, 67, 201, 162, 216, 124, 1, 61, 54, 84, 161, 236, 240, 6, + 19, 98, 167, 5, 243, 192, 199, 115, 140, 152, 147, 43, 217, 188, + 76, 130, 202, 30, 155, 87, 60, 253, 212, 224, 22, 103, 66, 111, 24, + 138, 23, 229, 18, 190, 78, 196, 214, 218, 158, 222, 73, 160, 251, + 245, 142, 187, 47, 238, 122, 169, 104, 121, 145, 21, 178, 7, 63, + 148, 194, 16, 137, 11, 34, 95, 33, 128, 127, 93, 154, 90, 144, 50, + 39, 53, 62, 204, 231, 191, 247, 151, 3, 255, 25, 48, 179, 72, 165, + 181, 209, 215, 94, 146, 42, 172, 86, 170, 198, 79, 184, 56, 210, + 150, 164, 125, 182, 118, 252, 107, 226, 156, 116, 4, 241, 69, 157, + 112, 89, 100, 113, 135, 32, 134, 91, 207, 101, 230, 45, 168, 2, 27, + 96, 37, 173, 174, 176, 185, 246, 28, 70, 97, 105, 52, 64, 126, 15, + 85, 71, 163, 35, 221, 81, 175, 58, 195, 92, 249, 206, 186, 197, + 234, 38, 44, 83, 13, 110, 133, 40, 132, 9, 211, 223, 205, 244, 65, + 129, 77, 82, 106, 220, 55, 200, 108, 193, 171, 250, 36, 225, 123, + 8, 12, 189, 177, 74, 120, 136, 149, 139, 227, 99, 232, 109, 233, + 203, 213, 254, 59, 0, 29, 57, 242, 239, 183, 14, 102, 88, 208, 228, + 166, 119, 114, 248, 235, 117, 75, 10, 49, 68, 80, 180, 143, 237, + 31, 26, 219, 153, 141, 51, 159, 17, 131, 20 + }; + + public DigestMD2() { + super(CLASS_NAME, false); + } + + public static void initialize() { + DigestMD2 md2 = new DigestMD2(); + GlobalVariable.getGlobalVariable("Digest::MD2::VERSION").set(new RuntimeScalar("2.04")); + try { + md2.registerMethod("new", "newInstance", null); + md2.registerMethod("clone", null); + md2.registerMethod("add", null); + md2.registerMethod("addfile", null); + md2.registerMethod("digest", null); + md2.registerMethod("hexdigest", null); + md2.registerMethod("b64digest", null); + md2.registerMethod("reset", null); + md2.registerMethod("md2", null); + md2.registerMethod("md2_hex", null); + md2.registerMethod("md2_base64", null); + } catch (NoSuchMethodException e) { + System.err.println("Warning: Missing Digest::MD2 method: " + e.getMessage()); + } + } + + public static RuntimeList newInstance(RuntimeArray args, int ctx) { + if (args.isEmpty()) { + return scalarUndef.getList(); + } + RuntimeScalar invocant = args.get(0); + if (invocant.value instanceof RuntimeHash) { + RuntimeHash self = invocant.hashDeref(); + self.put(STATE_KEY, new RuntimeScalar(new MD2State())); + return invocant.getList(); + } + + String className = invocant.toString(); + if (className.isEmpty()) { + className = CLASS_NAME; + } + RuntimeHash self = new RuntimeHash(); + self.blessId = NameNormalizer.getBlessId(className); + self.put(STATE_KEY, new RuntimeScalar(new MD2State())); + return self.createReference().getList(); + } + + public static RuntimeList clone(RuntimeArray args, int ctx) { + if (args.isEmpty()) { + return scalarUndef.getList(); + } + RuntimeHash self = args.get(0).hashDeref(); + RuntimeHash copy = new RuntimeHash(); + copy.blessId = self.blessId; + copy.put(STATE_KEY, new RuntimeScalar(new MD2State(getState(args.get(0))))); + return copy.createReference().getList(); + } + + public static RuntimeList reset(RuntimeArray args, int ctx) { + if (args.isEmpty()) { + return scalarFalse.getList(); + } + RuntimeHash self = args.get(0).hashDeref(); + self.put(STATE_KEY, new RuntimeScalar(new MD2State())); + return args.get(0).getList(); + } + + public static RuntimeList add(RuntimeArray args, int ctx) { + if (args.isEmpty()) { + return scalarFalse.getList(); + } + MD2State state = getState(args.get(0)); + for (int i = 1; i < args.size(); i++) { + RuntimeScalar data = args.get(i); + if (data.type == RuntimeScalarType.UNDEF) { + continue; + } + String str = data.toString(); + StringParser.assertNoWideCharacters(str, "add"); + state.update(str.getBytes(StandardCharsets.ISO_8859_1)); + } + return args.get(0).getList(); + } + + public static RuntimeList addfile(RuntimeArray args, int ctx) { + if (args.size() < 2) { + throw new PerlCompilerException("No filehandle passed"); + } + MD2State state = getState(args.get(0)); + RuntimeScalar fileArg = args.get(1); + RuntimeIO fh = RuntimeIO.getRuntimeIO(fileArg); + if (fh == null) { + String name = globLeafName(fileArg); + if (name != null) { + throw new PerlCompilerException("Bad filehandle: " + name); + } + throw new PerlCompilerException("No filehandle passed"); + } + if (fh.ioHandle == null || fh.ioHandle instanceof ClosedIOHandle) { + throw new PerlCompilerException("No filehandle passed"); + } + + fh.binmode(":raw"); + while (true) { + RuntimeScalar result = fh.ioHandle.read(4096); + if (result.type == RuntimeScalarType.UNDEF || result.toString().isEmpty()) { + break; + } + state.update(result.toString().getBytes(StandardCharsets.ISO_8859_1)); + } + return args.get(0).getList(); + } + + public static RuntimeList digest(RuntimeArray args, int ctx) { + if (args.isEmpty()) { + return scalarUndef.getList(); + } + return new RuntimeScalar(bytesToLatin1(finishAndReset(args.get(0)))).getList(); + } + + public static RuntimeList hexdigest(RuntimeArray args, int ctx) { + if (args.isEmpty()) { + return scalarUndef.getList(); + } + return new RuntimeScalar(toHex(finishAndReset(args.get(0)))).getList(); + } + + public static RuntimeList b64digest(RuntimeArray args, int ctx) { + if (args.isEmpty()) { + return scalarUndef.getList(); + } + return new RuntimeScalar(toB64NoPad(finishAndReset(args.get(0)))).getList(); + } + + public static RuntimeList md2(RuntimeArray args, int ctx) { + return new RuntimeScalar(bytesToLatin1(digestArgs(args))).getList(); + } + + public static RuntimeList md2_hex(RuntimeArray args, int ctx) { + return new RuntimeScalar(toHex(digestArgs(args))).getList(); + } + + public static RuntimeList md2_base64(RuntimeArray args, int ctx) { + return new RuntimeScalar(toB64NoPad(digestArgs(args))).getList(); + } + + private static MD2State getState(RuntimeScalar selfRef) { + RuntimeHash self = selfRef.hashDeref(); + RuntimeScalar stored = self.get(STATE_KEY); + if (stored != null && stored.type == JAVAOBJECT && stored.value instanceof MD2State state) { + return state; + } + MD2State state = new MD2State(); + self.put(STATE_KEY, new RuntimeScalar(state)); + return state; + } + + private static byte[] finishAndReset(RuntimeScalar selfRef) { + MD2State state = getState(selfRef); + byte[] digest = state.finish(); + state.reset(); + return digest; + } + + private static byte[] digestArgs(RuntimeArray args) { + MD2State state = new MD2State(); + for (int i = 0; i < args.size(); i++) { + RuntimeScalar data = args.get(i); + if (data.type == RuntimeScalarType.UNDEF) { + continue; + } + String str = data.toString(); + StringParser.assertNoWideCharacters(str, "md2"); + state.update(str.getBytes(StandardCharsets.ISO_8859_1)); + } + return state.finish(); + } + + private static String globLeafName(RuntimeScalar scalar) { + if (scalar.value instanceof RuntimeGlob glob && glob.globName != null) { + int idx = glob.globName.lastIndexOf("::"); + return idx >= 0 ? glob.globName.substring(idx + 2) : glob.globName; + } + return null; + } + + private static String bytesToLatin1(byte[] bytes) { + return new String(bytes, StandardCharsets.ISO_8859_1); + } + + private static String toHex(byte[] bytes) { + StringBuilder sb = new StringBuilder(bytes.length * 2); + for (byte b : bytes) { + sb.append(String.format("%02x", b & 0xff)); + } + return sb.toString(); + } + + private static String toB64NoPad(byte[] bytes) { + return Base64.getEncoder().encodeToString(bytes).replaceAll("=+$", ""); + } + + private static final class MD2State { + private final int[] state = new int[16]; + private final int[] checksum = new int[16]; + private final int[] buffer = new int[16]; + private int count; + + MD2State() { + } + + MD2State(MD2State other) { + System.arraycopy(other.state, 0, state, 0, state.length); + System.arraycopy(other.checksum, 0, checksum, 0, checksum.length); + System.arraycopy(other.buffer, 0, buffer, 0, buffer.length); + count = other.count; + } + + void reset() { + Arrays.fill(state, 0); + Arrays.fill(checksum, 0); + Arrays.fill(buffer, 0); + count = 0; + } + + void update(byte[] input) { + int index = count; + count = (index + input.length) & 0x0f; + int partLen = 16 - index; + int i; + + if (input.length >= partLen) { + for (int j = 0; j < partLen; j++) { + buffer[index + j] = input[j] & 0xff; + } + transform(buffer); + + for (i = partLen; i + 15 < input.length; i += 16) { + int[] block = new int[16]; + for (int j = 0; j < 16; j++) { + block[j] = input[i + j] & 0xff; + } + transform(block); + } + index = 0; + } else { + i = 0; + } + + for (; i < input.length; i++) { + buffer[index++] = input[i] & 0xff; + } + } + + byte[] finish() { + int padLen = 16 - count; + byte[] padding = new byte[padLen]; + Arrays.fill(padding, (byte) padLen); + update(padding); + + byte[] checksumBytes = new byte[16]; + for (int i = 0; i < 16; i++) { + checksumBytes[i] = (byte) checksum[i]; + } + update(checksumBytes); + + byte[] digest = new byte[16]; + for (int i = 0; i < 16; i++) { + digest[i] = (byte) state[i]; + } + return digest; + } + + private void transform(int[] block) { + int[] x = new int[48]; + for (int i = 0; i < 16; i++) { + x[i] = state[i]; + x[i + 16] = block[i]; + x[i + 32] = state[i] ^ block[i]; + } + + int t = 0; + for (int i = 0; i < 18; i++) { + for (int j = 0; j < 48; j++) { + x[j] ^= S[t]; + t = x[j] & 0xff; + } + t = (t + i) & 0xff; + } + + System.arraycopy(x, 0, state, 0, 16); + + t = checksum[15]; + for (int i = 0; i < 16; i++) { + checksum[i] ^= S[block[i] ^ t]; + checksum[i] &= 0xff; + t = checksum[i]; + } + } + } +} diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/Feature.java b/src/main/java/org/perlonjava/runtime/perlmodule/Feature.java index 67068568c..4e37d6281 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/Feature.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/Feature.java @@ -33,6 +33,17 @@ public static void initialize() { feature.registerMethod("feature_bundle", ";$"); feature.registerMethod("import", "useFeature", ";$"); feature.registerMethod("unimport", "noFeature", ";$"); + + // Perl exposes feature gates such as indirect, multidimensional, + // and bareword::filehandles as pragma packages with import and + // unimport methods even when their .pm files are not loaded. + // strictures' test suite fakes %INC entries for those packages and + // then calls ->unimport directly, so the methods must exist at + // startup just like they do on Perl. + for (String pragma : new String[]{"indirect", "multidimensional", "bareword::filehandles"}) { + feature.registerMethodInPackage(pragma, "import", "useFeaturePragma"); + feature.registerMethodInPackage(pragma, "unimport", "noFeaturePragma"); + } } catch (NoSuchMethodException e) { System.err.println("Warning: Missing Feature method: " + e.getMessage()); } @@ -116,6 +127,34 @@ public static RuntimeList noFeature(RuntimeArray args, int ctx) { return new RuntimeScalar().getList(); } + public static RuntimeList useFeaturePragma(RuntimeArray args, int ctx) { + String featureName = featureNameFromPragma(args); + if (featureName != null) { + featureManager.enableFeatureBundle(featureName); + } + return new RuntimeScalar().getList(); + } + + public static RuntimeList noFeaturePragma(RuntimeArray args, int ctx) { + String featureName = featureNameFromPragma(args); + if (featureName != null) { + featureManager.disableFeatureBundle(featureName); + } + return new RuntimeScalar().getList(); + } + + private static String featureNameFromPragma(RuntimeArray args) { + if (args.size() == 0) { + return null; + } + String pragma = args.get(0).toString(); + return switch (pragma) { + case "bareword::filehandles" -> "bareword_filehandles"; + case "indirect", "multidimensional" -> pragma; + default -> null; + }; + } + /** * Checks if a feature is enabled. * diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/FilterUtilCall.java b/src/main/java/org/perlonjava/runtime/perlmodule/FilterUtilCall.java index 8d91ea4d2..4a28bfbb7 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/FilterUtilCall.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/FilterUtilCall.java @@ -258,8 +258,11 @@ public static String applyFilters(String sourceCode) { context.sourceLines = sourceCode.split("(?<=\n)", -1); context.currentLine = 0; - // Apply each filter in the stack (LIFO order) + // Apply each filter in the stack (LIFO order). Source filters use $_ + // as a scratch buffer; localize the slot so caller aliases such as + // grep's $_ are not modified by filter_read/filter output. RuntimeScalar savedDefaultVar = GlobalVariable.getGlobalVariable("main::_"); + GlobalVariable.aliasGlobalVariable("main::_", new RuntimeScalar("")); StringBuilder filteredCode = new StringBuilder(); try { @@ -271,9 +274,6 @@ public static String applyFilters(String sourceCode) { RuntimeScalar packageName = filterEntry.get(1); RuntimeScalar isCodeRef = filterEntry.get(2); - // Clear $_ - GlobalVariable.getGlobalVariable("main::_").set(""); - if (isCodeRef.getBoolean()) { // Closure filter: call the coderef repeatedly RuntimeCode code = (RuntimeCode) filterObj.value; @@ -376,8 +376,8 @@ public static String applyFilters(String sourceCode) { return filteredCode.toString(); } finally { - // Restore $_ - GlobalVariable.getGlobalVariable("main::_").set(savedDefaultVar.toString()); + // Restore the caller's $_ slot, preserving aliases/tied scalars. + GlobalVariable.aliasGlobalVariable("main::_", savedDefaultVar); // Clean up context context.sourceLines = null; @@ -546,4 +546,3 @@ static class FilterContext { boolean inFilterRead = false; } } - diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/MIMEBase64.java b/src/main/java/org/perlonjava/runtime/perlmodule/MIMEBase64.java index 7d10aa4b1..3f34479a4 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/MIMEBase64.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/MIMEBase64.java @@ -70,8 +70,7 @@ public static RuntimeList decode_base64(RuntimeArray args, int ctx) { // Our custom decoder handles invalid characters and padding internally byte[] decodedBytes = Base64Util.decode(encoded); - String decoded = new String(decodedBytes, StandardCharsets.ISO_8859_1); - return new RuntimeScalar(decoded).getList(); + return new RuntimeScalar(decodedBytes).getList(); } public static RuntimeList encode_base64url(RuntimeArray args, int ctx) { @@ -103,7 +102,6 @@ public static RuntimeList decode_base64url(RuntimeArray args, int ctx) { String encoded = input.toString(); byte[] decodedBytes = Base64Util.decodeUrl(encoded); - String decoded = new String(decodedBytes, StandardCharsets.ISO_8859_1); - return new RuntimeScalar(decoded).getList(); + return new RuntimeScalar(decodedBytes).getList(); } } diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/ScalarUtil.java b/src/main/java/org/perlonjava/runtime/perlmodule/ScalarUtil.java index 5f7276068..e217bb0e2 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/ScalarUtil.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/ScalarUtil.java @@ -87,6 +87,12 @@ public static RuntimeList blessed(RuntimeArray args, int ctx) { if (scalar.type == RuntimeScalarType.REGEX) { return new RuntimeScalar("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 new RuntimeScalar().getList(); // undef } return new RuntimeScalar(NameNormalizer.getBlessStr(blessId)).getList(); diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/Socket.java b/src/main/java/org/perlonjava/runtime/perlmodule/Socket.java index a95061124..33b06aade 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/Socket.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/Socket.java @@ -26,10 +26,11 @@ public class Socket extends PerlModuleBase { public static final int AF_INET = 2; public static final int AF_INET6 = IS_MAC ? 30 : IS_WINDOWS ? 23 : 10; public static final int AF_UNIX = 1; + public static final int AF_UNSPEC = 0; public static final int PF_INET = 2; public static final int PF_INET6 = AF_INET6; public static final int PF_UNIX = 1; - public static final int PF_UNSPEC = 0; + public static final int PF_UNSPEC = AF_UNSPEC; public static final int SOMAXCONN = IS_WINDOWS ? 0x7fffffff : 128; public static final int SOCK_STREAM = 1; public static final int SOCK_DGRAM = 2; @@ -111,6 +112,7 @@ public static void initialize() { socket.registerMethod("AF_INET", ""); socket.registerMethod("AF_INET6", ""); socket.registerMethod("AF_UNIX", ""); + socket.registerMethod("AF_UNSPEC", ""); socket.registerMethod("PF_INET", ""); socket.registerMethod("PF_INET6", ""); socket.registerMethod("PF_UNIX", ""); @@ -644,6 +646,10 @@ public static RuntimeList AF_UNIX(RuntimeArray args, int ctx) { return new RuntimeScalar(AF_UNIX).getList(); } + public static RuntimeList AF_UNSPEC(RuntimeArray args, int ctx) { + return new RuntimeScalar(AF_UNSPEC).getList(); + } + public static RuntimeList PF_INET6(RuntimeArray args, int ctx) { return new RuntimeScalar(PF_INET6).getList(); } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/ErrorMessageUtil.java b/src/main/java/org/perlonjava/runtime/runtimetypes/ErrorMessageUtil.java index f91bdbab0..4cfa2d859 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/ErrorMessageUtil.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/ErrorMessageUtil.java @@ -172,12 +172,22 @@ public static String stringifyException(Throwable t, int skipLevels) { // Use the custom formatter to print the Perl message and stack trace StringBuilder sb = new StringBuilder(); - String message = innermostCause.getMessage(); + String perlDiePayloadMessage = null; + if (innermostCause instanceof PerlDieException pde && pde.getPayload() != null) { + RuntimeScalar first = pde.getPayload().getFirst(); + if (first != null && RuntimeScalarType.isReference(first)) { + perlDiePayloadMessage = first.toString(); + } + } + + String message = perlDiePayloadMessage != null + ? perlDiePayloadMessage + : innermostCause.getMessage(); // Use this for debugging // t.printStackTrace(); - String message1 = t.getMessage(); + String message1 = perlDiePayloadMessage != null ? message : t.getMessage(); // Check if the original message ends with \n - Perl skips stack trace in that case boolean suppressStackTrace = (message != null && message.endsWith("\n")) || (message1 != null && message1.endsWith("\n")); diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/MortalList.java b/src/main/java/org/perlonjava/runtime/runtimetypes/MortalList.java index dab680e5e..6c2e7494d 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/MortalList.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/MortalList.java @@ -371,6 +371,9 @@ public static void scopeExitCleanupHash(RuntimeHash hash) { // accessible through the reference. Cleanup will happen when the last // reference is released (in DestroyDispatch.callDestroy). if (hash.refCount > 0) return; + if (hash.type == RuntimeHash.TIED_HASH && hash.elements instanceof TieHash tieHash) { + tieHash.releaseTiedObject(); + } // Quick scan: skip if no value could transitively contain blessed/tracked refs. boolean needsWalk = false; for (RuntimeScalar val : hash.elements.values()) { @@ -437,6 +440,9 @@ public static void scopeExitCleanupArray(RuntimeArray arr) { // accessible through the reference. Cleanup will happen when the last // reference is released (in DestroyDispatch.callDestroy). if (arr.refCount > 0) return; + if (arr.type == RuntimeArray.TIED_ARRAY && arr.elements instanceof TieArray tieArray) { + tieArray.releaseTiedObject(); + } // Alias arrays such as @_ do not own their original elements. They can // still receive owned inserts via unshift/push before a goto ⊂ in // that mixed case, release only those inserted elements. diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java index 104621074..0f9370965 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java @@ -1315,16 +1315,24 @@ private static void popEvalRuntimeContext(EvalRuntimeContext context) { } private static void pushSyntheticEvalCallerFrame(String packageName, String filename, int line) { + pushSyntheticCallerFrame(packageName, filename, line, "(eval)", "virtual-eval"); + } + + public static void pushSyntheticCallerFrame(String packageName, String filename, int line, String subName) { + pushSyntheticCallerFrame(packageName, filename, line, subName, "synthetic-own-sub"); + } + + private static void pushSyntheticCallerFrame(String packageName, String filename, int line, String subName, String marker) { ArrayList frame = new ArrayList<>(); frame.add(packageName != null && !packageName.isEmpty() ? packageName : "main"); frame.add(filename != null ? filename : "(eval)"); frame.add(String.valueOf(line)); - frame.add("(eval)"); - frame.add("virtual-eval"); + frame.add(subName); + frame.add(marker); syntheticCallerFrames.get().addLast(frame); } - private static void popSyntheticEvalCallerFrame() { + public static void popSyntheticCallerFrame() { ArrayDeque> stack = syntheticCallerFrames.get(); if (!stack.isEmpty()) { stack.removeLast(); @@ -1334,6 +1342,10 @@ private static void popSyntheticEvalCallerFrame() { } } + private static void popSyntheticEvalCallerFrame() { + popSyntheticCallerFrame(); + } + private static void registerEvalRuntimeAlias(EvalRuntimeContext context, char sigil, String fullName, RuntimeBase value) { context.aliases().add(new EvalRuntimeAlias(sigil, fullName, value)); } @@ -3087,7 +3099,12 @@ public static RuntimeList callerWithSub(RuntimeList args, int ctx, RuntimeScalar int insertAt = Math.min(baseSkip + 1, stackTrace.size()); ArrayList> framesToInsert = new ArrayList<>(); for (Iterator> it = syntheticFrames.descendingIterator(); it.hasNext(); ) { - framesToInsert.add(new ArrayList<>(it.next())); + ArrayList syntheticFrame = new ArrayList<>(it.next()); + if (isSyntheticOwnSubFrame(syntheticFrame)) { + stackTrace.add(syntheticOwnSubInsertAt(stackTrace, syntheticFrame), syntheticFrame); + } else { + framesToInsert.add(syntheticFrame); + } } stackTrace.addAll(insertAt, framesToInsert); } @@ -3123,6 +3140,10 @@ public static RuntimeList callerWithSub(RuntimeList args, int ctx, RuntimeScalar res.add(new RuntimeScalar(normalizeCallerPackage(pkg))); } else { ArrayList frameInfo = stackTrace.get(frame); + int syntheticOwnSubFramesBefore = countSyntheticOwnSubFramesBefore(stackTrace, frame); + int trackedOriginalFrame = Math.max(0, originalFrame - syntheticOwnSubFramesBefore); + int trackedActiveCodeFrame = activeCodeFrameForCaller(trackedOriginalFrame); + int trackedArgsFrame = Math.max(0, argsFrame - syntheticOwnSubFramesBefore); String pkg = frameInfo.get(0); res.add(new RuntimeScalar(normalizeCallerPackage(pkg))); // package res.add(new RuntimeScalar(frameInfo.get(1))); // filename @@ -3138,14 +3159,16 @@ public static RuntimeList callerWithSub(RuntimeList args, int ctx, RuntimeScalar String frameSubName = frameInfo.size() > 3 ? frameInfo.get(3) : null; boolean virtualEvalFrame = frameInfo.size() > 4 && "virtual-eval".equals(frameInfo.get(4)); + boolean syntheticOwnSubFrame = frameInfo.size() > 4 + && "synthetic-own-sub".equals(frameInfo.get(4)); // Synthetic frames such as eval blocks describe the frame itself, // not the previous caller, so preserve their own subroutine name. - if (virtualEvalFrame && frameSubName != null && frameSubName.startsWith("(")) { + if ((syntheticOwnSubFrame || virtualEvalFrame && frameSubName != null && frameSubName.startsWith("("))) { subName = frameSubName; } - RuntimeCode activeCode = getActiveCodeAt(originalFrame); + RuntimeCode activeCode = getActiveCodeAt(trackedActiveCodeFrame); if (subName == null && activeCode != null) { subName = callerSubNameForCode(activeCode); } @@ -3227,7 +3250,15 @@ public static RuntimeList callerWithSub(RuntimeList args, int ctx, RuntimeScalar // out from under the caller. Perl preserves the invocation // args here — critical for DBIC TxnScopeGuard double-DESTROY // detection. - RuntimeArray frameArgs = getOriginalArgsAt(argsFrame); + RuntimeArray frameArgs = getOriginalArgsAt(trackedArgsFrame); + if (WarnDie.isInsideUnhandledDieHandler() && syntheticOwnSubFramesBefore > 0) { + RuntimeArray activeFrameArgs = getOriginalArgsAt(trackedActiveCodeFrame); + if (activeFrameArgs != null) { + frameArgs = activeFrameArgs; + } + } else if (frameArgs == null && WarnDie.isInsideUnhandledDieHandler()) { + frameArgs = getOriginalArgsAt(trackedActiveCodeFrame); + } if (frameArgs != null) { dbArgs.setFromListAliased(frameArgs.getList()); } else { @@ -3244,7 +3275,15 @@ public static RuntimeList callerWithSub(RuntimeList args, int ctx, RuntimeScalar // - apply(String, RuntimeArray, int) pushes true (fresh args / func()) // Fall back to the name-based heuristic for frames outside our tracking // (e.g., top-level code, eval frames). - Boolean hasArgsFromStack = getHasArgsAt(originalFrame); + Boolean hasArgsFromStack = getHasArgsAt(trackedOriginalFrame); + if (WarnDie.isInsideUnhandledDieHandler() && syntheticOwnSubFramesBefore > 0) { + Boolean activeHasArgs = getHasArgsAt(trackedActiveCodeFrame); + if (activeHasArgs != null) { + hasArgsFromStack = activeHasArgs; + } + } else if (hasArgsFromStack == null && WarnDie.isInsideUnhandledDieHandler()) { + hasArgsFromStack = getHasArgsAt(trackedActiveCodeFrame); + } if (hasArgsFromStack != null) { res.add(hasArgsFromStack ? RuntimeScalarCache.scalarTrue : RuntimeScalarCache.scalarUndef); } else { @@ -3324,7 +3363,8 @@ public static RuntimeList callerWithSub(RuntimeList args, int ctx, RuntimeScalar } // end if (hasExplicitExpr) } } else if (frame >= stackTraceSize) { - RuntimeCode activeCode = hasExplicitExpr ? getActiveCodeAt(originalFrame) : null; + int trackedOriginalFrame = Math.max(0, originalFrame - countSyntheticOwnSubFramesBefore(stackTrace, stackTrace.size())); + RuntimeCode activeCode = hasExplicitExpr ? getActiveCodeAt(activeCodeFrameForCaller(trackedOriginalFrame)) : null; String activeSubName = activeCode != null ? applyAnonNameOverride(callerSubNameForCode(activeCode)) : null; @@ -3339,7 +3379,7 @@ && hasExplicitlyRenamedActiveCode() res.add(new RuntimeScalar(activeCode.cvStartFile != null ? activeCode.cvStartFile : "")); res.add(new RuntimeScalar(activeCode.cvStartLine)); res.add(new RuntimeScalar(activeSubName)); - Boolean hasArgsFromStack = getHasArgsAt(originalFrame); + Boolean hasArgsFromStack = getHasArgsAt(trackedOriginalFrame); res.add(hasArgsFromStack != null && hasArgsFromStack ? RuntimeScalarCache.scalarTrue : RuntimeScalarCache.scalarUndef); @@ -3396,6 +3436,48 @@ private static String callerSubNameForCode(RuntimeCode code) { return pkg + "::" + code.subName; } + private static int activeCodeFrameForCaller(int originalFrame) { + return originalFrame + (WarnDie.isInsideUnhandledDieHandler() ? 1 : 0); + } + + private static boolean isSyntheticOwnSubFrame(ArrayList frame) { + return frame.size() > 4 && "synthetic-own-sub".equals(frame.get(4)); + } + + private static int syntheticOwnSubInsertAt(ArrayList> stackTrace, ArrayList syntheticFrame) { + String syntheticPackage = syntheticFrame.isEmpty() ? null : syntheticFrame.get(0); + String syntheticFile = syntheticFrame.size() > 1 ? syntheticFrame.get(1) : null; + for (int i = 0; i < stackTrace.size(); i++) { + ArrayList frame = stackTrace.get(i); + String framePackage = !frame.isEmpty() ? frame.get(0) : null; + String frameFile = frame.size() > 1 ? frame.get(1) : null; + if (Objects.equals(syntheticFile, frameFile) + && (Objects.equals(syntheticPackage, framePackage) + || syntheticPackage == null || syntheticPackage.isEmpty())) { + return i; + } + } + for (int i = 0; i < stackTrace.size(); i++) { + ArrayList frame = stackTrace.get(i); + String frameFile = frame.size() > 1 ? frame.get(1) : null; + if (Objects.equals(syntheticFile, frameFile)) { + return i; + } + } + return stackTrace.size(); + } + + private static int countSyntheticOwnSubFramesBefore(ArrayList> stackTrace, int frame) { + int count = 0; + int end = Math.min(frame, stackTrace.size()); + for (int i = 0; i < end; i++) { + if (isSyntheticOwnSubFrame(stackTrace.get(i))) { + count++; + } + } + return count; + } + private static boolean hasExplicitlyRenamedActiveCode() { for (RuntimeCode active : activeCodeStack.get()) { if (active.explicitlyRenamed) { @@ -3679,6 +3761,7 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, RuntimeArray a, int RuntimeList result = e.returnValue != null ? e.returnValue.getList() : new RuntimeList(); return coerceScalarCallResult(result, effectiveContext, callContext, !isLvalueCode(code)); } catch (RuntimeException e) { + e = WarnDie.maybeInvokeUnhandledDieHandler(e); // On die: run scopeExitCleanup for my-variables whose normal // SCOPE_EXIT_CLEANUP bytecodes were skipped by the exception. // PerlExitException (exit()) is excluded — global destruction handles it. @@ -3966,6 +4049,7 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, String subroutineNa RuntimeList result = e.returnValue != null ? e.returnValue.getList() : new RuntimeList(); return coerceScalarCallResult(result, effectiveContext, callContext, !isLvalueCode(code)); } catch (RuntimeException e) { + e = WarnDie.maybeInvokeUnhandledDieHandler(e); if (!(e instanceof PerlExitException)) { MyVarCleanupStack.unwindTo(cleanupMark); MortalList.flush(); @@ -4227,6 +4311,7 @@ private static RuntimeList applyImpl(RuntimeScalar runtimeScalar, String subrout RuntimeList result = e.returnValue != null ? e.returnValue.getList() : new RuntimeList(); return coerceScalarCallResult(result, effectiveContext, callContext, !isLvalueCode(code)); } catch (RuntimeException e) { + e = WarnDie.maybeInvokeUnhandledDieHandler(e); if (!(e instanceof PerlExitException)) { MyVarCleanupStack.unwindTo(cleanupMark); MortalList.flush(); @@ -4697,6 +4782,8 @@ public RuntimeList apply(RuntimeArray a, int callContext) { return detachTryExpressionLvalueResult( coerceScalarCallResult(result, effectiveContext, callContext, !isLvalueCode(this)), callContext); + } catch (RuntimeException e) { + throw WarnDie.maybeInvokeUnhandledDieHandler(e); } finally { if (warningBits != null) { WarningBitsRegistry.popCurrent(); @@ -4822,6 +4909,8 @@ public RuntimeList apply(String subroutineName, RuntimeArray a, int callContext) return detachTryExpressionLvalueResult( coerceScalarCallResult(result, effectiveContext, callContext, !isLvalueCode(this)), callContext); + } catch (RuntimeException e) { + throw WarnDie.maybeInvokeUnhandledDieHandler(e); } finally { if (warningBits != null) { WarningBitsRegistry.popCurrent(); diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java index 0cdc70253..4da33fd53 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java @@ -31,6 +31,10 @@ public class RuntimeGlob extends RuntimeScalar implements RuntimeScalarReference // values alive independently of later stash deletion, so slot access on // the saved scalar must consult these snapshots before global maps. private boolean slotSnapshot; + // True for detached copies of the current named glob where the HASH slot + // did not exist at copy time. A later `%{*$copy}` should vivify/share the + // canonical GV hash, matching Perl's behavior for globs passed through @_. + private boolean hashSlotAliasesNamedGlob; // Dynamic name override set by `*foo = $string` glob-as-scalar assignment. // Used to honor the `local *PKG::__ANON__ = 'name'` idiom (see SUPER.pm, // Try::Tiny, namespace::clean) — caller()/Sub::Util::subname report this @@ -103,8 +107,12 @@ public RuntimeGlob createDetachedCopy() { if (GlobalVariable.existsGlobalArray(this.globName)) { copy.arraySlot = GlobalVariable.getGlobalArray(this.globName); } - if (GlobalVariable.existsGlobalHash(this.globName) || this.globName.endsWith("::")) { + if (this.hashSlot != null) { + copy.hashSlot = this.hashSlot; + } else if (GlobalVariable.existsGlobalHash(this.globName) || this.globName.endsWith("::")) { copy.hashSlot = GlobalVariable.getGlobalHash(this.globName); + } else if (GlobalVariable.peekGlobalIO(this.globName) == this) { + copy.hashSlotAliasesNamedGlob = true; } return copy; } @@ -805,6 +813,10 @@ public RuntimeScalar getGlobSlot(RuntimeScalar index) { yield this.hashSlot.createReference(); } if (this.slotSnapshot) { + if (this.hashSlot == null && this.hashSlotAliasesNamedGlob + && GlobalVariable.existsGlobalHash(this.globName)) { + this.hashSlot = GlobalVariable.getGlobalHash(this.globName); + } yield this.hashSlot != null ? this.hashSlot.createReference() : new RuntimeScalar(); @@ -857,7 +869,9 @@ public RuntimeScalar getIO() { public RuntimeHash getGlobHash() { if (this.slotSnapshot) { if (this.hashSlot == null) { - this.hashSlot = new RuntimeHash(); + this.hashSlot = this.hashSlotAliasesNamedGlob && this.globName != null + ? GlobalVariable.getGlobalHash(this.globName) + : new RuntimeHash(); } return this.hashSlot; } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeHash.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeHash.java index a0a579c04..14f206f24 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeHash.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeHash.java @@ -820,7 +820,7 @@ public RuntimeScalar createReferenceWithTrackedElements() { if (this.refCount == -1) { this.refCount = 0; } - RuntimeScalar result = createReference(); + RuntimeScalar result = createAnonymousReference(); for (RuntimeScalar elem : this.elements.values()) { RuntimeScalar.incrementRefCountForContainerStore(elem); } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java index 7f15b0d9a..04fc5101d 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java @@ -2072,7 +2072,7 @@ public RuntimeArray arrayDeref() { // Autovivify: create array ref, store back to tied var, re-fetch RuntimeArray arr = new RuntimeArray(); arr.strictAutovivify = true; - tiedStore(arr.createReference()); + tiedStore(arr.createAnonymousReference()); yield arr; } yield fetched.arrayDeref(); @@ -2183,7 +2183,7 @@ public RuntimeHash hashDeref() { if (fetched.type == RuntimeScalarType.UNDEF) { // Autovivify: create hash ref, store back to tied var RuntimeHash hash = new RuntimeHash(); - tiedStore(hash.createReference()); + tiedStore(hash.createAnonymousReference()); yield hash; } yield fetched.hashDeref(); @@ -2366,7 +2366,7 @@ public RuntimeHash hashDerefNonStrict(String packageName) { RuntimeScalar fetched = tiedFetch(); if (fetched.type == RuntimeScalarType.UNDEF) { RuntimeHash hash = new RuntimeHash(); - tiedStore(hash.createReference()); + tiedStore(hash.createAnonymousReference()); yield hash; } yield fetched.hashDerefNonStrict(packageName); @@ -2441,7 +2441,7 @@ public RuntimeArray arrayDerefNonStrict(String packageName) { if (fetched.type == RuntimeScalarType.UNDEF) { RuntimeArray arr = new RuntimeArray(); arr.strictAutovivify = true; - tiedStore(arr.createReference()); + tiedStore(arr.createAnonymousReference()); yield arr; } yield fetched.arrayDerefNonStrict(packageName); diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalarType.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalarType.java index fa90b8b5a..f884a0c15 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalarType.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalarType.java @@ -39,6 +39,11 @@ private RuntimeScalarType() { // Get blessing ID as an integer public static int blessedId(RuntimeScalar runtimeScalar) { if (runtimeScalar.type == READONLY_SCALAR) return blessedId((RuntimeScalar) runtimeScalar.value); + if (runtimeScalar.type == FORMAT) { + return runtimeScalar.value instanceof RuntimeBase base + ? NameNormalizer.getEffectiveBlessId(base.blessId) + : 0; + } if ((runtimeScalar.type & REFERENCE_BIT) != 0) { if (runtimeScalar.value == null) return 0; return NameNormalizer.getEffectiveBlessId(((RuntimeBase) runtimeScalar.value).blessId); @@ -48,6 +53,7 @@ public static int blessedId(RuntimeScalar runtimeScalar) { public static boolean isReference(RuntimeScalar runtimeScalar) { if (runtimeScalar.type == READONLY_SCALAR) return isReference((RuntimeScalar) runtimeScalar.value); + if (runtimeScalar.type == FORMAT) return true; return (runtimeScalar.type & REFERENCE_BIT) != 0; } } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/TieArray.java b/src/main/java/org/perlonjava/runtime/runtimetypes/TieArray.java index c5398e370..65d3dc73e 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/TieArray.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/TieArray.java @@ -50,6 +50,8 @@ public class TieArray extends ArrayList { private final RuntimeArray parent; + private boolean tiedObjectReleased = false; + /** * Creates a new TieArray instance. * @@ -284,6 +286,9 @@ public RuntimeScalar get(int i) { * Decrements refCount and triggers DESTROY if it reaches 0. */ public void releaseTiedObject() { + if (tiedObjectReleased) return; + tiedObjectReleased = true; + if (self == null) return; if ((self.type & RuntimeScalarType.REFERENCE_BIT) != 0 && self.value instanceof RuntimeBase base) { if (base.refCount > 0 && --base.refCount == 0) { @@ -292,4 +297,4 @@ public void releaseTiedObject() { } } } -} \ No newline at end of file +} diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/TieHandle.java b/src/main/java/org/perlonjava/runtime/runtimetypes/TieHandle.java index d885c8d42..487edc6ec 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/TieHandle.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/TieHandle.java @@ -174,6 +174,13 @@ public static RuntimeScalar tiedWrite(TieHandle tieHandle, RuntimeScalar data, R return tieHandle.tieCall("WRITE", data, length, offset); } + @Override + public RuntimeScalar write(String data) { + RuntimeIO.lastWrittenHandle = this; + RuntimeList args = new RuntimeList(new RuntimeScalar(data)); + return tiedPrint(this, args); + } + /** * Unties a filehandle (delegates to UNTIE if exists). */ diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/TieHash.java b/src/main/java/org/perlonjava/runtime/runtimetypes/TieHash.java index 39cb14711..707898655 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/TieHash.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/TieHash.java @@ -42,6 +42,8 @@ public class TieHash extends HashMap { */ private final RuntimeHash previousValue; + private boolean tiedObjectReleased = false; + /** * Creates a new TieHash instance. * @@ -208,6 +210,9 @@ public String getTiedPackage() { * Decrements refCount and triggers DESTROY if it reaches 0. */ public void releaseTiedObject() { + if (tiedObjectReleased) return; + tiedObjectReleased = true; + if (self == null) return; if ((self.type & RuntimeScalarType.REFERENCE_BIT) != 0 && self.value instanceof RuntimeBase base) { if (base.refCount > 0 && --base.refCount == 0) { diff --git a/src/main/perl/lib/B/Concise.pm b/src/main/perl/lib/B/Concise.pm new file mode 100644 index 000000000..4410c4450 --- /dev/null +++ b/src/main/perl/lib/B/Concise.pm @@ -0,0 +1,11 @@ +package B::Concise; + +use strict; +use warnings; + +our $VERSION = '1.88'; + +# PerlOnJava does not expose Perl op trees. Loading B::Concise succeeds so +# probe-style tests can detect that compile() is unavailable and skip cleanly. + +1; diff --git a/src/main/perl/lib/DBI.pm b/src/main/perl/lib/DBI.pm index 693f961ad..2bf73c0d8 100644 --- a/src/main/perl/lib/DBI.pm +++ b/src/main/perl/lib/DBI.pm @@ -954,12 +954,14 @@ sub bind_col { sub selectrow_array { my $arr = selectrow_arrayref(@_); - return $arr ? @$arr : (); + return unless $arr; + return wantarray ? @$arr : $arr->[0]; } sub fetchrow_array { my $arr = fetchrow_arrayref(@_); - return $arr ? @$arr : (); + return unless $arr; + return wantarray ? @$arr : $arr->[0]; } sub fetch { @@ -1314,7 +1316,8 @@ sub connect_cached { my $fetchrow_array = sub { my $arr = fetchrow_arrayref(@_); - return $arr ? @$arr : (); + return unless $arr; + return wantarray ? @$arr : $arr->[0]; }; my $fetch = sub { diff --git a/src/main/perl/lib/Digest/MD2.pm b/src/main/perl/lib/Digest/MD2.pm new file mode 100644 index 000000000..a753c69c1 --- /dev/null +++ b/src/main/perl/lib/Digest/MD2.pm @@ -0,0 +1,18 @@ +package Digest::MD2; + +use strict; +use warnings; +use base "Digest::base"; + +our $VERSION = '2.04'; + +use Exporter (); +our @ISA = qw(Exporter); +our @EXPORT_OK = qw(md2 md2_hex md2_base64); + +require XSLoader; +XSLoader::load('Digest::MD2', $VERSION); + +*reset = \&new; + +1; diff --git a/src/main/perl/lib/ExtUtils/MakeMaker.pm b/src/main/perl/lib/ExtUtils/MakeMaker.pm index 465311e90..cd66ce5f9 100644 --- a/src/main/perl/lib/ExtUtils/MakeMaker.pm +++ b/src/main/perl/lib/ExtUtils/MakeMaker.pm @@ -840,20 +840,7 @@ sub _create_install_makefile { my $depend_rules_str = _make_depend_rules($args->{depend}); - # Build PREREQ_PM comment (MakeMaker writes these for tools to parse) - my $prereq_comment = ''; - if ($args->{PREREQ_PM} && %{$args->{PREREQ_PM}}) { - my @prereqs; - for my $mod (sort keys %{$args->{PREREQ_PM}}) { - next if $mod eq 'perl'; - my $ver = $args->{PREREQ_PM}{$mod}; - $ver = 0 unless defined $ver; - push @prereqs, "$mod=>q[$ver]"; - } - if (@prereqs) { - $prereq_comment = "#\tPREREQ_PM => { " . join(", ", @prereqs) . " }\n"; - } - } + my $prereq_comment = _make_prereq_comment($args); # Honor user-supplied `macro => { ... }` from WriteMakefile by emitting # extra Makefile macro definitions. LaTeXML and others stuff custom @@ -996,6 +983,31 @@ MAKEFILE close $fh; } +sub _make_prereq_comment { + my ($args) = @_; + + my $comment = ''; + for my $key (qw(PREREQ_PM BUILD_REQUIRES TEST_REQUIRES CONFIGURE_REQUIRES)) { + next unless ref $args->{$key} eq 'HASH' && %{$args->{$key}}; + + my @prereqs; + for my $mod (sort keys %{$args->{$key}}) { + next if $mod eq 'perl'; + my $ver = $args->{$key}{$mod}; + $ver = 0 unless defined $ver; + push @prereqs, "$mod=>q[$ver]"; + } + next unless @prereqs; + + # MakeMaker writes prerequisite hashes into generated Makefiles for + # CPAN tooling to parse. Keep the same recognizable shape for all + # supported prereq phases, not just runtime PREREQ_PM. + $comment .= "#\t$key => { " . join(", ", @prereqs) . " }\n"; + } + + return $comment; +} + # Helper: generate a shell mkdir -p command for Makefile sub _shell_mkdir { my ($dir) = @_; diff --git a/src/main/perl/lib/POSIX.pm b/src/main/perl/lib/POSIX.pm index 0ea171008..592e8ac89 100644 --- a/src/main/perl/lib/POSIX.pm +++ b/src/main/perl/lib/POSIX.pm @@ -35,6 +35,9 @@ use constant O_NONBLOCK => 04000; # 2048 use constant WNOHANG => 1; use constant WUNTRACED => 2; +# Floating-point constants +use constant HUGE_VAL => 9**9**9; + # Custom import to support legacy foo_h form (without colon) # This rewrites locale_h to :locale_h, errno_h to :errno_h, etc. sub import { @@ -56,6 +59,7 @@ our @EXPORT = qw( SEEK_CUR SEEK_END SEEK_SET F_OK R_OK W_OK X_OK LC_ALL LC_COLLATE LC_CTYPE LC_MESSAGES LC_MONETARY LC_NUMERIC LC_TIME + HUGE_VAL localeconv mktime setlocale strftime ); our @EXPORT_OK = qw( @@ -76,6 +80,7 @@ our @EXPORT_OK = qw( # Math functions abs acos asin atan atan2 ceil cos cosh exp fabs floor fmod frexp ldexp log log10 modf pow sin sinh sqrt tan tanh + HUGE_VAL # String functions memchr memcmp memcpy memmove memset strcat strchr strcmp strcoll diff --git a/src/main/perl/lib/Socket.pm b/src/main/perl/lib/Socket.pm index 646633bf1..9151c24c8 100644 --- a/src/main/perl/lib/Socket.pm +++ b/src/main/perl/lib/Socket.pm @@ -27,7 +27,7 @@ our @EXPORT = qw( pack_sockaddr_un unpack_sockaddr_un inet_aton inet_ntoa inet_pton inet_ntop getnameinfo getaddrinfo get_addr_info sockaddr_in sockaddr_un sockaddr_family - AF_INET AF_INET6 AF_UNIX + AF_INET AF_INET6 AF_UNIX AF_UNSPEC PF_INET PF_INET6 PF_UNIX PF_UNSPEC SOCK_STREAM SOCK_DGRAM SOCK_RAW SOL_SOCKET SO_REUSEADDR SO_KEEPALIVE SO_BROADCAST SO_LINGER SO_ERROR SO_ACCEPTCONN SO_TYPE SO_REUSEPORT @@ -91,6 +91,10 @@ essential socket constants and basic address manipulation functions. IPv4 address family (value: 2) +=item AF_UNSPEC, PF_UNSPEC + +Unspecified address family (value: 0) + =item AF_INET6, PF_INET6 IPv6 address family (value: 10) diff --git a/src/test/resources/unit/core_operator_source_position.t b/src/test/resources/unit/core_operator_source_position.t new file mode 100644 index 000000000..53a4a5148 --- /dev/null +++ b/src/test/resources/unit/core_operator_source_position.t @@ -0,0 +1,36 @@ +use strict; +use warnings; +use Test::More tests => 5; + +my $warning = ''; +{ + local $SIG{__WARN__} = sub { $warning = shift }; + eval qq{#line 1 "core-position-warning.pl"\nwarn;\n}; +} + +is($warning, + "Warning: something's wrong at core-position-warning.pl line 1.\n", + 'zero-argument warn uses the operator token source line'); + +$warning = ''; +{ + local $SIG{__WARN__} = sub { $warning = shift }; + eval qq{#line 1 "core-position-warning-eof.pl"\nwarn\n}; +} + +is($warning, + "Warning: something's wrong at core-position-warning-eof.pl line 1.\n", + 'EOF-terminated zero-argument warn uses the operator token source line'); + +my $ok = eval qq{#line 7 "core-position-die.pl"\ndie;\n1}; + +is($ok, undef, 'zero-argument die throws'); +is($@, + "Died at core-position-die.pl line 7.\n", + 'zero-argument die uses the operator token source line'); + +$@ = ''; +eval qq{#line 9 "core-position-die-eof.pl"\ndie\n}; +is($@, + "Died at core-position-die-eof.pl line 9.\n", + 'EOF-terminated zero-argument die uses the operator token source line'); diff --git a/src/test/resources/unit/data_section_package_data.t b/src/test/resources/unit/data_section_package_data.t new file mode 100644 index 000000000..e66f5e4a1 --- /dev/null +++ b/src/test/resources/unit/data_section_package_data.t @@ -0,0 +1,17 @@ +#!/usr/bin/env perl +use strict; +use warnings; +use Test::More tests => 1; + +package DataSectionPackage; + +package main; +is( + scalar , + "payload\n", + 'qualified package DATA handle is readline, not glob' +); + +package DataSectionPackage; +__DATA__ +payload diff --git a/src/test/resources/unit/dbi_selectrow_finish.t b/src/test/resources/unit/dbi_selectrow_finish.t index b6d984185..aff9f2938 100644 --- a/src/test/resources/unit/dbi_selectrow_finish.t +++ b/src/test/resources/unit/dbi_selectrow_finish.t @@ -29,4 +29,12 @@ my $hash_row = $dbh->selectrow_hashref($hash_sth); is($hash_row->{name}, 'one', 'selectrow_hashref returns the row'); ok(!$hash_sth->FETCH('Active'), 'selectrow_hashref finishes a successful statement'); +my $scalar_select = $dbh->selectrow_array('SELECT name FROM items WHERE id = 1'); +is($scalar_select, 'one', 'selectrow_array returns first column in scalar context'); + +my $fetch_sth = $dbh->prepare('SELECT name FROM items WHERE id = 1'); +$fetch_sth->execute; +my $scalar_fetch = $fetch_sth->fetchrow_array; +is($scalar_fetch, 'one', 'fetchrow_array returns first column in scalar context'); + done_testing; diff --git a/src/test/resources/unit/digest_md2_vectors.t b/src/test/resources/unit/digest_md2_vectors.t new file mode 100644 index 000000000..27e12a829 --- /dev/null +++ b/src/test/resources/unit/digest_md2_vectors.t @@ -0,0 +1,36 @@ +use strict; +use warnings; +use Test::More; + +eval { require Digest::MD2; Digest::MD2->import(qw(md2 md2_hex md2_base64)); 1 } + or plan skip_all => 'Digest::MD2 required'; + +my @cases = ( + [ '' => '8350e5a3e24c153df2275c9f80692773' ], + [ 'a' => '32ec01ec4a6dac72c0ab96fb34c0b5d1' ], + [ 'abc' => 'da853b0d3f88d99b30283a69e6ded6bb' ], + [ 'message digest' => 'ab4f496bfb2a530b219ff33031fe06b0' ], + [ 'abcdefghijklmnopqrstuvwxyz' => '4e8ddff3650292ab5a4108c3aa47940b' ], + [ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' => 'da33def2a42df13975352846c30338cd' ], + [ '12345678901234567890123456789012345678901234567890123456789012345678901234567890' => 'd5976f79d83d3a0dc9806c3c66f3efd8' ], +); + +plan tests => @cases * 4 + 3; + +for my $case (@cases) { + my ($message, $hex) = @$case; + my $raw = pack 'H*', $hex; + is(md2($message), $raw, "md2 raw for '$message'"); + is(md2_hex($message), $hex, "md2_hex for '$message'"); + is(Digest::MD2->new->add($message)->digest, $raw, "OO digest for '$message'"); + is(Digest::MD2->new->add($message)->hexdigest, $hex, "OO hexdigest for '$message'"); +} + +my $ctx = Digest::MD2->new; +$ctx->add('ab'); +my $clone = $ctx->clone; +$ctx->add('c'); +$clone->add('c'); +is($ctx->hexdigest, 'da853b0d3f88d99b30283a69e6ded6bb', 'clone leaves original state usable'); +is($clone->hexdigest, 'da853b0d3f88d99b30283a69e6ded6bb', 'clone copies digest state'); +is(Digest::MD2->new->add('abc')->b64digest, md2_base64('abc'), 'base64 OO and function agree'); diff --git a/src/test/resources/unit/feature_pragma_packages.t b/src/test/resources/unit/feature_pragma_packages.t new file mode 100644 index 000000000..055f2b33c --- /dev/null +++ b/src/test/resources/unit/feature_pragma_packages.t @@ -0,0 +1,21 @@ +#!/usr/bin/env perl +use strict; +use warnings; +use Test::More; + +BEGIN { + $INC{'indirect.pm'} = __FILE__; + $INC{'multidimensional.pm'} = __FILE__; + $INC{'bareword/filehandles.pm'} = __FILE__; +} + +for my $pragma (qw(indirect multidimensional bareword::filehandles)) { + ok($pragma->can('import'), "$pragma import exists without loading a .pm file"); + ok($pragma->can('unimport'), "$pragma unimport exists without loading a .pm file"); + ok(eval { $pragma->import; 1 }, "$pragma import can be called"); + ok(eval { $pragma->unimport; 1 }, "$pragma unimport can be called"); +} + +ok(eval { indirect->unimport(':fatal'); 1 }, 'indirect unimport accepts strictures-style arguments'); + +done_testing; diff --git a/src/test/resources/unit/format_ref_bless.t b/src/test/resources/unit/format_ref_bless.t new file mode 100644 index 000000000..a6a099c4e --- /dev/null +++ b/src/test/resources/unit/format_ref_bless.t @@ -0,0 +1,22 @@ +use strict; +use warnings; +use Test::More tests => 6; +use Scalar::Util qw(blessed reftype); + +format FH = +. + +my $missing = do { + no warnings 'once'; + *NO_SUCH_FORMAT{FORMAT}; +}; +ok(!defined($missing), 'missing format slot is undef'); + +my $fmt = *FH{FORMAT}; +is(ref($fmt), 'FORMAT', 'format slot reports FORMAT'); +is(reftype($fmt), 'FORMAT', 'Scalar::Util sees a FORMAT ref'); +is(blessed($fmt), undef, 'plain format ref is unblessed'); + +bless $fmt, 'FormatRef'; +is(ref($fmt), 'FormatRef', 'format ref can be blessed'); +is(reftype($fmt), 'FORMAT', 'blessed format keeps FORMAT reftype'); diff --git a/src/test/resources/unit/glob_hash_argument_slot.t b/src/test/resources/unit/glob_hash_argument_slot.t new file mode 100644 index 000000000..0b92f5093 --- /dev/null +++ b/src/test/resources/unit/glob_hash_argument_slot.t @@ -0,0 +1,36 @@ +#!/usr/bin/perl +use strict; +use warnings; +use Test::More; + +sub store_glob_hash_value { + my $fh = ref($_[0]) ? $_[0] : \($_[0]); + my $href = \%{*$fh}; + $href->{asn_buffer} = $_[1]; +} + +sub fetch_glob_hash_value { + my $fh = ref($_[0]) ? $_[0] : \($_[0]); + my $href = \%{*$fh}; + return $href->{asn_buffer}; +} + +store_glob_hash_value(*X, "buffered"); +is fetch_glob_hash_value(*X), "buffered", 'glob HASH slot survives typeglob argument copies'; +ok defined(*X{HASH}), 'HASH slot is visible on the canonical glob after writing through an argument copy'; + +my $g = *Y; +my $href = \%{*$g}; +$href->{k} = "shared"; +my $canonical = \%{*Y}; +is $canonical->{k}, "shared", 'hash deref of a copied named glob shares canonical HASH slot'; +ok defined(*$g{HASH}), 'copied glob reports HASH slot after hash deref vivifies it'; +ok defined(*Y{HASH}), 'canonical glob reports HASH slot after copied glob vivifies it'; + +my $devnull = $^O eq 'MSWin32' ? 'NUL' : '/dev/null'; +open(IN, '<', $devnull) or die "open $devnull: $!"; +store_glob_hash_value(*IN, "open-buffer"); +is fetch_glob_hash_value(*IN), "open-buffer", 'open filehandle glob preserves HASH slot through argument copies'; +close(IN); + +done_testing; diff --git a/src/test/resources/unit/hash_literal_anonymous_ref_cleanup.t b/src/test/resources/unit/hash_literal_anonymous_ref_cleanup.t new file mode 100644 index 000000000..7e31f33be --- /dev/null +++ b/src/test/resources/unit/hash_literal_anonymous_ref_cleanup.t @@ -0,0 +1,44 @@ +use strict; +use warnings; +use Test::More; + +package HLA::Client; + +sub new { + bless {}, shift; +} + +sub drop_messages { + my $self = shift; + if (my $msgs = delete $self->{msgs}) { + for my $msg (values %$msgs) { + $msg->{error} = 1; + } + } + return 1; +} + +sub DESTROY { + $main::destroyed++; +} + +package HLA::Message; + +sub new { + my ($class, $parent) = @_; + bless { parent => $parent }, $class; +} + +package main; + +$main::destroyed = 0; + +{ + my $client = HLA::Client->new; + $client->{msgs}->{1} = HLA::Message->new($client); + $client->drop_messages; +} + +is($main::destroyed, 1, 'hash literal objects clean up parent refs after deleted message table dies'); + +done_testing; diff --git a/src/test/resources/unit/indirect_object_block_list.t b/src/test/resources/unit/indirect_object_block_list.t new file mode 100644 index 000000000..3050b6d88 --- /dev/null +++ b/src/test/resources/unit/indirect_object_block_list.t @@ -0,0 +1,24 @@ +use strict; +use warnings; +use Test::More tests => 2; + +package IndirectBlock::Receiver; + +sub collect { + return join '|', @_; +} + +package main; + +is( + collect { 'IndirectBlock::Receiver' => 1, 2 }, + 'IndirectBlock::Receiver|1|2', + 'indirect-object block list passes first item as invocant', +); + +eval { missing_method { 'IndirectBlock::Receiver' => 1, 2 } }; +like( + $@, + qr/Can't locate object method "missing_method" via package "IndirectBlock::Receiver"/, + 'missing indirect-object method reports invocant package', +); diff --git a/src/test/resources/unit/io_slot_blessed.t b/src/test/resources/unit/io_slot_blessed.t new file mode 100644 index 000000000..7f6e6400f --- /dev/null +++ b/src/test/resources/unit/io_slot_blessed.t @@ -0,0 +1,10 @@ +use strict; +use warnings; +use Test::More tests => 3; +use Scalar::Util qw(blessed reftype); + +my $io = *STDOUT{IO}; + +is(reftype($io), 'IO', 'STDOUT IO slot has IO reftype'); +ok(blessed($io), 'STDOUT IO slot is blessed'); +ok(ref($io), 'STDOUT IO slot has a ref class'); diff --git a/src/test/resources/unit/makemaker_meta_merge_prereqs.t b/src/test/resources/unit/makemaker_meta_merge_prereqs.t index 9f2f6ae21..189630bf5 100644 --- a/src/test/resources/unit/makemaker_meta_merge_prereqs.t +++ b/src/test/resources/unit/makemaker_meta_merge_prereqs.t @@ -19,6 +19,8 @@ WriteMakefile( NAME => 'Foo::MetaMerge', VERSION => '0.001', PREREQ_PM => { 'List::Util' => 0 }, + BUILD_REQUIRES => { 'Test::More' => 0 }, + CONFIGURE_REQUIRES => { 'ExtUtils::MakeMaker' => 0 }, META_MERGE => { prereqs => { runtime => { @@ -50,6 +52,21 @@ like( qr/List::Util|PREREQ_PM/, 'Makefile records prerequisite metadata', ); +like( + $makefile, + qr/(?:_REQUIRES|PREREQ_PM)\s*=>\s*{\s*[^{]*List::Util\s*=>\s*q\[/, + 'Makefile prereq comments expose runtime requirements in MakeMaker shape', +); +like( + $makefile, + qr/(?:_REQUIRES|PREREQ_PM)\s*=>\s*{\s*[^{]*Test::More\s*=>\s*q\[/, + 'Makefile prereq comments expose build requirements in MakeMaker shape', +); +like( + $makefile, + qr/(?:_REQUIRES|PREREQ_PM)\s*=>\s*{\s*[^{]*ExtUtils::MakeMaker\s*=>\s*q\[/, + 'Makefile prereq comments expose configure requirements in MakeMaker shape', +); open my $ym, '<', 'MYMETA.yml' or die "open generated MYMETA.yml: $!"; my $mymeta = do { local $/; <$ym> }; diff --git a/src/test/resources/unit/mime_base64.t b/src/test/resources/unit/mime_base64.t index 1cafed3ca..f595ab4dc 100644 --- a/src/test/resources/unit/mime_base64.t +++ b/src/test/resources/unit/mime_base64.t @@ -156,6 +156,20 @@ subtest 'Edge cases' => sub { is(decode_base64('SGVs=bG8='), 'Hel', 'Padding in middle stops decoding'); }; +subtest 'Decoded data is byte string' => sub { + plan tests => 3; + + require Encode; + + my $encoded = 'ZG46Y249SMOkZ2FyLG91PXBlb3BsZSxvPW15b3JnLmNvbQ=='; + my $decoded = decode_base64($encoded); + + ok(!Encode::is_utf8($decoded), 'decoded base64 does not have UTF-8 flag'); + is(unpack('H*', $decoded), '646e3a636e3d48c3a46761722c6f753d70656f706c652c6f3d6d796f72672e636f6d', + 'decoded bytes preserve UTF-8 octets'); + is(encode_base64($decoded, ''), $encoded, 'decoded bytes re-encode without double UTF-8 encoding'); +}; + # Test undef handling subtest 'Undef and special cases' => sub { plan tests => 4; @@ -185,4 +199,3 @@ subtest 'Undef and special cases' => sub { }; done_testing(); - diff --git a/src/test/resources/unit/non_begin_phaser_pragma_scope.t b/src/test/resources/unit/non_begin_phaser_pragma_scope.t new file mode 100644 index 000000000..0bd5943ab --- /dev/null +++ b/src/test/resources/unit/non_begin_phaser_pragma_scope.t @@ -0,0 +1,27 @@ +#!/usr/bin/env perl +use strict; +use warnings; +use Test::More tests => 3; + +use integer; + +END { + my $x = 1; +} + +sub integer_division_after_end { + return 5 / 2; +} + +sub local_no_integer { + no integer; + return 5 / 2; +} + +sub integer_division_after_local_no_integer { + return 5 / 2; +} + +is(integer_division_after_end(), 2, 'END block does not clear file-scoped use integer'); +is(local_no_integer(), 2.5, 'no integer remains local to its sub body'); +is(integer_division_after_local_no_integer(), 2, 'sub-local no integer does not leak to later subs'); diff --git a/src/test/resources/unit/posix_huge_val.t b/src/test/resources/unit/posix_huge_val.t new file mode 100644 index 000000000..b6afeca37 --- /dev/null +++ b/src/test/resources/unit/posix_huge_val.t @@ -0,0 +1,16 @@ +#!/usr/bin/env perl +use strict; +use warnings; +use Test::More tests => 4; + +use POSIX qw(HUGE_VAL); + +ok(defined &HUGE_VAL, 'HUGE_VAL can be imported explicitly'); +ok(HUGE_VAL() > 1e308, 'imported HUGE_VAL is numeric infinity'); +ok(POSIX::HUGE_VAL() > 1e308, 'POSIX::HUGE_VAL is callable fully qualified'); + +{ + package POSIXHugeValDefault; + POSIX->import; + Test::More::ok(defined &HUGE_VAL, 'HUGE_VAL is exported by default'); +} diff --git a/src/test/resources/unit/range_operand_context.t b/src/test/resources/unit/range_operand_context.t new file mode 100644 index 000000000..d2aef33cd --- /dev/null +++ b/src/test/resources/unit/range_operand_context.t @@ -0,0 +1,25 @@ +use strict; +use warnings; +use Test::More tests => 6; + +our @contexts; + +sub probe { + push @contexts, defined(wantarray) ? (wantarray ? 'list' : 'scalar') : 'void'; + return shift; +} + +my @range = 1 .. probe(2); +is_deeply(\@contexts, ['scalar'], 'right range endpoint is scalar context'); +is_deeply(\@range, [1, 2], 'range still expands in list context'); + +@contexts = (); +@range = probe(2) .. 3; +is_deeply(\@contexts, ['scalar'], 'left range endpoint is scalar context'); +is_deeply(\@range, [2, 3], 'left endpoint value is used'); + +@contexts = (); +my $count = 0; +$count++ for 1 .. probe(2); +is_deeply(\@contexts, ['scalar'], 'foreach range endpoint is scalar context'); +is($count, 2, 'foreach iterates over generated range'); diff --git a/src/test/resources/unit/socket_options.t b/src/test/resources/unit/socket_options.t index 8a8443f52..9fe89d665 100644 --- a/src/test/resources/unit/socket_options.t +++ b/src/test/resources/unit/socket_options.t @@ -7,12 +7,13 @@ use IO::Handle; use IO::Poll qw(POLLERR POLLIN); use Test::More; use Socket qw( - AF_INET AF_UNIX PF_UNSPEC SOCK_DGRAM SOCK_STREAM - SOL_SOCKET SO_ACCEPTCONN SO_REUSEADDR SO_TYPE + AF_INET AF_UNSPEC AF_UNIX PF_UNSPEC SOCK_DGRAM SOCK_STREAM + SOL_SOCKET SO_ACCEPTCONN SO_REUSEADDR SO_TYPE MSG_PEEK INADDR_LOOPBACK IN6ADDR_ANY IN6ADDR_LOOPBACK sockaddr_in unpack_sockaddr_in ); +is AF_UNSPEC, PF_UNSPEC, 'AF_UNSPEC exports the unspecified protocol family value'; is length(IN6ADDR_ANY), 16, 'IN6ADDR_ANY is a 16-byte packed address'; is IN6ADDR_LOOPBACK, ("\0" x 15) . "\1", 'IN6ADDR_LOOPBACK is packed ::1'; @@ -61,6 +62,25 @@ SKIP: { ok $! == EAGAIN || $! == EWOULDBLOCK, 'nonblocking dgram recv reports EAGAIN'; } +SKIP: { + socketpair(my $left, my $right, AF_INET, SOCK_DGRAM, PF_UNSPEC) + or skip "inet dgram socketpair unavailable: $!", 3; + + send($right, "peeked", 0); + my $buf = ""; + recv($left, $buf, 1024, MSG_PEEK); + is $buf, "peeked", 'MSG_PEEK reads datagram payload'; + + $buf = ""; + recv($left, $buf, 1024, 0); + is $buf, "peeked", 'datagram remains available after MSG_PEEK'; + + $left->blocking(0); + $! = 0; + my $empty = recv($left, $buf, 1024, 0); + ok !defined($empty), 'datagram is consumed by non-peek recv'; +} + SKIP: { socketpair(my $left, my $right, AF_INET, SOCK_DGRAM, PF_UNSPEC) or skip "inet dgram socketpair unavailable: $!", 4; diff --git a/src/test/resources/unit/source_filter_caller_line.t b/src/test/resources/unit/source_filter_caller_line.t new file mode 100644 index 000000000..5f7232365 --- /dev/null +++ b/src/test/resources/unit/source_filter_caller_line.t @@ -0,0 +1,42 @@ +use strict; +use warnings; +use Test::More tests => 1; + +{ + package CallerLineFilter; + use Filter::Util::Call; + + sub import { + my $done = 0; + filter_add(sub { + return 0 if $done; + my ($data, $end) = ('', ''); + while (my $status = filter_read()) { + return $status if $status < 0; + if (/^__(?:END|DATA)__\r?$/) { + $end = $_; + last; + } + $data .= $_; + $_ = ''; + } + $_ = "use strict;use warnings;$data$end"; + $done = 1; + }); + } +} +BEGIN { $INC{'CallerLineFilter.pm'} = __FILE__ } + +use CallerLineFilter; + +sub caller_line { + my @caller = caller(1); + return $caller[2]; +} + +sub report_line { + return caller_line(); +} + +my $got = report_line(); +is($got, 41, 'source-filtered caller line numbers keep the use-statement newline'); diff --git a/src/test/resources/unit/source_filter_eval_require_defaultvar.t b/src/test/resources/unit/source_filter_eval_require_defaultvar.t new file mode 100644 index 000000000..c39dd790d --- /dev/null +++ b/src/test/resources/unit/source_filter_eval_require_defaultvar.t @@ -0,0 +1,53 @@ +use strict; +use warnings; +use Test::More tests => 2; +use File::Spec; +use File::Temp qw(tempdir); + +{ + package EvalRequireDefaultVarFilter; + use Filter::Util::Call; + + sub import { + my $done = 0; + filter_add(sub { + my $status = filter_read(); + return $status unless $status > 0; + $_ = "sub loaded { 1 }\n$_" unless $done++; + return $status; + }); + } +} +$INC{'EvalRequireDefaultVarFilter.pm'} = __FILE__; + +my $tmp = tempdir(CLEANUP => 1); +my $pm = File::Spec->catfile($tmp, 'EvalRequireDefaultVarTarget.pm'); +open my $fh, '>', $pm or die "open $pm: $!"; +print {$fh} <<'EOPM'; +package EvalRequireDefaultVarTarget; +use EvalRequireDefaultVarFilter; +1; +EOPM +close $fh; + +local @INC = ($tmp, @INC); + +my @seen; +my @items = ('EvalRequireDefaultVarTarget'); +my $count = grep { + push @seen, "before=$_"; + eval "require $_"; + die $@ if $@; + push @seen, "after=$_"; + $_->can('loaded'); +} @items; + +is($count, 1, 'eval-string require loads source-filtered module inside grep'); +is_deeply( + \@seen, + [ + 'before=EvalRequireDefaultVarTarget', + 'after=EvalRequireDefaultVarTarget', + ], + 'source filtering preserves grep default variable alias', +); diff --git a/src/test/resources/unit/source_filter_use_newline.t b/src/test/resources/unit/source_filter_use_newline.t new file mode 100644 index 000000000..b8f597790 --- /dev/null +++ b/src/test/resources/unit/source_filter_use_newline.t @@ -0,0 +1,41 @@ +#!/usr/bin/env perl +use strict; +use warnings; +use Test::More tests => 1; +use File::Spec; +use File::Temp qw(tempdir); + +{ + package NewlineFilter; + use Filter::Util::Call; + our $first_chunk; + + sub import { + filter_add(sub { + my $status = filter_read(); + $first_chunk = $_ if !defined $first_chunk; + return $status; + }); + } +} +$INC{'NewlineFilter.pm'} = __FILE__; + +my $tmp = tempdir(CLEANUP => 1); +my $pm = File::Spec->catfile($tmp, 'NewlineFilterTarget.pm'); +open my $fh, '>', $pm or die "open $pm: $!"; +print {$fh} <<'EOPM'; +package NewlineFilterTarget; +use NewlineFilter; +# first filtered line +1; +EOPM +close $fh; + +local @INC = ($tmp, @INC); +require NewlineFilterTarget; + +like( + $NewlineFilter::first_chunk, + qr/\A# first filtered line\n/, + 'source filter installed by use starts after the use statement newline' +); diff --git a/src/test/resources/unit/tied_hash_autoviv_refloop_cleanup.t b/src/test/resources/unit/tied_hash_autoviv_refloop_cleanup.t new file mode 100644 index 000000000..0a15439f9 --- /dev/null +++ b/src/test/resources/unit/tied_hash_autoviv_refloop_cleanup.t @@ -0,0 +1,71 @@ +use strict; +use warnings; +use Test::More tests => 6; +use Tie::Hash; + +our $destroyed = 0; + +{ + package THR::Client; + use Tie::Hash; + our @ISA = qw(Tie::StdHash); + + sub new { bless {}, shift } + + sub outer { + my $self = shift; + return $self if tied(%$self); + my %outer; + tie %outer, ref($self), $self; + ++$self->{refcnt}; + bless \%outer, ref($self); + } + + sub inner { tied(%{$_[0]}) || $_[0] } + sub TIEHASH { $_[1] } + + sub drop_conn { + my ($self, $err) = @_; + if (my $msgs = delete $self->{msgs}) { + $_->{error} = $err for values %$msgs; + } + } + + sub DESTROY { + my $self = shift; + ++$main::destroyed unless tied(%$self); + my $inner = tied(%$self) or return; + $inner->drop_conn(7) unless --$inner->{refcnt}; + } + + package THR::Message; + + sub new { + my ($class, $parent) = @_; + bless { parent => $parent->inner, id => 1 }, $class; + } + + sub code { shift->{error} || 0 } +} + +{ + my $client = THR::Client->new->outer; + $client->{msgs}->{1} = THR::Message->new($client); + ok(exists $client->inner->{msgs}, 'tied outer autovivifies into inner object'); +} +ok($destroyed, 'inner object destroyed after internal loop is dropped'); + +my ($ref, $msg); +$destroyed = 0; +{ + my $client = THR::Client->new->outer; + $msg = THR::Message->new($client); + $client->{msgs}->{1} = $msg; + $ref = $client->inner->outer; + ok($ref, 'extra outer reference created'); +} +ok(!$destroyed, 'extra outer reference keeps client alive'); +$ref = undef; +is($msg->code, 7, 'dropping last outer reference marks outstanding message'); +undef $msg; +ok($destroyed, 'inner object destroyed after held message is released'); diff --git a/src/test/resources/unit/tied_hash_destroy_release.t b/src/test/resources/unit/tied_hash_destroy_release.t new file mode 100644 index 000000000..4ca4d064d --- /dev/null +++ b/src/test/resources/unit/tied_hash_destroy_release.t @@ -0,0 +1,44 @@ +use strict; +use warnings; +use Test::More; + +package THD::LDAPLike; + +sub new { + bless {}, shift; +} + +sub outer { + my $self = shift; + my %outer; + tie %outer, ref($self), $self; + ++$self->{refcnt}; + bless \%outer, ref($self); +} + +sub inner { + tied(%{$_[0]}) || $_[0]; +} + +sub TIEHASH { + $_[1]; +} + +sub DESTROY { + my $self = shift; + ++$main::destroyed unless tied(%$self); + + my $inner = tied(%$self) or return; + --$inner->{refcnt}; +} + +package main; + +{ + my $outer = THD::LDAPLike->new->outer; + ok(tied(%$outer), 'outer object is backed by a tied hash'); +} + +is($main::destroyed, 1, 'destroying a tied outer hash releases the inner object'); + +done_testing; diff --git a/src/test/resources/unit/tied_stderr_warn.t b/src/test/resources/unit/tied_stderr_warn.t new file mode 100644 index 000000000..a8a8acdff --- /dev/null +++ b/src/test/resources/unit/tied_stderr_warn.t @@ -0,0 +1,28 @@ +use strict; +use warnings; +use Test::More tests => 1; + +package WarnTie::Handle; + +sub TIEHANDLE { + my $class = shift; + return bless \$_[0], $class; +} + +sub PRINT { + my $self = shift; + $$self .= join '', @_; +} + +package main; + +my $err = ''; +tie *STDERR, 'WarnTie::Handle', $err; +warn "warning\n"; +print STDERR "printed\n"; +{ + no warnings 'untie'; + untie *STDERR; +} + +is($err, "warning\nprinted\n", 'warn writes through tied STDERR');