mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-04 22:58:29 +08:00
Added opcode checks. Joined RegTufRefType and RegRefType. Improved type definitions.
This commit is contained in:
parent
949af36381
commit
3122bb4666
@ -160,10 +160,10 @@ Features that are in ***bold italics*** are planned but not yet implemented.
|
||||
## Bugs
|
||||
|
||||
- When a modal dialog is open, global keybindings should be disabled
|
||||
- The ASM editor is slow with big scripts, e.g. Seat of the Heart (#27)
|
||||
- Improve the default camera target for Crater Interior
|
||||
- Creating a new quest discards changes to previously open quest without asking user
|
||||
- Opening a new file discards changes to previously open quest without asking user
|
||||
- Toggling "Inline args" clears the undo stack
|
||||
- Entities with rendering issues:
|
||||
- Caves 4 Button door
|
||||
- Pofuilly Slime
|
||||
|
@ -190,9 +190,10 @@ fun paramsToCode(params: List<Map<String, Any>>, indent: Int): String {
|
||||
"string_label" -> "SLabelType"
|
||||
"string" -> "StringType"
|
||||
"instruction_label_var" -> "ILabelVarType"
|
||||
"reg_ref" -> "RegRefType"
|
||||
"reg_tup_ref" -> """RegTupRefType(${
|
||||
paramsToCode(param["reg_tup"] as List<Map<String, Any>>, indent + 4)
|
||||
"reg_ref" -> """RegRefType(${
|
||||
(param["registers"] as List<Map<String, Any>>?)?.let {
|
||||
paramsToCode(it, indent + 4)
|
||||
} ?: "null"
|
||||
})"""
|
||||
"reg_ref_var" -> "RegRefVarType"
|
||||
"pointer" -> "PointerType"
|
||||
|
@ -487,9 +487,8 @@ private class Assembler(private val asm: List<String>, private val inlineStackAr
|
||||
|
||||
Token.Register -> {
|
||||
typeMatch = stack ||
|
||||
param.type === RegRefType ||
|
||||
param.type === RegRefVarType ||
|
||||
param.type is RegTupRefType
|
||||
param.type is RegRefType
|
||||
|
||||
parseRegister()
|
||||
}
|
||||
@ -537,9 +536,8 @@ private class Assembler(private val asm: List<String>, private val inlineStackAr
|
||||
|
||||
StringType -> "a string"
|
||||
|
||||
RegRefType,
|
||||
RegRefVarType,
|
||||
is RegTupRefType,
|
||||
is RegRefType,
|
||||
-> "a register reference"
|
||||
|
||||
else -> null
|
||||
@ -552,7 +550,7 @@ private class Assembler(private val asm: List<String>, private val inlineStackAr
|
||||
// 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 (param.type is RegTupRefType) {
|
||||
if (param.type is RegRefType) {
|
||||
addInstruction(
|
||||
OP_ARG_PUSHB,
|
||||
listOf(arg),
|
||||
@ -572,8 +570,7 @@ private class Assembler(private val asm: List<String>, private val inlineStackAr
|
||||
} else {
|
||||
when (param.type) {
|
||||
ByteType,
|
||||
RegRefType,
|
||||
is RegTupRefType,
|
||||
is RegRefType,
|
||||
-> {
|
||||
addInstruction(
|
||||
OP_ARG_PUSHB,
|
||||
|
@ -196,9 +196,7 @@ class Instruction(
|
||||
val args = getArgs(i)
|
||||
|
||||
size += when (type) {
|
||||
ByteType,
|
||||
RegRefType,
|
||||
-> 1
|
||||
ByteType -> 1
|
||||
|
||||
// Ensure this case is before the LabelType case because ILabelVarType extends
|
||||
// LabelType.
|
||||
@ -220,8 +218,9 @@ class Instruction(
|
||||
|
||||
RegRefVarType -> 1 + args.size
|
||||
|
||||
// Check RegTupRefType and LabelType last, because "is" checks are very slow in JS.
|
||||
is RegTupRefType -> 1
|
||||
// Check RegRefType and LabelType last, because "is" checks are very slow in JS.
|
||||
|
||||
is RegRefType -> 1
|
||||
|
||||
is LabelType -> 2
|
||||
|
||||
|
@ -204,7 +204,7 @@ private fun StringBuilder.appendArgs(params: List<Param>, args: List<ArgWithType
|
||||
if (i < args.size) {
|
||||
val (arg, argType) = args[i]
|
||||
|
||||
if (argType is RegTupRefType) {
|
||||
if (argType is RegRefType) {
|
||||
append("r")
|
||||
append(arg.value)
|
||||
} else {
|
||||
@ -235,9 +235,7 @@ private fun StringBuilder.appendArgs(params: List<Param>, args: List<ArgWithType
|
||||
}
|
||||
}
|
||||
|
||||
RegRefType,
|
||||
is RegTupRefType,
|
||||
-> {
|
||||
is RegRefType -> {
|
||||
append("r")
|
||||
append(arg.value)
|
||||
}
|
||||
|
@ -17,7 +17,11 @@ private val UNKNOWN_OPCODE_MNEMONIC_REGEX = Regex("""^unknown_((f8|f9)?[0-9a-f]{
|
||||
* Abstract super type of all types.
|
||||
*/
|
||||
sealed class AnyType {
|
||||
object Instance : AnyType()
|
||||
abstract val uiName: String
|
||||
|
||||
object Instance : AnyType() {
|
||||
override val uiName = "Any"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -28,54 +32,74 @@ sealed class ValueType : AnyType()
|
||||
/**
|
||||
* 8-Bit integer.
|
||||
*/
|
||||
object ByteType : ValueType()
|
||||
object ByteType : ValueType() {
|
||||
override val uiName = "Byte"
|
||||
}
|
||||
|
||||
/**
|
||||
* 16-Bit integer.
|
||||
*/
|
||||
object ShortType : ValueType()
|
||||
object ShortType : ValueType() {
|
||||
override val uiName = "Short"
|
||||
}
|
||||
|
||||
/**
|
||||
* 32-Bit integer.
|
||||
*/
|
||||
object IntType : ValueType()
|
||||
object IntType : ValueType() {
|
||||
override val uiName = "Int"
|
||||
}
|
||||
|
||||
/**
|
||||
* 32-Bit floating point number.
|
||||
*/
|
||||
object FloatType : ValueType()
|
||||
object FloatType : ValueType() {
|
||||
override val uiName = "Float"
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract super type of all label types.
|
||||
*/
|
||||
sealed class LabelType : ValueType() {
|
||||
object Instance : LabelType()
|
||||
object Instance : LabelType() {
|
||||
override val uiName = "Label"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Named reference to an instruction.
|
||||
*/
|
||||
object ILabelType : LabelType()
|
||||
object ILabelType : LabelType() {
|
||||
override val uiName = "ILabel"
|
||||
}
|
||||
|
||||
/**
|
||||
* Named reference to a data segment.
|
||||
*/
|
||||
object DLabelType : LabelType()
|
||||
object DLabelType : LabelType() {
|
||||
override val uiName = "DLabel"
|
||||
}
|
||||
|
||||
/**
|
||||
* Named reference to a string segment.
|
||||
*/
|
||||
object SLabelType : LabelType()
|
||||
object SLabelType : LabelType() {
|
||||
override val uiName = "SLabel"
|
||||
}
|
||||
|
||||
/**
|
||||
* Arbitrary amount of instruction labels (variadic arguments).
|
||||
*/
|
||||
object ILabelVarType : LabelType()
|
||||
object ILabelVarType : LabelType() {
|
||||
override val uiName = "...ILabel"
|
||||
}
|
||||
|
||||
/**
|
||||
* String of arbitrary size.
|
||||
*/
|
||||
object StringType : ValueType()
|
||||
object StringType : ValueType() {
|
||||
override val uiName = "String"
|
||||
}
|
||||
|
||||
/**
|
||||
* Purely abstract super type of all reference types.
|
||||
@ -83,30 +107,41 @@ object StringType : ValueType()
|
||||
sealed class RefType : AnyType()
|
||||
|
||||
/**
|
||||
* Reference to one or more registers.
|
||||
* Register reference. If [registers] is null, references one or more consecutive registers of any
|
||||
* type (only stack_pushm and stack_popm use this). If [registers] is not null, references a fixed
|
||||
* amount of consecutive registers of specific types. [Param.type] can't be a variadic type.
|
||||
*/
|
||||
object RegRefType : RefType()
|
||||
class RegRefType(val registers: List<Param>?) : RefType() {
|
||||
override val uiName = buildString {
|
||||
append("Register")
|
||||
|
||||
/**
|
||||
* Reference to a fixed tuple of registers of specific types.
|
||||
* The only parameterized type.
|
||||
*/
|
||||
class RegTupRefType(val registerTuple: List<Param>) : RefType()
|
||||
if (registers != null) {
|
||||
if (registers.size > 1) append("s")
|
||||
append("<")
|
||||
registers.joinTo(this) { it.type.uiName }
|
||||
append(">")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Arbitrary amount of register references (variadic arguments).
|
||||
*/
|
||||
object RegRefVarType : RefType()
|
||||
object RegRefVarType : RefType() {
|
||||
override val uiName = "...Register"
|
||||
}
|
||||
|
||||
/**
|
||||
* Raw memory pointer.
|
||||
*/
|
||||
object PointerType : AnyType()
|
||||
object PointerType : AnyType() {
|
||||
override val uiName = "Pointer"
|
||||
}
|
||||
|
||||
enum class ParamAccess {
|
||||
Read,
|
||||
Write,
|
||||
ReadWrite,
|
||||
ReadWrite
|
||||
}
|
||||
|
||||
class Param(
|
||||
@ -125,6 +160,20 @@ class Param(
|
||||
* Whether or not this parameter takes a variable number of arguments.
|
||||
*/
|
||||
val varargs: Boolean = type === ILabelVarType || type === RegRefVarType
|
||||
|
||||
/**
|
||||
* Whether or not the instruction reads this parameter.
|
||||
*/
|
||||
val reads: Boolean
|
||||
get() =
|
||||
access === ParamAccess.Read || access === ParamAccess.ReadWrite
|
||||
|
||||
/**
|
||||
* Whether or not the instruction writes this parameter.
|
||||
*/
|
||||
val writes: Boolean
|
||||
get() =
|
||||
access === ParamAccess.Write || access === ParamAccess.ReadWrite
|
||||
}
|
||||
|
||||
enum class StackInteraction {
|
||||
|
@ -191,14 +191,11 @@ private class RegisterValueFinder {
|
||||
for (j in 0 until argLen) {
|
||||
val param = params[j]
|
||||
|
||||
if (param.type is RegTupRefType) {
|
||||
if (param.type is RegRefType && param.type.registers != null) {
|
||||
val regRef = args[j].value as Int
|
||||
|
||||
for ((k, reg_param) in param.type.registerTuple.withIndex()) {
|
||||
if ((reg_param.access == ParamAccess.Write ||
|
||||
reg_param.access == ParamAccess.ReadWrite) &&
|
||||
regRef + k == register
|
||||
) {
|
||||
for ((k, regParam) in param.type.registers.withIndex()) {
|
||||
if (regParam.writes && regRef + k == register) {
|
||||
return ValueSet.all()
|
||||
}
|
||||
}
|
||||
@ -254,8 +251,7 @@ private class RegisterValueFinder {
|
||||
if (register !in 1..7) return ValueSet.empty()
|
||||
|
||||
var vaStartIdx = -1
|
||||
// Pairs of type and value.
|
||||
val stack = mutableListOf<Pair<AnyType, Any>>()
|
||||
val stack = mutableListOf<Instruction>()
|
||||
|
||||
for (i in block.start until vaCallIdx) {
|
||||
val instruction = block.segment.instructions[i]
|
||||
@ -264,27 +260,29 @@ private class RegisterValueFinder {
|
||||
if (opcode.code == OP_VA_START.code) {
|
||||
vaStartIdx = i
|
||||
} else if (vaStartIdx != -1) {
|
||||
val type = when (opcode.code) {
|
||||
OP_ARG_PUSHR.code -> RegRefType
|
||||
OP_ARG_PUSHL.code -> IntType
|
||||
OP_ARG_PUSHB.code -> ByteType
|
||||
OP_ARG_PUSHW.code -> ShortType
|
||||
OP_ARG_PUSHA.code -> PointerType
|
||||
OP_ARG_PUSHO.code -> PointerType
|
||||
OP_ARG_PUSHS.code -> StringType
|
||||
else -> continue
|
||||
when (opcode.code) {
|
||||
OP_ARG_PUSHR.code,
|
||||
OP_ARG_PUSHL.code,
|
||||
OP_ARG_PUSHB.code,
|
||||
OP_ARG_PUSHW.code,
|
||||
OP_ARG_PUSHA.code,
|
||||
OP_ARG_PUSHO.code,
|
||||
OP_ARG_PUSHS.code -> stack.add(instruction)
|
||||
}
|
||||
|
||||
stack.add(Pair(type, instruction.args[0].value))
|
||||
}
|
||||
}
|
||||
|
||||
return if (register in 1..stack.size) {
|
||||
val (type, value) = stack[register - 1]
|
||||
val instruction = stack[register - 1]
|
||||
val value = instruction.args.first().value
|
||||
|
||||
when (instruction.opcode.code) {
|
||||
OP_ARG_PUSHR.code -> find(LinkedHashSet(path), block, vaStartIdx, value as Int)
|
||||
|
||||
OP_ARG_PUSHL.code,
|
||||
OP_ARG_PUSHB.code,
|
||||
OP_ARG_PUSHW.code -> ValueSet.of(value as Int)
|
||||
|
||||
when (type) {
|
||||
RegRefType -> find(LinkedHashSet(path), block, vaStartIdx, value as Int)
|
||||
IntType, ByteType, ShortType -> ValueSet.of(value as Int)
|
||||
// TODO: Deal with strings.
|
||||
else -> ValueSet.all() // String or pointer
|
||||
}
|
||||
|
@ -276,12 +276,12 @@ private fun findAndParseSegments(
|
||||
}
|
||||
}
|
||||
|
||||
is RegTupRefType -> {
|
||||
for (j in param.type.registerTuple.indices) {
|
||||
val regTup = param.type.registerTuple[j]
|
||||
is RegRefType -> if (param.type.registers != null) {
|
||||
for (j in param.type.registers.indices) {
|
||||
val registerParam = param.type.registers[j]
|
||||
|
||||
// Never on the stack.
|
||||
if (regTup.type is ILabelType) {
|
||||
if (registerParam.type is ILabelType) {
|
||||
val firstRegister = instruction.args[0].value as Int
|
||||
val labelValues = getRegisterValue(
|
||||
cfg,
|
||||
@ -609,9 +609,7 @@ private fun parseInstructionArguments(
|
||||
)
|
||||
}
|
||||
|
||||
is RegRefType,
|
||||
is RegTupRefType,
|
||||
-> {
|
||||
is RegRefType -> {
|
||||
args.add(Arg(cursor.uByte().toInt()))
|
||||
}
|
||||
|
||||
@ -827,7 +825,7 @@ fun writeBytecode(bytecodeIr: BytecodeIr, dcGcFormat: Boolean): BytecodeAndLabel
|
||||
if (dcGcFormat) cursor.writeStringAscii(str, str.length + 1)
|
||||
else cursor.writeStringUtf16(str, 2 * str.length + 2)
|
||||
}
|
||||
RegRefType, is RegTupRefType -> {
|
||||
is RegRefType -> {
|
||||
cursor.writeByte((arg.value as Int).toByte())
|
||||
}
|
||||
RegRefVarType -> {
|
||||
|
@ -279,8 +279,6 @@ class AssemblyTests : LibTestSuite {
|
||||
)
|
||||
|
||||
assertTrue(result is Success)
|
||||
assertEquals(1, result.problems.size)
|
||||
|
||||
assertDeepEquals(
|
||||
BytecodeIr(
|
||||
listOf(
|
||||
@ -298,14 +296,15 @@ class AssemblyTests : LibTestSuite {
|
||||
),
|
||||
),
|
||||
srcLoc = SegmentSrcLoc(
|
||||
labels = mutableListOf(SrcLoc(1, 1, 2))
|
||||
labels = mutableListOf(SrcLoc(1, 1, 2)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
result.value
|
||||
result.value,
|
||||
)
|
||||
|
||||
assertEquals(1, result.problems.size)
|
||||
val problem = result.problems.first()
|
||||
assertTrue(problem is AssemblyProblem)
|
||||
assertEquals(2, problem.lineNo)
|
||||
@ -313,4 +312,86 @@ class AssemblyTests : LibTestSuite {
|
||||
assertEquals(7, problem.len)
|
||||
assertEquals("Expected 0 arguments, got 1. At 2:5.", problem.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun too_few_arguments() {
|
||||
val result = assemble(
|
||||
"""
|
||||
5000:
|
||||
leti r100
|
||||
""".trimIndent().split('\n')
|
||||
)
|
||||
|
||||
assertTrue(result is Success)
|
||||
|
||||
// Bytecode contains no instructions.
|
||||
assertDeepEquals(
|
||||
BytecodeIr(
|
||||
listOf(
|
||||
InstructionSegment(
|
||||
labels = mutableListOf(5000),
|
||||
instructions = mutableListOf(),
|
||||
srcLoc = SegmentSrcLoc(
|
||||
labels = mutableListOf(SrcLoc(1, 1, 5)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
result.value,
|
||||
)
|
||||
|
||||
assertEquals(1, result.problems.size)
|
||||
val problem = result.problems.first()
|
||||
assertTrue(problem is AssemblyProblem)
|
||||
assertEquals(2, problem.lineNo)
|
||||
assertEquals(5, problem.col)
|
||||
assertEquals(9, problem.len)
|
||||
assertEquals("Expected 2 arguments, got 1. At 2:5.", problem.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun too_few_arguments_varargs() {
|
||||
val result = assemble(
|
||||
"""
|
||||
5000:
|
||||
switch_jmp r100
|
||||
""".trimIndent().split('\n')
|
||||
)
|
||||
|
||||
assertTrue(result is Success)
|
||||
|
||||
// Bytecode contains an instruction, since it's technically valid.
|
||||
assertDeepEquals(
|
||||
BytecodeIr(
|
||||
listOf(
|
||||
InstructionSegment(
|
||||
labels = mutableListOf(5000),
|
||||
instructions = mutableListOf(
|
||||
Instruction(
|
||||
opcode = OP_SWITCH_JMP,
|
||||
args = listOf(Arg(100)),
|
||||
srcLoc = InstructionSrcLoc(
|
||||
mnemonic = SrcLoc(2, 5, 10),
|
||||
args = listOf(SrcLoc(2, 16, 4)),
|
||||
stackArgs = emptyList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
srcLoc = SegmentSrcLoc(
|
||||
labels = mutableListOf(SrcLoc(1, 1, 5)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
result.value,
|
||||
)
|
||||
|
||||
assertEquals(1, result.problems.size)
|
||||
val problem = result.problems.first()
|
||||
assertTrue(problem is AssemblyProblem)
|
||||
assertEquals(2, problem.lineNo)
|
||||
assertEquals(5, problem.col)
|
||||
assertEquals(15, problem.len)
|
||||
assertEquals("Expected at least 2 arguments, got 1. At 2:5.", problem.message)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,42 @@
|
||||
package world.phantasmal.lib.asm
|
||||
|
||||
import world.phantasmal.lib.test.LibTestSuite
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class OpcodeTests : LibTestSuite {
|
||||
// We do these checks in a unit test instead of in the Opcode constructor to avoid the runtime
|
||||
// overhead. This is static data that is built up once and only needs to be verified once.
|
||||
@Test
|
||||
fun all_opcodes_are_consistent() = test {
|
||||
for (code in (0x00..0xFF).asSequence() + (0xF800..0xF8FF) + (0xF900..0xF9FF)) {
|
||||
val opcode = codeToOpcode(code)
|
||||
|
||||
assertEquals(code, opcode.code)
|
||||
assertTrue(opcode.mnemonic.isNotBlank())
|
||||
assertTrue(opcode.doc == null || opcode.doc!!.isNotBlank())
|
||||
// If an opcodes pushes something onto the stack, it needs at least one immediate
|
||||
// argument. If an opcode pops the stack, it needs at least one stack argument.
|
||||
assertTrue(opcode.stack == null || opcode.params.isNotEmpty())
|
||||
|
||||
// Varargs.
|
||||
val varargCount = opcode.params.count { it.varargs }
|
||||
val hasVarargs = varargCount >= 1
|
||||
// Only the last parameter can be variadic.
|
||||
assertTrue(varargCount <= 1)
|
||||
assertTrue(!hasVarargs || opcode.params.lastOrNull()?.varargs == true)
|
||||
assertEquals(hasVarargs, opcode.varargs)
|
||||
|
||||
// Register references.
|
||||
|
||||
for (param in opcode.params) {
|
||||
val type = param.type
|
||||
|
||||
if (type is RegRefType) {
|
||||
assertTrue(type.registers == null || type.registers!!.isNotEmpty())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -65,7 +65,7 @@
|
||||
"access": {
|
||||
"$ref": "#/definitions/access"
|
||||
},
|
||||
"reg_tup": {
|
||||
"registers": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"description": "Specifies the way the referenced registers will be interpreted. Should only be specified if the parameter type is \"reg_tup_ref\".",
|
||||
@ -106,7 +106,6 @@
|
||||
"string",
|
||||
"instruction_label_var",
|
||||
"reg_ref",
|
||||
"reg_tup_ref",
|
||||
"reg_ref_var",
|
||||
"pointer"
|
||||
]
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -328,31 +328,15 @@ class AssemblyWorker(private val sendMessage: (ServerMessage) -> Unit) {
|
||||
signature += ", "
|
||||
}
|
||||
|
||||
val paramTypeStr = when (param.type) {
|
||||
ByteType -> "Byte"
|
||||
ShortType -> "Short"
|
||||
IntType -> "Int"
|
||||
FloatType -> "Float"
|
||||
ILabelType -> "&Function"
|
||||
DLabelType -> "&Data"
|
||||
SLabelType -> "&String"
|
||||
ILabelVarType -> "...&Function"
|
||||
StringType -> "String"
|
||||
RegRefType, is RegTupRefType -> "Register"
|
||||
RegRefVarType -> "...Register"
|
||||
PointerType -> "Pointer"
|
||||
else -> "Any"
|
||||
}
|
||||
|
||||
params.add(
|
||||
Parameter(
|
||||
labelStart = signature.length,
|
||||
labelEnd = signature.length + paramTypeStr.length,
|
||||
labelEnd = signature.length + param.type.uiName.length,
|
||||
documentation = param.doc,
|
||||
)
|
||||
)
|
||||
|
||||
signature += paramTypeStr
|
||||
signature += param.type.uiName
|
||||
}
|
||||
|
||||
return Signature(
|
||||
|
Loading…
Reference in New Issue
Block a user