Skip to content

Commit c4e439b

Browse files
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>
1 parent 1c53ed5 commit c4e439b

5 files changed

Lines changed: 173 additions & 4 deletions

File tree

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 = "80856361b";
36+
public static final String gitCommitId = "b00c94f79";
3737

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

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

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@
55
import org.perlonjava.frontend.lexer.LexerToken;
66
import org.perlonjava.frontend.lexer.LexerTokenType;
77
import org.perlonjava.runtime.operators.WarnDie;
8+
import org.perlonjava.runtime.perlmodule.Warnings;
89
import org.perlonjava.runtime.runtimetypes.RuntimeScalar;
910
import org.perlonjava.runtime.runtimetypes.RuntimeScalarCache;
1011
import org.perlonjava.runtime.runtimetypes.RuntimeScalarType;
1112

13+
import static org.perlonjava.runtime.runtimetypes.GlobalVariable.getGlobalVariable;
14+
1215
import java.util.LinkedHashMap;
1316
import java.util.Map;
1417
import java.util.function.Function;
@@ -34,6 +37,30 @@ protected boolean removeEldestEntry(Map.Entry<String, RuntimeScalar> eldest) {
3437
private static final Pattern WINDOWS_INF_PATTERN = Pattern.compile("1\\.?#INF.*");
3538
private static final Pattern WINDOWS_NAN_PATTERN = Pattern.compile("\\+?1\\.?#(QNAN|NANQ|NAN|IND|SNAN).*"
3639
);
40+
41+
/**
42+
* Check if numeric warnings are enabled.
43+
* Returns true if numeric warnings should be shown based on:
44+
* 1. Runtime disabled state (from 'no warnings "numeric"' blocks) - takes precedence
45+
* 2. Lexical enabled state (from 'use warnings')
46+
* 3. $^W global flag (fallback)
47+
*/
48+
private static boolean numericWarningsEnabled() {
49+
// First check runtime disabled state - this handles 'no warnings "numeric"' at runtime
50+
if (Warnings.isNumericWarningDisabled()) {
51+
return false;
52+
}
53+
54+
// Check if lexically enabled
55+
if (Warnings.warningManager.isWarningEnabled("numeric")
56+
|| Warnings.warningManager.isWarningEnabled("all")) {
57+
return true;
58+
}
59+
60+
// Fall back to $^W (stored as main:: + character for 'W' - 'A' + 1)
61+
return getGlobalVariable("main::" + Character.toString('W' - 'A' + 1)).getBoolean();
62+
}
63+
3764
private static final NumberFormat BINARY_FORMAT = new NumberFormat(
3865
2,
3966
str -> str.matches("[01_]*"),
@@ -555,7 +582,18 @@ else if (WINDOWS_NAN_PATTERN.matcher(remaining).matches()) {
555582
numberEnd = exponentPos;
556583
}
557584

558-
if (numberEnd == start) return getScalarInt(0);
585+
if (numberEnd == start) {
586+
// String doesn't start with a digit - warn about non-numeric
587+
if (numericWarningsEnabled()) {
588+
String warnStr = str.trim();
589+
if (warnStr.startsWith("-") || warnStr.startsWith("+")) {
590+
warnStr = warnStr.substring(1);
591+
}
592+
WarnDie.warn(new RuntimeScalar("Argument \"" + warnStr + "\" isn't numeric"),
593+
RuntimeScalarCache.scalarEmptyString);
594+
}
595+
return getScalarInt(0);
596+
}
559597

560598
try {
561599
String numberStr = str.substring(start, numberEnd);
@@ -566,6 +604,10 @@ else if (WINDOWS_NAN_PATTERN.matcher(remaining).matches()) {
566604
long value = Long.parseLong(numberStr);
567605
result = getScalarInt(isNegative ? -value : value);
568606
}
607+
// Check for trailing non-numeric characters
608+
if (numberEnd < end) {
609+
shouldWarn = true;
610+
}
569611
} catch (NumberFormatException e) {
570612
try {
571613
double value = Double.parseDouble(str.substring(start, numberEnd));
@@ -576,8 +618,8 @@ else if (WINDOWS_NAN_PATTERN.matcher(remaining).matches()) {
576618
}
577619
}
578620

579-
// Generate warning if needed
580-
if (shouldWarn) {
621+
// Generate warning if needed and numeric warnings are enabled
622+
if (shouldWarn && numericWarningsEnabled()) {
581623
String warnStr = str.trim();
582624
if (warnStr.startsWith("-") || warnStr.startsWith("+")) {
583625
warnStr = warnStr.substring(1);

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/perlmodule/Warnings.java

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,34 @@
22

33
import org.perlonjava.runtime.runtimetypes.*;
44

5+
import java.util.ArrayDeque;
6+
import java.util.BitSet;
7+
import java.util.Deque;
8+
59
/**
610
* The Warnings class provides functionalities similar to the Perl warnings module.
711
*/
812
public class Warnings extends PerlModuleBase {
913

1014
public static final WarningFlags warningManager = new WarningFlags();
15+
16+
/**
17+
* ThreadLocal stack of disabled warning categories for runtime lexical scoping.
18+
* Each entry is a BitSet where set bits indicate disabled categories.
19+
* This allows 'no warnings "numeric"' to properly suppress warnings even when $^W is set.
20+
*/
21+
private static final ThreadLocal<Deque<BitSet>> runtimeDisabledStack =
22+
ThreadLocal.withInitial(() -> {
23+
Deque<BitSet> stack = new ArrayDeque<>();
24+
stack.push(new BitSet()); // Initial empty disabled set
25+
return stack;
26+
});
27+
28+
/**
29+
* Bit position for the "numeric" warning category in the runtime disabled stack.
30+
*/
31+
public static final int WARN_NUMERIC = 0;
32+
public static final int WARN_ALL = 1;
1133

1234
/**
1335
* Constructor for Warnings.
@@ -44,6 +66,9 @@ public static RuntimeList useWarnings(RuntimeArray args, int ctx) {
4466
// If no arguments, enable all warnings (use warnings;)
4567
if (args.size() == 1) {
4668
warningManager.initializeEnabledWarnings();
69+
// Clear runtime disabled flags for common categories
70+
enableRuntimeWarning(WARN_NUMERIC);
71+
enableRuntimeWarning(WARN_ALL);
4772
return new RuntimeScalar().getList();
4873
}
4974

@@ -60,6 +85,12 @@ public static RuntimeList useWarnings(RuntimeArray args, int ctx) {
6085
throw new PerlCompilerException("Unknown warnings category '" + category + "'");
6186
}
6287
warningManager.enableWarning(category);
88+
// Also enable at runtime (clear disabled flag)
89+
if (category.equals("numeric")) {
90+
enableRuntimeWarning(WARN_NUMERIC);
91+
} else if (category.equals("all")) {
92+
enableRuntimeWarning(WARN_ALL);
93+
}
6394
}
6495
}
6596
return new RuntimeScalar().getList();
@@ -79,6 +110,12 @@ public static RuntimeList noWarnings(RuntimeArray args, int ctx) {
79110
throw new PerlCompilerException("Unknown warnings category '" + category + "'");
80111
}
81112
warningManager.disableWarning(category);
113+
// Also disable at runtime for proper lexical scoping
114+
if (category.equals("numeric")) {
115+
disableRuntimeWarning(WARN_NUMERIC);
116+
} else if (category.equals("all")) {
117+
disableRuntimeWarning(WARN_ALL);
118+
}
82119
}
83120
return new RuntimeScalar().getList();
84121
}
@@ -143,4 +180,55 @@ public static RuntimeList warnIf(RuntimeArray args, int ctx) {
143180
}
144181
return new RuntimeScalar().getList();
145182
}
183+
184+
// ================== Runtime Warning Scope Management ==================
185+
186+
/**
187+
* Enter a new runtime warning scope. Called at the start of blocks with 'no warnings'.
188+
*/
189+
public static void enterWarningScope() {
190+
Deque<BitSet> stack = runtimeDisabledStack.get();
191+
stack.push((BitSet) stack.peek().clone());
192+
}
193+
194+
/**
195+
* Exit the current runtime warning scope. Called at the end of blocks with 'no warnings'.
196+
*/
197+
public static void exitWarningScope() {
198+
Deque<BitSet> stack = runtimeDisabledStack.get();
199+
if (stack.size() > 1) {
200+
stack.pop();
201+
}
202+
}
203+
204+
/**
205+
* Disable a warning category at runtime.
206+
*/
207+
public static void disableRuntimeWarning(int category) {
208+
runtimeDisabledStack.get().peek().set(category);
209+
}
210+
211+
/**
212+
* Enable a warning category at runtime (clear the disabled bit).
213+
*/
214+
public static void enableRuntimeWarning(int category) {
215+
runtimeDisabledStack.get().peek().clear(category);
216+
}
217+
218+
/**
219+
* Check if a warning category is disabled at runtime.
220+
* This is used by NumberParser to check if numeric warnings should be suppressed.
221+
*/
222+
public static boolean isRuntimeWarningDisabled(int category) {
223+
return runtimeDisabledStack.get().peek().get(category);
224+
}
225+
226+
/**
227+
* Check if numeric warnings are disabled at runtime.
228+
* Convenience method for the common case.
229+
*/
230+
public static boolean isNumericWarningDisabled() {
231+
BitSet disabled = runtimeDisabledStack.get().peek();
232+
return disabled.get(WARN_NUMERIC) || disabled.get(WARN_ALL);
233+
}
146234
}

src/main/java/org/perlonjava/runtime/runtimetypes/WarningFlags.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,4 +142,15 @@ public void setWarningState(String category, boolean state) {
142142
public boolean isWarningEnabled(String category) {
143143
return getCurrentScope().isWarningCategoryEnabled(category);
144144
}
145+
146+
/**
147+
* Checks if a warning category was explicitly disabled via 'no warnings'.
148+
* This is used to determine if $^W should be overridden.
149+
*
150+
* @param category The name of the warning category to check.
151+
* @return True if the category was explicitly disabled, false otherwise.
152+
*/
153+
public boolean isWarningDisabled(String category) {
154+
return getCurrentScope().isWarningCategoryDisabled(category);
155+
}
145156
}

0 commit comments

Comments
 (0)