Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Compound Conditionals API #7057

Open
wants to merge 21 commits into
base: dev/feature
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/main/java/ch/njol/skript/lang/Condition.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@
import ch.njol.skript.Skript;
import ch.njol.skript.lang.util.SimpleExpression;
import ch.njol.util.Checker;
import ch.njol.util.Kleenean;
import org.bukkit.event.Event;
import org.jetbrains.annotations.Nullable;
import org.skriptlang.skript.lang.condition.Conditional;

import java.util.Iterator;

Expand All @@ -31,7 +33,7 @@
*
* @see Skript#registerCondition(Class, String...)
*/
public abstract class Condition extends Statement {
public abstract class Condition extends Statement implements Conditional<Event> {

public enum ConditionType {
/**
Expand Down Expand Up @@ -67,6 +69,11 @@ protected Condition() {}
*/
public abstract boolean check(Event event);

@Override
public Kleenean evaluate(Event event) {
return Kleenean.get(check(event));
}

@Override
public final boolean run(Event event) {
return check(event);
Expand Down
96 changes: 36 additions & 60 deletions src/main/java/ch/njol/skript/sections/SecConditional.java
Original file line number Diff line number Diff line change
@@ -1,21 +1,3 @@
/**
* This file is part of Skript.
*
* Skript is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Skript is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Skript. If not, see <http://www.gnu.org/licenses/>.
*
* Copyright Peter Güttinger, SkriptLang team and contributors
*/
package ch.njol.skript.sections;

import ch.njol.skript.ScriptLoader;
Expand All @@ -37,6 +19,9 @@
import com.google.common.collect.Iterables;
import org.bukkit.event.Event;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.UnknownNullability;
import org.skriptlang.skript.lang.condition.Conditional;
import org.skriptlang.skript.lang.condition.Conditional.Operator;

import java.util.ArrayList;
import java.util.Iterator;
Expand Down Expand Up @@ -68,7 +53,6 @@
""
})
@Since("1.0")
@SuppressWarnings("NotNullFieldNotInitialized")
public class SecConditional extends Section {

private static final SkriptPattern THEN_PATTERN = PatternCompiler.compile("then [run]");
Expand All @@ -93,7 +77,7 @@ private enum ConditionalType {
}

private ConditionalType type;
private List<Condition> conditions = new ArrayList<>();
private @UnknownNullability Conditional<Event> conditional;
private boolean ifAny;
private boolean parseIf;
private boolean parseIfPassed;
Expand All @@ -112,7 +96,7 @@ public boolean init(Expression<?>[] exprs,
type = CONDITIONAL_PATTERNS.getInfo(matchedPattern);
ifAny = parseResult.hasTag("any");
parseIf = parseResult.hasTag("parse");
multiline = parseResult.regexes.size() == 0 && type != ConditionalType.ELSE;
multiline = parseResult.regexes.isEmpty() && type != ConditionalType.ELSE;
ParserInstance parser = getParser();

// ensure this conditional is chained correctly (e.g. an else must have an if)
Expand Down Expand Up @@ -168,6 +152,8 @@ public boolean init(Expression<?>[] exprs,
Class<? extends Event>[] currentEvents = parser.getCurrentEvents();
String currentEventName = parser.getCurrentEventName();

List<Conditional<Event>> conditionals = new ArrayList<>();

// Change event if using 'parse if'
if (parseIf) {
//noinspection unchecked
Expand Down Expand Up @@ -196,7 +182,7 @@ public boolean init(Expression<?>[] exprs,
// if this condition was invalid, don't bother parsing the rest
if (condition == null)
return false;
conditions.add(condition);
conditionals.add(condition);
}
}
parser.setNode(sectionNode);
Expand All @@ -206,16 +192,18 @@ public boolean init(Expression<?>[] exprs,
// Don't print a default error if 'if' keyword wasn't provided
Condition condition = Condition.parse(expr, parseResult.hasTag("implicit") ? null : "Can't understand this condition: '" + expr + "'");
if (condition != null)
conditions.add(condition);
conditionals.add(condition);
}

if (parseIf) {
parser.setCurrentEvents(currentEvents);
parser.setCurrentEventName(currentEventName);
}

if (conditions.isEmpty())
if (conditionals.isEmpty())
return false;

conditional = Conditional.compound(ifAny ? Operator.OR : Operator.AND, conditionals);
}

// ([else] parse if) If condition is valid and false, do not parse the section
Expand Down Expand Up @@ -289,14 +277,12 @@ && getElseIfs(triggerItems).stream().map(SecConditional::getHasDelayAfter).allMa
}

@Override
@Nullable
public TriggerItem getNext() {
public @Nullable TriggerItem getNext() {
return getSkippedNext();
}

@Nullable
@Override
protected TriggerItem walk(Event event) {
protected @Nullable TriggerItem walk(Event event) {
if (type == ConditionalType.THEN || (parseIf && !parseIfPassed)) {
return getActualNext();
} else if (parseIf || checkConditions(event)) {
Expand Down Expand Up @@ -327,30 +313,28 @@ public ExecutionIntent triggerExecutionIntent() {
@Nullable
private TriggerItem getSkippedNext() {
TriggerItem next = getActualNext();
while (next instanceof SecConditional && ((SecConditional) next).type != ConditionalType.IF)
while (next instanceof SecConditional nextSecCond && nextSecCond.type != ConditionalType.IF)
next = next.getActualNext();
return next;
}

@Override
public String toString(@Nullable Event event, boolean debug) {
String parseIf = this.parseIf ? "parse " : "";
switch (type) {
case IF:
return switch (type) {
case IF -> {
if (multiline)
return parseIf + "if " + (ifAny ? "any" : "all");
return parseIf + "if " + conditions.get(0).toString(event, debug);
case ELSE_IF:
yield parseIf + "if " + (ifAny ? "any" : "all");
yield parseIf + "if " + conditional.toString(event, debug);
}
case ELSE_IF -> {
if (multiline)
return "else " + parseIf + "if " + (ifAny ? "any" : "all");
return "else " + parseIf + "if " + conditions.get(0).toString(event, debug);
case ELSE:
return "else";
case THEN:
return "then";
default:
throw new IllegalStateException();
}
yield "else " + parseIf + "if " + (ifAny ? "any" : "all");
yield "else " + parseIf + "if " + conditional.toString(event, debug);
}
case ELSE -> "else";
case THEN -> "then";
};
}

private Kleenean getHasDelayAfter() {
Expand All @@ -363,19 +347,18 @@ private Kleenean getHasDelayAfter() {
* @param type the type of conditional section to find. if null is provided, any type is allowed.
* @return the closest conditional section
*/
@Nullable
private static SecConditional getPrecedingConditional(List<TriggerItem> triggerItems, @Nullable ConditionalType type) {
private static @Nullable SecConditional getPrecedingConditional(List<TriggerItem> triggerItems, @Nullable ConditionalType type) {
// loop through the triggerItems in reverse order so that we find the most recent items first
for (int i = triggerItems.size() - 1; i >= 0; i--) {
TriggerItem triggerItem = triggerItems.get(i);
if (triggerItem instanceof SecConditional conditionalSection) {
if (conditionalSection.type == ConditionalType.ELSE) {
if (triggerItem instanceof SecConditional precedingSecConditional) {
if (precedingSecConditional.type == ConditionalType.ELSE) {
// if the conditional is an else, return null because it belongs to a different condition and ends
// this one
return null;
} else if (type == null || conditionalSection.type == type) {
} else if (type == null || precedingSecConditional.type == type) {
// if the conditional matches the type argument, we found our most recent preceding conditional section
return conditionalSection;
return precedingSecConditional;
}
} else {
return null;
Expand Down Expand Up @@ -404,8 +387,8 @@ private static List<SecConditional> getElseIfs(List<TriggerItem> triggerItems) {
List<SecConditional> list = new ArrayList<>();
for (int i = triggerItems.size() - 1; i >= 0; i--) {
TriggerItem triggerItem = triggerItems.get(i);
if (triggerItem instanceof SecConditional secConditional && secConditional.type == ConditionalType.ELSE_IF) {
list.add(secConditional);
if (triggerItem instanceof SecConditional precedingSecConditional && precedingSecConditional.type == ConditionalType.ELSE_IF) {
list.add(precedingSecConditional);
} else {
break;
}
Expand All @@ -414,17 +397,10 @@ private static List<SecConditional> getElseIfs(List<TriggerItem> triggerItems) {
}

private boolean checkConditions(Event event) {
if (conditions.isEmpty()) { // else and then
return true;
} else if (ifAny) {
return conditions.stream().anyMatch(c -> c.check(event));
} else {
return conditions.stream().allMatch(c -> c.check(event));
}
return conditional == null || conditional.evaluate(event).isTrue();
}

@Nullable
private Node getNextNode(Node precedingNode, ParserInstance parser) {
private @Nullable Node getNextNode(Node precedingNode, ParserInstance parser) {
// iterating over the parent node causes the current node to change, so we need to store it to reset it later
Node originalCurrentNode = parser.getNode();
SectionNode parentNode = precedingNode.getParent();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package org.skriptlang.skript.lang.condition;

import ch.njol.util.Kleenean;
import com.google.common.base.Preconditions;
import org.bukkit.event.Event;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;

import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
* A {@link Conditional} that is built of other {@link Conditional}s.
* It is composed of an ordered {@link Set} of {@link Conditional}s that are acted upon by a single {@link Operator}.
* @param <T> The context class to use for evaluation.
* @see DNFConditionalBuilder
*/
class CompoundConditional<T> implements Conditional<T> {

private final LinkedHashSet<Conditional<T>> componentConditionals = new LinkedHashSet<>();
private final Operator operator;

/**
* Whether a cache will be used during evaluation. True if this compound contains other compounds, otherwise false.
*/
private boolean useCache;

/**
* @param operator The {@link Operator} to use to combine the conditionals. If {@link Operator#NOT} is used,
* {@code conditionals} must exactly 1 conditional.
* @param conditionals A collection of conditionals to combine using the operator. Must be >= 1 in length,
* or exactly 1 if {@link Operator#NOT} is used.
*/
CompoundConditional(Operator operator, @NotNull Collection<Conditional<T>> conditionals) {
Preconditions.checkArgument(!conditionals.isEmpty(),
"CompoundConditionals must contain at least 1 component conditional.");
Preconditions.checkArgument(!(operator == Operator.NOT && conditionals.size() != 1),
"The NOT operator cannot be applied to multiple Conditionals.");

this.componentConditionals.addAll(conditionals);
useCache = conditionals.stream().anyMatch(cond -> cond instanceof CompoundConditional);
this.operator = operator;
}

/**
* @param operator The {@link Operator} to use to combine the conditionals. If {@link Operator#NOT} is used,
* {@code conditionals} must exactly 1 conditional.
* @param conditionals Conditionals to combine using the operator. Must be >= 1 in length,
* or exactly 1 if {@link Operator#NOT} is used.
*/
@SafeVarargs
CompoundConditional(Operator operator, Conditional<T>... conditionals) {
this(operator, List.of(conditionals));
}

@Override
public Kleenean evaluate(T context) {
Map<Conditional<T>, Kleenean> cache = null;
// only use overhead of a cache if we think it will be useful (stacked conditionals)
if (useCache)
cache = new HashMap<>();
return evaluate(context, cache);
}

@Override
public Kleenean evaluate(T context, Map<Conditional<T>, Kleenean> cache) {
Kleenean result;
return switch (operator) {
case OR -> {
result = Kleenean.FALSE;
for (Conditional<T> conditional : componentConditionals) {
result = conditional.evaluateOr(result, context, cache);
}
yield result;
}
case AND -> {
result = Kleenean.TRUE;
for (Conditional<T> conditional : componentConditionals) {
result = conditional.evaluateAnd(result, context, cache);
}
yield result;
}
case NOT -> {
if (componentConditionals.size() > 1)
throw new IllegalStateException("Cannot apply NOT to multiple conditionals! Cannot evaluate.");
// best workaround since getFirst is java 21
for (Conditional<T> conditional : componentConditionals) {
yield conditional.evaluate(context, cache).not();
}
throw new IllegalStateException("Cannot apply NOT to zero conditionals! Cannot evaluate.");
}
};
}

/**
* @return An immutable list of the component conditionals of this object.
*/
public @Unmodifiable List<Conditional<T>> getConditionals() {
return componentConditionals.stream().toList();
}

/**
* @return The operator used in this object.
*/
public Operator getOperator() {
return operator;
}

/**
* @param conditionals Adds more conditionals to this object's component conditionals.
*/
@SafeVarargs
public final void addConditionals(Conditional<T>... conditionals) {
addConditionals(List.of(conditionals));
}

/**
* @param conditionals Adds more conditionals to this object's component conditionals.
*/
public void addConditionals(Collection<Conditional<T>> conditionals) {
componentConditionals.addAll(conditionals);
useCache |= conditionals.stream().anyMatch(cond -> cond instanceof CompoundConditional);
}

//TODO: replace event with context object in debuggable rework pr
@Override
public String toString(@Nullable Event event, boolean debug) {
if (operator == Operator.NOT)
return "!" + joinConditionals(event, debug);
return "(" + joinConditionals(event, debug) + ")";
}

private String joinConditionals(@Nullable Event event, boolean debug) {
return componentConditionals.stream()
.map(conditional -> conditional.toString(event, debug))
.collect(Collectors.joining(" " + operator.getSymbol() + " "));
}

@Override
public String toString() {
return toString(null, false);
}
sovdeeth marked this conversation as resolved.
Show resolved Hide resolved

}
Loading