Skip to content

Commit

Permalink
Variable-arity returns and function contracts. (#6568)
Browse files Browse the repository at this point in the history
* Add function contracts.

* Use contract in place of reference.

* Add permissible single return support.

* Provide contract in signature during verification.

* Allow contract in function creation.

* No longer defer basic use to contract.

* Add contract to clamp function.

* Add tests for singular clamping.

* Update src/test/skript/tests/syntaxes/functions/clamp.sk

Co-authored-by: Patrick Miller <apickledwalrus@gmail.com>

* Sorry nice annotation, you're going to a "better" place :(

* Move stuff around in circles.

* Sovde made me change the assertions. :(

* Move everything (again)

* Add null annotation.

---------

Co-authored-by: Patrick Miller <apickledwalrus@gmail.com>
Co-authored-by: sovdee <10354869+sovdeeth@users.noreply.github.com>
  • Loading branch information
3 people committed May 9, 2024
1 parent 524dc18 commit 389dbf9
Show file tree
Hide file tree
Showing 9 changed files with 163 additions and 15 deletions.
23 changes: 18 additions & 5 deletions src/main/java/ch/njol/skript/classes/data/DefaultFunctions.java
Expand Up @@ -19,6 +19,7 @@
package ch.njol.skript.classes.data;

import ch.njol.skript.expressions.base.EventValueExpression;
import ch.njol.skript.lang.Expression;
import ch.njol.skript.lang.function.FunctionEvent;
import ch.njol.skript.lang.function.Functions;
import ch.njol.skript.lang.function.JavaFunction;
Expand All @@ -40,6 +41,7 @@
import org.bukkit.entity.Player;
import org.bukkit.util.Vector;
import org.eclipse.jdt.annotation.Nullable;
import ch.njol.skript.util.Contract;

import java.math.BigDecimal;
import java.math.RoundingMode;
Expand Down Expand Up @@ -309,11 +311,22 @@ public Number[] executeSimple(Object[][] params) {
.examples("min(1) = 1", "min(1, 2, 3, 4) = 1", "min({some list variable::*})")
.since("2.2"));

Functions.registerFunction(new SimpleJavaFunction<Number>("clamp", new Parameter[]{
new Parameter<>("values", DefaultClasses.NUMBER, false, null),
new Parameter<>("min", DefaultClasses.NUMBER, true, null),
new Parameter<>("max", DefaultClasses.NUMBER, true, null)
}, DefaultClasses.NUMBER, false) {
Functions.registerFunction(new SimpleJavaFunction<Number>("clamp", new Parameter[] {
new Parameter<>("values", DefaultClasses.NUMBER, false, null),
new Parameter<>("min", DefaultClasses.NUMBER, true, null),
new Parameter<>("max", DefaultClasses.NUMBER, true, null)
}, DefaultClasses.NUMBER, false, new Contract() {

@Override
public boolean isSingle(Expression<?>... arguments) {
return arguments[0].isSingle();
}

@Override
public Class<?> getReturnType(Expression<?>... arguments) {
return Number.class;
}
}) {
@Override
public @Nullable Number[] executeSimple(Object[][] params) {
Number[] values = (Number[]) params[0];
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/ch/njol/skript/effects/EffChange.java
Expand Up @@ -248,7 +248,7 @@ else if (mode == ChangeMode.SET)
assert x != null;
changer = ch = v;

if (!ch.isSingle() && single) {
if (!ch.canBeSingle() && single) {
if (mode == ChangeMode.SET)
Skript.error(changed + " can only be set to one " + Classes.getSuperClassInfo(x).getName() + ", not more", ErrorQuality.SEMANTIC_ERROR);
else
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/ch/njol/skript/lang/Expression.java
Expand Up @@ -120,6 +120,20 @@ default Optional<T> getOptionalSingle(Event event) {
*/
boolean isSingle();

/**
* Whether there's a possibility this could return a single value.
* Designed for expressions that may return more than one value, but are equally appropriate to use where
* only singular values are accepted, in which case a single value (out of all available values) will be returned.
* An example would be functions that return results based on their inputs.
* Ideally, this will return {@link #isSingle()} based on its known inputs at initialisation, but for some syntax
* this may not be known (or a syntax may be intentionally vague in its permissible returns).
* @return Whether this can be used by single changers
* @see #getSingle(Event)
*/
default boolean canBeSingle() {
return this.isSingle();
}

/**
* Checks this expression against the given checker. This is the normal version of this method and the one which must be used for simple checks,
* or as the innermost check of nested checks.
Expand Down
46 changes: 39 additions & 7 deletions src/main/java/ch/njol/skript/lang/function/FunctionReference.java
Expand Up @@ -33,6 +33,7 @@
import ch.njol.util.StringUtils;
import org.bukkit.event.Event;
import org.eclipse.jdt.annotation.Nullable;
import ch.njol.skript.util.Contract;

import java.util.ArrayList;
import java.util.Arrays;
Expand All @@ -41,7 +42,7 @@
/**
* Reference to a Skript function.
*/
public class FunctionReference<T> {
public class FunctionReference<T> implements Contract {

/**
* Name of function that is called, for logging purposes.
Expand Down Expand Up @@ -97,7 +98,13 @@ public class FunctionReference<T> {
*/
@Nullable
public final String script;


/**
* The contract for this function (typically the function reference itself).
* Used to determine input-based return types and simple behaviour.
*/
private Contract contract;

public FunctionReference(
String functionName, @Nullable Node node, @Nullable String script,
@Nullable Class<? extends T>[] returnTypes, Expression<?>[] params
Expand All @@ -106,7 +113,8 @@ public FunctionReference(
this.node = node;
this.script = script;
this.returnTypes = returnTypes;
parameters = params;
this.parameters = params;
this.contract = this;
}

public boolean validateParameterArity(boolean first) {
Expand Down Expand Up @@ -257,6 +265,10 @@ public boolean validateFunction(boolean first) {

signature = (Signature<? extends T>) sign;
sign.calls.add(this);

Contract contract = sign.getContract();
if (contract != null)
this.contract = contract;

return true;
}
Expand Down Expand Up @@ -310,21 +322,41 @@ protected T[] execute(Event e) {
// Execute the function
return function.execute(params);
}

public boolean isSingle() {
return contract.isSingle(parameters);
}

@Override
public boolean isSingle(Expression<?>... arguments) {
return single;
}

@Nullable
public Class<? extends T> getReturnType() {
//noinspection unchecked
return (Class<? extends T>) contract.getReturnType(parameters);
}

@Override
@Nullable
public Class<?> getReturnType(Expression<?>... arguments) {
if (signature == null)
throw new SkriptAPIException("Signature of function is null when return type is asked!");

@SuppressWarnings("ConstantConditions")
ClassInfo<? extends T> ret = signature.returnType;
return ret == null ? null : ret.getC();
}


/**
* The contract is used in preference to the function for determining return type, etc.
* @return The contract determining this function's parse-time hints, potentially this reference
*/
public Contract getContract() {
return contract;
}

public String toString(@Nullable Event e, boolean debug) {
StringBuilder b = new StringBuilder(functionName + "(");
for (int i = 0; i < parameters.length; i++) {
Expand Down
7 changes: 6 additions & 1 deletion src/main/java/ch/njol/skript/lang/function/JavaFunction.java
Expand Up @@ -21,6 +21,7 @@
import org.eclipse.jdt.annotation.Nullable;

import ch.njol.skript.classes.ClassInfo;
import ch.njol.skript.util.Contract;

/**
* @author Peter Güttinger
Expand All @@ -32,7 +33,11 @@ public JavaFunction(Signature<T> sign) {
}

public JavaFunction(String name, Parameter<?>[] parameters, ClassInfo<T> returnType, boolean single) {
this(new Signature<>("none", name, parameters, false, returnType, single, Thread.currentThread().getStackTrace()[3].getClassName()));
this(name, parameters, returnType, single, null);
}

public JavaFunction(String name, Parameter<?>[] parameters, ClassInfo<T> returnType, boolean single, @Nullable Contract contract) {
this(new Signature<>("none", name, parameters, false, returnType, single, Thread.currentThread().getStackTrace()[3].getClassName(), contract));
}

@Override
Expand Down
25 changes: 24 additions & 1 deletion src/main/java/ch/njol/skript/lang/function/Signature.java
Expand Up @@ -20,6 +20,7 @@

import ch.njol.skript.classes.ClassInfo;
import org.eclipse.jdt.annotation.Nullable;
import ch.njol.skript.util.Contract;

import java.util.Collection;
import java.util.Collections;
Expand Down Expand Up @@ -75,23 +76,40 @@ public class Signature<T> {
@Nullable
final String originClassPath;

/**
* An overriding contract for this function (e.g. to base its return on its arguments).
*/
@Nullable
final Contract contract;

public Signature(String script,
String name,
Parameter<?>[] parameters, boolean local,
@Nullable ClassInfo<T> returnType,
boolean single,
@Nullable String originClassPath) {
@Nullable String originClassPath,
@Nullable Contract contract) {
this.script = script;
this.name = name;
this.parameters = parameters;
this.local = local;
this.returnType = returnType;
this.single = single;
this.originClassPath = originClassPath;
this.contract = contract;

calls = Collections.newSetFromMap(new WeakHashMap<>());
}

public Signature(String script,
String name,
Parameter<?>[] parameters, boolean local,
@Nullable ClassInfo<T> returnType,
boolean single,
@Nullable String originClassPath) {
this(script, name, parameters, local, returnType, single, originClassPath, null);
}

public Signature(String script, String name, Parameter<?>[] parameters, boolean local, @Nullable ClassInfo<T> returnType, boolean single) {
this(script, name, parameters, local, returnType, single, null);
}
Expand Down Expand Up @@ -126,6 +144,11 @@ public String getOriginClassPath() {
return originClassPath;
}

@Nullable
public Contract getContract() {
return contract;
}

/**
* Gets maximum number of parameters that the function described by this
* signature is able to take.
Expand Down
Expand Up @@ -21,6 +21,7 @@
import org.eclipse.jdt.annotation.Nullable;

import ch.njol.skript.classes.ClassInfo;
import ch.njol.skript.util.Contract;

/**
* A {@link JavaFunction} which doesn't make use of
Expand All @@ -36,6 +37,10 @@ public SimpleJavaFunction(Signature<T> sign) {
public SimpleJavaFunction(String name, Parameter<?>[] parameters, ClassInfo<T> returnType, boolean single) {
super(name, parameters, returnType, single);
}

public SimpleJavaFunction(String name, Parameter<?>[] parameters, ClassInfo<T> returnType, boolean single, Contract contract) {
super(name, parameters, returnType, single, contract);
}

@SuppressWarnings("ConstantConditions")
@Nullable
Expand Down
44 changes: 44 additions & 0 deletions src/main/java/ch/njol/skript/util/Contract.java
@@ -0,0 +1,44 @@
/**
* 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.util;

import ch.njol.skript.lang.Expression;
import org.eclipse.jdt.annotation.Nullable;

/**
* The 'contract' of a function or another callable.
* This is a non-exhaustive helper for type hints, singularity, etc. that may change based on the arguments
* passed to a callable, in order for it to make better judgements on correct use at parse time.
*/
public interface Contract {

/**
* @return Whether, given these parameters, this will return a single value
* @see Expression#isSingle()
*/
boolean isSingle(Expression<?>... arguments);

/**
* @return What this will return, given these parameters
* @see Expression#getReturnType()
*/
@Nullable
Class<?> getReturnType(Expression<?>... arguments);

}
12 changes: 12 additions & 0 deletions src/test/skript/tests/syntaxes/functions/clamp.sk
Expand Up @@ -41,3 +41,15 @@ test "clamp numbers":
assert number within {_got::1} is not number within {_got::1} with "(edge cases list) NaN" # need within because the variables weren't cooperating
assert {_got::2} is {_expected::2} with "(edge cases list) -infinity"
assert {_got::3} is {_expected::3} with "(edge cases list) infinity"

test "clamp numbers (single)":
set {_expected::*} to (1, 0.0, and 2.0)
set {_got::*} to clamp((1, -infinity value, infinity value), 0.0, 2.0)
assert size of {_got::*} is 3 with "(multiple) expected"
loop 3 times:
assert {_got::%loop-number%} is {_expected::%loop-number%} with "(plural) expected %{_expected::%loop-number%}% found %{_got::%loop-number%}%"

# single store
set {_expected} to 2
set {_got} to clamp(0, 2, 5)
assert {_got} is {_expected} with "(single) expected"

0 comments on commit 389dbf9

Please sign in to comment.