AsmAnalyser is now updated whenever text changes.

This commit is contained in:
Daan Vanden Bosch 2020-12-08 15:41:43 +01:00
parent fb7aaf2906
commit 540e35ffc9
9 changed files with 235 additions and 45 deletions

View File

@ -0,0 +1,29 @@
package world.phantasmal.core
fun <E> MutableList<E>.replaceAll(elements: Collection<E>): Boolean {
clear()
return addAll(elements)
}
fun <E> MutableList<E>.replaceAll(elements: Iterable<E>): Boolean {
clear()
return addAll(elements)
}
fun <E> MutableList<E>.replaceAll(elements: Sequence<E>): Boolean {
clear()
return addAll(elements)
}
/**
* Replace [amount] elements at [startIndex] with [elements].
*/
fun <E> MutableList<E>.splice(startIndex: Int, amount: Int, elements: Iterable<E>) {
repeat(amount) { removeAt(startIndex) }
var i = startIndex
for (element in elements) {
add(i++, element)
}
}

View File

@ -1,8 +1,33 @@
@file:Suppress("NOTHING_TO_INLINE")
package world.phantasmal.core package world.phantasmal.core
@Suppress("NOTHING_TO_INLINE") external interface JsArray<T> {
val length: Int
fun push(vararg elements: T): Int
fun slice(start: Int = definedExternally): JsArray<T>
fun slice(start: Int, end: Int = definedExternally): JsArray<T>
fun splice(start: Int, deleteCount: Int = definedExternally): JsArray<T>
fun splice(start: Int, deleteCount: Int, vararg items: T): JsArray<T>
}
inline operator fun <T> JsArray<T>.get(index: Int): T = asDynamic()[index].unsafeCast<T>()
inline operator fun <T> JsArray<T>.set(index: Int, value: T) {
asDynamic()[index] = value
}
inline fun <T> jsArrayOf(vararg elements: T): JsArray<T> = inline fun <T> jsArrayOf(vararg elements: T): JsArray<T> =
elements.unsafeCast<JsArray<T>>() elements.unsafeCast<JsArray<T>>()
inline fun <T> JsArray<T>.asArray(): Array<T> = inline fun <T> JsArray<T>.asArray(): Array<T> =
unsafeCast<Array<T>>() unsafeCast<Array<T>>()
inline fun <T> Array<T>.asJsArray(): JsArray<T> =
unsafeCast<JsArray<T>>()
inline fun <T> List<T>.toJsArray(): JsArray<T> =
toTypedArray().asJsArray()

View File

@ -1,5 +0,0 @@
package world.phantasmal.core
external interface JsArray<T> {
fun push(vararg elements: T): Int
}

View File

@ -2,6 +2,7 @@ package world.phantasmal.observable.value.list
import world.phantasmal.core.disposable.Disposable import world.phantasmal.core.disposable.Disposable
import world.phantasmal.core.disposable.disposable import world.phantasmal.core.disposable.disposable
import world.phantasmal.core.replaceAll
import world.phantasmal.observable.Observer import world.phantasmal.observable.Observer
import world.phantasmal.observable.value.AbstractVal import world.phantasmal.observable.value.AbstractVal
import world.phantasmal.observable.value.Val import world.phantasmal.observable.value.Val
@ -61,8 +62,7 @@ class DependentListVal<E>(
} }
private fun recompute() { private fun recompute() {
elements.clear() elements.replaceAll(computeElements())
elements.addAll(computeElements())
} }
private fun initDependencyObservers() { private fun initDependencyObservers() {

View File

@ -1,5 +1,6 @@
package world.phantasmal.observable.value.list package world.phantasmal.observable.value.list
import world.phantasmal.core.replaceAll
import world.phantasmal.observable.Observable import world.phantasmal.observable.Observable
import world.phantasmal.observable.value.MutableVal import world.phantasmal.observable.value.MutableVal
import world.phantasmal.observable.value.Val import world.phantasmal.observable.value.Val
@ -64,15 +65,13 @@ class SimpleListVal<E>(
override fun replaceAll(elements: Iterable<E>) { override fun replaceAll(elements: Iterable<E>) {
val removed = ArrayList(this.elements) val removed = ArrayList(this.elements)
this.elements.clear() this.elements.replaceAll(elements)
this.elements.addAll(elements)
finalizeUpdate(ListValChangeEvent.Change(0, removed, this.elements)) finalizeUpdate(ListValChangeEvent.Change(0, removed, this.elements))
} }
override fun replaceAll(elements: Sequence<E>) { override fun replaceAll(elements: Sequence<E>) {
val removed = ArrayList(this.elements) val removed = ArrayList(this.elements)
this.elements.clear() this.elements.replaceAll(elements)
this.elements.addAll(elements)
finalizeUpdate(ListValChangeEvent.Change(0, removed, this.elements)) finalizeUpdate(ListValChangeEvent.Change(0, removed, this.elements))
} }

View File

@ -1,6 +1,6 @@
package world.phantasmal.web.questEditor.asm package world.phantasmal.web.questEditor.asm
import world.phantasmal.core.Success import world.phantasmal.core.*
import world.phantasmal.lib.asm.* import world.phantasmal.lib.asm.*
import world.phantasmal.lib.asm.dataFlowAnalysis.getMapDesignations import world.phantasmal.lib.asm.dataFlowAnalysis.getMapDesignations
import world.phantasmal.observable.value.Val import world.phantasmal.observable.value.Val
@ -46,7 +46,7 @@ object AsmAnalyser {
} }
private var inlineStackArgs: Boolean = true private var inlineStackArgs: Boolean = true
private var asm: List<String> = emptyList() private val asm: JsArray<String> = jsArrayOf()
private var _bytecodeIr = mutableVal(BytecodeIr(emptyList())) private var _bytecodeIr = mutableVal(BytecodeIr(emptyList()))
private var _mapDesignations = mutableVal<Map<Int, Int>>(emptyMap()) private var _mapDesignations = mutableVal<Map<Int, Int>>(emptyMap())
private val _problems = mutableListVal<AssemblyProblem>() private val _problems = mutableListVal<AssemblyProblem>()
@ -57,13 +57,111 @@ object AsmAnalyser {
suspend fun setAsm(asm: List<String>, inlineStackArgs: Boolean) { suspend fun setAsm(asm: List<String>, inlineStackArgs: Boolean) {
this.inlineStackArgs = inlineStackArgs this.inlineStackArgs = inlineStackArgs
this.asm = asm this.asm.splice(0, this.asm.length, *asm.toTypedArray())
processAsm() processAsm()
} }
suspend fun updateAssembly(changes: List<AsmChange>) {
for (change in changes) {
val (startLineNo, startCol, endLineNo, endCol) = change.range
val linesChanged = endLineNo - startLineNo + 1
val newLines = change.newAsm.split("\n").toJsArray()
when {
linesChanged == 1 -> {
replaceLinePart(startLineNo, startCol, endCol, newLines)
}
newLines.length == 1 -> {
replaceLinesAndMergeLineParts(
startLineNo,
endLineNo,
startCol,
endCol,
newLines[0],
)
}
else -> {
// Keep the left part of the first changed line.
replaceLinePartRight(startLineNo, startCol, newLines[0])
// Keep the right part of the last changed line.
replaceLinePartLeft(endLineNo, endCol, newLines[newLines.length - 1])
// Replace all the lines in between.
// It's important that we do this last.
replaceLines(
startLineNo + 1,
endLineNo - 1,
newLines.slice(1, newLines.length - 1),
)
}
}
}
processAsm()
}
private fun replaceLinePart(
lineNo: Int,
startCol: Int,
endCol: Int,
newLineParts: JsArray<String>,
) {
val line = asm[lineNo - 1]
// We keep the parts of the line that weren't affected by the edit.
val lineStart = line.substring(0, startCol - 1)
val lineEnd = line.substring(endCol - 1)
if (newLineParts.length == 1) {
asm[lineNo - 1] = lineStart + newLineParts[0] + lineEnd
} else {
asm.splice(
lineNo - 1,
1,
lineStart + newLineParts[0],
*newLineParts.slice(1, newLineParts.length - 1).asArray(),
newLineParts[newLineParts.length - 1] + lineEnd,
)
}
}
private fun replaceLinePartLeft(lineNo: Int, endCol: Int, newLinePart: String) {
asm[lineNo - 1] = newLinePart + asm[lineNo - 1].substring(endCol - 1)
}
private fun replaceLinePartRight(lineNo: Int, startCol: Int, newLinePart: String) {
asm[lineNo - 1] = asm[lineNo - 1].substring(0, startCol - 1) + newLinePart
}
private fun replaceLines(startLineNo: Int, endLineNo: Int, newLines: JsArray<String>) {
asm.splice(startLineNo - 1, endLineNo - startLineNo + 1, *newLines.asArray())
}
private fun replaceLinesAndMergeLineParts(
startLineNo: Int,
endLineNo: Int,
startCol: Int,
endCol: Int,
newLinePart: String,
) {
val startLine = asm[startLineNo - 1]
val endLine = asm[endLineNo - 1]
// We keep the parts of the lines that weren't affected by the edit.
val startLineStart = startLine.substring(0, startCol - 1)
val endLineEnd = endLine.substring(endCol - 1)
asm.splice(
startLineNo - 1,
endLineNo - startLineNo + 1,
startLineStart + newLinePart + endLineEnd,
)
}
private fun processAsm() { private fun processAsm() {
val assemblyResult = assemble(asm, inlineStackArgs) val assemblyResult = assemble(asm.asArray().toList(), inlineStackArgs)
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
_problems.value = assemblyResult.problems as List<AssemblyProblem> _problems.value = assemblyResult.problems as List<AssemblyProblem>
@ -220,7 +318,7 @@ object AsmAnalyser {
return Hover(contents) return Hover(contents)
} }
suspend fun getDefinition(lineNo: Int, col: Int): List<TextRange> { suspend fun getDefinition(lineNo: Int, col: Int): List<AsmRange> {
getInstruction(lineNo, col)?.let { inst -> getInstruction(lineNo, col)?.let { inst ->
for ((paramIdx, param) in inst.opcode.params.withIndex()) { for ((paramIdx, param) in inst.opcode.params.withIndex()) {
if (param.type is LabelType) { if (param.type is LabelType) {
@ -290,14 +388,14 @@ object AsmAnalyser {
return null return null
} }
private fun getLabelDefinitions(label: Int): List<TextRange> = private fun getLabelDefinitions(label: Int): List<AsmRange> =
bytecodeIr.value.segments.asSequence() bytecodeIr.value.segments.asSequence()
.filter { label in it.labels } .filter { label in it.labels }
.mapNotNull { segment -> .mapNotNull { segment ->
val labelIdx = segment.labels.indexOf(label) val labelIdx = segment.labels.indexOf(label)
segment.srcLoc.labels.getOrNull(labelIdx)?.let { labelSrcLoc -> segment.srcLoc.labels.getOrNull(labelIdx)?.let { labelSrcLoc ->
TextRange( AsmRange(
startLineNo = labelSrcLoc.lineNo, startLineNo = labelSrcLoc.lineNo,
startCol = labelSrcLoc.col, startCol = labelSrcLoc.col,
endLineNo = labelSrcLoc.lineNo, endLineNo = labelSrcLoc.lineNo,
@ -314,5 +412,6 @@ object AsmAnalyser {
lineNo == srcLoc.lineNo && col >= srcLoc.col && col <= srcLoc.col + srcLoc.len lineNo == srcLoc.lineNo && col >= srcLoc.col && col <= srcLoc.col + srcLoc.len
} }
private fun getLine(lineNo: Int): String? = asm.getOrNull(lineNo - 1) @Suppress("RedundantNullableReturnType") // Can return undefined.
private fun getLine(lineNo: Int): String? = asm[lineNo - 1]
} }

View File

@ -1,10 +1,10 @@
package world.phantasmal.web.questEditor.asm package world.phantasmal.web.questEditor.asm
class TextRange( data class AsmRange(
var startLineNo: Int, val startLineNo: Int,
var startCol: Int, val startCol: Int,
var endLineNo: Int, val endLineNo: Int,
var endCol: Int, val endCol: Int,
) )
enum class CompletionItemType { enum class CompletionItemType {
@ -35,3 +35,8 @@ class Hover(
*/ */
val contents: List<String>, val contents: List<String>,
) )
class AsmChange(
val range: AsmRange,
val newAsm: String,
)

View File

@ -1,7 +1,10 @@
package world.phantasmal.web.questEditor.controllers package world.phantasmal.web.questEditor.controllers
import world.phantasmal.observable.Observable import world.phantasmal.observable.Observable
import world.phantasmal.observable.value.* import world.phantasmal.observable.value.Val
import world.phantasmal.observable.value.not
import world.phantasmal.observable.value.or
import world.phantasmal.observable.value.orElse
import world.phantasmal.web.externals.monacoEditor.ITextModel import world.phantasmal.web.externals.monacoEditor.ITextModel
import world.phantasmal.web.externals.monacoEditor.createModel import world.phantasmal.web.externals.monacoEditor.createModel
import world.phantasmal.web.questEditor.stores.AsmStore import world.phantasmal.web.questEditor.stores.AsmStore
@ -17,19 +20,24 @@ class AsmController(private val store: AsmStore) : Controller() {
val didRedo: Observable<Unit> = store.didRedo val didRedo: Observable<Unit> = store.didRedo
val inlineStackArgs: Val<Boolean> = store.inlineStackArgs val inlineStackArgs: Val<Boolean> = store.inlineStackArgs
val inlineStackArgsEnabled: Val<Boolean> = falseVal() // TODO val inlineStackArgsEnabled: Val<Boolean> = store.problems.map { it.isEmpty() }
val inlineStackArgsTooltip: Val<String> =
inlineStackArgsEnabled.map { enabled ->
buildString {
append("Transform arg_push* opcodes to be inline with the opcode the arguments are given to.")
// TODO: Notify user when disabled because of issues with the ASM. if (!enabled) {
val inlineStackArgsTooltip: Val<String> = value( append("\nThis mode cannot be toggled because there are issues in the script.")
"Transform arg_push* opcodes to be inline with the opcode the arguments are given to." }
) }
}
fun makeUndoCurrent() { fun makeUndoCurrent() {
store.makeUndoCurrent() store.makeUndoCurrent()
} }
fun setInlineStackArgs(value: Boolean) { fun setInlineStackArgs(inline: Boolean) {
TODO() store.setInlineStackArgs(inline)
} }
companion object { companion object {

View File

@ -1,13 +1,16 @@
package world.phantasmal.web.questEditor.stores package world.phantasmal.web.questEditor.stores
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import world.phantasmal.core.disposable.Disposer
import world.phantasmal.core.disposable.disposable
import world.phantasmal.lib.asm.AssemblyProblem
import world.phantasmal.lib.asm.disassemble import world.phantasmal.lib.asm.disassemble
import world.phantasmal.observable.ChangeEvent import world.phantasmal.observable.ChangeEvent
import world.phantasmal.observable.Observable import world.phantasmal.observable.Observable
import world.phantasmal.observable.emitter import world.phantasmal.observable.emitter
import world.phantasmal.observable.value.Val import world.phantasmal.observable.value.Val
import world.phantasmal.observable.value.list.ListVal
import world.phantasmal.observable.value.mutableVal import world.phantasmal.observable.value.mutableVal
import world.phantasmal.observable.value.trueVal
import world.phantasmal.web.core.undo.SimpleUndo import world.phantasmal.web.core.undo.SimpleUndo
import world.phantasmal.web.core.undo.UndoManager import world.phantasmal.web.core.undo.UndoManager
import world.phantasmal.web.externals.monacoEditor.* import world.phantasmal.web.externals.monacoEditor.*
@ -19,8 +22,15 @@ class AsmStore(
questEditorStore: QuestEditorStore, questEditorStore: QuestEditorStore,
private val undoManager: UndoManager, private val undoManager: UndoManager,
) : Store() { ) : Store() {
private val _inlineStackArgs = mutableVal(true)
private var _textModel = mutableVal<ITextModel?>(null) private var _textModel = mutableVal<ITextModel?>(null)
/**
* Contains all model-related disposables. All contained disposables are disposed whenever a new
* model is created.
*/
private val modelDisposer = addDisposable(Disposer())
private val _didUndo = emitter<Unit>() private val _didUndo = emitter<Unit>()
private val _didRedo = emitter<Unit>() private val _didRedo = emitter<Unit>()
private val undo = SimpleUndo( private val undo = SimpleUndo(
@ -30,7 +40,7 @@ class AsmStore(
{ _didRedo.emit(ChangeEvent(Unit)) }, { _didRedo.emit(ChangeEvent(Unit)) },
) )
val inlineStackArgs: Val<Boolean> = trueVal() val inlineStackArgs: Val<Boolean> = _inlineStackArgs
val textModel: Val<ITextModel?> = _textModel val textModel: Val<ITextModel?> = _textModel
@ -39,17 +49,38 @@ class AsmStore(
val didUndo: Observable<Unit> = _didUndo val didUndo: Observable<Unit> = _didUndo
val didRedo: Observable<Unit> = _didRedo val didRedo: Observable<Unit> = _didRedo
val problems: ListVal<AssemblyProblem> = AsmAnalyser.problems
init { init {
observe(questEditorStore.currentQuest, inlineStackArgs) { quest, inlineStackArgs -> observe(questEditorStore.currentQuest, inlineStackArgs) { quest, inlineStackArgs ->
_textModel.value?.dispose() modelDisposer.disposeAll()
quest?.let { quest?.let {
val asm = disassemble(quest.bytecodeIr, inlineStackArgs) val asm = disassemble(quest.bytecodeIr, inlineStackArgs)
scope.launch { AsmAnalyser.setAsm(asm, inlineStackArgs) } scope.launch { AsmAnalyser.setAsm(asm, inlineStackArgs) }
_textModel.value = _textModel.value = createModel(asm.joinToString("\n"), ASM_LANG_ID).also { model ->
createModel(asm.joinToString("\n"), ASM_LANG_ID) modelDisposer.add(disposable { model.dispose() })
.also(::addModelChangeListener)
setupUndoRedo(model)
model.onDidChangeContent { e ->
scope.launch {
AsmAnalyser.updateAssembly(e.changes.map {
AsmChange(
AsmRange(
it.range.startLineNumber,
it.range.startColumn,
it.range.endLineNumber,
it.range.endColumn,
),
it.text,
)
})
}
// TODO: Update breakpoints.
}
}
} }
} }
@ -66,10 +97,11 @@ class AsmStore(
undoManager.setCurrent(undo) undoManager.setCurrent(undo)
} }
/** fun setInlineStackArgs(inline: Boolean) {
* Sets up undo/redo, code analysis and breakpoint updates on model change. _inlineStackArgs.value = inline
*/ }
private fun addModelChangeListener(model: ITextModel) {
private fun setupUndoRedo(model: ITextModel) {
val initialVersion = model.getAlternativeVersionId() val initialVersion = model.getAlternativeVersionId()
var currentVersion = initialVersion var currentVersion = initialVersion
var lastVersion = initialVersion var lastVersion = initialVersion
@ -102,8 +134,6 @@ class AsmStore(
} }
currentVersion = version currentVersion = version
// TODO: Code analysis and breakpoint update.
} }
} }