Narrow integer literals to byte/short/char when expected i.e setByte(100) no longer throws "Expected byte, provided int"

This commit is contained in:
bigguy345 committed 2026-03-02 01:47:56 +02:00
1 parent edb285c90b
commit 36f7233297
3 files changed
+177 -5

No files matched your search

@@ -4616,6 +4616,17 @@ public class ScriptDocument {
return significantDigits > maxDigits;
}
private TypeInfo narrowIntLiteral(String literalText) {
TypeInfo expectedType = ExpressionTypeResolver.CURRENT_EXPECTED_TYPE;
if (expectedType != null) {
TypeInfo narrowed = TypeChecker.narrowLiteralToExpectedType(literalText, expectedType);
if (narrowed != null) {
return narrowed;
}
}
return TypeInfo.fromPrimitive("int");
}
/**
* Comprehensive expression type resolver that handles:
* - Literals (strings, numbers, booleans, null)
@@ -4692,9 +4703,17 @@ public class ScriptDocument {
if (expr.matches("-?\\d+[lL]")) {
return TypeInfo.fromPrimitive("long");
}
// Hex integer literals: 0x7F, -0x80, 0X1A
if (expr.matches("[-+]?0[xX][0-9a-fA-F][0-9a-fA-F_]*")) {
return narrowIntLiteral(expr);
}
// Binary integer literals: 0b1010, -0b1100, 0B1111
if (expr.matches("[-+]?0[bB][01][01_]*")) {
return narrowIntLiteral(expr);
}
// Int: plain integers without suffix
if (expr.matches("-?\\d+")) {
return TypeInfo.fromPrimitive("int");
if (expr.matches("[-+]?\\d[\\d_]*")) {
return narrowIntLiteral(expr);
}
// "this" keyword
@@ -5459,8 +5478,16 @@ public class ScriptDocument {
if (expr.matches("-?\\d+[lL]")) {
return TypeInfo.fromPrimitive("long");
}
if (expr.matches("-?\\d+")) {
return TypeInfo.fromPrimitive("int");
// Hex integer literals: 0x7F, -0x80, 0X1A
if (expr.matches("[-+]?0[xX][0-9a-fA-F][0-9a-fA-F_]*")) {
return narrowIntLiteral(expr);
}
// Binary integer literals: 0b1010, -0b1100, 0B1111
if (expr.matches("[-+]?0[bB][01][01_]*")) {
return narrowIntLiteral(expr);
}
if (expr.matches("[-+]?\\d[\\d_]*")) {
return narrowIntLiteral(expr);
}
// "this" keyword
@@ -75,7 +75,14 @@ public class ExpressionTypeResolver {
private TypeInfo resolveNodeType(ExpressionNode node) {
if (node == null) return null;
if (node instanceof ExpressionNode.IntLiteralNode) return TypeInfo.fromPrimitive("int");
if (node instanceof ExpressionNode.IntLiteralNode) {
if (CURRENT_EXPECTED_TYPE != null) {
TypeInfo narrowed = TypeChecker.narrowLiteralToExpectedType(
((ExpressionNode.IntLiteralNode) node).getValue(), CURRENT_EXPECTED_TYPE);
if (narrowed != null) return narrowed;
}
return TypeInfo.fromPrimitive("int");
}
if (node instanceof ExpressionNode.LongLiteralNode) return TypeInfo.fromPrimitive("long");
if (node instanceof ExpressionNode.FloatLiteralNode) return TypeInfo.fromPrimitive("float");
if (node instanceof ExpressionNode.DoubleLiteralNode) return TypeInfo.fromPrimitive("double");
@@ -132,6 +139,14 @@ public class ExpressionTypeResolver {
if (node instanceof ExpressionNode.UnaryOpNode) {
ExpressionNode.UnaryOpNode un = (ExpressionNode.UnaryOpNode) node;
// Special case: negation of an IntLiteralNode can narrow to byte/short
if (un.getOperator() == OperatorType.UNARY_MINUS
&& un.getOperand() instanceof ExpressionNode.IntLiteralNode
&& CURRENT_EXPECTED_TYPE != null) {
String negatedText = "-" + ((ExpressionNode.IntLiteralNode) un.getOperand()).getValue();
TypeInfo narrowed = TypeChecker.narrowLiteralToExpectedType(negatedText, CURRENT_EXPECTED_TYPE);
if (narrowed != null) return narrowed;
}
TypeInfo operandType = resolveNodeType(un.getOperand());
return TypeRules.resolveUnaryOperatorType(un.getOperator(), operandType);
}
@@ -332,6 +332,136 @@ public final class TypeChecker {
return null;
}
/**
* Attempt to narrow an integer literal to byte, short, or char when the
* expected type requires it. The expression resolver normally infers integer
* literals as {@code int}; this helper allows callers to re-type a literal
* when {@code ExpressionTypeResolver.CURRENT_EXPECTED_TYPE} is set (e.g.
* method-argument second pass or assignment RHS).
*
* <p>Supported literal formats:</p>
* <ul>
* <li>Decimal: {@code 42}, {@code -128}, {@code +127}</li>
* <li>Hexadecimal: {@code 0xFF}, {@code 0XAB}</li>
* <li>Binary: {@code 0b1010}, {@code 0B1111_0000}</li>
* <li>Underscores as digit separators: {@code 1_000}</li>
* <li>Optional trailing {@code l}/{@code L} suffix (stripped before parsing)</li>
* </ul>
*
* <p>Range checks:</p>
* <ul>
* <li>byte: [-128 .. 127]</li>
* <li>short: [-32768 .. 32767]</li>
* <li>char: [0 .. 65535]</li>
* </ul>
*
* @param literalText the raw text of the integer literal (e.g. {@code "0xFF"})
* @param expectedType the target type at the call/assignment site
* @return a {@link TypeInfo} for the narrowed primitive type, or {@code null}
* if the expected type is not byte/short/char, the literal is not a
* valid integer, or the value falls outside the target range
*/
public static TypeInfo narrowLiteralToExpectedType(String literalText, TypeInfo expectedType) {
if (literalText == null || literalText.isEmpty() || expectedType == null) {
return null;
}
// Determine the narrowing target from the expected type
String expectedSimple = expectedType.getSimpleName();
if (expectedSimple == null) {
return null;
}
String unboxed = getUnboxedName(expectedSimple);
if (!"byte".equals(unboxed) && !"short".equals(unboxed) && !"char".equals(unboxed)) {
return null; // Only narrow to byte, short, or char
}
// Parse the literal value
Long value = parseIntegerLiteral(literalText);
if (value == null) {
return null;
}
// Range check for the target type
long v = value;
switch (unboxed) {
case "byte":
if (v < -128 || v > 127) return null;
break;
case "short":
if (v < -32768 || v > 32767) return null;
break;
case "char":
if (v < 0 || v > 65535) return null;
break;
default:
return null;
}
return TypeInfo.fromPrimitive(unboxed);
}
/**
* Parse an integer literal string into a {@code Long} value.
* Supports decimal, hexadecimal (0x/0X), and binary (0b/0B) formats,
* optional leading sign (+/-), underscores as digit separators,
* and an optional trailing l/L suffix.
*
* @param text the raw literal text
* @return the parsed value, or {@code null} on parse failure
*/
private static Long parseIntegerLiteral(String text) {
String s = text.trim();
if (s.isEmpty()) {
return null;
}
// Determine sign
boolean negative = false;
if (s.charAt(0) == '-') {
negative = true;
s = s.substring(1);
} else if (s.charAt(0) == '+') {
s = s.substring(1);
}
if (s.isEmpty()) {
return null;
}
// Strip optional trailing l/L suffix
if (s.charAt(s.length() - 1) == 'l' || s.charAt(s.length() - 1) == 'L') {
s = s.substring(0, s.length() - 1);
}
if (s.isEmpty()) {
return null;
}
// Remove underscores (Java digit separators)
s = s.replace("_", "");
if (s.isEmpty()) {
return null;
}
try {
long value;
if (s.length() > 2 && s.charAt(0) == '0' && (s.charAt(1) == 'x' || s.charAt(1) == 'X')) {
// Hexadecimal
value = Long.parseUnsignedLong(s.substring(2), 16);
} else if (s.length() > 2 && s.charAt(0) == '0' && (s.charAt(1) == 'b' || s.charAt(1) == 'B')) {
// Binary
value = Long.parseUnsignedLong(s.substring(2), 2);
} else {
// Decimal
value = Long.parseLong(s);
}
return negative ? -value : value;
} catch (NumberFormatException e) {
return null;
}
}
public static String[] getJavaKeywords() {
String[] keywords = {
"if", "else", "for", "while", "do", "switch", "case", "break", "continue",