Skip to content

Commit 6be9375

Browse files
Improve UnparsedLiteral Handling (#6360)
1 parent 5dd60e5 commit 6be9375

File tree

5 files changed

+234
-36
lines changed

5 files changed

+234
-36
lines changed

src/main/java/ch/njol/skript/expressions/arithmetic/ExprArithmetic.java

Lines changed: 151 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import ch.njol.skript.lang.ExpressionType;
2929
import ch.njol.skript.lang.Literal;
3030
import ch.njol.skript.lang.SkriptParser.ParseResult;
31+
import ch.njol.skript.lang.UnparsedLiteral;
3132
import ch.njol.skript.lang.util.SimpleExpression;
3233
import ch.njol.skript.lang.util.SimpleLiteral;
3334
import ch.njol.skript.registrations.Classes;
@@ -117,43 +118,151 @@ public PatternInfo(Operator operator, boolean leftGrouped, boolean rightGrouped)
117118
private boolean leftGrouped, rightGrouped;
118119

119120
@Override
120-
@SuppressWarnings({"ConstantConditions", "unchecked"})
121+
@SuppressWarnings({"ConstantConditions", "rawtypes", "unchecked"})
121122
public boolean init(Expression<?>[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) {
122-
first = LiteralUtils.defendExpression(exprs[0]);
123-
second = LiteralUtils.defendExpression(exprs[1]);
124-
125-
if (!LiteralUtils.canInitSafely(first, second))
126-
return false;
127-
128-
Class<? extends L> firstClass = first.getReturnType();
129-
Class<? extends R> secondClass = second.getReturnType();
123+
first = (Expression<L>) exprs[0];
124+
second = (Expression<R>) exprs[1];
130125

131126
PatternInfo patternInfo = patterns.getInfo(matchedPattern);
132127
leftGrouped = patternInfo.leftGrouped;
133128
rightGrouped = patternInfo.rightGrouped;
134129
operator = patternInfo.operator;
135130

136-
if (firstClass != Object.class && secondClass == Object.class && Arithmetics.getOperations(operator, firstClass).isEmpty()) {
137-
// If the first class is known but doesn't have any operations, then we fail
138-
return error(firstClass, secondClass);
139-
} else if (firstClass == Object.class && secondClass != Object.class && Arithmetics.getOperations(operator).stream()
140-
.noneMatch(info -> info.getRight().isAssignableFrom(secondClass))) {
141-
// If the second class is known but doesn't have any operations, then we fail
142-
return error(firstClass, secondClass);
131+
/*
132+
* Step 1: UnparsedLiteral Resolving
133+
*
134+
* Since Arithmetic may be performed on a variety of types, it is possible that 'first' or 'second'
135+
* will represent unparsed literals. That is, the parser could not determine what their literal contents represent.
136+
* Thus, it is now up to this expression to determine what they mean.
137+
*
138+
* If there are no unparsed literals, nothing happens at this step.
139+
* If there are unparsed literals, one of three possible execution flows will occur:
140+
*
141+
* Case 1. 'first' and 'second' are unparsed literals
142+
* In this case, there is not a lot of information to work with.
143+
* 'first' and 'second' are attempted to be converted to fit one of all operations using 'operator'.
144+
* If they cannot be matched with the types of a known operation, init will fail.
145+
*
146+
* Case 2. 'first' is an unparsed literal, 'second' is not
147+
* In this case, 'first' needs to be converted into the "left" type of
148+
* any operation using 'operator' with the type of 'second' as the right type.
149+
* If 'first' cannot be converted, init will fail.
150+
* If no operations are found for converting 'first', init will fail, UNLESS the type of 'second' is object,
151+
* where operations will be searched again later with the context of the type of first.
152+
* TODO When 'first' can represent multiple literals, it might be worth checking which of those can work with 'operator' and 'second'
153+
*
154+
* Case 3. 'second' is an unparsed literal, 'first' is not
155+
* In this case, 'second' needs to be converted into the "right" type of
156+
* any operation using 'operator' with the type of 'first' as the "left" type.
157+
* If 'second' cannot be converted, init will fail.
158+
* If no operations are found for converting 'second', init will fail, UNLESS the type of 'first' is object,
159+
* where operations will be searched again later with the context of the type of second.
160+
* TODO When 'second' can represent multiple literals, it might be worth checking which of those can work with 'first' and 'operator'
161+
*/
162+
163+
if (first instanceof UnparsedLiteral) {
164+
if (second instanceof UnparsedLiteral) { // first and second need converting
165+
for (OperationInfo<?, ?, ?> operation : Arithmetics.getOperations(operator)) {
166+
// match left type with 'first'
167+
Expression<?> convertedFirst = first.getConvertedExpression(operation.getLeft());
168+
if (convertedFirst == null)
169+
continue;
170+
// match right type with 'second'
171+
Expression<?> convertedSecond = second.getConvertedExpression(operation.getRight());
172+
if (convertedSecond == null)
173+
continue;
174+
// success, set the values
175+
first = (Expression<L>) convertedFirst;
176+
second = (Expression<R>) convertedSecond;
177+
returnType = (Class<? extends T>) operation.getReturnType();
178+
}
179+
} else { // first needs converting
180+
// attempt to convert <first> to types that make valid operations with <second>
181+
Class<?> secondClass = second.getReturnType();
182+
Class[] leftTypes = Arithmetics.getOperations(operator).stream()
183+
.filter(info -> info.getRight().isAssignableFrom(secondClass))
184+
.map(OperationInfo::getLeft)
185+
.toArray(Class[]::new);
186+
if (leftTypes.length == 0) { // no known operations with second's type
187+
if (secondClass != Object.class) // there won't be any operations
188+
return error(first.getReturnType(), secondClass);
189+
first = (Expression<L>) first.getConvertedExpression(Object.class);
190+
} else {
191+
first = (Expression<L>) first.getConvertedExpression(leftTypes);
192+
}
193+
}
194+
} else if (second instanceof UnparsedLiteral) { // second needs converting
195+
// attempt to convert <second> to types that make valid operations with <first>
196+
Class<?> firstClass = first.getReturnType();
197+
List<? extends OperationInfo<?, ?, ?>> operations = Arithmetics.getOperations(operator, firstClass);
198+
if (operations.isEmpty()) { // no known operations with first's type
199+
if (firstClass != Object.class) // there won't be any operations
200+
return error(firstClass, second.getReturnType());
201+
second = (Expression<R>) second.getConvertedExpression(Object.class);
202+
} else {
203+
second = (Expression<R>) second.getConvertedExpression(operations.stream()
204+
.map(OperationInfo::getRight)
205+
.toArray(Class[]::new)
206+
);
207+
}
143208
}
144209

145-
OperationInfo<L, R, T> operationInfo;
210+
if (!LiteralUtils.canInitSafely(first, second)) // checks if there are still unparsed literals present
211+
return false;
212+
213+
/*
214+
* Step 2: Return Type Calculation
215+
*
216+
* After the first step, everything that can be known about 'first' and 'second' during parsing is known.
217+
* As a result, it is time to determine the return type of the operation.
218+
*
219+
* If the types of 'first' or 'second' are object, it is possible that multiple operations with different return types
220+
* will be found. If that is the case, the supertype of these operations will be the return type (can be object).
221+
* If the types of both are object (e.g. variables), the return type will be object (have to wait until runtime and hope it works).
222+
* Of course, if no operations are found, init will fail.
223+
*
224+
* After these checks, it is safe to assume returnType has a value, as init should have failed by now if not.
225+
* One final check is performed specifically for numerical types.
226+
* Any numerical operation involving division or exponents have a return type of Double.
227+
* Other operations will also return Double, UNLESS 'first' and 'second' are of integer types, in which case the return type will be Long.
228+
*
229+
* If the types of both are something meaningful, the search for a registered operation commences.
230+
* If no operation can be found, init will fail.
231+
*/
232+
233+
Class<? extends L> firstClass = first.getReturnType();
234+
Class<? extends R> secondClass = second.getReturnType();
235+
146236
if (firstClass == Object.class || secondClass == Object.class) {
147-
// If either of the types is unknown, then we resolve the operation at runtime
148-
operationInfo = null;
149-
} else {
150-
operationInfo = (OperationInfo<L, R, T>) Arithmetics.lookupOperationInfo(operator, firstClass, secondClass);
151-
if (operationInfo == null) // We error if we couldn't find an operation between the two types
237+
// if either of the types is unknown, then we resolve the operation at runtime
238+
Class<?>[] returnTypes = null;
239+
if (!(firstClass == Object.class && secondClass == Object.class)) { // both aren't object
240+
if (firstClass == Object.class) {
241+
returnTypes = Arithmetics.getOperations(operator).stream()
242+
.filter(info -> info.getRight().isAssignableFrom(secondClass))
243+
.map(OperationInfo::getReturnType)
244+
.toArray(Class[]::new);
245+
} else { // secondClass is Object
246+
returnTypes = Arithmetics.getOperations(operator, firstClass).stream()
247+
.map(OperationInfo::getReturnType)
248+
.toArray(Class[]::new);
249+
}
250+
}
251+
if (returnTypes == null) { // both are object; can't determine anything
252+
returnType = (Class<? extends T>) Object.class;
253+
} else if (returnTypes.length == 0) { // one of the classes is known but doesn't have any operations
254+
return error(firstClass, secondClass);
255+
} else {
256+
returnType = (Class<? extends T>) Classes.getSuperClassInfo(returnTypes).getC();
257+
}
258+
} else if (returnType == null) { // lookup
259+
OperationInfo<L, R, T> operationInfo = (OperationInfo<L, R, T>) Arithmetics.lookupOperationInfo(operator, firstClass, secondClass);
260+
if (operationInfo == null) // we error if we couldn't find an operation between the two types
152261
return error(firstClass, secondClass);
262+
returnType = operationInfo.getReturnType();
153263
}
154264

155-
returnType = operationInfo == null ? (Class<? extends T>) Object.class : operationInfo.getReturnType();
156-
265+
// ensure proper return types for numerical operations
157266
if (Number.class.isAssignableFrom(returnType)) {
158267
if (operator == Operator.DIVISION || operator == Operator.EXPONENTIATION) {
159268
returnType = (Class<? extends T>) Double.class;
@@ -164,19 +273,31 @@ public boolean init(Expression<?>[] exprs, int matchedPattern, Kleenean isDelaye
164273
firstIsInt |= i.isAssignableFrom(first.getReturnType());
165274
secondIsInt |= i.isAssignableFrom(second.getReturnType());
166275
}
167-
168276
returnType = (Class<? extends T>) (firstIsInt && secondIsInt ? Long.class : Double.class);
169277
}
170278
}
171279

172-
// Chaining
173-
if (first instanceof ExprArithmetic && !leftGrouped) {
280+
/*
281+
* Step 3: Chaining and Parsing
282+
*
283+
* This step builds the arithmetic chain that will be parsed into an ordered operation to be executed at runtime.
284+
* With larger operations, it is possible that 'first' or 'second' will be instances of ExprArithmetic.
285+
* As a result, their chains need to be incorporated into this instance's chain.
286+
* This is to ensure that, during parsing, a "gettable" that follows the order of operations is built.
287+
* However, in the case of parentheses, the chains will not be combined as the
288+
* order of operations dictates that the result of that chain be determined first.
289+
*
290+
* The chain (a list of values and operators) will then be parsed into a "gettable" that
291+
* can be evaluated during runtime for a final result.
292+
*/
293+
294+
if (first instanceof ExprArithmetic && !leftGrouped) { // combine chain of 'first' if we do not have parentheses
174295
chain.addAll(((ExprArithmetic<?, ?, L>) first).chain);
175296
} else {
176297
chain.add(first);
177298
}
178299
chain.add(operator);
179-
if (second instanceof ExprArithmetic && !rightGrouped) {
300+
if (second instanceof ExprArithmetic && !rightGrouped) { // combine chain of 'second' if we do not have parentheses
180301
chain.addAll(((ExprArithmetic<?, ?, R>) second).chain);
181302
} else {
182303
chain.add(second);
@@ -197,7 +318,8 @@ protected T[] get(Event event) {
197318

198319
private boolean error(Class<?> firstClass, Class<?> secondClass) {
199320
ClassInfo<?> first = Classes.getSuperClassInfo(firstClass), second = Classes.getSuperClassInfo(secondClass);
200-
Skript.error(operator.getName() + " can't be performed on " + first.getName().withIndefiniteArticle() + " and " + second.getName().withIndefiniteArticle());
321+
if (first.getC() != Object.class && second.getC() != Object.class) // errors with "object" are not very useful and often misleading
322+
Skript.error(operator.getName() + " can't be performed on " + first.getName().withIndefiniteArticle() + " and " + second.getName().withIndefiniteArticle());
201323
return false;
202324
}
203325

src/main/java/ch/njol/skript/log/ParseLogHandler.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@
1818
*/
1919
package ch.njol.skript.log;
2020

21-
import ch.njol.skript.Skript;
2221
import org.eclipse.jdt.annotation.Nullable;
22+
import org.jetbrains.annotations.ApiStatus;
23+
import org.jetbrains.annotations.Contract;
2324

2425
import java.util.ArrayList;
2526
import java.util.List;
@@ -31,6 +32,29 @@ public class ParseLogHandler extends LogHandler {
3132
private LogEntry error = null;
3233

3334
private final List<LogEntry> log = new ArrayList<>();
35+
36+
/**
37+
* Internal method for creating a backup of this log.
38+
* @return A new ParseLogHandler containing the contents of this ParseLogHandler.
39+
*/
40+
@ApiStatus.Internal
41+
@Contract("-> new")
42+
public ParseLogHandler backup() {
43+
ParseLogHandler copy = new ParseLogHandler();
44+
copy.error = this.error;
45+
copy.log.addAll(this.log);
46+
return copy;
47+
}
48+
49+
/**
50+
* Internal method for restoring a backup of this log.
51+
*/
52+
@ApiStatus.Internal
53+
public void restore(ParseLogHandler parseLogHandler) {
54+
this.error = parseLogHandler.error;
55+
this.log.clear();
56+
this.log.addAll(parseLogHandler.log);
57+
}
3458

3559
@Override
3660
public LogResult log(LogEntry entry) {

src/main/java/ch/njol/skript/patterns/TypePatternElement.java

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import ch.njol.skript.lang.Literal;
2525
import ch.njol.skript.lang.SkriptParser;
2626
import ch.njol.skript.lang.SkriptParser.ExprInfo;
27+
import ch.njol.skript.lang.UnparsedLiteral;
2728
import ch.njol.skript.lang.parser.ParserInstance;
2829
import ch.njol.skript.log.ErrorQuality;
2930
import ch.njol.skript.log.ParseLogHandler;
@@ -138,6 +139,10 @@ public MatchResult match(String expr, MatchResult matchResult) {
138139

139140
ExprInfo exprInfo = getExprInfo();
140141

142+
MatchResult matchBackup = null;
143+
ParseLogHandler loopLogHandlerBackup = null;
144+
ParseLogHandler expressionLogHandlerBackup = null;
145+
141146
ParseLogHandler loopLogHandler = SkriptLogger.startParseLogHandler();
142147
try {
143148
while (newExprOffset != -1) {
@@ -168,11 +173,37 @@ public MatchResult match(String expr, MatchResult matchResult) {
168173
}
169174
}
170175

171-
expressionLogHandler.printLog();
172-
loopLogHandler.printLog();
173-
174176
newMatchResult.expressions[expressionIndex] = expression;
175-
return newMatchResult;
177+
178+
/*
179+
* the parser will return unparsed literals in cases where it cannot interpret an input and object is the desired return type.
180+
* in those cases, it is up to the expression to interpret the input.
181+
* however, this presents a problem for input that is not intended as being one of these object-accepting expressions.
182+
* these object-accepting expressions will be matched instead but their parsing will fail as they cannot interpret the unparsed literals.
183+
* even though it can't interpret them, this loop will have returned a match and thus parsing has ended (and the correct interpretation never attempted).
184+
* to avoid this issue, while also permitting unparsed literals in cases where they are justified,
185+
* the code below forces the loop to continue in hopes of finding a match without unparsed literals.
186+
* if it is unsuccessful, a backup of the first successful match (with unparsed literals) is saved to be returned.
187+
*/
188+
boolean hasUnparsedLiteral = false;
189+
for (int i = expressionIndex + 1; i < newMatchResult.expressions.length; i++) {
190+
if (newMatchResult.expressions[i] instanceof UnparsedLiteral) {
191+
hasUnparsedLiteral = Classes.parse(((UnparsedLiteral) newMatchResult.expressions[i]).getData(), Object.class, newMatchResult.parseContext) == null;
192+
if (hasUnparsedLiteral) {
193+
break;
194+
}
195+
}
196+
}
197+
198+
if (!hasUnparsedLiteral) {
199+
expressionLogHandler.printLog();
200+
loopLogHandler.printLog();
201+
return newMatchResult;
202+
} else if (matchBackup == null) { // only backup the first occurrence of unparsed literals
203+
matchBackup = newMatchResult;
204+
loopLogHandlerBackup = loopLogHandler.backup();
205+
expressionLogHandlerBackup = expressionLogHandler.backup();
206+
}
176207
}
177208
} finally {
178209
expressionLogHandler.printError();
@@ -193,11 +224,19 @@ public MatchResult match(String expr, MatchResult matchResult) {
193224
}
194225
}
195226
} finally {
196-
if (!loopLogHandler.isStopped())
227+
if (loopLogHandlerBackup != null) { // print backup logs if applicable
228+
loopLogHandler.restore(loopLogHandlerBackup);
229+
assert expressionLogHandlerBackup != null;
230+
expressionLogHandlerBackup.printLog();
231+
}
232+
if (!loopLogHandler.isStopped()) {
197233
loopLogHandler.printError();
234+
}
198235
}
199236

200-
return null;
237+
// if there were unparsed literals, we will return the backup now
238+
// if there were not, this returns null
239+
return matchBackup;
201240
}
202241

203242
@Override
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
test "invalid unparsed literals":
2+
3+
parse:
4+
loop 9 times:
5+
set {_i} to loop-value - 1
6+
assert last parse logs is not set with "skript should be able to understand 'loop-value - 1' but did not"

src/test/skript/tests/syntaxes/expressions/ExprArithmetic.sk

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,3 +255,10 @@ local function component_equals(a: number, b: number) :: boolean:
255255
then:
256256
return true
257257
return true if {_a} is {_b}, else false
258+
259+
test "arithmetic return types":
260+
# the issue below is that <none> is now returned for the function
261+
# skript interprets this as "y-coordinate of ({_location} - 4)" which is valid due to Object.class return types
262+
# however, we can get more specific return types by returning the superclass of the return types of all Object-Number operations
263+
set {_location} to location(0,10,0,"world")
264+
assert (y-coordinate of {_location} - 4) is 6 with "y-coordinate of {_location} - 4 is not 6 (got '%y-coordinate of {_location} - 4%')"

0 commit comments

Comments
 (0)