Skip to content

Commit 500fdc3

Browse files
committed
lua4jvm: Split Lua numbers to floats and ints according to 5.4 spec
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!
1 parent 569f085 commit 500fdc3

File tree

15 files changed

+163
-93
lines changed

15 files changed

+163
-93
lines changed

lua4jvm/src/main/java/fi/benjami/code4jvm/lua/compiler/IrCompiler.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -447,8 +447,8 @@ public IrNode visitStringConcat(StringConcatContext ctx) {
447447

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

454454
@Override

lua4jvm/src/main/java/fi/benjami/code4jvm/lua/ir/LuaType.java

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,12 @@
66
import java.util.List;
77
import java.util.Map;
88
import java.util.Objects;
9-
import java.util.Set;
109

1110
import fi.benjami.code4jvm.Type;
1211
import fi.benjami.code4jvm.Value;
1312
import fi.benjami.code4jvm.lua.compiler.CompiledFunction;
1413
import fi.benjami.code4jvm.lua.compiler.CompiledShape;
1514
import fi.benjami.code4jvm.lua.compiler.FunctionCompiler;
16-
import fi.benjami.code4jvm.lua.compiler.LuaContext;
17-
import fi.benjami.code4jvm.lua.compiler.ShapeGenerator;
1815
import fi.benjami.code4jvm.lua.compiler.ShapeTypes;
1916
import fi.benjami.code4jvm.lua.ir.stmt.ReturnStmt;
2017
import fi.benjami.code4jvm.lua.runtime.LuaFunction;
@@ -232,7 +229,8 @@ public boolean equals(Object obj) {
232229
// Lua standard types
233230
static final LuaType NIL = new Simple("nil", Type.OBJECT);
234231
static final LuaType BOOLEAN = new Simple("boolean", Type.BOOLEAN);
235-
static final LuaType NUMBER = new Simple("number", Type.DOUBLE);
232+
static final LuaType INTEGER = new Simple("number", Type.INT);
233+
static final LuaType FLOAT = new Simple("number", Type.DOUBLE);
236234
static final LuaType STRING = new Simple("string", Type.STRING);
237235
static final LuaType TABLE = new Simple("table", LuaTable.TYPE);
238236
// TODO userdata, thread
@@ -281,23 +279,6 @@ public static Shape shape() {
281279
return new Shape();
282280
}
283281

284-
public static List<LuaType> readList(String str) {
285-
var types = new ArrayList<LuaType>();
286-
for (var i = 0; i < str.length(); i++) {
287-
types.add(switch (str.charAt(i)) {
288-
case 'V' -> LuaType.NIL;
289-
case 'B' -> LuaType.BOOLEAN;
290-
case 'N' -> LuaType.NUMBER;
291-
case 'S' -> LuaType.STRING;
292-
case 'U' -> LuaType.UNKNOWN;
293-
case 'T' -> throw new UnsupportedOperationException("todo");
294-
case 'F' -> throw new UnsupportedOperationException("todo");
295-
default -> throw new IllegalArgumentException("unknown type: " + str.charAt(i));
296-
});
297-
}
298-
return types;
299-
}
300-
301282
/**
302283
* Name of type for Lua code.
303284
* @return Lua type name.
@@ -317,4 +298,8 @@ public static List<LuaType> readList(String str) {
317298
default boolean isAssignableFrom(LuaType other) {
318299
return this == LuaType.UNKNOWN || equals(other);
319300
}
301+
302+
default boolean isNumber() {
303+
return this == LuaType.INTEGER || this == LuaType.FLOAT;
304+
}
320305
}

lua4jvm/src/main/java/fi/benjami/code4jvm/lua/ir/LuaTypeSupport.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,21 @@ class LuaTypeSupport {
1010
public static final Map<Type, LuaType> TYPE_TO_TYPE = Map.of(
1111
Type.BOOLEAN, LuaType.BOOLEAN,
1212
Type.of(Boolean.class), LuaType.BOOLEAN,
13-
Type.DOUBLE, LuaType.NUMBER,
14-
Type.of(Double.class), LuaType.NUMBER,
13+
Type.INT, LuaType.INTEGER,
14+
Type.of(Integer.class), LuaType.INTEGER,
15+
Type.DOUBLE, LuaType.FLOAT,
16+
Type.of(Double.class), LuaType.FLOAT,
1517
Type.STRING, LuaType.STRING,
1618
LuaTable.TYPE, LuaType.TABLE
1719
);
1820

1921
public static final Map<Class<?>, LuaType> CLASS_TO_TYPE = Map.of(
2022
boolean.class, LuaType.BOOLEAN,
2123
Boolean.class, LuaType.BOOLEAN,
22-
double.class, LuaType.NUMBER,
23-
Double.class, LuaType.NUMBER,
24+
int.class, LuaType.INTEGER,
25+
Integer.class, LuaType.INTEGER,
26+
double.class, LuaType.FLOAT,
27+
Double.class, LuaType.FLOAT,
2428
String.class, LuaType.STRING,
2529
LuaTable.class, LuaType.TABLE
2630
);

lua4jvm/src/main/java/fi/benjami/code4jvm/lua/ir/expr/ArithmeticExpr.java

Lines changed: 67 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import java.lang.invoke.MethodHandle;
44
import java.lang.invoke.MethodHandles;
55
import java.lang.invoke.MethodType;
6+
import java.util.List;
67
import java.util.function.BiFunction;
78

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

5458
Kind(BiFunction<Value, Value, Expression> directEmitter, String methodName, String metamethod) {
5559
this.directEmitter = directEmitter;
56-
MethodHandle fastPath;
60+
var intReturnType = methodName == "power" || methodName.equals("divide") ? double.class : int.class;
61+
MethodHandle doublePath, intPath;
5762
try {
5863
// Drop the call target argument, it is not needed
59-
fastPath = MethodHandles.dropArguments(LOOKUP.findStatic(ArithmeticExpr.class, methodName,
64+
doublePath = MethodHandles.dropArguments(LOOKUP.findStatic(ArithmeticExpr.class, methodName,
6065
MethodType.methodType(double.class, double.class, double.class)), 0, Object.class);
66+
intPath = MethodHandles.dropArguments(LOOKUP.findStatic(ArithmeticExpr.class, methodName,
67+
MethodType.methodType(intReturnType, int.class, int.class)), 0, Object.class);
6168
} catch (NoSuchMethodException | IllegalAccessException e) {
6269
throw new AssertionError(e);
6370
}
64-
this.callTarget = BinaryOp.newTarget(Double.class, fastPath, metamethod,
65-
(a, b) -> new LuaException("attempted to perform arithmetic on non-number values"));
71+
// If we have any doubles at all, take the double path
72+
var paths = List.of(
73+
new BinaryOp.Path(Integer.class, Integer.class, intPath),
74+
new BinaryOp.Path(Double.class, Double.class, doublePath),
75+
new BinaryOp.Path(Integer.class, Double.class, MethodHandles.explicitCastArguments(doublePath, MethodType.methodType(double.class, Object.class, int.class, double.class))),
76+
new BinaryOp.Path(Double.class, Integer.class, MethodHandles.explicitCastArguments(doublePath, MethodType.methodType(double.class, Object.class, double.class, int.class)))
77+
);
78+
this.callTarget = BinaryOp.newTarget(paths, metamethod,
79+
(a, b) -> new LuaException("cannot " + methodName + " "
80+
+ LuaType.of(a).name() + " and " + LuaType.of(b).name()));
6681
}
6782
}
6883

@@ -102,11 +117,44 @@ private static double subtract(double lhs, double rhs) {
102117
return lhs - rhs;
103118
}
104119

120+
@SuppressWarnings("unused")
121+
private static double power(int lhs, int rhs) {
122+
return Math.pow(lhs, rhs);
123+
}
124+
125+
@SuppressWarnings("unused")
126+
private static int multiply(int lhs, int rhs) {
127+
return lhs * rhs;
128+
}
129+
130+
private static double divide(int lhs, int rhs) {
131+
return ((double) lhs) / ((double) rhs);
132+
}
133+
134+
public static int floorDivide(int lhs, int rhs) {
135+
return (int) Math.floor(divide(lhs, rhs));
136+
}
137+
138+
@SuppressWarnings("unused")
139+
private static int modulo(int lhs, int rhs) {
140+
return Math.abs(lhs % rhs);
141+
}
142+
143+
@SuppressWarnings("unused")
144+
private static int add(int lhs, int rhs) {
145+
return lhs + rhs;
146+
}
147+
148+
@SuppressWarnings("unused")
149+
private static int subtract(int lhs, int rhs) {
150+
return lhs - rhs;
151+
}
152+
105153
@Override
106154
public Value emit(LuaContext ctx, Block block) {
107155
var lhsValue = lhs.emit(ctx, block);
108156
var rhsValue = rhs.emit(ctx, block);
109-
if (outputType(ctx).equals(LuaType.NUMBER)) {
157+
if (outputType(ctx).isNumber()) {
110158
// Both arguments are known to be numbers; emit arithmetic operation directly
111159
return block.add(kind.directEmitter.apply(lhsValue, rhsValue));
112160
} else {
@@ -117,8 +165,19 @@ public Value emit(LuaContext ctx, Block block) {
117165

118166
@Override
119167
public LuaType outputType(LuaContext ctx) {
120-
return lhs.outputType(ctx).equals(LuaType.NUMBER) && rhs.outputType(ctx).equals(LuaType.NUMBER)
121-
? LuaType.NUMBER : LuaType.UNKNOWN;
168+
var lhsOut = lhs.outputType(ctx);
169+
var rhsOut = rhs.outputType(ctx);
170+
if (lhsOut.isNumber() && rhsOut.isNumber()) {
171+
if (kind == Kind.POWER || kind == Kind.DIVIDE) {
172+
// Lua spec says that these always produce floats
173+
return LuaType.FLOAT;
174+
} else if (lhsOut == LuaType.INTEGER && rhsOut == LuaType.INTEGER) {
175+
return LuaType.INTEGER; // Both sides are integers
176+
}
177+
return LuaType.FLOAT; // Float on at least one side
178+
} else {
179+
return LuaType.UNKNOWN;
180+
}
122181
}
123182

124183
}

lua4jvm/src/main/java/fi/benjami/code4jvm/lua/ir/expr/LengthExpr.java

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,8 @@ public record LengthExpr(IrNode expr) implements IrNode {
2525
static {
2626
var lookup = MethodHandles.lookup();
2727
try {
28-
TABLE_LENGTH = MethodHandles.dropArguments(lookup.findVirtual(LuaTable.class, "arraySize", MethodType.methodType(int.class))
29-
.asType(MethodType.methodType(double.class, LuaTable.class)), 0, Object.class);
30-
STRING_LENGTH = MethodHandles.dropArguments(lookup.findVirtual(String.class, "length", MethodType.methodType(int.class))
31-
.asType(MethodType.methodType(double.class, String.class)), 0, Object.class);
28+
TABLE_LENGTH = MethodHandles.dropArguments(lookup.findVirtual(LuaTable.class, "arraySize", MethodType.methodType(int.class)), 0, Object.class);
29+
STRING_LENGTH = MethodHandles.dropArguments(lookup.findVirtual(String.class, "length", MethodType.methodType(int.class)), 0, Object.class);
3230
} catch (NoSuchMethodException | IllegalAccessException e) {
3331
throw new AssertionError(e);
3432
}
@@ -50,7 +48,7 @@ public Value emit(LuaContext ctx, Block block) {
5048
@Override
5149
public LuaType outputType(LuaContext ctx) {
5250
// We can't do type analysis through metatables (yet)
53-
return expr.outputType(ctx).equals(LuaType.STRING) ? LuaType.NUMBER : LuaType.UNKNOWN;
51+
return expr.outputType(ctx).equals(LuaType.STRING) ? LuaType.INTEGER : LuaType.UNKNOWN;
5452
}
5553

5654
}

lua4jvm/src/main/java/fi/benjami/code4jvm/lua/ir/expr/LuaConstant.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ public Value emit(LuaContext ctx, Block block) {
2727
return Constant.nullValue(Type.OBJECT);
2828
} else if (value instanceof Boolean bool) {
2929
return Constant.of(bool);
30+
} else if (value instanceof Integer num) {
31+
return Constant.of(num);
3032
} else if (value instanceof Double num) {
3133
return Constant.of(num);
3234
} else if (value instanceof String str) {

lua4jvm/src/main/java/fi/benjami/code4jvm/lua/ir/expr/NegateExpr.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ private static double negate(Object callable, double value) {
4141
@Override
4242
public Value emit(LuaContext ctx, Block block) {
4343
var value = expr.emit(ctx, block);
44-
if (outputType(ctx).equals(LuaType.NUMBER)) {
44+
if (outputType(ctx).isNumber()) {
4545
return block.add(Arithmetic.negate(value));
4646
} else {
4747
return block.add(LuaLinker.setupCall(ctx, CallSiteOptions.nonFunction(ctx.owner(), LuaType.UNKNOWN, LuaType.UNKNOWN), TARGET, value));
@@ -50,8 +50,14 @@ public Value emit(LuaContext ctx, Block block) {
5050

5151
@Override
5252
public LuaType outputType(LuaContext ctx) {
53+
var exprType = expr.outputType(ctx);
54+
if (exprType == LuaType.INTEGER) {
55+
return LuaType.INTEGER;
56+
} else if (exprType == LuaType.FLOAT) {
57+
return LuaType.FLOAT;
58+
}
5359
// We can't do type analysis through metatables (yet)
54-
return expr.outputType(ctx).equals(LuaType.NUMBER) ? LuaType.NUMBER : LuaType.UNKNOWN;
60+
return LuaType.UNKNOWN;
5561
}
5662

5763
}

lua4jvm/src/main/java/fi/benjami/code4jvm/lua/ir/expr/StringConcatExpr.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public record StringConcatExpr(
3535
throw new AssertionError();
3636
}
3737

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

lua4jvm/src/main/java/fi/benjami/code4jvm/lua/linker/BinaryOp.java

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import java.lang.invoke.MethodHandle;
44
import java.lang.invoke.MethodHandles;
55
import java.lang.invoke.MethodType;
6+
import java.util.List;
67
import java.util.function.BiFunction;
78

89
import fi.benjami.code4jvm.lua.ir.LuaType;
@@ -19,8 +20,8 @@
1920
*/
2021
public class BinaryOp {
2122

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

2627
@SuppressWarnings("unused") // MethodHandle
@@ -37,38 +38,44 @@ private static final boolean checkLhsMetamethod(String metamethod, Object callab
3738
var lookup = MethodHandles.lookup();
3839
try {
3940
CHECK_TYPES = lookup.findStatic(BinaryOp.class, "checkTypes",
40-
MethodType.methodType(boolean.class, Class.class, Object.class, Object.class, Object.class));
41+
MethodType.methodType(boolean.class, Class.class, Class.class, Object.class, Object.class, Object.class));
4142
CHECK_LHS_METAMETHOD = lookup.findStatic(BinaryOp.class, "checkLhsMetamethod",
4243
MethodType.methodType(boolean.class, String.class, Object.class, Object.class));
4344
} catch (NoSuchMethodException | IllegalAccessException e) {
4445
throw new AssertionError(e);
4546
}
4647
}
4748

49+
public record Path(
50+
Class<?> lhsType,
51+
Class<?> rhsType,
52+
MethodHandle target
53+
) {}
54+
4855
/**
4956
* Produces a dynamic call target for a binary operation call site.
50-
* @param expectedType Type that the fast path supports.
51-
* @param fastPath Fast path that is entered if both sides are of expected
52-
* type. The fast path should accept the call target as first parameter,
53-
* LHS as second and RHS as third.
57+
* @param fastPaths Fast paths, to be evaluated in order.
5458
* @param metamethod Name of the metamethod call if metatables are present.
5559
* @param errorHandler Called when either value has invalid type and
5660
* metamethods are not found. Returns a Lua exception that is thrown.
5761
* @return Call target.
5862
*/
59-
public static DynamicTarget newTarget(Class<?> expectedType, MethodHandle fastPath, String metamethod,
63+
public static DynamicTarget newTarget(List<Path> fastPaths, String metamethod,
6064
BiFunction<Object, Object, LuaException> errorHandler) {
61-
assert !expectedType.isPrimitive(); // LHS and RHS will be in their boxed forms
62-
assert !expectedType.equals(LuaType.class); // This is currently unnecessary for Lua
6365
return (meta, args) -> {
6466
assert args.length == 2;
6567
var lhs = args[0];
6668
var rhs = args[1];
67-
if (checkTypes(expectedType, null, lhs, rhs)) {
68-
// Fast path, e.g. arithmetic operation on numbers or string concatenation on strings
69-
var guard = CHECK_TYPES.bindTo(expectedType);
70-
return new LuaCallTarget(fastPath, guard);
71-
} else if (lhs instanceof LuaTable table
69+
for (var path : fastPaths) {
70+
if (checkTypes(path.lhsType, path.rhsType, null, lhs, rhs)) {
71+
// Fast path, e.g. arithmetic operation on numbers or string concatenation on strings
72+
var guard = MethodHandles.insertArguments(CHECK_TYPES, 0, path.lhsType, path.rhsType);
73+
return new LuaCallTarget(path.target, guard);
74+
}
75+
}
76+
77+
// None of the fast paths matched
78+
if (lhs instanceof LuaTable table
7279
&& table.metatable() != null
7380
&& table.metatable().get(metamethod) != null) {
7481
// Slower path, call LHS metamethod

0 commit comments

Comments
 (0)