Skip to content

Commit 05c15ab

Browse files
Fix jcpan DateTime installation and related issues (#348)
* Fix MYMETA.yml format and create jcpan DateTime fix plan 1. ExtUtils/MakeMaker.pm: Generate meta-spec v2 format MYMETA.yml - Test dependencies now properly detected by CPAN.pm - Uses nested prereqs structure instead of flat v1.4 format 2. dev/design/JCPAN_DATETIME_FIXES.md: Comprehensive fix plan - Documents all errors from clean cache DateTime install - Prioritized implementation plan - Critical: File::stat.pm needed for DateTime::Locale Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * Add Class::Struct and File::stat via import system; document JVM VerifyError Phase 17 DateTime fixes: - Import Class::Struct.pm from perl5/lib (required by File::stat) - Import File::stat.pm from perl5/lib (required by File::ShareDir::Install) - Document JVM VerifyError with minimal reproducer File::stat triggers a JVM bytecode verification error due to a bug when compiling: no strict 'refs' + for loop + defined eval { &{symbolic_ref} } Minimal reproducer: no strict 'refs'; for (qw(X Y Z)) { defined eval { &{"Fcntl::S_IF$_"} } } Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * Update JVM VerifyError analysis with simpler reproducer and root cause The minimal reproducer doesn't require a for loop: no strict 'refs'; my $result = defined eval { &{"Fcntl::S_IFX"} }; Root cause: The block dispatcher stores ordinal in controlFlowActionSlot, but different code paths merge at a label with inconsistent types for that slot (integer vs TOP). Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * Update design doc: JVM VerifyError fix completed Document the root cause analysis and fix for Issue #11 (VerifyError when loading File::stat.pm. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> EOF ) * Add missing S_* mode constants to Fcntl.pm File::stat.pm requires S_IRUSR, S_IWUSR, S_IXUSR and other mode constants from Fcntl. These were listed in @EXPORT_OK but never actually defined. Added: - File type masks: S_IFMT, S_IFREG, S_IFDIR, S_IFLNK, etc. - Special mode bits: S_ISUID, S_ISGID, S_ISVTX - User permissions: S_IRUSR, S_IWUSR, S_IXUSR, S_IRWXU - Group permissions: S_IRGRP, S_IWGRP, S_IXGRP, S_IRWXG - Other permissions: S_IROTH, S_IWOTH, S_IXOTH, S_IRWXO - File type test functions: S_ISREG, S_ISDIR, S_ISLNK, etc. - Permission extraction: S_IMODE Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * Update design doc: File::stat.pm now loads successfully Both the JVM VerifyError and the missing Fcntl constants have been fixed. File::stat.pm now loads and works correctly. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * Fix require bareword handling with CORE::GLOBAL::require override When CORE::GLOBAL::require is overridden and a module uses 'require Bareword;' under strict subs, the bareword was incorrectly flagged as a strict subs violation. Root cause: When the parser detected a CORE::GLOBAL::require override, it rewrote the require call to a subroutine call, but the bareword argument (e.g., 'Exporter') was parsed as an expression instead of using require's special bareword-to-filename conversion. Fix: Added special handling in ParsePrimary.java for 'require' when CORE::GLOBAL::require is overridden: 1. Parse the argument using standard require handling (converts bareword to filename) 2. Build a subroutine call node with the &CORE::GLOBAL::require code ref Also added Exporter::require_version() method which delegates to UNIVERSAL::VERSION for historical compatibility with older Exporter usage. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * Update JCPAN_DATETIME_FIXES.md with completed fixes - Marked Exporter::require_version as FIXED - Marked CORE::GLOBAL::require bareword handling as FIXED - Updated progress tracking section Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * Fix Exporter version check in import arguments When calling Module->import('0.03', 'symbol'), Perl's Exporter treats arguments starting with a digit as version checks, not symbols to export. Added this logic to the Java Exporter: 1. If symbol starts with digit, call $pkg->VERSION($version) 2. If version was only argument, import from @export 3. If version + empty string ('use Foo 1.23, ""'), import nothing 4. Otherwise skip version and continue with other imports This fixes: 'Symbol 0.03 not allowed for export in package File::ShareDir::Install' Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * Fix MakeMaker $(INST_LIB) variable expansion When Makefile.PL scripts provide an explicit PM hash with Make-style variables like $(INST_LIB)/Module.pm, expand them to actual paths. Without this fix, modules would be installed to a literal '$(INST_LIB)' directory instead of the actual install base. This fixes Class::Inspector and similar modules using explicit PM hashes. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * Update design doc: jcpan DateTime installation complete All blocking issues have been fixed: - Exporter version check in import arguments - MakeMaker $(INST_LIB) variable expansion jcpan install DateTime now works successfully. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * Add File::ShareDir::Install support to MakeMaker When Makefile.PL uses File::ShareDir::Install to register share directories (via install_share()), our MakeMaker now processes @file::ShareDir::Install::DIRS and copies those files to the proper location under auto/share/dist/<DistName>/. This enables DateTime::Locale to work correctly, as it uses share files for locale data (1070 .pl files for different locales). Test: jcpan install DateTime::Locale now installs all locale data files. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * Fix IPC::Open3 redirection directive handling When open3() is called with redirection directives like '>&STDERR', they are read-only strings that cannot be modified. This fix: 1. In IPCOpen3.java: - Added isOutputRedirection() and isInputRedirection() to detect >&/&< directives - Added handleOutputRedirection() to pipe process output to named handles - Added isUsableHandle() to properly detect undef handles (not just reference-to-undef) - Added getStringValue() to properly dereference scalar references 2. In Open3.pm: - Check for redirection directives before trying to update caller's variables - Skip assignment to $_[N] when it's a redirection directive (read-only) This fixes: 'open3: Modification of a read-only value attempted' errors in tests like t/00-compile.t that use open3($stdin, '>&STDERR', $stderr, ...). Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * Fix prototype parsing: allow trailing comma before semicolon When calling a prototype function without parentheses, a trailing comma before the statement terminator was incorrectly treated as 'too many arguments'. In Perl, trailing commas are allowed: like $warning, qr/foo/, 'test',; # valid Perl The fix checks if the comma is followed by a statement terminator (;, EOF, or other expression terminators) and allows it in that case. This fixes t/conflicts.t in Dist::CheckConflicts which uses this pattern. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * Fix version comparison with undef values - Version.java: Treat empty/undef values as version 0 in VCMP, matching Perl 5 behavior where `version->new("1.0") <=> undef` returns 1 - CompareOperators.java: Move checkUninitialized() call to after the overload check in greaterThan(), so overloaded comparison operators don't produce spurious "uninitialized value" warnings This fixes warnings during CPAN::Meta::Requirements checks when modules don't have a $VERSION defined. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * Fix file test operators for JAR directory entries - Jar.java: Add isFile() and isResourceDirectory() methods to distinguish between files and directories in JAR resources. Directory entries have contentLength of 0. - FileTestOperator.java: Check for JAR directory entries and return correct results for -d and -f tests - FileTestOperator.java: Always reset lastBasicAttr in updateLastStat() to prevent stale attributes from being used when switching between real filesystem paths and JAR resources This fixes Module::Metadata version detection for modules in the JAR, which was returning undef because it could not distinguish directories from files in JAR paths. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * Fix stat/lstat to set attributes after updateLastStat The updateLastStat function now resets lastBasicAttr to null, so stat and lstat need to set the attributes AFTER calling updateLastStat. This fixes the regression in op/filetest.t where lstat followed by -e _ and -l _ would fail with 'The stat preceding -l _ wasn't an lstat'. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * Fix numeric warnings with runtime ThreadLocal for proper scoping Added ThreadLocal-based runtime warning state tracking: - Added runtimeDisabledStack in Warnings class to track disabled categories - 'no warnings "numeric"' sets runtime disabled flag (overrides $^W) - 'use warnings' clears runtime disabled flag - NumberParser checks runtime disabled state before warning This properly handles the interaction between $^W and 'no warnings': - $^W = 1 enables warnings - 'no warnings "numeric"' suppresses them even when $^W is set - Matches standard Perl behavior Tests: - infnan.t: 1071/1088 passing (uses $^W = 1, works correctly) - DateTime tests: No spurious warnings (Test::Builder uses 'no warnings') Note: Full lexical scoping would require generating code at block boundaries to push/pop warning scope. Current implementation uses runtime flag that persists until 'use warnings' is called. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * Set STDERR to autoflush by default to match Perl behavior In Perl, STDERR is unbuffered by default while STDOUT is line-buffered. This ensures warnings are displayed immediately rather than being interleaved with stdout output. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * Add design doc for numeric warnings implementation Documents thread-local flag approach that leverages numification cache to minimize performance overhead. Only cache misses incur thread-local access. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * Revert experimental numeric warnings changes Reverting to master state. The proper implementation is documented in dev/design/NUMERIC_WARNINGS_IMPLEMENTATION.md and will be implemented as a separate effort. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * Simplify numeric warnings design: check flag on cache miss only - Warning happens inside parseNumber() on cache miss - No operator duplication needed - No caller changes needed - One thread-local read per cache miss (rare after warmup) - Uses existing local mechanism for proper scoping Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * Document numeric warnings implementation options Core decision: warn on cache miss only (not every use) Options for runtime state tracking: A. Perl global variable with local (correct scoping, slower) B. ThreadLocal with try/finally (fast, but conflicts with goto) C. Per-class static field + ThreadLocal on entry (fast, per-subroutine) D. Simple global flag (simplest, no block scoping) Decision deferred - all options documented with trade-offs. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent 1abf60e commit 05c15ab

11 files changed

Lines changed: 336 additions & 21 deletions

File tree

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
# Numeric Warnings Implementation Plan
2+
3+
## Problem Statement
4+
5+
Perl's `use warnings "numeric"` should emit warnings like `Argument "abc" isn't numeric` when non-numeric strings are used in numeric context. Currently:
6+
7+
1. `use warnings` and `no warnings` are compile-time pragmas
8+
2. Numification happens at runtime via `RuntimeScalar.getDouble()``NumberParser.parseNumber()`
9+
3. Runtime code doesn't know the compile-time warning state
10+
11+
### Current Behavior (broken)
12+
```perl
13+
use warnings;
14+
my $x = 0 + "abc"; # Should warn, but doesn't (or warns incorrectly)
15+
{
16+
no warnings "numeric";
17+
my $y = 0 + "def"; # Should NOT warn
18+
}
19+
my $z = 0 + "ghi"; # Should warn
20+
```
21+
22+
## Core Design Decision: Warn on Cache Miss
23+
24+
Key insight: `NumberParser` has a numification cache. Most strings are only parsed once.
25+
We only need to check the warning flag on **cache misses**.
26+
27+
### Flow
28+
```
29+
Cache hit path (fast, common):
30+
getDouble() → parseNumber() → cache hit → return
31+
[no warning check needed]
32+
33+
Cache miss path (rare):
34+
getDouble() → parseNumber() → parse string →
35+
if (isNonNumeric && warningsEnabled()) warn → cache result → return
36+
[one flag check only on cache miss]
37+
```
38+
39+
### Behavior Difference from Perl
40+
41+
- **Perl**: Warns every time a non-numeric string is used
42+
- **Our approach**: Warns only on cache miss (first use of that string)
43+
44+
This is acceptable. We can later add a warning flag to the cache entry if exact Perl behavior is needed.
45+
46+
## Open Question: How to Track Warning State at Runtime
47+
48+
The compile-time symbol table knows if warnings are enabled, but `parseNumber()` runs at runtime and needs to check this. Several options exist with different trade-offs.
49+
50+
### Option A: Perl Global Variable with `local`
51+
52+
Use a Perl global variable `$warnings::_numeric_enabled` that:
53+
- Is set to 1 by `use warnings "numeric"`
54+
- Is set to 0 by `no warnings "numeric"` using `local` for automatic scope restore
55+
56+
```java
57+
public static boolean isNumericWarningsEnabled() {
58+
return getGlobalVariable("warnings::_numeric_enabled").getBoolean();
59+
}
60+
```
61+
62+
**Pros:**
63+
- Automatic block scoping via existing `local`/DynamicVariableManager
64+
- Handles `goto` correctly (DynamicVariableManager already handles this)
65+
- Simple to implement
66+
67+
**Cons:**
68+
- Hash lookup on every cache miss (slower than ThreadLocal)
69+
70+
### Option B: ThreadLocal with try/finally
71+
72+
```java
73+
private static final ThreadLocal<Boolean> numericWarningsEnabled =
74+
ThreadLocal.withInitial(() -> false);
75+
76+
// Compiler generates for "no warnings" blocks:
77+
boolean _saved = Warnings.isNumericWarningsEnabled();
78+
Warnings.setNumericWarningsEnabled(false);
79+
try {
80+
// block code
81+
} finally {
82+
Warnings.setNumericWarningsEnabled(_saved);
83+
}
84+
```
85+
86+
**Pros:**
87+
- Fast ThreadLocal read
88+
- Proper block scoping
89+
90+
**Cons:**
91+
- **Conflicts with `goto`** - JVM bytecode issues when goto jumps out of try/finally
92+
- More complex compiler changes
93+
94+
### Option C: Per-Class Static Field + ThreadLocal on Entry
95+
96+
Each generated class (subroutine) has a compile-time constant:
97+
```java
98+
public class Sub_foo {
99+
static final boolean NUMERIC_WARNINGS = true; // set at compile time
100+
}
101+
```
102+
103+
On subroutine entry, set a ThreadLocal:
104+
```java
105+
// Generated at subroutine entry
106+
Warnings.setNumericWarnings(NUMERIC_WARNINGS);
107+
```
108+
109+
**Pros:**
110+
- Fast ThreadLocal read on cache miss
111+
- No try/finally (no goto issues)
112+
- Warning state is per-subroutine (matches Perl's lexical scoping)
113+
114+
**Cons:**
115+
- One ThreadLocal write per subroutine call
116+
- Nested calls overwrite - need to verify this matches Perl semantics
117+
- Doesn't handle block-level `no warnings` within a subroutine
118+
119+
### Option D: Simple Global Flag (No Block Scoping)
120+
121+
Just use a simple flag without block-level scoping:
122+
- `use warnings` → enable globally
123+
- `no warnings` → disable globally
124+
125+
**Pros:**
126+
- Simplest implementation
127+
- No goto issues
128+
- Correct for 99% of real code (most use file-level `use warnings`)
129+
130+
**Cons:**
131+
- Block-level `no warnings "numeric"` won't restore on block exit
132+
- Less correct than Perl
133+
134+
## NumberParser Changes (Common to All Options)
135+
136+
Regardless of which option is chosen for tracking state, the parseNumber() changes are the same:
137+
138+
```java
139+
// In parseNumber(), after parsing determines the string is non-numeric:
140+
// (isNonNumeric is already computed during parsing - no extra work)
141+
if (isNonNumeric && Warnings.isNumericWarningsEnabled()) {
142+
WarnDie.warn(new RuntimeScalar("Argument \"" + str + "\" isn't numeric"),
143+
RuntimeScalarCache.scalarEmptyString);
144+
}
145+
```
146+
147+
Note: `isNonNumeric` is already determined during parsing (e.g., "abc" → 0).
148+
The only new check is `isNumericWarningsEnabled()`.
149+
150+
## Files to Modify
151+
152+
1. `Warnings.java` - add `isNumericWarningsEnabled()` method (implementation depends on option chosen)
153+
2. `NumberParser.java` - check flag on cache miss, emit warning
154+
3. Possibly compiler changes depending on option chosen
155+
156+
## Testing
157+
158+
```perl
159+
use warnings;
160+
my $warned = 0;
161+
local $SIG{__WARN__} = sub { $warned++ };
162+
163+
my $x = 0 + "abc";
164+
ok($warned == 1, "warns on non-numeric");
165+
166+
$warned = 0;
167+
my $y = 0 + "123";
168+
ok($warned == 0, "no warning for numeric string");
169+
170+
$warned = 0;
171+
{
172+
no warnings "numeric";
173+
my $z = 0 + "def";
174+
}
175+
ok($warned == 0, "no warning in no-warnings block");
176+
177+
# After block, warnings should be restored (if block scoping implemented)
178+
$warned = 0;
179+
my $w = 0 + "ghi";
180+
ok($warned == 1, "warning restored after block");
181+
```
182+
183+
## Current Status
184+
185+
- [x] Design documented
186+
- [ ] Decision on runtime state tracking option (A, B, C, or D)
187+
- [ ] Implementation
188+
- [ ] Testing with op/numify.t

src/main/java/org/perlonjava/core/Configuration.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public final class Configuration {
3333
* Automatically populated by Gradle/Maven during build.
3434
* DO NOT EDIT MANUALLY - this value is replaced at build time.
3535
*/
36-
public static final String gitCommitId = "3f00c2f24";
36+
public static final String gitCommitId = "c4e439b01";
3737

3838
/**
3939
* Git commit date of the build (ISO format: YYYY-MM-DD).

src/main/java/org/perlonjava/frontend/parser/PrototypeArgs.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,9 +165,21 @@ static ListNode consumeArgsWithPrototype(Parser parser, String prototype, boolea
165165

166166
// Check for too many arguments without parentheses only if prototype expects 2+ args
167167
if (!hasParentheses && countPrototypeArgs(prototype) >= 2) {
168-
// If we see a comma after parsing all required args, there are too many
168+
// If we see a comma after parsing all required args, check if it's a trailing comma
169169
if (isComma(TokenUtils.peek(parser))) {
170-
throwTooManyArgumentsError(parser);
170+
// Consume the comma and check what follows
171+
int saveIndex = parser.tokenIndex;
172+
consumeCommas(parser);
173+
LexerToken nextToken = TokenUtils.peek(parser);
174+
// If followed by a statement terminator, it's a trailing comma (allowed)
175+
// Otherwise, it's too many arguments
176+
if (!Parser.isExpressionTerminator(nextToken) &&
177+
nextToken.type != LexerTokenType.EOF &&
178+
!nextToken.text.equals(")")) {
179+
throwTooManyArgumentsError(parser);
180+
}
181+
// Restore position - the comma will be handled by the caller
182+
parser.tokenIndex = saveIndex;
171183
}
172184
}
173185
}

src/main/java/org/perlonjava/frontend/semantic/ScopedSymbolTable.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ public class ScopedSymbolTable {
3636

3737
// Stack to manage warning categories for each scope
3838
public final Stack<BitSet> warningFlagsStack = new Stack<>();
39+
// Stack to track explicitly disabled warning categories (for proper $^W interaction)
40+
public final Stack<BitSet> warningDisabledStack = new Stack<>();
3941
// Stack to manage feature categories for each scope
4042
public final Stack<Integer> featureFlagsStack = new Stack<>();
4143
// Stack to manage strict options for each scope
@@ -65,6 +67,8 @@ public ScopedSymbolTable() {
6567
}
6668
}
6769
warningFlagsStack.push((BitSet) defaultWarnings.clone());
70+
// Initialize the disabled warnings stack (empty by default)
71+
warningDisabledStack.push(new BitSet());
6872
// Initialize the feature categories stack with an empty map for the global scope
6973
featureFlagsStack.push(0);
7074
// Initialize the strict options stack with 0 for the global scope
@@ -135,6 +139,8 @@ public int enterScope() {
135139
inSubroutineBodyStack.push(inSubroutineBodyStack.peek());
136140
// Push a copy of the current warning categories map onto the stack
137141
warningFlagsStack.push((BitSet) warningFlagsStack.peek().clone());
142+
// Push a copy of the current disabled warnings map onto the stack
143+
warningDisabledStack.push((BitSet) warningDisabledStack.peek().clone());
138144
// Push a copy of the current feature categories map onto the stack
139145
featureFlagsStack.push(featureFlagsStack.peek());
140146
// Push a copy of the current strict options onto the stack
@@ -159,6 +165,7 @@ public void exitScope(int scopeIndex) {
159165
subroutineStack.pop();
160166
inSubroutineBodyStack.pop();
161167
warningFlagsStack.pop();
168+
warningDisabledStack.pop();
162169
featureFlagsStack.pop();
163170
strictOptionsStack.pop();
164171
}
@@ -528,6 +535,10 @@ public ScopedSymbolTable snapShot() {
528535
st.warningFlagsStack.pop(); // Remove the initial value pushed by enterScope
529536
st.warningFlagsStack.push((BitSet) this.warningFlagsStack.peek().clone());
530537

538+
// Clone disabled warnings flags
539+
st.warningDisabledStack.pop(); // Remove the initial value pushed by enterScope
540+
st.warningDisabledStack.push((BitSet) this.warningDisabledStack.peek().clone());
541+
531542
// Clone feature flags
532543
st.featureFlagsStack.pop(); // Remove the initial value pushed by enterScope
533544
st.featureFlagsStack.push(this.featureFlagsStack.peek());
@@ -631,13 +642,17 @@ public void enableWarningCategory(String category) {
631642
Integer bitPosition = warningBitPositions.get(category);
632643
if (bitPosition != null) {
633644
warningFlagsStack.peek().set(bitPosition);
645+
// Clear the disabled bit when enabling
646+
warningDisabledStack.peek().clear(bitPosition);
634647
}
635648
}
636649

637650
public void disableWarningCategory(String category) {
638651
Integer bitPosition = warningBitPositions.get(category);
639652
if (bitPosition != null) {
640653
warningFlagsStack.peek().clear(bitPosition);
654+
// Mark as explicitly disabled (for proper $^W interaction)
655+
warningDisabledStack.peek().set(bitPosition);
641656
}
642657
}
643658

@@ -646,6 +661,15 @@ public boolean isWarningCategoryEnabled(String category) {
646661
return bitPosition != null && warningFlagsStack.peek().get(bitPosition);
647662
}
648663

664+
/**
665+
* Checks if a warning category was explicitly disabled via 'no warnings'.
666+
* This is used to determine if $^W should be overridden.
667+
*/
668+
public boolean isWarningCategoryDisabled(String category) {
669+
Integer bitPosition = warningBitPositions.get(category);
670+
return bitPosition != null && warningDisabledStack.peek().get(bitPosition);
671+
}
672+
649673
// Methods for managing features using bit positions
650674
public void enableFeatureCategory(String feature) {
651675
if (isNoOpFeature(feature)) {
@@ -705,6 +729,10 @@ public void copyFlagsFrom(ScopedSymbolTable source) {
705729
this.warningFlagsStack.pop();
706730
this.warningFlagsStack.push((BitSet) source.warningFlagsStack.peek().clone());
707731

732+
// Copy disabled warnings flags
733+
this.warningDisabledStack.pop();
734+
this.warningDisabledStack.push((BitSet) source.warningDisabledStack.peek().clone());
735+
708736
// Copy feature flags
709737
this.featureFlagsStack.pop();
710738
this.featureFlagsStack.push(source.featureFlagsStack.peek());

src/main/java/org/perlonjava/runtime/operators/CompareOperators.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,9 +128,6 @@ public static RuntimeScalar greaterThan(RuntimeScalar arg1, RuntimeScalar arg2)
128128
return getScalarBoolean((int) arg1.value > (int) arg2.value);
129129
}
130130

131-
// Check for uninitialized values
132-
checkUninitialized(arg1, arg2, "gt (>)");
133-
134131
// Prepare overload context and check if object is eligible for overloading
135132
int blessId = blessedId(arg1);
136133
int blessId2 = blessedId(arg2);
@@ -145,6 +142,9 @@ public static RuntimeScalar greaterThan(RuntimeScalar arg1, RuntimeScalar arg2)
145142
}
146143
}
147144

145+
// Check for uninitialized values (only when using numeric comparison fallback)
146+
checkUninitialized(arg1, arg2, "gt (>)");
147+
148148
// Convert strings to numbers if necessary
149149
arg1 = arg1.getNumber();
150150
arg2 = arg2.getNumber();

src/main/java/org/perlonjava/runtime/operators/FileTestOperator.java

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,10 @@ static void updateLastStat(RuntimeScalar arg, boolean ok, int errno, boolean was
5959
lastStatErrno = errno;
6060
lastStatWasLstat = wasLstat;
6161
Stat.lastNativeStatFields = null;
62-
if (!ok) {
63-
lastBasicAttr = null;
64-
lastPosixAttr = null;
65-
}
62+
// Always reset BasicFileAttributes - they should only be set by statForFileTest
63+
// for real filesystem paths. JAR resources don't have BasicFileAttributes.
64+
lastBasicAttr = null;
65+
lastPosixAttr = null;
6666
}
6767

6868
static void updateLastStat(RuntimeScalar arg, boolean ok, int errno) {
@@ -128,10 +128,11 @@ private static boolean statForFileTest(RuntimeScalar arg, Path path, boolean lst
128128
} catch (UnsupportedOperationException | IOException ignored) {
129129
}
130130

131-
lastBasicAttr = basicAttr;
132-
lastPosixAttr = posixAttr;
133131
getGlobalVariable("main::!").set(0);
134132
updateLastStat(arg, true, 0, lstat);
133+
// Set attributes after updateLastStat (which resets them to null)
134+
lastBasicAttr = basicAttr;
135+
lastPosixAttr = posixAttr;
135136
Stat.lastNativeStatFields = Stat.nativeStat(path.toString(), !lstat);
136137
return true;
137138
} catch (NoSuchFileException e) {
@@ -322,6 +323,17 @@ public static RuntimeScalar fileTest(String operator, RuntimeScalar fileHandle)
322323
}
323324
// JAR resource path (e.g., "jar:PERL5LIB/DBI.pm")
324325
if (Jar.exists(filename)) {
326+
// Check if it's a directory entry (not a file)
327+
if (Jar.isResourceDirectory(filename)) {
328+
updateLastStat(fileHandle, true, 0);
329+
return switch (operator) {
330+
case "-d", "-e", "-r", "-x" -> scalarTrue; // It's a readable, executable directory
331+
case "-f", "-l", "-w", "-z" -> scalarFalse; // Not a file, link, writable, or empty
332+
case "-s" -> RuntimeScalarCache.scalarZero; // Size 0
333+
default -> scalarUndef;
334+
};
335+
}
336+
// It's a regular file
325337
updateLastStat(fileHandle, true, 0);
326338
return switch (operator) {
327339
case "-e", "-f", "-r" -> scalarTrue; // Exists, is a file, is readable

0 commit comments

Comments
 (0)