diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/Profile.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/Profile.java index 8bb2977bbe..92aed92868 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/Profile.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/Profile.java @@ -7,6 +7,7 @@ import com.onthegomap.planetiler.reader.osm.OsmRelationInfo; import com.onthegomap.planetiler.util.Wikidata; import java.util.List; +import java.util.Locale; import java.util.function.Consumer; /** @@ -114,7 +115,9 @@ default List postProcessLayerFeatures(String layer, int zoom * * @see MBTiles specification */ - String name(); + default String name() { + return getClass().getSimpleName().toLowerCase(Locale.ROOT); + } /** * Returns the description of the generated tileset to put into {@link Mbtiles} metadata diff --git a/planetiler-core/src/test/java/com/onthegomap/planetiler/PlanetilerTests.java b/planetiler-core/src/test/java/com/onthegomap/planetiler/PlanetilerTests.java index d1224780f3..ade78a6639 100644 --- a/planetiler-core/src/test/java/com/onthegomap/planetiler/PlanetilerTests.java +++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/PlanetilerTests.java @@ -204,18 +204,6 @@ private PlanetilerResults runWithReaderFeaturesProfile( ); } - private PlanetilerResults runWithOsmElements( - Map args, - List features, - BiConsumer profileFunction - ) throws Exception { - return run( - args, - (featureGroup, profile, config) -> processOsmFeatures(featureGroup, profile, config, features), - TestProfile.processSourceFeatures(profileFunction) - ); - } - private PlanetilerResults runWithOsmElements( Map args, List features, diff --git a/planetiler-dist/src/main/java/com/onthegomap/planetiler/Main.java b/planetiler-dist/src/main/java/com/onthegomap/planetiler/Main.java index 0ffb7e72c0..3ce174b325 100644 --- a/planetiler-dist/src/main/java/com/onthegomap/planetiler/Main.java +++ b/planetiler-dist/src/main/java/com/onthegomap/planetiler/Main.java @@ -10,6 +10,7 @@ import com.onthegomap.planetiler.examples.OsmQaTiles; import com.onthegomap.planetiler.examples.ToiletsOverlay; import com.onthegomap.planetiler.examples.ToiletsOverlayLowLevelApi; +import com.onthegomap.planetiler.experimental.lua.GenerateLuaTypes; import com.onthegomap.planetiler.experimental.lua.LuaMain; import com.onthegomap.planetiler.experimental.lua.LuaValidator; import com.onthegomap.planetiler.mbtiles.Verify; @@ -46,6 +47,7 @@ public class Main { entry("custom", ConfiguredMapMain::main), entry("lua", LuaMain::main), + entry("lua-types", GenerateLuaTypes::main), entry("generate-shortbread", bundledSchema("shortbread.yml")), entry("shortbread", bundledSchema("shortbread.yml")), diff --git a/planetiler-experimental/src/main/java/com/onthegomap/planetiler/experimental/lua/GenerateLuaTypes.java b/planetiler-experimental/src/main/java/com/onthegomap/planetiler/experimental/lua/GenerateLuaTypes.java new file mode 100644 index 0000000000..6175383252 --- /dev/null +++ b/planetiler-experimental/src/main/java/com/onthegomap/planetiler/experimental/lua/GenerateLuaTypes.java @@ -0,0 +1,509 @@ +package com.onthegomap.planetiler.experimental.lua; + +import static java.util.Map.entry; + +import com.google.common.base.CaseFormat; +import com.google.common.base.Converter; +import com.google.common.reflect.Invokable; +import com.google.common.reflect.TypeToken; +import com.onthegomap.planetiler.util.Format; +import java.lang.reflect.Executable; +import java.lang.reflect.Field; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.RecordComponent; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Deque; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.stream.Collectors; +import org.luaj.vm2.LuaDouble; +import org.luaj.vm2.LuaInteger; +import org.luaj.vm2.LuaNumber; +import org.luaj.vm2.LuaString; +import org.luaj.vm2.LuaTable; +import org.luaj.vm2.LuaValue; +import org.luaj.vm2.lib.jse.LuaBindMethods; +import org.luaj.vm2.lib.jse.LuaFunctionType; +import org.luaj.vm2.lib.jse.LuaGetter; +import org.luaj.vm2.lib.jse.LuaSetter; +import org.luaj.vm2.lib.jse.LuaType; + +/** + * Generates a lua file with type definitions for the lua environment exposed by planetiler. + * + *
+ * java -jar planetiler.jar lua-types > types.lua
+ * 
+ * + * @see Lua Language Server type annotations + */ +public class GenerateLuaTypes { + private static final Map, String> TYPE_NAMES = Map.ofEntries( + entry(Object.class, "any"), + entry(LuaInteger.class, "integer"), + entry(LuaDouble.class, "number"), + entry(LuaNumber.class, "number"), + entry(LuaString.class, "string"), + entry(LuaTable.class, "table"), + entry(Class.class, "userdata"), + entry(String.class, "string"), + entry(Number.class, "number"), + entry(byte[].class, "string"), + entry(Integer.class, "integer"), + entry(int.class, "integer"), + entry(Long.class, "integer"), + entry(long.class, "integer"), + entry(Short.class, "integer"), + entry(short.class, "integer"), + entry(Byte.class, "integer"), + entry(byte.class, "integer"), + entry(Double.class, "number"), + entry(double.class, "number"), + entry(Float.class, "number"), + entry(float.class, "number"), + entry(boolean.class, "boolean"), + entry(Boolean.class, "boolean"), + entry(Void.class, "nil"), + entry(void.class, "nil") + ); + private static final Converter CAMEL_TO_SNAKE_CASE = + CaseFormat.LOWER_CAMEL.converterTo(CaseFormat.LOWER_UNDERSCORE); + private static final TypeToken> LIST_TYPE = new TypeToken<>() {}; + private static final TypeToken> MAP_TYPE = new TypeToken<>() {}; + private final Deque debugStack = new LinkedList<>(); + private final Set handled = new HashSet<>(); + private final StringBuilder builder = new StringBuilder(); + + GenerateLuaTypes() { + write(""" + ---@meta + local types = {} + """); + } + + public static void main(String[] args) { + var generator = new GenerateLuaTypes().generatePlanetiler(); + System.out.println(generator); + } + + private static String luaClassName(Class clazz) { + return clazz.getName().replaceAll("[\\$\\.]", "_"); + } + + private static boolean differentFromParents2(Invokable invokable, TypeToken superType) { + if (!invokable.getReturnType().equals(superType.resolveType(invokable.getReturnType().getType()))) { + return true; + } + var orig = + invokable.getParameters().stream() + .map(t -> invokable.getOwnerType().resolveType(t.getType().getType())) + .toList(); + var resolved = invokable.getParameters().stream().map(t -> superType.resolveType(t.getType().getType())).toList(); + return !orig.equals(resolved); + } + + private static boolean hasMethod(Class clazz, Method method) { + try { + clazz.getMethod(method.getName(), method.getParameterTypes()); + return true; + } catch (NoSuchMethodException e) { + return false; + } + } + + private static String transformMemberName(String fieldName) { + if (!fieldName.contains("_") && fieldName.matches("^.*[A-Z].*$") && fieldName.matches("^.*[a-z].*$")) { + fieldName = CAMEL_TO_SNAKE_CASE.convert(fieldName); + } + if (LuaConversions.LUA_AND_NOT_JAVA_KEYWORDS.contains(fieldName)) { + fieldName = fieldName.toUpperCase(Locale.ROOT); + } + return fieldName; + } + + private void write(String line) { + builder.append(line).append("\n"); + } + + GenerateLuaTypes generatePlanetiler() { + exportGlobalInstance("planetiler", LuaEnvironment.PlanetilerNamespace.class); + for (var clazz : LuaEnvironment.CLASSES_TO_EXPOSE) { + exportGlobalType(clazz); + } + return this; + } + + void exportGlobalInstance(String name, Class clazz) { + write("---@class (exact) " + getInstanceTypeName(clazz)); + write(name + " = {}"); + } + + void exportGlobalType(Class clazz) { + write("---@class (exact) " + getStaticTypeName(clazz)); + write(clazz.getSimpleName() + " = {}"); + } + + private String getStaticTypeName(Class clazz) { + String name = luaClassName(clazz) + "__class"; + debugStack.push(" -> " + clazz.getSimpleName()); + try { + if (handled.add(name)) { + write(getStaticTypeDefinition(clazz)); + } + return name; + } finally { + debugStack.pop(); + } + } + + private String getInstanceTypeName(TypeToken type) { + if (LIST_TYPE.isSupertypeOf(type)) { + return getInstanceTypeName(type.resolveType(LIST_TYPE.getRawType().getTypeParameters()[0])) + "[]"; + } else if (MAP_TYPE.isSupertypeOf(type)) { + return "{[%s]: %s}".formatted( + getInstanceTypeName(type.resolveType(MAP_TYPE.getRawType().getTypeParameters()[0])), + getInstanceTypeName(type.resolveType(MAP_TYPE.getRawType().getTypeParameters()[1])) + ); + } + return getInstanceTypeName(type.getRawType()); + } + + private String getInstanceTypeName(Class clazz) { + if (clazz.getPackageName().startsWith("com.google.protobuf")) { + return "any"; + } + if (LuaValue.class.equals(clazz)) { + throw new IllegalArgumentException("Unhandled LuaValue: " + String.join("", debugStack)); + } + debugStack.push(" -> " + clazz.getSimpleName()); + try { + if (TYPE_NAMES.containsKey(clazz)) { + return TYPE_NAMES.get(clazz); + } + if (clazz.isArray()) { + return getInstanceTypeName(clazz.getComponentType()) + "[]"; + } + if (LuaValue.class.isAssignableFrom(clazz)) { + return "any"; + } + + String name = luaClassName(clazz); + if (handled.add(name)) { + write(getTypeDefinition(clazz)); + } + return name; + } finally { + debugStack.pop(); + } + } + + String getTypeDefinition(Class clazz) { + return generateLuaInstanceTypeDefinition(clazz).generate(); + } + + String getStaticTypeDefinition(Class clazz) { + return generateLuaTypeDefinition(clazz, "__class", true).generate(); + } + + private LuaTypeDefinition generateLuaInstanceTypeDefinition(Class clazz) { + return generateLuaTypeDefinition(clazz, "", false); + } + + private LuaTypeDefinition generateLuaTypeDefinition(Class clazz, String suffix, boolean isStatic) { + TypeToken type = TypeToken.of(clazz); + var definition = new LuaTypeDefinition(type, suffix, isStatic); + + Type superclass = clazz.getGenericSuperclass(); + if (superclass != null && superclass != Object.class) { + definition.addParent(type.resolveType(superclass)); + } + for (var iface : clazz.getGenericInterfaces()) { + definition.addParent(type.resolveType(iface)); + } + + for (var field : clazz.getFields()) { + TypeToken rawType = TypeToken.of(field.getDeclaringClass()).resolveType(field.getGenericType()); + TypeToken typeOnThisClass = type.resolveType(field.getGenericType()); + if (Modifier.isPublic(field.getModifiers()) && isStatic == Modifier.isStatic(field.getModifiers()) && + (field.getDeclaringClass() == clazz || !rawType.equals(typeOnThisClass))) { + definition.addField(field); + } + } + + Set recordFields = clazz.isRecord() ? Arrays.stream(clazz.getRecordComponents()) + .map(RecordComponent::getAccessor) + .collect(Collectors.toSet()) : Set.of(); + + // - declare public (static and nonstatic) methods + for (var method : clazz.getMethods()) { + if (Modifier.isPublic(method.getModifiers()) && isStatic == Modifier.isStatic(method.getModifiers())) { + Invokable invokable = type.method(method); + if (!invokable.getOwnerType().equals(type) && !differentFromParents(invokable, type)) { + continue; + } + if (hasMethod(Object.class, method)) { + // skip object methods + } else if (method.isAnnotationPresent(LuaGetter.class) || + (method.getParameterCount() == 0 && recordFields.contains(method))) { + definition.addField(method, method.getGenericReturnType()); + } else if (method.isAnnotationPresent(LuaSetter.class)) { + definition.addField(method, method.getParameterTypes()[0]); + } else { + definition.addMethod(method); + } + } + } + + if (isStatic) { + for (var constructor : clazz.getConstructors()) { + if (Modifier.isPublic(constructor.getModifiers())) { + definition.addMethod("new", constructor, clazz); + } + } + } + return definition; + } + + private boolean differentFromParents(Invokable invokable, TypeToken type) { + Class superclass = type.getRawType().getSuperclass(); + if (superclass != null) { + var superType = TypeToken.of(superclass); + if (differentFromParents2(invokable, superType)) { + return true; + } + } + for (var iface : type.getTypes().interfaces()) { + if (differentFromParents2(invokable, iface)) { + return true; + } + } + return false; + } + + @Override + public String toString() { + return builder.toString(); + } + + private record LuaFieldDefinition(String name, String type) { + + void write(StringBuilder builder) { + builder.append("---@field %s %s%n".formatted(name, type)); + } + } + + private record LuaParameterDefinition(String name, String type) {} + + private record LuaTypeParameter(String name, String superType) { + + @Override + public String toString() { + return name + + ((superType.equals(luaClassName(Object.class)) || "any".equals(superType)) ? "" : (" : " + superType)); + } + } + + private record LuaMethodDefinitions(String name, List typeParameters, + List params, String returnType) { + + void write(String typeName, StringBuilder builder) { + for (var typeParam : typeParameters) { + builder.append("---@generic %s%n".formatted(typeParam)); + } + for (var param : params) { + builder.append("---@param %s %s%n".formatted(param.name, param.type)); + } + builder.append("---@return %s%n".formatted(returnType)); + builder.append("function types.%s:%s(%s) end%n".formatted( + typeName, + name, + params.stream().map(p -> p.name).collect(Collectors.joining(", ")) + )); + } + + public String functionTypeString() { + return "fun(%s): %s".formatted( + params.stream().map(p -> p.name + ": " + p.type).collect(Collectors.joining(", ")), + returnType + ); + } + + public void writeAsField(StringBuilder builder) { + builder.append("---@field %s %s%n".formatted( + name, + functionTypeString() + )); + } + } + + private class LuaTypeDefinition { + + private final TypeToken type; + private final boolean isStatic; + String name; + Set parents = new LinkedHashSet<>(); + Map fields = new TreeMap<>(); + Set methods = new TreeSet<>(Comparator.comparing(Record::toString)); + + LuaTypeDefinition(TypeToken type, String suffix, boolean isStatic) { + this.type = type; + this.name = luaClassName(type.getRawType()) + suffix; + this.isStatic = isStatic; + } + + LuaTypeDefinition(TypeToken type) { + this(type, "", false); + } + + void addParent(TypeToken type) { + parents.add(getInstanceTypeName(type)); + } + + LuaFieldDefinition addField(Member field, Type fieldType) { + try { + debugStack.push("." + field.getName()); + String fieldName = transformMemberName(field.getName()); + var fieldDefinition = + new LuaFieldDefinition(fieldName, getInstanceTypeName(type.resolveType(fieldType))); + fields.put(fieldName, fieldDefinition); + return fieldDefinition; + } finally { + debugStack.pop(); + } + } + + LuaFieldDefinition addField(Field field) { + if (field.getType().equals(LuaValue.class) && field.isAnnotationPresent(LuaFunctionType.class)) { + var functionDetails = field.getAnnotation(LuaFunctionType.class); + var target = functionDetails.target(); + var targetMethod = functionDetails.method().isBlank() ? + CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, field.getName()) : + functionDetails.method(); + var matchingMethods = Arrays.stream(target.getDeclaredMethods()) + .filter(m -> m.getName().equals(targetMethod)) + .toList(); + if (matchingMethods.size() != 1) { + throw new IllegalArgumentException("Expected exactly 1 method named " + targetMethod + + " on " + target.getSimpleName() + ", found " + matchingMethods.size() + " " + String.join("", debugStack)); + } + var definition = new LuaTypeDefinition(type); + var method = definition.createMethod(matchingMethods.get(0)); + var fieldName = transformMemberName(field.getName()); + var fieldDefinition = new LuaFieldDefinition( + fieldName, + method.functionTypeString() + ); + fields.put(fieldName, fieldDefinition); + return fieldDefinition; + } + return addField(field, field.getGenericType()); + } + + void addMethod(Method method) { + methods.add(createMethod(method)); + } + + void addMethod(String methodName, Executable method, Type returnType) { + methods.add(createMethod(methodName, method, returnType)); + } + + private LuaMethodDefinitions createMethod(Method method) { + return createMethod(method.getName(), method, method.getGenericReturnType()); + } + + private LuaMethodDefinitions createMethod(String methodName, Executable method, Type returnType) { + methodName = transformMemberName(methodName); + List typeParameters = new ArrayList<>(); + for (var param : method.getTypeParameters()) { + typeParameters.add(new LuaTypeParameter( + param.getName(), + getInstanceTypeName(type.resolveType(param.getBounds()[0])) + )); + } + List parameters = new ArrayList<>(); + for (var param : method.getParameters()) { + parameters.add(new LuaParameterDefinition( + transformMemberName(param.getName()), + param.isAnnotationPresent(LuaType.class) ? param.getAnnotation(LuaType.class).value() : + param.getType() == Path.class ? "%s|string|string[]".formatted(getInstanceTypeName(Path.class)) : + resolveType(param.getParameterizedType(), method.getTypeParameters()) + )); + } + return new LuaMethodDefinitions( + methodName, + typeParameters, + parameters, + resolveType(returnType, method.getTypeParameters()) + ); + } + + private String resolveType(Type elementType, TypeVariable[] typeParameters) { + var resolvedType = type.resolveType(elementType); + // only return type parameter name when it is parameterized at the method level, see: + // https://github.com/LuaLS/lua-language-server/issues/734 + // https://github.com/LuaLS/lua-language-server/issues/1861 + if (resolvedType.getType() instanceof TypeVariable variable) { + for (var typeParam : typeParameters) { + if (typeParam.getName().equals(variable.getName())) { + return typeParam.getName(); + } + } + } + return getInstanceTypeName(resolvedType); + } + + void write(StringBuilder builder) { + String nameToUse = this.name; + if (type.getRawType().isEnum() && !isStatic) { + nameToUse += "__enum"; + builder.append("---@alias %s%n".formatted(name)); + builder.append("---|%s%n".formatted(nameToUse)); + builder.append("---|integer\n"); + for (var constant : type.getRawType().getEnumConstants()) { + builder.append("---|%s%n".formatted(Format.quote(constant.toString()))); + } + } + builder.append("---@class (exact) %s".formatted(nameToUse)); + if (!parents.isEmpty()) { + builder.append(" : ").append(String.join(", ", parents)); + } + builder.append("\n"); + for (var field : fields.values()) { + field.write(builder); + } + boolean bindMethods = type.getRawType().isAnnotationPresent(LuaBindMethods.class); + if (bindMethods) { + for (var method : methods) { + method.writeAsField(builder); + } + } + builder.append("types.%s = {}%n".formatted(nameToUse)); + if (!bindMethods) { + for (var method : methods) { + method.write(nameToUse, builder); + } + } + } + + public String generate() { + StringBuilder tmp = new StringBuilder(); + write(tmp); + return tmp.toString(); + } + + } +} diff --git a/planetiler-experimental/src/main/java/com/onthegomap/planetiler/experimental/lua/LuaEnvironment.java b/planetiler-experimental/src/main/java/com/onthegomap/planetiler/experimental/lua/LuaEnvironment.java index 98c535017b..b52aebb411 100644 --- a/planetiler-experimental/src/main/java/com/onthegomap/planetiler/experimental/lua/LuaEnvironment.java +++ b/planetiler-experimental/src/main/java/com/onthegomap/planetiler/experimental/lua/LuaEnvironment.java @@ -37,8 +37,10 @@ import org.luaj.vm2.lib.jse.ExtraPlanetilerCoercions; import org.luaj.vm2.lib.jse.JsePlatform; import org.luaj.vm2.lib.jse.LuaBindMethods; +import org.luaj.vm2.lib.jse.LuaFunctionType; import org.luaj.vm2.lib.jse.LuaGetter; import org.luaj.vm2.lib.jse.LuaSetter; +import org.luaj.vm2.lib.jse.LuaType; import org.luaj.vm2.luajc.LuaJC; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -52,7 +54,7 @@ public class LuaEnvironment { private static final Logger LOGGER = LoggerFactory.getLogger(LuaEnvironment.class); - private static final Set> CLASSES_TO_EXPOSE = Set.of( + static final Set> CLASSES_TO_EXPOSE = Set.of( ZoomFunction.class, FeatureMerge.class, Parse.class, @@ -173,17 +175,29 @@ public class PlanetilerNamespace { public final Stats stats = runner.stats(); public final Arguments args = runner.arguments(); public final PlanetilerOutput output = new PlanetilerOutput(); + @LuaFunctionType(target = LuaProfile.class) public LuaValue process_feature; + @LuaFunctionType(target = LuaProfile.class) public LuaValue cares_about_source; + @LuaFunctionType(target = LuaProfile.class) public LuaValue cares_about_wikidata_translation; + @LuaFunctionType(target = LuaProfile.class) public LuaValue estimate_ram_required; + @LuaFunctionType(target = LuaProfile.class) public LuaValue estimate_intermediate_disk_bytes; + @LuaFunctionType(target = LuaProfile.class) public LuaValue estimate_output_bytes; + @LuaFunctionType(target = LuaProfile.class) public LuaValue finish; + @LuaFunctionType(target = LuaProfile.class) public LuaValue preprocess_osm_node; + @LuaFunctionType(target = LuaProfile.class) public LuaValue preprocess_osm_way; + @LuaFunctionType(target = LuaProfile.class) public LuaValue preprocess_osm_relation; + @LuaFunctionType(target = LuaProfile.class) public LuaValue release; + @LuaFunctionType(target = LuaProfile.class, method = "postProcessLayerFeatures") public LuaValue post_process; public String examples; @@ -215,7 +229,9 @@ public void fetch_wikidata_translations() { runner.fetchWikidataNameTranslations(Path.of("data", "sources", "wikidata_names.json")); } - public void add_source(String name, LuaValue map) { + public void add_source( + String name, + @LuaType("{type: 'osm'|'shapefile'|'geopackage'|'naturalearth', path: string|string[], url: string, projection: string, glob: string}") LuaValue map) { String type = get(map, "type", String.class); Path path = get(map, "path", Path.class); if (name == null || type == null) { diff --git a/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/CoerceLuaToJava.java b/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/CoerceLuaToJava.java index 54f617349a..2998129a34 100644 --- a/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/CoerceLuaToJava.java +++ b/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/CoerceLuaToJava.java @@ -22,8 +22,10 @@ package org.luaj.vm2.lib.jse; import java.lang.reflect.Array; +import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import org.luaj.vm2.LuaInteger; import org.luaj.vm2.LuaString; import org.luaj.vm2.LuaTable; import org.luaj.vm2.LuaValue; @@ -282,6 +284,40 @@ public Object coerce(LuaValue value) { } } + static final class EnumCoercion implements Coercion { + private final Map lookup = new HashMap<>(); + private final Class enumType; + + + public EnumCoercion(Class enumType) { + this.enumType = enumType; + for (Object e : enumType.getEnumConstants()) { + lookup.put(LuaString.valueOf(e.toString()), e); + lookup.put(LuaInteger.valueOf(((Enum) e).ordinal()), e); + } + } + + public String toString() { + return "EnumCoercion(" + enumType.getName() + ")"; + } + + public int score(LuaValue value) { + return switch (value.type()) { + case LuaValue.TNUMBER, LuaValue.TSTRING -> 0; + case LuaValue.TUSERDATA -> value.touserdata().getClass() == enumType ? 0 : SCORE_UNCOERCIBLE; + default -> SCORE_UNCOERCIBLE; + }; + } + + public Object coerce(LuaValue value) { + return switch (value.type()) { + case LuaValue.TNUMBER, LuaValue.TSTRING -> lookup.get(value); + case LuaValue.TUSERDATA -> value.touserdata(); + default -> null; + }; + } + } + private static final Map inheritanceLevelsCache = new ConcurrentHashMap<>(); private record ClassPair(Class baseClass, Class subclass) {} @@ -387,6 +423,8 @@ static Coercion getCoercion(Class c) { } if (c.isArray()) { co = new ArrayCoercion(c.getComponentType()); + } else if (c.isEnum()) { + co = new EnumCoercion(c); } else { co = new ObjectCoercion(c); } diff --git a/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/JavaClass.java b/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/JavaClass.java index 699d09c7da..7a985dc008 100644 --- a/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/JavaClass.java +++ b/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/JavaClass.java @@ -56,7 +56,7 @@ * @see CoerceJavaToLua * @see CoerceLuaToJava */ -class JavaClass extends JavaInstance { +public class JavaClass extends JavaInstance { private static final Converter CAMEL_TO_SNAKE_CASE = CaseFormat.LOWER_CAMEL.converterTo(CaseFormat.LOWER_UNDERSCORE); @@ -71,7 +71,7 @@ class JavaClass extends JavaInstance { private final Map> innerclasses = new HashMap<>(); public final boolean bindMethods; - static JavaClass forClass(Class c) { + public static JavaClass forClass(Class c) { // planetiler change: use ConcurrentHashMap instead of synchronized map to improve performance JavaClass j = classes.get(c); if (j == null) { diff --git a/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/JavaInstance.java b/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/JavaInstance.java index 4667d55ffa..61697a7853 100644 --- a/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/JavaInstance.java +++ b/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/JavaInstance.java @@ -113,6 +113,14 @@ public Varargs invoke(Varargs args) { } } + @Override + public LuaValue len() { + if (m_instance instanceof List c) { + return LuaValue.valueOf(c.size()); + } + return super.len(); + } + public LuaValue get(LuaValue key) { // planetiler change: allow lists to be accessed as tables if (m_instance instanceof List c) { diff --git a/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/LuaFunctionType.java b/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/LuaFunctionType.java new file mode 100644 index 0000000000..27ded01599 --- /dev/null +++ b/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/LuaFunctionType.java @@ -0,0 +1,17 @@ +package org.luaj.vm2.lib.jse; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation that that generates the type for a lua value from a method on another class. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface LuaFunctionType { + Class target(); + + String method() default ""; +} diff --git a/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/LuaType.java b/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/LuaType.java new file mode 100644 index 0000000000..c31d782d30 --- /dev/null +++ b/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/LuaType.java @@ -0,0 +1,15 @@ +package org.luaj.vm2.lib.jse; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation that lets you define the type of a lua value. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface LuaType { + String value(); +} diff --git a/planetiler-experimental/src/test/java/com/onthegomap/planetiler/experimental/lua/GenerateLuaTypesTest.java b/planetiler-experimental/src/test/java/com/onthegomap/planetiler/experimental/lua/GenerateLuaTypesTest.java new file mode 100644 index 0000000000..047a461672 --- /dev/null +++ b/planetiler-experimental/src/test/java/com/onthegomap/planetiler/experimental/lua/GenerateLuaTypesTest.java @@ -0,0 +1,486 @@ +package com.onthegomap.planetiler.experimental.lua; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.onthegomap.planetiler.config.Arguments; +import java.nio.file.Path; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Semaphore; +import java.util.function.Consumer; +import org.junit.jupiter.api.Test; +import org.luaj.vm2.LuaDouble; +import org.luaj.vm2.LuaInteger; +import org.luaj.vm2.LuaNumber; +import org.luaj.vm2.LuaString; +import org.luaj.vm2.LuaTable; +import org.luaj.vm2.LuaValue; +import org.luaj.vm2.lib.jse.LuaBindMethods; +import org.luaj.vm2.lib.jse.LuaFunctionType; +import org.luaj.vm2.lib.jse.LuaGetter; +import org.luaj.vm2.lib.jse.LuaSetter; + +@SuppressWarnings("unused") +class GenerateLuaTypesTest { + + @Test + void testSimpleClass() { + interface Test { + + String string(); + + int intMethod(Integer n); + + long longMethod(Long n); + + double doubleMethod(Double n); + + float floatMethod(Float n); + + short shortMethod(Short n); + + byte byteMethod(Byte n); + + boolean booleanMethod(Boolean n); + } + assertGenerated(""" + ---@class (exact) com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1Test + types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1Test = {} + ---@param n boolean + ---@return boolean + function types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1Test:boolean_method(n) end + ---@param n integer + ---@return integer + function types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1Test:byte_method(n) end + ---@param n number + ---@return number + function types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1Test:double_method(n) end + ---@param n number + ---@return number + function types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1Test:float_method(n) end + ---@param n integer + ---@return integer + function types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1Test:int_method(n) end + ---@param n integer + ---@return integer + function types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1Test:long_method(n) end + ---@param n integer + ---@return integer + function types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1Test:short_method(n) end + ---@return string + function types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1Test:string() end + """, Test.class); + } + + @Test + void testSimpleClassWithLuaBindMethods() { + @LuaBindMethods + interface TestBindMethods { + + String string(); + + int intMethod(Integer n); + + void voidMethod(); + } + assertGenerated(""" + ---@class (exact) com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1TestBindMethods + ---@field int_method fun(n: integer): integer + ---@field string fun(): string + ---@field void_method fun(): nil + types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1TestBindMethods = {} + """, TestBindMethods.class); + } + + @Test + void testSimpleClassWithField() { + class WithField { + + public Date field; + } + assertGenerated(""" + ---@class (exact) com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1WithField + ---@field field java_util_Date + types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1WithField = {} + """, WithField.class); + } + + @Test + void testSimpleClassWithGetter() { + class WithGetter { + + @LuaGetter + public Date field() { + return null; + } + + @LuaGetter + public List field2() { + return null; + } + + @LuaSetter + public void field2(List value) {} + } + assertGenerated(""" + ---@class (exact) com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1WithGetter + ---@field field java_util_Date + ---@field field2 string[] + types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1WithGetter = {} + """, WithGetter.class); + } + + @Test + void testSimpleClassWithSetter() { + class WithSetter { + + @LuaSetter + public void field(Date field) {} + } + assertGenerated(""" + ---@class (exact) com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1WithSetter + ---@field field java_util_Date + types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1WithSetter = {} + """, WithSetter.class); + } + + @Test + void testArrays() { + class WithArrays { + + public int[] intArray; + public Integer[] intObjectArray; + public Semaphore[] semaphoreArray; + } + assertGenerated(""" + ---@class (exact) com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1WithArrays + ---@field int_array integer[] + ---@field int_object_array integer[] + ---@field semaphore_array java_util_concurrent_Semaphore[] + types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1WithArrays = {} + """, WithArrays.class); + } + + @Test + void testKeywordCollision() { + class KeywordCollision { + + public int and; + } + assertGenerated( + """ + ---@class (exact) com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1KeywordCollision + ---@field AND integer + types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1KeywordCollision = {} + """, + KeywordCollision.class); + } + + @Test + void testLists() { + class WithLists { + + public List ints; + public List dates; + public List objs; + public List wildcards; + } + assertGenerated(""" + ---@class (exact) com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1WithLists + ---@field dates java_util_Date[] + ---@field ints integer[] + ---@field objs any[] + ---@field wildcards any[] + types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1WithLists = {} + """, WithLists.class); + } + + @Test + void testMaps() { + class WithLists { + + public Map stringToInt; + public Map dateToDouble; + public Map objectToWildcard; + public Map>> stringToDoubleStringList; + } + assertGenerated(""" + ---@class (exact) com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_2WithLists + ---@field date_to_double {[java_util_Date]: number} + ---@field object_to_wildcard {[any]: any} + ---@field string_to_double_string_list {[string]: string[][]} + ---@field string_to_int {[string]: integer} + types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_2WithLists = {} + """, WithLists.class); + } + + @Test + void testMethodOnGenericType() { + class Generic { + + public T value() { + return null; + } + } + class Concrete extends Generic {} + assertGenerated( + """ + ---@class (exact) com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1Concrete : com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1Generic + types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1Concrete = {} + ---@return string + function types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1Concrete:value() end + """, + Concrete.class); + } + + @Test + void testGenericMethod() { + class GenericMethod { + + public T apply(T input) { + return null; + } + + public T applyNumber(T input) { + return null; + } + } + assertGenerated( + """ + ---@class (exact) com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1GenericMethod + types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1GenericMethod = {} + ---@generic T + ---@param input T + ---@return T + function types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1GenericMethod:apply(input) end + ---@generic T : number + ---@param input T + ---@return T + function types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1GenericMethod:apply_number(input) end + """, + GenericMethod.class); + } + + @Test + void testGenericMethodInterface() { + interface Interface { + + T value(); + } + interface ConcreteInterface extends Interface {} + assertGenerated( + """ + ---@class (exact) com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1ConcreteInterface : com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1Interface + types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1ConcreteInterface = {} + ---@return string + function types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1ConcreteInterface:value() end + """, + ConcreteInterface.class); + } + + @Test + void testRecord() { + record Record(int x, int y) {} + assertGenerated( + """ + ---@class (exact) com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1Record : java_lang_Record + ---@field x integer + ---@field y integer + types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1Record = {} + """, + Record.class); + } + + @Test + void testGenericField() { + class Generic { + + public T field; + } + class ConcreteField extends Generic {} + assertGenerated( + """ + ---@class (exact) com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1ConcreteField : com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_2Generic + ---@field field integer + types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1ConcreteField = {} + """, + ConcreteField.class); + } + + @Test + void testLuaValueWithAnnotation() { + class Target { + public int method(int arg) { + return 1; + } + } + class LuaValues { + + public LuaString string; + public LuaInteger integer; + public LuaDouble number; + public LuaNumber number2; + public LuaTable table; + @LuaFunctionType( + target = Target.class, + method = "method" + ) + public LuaValue value; + } + assertGenerated( + """ + ---@class (exact) com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1LuaValues + ---@field integer integer + ---@field number number + ---@field number2 number + ---@field string string + ---@field table table + ---@field value fun(arg: integer): integer + types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1LuaValues = {} + """, + LuaValues.class); + } + + public static class StaticClass { + public static final int CONSTANT = 1; + public static int staticField; + public int instanceField; + + public StaticClass() {} + + public StaticClass(int i) {} + + public int instance(int arg) { + return 1; + } + + public static int staticMethod(int arg) { + return 1; + } + } + + @Test + void testStaticLuaInstanceWithConsructors() { + var g = new GenerateLuaTypes(); + var actual = g.getStaticTypeDefinition(StaticClass.class).trim(); + assertEquals( + """ + ---@class (exact) com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_StaticClass__class + ---@field CONSTANT integer + ---@field static_field integer + types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_StaticClass__class = {} + ---@param i integer + ---@return com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_StaticClass + function types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_StaticClass__class:new(i) end + ---@return com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_StaticClass + function types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_StaticClass__class:new() end + ---@param arg integer + ---@return integer + function types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_StaticClass__class:static_method(arg) end + """ + .trim(), + actual, "got:\n\n" + actual + "\n\n"); + } + + @Test + void testGenericClassMethod() { + assertGenerated( + """ + ---@class (exact) java_util_function_Consumer + types.java_util_function_Consumer = {} + ---@param arg0 any + ---@return nil + function types.java_util_function_Consumer:accept(arg0) end + ---@param arg0 java_util_function_Consumer + ---@return java_util_function_Consumer + function types.java_util_function_Consumer:and_then(arg0) end + """, + Consumer.class); + } + + @Test + void testEnum() { + enum TestEnum { + A, + B, + C + } + assertGenerated( + """ + ---@alias com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1TestEnum + ---|com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1TestEnum__enum + ---|integer + ---|"A" + ---|"B" + ---|"C" + ---@class (exact) com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1TestEnum__enum : java_lang_Enum + types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1TestEnum__enum = {} + ---@param arg0 any + ---@return integer + function types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1TestEnum__enum:compare_to(arg0) end + ---@param arg0 com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1TestEnum + ---@return integer + function types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1TestEnum__enum:compare_to(arg0) end + ---@return java_util_Optional + function types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1TestEnum__enum:describe_constable() end + ---@return userdata + function types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1TestEnum__enum:get_declaring_class() end + ---@return string + function types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1TestEnum__enum:name() end + ---@return integer + function types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1TestEnum__enum:ordinal() end + """, + TestEnum.class); + class UsesEnum { + public TestEnum field; + + public TestEnum method(TestEnum arg) { + return null; + } + } + assertGenerated( + """ + ---@class (exact) com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1UsesEnum + ---@field field com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1TestEnum + types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1UsesEnum = {} + ---@param arg com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1TestEnum + ---@return com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1TestEnum + function types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1UsesEnum:method(arg) end + """, + UsesEnum.class); + } + + @Test + void testPath() { + class UsesPath { + public Path field; + + public Path method(Path arg) { + return null; + } + } + assertGenerated( + """ + ---@class (exact) com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1UsesPath + ---@field field java_nio_file_Path + types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1UsesPath = {} + ---@param arg java_nio_file_Path|string|string[] + ---@return java_nio_file_Path + function types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1UsesPath:method(arg) end + """, + UsesPath.class); + } + + @Test + void testGeneratedMetaFileCompiles() { + String types = new GenerateLuaTypes().generatePlanetiler().toString(); + LuaEnvironment.loadScript(Arguments.of("luajc", "false"), types, "types.lua"); + } + + private static void assertGenerated(String expected, Class clazz) { + var g = new GenerateLuaTypes(); + var actual = g.getTypeDefinition(clazz).trim(); + assertEquals(expected.trim(), actual, "got:\n\n" + actual + "\n\n"); + } +} diff --git a/planetiler-experimental/src/test/java/com/onthegomap/planetiler/experimental/lua/LuaEnvironmentTests.java b/planetiler-experimental/src/test/java/com/onthegomap/planetiler/experimental/lua/LuaEnvironmentTests.java index 2b38bc93ad..60ae0da261 100644 --- a/planetiler-experimental/src/test/java/com/onthegomap/planetiler/experimental/lua/LuaEnvironmentTests.java +++ b/planetiler-experimental/src/test/java/com/onthegomap/planetiler/experimental/lua/LuaEnvironmentTests.java @@ -738,6 +738,88 @@ public int[] call() { assertConvertsTo("👍", env.main.call()); } + @Test + void testArrayLength() { + var env = load(""" + function main() + return #{1} + end + """, Map.of("obj", new Object() { + public int[] call() { + return new int[]{1}; + } + })); + assertConvertsTo(1, env.main.call()); + } + + @Test + void testJavaArrayLength() { + var env = load(""" + function main() + return #obj:call() + end + """, Map.of("obj", new Object() { + public int[] call() { + return new int[]{1}; + } + })); + assertConvertsTo(1, env.main.call()); + } + + @Test + void testJavaListLength() { + var env = load(""" + function main() + return #obj:call() + end + """, Map.of("obj", new Object() { + public List call() { + return List.of(1); + } + })); + assertConvertsTo(1, env.main.call()); + } + + @Test + void testEnum() { + enum MyEnum { + A, + B, + C + }; + var env = load(""" + function main() + return { + obj:call(0), + obj:call('B'), + obj:call(enum.C), + obj:call2(0) + } + end + """, Map.of("enum", MyEnum.class, "obj", new Object() { + + public int call(MyEnum e) { + return e.ordinal(); + } + + public MyEnum call2(int ordinal) { + return MyEnum.values()[ordinal]; + } + })); + assertConvertsTo(List.class, List.of( + 0, 1, 2, MyEnum.A + ), env.main.call()); + } + + public static class MyObj { + public int value = 1; + public static int staticValue = 1; + + public static int get() { + return 1; + } + } + private static void assertConvertsTo(T java, LuaValue lua) { assertConvertsTo(java.getClass(), java, lua); } diff --git a/pom.xml b/pom.xml index f26e7e5e43..d55c5b3d40 100644 --- a/pom.xml +++ b/pom.xml @@ -19,6 +19,8 @@ UTF-8 21 21 + + true true 2.16.0 5.10.1