Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
fecf0da
Fix MYMETA.yml format and create jcpan DateTime fix plan
fglock Mar 20, 2026
468715e
Add Class::Struct and File::stat via import system; document JVM Veri…
fglock Mar 20, 2026
a0563e0
Update JVM VerifyError analysis with simpler reproducer and root cause
fglock Mar 20, 2026
ab6e163
Update design doc: JVM VerifyError fix completed
fglock Mar 20, 2026
071549d
Add missing S_* mode constants to Fcntl.pm
fglock Mar 20, 2026
6ca4fd1
Update design doc: File::stat.pm now loads successfully
fglock Mar 20, 2026
a7184ef
Fix require bareword handling with CORE::GLOBAL::require override
fglock Mar 20, 2026
1d01e62
Update JCPAN_DATETIME_FIXES.md with completed fixes
fglock Mar 20, 2026
0ef2aae
Fix Exporter version check in import arguments
fglock Mar 21, 2026
b3bd5ba
Fix MakeMaker $(INST_LIB) variable expansion
fglock Mar 21, 2026
81b130e
Update design doc: jcpan DateTime installation complete
fglock Mar 21, 2026
d0ef79c
Add File::ShareDir::Install support to MakeMaker
fglock Mar 21, 2026
276e3cf
Fix IPC::Open3 redirection directive handling
fglock Mar 21, 2026
1c568e6
Fix prototype parsing: allow trailing comma before semicolon
fglock Mar 21, 2026
0ce7956
Fix version comparison with undef values
fglock Mar 21, 2026
31ff8de
Fix file test operators for JAR directory entries
fglock Mar 21, 2026
1c53ed5
Fix stat/lstat to set attributes after updateLastStat
fglock Mar 21, 2026
c4e439b
Fix numeric warnings with runtime ThreadLocal for proper scoping
fglock Mar 21, 2026
8e11f97
Set STDERR to autoflush by default to match Perl behavior
fglock Mar 21, 2026
972fb68
Add design doc for numeric warnings implementation
fglock Mar 21, 2026
e0cd018
Revert experimental numeric warnings changes
fglock Mar 21, 2026
1608f19
Simplify numeric warnings design: check flag on cache miss only
fglock Mar 21, 2026
5716f9f
Document numeric warnings implementation options
fglock Mar 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
188 changes: 188 additions & 0 deletions dev/design/NUMERIC_WARNINGS_IMPLEMENTATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
# Numeric Warnings Implementation Plan

## Problem Statement

Perl's `use warnings "numeric"` should emit warnings like `Argument "abc" isn't numeric` when non-numeric strings are used in numeric context. Currently:

1. `use warnings` and `no warnings` are compile-time pragmas
2. Numification happens at runtime via `RuntimeScalar.getDouble()` → `NumberParser.parseNumber()`
3. Runtime code doesn't know the compile-time warning state

### Current Behavior (broken)
```perl
use warnings;
my $x = 0 + "abc"; # Should warn, but doesn't (or warns incorrectly)
{
no warnings "numeric";
my $y = 0 + "def"; # Should NOT warn
}
my $z = 0 + "ghi"; # Should warn
```

## Core Design Decision: Warn on Cache Miss

Key insight: `NumberParser` has a numification cache. Most strings are only parsed once.
We only need to check the warning flag on **cache misses**.

### Flow
```
Cache hit path (fast, common):
getDouble() → parseNumber() → cache hit → return
[no warning check needed]

Cache miss path (rare):
getDouble() → parseNumber() → parse string →
if (isNonNumeric && warningsEnabled()) warn → cache result → return
[one flag check only on cache miss]
```

### Behavior Difference from Perl

- **Perl**: Warns every time a non-numeric string is used
- **Our approach**: Warns only on cache miss (first use of that string)

This is acceptable. We can later add a warning flag to the cache entry if exact Perl behavior is needed.

## Open Question: How to Track Warning State at Runtime

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.

### Option A: Perl Global Variable with `local`

Use a Perl global variable `$warnings::_numeric_enabled` that:
- Is set to 1 by `use warnings "numeric"`
- Is set to 0 by `no warnings "numeric"` using `local` for automatic scope restore

```java
public static boolean isNumericWarningsEnabled() {
return getGlobalVariable("warnings::_numeric_enabled").getBoolean();
}
```

**Pros:**
- Automatic block scoping via existing `local`/DynamicVariableManager
- Handles `goto` correctly (DynamicVariableManager already handles this)
- Simple to implement

**Cons:**
- Hash lookup on every cache miss (slower than ThreadLocal)

### Option B: ThreadLocal with try/finally

```java
private static final ThreadLocal<Boolean> numericWarningsEnabled =
ThreadLocal.withInitial(() -> false);

// Compiler generates for "no warnings" blocks:
boolean _saved = Warnings.isNumericWarningsEnabled();
Warnings.setNumericWarningsEnabled(false);
try {
// block code
} finally {
Warnings.setNumericWarningsEnabled(_saved);
}
```

**Pros:**
- Fast ThreadLocal read
- Proper block scoping

**Cons:**
- **Conflicts with `goto`** - JVM bytecode issues when goto jumps out of try/finally
- More complex compiler changes

### Option C: Per-Class Static Field + ThreadLocal on Entry

Each generated class (subroutine) has a compile-time constant:
```java
public class Sub_foo {
static final boolean NUMERIC_WARNINGS = true; // set at compile time
}
```

On subroutine entry, set a ThreadLocal:
```java
// Generated at subroutine entry
Warnings.setNumericWarnings(NUMERIC_WARNINGS);
```

**Pros:**
- Fast ThreadLocal read on cache miss
- No try/finally (no goto issues)
- Warning state is per-subroutine (matches Perl's lexical scoping)

**Cons:**
- One ThreadLocal write per subroutine call
- Nested calls overwrite - need to verify this matches Perl semantics
- Doesn't handle block-level `no warnings` within a subroutine

### Option D: Simple Global Flag (No Block Scoping)

Just use a simple flag without block-level scoping:
- `use warnings` → enable globally
- `no warnings` → disable globally

**Pros:**
- Simplest implementation
- No goto issues
- Correct for 99% of real code (most use file-level `use warnings`)

**Cons:**
- Block-level `no warnings "numeric"` won't restore on block exit
- Less correct than Perl

## NumberParser Changes (Common to All Options)

Regardless of which option is chosen for tracking state, the parseNumber() changes are the same:

```java
// In parseNumber(), after parsing determines the string is non-numeric:
// (isNonNumeric is already computed during parsing - no extra work)
if (isNonNumeric && Warnings.isNumericWarningsEnabled()) {
WarnDie.warn(new RuntimeScalar("Argument \"" + str + "\" isn't numeric"),
RuntimeScalarCache.scalarEmptyString);
}
```

Note: `isNonNumeric` is already determined during parsing (e.g., "abc" → 0).
The only new check is `isNumericWarningsEnabled()`.

## Files to Modify

1. `Warnings.java` - add `isNumericWarningsEnabled()` method (implementation depends on option chosen)
2. `NumberParser.java` - check flag on cache miss, emit warning
3. Possibly compiler changes depending on option chosen

## Testing

```perl
use warnings;
my $warned = 0;
local $SIG{__WARN__} = sub { $warned++ };

my $x = 0 + "abc";
ok($warned == 1, "warns on non-numeric");

$warned = 0;
my $y = 0 + "123";
ok($warned == 0, "no warning for numeric string");

$warned = 0;
{
no warnings "numeric";
my $z = 0 + "def";
}
ok($warned == 0, "no warning in no-warnings block");

# After block, warnings should be restored (if block scoping implemented)
$warned = 0;
my $w = 0 + "ghi";
ok($warned == 1, "warning restored after block");
```

## Current Status

- [x] Design documented
- [ ] Decision on runtime state tracking option (A, B, C, or D)
- [ ] Implementation
- [ ] Testing with op/numify.t
2 changes: 1 addition & 1 deletion src/main/java/org/perlonjava/core/Configuration.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public final class Configuration {
* Automatically populated by Gradle/Maven during build.
* DO NOT EDIT MANUALLY - this value is replaced at build time.
*/
public static final String gitCommitId = "3f00c2f24";
public static final String gitCommitId = "c4e439b01";

/**
* Git commit date of the build (ISO format: YYYY-MM-DD).
Expand Down
16 changes: 14 additions & 2 deletions src/main/java/org/perlonjava/frontend/parser/PrototypeArgs.java
Original file line number Diff line number Diff line change
Expand Up @@ -165,9 +165,21 @@ static ListNode consumeArgsWithPrototype(Parser parser, String prototype, boolea

// Check for too many arguments without parentheses only if prototype expects 2+ args
if (!hasParentheses && countPrototypeArgs(prototype) >= 2) {
// If we see a comma after parsing all required args, there are too many
// If we see a comma after parsing all required args, check if it's a trailing comma
if (isComma(TokenUtils.peek(parser))) {
throwTooManyArgumentsError(parser);
// Consume the comma and check what follows
int saveIndex = parser.tokenIndex;
consumeCommas(parser);
LexerToken nextToken = TokenUtils.peek(parser);
// If followed by a statement terminator, it's a trailing comma (allowed)
// Otherwise, it's too many arguments
if (!Parser.isExpressionTerminator(nextToken) &&
nextToken.type != LexerTokenType.EOF &&
!nextToken.text.equals(")")) {
throwTooManyArgumentsError(parser);
}
// Restore position - the comma will be handled by the caller
parser.tokenIndex = saveIndex;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ public class ScopedSymbolTable {

// Stack to manage warning categories for each scope
public final Stack<BitSet> warningFlagsStack = new Stack<>();
// Stack to track explicitly disabled warning categories (for proper $^W interaction)
public final Stack<BitSet> warningDisabledStack = new Stack<>();
// Stack to manage feature categories for each scope
public final Stack<Integer> featureFlagsStack = new Stack<>();
// Stack to manage strict options for each scope
Expand Down Expand Up @@ -65,6 +67,8 @@ public ScopedSymbolTable() {
}
}
warningFlagsStack.push((BitSet) defaultWarnings.clone());
// Initialize the disabled warnings stack (empty by default)
warningDisabledStack.push(new BitSet());
// Initialize the feature categories stack with an empty map for the global scope
featureFlagsStack.push(0);
// Initialize the strict options stack with 0 for the global scope
Expand Down Expand Up @@ -135,6 +139,8 @@ public int enterScope() {
inSubroutineBodyStack.push(inSubroutineBodyStack.peek());
// Push a copy of the current warning categories map onto the stack
warningFlagsStack.push((BitSet) warningFlagsStack.peek().clone());
// Push a copy of the current disabled warnings map onto the stack
warningDisabledStack.push((BitSet) warningDisabledStack.peek().clone());
// Push a copy of the current feature categories map onto the stack
featureFlagsStack.push(featureFlagsStack.peek());
// Push a copy of the current strict options onto the stack
Expand All @@ -159,6 +165,7 @@ public void exitScope(int scopeIndex) {
subroutineStack.pop();
inSubroutineBodyStack.pop();
warningFlagsStack.pop();
warningDisabledStack.pop();
featureFlagsStack.pop();
strictOptionsStack.pop();
}
Expand Down Expand Up @@ -528,6 +535,10 @@ public ScopedSymbolTable snapShot() {
st.warningFlagsStack.pop(); // Remove the initial value pushed by enterScope
st.warningFlagsStack.push((BitSet) this.warningFlagsStack.peek().clone());

// Clone disabled warnings flags
st.warningDisabledStack.pop(); // Remove the initial value pushed by enterScope
st.warningDisabledStack.push((BitSet) this.warningDisabledStack.peek().clone());

// Clone feature flags
st.featureFlagsStack.pop(); // Remove the initial value pushed by enterScope
st.featureFlagsStack.push(this.featureFlagsStack.peek());
Expand Down Expand Up @@ -631,13 +642,17 @@ public void enableWarningCategory(String category) {
Integer bitPosition = warningBitPositions.get(category);
if (bitPosition != null) {
warningFlagsStack.peek().set(bitPosition);
// Clear the disabled bit when enabling
warningDisabledStack.peek().clear(bitPosition);
}
}

public void disableWarningCategory(String category) {
Integer bitPosition = warningBitPositions.get(category);
if (bitPosition != null) {
warningFlagsStack.peek().clear(bitPosition);
// Mark as explicitly disabled (for proper $^W interaction)
warningDisabledStack.peek().set(bitPosition);
}
}

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

/**
* Checks if a warning category was explicitly disabled via 'no warnings'.
* This is used to determine if $^W should be overridden.
*/
public boolean isWarningCategoryDisabled(String category) {
Integer bitPosition = warningBitPositions.get(category);
return bitPosition != null && warningDisabledStack.peek().get(bitPosition);
}

// Methods for managing features using bit positions
public void enableFeatureCategory(String feature) {
if (isNoOpFeature(feature)) {
Expand Down Expand Up @@ -705,6 +729,10 @@ public void copyFlagsFrom(ScopedSymbolTable source) {
this.warningFlagsStack.pop();
this.warningFlagsStack.push((BitSet) source.warningFlagsStack.peek().clone());

// Copy disabled warnings flags
this.warningDisabledStack.pop();
this.warningDisabledStack.push((BitSet) source.warningDisabledStack.peek().clone());

// Copy feature flags
this.featureFlagsStack.pop();
this.featureFlagsStack.push(source.featureFlagsStack.peek());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,6 @@ public static RuntimeScalar greaterThan(RuntimeScalar arg1, RuntimeScalar arg2)
return getScalarBoolean((int) arg1.value > (int) arg2.value);
}

// Check for uninitialized values
checkUninitialized(arg1, arg2, "gt (>)");

// Prepare overload context and check if object is eligible for overloading
int blessId = blessedId(arg1);
int blessId2 = blessedId(arg2);
Expand All @@ -145,6 +142,9 @@ public static RuntimeScalar greaterThan(RuntimeScalar arg1, RuntimeScalar arg2)
}
}

// Check for uninitialized values (only when using numeric comparison fallback)
checkUninitialized(arg1, arg2, "gt (>)");

// Convert strings to numbers if necessary
arg1 = arg1.getNumber();
arg2 = arg2.getNumber();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,10 @@ static void updateLastStat(RuntimeScalar arg, boolean ok, int errno, boolean was
lastStatErrno = errno;
lastStatWasLstat = wasLstat;
Stat.lastNativeStatFields = null;
if (!ok) {
lastBasicAttr = null;
lastPosixAttr = null;
}
// Always reset BasicFileAttributes - they should only be set by statForFileTest
// for real filesystem paths. JAR resources don't have BasicFileAttributes.
lastBasicAttr = null;
lastPosixAttr = null;
}

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

lastBasicAttr = basicAttr;
lastPosixAttr = posixAttr;
getGlobalVariable("main::!").set(0);
updateLastStat(arg, true, 0, lstat);
// Set attributes after updateLastStat (which resets them to null)
lastBasicAttr = basicAttr;
lastPosixAttr = posixAttr;
Stat.lastNativeStatFields = Stat.nativeStat(path.toString(), !lstat);
return true;
} catch (NoSuchFileException e) {
Expand Down Expand Up @@ -322,6 +323,17 @@ public static RuntimeScalar fileTest(String operator, RuntimeScalar fileHandle)
}
// JAR resource path (e.g., "jar:PERL5LIB/DBI.pm")
if (Jar.exists(filename)) {
// Check if it's a directory entry (not a file)
if (Jar.isResourceDirectory(filename)) {
updateLastStat(fileHandle, true, 0);
return switch (operator) {
case "-d", "-e", "-r", "-x" -> scalarTrue; // It's a readable, executable directory
case "-f", "-l", "-w", "-z" -> scalarFalse; // Not a file, link, writable, or empty
case "-s" -> RuntimeScalarCache.scalarZero; // Size 0
default -> scalarUndef;
};
}
// It's a regular file
updateLastStat(fileHandle, true, 0);
return switch (operator) {
case "-e", "-f", "-r" -> scalarTrue; // Exists, is a file, is readable
Expand Down
Loading
Loading