From 941cc5e3fd81e2fe61ffe19762df0f967e86ffa5 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 28 Dec 2025 22:36:46 +0200 Subject: [PATCH] Expression resolving/type resolution framework with operator parsing --- .../script/interpreter/ScriptDocument.java | 352 ++++++++++++++++++ .../expression/ExpressionNode.java | 232 ++++++++++++ .../expression/ExpressionParser.java | 331 ++++++++++++++++ .../expression/ExpressionToken.java | 60 +++ .../expression/ExpressionTokenizer.java | 152 ++++++++ .../expression/ExpressionTypeResolver.java | 167 +++++++++ .../interpreter/expression/OperatorType.java | 147 ++++++++ .../interpreter/expression/TypeRules.java | 157 ++++++++ 8 files changed, 1598 insertions(+) create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionNode.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionParser.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionToken.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTokenizer.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTypeResolver.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/OperatorType.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/TypeRules.java 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 0367b159d..7554c1000 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 @@ -1,6 +1,8 @@ package noppes.npcs.client.gui.util.script.interpreter; import noppes.npcs.client.ClientProxy; +import noppes.npcs.client.gui.util.script.interpreter.expression.ExpressionNode; +import noppes.npcs.client.gui.util.script.interpreter.expression.ExpressionTypeResolver; import noppes.npcs.client.gui.util.script.interpreter.field.AssignmentInfo; import noppes.npcs.client.gui.util.script.interpreter.field.FieldAccessInfo; import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; @@ -2236,6 +2238,7 @@ public class ScriptDocument { * - Mixed chains (a.b().c.d()) * - Static access (Class.field, Class.method()) * - New expressions (new Type()) + * - All Java operators (arithmetic, logical, bitwise, etc.) * * This is THE method that should be used for all type resolution needs. */ @@ -2246,6 +2249,11 @@ public class ScriptDocument { return null; } + // Check if expression contains operators - if so, use the full expression resolver + if (containsOperators(expr)) { + return resolveExpressionWithOperators(expr, position); + } + // Invalid expressions starting with brackets if (expr.startsWith("[") || expr.startsWith("]")) { return null; // Invalid syntax @@ -2531,6 +2539,350 @@ public class ScriptDocument { } } + // ==================== OPERATOR EXPRESSION RESOLUTION ==================== + + /** + * Check if an expression contains operators that need advanced resolution. + * This is a quick heuristic check - it may have false positives for operators + * inside strings, but those are handled by the full parser. + */ + private boolean containsOperators(String expr) { + if (expr == null) return false; + + boolean inString = false; + boolean inChar = false; + + for (int i = 0; i < expr.length(); i++) { + char c = expr.charAt(i); + char next = (i + 1 < expr.length()) ? expr.charAt(i + 1) : 0; + + // Track string literals + if (c == '"' && !inChar) { + if (!inString) { + inString = true; + } else if (i > 0 && expr.charAt(i - 1) != '\\') { + inString = false; + } + continue; + } + + // Track char literals + if (c == '\'' && !inString) { + if (!inChar) { + inChar = true; + } else if (i > 0 && expr.charAt(i - 1) != '\\') { + inChar = false; + } + continue; + } + + // Skip content inside strings/chars + if (inString || inChar) continue; + + // Check for operators (excluding . which is used for member access) + switch (c) { + case '+': + // + is arithmetic unless it's unary at start or after operator + if (next == '+') return true; // ++ + if (next == '=') return true; // += + // Check if this is binary + by looking at previous non-whitespace + int prevIdx = i - 1; + while (prevIdx >= 0 && Character.isWhitespace(expr.charAt(prevIdx))) prevIdx--; + if (prevIdx >= 0) { + char prev = expr.charAt(prevIdx); + // If previous char is identifier char or ), this is binary + if (Character.isJavaIdentifierPart(prev) || prev == ')' || prev == ']' || Character.isDigit(prev)) { + return true; + } + } + break; + + case '-': + // - is arithmetic unless it's unary minus before a number + if (next == '-') return true; // -- + if (next == '=') return true; // -= + // Check if this is binary - by looking at previous non-whitespace + prevIdx = i - 1; + while (prevIdx >= 0 && Character.isWhitespace(expr.charAt(prevIdx))) prevIdx--; + if (prevIdx >= 0) { + char prev = expr.charAt(prevIdx); + // If previous char is identifier char or ), this is binary + if (Character.isJavaIdentifierPart(prev) || prev == ')' || prev == ']' || Character.isDigit(prev)) { + return true; + } + } + break; + + case '*': case '/': case '%': + return true; + + case '&': + if (next == '&' || next == '=') return true; + // Single & is bitwise AND + return true; + + case '|': + if (next == '|' || next == '=') return true; + // Single | is bitwise OR + return true; + + case '^': case '~': + return true; + + case '<': + // Could be < > <= >= << or generics + if (next == '<' || next == '=') return true; + // Check if this looks like relational (not generic type params) + // Generics typically follow a type name directly with no space + int nextNonSpace = i + 1; + while (nextNonSpace < expr.length() && Character.isWhitespace(expr.charAt(nextNonSpace))) nextNonSpace++; + if (nextNonSpace < expr.length() && !Character.isUpperCase(expr.charAt(nextNonSpace))) { + return true; + } + break; + + case '>': + if (next == '>' || next == '=') return true; + // Similar check for generics + break; + + case '!': + if (next == '=') return true; // != + // Standalone ! is logical NOT + return true; + + case '=': + if (next == '=') return true; // == + // Single = is assignment + return true; + + case '?': + // Ternary operator - but be careful of generics with ? + // If followed by :, it's definitely ternary + for (int j = i + 1; j < expr.length(); j++) { + if (expr.charAt(j) == ':') return true; + } + break; + } + } + + // Check for instanceof keyword + if (expr.contains(" instanceof ")) { + return true; + } + + return false; + } + + /** + * Resolve an expression that contains operators using the full expression parser. + * This handles all Java operators with proper precedence and type promotion rules. + */ + private TypeInfo resolveExpressionWithOperators(String expr, int position) { + // Create a context that bridges to ScriptDocument's existing resolution methods + ExpressionNode.TypeResolverContext context = createExpressionResolverContext(position); + + // Use the expression resolver to parse and resolve the type + ExpressionTypeResolver resolver = new ExpressionTypeResolver(context); + TypeInfo result = resolver.resolve(expr); + + // If the expression resolver couldn't resolve it, fall back to simple resolution + if (result == null || !result.isResolved()) { + // Try the simple path in case the operator detection was a false positive + return resolveSimpleExpression(expr, position); + } + + return result; + } + + /** + * Create a type resolver context that connects the expression resolver + * to ScriptDocument's existing type resolution infrastructure. + */ + private ExpressionNode.TypeResolverContext createExpressionResolverContext(int position) { + return new ExpressionNode.TypeResolverContext() { + @Override + public TypeInfo resolveIdentifier(String name) { + // Check for special keywords first + if ("this".equals(name)) { + return findEnclosingScriptType(position); + } + if ("true".equals(name) || "false".equals(name)) { + return TypeInfo.fromPrimitive("boolean"); + } + if ("null".equals(name)) { + return null; + } + + // Try to resolve as a variable + FieldInfo varInfo = resolveVariable(name, position); + if (varInfo != null) { + return varInfo.getTypeInfo(); + } + + // Try as a class name (for static access) + if (name.length() > 0 && Character.isUpperCase(name.charAt(0))) { + return resolveType(name); + } + + return null; + } + + @Override + public TypeInfo resolveMemberAccess(TypeInfo targetType, String memberName) { + if (targetType == null || !targetType.isResolved()) { + return null; + } + + if (targetType.hasField(memberName)) { + FieldInfo fieldInfo = targetType.getFieldInfo(memberName); + return (fieldInfo != null) ? fieldInfo.getTypeInfo() : null; + } + + return null; + } + + @Override + public TypeInfo resolveMethodCall(TypeInfo targetType, String methodName, TypeInfo[] argTypes) { + if (targetType == null || !targetType.isResolved()) { + // Try as a script-defined method + if (isScriptMethod(methodName)) { + MethodInfo scriptMethod = getScriptMethodInfo(methodName); + if (scriptMethod != null) { + return scriptMethod.getReturnType(); + } + } + return null; + } + + if (targetType.hasMethod(methodName)) { + MethodInfo methodInfo = targetType.getMethodInfo(methodName); + return (methodInfo != null) ? methodInfo.getReturnType() : null; + } + + return null; + } + + @Override + public TypeInfo resolveArrayAccess(TypeInfo arrayType) { + if (arrayType == null || !arrayType.isResolved()) { + return null; + } + + String typeName = arrayType.getFullName(); + if (typeName.endsWith("[]")) { + String elementTypeName = typeName.substring(0, typeName.length() - 2); + // Try to resolve the element type properly + return resolveType(elementTypeName); + } + + // For List or similar, try to extract the element type + // This is a simplified version - could be enhanced for full generic support + return null; + } + + @Override + public TypeInfo resolveTypeName(String typeName) { + return resolveType(typeName); + } + }; + } + + /** + * Resolve a simple expression without operators. + * This is the fallback path when operator detection was a false positive. + */ + private TypeInfo resolveSimpleExpression(String expr, int position) { + // String literals + if (expr.startsWith("\"") && expr.endsWith("\"")) { + return resolveType("String"); + } + + // Character literals + if (expr.startsWith("'") && expr.endsWith("'")) { + return TypeInfo.fromPrimitive("char"); + } + + // Boolean literals + if (expr.equals("true") || expr.equals("false")) { + return TypeInfo.fromPrimitive("boolean"); + } + + // Null literal + if (expr.equals("null")) { + return null; + } + + // Numeric literals + if (expr.matches("-?\\d*\\.?\\d+[fF]")) { + if (hasExcessivePrecision(expr, 7)) { + return TypeInfo.fromPrimitive("double"); + } + return TypeInfo.fromPrimitive("float"); + } + if (expr.matches("-?\\d*\\.\\d+[dD]?") || expr.matches("-?\\d+\\.[dD]?") || expr.matches("-?\\d+[dD]")) { + if (hasExcessivePrecision(expr, 15)) { + return null; + } + return TypeInfo.fromPrimitive("double"); + } + if (expr.matches("-?\\d+[lL]")) { + return TypeInfo.fromPrimitive("long"); + } + if (expr.matches("-?\\d+")) { + return TypeInfo.fromPrimitive("int"); + } + + // "this" keyword + if (expr.equals("this")) { + return findEnclosingScriptType(position); + } + + // "new Type()" expressions + if (expr.startsWith("new ")) { + Matcher newMatcher = NEW_TYPE_PATTERN.matcher(expr); + if (newMatcher.find()) { + return resolveType(newMatcher.group(1)); + } + } + + // Try as variable or field chain + List segments = parseExpressionChain(expr); + if (segments.isEmpty()) { + return null; + } + + ChainSegment first = segments.get(0); + TypeInfo currentType = null; + + if (first.name.equals("this")) { + currentType = findEnclosingScriptType(position); + } else if (Character.isUpperCase(first.name.charAt(0))) { + currentType = resolveType(first.name); + } else if (!first.isMethodCall) { + FieldInfo varInfo = resolveVariable(first.name, position); + if (varInfo != null) { + currentType = varInfo.getTypeInfo(); + } + } else { + if (isScriptMethod(first.name)) { + MethodInfo scriptMethod = getScriptMethodInfo(first.name); + if (scriptMethod != null) { + currentType = scriptMethod.getReturnType(); + } + } + } + + for (int i = 1; i < segments.size(); i++) { + currentType = resolveChainSegment(currentType, segments.get(i)); + if (currentType == null) { + return null; + } + } + + return currentType; + } + /** * Find the matching opening parenthesis when given a closing paren position. */ diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionNode.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionNode.java new file mode 100644 index 000000000..beb0397c0 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionNode.java @@ -0,0 +1,232 @@ +package noppes.npcs.client.gui.util.script.interpreter.expression; + +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public abstract class ExpressionNode { + protected final int start; + protected final int end; + + protected ExpressionNode(int start, int end) { + this.start = start; + this.end = end; + } + + public int getStart() { return start; } + public int getEnd() { return end; } + public abstract TypeInfo resolveType(TypeResolverContext resolver); + + public static class IntLiteralNode extends ExpressionNode { + private final String value; + public IntLiteralNode(String value, int start, int end) { super(start, end); this.value = value; } + public String getValue() { return value; } + public TypeInfo resolveType(TypeResolverContext resolver) { return TypeInfo.fromPrimitive("int"); } + } + + public static class LongLiteralNode extends ExpressionNode { + public LongLiteralNode(String value, int start, int end) { super(start, end); } + public TypeInfo resolveType(TypeResolverContext resolver) { return TypeInfo.fromPrimitive("long"); } + } + + public static class FloatLiteralNode extends ExpressionNode { + public FloatLiteralNode(String value, int start, int end) { super(start, end); } + public TypeInfo resolveType(TypeResolverContext resolver) { return TypeInfo.fromPrimitive("float"); } + } + + public static class DoubleLiteralNode extends ExpressionNode { + public DoubleLiteralNode(String value, int start, int end) { super(start, end); } + public TypeInfo resolveType(TypeResolverContext resolver) { return TypeInfo.fromPrimitive("double"); } + } + + public static class BooleanLiteralNode extends ExpressionNode { + public BooleanLiteralNode(boolean value, int start, int end) { super(start, end); } + public TypeInfo resolveType(TypeResolverContext resolver) { return TypeInfo.fromPrimitive("boolean"); } + } + + public static class CharLiteralNode extends ExpressionNode { + public CharLiteralNode(String value, int start, int end) { super(start, end); } + public TypeInfo resolveType(TypeResolverContext resolver) { return TypeInfo.fromPrimitive("char"); } + } + + public static class StringLiteralNode extends ExpressionNode { + private final String value; + public StringLiteralNode(String value, int start, int end) { super(start, end); this.value = value; } + public String getValue() { return value; } + public TypeInfo resolveType(TypeResolverContext resolver) { return TypeInfo.fromClass(String.class); } + } + + public static class NullLiteralNode extends ExpressionNode { + public NullLiteralNode(int start, int end) { super(start, end); } + public TypeInfo resolveType(TypeResolverContext resolver) { return null; } + } + + public static class IdentifierNode extends ExpressionNode { + private final String name; + public IdentifierNode(String name, int start, int end) { super(start, end); this.name = name; } + public String getName() { return name; } + public TypeInfo resolveType(TypeResolverContext resolver) { return resolver.resolveIdentifier(name); } + } + + public static class MemberAccessNode extends ExpressionNode { + private final ExpressionNode target; + private final String memberName; + public MemberAccessNode(ExpressionNode target, String memberName, int start, int end) { + super(start, end); this.target = target; this.memberName = memberName; + } + public ExpressionNode getTarget() { return target; } + public String getMemberName() { return memberName; } + public TypeInfo resolveType(TypeResolverContext resolver) { + TypeInfo targetType = target.resolveType(resolver); + if (targetType == null || !targetType.isResolved()) return null; + return resolver.resolveMemberAccess(targetType, memberName); + } + } + + public static class MethodCallNode extends ExpressionNode { + private final ExpressionNode target; + private final String methodName; + private final List arguments; + public MethodCallNode(ExpressionNode target, String methodName, List arguments, int start, int end) { + super(start, end); this.target = target; this.methodName = methodName; + this.arguments = arguments != null ? new ArrayList<>(arguments) : new ArrayList<>(); + } + public ExpressionNode getTarget() { return target; } + public String getMethodName() { return methodName; } + public List getArguments() { return Collections.unmodifiableList(arguments); } + public TypeInfo resolveType(TypeResolverContext resolver) { + TypeInfo targetType = target != null ? target.resolveType(resolver) : null; + TypeInfo[] argTypes = new TypeInfo[arguments.size()]; + for (int i = 0; i < arguments.size(); i++) argTypes[i] = arguments.get(i).resolveType(resolver); + return resolver.resolveMethodCall(targetType, methodName, argTypes); + } + } + + public static class ArrayAccessNode extends ExpressionNode { + private final ExpressionNode array; + private final ExpressionNode index; + public ArrayAccessNode(ExpressionNode array, ExpressionNode index, int start, int end) { + super(start, end); this.array = array; this.index = index; + } + public ExpressionNode getArray() { return array; } + public ExpressionNode getIndex() { return index; } + public TypeInfo resolveType(TypeResolverContext resolver) { + TypeInfo arrayType = array.resolveType(resolver); + if (arrayType == null || !arrayType.isResolved()) return null; + return resolver.resolveArrayAccess(arrayType); + } + } + + public static class NewNode extends ExpressionNode { + private final String typeName; + private final List arguments; + public NewNode(String typeName, List arguments, int start, int end) { + super(start, end); this.typeName = typeName; + this.arguments = arguments != null ? new ArrayList<>(arguments) : new ArrayList<>(); + } + public String getTypeName() { return typeName; } + public List getArguments() { return Collections.unmodifiableList(arguments); } + public TypeInfo resolveType(TypeResolverContext resolver) { return resolver.resolveTypeName(typeName); } + } + + public static class BinaryOpNode extends ExpressionNode { + private final ExpressionNode left; + private final OperatorType operator; + private final ExpressionNode right; + public BinaryOpNode(ExpressionNode left, OperatorType operator, ExpressionNode right, int start, int end) { + super(start, end); this.left = left; this.operator = operator; this.right = right; + } + public ExpressionNode getLeft() { return left; } + public OperatorType getOperator() { return operator; } + public ExpressionNode getRight() { return right; } + public TypeInfo resolveType(TypeResolverContext resolver) { + TypeInfo leftType = left.resolveType(resolver); + TypeInfo rightType = right.resolveType(resolver); + return TypeRules.resolveBinaryOperatorType(operator, leftType, rightType); + } + } + + public static class UnaryOpNode extends ExpressionNode { + private final OperatorType operator; + private final ExpressionNode operand; + private final boolean prefix; + public UnaryOpNode(OperatorType operator, ExpressionNode operand, boolean prefix, int start, int end) { + super(start, end); this.operator = operator; this.operand = operand; this.prefix = prefix; + } + public OperatorType getOperator() { return operator; } + public ExpressionNode getOperand() { return operand; } + public boolean isPrefix() { return prefix; } + public TypeInfo resolveType(TypeResolverContext resolver) { + TypeInfo operandType = operand.resolveType(resolver); + return TypeRules.resolveUnaryOperatorType(operator, operandType); + } + } + + public static class TernaryNode extends ExpressionNode { + private final ExpressionNode condition; + private final ExpressionNode thenExpr; + private final ExpressionNode elseExpr; + public TernaryNode(ExpressionNode condition, ExpressionNode thenExpr, ExpressionNode elseExpr, int start, int end) { + super(start, end); this.condition = condition; this.thenExpr = thenExpr; this.elseExpr = elseExpr; + } + public ExpressionNode getCondition() { return condition; } + public ExpressionNode getThenExpr() { return thenExpr; } + public ExpressionNode getElseExpr() { return elseExpr; } + public TypeInfo resolveType(TypeResolverContext resolver) { + TypeInfo thenType = thenExpr.resolveType(resolver); + TypeInfo elseType = elseExpr.resolveType(resolver); + return TypeRules.resolveTernaryType(thenType, elseType); + } + } + + public static class InstanceofNode extends ExpressionNode { + private final ExpressionNode expression; + private final String typeName; + public InstanceofNode(ExpressionNode expression, String typeName, int start, int end) { + super(start, end); this.expression = expression; this.typeName = typeName; + } + public ExpressionNode getExpression() { return expression; } + public String getTypeName() { return typeName; } + public TypeInfo resolveType(TypeResolverContext resolver) { return TypeInfo.fromPrimitive("boolean"); } + } + + public static class CastNode extends ExpressionNode { + private final String typeName; + private final ExpressionNode expression; + public CastNode(String typeName, ExpressionNode expression, int start, int end) { + super(start, end); this.typeName = typeName; this.expression = expression; + } + public String getTypeName() { return typeName; } + public ExpressionNode getExpression() { return expression; } + public TypeInfo resolveType(TypeResolverContext resolver) { return resolver.resolveTypeName(typeName); } + } + + public static class AssignmentNode extends ExpressionNode { + private final ExpressionNode target; + private final OperatorType operator; + private final ExpressionNode value; + public AssignmentNode(ExpressionNode target, OperatorType operator, ExpressionNode value, int start, int end) { + super(start, end); this.target = target; this.operator = operator; this.value = value; + } + public ExpressionNode getTarget() { return target; } + public OperatorType getOperator() { return operator; } + public ExpressionNode getValue() { return value; } + public TypeInfo resolveType(TypeResolverContext resolver) { return target.resolveType(resolver); } + } + + public static class ParenthesizedNode extends ExpressionNode { + private final ExpressionNode inner; + public ParenthesizedNode(ExpressionNode inner, int start, int end) { super(start, end); this.inner = inner; } + public ExpressionNode getInner() { return inner; } + public TypeInfo resolveType(TypeResolverContext resolver) { return inner.resolveType(resolver); } + } + + public interface TypeResolverContext { + TypeInfo resolveIdentifier(String name); + TypeInfo resolveMemberAccess(TypeInfo targetType, String memberName); + TypeInfo resolveMethodCall(TypeInfo targetType, String methodName, TypeInfo[] argTypes); + TypeInfo resolveArrayAccess(TypeInfo arrayType); + TypeInfo resolveTypeName(String typeName); + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionParser.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionParser.java new file mode 100644 index 000000000..5bb2f9fd7 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionParser.java @@ -0,0 +1,331 @@ +package noppes.npcs.client.gui.util.script.interpreter.expression; + +import java.util.ArrayList; +import java.util.List; + +public class ExpressionParser { + private final List tokens; + private int pos; + + public ExpressionParser(List tokens) { + this.tokens = tokens; + this.pos = 0; + } + + public ExpressionNode parse() { + if (tokens.isEmpty()) return null; + return parseExpression(0); + } + + private ExpressionToken current() { + if (pos >= tokens.size()) return tokens.get(tokens.size() - 1); + return tokens.get(pos); + } + + private ExpressionToken advance() { + ExpressionToken tok = current(); + if (pos < tokens.size()) pos++; + return tok; + } + + private boolean check(ExpressionToken.TokenKind kind) { return current().getKind() == kind; } + + private boolean match(ExpressionToken.TokenKind kind) { + if (check(kind)) { advance(); return true; } + return false; + } + + private ExpressionNode parseExpression(int minPrecedence) { + ExpressionNode left = parsePrefixExpression(); + if (left == null) return null; + + while (true) { + if (check(ExpressionToken.TokenKind.QUESTION) && minPrecedence <= 2) { + left = parseTernary(left); + continue; + } + + OperatorType op = getCurrentBinaryOperator(); + if (op == null) break; + + int precedence = op.getPrecedence(); + if (precedence < minPrecedence) break; + + advance(); + + int nextMinPrecedence = (op.getAssociativity() == OperatorType.Associativity.LEFT) + ? precedence + 1 : precedence; + + ExpressionNode right = parseExpression(nextMinPrecedence); + if (right == null) break; + + if (op == OperatorType.INSTANCEOF) { + String typeName = extractTypeName(right); + left = new ExpressionNode.InstanceofNode(left, typeName, left.getStart(), right.getEnd()); + } else if (op.isAssignment()) { + left = new ExpressionNode.AssignmentNode(left, op, right, left.getStart(), right.getEnd()); + } else { + left = new ExpressionNode.BinaryOpNode(left, op, right, left.getStart(), right.getEnd()); + } + } + + return parsePostfixExpression(left); + } + + private String extractTypeName(ExpressionNode node) { + if (node instanceof ExpressionNode.IdentifierNode) { + return ((ExpressionNode.IdentifierNode) node).getName(); + } + if (node instanceof ExpressionNode.MemberAccessNode) { + ExpressionNode.MemberAccessNode ma = (ExpressionNode.MemberAccessNode) node; + return extractTypeName(ma.getTarget()) + "." + ma.getMemberName(); + } + return "Object"; + } + + private ExpressionNode parseTernary(ExpressionNode condition) { + int start = condition.getStart(); + advance(); + ExpressionNode thenExpr = parseExpression(0); + if (thenExpr == null) return condition; + if (!match(ExpressionToken.TokenKind.COLON)) return condition; + ExpressionNode elseExpr = parseExpression(2); + if (elseExpr == null) return condition; + return new ExpressionNode.TernaryNode(condition, thenExpr, elseExpr, start, elseExpr.getEnd()); + } + + private OperatorType getCurrentBinaryOperator() { + ExpressionToken tok = current(); + if (tok.getKind() == ExpressionToken.TokenKind.OPERATOR) { + OperatorType op = tok.getOperatorType(); + if (op != null && (op.isBinary() || op.isAssignment())) return op; + } + if (tok.getKind() == ExpressionToken.TokenKind.INSTANCEOF) return OperatorType.INSTANCEOF; + return null; + } + + private ExpressionNode parsePrefixExpression() { + ExpressionToken tok = current(); + int start = tok.getStart(); + + if (tok.getKind() == ExpressionToken.TokenKind.OPERATOR) { + OperatorType op = tok.getOperatorType(); + if (op != null && isUnaryOperator(op)) { + advance(); + ExpressionNode operand = parsePrefixExpression(); + if (operand == null) return null; + OperatorType unaryOp = toUnaryOperator(op); + return new ExpressionNode.UnaryOpNode(unaryOp, operand, true, start, operand.getEnd()); + } + } + + if (tok.getKind() == ExpressionToken.TokenKind.LEFT_PAREN) { + return parseCastOrParenthesized(); + } + + return parsePrimaryExpression(); + } + + private boolean isUnaryOperator(OperatorType op) { + switch (op) { + case ADD: case SUBTRACT: case LOGICAL_NOT: case BITWISE_NOT: + case PRE_INCREMENT: case PRE_DECREMENT: return true; + default: return false; + } + } + + private OperatorType toUnaryOperator(OperatorType op) { + switch (op) { + case ADD: return OperatorType.UNARY_PLUS; + case SUBTRACT: return OperatorType.UNARY_MINUS; + default: return op; + } + } + + private ExpressionNode parseCastOrParenthesized() { + int start = current().getStart(); + advance(); + + if (check(ExpressionToken.TokenKind.IDENTIFIER)) { + String possibleType = current().getText(); + int savedPos = pos; + advance(); + + while (check(ExpressionToken.TokenKind.DOT)) { + advance(); + if (check(ExpressionToken.TokenKind.IDENTIFIER)) { + possibleType += "." + current().getText(); + advance(); + } + } + + while (check(ExpressionToken.TokenKind.LEFT_BRACKET)) { + advance(); + if (!match(ExpressionToken.TokenKind.RIGHT_BRACKET)) { pos = savedPos; break; } + possibleType += "[]"; + } + + if (check(ExpressionToken.TokenKind.RIGHT_PAREN)) { + advance(); + if (canStartExpression()) { + ExpressionNode expr = parsePrefixExpression(); + if (expr != null) { + return new ExpressionNode.CastNode(possibleType, expr, start, expr.getEnd()); + } + } + } + pos = savedPos; + } + + ExpressionNode inner = parseExpression(0); + if (inner != null && match(ExpressionToken.TokenKind.RIGHT_PAREN)) { + return new ExpressionNode.ParenthesizedNode(inner, start, current().getStart()); + } + return inner; + } + + private boolean canStartExpression() { + ExpressionToken.TokenKind kind = current().getKind(); + switch (kind) { + case IDENTIFIER: case INT_LITERAL: case LONG_LITERAL: case FLOAT_LITERAL: + case DOUBLE_LITERAL: case BOOLEAN_LITERAL: case CHAR_LITERAL: case STRING_LITERAL: + case NULL_LITERAL: case NEW: case LEFT_PAREN: case OPERATOR: return true; + default: return false; + } + } + + private ExpressionNode parsePrimaryExpression() { + ExpressionToken tok = current(); + int start = tok.getStart(); + + switch (tok.getKind()) { + case INT_LITERAL: advance(); return new ExpressionNode.IntLiteralNode(tok.getText(), start, tok.getEnd()); + case LONG_LITERAL: advance(); return new ExpressionNode.LongLiteralNode(tok.getText(), start, tok.getEnd()); + case FLOAT_LITERAL: advance(); return new ExpressionNode.FloatLiteralNode(tok.getText(), start, tok.getEnd()); + case DOUBLE_LITERAL: advance(); return new ExpressionNode.DoubleLiteralNode(tok.getText(), start, tok.getEnd()); + case BOOLEAN_LITERAL: advance(); return new ExpressionNode.BooleanLiteralNode("true".equals(tok.getText()), start, tok.getEnd()); + case CHAR_LITERAL: advance(); return new ExpressionNode.CharLiteralNode(tok.getText(), start, tok.getEnd()); + case STRING_LITERAL: advance(); return new ExpressionNode.StringLiteralNode(tok.getText(), start, tok.getEnd()); + case NULL_LITERAL: advance(); return new ExpressionNode.NullLiteralNode(start, tok.getEnd()); + case NEW: return parseNewExpression(); + case IDENTIFIER: return parseIdentifierOrMethodCall(); + case LEFT_PAREN: return parseCastOrParenthesized(); + default: return null; + } + } + + private ExpressionNode parseNewExpression() { + int start = current().getStart(); + advance(); + if (!check(ExpressionToken.TokenKind.IDENTIFIER)) return null; + + StringBuilder typeName = new StringBuilder(current().getText()); + advance(); + + while (check(ExpressionToken.TokenKind.DOT)) { + advance(); + if (check(ExpressionToken.TokenKind.IDENTIFIER)) { + typeName.append(".").append(current().getText()); + advance(); + } + } + + List args = new ArrayList<>(); + if (match(ExpressionToken.TokenKind.LEFT_PAREN)) { + args = parseArgumentList(); + match(ExpressionToken.TokenKind.RIGHT_PAREN); + } + + return new ExpressionNode.NewNode(typeName.toString(), args, start, current().getStart()); + } + + private ExpressionNode parseIdentifierOrMethodCall() { + int start = current().getStart(); + String name = current().getText(); + advance(); + + ExpressionNode result = new ExpressionNode.IdentifierNode(name, start, current().getStart()); + return parseAccessChain(result); + } + + private ExpressionNode parseAccessChain(ExpressionNode base) { + while (true) { + if (check(ExpressionToken.TokenKind.LEFT_PAREN)) { + advance(); + List args = parseArgumentList(); + int end = current().getStart(); + match(ExpressionToken.TokenKind.RIGHT_PAREN); + + String methodName; + ExpressionNode target; + + if (base instanceof ExpressionNode.IdentifierNode) { + methodName = ((ExpressionNode.IdentifierNode) base).getName(); + target = null; + } else if (base instanceof ExpressionNode.MemberAccessNode) { + ExpressionNode.MemberAccessNode ma = (ExpressionNode.MemberAccessNode) base; + methodName = ma.getMemberName(); + target = ma.getTarget(); + } else { + methodName = "apply"; + target = base; + } + + base = new ExpressionNode.MethodCallNode(target, methodName, args, base.getStart(), end); + } else if (check(ExpressionToken.TokenKind.DOT)) { + advance(); + if (!check(ExpressionToken.TokenKind.IDENTIFIER)) break; + String memberName = current().getText(); + int end = current().getEnd(); + advance(); + base = new ExpressionNode.MemberAccessNode(base, memberName, base.getStart(), end); + } else if (check(ExpressionToken.TokenKind.LEFT_BRACKET)) { + advance(); + ExpressionNode index = parseExpression(0); + int end = current().getStart(); + match(ExpressionToken.TokenKind.RIGHT_BRACKET); + if (index != null) { + base = new ExpressionNode.ArrayAccessNode(base, index, base.getStart(), end); + } + } else { + break; + } + } + return base; + } + + private List parseArgumentList() { + List args = new ArrayList<>(); + if (check(ExpressionToken.TokenKind.RIGHT_PAREN)) return args; + + ExpressionNode first = parseExpression(0); + if (first != null) args.add(first); + + while (match(ExpressionToken.TokenKind.COMMA)) { + ExpressionNode arg = parseExpression(0); + if (arg != null) args.add(arg); + } + return args; + } + + private ExpressionNode parsePostfixExpression(ExpressionNode expr) { + if (expr == null) return null; + + while (true) { + ExpressionToken tok = current(); + if (tok.getKind() != ExpressionToken.TokenKind.OPERATOR) break; + + OperatorType op = tok.getOperatorType(); + if (op == OperatorType.PRE_INCREMENT) { + advance(); + expr = new ExpressionNode.UnaryOpNode(OperatorType.POST_INCREMENT, expr, false, expr.getStart(), tok.getEnd()); + } else if (op == OperatorType.PRE_DECREMENT) { + advance(); + expr = new ExpressionNode.UnaryOpNode(OperatorType.POST_DECREMENT, expr, false, expr.getStart(), tok.getEnd()); + } else { + break; + } + } + return expr; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionToken.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionToken.java new file mode 100644 index 000000000..655adea9a --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionToken.java @@ -0,0 +1,60 @@ +package noppes.npcs.client.gui.util.script.interpreter.expression; + +public class ExpressionToken { + + public enum TokenKind { + INT_LITERAL, LONG_LITERAL, FLOAT_LITERAL, DOUBLE_LITERAL, + BOOLEAN_LITERAL, CHAR_LITERAL, STRING_LITERAL, NULL_LITERAL, + IDENTIFIER, NEW, INSTANCEOF, + OPERATOR, + LEFT_PAREN, RIGHT_PAREN, LEFT_BRACKET, RIGHT_BRACKET, + DOT, COMMA, QUESTION, COLON, SEMICOLON, + EOF + } + + private final TokenKind kind; + private final String text; + private final int start; + private final int end; + private final OperatorType operatorType; + + public ExpressionToken(TokenKind kind, String text, int start, int end) { + this(kind, text, start, end, null); + } + + public ExpressionToken(TokenKind kind, String text, int start, int end, OperatorType operatorType) { + this.kind = kind; + this.text = text; + this.start = start; + this.end = end; + this.operatorType = operatorType; + } + + public TokenKind getKind() { return kind; } + public String getText() { return text; } + public int getStart() { return start; } + public int getEnd() { return end; } + public OperatorType getOperatorType() { return operatorType; } + + public static ExpressionToken operator(String symbol, int start, int end) { + OperatorType op = OperatorType.fromBinarySymbol(symbol); + if (op == null) op = OperatorType.fromSymbol(symbol); + return new ExpressionToken(TokenKind.OPERATOR, symbol, start, end, op); + } + + public static ExpressionToken identifier(String name, int start, int end) { + if ("true".equals(name) || "false".equals(name)) { + return new ExpressionToken(TokenKind.BOOLEAN_LITERAL, name, start, end); + } + if ("null".equals(name)) { + return new ExpressionToken(TokenKind.NULL_LITERAL, name, start, end); + } + if ("new".equals(name)) { + return new ExpressionToken(TokenKind.NEW, name, start, end); + } + if ("instanceof".equals(name)) { + return new ExpressionToken(TokenKind.INSTANCEOF, name, start, end); + } + return new ExpressionToken(TokenKind.IDENTIFIER, name, start, end); + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTokenizer.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTokenizer.java new file mode 100644 index 000000000..25ee10ea5 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTokenizer.java @@ -0,0 +1,152 @@ +package noppes.npcs.client.gui.util.script.interpreter.expression; + +import java.util.ArrayList; +import java.util.List; + +public class ExpressionTokenizer { + + public static List tokenize(String expr) { + List tokens = new ArrayList<>(); + int pos = 0; + int len = expr.length(); + + while (pos < len) { + char c = expr.charAt(pos); + + if (Character.isWhitespace(c)) { pos++; continue; } + + if (c == '"') { + int start = pos++; + while (pos < len && expr.charAt(pos) != '"') { + if (expr.charAt(pos) == '\\' && pos + 1 < len) pos++; + pos++; + } + if (pos < len) pos++; + tokens.add(new ExpressionToken(ExpressionToken.TokenKind.STRING_LITERAL, expr.substring(start, pos), start, pos)); + continue; + } + + if (c == '\'') { + int start = pos++; + while (pos < len && expr.charAt(pos) != '\'') { + if (expr.charAt(pos) == '\\' && pos + 1 < len) pos++; + pos++; + } + if (pos < len) pos++; + tokens.add(new ExpressionToken(ExpressionToken.TokenKind.CHAR_LITERAL, expr.substring(start, pos), start, pos)); + continue; + } + + if (Character.isDigit(c) || (c == '.' && pos + 1 < len && Character.isDigit(expr.charAt(pos + 1)))) { + int start = pos; + if (c == '0' && pos + 1 < len) { + char next = expr.charAt(pos + 1); + if (next == 'x' || next == 'X') { + pos += 2; + while (pos < len && isHexDigit(expr.charAt(pos))) pos++; + if (pos < len && (expr.charAt(pos) == 'L' || expr.charAt(pos) == 'l')) { + pos++; + tokens.add(new ExpressionToken(ExpressionToken.TokenKind.LONG_LITERAL, expr.substring(start, pos), start, pos)); + } else { + tokens.add(new ExpressionToken(ExpressionToken.TokenKind.INT_LITERAL, expr.substring(start, pos), start, pos)); + } + continue; + } + if (next == 'b' || next == 'B') { + pos += 2; + while (pos < len && (expr.charAt(pos) == '0' || expr.charAt(pos) == '1')) pos++; + tokens.add(new ExpressionToken(ExpressionToken.TokenKind.INT_LITERAL, expr.substring(start, pos), start, pos)); + continue; + } + } + + while (pos < len && Character.isDigit(expr.charAt(pos))) pos++; + boolean hasDecimal = false; + if (pos < len && expr.charAt(pos) == '.') { + if (pos + 1 < len && Character.isDigit(expr.charAt(pos + 1))) { + hasDecimal = true; + pos++; + while (pos < len && Character.isDigit(expr.charAt(pos))) pos++; + } + } + if (pos < len && (expr.charAt(pos) == 'e' || expr.charAt(pos) == 'E')) { + pos++; + if (pos < len && (expr.charAt(pos) == '+' || expr.charAt(pos) == '-')) pos++; + while (pos < len && Character.isDigit(expr.charAt(pos))) pos++; + hasDecimal = true; + } + + ExpressionToken.TokenKind kind = ExpressionToken.TokenKind.INT_LITERAL; + if (pos < len) { + char suffix = expr.charAt(pos); + if (suffix == 'f' || suffix == 'F') { kind = ExpressionToken.TokenKind.FLOAT_LITERAL; pos++; } + else if (suffix == 'd' || suffix == 'D') { kind = ExpressionToken.TokenKind.DOUBLE_LITERAL; pos++; } + else if (suffix == 'L' || suffix == 'l') { kind = ExpressionToken.TokenKind.LONG_LITERAL; pos++; } + else if (hasDecimal) kind = ExpressionToken.TokenKind.DOUBLE_LITERAL; + } else if (hasDecimal) kind = ExpressionToken.TokenKind.DOUBLE_LITERAL; + + tokens.add(new ExpressionToken(kind, expr.substring(start, pos), start, pos)); + continue; + } + + if (Character.isJavaIdentifierStart(c)) { + int start = pos; + while (pos < len && Character.isJavaIdentifierPart(expr.charAt(pos))) pos++; + tokens.add(ExpressionToken.identifier(expr.substring(start, pos), start, pos)); + continue; + } + + int start = pos; + String op = null; + if (pos + 3 <= len) { + String s3 = expr.substring(pos, pos + 3); + if (">>>".equals(s3) || "<<=".equals(s3) || ">>=".equals(s3)) op = s3; + } + if (op == null && pos + 4 <= len) { + String s4 = expr.substring(pos, pos + 4); + if (">>>=".equals(s4)) op = s4; + } + if (op == null && pos + 2 <= len) { + String s2 = expr.substring(pos, pos + 2); + if ("++".equals(s2) || "--".equals(s2) || "+=".equals(s2) || "-=".equals(s2) || + "*=".equals(s2) || "/=".equals(s2) || "%=".equals(s2) || "&=".equals(s2) || + "|=".equals(s2) || "^=".equals(s2) || "==".equals(s2) || "!=".equals(s2) || + "<=".equals(s2) || ">=".equals(s2) || "&&".equals(s2) || "||".equals(s2) || + "<<".equals(s2) || ">>".equals(s2)) op = s2; + } + if (op == null) { + switch (c) { + case '+': case '-': case '*': case '/': case '%': case '&': case '|': + case '^': case '~': case '!': case '<': case '>': case '=': + op = String.valueOf(c); break; + } + } + + if (op != null) { + pos += op.length(); + tokens.add(ExpressionToken.operator(op, start, pos)); + continue; + } + + switch (c) { + case '(': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.LEFT_PAREN, "(", pos, pos + 1)); break; + case ')': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.RIGHT_PAREN, ")", pos, pos + 1)); break; + case '[': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.LEFT_BRACKET, "[", pos, pos + 1)); break; + case ']': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.RIGHT_BRACKET, "]", pos, pos + 1)); break; + case '.': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.DOT, ".", pos, pos + 1)); break; + case ',': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.COMMA, ",", pos, pos + 1)); break; + case '?': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.QUESTION, "?", pos, pos + 1)); break; + case ':': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.COLON, ":", pos, pos + 1)); break; + case ';': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.SEMICOLON, ";", pos, pos + 1)); break; + } + pos++; + } + + tokens.add(new ExpressionToken(ExpressionToken.TokenKind.EOF, "", len, len)); + return tokens; + } + + private static boolean isHexDigit(char c) { + return Character.isDigit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); + } +} 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 new file mode 100644 index 000000000..c07ffefcf --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTypeResolver.java @@ -0,0 +1,167 @@ +package noppes.npcs.client.gui.util.script.interpreter.expression; + +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; +import java.util.List; + +public class ExpressionTypeResolver { + private final ExpressionNode.TypeResolverContext context; + + public ExpressionTypeResolver(ExpressionNode.TypeResolverContext context) { + this.context = context; + } + + public TypeInfo resolve(String expression) { + if (expression == null || expression.trim().isEmpty()) return null; + + try { + List tokens = ExpressionTokenizer.tokenize(expression); + if (tokens.isEmpty()) return null; + + ExpressionParser parser = new ExpressionParser(tokens); + ExpressionNode ast = parser.parse(); + if (ast == null) return null; + + return resolveNodeType(ast); + } catch (Exception e) { + return null; + } + } + + public static boolean containsOperators(String expression) { + if (expression == null) return false; + for (int i = 0; i < expression.length(); i++) { + char c = expression.charAt(i); + switch (c) { + case '+': case '-': case '*': case '/': case '%': + case '&': case '|': case '^': case '~': + case '<': case '>': case '=': case '!': + case '?': case ':': return true; + } + } + return false; + } + + private TypeInfo resolveNodeType(ExpressionNode node) { + if (node == null) return null; + + if (node instanceof ExpressionNode.IntLiteralNode) 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"); + if (node instanceof ExpressionNode.BooleanLiteralNode) return TypeInfo.fromPrimitive("boolean"); + if (node instanceof ExpressionNode.CharLiteralNode) return TypeInfo.fromPrimitive("char"); + if (node instanceof ExpressionNode.StringLiteralNode) return TypeInfo.fromClass(String.class); + if (node instanceof ExpressionNode.NullLiteralNode) return null; + + if (node instanceof ExpressionNode.IdentifierNode) { + return context.resolveIdentifier(((ExpressionNode.IdentifierNode) node).getName()); + } + + if (node instanceof ExpressionNode.MemberAccessNode) { + ExpressionNode.MemberAccessNode ma = (ExpressionNode.MemberAccessNode) node; + TypeInfo targetType = resolveNodeType(ma.getTarget()); + if (targetType == null || !targetType.isResolved()) return null; + return context.resolveMemberAccess(targetType, ma.getMemberName()); + } + + if (node instanceof ExpressionNode.MethodCallNode) { + ExpressionNode.MethodCallNode mc = (ExpressionNode.MethodCallNode) node; + TypeInfo targetType = mc.getTarget() == null ? context.resolveIdentifier("this") : resolveNodeType(mc.getTarget()); + if (targetType == null || !targetType.isResolved()) return null; + TypeInfo[] argTypes = new TypeInfo[mc.getArguments().size()]; + for (int i = 0; i < argTypes.length; i++) argTypes[i] = resolveNodeType(mc.getArguments().get(i)); + return context.resolveMethodCall(targetType, mc.getMethodName(), argTypes); + } + + if (node instanceof ExpressionNode.ArrayAccessNode) { + ExpressionNode.ArrayAccessNode aa = (ExpressionNode.ArrayAccessNode) node; + TypeInfo arrayType = resolveNodeType(aa.getArray()); + if (arrayType == null || !arrayType.isResolved()) return null; + String typeName = arrayType.getFullName(); + if (typeName.endsWith("[]")) { + String elementTypeName = typeName.substring(0, typeName.length() - 2); + return context.resolveTypeName(elementTypeName); + } + return context.resolveArrayAccess(arrayType); + } + + if (node instanceof ExpressionNode.NewNode) { + return context.resolveTypeName(((ExpressionNode.NewNode) node).getTypeName()); + } + + if (node instanceof ExpressionNode.BinaryOpNode) { + ExpressionNode.BinaryOpNode bin = (ExpressionNode.BinaryOpNode) node; + TypeInfo leftType = resolveNodeType(bin.getLeft()); + TypeInfo rightType = resolveNodeType(bin.getRight()); + return TypeRules.resolveBinaryOperatorType(bin.getOperator(), leftType, rightType); + } + + if (node instanceof ExpressionNode.UnaryOpNode) { + ExpressionNode.UnaryOpNode un = (ExpressionNode.UnaryOpNode) node; + TypeInfo operandType = resolveNodeType(un.getOperand()); + return TypeRules.resolveUnaryOperatorType(un.getOperator(), operandType); + } + + if (node instanceof ExpressionNode.TernaryNode) { + ExpressionNode.TernaryNode tern = (ExpressionNode.TernaryNode) node; + TypeInfo thenType = resolveNodeType(tern.getThenExpr()); + TypeInfo elseType = resolveNodeType(tern.getElseExpr()); + return TypeRules.resolveTernaryType(thenType, elseType); + } + + if (node instanceof ExpressionNode.CastNode) { + return context.resolveTypeName(((ExpressionNode.CastNode) node).getTypeName()); + } + + if (node instanceof ExpressionNode.InstanceofNode) { + return TypeInfo.fromPrimitive("boolean"); + } + + if (node instanceof ExpressionNode.AssignmentNode) { + return resolveNodeType(((ExpressionNode.AssignmentNode) node).getTarget()); + } + + if (node instanceof ExpressionNode.ParenthesizedNode) { + return resolveNodeType(((ExpressionNode.ParenthesizedNode) node).getInner()); + } + + return null; + } + + public static ExpressionNode.TypeResolverContext createBasicContext() { + return new ExpressionNode.TypeResolverContext() { + public TypeInfo resolveIdentifier(String name) { + if ("true".equals(name) || "false".equals(name)) return TypeInfo.fromPrimitive("boolean"); + if ("null".equals(name)) return null; + return null; + } + public TypeInfo resolveMemberAccess(TypeInfo targetType, String memberName) { return null; } + public TypeInfo resolveMethodCall(TypeInfo targetType, String methodName, TypeInfo[] argTypes) { return null; } + public TypeInfo resolveArrayAccess(TypeInfo arrayType) { + String typeName = arrayType.getFullName(); + if (typeName.endsWith("[]")) { + // For primitive arrays, we need to map back to primitive TypeInfo + String elementType = typeName.substring(0, typeName.length() - 2); + switch (elementType) { + case "int": case "long": case "float": case "double": + case "byte": case "short": case "char": case "boolean": + return TypeInfo.fromPrimitive(elementType); + default: + return TypeInfo.unresolved(elementType, elementType); + } + } + return null; + } + public TypeInfo resolveTypeName(String typeName) { + // Handle primitives + switch (typeName) { + case "int": case "long": case "float": case "double": + case "byte": case "short": case "char": case "boolean": case "void": + return TypeInfo.fromPrimitive(typeName); + default: + return TypeInfo.unresolved(typeName, typeName); + } + } + }; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/OperatorType.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/OperatorType.java new file mode 100644 index 000000000..2d4bf32dc --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/OperatorType.java @@ -0,0 +1,147 @@ +package noppes.npcs.client.gui.util.script.interpreter.expression; + +public enum OperatorType { + + ADD("+", 11, Associativity.LEFT, Category.ARITHMETIC), + SUBTRACT("-", 11, Associativity.LEFT, Category.ARITHMETIC), + MULTIPLY("*", 12, Associativity.LEFT, Category.ARITHMETIC), + DIVIDE("/", 12, Associativity.LEFT, Category.ARITHMETIC), + MODULO("%", 12, Associativity.LEFT, Category.ARITHMETIC), + + + ASSIGN("=", 1, Associativity.RIGHT, Category.ASSIGNMENT), + ADD_ASSIGN("+=", 1, Associativity.RIGHT, Category.ASSIGNMENT), + SUBTRACT_ASSIGN("-=", 1, Associativity.RIGHT, Category.ASSIGNMENT), + MULTIPLY_ASSIGN("*=", 1, Associativity.RIGHT, Category.ASSIGNMENT), + DIVIDE_ASSIGN("/=", 1, Associativity.RIGHT, Category.ASSIGNMENT), + MODULO_ASSIGN("%=", 1, Associativity.RIGHT, Category.ASSIGNMENT), + AND_ASSIGN("&=", 1, Associativity.RIGHT, Category.ASSIGNMENT), + OR_ASSIGN("|=", 1, Associativity.RIGHT, Category.ASSIGNMENT), + XOR_ASSIGN("^=", 1, Associativity.RIGHT, Category.ASSIGNMENT), + LEFT_SHIFT_ASSIGN("<<=", 1, Associativity.RIGHT, Category.ASSIGNMENT), + RIGHT_SHIFT_ASSIGN(">>=", 1, Associativity.RIGHT, Category.ASSIGNMENT), + UNSIGNED_RIGHT_SHIFT_ASSIGN(">>>=", 1, Associativity.RIGHT, Category.ASSIGNMENT), + + TERNARY_QUESTION("?", 2, Associativity.RIGHT, Category.TERNARY), + TERNARY_COLON(":", 2, Associativity.RIGHT, Category.TERNARY), + + LOGICAL_OR("||", 3, Associativity.LEFT, Category.LOGICAL), + LOGICAL_AND("&&", 4, Associativity.LEFT, Category.LOGICAL), + + BITWISE_OR("|", 5, Associativity.LEFT, Category.BITWISE), + BITWISE_XOR("^", 6, Associativity.LEFT, Category.BITWISE), + BITWISE_AND("&", 7, Associativity.LEFT, Category.BITWISE), + + EQUALS("==", 8, Associativity.LEFT, Category.RELATIONAL), + NOT_EQUALS("!=", 8, Associativity.LEFT, Category.RELATIONAL), + LESS_THAN("<", 9, Associativity.LEFT, Category.RELATIONAL), + GREATER_THAN(">", 9, Associativity.LEFT, Category.RELATIONAL), + LESS_THAN_OR_EQUAL("<=", 9, Associativity.LEFT, Category.RELATIONAL), + GREATER_THAN_OR_EQUAL(">=", 9, Associativity.LEFT, Category.RELATIONAL), + + LEFT_SHIFT("<<", 10, Associativity.LEFT, Category.BITWISE), + RIGHT_SHIFT(">>", 10, Associativity.LEFT, Category.BITWISE), + UNSIGNED_RIGHT_SHIFT(">>>", 10, Associativity.LEFT, Category.BITWISE), + + UNARY_PLUS("+", 14, Associativity.RIGHT, Category.UNARY), + UNARY_MINUS("-", 14, Associativity.RIGHT, Category.UNARY), + LOGICAL_NOT("!", 14, Associativity.RIGHT, Category.UNARY), + BITWISE_NOT("~", 14, Associativity.RIGHT, Category.UNARY), + PRE_INCREMENT("++", 14, Associativity.RIGHT, Category.UNARY), + PRE_DECREMENT("--", 14, Associativity.RIGHT, Category.UNARY), + POST_INCREMENT("++", 15, Associativity.LEFT, Category.UNARY_POSTFIX), + POST_DECREMENT("--", 15, Associativity.LEFT, Category.UNARY_POSTFIX), + + MEMBER_ACCESS(".", 16, Associativity.LEFT, Category.ACCESS), + ARRAY_ACCESS("[]", 16, Associativity.LEFT, Category.ACCESS), + METHOD_CALL("()", 16, Associativity.LEFT, Category.ACCESS), + + CAST("(type)", 14, Associativity.RIGHT, Category.CAST), + INSTANCEOF("instanceof", 9, Associativity.LEFT, Category.INSTANCEOF); + + + private final String symbol; + private final int precedence; + private final Associativity associativity; + private final Category category; + + OperatorType(String symbol, int precedence, Associativity associativity, Category category) { + this.symbol = symbol; + this.precedence = precedence; + this.associativity = associativity; + this.category = category; + } + + public String getSymbol() { return symbol; } + public int getPrecedence() { return precedence; } + public Associativity getAssociativity() { return associativity; } + public Category getCategory() { return category; } + + public boolean isBinary() { + return category == Category.ARITHMETIC || category == Category.RELATIONAL || + category == Category.LOGICAL || category == Category.BITWISE; + } + + public boolean isUnary() { + return category == Category.UNARY || category == Category.UNARY_POSTFIX; + } + + public boolean isAssignment() { + return category == Category.ASSIGNMENT; + } + + public boolean isComparison() { + return category == Category.RELATIONAL; + } + + public enum Associativity { LEFT, RIGHT } + + public enum Category { + ARITHMETIC, UNARY, UNARY_POSTFIX, RELATIONAL, LOGICAL, BITWISE, + TERNARY, INSTANCEOF, ASSIGNMENT, CAST, ACCESS + } + + public static OperatorType fromSymbol(String symbol) { + for (OperatorType op : values()) { + if (op.symbol.equals(symbol)) return op; + } + return null; + } + + public static OperatorType fromBinarySymbol(String symbol) { + switch (symbol) { + case "+": return ADD; + case "-": return SUBTRACT; + case "*": return MULTIPLY; + case "/": return DIVIDE; + case "%": return MODULO; + case "==": return EQUALS; + case "!=": return NOT_EQUALS; + case "<": return LESS_THAN; + case ">": return GREATER_THAN; + case "<=": return LESS_THAN_OR_EQUAL; + case ">=": return GREATER_THAN_OR_EQUAL; + case "&&": return LOGICAL_AND; + case "||": return LOGICAL_OR; + case "&": return BITWISE_AND; + case "|": return BITWISE_OR; + case "^": return BITWISE_XOR; + case "<<": return LEFT_SHIFT; + case ">>": return RIGHT_SHIFT; + case ">>>": return UNSIGNED_RIGHT_SHIFT; + case "=": return ASSIGN; + case "+=": return ADD_ASSIGN; + case "-=": return SUBTRACT_ASSIGN; + case "*=": return MULTIPLY_ASSIGN; + case "/=": return DIVIDE_ASSIGN; + case "%=": return MODULO_ASSIGN; + case "&=": return AND_ASSIGN; + case "|=": return OR_ASSIGN; + case "^=": return XOR_ASSIGN; + case "<<=": return LEFT_SHIFT_ASSIGN; + case ">>=": return RIGHT_SHIFT_ASSIGN; + case ">>>=": return UNSIGNED_RIGHT_SHIFT_ASSIGN; + default: return null; + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/TypeRules.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/TypeRules.java new file mode 100644 index 000000000..8b3fa8b17 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/TypeRules.java @@ -0,0 +1,157 @@ +package noppes.npcs.client.gui.util.script.interpreter.expression; + +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; + +public class TypeRules { + + public static boolean isNumeric(TypeInfo type) { + if (type == null || !type.isResolved()) return false; + String name = type.getSimpleName(); + return "int".equals(name) || "long".equals(name) || "float".equals(name) || + "double".equals(name) || "byte".equals(name) || "short".equals(name) || "char".equals(name); + } + + public static boolean isIntegral(TypeInfo type) { + if (type == null || !type.isResolved()) return false; + String name = type.getSimpleName(); + return "int".equals(name) || "long".equals(name) || "byte".equals(name) || + "short".equals(name) || "char".equals(name); + } + + public static boolean isFloatingPoint(TypeInfo type) { + if (type == null || !type.isResolved()) return false; + String name = type.getSimpleName(); + return "float".equals(name) || "double".equals(name); + } + + public static boolean isBoolean(TypeInfo type) { + if (type == null || !type.isResolved()) return false; + return "boolean".equals(type.getSimpleName()); + } + + public static boolean isString(TypeInfo type) { + if (type == null || !type.isResolved()) return false; + String name = type.getSimpleName(); + String fullName = type.getFullName(); + return "String".equals(name) || "java.lang.String".equals(fullName); + } + + public static TypeInfo binaryNumericPromotion(TypeInfo left, TypeInfo right) { + if (left == null || right == null || !left.isResolved() || !right.isResolved()) { + return null; + } + String l = left.getSimpleName(); + String r = right.getSimpleName(); + if ("double".equals(l) || "double".equals(r)) return TypeInfo.fromPrimitive("double"); + if ("float".equals(l) || "float".equals(r)) return TypeInfo.fromPrimitive("float"); + if ("long".equals(l) || "long".equals(r)) return TypeInfo.fromPrimitive("long"); + return TypeInfo.fromPrimitive("int"); + } + + public static TypeInfo unaryNumericPromotion(TypeInfo type) { + if (type == null || !type.isResolved()) return null; + String name = type.getSimpleName(); + if ("double".equals(name) || "float".equals(name) || "long".equals(name)) return type; + if ("byte".equals(name) || "short".equals(name) || "char".equals(name) || "int".equals(name)) { + return TypeInfo.fromPrimitive("int"); + } + return null; + } + + public static TypeInfo resolveBinaryOperatorType(OperatorType op, TypeInfo left, TypeInfo right) { + if (op == null) return null; + + switch (op.getCategory()) { + case ARITHMETIC: + if (op == OperatorType.ADD && (isString(left) || isString(right))) { + return TypeInfo.fromClass(String.class); + } + if (isNumeric(left) && isNumeric(right)) { + return binaryNumericPromotion(left, right); + } + return null; + + case RELATIONAL: + if (op == OperatorType.EQUALS || op == OperatorType.NOT_EQUALS) { + return TypeInfo.fromPrimitive("boolean"); + } + if (isNumeric(left) && isNumeric(right)) { + return TypeInfo.fromPrimitive("boolean"); + } + return null; + + case LOGICAL: + if (isBoolean(left) && isBoolean(right)) { + return TypeInfo.fromPrimitive("boolean"); + } + return null; + + case BITWISE: + if (op == OperatorType.LEFT_SHIFT || op == OperatorType.RIGHT_SHIFT || + op == OperatorType.UNSIGNED_RIGHT_SHIFT) { + if (isIntegral(left)) { + return unaryNumericPromotion(left); + } + } + if (isIntegral(left) && isIntegral(right)) { + return binaryNumericPromotion(left, right); + } + if (isBoolean(left) && isBoolean(right)) { + return TypeInfo.fromPrimitive("boolean"); + } + return null; + + case ASSIGNMENT: + return left; + + default: + return null; + } + } + + public static TypeInfo resolveUnaryOperatorType(OperatorType op, TypeInfo operand) { + if (op == null || operand == null || !operand.isResolved()) { + return null; + } + + switch (op) { + case UNARY_PLUS: + case UNARY_MINUS: + if (isNumeric(operand)) return unaryNumericPromotion(operand); + return null; + + case BITWISE_NOT: + if (isIntegral(operand)) return unaryNumericPromotion(operand); + return null; + + case LOGICAL_NOT: + if (isBoolean(operand)) return TypeInfo.fromPrimitive("boolean"); + return null; + + case PRE_INCREMENT: + case PRE_DECREMENT: + case POST_INCREMENT: + case POST_DECREMENT: + if (isNumeric(operand)) return operand; + return null; + + default: + return null; + } + } + + public static TypeInfo resolveTernaryType(TypeInfo thenType, TypeInfo elseType) { + if (thenType == null && elseType == null) return null; + if (thenType == null) return elseType; + if (elseType == null) return thenType; + if (!thenType.isResolved()) return elseType.isResolved() ? elseType : null; + if (!elseType.isResolved()) return thenType; + + if (thenType.equals(elseType)) return thenType; + if (isNumeric(thenType) && isNumeric(elseType)) { + return binaryNumericPromotion(thenType, elseType); + } + + return thenType; + } +}