From 797c5a298e14334ee9f67870e5f8428fb82d3f86 Mon Sep 17 00:00:00 2001 From: Daan Vanden Bosch Date: Sun, 25 Apr 2021 16:53:26 +0200 Subject: [PATCH] Fixed bugs in ASM editor signature help. The bytecode IR now contains all ASM instruction arguments, even when they are the wrong type or there are too many. --- .../phantasmal/core/PrimitiveExtensions.kt | 6 - .../phantasmal/core/PrimitiveExtensions.kt | 20 - .../phantasmal/core/PrimitiveExtensions.kt | 9 - .../world/phantasmal/lib/asm/Assembly.kt | 507 +++++++++--------- .../world/phantasmal/lib/asm/BytecodeIr.kt | 82 +-- .../world/phantasmal/lib/asm/Disassembly.kt | 5 +- .../asm/dataFlowAnalysis/ControlFlowGraph.kt | 12 +- .../dataFlowAnalysis/GetMapDesignations.kt | 11 +- .../asm/dataFlowAnalysis/GetRegisterValue.kt | 29 +- .../lib/asm/dataFlowAnalysis/GetStackValue.kt | 4 +- .../lib/fileFormats/quest/Bytecode.kt | 47 +- .../world/phantasmal/lib/asm/AssemblyTests.kt | 108 ++-- .../phantasmal/lib/asm/DisassemblyTests.kt | 19 +- .../world/phantasmal/lib/asm/OpcodeTests.kt | 6 +- .../lib/test/BytecodeIrAssertions.kt | 3 +- .../web/assemblyWorker/AsmAnalyser.kt | 40 +- .../web/assemblyWorker/AsmAnalyserTests.kt | 74 +++ .../conversion/NinjaGeometryConversion.kt | 22 +- .../phantasmal/web/test/TestComponents.kt | 12 +- 19 files changed, 549 insertions(+), 467 deletions(-) diff --git a/core/src/commonMain/kotlin/world/phantasmal/core/PrimitiveExtensions.kt b/core/src/commonMain/kotlin/world/phantasmal/core/PrimitiveExtensions.kt index 8945a4b0..b811a46f 100644 --- a/core/src/commonMain/kotlin/world/phantasmal/core/PrimitiveExtensions.kt +++ b/core/src/commonMain/kotlin/world/phantasmal/core/PrimitiveExtensions.kt @@ -30,9 +30,3 @@ fun Int.setBit(bit: Int, value: Boolean): Int = } else { this and (1 shl bit).inv() } - -expect fun Int.reinterpretAsFloat(): Float - -expect fun Float.reinterpretAsInt(): Int - -expect fun Float.reinterpretAsUInt(): UInt diff --git a/core/src/jsMain/kotlin/world/phantasmal/core/PrimitiveExtensions.kt b/core/src/jsMain/kotlin/world/phantasmal/core/PrimitiveExtensions.kt index f5ad909c..188cc613 100644 --- a/core/src/jsMain/kotlin/world/phantasmal/core/PrimitiveExtensions.kt +++ b/core/src/jsMain/kotlin/world/phantasmal/core/PrimitiveExtensions.kt @@ -1,10 +1,5 @@ package world.phantasmal.core -import org.khronos.webgl.ArrayBuffer -import org.khronos.webgl.DataView - -private val dataView = DataView(ArrayBuffer(4)) - @Suppress("NOTHING_TO_INLINE") actual inline fun Char.fastIsWhitespace(): Boolean = asDynamic() == 0x20 || (asDynamic() >= 0x09 && asDynamic() <= 0x0D) @@ -12,18 +7,3 @@ actual inline fun Char.fastIsWhitespace(): Boolean = @Suppress("NOTHING_TO_INLINE") actual inline fun Char.isDigit(): Boolean = asDynamic() >= 0x30 && asDynamic() <= 0x39 - -actual fun Int.reinterpretAsFloat(): Float { - dataView.setInt32(0, this) - return dataView.getFloat32(0) -} - -actual fun Float.reinterpretAsInt(): Int { - dataView.setFloat32(0, this) - return dataView.getInt32(0) -} - -actual fun Float.reinterpretAsUInt(): UInt { - dataView.setFloat32(0, this) - return dataView.getUint32(0).toUInt() -} diff --git a/core/src/jvmMain/kotlin/world/phantasmal/core/PrimitiveExtensions.kt b/core/src/jvmMain/kotlin/world/phantasmal/core/PrimitiveExtensions.kt index 629f41f5..eeb57f16 100644 --- a/core/src/jvmMain/kotlin/world/phantasmal/core/PrimitiveExtensions.kt +++ b/core/src/jvmMain/kotlin/world/phantasmal/core/PrimitiveExtensions.kt @@ -2,17 +2,8 @@ package world.phantasmal.core -import java.lang.Float.floatToIntBits -import java.lang.Float.intBitsToFloat - @Suppress("NOTHING_TO_INLINE") actual inline fun Char.fastIsWhitespace(): Boolean = isWhitespace() @Suppress("NOTHING_TO_INLINE") actual inline fun Char.isDigit(): Boolean = this in '0'..'9' - -actual fun Int.reinterpretAsFloat(): Float = intBitsToFloat(this) - -actual fun Float.reinterpretAsInt(): Int = floatToIntBits(this) - -actual fun Float.reinterpretAsUInt(): UInt = reinterpretAsInt().toUInt() diff --git a/lib/src/commonMain/kotlin/world/phantasmal/lib/asm/Assembly.kt b/lib/src/commonMain/kotlin/world/phantasmal/lib/asm/Assembly.kt index e82a9eec..1fc3b399 100644 --- a/lib/src/commonMain/kotlin/world/phantasmal/lib/asm/Assembly.kt +++ b/lib/src/commonMain/kotlin/world/phantasmal/lib/asm/Assembly.kt @@ -131,8 +131,9 @@ private class Assembler(private val asm: List, private val inlineStackAr opcode: Opcode, args: List, mnemonicSrcLoc: SrcLoc?, + valid: Boolean, argSrcLocs: List, - stackArgSrcLocs: List, + trailingArgSeparator: Boolean, ) { when (val seg = segment) { null -> { @@ -151,11 +152,12 @@ private class Assembler(private val asm: List, private val inlineStackAr Instruction( opcode, args, + valid, InstructionSrcLoc( mnemonic = mnemonicSrcLoc, args = argSrcLocs, - stackArgs = stackArgSrcLocs, - ) + trailingArgSeparator, + ), ) ) } @@ -359,200 +361,169 @@ private class Assembler(private val asm: List, private val inlineStackAr if (opcode == null) { addError("Unknown opcode.") } else { - // Inline arguments. - val inlineArgs = mutableListOf() - val inlineArgSrcLocs = mutableListOf() - // Stack arguments. - val stackArgs = mutableListOf() - val stackArgSrcLocs = mutableListOf() - if (opcode.stack !== StackInteraction.Pop) { - // Arguments should be inlined right after the opcode. - if (!parseArgs( - opcode, - mnemonicSrcLoc.col, - inlineArgs, - inlineArgSrcLocs, - stack = false, - ) - ) { - return - } + // Arguments should be inlined immediately after the opcode. + parseArgs( + opcode, + mnemonicSrcLoc, + stack = false, + ) } else { // Arguments should be passed to the opcode via the stack. - if (!parseArgs( - opcode, - mnemonicSrcLoc.col, - stackArgs, - stackArgSrcLocs, - stack = true, - ) - ) { - return - } + parseArgs( + opcode, + mnemonicSrcLoc, + stack = true, + ) } - - addInstruction( - opcode, - inlineArgs, - mnemonicSrcLoc, - inlineArgSrcLocs, - stackArgSrcLocs, - ) } } - /** - * Returns true iff arguments can be translated to byte code, possibly after truncation. - */ - private fun parseArgs( - opcode: Opcode, - startCol: Int, - args: MutableList, - srcLocs: MutableList, - stack: Boolean, - ): Boolean { + private fun parseArgs(opcode: Opcode, mnemonicSrcLoc: SrcLoc, stack: Boolean) { + val immediateArgs = mutableListOf() + val srcLocs = mutableListOf() var argCount = 0 - var semiValid = true + var valid = true var shouldBeArg = true var paramI = 0 + var prevToken: Token? var prevCol: Int var prevLen: Int + var token = tokenizer.type var col = tokenizer.col var len = tokenizer.len tokenizer.nextToken() while (true) { + // Previous token data. + prevToken = token prevCol = col prevLen = len - val token = tokenizer.type - val value = tokenizer.value + // Current token data. + token = tokenizer.type col = tokenizer.col len = tokenizer.len + val value = tokenizer.value if (token == null) { break } + // Next token data. tokenizer.nextToken() + val nextToken = tokenizer.type + val nextCol = tokenizer.col + val nextLen = tokenizer.len + + val param = opcode.params.getOrNull(paramI) + val paramType = param?.type + + // Coarse source position, including surrounding whitespace. val coarseCol = prevCol + prevLen val coarseLen = - if (tokenizer.type === Token.ArgSeparator) tokenizer.col + tokenizer.len - coarseCol - else tokenizer.col - coarseCol + if (nextToken === Token.ArgSeparator) nextCol + nextLen - coarseCol + else nextCol - coarseCol + + if (token === Token.ArgSeparator) { + if (shouldBeArg) { + addError("Expected an argument.") + } else if (param == null || !param.varargs) { + paramI++ + } + + shouldBeArg = true + } else { + if (!shouldBeArg) { + addError(coarseCol, col - coarseCol, "Expected a comma.") + } + + shouldBeArg = false - if (token !== Token.ArgSeparator) { argCount++ - } - if (paramI < opcode.params.size) { - val param = opcode.params[paramI] + // Try to match token type to parameter type. + var typeMatch: Boolean - if (token === Token.ArgSeparator) { - if (shouldBeArg) { - addError("Expected an argument.") - } else if (!param.varargs) { - paramI++ - } + // If arg is nonnull, types match and argument is syntactically valid. + val arg: Arg = when (token) { + Token.Int32 -> { + value as Int - shouldBeArg = true - } else { - if (!shouldBeArg) { - addError(coarseCol, col - coarseCol, "Expected a comma.") - } - - shouldBeArg = false - - // Try to match token type parameter type. - var typeMatch: Boolean - - // If arg is nonnull, types match and argument is syntactically valid. - val arg: Arg? = when (token) { - Token.Int32 -> { - value as Int - - when (param.type) { - ByteType -> { - typeMatch = true - intValueToArg(value, 1) - } - ShortType, - is LabelType, - -> { - typeMatch = true - intValueToArg(value, 2) - } - IntType -> { - typeMatch = true - intValueToArg(value, 4) - } - FloatType -> { - typeMatch = true - Arg(value.toFloat()) - } - else -> { - typeMatch = false - null - } + when (paramType) { + ByteType -> { + typeMatch = true + checkIntValue(col, len, value, 1) } - } - - Token.Float32 -> { - typeMatch = param.type === FloatType - - if (typeMatch) { - Arg(value as Float) - } else { - null + ShortType, + is LabelType, + -> { + typeMatch = true + checkIntValue(col, len, value, 2) } - } - - Token.Register -> { - value as Int - - typeMatch = stack || - param.type === RegVarType || - param.type is RegType - - if (value > 255) { - addError("Invalid register reference, expected r0-r255.") - null - } else { - Arg(value) + IntType -> { + typeMatch = true + checkIntValue(col, len, value, 4) } - } - - Token.Str -> { - typeMatch = param.type === StringType - - if (typeMatch) { - Arg(value as String) - } else { - null + FloatType -> { + typeMatch = true + FloatArg(value.toFloat()) + } + else -> { + typeMatch = false + IntArg(value) } - } - - else -> { - typeMatch = false - null } } - val srcLoc = ArgSrcLoc( - precise = SrcLoc(lineNo, col, len), - coarse = SrcLoc(lineNo, coarseCol, coarseLen), - ) - - if (arg != null) { - args.add(arg) - srcLocs.add(srcLoc) + Token.Float32 -> { + typeMatch = paramType === FloatType + FloatArg(value as Float) } - if (!typeMatch) { - semiValid = false + Token.Register -> { + typeMatch = stack || + paramType === RegVarType || + paramType is RegType - val typeStr: String? = when (param.type) { + value as Int + + if (value > 255) { + addError(col, len, "Invalid register reference, expected r0-r255.") + } + + IntArg(value) + } + + Token.Str -> { + typeMatch = paramType === StringType + StringArg(value as String) + } + + else -> { + typeMatch = false + UnknownArg(value!!) + } + } + + val srcLoc = ArgSrcLoc( + precise = SrcLoc(lineNo, col, len), + coarse = SrcLoc(lineNo, coarseCol, coarseLen), + ) + + if (!stack) { + immediateArgs.add(arg) + } + + srcLocs.add(srcLoc) + + if (!typeMatch) { + valid = false + + // Don't add a type errors for surplus arguments. + if (param != null) { + val typeStr = when (param.type) { ByteType -> "an 8-bit integer" ShortType -> "a 16-bit integer" IntType -> "a 32-bit integer" @@ -573,93 +544,103 @@ private class Assembler(private val asm: List, private val inlineStackAr is RegType, -> "a register reference" - else -> null + PointerType -> "a pointer" // No known opcodes directly take a pointer. + + AnyType.Instance -> "an argument" // Should never happen. } - addError( - if (typeStr == null) "Unexpected token." else "Expected ${typeStr}." - ) - } else if (stack && arg != null) { - // Inject stack push instructions if necessary. - // If the token is a register, push it as a register, otherwise coerce type. - if (token === Token.Register) { - if (param.type is RegType) { + addError(col, len, "Expected ${typeStr}.") + } + } else if (stack) { + // Inject stack push instructions if necessary. + checkNotNull(paramType) + + // If the token is a register, push it as a register, otherwise coerce type. + if (token === Token.Register) { + if (paramType is RegType) { + addInstruction( + OP_ARG_PUSHB, + listOf(arg), + mnemonicSrcLoc = null, + valid = true, + listOf(srcLoc), + trailingArgSeparator = false, + ) + } else { + addInstruction( + OP_ARG_PUSHR, + listOf(arg), + mnemonicSrcLoc = null, + valid = true, + listOf(srcLoc), + trailingArgSeparator = false, + ) + } + } else { + when (paramType) { + ByteType, + is RegType, + -> { addInstruction( OP_ARG_PUSHB, listOf(arg), - null, + mnemonicSrcLoc = null, + valid = true, listOf(srcLoc), - emptyList(), - ) - } else { - addInstruction( - OP_ARG_PUSHR, - listOf(arg), - null, - listOf(srcLoc), - emptyList(), + trailingArgSeparator = false, ) } - } else { - when (param.type) { - ByteType, - is RegType, - -> { - addInstruction( - OP_ARG_PUSHB, - listOf(arg), - null, - listOf(srcLoc), - emptyList(), - ) - } - ShortType, - is LabelType, - -> { - addInstruction( - OP_ARG_PUSHW, - listOf(arg), - null, - listOf(srcLoc), - emptyList(), - ) - } + ShortType, + is LabelType, + -> { + addInstruction( + OP_ARG_PUSHW, + listOf(arg), + mnemonicSrcLoc = null, + valid = true, + listOf(srcLoc), + trailingArgSeparator = false, + ) + } - IntType -> { - addInstruction( - OP_ARG_PUSHL, - listOf(arg), - null, - listOf(srcLoc), - emptyList(), - ) - } + IntType -> { + addInstruction( + OP_ARG_PUSHL, + listOf(arg), + mnemonicSrcLoc = null, + valid = true, + listOf(srcLoc), + trailingArgSeparator = false, + ) + } - FloatType -> { - addInstruction( - OP_ARG_PUSHL, - listOf(Arg((arg.value as Float).toRawBits())), - null, - listOf(srcLoc), - emptyList(), - ) - } + // Floats are pushed as ints. + FloatType -> { + addInstruction( + OP_ARG_PUSHL, + listOf(IntArg((arg as FloatArg).value.toRawBits())), + mnemonicSrcLoc = null, + valid = true, + listOf(srcLoc), + trailingArgSeparator = false, + ) + } - StringType -> { - addInstruction( - OP_ARG_PUSHS, - listOf(arg), - null, - listOf(srcLoc), - emptyList(), - ) - } + StringType -> { + addInstruction( + OP_ARG_PUSHS, + listOf(arg), + mnemonicSrcLoc = null, + valid = true, + listOf(srcLoc), + trailingArgSeparator = false, + ) + } - else -> { - logger.error { - "Line $lineNo: Type ${param.type::class} not implemented." - } + else -> { + logger.error { + "Line $lineNo: Type ${paramType::class} not implemented." } } } @@ -672,59 +653,67 @@ private class Assembler(private val asm: List, private val inlineStackAr if (!inlineStackArgs && opcode.stack === StackInteraction.Pop) 0 else opcode.params.size - val errorLength = prevCol + prevLen - startCol + val trailingArgSeparator = prevToken === Token.ArgSeparator - if (!opcode.varargs && argCount != paramCount) { - semiValid = argCount >= paramCount - addError( - startCol, - errorLength, - "Expected $paramCount argument${ - if (paramCount == 1) "" else "s" - }, got $argCount.", - ) - } else if (opcode.varargs && argCount < paramCount) { - semiValid = argCount >= paramCount - 1 - // TODO: This check assumes we want at least 1 argument for a vararg parameter. - // Is this correct? - addError( - startCol, - errorLength, - "Expected at least $paramCount argument${ - if (paramCount == 1) "" else "s" - }, got $argCount.", - ) + // Length from the start of the mnemonic until the end of the last token. + val errorLength = prevCol + prevLen - mnemonicSrcLoc.col + + if (opcode.varargs) { + // Argument count should be equal to or greater than the amount of parameters for variadic + // opcodes. + if (argCount < paramCount) { + valid = false + addError( + mnemonicSrcLoc.col, + errorLength, + "Expected at least $paramCount argument${ + if (paramCount == 1) "" else "s" + }, got $argCount.", + ) + } + } else { + // Argument count should match parameter count exactly for non-variadic opcodes. + if (argCount != paramCount) { + valid = false + addError( + mnemonicSrcLoc.col, + errorLength, + "Expected $paramCount argument${ + if (paramCount == 1) "" else "s" + }, got $argCount.", + ) + } } - return semiValid + // Trailing argument separators are not allowed. + if (trailingArgSeparator) { + addError(prevCol, prevLen, "Unexpected comma.") + } + + addInstruction(opcode, immediateArgs, mnemonicSrcLoc, valid, srcLocs, trailingArgSeparator) } - private fun intValueToArg(value: Int, size: Int): Arg? { + private fun checkIntValue(col: Int, len: Int, value: Int, size: Int): Arg { // Fast-path 32-bit ints for improved JS perf. Otherwise maxValue would have to be a Long // or UInt, which incurs a perf hit in JS. - if (size == 4) { - return Arg(value) - } else { + if (size != 4) { val bitSize = 8 * size // Minimum of the signed version of this integer type. val minValue = -(1 shl (bitSize - 1)) // Maximum of the unsigned version of this integer type. val maxValue = (1 shl (bitSize)) - 1 - return when { + when { value < minValue -> { - addError("${bitSize}-Bit integer can't be less than ${minValue}.") - null + addError(col, len, "${bitSize}-Bit integer can't be less than ${minValue}.") } value > maxValue -> { - addError("${bitSize}-Bit integer can't be greater than ${maxValue}.") - null - } - else -> { - Arg(value) + addError(col, len, "${bitSize}-Bit integer can't be greater than ${maxValue}.") } } } + + return IntArg(value) } private fun parseBytes() { diff --git a/lib/src/commonMain/kotlin/world/phantasmal/lib/asm/BytecodeIr.kt b/lib/src/commonMain/kotlin/world/phantasmal/lib/asm/BytecodeIr.kt index 854f2314..a0fa0ae0 100644 --- a/lib/src/commonMain/kotlin/world/phantasmal/lib/asm/BytecodeIr.kt +++ b/lib/src/commonMain/kotlin/world/phantasmal/lib/asm/BytecodeIr.kt @@ -103,6 +103,7 @@ class Instruction( * Immediate arguments for the opcode. */ val args: List, + val valid: Boolean, val srcLoc: InstructionSrcLoc?, ) { /** @@ -144,41 +145,19 @@ class Instruction( } /** - * Returns the source locations of the immediate arguments for the parameter at the given index. + * Returns the source locations of the (immediate or stack) arguments for the parameter at the + * given index. */ fun getArgSrcLocs(paramIndex: Int): List { val argSrcLocs = srcLoc?.args ?: return emptyList() - val param = opcode.params[paramIndex] - - // Variable length arguments are always last, so we can just gobble up all SrcLocs from - // paramIndex onward. - return if (param.varargs) { + return if (opcode.params[paramIndex].varargs) { + // Variadic parameters are always last, so we can just gobble up all SrcLocs from + // paramIndex onward. argSrcLocs.drop(paramIndex) } else { - listOf(argSrcLocs[paramIndex]) - } - } - - /** - * Returns the source locations of the stack arguments for the parameter at the given index. - */ - fun getStackArgSrcLocs(paramIndex: Int): List { - val argSrcLocs = srcLoc?.stackArgs - - if (argSrcLocs == null || paramIndex > argSrcLocs.lastIndex) { - return emptyList() - } - - val param = opcode.params[paramIndex] - - // Variable length arguments are always last, so we can just gobble up all SrcLocs from - // paramIndex onward. - return if (param.varargs) { - argSrcLocs.drop(paramIndex) - } else { - listOf(argSrcLocs[paramIndex]) + listOfNotNull(argSrcLocs.getOrNull(paramIndex)) } } @@ -210,9 +189,9 @@ class Instruction( StringType -> { if (dcGcFormat) { - (args[0].value as String).length + 1 + (args[0] as StringArg).value.length + 1 } else { - 2 * (args[0].value as String).length + 2 + 2 * (args[0] as StringArg).value.length + 2 } } @@ -232,13 +211,43 @@ class Instruction( } fun copy(): Instruction = - Instruction(opcode, args, srcLoc) + Instruction(opcode, args, valid, srcLoc).also { it.paramToArgs = paramToArgs } } /** * Instruction argument. */ -data class Arg(val value: Any) +sealed class Arg { + abstract val value: Any? + + abstract fun coerceInt(): Int + abstract fun coerceFloat(): Float + abstract fun coerceString(): String +} + +data class IntArg(override val value: Int) : Arg() { + override fun coerceInt(): Int = value + override fun coerceFloat(): Float = Float.fromBits(value) + override fun coerceString(): String = value.toString() +} + +data class FloatArg(override val value: Float) : Arg() { + override fun coerceInt(): Int = value.toRawBits() + override fun coerceFloat(): Float = value + override fun coerceString(): String = value.toString() +} + +data class StringArg(override val value: String) : Arg() { + override fun coerceInt(): Int = 0 + override fun coerceFloat(): Float = 0f + override fun coerceString(): String = value +} + +data class UnknownArg(override val value: Any?) : Arg() { + override fun coerceInt(): Int = 0 + override fun coerceFloat(): Float = 0f + override fun coerceString(): String = "" +} /** * Position and length of related source assembly code. @@ -254,8 +263,15 @@ class SrcLoc( */ class InstructionSrcLoc( val mnemonic: SrcLoc?, + /** + * Immediate or stack argument locations. + */ val args: List = emptyList(), - val stackArgs: List = emptyList(), + /** + * Does the instruction end with a comma? This can be the case when a user has partially typed + * an instruction. + */ + val trailingArgSeparator: Boolean, ) /** diff --git a/lib/src/commonMain/kotlin/world/phantasmal/lib/asm/Disassembly.kt b/lib/src/commonMain/kotlin/world/phantasmal/lib/asm/Disassembly.kt index 2df58bfa..b67a8b17 100644 --- a/lib/src/commonMain/kotlin/world/phantasmal/lib/asm/Disassembly.kt +++ b/lib/src/commonMain/kotlin/world/phantasmal/lib/asm/Disassembly.kt @@ -1,7 +1,6 @@ package world.phantasmal.lib.asm import mu.KotlinLogging -import world.phantasmal.core.reinterpretAsFloat import kotlin.math.min private val logger = KotlinLogging.logger {} @@ -212,7 +211,7 @@ private fun StringBuilder.appendArgs(params: List, args: List { // Floats are pushed onto the stack as integers with arg_pushl. if (stack) { - append((arg.value as Int).reinterpretAsFloat()) + append(Float.fromBits((arg as IntArg).value)) } else { append(arg.value) } @@ -241,7 +240,7 @@ private fun StringBuilder.appendArgs(params: List, args: List { - appendStringArg(arg.value as String) + appendStringArg((arg as StringArg).value) } else -> { diff --git a/lib/src/commonMain/kotlin/world/phantasmal/lib/asm/dataFlowAnalysis/ControlFlowGraph.kt b/lib/src/commonMain/kotlin/world/phantasmal/lib/asm/dataFlowAnalysis/ControlFlowGraph.kt index 181892d8..45f48584 100644 --- a/lib/src/commonMain/kotlin/world/phantasmal/lib/asm/dataFlowAnalysis/ControlFlowGraph.kt +++ b/lib/src/commonMain/kotlin/world/phantasmal/lib/asm/dataFlowAnalysis/ControlFlowGraph.kt @@ -167,7 +167,7 @@ private fun createBasicBlocks(cfg: ControlFlowGraphBuilder, segment: Instruction // Unconditional jump. OP_JMP.code -> { branchType = BranchType.Jump - branchLabels = listOf(inst.args[0].value as Int) + branchLabels = listOf((inst.args[0] as IntArg).value) } // Conditional jumps. @@ -175,7 +175,7 @@ private fun createBasicBlocks(cfg: ControlFlowGraphBuilder, segment: Instruction OP_JMP_OFF.code, -> { branchType = BranchType.ConditionalJump - branchLabels = listOf(inst.args[0].value as Int) + branchLabels = listOf((inst.args[0] as IntArg).value) } OP_JMP_E.code, OP_JMPI_E.code, @@ -199,11 +199,11 @@ private fun createBasicBlocks(cfg: ControlFlowGraphBuilder, segment: Instruction OP_JMPI_LE.code, -> { branchType = BranchType.ConditionalJump - branchLabels = listOf(inst.args[2].value as Int) + branchLabels = listOf((inst.args[2] as IntArg).value) } OP_SWITCH_JMP.code -> { branchType = BranchType.ConditionalJump - branchLabels = inst.args.drop(1).map { it.value as Int } + branchLabels = inst.args.drop(1).map { (it as IntArg).value } } // Calls. @@ -211,11 +211,11 @@ private fun createBasicBlocks(cfg: ControlFlowGraphBuilder, segment: Instruction OP_VA_CALL.code, -> { branchType = BranchType.Call - branchLabels = listOf(inst.args[0].value as Int) + branchLabels = listOf((inst.args[0] as IntArg).value) } OP_SWITCH_CALL.code -> { branchType = BranchType.Call - branchLabels = inst.args.drop(1).map { it.value as Int } + branchLabels = inst.args.drop(1).map { (it as IntArg).value } } // All other opcodes. diff --git a/lib/src/commonMain/kotlin/world/phantasmal/lib/asm/dataFlowAnalysis/GetMapDesignations.kt b/lib/src/commonMain/kotlin/world/phantasmal/lib/asm/dataFlowAnalysis/GetMapDesignations.kt index 5112ba45..09146d14 100644 --- a/lib/src/commonMain/kotlin/world/phantasmal/lib/asm/dataFlowAnalysis/GetMapDesignations.kt +++ b/lib/src/commonMain/kotlin/world/phantasmal/lib/asm/dataFlowAnalysis/GetMapDesignations.kt @@ -1,10 +1,7 @@ package world.phantasmal.lib.asm.dataFlowAnalysis import mu.KotlinLogging -import world.phantasmal.lib.asm.InstructionSegment -import world.phantasmal.lib.asm.OP_BB_MAP_DESIGNATE -import world.phantasmal.lib.asm.OP_MAP_DESIGNATE -import world.phantasmal.lib.asm.OP_MAP_DESIGNATE_EX +import world.phantasmal.lib.asm.* private val logger = KotlinLogging.logger {} @@ -24,7 +21,7 @@ fun getMapDesignations( cfg = createCfg() } - val areaId = getRegisterValue(cfg, inst, inst.args[0].value as Int) + val areaId = getRegisterValue(cfg, inst, (inst.args[0] as IntArg).value) if (areaId.size > 1) { logger.warn { @@ -34,7 +31,7 @@ fun getMapDesignations( } val variantIdRegister = - inst.args[0].value as Int + (if (inst.opcode == OP_MAP_DESIGNATE) 2 else 3) + (inst.args[0] as IntArg).value + (if (inst.opcode == OP_MAP_DESIGNATE) 2 else 3) val variantId = getRegisterValue(cfg, inst, variantIdRegister) if (variantId.size > 1) { @@ -48,7 +45,7 @@ fun getMapDesignations( } OP_BB_MAP_DESIGNATE.code -> { - mapDesignations[inst.args[0].value as Int] = inst.args[2].value as Int + mapDesignations[(inst.args[0] as IntArg).value] = (inst.args[2] as IntArg).value } } } diff --git a/lib/src/commonMain/kotlin/world/phantasmal/lib/asm/dataFlowAnalysis/GetRegisterValue.kt b/lib/src/commonMain/kotlin/world/phantasmal/lib/asm/dataFlowAnalysis/GetRegisterValue.kt index e1adb30d..5e3b56c0 100644 --- a/lib/src/commonMain/kotlin/world/phantasmal/lib/asm/dataFlowAnalysis/GetRegisterValue.kt +++ b/lib/src/commonMain/kotlin/world/phantasmal/lib/asm/dataFlowAnalysis/GetRegisterValue.kt @@ -58,7 +58,7 @@ private class RegisterValueFinder { OP_LET.code -> { if (args[0].value == register) { - return find(LinkedHashSet(path), block, i, args[1].value as Int) + return find(LinkedHashSet(path), block, i, (args[1] as IntArg).value) } } @@ -68,7 +68,7 @@ private class RegisterValueFinder { OP_SYNC_LETI.code, -> { if (args[0].value == register) { - return ValueSet.of(args[1].value as Int) + return ValueSet.of((args[1] as IntArg).value) } } @@ -101,7 +101,7 @@ private class RegisterValueFinder { OP_ADDI.code -> { if (args[0].value == register) { val prevVals = find(LinkedHashSet(path), block, i, register) - prevVals += args[1].value as Int + prevVals += (args[1] as IntArg).value return prevVals } } @@ -109,7 +109,7 @@ private class RegisterValueFinder { OP_SUBI.code -> { if (args[0].value == register) { val prevVals = find(LinkedHashSet(path), block, i, register) - prevVals -= args[1].value as Int + prevVals -= (args[1] as IntArg).value return prevVals } } @@ -117,7 +117,7 @@ private class RegisterValueFinder { OP_MULI.code -> { if (args[0].value == register) { val prevVals = find(LinkedHashSet(path), block, i, register) - prevVals *= args[1].value as Int + prevVals *= (args[1] as IntArg).value return prevVals } } @@ -125,7 +125,7 @@ private class RegisterValueFinder { OP_DIVI.code -> { if (args[0].value == register) { val prevVals = find(LinkedHashSet(path), block, i, register) - prevVals /= args[1].value as Int + prevVals /= (args[1] as IntArg).value return prevVals } } @@ -155,7 +155,7 @@ private class RegisterValueFinder { LinkedHashSet(path), block, i, - args[0].value as Int + (args[0] as IntArg).value ).minOrNull()!! val max = max( @@ -163,7 +163,7 @@ private class RegisterValueFinder { LinkedHashSet(path), block, i, - args[0].value as Int + 1 + (args[0] as IntArg).value + 1 ).maxOrNull()!!, min + 1, ) @@ -175,8 +175,8 @@ private class RegisterValueFinder { OP_STACK_PUSHM.code, OP_STACK_POPM.code, -> { - val minReg = args[0].value as Int - val maxReg = args[0].value as Int + args[1].value as Int + val minReg = (args[0] as IntArg).value + val maxReg = (args[0] as IntArg).value + (args[1] as IntArg).value if (register in minReg until maxReg) { return ValueSet.all() @@ -192,7 +192,7 @@ private class RegisterValueFinder { val param = params[j] if (param.type is RegType && param.type.registers != null) { - val regRef = args[j].value as Int + val regRef = (args[j] as IntArg).value for ((k, regParam) in param.type.registers.withIndex()) { if (regParam.write && regRef + k == register) { @@ -274,14 +274,15 @@ private class RegisterValueFinder { return if (register in 1..stack.size) { val instruction = stack[register - 1] - val value = instruction.args.first().value + val arg = instruction.args.first() when (instruction.opcode.code) { - OP_ARG_PUSHR.code -> find(LinkedHashSet(path), block, vaStartIdx, value as Int) + OP_ARG_PUSHR.code -> + find(LinkedHashSet(path), block, vaStartIdx, (arg as IntArg).value) OP_ARG_PUSHL.code, OP_ARG_PUSHB.code, - OP_ARG_PUSHW.code -> ValueSet.of(value as Int) + OP_ARG_PUSHW.code -> ValueSet.of((arg as IntArg).value) // TODO: Deal with strings. else -> ValueSet.all() // String or pointer diff --git a/lib/src/commonMain/kotlin/world/phantasmal/lib/asm/dataFlowAnalysis/GetStackValue.kt b/lib/src/commonMain/kotlin/world/phantasmal/lib/asm/dataFlowAnalysis/GetStackValue.kt index 5348bb43..8ceff963 100644 --- a/lib/src/commonMain/kotlin/world/phantasmal/lib/asm/dataFlowAnalysis/GetStackValue.kt +++ b/lib/src/commonMain/kotlin/world/phantasmal/lib/asm/dataFlowAnalysis/GetStackValue.kt @@ -51,7 +51,7 @@ private class StackValueFinder { when (instruction.opcode.code) { OP_ARG_PUSHR.code -> { if (pos == 0) { - return getRegisterValue(cfg, instruction, args[0].value as Int) + return getRegisterValue(cfg, instruction, (args[0] as IntArg).value) } else { pos-- } @@ -62,7 +62,7 @@ private class StackValueFinder { OP_ARG_PUSHW.code, -> { if (pos == 0) { - return ValueSet.of(args[0].value as Int) + return ValueSet.of((args[0] as IntArg).value) } else { pos-- } diff --git a/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/quest/Bytecode.kt b/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/quest/Bytecode.kt index 6c3de21e..44bbef17 100644 --- a/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/quest/Bytecode.kt +++ b/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/quest/Bytecode.kt @@ -242,8 +242,9 @@ private fun findAndParseSegments( // Never on the stack. // Eat all remaining arguments. while (i < instruction.args.size) { - newLabels[instruction.args[i].value as Int] = + newLabels[(instruction.args[i] as IntArg).value] = SegmentType.Instructions + i++ } } @@ -347,7 +348,7 @@ private fun getArgLabelValues( return true } } else { - val value = instruction.args[paramIdx].value as Int + val value = (instruction.args[paramIdx] as IntArg).value val oldType = labels[value] if ( @@ -466,13 +467,13 @@ private fun parseInstructionsSegment( // Parse the arguments. try { val args = parseInstructionArguments(cursor, opcode, dcGcFormat) - instructions.add(Instruction(opcode, args, srcLoc = null)) + instructions.add(Instruction(opcode, args, srcLoc = null, valid = true)) } catch (e: Exception) { if (lenient) { logger.error(e) { "Exception occurred while parsing arguments for instruction ${opcode.mnemonic}." } - instructions.add(Instruction(opcode, emptyList(), srcLoc = null)) + instructions.add(Instruction(opcode, emptyList(), srcLoc = null, valid = false)) } else { throw e } @@ -565,33 +566,33 @@ private fun parseInstructionArguments( for (param in opcode.params) { when (param.type) { is ByteType -> - args.add(Arg(cursor.uByte().toInt())) + args.add(IntArg(cursor.uByte().toInt())) is ShortType -> - args.add(Arg(cursor.uShort().toInt())) + args.add(IntArg(cursor.uShort().toInt())) is IntType -> - args.add(Arg(cursor.int())) + args.add(IntArg(cursor.int())) is FloatType -> - args.add(Arg(cursor.float())) + args.add(FloatArg(cursor.float())) // Ensure this case is before the LabelType case because ILabelVarType extends // LabelType. is ILabelVarType -> { varargCount++ val argSize = cursor.uByte() - args.addAll(cursor.uShortArray(argSize.toInt()).map { Arg(it.toInt()) }) + args.addAll(cursor.uShortArray(argSize.toInt()).map { IntArg(it.toInt()) }) } is LabelType -> { - args.add(Arg(cursor.uShort().toInt())) + args.add(IntArg(cursor.uShort().toInt())) } is StringType -> { val maxBytes = min(4096, cursor.bytesLeft) args.add( - Arg( + StringArg( if (dcGcFormat) { cursor.stringAscii( maxBytes, @@ -610,13 +611,13 @@ private fun parseInstructionArguments( } is RegType -> { - args.add(Arg(cursor.uByte().toInt())) + args.add(IntArg(cursor.uByte().toInt())) } is RegVarType -> { varargCount++ val argSize = cursor.uByte() - args.addAll(cursor.uByteArray(argSize.toInt()).map { Arg(it.toInt()) }) + args.addAll(cursor.uByteArray(argSize.toInt()).map { IntArg(it.toInt()) }) } else -> error("Parameter type ${param.type} not implemented.") @@ -725,7 +726,7 @@ private fun tryParseInstructionsSegment( for (arg in inst.getArgs(index)) { labelCount++ - if (!labelHolder.hasLabel(arg.value as Int)) { + if (!labelHolder.hasLabel((arg as IntArg).value)) { unknownLabelCount++ } } @@ -805,34 +806,34 @@ fun writeBytecode(bytecodeIr: BytecodeIr, dcGcFormat: Boolean): BytecodeAndLabel } when (param.type) { - ByteType -> cursor.writeByte((arg.value as Int).toByte()) - ShortType -> cursor.writeShort((arg.value as Int).toShort()) - IntType -> cursor.writeInt(arg.value as Int) - FloatType -> cursor.writeFloat(arg.value as Float) + ByteType -> cursor.writeByte(arg.coerceInt().toByte()) + ShortType -> cursor.writeShort(arg.coerceInt().toShort()) + IntType -> cursor.writeInt(arg.coerceInt()) + FloatType -> cursor.writeFloat(arg.coerceFloat()) // Ensure this case is before the LabelType case because // ILabelVarType extends LabelType. ILabelVarType -> { cursor.writeByte(args.size.toByte()) for (a in args) { - cursor.writeShort((a.value as Int).toShort()) + cursor.writeShort(a.coerceInt().toShort()) } } - is LabelType -> cursor.writeShort((arg.value as Int).toShort()) + is LabelType -> cursor.writeShort(arg.coerceInt().toShort()) StringType -> { - val str = arg.value as String + val str = arg.coerceString() if (dcGcFormat) cursor.writeStringAscii(str, str.length + 1) else cursor.writeStringUtf16(str, 2 * str.length + 2) } is RegType -> { - cursor.writeByte((arg.value as Int).toByte()) + cursor.writeByte(arg.coerceInt().toByte()) } RegVarType -> { cursor.writeByte(args.size.toByte()) for (a in args) { - cursor.writeByte((a.value as Int).toByte()) + cursor.writeByte(a.coerceInt().toByte()) } } else -> error( diff --git a/lib/src/commonTest/kotlin/world/phantasmal/lib/asm/AssemblyTests.kt b/lib/src/commonTest/kotlin/world/phantasmal/lib/asm/AssemblyTests.kt index 72319c3d..71cd9bb4 100644 --- a/lib/src/commonTest/kotlin/world/phantasmal/lib/asm/AssemblyTests.kt +++ b/lib/src/commonTest/kotlin/world/phantasmal/lib/asm/AssemblyTests.kt @@ -34,16 +34,17 @@ class AssemblyTests : LibTestSuite { instructions = mutableListOf( Instruction( opcode = OP_SET_EPISODE, - args = listOf(Arg(0)), + args = listOf(IntArg(0)), srcLoc = InstructionSrcLoc( mnemonic = SrcLoc(2, 5, 11), args = listOf(ArgSrcLoc(SrcLoc(2, 17, 1), SrcLoc(2, 16, 2))), - stackArgs = emptyList(), + trailingArgSeparator = false, ), + valid = true, ), Instruction( opcode = OP_BB_MAP_DESIGNATE, - args = listOf(Arg(1), Arg(2), Arg(3), Arg(4)), + args = listOf(IntArg(1), IntArg(2), IntArg(3), IntArg(4)), srcLoc = InstructionSrcLoc( mnemonic = SrcLoc(3, 5, 16), args = listOf( @@ -52,38 +53,42 @@ class AssemblyTests : LibTestSuite { ArgSrcLoc(SrcLoc(3, 28, 1), SrcLoc(3, 27, 3)), ArgSrcLoc(SrcLoc(3, 31, 1), SrcLoc(3, 30, 2)), ), - stackArgs = emptyList(), + trailingArgSeparator = false, ), + valid = true, ), Instruction( opcode = OP_ARG_PUSHL, - args = listOf(Arg(0)), + args = listOf(IntArg(0)), srcLoc = InstructionSrcLoc( mnemonic = null, args = listOf(ArgSrcLoc(SrcLoc(4, 23, 1), SrcLoc(4, 22, 3))), - stackArgs = emptyList(), + trailingArgSeparator = false, ), + valid = true, ), Instruction( opcode = OP_ARG_PUSHW, - args = listOf(Arg(150)), + args = listOf(IntArg(150)), srcLoc = InstructionSrcLoc( mnemonic = null, args = listOf(ArgSrcLoc(SrcLoc(4, 26, 3), SrcLoc(4, 25, 4))), - stackArgs = emptyList(), + trailingArgSeparator = false, ), + valid = true, ), Instruction( opcode = OP_SET_FLOOR_HANDLER, args = emptyList(), srcLoc = InstructionSrcLoc( mnemonic = SrcLoc(4, 5, 17), - args = emptyList(), - stackArgs = listOf( + args = listOf( ArgSrcLoc(SrcLoc(4, 23, 1), SrcLoc(4, 22, 3)), ArgSrcLoc(SrcLoc(4, 26, 3), SrcLoc(4, 25, 4)), ), + trailingArgSeparator = false, ), + valid = true, ), Instruction( opcode = OP_RET, @@ -91,8 +96,9 @@ class AssemblyTests : LibTestSuite { srcLoc = InstructionSrcLoc( mnemonic = SrcLoc(5, 5, 3), args = emptyList(), - stackArgs = emptyList(), + trailingArgSeparator = false, ), + valid = true, ), ), srcLoc = SegmentSrcLoc(labels = mutableListOf(SrcLoc(1, 1, 2))), @@ -102,23 +108,25 @@ class AssemblyTests : LibTestSuite { instructions = mutableListOf( Instruction( opcode = OP_ARG_PUSHL, - args = listOf(Arg(1)), + args = listOf(IntArg(1)), srcLoc = InstructionSrcLoc( mnemonic = null, args = listOf(ArgSrcLoc(SrcLoc(7, 18, 1), SrcLoc(7, 17, 2))), - stackArgs = emptyList(), + trailingArgSeparator = false, ), + valid = true, ), Instruction( opcode = OP_SET_MAINWARP, args = emptyList(), srcLoc = InstructionSrcLoc( mnemonic = SrcLoc(7, 5, 12), - args = emptyList(), - stackArgs = listOf( + args = listOf( ArgSrcLoc(SrcLoc(7, 18, 1), SrcLoc(7, 17, 2)), ), + trailingArgSeparator = false, ), + valid = true, ), Instruction( opcode = OP_RET, @@ -126,8 +134,9 @@ class AssemblyTests : LibTestSuite { srcLoc = InstructionSrcLoc( mnemonic = SrcLoc(8, 5, 3), args = emptyList(), - stackArgs = emptyList(), + trailingArgSeparator = false, ), + valid = true, ), ), srcLoc = SegmentSrcLoc(labels = mutableListOf(SrcLoc(6, 1, 4))), @@ -160,35 +169,38 @@ class AssemblyTests : LibTestSuite { instructions = mutableListOf( Instruction( opcode = OP_LETI, - args = listOf(Arg(255), Arg(7)), + args = listOf(IntArg(255), IntArg(7)), srcLoc = InstructionSrcLoc( mnemonic = SrcLoc(2, 5, 4), args = listOf( ArgSrcLoc(SrcLoc(2, 10, 4), SrcLoc(2, 9, 6)), ArgSrcLoc(SrcLoc(2, 16, 1), SrcLoc(2, 15, 2)), ), - stackArgs = emptyList(), + trailingArgSeparator = false, ), + valid = true, ), Instruction( opcode = OP_ARG_PUSHR, - args = listOf(Arg(255)), + args = listOf(IntArg(255)), srcLoc = InstructionSrcLoc( mnemonic = null, args = listOf(ArgSrcLoc(SrcLoc(3, 10, 4), SrcLoc(3, 9, 5))), - stackArgs = emptyList(), + trailingArgSeparator = false, ), + valid = true, ), Instruction( opcode = OP_EXIT, args = emptyList(), srcLoc = InstructionSrcLoc( mnemonic = SrcLoc(3, 5, 4), - args = emptyList(), - stackArgs = listOf( + args = listOf( ArgSrcLoc(SrcLoc(3, 10, 4), SrcLoc(3, 9, 5)), ), + trailingArgSeparator = false, ), + valid = true, ), Instruction( opcode = OP_RET, @@ -196,8 +208,9 @@ class AssemblyTests : LibTestSuite { srcLoc = InstructionSrcLoc( mnemonic = SrcLoc(4, 5, 3), args = emptyList(), - stackArgs = emptyList(), + trailingArgSeparator = false, ), + valid = true, ), ), srcLoc = SegmentSrcLoc( @@ -231,33 +244,36 @@ class AssemblyTests : LibTestSuite { instructions = mutableListOf( Instruction( opcode = OP_ARG_PUSHB, - args = listOf(Arg(200)), + args = listOf(IntArg(200)), srcLoc = InstructionSrcLoc( mnemonic = null, args = listOf(ArgSrcLoc(SrcLoc(2, 15, 4), SrcLoc(2, 14, 6))), - stackArgs = emptyList(), + trailingArgSeparator = false, ), + valid = true, ), Instruction( opcode = OP_ARG_PUSHL, - args = listOf(Arg(3)), + args = listOf(IntArg(3)), srcLoc = InstructionSrcLoc( mnemonic = null, args = listOf(ArgSrcLoc(SrcLoc(2, 21, 1), SrcLoc(2, 20, 2))), - stackArgs = emptyList(), + trailingArgSeparator = false, ), + valid = true, ), Instruction( opcode = OP_P_DEAD_V3, args = emptyList(), srcLoc = InstructionSrcLoc( mnemonic = SrcLoc(2, 5, 9), - args = emptyList(), - stackArgs = listOf( + args = listOf( ArgSrcLoc(SrcLoc(2, 15, 4), SrcLoc(2, 14, 6)), ArgSrcLoc(SrcLoc(2, 21, 1), SrcLoc(2, 20, 2)), ), + trailingArgSeparator = false, ), + valid = true, ), Instruction( opcode = OP_RET, @@ -265,8 +281,9 @@ class AssemblyTests : LibTestSuite { srcLoc = InstructionSrcLoc( mnemonic = SrcLoc(3, 5, 3), args = emptyList(), - stackArgs = emptyList(), + trailingArgSeparator = false, ), + valid = true, ), ), srcLoc = SegmentSrcLoc( @@ -289,6 +306,7 @@ class AssemblyTests : LibTestSuite { ) assertTrue(result is Success) + // Bytecode contains one invalid instruction. assertDeepEquals( BytecodeIr( listOf( @@ -297,12 +315,13 @@ class AssemblyTests : LibTestSuite { instructions = mutableListOf( Instruction( opcode = OP_RET, - args = emptyList(), + args = listOf(IntArg(100)), srcLoc = InstructionSrcLoc( mnemonic = SrcLoc(2, 5, 3), - args = emptyList(), - stackArgs = emptyList(), + args = listOf(ArgSrcLoc(SrcLoc(2, 9, 3), SrcLoc(2, 8, 4))), + trailingArgSeparator = false, ), + valid = false, ), ), srcLoc = SegmentSrcLoc( @@ -333,14 +352,24 @@ class AssemblyTests : LibTestSuite { ) assertTrue(result is Success) - - // Bytecode contains no instructions. + // Bytecode contains one invalid instruction. assertDeepEquals( BytecodeIr( listOf( InstructionSegment( labels = mutableListOf(5000), - instructions = mutableListOf(), + instructions = mutableListOf( + Instruction( + opcode = OP_LETI, + args = listOf(IntArg(100)), + srcLoc = InstructionSrcLoc( + mnemonic = SrcLoc(2, 5, 4), + args = listOf(ArgSrcLoc(SrcLoc(2, 10, 4), SrcLoc(2, 9, 5))), + trailingArgSeparator = false, + ), + valid = false, + ), + ), srcLoc = SegmentSrcLoc( labels = mutableListOf(SrcLoc(1, 1, 5)), ), @@ -370,7 +399,7 @@ class AssemblyTests : LibTestSuite { assertTrue(result is Success) - // Bytecode contains an instruction, since it's technically valid. + // Bytecode contains one invalid instruction. assertDeepEquals( BytecodeIr( listOf( @@ -379,12 +408,13 @@ class AssemblyTests : LibTestSuite { instructions = mutableListOf( Instruction( opcode = OP_SWITCH_JMP, - args = listOf(Arg(100)), + args = listOf(IntArg(100)), srcLoc = InstructionSrcLoc( mnemonic = SrcLoc(2, 5, 10), args = listOf(ArgSrcLoc(SrcLoc(2, 16, 4), SrcLoc(2, 15, 5))), - stackArgs = emptyList(), + trailingArgSeparator = false, ), + valid = false, ), ), srcLoc = SegmentSrcLoc( diff --git a/lib/src/commonTest/kotlin/world/phantasmal/lib/asm/DisassemblyTests.kt b/lib/src/commonTest/kotlin/world/phantasmal/lib/asm/DisassemblyTests.kt index 1c9bc49e..3f29ed75 100644 --- a/lib/src/commonTest/kotlin/world/phantasmal/lib/asm/DisassemblyTests.kt +++ b/lib/src/commonTest/kotlin/world/phantasmal/lib/asm/DisassemblyTests.kt @@ -15,17 +15,19 @@ class DisassemblyTests : LibTestSuite { Instruction( opcode = OP_SWITCH_JMP, args = listOf( - Arg(90), - Arg(100), - Arg(101), - Arg(102), + IntArg(90), + IntArg(100), + IntArg(101), + IntArg(102), ), srcLoc = null, + valid = true, ), Instruction( opcode = OP_RET, args = emptyList(), srcLoc = null, + valid = true, ), ), ) @@ -57,26 +59,31 @@ class DisassemblyTests : LibTestSuite { opcode = OP_VA_START, args = emptyList(), srcLoc = null, + valid = true, ), Instruction( opcode = OP_ARG_PUSHW, - args = listOf(Arg(1337)), + args = listOf(IntArg(1337)), srcLoc = null, + valid = true, ), Instruction( opcode = OP_VA_CALL, - args = listOf(Arg(100)), + args = listOf(IntArg(100)), srcLoc = null, + valid = true, ), Instruction( opcode = OP_VA_END, args = emptyList(), srcLoc = null, + valid = true, ), Instruction( opcode = OP_RET, args = emptyList(), srcLoc = null, + valid = true, ), ), ) diff --git a/lib/src/commonTest/kotlin/world/phantasmal/lib/asm/OpcodeTests.kt b/lib/src/commonTest/kotlin/world/phantasmal/lib/asm/OpcodeTests.kt index e32cf3be..207f1aba 100644 --- a/lib/src/commonTest/kotlin/world/phantasmal/lib/asm/OpcodeTests.kt +++ b/lib/src/commonTest/kotlin/world/phantasmal/lib/asm/OpcodeTests.kt @@ -3,6 +3,7 @@ package world.phantasmal.lib.asm import world.phantasmal.lib.test.LibTestSuite import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertNotEquals import kotlin.test.assertTrue class OpcodeTests : LibTestSuite { @@ -28,10 +29,13 @@ class OpcodeTests : LibTestSuite { assertTrue(!hasVarargs || opcode.params.lastOrNull()?.varargs == true) assertEquals(hasVarargs, opcode.varargs) - // Register references. for (param in opcode.params) { val type = param.type + // Any should only be used with register parameters. + assertNotEquals(AnyType.Instance, type) + + // Register references. if (type is RegType) { val registers = type.registers diff --git a/lib/src/commonTest/kotlin/world/phantasmal/lib/test/BytecodeIrAssertions.kt b/lib/src/commonTest/kotlin/world/phantasmal/lib/test/BytecodeIrAssertions.kt index f69c9078..b639282c 100644 --- a/lib/src/commonTest/kotlin/world/phantasmal/lib/test/BytecodeIrAssertions.kt +++ b/lib/src/commonTest/kotlin/world/phantasmal/lib/test/BytecodeIrAssertions.kt @@ -61,6 +61,7 @@ fun assertDeepEquals( message: String? = null, ) { assertEquals(expected.opcode, actual.opcode, message) + assertEquals(expected.valid, actual.valid, message) assertDeepEquals(expected.args, actual.args, ::assertEquals, message) if (!ignoreSrcLocs) { @@ -91,9 +92,9 @@ fun assertDeepEquals( } assertNotNull(actual, message) + assertEquals(expected.trailingArgSeparator, actual.trailingArgSeparator, message) assertDeepEquals(expected.mnemonic, actual.mnemonic, message) assertDeepEquals(expected.args, actual.args, ::assertDeepEquals, message) - assertDeepEquals(expected.stackArgs, actual.stackArgs, ::assertDeepEquals, message) } fun assertDeepEquals(expected: ArgSrcLoc?, actual: ArgSrcLoc?, message: String? = null) { diff --git a/web/assembly-worker/src/main/kotlin/world/phantasmal/web/assemblyWorker/AsmAnalyser.kt b/web/assembly-worker/src/main/kotlin/world/phantasmal/web/assemblyWorker/AsmAnalyser.kt index 37103287..3b0880f3 100644 --- a/web/assembly-worker/src/main/kotlin/world/phantasmal/web/assemblyWorker/AsmAnalyser.kt +++ b/web/assembly-worker/src/main/kotlin/world/phantasmal/web/assemblyWorker/AsmAnalyser.kt @@ -7,6 +7,7 @@ import world.phantasmal.lib.asm.dataFlowAnalysis.getMapDesignations import world.phantasmal.lib.asm.dataFlowAnalysis.getStackValue import world.phantasmal.web.shared.messages.* import world.phantasmal.web.shared.messages.AssemblyProblem +import kotlin.math.max import kotlin.math.min import world.phantasmal.lib.asm.AssemblyProblem as LibAssemblyProblem @@ -197,9 +198,9 @@ class AsmAnalyser { var signature: Signature? = null var activeParam = -1 - getInstructionForSrcLoc(lineNo, col)?.let { (inst, argIdx) -> + getInstructionForSrcLoc(lineNo, col)?.let { (inst, paramIdx) -> signature = getSignature(inst.opcode) - activeParam = argIdx + activeParam = paramIdx } return signature?.let { sig -> @@ -275,14 +276,14 @@ class AsmAnalyser { val srcLoc = argSrcLocs[i].coarse if (positionInside(lineNo, col, srcLoc)) { - val label = arg.value as Int + val label = (arg as IntArg).value result = getLabelDefinitions(label) break@loop } } } else { // Stack arguments. - val argSrcLocs = inst.getStackArgSrcLocs(paramIdx) + val argSrcLocs = inst.getArgSrcLocs(paramIdx) for ((i, argSrcLoc) in argSrcLocs.withIndex()) { if (positionInside(lineNo, col, argSrcLoc.coarse)) { @@ -332,7 +333,7 @@ class AsmAnalyser { is Ir.Inst -> { val srcLoc = ir.inst.srcLoc?.mnemonic - if (ir.argIdx == -1 || + if (ir.paramIdx == -1 || // Also return this instruction if we're right past the mnemonic. E.g. at the // first whitespace character preceding the first argument. (srcLoc != null && col <= srcLoc.col + srcLoc.len) @@ -373,7 +374,7 @@ class AsmAnalyser { lastCol = mnemonicSrcLoc.col + mnemonicSrcLoc.len if (positionInside(lineNo, col, mnemonicSrcLoc)) { - return Ir.Inst(inst, argIdx = -1) + return Ir.Inst(inst, paramIdx = -1) } } @@ -386,26 +387,13 @@ class AsmAnalyser { } } - if (inlineStackArgs) { - for ((argIdx, argSrcLoc) in srcLoc.stackArgs.withIndex()) { - instLineNo = argSrcLoc.coarse.lineNo - lastCol = argSrcLoc.coarse.col + argSrcLoc.coarse.len - - if (positionInside(lineNo, col, argSrcLoc.coarse)) { - return Ir.Inst(inst, argIdx) - } - } - } - if (lineNo == instLineNo && col >= lastCol) { - return Ir.Inst( - inst, - if (inlineStackArgs && inst.opcode.stack === StackInteraction.Pop) { - srcLoc.stackArgs.lastIndex - } else { - srcLoc.args.lastIndex - }, - ) + val argIdx = max(0, srcLoc.args.lastIndex) + + (if (srcLoc.trailingArgSeparator) 1 else 0) + + val paramIdx = min(argIdx, inst.opcode.params.lastIndex) + + return Ir.Inst(inst, paramIdx) } } } @@ -434,7 +422,7 @@ class AsmAnalyser { ) private sealed class Ir { - data class Inst(val inst: Instruction, val argIdx: Int) : Ir() + data class Inst(val inst: Instruction, val paramIdx: Int) : Ir() } companion object { diff --git a/web/assembly-worker/src/test/kotlin/world/phantasmal/web/assemblyWorker/AsmAnalyserTests.kt b/web/assembly-worker/src/test/kotlin/world/phantasmal/web/assemblyWorker/AsmAnalyserTests.kt index 7c918858..a7171f11 100644 --- a/web/assembly-worker/src/test/kotlin/world/phantasmal/web/assemblyWorker/AsmAnalyserTests.kt +++ b/web/assembly-worker/src/test/kotlin/world/phantasmal/web/assemblyWorker/AsmAnalyserTests.kt @@ -53,6 +53,80 @@ class AsmAnalyserTests : AssemblyWorkerTestSuite() { } } + @Test + fun getSignatureHelp_incomplete_instruction() = test { + // Three kinds of incomplete instructions. + val analyser = createAsmAnalyser( + """ + .code + 0: + leti + leti r0 + leti r0, + """.trimIndent() + ) + + val requestId = 113 + + for (col in 1..4) { + val response = analyser.getSignatureHelp(requestId, lineNo = 3, col) + + assertDeepEquals(Response.GetSignatureHelp(requestId, null), response) + } + + fun sigHelp(activeParameter: Int) = Response.GetSignatureHelp( + requestId, + SignatureHelp( + Signature( + label = "leti Reg, Int", + documentation = "Sets a register to the given value.", + listOf( + Parameter(labelStart = 5, labelEnd = 17, null), + Parameter(labelStart = 19, labelEnd = 22, null), + ), + ), + activeParameter, + ), + ) + + // First instruction: leti + for ((colRange, sigHelp) in listOf( + 5..8 to sigHelp(-1), + 9..9 to sigHelp(0), + )) { + for (col in colRange) { + val response = analyser.getSignatureHelp(requestId, 3, col) + + assertDeepEquals(sigHelp, response, "col = $col") + } + } + + // Second instruction: leti r0 + for ((colRange, sigHelp) in listOf( + 5..8 to sigHelp(-1), + 9..12 to sigHelp(0), + )) { + for (col in colRange) { + val response = analyser.getSignatureHelp(requestId, 4, col) + + assertDeepEquals(sigHelp, response, "col = $col") + } + } + + // Third instruction: leti r0, + for ((colRange, sigHelp) in listOf( + 5..8 to sigHelp(-1), + 9..12 to sigHelp(0), + 13..13 to sigHelp(1), + )) { + for (col in colRange) { + val response = analyser.getSignatureHelp(requestId, 5, col) + + assertDeepEquals(sigHelp, response, "col = $col") + } + } + } + @Test fun getHighlights_for_instruction() = test { val analyser = createAsmAnalyser( diff --git a/web/src/main/kotlin/world/phantasmal/web/core/rendering/conversion/NinjaGeometryConversion.kt b/web/src/main/kotlin/world/phantasmal/web/core/rendering/conversion/NinjaGeometryConversion.kt index 5055c9af..970142ca 100644 --- a/web/src/main/kotlin/world/phantasmal/web/core/rendering/conversion/NinjaGeometryConversion.kt +++ b/web/src/main/kotlin/world/phantasmal/web/core/rendering/conversion/NinjaGeometryConversion.kt @@ -3,7 +3,10 @@ package world.phantasmal.web.core.rendering.conversion import mu.KotlinLogging import org.khronos.webgl.Float32Array import org.khronos.webgl.Uint16Array -import world.phantasmal.core.* +import world.phantasmal.core.JsArray +import world.phantasmal.core.asArray +import world.phantasmal.core.isBitSet +import world.phantasmal.core.jsArrayOf import world.phantasmal.core.unsafe.UnsafeMap import world.phantasmal.lib.fileFormats.* import world.phantasmal.lib.fileFormats.ninja.* @@ -11,6 +14,21 @@ import world.phantasmal.web.core.dot import world.phantasmal.web.core.toQuaternion import world.phantasmal.web.externals.three.* import world.phantasmal.webui.obj +import kotlin.collections.List +import kotlin.collections.MutableList +import kotlin.collections.component1 +import kotlin.collections.component2 +import kotlin.collections.forEach +import kotlin.collections.forEachIndexed +import kotlin.collections.getOrPut +import kotlin.collections.indices +import kotlin.collections.iterator +import kotlin.collections.last +import kotlin.collections.map +import kotlin.collections.mutableListOf +import kotlin.collections.mutableMapOf +import kotlin.collections.sum +import kotlin.collections.withIndex private val logger = KotlinLogging.logger {} @@ -200,7 +218,7 @@ fun AreaObject.fingerPrint(): String = append('_') append(meshCount.toString(36)) append('_') - append(radius.reinterpretAsUInt().toString(36)) + append(radius.toRawBits().toUInt().toString(36)) } private fun areaObjectToMesh( diff --git a/web/src/test/kotlin/world/phantasmal/web/test/TestComponents.kt b/web/src/test/kotlin/world/phantasmal/web/test/TestComponents.kt index 81a92ec1..56465daa 100644 --- a/web/src/test/kotlin/world/phantasmal/web/test/TestComponents.kt +++ b/web/src/test/kotlin/world/phantasmal/web/test/TestComponents.kt @@ -68,7 +68,7 @@ class TestComponents(private val ctx: TestContext) { // Rendering var createThreeRenderer: (HTMLCanvasElement) -> DisposableThreeRenderer by default { - { _ -> + { object : DisposableThreeRenderer { override val renderer = NopRenderer().unsafeCast() override fun dispose() {} @@ -76,15 +76,7 @@ class TestComponents(private val ctx: TestContext) { } } - private fun default(defaultValue: () -> T) = LazyDefault { - val value = defaultValue() - - if (value is Disposable) { - ctx.disposer.add(value) - } - - value - } + private fun default(defaultValue: () -> T) = LazyDefault(defaultValue) private inner class LazyDefault(private val defaultValue: () -> T) { private var initialized = false