Files
fabric-carpet/src/main/java/carpet/script/annotation/AnnotationParser.java
T
gnembon 529a5b358d Merge branch 'master' into future
# Conflicts:
#	gradle.properties
#	src/main/java/carpet/script/annotation/AnnotationParser.java
#	src/main/java/carpet/script/utils/BiomeInfo.java
#	src/main/resources/fabric.mod.json
2022-01-11 23:44:52 +01:00

322 lines
17 KiB
Java

package carpet.script.annotation;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.ClassUtils;
import carpet.CarpetExtension;
import carpet.script.Context;
import carpet.script.Expression;
import carpet.script.Fluff.AbstractLazyFunction;
import carpet.script.Fluff.TriFunction;
import carpet.script.Fluff.UsageProvider;
import carpet.script.exception.InternalExpressionException;
import carpet.script.LazyValue;
import carpet.script.value.Value;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
/**
* <p>This class parses methods annotated with the {@link ScarpetFunction} annotation in a given {@link Class}, generating
* fully-featured, automatically parsed and converted functions to be used in the Scarpet language.</p>
*
* <p>This class and the rest in this package will try to ensure that the annotated method receives the proper parameters
* directly, without all the always-repeated code of evaluating {@link Value}s, checking and casting them to their respective
* types, and converting them to the final needed object.</p>
*
* <p>To do that, functions will save a list of {@link ValueConverter}s to convert all their parameters. {@link ValueConverter}s
* are able to convert from any compatible {@link Value} instance into the requested parameter type, as long as they are registered
* using their respective {@code register} functions.</p>
*
* <p>Built-in {@link ValueConverter}s include but are not limited to converters to convert {@link List}s to actual Java lists while also
* converting every item inside of the {@link List} to the specified generic parameter ({@code <>}), with the same applying for maps</p>
*
* <p>Parameters can be given the annotations (present in the {@link Locator} and {@link Param} interfaces) in order to restrict them or
* make them more permissive to accept types, such as {@link Param.AllowSingleton} for lists.</p>
*
* <p>You can also declare optional parameters by using Java's {@link Optional} as the type of one of your parameters, though it must be
* at the end of the function, just before varargs if present and/or any other {@link Optional} parameters.</p>
*
* <p>Output of the annotated methods will also be converted to a compatible {@link LazyValue} using the registered {@link OutputConverter}s,
* allowing to remove the need of explicitly converting to a {@link Value} and then to a {@link LazyValue} just to end the method (though you can
* return a {@link LazyValue} if you want.</p>
*
* <p>For a variable argument count, the Java varargs notation can be used in the last parameter, converting the function into a variable argument
* function that will pass all the rest of parameters to that last varargs parameter, also converted into the specified type.</p>
*
* <p>To begin, use the {@link #parseFunctionClass(Class)} method in a class with methods annotated with the {@link ScarpetFunction} annotation.</p>
*
* @see ScarpetFunction
* @see Locator.Block
* @see Locator.Vec3d
* @see Param.Strict
* @see Param.AllowSingleton
* @see Param.KeyValuePairs
* @see Param.Custom
* @see Optional
* @see OutputConverter#registerToValue(Class, java.util.function.Function)
* @see OutputConverter#register(Class, java.util.function.Function)
* @see ValueCaster#register(Class, String)
* @see SimpleTypeConverter#registerType(Class, Class, java.util.function.Function, String)
* @see Param.Params#registerStrictConverter(Class, boolean, ValueConverter)
* @see Param.Params#registerCustomConverterFactory(java.util.function.BiFunction)
*/
public final class AnnotationParser
{
static final int UNDEFINED_PARAMS = -2;
private static final List<ParsedFunction> functionList = new ArrayList<>();
/**
* <p>Parses a given {@link Class} and registers its annotated methods, the ones with the {@link ScarpetFunction} annotation,
* to be used in the Scarpet language.</p>
*
* <p><b>Only call this method once per class per lifetime of the JVM!</b> (for example, at {@link CarpetExtension#onGameStarted()})</p>
*
* <p>There is a set of requirements for the class and its methods:</p>
* <ul>
* <li>Class must be concrete. That is, no interfaces or abstract classes should be passed</li>
* <li>Class must have the default constructor (or an equivalent) available. That is done in order to not need the {@code static} modifier in every
* method, making them faster to code and simpler to look at.</li>
* <li>Annotated methods must not throw checked exceptions. They can throw regular {@link RuntimeException}s (including but not limited to
* {@link InternalExpressionException}).
* Basically, it's fine as long as you don't add a {@code throws} declaration to your methods.</li>
* <li>Varargs (or effectively varargs) annotated methods must explicitly declare a maximum number of parameters to ingest in the {@link ScarpetFunction}
* annotation. They can still declare an unlimited amount by setting that maximum to {@link ScarpetFunction#UNLIMITED_PARAMS}. "Effectively varargs"
* means a function that has at least a parameter requiring a {@link ValueConverter} that has declared {@link ValueConverter#consumesVariableArgs()}.</li>
* <li>Annotated methods must not have a parameter with generics as the varargs parameter. This is just because it was painful for me (altrisi) and
* didn't want to support it. Those will crash with a {@code ClassCastException}</li>
* </ul>
*
* @see ScarpetFunction
* @param <T> The generic type of the class to parse.
* @param clazz The class to parse
*/
public static <T> void parseFunctionClass(Class<T> clazz)
{
if (Modifier.isAbstract(clazz.getModifiers()))
throw new IllegalArgumentException("Function class must be concrete! Class: " + clazz.getSimpleName());
T instance;
try
{
instance = clazz.getConstructor().newInstance();
}
catch (ReflectiveOperationException e)
{
throw new IllegalArgumentException(
"Couldn't create instance of given " + clazz + ". Make sure default constructor is available", e);
}
Method[] methodz = clazz.getDeclaredMethods();
for (Method method : methodz)
{
if (!method.isAnnotationPresent(ScarpetFunction.class))
continue;
if (method.getExceptionTypes().length != 0)
throw new IllegalArgumentException("Annotated method '" + method.getName() +"', provided in '"+clazz+"' must not declare checked exceptions");
ParsedFunction function = new ParsedFunction(method, instance);
functionList.add(function);
}
}
/**
* <p>Adds all parsed functions to the given {@link Expression}.</p>
* <p>This is handled automatically by Carpet</p>
*
* @param expr The expression to add every function to
*/
public static void apply(Expression expr)
{
for (ParsedFunction function : functionList)
expr.addLazyFunction(function.name, function.scarpetParamCount, function);
}
private static class ParsedFunction implements TriFunction<Context, Context.Type, List<LazyValue>, LazyValue>, UsageProvider
{
private final String name;
private final boolean isMethodVarArgs;
private final int methodParamCount;
private final List<ValueConverter<?>> valueConverters;
private final Class<?> varArgsType;
private final boolean primitiveVarArgs;
private final ValueConverter<?> varArgsConverter;
private final OutputConverter<Object> outputConverter;
private final boolean isEffectivelyVarArgs;
private final int minParams;
private final int maxParams;
private final MethodHandle handle;
private final int scarpetParamCount;
private final Context.Type contextType;
private ParsedFunction(final Method method, final Object instance)
{
this.name = method.getName();
this.isMethodVarArgs = method.isVarArgs();
this.methodParamCount = method.getParameterCount();
Parameter[] methodParameters = method.getParameters();
this.valueConverters = new ObjectArrayList<>();
for (int i = 0; i < this.methodParamCount; i++)
{
Parameter param = methodParameters[i];
if (!isMethodVarArgs || i != this.methodParamCount - 1) // Varargs converter is separate
this.valueConverters.add(ValueConverter.fromAnnotatedType(param.getAnnotatedType()));
}
Class<?> originalVarArgsType = isMethodVarArgs ? methodParameters[methodParamCount - 1].getType().getComponentType() : null;
this.varArgsType = ClassUtils.primitiveToWrapper(originalVarArgsType); // Primitive array cannot be cast to Obj[]
this.primitiveVarArgs = originalVarArgsType != null && originalVarArgsType.isPrimitive();
this.varArgsConverter = isMethodVarArgs ? ValueConverter.fromAnnotatedType(methodParameters[methodParamCount - 1].getAnnotatedType()) : null;
@SuppressWarnings("unchecked") // Yes. Making a T is not worth
OutputConverter<Object> converter = OutputConverter.get((Class<Object>) method.getReturnType());
this.outputConverter = converter;
this.isEffectivelyVarArgs = isMethodVarArgs ? true : valueConverters.stream().anyMatch(ValueConverter::consumesVariableArgs);
this.minParams = valueConverters.stream().mapToInt(ValueConverter::valueConsumption).sum(); // Note: In !varargs, this is params
int maxParams = this.minParams; // Unlimited == Integer.MAX_VALUE
if (this.isEffectivelyVarArgs)
{
maxParams = method.getAnnotation(ScarpetFunction.class).maxParams();
if (maxParams == UNDEFINED_PARAMS)
throw new IllegalArgumentException("No maximum number of params specified for " + name + ", use ScarpetFunction.UNLIMITED_PARAMS for unlimited. "
+ "Provided in " + instance.getClass());
if (maxParams == ScarpetFunction.UNLIMITED_PARAMS)
maxParams = Integer.MAX_VALUE;
if (maxParams < this.minParams)
throw new IllegalArgumentException("Provided maximum number of params for " + name + " is smaller than method's param count."
+ "Provided in " + instance.getClass());
}
this.maxParams = maxParams;
// Why MethodHandles?
// MethodHandles are blazing fast (in some situations they can even compile to a single invokeVirtual), but slightly complex to work with.
// Their "polymorphic signature" makes them (by default) require the exact signature of the method and return type, in order to call the
// functions directly, not even accepting Objects. Therefore we change them to spreaders (accept array instead), return type of Object,
// and we also bind them to our instance. That makes them ever-so-slightly slower, since they have to cast the params, but it's not
// noticeable, and comparing the overhead that reflection's #invoke had over this, the difference is substantial
// (checking access at invoke vs create).
// Note: there is also MethodHandle#invoke and #invokeWithArguments, but those run #asType at every invocation, which is quite slow, so we
// are basically running it here.
try
{
MethodHandle tempHandle = MethodHandles.publicLookup().unreflect(method).asFixedArity().asSpreader(Object[].class, this.methodParamCount);
tempHandle = tempHandle.asType(tempHandle.type().changeReturnType(Object.class));
this.handle = Modifier.isStatic(method.getModifiers()) ? tempHandle : tempHandle.bindTo(instance);
} catch (IllegalAccessException e)
{
throw new IllegalArgumentException(e);
}
this.scarpetParamCount = this.isEffectivelyVarArgs ? -1 : this.minParams;
this.contextType = method.getAnnotation(ScarpetFunction.class).contextType();
}
@Override
public LazyValue apply(Context context, Context.Type t, List<LazyValue> lazyValues)
{
List<Value> lv = AbstractLazyFunction.unpackLazy(lazyValues, context, contextType);
if (isEffectivelyVarArgs)
{
if (lv.size() < minParams)
throw new InternalExpressionException("Function '" + name + "' expected at least " + minParams + " arguments, got " + lv.size() + ". "
+ getUsage());
if (lv.size() > maxParams)
throw new InternalExpressionException("Function '" + name + " expected up to " + maxParams + " arguments, got " + lv.size() + ". "
+ getUsage());
} else
{
if (lv.size() != minParams) // min == max if not varargs. We do this cause we don't use function's arg checking to be able to return lazy
throw new InternalExpressionException("Function '" + name + "' expected " + minParams + " arguments, got " + lv.size() +". " + getUsage());
}
Object[] params = getMethodParams(lv, context, t);
try
{
return outputConverter.convert(handle.invokeExact(params));
}
catch (Throwable e)
{
if (e instanceof RuntimeException)
throw (RuntimeException) e;
throw (Error) e; // Stack overflow or something. Methods are guaranteed not to throw checked exceptions
}
}
// Hot code: Must be optimized
private Object[] getMethodParams(final List<Value> lv, final Context context, Context.Type theLazyT)
{
Object[] params = new Object[methodParamCount];
ListIterator<Value> lvIterator = lv.listIterator();
int regularArgs = isMethodVarArgs ? methodParamCount - 1 : methodParamCount;
for (int i = 0; i < regularArgs; i++)
{
params[i] = valueConverters.get(i).checkAndConvert(lvIterator, context, theLazyT);
if (params[i] == null)
throw new InternalExpressionException("Incorrect argument passsed to '" + name + "' function.\n" + getUsage());
}
if (isMethodVarArgs)
{
int remaining = lv.size() - lvIterator.nextIndex();
Object[] varArgs;
if (varArgsConverter.consumesVariableArgs())
{
List<Object> varArgsList = new ArrayList<>();
while (lvIterator.hasNext())
{
Object obj = varArgsConverter.checkAndConvert(lvIterator, context, theLazyT);
if (obj == null)
throw new InternalExpressionException("Incorrect argument passsed to '" + name + "' function.\n" + getUsage());
varArgsList.add(obj);
}
varArgs = varArgsList.toArray();
} else
{
varArgs = (Object[]) Array.newInstance(varArgsType, remaining / varArgsConverter.valueConsumption());
for (int i = 0; lvIterator.hasNext(); i++)
{
varArgs[i] = varArgsConverter.checkAndConvert(lvIterator, context, theLazyT);
if (varArgs[i] == null)
throw new InternalExpressionException("Incorrect argument passsed to '" + name + "' function.\n" + getUsage());
}
}
params[methodParamCount - 1] = primitiveVarArgs ? ArrayUtils.toPrimitive(varArgs) : varArgs; // Copies the array
}
return params;
}
@Override
public String getUsage()
{
// Possibility: More descriptive messages using param.getName()? Would need changing gradle setup to keep those
StringBuilder builder = new StringBuilder("Usage: '");
builder.append(name);
builder.append('(');
builder.append(valueConverters.stream().map(ValueConverter::getTypeName).filter(Objects::nonNull).collect(Collectors.joining(", ")));
if (varArgsConverter != null)
{
builder.append(", ");
builder.append(varArgsConverter.getTypeName());
builder.append("s...)");
} else
{
builder.append(')');
}
builder.append("'");
return builder.toString();
}
}
private AnnotationParser() {}
}