From 36f7233297afa748b0334093df9cfd270a72a456 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 2 Mar 2026 01:47:56 +0200 Subject: [PATCH] Narrow integer literals to byte/short/char when expected i.e setByte(100) no longer throws "Expected byte, provided int" --- .../script/interpreter/ScriptDocument.java | 35 ++++- .../expression/ExpressionTypeResolver.java | 17 ++- .../script/interpreter/type/TypeChecker.java | 130 ++++++++++++++++++ 3 files changed, 177 insertions(+), 5 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptDocument.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptDocument.java index 4318facae..8a9a6c44d 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptDocument.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptDocument.java @@ -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 diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTypeResolver.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTypeResolver.java index 17557668b..eee81cd2c 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTypeResolver.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTypeResolver.java @@ -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); } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeChecker.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeChecker.java index c92f384a3..cd4a63261 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeChecker.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeChecker.java @@ -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). + * + *

Supported literal formats:

+ * + * + *

Range checks:

+ * + * + * @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",