Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master' into java-6
Browse files Browse the repository at this point in the history
  • Loading branch information
SquidDev committed Feb 28, 2024
2 parents 6b0d2e2 + 3a7f603 commit 6536189
Show file tree
Hide file tree
Showing 227 changed files with 7,268 additions and 5,409 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ hs_err_pid*
/build
/.gradle
/build-tools/build
/libs
/*.txt
/*.out
/jmh-result*.json

# Editors
*.iml
Expand Down
32 changes: 24 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,28 @@
# Cobalt [![Current build status](https://github.com/SquidDev/Cobalt/workflows/Build/badge.svg)](https://github.com/SquidDev/Cobalt/actions "Current build status")
# Cobalt
Cobalt is an Lua implementation for Java, designed for use in the Minecraft mod
[CC: Tweaked]. Is is based on [LuaJ 2.0][LuaJ], though has diverged
significantly over the years.

## What?
Cobalt is a fork of LuaJ 2.0 (Lua 5.1) with many features of LuaJ 3.0 backported.
## Features
Cobalt implements a (mostly) compliant Lua 5.1 implementation, with several
interesting additional features:

It allows multiple Lua instances to be run at once, with no shared metatables.
- Backports much of the Lua 5.2-5.4 standard library.
- Allows yielding _anywhere_ within a Lua program, including debug hooks and
any inside any native function.
- Support for interrupting and resuming the VM at arbitrary points.
- Efficient concatenation of strings using ropes.

## Why?
LuaJ 2.0 had too many bugs, mostly minor but annoying. Cobalt is an attempt to slim down LuaJ (only the JSE will be supported) and fix most of the bugs.
## Using
Don't.

## But Lua 5.1 is outdated!
I am considering having a separate Lua 5.3 branch but that is not an immediate priority right now.
No seriously, don't. Cobalt is developed in-sync with CC: Tweaked, and so grows
and changes according to the mod's needs. There is no guarantee of API stability
between versions. It makes many design decisions which make sense for CC, but
not for anything which needs a normal Lua implementation.

Instead I recommend using one of the alternative Lua implementations, like
LuaJ, JNLua or Rembulan.

[CC: Tweaked]: https://github.com/cc-tweaked/CC-Tweaked "cc-tweaked/CC-Tweaked: Just another ComputerCraft fork"
[LuaJ]: https://github.com/luaj/luaj "luaj/luaj: Lightweight, fast, Java-centric Lua interpreter written for JME and JSE."
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ private val subclassRelations = mapOf(
UnorderedPair("org/squiddev/cobalt/LuaValue", "org/squiddev/cobalt/LuaBoolean") to "org/squiddev/cobalt/LuaValue",
UnorderedPair("org/squiddev/cobalt/LuaValue", "org/squiddev/cobalt/LuaString") to "org/squiddev/cobalt/LuaValue",
UnorderedPair("org/squiddev/cobalt/Varargs", "org/squiddev/cobalt/LuaValue") to "org/squiddev/cobalt/Varargs",
UnorderedPair("org/squiddev/cobalt/LuaError", "org/squiddev/cobalt/compiler/CompileException") to "java/lang/Exception",
)

fun getCommonSuperclass(type1: String, type2: String): String? = subclassRelations[UnorderedPair(type1, type2)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,15 @@ class CompatibilityChecker(cv: ClassVisitor) : ClassRemapper(Opcodes.ASM9, cv, C
super.visitNestMember(nestMember)
}

override fun visitMethod(access: Int, name: String?, descriptor: String?, signature: String?, exceptions: Array<out String>?): MethodVisitor {
override fun visitMethod(access: Int, name: String?, descriptor: String?, signature: String?, exceptions: Array<out String>?): MethodVisitor? {
if (isInterface && name != "<clinit>") {
if ((access and Opcodes.ACC_STATIC) != 0) logger.error("Unsupported static method {}.{} on an interface", className, name)
else if ((access and Opcodes.ACC_ABSTRACT) == 0) logger.error("Unsupported default method {}.{} on an interface", className, name)
if ((access and Opcodes.ACC_STATIC) != 0) {
logger.error("Unsupported static method {}.{} on an interface", className, name)
return null
} else if ((access and Opcodes.ACC_ABSTRACT) == 0) {
logger.error("Unsupported default method {}.{} on an interface", className, name)
return null
}
}

return super.visitMethod(access, name, descriptor, signature, exceptions)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ fun AbstractInsnNode.isConstant(): Boolean = when (opcode) {
val Type.isReference: Boolean
get() = when (sort) {
Type.OBJECT, Type.ARRAY -> true
Type.BOOLEAN, Type.CHAR, Type.SHORT, Type.INT, Type.FLOAT, Type.DOUBLE, Type.LONG -> false
Type.BOOLEAN, Type.BYTE, Type.CHAR, Type.SHORT, Type.INT, Type.FLOAT, Type.DOUBLE, Type.LONG -> false
else -> throw IllegalArgumentException("$this is not a value type")
}

Expand All @@ -150,7 +150,7 @@ val Type.isReference: Boolean
* @see Type.getOpcode for the other cases.
*/
fun Type.getDefaultOpcode(): Int = when (sort) {
Type.BOOLEAN, Type.CHAR, Type.SHORT, Type.INT -> ICONST_0
Type.BOOLEAN, Type.BYTE, Type.CHAR, Type.SHORT, Type.INT -> ICONST_0
Type.FLOAT -> FCONST_0
Type.DOUBLE -> DCONST_0
Type.LONG -> LCONST_0
Expand All @@ -176,6 +176,7 @@ fun MethodVisitor.visitLoadInt(i: Int) = when (i) {
val Type.boxedName: String?
get() = when (sort) {
Type.BOOLEAN -> "java/lang/Boolean"
Type.BYTE -> "java/lang/Byte"
Type.CHAR -> "java/lang/Character"
Type.SHORT -> "java/lang/Short"
Type.INT -> "java/lang/Integer"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ interface DefinitionData {

private val builtinMethods = run {
val opHelper = "org/squiddev/cobalt/OperationHelper"
val dispatch = "org/squiddev/cobalt/function/Dispatch"
val first = YieldType.Direct { mw ->
mw.visitMethodInsn(INVOKEVIRTUAL, VARARGS.internalName, "first", "()${LUA_VALUE.descriptor}", false)
}
Expand All @@ -73,10 +74,11 @@ private val builtinMethods = run {

mapOf(
Desc("org/squiddev/cobalt/compiler/InputReader", "read", "()I") to YieldType.Resume,
Desc("org/squiddev/cobalt/unwind/SuspendedFunction", "call", "(Lorg/squiddev/cobalt/LuaState;)Ljava/lang/Object;") to YieldType.Resume,
Desc(dispatch, "call", Type.getMethodDescriptor(LUA_VALUE, LUA_STATE, LUA_VALUE, LUA_VALUE)) to first,
Desc(dispatch, "call", Type.getMethodDescriptor(LUA_VALUE, LUA_STATE, LUA_VALUE, LUA_VALUE, LUA_VALUE)) to first,
Desc(opHelper, "eq", Type.getMethodDescriptor(Type.BOOLEAN_TYPE, LUA_STATE, LUA_VALUE, LUA_VALUE)) to firstBool,
Desc(opHelper, "lt", Type.getMethodDescriptor(Type.BOOLEAN_TYPE, LUA_STATE, LUA_VALUE, LUA_VALUE)) to firstBool,
Desc(opHelper, "call", Type.getMethodDescriptor(LUA_VALUE, LUA_STATE, LUA_VALUE, LUA_VALUE)) to first,
Desc(opHelper, "call", Type.getMethodDescriptor(LUA_VALUE, LUA_STATE, LUA_VALUE, LUA_VALUE, LUA_VALUE)) to first,
Desc(opHelper, "length", Type.getMethodDescriptor(LUA_VALUE, LUA_STATE, LUA_VALUE)) to first,
Desc(opHelper, "getTable", Type.getMethodDescriptor(LUA_VALUE, LUA_STATE, LUA_VALUE, Type.INT_TYPE)) to first,
Desc(opHelper, "getTable", Type.getMethodDescriptor(LUA_VALUE, LUA_STATE, LUA_VALUE, LUA_VALUE)) to first,
Expand All @@ -102,7 +104,7 @@ class DefinitionScanner : DefinitionData {
private var autoUnwind: Boolean = false
var instrument: Boolean = false

override fun visit(version: Int, access: Int, name: String, signature: String?, superName: String, interfaces: Array<out String>?) {
override fun visit(version: Int, access: Int, name: String, signature: String?, superName: String?, interfaces: Array<out String>?) {
className = name
super.visit(version, access, name, signature, superName, interfaces)
}
Expand Down Expand Up @@ -153,15 +155,15 @@ class DefinitionScanner : DefinitionData {
clearLambda()

// If we're invoking the lambda metafactory to make an Action, then keep track of this lambda.
if (name == "run" && Type.getReturnType(descriptor) == SUSPENDED_TASK_ACTION && bootstrapMethodHandle == LAMBDA_METAFACTORY) {
if (name == "run" && Type.getReturnType(descriptor) == SUSPENDED_ACTION && bootstrapMethodHandle == LAMBDA_METAFACTORY) {
lastLambda = bootstrapMethodArguments[1] as Handle
}
}

override fun visitMethodInsn(opcode: Int, owner: String, callName: String, callDesc: String, isInterface: Boolean) {
// If we're invoking a static method on SuspendedTask, then both this function and the lambda should
// be instrumented.
if (opcode == INVOKESTATIC && owner == SUSPENDED_TASK.internalName) {
if (opcode == INVOKESTATIC && owner == SUSPENDED_ACTION.internalName) {
val lastLambda = this.lastLambda
if (lastLambda == null || !lastLambda.name.contains("lambda")) {
throw UnsupportedConstruct("$className.$name calls $owner.$callName with a non-lambda argument.")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
package cc.tweaked.cobalt.build.coroutine

import cc.tweaked.cobalt.build.*
import org.objectweb.asm.Handle
import org.objectweb.asm.Label
import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.*
import org.objectweb.asm.Opcodes.*
import org.objectweb.asm.Type
import org.objectweb.asm.commons.LocalVariablesSorter
import org.objectweb.asm.tree.*
import kotlin.math.ceil
Expand Down Expand Up @@ -60,6 +57,33 @@ private fun makeStateClass(generator: ClassEmitter, yieldPoints: List<YieldPoint
constructor.visitMaxs(1, 1)
constructor.visitEnd()

// Create a helper function in the style of UnwindState.getOrCreate. We call this function a lot, so it's worth
// moving it into its own helper.
cw.visitMethod(ACC_PUBLIC.or(ACC_STATIC), "getOrCreate", "(${OBJECT.descriptor})L$name;", null, null)
.also { mw ->
mw.visitCode()

val isNotNull = Label()
val endIf = Label()
mw.visitVarInsn(ALOAD, 0)
mw.visitJumpInsn(IFNONNULL, isNotNull)
// if(null): new State();
mw.visitTypeInsn(NEW, name)
mw.visitInsn(DUP)
mw.visitMethodInsn(INVOKESPECIAL, name, "<init>", "()V", false)
mw.visitJumpInsn(GOTO, endIf)
// if(non-null): (State) state
mw.visitLabel(isNotNull)
mw.visitVarInsn(ALOAD, 0)
mw.visitTypeInsn(CHECKCAST, name)
// end
mw.visitLabel(endIf)
mw.visitInsn(ARETURN)

mw.visitMaxs(2, 1)
mw.visitEnd()
}

cw.visitEnd()
}

Expand All @@ -74,23 +98,24 @@ private fun getExceptionType(yieldType: YieldType): Type = when (yieldType) {
}

/** Add labels after each yield point, so we can insert our try/catch handler. */
private fun instrumentMethodCalls(method: MethodNode, yieldPoints: List<YieldPoint>): List<Label> = yieldPoints.map { yieldPoint ->
// TODO: This method (especially adding the null argument) is conceptually quite nasty. It'd be nice to mutate the
// method node, but it's not the end of the world.

// @AutoUnwind calls require an implicit state argument. Note this must appear BEFORE the label.
if (yieldPoint.yieldType == YieldType.AutoUnwind) method.instructions.insertBefore(yieldPoint.label, InsnNode(ACONST_NULL))

// We also require a label after each resume point, so we can wrap it in a try block.
when (val after = yieldPoint.yieldAt.next) {
is LabelNode -> after.label
else -> {
val label = LabelNode()
method.instructions.insert(yieldPoint.yieldAt, label)
label.label
private fun instrumentMethodCalls(method: MethodNode, yieldPoints: List<YieldPoint>): List<Label> =
yieldPoints.map { yieldPoint ->
// TODO: This method (especially adding the null argument) is conceptually quite nasty. It'd be nice to mutate the
// method node, but it's not the end of the world.

// @AutoUnwind calls require an implicit state argument. Note this must appear BEFORE the label.
if (yieldPoint.yieldType == YieldType.AutoUnwind) method.instructions.insertBefore(yieldPoint.label, InsnNode(ACONST_NULL))

// We also require a label after each resume point, so we can wrap it in a try block.
when (val after = yieldPoint.yieldAt.next) {
is LabelNode -> after.label
else -> {
val label = LabelNode()
method.instructions.insert(yieldPoint.yieldAt, label)
label.label
}
}
}
}

/**
* The main rewriter for @AutoUnwind-annotated functions.
Expand Down Expand Up @@ -156,7 +181,14 @@ private class AutoUnwindRewriter(
// switch(state.state) {
val invalidState = Label()
sink.visitFieldInsn(GETFIELD, state.internalName, "state", "I")
sink.visitTableSwitchInsn(0, yieldPoints.size - 1, invalidState, *rebuild.toTypedArray())
if (yieldPoints.size == 1) {
// TABLESWITCH instructions are very large - in the trivial case we can do a much smaller comparison. We
// could probably do an if-else chain for small sizes too, but then we have to load the state multiple times
// so the trade-offs are less clear.
sink.visitJumpInsn(IFEQ, rebuild[0])
} else {
sink.visitTableSwitchInsn(0, yieldPoints.size - 1, invalidState, *rebuild.toTypedArray())
}

// default: throw new IllegalStateExeception("Resuming into unknown state")
sink.visitLabel(invalidState)
Expand Down Expand Up @@ -258,32 +290,17 @@ private class AutoUnwindRewriter(
val tryCatch = tryCatches[stateIdx]
sink.visitLabel(tryCatch)

// state = state == null ? new State() : (State)state;
val isNotNull = Label()
val endIf = Label()
sink.visitVarInsn(ALOAD, stateLocal)
sink.visitJumpInsn(IFNONNULL, isNotNull)
// if(null): new State();
sink.visitTypeInsn(NEW, state.internalName)
sink.visitInsn(DUP)
sink.visitMethodInsn(INVOKESPECIAL, state.internalName, "<init>", "()V", false)
sink.visitJumpInsn(GOTO, endIf)
// if(non-null): (State) state
sink.visitLabel(isNotNull)
// state = State.getOrCreate(state);
sink.visitVarInsn(ALOAD, stateLocal)
sink.visitTypeInsn(CHECKCAST, state.internalName)
// end
sink.visitLabel(endIf)
sink.visitVarInsn(ASTORE, stateLocal) // Actually the store is done later, but same difference.
sink.visitMethodInsn(INVOKESTATIC, state.internalName, "getOrCreate", "(${OBJECT.descriptor})${state.descriptor}", false)
sink.visitVarInsn(ASTORE, stateLocal)

when (yieldPoint.yieldType) {
is YieldType.AutoUnwind -> {
// state.child = e.child
// e.pushState(child)
sink.visitInsn(DUP)
sink.visitVarInsn(ALOAD, stateLocal)
sink.visitInsn(SWAP)
sink.visitFieldInsn(GETFIELD, PAUSE.internalName, "state", OBJECT.descriptor)
sink.visitFieldInsn(PUTFIELD, UNWIND_STATE.internalName, "child", OBJECT.descriptor)
sink.visitMethodInsn(INVOKEVIRTUAL, PAUSE.internalName, "pushState", "(${UNWIND_STATE.descriptor})V", false)
}

is YieldType.Resume, is YieldType.Direct -> {
Expand Down Expand Up @@ -323,11 +340,6 @@ private class AutoUnwindRewriter(
}
}

// exception.state = state
sink.visitInsn(DUP)
sink.visitVarInsn(ALOAD, stateLocal)
sink.visitFieldInsn(PUTFIELD, PAUSE.internalName, "state", OBJECT.descriptor)

// throw exception
sink.visitInsn(ATHROW)
}
Expand All @@ -347,7 +359,7 @@ private class AutoUnwindRewriter(
private fun packValue(type: Type): Unit = when (type.sort) {
Type.LONG -> Unit
// We widen an int to a long
Type.BOOLEAN, Type.CHAR, Type.SHORT, Type.INT -> sink.visitInsn(I2L)
Type.BOOLEAN, Type.BYTE, Type.CHAR, Type.SHORT, Type.INT -> sink.visitInsn(I2L)
// Floats and doubles are packed into longs.
Type.DOUBLE -> sink.visitMethodInsn(INVOKESTATIC, "java/lang/Double", "doubleToRawLongBits", "(D)J", false)
Type.FLOAT -> {
Expand All @@ -361,7 +373,7 @@ private class AutoUnwindRewriter(
/** Unpack a primitive value from a long. */
private fun unpackValue(type: Type): Unit = when (type.sort) {
Type.LONG -> Unit
Type.BOOLEAN, Type.CHAR, Type.SHORT, Type.INT -> sink.visitInsn(L2I)
Type.BOOLEAN, Type.BYTE, Type.CHAR, Type.SHORT, Type.INT -> sink.visitInsn(L2I)
Type.DOUBLE -> sink.visitMethodInsn(INVOKESTATIC, "java/lang/Double", "longBitsToDouble", "(J)D", false)
Type.FLOAT -> {
sink.visitInsn(L2I)
Expand Down Expand Up @@ -537,13 +549,13 @@ private fun makeSuspendedFunction(emitter: ClassEmitter, methodReference: Handle
}

/**
* Rewrite methods which call static functions on [SUSPENDED_TASK].
* Rewrite methods which call static functions on [SUSPENDED_ACTION].
*/
fun instrumentDispatch(method: MethodNode, emitter: ClassEmitter, mw: MethodVisitor) {
// We do our modifications directly on the MethodNode - it's a bit sad, but much easier as we're dealing with
// two adjacent nodes.
for (insn in method.instructions) {
if (insn.opcode != INVOKESTATIC || (insn as MethodInsnNode).owner != SUSPENDED_TASK.internalName) continue
if (insn.opcode != INVOKESTATIC || (insn as MethodInsnNode).owner != SUSPENDED_ACTION.internalName) continue

val invokeInsn = insn.previous as InvokeDynamicInsnNode
val methodReference = invokeInsn.bsmArgs[1] as Handle
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,20 @@ val LUA_ERROR = Type.getObjectType("org/squiddev/cobalt/LuaError")
/** The [org.squiddev.cobalt.UnwindThrowable] throwable. */
val UNWIND_THROWABLE = Type.getObjectType("org/squiddev/cobalt/UnwindThrowable")

/** The [org.squiddev.cobalt.unwind.AutoUnwind] annotation. */
val AUTO_UNWIND = Type.getObjectType("org/squiddev/cobalt/unwind/AutoUnwind")
/** The [cc.tweaked.cobalt.internal.unwind.AutoUnwind] annotation. */
val AUTO_UNWIND = Type.getObjectType("cc/tweaked/cobalt/internal/unwind/AutoUnwind")

/** The [org.squiddev.cobalt.unwind.Pause] throwable. */
val PAUSE = Type.getObjectType("org/squiddev/cobalt/unwind/Pause")
/** The [cc.tweaked.cobalt.internal.unwind.Pause] throwable. */
val PAUSE = Type.getObjectType("cc/tweaked/cobalt/internal/unwind/Pause")

/** The [org.squiddev.cobalt.unwind.UnwindState] class. */
val UNWIND_STATE = Type.getObjectType("org/squiddev/cobalt/unwind/UnwindState")
/** The [cc.tweaked.cobalt.internal.unwind.UnwindState] class. */
val UNWIND_STATE = Type.getObjectType("cc/tweaked/cobalt/internal/unwind/UnwindState")

/** The [org.squiddev.cobalt.unwind.SuspendedTask] class. */
val SUSPENDED_TASK = Type.getObjectType("org/squiddev/cobalt/unwind/SuspendedTask")

/** The [org.squiddev.cobalt.unwind.SuspendedTask.Action] class. */
val SUSPENDED_TASK_ACTION = Type.getObjectType("org/squiddev/cobalt/unwind/SuspendedTask\$Action")
/** The [cc.tweaked.cobalt.internal.unwind.SuspendedAction] class. */
val SUSPENDED_ACTION = Type.getObjectType("cc/tweaked/cobalt/internal/unwind/SuspendedAction")

/** The [org.squiddev.cobalt.unwind.SuspendedFunction] class. */
val SUSPENDED_FUNCTION = Type.getObjectType("org/squiddev/cobalt/unwind/SuspendedFunction")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,17 @@ private object DefinitionInterpreter : Interpreter<DefinedValue>(ASM9) {
else -> toValue(insn)
}

override fun binaryOperation(insn: AbstractInsnNode, value1: DefinedValue, value2: DefinedValue): DefinedValue? = toValue(insn)
override fun binaryOperation(insn: AbstractInsnNode, value1: DefinedValue, value2: DefinedValue): DefinedValue? =
when (insn.opcode) {
AALOAD -> {
val type = value1.type
if (type.sort != Type.ARRAY) throw IllegalStateException("Not an array.")
DefinedValue(type.elementType, Definition.Value(insn))
}

else -> toValue(insn)
}

override fun ternaryOperation(insn: AbstractInsnNode, value1: DefinedValue, value2: DefinedValue, value3: DefinedValue): DefinedValue? = toValue(insn)
override fun naryOperation(insn: AbstractInsnNode, values: MutableList<out DefinedValue>): DefinedValue? = toValue(insn)
}
Expand Down
Loading

0 comments on commit 6536189

Please sign in to comment.