Skip to content

Commit

Permalink
lua4jvm: Split Lua numbers to floats and ints according to 5.4 spec
Browse files Browse the repository at this point in the history
This might improve performance. It certainly makes using e.g. Lua tables from
Java code easier - things no longer blow up if you accidentally use ints
instead of doubles. Same goes for Java FFI.

Further testing needed. Many of the existing tests inject doubles from Java
to Lua, which doesn't necessarily mean that the int paths work!
  • Loading branch information
bensku committed Aug 11, 2024
1 parent 569f085 commit 500fdc3
Show file tree
Hide file tree
Showing 15 changed files with 163 additions and 93 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -447,8 +447,8 @@ public IrNode visitStringConcat(StringConcatContext ctx) {

@Override
public IrNode visitNumberLiteral(NumberLiteralContext ctx) {
// TODO non-decimal numbers
return new LuaConstant(Double.valueOf(ctx.Numeral().getText()));
var value = Double.valueOf(ctx.Numeral().getText());
return new LuaConstant(value.intValue() == value ? value.intValue() : value);
}

@Override
Expand Down
27 changes: 6 additions & 21 deletions lua4jvm/src/main/java/fi/benjami/code4jvm/lua/ir/LuaType.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,12 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import fi.benjami.code4jvm.Type;
import fi.benjami.code4jvm.Value;
import fi.benjami.code4jvm.lua.compiler.CompiledFunction;
import fi.benjami.code4jvm.lua.compiler.CompiledShape;
import fi.benjami.code4jvm.lua.compiler.FunctionCompiler;
import fi.benjami.code4jvm.lua.compiler.LuaContext;
import fi.benjami.code4jvm.lua.compiler.ShapeGenerator;
import fi.benjami.code4jvm.lua.compiler.ShapeTypes;
import fi.benjami.code4jvm.lua.ir.stmt.ReturnStmt;
import fi.benjami.code4jvm.lua.runtime.LuaFunction;
Expand Down Expand Up @@ -232,7 +229,8 @@ public boolean equals(Object obj) {
// Lua standard types
static final LuaType NIL = new Simple("nil", Type.OBJECT);
static final LuaType BOOLEAN = new Simple("boolean", Type.BOOLEAN);
static final LuaType NUMBER = new Simple("number", Type.DOUBLE);
static final LuaType INTEGER = new Simple("number", Type.INT);
static final LuaType FLOAT = new Simple("number", Type.DOUBLE);
static final LuaType STRING = new Simple("string", Type.STRING);
static final LuaType TABLE = new Simple("table", LuaTable.TYPE);
// TODO userdata, thread
Expand Down Expand Up @@ -281,23 +279,6 @@ public static Shape shape() {
return new Shape();
}

public static List<LuaType> readList(String str) {
var types = new ArrayList<LuaType>();
for (var i = 0; i < str.length(); i++) {
types.add(switch (str.charAt(i)) {
case 'V' -> LuaType.NIL;
case 'B' -> LuaType.BOOLEAN;
case 'N' -> LuaType.NUMBER;
case 'S' -> LuaType.STRING;
case 'U' -> LuaType.UNKNOWN;
case 'T' -> throw new UnsupportedOperationException("todo");
case 'F' -> throw new UnsupportedOperationException("todo");
default -> throw new IllegalArgumentException("unknown type: " + str.charAt(i));
});
}
return types;
}

/**
* Name of type for Lua code.
* @return Lua type name.
Expand All @@ -317,4 +298,8 @@ public static List<LuaType> readList(String str) {
default boolean isAssignableFrom(LuaType other) {
return this == LuaType.UNKNOWN || equals(other);
}

default boolean isNumber() {
return this == LuaType.INTEGER || this == LuaType.FLOAT;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,21 @@ class LuaTypeSupport {
public static final Map<Type, LuaType> TYPE_TO_TYPE = Map.of(
Type.BOOLEAN, LuaType.BOOLEAN,
Type.of(Boolean.class), LuaType.BOOLEAN,
Type.DOUBLE, LuaType.NUMBER,
Type.of(Double.class), LuaType.NUMBER,
Type.INT, LuaType.INTEGER,
Type.of(Integer.class), LuaType.INTEGER,
Type.DOUBLE, LuaType.FLOAT,
Type.of(Double.class), LuaType.FLOAT,
Type.STRING, LuaType.STRING,
LuaTable.TYPE, LuaType.TABLE
);

public static final Map<Class<?>, LuaType> CLASS_TO_TYPE = Map.of(
boolean.class, LuaType.BOOLEAN,
Boolean.class, LuaType.BOOLEAN,
double.class, LuaType.NUMBER,
Double.class, LuaType.NUMBER,
int.class, LuaType.INTEGER,
Integer.class, LuaType.INTEGER,
double.class, LuaType.FLOAT,
Double.class, LuaType.FLOAT,
String.class, LuaType.STRING,
LuaTable.class, LuaType.TABLE
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.List;
import java.util.function.BiFunction;

import fi.benjami.code4jvm.Expression;
Expand Down Expand Up @@ -38,7 +39,10 @@ public record ArithmeticExpr(
public enum Kind {
POWER(MATH_POW::call, "power", "__pow"),
MULTIPLY(Arithmetic::multiply, "multiply", "__mul"),
DIVIDE(Arithmetic::divide, "divide", "__div"),
DIVIDE((lhs, rhs) -> {
// Lua uses float division unless integer division is explicitly request (see below)
return Arithmetic.divide(lhs.cast(Type.DOUBLE), rhs.cast(Type.DOUBLE));
}, "divide", "__div"),
FLOOR_DIVIDE(FLOOR_DIV::call, "floorDivide", "__idiv"),
MODULO((lhs, rhs) -> (block -> {
// Lua expects modulo to be always positive; Java's remainder can return negative values
Expand All @@ -53,16 +57,27 @@ public enum Kind {

Kind(BiFunction<Value, Value, Expression> directEmitter, String methodName, String metamethod) {
this.directEmitter = directEmitter;
MethodHandle fastPath;
var intReturnType = methodName == "power" || methodName.equals("divide") ? double.class : int.class;
MethodHandle doublePath, intPath;
try {
// Drop the call target argument, it is not needed
fastPath = MethodHandles.dropArguments(LOOKUP.findStatic(ArithmeticExpr.class, methodName,
doublePath = MethodHandles.dropArguments(LOOKUP.findStatic(ArithmeticExpr.class, methodName,
MethodType.methodType(double.class, double.class, double.class)), 0, Object.class);
intPath = MethodHandles.dropArguments(LOOKUP.findStatic(ArithmeticExpr.class, methodName,
MethodType.methodType(intReturnType, int.class, int.class)), 0, Object.class);
} catch (NoSuchMethodException | IllegalAccessException e) {
throw new AssertionError(e);
}
this.callTarget = BinaryOp.newTarget(Double.class, fastPath, metamethod,
(a, b) -> new LuaException("attempted to perform arithmetic on non-number values"));
// If we have any doubles at all, take the double path
var paths = List.of(
new BinaryOp.Path(Integer.class, Integer.class, intPath),
new BinaryOp.Path(Double.class, Double.class, doublePath),
new BinaryOp.Path(Integer.class, Double.class, MethodHandles.explicitCastArguments(doublePath, MethodType.methodType(double.class, Object.class, int.class, double.class))),
new BinaryOp.Path(Double.class, Integer.class, MethodHandles.explicitCastArguments(doublePath, MethodType.methodType(double.class, Object.class, double.class, int.class)))
);
this.callTarget = BinaryOp.newTarget(paths, metamethod,
(a, b) -> new LuaException("cannot " + methodName + " "
+ LuaType.of(a).name() + " and " + LuaType.of(b).name()));
}
}

Expand Down Expand Up @@ -102,11 +117,44 @@ private static double subtract(double lhs, double rhs) {
return lhs - rhs;
}

@SuppressWarnings("unused")
private static double power(int lhs, int rhs) {
return Math.pow(lhs, rhs);
}

@SuppressWarnings("unused")
private static int multiply(int lhs, int rhs) {
return lhs * rhs;
}

private static double divide(int lhs, int rhs) {
return ((double) lhs) / ((double) rhs);
}

public static int floorDivide(int lhs, int rhs) {
return (int) Math.floor(divide(lhs, rhs));
}

@SuppressWarnings("unused")
private static int modulo(int lhs, int rhs) {
return Math.abs(lhs % rhs);
}

@SuppressWarnings("unused")
private static int add(int lhs, int rhs) {
return lhs + rhs;
}

@SuppressWarnings("unused")
private static int subtract(int lhs, int rhs) {
return lhs - rhs;
}

@Override
public Value emit(LuaContext ctx, Block block) {
var lhsValue = lhs.emit(ctx, block);
var rhsValue = rhs.emit(ctx, block);
if (outputType(ctx).equals(LuaType.NUMBER)) {
if (outputType(ctx).isNumber()) {
// Both arguments are known to be numbers; emit arithmetic operation directly
return block.add(kind.directEmitter.apply(lhsValue, rhsValue));
} else {
Expand All @@ -117,8 +165,19 @@ public Value emit(LuaContext ctx, Block block) {

@Override
public LuaType outputType(LuaContext ctx) {
return lhs.outputType(ctx).equals(LuaType.NUMBER) && rhs.outputType(ctx).equals(LuaType.NUMBER)
? LuaType.NUMBER : LuaType.UNKNOWN;
var lhsOut = lhs.outputType(ctx);
var rhsOut = rhs.outputType(ctx);
if (lhsOut.isNumber() && rhsOut.isNumber()) {
if (kind == Kind.POWER || kind == Kind.DIVIDE) {
// Lua spec says that these always produce floats
return LuaType.FLOAT;
} else if (lhsOut == LuaType.INTEGER && rhsOut == LuaType.INTEGER) {
return LuaType.INTEGER; // Both sides are integers
}
return LuaType.FLOAT; // Float on at least one side
} else {
return LuaType.UNKNOWN;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,8 @@ public record LengthExpr(IrNode expr) implements IrNode {
static {
var lookup = MethodHandles.lookup();
try {
TABLE_LENGTH = MethodHandles.dropArguments(lookup.findVirtual(LuaTable.class, "arraySize", MethodType.methodType(int.class))
.asType(MethodType.methodType(double.class, LuaTable.class)), 0, Object.class);
STRING_LENGTH = MethodHandles.dropArguments(lookup.findVirtual(String.class, "length", MethodType.methodType(int.class))
.asType(MethodType.methodType(double.class, String.class)), 0, Object.class);
TABLE_LENGTH = MethodHandles.dropArguments(lookup.findVirtual(LuaTable.class, "arraySize", MethodType.methodType(int.class)), 0, Object.class);
STRING_LENGTH = MethodHandles.dropArguments(lookup.findVirtual(String.class, "length", MethodType.methodType(int.class)), 0, Object.class);
} catch (NoSuchMethodException | IllegalAccessException e) {
throw new AssertionError(e);
}
Expand All @@ -50,7 +48,7 @@ public Value emit(LuaContext ctx, Block block) {
@Override
public LuaType outputType(LuaContext ctx) {
// We can't do type analysis through metatables (yet)
return expr.outputType(ctx).equals(LuaType.STRING) ? LuaType.NUMBER : LuaType.UNKNOWN;
return expr.outputType(ctx).equals(LuaType.STRING) ? LuaType.INTEGER : LuaType.UNKNOWN;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ public Value emit(LuaContext ctx, Block block) {
return Constant.nullValue(Type.OBJECT);
} else if (value instanceof Boolean bool) {
return Constant.of(bool);
} else if (value instanceof Integer num) {
return Constant.of(num);
} else if (value instanceof Double num) {
return Constant.of(num);
} else if (value instanceof String str) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ private static double negate(Object callable, double value) {
@Override
public Value emit(LuaContext ctx, Block block) {
var value = expr.emit(ctx, block);
if (outputType(ctx).equals(LuaType.NUMBER)) {
if (outputType(ctx).isNumber()) {
return block.add(Arithmetic.negate(value));
} else {
return block.add(LuaLinker.setupCall(ctx, CallSiteOptions.nonFunction(ctx.owner(), LuaType.UNKNOWN, LuaType.UNKNOWN), TARGET, value));
Expand All @@ -50,8 +50,14 @@ public Value emit(LuaContext ctx, Block block) {

@Override
public LuaType outputType(LuaContext ctx) {
var exprType = expr.outputType(ctx);
if (exprType == LuaType.INTEGER) {
return LuaType.INTEGER;
} else if (exprType == LuaType.FLOAT) {
return LuaType.FLOAT;
}
// We can't do type analysis through metatables (yet)
return expr.outputType(ctx).equals(LuaType.NUMBER) ? LuaType.NUMBER : LuaType.UNKNOWN;
return LuaType.UNKNOWN;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public record StringConcatExpr(
throw new AssertionError();
}

TARGET = BinaryOp.newTarget(String.class, CONCAT_TWO, "__concat",
TARGET = BinaryOp.newTarget(List.of(new BinaryOp.Path(String.class, String.class, CONCAT_TWO)), "__concat",
(a, b) -> new LuaException("attempted to concatenate non-string values"));
}

Expand Down
37 changes: 22 additions & 15 deletions lua4jvm/src/main/java/fi/benjami/code4jvm/lua/linker/BinaryOp.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.List;
import java.util.function.BiFunction;

import fi.benjami.code4jvm.lua.ir.LuaType;
Expand All @@ -19,8 +20,8 @@
*/
public class BinaryOp {

private static final boolean checkTypes(Class<?> expected, Object callable, Object lhs, Object rhs) {
return lhs != null && lhs.getClass() == expected && rhs != null && rhs.getClass() == expected;
private static final boolean checkTypes(Class<?> expectedLhs, Class<?> expectedRhs, Object callable, Object lhs, Object rhs) {
return lhs != null && lhs.getClass() == expectedLhs && rhs != null && rhs.getClass() == expectedRhs;
}

@SuppressWarnings("unused") // MethodHandle
Expand All @@ -37,38 +38,44 @@ private static final boolean checkLhsMetamethod(String metamethod, Object callab
var lookup = MethodHandles.lookup();
try {
CHECK_TYPES = lookup.findStatic(BinaryOp.class, "checkTypes",
MethodType.methodType(boolean.class, Class.class, Object.class, Object.class, Object.class));
MethodType.methodType(boolean.class, Class.class, Class.class, Object.class, Object.class, Object.class));
CHECK_LHS_METAMETHOD = lookup.findStatic(BinaryOp.class, "checkLhsMetamethod",
MethodType.methodType(boolean.class, String.class, Object.class, Object.class));
} catch (NoSuchMethodException | IllegalAccessException e) {
throw new AssertionError(e);
}
}

public record Path(
Class<?> lhsType,
Class<?> rhsType,
MethodHandle target
) {}

/**
* Produces a dynamic call target for a binary operation call site.
* @param expectedType Type that the fast path supports.
* @param fastPath Fast path that is entered if both sides are of expected
* type. The fast path should accept the call target as first parameter,
* LHS as second and RHS as third.
* @param fastPaths Fast paths, to be evaluated in order.
* @param metamethod Name of the metamethod call if metatables are present.
* @param errorHandler Called when either value has invalid type and
* metamethods are not found. Returns a Lua exception that is thrown.
* @return Call target.
*/
public static DynamicTarget newTarget(Class<?> expectedType, MethodHandle fastPath, String metamethod,
public static DynamicTarget newTarget(List<Path> fastPaths, String metamethod,
BiFunction<Object, Object, LuaException> errorHandler) {
assert !expectedType.isPrimitive(); // LHS and RHS will be in their boxed forms
assert !expectedType.equals(LuaType.class); // This is currently unnecessary for Lua
return (meta, args) -> {
assert args.length == 2;
var lhs = args[0];
var rhs = args[1];
if (checkTypes(expectedType, null, lhs, rhs)) {
// Fast path, e.g. arithmetic operation on numbers or string concatenation on strings
var guard = CHECK_TYPES.bindTo(expectedType);
return new LuaCallTarget(fastPath, guard);
} else if (lhs instanceof LuaTable table
for (var path : fastPaths) {
if (checkTypes(path.lhsType, path.rhsType, null, lhs, rhs)) {
// Fast path, e.g. arithmetic operation on numbers or string concatenation on strings
var guard = MethodHandles.insertArguments(CHECK_TYPES, 0, path.lhsType, path.rhsType);
return new LuaCallTarget(path.target, guard);
}
}

// None of the fast paths matched
if (lhs instanceof LuaTable table
&& table.metatable() != null
&& table.metatable().get(metamethod) != null) {
// Slower path, call LHS metamethod
Expand Down
Loading

0 comments on commit 500fdc3

Please sign in to comment.