mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-04 06:28:28 +08:00
All instructions using the same opcode are now highlighted when the cursor is on an opcode mnemonic in the ASM editor.
This commit is contained in:
parent
2f0ebd9443
commit
b973c99c6a
@ -30,7 +30,8 @@ class LineTokenizer {
|
||||
private var index = 0
|
||||
private var startIndex = 0
|
||||
|
||||
private var value: Any? = null
|
||||
var value: Any? = null
|
||||
private set
|
||||
|
||||
var type: Token? = null
|
||||
private set
|
||||
@ -109,7 +110,13 @@ class LineTokenizer {
|
||||
break
|
||||
}
|
||||
|
||||
return type != null
|
||||
return if (type == null) {
|
||||
startIndex = line.length
|
||||
index = line.length
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasNext(): Boolean = index < line.length
|
||||
|
@ -131,8 +131,8 @@ private class Assembler(private val asm: List<String>, private val inlineStackAr
|
||||
opcode: Opcode,
|
||||
args: List<Arg>,
|
||||
mnemonicSrcLoc: SrcLoc?,
|
||||
argSrcLocs: List<SrcLoc>,
|
||||
stackArgSrcLocs: List<SrcLoc>,
|
||||
argSrcLocs: List<ArgSrcLoc>,
|
||||
stackArgSrcLocs: List<ArgSrcLoc>,
|
||||
) {
|
||||
when (val seg = segment) {
|
||||
null -> {
|
||||
@ -361,10 +361,10 @@ private class Assembler(private val asm: List<String>, private val inlineStackAr
|
||||
} else {
|
||||
// Inline arguments.
|
||||
val inlineArgs = mutableListOf<Arg>()
|
||||
val inlineArgSrcLocs = mutableListOf<SrcLoc>()
|
||||
val inlineArgSrcLocs = mutableListOf<ArgSrcLoc>()
|
||||
// Stack arguments.
|
||||
val stackArgs = mutableListOf<Arg>()
|
||||
val stackArgSrcLocs = mutableListOf<SrcLoc>()
|
||||
val stackArgSrcLocs = mutableListOf<ArgSrcLoc>()
|
||||
|
||||
if (opcode.stack !== StackInteraction.Pop) {
|
||||
// Arguments should be inlined right after the opcode.
|
||||
@ -409,25 +409,47 @@ private class Assembler(private val asm: List<String>, private val inlineStackAr
|
||||
opcode: Opcode,
|
||||
startCol: Int,
|
||||
args: MutableList<Arg>,
|
||||
srcLocs: MutableList<SrcLoc>,
|
||||
srcLocs: MutableList<ArgSrcLoc>,
|
||||
stack: Boolean,
|
||||
): Boolean {
|
||||
var argCount = 0
|
||||
var semiValid = true
|
||||
var shouldBeArg = true
|
||||
var paramI = 0
|
||||
var prevCol = 0
|
||||
var prevLen = 0
|
||||
var prevCol: Int
|
||||
var prevLen: Int
|
||||
var col = tokenizer.col
|
||||
var len = tokenizer.len
|
||||
|
||||
while (tokenizer.nextToken()) {
|
||||
if (tokenizer.type !== Token.ArgSeparator) {
|
||||
tokenizer.nextToken()
|
||||
|
||||
while (true) {
|
||||
prevCol = col
|
||||
prevLen = len
|
||||
|
||||
val token = tokenizer.type
|
||||
val value = tokenizer.value
|
||||
col = tokenizer.col
|
||||
len = tokenizer.len
|
||||
|
||||
if (token == null) {
|
||||
break
|
||||
}
|
||||
|
||||
tokenizer.nextToken()
|
||||
val coarseCol = prevCol + prevLen
|
||||
val coarseLen =
|
||||
if (tokenizer.type === Token.ArgSeparator) tokenizer.col + tokenizer.len - coarseCol
|
||||
else tokenizer.col - coarseCol
|
||||
|
||||
if (token !== Token.ArgSeparator) {
|
||||
argCount++
|
||||
}
|
||||
|
||||
if (paramI < opcode.params.size) {
|
||||
val param = opcode.params[paramI]
|
||||
|
||||
if (tokenizer.type === Token.ArgSeparator) {
|
||||
if (token === Token.ArgSeparator) {
|
||||
if (shouldBeArg) {
|
||||
addError("Expected an argument.")
|
||||
} else if (!param.varargs) {
|
||||
@ -437,8 +459,7 @@ private class Assembler(private val asm: List<String>, private val inlineStackAr
|
||||
shouldBeArg = true
|
||||
} else {
|
||||
if (!shouldBeArg) {
|
||||
val col = prevCol + prevLen
|
||||
addError(col, tokenizer.col - col, "Expected a comma.")
|
||||
addError(coarseCol, col - coarseCol, "Expected a comma.")
|
||||
}
|
||||
|
||||
shouldBeArg = false
|
||||
@ -447,26 +468,28 @@ private class Assembler(private val asm: List<String>, private val inlineStackAr
|
||||
var typeMatch: Boolean
|
||||
|
||||
// If arg is nonnull, types match and argument is syntactically valid.
|
||||
val arg: Arg? = when (tokenizer.type) {
|
||||
val arg: Arg? = when (token) {
|
||||
Token.Int32 -> {
|
||||
value as Int
|
||||
|
||||
when (param.type) {
|
||||
ByteType -> {
|
||||
typeMatch = true
|
||||
parseInt(1)
|
||||
intValueToArg(value, 1)
|
||||
}
|
||||
ShortType,
|
||||
is LabelType,
|
||||
-> {
|
||||
typeMatch = true
|
||||
parseInt(2)
|
||||
intValueToArg(value, 2)
|
||||
}
|
||||
IntType -> {
|
||||
typeMatch = true
|
||||
parseInt(4)
|
||||
intValueToArg(value, 4)
|
||||
}
|
||||
FloatType -> {
|
||||
typeMatch = true
|
||||
Arg(tokenizer.intValue.toFloat())
|
||||
Arg(value.toFloat())
|
||||
}
|
||||
else -> {
|
||||
typeMatch = false
|
||||
@ -479,25 +502,32 @@ private class Assembler(private val asm: List<String>, private val inlineStackAr
|
||||
typeMatch = param.type === FloatType
|
||||
|
||||
if (typeMatch) {
|
||||
Arg(tokenizer.floatValue)
|
||||
Arg(value as Float)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
Token.Register -> {
|
||||
value as Int
|
||||
|
||||
typeMatch = stack ||
|
||||
param.type === RegVarType ||
|
||||
param.type is RegType
|
||||
|
||||
parseRegister()
|
||||
if (value > 255) {
|
||||
addError("Invalid register reference, expected r0-r255.")
|
||||
null
|
||||
} else {
|
||||
Arg(value)
|
||||
}
|
||||
}
|
||||
|
||||
Token.Str -> {
|
||||
typeMatch = param.type === StringType
|
||||
|
||||
if (typeMatch) {
|
||||
Arg(tokenizer.strValue)
|
||||
Arg(value as String)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@ -509,7 +539,10 @@ private class Assembler(private val asm: List<String>, private val inlineStackAr
|
||||
}
|
||||
}
|
||||
|
||||
val srcLoc = srcLocFromTokenizer()
|
||||
val srcLoc = ArgSrcLoc(
|
||||
precise = SrcLoc(lineNo, col, len),
|
||||
coarse = SrcLoc(lineNo, coarseCol, coarseLen),
|
||||
)
|
||||
|
||||
if (arg != null) {
|
||||
args.add(arg)
|
||||
@ -549,7 +582,7 @@ private class Assembler(private val asm: List<String>, private val inlineStackAr
|
||||
} 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 (tokenizer.type === Token.Register) {
|
||||
if (token === Token.Register) {
|
||||
if (param.type is RegType) {
|
||||
addInstruction(
|
||||
OP_ARG_PUSHB,
|
||||
@ -633,9 +666,6 @@ private class Assembler(private val asm: List<String>, private val inlineStackAr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prevCol = tokenizer.col
|
||||
prevLen = tokenizer.len
|
||||
}
|
||||
|
||||
val paramCount =
|
||||
@ -669,9 +699,7 @@ private class Assembler(private val asm: List<String>, private val inlineStackAr
|
||||
return semiValid
|
||||
}
|
||||
|
||||
private fun parseInt(size: Int): Arg? {
|
||||
val value = tokenizer.intValue
|
||||
|
||||
private fun intValueToArg(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) {
|
||||
@ -699,17 +727,6 @@ private class Assembler(private val asm: List<String>, private val inlineStackAr
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseRegister(): Arg? {
|
||||
val value = tokenizer.intValue
|
||||
|
||||
return if (value > 255) {
|
||||
addError("Invalid register reference, expected r0-r255.")
|
||||
null
|
||||
} else {
|
||||
Arg(value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseBytes() {
|
||||
val bytes = mutableListOf<Byte>()
|
||||
|
||||
|
@ -146,7 +146,7 @@ class Instruction(
|
||||
/**
|
||||
* Returns the source locations of the immediate arguments for the parameter at the given index.
|
||||
*/
|
||||
fun getArgSrcLocs(paramIndex: Int): List<SrcLoc> {
|
||||
fun getArgSrcLocs(paramIndex: Int): List<ArgSrcLoc> {
|
||||
val argSrcLocs = srcLoc?.args
|
||||
?: return emptyList()
|
||||
|
||||
@ -164,7 +164,7 @@ class Instruction(
|
||||
/**
|
||||
* Returns the source locations of the stack arguments for the parameter at the given index.
|
||||
*/
|
||||
fun getStackArgSrcLocs(paramIndex: Int): List<SrcLoc> {
|
||||
fun getStackArgSrcLocs(paramIndex: Int): List<ArgSrcLoc> {
|
||||
val argSrcLocs = srcLoc?.stackArgs
|
||||
|
||||
if (argSrcLocs == null || paramIndex > argSrcLocs.lastIndex) {
|
||||
@ -254,8 +254,23 @@ class SrcLoc(
|
||||
*/
|
||||
class InstructionSrcLoc(
|
||||
val mnemonic: SrcLoc?,
|
||||
val args: List<SrcLoc> = emptyList(),
|
||||
val stackArgs: List<SrcLoc> = emptyList(),
|
||||
val args: List<ArgSrcLoc> = emptyList(),
|
||||
val stackArgs: List<ArgSrcLoc> = emptyList(),
|
||||
)
|
||||
|
||||
/**
|
||||
* Location of an instruction argument in the source assembly code.
|
||||
*/
|
||||
class ArgSrcLoc(
|
||||
/**
|
||||
* The precise location of this argument.
|
||||
*/
|
||||
val precise: SrcLoc,
|
||||
/**
|
||||
* The location of this argument, its surrounding whitespace and the following comma if there is
|
||||
* one.
|
||||
*/
|
||||
val coarse: SrcLoc,
|
||||
)
|
||||
|
||||
/**
|
||||
|
@ -170,6 +170,8 @@ class Opcode internal constructor(
|
||||
override fun equals(other: Any?): Boolean = this === other
|
||||
|
||||
override fun hashCode(): Int = code
|
||||
|
||||
override fun toString(): String = mnemonic
|
||||
}
|
||||
|
||||
fun codeToOpcode(code: Int): Opcode =
|
||||
|
@ -37,7 +37,7 @@ class AssemblyTests : LibTestSuite {
|
||||
args = listOf(Arg(0)),
|
||||
srcLoc = InstructionSrcLoc(
|
||||
mnemonic = SrcLoc(2, 5, 11),
|
||||
args = listOf(SrcLoc(2, 17, 1)),
|
||||
args = listOf(ArgSrcLoc(SrcLoc(2, 17, 1), SrcLoc(2, 16, 2))),
|
||||
stackArgs = emptyList(),
|
||||
),
|
||||
),
|
||||
@ -47,10 +47,10 @@ class AssemblyTests : LibTestSuite {
|
||||
srcLoc = InstructionSrcLoc(
|
||||
mnemonic = SrcLoc(3, 5, 16),
|
||||
args = listOf(
|
||||
SrcLoc(3, 22, 1),
|
||||
SrcLoc(3, 25, 1),
|
||||
SrcLoc(3, 28, 1),
|
||||
SrcLoc(3, 31, 1),
|
||||
ArgSrcLoc(SrcLoc(3, 22, 1), SrcLoc(3, 21, 3)),
|
||||
ArgSrcLoc(SrcLoc(3, 25, 1), SrcLoc(3, 24, 3)),
|
||||
ArgSrcLoc(SrcLoc(3, 28, 1), SrcLoc(3, 27, 3)),
|
||||
ArgSrcLoc(SrcLoc(3, 31, 1), SrcLoc(3, 30, 2)),
|
||||
),
|
||||
stackArgs = emptyList(),
|
||||
),
|
||||
@ -60,7 +60,7 @@ class AssemblyTests : LibTestSuite {
|
||||
args = listOf(Arg(0)),
|
||||
srcLoc = InstructionSrcLoc(
|
||||
mnemonic = null,
|
||||
args = listOf(SrcLoc(4, 23, 1)),
|
||||
args = listOf(ArgSrcLoc(SrcLoc(4, 23, 1), SrcLoc(4, 22, 3))),
|
||||
stackArgs = emptyList(),
|
||||
),
|
||||
),
|
||||
@ -69,7 +69,7 @@ class AssemblyTests : LibTestSuite {
|
||||
args = listOf(Arg(150)),
|
||||
srcLoc = InstructionSrcLoc(
|
||||
mnemonic = null,
|
||||
args = listOf(SrcLoc(4, 26, 3)),
|
||||
args = listOf(ArgSrcLoc(SrcLoc(4, 26, 3), SrcLoc(4, 25, 4))),
|
||||
stackArgs = emptyList(),
|
||||
),
|
||||
),
|
||||
@ -80,8 +80,8 @@ class AssemblyTests : LibTestSuite {
|
||||
mnemonic = SrcLoc(4, 5, 17),
|
||||
args = emptyList(),
|
||||
stackArgs = listOf(
|
||||
SrcLoc(4, 23, 1),
|
||||
SrcLoc(4, 26, 3),
|
||||
ArgSrcLoc(SrcLoc(4, 23, 1), SrcLoc(4, 22, 3)),
|
||||
ArgSrcLoc(SrcLoc(4, 26, 3), SrcLoc(4, 25, 4)),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -105,7 +105,7 @@ class AssemblyTests : LibTestSuite {
|
||||
args = listOf(Arg(1)),
|
||||
srcLoc = InstructionSrcLoc(
|
||||
mnemonic = null,
|
||||
args = listOf(SrcLoc(7, 18, 1)),
|
||||
args = listOf(ArgSrcLoc(SrcLoc(7, 18, 1), SrcLoc(7, 17, 2))),
|
||||
stackArgs = emptyList(),
|
||||
),
|
||||
),
|
||||
@ -115,7 +115,9 @@ class AssemblyTests : LibTestSuite {
|
||||
srcLoc = InstructionSrcLoc(
|
||||
mnemonic = SrcLoc(7, 5, 12),
|
||||
args = emptyList(),
|
||||
stackArgs = listOf(SrcLoc(7, 18, 1)),
|
||||
stackArgs = listOf(
|
||||
ArgSrcLoc(SrcLoc(7, 18, 1), SrcLoc(7, 17, 2)),
|
||||
),
|
||||
),
|
||||
),
|
||||
Instruction(
|
||||
@ -161,7 +163,10 @@ class AssemblyTests : LibTestSuite {
|
||||
args = listOf(Arg(255), Arg(7)),
|
||||
srcLoc = InstructionSrcLoc(
|
||||
mnemonic = SrcLoc(2, 5, 4),
|
||||
args = listOf(SrcLoc(2, 10, 4), SrcLoc(2, 16, 1)),
|
||||
args = listOf(
|
||||
ArgSrcLoc(SrcLoc(2, 10, 4), SrcLoc(2, 9, 6)),
|
||||
ArgSrcLoc(SrcLoc(2, 16, 1), SrcLoc(2, 15, 2)),
|
||||
),
|
||||
stackArgs = emptyList(),
|
||||
),
|
||||
),
|
||||
@ -170,7 +175,7 @@ class AssemblyTests : LibTestSuite {
|
||||
args = listOf(Arg(255)),
|
||||
srcLoc = InstructionSrcLoc(
|
||||
mnemonic = null,
|
||||
args = listOf(SrcLoc(3, 10, 4)),
|
||||
args = listOf(ArgSrcLoc(SrcLoc(3, 10, 4), SrcLoc(3, 9, 5))),
|
||||
stackArgs = emptyList(),
|
||||
),
|
||||
),
|
||||
@ -180,7 +185,9 @@ class AssemblyTests : LibTestSuite {
|
||||
srcLoc = InstructionSrcLoc(
|
||||
mnemonic = SrcLoc(3, 5, 4),
|
||||
args = emptyList(),
|
||||
stackArgs = listOf(SrcLoc(3, 10, 4)),
|
||||
stackArgs = listOf(
|
||||
ArgSrcLoc(SrcLoc(3, 10, 4), SrcLoc(3, 9, 5)),
|
||||
),
|
||||
),
|
||||
),
|
||||
Instruction(
|
||||
@ -227,7 +234,7 @@ class AssemblyTests : LibTestSuite {
|
||||
args = listOf(Arg(200)),
|
||||
srcLoc = InstructionSrcLoc(
|
||||
mnemonic = null,
|
||||
args = listOf(SrcLoc(2, 15, 4)),
|
||||
args = listOf(ArgSrcLoc(SrcLoc(2, 15, 4), SrcLoc(2, 14, 6))),
|
||||
stackArgs = emptyList(),
|
||||
),
|
||||
),
|
||||
@ -236,7 +243,7 @@ class AssemblyTests : LibTestSuite {
|
||||
args = listOf(Arg(3)),
|
||||
srcLoc = InstructionSrcLoc(
|
||||
mnemonic = null,
|
||||
args = listOf(SrcLoc(2, 21, 1)),
|
||||
args = listOf(ArgSrcLoc(SrcLoc(2, 21, 1), SrcLoc(2, 20, 2))),
|
||||
stackArgs = emptyList(),
|
||||
),
|
||||
),
|
||||
@ -246,7 +253,10 @@ class AssemblyTests : LibTestSuite {
|
||||
srcLoc = InstructionSrcLoc(
|
||||
mnemonic = SrcLoc(2, 5, 9),
|
||||
args = emptyList(),
|
||||
stackArgs = listOf(SrcLoc(2, 15, 4), SrcLoc(2, 21, 1)),
|
||||
stackArgs = listOf(
|
||||
ArgSrcLoc(SrcLoc(2, 15, 4), SrcLoc(2, 14, 6)),
|
||||
ArgSrcLoc(SrcLoc(2, 21, 1), SrcLoc(2, 20, 2)),
|
||||
),
|
||||
),
|
||||
),
|
||||
Instruction(
|
||||
@ -372,7 +382,7 @@ class AssemblyTests : LibTestSuite {
|
||||
args = listOf(Arg(100)),
|
||||
srcLoc = InstructionSrcLoc(
|
||||
mnemonic = SrcLoc(2, 5, 10),
|
||||
args = listOf(SrcLoc(2, 16, 4)),
|
||||
args = listOf(ArgSrcLoc(SrcLoc(2, 16, 4), SrcLoc(2, 15, 5))),
|
||||
stackArgs = emptyList(),
|
||||
),
|
||||
),
|
||||
|
@ -7,6 +7,7 @@ import world.phantasmal.lib.fileFormats.quest.writeBytecode
|
||||
import world.phantasmal.lib.test.LibTestSuite
|
||||
import world.phantasmal.lib.test.assertDeepEquals
|
||||
import world.phantasmal.lib.test.readFile
|
||||
import world.phantasmal.testUtils.assertDeepEquals
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
@ -96,16 +97,18 @@ class DisassemblyAssemblyRoundTripTests : LibTestSuite {
|
||||
) {
|
||||
val origBin = parseBin(readFile("/quest27_e_decompressed.bin"))
|
||||
val origBytecode = origBin.bytecode
|
||||
val result = assemble(disassemble(
|
||||
parseBytecode(
|
||||
origBytecode,
|
||||
origBin.labelOffsets,
|
||||
setOf(0),
|
||||
dcGcFormat = false,
|
||||
lenient = false,
|
||||
).unwrap(),
|
||||
inlineStackArgs,
|
||||
), inlineStackArgs)
|
||||
val result = assemble(
|
||||
disassemble(
|
||||
parseBytecode(
|
||||
origBytecode,
|
||||
origBin.labelOffsets,
|
||||
setOf(0),
|
||||
dcGcFormat = false,
|
||||
lenient = false,
|
||||
).unwrap(),
|
||||
inlineStackArgs,
|
||||
), inlineStackArgs
|
||||
)
|
||||
|
||||
assertTrue(result is Success)
|
||||
assertTrue(result.problems.isEmpty())
|
||||
|
@ -9,6 +9,7 @@ import world.phantasmal.lib.test.LibTestSuite
|
||||
import world.phantasmal.lib.test.assertDeepEquals
|
||||
import world.phantasmal.lib.test.readFile
|
||||
import world.phantasmal.lib.test.testWithTetheallaQuests
|
||||
import world.phantasmal.testUtils.assertDeepEquals
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
@ -1,82 +1,118 @@
|
||||
package world.phantasmal.lib.test
|
||||
|
||||
import world.phantasmal.lib.asm.*
|
||||
import world.phantasmal.testUtils.assertDeepEquals
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertNull
|
||||
|
||||
fun assertDeepEquals(expected: BytecodeIr, actual: BytecodeIr, ignoreSrcLocs: Boolean = false) {
|
||||
fun assertDeepEquals(
|
||||
expected: BytecodeIr,
|
||||
actual: BytecodeIr,
|
||||
ignoreSrcLocs: Boolean = false,
|
||||
message: String? = null,
|
||||
) {
|
||||
assertDeepEquals(
|
||||
expected.segments,
|
||||
actual.segments,
|
||||
) { a, b -> assertDeepEquals(a, b, ignoreSrcLocs) }
|
||||
{ a, b, m -> assertDeepEquals(a, b, ignoreSrcLocs, m) },
|
||||
message,
|
||||
)
|
||||
}
|
||||
|
||||
fun assertDeepEquals(expected: Segment, actual: Segment, ignoreSrcLocs: Boolean = false) {
|
||||
assertEquals(expected::class, actual::class)
|
||||
assertDeepEquals(expected.labels, actual.labels, ::assertEquals)
|
||||
fun assertDeepEquals(
|
||||
expected: Segment,
|
||||
actual: Segment,
|
||||
ignoreSrcLocs: Boolean = false,
|
||||
message: String? = null,
|
||||
) {
|
||||
assertEquals(expected::class, actual::class, message)
|
||||
assertDeepEquals(expected.labels, actual.labels, ::assertEquals, message)
|
||||
|
||||
if (!ignoreSrcLocs) {
|
||||
assertDeepEquals(expected.srcLoc, actual.srcLoc)
|
||||
assertDeepEquals(expected.srcLoc, actual.srcLoc, message)
|
||||
}
|
||||
|
||||
when (expected) {
|
||||
is InstructionSegment -> {
|
||||
actual as InstructionSegment
|
||||
assertDeepEquals(expected.instructions, actual.instructions) { a, b ->
|
||||
assertDeepEquals(a, b, ignoreSrcLocs)
|
||||
}
|
||||
assertDeepEquals(
|
||||
expected.instructions,
|
||||
actual.instructions,
|
||||
{ a, b, m -> assertDeepEquals(a, b, ignoreSrcLocs, m) },
|
||||
message,
|
||||
)
|
||||
}
|
||||
is DataSegment -> {
|
||||
actual as DataSegment
|
||||
assertDeepEquals(expected.data, actual.data)
|
||||
assertDeepEquals(expected.data, actual.data, message)
|
||||
}
|
||||
is StringSegment -> {
|
||||
actual as StringSegment
|
||||
assertEquals(expected.value, actual.value)
|
||||
assertEquals(expected.value, actual.value, message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun assertDeepEquals(expected: Instruction, actual: Instruction, ignoreSrcLocs: Boolean = false) {
|
||||
assertEquals(expected.opcode, actual.opcode)
|
||||
assertDeepEquals(expected.args, actual.args, ::assertEquals)
|
||||
fun assertDeepEquals(
|
||||
expected: Instruction,
|
||||
actual: Instruction,
|
||||
ignoreSrcLocs: Boolean = false,
|
||||
message: String? = null,
|
||||
) {
|
||||
assertEquals(expected.opcode, actual.opcode, message)
|
||||
assertDeepEquals(expected.args, actual.args, ::assertEquals, message)
|
||||
|
||||
if (!ignoreSrcLocs) {
|
||||
assertDeepEquals(expected.srcLoc, actual.srcLoc)
|
||||
assertDeepEquals(expected.srcLoc, actual.srcLoc, message)
|
||||
}
|
||||
}
|
||||
|
||||
fun assertDeepEquals(expected: SrcLoc?, actual: SrcLoc?) {
|
||||
fun assertDeepEquals(expected: SrcLoc?, actual: SrcLoc?, message: String? = null) {
|
||||
if (expected == null) {
|
||||
assertNull(actual)
|
||||
assertNull(actual, message)
|
||||
return
|
||||
}
|
||||
|
||||
assertNotNull(actual)
|
||||
assertEquals(expected.lineNo, actual.lineNo)
|
||||
assertEquals(expected.col, actual.col)
|
||||
assertEquals(expected.len, actual.len)
|
||||
assertNotNull(actual, message)
|
||||
assertEquals(expected.lineNo, actual.lineNo, message)
|
||||
assertEquals(expected.col, actual.col, message)
|
||||
assertEquals(expected.len, actual.len, message)
|
||||
}
|
||||
|
||||
fun assertDeepEquals(expected: InstructionSrcLoc?, actual: InstructionSrcLoc?) {
|
||||
fun assertDeepEquals(
|
||||
expected: InstructionSrcLoc?,
|
||||
actual: InstructionSrcLoc?,
|
||||
message: String? = null,
|
||||
) {
|
||||
if (expected == null) {
|
||||
assertNull(actual)
|
||||
assertNull(actual, message)
|
||||
return
|
||||
}
|
||||
|
||||
assertNotNull(actual)
|
||||
assertDeepEquals(expected.mnemonic, actual.mnemonic)
|
||||
assertDeepEquals(expected.args, actual.args, ::assertDeepEquals)
|
||||
assertDeepEquals(expected.stackArgs, actual.stackArgs, ::assertDeepEquals)
|
||||
assertNotNull(actual, message)
|
||||
assertDeepEquals(expected.mnemonic, actual.mnemonic, message)
|
||||
assertDeepEquals(expected.args, actual.args, ::assertDeepEquals, message)
|
||||
assertDeepEquals(expected.stackArgs, actual.stackArgs, ::assertDeepEquals, message)
|
||||
}
|
||||
|
||||
fun assertDeepEquals(expected: SegmentSrcLoc?, actual: SegmentSrcLoc?) {
|
||||
fun assertDeepEquals(expected: ArgSrcLoc?, actual: ArgSrcLoc?, message: String? = null) {
|
||||
if (expected == null) {
|
||||
assertNull(actual)
|
||||
assertNull(actual, message)
|
||||
return
|
||||
}
|
||||
|
||||
assertNotNull(actual)
|
||||
assertDeepEquals(expected.labels, actual.labels, ::assertDeepEquals)
|
||||
assertNotNull(actual, message)
|
||||
assertDeepEquals(expected.precise, actual.precise, message)
|
||||
assertDeepEquals(expected.coarse, actual.coarse, message)
|
||||
}
|
||||
|
||||
fun assertDeepEquals(expected: SegmentSrcLoc?, actual: SegmentSrcLoc?, message: String? = null) {
|
||||
if (expected == null) {
|
||||
assertNull(actual, message)
|
||||
return
|
||||
}
|
||||
|
||||
assertNotNull(actual, message)
|
||||
assertDeepEquals(expected.labels, actual.labels, ::assertDeepEquals, message)
|
||||
}
|
||||
|
@ -19,32 +19,15 @@ fun toInstructions(assembly: String): List<InstructionSegment> {
|
||||
return result.value.instructionSegments()
|
||||
}
|
||||
|
||||
fun <T> assertDeepEquals(expected: List<T>, actual: List<T>, assertDeepEquals: (T, T) -> Unit) {
|
||||
assertEquals(expected.size, actual.size, "Unexpected list size")
|
||||
|
||||
for (i in expected.indices) {
|
||||
assertDeepEquals(expected[i], actual[i])
|
||||
}
|
||||
}
|
||||
|
||||
fun <K, V> assertDeepEquals(
|
||||
expected: Map<K, V>,
|
||||
actual: Map<K, V>,
|
||||
assertDeepEquals: (V, V) -> Unit,
|
||||
) {
|
||||
assertEquals(expected.size, actual.size, "Unexpected map size")
|
||||
|
||||
for ((key, value) in expected) {
|
||||
assertTrue(key in actual)
|
||||
assertDeepEquals(value, actual[key]!!)
|
||||
}
|
||||
}
|
||||
|
||||
fun assertDeepEquals(expected: Buffer, actual: Buffer) {
|
||||
assertEquals(expected.size, actual.size, "Unexpected buffer size")
|
||||
fun assertDeepEquals(expected: Buffer, actual: Buffer, message: String? = null) {
|
||||
assertEquals(
|
||||
expected.size,
|
||||
actual.size,
|
||||
"Unexpected buffer size" + (if (message == null) "" else ". $message"),
|
||||
)
|
||||
|
||||
for (i in 0 until expected.size) {
|
||||
assertEquals(expected.getByte(i), actual.getByte(i))
|
||||
assertEquals(expected.getByte(i), actual.getByte(i), message)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,8 @@
|
||||
package world.phantasmal.testUtils
|
||||
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* Ensure you return the value of this function in your test function. On Kotlin/JS this function
|
||||
* actually returns a Promise. If this promise is not returned from the test function, the testing
|
||||
@ -9,3 +12,38 @@ package world.phantasmal.testUtils
|
||||
internal expect fun testAsync(block: suspend () -> Unit)
|
||||
|
||||
internal expect fun canExecuteSlowTests(): Boolean
|
||||
|
||||
fun <T> assertDeepEquals(
|
||||
expected: List<T>,
|
||||
actual: List<T>,
|
||||
assertDeepEquals: (T, T, String?) -> Unit,
|
||||
message: String? = null,
|
||||
) {
|
||||
assertEquals(
|
||||
expected.size,
|
||||
actual.size,
|
||||
"Unexpected list size" + (if (message == null) "" else ". $message"),
|
||||
)
|
||||
|
||||
for (i in expected.indices) {
|
||||
assertDeepEquals(expected[i], actual[i], message)
|
||||
}
|
||||
}
|
||||
|
||||
fun <K, V> assertDeepEquals(
|
||||
expected: Map<K, V>,
|
||||
actual: Map<K, V>,
|
||||
assertDeepEquals: (V, V, String?) -> Unit,
|
||||
message: String? = null
|
||||
) {
|
||||
assertEquals(
|
||||
expected.size,
|
||||
actual.size,
|
||||
"Unexpected map size" + (if (message == null) "" else ". $message"),
|
||||
)
|
||||
|
||||
for ((key, value) in expected) {
|
||||
assertTrue(key in actual, message)
|
||||
assertDeepEquals(value, actual[key]!!, message)
|
||||
}
|
||||
}
|
||||
|
@ -1,24 +1,16 @@
|
||||
package world.phantasmal.web.assemblyWorker
|
||||
|
||||
import mu.KotlinLogging
|
||||
import world.phantasmal.core.*
|
||||
import world.phantasmal.lib.asm.*
|
||||
import world.phantasmal.lib.asm.dataFlowAnalysis.ControlFlowGraph
|
||||
import world.phantasmal.lib.asm.dataFlowAnalysis.getMapDesignations
|
||||
import world.phantasmal.lib.asm.dataFlowAnalysis.getStackValue
|
||||
import world.phantasmal.web.shared.Throttle
|
||||
import world.phantasmal.web.shared.messages.*
|
||||
import world.phantasmal.web.shared.messages.AssemblyProblem
|
||||
import kotlin.math.min
|
||||
import kotlin.time.measureTime
|
||||
import world.phantasmal.lib.asm.AssemblyProblem as AssemblerAssemblyProblem
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
class AssemblyWorker(private val sendMessage: (ServerMessage) -> Unit) {
|
||||
private val messageQueue: MutableList<ClientMessage> = mutableListOf()
|
||||
private val messageProcessingThrottle = Throttle(wait = 100)
|
||||
private val tokenizer = LineTokenizer()
|
||||
import world.phantasmal.lib.asm.AssemblyProblem as LibAssemblyProblem
|
||||
|
||||
class AsmAnalyser {
|
||||
// User input.
|
||||
private var inlineStackArgs: Boolean = true
|
||||
private val asm: JsArray<String> = jsArrayOf()
|
||||
@ -37,97 +29,13 @@ class AssemblyWorker(private val sendMessage: (ServerMessage) -> Unit) {
|
||||
|
||||
private var mapDesignations: Map<Int, Int>? = null
|
||||
|
||||
fun receiveMessage(message: ClientMessage) {
|
||||
messageQueue.add(message)
|
||||
messageProcessingThrottle(::processMessages)
|
||||
}
|
||||
|
||||
private fun processMessages() {
|
||||
// Split messages into ASM changes and other messages. Remove useless/duplicate
|
||||
// notifications.
|
||||
val asmChanges = mutableListOf<ClientNotification>()
|
||||
val otherMessages = mutableListOf<ClientMessage>()
|
||||
|
||||
for (message in messageQueue) {
|
||||
when (message) {
|
||||
is ClientNotification.SetAsm -> {
|
||||
// All previous ASM change messages can be discarded when the entire ASM has
|
||||
// changed.
|
||||
asmChanges.clear()
|
||||
asmChanges.add(message)
|
||||
}
|
||||
|
||||
is ClientNotification.UpdateAsm ->
|
||||
asmChanges.add(message)
|
||||
|
||||
else ->
|
||||
otherMessages.add(message)
|
||||
}
|
||||
}
|
||||
|
||||
messageQueue.clear()
|
||||
|
||||
// Process ASM changes first.
|
||||
processAsmChanges(asmChanges)
|
||||
otherMessages.forEach(::processMessage)
|
||||
}
|
||||
|
||||
private fun processAsmChanges(messages: List<ClientNotification>) {
|
||||
if (messages.isNotEmpty()) {
|
||||
val time = measureTime {
|
||||
for (message in messages) {
|
||||
when (message) {
|
||||
is ClientNotification.SetAsm ->
|
||||
setAsm(message.asm, message.inlineStackArgs)
|
||||
|
||||
is ClientNotification.UpdateAsm ->
|
||||
updateAsm(message.changes)
|
||||
}
|
||||
}
|
||||
|
||||
processAsm()
|
||||
}
|
||||
|
||||
logger.trace {
|
||||
"Processed ${messages.size} assembly changes in ${time.inMilliseconds}ms."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun processMessage(message: ClientMessage) {
|
||||
val time = measureTime {
|
||||
when (message) {
|
||||
is ClientNotification.SetAsm,
|
||||
is ClientNotification.UpdateAsm ->
|
||||
logger.error { "Unexpected ${message::class.simpleName}." }
|
||||
|
||||
is Request.GetCompletions ->
|
||||
getCompletions(message.id, message.lineNo, message.col)
|
||||
|
||||
is Request.GetSignatureHelp ->
|
||||
getSignatureHelp(message.id, message.lineNo, message.col)
|
||||
|
||||
is Request.GetHover ->
|
||||
getHover(message.id, message.lineNo, message.col)
|
||||
|
||||
is Request.GetDefinition ->
|
||||
getDefinition(message.id, message.lineNo, message.col)
|
||||
|
||||
is Request.GetLabels ->
|
||||
getLabels(message.id)
|
||||
}
|
||||
}
|
||||
|
||||
logger.trace { "Processed ${message::class.simpleName} in ${time.inMilliseconds}ms." }
|
||||
}
|
||||
|
||||
private fun setAsm(asm: List<String>, inlineStackArgs: Boolean) {
|
||||
fun setAsm(asm: List<String>, inlineStackArgs: Boolean) {
|
||||
this.inlineStackArgs = inlineStackArgs
|
||||
this.asm.splice(0, this.asm.length, *asm.toTypedArray())
|
||||
mapDesignations = null
|
||||
}
|
||||
|
||||
private fun updateAsm(changes: List<AsmChange>) {
|
||||
fun updateAsm(changes: List<AsmChange>) {
|
||||
for (change in changes) {
|
||||
val (startLineNo, startCol, endLineNo, endCol) = change.range
|
||||
val linesChanged = endLineNo - startLineNo + 1
|
||||
@ -223,19 +131,21 @@ class AssemblyWorker(private val sendMessage: (ServerMessage) -> Unit) {
|
||||
)
|
||||
}
|
||||
|
||||
private fun processAsm() {
|
||||
fun processAsm(): List<ServerNotification> {
|
||||
_cfg = null
|
||||
|
||||
val notifications = mutableListOf<ServerNotification>()
|
||||
val assemblyResult = assemble(asm.asArray().toList(), inlineStackArgs)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val problems = (assemblyResult.problems as List<AssemblerAssemblyProblem>).map {
|
||||
AssemblyProblem(it.severity, it.uiMessage, it.lineNo, it.col, it.len)
|
||||
}
|
||||
val problems =
|
||||
(assemblyResult.problems as List<LibAssemblyProblem>).map {
|
||||
AssemblyProblem(it.severity, it.uiMessage, it.lineNo, it.col, it.len)
|
||||
}
|
||||
|
||||
if (problems != this.problems) {
|
||||
this.problems = problems
|
||||
sendMessage(ServerNotification.Problems(problems))
|
||||
notifications.add(ServerNotification.Problems(problems))
|
||||
}
|
||||
|
||||
if (assemblyResult is Success) {
|
||||
@ -248,17 +158,17 @@ class AssemblyWorker(private val sendMessage: (ServerMessage) -> Unit) {
|
||||
|
||||
if (designations != mapDesignations) {
|
||||
mapDesignations = designations
|
||||
sendMessage(
|
||||
ServerNotification.MapDesignations(
|
||||
designations
|
||||
)
|
||||
notifications.add(
|
||||
ServerNotification.MapDesignations(designations)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return notifications
|
||||
}
|
||||
|
||||
private fun getCompletions(requestId: Int, lineNo: Int, col: Int) {
|
||||
fun getCompletions(requestId: Int, lineNo: Int, col: Int): Response.GetCompletions {
|
||||
val text = getLine(lineNo)?.take(col)?.trim()?.toLowerCase() ?: ""
|
||||
|
||||
val completions: List<CompletionItem> = when {
|
||||
@ -277,38 +187,19 @@ class AssemblyWorker(private val sendMessage: (ServerMessage) -> Unit) {
|
||||
else -> emptyList()
|
||||
}
|
||||
|
||||
sendMessage(Response.GetCompletions(requestId, completions))
|
||||
return Response.GetCompletions(requestId, completions)
|
||||
}
|
||||
|
||||
private fun getSignatureHelp(requestId: Int, lineNo: Int, col: Int) {
|
||||
sendMessage(Response.GetSignatureHelp(requestId, signatureHelp(lineNo, col)))
|
||||
}
|
||||
fun getSignatureHelp(requestId: Int, lineNo: Int, col: Int): Response.GetSignatureHelp =
|
||||
Response.GetSignatureHelp(requestId, signatureHelp(lineNo, col))
|
||||
|
||||
private fun signatureHelp(lineNo: Int, col: Int): SignatureHelp? {
|
||||
// Hacky way of providing parameter hints.
|
||||
// We just tokenize the current line and look for the first identifier and check whether
|
||||
// it's a valid opcode.
|
||||
var signature: Signature? = null
|
||||
var activeParam = -1
|
||||
|
||||
getLine(lineNo)?.let { text ->
|
||||
tokenizer.tokenize(text)
|
||||
|
||||
while (tokenizer.nextToken()) {
|
||||
if (tokenizer.type === Token.Ident) {
|
||||
mnemonicToOpcode(tokenizer.strValue)?.let { opcode ->
|
||||
signature = getSignature(opcode)
|
||||
}
|
||||
}
|
||||
|
||||
if (tokenizer.col + tokenizer.len > col) {
|
||||
break
|
||||
} else if (tokenizer.type === Token.Ident && activeParam == -1) {
|
||||
activeParam = 0
|
||||
} else if (tokenizer.type === Token.ArgSeparator) {
|
||||
activeParam++
|
||||
}
|
||||
}
|
||||
getInstructionForSrcLoc(lineNo, col)?.let { (inst, argIdx) ->
|
||||
signature = getSignature(inst.opcode)
|
||||
activeParam = argIdx
|
||||
}
|
||||
|
||||
return signature?.let { sig ->
|
||||
@ -319,7 +210,7 @@ class AssemblyWorker(private val sendMessage: (ServerMessage) -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun getHover(requestId: Int, lineNo: Int, col: Int) {
|
||||
fun getHover(requestId: Int, lineNo: Int, col: Int): Response.GetHover {
|
||||
val hover = signatureHelp(lineNo, col)?.let { help ->
|
||||
val sig = help.signature
|
||||
val param = sig.parameters.getOrNull(help.activeParameter)
|
||||
@ -364,13 +255,13 @@ class AssemblyWorker(private val sendMessage: (ServerMessage) -> Unit) {
|
||||
Hover(contents)
|
||||
}
|
||||
|
||||
sendMessage(Response.GetHover(requestId, hover))
|
||||
return Response.GetHover(requestId, hover)
|
||||
}
|
||||
|
||||
private fun getDefinition(requestId: Int, lineNo: Int, col: Int) {
|
||||
fun getDefinition(requestId: Int, lineNo: Int, col: Int): Response.GetDefinition {
|
||||
var result = emptyList<AsmRange>()
|
||||
|
||||
getInstruction(lineNo, col)?.let { inst ->
|
||||
getInstructionForSrcLoc(lineNo, col)?.inst?.let { inst ->
|
||||
loop@
|
||||
for ((paramIdx, param) in inst.opcode.params.withIndex()) {
|
||||
if (param.type is LabelType) {
|
||||
@ -381,7 +272,7 @@ class AssemblyWorker(private val sendMessage: (ServerMessage) -> Unit) {
|
||||
|
||||
for (i in 0 until min(args.size, argSrcLocs.size)) {
|
||||
val arg = args[i]
|
||||
val srcLoc = argSrcLocs[i]
|
||||
val srcLoc = argSrcLocs[i].coarse
|
||||
|
||||
if (positionInside(lineNo, col, srcLoc)) {
|
||||
val label = arg.value as Int
|
||||
@ -394,7 +285,7 @@ class AssemblyWorker(private val sendMessage: (ServerMessage) -> Unit) {
|
||||
val argSrcLocs = inst.getStackArgSrcLocs(paramIdx)
|
||||
|
||||
for ((i, argSrcLoc) in argSrcLocs.withIndex()) {
|
||||
if (positionInside(lineNo, col, argSrcLoc)) {
|
||||
if (positionInside(lineNo, col, argSrcLoc.coarse)) {
|
||||
val labelValues = getStackValue(cfg, inst, argSrcLocs.lastIndex - i)
|
||||
|
||||
if (labelValues.size <= 5) {
|
||||
@ -409,41 +300,7 @@ class AssemblyWorker(private val sendMessage: (ServerMessage) -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
sendMessage(Response.GetDefinition(requestId, result))
|
||||
}
|
||||
|
||||
private fun getInstruction(lineNo: Int, col: Int): Instruction? {
|
||||
for (segment in bytecodeIr.segments) {
|
||||
if (segment is InstructionSegment) {
|
||||
// Loop over instructions in reverse order so stack popping instructions will be
|
||||
// handled before the related stack pushing instructions when inlineStackArgs is on.
|
||||
for (i in segment.instructions.lastIndex downTo 0) {
|
||||
val inst = segment.instructions[i]
|
||||
|
||||
inst.srcLoc?.let { srcLoc ->
|
||||
if (positionInside(lineNo, col, srcLoc.mnemonic)) {
|
||||
return inst
|
||||
}
|
||||
|
||||
for (argSrcLoc in srcLoc.args) {
|
||||
if (positionInside(lineNo, col, argSrcLoc)) {
|
||||
return inst
|
||||
}
|
||||
}
|
||||
|
||||
if (inlineStackArgs) {
|
||||
for (argSrcLoc in srcLoc.stackArgs) {
|
||||
if (positionInside(lineNo, col, argSrcLoc)) {
|
||||
return inst
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
return Response.GetDefinition(requestId, result)
|
||||
}
|
||||
|
||||
private fun getLabelDefinitions(label: Int): List<AsmRange> =
|
||||
@ -455,7 +312,7 @@ class AssemblyWorker(private val sendMessage: (ServerMessage) -> Unit) {
|
||||
}
|
||||
.toList()
|
||||
|
||||
private fun getLabels(requestId: Int) {
|
||||
fun getLabels(requestId: Int): Response.GetLabels {
|
||||
val result = bytecodeIr.segments.asSequence()
|
||||
.flatMap { segment ->
|
||||
segment.labels.mapIndexed { labelIdx, label ->
|
||||
@ -465,14 +322,104 @@ class AssemblyWorker(private val sendMessage: (ServerMessage) -> Unit) {
|
||||
}
|
||||
.toList()
|
||||
|
||||
sendMessage(Response.GetLabels(requestId, result))
|
||||
return Response.GetLabels(requestId, result)
|
||||
}
|
||||
|
||||
fun getHighlights(requestId: Int, lineNo: Int, col: Int): Response.GetHighlights {
|
||||
val result = mutableListOf<AsmRange>()
|
||||
|
||||
when (val ir = getIrForSrcLoc(lineNo, col)) {
|
||||
is Ir.Inst -> {
|
||||
val srcLoc = ir.inst.srcLoc?.mnemonic
|
||||
|
||||
if (ir.argIdx == -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)
|
||||
) {
|
||||
for (segment in bytecodeIr.segments) {
|
||||
if (segment is InstructionSegment) {
|
||||
for (inst in segment.instructions) {
|
||||
if (inst.opcode.code == ir.inst.opcode.code) {
|
||||
inst.srcLoc?.mnemonic?.toAsmRange()?.let(result::add)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Response.GetHighlights(requestId, result)
|
||||
}
|
||||
|
||||
private fun getInstructionForSrcLoc(lineNo: Int, col: Int): Ir.Inst? =
|
||||
getIrForSrcLoc(lineNo, col) as? Ir.Inst
|
||||
|
||||
private fun getIrForSrcLoc(lineNo: Int, col: Int): Ir? {
|
||||
for (segment in bytecodeIr.segments) {
|
||||
if (segment is InstructionSegment) {
|
||||
// Loop over instructions in reverse order so stack popping instructions will be
|
||||
// handled before the related stack pushing instructions when inlineStackArgs is on.
|
||||
for (i in segment.instructions.lastIndex downTo 0) {
|
||||
val inst = segment.instructions[i]
|
||||
|
||||
inst.srcLoc?.let { srcLoc ->
|
||||
var instLineNo = -1
|
||||
var lastCol = -1
|
||||
|
||||
srcLoc.mnemonic?.let { mnemonicSrcLoc ->
|
||||
instLineNo = mnemonicSrcLoc.lineNo
|
||||
lastCol = mnemonicSrcLoc.col + mnemonicSrcLoc.len
|
||||
|
||||
if (positionInside(lineNo, col, mnemonicSrcLoc)) {
|
||||
return Ir.Inst(inst, argIdx = -1)
|
||||
}
|
||||
}
|
||||
|
||||
for ((argIdx, argSrcLoc) in srcLoc.args.withIndex()) {
|
||||
instLineNo = argSrcLoc.coarse.lineNo
|
||||
lastCol = argSrcLoc.coarse.col + argSrcLoc.coarse.len
|
||||
|
||||
if (positionInside(lineNo, col, argSrcLoc.coarse)) {
|
||||
return Ir.Inst(inst, argIdx)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun positionInside(lineNo: Int, col: Int, srcLoc: SrcLoc?): Boolean =
|
||||
if (srcLoc == null) {
|
||||
false
|
||||
} else {
|
||||
lineNo == srcLoc.lineNo && col >= srcLoc.col && col <= srcLoc.col + srcLoc.len
|
||||
lineNo == srcLoc.lineNo && col >= srcLoc.col && col < srcLoc.col + srcLoc.len
|
||||
}
|
||||
|
||||
@Suppress("RedundantNullableReturnType") // Can return undefined.
|
||||
@ -486,6 +433,10 @@ class AssemblyWorker(private val sendMessage: (ServerMessage) -> Unit) {
|
||||
endCol = col + len,
|
||||
)
|
||||
|
||||
private sealed class Ir {
|
||||
data class Inst(val inst: Instruction, val argIdx: Int) : Ir()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val KEYWORD_REGEX = Regex("""^\s*\.[a-z]+${'$'}""")
|
||||
private val KEYWORD_SUGGESTIONS: List<CompletionItem> =
|
@ -0,0 +1,121 @@
|
||||
package world.phantasmal.web.assemblyWorker
|
||||
|
||||
import mu.KotlinLogging
|
||||
import world.phantasmal.web.shared.Throttle
|
||||
import world.phantasmal.web.shared.messages.ClientMessage
|
||||
import world.phantasmal.web.shared.messages.ClientNotification
|
||||
import world.phantasmal.web.shared.messages.Request
|
||||
import world.phantasmal.web.shared.messages.ServerMessage
|
||||
import kotlin.time.measureTime
|
||||
|
||||
class AsmServer(
|
||||
private val asmAnalyser: AsmAnalyser,
|
||||
private val sendMessage: (ServerMessage) -> Unit,
|
||||
) {
|
||||
private val messageQueue: MutableList<ClientMessage> = mutableListOf()
|
||||
private val messageProcessingThrottle = Throttle(wait = 100)
|
||||
|
||||
fun receiveMessage(message: ClientMessage) {
|
||||
messageQueue.add(message)
|
||||
messageProcessingThrottle(::processMessages)
|
||||
}
|
||||
|
||||
private fun processMessages() {
|
||||
// Split messages into ASM changes and other messages. Remove useless/duplicate
|
||||
// notifications.
|
||||
val asmChanges = mutableListOf<ClientNotification>()
|
||||
val otherMessages = mutableListOf<ClientMessage>()
|
||||
|
||||
for (message in messageQueue) {
|
||||
when (message) {
|
||||
is ClientNotification.SetAsm -> {
|
||||
// All previous ASM change messages can be discarded when the entire ASM has
|
||||
// changed.
|
||||
asmChanges.clear()
|
||||
asmChanges.add(message)
|
||||
}
|
||||
|
||||
is ClientNotification.UpdateAsm ->
|
||||
asmChanges.add(message)
|
||||
|
||||
else ->
|
||||
otherMessages.add(message)
|
||||
}
|
||||
}
|
||||
|
||||
messageQueue.clear()
|
||||
|
||||
// Process ASM changes first.
|
||||
processAsmChanges(asmChanges)
|
||||
otherMessages.forEach(::processMessage)
|
||||
}
|
||||
|
||||
private fun processAsmChanges(messages: List<ClientNotification>) {
|
||||
if (messages.isNotEmpty()) {
|
||||
val time = measureTime {
|
||||
for (message in messages) {
|
||||
when (message) {
|
||||
is ClientNotification.SetAsm ->
|
||||
asmAnalyser.setAsm(message.asm, message.inlineStackArgs)
|
||||
|
||||
is ClientNotification.UpdateAsm ->
|
||||
asmAnalyser.updateAsm(message.changes)
|
||||
|
||||
else ->
|
||||
// Should be processed by processMessage.
|
||||
logger.error { "Unexpected ${message::class.simpleName}." }
|
||||
}
|
||||
}
|
||||
|
||||
asmAnalyser.processAsm().forEach(sendMessage)
|
||||
}
|
||||
|
||||
logger.trace {
|
||||
"Processed ${messages.size} assembly changes in ${time.inMilliseconds}ms."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun processMessage(message: ClientMessage) {
|
||||
val time = measureTime {
|
||||
when (message) {
|
||||
is ClientNotification.SetAsm,
|
||||
is ClientNotification.UpdateAsm ->
|
||||
// Should have been processed by processAsmChanges.
|
||||
logger.error { "Unexpected ${message::class.simpleName}." }
|
||||
|
||||
is Request -> processRequest(message)
|
||||
}
|
||||
}
|
||||
|
||||
logger.trace { "Processed ${message::class.simpleName} in ${time.inMilliseconds}ms." }
|
||||
}
|
||||
|
||||
private fun processRequest(message: Request) {
|
||||
val response = when (message) {
|
||||
is Request.GetCompletions ->
|
||||
asmAnalyser.getCompletions(message.id, message.lineNo, message.col)
|
||||
|
||||
is Request.GetSignatureHelp ->
|
||||
asmAnalyser.getSignatureHelp(message.id, message.lineNo, message.col)
|
||||
|
||||
is Request.GetHover ->
|
||||
asmAnalyser.getHover(message.id, message.lineNo, message.col)
|
||||
|
||||
is Request.GetDefinition ->
|
||||
asmAnalyser.getDefinition(message.id, message.lineNo, message.col)
|
||||
|
||||
is Request.GetLabels ->
|
||||
asmAnalyser.getLabels(message.id)
|
||||
|
||||
is Request.GetHighlights ->
|
||||
asmAnalyser.getHighlights(message.id, message.lineNo, message.col)
|
||||
}
|
||||
|
||||
sendMessage(response)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val logger = KotlinLogging.logger {}
|
||||
}
|
||||
}
|
@ -17,7 +17,8 @@ fun main() {
|
||||
KotlinLoggingConfiguration.LOG_LEVEL = KotlinLoggingLevel.TRACE
|
||||
}
|
||||
|
||||
val asmWorker = AssemblyWorker(
|
||||
val asmServer = AsmServer(
|
||||
AsmAnalyser(),
|
||||
sendMessage = { message ->
|
||||
self.postMessage(JSON_FORMAT.encodeToString(message))
|
||||
}
|
||||
@ -25,6 +26,6 @@ fun main() {
|
||||
|
||||
self.onmessage = { e ->
|
||||
val json = e.data as String
|
||||
asmWorker.receiveMessage(JSON_FORMAT.decodeFromString(json))
|
||||
asmServer.receiveMessage(JSON_FORMAT.decodeFromString(json))
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,128 @@
|
||||
package world.phantasmal.web.assemblyWorker
|
||||
|
||||
import world.phantasmal.web.assemblyWorker.test.AssemblyWorkerTestSuite
|
||||
import world.phantasmal.web.assemblyWorker.test.assertDeepEquals
|
||||
import world.phantasmal.web.shared.messages.*
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class AsmAnalyserTests : AssemblyWorkerTestSuite() {
|
||||
@Test
|
||||
fun getSignatureHelp() = test {
|
||||
val analyser = createAsmAnalyser(
|
||||
"""
|
||||
.code
|
||||
0: leti r255, 1337
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
val requestId = 113
|
||||
|
||||
for (col in 1..3) {
|
||||
val response = analyser.getSignatureHelp(requestId, lineNo = 2, col)
|
||||
|
||||
assertDeepEquals(Response.GetSignatureHelp(requestId, null), response)
|
||||
}
|
||||
|
||||
fun sigHelp(activeParameter: Int) = Response.GetSignatureHelp(
|
||||
requestId,
|
||||
SignatureHelp(
|
||||
Signature(
|
||||
label = "leti Reg<out Int>, Int",
|
||||
documentation = "Sets a register to the given value.",
|
||||
listOf(
|
||||
Parameter(labelStart = 5, labelEnd = 17, null),
|
||||
Parameter(labelStart = 19, labelEnd = 22, null),
|
||||
),
|
||||
),
|
||||
activeParameter,
|
||||
),
|
||||
)
|
||||
|
||||
for ((colRange, sigHelp) in listOf(
|
||||
4..7 to sigHelp(-1),
|
||||
8..13 to sigHelp(0),
|
||||
14..19 to sigHelp(1),
|
||||
)) {
|
||||
for (col in colRange) {
|
||||
val response = analyser.getSignatureHelp(requestId, 2, col)
|
||||
|
||||
assertDeepEquals(sigHelp, response, "col = $col")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getHighlights_for_instruction() = test {
|
||||
val analyser = createAsmAnalyser(
|
||||
"""
|
||||
.code
|
||||
0:
|
||||
ret
|
||||
ret
|
||||
ret
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
val requestId = 223
|
||||
|
||||
// Center char "e" of center ret instruction.
|
||||
val response = analyser.getHighlights(requestId, 4, 6)
|
||||
|
||||
assertEquals(3, response.result.size)
|
||||
assertEquals(AsmRange(3, 5, 3, 8), response.result[0])
|
||||
assertEquals(AsmRange(4, 5, 4, 8), response.result[1])
|
||||
assertEquals(AsmRange(5, 5, 5, 8), response.result[2])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getHighlights_for_int() = test {
|
||||
val analyser = createAsmAnalyser(
|
||||
"""
|
||||
.code
|
||||
0:
|
||||
set_episode 0
|
||||
set_episode 0
|
||||
set_episode 0
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
val requestId = 137
|
||||
|
||||
// 0 Argument of center set_episode instruction.
|
||||
val response = analyser.getHighlights(requestId, 4, 17)
|
||||
|
||||
assertTrue(response.result.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getHighlights_col_right_after_mnemonic() = test {
|
||||
val analyser = createAsmAnalyser(
|
||||
"""
|
||||
.code
|
||||
0:
|
||||
leti r10, 4000
|
||||
leti r10, 4000
|
||||
leti r10, 4000
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
val requestId = 2999
|
||||
|
||||
// Cursor is right after the center leti instruction.
|
||||
val response = analyser.getHighlights(requestId, 4, 9)
|
||||
|
||||
assertEquals(3, response.result.size)
|
||||
assertEquals(AsmRange(3, 5, 3, 9), response.result[0])
|
||||
assertEquals(AsmRange(4, 5, 4, 9), response.result[1])
|
||||
assertEquals(AsmRange(5, 5, 5, 9), response.result[2])
|
||||
}
|
||||
|
||||
private fun createAsmAnalyser(asm: String): AsmAnalyser {
|
||||
val analyser = AsmAnalyser()
|
||||
analyser.setAsm(asm.split("\n"), inlineStackArgs = true)
|
||||
analyser.processAsm()
|
||||
return analyser
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package world.phantasmal.web.assemblyWorker.test
|
||||
|
||||
import world.phantasmal.core.disposable.Disposer
|
||||
import world.phantasmal.testUtils.AbstractTestSuite
|
||||
import world.phantasmal.testUtils.TestContext
|
||||
|
||||
abstract class AssemblyWorkerTestSuite : AbstractTestSuite<TestContext> {
|
||||
override fun createContext(disposer: Disposer) = TestContext(disposer)
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package world.phantasmal.web.assemblyWorker.test
|
||||
|
||||
import world.phantasmal.testUtils.assertDeepEquals
|
||||
import world.phantasmal.web.shared.messages.Parameter
|
||||
import world.phantasmal.web.shared.messages.Response
|
||||
import world.phantasmal.web.shared.messages.Signature
|
||||
import world.phantasmal.web.shared.messages.SignatureHelp
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertNull
|
||||
|
||||
fun assertDeepEquals(
|
||||
expected: Response.GetSignatureHelp,
|
||||
actual: Response.GetSignatureHelp,
|
||||
message: String? = null,
|
||||
) {
|
||||
assertEquals(expected.id, actual.id, message)
|
||||
|
||||
if (expected.result == null) {
|
||||
assertNull(actual.result, message)
|
||||
} else {
|
||||
assertNotNull(actual.result, message)
|
||||
assertDeepEquals(expected.result!!, actual.result!!, message)
|
||||
}
|
||||
}
|
||||
|
||||
fun assertDeepEquals(expected: SignatureHelp, actual: SignatureHelp, message: String? = null) {
|
||||
assertDeepEquals(expected.signature, actual.signature, message)
|
||||
assertEquals(expected.activeParameter, actual.activeParameter, message)
|
||||
}
|
||||
|
||||
fun assertDeepEquals(expected: Signature, actual: Signature, message: String? = null) {
|
||||
assertEquals(expected.label, actual.label, message)
|
||||
assertEquals(expected.documentation, actual.documentation, message)
|
||||
assertDeepEquals(expected.parameters, actual.parameters, ::assertDeepEquals, message)
|
||||
}
|
||||
|
||||
fun assertDeepEquals(expected: Parameter, actual: Parameter, message: String? = null) {
|
||||
assertEquals(expected.labelStart, actual.labelStart, message)
|
||||
assertEquals(expected.labelEnd, actual.labelEnd, message)
|
||||
assertEquals(expected.documentation, actual.documentation, message)
|
||||
}
|
@ -51,6 +51,9 @@ sealed class Request : ClientMessage() {
|
||||
|
||||
@Serializable
|
||||
class GetLabels(override val id: Int) : Request()
|
||||
|
||||
@Serializable
|
||||
class GetHighlights(override val id: Int, val lineNo: Int, val col: Int) : Request()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@ -107,6 +110,12 @@ sealed class Response<T> : ServerMessage() {
|
||||
override val id: Int,
|
||||
override val result: List<Label>,
|
||||
) : Response<List<Label>>()
|
||||
|
||||
@Serializable
|
||||
class GetHighlights(
|
||||
override val id: Int,
|
||||
override val result: List<AsmRange>,
|
||||
) : Response<List<AsmRange>>()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@ -139,11 +148,11 @@ class Signature(val label: String, val documentation: String?, val parameters: L
|
||||
@Serializable
|
||||
class Parameter(
|
||||
/**
|
||||
* Start column of the parameter label within [Signature.label].
|
||||
* Start index of the parameter label within [Signature.label].
|
||||
*/
|
||||
val labelStart: Int,
|
||||
/**
|
||||
* End column (exclusive) of the parameter label within [Signature.label].
|
||||
* End index (exclusive) of the parameter label within [Signature.label].
|
||||
*/
|
||||
val labelEnd: Int,
|
||||
val documentation: String?,
|
||||
|
@ -54,6 +54,11 @@ external fun registerDocumentSymbolProvider(
|
||||
provider: DocumentSymbolProvider,
|
||||
): IDisposable
|
||||
|
||||
external fun registerDocumentHighlightProvider(
|
||||
languageId: String,
|
||||
provider: DocumentHighlightProvider,
|
||||
): IDisposable
|
||||
|
||||
external interface CommentRule {
|
||||
var lineComment: String?
|
||||
get() = definedExternally
|
||||
@ -727,3 +732,22 @@ external interface DocumentSymbolProvider {
|
||||
token: CancellationToken,
|
||||
): Promise<Array<DocumentSymbol>>
|
||||
}
|
||||
|
||||
external enum class DocumentHighlightKind {
|
||||
Text /* = 0 */,
|
||||
Read /* = 1 */,
|
||||
Write /* = 2 */
|
||||
}
|
||||
|
||||
external interface DocumentHighlight {
|
||||
var range: IRange
|
||||
var kind: DocumentHighlightKind?
|
||||
}
|
||||
|
||||
external interface DocumentHighlightProvider {
|
||||
fun provideDocumentHighlights(
|
||||
model: ITextModel,
|
||||
position: Position,
|
||||
token: CancellationToken,
|
||||
): Promise<Array<DocumentHighlight>>
|
||||
}
|
||||
|
@ -69,6 +69,9 @@ class AsmAnalyser {
|
||||
suspend fun getLabels(): List<Label> =
|
||||
sendRequest { id -> Request.GetLabels(id) }
|
||||
|
||||
suspend fun getHighlights(lineNo: Int, col: Int): List<AsmRange> =
|
||||
sendRequest { id -> Request.GetHighlights(id, lineNo, col) }
|
||||
|
||||
private suspend fun <T> sendRequest(createRequest: (id: Int) -> Request): T {
|
||||
val id = nextRequestId.getAndIncrement()
|
||||
|
||||
|
@ -0,0 +1,25 @@
|
||||
package world.phantasmal.web.questEditor.asm.monaco
|
||||
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.promise
|
||||
import world.phantasmal.web.externals.monacoEditor.*
|
||||
import world.phantasmal.web.questEditor.asm.AsmAnalyser
|
||||
import world.phantasmal.webui.obj
|
||||
import kotlin.js.Promise
|
||||
|
||||
class AsmDocumentHighlightProvider(private val analyser: AsmAnalyser) : DocumentHighlightProvider {
|
||||
override fun provideDocumentHighlights(
|
||||
model: ITextModel,
|
||||
position: Position,
|
||||
token: CancellationToken
|
||||
): Promise<Array<DocumentHighlight>> =
|
||||
GlobalScope.promise {
|
||||
val highlights = analyser.getHighlights(position.lineNumber, position.column)
|
||||
|
||||
Array(highlights.size) {
|
||||
obj {
|
||||
range = highlights[it].toIRange()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -172,6 +172,10 @@ class AsmStore(
|
||||
registerHoverProvider(ASM_LANG_ID, AsmHoverProvider(asmAnalyser))
|
||||
registerDefinitionProvider(ASM_LANG_ID, AsmDefinitionProvider(asmAnalyser))
|
||||
registerDocumentSymbolProvider(ASM_LANG_ID, AsmDocumentSymbolProvider(asmAnalyser))
|
||||
registerDocumentHighlightProvider(
|
||||
ASM_LANG_ID,
|
||||
AsmDocumentHighlightProvider(asmAnalyser)
|
||||
)
|
||||
// TODO: Add semantic highlighting with registerDocumentSemanticTokensProvider (or
|
||||
// registerDocumentRangeSemanticTokensProvider?).
|
||||
// Enable when calling editor.create with 'semanticHighlighting.enabled': true.
|
||||
|
@ -26,7 +26,7 @@ class AsmEditorWidget(private val ctrl: AsmEditorController) : Widget() {
|
||||
renderIndentGuides = false
|
||||
folding = false
|
||||
wordBasedSuggestions = false
|
||||
occurrencesHighlight = false
|
||||
occurrencesHighlight = true
|
||||
fixedOverflowWidgets = true
|
||||
})
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user