diff --git a/.idea/.name b/.idea/.name deleted file mode 100644 index 8f91b47..0000000 --- a/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -template \ No newline at end of file diff --git a/.idea/artifacts/mtmc_web_js_0_1_0_SNAPSHOT.xml b/.idea/artifacts/mtmc_web_js_0_1_0_SNAPSHOT.xml new file mode 100644 index 0000000..a49e418 --- /dev/null +++ b/.idea/artifacts/mtmc_web_js_0_1_0_SNAPSHOT.xml @@ -0,0 +1,8 @@ + + + $PROJECT_DIR$/build/libs + + + + + \ No newline at end of file diff --git a/.idea/artifacts/mtmc_web_jvm_0_1_0_SNAPSHOT.xml b/.idea/artifacts/mtmc_web_jvm_0_1_0_SNAPSHOT.xml new file mode 100644 index 0000000..7b2b661 --- /dev/null +++ b/.idea/artifacts/mtmc_web_jvm_0_1_0_SNAPSHOT.xml @@ -0,0 +1,8 @@ + + + $PROJECT_DIR$/build/libs + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 7b907e9..2ac0fd5 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,7 +3,7 @@ - + diff --git a/build.gradle.kts b/build.gradle.kts index 6b501dd..276852b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,6 @@ @file:OptIn(ExperimentalDistributionDsl::class) +import org.codehaus.groovy.tools.shell.util.Logger.io import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalDistributionDsl plugins { @@ -50,6 +51,9 @@ kotlin { implementation("nl.astraeus:simple-jdbc-stats:1.6.1") { exclude(group = "org.slf4j", module = "slf4j-api") } + + implementation("io.pebbletemplates:pebble:3.2.3") + implementation("com.google.code.gson:gson:2.12.1") } } val jvmTest by getting { diff --git a/settings.gradle.kts b/settings.gradle.kts index 5b17425..9a9c5ba 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,5 +1,5 @@ plugins { id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0" } -val REPO_NAME = "dummy so the gitea template compiles, please remove" + rootProject.name = "mtmc-web" diff --git a/src/jsMain/kotlin/nl/astraeus/tmpl/Main.kt b/src/jsMain/kotlin/mtmc/Main.kt similarity index 92% rename from src/jsMain/kotlin/nl/astraeus/tmpl/Main.kt rename to src/jsMain/kotlin/mtmc/Main.kt index c141d33..be7f6c6 100644 --- a/src/jsMain/kotlin/nl/astraeus/tmpl/Main.kt +++ b/src/jsMain/kotlin/mtmc/Main.kt @@ -1,11 +1,10 @@ -package nl.astraeus.tmpl +package mtmc import kotlinx.browser.document import kotlinx.html.div import nl.astraeus.komp.HtmlBuilder import nl.astraeus.komp.Komponent - class HelloKomponent : Komponent() { override fun HtmlBuilder.render() { div { diff --git a/src/jvmMain/kotlin/nl/astraeus/tmpl/Main.kt b/src/jvmMain/kotlin/mtmc/Main.kt similarity index 93% rename from src/jvmMain/kotlin/nl/astraeus/tmpl/Main.kt rename to src/jvmMain/kotlin/mtmc/Main.kt index 74beed4..e69d885 100644 --- a/src/jvmMain/kotlin/nl/astraeus/tmpl/Main.kt +++ b/src/jvmMain/kotlin/mtmc/Main.kt @@ -1,4 +1,4 @@ -package nl.astraeus.tmpl +package mtmc import com.zaxxer.hikari.HikariConfig import io.undertow.Undertow @@ -8,8 +8,8 @@ import io.undertow.server.handlers.encoding.ContentEncodingRepository import io.undertow.server.handlers.encoding.EncodingHandler import io.undertow.server.handlers.encoding.GzipEncodingProvider import nl.astraeus.logger.Logger -import nl.astraeus.tmpl.db.Database -import nl.astraeus.tmpl.web.RequestHandler +import mtmc.db.Database +import mtmc.web.RequestHandler val log = Logger() diff --git a/src/jvmMain/kotlin/mtmc/Placeholders.kt b/src/jvmMain/kotlin/mtmc/Placeholders.kt new file mode 100644 index 0000000..762887d --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/Placeholders.kt @@ -0,0 +1,8 @@ +package mtmc + +val SERVER_PORT = 4001 +val JDBC_PORT = 4002 + +val pageTitle = "mtmc-web" +val itemUrl = "mtmc-web" +val repoName = "mtmc-web" diff --git a/src/jvmMain/kotlin/mtmc/asm/ASMElement.kt b/src/jvmMain/kotlin/mtmc/asm/ASMElement.kt new file mode 100644 index 0000000..9deb269 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/asm/ASMElement.kt @@ -0,0 +1,19 @@ +package mtmc.asm + +import mtmc.tokenizer.MTMCToken + +abstract class ASMElement( + val labels: List, + @JvmField var lineNumber: Int +) : HasLocation { + @JvmField + var errors: MutableList = ArrayList() + override var location: Int = 0 + override var sizeInBytes: Int = 0 + + fun addError(token: MTMCToken, error: String) { + errors.add(ASMError(token, error)) + } + + abstract fun addError(integerValueRequired: String) +} diff --git a/src/jvmMain/kotlin/mtmc/asm/ASMError.kt b/src/jvmMain/kotlin/mtmc/asm/ASMError.kt new file mode 100644 index 0000000..e6f692e --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/asm/ASMError.kt @@ -0,0 +1,13 @@ +package mtmc.asm + +import mtmc.tokenizer.MTMCToken + +@JvmRecord +data class ASMError( + @JvmField val token: MTMCToken, + @JvmField val error: String +) { + fun formattedErrorMessage(): String { + return "Line " + token.line + ": " + error + } +} diff --git a/src/jvmMain/kotlin/mtmc/asm/Assembler.kt b/src/jvmMain/kotlin/mtmc/asm/Assembler.kt new file mode 100644 index 0000000..67c06f8 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/asm/Assembler.kt @@ -0,0 +1,798 @@ +package mtmc.asm + +import mtmc.asm.data.Data +import mtmc.asm.graphics.Graphic +import mtmc.asm.instructions.ALUInstruction +import mtmc.asm.instructions.ALUOp.Companion.isALUOp +import mtmc.asm.instructions.ErrorInstruction +import mtmc.asm.instructions.Instruction +import mtmc.asm.instructions.InstructionType +import mtmc.asm.instructions.InstructionType.Companion.fromString +import mtmc.asm.instructions.InstructionType.InstructionClass +import mtmc.asm.instructions.JumpInstruction +import mtmc.asm.instructions.JumpRegisterInstruction +import mtmc.asm.instructions.LoadStoreInstruction +import mtmc.asm.instructions.LoadStoreRegisterInstruction +import mtmc.asm.instructions.MetaInstruction +import mtmc.asm.instructions.MiscInstruction +import mtmc.asm.instructions.StackInstruction +import mtmc.asm.instructions.TestInstruction +import mtmc.emulator.DebugInfo +import mtmc.emulator.DebugInfo.GlobalInfo +import mtmc.emulator.DebugInfo.LocalInfo +import mtmc.emulator.MonTanaMiniComputer +import mtmc.emulator.Register.Companion.isReadable +import mtmc.emulator.Register.Companion.isWriteable +import mtmc.os.SysCall +import mtmc.os.exec.Executable +import mtmc.tokenizer.MTMCToken +import mtmc.tokenizer.MTMCTokenizer +import java.io.File +import java.nio.charset.StandardCharsets +import java.util.* +import java.util.stream.Collectors + +class Assembler { + var instructions: MutableList = ArrayList() + var instructionsSizeInBytes: Int = 0 + var data: MutableList = ArrayList() + var graphics: MutableList = ArrayList() + var dataSize: Int = 0 + var labels: HashMap + + private var mode: ASMMode = ASMMode.TEXT + var tokenizer: MTMCTokenizer? = null + private var lastLabels: MutableList = mutableListOf() + private var srcName = "disk/file.asm" + private var debugStrings: MutableList = mutableListOf() + + init { + labels = HashMap() + } + + fun assemble(asm: String): AssemblyResult { + return assemble(null, asm) + } + + fun assemble(file: String?, asm: String): AssemblyResult { + tokenizer = MTMCTokenizer(asm, "#") + debugStrings = ArrayList() + parseAssembly() + resolveLocations() + val errors = collectErrors() + var debugInfo: DebugInfo? = null + var code: ByteArray = ByteArray(0) + var data: ByteArray = ByteArray(0) + var graphics: Array = arrayOf() + if (errors.isEmpty()) { + code = genCode() + data = genData() + graphics = genGraphics() + debugInfo = genDebugInfo(file, asm, code) + } + return AssemblyResult(code, data, graphics, debugInfo, errors) + } + + private fun genDebugInfo( + assemblyFile: String?, + assemblySource: String, + code: ByteArray + ): DebugInfo { + val assemblyLineNumbers = IntArray(code.size) + + var originalFile: String = "" + val originalLineNumbers = IntArray(code.size) + + val globals: MutableList = ArrayList() + val locals = Array>(code.size) { arrayOf() } + + var location = 0 + var originalLineNumber = 0 + val currentLocals: MutableMap = TreeMap() + for (instruction in instructions) { + val asmLineNumber = instruction.lineNumber + if (instruction is MetaInstruction) { + if (instruction.isFileDirective) { + originalFile = instruction.getOriginalFilePath()!! + } else if (instruction.isLineDirective) { + originalLineNumber = instruction.originalLineNumber + } else if (instruction.isGlobalDirective) { + globals.add( + GlobalInfo( + instruction.getGlobalName(), + instruction.getGlobalLocation(), + instruction.getGlobalType() + ) + ) + } else if (instruction.isLocalDirective) { + currentLocals.put( + instruction.getLocalName(), + LocalInfo( + instruction.getLocalName(), + instruction.getLocalOffset(), + instruction.getLocalType() + ) + ) + } else if (instruction.isEndLocalDirective) { + currentLocals.remove(instruction.getLocalName()) + } + } + while (location < instruction.location + instruction.sizeInBytes) { + assemblyLineNumbers[location] = asmLineNumber + originalLineNumbers[location] = originalLineNumber + locals[location] = currentLocals.values.toTypedArray() + location++ + } + } + + val debugInfo = DebugInfo( + debugStrings, + assemblyFile, + assemblySource, + assemblyLineNumbers, + originalFile, + originalLineNumbers, + globals.toTypedArray(), + locals + ) + + return debugInfo + } + + fun assembleExecutable(srcName: String, asm: String): Executable { + this.srcName = "disk/" + srcName //TODO: The prefix should be replaced with FileSystem access + + val result = assemble(srcName, asm) + if (!result.errors.isEmpty()) { + throw RuntimeException( + "Errors:\n" + result.errors + .stream() + .map { e: ASMError? -> " - " + e!!.formattedErrorMessage() } + .collect(Collectors.joining("\n"))) + } + + return Executable( + Executable.Format.Orc1, + result.code, + result.data, + result.graphics, + srcName, + result.debugInfo + ) + } + + private fun parseAssembly() { + while (tokenizer!!.more()) { + parseLine() + } + } + + private fun genData(): ByteArray { + val dataBytes = ByteArray(dataSize) + for (dataElt in data) { + dataElt.genData(dataBytes, this) + } + return dataBytes + } + + private fun genCode(): ByteArray { + val code = ByteArray(this.instructionsSizeInBytes) + for (instruction in instructions) { + instruction.genCode(code, this) + } + return code + } + + private fun genOriginalLineNumbers(length: Int): IntArray { + val asmLineNumbers = IntArray(length) + var currentLineNumber = 0 + var location = 0 + for (instruction in instructions) { + if (instruction is MetaInstruction && instruction.isLineDirective) { + currentLineNumber = instruction.originalLineNumber + } + while (location < instruction.location + instruction.sizeInBytes) { + asmLineNumbers[location] = currentLineNumber + location++ + } + } + return asmLineNumbers + } + + private fun genGraphics(): Array { + val graphics = Array(this.graphics.size) { index -> + graphics[index].imageData + } + + return graphics + } + + private fun collectErrors(): MutableList { + val errors: MutableList = ArrayList() + for (instruction in instructions) { + errors.addAll(instruction.errors) + } + for (data in this.data) { + errors.addAll(data.errors) + } + for (graphic in this.graphics) { + errors.addAll(graphic.errors) + } + return errors + } + + private fun resolveLocations() { + // assign locations + var offset = 0 + for (instruction in instructions) { + instruction.location = offset + offset += instruction.sizeInBytes + this.instructionsSizeInBytes += instruction.sizeInBytes + } + for (data in data) { + data.location = offset + offset += data.sizeInBytes + dataSize += data.sizeInBytes + } + for (instruction in instructions) { + instruction.validateLabel(this) + } + } + + + private fun parseLine() { + val tokens = this.tokensForLine + + if (parseMetaDirective(tokens)) { + return + } + + val newMode = parseModeFlag(tokens) + if (newMode != null) { + mode = newMode + return + } + // label only lines are ok and propagate to the next instruction + val labelTokens: MutableList = maybeGetLabels(tokens) + val labels = ArrayList(lastLabels) + labels.addAll(labelTokens) + lastLabels = labels + if (tokens.isEmpty()) { + for (labelToken in labelTokens) { + val labelData = Data(labelTokens, labelToken.line) + if (hasLabel(labelToken.stringValue)) { + labelData.addError(tokens.poll(), "Label already defined: " + labelToken.stringValue) + } else { + this.labels.put(labelToken.labelValue(), labelData) + } + data.add(labelData) + } + } else if (mode == ASMMode.TEXT) { + parseInstruction(tokens, labels) + } else { + parseData(tokens, labels) + } + } + + private fun parseMetaDirective(tokens: LinkedList): Boolean { + if (tokens.size >= 2 && tokens.get(0).type == MTMCToken.TokenType.AT) { + tokens.removeFirst() + val metaInstruction = MetaInstruction(tokens.removeFirst()) + if (metaInstruction.isFileDirective) { + val path = requireString(tokens, metaInstruction) + metaInstruction.setOriginalFilePath(path) + } else if (metaInstruction.isGlobalDirective) { + val name = requireString(tokens, metaInstruction) + val location = requireIntegerToken(tokens, metaInstruction, Int.Companion.MAX_VALUE) + val type = requireString(tokens, metaInstruction) + metaInstruction.setGlobalInfo(name, location, type) + } else if (metaInstruction.isLocalDirective) { + val name = requireString(tokens, metaInstruction) + val offset = requireIntegerToken(tokens, metaInstruction, Int.Companion.MAX_VALUE) + val type = requireString(tokens, metaInstruction) + metaInstruction.setLocalInfo(name, offset, type) + } else if (metaInstruction.isEndLocalDirective) { + val name = requireString(tokens, metaInstruction) + metaInstruction.setEndLocalInfo(name) + } else if (metaInstruction.isLineDirective) { + val lineNumber = requireIntegerToken(tokens, metaInstruction, Int.Companion.MAX_VALUE) + metaInstruction.setOriginalLineNumber(lineNumber) + } else { + metaInstruction.addError("Unknown meta directive") + } + instructions.add(metaInstruction) + return true + } + return false + } + + private fun parseModeFlag(tokens: LinkedList): ASMMode? { + if (tokens.size >= 2 && tokens.get(0).type == MTMCToken.TokenType.DOT && tokens.get(1).type == MTMCToken.TokenType.IDENTIFIER && tokens.get( + 0 + ).end == tokens.get(1).start && (tokens.get(1).stringValue == "data" || + tokens.get(1).stringValue == "text") + ) { + return ASMMode.valueOf(tokens.get(1).stringValue.uppercase(Locale.getDefault())) + } + return null + } + + private fun parseData(tokens: LinkedList, labelTokens: MutableList) { + lastLabels = mutableListOf() + var dataToken = tokens.poll() + var dataElt = Data(labelTokens, if (dataToken == null) 0 else dataToken.line) + if (dataToken != null) { + if (dataToken.type == MTMCToken.TokenType.STRING) { + val stringBytes = dataToken.stringValue.toByteArray(StandardCharsets.US_ASCII) + val nullTerminated = ByteArray(stringBytes.size + 1) + System.arraycopy(stringBytes, 0, nullTerminated, 0, stringBytes.size) + nullTerminated[stringBytes.size] = '\u0000'.code.toByte() + dataElt.setValue(dataToken, nullTerminated) + } else if (isInteger(dataToken)) { + val integerValue = dataToken.intValue() + if (integerValue > Short.Companion.MAX_VALUE || integerValue < Short.Companion.MIN_VALUE) { + dataElt.addError(dataToken, "Number is too large") + } + dataElt.setValue( + dataToken, + byteArrayOf((integerValue ushr 8).toByte(), integerValue.toByte()) + ) + } else if (dataToken.type == MTMCToken.TokenType.DOT) { + dataToken = tokens.poll() + dataElt = Data(labelTokens, dataToken.line) + if (dataToken.stringValue == "int") { + val intToken = requireIntegerToken(tokens, dataElt, MonTanaMiniComputer.MEMORY_SIZE) + if (intToken != null) { + dataElt.setValue(intToken, ByteArray(intToken.intValue() * 2)) + } + } else if (dataToken.stringValue == "byte") { + val intToken = requireIntegerToken(tokens, dataElt, MonTanaMiniComputer.MEMORY_SIZE) + if (intToken != null) { + dataElt.setValue(intToken, ByteArray(intToken.intValue())) + } + } else if (dataToken.stringValue == "image") { + val stringToken = requireString(tokens, dataElt) + if (stringToken != null) { + loadGraphic(labelTokens, dataElt, stringToken) + } + } else { + dataElt.addError(dataToken, "only data types are .int, .byte, and .image") + } + } else if (dataToken.type == MTMCToken.TokenType.MINUS) { + val nextToken = tokens.poll() // get next + if (nextToken == null || (nextToken.type != MTMCToken.TokenType.INTEGER && nextToken.type != MTMCToken.TokenType.HEX && nextToken.type != MTMCToken.TokenType.BINARY)) { + dataElt.addError(dataToken, "Number is too negative") + } else { + val integerValue = -1 * nextToken.intValue() + if (integerValue < Short.Companion.MIN_VALUE) { + dataElt.addError(dataToken, "Number is too negative") + } + val joinToken = MTMCToken.join(dataToken, nextToken, MTMCToken.TokenType.INTEGER) + dataElt.setValue( + joinToken, + byteArrayOf((integerValue ushr 8).toByte(), integerValue.toByte()) + ) + } + } else { + dataElt.addError(dataToken, "Unknown token type: " + dataToken.toString()) + } + } + + for (labelToken in labelTokens) { + if (hasLabel(labelToken.stringValue)) { + dataElt.addError(tokens.poll(), "Label already defined: " + labelToken.stringValue) + } else { + labels.put(labelToken.labelValue(), dataElt) + } + } + data.add(dataElt) + } + + private fun loadGraphic( + labelTokens: MutableList, + data: Data, + token: MTMCToken + ): Graphic { + val graphic = Graphic(labelTokens, token.line) + val filename = token.stringValue + val file = File(File(this.srcName).getParent(), filename) + val index = graphics.size + + data.setValue(token, byteArrayOf(((index shr 8) and 0xFF).toByte(), (index and 0xFF).toByte())) + graphic.setImage(file.getPath()) + graphics.add(graphic) + + return graphic + } + + private fun parseInstruction(tokens: LinkedList, labelTokens: MutableList) { + var tokens = tokens + var instructionToken = tokens.peekFirst() + if (instructionToken == null) return + + lastLabels = mutableListOf() + if (instructionToken.type != MTMCToken.TokenType.IDENTIFIER) { + instructions.add(ErrorInstruction(labelTokens, instructionToken, "Invalid Token")) + return + } + + tokens = transformSyntheticInstructions(tokens) + + instructionToken = tokens.poll() + val type = fromString(instructionToken.stringValue) + + if (type == null) { + instructions.add( + ErrorInstruction( + labelTokens, + instructionToken, + "Unknown instruction token type: " + instructionToken.stringValue + ) + ) + return + } + + val instruction: Instruction? + if (type.instructionClass == InstructionClass.MISC) { + val miscInst = MiscInstruction(type, labelTokens, instructionToken) + if (type == InstructionType.SYS) { + val sysCallType = requireSysCall(tokens, miscInst) + miscInst.setSyscallType(sysCallType) + } else if (type == InstructionType.MOV) { + val toRegister = requireWriteableRegister(tokens, miscInst) + miscInst.setTo(toRegister) + val fromRegister = requireReadableRegister(tokens, miscInst) + miscInst.setFrom(fromRegister) + } else if (type == InstructionType.INC || type == InstructionType.DEC) { + val toRegister = requireWriteableRegister(tokens, miscInst) + miscInst.setTo(toRegister) + if (!tokens.isEmpty()) { + val value = requireIntegerToken(tokens, miscInst, 15) + miscInst.setValue(value) + } + } else if (type == InstructionType.SETI) { + val toRegister = requireWriteableRegister(tokens, miscInst) + miscInst.setTo(toRegister) + val value = requireIntegerToken(tokens, miscInst, 15) + miscInst.setValue(value) + } else if (type == InstructionType.MCP) { + val fromRegister = requireWriteableRegister(tokens, miscInst) + miscInst.setFrom(fromRegister) + val toRegister = requireWriteableRegister(tokens, miscInst) + miscInst.setTo(toRegister) + val value = requireIntegerToken(tokens, miscInst, Short.Companion.MAX_VALUE.toInt()) + miscInst.setValue(value) + } else if (type == InstructionType.DEBUG) { + val debugString = requireString(tokens, miscInst) + // create a dummy int token representing the offset of the debug string in the debug info + val debugStringIndex = debugStrings!!.size + debugStrings!!.add(debugString.stringValue) + val value = MTMCToken(0, 0, 0, 0, debugStringIndex.toString(), MTMCToken.TokenType.INTEGER) + miscInst.setValue(value) + } + instruction = miscInst + } else if (type.instructionClass == InstructionClass.ALU) { + val aluInst = ALUInstruction(type, labelTokens, instructionToken) + if (aluInst.isBinaryOp) { + val toRegister = requireWriteableRegister(tokens, aluInst) + aluInst.setTo(toRegister) + val fromRegister = requireReadableRegister(tokens, aluInst) + aluInst.setFrom(fromRegister) + } else if (aluInst.isImmediateOp()) { + val immediateOp = requireALUOp(tokens, aluInst) + // TODO - validate is max or lower op + aluInst.setImmediateOp(immediateOp) + + val toRegister = requireWriteableRegister(tokens, aluInst) + aluInst.setTo(toRegister) + + val value = requireIntegerToken(tokens, aluInst, Short.Companion.MAX_VALUE.toInt()) + aluInst.setImmediateValue(value) + } else { + val toRegister = requireWriteableRegister(tokens, aluInst) + aluInst.setTo(toRegister) + } + instruction = aluInst + } else if (type.instructionClass == InstructionClass.STACK) { + val stackInst = StackInstruction(type, labelTokens, instructionToken) + if (type == InstructionType.PUSH) { + val targetRegister = requireReadableRegister(tokens, stackInst) + stackInst.setTarget(targetRegister) + } else if (type == InstructionType.POP) { + val toRegister = requireWriteableRegister(tokens, stackInst) + stackInst.setTarget(toRegister) + } else if (type == InstructionType.SOP) { + val aluOp = requireAluOp(tokens, stackInst) + stackInst.setALUOp(aluOp) + } else if (type == InstructionType.PUSHI) { + val value = requireIntegerToken(tokens, stackInst, Short.Companion.MAX_VALUE.toInt()) + stackInst.setValue(value) + } + + // if there is a stack register specified, consume it + if (!tokens.isEmpty() && tokens.peekFirst().type == MTMCToken.TokenType.IDENTIFIER) { + val stackReg = requireReadableRegister(tokens, stackInst) + stackInst.setStackRegister(stackReg) + } + + instruction = stackInst + } else if (type.instructionClass == InstructionClass.TEST) { + val testInst = TestInstruction(type, labelTokens, instructionToken) + testInst.setFirst(requireReadableRegister(tokens, testInst)) + if (testInst.isImmediate) { + testInst.setImmediateValue(requireIntegerToken(tokens, testInst, 15)) + } else { + testInst.setSecond(requireReadableRegister(tokens, testInst)) + } + instruction = testInst + } else if (type.instructionClass == InstructionClass.LOAD_STORE) { + val loadInst = LoadStoreInstruction(type, labelTokens, instructionToken) + + val targetReg = requireReadableRegister(tokens, loadInst) + loadInst.setTargetToken(targetReg) + + if (loadInst.isOffset) { + val offsetReg = requireReadableRegister(tokens, loadInst) + loadInst.setOffsetToken(offsetReg) + } + + val value = requireIntegerOrLabelReferenceToken(tokens, loadInst) + loadInst.setValue(value) + + instruction = loadInst + } else if (type.instructionClass == InstructionClass.LOAD_STORE_REGISTER) { + val loadInst = LoadStoreRegisterInstruction(type, labelTokens, instructionToken) + + val targetReg = requireWriteableRegister(tokens, loadInst) + loadInst.setTargetToken(targetReg) + + val pointerReg = requireWriteableRegister(tokens, loadInst) + loadInst.setPointerToken(pointerReg) + + // if there is an offset register specified, consume it + if (!tokens.isEmpty() && tokens.peekFirst().type == MTMCToken.TokenType.IDENTIFIER) { + val offsetReg = requireReadableRegister(tokens, loadInst) + loadInst.setOffsetToken(offsetReg) + } + instruction = loadInst + } else if (type.instructionClass == InstructionClass.JUMP_REGISTER) { + val jumpInst = JumpRegisterInstruction(type, labelTokens, instructionToken) + val register = requireReadableRegister(tokens, jumpInst) + jumpInst.setRegister(register) + instruction = jumpInst + } else if (type.instructionClass == InstructionClass.JUMP) { + val jumpInst = JumpInstruction(type, labelTokens, instructionToken) + val labelValue: MTMCToken? = maybeGetLabelReference(tokens) + val valueToken: MTMCToken? + if (labelValue != null) { + valueToken = labelValue + } else { + valueToken = requireIntegerToken(tokens, jumpInst, MonTanaMiniComputer.MEMORY_SIZE) + } + jumpInst.setAddressToken(valueToken) + instruction = jumpInst + } else { + instruction = ErrorInstruction( + listOf(), + instructionToken, + "Unexpected Token" + ) + } + + for (labelToken in labelTokens) { + if (hasLabel(labelToken.stringValue)) { + instruction.addError(tokens.poll(), "Label already defined: " + labelToken.stringValue) + } else { + labels.put(labelToken.labelValue(), instruction) + } + } + + // add error if any tokens are left on the line + if (!tokens.isEmpty()) { + instruction.addError(tokens.poll(), "Unexpected Token") + } + + instructions.add(instruction) + } + + //=================================================== + // tokenization helper functions + //=================================================== + private fun requireSysCall(tokens: LinkedList, inst: Instruction): MTMCToken { + val sysCallType = tokens.poll() + if (sysCallType == null) { + inst.addError("Syscall required") + } else if (sysCallType.type != MTMCToken.TokenType.IDENTIFIER) { + inst.addError(sysCallType, "Syscall required") + } else if (!SysCall.isSysCall(sysCallType.stringValue)) { + inst.addError(sysCallType, "Unknown syscall : " + sysCallType.stringValue) + } + return sysCallType!! + } + + private fun requireAluOp(tokens: LinkedList, inst: Instruction): MTMCToken { + val sysCallType = tokens.poll() + if (sysCallType == null) { + inst.addError("Syscall required") + } else if (sysCallType.type != MTMCToken.TokenType.IDENTIFIER) { + inst.addError(sysCallType, "Syscall required") + } else if (!isALUOp(sysCallType.stringValue)) { + inst.addError(sysCallType, "Unknown alu operation : " + sysCallType.stringValue) + } + return sysCallType!! + } + + private fun requireWriteableRegister( + tokens: LinkedList, + instruction: Instruction + ): MTMCToken { + val nextToken = tokens.poll() + if (nextToken == null) { + instruction.addError("Register required") + } else if (nextToken.type != MTMCToken.TokenType.IDENTIFIER) { + instruction.addError(nextToken, "Invalid Register : " + nextToken.stringValue) + } else if (!isWriteable(nextToken.stringValue)) { + instruction.addError(nextToken, "Register not writeable : " + nextToken.stringValue) + } + return nextToken!! + } + + private fun requireReadableRegister( + tokens: LinkedList, + instruction: Instruction + ): MTMCToken { + val nextToken = tokens.poll() + if (nextToken == null) { + instruction.addError("Register required") + } else if (nextToken.type != MTMCToken.TokenType.IDENTIFIER) { + instruction.addError(nextToken, "Invalid Register : " + nextToken.stringValue) + } else if (!isReadable(nextToken.stringValue)) { + instruction.addError(nextToken, "Register not readable : " + nextToken.stringValue) + } + return nextToken!! + } + + private fun requireALUOp(tokens: LinkedList, instruction: Instruction): MTMCToken { + val nextToken = tokens.poll() + if (nextToken == null || nextToken.type != MTMCToken.TokenType.IDENTIFIER || !isALUOp(nextToken.stringValue)) { + instruction.addError("ALU operation required") + } + return nextToken!! + } + + private fun requireString(tokens: LinkedList, instruction: ASMElement): MTMCToken { + val nextToken = tokens.poll() + if (nextToken == null || nextToken.type != MTMCToken.TokenType.STRING) { + instruction.addError("String required") + } + return nextToken!! + } + + private fun requireIntegerToken( + tokens: LinkedList, + inst: ASMElement, + max: Int + ): MTMCToken { + val token = tokens.poll() + if (token == null) { + inst.addError("Integer value required") + } else if (isInteger(token)) { + val integerValue = token.intValue() + if (integerValue < 0 || max < integerValue) { + inst.addError(token, "Integer value out of range: 0-" + max) + } + } else { + inst.addError(token, "Integer value expected") + } + return token!! + } + + private fun requireToken( + tokens: LinkedList, + type: MTMCToken.TokenType, + inst: ASMElement + ): MTMCToken? { + val token = tokens.poll() + if (token == null || token.type != type) { + inst.addError(token, "Token " + type.name + " required") + } + return token + } + + private fun requireIntegerOrLabelReferenceToken( + tokens: LinkedList, + inst: LoadStoreInstruction + ): MTMCToken { + val token = tokens.poll() + if (token == null) { + inst.addError("Integer or label value required") + } else if (!isInteger(token) && token.type != MTMCToken.TokenType.IDENTIFIER) { + inst.addError(token, "Integer or label value expected") + } + return token!! + } + + + private val tokensForLine: LinkedList + get() { + val tokens = LinkedList() + if (tokenizer!!.more()) { + val first = tokenizer!!.consume() + tokens.add(first) + while (tokenizer!!.more() && + first.line == tokenizer!!.currentToken().line + ) { + tokens.add(tokenizer!!.consume()) + } + } + return tokens + } + + fun hasLabel(label: String?): Boolean { + return labels.containsKey(label) + } + + fun resolveLabel(label: String?): Int { + return labels.get(label)!!.location + } + + internal enum class ASMMode { + DATA, + TEXT + } + + companion object { + fun transformSyntheticInstructions(tokens: LinkedList): LinkedList { + if (!tokens.isEmpty()) { + val first = tokens.peekFirst() + if (first.type == MTMCToken.TokenType.IDENTIFIER) { + val stringVal = first.stringValue + if (stringVal.endsWith("i")) { + val op = stringVal.substring(0, stringVal.length - 1) + if (isALUOp(op)) { + val syntheticImmediate = tokens.removeFirst() + tokens.addFirst(syntheticImmediate.cloneWithVal(op)) + tokens.addFirst(syntheticImmediate.cloneWithVal("imm")) + } + } else if (stringVal.startsWith("s")) { + val op = stringVal.substring(1, stringVal.length) + if (isALUOp(op)) { + val syntheticImmediate = tokens.removeFirst() + tokens.addFirst(syntheticImmediate.cloneWithVal(op)) + tokens.addFirst(syntheticImmediate.cloneWithVal("sop")) + } + } else if (stringVal == "la") { + val syntheticImmediate = tokens.removeFirst() + tokens.addFirst(syntheticImmediate.cloneWithVal("li")) + } else if (stringVal == "ret") { + val syntheticImmediate = tokens.removeFirst() + tokens.addFirst(syntheticImmediate.cloneWithVal("ra")) + tokens.addFirst(syntheticImmediate.cloneWithVal("jr")) + } + } + } + return tokens + } + + private fun maybeGetLabels(tokens: LinkedList): MutableList { + val labels = LinkedList() + while (!tokens.isEmpty() && tokens.getFirst().type == MTMCToken.TokenType.LABEL) { + val label = tokens.poll() + labels.add(label!!) + } + return labels + } + + private fun maybeGetLabelReference(tokens: LinkedList): MTMCToken? { + var label: MTMCToken? = null + if (tokens.getFirst().type == MTMCToken.TokenType.IDENTIFIER) { + label = tokens.poll() + } + return label + } + + private fun isInteger(token: MTMCToken?): Boolean { + return token != null && (token.type == MTMCToken.TokenType.INTEGER || token.type == MTMCToken.TokenType.HEX || token.type == MTMCToken.TokenType.BINARY) + } + } +} diff --git a/src/jvmMain/kotlin/mtmc/asm/AssemblyResult.kt b/src/jvmMain/kotlin/mtmc/asm/AssemblyResult.kt new file mode 100644 index 0000000..38e88d8 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/asm/AssemblyResult.kt @@ -0,0 +1,20 @@ +package mtmc.asm + +import mtmc.emulator.DebugInfo + +@JvmRecord +data class AssemblyResult( + @JvmField val code: ByteArray, + val data: ByteArray, + val graphics: Array, + @JvmField val debugInfo: DebugInfo?, + @JvmField val errors: MutableList +) { + fun printErrors(): String { + val builder = StringBuilder("Errors:\n") + for (error in errors) { + builder.append(" Line " + error.token.line + ": " + error.error).append('\n') + } + return builder.toString() + } +} diff --git a/src/jvmMain/kotlin/mtmc/asm/HasLocation.kt b/src/jvmMain/kotlin/mtmc/asm/HasLocation.kt new file mode 100644 index 0000000..11d4b43 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/asm/HasLocation.kt @@ -0,0 +1,6 @@ +package mtmc.asm + +interface HasLocation { + var location: Int + var sizeInBytes: Int +} diff --git a/src/jvmMain/kotlin/mtmc/asm/data/Data.kt b/src/jvmMain/kotlin/mtmc/asm/data/Data.kt new file mode 100644 index 0000000..70471d9 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/asm/data/Data.kt @@ -0,0 +1,37 @@ +package mtmc.asm.data + +import mtmc.asm.ASMElement +import mtmc.asm.Assembler +import mtmc.tokenizer.MTMCToken + +class Data(labels: MutableList, lineNumber: Int) : ASMElement(labels, lineNumber) { + var valueToken: MTMCToken? = null + private var value: ByteArray = ByteArray(0) + + override var sizeInBytes: Int + get() = value.size + set(value) {} + + fun genData(dataBytes: ByteArray, assembler: Assembler) { + val offset = location - assembler.instructionsSizeInBytes + for (i in 0.., + instructionToken: MTMCToken +) : Instruction(type, label, instructionToken) { + private var toToken: MTMCToken? = null + private var fromToken: MTMCToken? = null + private var immediateOp: MTMCToken? = null + private var value: MTMCToken? = null + + fun setTo(to: MTMCToken) { + this.toToken = to + } + + fun setFrom(from: MTMCToken?) { + this.fromToken = from + } + + fun setImmediateValue(value: MTMCToken) { + this.value = value + } + + fun setImmediateOp(immediateOp: MTMCToken) { + this.immediateOp = immediateOp + } + + fun isImmediateOp(): Boolean { + return this.type == InstructionType.IMM + } + + val isBinaryOp: Boolean + get() = !ALUOp.valueOf(instructionToken.stringValue.uppercase(Locale.getDefault())).isUnary + + override fun genCode(output: ByteArray, assembler: Assembler) { + val opCode = ALUOp.toInteger(instructionToken.stringValue) + val to = toInteger(toToken!!.stringValue) + var from = 0 + if (fromToken != null) { + from = toInteger(fromToken!!.stringValue) + } + output[location] = (16 or opCode).toByte() + if (this.isBinaryOp) { + output[location + 1] = (to shl 4 or from).toByte() + } else if (isImmediateOp()) { + val immediateValue = value!!.intValue() + val immediateOpValue = ALUOp.toInteger(immediateOp!!.stringValue) + output[location + 1] = (to shl 4 or immediateOpValue).toByte() + output[location + 2] = (immediateValue ushr 8).toByte() + output[location + 3] = immediateValue.toByte() + } else { // unary op + output[location + 1] = (to shl 4).toByte() + } + } + + companion object { + @JvmStatic + fun disassemble(instruction: Short): String? { + if (BinaryUtils.getBits(16, 4, instruction).toInt() == 1) { + val builder = StringBuilder() + val opCode = BinaryUtils.getBits(12, 4, instruction) + val op = ALUOp.fromInt(opCode) + val aluOp = ALUOp.valueOf(op.uppercase(Locale.getDefault())) + if (aluOp == ALUOp.IMM) { + builder.append(op).append(" ") + builder.append(fromInteger(BinaryUtils.getBits(8, 4, instruction).toInt())).append(" ") + builder.append(ALUOp.fromInt(BinaryUtils.getBits(4, 4, instruction))).append(" ") + } else if (aluOp.isUnary) { + builder.append(op).append(" ") + builder.append(fromInteger(BinaryUtils.getBits(8, 4, instruction).toInt())).append(" ") + } else { + builder.append(op).append(" ") + builder.append(fromInteger(BinaryUtils.getBits(8, 4, instruction).toInt())).append(" ") + builder.append(fromInteger(BinaryUtils.getBits(4, 4, instruction).toInt())).append(" ") + } + return builder.toString() + } + return null + } + } +} diff --git a/src/jvmMain/kotlin/mtmc/asm/instructions/ALUOp.kt b/src/jvmMain/kotlin/mtmc/asm/instructions/ALUOp.kt new file mode 100644 index 0000000..821897f --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/asm/instructions/ALUOp.kt @@ -0,0 +1,44 @@ +package mtmc.asm.instructions + +import java.util.* + +enum class ALUOp(val opCode: Int, val isUnary: Boolean) { + ADD(0x0000, false), + SUB(0x0001, false), + MUL(0x0002, false), + DIV(0x0003, false), + MOD(0x0004, false), + AND(0x0005, false), + OR(0x0006, false), + XOR(0x0007, false), + SHL(0x0008, false), + SHR(0x0009, false), + MIN(0x000A, false), + MAX(0x000B, false), + NOT(0x000C, true), + LNOT(0x000D, true), + NEG(0x000E, true), + IMM(0x000F, true); + + companion object { + @JvmStatic + fun toInteger(instruction: String): Int { + return valueOf(instruction.uppercase(Locale.getDefault())).opCode + } + + @JvmStatic + fun fromInt(opCode: Short): String { + return entries[opCode.toInt()].name.lowercase(Locale.getDefault()) + } + + @JvmStatic + fun isALUOp(op: String): Boolean { + try { + val aluOp = valueOf(op.uppercase(Locale.getDefault())) + return true + } catch (e: IllegalArgumentException) { + return false + } + } + } +} diff --git a/src/jvmMain/kotlin/mtmc/asm/instructions/ErrorInstruction.kt b/src/jvmMain/kotlin/mtmc/asm/instructions/ErrorInstruction.kt new file mode 100644 index 0000000..3427536 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/asm/instructions/ErrorInstruction.kt @@ -0,0 +1,23 @@ +package mtmc.asm.instructions + +import mtmc.asm.Assembler +import mtmc.tokenizer.MTMCToken + +class ErrorInstruction( + labels: List, + instruction: MTMCToken, + error: String +) : Instruction( + InstructionType.ERROR, + labels, + instruction +) { + + init { + addError(instruction, error) + } + + override fun genCode(output: ByteArray, assembler: Assembler) { + // do nothing + } +} diff --git a/src/jvmMain/kotlin/mtmc/asm/instructions/Instruction.kt b/src/jvmMain/kotlin/mtmc/asm/instructions/Instruction.kt new file mode 100644 index 0000000..0f08b63 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/asm/instructions/Instruction.kt @@ -0,0 +1,72 @@ +package mtmc.asm.instructions + +import mtmc.asm.ASMElement +import mtmc.asm.Assembler +import mtmc.asm.instructions.ALUInstruction.Companion.disassemble +import mtmc.emulator.MonTanaMiniComputer.Companion.isDoubleWordInstruction +import mtmc.tokenizer.MTMCToken + +abstract class Instruction( + @JvmField val type: InstructionType, + labels: List, + @JvmField val instructionToken: MTMCToken +) : ASMElement(labels, instructionToken.line) { + open fun validateLabel(assembler: Assembler) { + // default does nothing + } + + override fun addError(error: String) { + addError(instructionToken, error) + } + + abstract fun genCode(output: ByteArray, assembler: Assembler) + + override var sizeInBytes: Int = type.sizeInBytes + get() = type.sizeInBytes + + companion object { + @JvmStatic + fun isInstruction(cmd: String): Boolean { + return InstructionType.fromString(cmd) != null + } + + fun disassemble(instruction: Short, previousInstruction: Short): String { + if (isDoubleWordInstruction(previousInstruction)) { + return instruction.toInt().toString() + } + val misc = MiscInstruction.disassemble(instruction) + if (misc != null) { + return misc + } + val aluOp = disassemble(instruction) + if (aluOp != null) { + return aluOp + } + val stack = StackInstruction.disassemble(instruction) + if (stack != null) { + return stack + } + val test = TestInstruction.disassemble(instruction) + if (test != null) { + return test + } + val lsr = LoadStoreRegisterInstruction.disassemble(instruction) + if (lsr != null) { + return lsr + } + val ls = LoadStoreInstruction.disassemble(instruction) + if (ls != null) { + return ls + } + val jumpReg = JumpInstruction.disassemble(instruction) + if (jumpReg != null) { + return jumpReg + } + val jump = JumpInstruction.disassemble(instruction) + if (jump != null) { + return jump + } + return "" + } + } +} diff --git a/src/jvmMain/kotlin/mtmc/asm/instructions/InstructionType.kt b/src/jvmMain/kotlin/mtmc/asm/instructions/InstructionType.kt new file mode 100644 index 0000000..5267bdb --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/asm/instructions/InstructionType.kt @@ -0,0 +1,98 @@ +package mtmc.asm.instructions + +import java.util.* + +enum class InstructionType @JvmOverloads constructor( + @JvmField val instructionClass: InstructionClass?, + val sizeInBytes: Int = 2 +) { + SYS(InstructionClass.MISC), + MOV(InstructionClass.MISC), + INC(InstructionClass.MISC), + DEC(InstructionClass.MISC), + SETI(InstructionClass.MISC), + NOP(InstructionClass.MISC), + MCP(InstructionClass.MISC, 4), + DEBUG(InstructionClass.MISC), + ADD(InstructionClass.ALU), + SUB(InstructionClass.ALU), + MUL(InstructionClass.ALU), + DIV(InstructionClass.ALU), + MOD(InstructionClass.ALU), + AND(InstructionClass.ALU), + OR(InstructionClass.ALU), + XOR(InstructionClass.ALU), + SHL(InstructionClass.ALU), + SHR(InstructionClass.ALU), + MIN(InstructionClass.ALU), + MAX(InstructionClass.ALU), + NOT(InstructionClass.ALU), + LNOT(InstructionClass.ALU), + NEG(InstructionClass.ALU), + IMM(InstructionClass.ALU, 4), + PUSH(InstructionClass.STACK), + POP(InstructionClass.STACK), + DUP(InstructionClass.STACK), + SWAP(InstructionClass.STACK), + DROP(InstructionClass.STACK), + OVER(InstructionClass.STACK), + ROT(InstructionClass.STACK), + SOP(InstructionClass.STACK), + PUSHI(InstructionClass.STACK, 4), + EQ(InstructionClass.TEST), + NEQ(InstructionClass.TEST), + GT(InstructionClass.TEST), + GTE(InstructionClass.TEST), + LT(InstructionClass.TEST), + LTE(InstructionClass.TEST), + EQI(InstructionClass.TEST), + NEQI(InstructionClass.TEST), + GTI(InstructionClass.TEST), + GTEI(InstructionClass.TEST), + LTI(InstructionClass.TEST), + LTEI(InstructionClass.TEST), + LWR(InstructionClass.LOAD_STORE_REGISTER), + LBR(InstructionClass.LOAD_STORE_REGISTER), + SWR(InstructionClass.LOAD_STORE_REGISTER), + SBR(InstructionClass.LOAD_STORE_REGISTER), + LW(InstructionClass.LOAD_STORE, 4), + LWO(InstructionClass.LOAD_STORE, 4), + LI(InstructionClass.LOAD_STORE, 4), + LB(InstructionClass.LOAD_STORE, 4), + LBO(InstructionClass.LOAD_STORE, 4), + SW(InstructionClass.LOAD_STORE, 4), + SWO(InstructionClass.LOAD_STORE, 4), + SB(InstructionClass.LOAD_STORE, 4), + SBO(InstructionClass.LOAD_STORE, 4), + JR(InstructionClass.JUMP_REGISTER), + J(InstructionClass.JUMP), + JZ(InstructionClass.JUMP), + JNZ(InstructionClass.JUMP), + JAL(InstructionClass.JUMP), + META(InstructionClass.MISC), + ERROR(InstructionClass.MISC), + ; + + + enum class InstructionClass { + MISC, + ALU, + STACK, + TEST, + LOAD_STORE_REGISTER, + LOAD_STORE, + JUMP_REGISTER, + JUMP + } + + companion object { + @JvmStatic + fun fromString(string: String): InstructionType? { + try { + return valueOf(string.uppercase(Locale.getDefault())) + } catch (e: IllegalArgumentException) { + return null + } + } + } +} diff --git a/src/jvmMain/kotlin/mtmc/asm/instructions/JumpInstruction.kt b/src/jvmMain/kotlin/mtmc/asm/instructions/JumpInstruction.kt new file mode 100644 index 0000000..d784bde --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/asm/instructions/JumpInstruction.kt @@ -0,0 +1,69 @@ +package mtmc.asm.instructions + +import mtmc.asm.Assembler +import mtmc.tokenizer.MTMCToken +import mtmc.util.BinaryUtils + +class JumpInstruction( + type: InstructionType, + label: List, + instructionToken: MTMCToken +) : Instruction(type, label, instructionToken) { + private var addressToken: MTMCToken? = null + + fun setAddressToken(addressToken: MTMCToken) { + this.addressToken = addressToken + } + + override fun validateLabel(assembler: Assembler) { + if (addressToken!!.type == MTMCToken.TokenType.IDENTIFIER) { + if (!assembler.hasLabel(addressToken!!.stringValue)) { + addError("Unresolved label: " + addressToken!!.stringValue) + } + } + } + + private fun resolveTargetAddress(assembler: Assembler): Int? { + if (addressToken!!.type == MTMCToken.TokenType.IDENTIFIER) { + return assembler.resolveLabel(addressToken!!.stringValue) + } else { + return addressToken!!.intValue() + } + } + + override fun genCode(output: ByteArray, assembler: Assembler) { + var opcode = 0 + when (type) { + InstructionType.J -> opcode = 12 + InstructionType.JZ -> opcode = 13 + InstructionType.JNZ -> opcode = 14 + InstructionType.JAL -> opcode = 15 + else -> println("Invalid instruction type") + } + val address = resolveTargetAddress(assembler)!! + output[location] = (opcode shl 4 or (address ushr 8)).toByte() + output[location + 1] = address.toByte() + } + + companion object { + fun disassemble(instruction: Short): String? { + if (BinaryUtils.getBits(16, 2, instruction).toInt() == 3) { + val jumpType = BinaryUtils.getBits(14, 2, instruction) + val sb = StringBuilder() + if (jumpType.toInt() == 0) { + sb.append("j") + } else if (jumpType.toInt() == 1) { + sb.append("jz") + } else if (jumpType.toInt() == 2) { + sb.append("jnz") + } else if (jumpType.toInt() == 3) { + sb.append("jal") + } + val target = BinaryUtils.getBits(12, 12, instruction) + sb.append(" ").append(target.toInt()) + return sb.toString() + } + return null + } + } +} diff --git a/src/jvmMain/kotlin/mtmc/asm/instructions/JumpRegisterInstruction.kt b/src/jvmMain/kotlin/mtmc/asm/instructions/JumpRegisterInstruction.kt new file mode 100644 index 0000000..7e8596f --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/asm/instructions/JumpRegisterInstruction.kt @@ -0,0 +1,37 @@ +package mtmc.asm.instructions + +import mtmc.asm.Assembler +import mtmc.emulator.Register.Companion.fromInteger +import mtmc.emulator.Register.Companion.toInteger +import mtmc.tokenizer.MTMCToken +import mtmc.util.BinaryUtils + +class JumpRegisterInstruction( + type: InstructionType, + label: List, + instructionToken: MTMCToken +) : Instruction(type, label, instructionToken) { + private var register: MTMCToken? = null + + fun setRegister(register: MTMCToken) { + this.register = register + } + + public override fun genCode(output: ByteArray, assembler: Assembler) { + val opcode = 9 + output[location] = (opcode shl 4).toByte() + val reg = toInteger(register!!.stringValue) + output[location + 1] = reg.toByte() + } + + companion object { + fun disassemble(instruction: Short): String? { + if (BinaryUtils.getBits(16, 5, instruction).toInt() == 9) { + val reg = BinaryUtils.getBits(4, 4, instruction) + val sb = StringBuilder("jr") + sb.append(fromInteger(reg.toInt())) + } + return null + } + } +} diff --git a/src/jvmMain/kotlin/mtmc/asm/instructions/LoadStoreInstruction.kt b/src/jvmMain/kotlin/mtmc/asm/instructions/LoadStoreInstruction.kt new file mode 100644 index 0000000..3eff67a --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/asm/instructions/LoadStoreInstruction.kt @@ -0,0 +1,115 @@ +package mtmc.asm.instructions + +import mtmc.asm.Assembler +import mtmc.emulator.Register.Companion.fromInteger +import mtmc.emulator.Register.Companion.toInteger +import mtmc.tokenizer.MTMCToken +import mtmc.util.BinaryUtils + +class LoadStoreInstruction( + type: InstructionType, + label: List, + instructionToken: MTMCToken +) : Instruction(type, label, instructionToken) { + private var targetToken: MTMCToken? = null + private var offsetToken: MTMCToken? = null + private var value: MTMCToken? = null + + fun setTargetToken(targetToken: MTMCToken) { + this.targetToken = targetToken + } + + fun setOffsetToken(offsetToken: MTMCToken) { + this.offsetToken = offsetToken + } + + fun setValue(value: MTMCToken) { + this.value = value + } + + val isOffset: Boolean + get() = type.name.endsWith("O") + + public override fun validateLabel(assembler: Assembler) { + if (value!!.type == MTMCToken.TokenType.IDENTIFIER) { + if (!assembler.hasLabel(value!!.stringValue)) { + addError("Unresolved label: " + value!!.stringValue) + } + } + } + + private fun resolveValue(assembler: Assembler): Int? { + if (value!!.type == MTMCToken.TokenType.IDENTIFIER) { + return assembler.resolveLabel(value!!.stringValue) + } else { + return value!!.intValue() + } + } + + override fun genCode(output: ByteArray, assembler: Assembler) { + val upperByte = when (type) { + InstructionType.LW -> 128 + InstructionType.LWO -> 129 + InstructionType.LB -> 130 + InstructionType.LBO -> 131 + InstructionType.SW -> 132 + InstructionType.SWO -> 133 + InstructionType.SB -> 134 + InstructionType.SBO -> 135 + InstructionType.LI -> 143 + else -> 0 + } + + val target = toInteger(targetToken!!.stringValue) + output[location] = upperByte.toByte() + + if (this.isOffset) { + val offsetReg = toInteger(offsetToken!!.stringValue) + output[location + 1] = (target shl 4 or offsetReg).toByte() + } else { + output[location + 1] = (target shl 4).toByte() + } + + val numericValue = resolveValue(assembler)!! + output[location + 2] = (numericValue ushr 8).toByte() + output[location + 3] = numericValue.toByte() + } + + companion object { + fun disassemble(instruction: Short): String? { + if (BinaryUtils.getBits(16, 4, instruction).toInt() == 8) { + val topNibble = BinaryUtils.getBits(12, 4, instruction) + val sb = StringBuilder() + if (topNibble.toInt() == 15) { + sb.append("li ") + } else if (topNibble.toInt() == 0) { + sb.append("lw ") + } else if (topNibble.toInt() == 1) { + sb.append("lwo ") + } else if (topNibble.toInt() == 2) { + sb.append("lb ") + } else if (topNibble.toInt() == 3) { + sb.append("lbo ") + } else if (topNibble.toInt() == 4) { + sb.append("sw ") + } else if (topNibble.toInt() == 5) { + sb.append("swo ") + } else if (topNibble.toInt() == 6) { + sb.append("sb ") + } else if (topNibble.toInt() == 7) { + sb.append("sbo ") + } + val target = BinaryUtils.getBits(8, 4, instruction) + val reg = fromInteger(target.toInt()) + sb.append(reg) + if (topNibble.toInt() == 1 || topNibble.toInt() == 3 || topNibble.toInt() == 5 || topNibble.toInt() == 7) { + val offset = BinaryUtils.getBits(4, 4, instruction) + val offsetReg = fromInteger(offset.toInt()) + sb.append(" ").append(offsetReg) + } + return sb.toString() + } + return null + } + } +} diff --git a/src/jvmMain/kotlin/mtmc/asm/instructions/LoadStoreRegisterInstruction.kt b/src/jvmMain/kotlin/mtmc/asm/instructions/LoadStoreRegisterInstruction.kt new file mode 100644 index 0000000..2778b52 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/asm/instructions/LoadStoreRegisterInstruction.kt @@ -0,0 +1,75 @@ +package mtmc.asm.instructions + +import mtmc.asm.Assembler +import mtmc.emulator.Register +import mtmc.emulator.Register.Companion.fromInteger +import mtmc.emulator.Register.Companion.toInteger +import mtmc.tokenizer.MTMCToken +import mtmc.util.BinaryUtils + +class LoadStoreRegisterInstruction( + type: InstructionType, + label: List, + instructionToken: MTMCToken +) : Instruction(type, label, instructionToken) { + private var targetToken: MTMCToken? = null + private var pointerToken: MTMCToken? = null + private var offsetToken: MTMCToken? = null + + fun setTargetToken(targetToken: MTMCToken) { + this.targetToken = targetToken + } + + fun setPointerToken(pointerToken: MTMCToken) { + this.pointerToken = pointerToken + } + + fun setOffsetToken(offsetToken: MTMCToken?) { + this.offsetToken = offsetToken + } + + override fun genCode(output: ByteArray, assembler: Assembler) { + var opcode = 0 + when (type) { + InstructionType.LWR -> opcode = 4 + InstructionType.LBR -> opcode = 5 + InstructionType.SWR -> opcode = 6 + InstructionType.SBR -> opcode = 7 + else -> println("Invalid instruction type") + } + val target = toInteger(targetToken!!.stringValue) + val pointer = toInteger(pointerToken!!.stringValue) + var offset = Register.PC.ordinal + if (offsetToken != null) { + offset = toInteger(offsetToken!!.stringValue) + } + output[location] = (opcode shl 4 or target).toByte() + output[location + 1] = (pointer shl 4 or offset).toByte() + } + + companion object { + fun disassemble(instruction: Short): String? { + if (BinaryUtils.getBits(16, 2, instruction).toInt() == 1) { + val builder = StringBuilder() + val type = BinaryUtils.getBits(14, 2, instruction) + if (type.toInt() == 0) { + builder.append("lwr ") + } else if (type.toInt() == 1) { + builder.append("lbr ") + } else if (type.toInt() == 2) { + builder.append("swr ") + } else if (type.toInt() == 3) { + builder.append("sbr ") + } + val srcDestReg = BinaryUtils.getBits(12, 4, instruction) + val addrReg = BinaryUtils.getBits(8, 4, instruction) + val offsetReg = BinaryUtils.getBits(4, 4, instruction) + builder.append(fromInteger(srcDestReg.toInt())).append(" ") + builder.append(fromInteger(addrReg.toInt())).append(" ") + builder.append(fromInteger(offsetReg.toInt())) + return builder.toString() + } + return null + } + } +} diff --git a/src/jvmMain/kotlin/mtmc/asm/instructions/MetaInstruction.kt b/src/jvmMain/kotlin/mtmc/asm/instructions/MetaInstruction.kt new file mode 100644 index 0000000..4d45e25 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/asm/instructions/MetaInstruction.kt @@ -0,0 +1,90 @@ +package mtmc.asm.instructions + +import mtmc.asm.Assembler +import mtmc.tokenizer.MTMCToken + +class MetaInstruction(instruction: MTMCToken) : + Instruction(InstructionType.META, listOf(), instruction) { + private var originalFilePath: MTMCToken? = null + private var originaLineNumber: MTMCToken? = null + private var globalName: MTMCToken? = null + private var globalLocation: MTMCToken? = null + private var globalType: MTMCToken? = null + private var localName: MTMCToken? = null + private var localOffset: MTMCToken? = null + private var localType: MTMCToken? = null + + override fun genCode(output: ByteArray, assembler: Assembler) { + // do nothing + } + + val isFileDirective: Boolean + get() = "file" == this.instructionToken.stringValue + + val isLineDirective: Boolean + get() = "line" == this.instructionToken.stringValue + + val isGlobalDirective: Boolean + get() = "global" == this.instructionToken.stringValue + + val isLocalDirective: Boolean + get() = "local" == this.instructionToken.stringValue + + val isEndLocalDirective: Boolean + get() = "endlocal" == this.instructionToken.stringValue + + fun setOriginalFilePath(path: MTMCToken) { + this.originalFilePath = path + } + + fun setOriginalLineNumber(lineNumber: MTMCToken) { + this.originaLineNumber = lineNumber + } + + val originalLineNumber: Int + get() = this.originaLineNumber!!.intValue() + + fun setGlobalInfo(name: MTMCToken, location: MTMCToken, type: MTMCToken) { + this.globalName = name + this.globalLocation = location + this.globalType = type + } + + fun setLocalInfo(name: MTMCToken, offset: MTMCToken, type: MTMCToken) { + this.localName = name + this.localOffset = offset + this.localType = type + } + + fun setEndLocalInfo(name: MTMCToken) { + this.localName = name + } + + fun getOriginalFilePath(): String? { + return this.originalFilePath!!.stringValue + } + + fun getGlobalName(): String? { + return this.globalName!!.stringValue + } + + fun getGlobalLocation(): Int { + return this.globalLocation!!.intValue() + } + + fun getGlobalType(): String? { + return this.globalType!!.stringValue + } + + fun getLocalName(): String? { + return this.localName!!.stringValue + } + + fun getLocalOffset(): Int { + return this.localOffset!!.intValue() + } + + fun getLocalType(): String? { + return this.localType!!.stringValue + } +} diff --git a/src/jvmMain/kotlin/mtmc/asm/instructions/MiscInstruction.kt b/src/jvmMain/kotlin/mtmc/asm/instructions/MiscInstruction.kt new file mode 100644 index 0000000..994c61e --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/asm/instructions/MiscInstruction.kt @@ -0,0 +1,145 @@ +package mtmc.asm.instructions + +import mtmc.asm.Assembler +import mtmc.emulator.Register.Companion.fromInteger +import mtmc.emulator.Register.Companion.toInteger +import mtmc.os.SysCall +import mtmc.tokenizer.MTMCToken +import mtmc.util.BinaryUtils + +class MiscInstruction( + type: InstructionType, + labels: List, + instructionToken: MTMCToken +) : Instruction(type, labels, instructionToken) { + private var syscallType: MTMCToken? = null + private var fromRegister: MTMCToken? = null + private var toRegister: MTMCToken? = null + private var value: MTMCToken? = null + + fun setSyscallType(type: MTMCToken) { + this.syscallType = type + } + + fun setFrom(fromRegister: MTMCToken) { + this.fromRegister = fromRegister + } + + fun setTo(toRegister: MTMCToken) { + this.toRegister = toRegister + } + + fun setValue(value: MTMCToken?) { + this.value = value + } + + override fun genCode(output: ByteArray, assembler: Assembler) { + if (type == InstructionType.SYS) { + output[location] = 0 + output[location + 1] = SysCall.getValue(this.syscallType!!.stringValue) + } else if (type == InstructionType.MOV) { + val to = toInteger(toRegister!!.stringValue) + val from = toInteger(fromRegister!!.stringValue) + output[location] = 1 + output[location + 1] = (to shl 4 or from).toByte() + } else if (type == InstructionType.INC) { + output[location] = 2 + val to = toInteger(toRegister!!.stringValue) + var immediateVal = 1 + if (value != null) { + immediateVal = value!!.intValue() + } + output[location + 1] = (to shl 4 or immediateVal).toByte() + } else if (type == InstructionType.DEC) { + output[location] = 3 + val to = toInteger(toRegister!!.stringValue) + var immediateVal = 1 + if (value != null) { + immediateVal = value!!.intValue() + } + output[location + 1] = (to shl 4 or immediateVal).toByte() + } else if (type == InstructionType.SETI) { + output[location] = 4 + val to = toInteger(toRegister!!.stringValue) + val immediateVal = value!!.intValue() + output[location + 1] = (to shl 4 or immediateVal).toByte() + } else if (type == InstructionType.MCP) { + output[location] = 5 + val from = toInteger(fromRegister!!.stringValue) + val to = toInteger(toRegister!!.stringValue) + val value = this.value!!.intValue() + output[location + 1] = (from shl 4 or to).toByte() + output[location + 2] = (value shl 8).toByte() + output[location + 3] = (value and 0xFF).toByte() + } else if (type == InstructionType.DEBUG) { + output[location] = 8 + output[location + 1] = value!!.intValue().toByte() + } else if (type == InstructionType.NOP) { + output[location] = 15 + output[location + 1] = 255.toByte() + } + } + + companion object { + fun disassemble(instruction: Short): String? { + if (BinaryUtils.getBits(16, 4, instruction).toInt() == 0) { + val topNibble = BinaryUtils.getBits(12, 4, instruction) + if (topNibble.toInt() == 0) { + val builder = StringBuilder("sys ") + val bits = BinaryUtils.getBits(8, 8, instruction) + val name = SysCall.getString(bits.toByte()) + builder.append(name) + return builder.toString() + } else if (topNibble.toInt() == 1) { + val builder = StringBuilder("mov ") + val to = BinaryUtils.getBits(8, 4, instruction) + val from = BinaryUtils.getBits(4, 4, instruction) + val toName = fromInteger(to.toInt()) + builder.append(toName).append(" ") + val fromName = fromInteger(from.toInt()) + builder.append(fromName) + return builder.toString() + } else if (topNibble.toInt() == 2) { + val builder = StringBuilder("inc ") + val to = BinaryUtils.getBits(8, 4, instruction) + val amount = BinaryUtils.getBits(4, 4, instruction) + val toName = fromInteger(to.toInt()) + builder.append(toName).append(" ") + builder.append(amount.toInt()) + return builder.toString() + } else if (topNibble.toInt() == 3) { + val builder = StringBuilder("dec ") + val to = BinaryUtils.getBits(8, 4, instruction) + val amount = BinaryUtils.getBits(4, 4, instruction) + val toName = fromInteger(to.toInt()) + builder.append(toName).append(" ") + builder.append(amount.toInt()) + return builder.toString() + } else if (topNibble.toInt() == 4) { + val builder = StringBuilder("seti ") + val to = BinaryUtils.getBits(8, 4, instruction) + val amount = BinaryUtils.getBits(4, 4, instruction) + val toName = fromInteger(to.toInt()) + builder.append(toName).append(" ") + builder.append(amount.toInt()) + return builder.toString() + } else if (topNibble.toInt() == 5) { + val from = BinaryUtils.getBits(8, 4, instruction) + val fromName = fromInteger(from.toInt()) + val to = BinaryUtils.getBits(4, 4, instruction) + val toName = fromInteger(to.toInt()) + return "mcp " + fromName + " " + toName + } else if (topNibble.toInt() == 8) { + val builder = StringBuilder("debug ") + val stringIndex = BinaryUtils.getBits(8, 8, instruction) + builder.append(stringIndex.toInt()) + return builder.toString() + } else if (topNibble.toInt() == 15) { + val builder = StringBuilder("noop") + return builder.toString() + } + } + return null + } + } +} diff --git a/src/jvmMain/kotlin/mtmc/asm/instructions/StackInstruction.kt b/src/jvmMain/kotlin/mtmc/asm/instructions/StackInstruction.kt new file mode 100644 index 0000000..240bd23 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/asm/instructions/StackInstruction.kt @@ -0,0 +1,108 @@ +package mtmc.asm.instructions + +import mtmc.asm.Assembler +import mtmc.asm.instructions.ALUOp.Companion.fromInt +import mtmc.emulator.Register +import mtmc.emulator.Register.Companion.fromInteger +import mtmc.tokenizer.MTMCToken +import mtmc.util.BinaryUtils + +class StackInstruction( + type: InstructionType, + label: List, + instructionToken: MTMCToken +) : Instruction(type, label, instructionToken) { + private var targetToken: MTMCToken? = null + private var stackRegisterToken: MTMCToken? = null + private var aluOpToken: MTMCToken? = null + private var value: MTMCToken? = null + + fun setTarget(target: MTMCToken) { + this.targetToken = target + } + + fun setStackRegister(stackRegister: MTMCToken?) { + this.stackRegisterToken = stackRegister + } + + fun setALUOp(aluOp: MTMCToken) { + this.aluOpToken = aluOp + } + + fun setValue(value: MTMCToken) { + this.value = value + } + + override fun genCode(output: ByteArray, assembler: Assembler) { + var stackReg = Register.SP.ordinal + if (stackRegisterToken != null) { + stackReg = Register.Companion.toInteger(stackRegisterToken!!.stringValue) + } + if (type == InstructionType.PUSH) { + val target = Register.Companion.toInteger(targetToken!!.stringValue) + output[location] = 32 + output[location + 1] = (target shl 4 or stackReg).toByte() + } else if (type == InstructionType.POP) { + val target = Register.Companion.toInteger(targetToken!!.stringValue) + output[location] = 33 + output[location + 1] = (target shl 4 or stackReg).toByte() + } else if (type == InstructionType.SOP) { + val aluOp = ALUOp.toInteger(aluOpToken!!.stringValue) + output[location] = 39 + output[location + 1] = (aluOp shl 4 or stackReg).toByte() + } else if (type == InstructionType.PUSHI) { + val immediateValue = value!!.intValue() + output[location] = 47 + output[location + 1] = stackReg.toByte() + output[location + 2] = (immediateValue ushr 8).toByte() + output[location + 3] = immediateValue.toByte() + } else { + val stackOp: Int + stackOp = when (type) { + InstructionType.DUP -> 34 + InstructionType.SWAP -> 35 + InstructionType.DROP -> 36 + InstructionType.OVER -> 37 + InstructionType.ROT -> 38 + else -> 0 + } + output[location] = stackOp.toByte() + output[location + 1] = stackReg.toByte() + } + } + + companion object { + fun disassemble(instruction: Short): String? { + if (BinaryUtils.getBits(16, 4, instruction).toInt() == 2) { + val opCode = BinaryUtils.getBits(12, 4, instruction) + val stackReg = BinaryUtils.getBits(4, 4, instruction) + if (opCode.toInt() == 0) { + val sourceReg = BinaryUtils.getBits(8, 4, instruction) + return "push " + fromInteger(sourceReg.toInt()) + " " + fromInteger(stackReg.toInt()) + } + if (opCode.toInt() == 1) { + val destReg = BinaryUtils.getBits(8, 4, instruction) + return "pop " + fromInteger(destReg.toInt()) + " " + fromInteger(stackReg.toInt()) + } + if (opCode.toInt() == 2) { + return "dup " + fromInteger(stackReg.toInt()) + } else if (opCode.toInt() == 3) { + return "swap " + fromInteger(stackReg.toInt()) + } else if (opCode.toInt() == 4) { + return "drop " + fromInteger(stackReg.toInt()) + } else if (opCode.toInt() == 5) { + return "over " + fromInteger(stackReg.toInt()) + } else if (opCode.toInt() == 6) { + return "rot " + fromInteger(stackReg.toInt()) + } else if (opCode.toInt() == 7) { + val aluOp = BinaryUtils.getBits(8, 4, instruction) + val op = fromInt(aluOp) + return "sop " + op + " " + fromInteger(stackReg.toInt()) + } else if (opCode.toInt() == 15) { + return "pushi " + fromInteger(stackReg.toInt()) + } + } + return null + } + } +} diff --git a/src/jvmMain/kotlin/mtmc/asm/instructions/TestInstruction.kt b/src/jvmMain/kotlin/mtmc/asm/instructions/TestInstruction.kt new file mode 100644 index 0000000..235c8b3 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/asm/instructions/TestInstruction.kt @@ -0,0 +1,126 @@ +package mtmc.asm.instructions + +import mtmc.asm.Assembler +import mtmc.emulator.Register.Companion.fromInteger +import mtmc.emulator.Register.Companion.toInteger +import mtmc.tokenizer.MTMCToken +import mtmc.util.BinaryUtils + +class TestInstruction( + type: InstructionType, + label: List, + instructionToken: MTMCToken +) : Instruction(type, label, instructionToken) { + private var first: MTMCToken? = null + private var second: MTMCToken? = null + private var value: MTMCToken? = null + + fun setFirst(to: MTMCToken) { + this.first = to + } + + fun setSecond(from: MTMCToken) { + this.second = from + } + + fun setImmediateValue(value: MTMCToken) { + this.value = value + } + + val isImmediate: Boolean + get() = type.name.endsWith("I") + + override fun genCode(output: ByteArray, assembler: Assembler) { + val firstReg = toInteger(first!!.stringValue) + if (this.isImmediate) { + val upperByte = when (type) { + InstructionType.EQI -> 56 + InstructionType.NEQI -> 57 + InstructionType.GTI -> 58 + InstructionType.GTEI -> 59 + InstructionType.LTI -> 60 + InstructionType.LTEI -> 61 + else -> 0 + } + val immediateValue = value!!.intValue() + output[location] = upperByte.toByte() + output[location + 1] = (firstReg shl 4 or immediateValue).toByte() + } else { + val upperByte = when (type) { + InstructionType.EQ -> 48 + InstructionType.NEQ -> 49 + InstructionType.GT -> 50 + InstructionType.GTE -> 51 + InstructionType.LT -> 52 + InstructionType.LTE -> 53 + else -> 0 + } + val secondReg = toInteger(second!!.stringValue) + output[location] = upperByte.toByte() + output[location + 1] = (firstReg shl 4 or secondReg).toByte() + } + } + + companion object { + fun disassemble(instruction: Short): String? { + if (BinaryUtils.getBits(16, 4, instruction).toInt() == 3) { + val sb = StringBuilder() + val opCode = BinaryUtils.getBits(12, 4, instruction) + val thirdNibble = BinaryUtils.getBits(8, 4, instruction) + val fourthNibble = BinaryUtils.getBits(4, 4, instruction) + if (opCode.toInt() == 0) { + sb.append("eq ") + sb.append(fromInteger(thirdNibble.toInt())) + .append(" ").append(fromInteger(fourthNibble.toInt())) + } else if (opCode.toInt() == 1) { + sb.append("neq ") + sb.append(fromInteger(thirdNibble.toInt())) + .append(" ").append(fromInteger(fourthNibble.toInt())) + } else if (opCode.toInt() == 2) { + sb.append("gt ") + sb.append(fromInteger(thirdNibble.toInt())) + .append(" ").append(fromInteger(fourthNibble.toInt())) + } else if (opCode.toInt() == 3) { + sb.append("gte ") + sb.append(fromInteger(thirdNibble.toInt())) + .append(" ").append(fromInteger(fourthNibble.toInt())) + } else if (opCode.toInt() == 4) { + sb.append("lt ") + sb.append(fromInteger(thirdNibble.toInt())) + .append(" ").append(fromInteger(fourthNibble.toInt())) + } else if (opCode.toInt() == 5) { + sb.append("lte ") + sb.append(fromInteger(thirdNibble.toInt())) + .append(" ").append(fromInteger(fourthNibble.toInt())) + } + if (opCode.toInt() == 8) { + sb.append("eqi ") + sb.append(fromInteger(thirdNibble.toInt())) + .append(" ").append(fourthNibble.toInt()) + } else if (opCode.toInt() == 9) { + sb.append("neqi ") + sb.append(fromInteger(thirdNibble.toInt())) + .append(" ").append(fourthNibble.toInt()) + } else if (opCode.toInt() == 10) { + sb.append("gti ") + sb.append(fromInteger(thirdNibble.toInt())) + .append(" ").append(fourthNibble.toInt()) + } else if (opCode.toInt() == 11) { + sb.append("gtei ") + sb.append(fromInteger(thirdNibble.toInt())) + .append(" ").append(fourthNibble.toInt()) + } else if (opCode.toInt() == 12) { + sb.append("lti ") + sb.append(fromInteger(thirdNibble.toInt())) + .append(" ").append(fourthNibble.toInt()) + } else if (opCode.toInt() == 13) { + sb.append("ltei ") + sb.append(fromInteger(thirdNibble.toInt())) + .append(" ").append(fourthNibble.toInt()) + } + return sb.toString() + } + return null + } + } +} diff --git a/src/jvmMain/kotlin/nl/astraeus/tmpl/db/Database.kt b/src/jvmMain/kotlin/mtmc/db/Database.kt similarity index 98% rename from src/jvmMain/kotlin/nl/astraeus/tmpl/db/Database.kt rename to src/jvmMain/kotlin/mtmc/db/Database.kt index 9df850a..d29586c 100644 --- a/src/jvmMain/kotlin/nl/astraeus/tmpl/db/Database.kt +++ b/src/jvmMain/kotlin/mtmc/db/Database.kt @@ -1,4 +1,4 @@ -package nl.astraeus.tmpl.db +package mtmc.db import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariDataSource diff --git a/src/jvmMain/kotlin/nl/astraeus/tmpl/db/Migrations.kt b/src/jvmMain/kotlin/mtmc/db/Migrations.kt similarity index 97% rename from src/jvmMain/kotlin/nl/astraeus/tmpl/db/Migrations.kt rename to src/jvmMain/kotlin/mtmc/db/Migrations.kt index 60d075b..7a70d22 100644 --- a/src/jvmMain/kotlin/nl/astraeus/tmpl/db/Migrations.kt +++ b/src/jvmMain/kotlin/mtmc/db/Migrations.kt @@ -1,6 +1,6 @@ -package nl.astraeus.tmpl.db +package mtmc.db -import nl.astraeus.tmpl.log +import mtmc.log import java.sql.Connection import java.sql.SQLException import java.sql.Timestamp diff --git a/src/jvmMain/kotlin/mtmc/emulator/DebugInfo.kt b/src/jvmMain/kotlin/mtmc/emulator/DebugInfo.kt new file mode 100644 index 0000000..e40ec01 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/emulator/DebugInfo.kt @@ -0,0 +1,45 @@ +package mtmc.emulator + +import java.util.* +import java.util.regex.Pattern + +@JvmRecord +data class DebugInfo( + val debugStrings: MutableList, + val assemblyFile: String?, + val assemblySource: String, + val assemblyLineNumbers: IntArray, + val originalFile: String, + val originalLineNumbers: IntArray, + val globals: Array, + val locals: Array> +) { + fun handleDebugString(debugIndex: Short, monTanaMiniComputer: MonTanaMiniComputer) { + val debugString = debugStrings!!.get(debugIndex.toInt()) + val compile = Pattern.compile("(\\$[a-zA-Z][a-zA-Z0-9])") + val matcher = compile.matcher(debugString) + val formattedString = StringBuilder() + var start = 0 + var end: Int + while (matcher.find()) { + val match = matcher.group().substring(1) + try { + end = matcher.start() + formattedString.append(debugString, start, end) + val register = Register.valueOf(match.uppercase(Locale.getDefault())) + formattedString.append(monTanaMiniComputer.getRegisterValue(register).toInt()) + start = matcher.end() + } catch (e: Exception) { + formattedString.append(match) + } + } + formattedString.append(debugString.substring(start)) + println("DEBUG[" + System.nanoTime() + "] : " + formattedString) + } + + @JvmRecord + data class GlobalInfo(val name: String?, val location: Int, val type: String?) + + @JvmRecord + data class LocalInfo(val name: String?, val offset: Int, val type: String?) +} \ No newline at end of file diff --git a/src/jvmMain/kotlin/mtmc/emulator/MTMCClock.kt b/src/jvmMain/kotlin/mtmc/emulator/MTMCClock.kt new file mode 100644 index 0000000..d440874 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/emulator/MTMCClock.kt @@ -0,0 +1,71 @@ +package mtmc.emulator + +import mtmc.emulator.MonTanaMiniComputer.ComputerStatus +import kotlin.math.max + +/** + * + * @author jbanes + */ +class MTMCClock + (private val computer: MonTanaMiniComputer) { + fun run() { + var instructions: Long = 0 + var ips: Long = 0 + var expected: Long = 0 + var virtual: Long = 0 + + var startTime = System.currentTimeMillis() + var deltaStart: Long + var delta: Long + + var speed: Long = 0 + var pulse: Long + var ms: Long = 10 + + while (computer.getStatus() == ComputerStatus.EXECUTING) { + speed = max(computer.speed, 0).toLong() + pulse = (if (speed <= 0) 1000000 else max(speed / 100, 1)) + ms = (if (pulse < 10) 1000 / speed else 10) + + deltaStart = System.currentTimeMillis() + delta = ms - (System.currentTimeMillis() - deltaStart) + + + /* We've lost more than a second. Recalibrate. */ + if ((expected - virtual) > pulse * 100) { + startTime = deltaStart + virtual = 0 + } + + + /* Throttles to every 10ms, but "catches up" if we're behind */ + if (delta > 0 && (expected - virtual) < pulse && speed != 0L) { + try { + Thread.sleep(delta) + } catch (e: InterruptedException) { + } + } + + instructions += computer.pulse(pulse) + + virtual += pulse + ips = (virtual * 1000) / max(1, System.currentTimeMillis() - startTime) + expected = (System.currentTimeMillis() - startTime) * speed / 1000 + } + + System.err.println("Executed " + instructions + " instructions at a rate of " + ips + " ips (speed = " + speed + ")") + } + + fun step() { + computer.fetchAndExecute() + computer.fetchCurrentInstruction() + computer.notifyOfStepExecution() + } + + fun back() { + computer.rewind() + computer.fetchCurrentInstruction() + computer.notifyOfStepExecution() + } +} diff --git a/src/jvmMain/kotlin/mtmc/emulator/MTMCConsole.kt b/src/jvmMain/kotlin/mtmc/emulator/MTMCConsole.kt new file mode 100644 index 0000000..75fb03f --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/emulator/MTMCConsole.kt @@ -0,0 +1,131 @@ +package mtmc.emulator + +import mtmc.os.shell.Shell +import mtmc.tokenizer.MTMCScanner +import mtmc.tokenizer.MTMCToken +import java.io.Console + +class MTMCConsole(private val computer: MonTanaMiniComputer) { + var mode: Mode = Mode.NON_INTERACTIVE + var sysConsole: Console? = null + + // non-interactive data + private val output = StringBuffer() + private var shortValueSet = false + private var shortValue: Short = 0 + private var stringValue: String? = null + + // TODO invert so shell is driving and console is just IO + fun start() { + mode = Mode.INTERACTIVE + sysConsole = System.console() + Shell.printShellWelcome(computer) + while (true) { + val cmd = sysConsole!!.readLine("mtmc > ") + computer.oS.processCommand(cmd) + } + } + + fun println(x: String) { + print(x) + print("\n") + } + + fun print(x: String) { + output.append(x) + if (mode == Mode.INTERACTIVE) { + kotlin.io.print(x) + } else { + if (x.contains("\n")) { + computer.notifyOfConsoleUpdate() + } else { + computer.notifyOfConsolePrinting() + } + } + } + + fun readChar(): Char { + if (mode == Mode.INTERACTIVE) { + val tokens = MTMCScanner(sysConsole!!.readLine(), "#").tokenize() + val token = tokens.first() + assert(token.type === MTMCToken.TokenType.CHAR) + return token.charValue() + } else { + this.shortValueSet = false + return Char(this.shortValue.toUShort()) + } + } + + fun readInt(): Short { + if (mode == Mode.INTERACTIVE) { + return sysConsole!!.readLine().toShort() + } else { + this.shortValueSet = false + return shortValue + } + } + + fun hasShortValue(): Boolean { + return (mode == Mode.INTERACTIVE || shortValueSet) + } + + fun setShortValue(shortValue: Short) { + this.shortValue = shortValue + this.shortValueSet = true + } + + fun setCharValue(charValue: Char) { + this.shortValue = charValue.code.toShort() + this.shortValueSet = true + } + + fun getOutput(): String { + return output.toString() + } + + fun consumeLines(): String { + val index = output.lastIndexOf("\n") + val text = if (index >= 0) output.substring(0, index + 1) else "" + + if (index >= 0) { + output.delete(0, index + 1) + } + + return text + } + + fun writeInt(value: Short) { + print(value.toString() + "") + } + + fun setStringValue(stringValue: String?) { + this.stringValue = stringValue + } + + fun readString(): String? { + if (mode == Mode.INTERACTIVE) { + return sysConsole!!.readLine() + } else { + val stringValue = this.stringValue + this.stringValue = null + return stringValue + } + } + + fun hasReadString(): Boolean { + return (mode == Mode.INTERACTIVE || stringValue != null) + } + + fun setReadString(stringValue: String?) { + this.stringValue = stringValue + } + + fun resetOutput() { + output.delete(0, output.length) + } + + enum class Mode { + NON_INTERACTIVE, + INTERACTIVE, + } +} diff --git a/src/jvmMain/kotlin/mtmc/emulator/MTMCDisplay.kt b/src/jvmMain/kotlin/mtmc/emulator/MTMCDisplay.kt new file mode 100644 index 0000000..70d984d --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/emulator/MTMCDisplay.kt @@ -0,0 +1,284 @@ +package mtmc.emulator + +import java.awt.Color +import java.awt.Dimension +import java.awt.RenderingHints +import java.awt.image.BufferedImage +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.util.* +import javax.imageio.ImageIO +import kotlin.math.min + +class MTMCDisplay(private val computer: MonTanaMiniComputer) { + private val buffer = BufferedImage( + COLS, + ROWS, + BufferedImage.TYPE_INT_ARGB + ) + private var currentColor: DisplayColor? = null + private var graphics: Array = arrayOf() + private var byteArray: ByteArray = ByteArray(0) + + fun setColor(registerValue: Short) { + currentColor = DisplayColor.entries[min(registerValue.toInt(), 3)] + } + + enum class DisplayColor(val r: Int, val g: Int, val b: Int) { + DARK(42, 69, 59), + MEDIUM(54, 93, 72), + LIGHT(87, 124, 68), + LIGHTEST(127, 134, 15); + + val intVal: Int + val javaColor: Color + + init { + this.intVal = 0xFF shl 24 or (r shl 16) or (g shl 8) or b + javaColor = Color(r, g, b) + } + + fun distance(r: Int, g: Int, b: Int): Int { + val dr = this.r - r + val dg = this.g - g + val db = this.b - b + val square = dr * dr + dg * dg + db * db + return square + } + + companion object { + fun indexFromInt(value: Int): Short { + if (value == LIGHTEST.intVal) { + return 3 + } else if (value == LIGHT.intVal) { + return 2 + } else if (value == MEDIUM.intVal) { + return 1 + } else { + return 0 + } + } + } + } + + init { + reset() + loadSplashScreen() + sync() + } + + private fun loadSplashScreen() { + try { + val bytes: ByteArray = Base64.getDecoder().decode(SPLASH_SCREEN) + val bais = ByteArrayInputStream(bytes) + var img: BufferedImage? = null + img = ImageIO.read(bais) + loadScaledImage(img) + } catch (e: IOException) { + e.printStackTrace() + } + } + + private fun loadImage(data: ByteArray): BufferedImage? { + try { + return ImageIO.read(ByteArrayInputStream(data)) + } catch (e: IOException) { + e.printStackTrace() + throw IllegalStateException(e) + } + } + + fun loadGraphics(data: Array) { + if (data == null) { + graphics = arrayOf() + return + } + + graphics = Array(data.size) { index -> + loadImage(data[index]) ?: BufferedImage(160, 144, BufferedImage.TYPE_INT_RGB) + } + } + + fun reset() { + currentColor = DisplayColor.DARK + for (col in 0..= 0 && index < graphics!!.size) + } + + fun setPixel(col: Int, row: Int, value: Int) { + val color = DisplayColor.entries[value] + setPixel(col, row, color) + } + + fun setPixel(col: Int, row: Int, color: DisplayColor) { + buffer.setRGB(col, row, color.intVal) + } + + fun getPixel(col: Int, row: Int): Short { + val rgb = buffer.getRGB(col, row) + return DisplayColor.Companion.indexFromInt(rgb) + } + + fun drawLine(startCol: Short, startRow: Short, endCol: Short, endRow: Short) { + val graphics = buffer.getGraphics() + graphics.setColor(currentColor!!.javaColor) + graphics.drawLine(startCol.toInt(), startRow.toInt(), endCol.toInt(), endRow.toInt()) + graphics.dispose() + } + + fun drawRectangle(startCol: Short, startRow: Short, width: Short, height: Short) { + val graphics = buffer.getGraphics() + graphics.setColor(currentColor!!.javaColor) + graphics.fillRect(startCol.toInt(), startRow.toInt(), width.toInt(), height.toInt()) + graphics.dispose() + } + + fun drawImage(image: Int, x: Int, y: Int) { + val graphic = graphics!![image] + val graphics = buffer.getGraphics() + graphics.drawImage(graphic, x, y, null) + graphics.dispose() + } + + fun drawImage(image: Int, x: Int, y: Int, width: Int, height: Int) { + val graphic = graphics!![image] + val graphics = buffer.getGraphics() + graphics.drawImage(graphic, x, y, width, height, null) + graphics.dispose() + } + + fun drawImage( + image: Int, + sx: Int, + sy: Int, + sw: Int, + sh: Int, + dx: Int, + dy: Int, + dw: Int, + dh: Int + ) { + val graphic = graphics!![image] + val graphics = buffer.getGraphics() + graphics.drawImage(graphic, dx, dy, dx + dw, dy + dh, sx, sy, sx + sw, sy + sh, null) + graphics.dispose() + } + + fun sync() { + val baos = ByteArrayOutputStream() + try { + buffer.flush() + ImageIO.write(buffer, "png", baos) + byteArray = baos.toByteArray() + } catch (e: IOException) { + throw RuntimeException(e) + } + computer.notifyOfDisplayUpdate() + } + + fun toPng(): ByteArray? { + return byteArray + } + + //============================================= + // utilities + //============================================= + fun loadScaledImage(img: BufferedImage) { + val scaleDimensions: Dimension = getScaledDimension(img, COLS, ROWS) + val scaledImage: BufferedImage = scaleImage(img, scaleDimensions) + val xpad: Int = (COLS - scaledImage.getWidth()) / 2 + val ypad: Int = (ROWS - scaledImage.getHeight()) / 2 + for (x in 0.. 0xFF / 2) { + val displayColor: DisplayColor = findClosestColor(rgb) + setPixel((x + xpad).toShort().toInt(), (y + ypad).toShort().toInt(), displayColor) + } + } + } + } + + companion object { + const val ROWS: Int = 144 + const val COLS: Int = 160 + const val SPLASH_SCREEN: String = + "iVBORw0KGgoAAAANSUhEUgAAAKAAAACQCAIAAAAA1/fXAAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAIwdJREFUeJztXXlYFEfe3u+PqDFmzbqb7GaziSee8b6j8Y4aozGJMep0g4CooKiIIqcgKqioKKIo3iIKyileoOKN4gHeB4ioKCKIinIfQ753poeamp6eYZCRIcP08z4+WF3TXV1v1e+q62/1mrEG6DH+pvMSGPBBYSBYz2EgWM9hIFjPYSBYz2EgWM9hILhqiE98sHrTIZ0XQ3MYCK4afLYeFYvFZ+Pv6bwkGsJAcNXg4rXvT+n14mXODxM9dV6eSmEguGrYf/Ai2I05fb2wqORNTp7Oy1MpDARXDTnv8sXi8h8meljM88/NK8zKfqvzIqmHgeAqwHvzYXTfxFuPGrc1r9+ctV0UALL3RsTpvGBqYCBYU0yw8oFYLisTM9a+XEqTDhZ3k58VFBbrvGxqYCBYI8xdFFBcXAp2p9ltRt8l6eMsvNGnF60O1XkJVcFAcOXYsvdkfkFxXn6R/dI99Zsb07c+bmny5Fn29TuPdV5IVTAQrA5T7TY/fPwCfTTl0Yu+YxYK5tkdevZdXqHOi6oKBoKF0az3rAtXk8Xl5UXFJct9I//XfYaqnK4r95eXl+u8wKpgIFgAJ+PuoNeC2qhjV3v+6KQ+s7XTdmTWeZlVwUCwAsKOXIauzc0vDAw/12Ok48ctTASyNVX4L+wvA8F/Adi4BTx9/gpUXb6W8pPJCtpUBuo3Y416sL+MZeZMYeymM98Nlt9a5htpENG1HUkPn4Pa5NSM0SYrYBjTt77qyE43YXYtFcVuEJ2owNZFIpLhSOy1N29rb8yyrhPstDz41ZvcgsJi3+3RRv1s6Fugdqoxs8dTziuBpy3D5WlkZJqV/fbG3Sc6/xBVqNMEx5y+Aen6IDWj1yhnnoNrPJE5sFqAWg5DhssInjLPH13f3mOvzr9FFeouwY/SssTi8sCwc//takmnd+jNblzIqKIWCF0p+mdbSc6GrSZfuJpUWlqm829Rg7pIMLTsi6ycwsLipT7hdMdt0JwdP445tFYltRyWzpV13z6jXUpKy46cvKbzL1KDOkcwGM1+/e7tu4JfzFbRXtCX37L2lszx9TIW8ccaB2axjTyFALY09xOfrUchn0UzfXX+UWpQ5wjOyHwDdi3m+9OJELkr7eRiGaTOMGXa9mRDvfjs4lb73pKfNOlgcfv+UxAcdyVpodd+nX+XKtQtgu89SC8uLh0/bQ2d+E1ndu8yOYW7PURQw41asoEeAvI5YrXoi/aSX33Radq0BVv2hJ9PS8/mJvHgj1W1bz6e/hM8wcpnY8DxiwnJL1+9Aw2bA080pDzdJm3YtQ7yvrvJVfRtH0n6yFHCdtb+FaLGreUPb9DCuEWf2Yy1L5hOz3iN5+MtgWHndf7V+k/whp0xN+89yS8o5rpXbl5h2OFL1s7bP21tSvI0bMmuXMDQfffz9rJbPo7CBENo/6ON8BvRpyda+Vy+loLXwbQ+d+m+zitBDwlety36bvKz8nIJqW9zC6KOJTgtC+47ZmHjNma8GDIw7lc5iyFeok59ZemftmKj1wmb0EfXiVp0k+QZMpzt1p/9pCX/mejTPzLLDh5LgIddXFK6L+qigWAtYKbT9vspknAjqvV55psD0Vdg3DYymqzmJy27sfspG+r33yS28UfN2P5D2ZZd2ZN+Kt2kSeMlOdc5SRoHfCq76UzvgSy/WzdlR4qWXb6egvK8yyuc4bjNQPD74IdJnuFHL+e8zUc9pjx+sS3o1IhJnv/pMl0wM6+3uc5S6L7/aidJ/Kgpu2oB4+eizg9GZhjSzjMVZHjwcpGdJdOlH9uwhfwVjduas7PWP3j0QiwuP385yUBwFSCy9r10LaWsTIxeiz/mue/+pqe1shAGPm7B9hkkGTD4YxxDEj9rzR7xkdOz2l5+Cz1SfaADgNWNbMou8lEfkYs107GvQgHafD8X6h8cZ2a/RYs0EFwJPNZFZr7MAa+wWtdsPtx1uINgNti6w0Yy7nOYyIqQ8hIbOYs/jFRgEfKWjA+CnqMqFLCGiN0gCZJ0/k4yyMg9E3/8OmX1k2cvIWm27DlpIFgYtu67Ie5gPz18krly48E2A2xh0fDyoL+26s6aTGK2LhLF+CrUO5j+1EiWbZqJAsEH14iMeshuNWguGUTi/fY9cMBbhOdwUWsO3Uc6Xr/zGFIn4ugVA8EKcPAM4uIJL7JyLO23wiFRzgNi+g6C7SOiZS8PY36WdWIHK74c9ndljLrLn4YuXn2OOWHevb/8sf/sYLEv6gI+5NiZmwaCJZhg5XPlxkOO2kWrQz/vKEAtvJpRPzFgqNLqDlom+ncHyU8EFW20r0RWe9gyELAwmqrPLgc0OEiUhhUmXpP2U4Ii4vBF24NP13WCgw9cgNIqKCzeuf/M1z1mClI7YBgjGFNUhQ0ukkgFO6Hy1qAhwlaJUIDQlRILS002t1lMkwpx/XFLk4SbqZDV6NB1lGAXr32v3uSWlJQGRcYJTkgGtT+PlfTa41WnBI6Qqkhk9DrRsSpK5ihv0ZZFIntL5tdfmKEjGHjJi22YICEBsHGh6PN2svL3+NGpsKgk7sP7TrWRYOgniZH84rWV47ZPWgkEK2DowjyuTrfb4ynimcqbXEXwjLe5M+o7ohrEShS5yGQi801nyeDjb78ya5XinW6z5bJ6656TRcWlf1j61CGCF68JKyktQ9NevyP6845TlTOg7pxnMseUHND3BrSy6SRm4DB28RztWFUc0z6OzIChDEz65l0lw8x0gQf+IDP0IJkgpaNPXa8rBB88noAPTn2SyVqv501trCeNQ8Gv3V0VdaseIV4iq8kSy3nieCZ8ldYeSwDbCrb6150lsa3+QyVuG5cOvrkv+rzTtKSHz8Vi8f6D8XpOsPHsDdmvJWN5oYfi/91ZIND4RXt2xXyByNH7Ac+ByQNzulkX1t9N+9QCgZ6isT8z6L7/7Sj7hL8bsUHSUWfY5yRw3W6AbVTMVXz4/ZTnekuwh08EOm5uXqGN6y7llQTwbgcP12YPQ8cdOIyp34yFqBQ0hbQFNKOpxkwDagL99Iroypwp8pgajAxv/8MwJ1++eqeHBEdEX0H7ffDoRf9f3JTvNm7NztcgMqw5YPVAGNRvzvYbwmpRkavhGP4YFwStL532RWb0QfHTgx/z3HeD44ysHL0i+H5KOvouZJSgj9upLwujVFt1Df9nmgnzqZGkomHfVjp1UoscL5zFDB7O2lgw/2rHDhvBHK54NVobbEbuYxu0MJ5o5ZOXX5SWnq0nBKc+yZJE3gNj/97GTPlur4FsiNKEt+rUspWpxKbFkzt/x0atqSF2CWKlo8uwuaB9pzByY8LXSdS0i/yrHT2DIM+On9VmFFMHBE+125JfUAyY2WwUHC2A6tJEfsJaoSfLqcHoMTJd2K4Xe7DG2eU1tX3LRfR0kf1eIthi5POdlweLy8ujjiX8VQm2dt4Bat++KzC33aR8FyLUdioTXZk/esBbBG/YfTZzuLKgBBrKAksZu9B5JPIAHwZ/65Zsgm3u8n78Wbsppy7cgebqOkJ4GLRWE2zvsbe0rAweUf+xAiYV/EX4QpVWxzonybRWDY2vhdZMo1YVbctc9pPV9kzvgcKrynQFX2fm04pydh/pmPny7fPMN38xgp1XBJeJxU+eZfce7aJ89/P2rK9zJbUA82TcrxLCXKw1YhcyHIYr93yjHhILVjKwM1HiAXvYatM41wRntk27cWT1w0shT28de37vTNqNo0nnAi7tczzhJysJZJK8JyzdA2Xs4Bn0lyF4lsuOklL03dwuw+2V76LGN1U22OfvJpnOCGE7w1QjbiCcB/2gMBFn/wpRjwESH2n2lBpl93zAbNApLiv5U+Aqz067uWCWFT7Kx5HpNaCiuXecml9QdO22FjbvqQmCZzhtLy0TJ6dmdBPSK//pwG5WG06K8RXNNJNNjTBnNOUGjJJXdO3PLp/H/K+T5O/vBld3Rg7ByU0mZ7dbxu22iQ92uBziAsQH28ftnoPOGuvHcnkSD3gUF+QIUSu/bt9/CkexvnSmGClzwq1USLu/AMHNes/KzSvMeZc/bKKH8l24hmsc1HEGsWw6SUYVPBwNuUGbaEa5Hz/+xHBWDLyU6kezL+yZd/u435PrR14+Tsx/nV5anF9eLkZflM7ZFZcU5eZmP8lMiU+5GHzjqHdR3mv17HLXTKftdLU0MpoMaaeVdeUfnGA476/e5MJwUL7VuHUl7EasEvX4Xpa5VXc2UvWKbB4g8egXNaiYyvrewvmkvyk0aPrdU8X5bzQhrKpXQMhZUtqGrSYvXRuORNdVIbWdYAif4uLSyXP8lG/BZoatpGb8IMRL1HeQLDOcY7dZmnKDZvFVR4HCfN2ZjfKuMrVnd1glxwXmZCSJy0o1oQqmRkFhMb66nFteodnl7h1a0RaN3VaFwJN8kJqhFQo+IMHcWNhUxYWaBBasOsKO+oja9ZJnHvVTFcaAHWYwgm9cqJntTQMytiDnRaX0FJeUhh2+NN1+y8Df3DsMmt+6n027Abbf/ew62cYvMOxcSUklLeP0hbtfdpHtMtBp2IL8wuJHaVnaYuFDERx25BKKvjHguHKsqp507Z6amTGR3qJeA+UkQXFWaTSp3xABgtGnD1S9+57YMOnM1qkZ98+JxcIk3U1+Nsd1l+AQJ0Hz3rM810W+yytU/vmd5Gf2HnvJrJUGLUwio69IZJ7NxlpNsMMySUz14PEEwUllRt3ViUrYR6NGyxmCbVmlCXJbF4noqcgEY8dWsfv6MReD7B4nRpUU5gpS+za3wMsv6hPl5U9NAYEWtnVPLPfDFy9zDh1PXLkxaugfSxso7vyyQOr+RkRrc9a09gkGqfj45IcZ/+1mpXwXLu/OJfJ65Dafil4nWcUF3Qm9O3G8whgqep6fi8SFRSdGs4AVDVkdq5oYy8kClYsH0gv41SN2o3Fi1LKXjxJUeK6yCz2PHr3+pudMxtrXd3t0xNHLaNk7951GhoHj3P/RfgqXYXvQqT8l2yOW/mK+SrmEX3Sattg7DKI+5fEL7dKhfYKfpGfDKYI2ErxrMpHxW8h4zJXsFwc1PH6cZIJjvyFs135s6x6SKVe8LeYatpRM54AL27I7+21ftvdAdshwBt2RmcBYmzFOMxgvO2aru4R+2GuxfqLBwwUIbtGNDVupEbvnA2Y/vRmjiT2VmpYFD5B7vmimL4Stcp6SkrJTF+40lEpgKGmkrFh/gDcbqWFLk7FmK0+cuwWT7PHTl1qnQ8sE74u6KBaXW9pvVZXhI6H1YdUHHtuolaS7k4gujd9/q7z7nvI3Sz4fUFZaXCm15Oo7xoV7flb2O1V5YA/Xk+78cvlaivOKYC4/JLPFfP/l6w/EnL4BH/JP6fr0TbtPfIia0SbB7Kz1cBIOxFz9rK35hyjre2OBZSUEn9tlnZkSXy4u05zd0tIyLjD3dY+ZajyitPTsj5oyf29j9pPxCm7riE9bm8ERQjfAj96+K7iT9BSC/cN9uzYJznyZ8zzzjeB0V93CT+3GZpf2O2kYb6KvjKw3LfvOxsPHTPZSk+3cJYUjtJq0twg9FA9uE26m1sy3a41geET4HrO5ldv3aNGftjZt1tu6z+iFY0y9zG038ba90TKashGqvayEyCWVxooFL4hcrik7eOxVkw3GM1cMSDUrh63PMl6D3djzt2uGXa0R3HW4Q15+0ZHYa/UV0xu3NYcl0n2k469TVs922bHG/zC0DkyJYkXfXzDUpS3ATFPF7ulddjdvJ8Po9d0W7e4dard0z6LVIdv2nuTm8Kq/oo5d5cylzYEn1GSzdt7R6ydn15X7uT1+oHG1EoCsaYLvJD17/Sa331hXMD3Rygffszv0bHzCA5iaUDNisWQdfplY/C634OnzV7eTnp6Mu7M3Iu7IyWtIT7iV2lBpmnsjo8mfd5zWtJd124G2aB9Dfl8MSSiauW7KvE2oMlv33Q6eQc7L9xE4LgtG4o/scuWyNWnLxipuuHHYR+Rtz4wYxbToacFbGvObhTe3k3+lBAeEyqLHsCvVZAOv0NZlZeKkh8891kXUJLVaI9jCbjO+5M3b/Nv3nxYVyX3H4uLSR2lZx8/e3BwY67gsiLFez/vh4ROJMMpGm6wAne0GzRvFLre034Ja2BZ0CreuXH+Ivv4ut5Dbp0GTK/zIZeXi/VORYHjhg35Q2EmDBuzbzsMWuHjtu/tAwO2hr70R5zmPztv/sJpshUUlaAqC85P+MgS/zsmTfElhyfXbj/eEn3daHvz9b4s0+eGfUi8CZiTcf1Ij5VI5dvNeWsyZm6iaNVuOwOac6xYw1W4z3E1ICOXoGEQlbuW8zT8ppNsaG8kIjvaVbJDToLlGHwWmx031vnb7kSrmLl5N5koCK/r6nce8u+kZrz3XRdy6n4YWrytqtUPwvztP9/I7OGnGuqr+cFfImT+lI6hoH6Az5FD84rXhk23eXxk/efYSbeUjpTAh+tnRdZIo2PAfVbPbVDjE2KLv7PU7YgRFSF5BEZms/3XPmXZLAmEeHzqR6B94wmKev1F/GzwtXBqQ/2sT/N5AD9DKnCOCqzdS8wuLlcc2PmoqCY4OGyk8xARjHhYDiIHqhUuzY99p2BB0DLJ+c2Mbt10FhQIBEBiVfOuBDuM0ZZBB58c56H5tkrawN1KyL0L7gfOUb3Xow4+AAm0H2G7ZE1soNRrKpcqisMKASE3LdF4eTM/IhxItKeGHQUDe3vDzHQbPh9hAM8K/X3SaBmG2YUfMZ+3M8fPkhxkfYjVKHSXY3lPij2rocfUa5Zz9Ohcm3tn4e7zpMjDxODfpfkp6n4oJoHD/fLdHCypj0AxdG336xtUbD9EIct7lD5CaIEP/WAL7eU+Ejjcm1R+CAbjXASFnldcW8zDOwjsr+y261xhTL1V5VvhFoUOjEdi47uKce5j68AhU2VycGLiUmMIp5s/aTYFBABdR53WiVwRfvZkK85V3eAoPX3a1fPAoAx55pU8zn7cJvh8sfOJefzt4PlqGKoLRX1v3l73aZYXkJHh6ppWuoFcEcwPm8xcHqsmzPfg0XBc4XRo+U+KL5xVOqNhJo3nvWcfP3hJgt0x88HgCOq4kW1M2PiEZzp7OK6SenhEMoKJhnwtu3QJ0HGIHMX7i3C3NHwiBDxM6NS2rXYX59mUXS7QkuMjcApNrtx757z4x1mxl44oxtK97zECbiE98oPPaqKd/BEefugGOBWdN1JPuJoDexs7aUKVnznDcBn388PELbviIAxwkSPsvOk/nOWaNjEyPnryOt4yZrFLB1yT0jeCvus/Iyy+6n/JcOb4NNwa96v0MH7ule8rKxPcePOs8TGDpjQxN2UHj3GPP3wa7MadraKfCSqFvBNercIiVBy4/MZoMYQvj9v0eu9QnHAZXcXHpYu8wWFuftTVH363f3Bj+Lnr2uKneh44nSpVx2aETiTqvBAI9JBiAgZOWni3ZQZpKhO8LAiKrMWcR4vcKnF3pTl6P0rJu3396615a6pPM3LxCeMPwnoMPXND5t/OgnwRz5nRQZBwddPx96hokVn/qk9ncTXvCz1+/8/j5i9fwmkBz6KFLeKPOv1oQ+kkwcDj2mlhcbkUdlmBuuwkEe/pG6rxsNQm9JRhIf/H6WcaroX8s4f5rMnsDCHZbFarzgtUk9JlgANoxI/NN014SZTyKXQ6Cl68/oPNS1ST0nOBZLjth98ZdSWrVz6ZN/7kg2G/XMZ2Xqiah5wTXk570AGV84UrSl10s4SLvP6jjk6pqGPpPcD3JLsXh+QXFCTdTn2e+uZiQrPPy1CTqBMGAu3cYN7lTi0tv/xKoKwQD0+235hcUFRQW67wkNYk6RDBgZrsp523+V91m6LwkNYa6RTCH2Qt36rwMNYa6SHCdgoFgPYeBYD2HgWA9h4FgPYeBYD2HgWA9h4FgPccHJHjQ74st7bc6r9jHzlr/dU/+wTkjmWUOnkFL1oYrLwz/X4+ZwyZ6aIJveslnXf2ni+UEK58FHntnu+5U3rq4eZ/ZZnM3Llod6rYqpPX3c9UUu35zY/zc3HaT0/JgFA8/Gfz74vrNBTZkVIVW/WwES9vrJ2flzA1aGP8wyXPKPH9U1Pu9TgcEg84jsdfoif8FhcUzKmbPDBznfj8lnb5770E6TZWXX5SaJUD0NfSPpdxPjGdv4DacItelaylNKzYqW7nxIO+Hqlau/mG5Njk1Q/lFaenZphrvH7lXev6z8rVl70leTtFM34ePM5VzPst4pa1NAbRPcOO25jfuPlHFxxjTlYK3du4/Q54AbgTz8K6i4tKGLSUrGMZPXyuY4cJVycggN1FZ+fp2iB2v5Oj66t+IF2lSAy+yhLft4S2Tt5PODFRzobXVRoJVLbPEtTvsXOZL4Y/PzS/kft6kvYWGOy1zGzZ81c0qL79IVR7lvksuN8XdbkYZr6j0jWBOcPNcGh0Gzxf8LT7qi07y8+l/s/Cu9HWoK64F1yKC/9vVSnAxvCbXf6R7Jv9sJtzFlS9uP6JlvpHv97qtQQoC88qNh5r8qo/QceQ0ZjptF/xh4q1HJA9ayV2hvS2VL8EjHXVJMO/zOKG0ZrPCPjTpGa+hHaF0HzxS0HbcinpYQ8QqgfVB76h1IOYqbbOgMSE/tCPJcPNeGnpJ24G2PDkBpY6cc1x30Ynrth0lxe40bAF9CzYBUj5uaQI7kWcuwDZUXwN4O1c85Cwtk28KwJWBw/e/LqKfmfTwOfc6mFe8Ohld7QVOWiY4VLqnKnddvyM7FabfWDe60DAXufRdIWdJYobQOVB9xyykf2iitHq/3cB5dAai5CKlh5pyFyQK13TQqujMcxcFkOfYuAXQt7oOl58OM23BFvoWuNGwKtA4VLUMaAf6Vl9KKvB6CCivXQS/eZtPCrfa/xCXONnGjy40ORgrPvEBSRQ8rc/Fax/9Q1qHcbB2VqiOL7vKNsa/nfSUJJKzHiEPVFF16EQiSeft6Wvrvpv+Fdn/uVIskR6swV2wBz9tbUpunbl4V7ohgOTitewFihsjqt9LvqYJ5nU4Il62B58miWgBXOJn7cxpYwofpvzA0xfukAyCZ8xEHJX3VCIwPu84jS6G4zKZR+RJaWvUONnpAYYMbaZtCz5Fv4KWSVCcmtcGbHjyQ3oDLzBNi27eJh70F8GDqj4p2iTYWbpvAbenFL6hkZGszT59LleTqC8ukWdG9vjRifc0XkVAkSu/kRYY3hUZjKUrGMhFwgu0wIg+dZ08BH45nR++Kf2KkEPxZ+LvcXBaHqxhVaCj04UnWqmeNMJDv47n76J+yOsWrdbCIgxtEnyS8jjPVmyj236QgttAwh1+u46RxNc5ecpP+8lEwW/Bf3kZvvvZlc5ABAZcapKoSmDYUWvFFq8Jo59TfalYT6n50lqWF8b5n9Dh2FqE1ghGf6V3JCStb5bLDvp72gyw5dJp61RwMjpte6M30DqMA62haYFB75OC/scl/j5tDV0MWmCcv5JE0rVy2Fg9xeaLRkaHHml/LFlLhyPVBME/Slf+kAtyj0unlQpkNZf4VTcrOrPgEQDweUiGU3F3lDPQGpoIDF6cwcpBJjA27jpOEmmBwZOlgorgPQDPhzwz7Mgl+nW0IPlA2/h/EIJXbTpEyg2bhZgwtP1CgrEW8/1pGlr0ncN72lfdZ9AZ0Fl5GdBfaWKIwOCFG+FVc+l0hHlflFxgjDVfRecfY7qy+lXBKzy90dq4qQqie/z0tU3aW8xbHAiTGyCh9dpIcOKtR6Tch2NlexjwPGDip9Lh+NQnArYiXF76h32V4ke8yCIRGAdirpJEYoWqERjrth0l6YKK4D0wVbq/MrnaDrQlt+g4rlhcjpxZrxQ2H4dTR2+hWFsIhmdCSx74jlz6wpX7SSIyIBuXTqtJ5TGWeooxEJ4O4wAnm2RQJTA2V2ynz6txWmDQHvNZxfMV3hvBBy6QZ6alKxwRS7+upFT4DBCtGM9aJhhdky4iiQRJPXrZRY6h6DbCkc4suBUx3QJoHUZw7bZ8i2YiMHghwAlWPso1TjuXPFnq7h2mldqgnTc64s0TJKou7a6e0g7B6IWkfJkvZfur8tTkyo0HufT5SwLp71GOT3UcqhAZnkFtw8CBF8ogAoMOAdICg65xWmDwQmxEzlcHsM/pZ9JeNc9BV3VBdNe6AX/oUVK+oMg4LnG04nkzJBgbfeo6SRQ8xZwXGVY2wVBrdAYiMCBjSeLVCoHBq3FaYNCKALK9+mNzgL1irJFuvjv2naZvua4KadDC+OueM5f6hNPp2l0epwWCQQBdPrINJO3IwkXm/FQoS9pdJvFqGnRkWDBcB7lHMqgSGCs2yEZvFqiucVoREDlfTdA70vKab8pj+Um1vNMH6DkC2nWOtUAwRChdg6TD0fM6yElB8ATozMrxKV5kmBhKNOghQiIweHNFhos8ufSY0zcEa/zbIXZ0fiLnqwMY4XTzhetIbrXqZ0O/jo69QyDTg+ja3YNACwTT4fgHj2Stj6cmSRSXjvjT4ScC3igbMZQI4HXQGYjA8Nkqd3g0ERg8j7nLcNV7FGoMtCr6mfQxP/DN6Fv0BLw+ioM0Wmlq2iSYNmH8A2UdjmdQCA4RwsZWfho9ysab5sKBN2JKBMat+/LIFxkiHDbRg84M75k8h/aYiZyvJug4M2+IEP2S3OLF3h08g7Te1LRGMJijCzfRSmbC0AaFqoi/m9AZYHFUZPjKjYfKGbizxbmLnLbL80DIpMllKgQGpCKtCPZGxGmlNmHZkWfywqt0NyBDahxoJQKzQIvsaoFgx2Xy1kd3OMEhQl7EX3l2BC8yLLinlaDA4Dk8RABevi6foEkLjP6/uNH5Leb7V78q8e1086XDqz2l22SSiw5ewuaglUhguJbPeKguwSfOybc/J/PKNIn4owMpP403yqY8f52nrojA4EW+uESewKAPDXRVnDSjlTE7XrSHDq/yfCc6eMmzOTQ53bPmCOYNERKjcfZC4Yg/PcZy8LjAHJ0NO2NIBjLtmQaMNZKBFhi0w0OsUN58aVpg0CNRKBX9CjjZZMgd4Kb2aQI62sMLrx47I/edeMHLRatD6UJ+1V3L+4dUi+ARipMTiAlD2y9kLEFRTYpt3AKUH0gPEkMzKWeg5xQQgcGLfJGxhE275eeC0gKDN1dkY8Bx+hW0RnyW8Urz2qC1En2KIk8Iwzqhf0VHc+8kVWFKUE0QTBuNtAkjOETIi/h3GraA9zReZNhOaYNeVQKDNx+WCAx6Cio9qW/Abwoha3pxFIwAegxA830Pec6btbNcy/KG1Ggh/Pc2ZvS5efRM3lpBMG00EhOGZ78QNRkUKR8iFHRLeC2g2whHXoaRKgQGyCOJxK4GzXRmWmBMt1eYCUtPaOX5YJoPD/Pmd7ajDmDjecAkAlNPaVz8ZzMtjEZrjWDeECExYTSJ+AvainQLEPQW6HUoqgQGsat5LNICgxfrDj5wAVIU5TS12ZibX0jSC4tKYKZpWBuQyeSHPC3Li5Xu3H+mQQtjWA94Xc47eZ1AcjRpzz9YVZcET7DyoctNTBgYJiSRDBF2H6kwRCi4dI5uAYKOaYKQwODJWxL5os9t/j9FgcHOWv+nBhdpK5UC9hRdeN7EW15TU3WRE6drC8H4flI4YsLwIv5kvQavFSvbirwxnynz+I6pKoHh7i2fE6mhwPiyq2V+QSULqNCf2gmdcykInpblrXhu/f1csbiS5XTIQC+nqBUE02MjxOfhRfxHVKg32jSFqaz8tEpbAHQ5nYEIjHOX75NEEvniBRaUBQaUn5o1jGC3SuqQnrjyp9AIt6faFXJg9z0OYP6wBPOGCIkJs3bLEZKoKuIvaJrSnuK9BwItYPMeAYEBHSk4RGhfWXMBhoxfIrjWO+5KUlVH/mlXhyywoAEZvnhNGK1x6fy02VVbCNYPoN4hP0dP9oKj5bgsaLKN33c/u36418G26jB4/ljzVbbuux08g2BkaWUOiXrUaYLrAgwE6zkMBOs5DATrOQwE6zkMBOs5/h+bD9K/1kGKGQAAAABJRU5ErkJggg==" + + fun getScaledDimension(image: BufferedImage, widthBound: Int, heightBound: Int): Dimension { + val originalWidth = image.getWidth() + val originalHeight = image.getHeight() + var newWidth = originalWidth + var newHeight = originalHeight + + // first check if we need to scale width + if (originalWidth > widthBound) { + //scale width to fit + newWidth = widthBound + //scale height to maintain aspect ratio + newHeight = (newWidth * originalHeight) / originalWidth + } + + // then check if we need to scale even with the new height + if (newHeight > heightBound) { + //scale height to fit instead + newHeight = heightBound + //scale width to maintain aspect ratio + newWidth = (newHeight * originalWidth) / originalHeight + } + return Dimension(newWidth, newHeight) + } + + fun scaleImage(original: BufferedImage, scaleDimensions: Dimension): BufferedImage { + val resized = BufferedImage(scaleDimensions.width, scaleDimensions.height, original.getType()) + val g = resized.createGraphics() + g.setRenderingHint( + RenderingHints.KEY_INTERPOLATION, + RenderingHints.VALUE_INTERPOLATION_BILINEAR + ) + g.drawImage( + original, 0, 0, scaleDimensions.width, scaleDimensions.height, 0, 0, original.getWidth(), + original.getHeight(), null + ) + g.dispose() + return resized + } + + fun convertImage(image: BufferedImage): BufferedImage { + val arbg = BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_4BYTE_ABGR) + for (x in 0.. 0xFF / 2) { + val displayColor: DisplayColor = findClosestColor(rgb) + arbg.setRGB(x, y, displayColor.intVal) + } + } + } + return arbg + } + + private fun findClosestColor(colorVal: Int): DisplayColor { + val r = colorVal shr 16 and 255 + val g = colorVal shr 8 and 255 + val b = colorVal and 255 + var closest = DisplayColor.DARK + for (color in DisplayColor.entries) { + if (color.distance(r, g, b) < closest.distance(r, g, b)) { + closest = color + } + } + return closest + } + } +} diff --git a/src/jvmMain/kotlin/mtmc/emulator/MTMCIO.kt b/src/jvmMain/kotlin/mtmc/emulator/MTMCIO.kt new file mode 100644 index 0000000..5cbec59 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/emulator/MTMCIO.kt @@ -0,0 +1,28 @@ +package mtmc.emulator + +import java.util.* + +class MTMCIO { + var value: Int = 0 + + internal enum class Buttons(val mask: Int) { + UP(128), + DOWN(64), + LEFT(32), + RIGHT(16), + START(8), + SELECT(4), + B(2), + A(1) + } + + fun keyPressed(key: String) { + val button = Buttons.valueOf(key.uppercase(Locale.getDefault())) + value = value or button.mask + } + + fun keyReleased(key: String) { + val button = Buttons.valueOf(key.uppercase(Locale.getDefault())) + value = value and button.mask.inv() + } +} diff --git a/src/jvmMain/kotlin/mtmc/emulator/MTMCObserver.kt b/src/jvmMain/kotlin/mtmc/emulator/MTMCObserver.kt new file mode 100644 index 0000000..d3fb450 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/emulator/MTMCObserver.kt @@ -0,0 +1,33 @@ +package mtmc.emulator + +interface MTMCObserver { + fun consoleUpdated() + + fun consolePrinting() + + fun executionUpdated() + + fun filesystemUpdated() + + fun registerUpdated(register: Int, value: Int) + + fun memoryUpdated(address: Int, value: Byte) + + fun displayUpdated() + + fun instructionFetched(instruction: Short) + + fun beforeExecution(instruction: Short) + + fun afterExecution(instruction: Short) + + fun stepExecution() + + fun computerReset() + + fun requestCharacter() + + fun requestInteger() + + fun requestString() +} diff --git a/src/jvmMain/kotlin/mtmc/emulator/MonTanaMiniComputer.kt b/src/jvmMain/kotlin/mtmc/emulator/MonTanaMiniComputer.kt new file mode 100644 index 0000000..015d4df --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/emulator/MonTanaMiniComputer.kt @@ -0,0 +1,1030 @@ +package mtmc.emulator + +import mtmc.asm.instructions.Instruction +import mtmc.os.MTOS +import mtmc.os.fs.FileSystem +import mtmc.util.BinaryUtils.getBits +import java.lang.Byte +import java.util.* +import java.util.function.Consumer +import java.util.stream.IntStream +import kotlin.Array +import kotlin.Boolean +import kotlin.ByteArray +import kotlin.Int +import kotlin.IntArray +import kotlin.Long +import kotlin.Short +import kotlin.ShortArray +import kotlin.String +import kotlin.experimental.and +import kotlin.experimental.inv +import kotlin.experimental.or +import kotlin.math.max +import kotlin.math.min + +class MonTanaMiniComputer { + // core model + var registerFile: ShortArray = + ShortArray(Register.entries.size)// 16 user visible + the instruction register + var memory: ByteArray = ByteArray(MEMORY_SIZE) + var breakpoints: ByteArray = ByteArray(MEMORY_SIZE) + private var status: ComputerStatus? = ComputerStatus.READY + var speed: Int = 1000000 + set(speed) { + field = speed + this.notifyOfExecutionUpdate() + } + val iO: MTMCIO = MTMCIO() + + // helpers + var os: MTOS = MTOS(this) + var console: MTMCConsole = MTMCConsole(this) + var display: MTMCDisplay = MTMCDisplay(this) + var clock: MTMCClock = MTMCClock(this) + var fileSystem: FileSystem = FileSystem(this) + var rewindSteps: LinkedList? = LinkedList() + + // listeners + private val observers: MutableList = ArrayList() + var debugInfo: DebugInfo? = null + private var currentRewindStep: RewindStep? = null + + init { + initMemory() + } + + fun initMemory() { + registerFile = ShortArray(Register.entries.size) + memory = ByteArray(MEMORY_SIZE) + breakpoints = ByteArray(MEMORY_SIZE) + rewindSteps = null + setRegisterValue( + Register.SP, + MEMORY_SIZE.toShort().toInt() + ) // default the stack pointer to the top of memory + rewindSteps = LinkedList() + observers!!.forEach(Consumer { obj: MTMCObserver? -> obj!!.computerReset() }) + } + + fun load(code: ByteArray, data: ByteArray, debugInfo: DebugInfo?) { + load(code, data, Array(0) { ByteArray(0) }, debugInfo) + } + + fun load(code: ByteArray, data: ByteArray, graphics: Array, debugInfo: DebugInfo?) { + this.debugInfo = debugInfo + + // reset memory + initMemory() + + + val codeBoundary = code.size + System.arraycopy(code, 0, memory, 0, codeBoundary) + setRegisterValue(Register.CB, codeBoundary - 1) + + val dataBoundary = codeBoundary + data.size + System.arraycopy(data, 0, memory, codeBoundary, data.size) + setRegisterValue(Register.DB, dataBoundary - 1) + + + // base pointer starts just past the end of the data boundary + setRegisterValue(Register.BP, dataBoundary) + + fetchCurrentInstruction() // fetch the initial instruction for display purposes + notifyOfStepExecution() // prepare for step execution + display.loadGraphics(graphics) // ready graphics for use + + // reset computer status + setStatus(ComputerStatus.READY) + } + + fun pulse(instructions: Long): Long { + var count: Long = 0 + + var i: Long = 0 + while (i < instructions && status == ComputerStatus.EXECUTING) { + fetchAndExecute() + count++ + + if (breakpoints[getRegisterValue(Register.PC).toInt()].toInt() != 0) { + setStatus(ComputerStatus.BREAK) + } + i++ + } + + return count + } + + fun run() { + setStatus(ComputerStatus.EXECUTING) + clock.run() + } + + fun back() { + clock.back() + } + + fun step() { + clock.step() + } + + fun setStatus(status: ComputerStatus?) { + this.status = status + this.notifyOfExecutionUpdate() + + if (status == ComputerStatus.FINISHED || status == ComputerStatus.BREAK) { + this.notifyOfStepExecution() + } + } + + fun getStatus(): ComputerStatus? { + return status + } + + fun fetchAndExecute() { + currentRewindStep = RewindStep() + rewindSteps!!.push(currentRewindStep) + fetchCurrentInstruction() + val instruction = getRegisterValue(Register.IR) + if (isDoubleWordInstruction(instruction)) { + setRegisterValue( + Register.PC, + (getRegisterValue(Register.PC) + 2 * WORD_SIZE).toShort().toInt() + ) + } else { + setRegisterValue(Register.PC, (getRegisterValue(Register.PC) + WORD_SIZE).toShort().toInt()) + } + execInstruction(instruction) + } + + fun execInstruction(instruction: Short) { + observers!!.forEach(Consumer { o: MTMCObserver? -> o!!.beforeExecution(instruction) }) + val instructionType: Short = getBits(16, 4, instruction) + if (instructionType.toInt() == 0x0000) { // MISC + val topNibble: Int = getBits(12, 4, instruction).toInt() + when (topNibble) { + 0 -> { + // sys call + val sysCall: Short = getBits(8, 8, instruction) + os.handleSysCall(sysCall) + } + + 1 -> { + // mov + val to: Short = getBits(8, 4, instruction) + val from: Short = getBits(4, 4, instruction) + val value = getRegisterValue(from.toInt()) + setRegisterValue(to.toInt(), value.toInt()) + } + + 2 -> { + // inc + val target: Short = getBits(8, 4, instruction) + val immediateValue: Short = getBits(4, 4, instruction) + val registerValue = getRegisterValue(target.toInt()) + val value = registerValue + immediateValue + setRegisterValue(target.toInt(), value) + } + + 3 -> { + // dec + val target: Short = getBits(8, 4, instruction) + val immediateValue: Short = getBits(4, 4, instruction) + val registerValue = getRegisterValue(target.toInt()) + val value = registerValue - immediateValue + setRegisterValue(target.toInt(), value) + } + + 4 -> { + // dec + val target: Short = getBits(8, 4, instruction) + val immediateValue: Short = getBits(4, 4, instruction) + setRegisterValue(target.toInt(), immediateValue.toInt()) + } + + 5 -> { + // mcp + var source: Short = getBits(8, 4, instruction) + source = getRegisterValue(source.toInt()) + var dest: Short = getBits(4, 4, instruction) + dest = getRegisterValue(dest.toInt()) + val size = getRegisterValue(Register.DR).toInt() + for (i in 0.. { + // debug + val debugIndex: Short = getBits(8, 8, instruction) + debugInfo!!.handleDebugString(debugIndex, this) + } + + 15 -> { + // noop + } + + else -> badInstruction(instruction) + } + } else if (instructionType.toInt() == 0x0001) { // ALU + var opCode: Short = getBits(12, 4, instruction) + + val targetReg: Short = getBits(8, 4, instruction) + val sourceReg: Short + + if (opCode.toInt() == 15) { + // immediate, source is data register, opcode is the lowest nibble + sourceReg = Register.DR.ordinal.toShort() + opCode = getBits(4, 4, instruction) + } else if (opCode.toInt() == 12 || opCode.toInt() == 13 || opCode.toInt() == 14) { + // unary + sourceReg = targetReg + } else { + // binary op, source is in the lowest nibble + sourceReg = getBits(4, 4, instruction) + } + + val sourceValue = getRegisterValue(sourceReg.toInt()) + val targetValue = getRegisterValue(targetReg.toInt()) + + var result = 0 + when (opCode.toInt()) { + 0 -> { + result = targetValue + sourceValue + setRegisterValue(targetReg.toInt(), result) + } + + 1 -> { + // sub + result = targetValue - sourceValue + setRegisterValue(targetReg.toInt(), result) + } + + 2 -> { + // mul + result = targetValue * sourceValue + setRegisterValue(targetReg.toInt(), result) + } + + 3 -> { + // div + result = targetValue / sourceValue + setRegisterValue(targetReg.toInt(), result) + } + + 4 -> { + // mod + result = targetValue % sourceValue + setRegisterValue(targetReg.toInt(), result) + } + + 5 -> { + // and + result = targetValue.toInt() and sourceValue.toInt() + setRegisterValue(targetReg.toInt(), result) + } + + 6 -> { + // or + result = targetValue.toInt() or sourceValue.toInt() + setRegisterValue(targetReg.toInt(), result) + } + + 7 -> { + // xor + result = targetValue.toInt() xor sourceValue.toInt() + setRegisterValue(targetReg.toInt(), result) + } + + 8 -> { + // shift left + result = targetValue.toInt() shl sourceValue.toInt() + setRegisterValue(targetReg.toInt(), result) + } + + 9 -> { + result = targetValue.toInt() ushr sourceValue.toInt() + setRegisterValue(targetReg.toInt(), result) + } + + 10 -> { + result = min(targetValue.toInt(), sourceValue.toInt()) + setRegisterValue(targetReg.toInt(), result) + } + + 11 -> { + result = max(targetValue.toInt(), sourceValue.toInt()) + setRegisterValue(targetReg.toInt(), result) + } + + 12 -> { + result = targetValue.inv().toInt() + setRegisterValue(targetReg.toInt(), result.toShort().toInt()) + } + + 13 -> { + result = if (targetValue.toInt() == 0) 1 else 0 + setRegisterValue(targetReg.toInt(), result) + } + + 14 -> { + // negate + result = -targetValue + setRegisterValue(targetReg.toInt(), result.toShort().toInt()) + } + + else -> badInstruction(instruction) + } + + setFlagTestBit(result != 0) + } else if (instructionType.toInt() == 2) { + val opcode: Short = getBits(12, 4, instruction) + val stackReg: Short = getBits(4, 4, instruction) + when (opcode.toInt()) { + 0 -> { + // push + val sourceRegister: Short = getBits(8, 4, instruction) + // decrement the stack pointer + setRegisterValue(stackReg.toInt(), getRegisterValue(stackReg.toInt()) - WORD_SIZE) + // write the value out to the location + val stackPointerValue = getRegisterValue(stackReg.toInt()) + val valueToPush = getRegisterValue(sourceRegister.toInt()) + writeWordToMemory(stackPointerValue.toInt(), valueToPush.toInt()) + } + + 1 -> { + // pop + val targetRegister: Short = getBits(8, 4, instruction) + val stackPointerValue = getRegisterValue(stackReg.toInt()) + val value = fetchWordFromMemory(stackPointerValue.toInt()) + setRegisterValue(targetRegister.toInt(), value.toInt()) + setRegisterValue(stackReg.toInt(), getRegisterValue(stackReg.toInt()) + WORD_SIZE) + } + + 2 -> { + // dup + val currentVal = fetchWordFromMemory(getRegisterValue(stackReg.toInt()).toInt()) + setRegisterValue(stackReg.toInt(), getRegisterValue(stackReg.toInt()) - WORD_SIZE) + writeWordToMemory(getRegisterValue(stackReg.toInt()).toInt(), currentVal.toInt()) + } + + 3 -> { + // swap + val currentTop = fetchWordFromMemory(getRegisterValue(stackReg.toInt()).toInt()) + val nextDown = fetchWordFromMemory(getRegisterValue(stackReg.toInt()) + WORD_SIZE) + writeWordToMemory(getRegisterValue(stackReg.toInt()).toInt(), nextDown.toInt()) + writeWordToMemory(getRegisterValue(stackReg.toInt()) + WORD_SIZE, currentTop.toInt()) + } + + 4 -> // drop + setRegisterValue(stackReg.toInt(), getRegisterValue(stackReg.toInt()) + WORD_SIZE) + + 5 -> { + // over + val nextDown = fetchWordFromMemory(getRegisterValue(stackReg.toInt()) + WORD_SIZE) + setRegisterValue(stackReg.toInt(), getRegisterValue(stackReg.toInt()) - WORD_SIZE) + // write the value out to the location + writeWordToMemory(getRegisterValue(stackReg.toInt()).toInt(), nextDown.toInt()) + } + + 6 -> { + // rot + val currentTop = fetchWordFromMemory(getRegisterValue(stackReg.toInt()).toInt()) + val nextDown = fetchWordFromMemory(getRegisterValue(stackReg.toInt()) + WORD_SIZE) + val thirdDown = fetchWordFromMemory(getRegisterValue(stackReg.toInt()) + 2 * WORD_SIZE) + writeWordToMemory(getRegisterValue(stackReg.toInt()).toInt(), thirdDown.toInt()) + writeWordToMemory(getRegisterValue(stackReg.toInt()) + WORD_SIZE, currentTop.toInt()) + writeWordToMemory(getRegisterValue(stackReg.toInt()) + 2 * WORD_SIZE, nextDown.toInt()) + } + + 7 -> { + // sop + val aluOpCode: Short = getBits(8, 4, instruction) + when (aluOpCode.toInt()) { + 0 -> { + val currentTop = fetchWordFromMemory(getRegisterValue(stackReg.toInt()).toInt()) + val nextDown = fetchWordFromMemory(getRegisterValue(stackReg.toInt()) + WORD_SIZE) + setRegisterValue(stackReg.toInt(), getRegisterValue(stackReg.toInt()) + WORD_SIZE) + val value = nextDown + currentTop + writeWordToMemory(getRegisterValue(stackReg.toInt()).toInt(), value) + } + + 1 -> { + val currentTop = fetchWordFromMemory(getRegisterValue(stackReg.toInt()).toInt()) + val nextDown = fetchWordFromMemory(getRegisterValue(stackReg.toInt()) + WORD_SIZE) + setRegisterValue(stackReg.toInt(), getRegisterValue(stackReg.toInt()) + WORD_SIZE) + val value = nextDown - currentTop + writeWordToMemory(getRegisterValue(stackReg.toInt()).toInt(), value) + } + + 2 -> { + val currentTop = fetchWordFromMemory(getRegisterValue(stackReg.toInt()).toInt()) + val nextDown = fetchWordFromMemory(getRegisterValue(stackReg.toInt()) + WORD_SIZE) + setRegisterValue(stackReg.toInt(), getRegisterValue(stackReg.toInt()) + WORD_SIZE) + val value = nextDown * currentTop + writeWordToMemory(getRegisterValue(stackReg.toInt()).toInt(), value) + } + + 3 -> { + val currentTop = fetchWordFromMemory(getRegisterValue(stackReg.toInt()).toInt()) + val nextDown = fetchWordFromMemory(getRegisterValue(stackReg.toInt()) + WORD_SIZE) + setRegisterValue(stackReg.toInt(), getRegisterValue(stackReg.toInt()) + WORD_SIZE) + val value = nextDown / currentTop + writeWordToMemory(getRegisterValue(stackReg.toInt()).toInt(), value) + } + + 4 -> { + val currentTop = fetchWordFromMemory(getRegisterValue(stackReg.toInt()).toInt()) + val nextDown = fetchWordFromMemory(getRegisterValue(stackReg.toInt()) + WORD_SIZE) + setRegisterValue(stackReg.toInt(), getRegisterValue(stackReg.toInt()) + WORD_SIZE) + val value = nextDown % currentTop + writeWordToMemory(getRegisterValue(stackReg.toInt()).toInt(), value) + } + + 5 -> { + val currentTop = fetchWordFromMemory(getRegisterValue(stackReg.toInt()).toInt()) + val nextDown = fetchWordFromMemory(getRegisterValue(stackReg.toInt()) + WORD_SIZE) + setRegisterValue(stackReg.toInt(), getRegisterValue(stackReg.toInt()) + WORD_SIZE) + val value = nextDown.toInt() and currentTop.toInt() + writeWordToMemory(getRegisterValue(stackReg.toInt()).toInt(), value) + } + + 6 -> { + val currentTop = fetchWordFromMemory(getRegisterValue(stackReg.toInt()).toInt()) + val nextDown = fetchWordFromMemory(getRegisterValue(stackReg.toInt()) + WORD_SIZE) + setRegisterValue(stackReg.toInt(), getRegisterValue(stackReg.toInt()) + WORD_SIZE) + val value = nextDown.toInt() or currentTop.toInt() + writeWordToMemory(getRegisterValue(stackReg.toInt()).toInt(), value) + } + + 7 -> { + val currentTop = fetchWordFromMemory(getRegisterValue(stackReg.toInt()).toInt()) + val nextDown = fetchWordFromMemory(getRegisterValue(stackReg.toInt()) + WORD_SIZE) + setRegisterValue(stackReg.toInt(), getRegisterValue(stackReg.toInt()) + WORD_SIZE) + val value = nextDown.toInt() xor currentTop.toInt() + writeWordToMemory(getRegisterValue(stackReg.toInt()).toInt(), value) + } + + 8 -> { + val currentTop = fetchWordFromMemory(getRegisterValue(stackReg.toInt()).toInt()) + val nextDown = fetchWordFromMemory(getRegisterValue(stackReg.toInt()) + WORD_SIZE) + setRegisterValue(stackReg.toInt(), getRegisterValue(stackReg.toInt()) + WORD_SIZE) + val value = nextDown.toInt() shl currentTop.toInt() + writeWordToMemory(getRegisterValue(stackReg.toInt()).toInt(), value) + } + + 9 -> { + val currentTop = fetchWordFromMemory(getRegisterValue(stackReg.toInt()).toInt()) + val nextDown = fetchWordFromMemory(getRegisterValue(stackReg.toInt()) + WORD_SIZE) + setRegisterValue(stackReg.toInt(), getRegisterValue(stackReg.toInt()) + WORD_SIZE) + val value = nextDown.toInt() ushr currentTop.toInt() + writeWordToMemory(getRegisterValue(stackReg.toInt()).toInt(), value) + } + + 10 -> { + val currentTop = fetchWordFromMemory(getRegisterValue(stackReg.toInt()).toInt()) + val nextDown = fetchWordFromMemory(getRegisterValue(stackReg.toInt()) + WORD_SIZE) + setRegisterValue(stackReg.toInt(), getRegisterValue(stackReg.toInt()) + WORD_SIZE) + val min = min(currentTop.toInt(), nextDown.toInt()) + writeWordToMemory(getRegisterValue(stackReg.toInt()).toInt(), min) + } + + 11 -> { + val currentTop = fetchWordFromMemory(getRegisterValue(stackReg.toInt()).toInt()) + val nextDown = fetchWordFromMemory(getRegisterValue(stackReg.toInt()) + WORD_SIZE) + setRegisterValue(stackReg.toInt(), getRegisterValue(stackReg.toInt()) + WORD_SIZE) + val max = max(currentTop.toInt(), nextDown.toInt()) + writeWordToMemory(getRegisterValue(stackReg.toInt()).toInt(), max) + } + + 12 -> { + val currentTop = fetchWordFromMemory(getRegisterValue(stackReg.toInt()).toInt()) + val value = currentTop.toInt().inv() + writeWordToMemory(getRegisterValue(stackReg.toInt()).toInt(), value) + } + + 13 -> { + val currentTop = fetchWordFromMemory(getRegisterValue(stackReg.toInt()).toInt()) + val value = if (currentTop.toInt() == 0) 1 else 0 + writeWordToMemory(getRegisterValue(stackReg.toInt()).toInt(), value) + } + + 14 -> { + val currentTop = fetchWordFromMemory(getRegisterValue(stackReg.toInt()).toInt()) + val value = -currentTop.toInt() + writeWordToMemory(getRegisterValue(stackReg.toInt()).toInt(), value) + } + + else -> badInstruction(instruction) + } + } + + 15 -> { + val immediateValue = getRegisterValue(Register.DR) + setRegisterValue(stackReg.toInt(), getRegisterValue(stackReg.toInt()) - WORD_SIZE) + writeWordToMemory(getRegisterValue(stackReg.toInt()).toInt(), immediateValue.toInt()) + } + + else -> badInstruction(instruction) + } + } else if (instructionType.toInt() == 3) { + val opCode: Int = getBits(12, 4, instruction).toInt() + val lhs: Int = getBits(8, 4, instruction).toInt() + val rhs: Int = getBits(4, 4, instruction).toInt() + when (opCode) { + 0 -> { + val lhsVal = getRegisterValue(lhs) + val rhsVal = getRegisterValue(rhs) + setFlagTestBit(lhsVal == rhsVal) + } + + 1 -> { + val lhsVal = getRegisterValue(lhs) + val rhsVal = getRegisterValue(rhs) + setFlagTestBit(lhsVal != rhsVal) + } + + 2 -> { + val lhsVal = getRegisterValue(lhs) + val rhsVal = getRegisterValue(rhs) + setFlagTestBit(lhsVal > rhsVal) + } + + 3 -> { + val lhsVal = getRegisterValue(lhs) + val rhsVal = getRegisterValue(rhs) + setFlagTestBit(lhsVal >= rhsVal) + } + + 4 -> { + val lhsVal = getRegisterValue(lhs) + val rhsVal = getRegisterValue(rhs) + setFlagTestBit(lhsVal < rhsVal) + } + + 5 -> { + val lhsVal = getRegisterValue(lhs) + val rhsVal = getRegisterValue(rhs) + setFlagTestBit(lhsVal <= rhsVal) + } + + 8 -> { + val lhsVal = getRegisterValue(lhs) + val rhsVal = rhs.toShort() + setFlagTestBit(lhsVal == rhsVal) + } + + 9 -> { + val lhsVal = getRegisterValue(lhs) + val rhsVal = rhs.toShort() + setFlagTestBit(lhsVal != rhsVal) + } + + 10 -> { + val lhsVal = getRegisterValue(lhs) + val rhsVal = rhs.toShort() + setFlagTestBit(lhsVal > rhsVal) + } + + 11 -> { + val lhsVal = getRegisterValue(lhs) + val rhsVal = rhs.toShort() + setFlagTestBit(lhsVal >= rhsVal) + } + + 12 -> { + val lhsVal = getRegisterValue(lhs) + val rhsVal = rhs.toShort() + setFlagTestBit(lhsVal < rhsVal) + } + + 13 -> { + val lhsVal = getRegisterValue(lhs) + val rhsVal = rhs.toShort() + setFlagTestBit(lhsVal <= rhsVal) + } + + else -> badInstruction(instruction) + } + } else if (8 == instructionType.toInt()) { + // load/store + val opCode: Int = getBits(12, 4, instruction).toInt() + val reg: Int = getBits(8, 4, instruction).toInt() + val offsetReg: Int = getBits(4, 4, instruction).toInt() + val address = getRegisterValue(Register.DR).toInt() + when (opCode) { + 0 -> { + val value = fetchWordFromMemory(address) + setRegisterValue(reg, value.toInt()) + } + + 1 -> { + val value = fetchWordFromMemory(address + getRegisterValue(offsetReg)) + setRegisterValue(reg, value.toInt()) + } + + 2 -> { + val value = fetchByteFromMemory(address).toShort() + setRegisterValue(reg, value.toInt()) + } + + 3 -> { + val value = fetchByteFromMemory(address + getRegisterValue(offsetReg)).toShort() + setRegisterValue(reg, value.toInt()) + } + + 4 -> { + val value = getRegisterValue(reg) + writeWordToMemory(address, value.toInt()) + } + + 5 -> { + val value = getRegisterValue(reg) + writeWordToMemory(address + getRegisterValue(offsetReg), value.toInt()) + } + + 6 -> { + val value = getRegisterValue(reg).toByte() + writeByteToMemory(address, value) + } + + 7 -> { + val value = getRegisterValue(reg).toByte() + writeByteToMemory(address + getRegisterValue(offsetReg), value) + } + + 15 -> { + setRegisterValue(reg, address) + } + + else -> badInstruction(instruction) + } + } else if (4 <= instructionType && instructionType <= 7) { + // load/store register + val loadStoreType: Short = getBits(14, 2, instruction) + val targetRegister: Short = getBits(12, 4, instruction) + val addressRegister: Short = getBits(8, 4, instruction) + val offsetRegister: Short = getBits(4, 4, instruction) + val targetAddress = + getRegisterValue(addressRegister.toInt()) + getRegisterValue(offsetRegister.toInt()) + if (loadStoreType.toInt() == 0x0) { + setRegisterValue(targetRegister.toInt(), fetchWordFromMemory(targetAddress).toInt()) + } else if (loadStoreType.toInt() == 0x1) { + setRegisterValue(targetRegister.toInt(), fetchByteFromMemory(targetAddress).toInt()) + } else if (loadStoreType.toInt() == 0x2) { + writeWordToMemory(targetAddress, getRegisterValue(targetRegister.toInt()).toInt()) + } else if (loadStoreType.toInt() == 0x3) { + writeByteToMemory(targetAddress, getRegisterValue(targetRegister.toInt()).toByte()) + } + } else if (9 == instructionType.toInt()) { + // jump reg + val reg: Short = getBits(4, 4, instruction) + val location = getRegisterValue(reg.toInt()) + setRegisterValue(Register.PC, location.toInt()) + } else if (12 <= instructionType && instructionType <= 15) { + // jumps + val jumpType: Short = getBits(14, 2, instruction) + if (jumpType.toInt() == 0) { + // unconditional + val location: Short = getBits(12, 12, instruction) + setRegisterValue(Register.PC, location.toInt()) + } else if (jumpType.toInt() == 1) { + // jz + val location: Short = getBits(12, 12, instruction) + if (!this.isFlagTestBitSet) { + setRegisterValue(Register.PC, location.toInt()) + } + } else if (jumpType.toInt() == 2) { + // jnz + val location: Short = getBits(12, 12, instruction) + if (this.isFlagTestBitSet) { + setRegisterValue(Register.PC, location.toInt()) + } + } else if (jumpType.toInt() == 3) { + // jump & link + val location: Short = getBits(12, 12, instruction) + setRegisterValue(Register.RA, getRegisterValue(Register.PC).toInt()) + setRegisterValue(Register.PC, location.toInt()) + } + } else { + badInstruction(instruction) + } + observers.forEach(Consumer { o: MTMCObserver? -> o!!.afterExecution(instruction) }) + } + + val isFlagTestBitSet: Boolean + get() { + val i = getRegisterValue(Register.FLAGS).toInt() and 1 + val b = i != 0 + return b + } + + fun setFlagTestBit(testVal: Boolean) { + var value = getRegisterValue(Register.FLAGS) + value = if (testVal) { + value or 1 + } else { + value and 14 + } + setRegisterValue(Register.FLAGS, value.toInt()) + } + + private fun badInstruction(instruction: Short) { + setStatus(ComputerStatus.PERMANENT_ERROR) + // TODO implement flags + console.println("BAD INSTRUCTION: 0x" + Integer.toHexString(instruction.toInt() and 0xFFFF)) + } + + fun fetchCurrentInstruction() { + val pc = getRegisterValue(Register.PC) + val instruction = fetchWordFromMemory(pc.toInt()) + setRegisterValue(Register.IR, instruction.toInt()) + if (isDoubleWordInstruction(instruction)) { + val data = fetchWordFromMemory(pc + WORD_SIZE) + setRegisterValue(Register.DR, data.toInt()) + } else { + setRegisterValue(Register.DR, 0) + } + observers!!.forEach(Consumer { o: MTMCObserver? -> o!!.instructionFetched(instruction) }) + } + + fun fetchWordFromMemory(address: Int): Short { + val upperByte = fetchByteFromMemory(address).toShort() + val lowerByte = fetchByteFromMemory(address + 1) + var value = (upperByte.toInt() shl 8).toShort() + val i = value.toInt() or Byte.toUnsignedInt(lowerByte) + value = i.toShort() + return value + } + + fun fetchByteFromMemory(address: Int): kotlin.Byte { + if (address < 0 || address >= memory.size) { + setStatus(ComputerStatus.PERMANENT_ERROR) + console.println( + "BAD MEMORY LOCATION ON READ: " + address + " (0x" + Integer.toHexString( + address and 0xFFFF + ) + ")" + ) + return 0 + } else { + return memory[address] + } + } + + fun writeWordToMemory(address: Int, value: Int) { + val i = (value ushr 8).toByte() + writeByteToMemory(address, i) + writeByteToMemory(address + 1, (value and 255).toByte()) + } + + fun writeByteToMemory(address: Int, value: kotlin.Byte) { + if (address < 0 || address >= memory.size) { + setStatus(ComputerStatus.PERMANENT_ERROR) + console.println( + "BAD MEMORY LOCATION ON WRITE: " + address + " (0x" + Integer.toHexString( + address and 0xFFFF + ) + ")" + ) + return + } + val currentValue = memory[address] + addRewindStep(Runnable { memory[address] = currentValue }) + memory[address] = value + observers!!.forEach(Consumer { o: MTMCObserver? -> o!!.memoryUpdated(address, value) }) + } + + private fun addRewindStep(runnable: Runnable?) { + if (currentRewindStep != null && rewindSteps != null) { + currentRewindStep!!.addSubStep(runnable) + if (rewindSteps!!.size > MAX_REWIND_STEPS) { + rewindSteps!!.removeLast() + } + } + } + + fun setRegisterValue(register: Register, value: Int) { + setRegisterValue(register.ordinal, value) + } + + fun setRegisterValue(register: Int, value: Int) { + if (Short.Companion.MAX_VALUE < value || value < Short.Companion.MIN_VALUE) { + // TODO mark as overflow + } + val currentValue = registerFile[register] + addRewindStep(Runnable { + registerFile[register] = currentValue + }) + registerFile[register] = value.toShort() + observers!!.forEach(Consumer { o: MTMCObserver? -> o!!.registerUpdated(register, value) }) + } + + fun getRegisterValue(register: Register): Short { + return registerFile[register.ordinal] + } + + fun getRegisterValue(register: Int): Short { + return registerFile[register] + } + + fun getBreakpoints(): IntArray { + val list = ArrayList() + + for (i in breakpoints.indices) { + if (breakpoints[i].toInt() != 0) list.add(i) + } + + val result = IntArray(list.size) + + for (i in result.indices) result[i] = list.get(i)!! + + return result + } + + fun setBreakpoint(address: Int, active: Boolean) { + breakpoints[address] = (if (active) 1.toByte() else 0.toByte()) + } + + private fun start() { + console.start() // start the interactive console + } + + fun getBytesFromMemory(address: Int, length: Int): ByteArray { + return Arrays.copyOfRange(memory, address, address + length) + } + + val oS: MTOS + get() = os + + val memoryAddresses: Iterable + get() = Iterable { + IntStream.range( + 0, + MEMORY_SIZE + ).iterator() + } + + fun addObserver(observer: MTMCObserver?) { + observers!!.add(observer!!) + } + + fun removeObserver(observer: MTMCObserver?) { + observers!!.remove(observer!!) + } + + fun pause() { + setStatus(ComputerStatus.READY) + } + + fun notifyOfConsoleUpdate() { + for (observer in observers) { + observer.consoleUpdated() + } + } + + fun notifyOfConsolePrinting() { + for (observer in observers) { + observer.consolePrinting() + } + } + + fun notifyOfDisplayUpdate() { + for (observer in observers) { + observer.displayUpdated() + } + } + + fun notifyOfFileSystemUpdate() { + for (observer in observers) { + observer.filesystemUpdated() + } + } + + fun notifyOfExecutionUpdate() { + for (observer in observers) { + observer.executionUpdated() + } + } + + fun notifyOfStepExecution() { + for (observer in observers) { + observer.stepExecution() + } + } + + fun notifyOfRequestString() { + for (observer in observers) { + observer.requestString() + } + } + + fun notifyOfRequestCharacter() { + for (observer in observers) { + observer.requestCharacter() + } + } + + fun notifyOfRequestInteger() { + for (observer in observers) { + observer.requestInteger() + } + } + + val iOState: Int + get() = iO.value + + fun setArg(arg: String) { + if (!arg.isEmpty()) { + val start = getRegisterValue(Register.BP) + val bytes = arg.toByteArray() + writeStringToMemory(start.toInt(), bytes) + setRegisterValue(Register.A0, start.toInt()) + setRegisterValue(Register.BP, start + bytes.size + 1) + } + } + + fun writeStringToMemory(start: Int, bytes: ByteArray) { + for (i in bytes.indices) { + val aByte = bytes[i] + writeByteToMemory(start + i, aByte) + } + // null terminate + writeByteToMemory(start + bytes.size, 0.toByte()) + } + + fun rewind() { + val latestRewindStep = rewindSteps!!.pop() + latestRewindStep.rewind() + } + + val isBackAvailable: Boolean + get() = !rewindSteps!!.isEmpty() + + enum class ComputerStatus { + READY, + EXECUTING, + PERMANENT_ERROR, + FINISHED, + WAITING, + BREAK + } + + override fun toString(): String { + val s = StringBuilder() + s.append("CI: ").append( + Instruction.disassemble( + getRegisterValue(Register.IR), getRegisterValue( + Register.DR + ) + ) + ) + s.append(", Status: ").append(getStatus()!!.name) + val fp = getRegisterValue(Register.FP) + val sp = getRegisterValue(Register.SP) + s.append(", FP:SP=").append(fp.toInt()).append(':').append(sp.toInt()) + s.append(", STACK: ") + + for (i in fp downTo sp + 1) { + val value = getBytesFromMemory(i - 2, 2) + val v = value[1].toInt() ushr 8 or value[0].toInt() + s.append(v).append(' ') + } + + return s.toString() + } + + companion object { + // constants + const val WORD_SIZE: Short = 2 + const val MEMORY_SIZE: Int = 4096 + + const val MAX_REWIND_STEPS: Int = 100 + + fun isDoubleWordInstruction(instruction: Short): Boolean { + val isLoadStore = getBits(16, 4, instruction) == 8.toShort() + if (isLoadStore) { + return true + } + val isALUImmediate = getBits(16, 8, instruction) == 31.toShort() + if (isALUImmediate) { + return true + } + val isPushImmediate = getBits(16, 8, instruction) == 47.toShort() + if (isPushImmediate) { + return true + } + val isMcp = getBits(16, 8, instruction) == 5.toShort() + if (isMcp) { + return true + } + return false + } + + @JvmStatic + fun main(args: Array) { + val computer = MonTanaMiniComputer() + computer.speed = 1 // default to 1hz + computer.start() + } + } +} diff --git a/src/jvmMain/kotlin/mtmc/emulator/Register.kt b/src/jvmMain/kotlin/mtmc/emulator/Register.kt new file mode 100644 index 0000000..20b8a0f --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/emulator/Register.kt @@ -0,0 +1,76 @@ +package mtmc.emulator + +enum class Register { + + //=== user-facing registers + T0, // temp registers + T1, + T2, + T3, + T4, + T5, + A0, // arg registers + A1, + A2, + A3, + RV, // return value + RA, // return address + FP, // frame pointer + SP, // stack pointer + BP, // break pointer + PC, + + //=== non-user-facing registers + IR, // instruction register + DR, // data register + CB, // code boundary + DB, // data boundary + IO, // I/O register + FLAGS; // flags register + + companion object { + fun toInteger(reg: String): Int { + return valueOf(reg.uppercase()).ordinal + } + + fun fromInteger(reg: Int): String { + return values()[reg].name.lowercase() + } + + fun isWriteable(reg: Int): Boolean { + return reg in 0..15 + } + + fun isReadable(reg: Int): Boolean { + return reg in 0..15 + } + + private fun isTempRegister(reg: Int): Boolean { + return reg in 0..5 + } + + fun isWriteable(register: String): Boolean { + return try { + isWriteable(toInteger(register)) + } catch (e: Exception) { + false + } + } + + fun isReadable(register: String): Boolean { + return try { + isReadable(toInteger(register)) + } catch (e: Exception) { + false + } + } + + fun isTempRegister(register: String): Boolean { + return try { + isTempRegister(toInteger(register)) + } catch (e: Exception) { + false + } + } + } +} diff --git a/src/jvmMain/kotlin/mtmc/emulator/RewindStep.kt b/src/jvmMain/kotlin/mtmc/emulator/RewindStep.kt new file mode 100644 index 0000000..bfa8b49 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/emulator/RewindStep.kt @@ -0,0 +1,13 @@ +package mtmc.emulator + +class RewindStep { + var subSteps: MutableList = ArrayList() + + fun rewind() { + subSteps.reversed().forEach({ obj: Runnable? -> obj!!.run() }) + } + + fun addSubStep(subStep: Runnable?) { + subSteps.add(subStep) + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/CompilationException.java b/src/jvmMain/kotlin/mtmc/lang/CompilationException.java new file mode 100644 index 0000000..c23187a --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/CompilationException.java @@ -0,0 +1,19 @@ +package mtmc.lang; + +public class CompilationException extends Exception { + protected Span span; + + public CompilationException(String message, Span span) { + super(message); + this.span = span; + } + + public CompilationException(CompilationException parent, String message) { + super(message, parent); + this.span = parent.span; + } + + public Span getSpan() { + return span; + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/Language.java b/src/jvmMain/kotlin/mtmc/lang/Language.java new file mode 100644 index 0000000..cbc402a --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/Language.java @@ -0,0 +1,7 @@ +package mtmc.lang; + +import mtmc.os.exec.Executable; + +public interface Language { + Executable compileExecutable(String filename, String source) throws ParseException, CompilationException; +} diff --git a/src/jvmMain/kotlin/mtmc/lang/Location.java b/src/jvmMain/kotlin/mtmc/lang/Location.java new file mode 100644 index 0000000..28cb477 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/Location.java @@ -0,0 +1,55 @@ +package mtmc.lang; + +public record Location(int index) { + + public static int[] getLineNos(String source, int... indices) { + int[] out = new int[indices.length]; + int index = 0, line = 1; + while (index < source.length()) { + for (int i = 0; i < indices.length; i++) { + if (indices[i] == index) { + out[i] = line; + } + } + + int cp = source.charAt(index); + if (cp == '\n') { + line += 1; + } + index += Character.charCount(cp); + } + return out; + } + + public LineInfo getLineInfo(String source) { + int index = 0, lineStart = 0; + int lineno = 1; + int column = 1; + while (index < source.length()) { + if (index == this.index) { + break; + } + int cp = source.charAt(index); + + if (cp == '\n') { + lineno += 1; + lineStart = index + 1; + } else { + column += 1; + } + + index += Character.charCount(cp); + } + + while (index < source.length()) { + int cp = source.charAt(index); + index += Character.charCount(cp); + if (cp == '\n') break; + } + String line = source.substring(lineStart, index); + + return new LineInfo(lineno, column, line); + } + + public record LineInfo(int lineno, int column, String line) {} +} diff --git a/src/jvmMain/kotlin/mtmc/lang/ParseException.java b/src/jvmMain/kotlin/mtmc/lang/ParseException.java new file mode 100644 index 0000000..4549eb6 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/ParseException.java @@ -0,0 +1,43 @@ +package mtmc.lang; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class ParseException extends Exception { + public final List messages; + + public ParseException(Message message, Message ...rest) { + var messages = new ArrayList(1 + rest.length); + messages.add(message); + messages.addAll(Arrays.asList(rest)); + this.messages = Collections.unmodifiableList(messages); + } + + public ParseException(ParseException parent, Message message, Message ...rest) { + var messages = new ArrayList( 1 + rest.length + parent.messages.size()); + messages.add(message); + messages.addAll(Arrays.asList(rest)); + messages.addAll(parent.messages); + this.messages = Collections.unmodifiableList(messages); + } + + public record Message(Span span, String message) { + public Message(Token token, String message) { + this(Span.of(token), message); + } + + public Token start() { + return span.start(); + } + + public Token end() { + return span.end(); + } + } + + public String report(String source) { + return "TODO: I ain't no snitch"; + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/Span.java b/src/jvmMain/kotlin/mtmc/lang/Span.java new file mode 100644 index 0000000..fa750c7 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/Span.java @@ -0,0 +1,17 @@ +package mtmc.lang; + +public record Span(Token start, Token end) { + + public static Span of(Token token) { + return new Span(token, token); + } + + public static Span of(Token start, Token end) { + return new Span(start, end); + } + + public boolean isOnSingleLine(String source) { + int[] lines = Location.getLineNos(source, start.start(), end.end()); + return lines[0] == lines[1]; + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/Token.java b/src/jvmMain/kotlin/mtmc/lang/Token.java new file mode 100644 index 0000000..67bdd99 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/Token.java @@ -0,0 +1,15 @@ +package mtmc.lang; + +public interface Token { + Location getStart(); + Location getEnd(); + String getContent(); + + default int start() { + return getStart().index(); + } + + default int end() { + return getEnd().index(); + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/SeaCompiler.java b/src/jvmMain/kotlin/mtmc/lang/sea/SeaCompiler.java new file mode 100644 index 0000000..9d5c193 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/SeaCompiler.java @@ -0,0 +1,1415 @@ +package mtmc.lang.sea; + +import mtmc.asm.Assembler; +import mtmc.lang.CompilationException; +import mtmc.lang.Span; +import mtmc.lang.sea.ast.Declaration; +import mtmc.lang.sea.ast.DeclarationFunc; +import mtmc.lang.sea.ast.DeclarationStruct; +import mtmc.lang.sea.ast.DeclarationSyntaxError; +import mtmc.lang.sea.ast.DeclarationTypedef; +import mtmc.lang.sea.ast.DeclarationVar; +import mtmc.lang.sea.ast.Expression; +import mtmc.lang.sea.ast.ExpressionAccess; +import mtmc.lang.sea.ast.ExpressionBin; +import mtmc.lang.sea.ast.ExpressionCall; +import mtmc.lang.sea.ast.ExpressionCast; +import mtmc.lang.sea.ast.ExpressionChar; +import mtmc.lang.sea.ast.ExpressionIdent; +import mtmc.lang.sea.ast.ExpressionIndex; +import mtmc.lang.sea.ast.ExpressionInitializer; +import mtmc.lang.sea.ast.ExpressionInteger; +import mtmc.lang.sea.ast.ExpressionParens; +import mtmc.lang.sea.ast.ExpressionPostfix; +import mtmc.lang.sea.ast.ExpressionPrefix; +import mtmc.lang.sea.ast.ExpressionString; +import mtmc.lang.sea.ast.ExpressionSyntaxError; +import mtmc.lang.sea.ast.ExpressionTernary; +import mtmc.lang.sea.ast.ExpressionTypeError; +import mtmc.lang.sea.ast.Statement; +import mtmc.lang.sea.ast.StatementBlock; +import mtmc.lang.sea.ast.StatementBreak; +import mtmc.lang.sea.ast.StatementContinue; +import mtmc.lang.sea.ast.StatementDoWhile; +import mtmc.lang.sea.ast.StatementExpression; +import mtmc.lang.sea.ast.StatementFor; +import mtmc.lang.sea.ast.StatementGoto; +import mtmc.lang.sea.ast.StatementIf; +import mtmc.lang.sea.ast.StatementReturn; +import mtmc.lang.sea.ast.StatementSyntaxError; +import mtmc.lang.sea.ast.StatementVar; +import mtmc.lang.sea.ast.StatementWhile; +import mtmc.lang.sea.ast.Unit; +import mtmc.os.exec.Executable; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Stack; + +import static mtmc.lang.sea.SeaCompiler.MemLoc.RV; +import static mtmc.lang.sea.SeaCompiler.MemLoc.SP; +import static mtmc.lang.sea.SeaCompiler.MemLoc.STACK; +import static mtmc.lang.sea.SeaCompiler.MemLoc.T0; +import static mtmc.lang.sea.SeaCompiler.MemLoc.T1; +import static mtmc.lang.sea.SeaCompiler.MemLoc.T2; +import static mtmc.lang.sea.SeaCompiler.MemLoc.T4; +import static mtmc.lang.sea.SeaCompiler.MemLoc.T5; +import static mtmc.util.StringEscapeUtils.escapeString; + +public class SeaCompiler { + protected Unit program; + + private StringBuilder data = new StringBuilder(); + private StringBuilder code = new StringBuilder(); + private HashMap globalLabels = new HashMap<>(); + private HashMap dataLabels = new HashMap<>(); + private int currentLineNo = -1; + + public SeaCompiler(Unit program) { + this.program = program; + } + + public Executable compile() throws CompilationException { + // t0, t1 are temporary values, t3 is a stack offset, t4 & t5 are comparison temporaries + // data-types: pointers, words, and bytes + // globals end in the 'data' segment + // locals end up on the stack + // code in the 'text' segment + + // Calling Convention: + // jal # (mov ra, pc+sizeof(*pc), j addr) + // + // -- begin inner function + // max 4 fields, a0-a3 + // push fp + // mov fp, sp + // sp -= (sizeof locals (max 15 words)) + // via: 'dec sp, 14' + // 'lwr/swr $target_reg, fp, $offset_reg' to save & load relative + // then we have an expression stack + // ...compilation + // return value in the rv register + // mov sp, fp + // pop fp + // ret # 'jr $ra' + + for (var decl : program.declarations) { + compile(decl); + } + + String data = this.data.toString(); + String code = this.code.toString(); + + String asm = ""; + + if (this.program.filename != null) { + asm += "@file " + escapeString(this.program.filename) + "\n"; + } + + if (!data.isBlank()) { + asm += ".data\n" + data; + } + + asm += '\n'; + asm += ".text\n"; + asm += """ + mov fp sp + jal main + mov a0 rv + sys exit + """; + asm += code; + + System.out.println("generated the following assembly:"); + System.out.println("=".repeat(20)); + int lineno = 1; + for (Iterator it = asm.lines().iterator(); it.hasNext(); ) { + String line = it.next(); + System.out.printf("%3d | %s%n", lineno, line); + lineno += 1; + } + System.out.println("=".repeat(20)); + var assembler = new Assembler(); + var exec = assembler.assembleExecutable("program", asm); + return exec; + } + + protected void compile(Declaration decl) throws CompilationException { + switch (decl) { + case DeclarationFunc declarationFunc -> compile(declarationFunc); + case DeclarationSyntaxError ignored -> + throw new UnsupportedOperationException("error checking should happen before compilation"); + case DeclarationTypedef declarationTypedef -> compile(declarationTypedef); + case DeclarationVar declarationVar -> compile(declarationVar); + case DeclarationStruct declarationStruct -> compile(declarationStruct); + } + } + + // the tail label of the current loop + Stack currentLoopBreakLabel = new Stack<>(); + Stack currentLoopContinueLabel = new Stack<>(); + int labelNo = 0; + + protected void compile(DeclarationFunc func) throws CompilationException { + switch (func.name.content()) { + case "printf" -> { + if (!func.returnType.type().isInt()) { + throw new CompilationException("'printf' must return int", func.returnType.span()); + } + + if (func.params.size() != 1 || !func.params.getParamType(0).isAPointerTo(SeaType.CHAR) || !func.params.isVararg()) { + throw new CompilationException("'printf' must have signature (char *, ...)", Span.of(func.name)); + } + + code.append("printf:\n"); + code.append(" sys printf\n"); + code.append(" mov sp a1\n"); // restore the stack + code.append(" ret\n"); + return; + } + case "atoi" -> { + if (!func.returnType.type().isInt()) { + throw new CompilationException("'atoi' must return int", func.returnType.span()); + } + if (func.params.size() != 1 || !func.params.getParamType(0).isAPointerTo(SeaType.CHAR)) { + throw new CompilationException("'atoi' must have signature (char *)", Span.of(func.name)); + } + code.append("atoi:\n"); + code.append(" sys atoi\n"); + code.append(" ret\n"); + return; + } + case "putc" -> { + if (!func.returnType.type().isVoid()) { + throw new CompilationException("'putc' must return void", func.returnType.span()); + } + if (func.params.size() != 1 || !func.params.getParamType(0).isChar()) { + throw new CompilationException("'puts' must take a char as its only argument", Span.of(func.name)); + } + + code.append("putc:\n"); + code.append(" sys wchr\n"); + code.append(" ret\n"); + return; + } + case "puts" -> { + if (!func.returnType.type().isVoid()) { + throw new CompilationException("'puts' must return void", func.returnType.span()); + } + if (func.params.size() != 1 || !func.params.getParamType(0).isAPointerTo(SeaType.CHAR)) { + throw new CompilationException("'puts' must take a char* as its only argument", Span.of(func.name)); + } + + code.append("puts:\n"); + code.append(" sys wstr\n"); + code.append(" ret\n"); + return; + } + case "putn" -> { + if (!func.returnType.type().isVoid()) { + throw new CompilationException("'putn' must return void", func.returnType.span()); + } + if (func.params.size() != 1 || !func.params.getParamType(0).isInt()) { + throw new CompilationException("'putn' must take an int as its only argument", Span.of(func.name)); + } + + code.append("putn:\n"); + code.append(" sys wint\n"); + code.append(" ret\n"); + return; + } + } + + code.append(func.name.content()).append(":\n"); + int lineNo = Token.getLineAndOffset(program.source, func.start.start())[0]; + if (currentLineNo != lineNo) { + code.append("@line ").append(lineNo).append('\n'); + currentLineNo = lineNo; + } + code.append(" push fp\n"); + code.append(" mov fp sp\n"); + + var frame = new Frame(func.params); + + int maxLocalSize = calcMaxLocalSize(func.body); + if (maxLocalSize > 0) { + if (maxLocalSize > 15) { + code.append(" li t0 ").append(maxLocalSize).append("\n"); + code.append(" sub sp t0\n"); + } else { + code.append(" dec sp ").append(maxLocalSize).append("\n"); + } + } + + compile(func.body, frame); + labelNo = 0; + + // TODO: dangling return + } + + private int calcMaxLocalSize(Statement body) { + return switch (body) { + case StatementBlock statementBlock -> { + int vars = 0; + int temps = 0; + for (var child : statementBlock.statements) { + if (child instanceof StatementVar sv) { + vars += sv.type.type().size(); + } else { + int r = calcMaxLocalSize(child); + temps = Math.max(temps, r); + } + } + yield vars + temps; + } + case StatementBreak ignored -> 0; + case StatementContinue ignored -> 0; + case StatementDoWhile stmt -> { + int n = calcMaxLocalSize(stmt.body); + yield n; + } + case StatementExpression ignored -> 0; + case StatementFor stmt -> { + int n = 0; + if (stmt.initStatement != null) n += calcMaxLocalSize(stmt.initStatement); + n += calcMaxLocalSize(stmt.body); + yield n; + } + case StatementGoto ignored -> 0; + case StatementIf statementIf -> { + int t = calcMaxLocalSize(statementIf.body); + int e = statementIf.elseBody == null ? 0 : calcMaxLocalSize(statementIf.elseBody); + yield Math.max(t, e); + } + case StatementReturn ignored -> 0; + case StatementSyntaxError ignored -> 0; + case StatementVar sv -> sv.type.type().size(); + case StatementWhile stmt -> { + int n = calcMaxLocalSize(stmt.body); + yield n; + } + }; + } + + private void compile(Statement statement, Frame frame) { + int lineNo = Token.getLineAndOffset(program.source, statement.start.start())[0]; + if (currentLineNo != lineNo) { + code.append("@line ").append(lineNo).append('\n'); + currentLineNo = lineNo; + } + + if (statement.getLabelAnchor() != null) { + code.append(statement.getLabelAnchor().content()).append(":\n"); + } + switch (statement) { + case StatementBlock block -> compile(block, frame); + case StatementBreak ignored -> compileBreak(); + case StatementContinue ignored -> compileContinue(); + case StatementDoWhile stmt -> compile(stmt, frame); + case StatementExpression stmt -> compile(stmt, frame); + case StatementFor stmt -> compileFor(stmt, frame); + case StatementGoto stmt -> compileGoto(stmt); + case StatementIf stmt -> compile(stmt, frame); + case StatementReturn stmt -> compile(stmt, frame); + case StatementSyntaxError stmt -> compile(stmt, frame); + case StatementVar stmt -> compile(stmt, frame); + case StatementWhile stmt -> compileWhile(stmt, frame); + } + } + + void compile(StatementBlock stmt, Frame frame) { + var added = new HashSet(); + for (Statement statement : stmt.statements) { + if (statement instanceof StatementVar sv) { + frame.add(sv.name(), sv.type.type().size()); + added.add(sv.name()); + } + compile(statement, frame); + } + for (String s : added) { + frame.remove(s); + } + } + + void compileBreak() { + code.append(" j ").append(currentLoopBreakLabel.peek()).append("\n"); + } + + void compileContinue() { + code.append(" j ").append(currentLoopContinueLabel.peek()).append("\n"); + } + + void compile(StatementDoWhile stmt, Frame frame) { + int label = labelNo++; + String head = "doWhile" + label; + String cond = "doWhileCond" + label; + String tail = "endDoWhile" + label; + + code.append(head).append(":\n"); + currentLoopContinueLabel.push(cond); + currentLoopBreakLabel.push(tail); + compile(stmt.body, frame); + currentLoopContinueLabel.pop(); + currentLoopBreakLabel.pop(); + + code.append(cond).append(":\n"); + compile(stmt.condition, frame, T0); + code.append(" eqi t0 1\n"); + code.append(" jnz ").append(head).append("\n"); + code.append(tail).append(":\n"); + } + + void compile(StatementExpression stmt, Frame frame) { + code.append("# stmt expr\n"); + compile(stmt.expression, frame, T0); + } + + void compileFor(StatementFor stmt, Frame frame) { + int label = labelNo++; + String head = "for" + label; + String cont = "forInc" + label; + String tail = "endFor" + label; + + if (stmt.initExpression != null) { + compile(stmt.initExpression, frame, T0); + } else if (stmt.initStatement != null) { + frame.add(stmt.initStatement.name(), stmt.initStatement.type.type().size()); + compile(stmt.initStatement, frame); + } + + code.append(head).append(":\n"); + compile(stmt.condition, frame, T0); + code.append(" eqi t0 0\n"); + code.append(" jnz ").append(tail).append("\n"); + + currentLoopContinueLabel.push(stmt.inc == null ? head : cont); + currentLoopBreakLabel.push(tail); + compile(stmt.body, frame); + currentLoopContinueLabel.pop(); + currentLoopBreakLabel.pop(); + + if (stmt.inc != null) { + code.append(cont).append(":\n"); + compile(stmt.inc, frame, T0); + } + code.append(" j ").append(head).append("\n"); + code.append(tail).append(":\n"); + + if (stmt.initStatement != null) { + frame.remove(stmt.initStatement.name()); + } + } + + void compileGoto(StatementGoto stmt) { + code.append(" j ").append(stmt.label.content()).append("\n"); + } + + void compile(StatementIf stmt, Frame frame) { + String elseLabel = "ifElse" + labelNo++; + String endLabel = "endIf" + labelNo++; + + compile(stmt.condition, frame, T0); + code.append(" eqi t0 0\n"); + if (stmt.elseBody != null) { + code.append(" jnz ").append(elseLabel).append("\n"); + compile(stmt.body, frame); + code.append(" j ").append(endLabel).append('\n'); + code.append(elseLabel).append(":\n"); + compile(stmt.elseBody, frame); + } else { + code.append(" jnz ").append(endLabel).append("\n"); + compile(stmt.body, frame); + } + code.append(endLabel).append(":\n"); + } + + void compile(StatementReturn stmt, Frame frame) { + if (stmt.value != null) { + compile(stmt.value, frame, RV); + } + code.append(" mov sp fp\n"); + code.append(" pop fp\n"); + code.append(" ret\n"); + } + + void compile(StatementVar stmt, Frame frame) { + if (stmt.initValue != null) { + code.append("# var ").append(stmt.name()).append('\n'); + int valueSize = stmt.type.type().size(); + var offset = frame.getBaseOffset(stmt.name()); + if (valueSize <= 2) { + compile(stmt.initValue, frame, T0); + storeToFrame(T0, offset, valueSize); + } else { + compile(stmt.initValue, frame, STACK); + storeToFrame(STACK, offset, valueSize); + } + } + } + + void compileWhile(StatementWhile stmt, Frame frame) { + int label = labelNo++; + var headLabel = "while" + label; + var tailLabel = "endWhile" + label; + + code.append(headLabel).append(":\n"); + compile(stmt.condition, frame, T0); + code.append(" eqi t0 0\n"); + code.append(" jnz ").append(tailLabel).append("\n"); + currentLoopContinueLabel.push(headLabel); + currentLoopBreakLabel.push(tailLabel); + compile(stmt.body, frame); + currentLoopContinueLabel.pop(); + currentLoopBreakLabel.pop(); + code.append(" j ").append(headLabel).append("\n"); + code.append(tailLabel).append(":\n"); + + } + + void compile(Expression expression, Frame frame, MemLoc dst) { + int lineNo = Token.getLineAndOffset(program.source, expression.start.start())[0]; + if (currentLineNo != lineNo) { + code.append("@line ").append(lineNo).append('\n'); + currentLineNo = lineNo; + } + switch (expression) { + case ExpressionAccess expr -> compileAccess(expr, frame, dst); + case ExpressionBin expr -> compileBin(expr, frame, dst); + case ExpressionCall expr -> compileCall(expr, frame, dst); + case ExpressionCast expr -> compileCast(expr, frame, dst); + case ExpressionChar expr -> compileChar(expr, frame, dst); + case ExpressionIdent expr -> compileIdent(expr, frame, dst); + case ExpressionIndex expr -> compileIndex(expr, frame, dst); + case ExpressionInteger expr -> compileInt(expr, frame, dst); + case ExpressionParens expr -> compileParens(expr, frame, dst); + case ExpressionPostfix expr -> compilePostfix(expr, frame, dst); + case ExpressionPrefix expr -> compilePrefix(expr, frame, dst); + case ExpressionString expr -> compileStr(expr, frame, dst); + case ExpressionSyntaxError expr -> compileSyntaxError(expr, frame, dst); + case ExpressionTernary expr -> compileTernary(expr, frame, dst); + case ExpressionTypeError expr -> compileTypeError(expr, frame, dst); + case ExpressionInitializer expr -> compileInitializer(expr, frame, dst); + } + } + + int getFieldOffset(SeaType.Struct struct, String field) { + int off = 0; + for (Map.Entry entry : struct.fields().entrySet()) { + if (entry.getKey().equals(field)) { + break; + } + off += entry.getValue().size(); + } + return off; + } + + void addRegister(MemLoc dst, int value) { + assert value >= 0; + assert dst != null && dst != STACK; + if (value == 0) return; + if (value > 15) { + MemLoc tmp = dst == T0 ? T1 : T0; + code.append(" li ").append(tmp).append(" ").append(value).append('\n'); + code.append(" add ").append(dst).append(" t0").append('\n'); + } else { + code.append(" inc ").append(dst).append(" ").append(value).append('\n'); + } + } + + void subRegister(MemLoc dst, int value) { + assert value >= 0; + assert dst != null && dst != STACK; + if (value == 0) return; + if (value > 15) { + MemLoc tmp = dst == T0 ? T1 : T0; + code.append(" li ").append(tmp).append(" ").append(value).append('\n'); + code.append(" sub ").append(dst).append(" t0").append('\n'); + } else { + code.append(" dec ").append(dst).append(" ").append(value).append('\n'); + } + } + + void compileAccess(ExpressionAccess expr, Frame frame, MemLoc dst) { + int fieldOffset = 0; + Expression base = expr; + while (base instanceof ExpressionAccess acc) { + fieldOffset += getFieldOffset((SeaType.Struct) acc.value.type(), acc.prop.content()); + base = acc.value; + } + if (!(base instanceof ExpressionIdent ident)) { + throw new UnsupportedOperationException("unimplemented"); + } + + int valueSize = expr.type().size(); + if (valueSize == 2) { + if (globalLabels.containsKey(ident.name())) { + var label = globalLabels.get(ident.name()); + code.append(" la t2 ").append(label).append('\n'); + int off = -(fieldOffset + 2); + if (dst == STACK) { + code.append(" lwo t0 t2 ").append(off).append('\n'); + code.append(" push t0\n"); + } else { + code.append(" lwo ").append(dst).append(" ").append(off).append('\n'); + } + } else { + int offset = frame.getBaseOffset(ident.name()); + int off = -(offset + fieldOffset + valueSize); + if (dst == STACK) { + code.append(" lwo t0 fp ").append(off).append("\n"); + code.append(" push t0\n"); + } else { + code.append(" lwo ").append(dst).append(" fp ").append(off).append("\n"); + } + } + } else { + assert dst == STACK; + if (globalLabels.containsKey(ident.name())) { + var label = globalLabels.get(ident.name()); + code.append(" la t2 ").append(label).append('\n'); + addRegister(T2, fieldOffset); + } else { + var offset = frame.getBaseOffset(ident.name()); + code.append(" mov t2 fp\n"); + addRegister(T2, offset + fieldOffset + valueSize); + } + subRegister(SP, valueSize); + code.append(" mcp t2 sp ").append(valueSize).append('\n'); + } + } + + void compileBin(ExpressionBin bin, Frame frame, MemLoc dst) { + switch (bin.op()) { + case "=", "+=", "-=", "*=", "/=", "%=", "&=", "|=", "^=", "<<=", ">>=" -> { + int fieldOffset = 0; + int valueSize = bin.lhs.type().size(); + LinkedList nameParts = new LinkedList<>(); + Expression base = bin.lhs; + while (base instanceof ExpressionAccess acc) { + nameParts.addFirst(acc.prop.content()); + fieldOffset += getFieldOffset((SeaType.Struct) acc.value.type(), acc.prop.content()); + base = acc.value; + } + if (!(base instanceof ExpressionIdent id)) { + throw new UnsupportedOperationException("cannot compile non-ident/access"); + } + + code.append("# store ").append(String.join(".", nameParts)).append('\n'); + + String name = id.name(); + if (valueSize <= 2) { + if (bin.op().equals("=")) { + compile(bin.rhs, frame, T0); + } else { + compile(id, frame, T0); + compile(bin.rhs, frame, T1); + switch (bin.op()) { + case "+=" -> code.append(" add t0 t1\n"); + case "-=" -> code.append(" sub t0 t1\n"); + case "*=" -> code.append(" mul t0 t1\n"); + case "/=" -> code.append(" div t0 t1\n"); + case "%=" -> code.append(" mod t0 t1\n"); + case "&=" -> code.append(" and t0 t1\n"); + case "|=" -> code.append(" or t0 t1\n"); + case "^=" -> code.append(" xor t0 t1\n"); + case "<<=" -> code.append(" lsh t0 t1\n"); + case ">>=" -> code.append(" rsh t0 t1\n"); + case String op -> throw new UnsupportedOperationException("I dunnot how to compile " + op); + } + } + + if (globalLabels.containsKey(name)) { + var label = globalLabels.get(name); + storeGlobalValue(label, fieldOffset, valueSize, T0); + } else if (frame.hasParameter(name)) { + assert fieldOffset == 0; + storeToRegister(T0, frame.getParamRegister(name)); + } else { + var offset = frame.getBaseOffset(name); + storeToFrame(T0, offset + fieldOffset, 2); + } + } else { + assert bin.op().equals("="); + compile(bin.rhs, frame, STACK); + + if (globalLabels.containsKey(name)) { + var label = globalLabels.get(name); + storeGlobalValue(label, fieldOffset, valueSize, STACK); + } else { + var offset = frame.getBaseOffset(name); + storeToFrame(STACK, offset + fieldOffset, valueSize); + } + } + } + case "<", ">", "<=", ">=", "==", "!=" -> { + int label = labelNo++; + var insr = dst == STACK ? "pushi" : "li " + dst; + compileBranchingInstruction( + bin, + frame, + "cmpFalse" + label, + "cmpEnd" + label, + () -> code.append(" ").append(insr).append(" 1\n"), + () -> code.append(" ").append(insr).append(" 0\n") + ); + } + case "&&" -> { + int label = labelNo++; + var insr = dst == STACK ? "pushi" : "li " + dst; + + String falseLabel = "andFalse" + label; + String endLabel = "andEnd" + label; + + compile(bin.lhs, frame, T4); + code.append(" eqi t4 0\n"); + code.append(" jnz ").append(falseLabel).append('\n'); + compile(bin.rhs, frame, T4); + code.append(" eqi t4 0\n"); + code.append(" jnz ").append(falseLabel).append('\n'); + code.append(" ").append(insr).append(" 1\n"); + code.append(" j ").append(endLabel).append('\n'); + code.append(falseLabel).append(":\n"); + code.append(" ").append(insr).append(" 0\n"); + code.append(endLabel).append(":\n"); + } + case "||" -> { + int label = labelNo++; + var insr = dst == STACK ? "pushi" : "li " + dst; + + String trueLabel = "orTrue" + label; + String endLabel = "orEnd" + label; + + compile(bin.lhs, frame, T4); + code.append(" eqi t4 0\n"); + code.append(" jz ").append(trueLabel).append('\n'); + compile(bin.rhs, frame, T4); + code.append(" eqi t4 0\n"); + code.append(" jz ").append(trueLabel).append('\n'); + code.append(" ").append(insr).append(" 0\n"); + code.append(" j ").append(endLabel).append('\n'); + code.append(trueLabel).append(":\n"); + code.append(" ").append(insr).append(" 1\n"); + code.append(endLabel).append(":\n"); + } + default -> { + compile(bin.lhs, frame, STACK); + compile(bin.rhs, frame, STACK); + code.append(" "); + switch (bin.op()) { + case "+" -> code.append("sadd"); + case "-" -> code.append("ssub"); + case "*" -> code.append("smul"); + case "/" -> code.append("sdiv"); + case "%" -> code.append("smod"); + case "&" -> code.append("sand"); + case "|" -> code.append("sor"); + case "^" -> code.append("sxor"); + case "<<" -> code.append("sshl"); + case ">>" -> code.append("sshr"); + case String s -> throw new RuntimeException(" unimplemented, bin " + s); + } + code.append("\n"); + if (dst != STACK) { + code.append(" pop ").append(dst).append("\n"); + } + } + } + } + + // TODO push/pop ra + void compileCall(ExpressionCall expr, Frame frame, MemLoc dst) { + if (expr.functor instanceof ExpressionIdent ident) { + var func = program.symbols.get(ident.name()); + if (!(func.type instanceof SeaType.Func f)) { + throw new UnsupportedOperationException("cannot invoke non-function"); + } + + int noParams = frame.parameters.size(); + for (int i = 0; i < noParams; i++) { + code.append(" push a").append(i).append("\n"); + } + code.append(" push ra\n"); + + for (int i = 0; i < f.params().size(); i++) { + compile(expr.args.get(i), frame, MemLoc.of("a" + i)); + } + if (f.isVararg()) { + code.append(" mov a").append(f.params().size()).append(" sp\n"); // move the starting pointer as the last hidden argument + for (int i = f.params().size(); i < expr.args.size(); i++) { + compile(expr.args.get(i), frame, STACK); + } + } + + code.append(" jal ").append(ident.name()).append("\n"); + code.append(" pop ra\n"); + for (int i = noParams - 1; i >= 0; i--) { + code.append(" pop a").append(i).append("\n"); + } + + if (!f.resultType().isVoid()) { + if (dst != STACK) { + if (dst != RV) { + code.append(" mov ").append(dst).append(" rv\n"); + } + } else { + code.append(" push rv\n"); + } + } + + } else { + throw new RuntimeException("unimplemented"); + } + } + + void compileCast(ExpressionCast ignoredExpr, Frame ignored, MemLoc ignoredDst) { + throw new RuntimeException("unimplemented"); + } + + String internConstant(Object constant) { + if (constant instanceof Character c) { + constant = (int) c; + } + if (dataLabels.containsKey(constant)) { + return dataLabels.get(constant); + } + + String label; + String valueStr; + switch (constant) { + case String s -> { + label = "str" + Integer.toHexString(s.hashCode()).toUpperCase(); + valueStr = escapeString(s); + } + case Integer i -> { + label = "num" + (i < 0 ? "_" + -i : i); + valueStr = "" + i; + } + default -> + throw new UnsupportedOperationException("cannot compile constant " + constant.getClass().getName() + " : " + constant); + } + data.append(" ").append(label).append(": ").append(valueStr).append('\n'); + dataLabels.put(constant, label); + return label; + } + + void compileChar(ExpressionChar expr, Frame ignored, MemLoc dst) { + var rune = (int) expr.content(); + if (rune >= 16) { + String label = internConstant(rune); + if (dst != STACK) { + code.append(" lw ").append(dst).append(" ").append(label).append('\n'); + } else { + code.append(" lw t0 ").append(label).append('\n'); + code.append(" spush\n"); + } + } else if (dst == STACK) { + code.append(" pushi ").append(rune).append("\n"); + } else { + code.append(" li ").append(dst).append(" ").append(rune).append("\n"); + } + } + + enum MemLoc { + A0("a0"), + T0("t0"), + T1("t1"), + T2("t2"), + T3("t3"), + T4("t4"), + T5("t5"), + SP("sp"), + RV("rv"), + STACK(null); + + public final String label; + + MemLoc(String label) { + this.label = label; + } + + public static MemLoc of(String value) { + for (MemLoc l : values()) { + if (Objects.equals(value, l.label)) { + return l; + } + } + throw new IllegalArgumentException("invalid location %s\n".formatted(escapeString(value))); + } + + @Override + public String toString() { + return label; + } + } + + void loadConstant(MemLoc dst, int value) { + code.append(" li %s %d\n".formatted(dst, value)); + } + + void loadGlobalValue(String label, int offset, int size, MemLoc dst) { + code.append("# load global %s\n".formatted(escapeString(label))); + if (size == 1) { + if (offset == 0) { + if (dst == STACK) { + code.append(String.format(" lb t0 %s\n", label)); + code.append(" push t0\n"); + } else { + code.append(String.format(" lb %s %s\n", dst, label)); + } + } else { + if (dst == STACK) { + code.append(String.format(" lbo t0 %s %d\n", label, offset)); + code.append(" push t0\n"); + } else { + code.append(String.format(" lbo %s %s %d\n", dst, label, offset)); + } + } + } else if (size == 2) { + if (offset == 0) { + if (dst == STACK) { + code.append(String.format(" lw t0 %s\n", label)); + code.append(" push t0\n"); + } else { + code.append(String.format(" lw %s %s\n", dst, label)); + } + } else { + if (dst == STACK) { + code.append(String.format(" lwo t0 %s %d\n", label, offset)); + code.append(" push t0\n"); + } else { + code.append(String.format(" lwo %s %s %d\n", dst, label, offset)); + } + } + } else { + if (dst != STACK) { + throw new AssertionError("cannot load global %s to register %s".formatted(escapeString(label), dst)); + } + + code.append(" la t0 %s\n".formatted(label)); + addRegister(T0, offset); + subRegister(SP, size); + code.append(" mcp sp t0 %d\n".formatted(size)); + } + code.append("# end load global %s\n".formatted(escapeString(label))); + } + + void storeGlobalValue(String label, int offset, int size, MemLoc src) { + if (size == 1) { + if (offset == 0) { + if (src == STACK) { + code.append(" pop t0\n"); + code.append(" sb t0 %s\n".formatted(label)); + } else { + code.append(" sb %s %s\n".formatted(src, label)); + } + } else { + if (src == STACK) { + code.append(" pop t0\n"); + code.append(" sbo t0 %s %d\n".formatted(label, offset)); + } else { + code.append(" sbo %s %s %d\n".formatted(src, label, offset)); + } + } + } else if (size == 2) { + if (offset == 0) { + if (src == STACK) { + code.append(" pop t0\n"); + code.append(" sw t0 %s\n".formatted(label)); + } else { + code.append(" sw %s %s\n".formatted(src, label)); + } + } else { + if (src == STACK) { + code.append(" pop t0\n"); + code.append(" swo t0 %s %d\n".formatted(label, offset)); + } else { + code.append(" swo %s %s %d\n".formatted(src, label, offset)); + } + } + } else { + throw new UnsupportedOperationException("bye"); + } + } + + void loadGlobalAddress(String label, MemLoc dst) { + if (dst == STACK) { + loadGlobalAddress(label, T0); + code.append(" push t0\n"); + } else { + code.append(" la %s %s\n".formatted(dst, label)); + } + } + + // TODO: value size + 2 + void compileIdent(ExpressionIdent ident, Frame frame, MemLoc dst) { + int valueSize = ident.type().size(); + if (globalLabels.containsKey(ident.name())) { + String label = globalLabels.get(ident.name()); + // TODO: global array pointers here + if (ident.type().isAPointer()) { + loadGlobalAddress(label, dst); + } else { + loadGlobalValue(label, 0, valueSize, dst); + } + } else if (frame.hasParameter(ident.name())) { + assert valueSize == 2; + var reg = frame.getParamRegister(ident.name()); + loadFromRegister(dst, reg); + } else { + var offset = frame.getBaseOffset(ident.name()); + loadFromFrame(dst, offset, valueSize); + } + } + + void loadFromRegister(MemLoc dst, MemLoc src) { + if (dst == src) return; + if (dst == STACK) { + code.append(" push %s\n".formatted(src)); + } else { + code.append(" mov %s %s\n".formatted(dst, src)); + } + } + + void storeToRegister(MemLoc src, MemLoc dst) { + if (src == dst) return; + if (src == STACK) { + code.append(" pop %s\n".formatted(dst)); + } else { + code.append(" mov %s %s\n".formatted(dst, src)); + } + } + + void storeToFrame(MemLoc src, int offset, int size) { + if (src == STACK) { + if (size <= 2) { + code.append(" pop t0\n"); + storeToFrame(T0, offset, size); + } else { + // memcpy(fp - (offset + size), sp - size, size); sp += size; + code.append(" mov t1 fp\n"); + subRegister(T1, offset + size); + code.append(" mov t2 sp\n"); + code.append(" mcp t2 t1 %d\n".formatted(size)); + addRegister(SP, size); + } + } else { + if (size == 1) { + code.append(" sbo %s fp %d\n".formatted(src, -(offset + 1))); + } else if (size == 2) { + code.append(" swo %s fp %d\n".formatted(src, -(offset + 2))); + } else { + throw new IllegalStateException("cannot store value to the stack of size > 2"); + } + } + } + + void loadFromFrame(MemLoc dst, int offset, int size) { + if (dst == STACK) { + if (size <= 2) { + loadFromFrame(T0, offset, size); + code.append(" push t0\n"); + } else { + subRegister(SP, size); + code.append(" mov t0 fp\n"); + subRegister(T0, offset); + // memcopy(sp - size, fp + offset, size); + code.append(" mcp sp t0 %d\n".formatted(size)); + } + } else { + if (size == 1) { + code.append(" lbo %s fp %d\n".formatted(dst, -(offset + 1))); + } else if (size == 2) { + code.append(" lwo %s fp %d\n".formatted(dst, -(offset + 2))); + } else { + throw new AssertionError("cannot load value from stack frame"); + } + } + } + + void compileIndex(ExpressionIndex expr, Frame frame, MemLoc dst) { + compile(expr.array, frame, STACK); + compile(expr.index, frame, T2); + if (expr.array.type().size() != 1) { + code.append(" li t0 ").append(expr.array.type().size()).append('\n'); + code.append(" mul t2 t2 t0\n"); + } + code.append(" pop t1\n"); // load the pointer into t1 + if (dst == STACK) { + code.append(" lwr t0 t1 t2\n"); + code.append(" push t0\n"); + } else { + code.append(" lwr ").append(dst).append(" t1 t2\n"); + } + } + + void compileInt(ExpressionInteger expr, Frame ignored, MemLoc dst) { + int val = expr.value; + if (val >= 0 && val < 16) { + if (dst == STACK) { + code.append(" pushi ").append(val).append('\n'); + } else { + code.append(" li ").append(dst).append(' ').append(val).append('\n'); + } + } else { + String label = internConstant(val); + if (dst == STACK) { + code.append(" lw t0 ").append(label).append('\n'); + code.append(" push t0\n"); + } else { + code.append(" lw ").append(dst).append(" ").append(label).append('\n'); + } + } + } + + void compileParens(ExpressionParens expr, Frame frame, MemLoc dst) { + compile(expr.inner, frame, dst); + } + + // a[i]++ + // ++a[i] + // load a[i] + // (hook) : value is on the stack, do whatever, leave (stack-value, assign-value) + // write a[i] from stack? + void compilePostfix(ExpressionPostfix expr, Frame frame, MemLoc dst) { + compile(expr.inner, frame, dst); + switch (expr.op()) { +// case "++" -> { +// compileWriteHooked(expr.inner, () -> { +// code.append(" sdup\n"); +// code.append(" pop t0\n"); +// code.append(" li t1 1\n"); +// code.append(" sub t0 t0 t1\n"); +// }); +// } +// case "--" -> { +// +// } + case String op -> throw new UnsupportedOperationException("don't know how to compile " + op); + } + } + + void compilePrefix(ExpressionPrefix expr, Frame frame, MemLoc dst) { + switch (expr.op()) { + case "!" -> { + compile(expr.inner, frame, T4); + String nt = "nottrue" + labelNo++; + String end = "endnot" + labelNo++; + String insr = dst == STACK ? "pushi" : "li " + dst; + code.append(" eqi t4 0\n"); + code.append(" jnz ").append(nt).append('\n'); + code.append(" ").append(insr).append(" 0\n"); + code.append(" j ").append(end).append('\n'); + code.append(nt).append(":\n"); + code.append(" ").append(insr).append(" 1\n"); + code.append(end).append(":\n"); + } + case "-" -> { + compile(expr.inner, frame, dst); + if (dst == STACK) { + code.append(" sneg\n"); + } else { + code.append(" neg ").append(dst).append('\n'); + } + } + case String op -> throw new UnsupportedOperationException("cannot compile: " + op); + } + } + + void compileStr(ExpressionString expr, Frame frame, MemLoc dst) { + String content = expr.content(); + String label = internConstant(content); + if (dst == STACK) { + code.append(" la t0 ").append(label).append('\n'); + code.append(" push t0\n"); + } else { + code.append(" la ").append(dst).append(' ').append(label).append('\n'); + } + } + + void compileSyntaxError(ExpressionSyntaxError expr, Frame frame, MemLoc dst) { + throw new UnsupportedOperationException("cannot compile errors"); + } + + void compileTernary(ExpressionTernary expr, Frame frame, MemLoc dst) { + int label = labelNo++; + compileBranchingInstruction( + expr.cond, + frame, + "ternaryElse" + label, + "ternaryEnd" + label, + () -> compile(expr.then, frame, dst), + () -> compile(expr.otherwise, frame, dst) + ); + } + + void compileBranchingInstruction( + Expression expression, + Frame frame, + String falseLabel, + String endLabel, + Runnable trueBlock, + Runnable falseBlock + ) { + compileBranchingInstruction(expression, frame, falseLabel, endLabel, trueBlock, falseBlock, false); + } + + private final static List BRANCHING_BIN_INSRS = List.of("<", ">", "<=", ">=", "==", "!="); + + void compileBranchingInstruction( + Expression expression, + Frame frame, + String condFailedLabel, + String endLabel, + Runnable trueBlock, + Runnable falseBlock, + boolean negated + ) { + if (expression instanceof ExpressionBin bin) { + if (BRANCHING_BIN_INSRS.contains(bin.op())) { + compile(bin.lhs, frame, T4); + compile(bin.rhs, frame, T5); + String insr = switch (bin.op()) { + case "<" -> "lt"; + case ">" -> "gt"; + case "<=" -> "lte"; + case ">=" -> "gte"; + case "==" -> "eq"; + case "!=" -> "neq"; + default -> throw new IllegalStateException("Unexpected value: " + bin.op()); + }; + code.append(" ").append(insr).append(" t4 t5\n"); + String jump = negated ? "jnz" : "jz"; + code.append(" ").append(jump).append(" ").append(condFailedLabel).append('\n'); + trueBlock.run(); + if (falseBlock != null) { + code.append(" j ").append(endLabel).append('\n'); + code.append(condFailedLabel).append(":\n"); + falseBlock.run(); + code.append(endLabel).append(":\n"); + } + return; + } + + if (bin.op().equals("&&")) { + // we're essentially applying De Morgan's Law here, this is `!a || !b` + compileBranchingInstruction( + bin.lhs, + frame, + condFailedLabel, + endLabel, + () -> { + compileBranchingInstruction( + bin.rhs, + frame, + condFailedLabel, + endLabel, + trueBlock, + null, + false + ); + }, + falseBlock, + negated + ); + return; + } + } + + if (expression instanceof ExpressionParens p) { + compileBranchingInstruction(p.inner, frame, condFailedLabel, endLabel, trueBlock, falseBlock, negated); + return; + } + + String insr = negated ? "jz" : "jnz"; + compile(expression, frame, T4); + code.append(" eqi t4 0\n"); + code.append(" ").append(insr).append(" ").append(condFailedLabel).append('\n'); + trueBlock.run(); + if (falseBlock != null) { + code.append(" j ").append(endLabel).append('\n'); + code.append(condFailedLabel).append(":\n"); + falseBlock.run(); + code.append(endLabel).append(":\n"); + } + } + + void compileTypeError(ExpressionTypeError expr, Frame frame, MemLoc dst) { + throw new UnsupportedOperationException(); + } + + void compileInitializer(ExpressionInitializer expr, Frame frame, MemLoc dst) { + assert dst == STACK; + for (var value : expr.values) { + compile(value, frame, dst); + } + } + + protected void compile(DeclarationTypedef ignored) { + } + + protected void compile(DeclarationVar item) throws CompilationException { + Object value = null; + if (item.initializer != null) { + value = evalConstant(item.initializer); + } + var label = internConstant(value); + globalLabels.put(item.name.content(), label); + } + + protected void compile(DeclarationStruct struct) throws CompilationException { + + } + + protected Object evalConstant(Expression expression) throws CompilationException { + return switch (expression) { + case ExpressionBin bin -> { + var lhs = evalConstant(bin.lhs); + var rhs = evalConstant(bin.rhs); + yield switch (bin.op.content()) { + case "+" -> ((int) lhs) + ((int) rhs); + case "-" -> ((int) lhs) - ((int) rhs); + case "*" -> ((int) lhs) * ((int) rhs); + case "/" -> ((int) lhs) / ((int) rhs); + case "%" -> ((int) lhs) % ((int) rhs); + case ">>" -> ((int) lhs) >> ((int) rhs); + case "<<" -> ((int) lhs) << ((int) rhs); + case "&" -> ((int) lhs) & ((int) rhs); + case "|" -> ((int) lhs) | ((int) rhs); + case "^" -> ((int) lhs) ^ ((int) rhs); + case "==" -> ((int) lhs) == ((int) rhs); + case "!=" -> ((int) lhs) != ((int) rhs); + case "<" -> ((int) lhs) < ((int) rhs); + case "<=" -> ((int) lhs) <= ((int) rhs); + case ">" -> ((int) lhs) > ((int) rhs); + case ">=" -> ((int) lhs) >= ((int) rhs); + case String op -> throw new UnsupportedOperationException("invalid constant operator: " + op); + }; + } + case ExpressionCall call -> + throw new CompilationException("invalid constant expression: function call are not allowed in constant contexts", call.functor.span()); + case ExpressionCast cast -> + throw new CompilationException("invalid constant expression: casting is not allowed in constant contexts", cast.type.span()); + case ExpressionChar chr -> (int) chr.content(); + case ExpressionIdent ident -> + throw new CompilationException("invalid constant expression: identifiers are not allowed in constant contexts", (ident).span()); + case ExpressionIndex index -> + throw new CompilationException("invalid constant expression: indexing is not allowed in constant contexts", (index.index).span()); + case ExpressionInteger exprInt -> exprInt.value; + case ExpressionParens group -> evalConstant(group.inner); + case ExpressionPostfix postfix -> { + switch (postfix.op()) { + case "++" -> + throw new CompilationException("invalid constant expression: postfix increment is not allowed in constant contexts", postfix.span()); + case "--" -> + throw new CompilationException("invalid constant expression: postfix decrement is not allowed in constant contexts", postfix.span()); + case String op -> throw new UnsupportedOperationException("unknown postfix operator " + op); + } + } + case ExpressionPrefix prefix -> switch (prefix.op()) { + case "++" -> + throw new CompilationException("invalid constant expression: prefix increment is not allowed in constant contexts", prefix.span()); + case "--" -> + throw new CompilationException("invalid constant expression: prefix decrement is not allowed in constant contexts", prefix.span()); + case "&" -> + throw new CompilationException("invalid constant expression: addressing is not allowed in constant contexts", prefix.span()); + case "*" -> + throw new CompilationException("invalid constant expression: dereference is not allowed in constant contexts", prefix.span()); + case "~" -> { + var value = evalConstant(prefix.inner); + yield ~((Integer) value); + } + case "-" -> { + var value = evalConstant(prefix.inner); + yield -((Integer) value); + } + case "!" -> { + var value = evalConstant(prefix.inner); + yield value.equals(0); + } + case "sizeof" -> { + var value = evalConstant(prefix.inner); + yield switch (value) { + case String s -> s.length(); + case Integer i -> 2; + case Object v -> + throw new CompilationException("cannot take sizeof " + v.getClass(), prefix.span()); + }; + } + default -> throw new IllegalStateException("Unexpected value: " + prefix.op()); + }; + case ExpressionString str -> str.content(); + case ExpressionSyntaxError ignored -> + throw new UnsupportedOperationException("errors should be checked before compilation"); + case ExpressionTernary expressionTernary -> { + var cond = evalConstant(expressionTernary.cond); + var condV = switch (cond) { + case Integer i -> i != 0; + default -> throw new UnsupportedOperationException("expected integer value"); + }; + + if (condV) { + yield evalConstant(expressionTernary.then); + } else { + yield evalConstant(expressionTernary.otherwise); + } + } + case ExpressionInitializer expr -> { + var values = new ArrayList<>(); + for (var val : expr.values) { + var value = evalConstant(val); + values.add(value); + } + yield values; + } + default -> + throw new UnsupportedOperationException("invalid constant expression: " + expression.getClass().getSimpleName()); + }; + } + + static class Frame { + private final LinkedHashMap parameters = new LinkedHashMap<>(); + private final LinkedHashMap paramRegisters = new LinkedHashMap<>(); + private final LinkedHashMap offsets = new LinkedHashMap<>(); + private int totalSize = 0; + + Frame(DeclarationFunc.ParamList params) { + int i = 0; + for (var param : params.params()) { + int typeSize = param.type.type().size(); + assert typeSize == 2; + parameters.put(param.name.content(), param); + paramRegisters.put(param.name.content(), MemLoc.of("a" + i)); + i += 1; + } + } + + boolean hasParameter(String name) { + return parameters.containsKey(name); + } + + MemLoc getParamRegister(String name) { + return paramRegisters.get(name); + } + + int getBaseOffset(String name) { + return offsets.get(name); + } + + void add(String name, int size) { + offsets.put(name, totalSize); + totalSize += size; + } + + void remove(String name) { + int size = offsets.get(name); + totalSize -= size; + } + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/SeaLanguage.java b/src/jvmMain/kotlin/mtmc/lang/sea/SeaLanguage.java new file mode 100644 index 0000000..0a0266e --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/SeaLanguage.java @@ -0,0 +1,47 @@ +package mtmc.lang.sea; + +import mtmc.lang.CompilationException; +import mtmc.lang.Language; +import mtmc.lang.ParseException; +import mtmc.lang.sea.ast.Error; +import mtmc.lang.sea.ast.Unit; +import mtmc.os.exec.Executable; + +public class SeaLanguage implements Language { + @Override + public Executable compileExecutable(String filename, String source) throws ParseException, CompilationException { + var tokens = Token.tokenize(source); + var parser = new SeaParser(filename, source, tokens); + Unit program = parser.parseUnit(); + var errors = program.collectErrors(); + if (!errors.isEmpty()) { + StringBuilder sb = new StringBuilder(); + for (Error error : errors) { + reportError(source, sb, error.exception()); + } + throw new RuntimeException(sb.toString()); + } + + + var compiler = new SeaCompiler(program); + return compiler.compile(); + } + + private static void reportError(String src, StringBuilder sb, ParseException e) { + sb.append("Error:\n"); + for (var msg : e.messages) { + var lo = Token.getLineAndOffset(src, msg.start().start()); + int lineNo = lo[0]; + int column = lo[1]; + var line = Token.getLineFor(src, msg.start().start()); + String prefix = " %03d:%03d | ".formatted(lineNo, column); + String info = " ".repeat(prefix.length() - 2) + "| "; + sb.append(info).append(msg.message()).append('\n'); + sb.append(prefix).append(line).append('\n'); + sb + .repeat(' ', prefix.length() + column - 1) + .repeat('^', Math.max(1, msg.end().end() - msg.start().start())); + sb.append("\n\n"); + } + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/SeaParser.java b/src/jvmMain/kotlin/mtmc/lang/sea/SeaParser.java new file mode 100644 index 0000000..b1028f5 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/SeaParser.java @@ -0,0 +1,1555 @@ +package mtmc.lang.sea; + +import mtmc.lang.ParseException; +import mtmc.lang.ParseException.Message; +import mtmc.lang.Span; +import mtmc.lang.sea.ast.Declaration; +import mtmc.lang.sea.ast.DeclarationFunc; +import mtmc.lang.sea.ast.DeclarationStruct; +import mtmc.lang.sea.ast.DeclarationSyntaxError; +import mtmc.lang.sea.ast.DeclarationTypedef; +import mtmc.lang.sea.ast.DeclarationVar; +import mtmc.lang.sea.ast.Expression; +import mtmc.lang.sea.ast.ExpressionAccess; +import mtmc.lang.sea.ast.ExpressionBin; +import mtmc.lang.sea.ast.ExpressionCall; +import mtmc.lang.sea.ast.ExpressionCast; +import mtmc.lang.sea.ast.ExpressionChar; +import mtmc.lang.sea.ast.ExpressionIdent; +import mtmc.lang.sea.ast.ExpressionIndex; +import mtmc.lang.sea.ast.ExpressionInitializer; +import mtmc.lang.sea.ast.ExpressionInteger; +import mtmc.lang.sea.ast.ExpressionParens; +import mtmc.lang.sea.ast.ExpressionPostfix; +import mtmc.lang.sea.ast.ExpressionPrefix; +import mtmc.lang.sea.ast.ExpressionString; +import mtmc.lang.sea.ast.ExpressionSyntaxError; +import mtmc.lang.sea.ast.ExpressionTernary; +import mtmc.lang.sea.ast.ExpressionTypeError; +import mtmc.lang.sea.ast.Statement; +import mtmc.lang.sea.ast.StatementBlock; +import mtmc.lang.sea.ast.StatementBreak; +import mtmc.lang.sea.ast.StatementContinue; +import mtmc.lang.sea.ast.StatementDoWhile; +import mtmc.lang.sea.ast.StatementExpression; +import mtmc.lang.sea.ast.StatementFor; +import mtmc.lang.sea.ast.StatementGoto; +import mtmc.lang.sea.ast.StatementIf; +import mtmc.lang.sea.ast.StatementReturn; +import mtmc.lang.sea.ast.StatementSyntaxError; +import mtmc.lang.sea.ast.StatementVar; +import mtmc.lang.sea.ast.StatementWhile; +import mtmc.lang.sea.ast.TypeExpr; +import mtmc.lang.sea.ast.TypeExprArray; +import mtmc.lang.sea.ast.TypeExprChar; +import mtmc.lang.sea.ast.TypeExprInt; +import mtmc.lang.sea.ast.TypeExprRef; +import mtmc.lang.sea.ast.TypeExprVoid; +import mtmc.lang.sea.ast.TypePointer; +import mtmc.lang.sea.ast.Unit; +import mtmc.util.SafeClosable; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Vector; + +import static mtmc.lang.sea.Token.Type.*; + +public class SeaParser { + private final String filename; + private final String source; + private final List tokens; + private int index = 0; + + // a list of "global" symbols + private final LinkedHashMap symbols; + private Scope scope = null; + + public SeaParser(String filename, String source, @NotNull List tokens) { + this.filename = filename; + this.source = source; + this.tokens = tokens; + this.symbols = new LinkedHashMap<>(); + } + + public Unit parseUnit() throws ParseException { + final var declarations = new ArrayList(); + + while (hasMoreTokens()) { + Token start = peekToken(); + + try { + var decl = parseDeclaration(); + declarations.add(decl); + } catch (ParseException e) { + declarations.add(new DeclarationSyntaxError(start, e)); + if (bodyBrace != null) { + consumeMatchingBrace(bodyBrace); + } else { + consumeThrough(SEMICOLON); + } + } + + if (peekToken().equals(start)) { + var msg = new Message(start, "fatal(langdev): infinite loop detected"); + throw new ParseException(msg); + } + } + + if (!symbols.containsKey("main")) { + var msg = new Message(Token.SOF, "no entrypoint, 'int main(char*)' or 'int main()' was defined"); + declarations.add(new DeclarationSyntaxError(Token.SOF, new ParseException(msg))); + } else { + var main = symbols.get("main"); + SeaType.Func type = (SeaType.Func) main.type; + if (!type.params().isEmpty() && (type.params().size() != 1 || !type.params().getFirst().isAPointerTo(SeaType.CHAR))) { + var msg = new Message(Token.SOF, "no entrypoint, 'int main(char*)' or 'int main()' was defined"); + declarations.add(new DeclarationSyntaxError(Token.SOF, new ParseException(msg))); + } + } + + return new Unit(this.filename, this.source, declarations, this.symbols); + } + + public DeclarationFunc.ParamList parseParamList() throws ParseException { + var params = new ArrayList(); + var names = new HashMap(); + boolean isVararg = false; + while (hasMoreTokens() && !match(RIGHT_PAREN)) { + if (take(DOT3)) { + isVararg = true; + break; + } + + TypeExpr paramType = parseSimpleType(); + + if (!match(LIT_IDENT)) { + throw new ParseException(new Message(paramType.span(), "expected parameter name after type declarator")); + } + var paramName = consume(); + paramType = parseCompoundType(paramType, paramName); + + SeaType paramTy = paramType.type(); + if (paramType.type().size() != 2) { + throw new ParseException( + new Message(paramName, "parameters must be word-sized arguments, the type " + paramTy.repr() + " is in correctly sized!") + ); + } + + Token prevName = names.put(paramName.content(), paramName); + if (prevName != null) { + throw new ParseException( + new Message(paramName, "the parameter name '" + paramName.content() + "' was used twice!"), + new Message(prevName, "it was previously defined here") + ); + } + + params.add(new DeclarationFunc.Param(paramType, paramName)); + + if (!take(COMMA)) break; + } + + if (!take(RIGHT_PAREN)) { + if (isVararg) { + var msg = new Message(peekToken(), "expected ')' after vararg parameters '...'"); + throw new ParseException(msg); + } + + try { + parseSimpleType(); + var msg = new Message(peekToken(), "expected ')', did you forget a comma?"); + throw new ParseException(msg); + } catch (ParseException ignored) { + } + + var msg = new Message(peekToken(), "expected ')' after parameter list"); + throw new ParseException(msg); + } + + return new DeclarationFunc.ParamList(params, isVararg); + } + + public DeclarationTypedef parseDeclarationTypedef() throws ParseException { + if (!take(KW_TYPEDEF)) { + return null; + } + var start = lastToken(); + + var type = parseSimpleType(); + + if (!take(LIT_IDENT)) { + throw new ParseException(new Message(type.span(), "expected typedef name after type declarator")); + } + var name = lastToken(); + if (symbols.containsKey(name.content())) { + throw new ParseException(new Message(name, "the symbol '" + name.content() + "' was previously defined")); + } + + type = parseCompoundType(type, name); + + if (!take(SEMICOLON)) { + throw new ParseException(new Message(lastToken(), "expected ';' after typedef")); + } + + var typedef = new DeclarationTypedef(start, type, name, type.end.end() < name.end() ? name : type.end); + symbols.put(name.content(), new Symbol(typedef)); + return typedef; + } + + public DeclarationStruct parseDeclarationStruct() throws ParseException { + if (!take(KW_STRUCT)) return null; + var start = lastToken(); + + if (!take(LIT_IDENT)) { + throw new ParseException(new Message(start, "expected type name after 'struct'")); + } + var name = lastToken(); + + if (!take(LEFT_BRACE)) { + throw new ParseException(new Message(start, "expected '{' after struct name")); + } + + var fieldNames = new HashSet(); + var fields = new ArrayList(); + while (hasMoreTokens() && !match(RIGHT_BRACE)) { + var fieldType = parseSimpleType(); + + if (!take(LIT_IDENT)) { + throw new ParseException(new Message(lastToken(), "expected field name after field type")); + } + var fieldName = lastToken(); + + if (!take(SEMICOLON)) { + throw new ParseException(new Message(lastToken(), "expected ';' after struct field")); + } + + if (!fieldNames.add(fieldName.content())) { + throw new ParseException(new Message(fieldName, "duplicate field '" + fieldName.content() + "'")); + } + + if (typeIsRecursive(name.content(), fieldType)) { + throw new ParseException(new Message(fieldName, "infinitely sized field type '" + fieldName.content() + "'")); + } + + fields.add(new DeclarationStruct.Field(fieldType, fieldName)); + } + + if (!take(RIGHT_BRACE)) { + throw new ParseException(new Message(start, "expected '}' after struct field list")); + } + var end = lastToken(); + + if (!take(SEMICOLON)) { + throw new ParseException(new Message(lastToken(), "expected ';' after struct definition")); + } + + var struct = new DeclarationStruct(start, name, fields, end); + symbols.put(name.content(), new Symbol(struct)); + return struct; + } + + boolean typeIsRecursive(LinkedHashSet parentChain, HashSet checkedStructs, SeaType type) { + return switch (type) { + case SeaType.Func func -> false; + case SeaType.Pointer pointer -> false; + case SeaType.Primitive primitive -> false; + case SeaType.Struct struct -> { + if (checkedStructs.contains(struct.name())) yield false; + if (!parentChain.add(struct.name())) yield true; + checkedStructs.add(struct.name()); + + for (SeaType ty : struct.fields().values()) { + if (typeIsRecursive(parentChain, checkedStructs, ty)) { + yield true; + } + } + + parentChain.remove(struct.name()); + yield false; + } + case SeaType.Initializer initializer -> { + throw new UnsupportedOperationException("cannot recursive check blob types!"); + } + }; + } + + boolean typeIsRecursive(String parentName, SeaType fieldType) { + return switch (fieldType) { + case SeaType.Func ignored -> false; + case SeaType.Pointer ignored -> false; + case SeaType.Primitive ignored -> false; + case SeaType.Struct struct -> { + var parentChain = new LinkedHashSet(); + parentChain.add(parentName); + var checked = new HashSet(); + yield typeIsRecursive(parentChain, checked, struct); + } + case SeaType.Initializer initializer -> { + throw new UnsupportedOperationException("cannot recursive check blob types!"); + } + }; + } + + boolean typeIsRecursive(String parentName, TypeExpr fieldType) { + return switch (fieldType) { + case TypeExprArray array -> typeIsRecursive(parentName, array.inner); + case TypeExprRef ref -> typeIsRecursive(parentName, ref.type()); + case TypeExprVoid ignored -> false; + case TypeExprChar ignored -> false; + case TypeExprInt ignored -> false; + case TypePointer ignored -> false; + }; + } + + Token bodyBrace = null; + + @NotNull + public Declaration parseDeclaration() throws ParseException { + var typedef = parseDeclarationTypedef(); + if (typedef != null) return typedef; + + var struct = parseDeclarationStruct(); + if (struct != null) return struct; + + TypeExpr type; + final var t2 = peekToken2(); + if (t2.type().equals(EQUAL) || t2.type().equals(SEMICOLON) || t2.type().equals(LEFT_PAREN)) { + type = new TypeExprInt(peekToken()); + } else { + type = parseSimpleType(); + } + + if (!match(LIT_IDENT)) { + throw new ParseException(new Message(type.span(), "expected declaration name after type expression")); + } + final Token name = consume(); + if (symbols.containsKey(name.content())) { + throw new ParseException(new Message(name, "the symbol '" + name.content() + "' was previously defined")); + } + + if (take(LEFT_PAREN)) { + var lparen = lastToken(); + var paramList = parseParamList(); + + if (paramList.isVararg() && paramList.size() > 3) { + throw new ParseException(new Message(Span.of(lparen, lastToken()), "a vararg function can have at most 3 parameters!")); + } else if (paramList.size() > 4) { + throw new ParseException(new Message(Span.of(lparen, lastToken()), "a function can have at most 4 parameters!")); + } + + StatementBlock body = null; + if (!match(LEFT_BRACE)) { + if (!take(SEMICOLON)) { + throw new ParseException(new Message(lastToken(), "expected ';' after function declaration")); + } + } else { + bodyBrace = peekToken(); + scope = new Scope(paramList); + body = parseStatementBlock(); + scope = null; + bodyBrace = null; + } + + var func = new DeclarationFunc(type, name, paramList, body, body == null ? lastToken() : body.end); + symbols.put(name.content(), new Symbol(func)); + return func; + } else { + type = parseCompoundType(type, name); + + Expression init = null; + if (take(EQUAL)) { + init = parseExpression(); + } + + if (!take(SEMICOLON)) { + throw new ParseException(new Message(init == null ? Span.of(lastToken()) : init.span(), "expected ';' after variable declaration")); + } + + var decl = new DeclarationVar(type, name, init); + symbols.put(name.content(), new Symbol(decl)); + return decl; + } + } + + public TypeExpr parseCompoundType(TypeExpr simpleType, @Nullable Token ignored) throws ParseException { + if (take(LEFT_BRACKET)) { + if (!take(RIGHT_BRACKET)) { + var msg = new Message(peekToken(), "expected '[]' in array type"); + throw new ParseException(msg); + } + return new TypeExprArray(simpleType, lastToken()); + } else { + return simpleType; + } + } + + public TypeExpr parseSimpleType() throws ParseException { + TypeExpr type = parseTypeName(); + while (match(STAR)) { + final var tok = consume(); + type = new TypePointer(type, tok); + } + return type; + } + + public boolean peekTypeName() { + if (match(KW_INT, KW_CHAR)) return true; + if (!match(LIT_IDENT)) return false; + var ident = peekToken(); + if (!symbols.containsKey(ident.content())) return false; + return symbols.get(ident.content()).typeDecl != null; + } + + @NotNull + public TypeExpr parseTypeName() throws ParseException { + if (take(KW_INT)) return new TypeExprInt(lastToken()); + if (take(KW_CHAR)) return new TypeExprChar(lastToken()); + if (take(KW_VOID)) return new TypeExprVoid(lastToken()); + + if (!take(LIT_IDENT)) { + throw new ParseException(new Message(peekToken(), "expected a simple type name")); + } + var tok = lastToken(); + if (symbols.containsKey(tok.content())) { + var sym = symbols.get(tok.content()); + if (sym.typeDecl != null) { + return new TypeExprRef(tok, sym.typeDecl); + } else { + throw new ParseException(new Message(tok, tok.content() + " is not a type")); + } + } else { + throw new ParseException(new Message(tok, "unknown type '" + tok.content() + "'")); + } + } + + public Statement parseStatement() throws ParseException { + if (scope == null) { + scope = new Scope(new DeclarationFunc.ParamList(new ArrayList<>(), false)); + } + Token label = null; + if (peekToken2().type().equals(COLON)) { + if (!take(LIT_IDENT)) { + throw new ParseException(new Message(peekToken(), "expected label!")); + } + label = lastToken(); + consume(); + } + + var blockStmt = parseStatementBlock(); + if (blockStmt != null) { + blockStmt.setLabelAnchor(label); + return blockStmt; + } + + var ifStmt = parseStatementIf(); + if (ifStmt != null) { + ifStmt.setLabelAnchor(label); + return ifStmt; + } + + var forStmt = parseStatementFor(); + if (forStmt != null) { + forStmt.setLabelAnchor(label); + return forStmt; + } + + var whileStmt = parseStatementWhile(); + if (whileStmt != null) { + whileStmt.setLabelAnchor(label); + return whileStmt; + } + + var doWhileStmt = parseStatementDoWhile(); + if (doWhileStmt != null) { + if (!take(SEMICOLON)) + throw new ParseException(new Message(lastToken(), "expected ';' after do-while statement")); + doWhileStmt.setLabelAnchor(label); + return doWhileStmt; + } + + var varStmt = parseStatementVar(); + if (varStmt != null) { + if (!take(SEMICOLON)) + throw new ParseException(new Message(lastToken(), "expected ';' after variable statement")); + varStmt.setLabelAnchor(label); + return varStmt; + } + + var gotoStmt = parseGotoStatement(); + if (gotoStmt != null) { + if (!take(SEMICOLON)) + throw new ParseException(new Message(lastToken(), "expected ';' after goto statement")); + gotoStmt.setLabelAnchor(label); + return gotoStmt; + } + + var continueStmt = parseStatementContinue(); + if (continueStmt != null) { + if (!take(SEMICOLON)) + throw new ParseException(new Message(lastToken(), "expected ';' after continue statement")); + continueStmt.setLabelAnchor(label); + return continueStmt; + } + + var breakStmt = parseStatementBreak(); + if (breakStmt != null) { + if (!take(SEMICOLON)) + throw new ParseException(new Message(lastToken(), "expected ';' after break statement")); + breakStmt.setLabelAnchor(label); + return breakStmt; + } + + var returnStmt = parseStatementReturn(); + if (returnStmt != null) { + if (!take(SEMICOLON)) + throw new ParseException(new Message(lastToken(), "expected ';' after return statement")); + returnStmt.setLabelAnchor(label); + return returnStmt; + } + + Expression expr = parseExpression(); + if (expr != null) { + if (!take(SEMICOLON)) throw new ParseException(new Message(lastToken(), "expected ';' after expression")); + var stmt = new StatementExpression(expr); + stmt.setLabelAnchor(label); + return stmt; + } + + throw new ParseException(new Message(lastToken(), "expected statement")); + } + + StatementIf parseStatementIf() throws ParseException { + if (!take(KW_IF)) return null; + var start = lastToken(); + + if (!take(LEFT_PAREN)) throw new ParseException(new Message(lastToken(), "expected '(' after 'if'")); + var cond = parseExpression(); + if (cond == null) throw new ParseException(new Message(lastToken(), "expected 'if' condition")); + if (!take(RIGHT_PAREN)) throw new ParseException(new Message(lastToken(), "expected ')' after 'if'")); + + var body = parseStatement(); + + Statement elseBody = null; + if (take(KW_ELSE)) { + elseBody = parseStatement(); + } + + return new StatementIf(start, cond, body, elseBody); + } + + StatementFor parseStatementFor() throws ParseException { + if (!take(KW_FOR)) return null; + var start = lastToken(); + + if (!take(LEFT_PAREN)) throw new ParseException(new Message(lastToken(), "expected '(' after 'for'")); + + try (var _frame = scope.push()) { + Expression initExpr = null; + StatementVar initStmt = null; + if (peekTypeName()) { + initStmt = parseStatementVar(); + } else { + initExpr = parseExpression(); + } + + if (!take(SEMICOLON)) { + throw new ParseException(new Message(peekToken(), "expected ';' after for initializer")); + } + + @Nullable Expression condition = parseExpression(); + + if (!take(SEMICOLON)) + throw new ParseException(new Message(peekToken(), "expected ';' after for condition")); + Expression incr = parseExpression(); + + if (!take(RIGHT_PAREN)) + throw new ParseException(new Message(peekToken(), "expected ')' after for-loop condition")); + + var body = parseStatement(); + return new StatementFor(start, initExpr, initStmt, condition, incr, body); + } + } + + Symbol resolveSymbol(String name) { + if (scope != null) { + Symbol symbol = scope.getOrNull(name); + if (symbol != null) { + return symbol; + } + } + if (symbols.containsKey(name)) { + return symbols.get(name); + } + return null; + } + + StatementVar parseStatementVar() throws ParseException { + if (!peekTypeName()) return null; + + TypeExpr type = parseSimpleType(); + + if (!take(LIT_IDENT)) throw new ParseException(new Message(peekToken(), "expected variable name")); + Token name = lastToken(); + type = parseCompoundType(type, name); + + if (scope.has(name.content())) { + throw new ParseException(new Message(name, "the symbol '" + name.content() + "' shadows a previously defined symbol")); + } + + Expression value = null; + if (take(EQUAL)) { + value = parseExpression(); + if (value == null) { + throw new ParseException(new Message(lastToken(), "expected initializer value after '='")); + } + + try { + value.type().checkConversionTo(type.type()); + } catch (SeaType.ConversionError error) { + value = new ExpressionTypeError(value, "cannot assign " + value.type().repr() + " to " + + type.type().repr() + ": " + error.getMessage()); + } + } + + var stmt = new StatementVar(type, name, value); + scope.define(stmt); + return stmt; + } + + StatementWhile parseStatementWhile() throws ParseException { + if (!take(KW_WHILE)) return null; + var start = lastToken(); + + if (!take(LEFT_PAREN)) throw new ParseException(new Message(lastToken(), "expected '(' after 'while'")); + Expression condition = parseExpression(); + if (condition == null) throw new ParseException(new Message(lastToken(), "expected while-condition")); + if (!take(RIGHT_PAREN)) throw new ParseException(new Message(lastToken(), "expected ')' after 'while'")); + + var body = parseStatement(); + return new StatementWhile(start, condition, body); + } + + StatementDoWhile parseStatementDoWhile() throws ParseException { + if (!take(KW_DO)) return null; + var start = lastToken(); + + var body = parseStatement(); + + if (!take(KW_WHILE)) { + throw new ParseException(new Message(lastToken(), "expected 'while' after do body")); + } + + if (!take(LEFT_PAREN)) throw new ParseException(new Message(lastToken(), "expected '(' after do body")); + Expression condition = parseExpression(); + if (condition == null) throw new ParseException(new Message(lastToken(), "expected do-while-condition")); + if (!take(RIGHT_PAREN)) + throw new ParseException(new Message(lastToken(), "expected ')' after do-while condition")); + + return new StatementDoWhile(start, body, condition, lastToken()); + } + + StatementGoto parseGotoStatement() throws ParseException { + if (!take(KW_GOTO)) return null; + var start = lastToken(); + + if (!take(LIT_IDENT)) throw new ParseException(new Message(peekToken(), "expected label name after 'goto'")); + var labelName = lastToken(); + + return new StatementGoto(start, labelName); + } + + StatementBreak parseStatementBreak() { + if (!take(KW_BREAK)) return null; + return new StatementBreak(lastToken()); + } + + StatementReturn parseStatementReturn() { + if (!take(KW_RETURN)) return null; + var start = lastToken(); + + Expression value = null; + if (!match(SEMICOLON)) { + value = parseExpression(); + } + + return new StatementReturn(start, value); + } + + StatementContinue parseStatementContinue() { + if (!take(KW_CONTINUE)) return null; + return new StatementContinue(lastToken()); + } + + void recover(Token openingBrace) throws ParseException { + var pos = index; + index = tokens.indexOf(openingBrace) + 1; + int open = 1; + while (hasMoreTokens() && open > 0 && index < pos) { + if (take(LEFT_BRACE)) open++; + else if (take(RIGHT_BRACE)) open--; + else consume(); + } + + // we want to consume all open braces from other blocks + index = pos; + while (open > 1) { + if (take(LEFT_BRACE)) open++; + else if (take(RIGHT_BRACE)) open--; + else consume(); + } + + while (hasMoreTokens()) { + if (match(RIGHT_BRACE, SEMICOLON)) return; + if (match(KW_IF, KW_FOR, KW_DO, KW_WHILE, KW_CONTINUE, KW_BREAK, KW_RETURN, KW_GOTO)) return; + consume(); + } + } + + @Nullable + public StatementBlock parseStatementBlock() throws ParseException { + if (!take(LEFT_BRACE)) return null; + var start = lastToken(); + + try (var ignoredLayer = scope.push()) { + var stmts = new ArrayList(); + while (hasMoreTokens() && !match(RIGHT_BRACE)) { + var before = peekToken(); + try { + var stmt = parseStatement(); + stmts.add(stmt); + } catch (ParseException e) { + var stmt = new StatementSyntaxError(before, e); + stmts.add(stmt); + recover(start); + if (peekToken() == before) break; + } + } + + if (!take(RIGHT_BRACE)) { + var msg = new Message(lastToken(), "expected '}' after block statement"); + throw new ParseException(msg); + } + + return new StatementBlock(start, stmts, lastToken()); + } + } + + public Expression parseExpression() { + return parseAssignExpression(); + } + + public Expression parseAssignExpression() { + var expr = parseTernaryExpression(); + if (expr == null) return null; + + Token.Type[] types = {EQUAL, STAR_EQ, SLASH_EQ, PERCENT_EQ, PLUS_EQ, DASH_EQ, LEFT_ARROW2_EQ, RIGHT_ARROW2_EQ, AMPERSAND_EQ, BAR_EQ}; + + if (take(types)) { + var op = lastToken(); + var rhs = parseTernaryExpression(); + if (rhs == null) { + rhs = new ExpressionSyntaxError(peekToken(), "expected right hand side of assignment expression"); + } + + if (valueKind(expr) != Expression.ValueKind.Addressable) { + expr = new ExpressionTypeError(expr, "cannot assign to rvalue"); + } + try { + rhs.type().checkConversionTo(expr.type()); + } catch (SeaType.ConversionError error) { + rhs = new ExpressionTypeError(rhs, "cannot assign " + rhs.type().repr() + " to " + expr.type().repr() + ": " + error.getMessage()); + } + + expr = new ExpressionBin(expr, op, rhs, SeaType.VOID); + } + + if (match(types)) { + expr = new ExpressionSyntaxError(expr, peekToken(), "chained assignments are not allowed"); + } + + return expr; + } + + public Expression parseTernaryExpression() { + var expr = parseOrExpression(); + if (expr == null) return null; + + if (take(QUESTION)) { + var then = parseTernaryExpression(); + if (then == null) { + then = new ExpressionSyntaxError(lastToken(), "expected then-expression after ternary '?'"); + consumeTo(COLON); + } + + if (!take(COLON)) { + expr = new ExpressionSyntaxError(lastToken(), "expected ':' in ternary value"); + } + + var otherwise = parseTernaryExpression(); + if (otherwise == null) { + otherwise = new ExpressionSyntaxError(lastToken(), "expected else-expression after ternary ':'"); + } + + var condType = expr.type(); + if (!condType.isIntegral()) { + expr = new ExpressionTypeError(expr, "cannot interpret " + condType.repr() + " as a boolean"); + } + + var thenType = then.type(); + var otherType = otherwise.type(); + + if (thenType.isVoid()) { + then = new ExpressionTypeError(then, "ternary branch cannot yield void"); + } + if (otherType.isVoid()) { + otherwise = new ExpressionTypeError(otherwise, "ternary branch cannot yield void"); + } + + try { + otherType.checkConversionTo(thenType); + } catch (SeaType.ConversionError error) { + otherwise = new ExpressionTypeError(otherwise, "the ternary branches disagree, " + otherType.repr() + " is not assignable to " + then.type() + ": " + error.getMessage()); + } + + expr = new ExpressionTernary(expr, then, otherwise, thenType); + } + + return expr; + } + + public Expression parseOrExpression() { + var expr = parseAndExpression(); + if (expr == null) return null; + + while (take(BAR2)) { + var op = lastToken(); + var rhs = parseAndExpression(); + if (rhs == null) { + rhs = new ExpressionSyntaxError(lastToken(), "expected right hand side of logical-or expression"); + } + + var lhsType = expr.type(); + if (!lhsType.isIntegral()) { + expr = new ExpressionTypeError(expr, "cannot logical or the type " + lhsType.repr()); + } + + var rhsType = rhs.type(); + if (!rhsType.isIntegral()) { + rhs = new ExpressionTypeError(rhs, "cannot logical or the type " + rhsType.repr()); + } + expr = new ExpressionBin(expr, op, rhs, SeaType.INT); + } + + return expr; + } + + public Expression parseAndExpression() { + var expr = parseBinaryOrExpression(); + if (expr == null) return null; + + while (take(AMPERSAND2)) { + var op = lastToken(); + var rhs = parseBinaryOrExpression(); + if (rhs == null) { + rhs = new ExpressionSyntaxError(lastToken(), "expected right hand side of logical-and expression"); + } + + var lhsType = expr.type(); + var rhsType = rhs.type(); + if (!lhsType.isIntegral()) { + expr = new ExpressionTypeError(expr, "cannot logical and the type " + lhsType.repr()); + } else if (!rhsType.isIntegral()) { + rhs = new ExpressionTypeError(rhs, "cannot logical and the type " + rhsType.repr()); + } + + expr = new ExpressionBin(expr, op, rhs, SeaType.INT); + } + + return expr; + } + + public Expression parseBinaryOrExpression() { + var expr = parseBinaryXOrExpression(); + if (expr == null) return null; + + while (take(BAR)) { + var op = lastToken(); + var rhs = parseBinaryXOrExpression(); + if (rhs == null) { + rhs = new ExpressionSyntaxError(lastToken(), "expected right hand side of bitwise-or expression"); + } + + var lhsType = expr.type(); + var rhsType = rhs.type(); + if (!lhsType.isArithmetic()) { + expr = new ExpressionTypeError(expr, "cannot bitwise-or the type " + lhsType.repr()); + } else if (!rhsType.isArithmetic()) { + rhs = new ExpressionTypeError(rhs, "cannot bitwise-or the type " + rhsType.repr()); + } + + expr = new ExpressionBin(expr, op, rhs, SeaType.INT); + } + + return expr; + } + + public Expression parseBinaryXOrExpression() { + var expr = parseBinaryAndExpression(); + if (expr == null) return null; + + while (take(CARET)) { + var op = lastToken(); + var rhs = parseBinaryAndExpression(); + if (rhs == null) { + rhs = new ExpressionSyntaxError(lastToken(), "expected right hand side of bitwise-xor expression"); + } + + var lhsType = expr.type(); + var rhsType = rhs.type(); + if (!lhsType.isArithmetic()) { + expr = new ExpressionTypeError(expr, "cannot bitwise-xor the type " + lhsType.repr()); + } else if (!rhsType.isArithmetic()) { + rhs = new ExpressionTypeError(rhs, "cannot bitwise-xor the type " + rhsType.repr()); + } + + SeaType type = SeaType.INT; + expr = new ExpressionBin(expr, op, rhs, type); + } + + return expr; + } + + public Expression parseBinaryAndExpression() { + var expr = parseEqualityExpression(); + if (expr == null) return null; + + while (take(AMPERSAND)) { + var op = lastToken(); + var rhs = parseEqualityExpression(); + if (rhs == null) { + rhs = new ExpressionSyntaxError(lastToken(), "expected right hand side of bitwise-and expression"); + } + + var lhsType = expr.type(); + var rhsType = rhs.type(); + if (!lhsType.isArithmetic()) { + expr = new ExpressionTypeError(expr, "cannot bitwise-and the type " + lhsType.repr()); + } else if (!rhsType.isArithmetic()) { + rhs = new ExpressionTypeError(rhs, "cannot bitwise-and the type " + rhsType.repr()); + } + + expr = new ExpressionBin(expr, op, rhs, SeaType.INT); + } + + return expr; + } + + public Expression parseEqualityExpression() { + var expr = parseComparisonExpression(); + if (expr == null) return null; + + while (take(EQUAL2, BANG_EQ)) { + var op = lastToken(); + var rhs = parseComparisonExpression(); + if (rhs == null) { + rhs = new ExpressionSyntaxError(lastToken(), "expected right hand side of equality expression"); + } + + var lhsType = expr.type(); + var rhsType = rhs.type(); + if (!lhsType.isIntegral()) { + expr = new ExpressionTypeError(expr, "cannot compare the type " + lhsType.repr()); + } else if (!rhsType.isIntegral()) { + rhs = new ExpressionTypeError(rhs, "cannot compare the type " + rhsType.repr()); + } + + expr = new ExpressionBin(expr, op, rhs, SeaType.INT); + } + + return expr; + } + + public Expression parseComparisonExpression() { + var expr = parseShiftExpression(); + if (expr == null) return null; + + while (take(LEFT_ARROW, LEFT_ARROW_EQ, RIGHT_ARROW, RIGHT_ARROW_EQ)) { + var op = lastToken(); + var rhs = parseShiftExpression(); + if (rhs == null) { + rhs = new ExpressionSyntaxError(lastToken(), "expected right hand side of comparison expression"); + } + + var lhsType = expr.type(); + var rhsType = rhs.type(); + if (!lhsType.isIntegral()) { + expr = new ExpressionTypeError(expr, "cannot compare the type " + lhsType.repr()); + } else if (!rhsType.isIntegral()) { + rhs = new ExpressionTypeError(rhs, "cannot compare the type " + rhsType.repr()); + } + + expr = new ExpressionBin(expr, op, rhs, SeaType.INT); + } + + return expr; + } + + public Expression parseShiftExpression() { + var expr = parseAdditiveExpression(); + if (expr == null) return null; + + while (take(LEFT_ARROW2, RIGHT_ARROW2)) { + var op = lastToken(); + var rhs = parseAdditiveExpression(); + if (rhs == null) { + rhs = new ExpressionSyntaxError(lastToken(), "expected right hand side of shift expression"); + } + + String term = switch (op.content()) { + case "<<" -> "left-shift"; + case ">>" -> "right-shift"; + default -> throw new UnsupportedOperationException(); + }; + + var lhsType = expr.type(); + if (!lhsType.isArithmetic()) { + expr = new ExpressionTypeError(expr, "cannot " + term + " the type " + lhsType.repr()); + } + + var rhsType = rhs.type(); + if (!rhsType.isArithmetic()) { + rhs = new ExpressionTypeError(rhs, "cannot " + term + " the type " + rhsType.repr()); + } + expr = new ExpressionBin(expr, op, rhs, arithmetic(lhsType, rhsType)); + } + + return expr; + } + + SeaType arithmetic(SeaType lhs, SeaType rhs) { + if (lhs.equals(rhs)) return lhs; + if (lhs.isInt() || rhs.isInt()) return SeaType.INT; + return lhs; + } + + public Expression parseAdditiveExpression() { + var expr = parseMultiplicativeExpression(); + if (expr == null) return null; + + while (take(PLUS, DASH)) { + var op = lastToken(); + var rhs = parseMultiplicativeExpression(); + if (rhs == null) { + rhs = new ExpressionSyntaxError(lastToken(), "expected right hand side of additive expression"); + } + + String term = switch (op.content()) { + case "+" -> "add"; + case "-" -> "subtract"; + default -> throw new IllegalStateException("impossible"); + }; + + var lhsType = expr.type(); + var rhsType = rhs.type(); + + SeaType type; + if (!lhsType.isIntegral()) { + expr = new ExpressionTypeError(expr, "cannot " + term + " " + lhsType.repr() + " with " + rhsType.repr()); + type = SeaType.INT; + } else if (!rhsType.isIntegral()) { + rhs = new ExpressionTypeError(rhs, "cannot " + term + " " + lhsType.repr() + " with " + rhsType.repr()); + type = SeaType.INT; + } else if (lhsType.isAPointer() && rhsType.isAPointer()) { + if (!op.content().equals("-")) { + expr = new ExpressionTypeError(expr, "cannot " + term + " " + lhsType.repr() + " with " + rhsType.repr()); + } + type = SeaType.INT; + } else if (lhsType.isArithmetic() && rhsType.isArithmetic()) { + type = arithmetic(lhsType, rhsType); + } else if (lhsType.isAPointer() && rhsType.isArithmetic()) { + type = lhsType; + } else if (rhsType.isAPointer() && lhsType.isArithmetic()) { + type = rhsType; + } else { + throw new IllegalStateException("unreachable?"); + } + + expr = new ExpressionBin(expr, op, rhs, type); + } + + return expr; + } + + public Expression parseMultiplicativeExpression() { + var expr = parseCastExpression(); + if (expr == null) return null; + + while (take(STAR, SLASH, PERCENT)) { + var op = lastToken(); + var rhs = parseCastExpression(); + if (rhs == null) { + rhs = new ExpressionSyntaxError(lastToken(), "expected right hand side of multiplicative expression"); + } + + String term = switch (op.content()) { + case "*" -> "product"; + case "/" -> "division"; + case "%" -> "remainder"; + default -> throw new UnsupportedOperationException(); + }; + + var lhsType = expr.type(); + var rhsType = rhs.type(); + SeaType type; + if (!lhsType.isArithmetic()) { + expr = new ExpressionTypeError(expr, "cannot take the " + term + " of " + lhsType.repr() + " and " + rhsType.repr()); + type = SeaType.INT; + } else if (!rhsType.isArithmetic()) { + expr = new ExpressionTypeError(expr, "cannot take the " + term + " of " + lhsType.repr() + " and " + rhsType.repr()); + type = SeaType.INT; + } else { + type = arithmetic(lhsType, rhsType); + } + + expr = new ExpressionBin(expr, op, rhs, type); + } + + return expr; + } + + public Expression parseCastExpression() { + var start = index; + if (!take(LEFT_PAREN)) return parsePrefixExpression(); + var startToken = lastToken(); + + try { + var type = parseSimpleType(); + if (!take(RIGHT_PAREN)) { + return new ExpressionSyntaxError(lastToken(), "expected type name after cast parens"); + } + + var exprValue = parsePrefixExpression(); + if (exprValue == null) { + return new ExpressionSyntaxError(lastToken(), "expected expression after cast"); + } + + if (!exprValue.type().isCastableTo(type.type())) { + exprValue = new ExpressionTypeError(exprValue, "cannot cast " + exprValue.type().repr() + " to " + type.type().repr()); + } + + return new ExpressionCast(startToken, type, exprValue); + } catch (ParseException e) { + index = start; + return parsePrefixExpression(); + } + } + + public Expression parsePrefixExpression() { + var stack = new Vector(); + while (match(PLUS2, DASH2, AMPERSAND, STAR, PLUS, DASH, TILDE, BANG, KW_SIZEOF)) { + stack.addLast(consume()); + } + + var expr = parsePostfixExpression(); + if (expr == null) { + if (stack.isEmpty()) return null; + return new ExpressionSyntaxError(lastToken(), "expected expression after unary operator"); + } + while (!stack.isEmpty()) { + var op = stack.removeLast(); + var exprTy = expr.type(); + var type = switch (op.content()) { + case "++", "--" -> { + if (!exprTy.isIntegral()) { + expr = new ExpressionTypeError(expr, "prefix operator '" + op.content() + "' is undefined on the type " + exprTy.repr()); + yield SeaType.INT; + } + if (valueKind(expr) != Expression.ValueKind.Addressable) { + expr = new ExpressionTypeError(expr, "cannot modify rvalue"); + } + yield exprTy; + } + case "-", "~", "!" -> { + if (!exprTy.isArithmetic()) { + expr = new ExpressionTypeError(expr, "prefix operator '" + op.content() + "' is undefined on the type " + exprTy.repr()); + yield SeaType.INT; + } + + if (op.content().equals("!")) { + yield SeaType.INT; + } else { + yield exprTy; + } + } + case "*" -> { + if (!exprTy.isAPointer()) { + expr = new ExpressionTypeError(expr, "cannot deference the type " + exprTy.repr()); + yield SeaType.INT; + } + yield exprTy.componentType(); + } + case "&" -> { + if (valueKind(expr) != Expression.ValueKind.Addressable) { + expr = new ExpressionTypeError(expr, "cannot reference non-lvalue"); + } + yield new SeaType.Pointer(expr.type()); + } + default -> throw new IllegalStateException("Unexpected value: " + op.content()); + }; + expr = new ExpressionPrefix(op, expr, type); + } + + return expr; + } + + public Expression.ValueKind valueKind(Expression expr) { + return switch (expr) { + case ExpressionAccess ignored -> Expression.ValueKind.Addressable; + case ExpressionBin ignored -> Expression.ValueKind.Immediate; + case ExpressionCall ignored -> Expression.ValueKind.Immediate; + case ExpressionCast ignored -> Expression.ValueKind.Immediate; + case ExpressionChar ignored -> Expression.ValueKind.Immediate; + case ExpressionIdent id -> { + if (!id.isAddressable) yield Expression.ValueKind.Immediate; + yield Expression.ValueKind.Addressable; + } + case ExpressionInitializer ignored -> Expression.ValueKind.Immediate; + case ExpressionIndex ignored -> Expression.ValueKind.Addressable; + case ExpressionInteger ignored -> Expression.ValueKind.Immediate; + case ExpressionParens expressionParens -> valueKind(expressionParens.inner); + case ExpressionPostfix ignored -> Expression.ValueKind.Immediate; + case ExpressionPrefix prefix -> { + if (prefix.op().equals("*")) { + yield Expression.ValueKind.Addressable; + } else { + yield Expression.ValueKind.Immediate; + } + } + case ExpressionString ignored -> Expression.ValueKind.Addressable; + case ExpressionSyntaxError ignored -> Expression.ValueKind.Immediate; + case ExpressionTernary ignored -> Expression.ValueKind.Immediate; + case ExpressionTypeError ignored -> valueKind(ignored.inner); + }; + } + + public Expression parsePostfixExpression() { + var expr = parsePrimaryExpression(); + if (expr == null) return null; + + while (hasMoreTokens()) { + if (take(PLUS2, DASH2)) { + var op = lastToken(); + var exprType = expr.type(); + if (!exprType.isIntegral()) { + expr = new ExpressionTypeError(expr, "cannot increment non-integer type " + exprType.repr()); + } + if (valueKind(expr) != Expression.ValueKind.Addressable) { + expr = new ExpressionTypeError(expr, "cannot modify rvalue"); + } + + expr = new ExpressionPostfix(expr, op, exprType); + } else if (take(DOT, ARROW)) { + var access = lastToken(); + if (!take(LIT_IDENT)) { + return new ExpressionSyntaxError(lastToken(), "expected property name after accessor"); + } + var prop = lastToken(); + + SeaType type = SeaType.INT; + var parentTy = expr.type(); + if (!(parentTy instanceof SeaType.Struct structType)) { + expr = new ExpressionTypeError(expr, "cannot access non-structure types"); + } else if (!structType.fields().containsKey(prop.content())) { + expr = new ExpressionAccess(expr, access, prop, SeaType.INT); + return new ExpressionTypeError(expr, "the struct " + parentTy.repr() + " has no field '" + prop.content() + "'"); + } else { + type = structType.field(prop.content()); + } + + expr = new ExpressionAccess(expr, access, prop, type); + } else if (take(LEFT_PAREN)) { + var args = new ArrayList(); + while (hasMoreTokens() && !match(RIGHT_PAREN)) { + var arg = parseExpression(); + if (arg instanceof ExpressionSyntaxError) { + consumeTo(COMMA, RIGHT_PAREN); + } else if (arg == null) { + arg = new ExpressionSyntaxError(lastToken(), "expected call argument"); + } + args.add(arg); + + if (!take(COMMA)) break; + } + + if (!take(RIGHT_PAREN)) { + var trailing = parseExpression(); + if (trailing instanceof ExpressionSyntaxError) { + return new ExpressionSyntaxError(trailing.start, "expected ')', did you forget a comma?"); + } else { + return new ExpressionSyntaxError(lastToken(), "expected ')' after function call arguments"); + } + } + + SeaType type; + var functorTy = expr.type(); + if (!(functorTy instanceof SeaType.Func(List params, boolean isVararg, SeaType result))) { + expr = new ExpressionTypeError(expr, "cannot invoke " + functorTy.repr()); + type = SeaType.INT; + } else if (isVararg ? args.size() < params.size() : args.size() != params.size()) { + var s = new StringBuilder(); + s.append("argument mismatch, expected ("); + for (int i = 0; i < params.size(); i++) { + if (i > 0) s.append(", "); + s.append(params.get(i).repr()); + } + s.append(isVararg ? ", ...)" : ")"); + s.append(", instead found ("); + for (int i = 0; i < args.size(); i++) { + if (i > 0) s.append(", "); + s.append(args.get(i).type().repr()); + } + s.append(")"); + expr = new ExpressionTypeError(expr, s.toString()); + type = result; + } else { + for (int i = 0; i < params.size(); i++) { + var paramTy = params.get(i); + var argTy = args.get(i).type(); + try { + argTy.checkConversionTo(paramTy); + continue; + } catch (SeaType.ConversionError ignored) { + } + + String s = "argument of type " + argTy.repr() + " is not convertible to " + paramTy.repr(); + args.set(i, new ExpressionTypeError(args.get(i), s)); + } + + type = result; + } + + expr = new ExpressionCall(expr, args, lastToken(), type); + } else if (take(LEFT_BRACKET)) { + var index = parseExpression(); + if (index == null) { + return new ExpressionSyntaxError(lastToken(), "expected index after '['"); + } + if (!take(RIGHT_BRACKET)) { + return new ExpressionSyntaxError(lastToken(), "expected ']' after array index"); + } + if (!index.type().isArithmetic()) { + expr = new ExpressionTypeError(index, "index must be an integral type"); + } + + SeaType resultType; + var arrayType = expr.type(); + if (!arrayType.isAPointer()) { + expr = new ExpressionTypeError(expr, "cannot index " + arrayType.repr()); + resultType = SeaType.INT; + } else if (arrayType.componentType().isVoid()) { + expr = new ExpressionTypeError(expr, "cannot index void*, it has no size"); + resultType = arrayType.componentType(); + } else { + resultType = arrayType.componentType(); + } + expr = new ExpressionIndex(expr, index, lastToken(), resultType); + } else { + break; + } + } + + return expr; + } + + public Expression parsePrimaryExpression() { + if (match(LIT_INT)) { + return new ExpressionInteger(consume()); + } else if (match(LIT_STR)) { + return new ExpressionString(consume()); + } else if (match(LIT_CHAR)) { + return new ExpressionChar(consume()); + } else if (match(LIT_IDENT)) { + return parseIdentExpression(); + } else if (take(LEFT_PAREN)) { + var start = lastToken(); + var inner = parseExpression(); + if (inner == null) return new ExpressionSyntaxError(lastToken(), "expected expression after '('"); + if (!take(RIGHT_PAREN)) + return new ExpressionSyntaxError(lastToken(), "expected ')' after grouped expression"); + return new ExpressionParens(start, inner, lastToken()); + } else if (match(LEFT_BRACE)) { + var start = consume(); + var values = new ArrayList(); + + while (hasMoreTokens() && !match(RIGHT_BRACE)) { + var value = parseExpression(); + if (value == null) { + return new ExpressionSyntaxError(peekToken(), "expected initializer value"); + } + values.add(value); + + if (!take(COMMA)) { + break; + } + } + + if (!take(RIGHT_BRACE)) return new ExpressionSyntaxError(lastToken(), "expected '}' after initializer"); + var end = lastToken(); + + return new ExpressionInitializer(start, values, end); + } else { + return null; + } + } + + public Expression parseIdentExpression() { + if (!take(LIT_IDENT)) return null; + var ident = lastToken(); + var sym = resolveSymbol(ident.content()); + + if (sym == null || sym.type == null) { + var expr = new ExpressionIdent(ident, SeaType.INT, false); + return new ExpressionTypeError(expr, "undefined symbol '" + ident.content() + "'"); + } else { + return new ExpressionIdent(ident, sym.type, true); + } + } + + // ================================= + // UTILITIES BEYOND HERE LIE + // ================================= + + protected boolean hasMoreTokens() { + return index < tokens.size(); + } + + protected boolean match(@NotNull Token.Type... types) { + var token = peekToken(); + for (var type : types) { + if (token.type().equals(type)) return true; + } + return false; + } + + protected boolean take(@NotNull Token.Type... types) { + for (Token.Type type : types) { + if (match(type)) { + consume(); + return true; + } + } + return false; + } + + protected Token consume() { + if (index >= tokens.size()) { + return Token.EOF; + } + var token = tokens.get(index); + index += 1; + return token; + } + + protected void consumeThrough(@NotNull Token.Type... types) { + while (hasMoreTokens()) { + var token = consume(); + if (Arrays.stream(types).anyMatch(t -> t.equals(token.type()))) { + return; + } + index += 1; + } + } + + + protected void consumeTo(@NotNull Token.Type... types) { + while (hasMoreTokens()) { + var token = peekToken(); + if (Arrays.stream(types).anyMatch(t -> t.equals(token.type()))) { + return; + } + index += 1; + } + } + + protected Token lastToken() { + if (index == 0) return Token.SOF; + return tokens.get(index - 1); + } + + protected Token peekToken() { + if (index >= tokens.size()) return Token.EOF; + return tokens.get(index); + } + + protected Token peekToken2() { + if (index + 1 >= tokens.size()) return Token.EOF; + return tokens.get(index + 1); + } + + void consumeMatchingBrace(Token openingBrace) throws ParseException { + assert openingBrace.type().equals(LEFT_BRACE); + index = tokens.indexOf(openingBrace) + 1; + var open = 1; + while (hasMoreTokens() && open > 0) { + if (take(LEFT_BRACE)) open++; + else if (take(RIGHT_BRACE)) open--; + else consume(); + } + if (open > 0) { + var msg = new Message(openingBrace, "unterminated '{'"); + throw new ParseException(msg); + } + } + + public List remainingTokens() { + return tokens.subList(index, tokens.size()); + } + + private static class Scope { + private final Vector> layers = new Vector<>(); + + Scope(DeclarationFunc.ParamList params) { + var map = new LinkedHashMap(); + for (var param : params.params()) { + map.put(param.name.content(), new Symbol(param)); + } + layers.add(map); + } + + public Symbol getOrNull(String name) { + for (var layer : layers.reversed()) { + if (layer.containsKey(name)) { + return layer.get(name); + } + } + return null; + } + + public SafeClosable push() { + var layer = new LinkedHashMap(); + layers.add(layer); + return () -> { + var removed = layers.removeLast(); + if (layer != removed) { + throw new AssertionError("scope popped out of order!"); + } + }; + } + + public boolean define(StatementVar stmt) { + layers.lastElement().put(stmt.name(), new Symbol(stmt)); + return true; + } + + public boolean has(String content) { + return getOrNull(content) != null; + } + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/SeaType.java b/src/jvmMain/kotlin/mtmc/lang/sea/SeaType.java new file mode 100644 index 0000000..7cdd913 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/SeaType.java @@ -0,0 +1,231 @@ +package mtmc.lang.sea; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public sealed interface SeaType { + default int size() { + if (this == CHAR) return 2; + if (this == INT) return 2; + if (this == VOID) return 0; + if (this instanceof Pointer) return 2; + if (this instanceof Struct struct) { + int size = 0; + for (Map.Entry fieldSet : struct.fields.entrySet()) { + SeaType type = fieldSet.getValue(); + size += type.size(); + } + return size; + } + throw new IllegalStateException("sizeof " + getClass().getName() + " is undefined"); + } + + default boolean isArithmetic() { + return this == CHAR || this == INT; + } + + default boolean isIntegral() { + return this == CHAR || this == INT || this instanceof Pointer; + } + + default boolean isStructure() { + return this instanceof Struct; + } + + default boolean isInt() { + return this == INT; + } + + default boolean isChar() { + return this == CHAR; + } + + default boolean isVoid() { + return this == VOID; + } + + default boolean isAnIntegralPointer() { + return this instanceof Pointer(SeaType component) && component.isIntegral(); + } + + default boolean isAPointerToAnInt() { + return this instanceof Pointer(SeaType component) && component.isInt(); + } + + default boolean isAPointerTo(SeaType inner) { + return this instanceof Pointer(SeaType it) && it.equals(inner); + } + + default boolean isAPointer() { + return this instanceof Pointer; + } + + default boolean isFunc() { + return this instanceof Func; + } + + default SeaType componentType() { + return ((Pointer) this).component; + } + + default SeaType resultType() { + return ((Func) this).result(); + } + + default void checkConversionTo(SeaType other) throws ConversionError { + if (this.isVoid() || other.isVoid()) throw new ConversionError(this, other, "void is not assignable to void"); + if (this.isArithmetic() && other.isArithmetic()) return; + if (this.isAPointer() && other.isAPointer()) return; + if (this instanceof Initializer initializer && other instanceof Struct s) { + // this is kinda complex + // the way this should work is we process named assignments and then based on the last + // index of the named assignments, we start putting in values + // the challenge here is that the blob may have too many values or may initialize them improperly + if (initializer.values.size() != s.fields.size()) { + throw new ConversionError(this, other, "initializer has too many or too few values"); + } + + int i = 0; + for (Map.Entry entry : s.fields.entrySet()) { + var name = entry.getKey(); + var ty = entry.getValue(); + var valueTy = initializer.values.get(i); + + try { + valueTy.checkConversionTo(ty); + } catch (ConversionError error) { + throw new ConversionError(ty, valueTy, + "value cannot be assigned to " + ty.repr() + " for '" + name + "'", error); + } + + i += 1; + } + return; + } + if (!this.equals(other)) { + throw new ConversionError(this, other, this.repr() + " is not convertible to " + other.repr()); + } + } + + class ConversionError extends Exception { + public final SeaType fromType, toType; + + private ConversionError(SeaType fromType, SeaType toType, String message) { + super(message); + this.fromType = fromType; + this.toType = toType; + } + + private ConversionError(SeaType fromType, SeaType toType, String message, ConversionError parent) { + super(message, parent); + this.fromType = fromType; + this.toType = toType; + } + } + + default boolean isCastableTo(SeaType target) { + if (target.isVoid()) return true; + if (this.isAPointer() && target.isInt()) return true; + if (this.isArithmetic() && target.isArithmetic()) return true; + return this.equals(target); + } + + default String repr() { + if (this instanceof Pointer p) { + if (p.baseType() instanceof Func(List params, boolean isVararg, SeaType result)) { + var s = new StringBuilder(); + s.append(result.repr()).append("(*"); + var x = p.component; + while (x instanceof Pointer p2) { + x = p2.component; + s.append("*"); + } + s.append(")("); + int i = 0; + for (var param : params) { + if (i > 0) s.append(", "); + s.append(param.repr()); + i = i + 1; + } + s.append(")"); + return s.toString(); + } else { + return p.component.repr() + "*"; + } + } + if (this == CHAR) return "char"; + if (this == INT) return "int"; + if (this == VOID) return "void"; + if (this instanceof Func(List params, boolean isVararg, SeaType result)) { + var s = new StringBuilder(); + s.append(result.repr()); + s.append("("); + int i = 0; + for (var param : params) { + if (i > 0) s.append(", "); + s.append(param.repr()); + i = i + 1; + } + s.append(")"); + return s.toString(); + } + if (this instanceof Initializer(List values)) { + var s = new StringBuilder(); + s.append("{"); + for (int i = 0; i < values.size(); i++) { + if (i > 0) s.append(", "); + s.append(values.get(i).repr()); + } + s.append("}"); + return s.toString(); + } + if (this instanceof Struct s) { + return "struct " + s.name; + } + throw new UnsupportedOperationException("unknown type " + this); + } + + SeaType CHAR = new Primitive("char"); + SeaType INT = new Primitive("int"); + SeaType VOID = new Primitive("void"); + + record Pointer(SeaType component) implements SeaType { + SeaType baseType() { + var ty = component; + while (ty instanceof Pointer(SeaType c)) { + ty = c; + } + return ty; + } + } + + final class Primitive implements SeaType { + // this is purely for debug info lmfao + public final String name; + + private Primitive(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } + } + + record Func(List params, boolean isVararg, SeaType result) implements SeaType { + public Func(List params, SeaType result) { + this(params, false, result); + } + } + + record Struct(String name, LinkedHashMap fields) implements SeaType { + public SeaType field(String name) { + return fields.get(name); + } + } + + record Initializer(List values) implements SeaType { + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/Symbol.java b/src/jvmMain/kotlin/mtmc/lang/sea/Symbol.java new file mode 100644 index 0000000..7e5b459 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/Symbol.java @@ -0,0 +1,59 @@ +package mtmc.lang.sea; + +import mtmc.lang.sea.ast.DeclarationFunc; +import mtmc.lang.sea.ast.DeclarationVar; +import mtmc.lang.sea.ast.StatementVar; +import mtmc.lang.sea.ast.TypeDeclaration; + +public class Symbol { + public final String name; + public final SeaType type; + public final TypeDeclaration typeDecl; + public final boolean isParam, isGlobal; + + public Symbol(DeclarationFunc.Param param) { + this.name = param.name.content(); + this.type = param.type.type(); + this.typeDecl = null; + this.isParam = true; + this.isGlobal = false; + } + + public Symbol(DeclarationVar decl) { + this.name = decl.name(); + this.type = decl.type.type(); + this.typeDecl = null; + this.isParam = false; + this.isGlobal = true; + } + + public Symbol(StatementVar stmt) { + this.name = stmt.name(); + this.type = stmt.type.type(); + this.typeDecl = null; + this.isParam = false; + this.isGlobal = false; + } + + public Symbol(DeclarationFunc func) { + this.name = func.name.content(); + this.type = func.type(); + this.typeDecl = null; + this.isParam = false; + this.isGlobal = true; + } + + public Symbol(TypeDeclaration declaration) { + this.name = declaration.name(); + this.type = null; + this.typeDecl = declaration; + this.isParam = false; + this.isGlobal = false; + } + + public boolean isAddressable() { + if (this.typeDecl != null) throw new IllegalStateException("cannot address non-data symbol"); + if (this.isParam) return false; // parameters are not addressable! + return true; + } +} \ No newline at end of file diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/Token.java b/src/jvmMain/kotlin/mtmc/lang/sea/Token.java new file mode 100644 index 0000000..0edc91f --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/Token.java @@ -0,0 +1,426 @@ +package mtmc.lang.sea; + +import mtmc.lang.Location; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public record Token( + Type type, + @NotNull + String content, + int start, + int end +) implements mtmc.lang.Token { + public static final Token SOF = new Token(Type.SOF, "", 0, 0); + public static final Token EOF = new Token(Type.EOF, "", Integer.MAX_VALUE, Integer.MAX_VALUE); + + public enum Type { + // Special + LIT_INT(null), + LIT_STR(null), + LIT_CHAR(null), + LIT_IDENT(null), + KW_TYPEDEF("typedef"), + KW_STRUCT("struct"), + KW_IF("if"), + KW_ELSE("else"), + KW_FOR("for"), + KW_WHILE("while"), + KW_DO("do"), + KW_GOTO("goto"), + KW_CONTINUE("continue"), + KW_BREAK("break"), + KW_RETURN("return"), + KW_SIZEOF("sizeof"), + KW_INT("int"), + KW_CHAR("char"), + KW_VOID("void"), + SOF(null), + EOF(null), + + // Groups + LEFT_PAREN("("), + RIGHT_PAREN(")"), + LEFT_BRACKET("["), + RIGHT_BRACKET("]"), + LEFT_BRACE("{"), + RIGHT_BRACE("}"), + + // Simple Punct + DOT3("..."), + DOT("."), + SEMICOLON(";"), + COMMA(","), + COLON(":"), + TILDE("~"), + QUESTION("?"), + + PLUS2("++"), + PLUS_EQ("+="), + PLUS("+"), + + DASH2("--"), + DASH_EQ("-="), + ARROW("->"), + DASH("-"), + + STAR_EQ("*="), + STAR("*"), + + SLASH_EQ("/="), + SLASH("/"), + + PERCENT_EQ("%="), + PERCENT("%"), + + AMPERSAND2("&&"), + AMPERSAND_EQ("&="), + AMPERSAND("&"), + + BAR2("||"), + BAR_EQ("|="), + BAR("|"), + + CARET("^"), + CARET_EQ("^="), + + LEFT_ARROW2_EQ("<<="), + LEFT_ARROW2("<<"), + LEFT_ARROW_EQ("<="), + LEFT_ARROW("<"), + + RIGHT_ARROW2_EQ(">>="), + RIGHT_ARROW2(">>"), + RIGHT_ARROW_EQ(">="), + RIGHT_ARROW(">"), + + EQUAL2("=="), + EQUAL("="), + + BANG_EQ("!="), + BANG("!"); + + public final String lex; + + public static final Type[] PUNCT; + static { + List list = new ArrayList<>(); + for (Type t : Type.values()) { + if (t.lex != null) { + list.add(t); + } + } + PUNCT = list.toArray(new Type[0]); + } + + public static final Type[] KEYWORDS; + static { + List list = new ArrayList<>(); + for (Type t : Type.values()) { + if (t.name().startsWith("KW_")) { + list.add(t); + } + } + KEYWORDS = list.toArray(new Type[0]); + } + + Type(String lex) { + this.lex = lex; + } + } + + public static int[] getLineAndOffset(String src, int index) { + int line = 1; + int column = 1; + for (int i = 0; i < index && i < src.length(); i++) { + char c = src.charAt(i); + if (c == '\n') { + line = line + 1; + column = 1; + } else { + column = column + 1; + } + } + return new int[]{line, column}; + } + + public static String getLineFor(String src, int index) { + int start = 0; + for (int i = Math.min(index, src.length() - 1); i >= 0; i--) { + if (src.charAt(i) == '\n') { + start = i + 1; + break; + } + } + + int end = src.length(); + for (int i = index; i < src.length(); i++) { + if (src.charAt(i) == '\n') { + break; + } + end = i + 1; + } + return src.substring(start, end); + } + + public static String highlight(String src, int start, int end) { + var s = getLineAndOffset(src, start); + var e = getLineAndOffset(src, end); + + int lineStart; + if (s[0] != e[0]) { + lineStart = 0; + } else { + lineStart = s[1] - 1; + } + + int lineEnd = e[1] - 1; + + String line = getLineFor(src, end); + + StringBuilder result = new StringBuilder(); + int off = 0; + + if (lineStart > 10) { + result.append("... "); + off += 4; + result.append(line.substring(lineStart, lineEnd)); + } else { + result.append(line.substring(0, lineEnd)); + } + + result.append('\n'); + result.repeat(' ', off + lineStart); + if (start == Integer.MAX_VALUE) { + result.append("^ (at EOL)"); + } else { + result.repeat('^', lineEnd - lineStart); + result.append(" (here)"); + } + return result.toString(); + } + + public static List tokenize(String src) throws TokenizeException { + List tokens = new ArrayList<>(); + int offset = 0; + do { + Token token = tokenizeOne(src, offset); + if (token == null) break; + tokens.add(token); + offset = token.end(); + } while (true); + return tokens; + } + + private static boolean match(String str, int start, String token) { + if (str == null) return false; + if (str.length() - start < token.length()) return false; + for (int i = 0; i < token.length(); i++) { + char c = str.charAt(start + i); + char d = token.charAt(i); + if (c != d) return false; + } + return true; + } + + private static boolean match(String str, int start, char c) { + if (str == null) return false; + if (str.length() - start < Character.charCount(c)) return false; + return str.charAt(start) == c; + } + + public static Token tokenizeOne(String src, int offset) throws TokenizeException { + while (offset < src.length()) { + if (Character.isWhitespace(src.charAt(offset))) { + offset += Character.charCount(src.charAt(offset)); + } else if (match(src, offset, "//")) { + offset += 2; + while (offset < src.length()) { + char c = src.charAt(offset); + offset += Character.charCount(c); + if (c == '\n') { + break; + } + } + } else if (match(src, offset, "/*")) { + offset += 2; + while (offset < src.length()) { + if (match(src, offset, "*/")) { + offset += 2; + break; + } else { + offset += Character.charCount(src.charAt(offset)); + } + } + } else { + break; + } + } + if (offset >= src.length()) return null; + + int start = offset; + Type type; + String content = null; + + char c = src.charAt(offset); + if (Character.isDigit(c)) { + do { + offset += Character.charCount(src.charAt(offset)); + } while (offset < src.length() && Character.isDigit(src.charAt(offset))); + content = src.substring(start, offset); + type = Type.LIT_INT; + } else if (Character.isLetter(c) || c == '_') { + do { + offset += Character.charCount(src.charAt(offset)); + } while (offset < src.length() && (Character.isLetter(src.charAt(offset)) || Character.isDigit(src.charAt(offset)) || src.charAt(offset) == '_')); + content = src.substring(start, offset); + type = Type.LIT_IDENT; + for (var ty : Type.KEYWORDS) { + if (content.equals(ty.lex)) { + type = ty; + break; + } + } + } else if (c == '\'') { + offset += Character.charCount(c); + char d = src.charAt(offset); + offset += Character.charCount(d); + if (d == '\\') { + if (offset >= src.length()) throw new TokenizeException("invalid character escape " + d, start, offset); + d = src.charAt(offset); + offset += Character.charCount(d); + content = switch (d) { + case 'n': + yield "\n"; + case 'r': + yield "\r"; + case 't': + yield "\t"; + case '\\': + yield "\\"; + case '\'': + yield "'"; + case '"': + yield "\""; + case '?': + yield "?"; + default: + throw new TokenizeException("invalid character escape " + d, start, offset); + }; + } else { + content = String.valueOf(d); + } + + if (offset >= src.length() || src.charAt(offset) != '\'') { + throw new TokenizeException("unterminated character literal", start, offset); + } + offset += Character.charCount('\''); + type = Type.LIT_CHAR; + } else if (c == '"') { + offset += Character.charCount(src.charAt(offset)); + StringBuilder sb = new StringBuilder(); + while (offset < src.length() && src.charAt(offset) != '"') { + char d = src.charAt(offset); + offset += Character.charCount(d); + + if (d == '\\') { + d = src.charAt(offset); + offset += Character.charCount(d); + char s = switch (d) { + case 'n': + yield '\n'; + case 'r': + yield '\r'; + case 't': + yield '\t'; + case '\\': + yield '\\'; + case '\'': + yield '\''; + case '"': + yield '"'; + case '?': + yield '?'; + default: + throw new TokenizeException("invalid string escape " + d, start, offset); + }; + sb.append(s); + } else if (d == '\n') { + break; + } else { + sb.append(d); + } + } + + if (offset >= src.length() || src.charAt(offset) != '"') { + throw new TokenizeException("unterminated string literal", start, offset); + } + + content = sb.toString(); + offset += Character.charCount('\"'); + type = Type.LIT_STR; + } else { + type = null; + for (Type t : Type.PUNCT) { + if (match(src, start, t.lex)) { + type = t; + content = t.lex; + offset += t.lex.length(); + break; + } + } + + if (type == null) { + throw new TokenizeException("unexpected character '" + src.charAt(start) + "'", start, offset); + } + } + + Objects.requireNonNull(content); + return new Token(type, content, start, offset); + } + + public static class TokenizeException extends IllegalArgumentException { + public final int start, end; + + public TokenizeException(String msg, int start, int end) { + super(msg); + this.start = start; + this.end = end; + } + + @Override + public String toString() { + return "TokenizeException at " + start + ":" + end + ", " + getLocalizedMessage(); + } + } + + @Override + public boolean equals(Object o) { + if (o instanceof String s) return Objects.equals(content, s); + if (!(o instanceof Token token)) return false; + return end == token.end && start == token.start && Objects.equals(content, token.content) && type == token.type; + } + + @Override + public int hashCode() { + return Objects.hash(type, content, start, end); + } + + @Override + public Location getStart() { + return new Location(start); + } + + @Override + public Location getEnd() { + return new Location(end); + } + + @Override + public String getContent() { + return content(); + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/ast/Ast.java b/src/jvmMain/kotlin/mtmc/lang/sea/ast/Ast.java new file mode 100644 index 0000000..d995cb6 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/ast/Ast.java @@ -0,0 +1,139 @@ +package mtmc.lang.sea.ast; + +import mtmc.lang.Span; +import mtmc.lang.sea.Token; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +public sealed abstract class Ast permits Declaration, DeclarationFunc.Param, DeclarationStruct.Field, Expression, Statement, TypeExpr, Unit { + public final Token start, end; + + public Ast(Token start, Token end) { + this.start = start; + this.end = end; + } + + public Span span() { + return Span.of(start, end); + } + + public Stream getChildren() { + return switch (this) { + case DeclarationFunc declarationFunc -> { + Stream out = Stream.of(declarationFunc.returnType); + out = Stream.concat(out, declarationFunc.params.params().stream()); + if (declarationFunc.body != null) { + out = Stream.concat(out, Stream.of(declarationFunc.body)); + } + yield out; + } + case DeclarationSyntaxError ignored -> Stream.empty(); + case DeclarationTypedef declarationTypedef -> Stream.of(declarationTypedef.type); + case DeclarationVar declarationVar -> { + Stream out = Stream.of(declarationVar.type); + if (declarationVar.initializer != null) { + out = Stream.concat(out, Stream.of(declarationVar.initializer)); + } + yield out; + } + case DeclarationStruct struct -> struct.fields.stream().map(x -> x); + case DeclarationStruct.Field field -> Stream.of(field.type); + case DeclarationFunc.Param param -> Stream.of(param.type); + case ExpressionAccess expressionAccess -> Stream.of(expressionAccess.value); + case ExpressionBin expressionBin -> Stream.of(expressionBin.lhs, expressionBin.rhs); + case ExpressionCall expressionCall -> { + Stream out = Stream.of(expressionCall.functor); + out = Stream.concat(out, expressionCall.args.stream()); + yield out; + } + case ExpressionInitializer init -> init.values.stream().map(x -> x); + case ExpressionCast expressionCast -> Stream.of(expressionCast.type, expressionCast.value); + case ExpressionChar ignored -> Stream.empty(); + case ExpressionIdent ignored -> Stream.empty(); + case ExpressionIndex expressionIndex -> Stream.of(expressionIndex.array, expressionIndex.index); + case ExpressionInteger ignored -> Stream.empty(); + case ExpressionParens expressionParens -> Stream.of(expressionParens.inner); + case ExpressionPostfix expressionPostfix -> Stream.of(expressionPostfix.inner); + case ExpressionPrefix expressionPrefix -> Stream.of(expressionPrefix.inner); + case ExpressionString ignored -> Stream.empty(); + case ExpressionTypeError typeError -> Stream.of(typeError.inner); + case ExpressionSyntaxError expressionSyntaxError -> { + if (expressionSyntaxError.child != null) { + yield Stream.of(expressionSyntaxError.child); + } else { + yield Stream.empty(); + } + } + case ExpressionTernary expressionTernary -> Stream.of( + expressionTernary.cond, + expressionTernary.then, + expressionTernary.otherwise + ); + case StatementBlock statementBlock -> statementBlock.statements.stream().map(x -> x); + case StatementBreak ignored -> Stream.empty(); + case StatementContinue ignored -> Stream.empty(); + case StatementDoWhile statementDoWhile -> Stream.of(statementDoWhile.body, statementDoWhile.condition); + case StatementExpression statementExpression -> Stream.of(statementExpression.expression); + case StatementFor statementFor -> { + Stream out = Stream.empty(); + if (statementFor.initExpression != null) { + out = Stream.concat(out, Stream.of(statementFor.initExpression)); + } else if (statementFor.initStatement != null) { + out = Stream.concat(out, Stream.of(statementFor.initStatement)); + } + if (statementFor.condition != null) { + out = Stream.concat(out, Stream.of(statementFor.condition)); + } + if (statementFor.inc != null) { + out = Stream.concat(out, Stream.of(statementFor.inc)); + } + out = Stream.concat(out, Stream.of(statementFor.body)); + yield out; + } + case StatementGoto ignored -> Stream.empty(); + case StatementIf statementIf -> { + Stream out = Stream.of(statementIf.condition, statementIf.body); + if (statementIf.elseBody != null) out = Stream.concat(out, Stream.of(statementIf.elseBody)); + yield out; + } + case StatementReturn statementReturn -> { + if (statementReturn.value == null) { + yield Stream.empty(); + } else { + yield Stream.of(statementReturn.value); + } + } + case StatementSyntaxError ignored -> Stream.empty(); + case StatementVar statementVar -> { + Stream out = Stream.of(statementVar.type); + if (statementVar.initValue != null) { + out = Stream.concat(out, Stream.of(statementVar.initValue)); + } + yield out; + } + case StatementWhile statementWhile -> Stream.of(statementWhile.condition, statementWhile.body); + case TypeExprArray typeExprArray -> Stream.of(typeExprArray.inner); + case TypeExprChar ignored -> Stream.empty(); + case TypeExprInt ignored -> Stream.empty(); + case TypeExprRef ignored -> Stream.empty(); + case TypeExprVoid ignored -> Stream.empty(); + case TypePointer ignored -> Stream.empty(); + case Unit unit -> unit.declarations.stream().map(x -> x); + }; + } + + public List collectErrors() { + var errors = new ArrayList(); + collectErrors(errors); + return errors; + } + + public void collectErrors(List errors) { + if (this instanceof Error e) { + errors.add(e); + } + getChildren().forEach(child -> child.collectErrors(errors)); + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/ast/Declaration.java b/src/jvmMain/kotlin/mtmc/lang/sea/ast/Declaration.java new file mode 100644 index 0000000..fad8557 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/ast/Declaration.java @@ -0,0 +1,9 @@ +package mtmc.lang.sea.ast; + +import mtmc.lang.sea.Token; + +public abstract sealed class Declaration extends Ast permits DeclarationFunc, DeclarationStruct, DeclarationSyntaxError, DeclarationTypedef, DeclarationVar { + public Declaration(Token start, Token end) { + super(start, end); + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/ast/DeclarationFunc.java b/src/jvmMain/kotlin/mtmc/lang/sea/ast/DeclarationFunc.java new file mode 100644 index 0000000..1455258 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/ast/DeclarationFunc.java @@ -0,0 +1,59 @@ +package mtmc.lang.sea.ast; + +import mtmc.lang.sea.SeaType; +import mtmc.lang.sea.Token; + +import java.util.ArrayList; +import java.util.List; + +public final class DeclarationFunc extends Declaration { + public final TypeExpr returnType; + public final Token name; + public final ParamList params; + public final StatementBlock body; + + public DeclarationFunc(TypeExpr returnType, Token name, ParamList paramList, StatementBlock body, Token end) { + super(returnType.start, end); + this.returnType = returnType; + this.name = name; + this.params = paramList; + this.body = body; + } + + public static final class Param extends Ast { + public final TypeExpr type; + public final Token name; + + public Param(TypeExpr type, Token name) { + super(type.start, name.end() < type.end.end() ? type.end : name); + this.type = type; + this.name = name; + } + } + + private SeaType.Func type; + public SeaType.Func type() { + if (type == null) { + var paramTypes = new ArrayList(params.size()); + for (Param param : params.params) { + paramTypes.add(param.type.type()); + } + type = new SeaType.Func( + paramTypes, + params.isVararg, + returnType.type() + ); + } + return type; + } + + public record ParamList(List params, boolean isVararg) { + public int size() { + return params.size(); + } + + public SeaType getParamType(int i) { + return params.get(i).type.type(); + } + } +} \ No newline at end of file diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/ast/DeclarationStruct.java b/src/jvmMain/kotlin/mtmc/lang/sea/ast/DeclarationStruct.java new file mode 100644 index 0000000..cca509f --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/ast/DeclarationStruct.java @@ -0,0 +1,54 @@ +package mtmc.lang.sea.ast; + +import mtmc.lang.sea.SeaType; +import mtmc.lang.sea.Token; + +import java.util.LinkedHashMap; +import java.util.List; + +public final class DeclarationStruct extends Declaration implements TypeDeclaration { + public final Token name; + public final List fields; + + public DeclarationStruct(Token start, Token name, List fields, Token end) { + super(start, end); + this.name = name; + this.fields = List.copyOf(fields); + } + + public String name() { + return name.content(); + } + + private SeaType type; + @Override + public SeaType type() { + if (type == null) { + var fields = new LinkedHashMap(); + for (var field : this.fields) { + fields.put(field.name(), field.type()); + } + type = new SeaType.Struct(name(), fields); + } + return type; + } + + public static final class Field extends Ast { + public final TypeExpr type; + public final Token name; + + public Field(TypeExpr type, Token name) { + super(type.start, name); + this.type = type; + this.name = name; + } + + public String name() { + return name.content(); + } + + public SeaType type() { + return type.type(); + } + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/ast/DeclarationSyntaxError.java b/src/jvmMain/kotlin/mtmc/lang/sea/ast/DeclarationSyntaxError.java new file mode 100644 index 0000000..b570d95 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/ast/DeclarationSyntaxError.java @@ -0,0 +1,18 @@ +package mtmc.lang.sea.ast; + +import mtmc.lang.ParseException; +import mtmc.lang.sea.Token; + +public final class DeclarationSyntaxError extends Declaration implements SyntaxError { + public final ParseException exception; + + public DeclarationSyntaxError(Token token, ParseException parseException) { + super(token, token); + this.exception = parseException; + } + + @Override + public ParseException exception() { + return exception; + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/ast/DeclarationTypedef.java b/src/jvmMain/kotlin/mtmc/lang/sea/ast/DeclarationTypedef.java new file mode 100644 index 0000000..f08db15 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/ast/DeclarationTypedef.java @@ -0,0 +1,26 @@ +package mtmc.lang.sea.ast; + +import mtmc.lang.sea.SeaType; +import mtmc.lang.sea.Token; + +public final class DeclarationTypedef extends Declaration implements TypeDeclaration { + public final TypeExpr type; + public final Token name; + + + public DeclarationTypedef(Token start, TypeExpr type, Token name, Token end) { + super(start, type.end); + this.type = type; + this.name = name; + } + + @Override + public String name() { + return name.content(); + } + + @Override + public SeaType type() { + return type.type(); + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/ast/DeclarationVar.java b/src/jvmMain/kotlin/mtmc/lang/sea/ast/DeclarationVar.java new file mode 100644 index 0000000..9b8b636 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/ast/DeclarationVar.java @@ -0,0 +1,20 @@ +package mtmc.lang.sea.ast; + +import mtmc.lang.sea.Token; + +public final class DeclarationVar extends Declaration { + public final TypeExpr type; + public final Token name; + public final Expression initializer; + + public DeclarationVar(TypeExpr type, Token name, Expression initializer) { + super(type.start, initializer == null ? name : initializer.end); + this.type = type; + this.name = name; + this.initializer = initializer; + } + + public String name() { + return this.name.content(); + } +} \ No newline at end of file diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/ast/Error.java b/src/jvmMain/kotlin/mtmc/lang/sea/ast/Error.java new file mode 100644 index 0000000..72486ab --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/ast/Error.java @@ -0,0 +1,7 @@ +package mtmc.lang.sea.ast; + +import mtmc.lang.ParseException; + +public interface Error { + ParseException exception(); +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/ast/Expression.java b/src/jvmMain/kotlin/mtmc/lang/sea/ast/Expression.java new file mode 100644 index 0000000..96f2de9 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/ast/Expression.java @@ -0,0 +1,23 @@ +package mtmc.lang.sea.ast; + +import mtmc.lang.sea.SeaType; +import mtmc.lang.sea.Token; + +import java.util.Objects; + +public sealed abstract class Expression extends Ast permits ExpressionAccess, ExpressionBin, ExpressionCall, ExpressionCast, ExpressionChar, ExpressionIdent, ExpressionIndex, ExpressionInitializer, ExpressionInteger, ExpressionParens, ExpressionPostfix, ExpressionPrefix, ExpressionString, ExpressionSyntaxError, ExpressionTernary, ExpressionTypeError { + private final SeaType type; + public Expression(Token start, Token end, SeaType type) { + super(start, end); + this.type = Objects.requireNonNull(type, "'type' cannot be null"); + } + + public SeaType type() { + return type; + } + + public enum ValueKind { + Addressable, + Immediate + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/ast/ExpressionAccess.java b/src/jvmMain/kotlin/mtmc/lang/sea/ast/ExpressionAccess.java new file mode 100644 index 0000000..32df47f --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/ast/ExpressionAccess.java @@ -0,0 +1,17 @@ +package mtmc.lang.sea.ast; + +import mtmc.lang.sea.SeaType; +import mtmc.lang.sea.Token; + +public final class ExpressionAccess extends Expression { + public final Expression value; + public final Token access; + public final Token prop; + + public ExpressionAccess(Expression value, Token access, Token prop, SeaType type) { + super(value.start, prop, type); + this.value = value; + this.access = access; + this.prop = prop; + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/ast/ExpressionBin.java b/src/jvmMain/kotlin/mtmc/lang/sea/ast/ExpressionBin.java new file mode 100644 index 0000000..801aaf3 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/ast/ExpressionBin.java @@ -0,0 +1,21 @@ +package mtmc.lang.sea.ast; + +import mtmc.lang.sea.SeaType; +import mtmc.lang.sea.Token; + +public final class ExpressionBin extends Expression { + public final Expression lhs; + public final Token op; + public final Expression rhs; + + public ExpressionBin(Expression lhs, Token op, Expression rhs, SeaType type) { + super(lhs.start, rhs.end, type); + this.lhs = lhs; + this.op = op; + this.rhs = rhs; + } + + public String op() { + return op.content(); + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/ast/ExpressionCall.java b/src/jvmMain/kotlin/mtmc/lang/sea/ast/ExpressionCall.java new file mode 100644 index 0000000..d153a67 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/ast/ExpressionCall.java @@ -0,0 +1,17 @@ +package mtmc.lang.sea.ast; + +import mtmc.lang.sea.SeaType; +import mtmc.lang.sea.Token; + +import java.util.List; + +public final class ExpressionCall extends Expression { + public final Expression functor; + public final List args; + + public ExpressionCall(Expression functor, List args, Token end, SeaType type) { + super(functor.start, end, type); + this.functor = functor; + this.args = args; + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/ast/ExpressionCast.java b/src/jvmMain/kotlin/mtmc/lang/sea/ast/ExpressionCast.java new file mode 100644 index 0000000..6271acc --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/ast/ExpressionCast.java @@ -0,0 +1,14 @@ +package mtmc.lang.sea.ast; + +import mtmc.lang.sea.Token; + +public final class ExpressionCast extends Expression { + public final TypeExpr type; + public final Expression value; + + public ExpressionCast(Token start, TypeExpr type, Expression value) { + super(start, value.end, type.type()); + this.type = type; + this.value = value; + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/ast/ExpressionChar.java b/src/jvmMain/kotlin/mtmc/lang/sea/ast/ExpressionChar.java new file mode 100644 index 0000000..5d75b38 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/ast/ExpressionChar.java @@ -0,0 +1,14 @@ +package mtmc.lang.sea.ast; + +import mtmc.lang.sea.SeaType; +import mtmc.lang.sea.Token; + +public final class ExpressionChar extends Expression { + public ExpressionChar(Token token) { + super(token, token, SeaType.CHAR); + } + + public Character content() { + return start.content().charAt(0); + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/ast/ExpressionIdent.java b/src/jvmMain/kotlin/mtmc/lang/sea/ast/ExpressionIdent.java new file mode 100644 index 0000000..ae9d484 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/ast/ExpressionIdent.java @@ -0,0 +1,17 @@ +package mtmc.lang.sea.ast; + +import mtmc.lang.sea.SeaType; +import mtmc.lang.sea.Token; + +public final class ExpressionIdent extends Expression { + public final boolean isAddressable; + + public ExpressionIdent(Token token, SeaType type, boolean isAddressable) { + super(token, token, type); + this.isAddressable = isAddressable; + } + + public String name() { + return start.content(); + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/ast/ExpressionIndex.java b/src/jvmMain/kotlin/mtmc/lang/sea/ast/ExpressionIndex.java new file mode 100644 index 0000000..285e4fd --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/ast/ExpressionIndex.java @@ -0,0 +1,14 @@ +package mtmc.lang.sea.ast; + +import mtmc.lang.sea.SeaType; +import mtmc.lang.sea.Token; + +public final class ExpressionIndex extends Expression { + public final Expression array, index; + + public ExpressionIndex(Expression array, Expression index, Token end, SeaType type) { + super(array.start, end, type); + this.array = array; + this.index = index; + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/ast/ExpressionInitializer.java b/src/jvmMain/kotlin/mtmc/lang/sea/ast/ExpressionInitializer.java new file mode 100644 index 0000000..6b17ec4 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/ast/ExpressionInitializer.java @@ -0,0 +1,27 @@ +package mtmc.lang.sea.ast; + +import mtmc.lang.sea.SeaType; +import mtmc.lang.sea.Token; + +import java.util.ArrayList; +import java.util.List; + +public final class ExpressionInitializer extends Expression { + public final List values; + + private static SeaType.Initializer blobType(List values) { + var types = new ArrayList(); + + for (var value : values) { + types.add(value.type()); + } + + return new SeaType.Initializer(types); + } + + public ExpressionInitializer(Token start, List values, Token end) { + super(start, end, blobType(values)); + this.values = List.copyOf(values); + } + +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/ast/ExpressionInteger.java b/src/jvmMain/kotlin/mtmc/lang/sea/ast/ExpressionInteger.java new file mode 100644 index 0000000..ba713c7 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/ast/ExpressionInteger.java @@ -0,0 +1,13 @@ +package mtmc.lang.sea.ast; + +import mtmc.lang.sea.SeaType; +import mtmc.lang.sea.Token; + +public final class ExpressionInteger extends Expression { + public final int value; + + public ExpressionInteger(Token start) { + super(start, start, SeaType.INT); + this.value = Integer.parseInt(start.content()); + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/ast/ExpressionParens.java b/src/jvmMain/kotlin/mtmc/lang/sea/ast/ExpressionParens.java new file mode 100644 index 0000000..7bf1785 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/ast/ExpressionParens.java @@ -0,0 +1,12 @@ +package mtmc.lang.sea.ast; + +import mtmc.lang.sea.Token; + +public final class ExpressionParens extends Expression { + public final Expression inner; + + public ExpressionParens(Token start, Expression inner, Token end) { + super(start, end, inner.type()); + this.inner = inner; + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/ast/ExpressionPostfix.java b/src/jvmMain/kotlin/mtmc/lang/sea/ast/ExpressionPostfix.java new file mode 100644 index 0000000..3740687 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/ast/ExpressionPostfix.java @@ -0,0 +1,17 @@ +package mtmc.lang.sea.ast; + +import mtmc.lang.sea.SeaType; +import mtmc.lang.sea.Token; + +public final class ExpressionPostfix extends Expression { + public Expression inner; + + public ExpressionPostfix(Expression lhs, Token op, SeaType type) { + super(lhs.start, op, type); + this.inner = lhs; + } + + public String op() { + return end.content(); + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/ast/ExpressionPrefix.java b/src/jvmMain/kotlin/mtmc/lang/sea/ast/ExpressionPrefix.java new file mode 100644 index 0000000..4191d59 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/ast/ExpressionPrefix.java @@ -0,0 +1,17 @@ +package mtmc.lang.sea.ast; + +import mtmc.lang.sea.SeaType; +import mtmc.lang.sea.Token; + +public final class ExpressionPrefix extends Expression { + public final Expression inner; + + public ExpressionPrefix(Token operator, Expression rhs, SeaType type) { + super(operator, rhs.end, type); + this.inner = rhs; + } + + public String op() { + return start.content(); + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/ast/ExpressionString.java b/src/jvmMain/kotlin/mtmc/lang/sea/ast/ExpressionString.java new file mode 100644 index 0000000..4f92411 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/ast/ExpressionString.java @@ -0,0 +1,18 @@ +package mtmc.lang.sea.ast; + +import mtmc.lang.sea.SeaType; +import mtmc.lang.sea.Token; + +public final class ExpressionString extends Expression { + public ExpressionString(Token token) { + super(token, token, new SeaType.Pointer(SeaType.CHAR)); + } + + public byte[] getBytes() { + return start.content().getBytes(); + } + + public String content() { + return start.content(); + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/ast/ExpressionSyntaxError.java b/src/jvmMain/kotlin/mtmc/lang/sea/ast/ExpressionSyntaxError.java new file mode 100644 index 0000000..fd3394d --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/ast/ExpressionSyntaxError.java @@ -0,0 +1,27 @@ +package mtmc.lang.sea.ast; + +import mtmc.lang.ParseException; +import mtmc.lang.sea.SeaType; +import mtmc.lang.sea.Token; +import org.jetbrains.annotations.Nullable; + +public final class ExpressionSyntaxError extends Expression implements SyntaxError { + @Nullable + public final Expression child; + public final ParseException exception; + + public ExpressionSyntaxError(Token token, String message) { + this(null, token, message); + } + + public ExpressionSyntaxError(@Nullable Expression child, Token token, String message) { + super(child == null ? token : child.start, token, SeaType.INT); + this.child = child; + this.exception = new ParseException(new ParseException.Message(token, message)); + } + + @Override + public ParseException exception() { + return exception; + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/ast/ExpressionTernary.java b/src/jvmMain/kotlin/mtmc/lang/sea/ast/ExpressionTernary.java new file mode 100644 index 0000000..6b45cfe --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/ast/ExpressionTernary.java @@ -0,0 +1,16 @@ +package mtmc.lang.sea.ast; + +import mtmc.lang.sea.SeaType; + +public final class ExpressionTernary extends Expression { + public final Expression cond; + public final Expression then; + public final Expression otherwise; + + public ExpressionTernary(Expression cond, Expression then, Expression otherwise, SeaType type) { + super(cond.start, otherwise.end, type); + this.cond = cond; + this.then = then; + this.otherwise = otherwise; + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/ast/ExpressionTypeError.java b/src/jvmMain/kotlin/mtmc/lang/sea/ast/ExpressionTypeError.java new file mode 100644 index 0000000..21aebdb --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/ast/ExpressionTypeError.java @@ -0,0 +1,20 @@ +package mtmc.lang.sea.ast; + +import mtmc.lang.ParseException; + +public final class ExpressionTypeError extends Expression implements Error { + public final Expression inner; + public final ParseException exception; + + public ExpressionTypeError(Expression inner, String message) { + super(inner.start, inner.end, inner.type()); + this.inner = inner; + this.exception = new ParseException(new ParseException.Message(inner.span(), message)); + } + + + @Override + public ParseException exception() { + return exception; + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/ast/Statement.java b/src/jvmMain/kotlin/mtmc/lang/sea/ast/Statement.java new file mode 100644 index 0000000..7852c06 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/ast/Statement.java @@ -0,0 +1,25 @@ +package mtmc.lang.sea.ast; + +import mtmc.lang.ParseException; +import mtmc.lang.sea.Token; + +public abstract sealed class Statement extends Ast permits StatementBlock, StatementBreak, StatementContinue, StatementDoWhile, StatementExpression, StatementFor, StatementGoto, StatementIf, StatementReturn, StatementSyntaxError, StatementVar, StatementWhile +{ + private Token labelAnchor = null; + + public Statement(Token start, Token end) { + super(start, end); + } + + public void setLabelAnchor(Token labelAnchor) throws ParseException { + if (labelAnchor == null) return; + if (this.labelAnchor != null) { + throw new ParseException(new ParseException.Message(labelAnchor, "this statement has been labeled twice!!")); + } + this.labelAnchor = labelAnchor; + } + + public Token getLabelAnchor() { + return labelAnchor; + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/ast/StatementBlock.java b/src/jvmMain/kotlin/mtmc/lang/sea/ast/StatementBlock.java new file mode 100644 index 0000000..16ab75a --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/ast/StatementBlock.java @@ -0,0 +1,14 @@ +package mtmc.lang.sea.ast; + +import mtmc.lang.sea.Token; + +import java.util.List; + +public final class StatementBlock extends Statement { + public final List statements; + + public StatementBlock(Token start, List children, Token end) { + super(start, end); + this.statements = List.copyOf(children); + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/ast/StatementBreak.java b/src/jvmMain/kotlin/mtmc/lang/sea/ast/StatementBreak.java new file mode 100644 index 0000000..e253f9d --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/ast/StatementBreak.java @@ -0,0 +1,9 @@ +package mtmc.lang.sea.ast; + +import mtmc.lang.sea.Token; + +public final class StatementBreak extends Statement { + public StatementBreak(Token breakToken) { + super(breakToken, breakToken); + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/ast/StatementContinue.java b/src/jvmMain/kotlin/mtmc/lang/sea/ast/StatementContinue.java new file mode 100644 index 0000000..780f0c8 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/ast/StatementContinue.java @@ -0,0 +1,9 @@ +package mtmc.lang.sea.ast; + +import mtmc.lang.sea.Token; + +public final class StatementContinue extends Statement { + public StatementContinue(Token continueToken) { + super(continueToken, continueToken); + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/ast/StatementDoWhile.java b/src/jvmMain/kotlin/mtmc/lang/sea/ast/StatementDoWhile.java new file mode 100644 index 0000000..ece417b --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/ast/StatementDoWhile.java @@ -0,0 +1,14 @@ +package mtmc.lang.sea.ast; + +import mtmc.lang.sea.Token; + +public final class StatementDoWhile extends Statement { + public final Statement body; + public final Expression condition; + + public StatementDoWhile(Token start, Statement body, Expression condition, Token end) { + super(start, end); + this.body = body; + this.condition = condition; + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/ast/StatementExpression.java b/src/jvmMain/kotlin/mtmc/lang/sea/ast/StatementExpression.java new file mode 100644 index 0000000..192d8c8 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/ast/StatementExpression.java @@ -0,0 +1,10 @@ +package mtmc.lang.sea.ast; + +public final class StatementExpression extends Statement { + public final Expression expression; + + public StatementExpression(Expression expression) { + super(expression.start, expression.end); + this.expression = expression; + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/ast/StatementFor.java b/src/jvmMain/kotlin/mtmc/lang/sea/ast/StatementFor.java new file mode 100644 index 0000000..9fe2849 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/ast/StatementFor.java @@ -0,0 +1,20 @@ +package mtmc.lang.sea.ast; + +import mtmc.lang.sea.Token; + +public final class StatementFor extends Statement { + public final Expression initExpression; + public final StatementVar initStatement; + public final Expression condition; + public final Expression inc; + public final Statement body; + + public StatementFor(Token start, Expression initExpression, StatementVar initStatement, Expression condition, Expression inc, Statement body) { + super(start, body.end); + this.initExpression = initExpression; + this.initStatement = initStatement; + this.condition = condition; + this.inc = inc; + this.body = body; + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/ast/StatementGoto.java b/src/jvmMain/kotlin/mtmc/lang/sea/ast/StatementGoto.java new file mode 100644 index 0000000..c828f60 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/ast/StatementGoto.java @@ -0,0 +1,12 @@ +package mtmc.lang.sea.ast; + +import mtmc.lang.sea.Token; + +public final class StatementGoto extends Statement { + public final Token label; + + public StatementGoto(Token start, Token label) { + super(start, label); + this.label = label; + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/ast/StatementIf.java b/src/jvmMain/kotlin/mtmc/lang/sea/ast/StatementIf.java new file mode 100644 index 0000000..8a581cf --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/ast/StatementIf.java @@ -0,0 +1,16 @@ +package mtmc.lang.sea.ast; + +import mtmc.lang.sea.Token; + +public final class StatementIf extends Statement { + public final Expression condition; + public final Statement body; + public final Statement elseBody; + + public StatementIf(Token start, Expression condition, Statement body, Statement elseBody) { + super(start, elseBody == null ? body.end : elseBody.end); + this.condition = condition; + this.body = body; + this.elseBody = elseBody; + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/ast/StatementReturn.java b/src/jvmMain/kotlin/mtmc/lang/sea/ast/StatementReturn.java new file mode 100644 index 0000000..8309a11 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/ast/StatementReturn.java @@ -0,0 +1,12 @@ +package mtmc.lang.sea.ast; + +import mtmc.lang.sea.Token; + +public final class StatementReturn extends Statement { + public final Expression value; + + public StatementReturn(Token start, Expression value) { + super(start, value == null ? start : value.end); + this.value = value; + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/ast/StatementSyntaxError.java b/src/jvmMain/kotlin/mtmc/lang/sea/ast/StatementSyntaxError.java new file mode 100644 index 0000000..7b2e98e --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/ast/StatementSyntaxError.java @@ -0,0 +1,18 @@ +package mtmc.lang.sea.ast; + +import mtmc.lang.ParseException; +import mtmc.lang.sea.Token; + +public final class StatementSyntaxError extends Statement implements SyntaxError { + public final ParseException exception; + + public StatementSyntaxError(Token token, ParseException exception) { + super(token, token); + this.exception = exception; + } + + @Override + public ParseException exception() { + return exception; + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/ast/StatementVar.java b/src/jvmMain/kotlin/mtmc/lang/sea/ast/StatementVar.java new file mode 100644 index 0000000..cbe2887 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/ast/StatementVar.java @@ -0,0 +1,20 @@ +package mtmc.lang.sea.ast; + +import mtmc.lang.sea.Token; + +public final class StatementVar extends Statement { + public final TypeExpr type; + public final Token name; + public final Expression initValue; + + public StatementVar(TypeExpr type, Token name, Expression initValue) { + super(type.start, initValue == null ? name : initValue.end); + this.type = type; + this.name = name; + this.initValue = initValue; + } + + public String name() { + return name.content(); + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/ast/StatementWhile.java b/src/jvmMain/kotlin/mtmc/lang/sea/ast/StatementWhile.java new file mode 100644 index 0000000..8025b74 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/ast/StatementWhile.java @@ -0,0 +1,14 @@ +package mtmc.lang.sea.ast; + +import mtmc.lang.sea.Token; + +public final class StatementWhile extends Statement { + public final Expression condition; + public final Statement body; + + public StatementWhile(Token start, Expression condition, Statement body) { + super(start, body.end); + this.condition = condition; + this.body = body; + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/ast/SyntaxError.java b/src/jvmMain/kotlin/mtmc/lang/sea/ast/SyntaxError.java new file mode 100644 index 0000000..6464ef0 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/ast/SyntaxError.java @@ -0,0 +1,7 @@ +package mtmc.lang.sea.ast; + +import mtmc.lang.ParseException; + +public interface SyntaxError extends Error { + ParseException exception(); +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/ast/TypeDeclaration.java b/src/jvmMain/kotlin/mtmc/lang/sea/ast/TypeDeclaration.java new file mode 100644 index 0000000..00cc3f4 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/ast/TypeDeclaration.java @@ -0,0 +1,8 @@ +package mtmc.lang.sea.ast; + +import mtmc.lang.sea.SeaType; + +public interface TypeDeclaration { + String name(); + SeaType type(); +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/ast/TypeExpr.java b/src/jvmMain/kotlin/mtmc/lang/sea/ast/TypeExpr.java new file mode 100644 index 0000000..982ca9a --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/ast/TypeExpr.java @@ -0,0 +1,19 @@ +package mtmc.lang.sea.ast; + +import mtmc.lang.sea.SeaType; +import mtmc.lang.sea.Token; + +import java.util.Objects; + +public abstract sealed class TypeExpr extends Ast permits TypeExprArray, TypeExprChar, TypeExprInt, TypeExprRef, TypeExprVoid, TypePointer { + private final SeaType type; + + public TypeExpr(Token start, Token end, SeaType type) { + super(start, end); + this.type = Objects.requireNonNull(type); + } + + public SeaType type() { + return type; + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/ast/TypeExprArray.java b/src/jvmMain/kotlin/mtmc/lang/sea/ast/TypeExprArray.java new file mode 100644 index 0000000..55a2907 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/ast/TypeExprArray.java @@ -0,0 +1,13 @@ +package mtmc.lang.sea.ast; + +import mtmc.lang.sea.SeaType; +import mtmc.lang.sea.Token; + +public final class TypeExprArray extends TypeExpr { + public final TypeExpr inner; + + public TypeExprArray(TypeExpr inner, Token end) { + super(inner.start, end, new SeaType.Pointer(inner.type())); + this.inner = inner; + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/ast/TypeExprChar.java b/src/jvmMain/kotlin/mtmc/lang/sea/ast/TypeExprChar.java new file mode 100644 index 0000000..1fc3f97 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/ast/TypeExprChar.java @@ -0,0 +1,13 @@ +package mtmc.lang.sea.ast; + +import mtmc.lang.sea.SeaType; +import mtmc.lang.sea.Token; + +public final class TypeExprChar extends TypeExpr { + public final Token token; + + public TypeExprChar(Token token) { + super(token, token, SeaType.CHAR); + this.token = token; + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/ast/TypeExprInt.java b/src/jvmMain/kotlin/mtmc/lang/sea/ast/TypeExprInt.java new file mode 100644 index 0000000..5ebd61f --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/ast/TypeExprInt.java @@ -0,0 +1,10 @@ +package mtmc.lang.sea.ast; + +import mtmc.lang.sea.SeaType; +import mtmc.lang.sea.Token; + +public final class TypeExprInt extends TypeExpr { + public TypeExprInt(Token token) { + super(token, token, SeaType.INT); + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/ast/TypeExprRef.java b/src/jvmMain/kotlin/mtmc/lang/sea/ast/TypeExprRef.java new file mode 100644 index 0000000..9476624 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/ast/TypeExprRef.java @@ -0,0 +1,12 @@ +package mtmc.lang.sea.ast; + +import mtmc.lang.sea.Token; + +public final class TypeExprRef extends TypeExpr { + public final TypeDeclaration decl; + + public TypeExprRef(Token name, TypeDeclaration decl) { + super(name, name, decl.type()); + this.decl = decl; + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/ast/TypeExprVoid.java b/src/jvmMain/kotlin/mtmc/lang/sea/ast/TypeExprVoid.java new file mode 100644 index 0000000..bb63e05 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/ast/TypeExprVoid.java @@ -0,0 +1,10 @@ +package mtmc.lang.sea.ast; + +import mtmc.lang.sea.SeaType; +import mtmc.lang.sea.Token; + +public final class TypeExprVoid extends TypeExpr { + public TypeExprVoid(Token token) { + super(token, token, SeaType.VOID); + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/ast/TypePointer.java b/src/jvmMain/kotlin/mtmc/lang/sea/ast/TypePointer.java new file mode 100644 index 0000000..e299095 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/ast/TypePointer.java @@ -0,0 +1,13 @@ +package mtmc.lang.sea.ast; + +import mtmc.lang.sea.SeaType; +import mtmc.lang.sea.Token; + +public final class TypePointer extends TypeExpr { + public final TypeExpr component; + + public TypePointer(TypeExpr component, Token star) { + super(component.start, star, new SeaType.Pointer(component.type())); + this.component = component; + } +} diff --git a/src/jvmMain/kotlin/mtmc/lang/sea/ast/Unit.java b/src/jvmMain/kotlin/mtmc/lang/sea/ast/Unit.java new file mode 100644 index 0000000..7f28803 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/lang/sea/ast/Unit.java @@ -0,0 +1,22 @@ +package mtmc.lang.sea.ast; + +import mtmc.lang.sea.Symbol; +import mtmc.lang.sea.Token; + +import java.util.LinkedHashMap; +import java.util.List; + +public final class Unit extends Ast { + public final String source; + public final String filename; + public final List declarations; + public final LinkedHashMap symbols; + + public Unit(String filename, String source, List declarations, LinkedHashMap globals) { + super(Token.SOF, Token.EOF); + this.source = source; + this.filename = filename; + this.declarations = declarations; + this.symbols = globals; + } +} diff --git a/src/jvmMain/kotlin/mtmc/os/MTOS.kt b/src/jvmMain/kotlin/mtmc/os/MTOS.kt new file mode 100644 index 0000000..756bfa6 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/os/MTOS.kt @@ -0,0 +1,455 @@ +package mtmc.os + +import mtmc.emulator.MonTanaMiniComputer +import mtmc.emulator.Register +import mtmc.os.SysCall.Companion.getValue +import mtmc.os.shell.Shell +import java.io.File +import java.io.IOException +import java.nio.file.Files +import java.util.* +import kotlin.math.max +import kotlin.math.min +import kotlin.text.Charsets.US_ASCII + +class MTOS(private val computer: MonTanaMiniComputer) { + private var timer: Long = 0 + var random: Random = Random() + + // Editor support + var currentFile: String? = null + var currentFileMime: String? = null + var breakpoints: IntArray? = null + + fun applyBreakpoints() { + if (computer.debugInfo == null || breakpoints == null) { + return + } + + var debug = computer.debugInfo!!.originalLineNumbers + var name: String? = computer.debugInfo!!.originalFile + + if (this.currentFileMime == "text/x-asm") { + debug = computer.debugInfo!!.assemblyLineNumbers + name = computer.debugInfo!!.assemblyFile + } + + if (debug == null || name != currentFile) { + return + } + + for (index in breakpoints!!.indices) { + val line = breakpoints!![index] + for (i in debug.indices) { + if (debug[i] == line) { + computer.setBreakpoint(i, true) + break + } + } + } + } + + fun handleSysCall(syscallNumber: Short) { + if (syscallNumber == getValue("exit").toShort()) { + computer.setStatus(MonTanaMiniComputer.ComputerStatus.FINISHED) + } else if (syscallNumber == getValue("rint").toShort()) { + // rint + if (!computer.console.hasShortValue()) { + computer.notifyOfRequestInteger() + } + while (!computer.console.hasShortValue() && computer.getStatus() == MonTanaMiniComputer.ComputerStatus.EXECUTING) { + try { + Thread.sleep(10) + } catch (e: InterruptedException) { + } + } + val `val` = computer.console.readInt() + computer.setRegisterValue(Register.RV, `val`.toInt()) + } else if (syscallNumber == getValue("wint").toShort()) { + // wint + val value = computer.getRegisterValue(Register.A0) + computer.console.writeInt(value) + } else if (syscallNumber == getValue("rchr").toShort()) { + if (!computer.console.hasShortValue()) { + computer.notifyOfRequestCharacter() + } + while (!computer.console.hasShortValue() && computer.getStatus() == MonTanaMiniComputer.ComputerStatus.EXECUTING) { + try { + Thread.sleep(10) + } catch (e: InterruptedException) { + } + } + val `val` = computer.console.readChar() + computer.setRegisterValue(Register.RV, `val`.code) + } else if (syscallNumber == getValue("wchr").toShort()) { + val value = computer.getRegisterValue(Register.A0) + computer.console.print("" + Char(value.toUShort())) + } else if (syscallNumber == getValue("rstr").toShort()) { + // rstr + val pointer = computer.getRegisterValue(Register.A0) + val maxLen = computer.getRegisterValue(Register.A1) + if (!computer.console.hasReadString()) { + computer.notifyOfRequestString() + } + while (!computer.console.hasReadString() && computer.getStatus() == MonTanaMiniComputer.ComputerStatus.EXECUTING) { + try { + Thread.sleep(10) + } catch (e: InterruptedException) { + } + } + val string = computer.console.readString() + val bytes = string!!.toByteArray(US_ASCII) + val bytesToRead = min(bytes.size, maxLen.toInt()) + for (i in 0..= fmtString.length) break + c = fmtString.get(i++) + + if (c == 'd') { + stackOff += 2 + val v = computer.fetchWordFromMemory(initSP - stackOff).toInt() + sb.append(v) + } else if (c == 'c') { + stackOff += 2 + val v = Char(computer.fetchWordFromMemory(initSP - stackOff).toUShort()) + sb.append(v) + } else if (c == 's') { + stackOff += 2 + val valuePointer = computer.fetchWordFromMemory(initSP - stackOff) + val s = readStringFromMemory(valuePointer) + sb.append(s) + } else { + sb.append('%').append(c) + } + } + + computer.console.print(sb.toString()) + computer.setRegisterValue(Register.RV, sb.length) + } else if (syscallNumber == getValue("atoi").toShort()) { + val pointer = computer.getRegisterValue(Register.A0) + val string = readStringFromMemory(pointer) + val split = string.trim { it <= ' ' }.split("\\s+".toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray() + val firstNum = split[0] + try { + val value = firstNum.toShort() + computer.setRegisterValue(Register.RV, value.toInt()) + } catch (e: NumberFormatException) { + computer.setRegisterValue(Register.RV, 0) + } + } else if (syscallNumber == getValue("rnd").toShort()) { + // rnd + var low = computer.getRegisterValue(Register.A0) + var high = computer.getRegisterValue(Register.A1) + val temp: Short + + if (low > high) { + temp = low + low = high + high = temp + } + + computer.setRegisterValue(Register.RV, random.nextInt(low.toInt(), high + 1)) + } else if (syscallNumber == getValue("sleep").toShort()) { + // sleep + val millis = computer.getRegisterValue(Register.A0) + try { + if (millis > 0) Thread.sleep(millis.toLong()) + } catch (e: InterruptedException) { + throw RuntimeException(e) + } + } else if (syscallNumber == getValue("fbreset").toShort()) { + // fbreset + computer.display.reset() + } else if (syscallNumber == getValue("fbstat").toShort()) { + // fbstat + val x = computer.getRegisterValue(Register.A0) + val y = computer.getRegisterValue(Register.A1) + val `val` = computer.display.getPixel(x.toInt(), y.toInt()) + computer.setRegisterValue(Register.RV, `val`.toInt()) + } else if (syscallNumber == getValue("fbset").toShort()) { + // fbset + val x = computer.getRegisterValue(Register.A0) + val y = computer.getRegisterValue(Register.A1) + val color = computer.getRegisterValue(Register.A2) + computer.display.setPixel(x.toInt(), y.toInt(), color.toInt()) + } else if (syscallNumber == getValue("fbline").toShort()) { + val startX = computer.getRegisterValue(Register.A0) + val startY = computer.getRegisterValue(Register.A1) + val endX = computer.getRegisterValue(Register.A2) + val endY = computer.getRegisterValue(Register.A3) + computer.display.drawLine(startX, startY, endX, endY) + } else if (syscallNumber == getValue("fbrect").toShort()) { + val startX = computer.getRegisterValue(Register.A0) + val startY = computer.getRegisterValue(Register.A1) + val width = computer.getRegisterValue(Register.A2) + val height = computer.getRegisterValue(Register.A3) + computer.display.drawRectangle(startX, startY, width, height) + } else if (syscallNumber == getValue("fbflush").toShort()) { + computer.display.sync() + } else if (syscallNumber == getValue("joystick").toShort()) { + computer.setRegisterValue(Register.RV, computer.iOState) + } else if (syscallNumber == getValue("scolor").toShort()) { + computer.display.setColor(computer.getRegisterValue(Register.A0)) + } else if (syscallNumber == getValue("memcpy").toShort()) { + val fromPointer = computer.getRegisterValue(Register.A0) + val toPointer = computer.getRegisterValue(Register.A1) + val bytes = computer.getRegisterValue(Register.A2) + for (i in 0..(str.split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) + .filter { s: String? -> !s!!.startsWith("!") } + .toList() + + val linesTotal = lines.size + val cappedLines = min(linesTotal, maxSize2.toInt()) + + for (lineNum in 0.. 0) this.timer = System.currentTimeMillis() + value + + computer.setRegisterValue( + Register.RV, + max(0, this.timer - System.currentTimeMillis()).toInt() + ) + } else if (syscallNumber == getValue("drawimg").toShort()) { + val image = computer.getRegisterValue(Register.A0) + val x = computer.getRegisterValue(Register.A1) + val y = computer.getRegisterValue(Register.A2) + + if (!computer.display.hasGraphic(image.toInt())) { + computer.setRegisterValue(Register.RV, 1) + return + } + + computer.display.drawImage(image.toInt(), x.toInt(), y.toInt()) + computer.setRegisterValue(Register.RV, 0) + } else if (syscallNumber == getValue("drawimgsz").toShort()) { + val image = computer.getRegisterValue(Register.A0) + val pointer = computer.getRegisterValue(Register.A1) + val x = computer.fetchWordFromMemory(pointer.toInt()) + val y = computer.fetchWordFromMemory(pointer + 2) + val width = computer.fetchWordFromMemory(pointer + 4) + val height = computer.fetchWordFromMemory(pointer + 6) + + if (!computer.display.hasGraphic(image.toInt())) { + computer.setRegisterValue(Register.RV, 1) + return + } + + computer.display.drawImage(image.toInt(), x.toInt(), y.toInt(), width.toInt(), height.toInt()) + computer.setRegisterValue(Register.RV, 0) + } else if (syscallNumber == getValue("drawimgclip").toShort()) { + val image = computer.getRegisterValue(Register.A0) + val source = computer.getRegisterValue(Register.A1) + val destination = computer.getRegisterValue(Register.A2) + + val sx = computer.fetchWordFromMemory(source.toInt()) + val sy = computer.fetchWordFromMemory(source + 2) + val sw = computer.fetchWordFromMemory(source + 4) + val sh = computer.fetchWordFromMemory(source + 6) + + val dx = computer.fetchWordFromMemory(destination.toInt()) + val dy = computer.fetchWordFromMemory(destination + 2) + val dw = computer.fetchWordFromMemory(destination + 4) + val dh = computer.fetchWordFromMemory(destination + 6) + + if (!computer.display.hasGraphic(image.toInt())) { + computer.setRegisterValue(Register.RV, 1) + return + } + + computer.display.drawImage( + image.toInt(), + sx.toInt(), + sy.toInt(), + sw.toInt(), + sh.toInt(), + dx.toInt(), + dy.toInt(), + dw.toInt(), + dh.toInt() + ) + computer.setRegisterValue(Register.RV, 0) + } else if (syscallNumber == getValue("dirent").toShort()) { + val dirent = computer.getRegisterValue(Register.A0) + val command = computer.getRegisterValue(Register.A1) + val offset = computer.getRegisterValue(Register.A2) + val destination = computer.getRegisterValue(Register.A3) + + val maxSize = computer.fetchWordFromMemory(dirent + 2) + val maxSizeOut = computer.fetchWordFromMemory(destination + 2) + + val dir = readStringFromMemory(dirent) + val list: Array + + if (!computer.fileSystem.exists(dir)) { + computer.setRegisterValue(Register.RV, -1) + return + } + + list = computer.fileSystem.getFileList(dir) + + if (command.toInt() == 0) { // Count of files in the directory + computer.setRegisterValue(Register.RV, list.size) + } + if (command.toInt() == 1) { + if (offset < 0 || offset >= list.size) { + computer.setRegisterValue(Register.RV, -2) + return + } + + val file = list[offset.toInt()] + val name = file.getName() + val size = min(maxSizeOut - 1, name.length) + + computer.writeWordToMemory(destination.toInt(), if (file.isDirectory()) 1 else 0) + + for (i in 0.., + val sourceName: String, + @JvmField val debugInfo: DebugInfo +) { + enum class Format(val formatName: String) { + Orc1("orc1"); + } + + fun dump(): String? { + return Gson().toJson(this) + } + + @Throws(IOException::class) + fun dump(path: Path) { + FileWriter(path.toFile()).use { fw -> + dump(fw) + } + } + + fun dump(writer: Writer?) { + val gson = Gson() + gson.toJson(this, writer) + } + + companion object { + fun load(exe: String?): Executable? { + return Gson().fromJson(exe, Executable::class.java) + } + + @JvmStatic + @Throws(IOException::class) + fun load(path: Path): Executable? { + FileReader(path.toFile()).use { fw -> + return load(fw) + } + } + + @Throws(IOException::class) + fun load(reader: Reader): Executable? { + val gson = Gson() + return gson.fromJson(reader, Executable::class.java) + } + } +} diff --git a/src/jvmMain/kotlin/mtmc/os/fs/ExecutableFileTypeDetector.java b/src/jvmMain/kotlin/mtmc/os/fs/ExecutableFileTypeDetector.java new file mode 100644 index 0000000..fb62276 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/os/fs/ExecutableFileTypeDetector.java @@ -0,0 +1,32 @@ +package mtmc.os.fs; + +import mtmc.os.exec.Executable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.spi.FileTypeDetector; + +/** + * + * @author jbanes + */ +public class ExecutableFileTypeDetector extends FileTypeDetector { + + @Override + public String probeContentType(Path path) throws IOException { + try(var in = Files.newInputStream(path)) { + if(in.read() != '{') return null; + } + + try { + Executable.load(path); + + return "text/mtmc16-bin"; + } catch(IOException e) { + return null; + } + + } + +} diff --git a/src/jvmMain/kotlin/mtmc/os/fs/FileSystem.java b/src/jvmMain/kotlin/mtmc/os/fs/FileSystem.java new file mode 100644 index 0000000..27a0c54 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/os/fs/FileSystem.java @@ -0,0 +1,199 @@ +package mtmc.os.fs; + +import mtmc.emulator.MonTanaMiniComputer; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +public class FileSystem { + private String cwd = "/home"; + private MonTanaMiniComputer computer; + static final Path DISK_PATH = Path.of(System.getProperty("user.dir"), "disk").toAbsolutePath(); + + public FileSystem(MonTanaMiniComputer computer) { + this.computer = computer; + + initFileSystem(); + } + + private void initFileSystem() { + if (DISK_PATH.toFile().exists()) return; + + // Make the disk/ directory + DISK_PATH.toFile().mkdirs(); + + try (var in = new ZipInputStream(getClass().getResourceAsStream("/disk.zip"))) { + ZipEntry entry; + File file; + + byte[] data = new byte[4096]; + int count; + + while ((entry = in.getNextEntry()) != null) { + file = new File(entry.getName()); + + if (entry.isDirectory()) { + file.mkdirs(); + } else { + file.getParentFile().mkdirs(); + + try (var out = new FileOutputStream(file)) { + while((count = in.read(data)) > 0) { + out.write(data, 0, count); + } + } + } + } + } catch(IOException e) { + e.printStackTrace(); + } + } + + private void notifyOfFileSystemUpdate() { + if (this.computer != null) { + computer.notifyOfFileSystemUpdate(); + } + } + + public String getCWD() { + return this.cwd; + } + + public void setCWD(String cwd) { + this.cwd = resolve(cwd); + this.notifyOfFileSystemUpdate(); + } + + public boolean exists(String path) { + return new File(DISK_PATH.toFile(), resolve(path)).exists(); + } + + public boolean mkdir(String path) { + boolean success = new File(DISK_PATH.toFile(), resolve(path)).mkdir(); + + if (success) { + computer.notifyOfFileSystemUpdate(); + } + + return success; + } + + public boolean delete(String path) { + boolean success = new File(DISK_PATH.toFile(), resolve(path)).delete(); + + if (success) { + computer.notifyOfFileSystemUpdate(); + } + + return success; + } + + public String resolve(String filename) { + File root = DISK_PATH.toFile(); + File directory = filename.startsWith("/") ? root : new File(root, cwd); + String[] path = filename.split("/"); + + for (String name : path) { + if (name.equals(".")) { + continue; + } else if (name.equals("..") && directory.equals(root)) { + continue; + } else if (name.equals("..")) { + directory = directory.getParentFile(); + } else { + directory = new File(directory, name); + } + } + + if(directory.equals(root)) { + return "/"; + } + + return directory.getAbsolutePath().substring(root.getAbsolutePath().length()); + } + + public Path getRealPath(String path) { // Resolves given path and returns /disk/ + path + String resolvedPath = resolve(path); + String slashGone = resolvedPath.length() > 0 ? resolvedPath.substring(1) : ""; + File file = DISK_PATH.resolve(slashGone).toFile(); + + if (file.getAbsolutePath().length() < DISK_PATH.toFile().getAbsolutePath().length()) { + return DISK_PATH; + } + + return file.toPath(); + } + + public File[] getFileList(String path) { + File resolvedPath = getRealPath(path).toFile(); + + if (!resolvedPath.isDirectory()) return new File[]{ resolvedPath }; + + return resolvedPath.listFiles(); + } + + public Listing listFiles(String path) { + File resolvedPath = getRealPath(path).toFile(); + + return new Listing(this, resolvedPath); + } + + public Listing listCWD() { + return listFiles(cwd); + } + + public Listing listRoot() { + return listFiles("/"); + } + + public void writeFile(String path, String contents) throws IOException { + Path filePath = getRealPath(path); + Files.writeString(filePath, contents); + } + + public String readFile(String path) throws FileNotFoundException, IOException { + Path filePath = getRealPath(path); + var contents = Files.readString(filePath); + return contents; + } + + public String getMimeType(String path) throws IOException { + var file = getRealPath(path); + var name = file.toFile().getName().toLowerCase(); + var probed = Files.probeContentType(file); + + if(name.endsWith(".asm")) return "text/x-asm"; + if(name.endsWith(".c")) return "text/x-csrc"; + if(name.endsWith(".sea")) return "text/x-csrc"; + if(name.endsWith(".h")) return "text/x-csrc"; + if(probed != null) return probed; + + return "application/octet-stream"; + } + + public InputStream openFile(String path) throws IOException { + var file = getRealPath(path).toFile(); + + return new FileInputStream(file); + } + + public void saveFile(String path, InputStream contents) throws IOException { + var file = getRealPath(path).toFile(); + byte[] data = new byte[4096]; + int count; + + try(var out = new FileOutputStream(file)) { + while((count = contents.read(data)) > 0) { + out.write(data, 0, count); + } + } + } +} \ No newline at end of file diff --git a/src/jvmMain/kotlin/mtmc/os/fs/Listing.java b/src/jvmMain/kotlin/mtmc/os/fs/Listing.java new file mode 100644 index 0000000..c2af8c5 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/os/fs/Listing.java @@ -0,0 +1,52 @@ +package mtmc.os.fs; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class Listing { + public final String path; + public final String name; + public final boolean directory; + public final boolean root; + + private FileSystem fs; + private File file; + + public Listing(FileSystem fs, File file) { + String root = FileSystem.DISK_PATH.toFile().getAbsolutePath().replace('\\', '/'); + String path = file.getAbsolutePath().substring(root.length()).replace('\\', '/'); + + this.fs = fs; + this.file = file; + + this.path = path.length() > 0 ? path : "/"; + this.name = path.length() > 0 ? file.getName() : "/"; + this.directory = file.isDirectory(); + this.root = this.path.equals("/"); + } + + public Listing getParent() { + return new Listing(fs, file.getParentFile()); + } + + public List list() { + if (!directory) { + return new ArrayList<>(); + } + + var list = new ArrayList(); + var children = file.listFiles(); + + Arrays.sort(children, (a, b) -> { + return a.getName().compareTo(b.getName()); + }); + + for (File child : children) { + list.add(new Listing(fs, child)); + } + + return list; + } +} diff --git a/src/jvmMain/kotlin/mtmc/os/fs/PlainTextFileTypeDetector.java b/src/jvmMain/kotlin/mtmc/os/fs/PlainTextFileTypeDetector.java new file mode 100644 index 0000000..23d5ca2 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/os/fs/PlainTextFileTypeDetector.java @@ -0,0 +1,30 @@ +package mtmc.os.fs; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.spi.FileTypeDetector; + +/** + * + * @author jbanes + */ +public class PlainTextFileTypeDetector extends FileTypeDetector { + + @Override + public String probeContentType(Path path) throws IOException { + int c; + + try(var in = Files.newInputStream(path)) { + // Detect non-ASCII characters + while ((c = in.read()) >= 0) { + if (c < 9 || c > 126) { + return null; + } + } + } + + return "text/plain"; + } + +} diff --git a/src/jvmMain/kotlin/mtmc/os/shell/Shell.java b/src/jvmMain/kotlin/mtmc/os/shell/Shell.java new file mode 100644 index 0000000..450e018 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/os/shell/Shell.java @@ -0,0 +1,160 @@ +package mtmc.os.shell; + +import mtmc.asm.Assembler; +import mtmc.asm.AssemblyResult; +import mtmc.asm.instructions.Instruction; +import mtmc.emulator.DebugInfo; +import mtmc.emulator.MonTanaMiniComputer; +import mtmc.emulator.Register; +import mtmc.os.exec.Executable; +import mtmc.os.fs.FileSystem; +import mtmc.os.shell.builtins.AssembleCommand; +import mtmc.os.shell.builtins.DisplayCommand; +import mtmc.os.shell.builtins.ExitCommand; +import mtmc.os.shell.builtins.GetCommand; +import mtmc.os.shell.builtins.HelpCommand; +import mtmc.os.shell.builtins.LoadCommand; +import mtmc.os.shell.builtins.PauseCommand; +import mtmc.os.shell.builtins.RunCommand; +import mtmc.os.shell.builtins.SeacCommand; +import mtmc.os.shell.builtins.SetCommand; +import mtmc.os.shell.builtins.SpeedCommand; +import mtmc.os.shell.builtins.StepCommand; +import mtmc.os.shell.builtins.WebCommand; +import mtmc.tokenizer.MTMCToken; +import mtmc.tokenizer.MTMCTokenizer; + +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.Map; + +import static mtmc.tokenizer.MTMCToken.TokenType.IDENTIFIER; +import static mtmc.tokenizer.MTMCToken.TokenType.QUESTION_MARK; + +public class Shell { + private static final Map COMMANDS = new LinkedHashMap<>(); + + static { + COMMANDS.put("help", new HelpCommand()); + COMMANDS.put("exit", new ExitCommand()); + COMMANDS.put("set", new SetCommand()); + COMMANDS.put("get", new GetCommand()); + COMMANDS.put("web", new WebCommand()); + COMMANDS.put("disp", new DisplayCommand()); + COMMANDS.put("asm", new AssembleCommand()); + COMMANDS.put("load", new LoadCommand()); + COMMANDS.put("step", new StepCommand()); + COMMANDS.put("run", new RunCommand()); + COMMANDS.put("pause", new PauseCommand()); + COMMANDS.put("speed", new SpeedCommand()); + COMMANDS.put("sc", new SeacCommand()); + } + + public static boolean isCommand(String cmd) { + return COMMANDS.containsKey(cmd.toLowerCase()); + } + + private static boolean findExecutable(String path, FileSystem fs) { + if (path == null || path.equals("")) return false; + if (fs.exists(path) && !fs.listFiles(path).directory) return true; + if (fs.exists("/bin/" + path) && !fs.listFiles("/bin/" + path).directory) return true; + + return false; + } + + private static void runExecutable(String file, String command, MTMCTokenizer tokens, MonTanaMiniComputer computer) throws Exception { + FileSystem fs = computer.getFileSystem(); + Path srcPath = Path.of("disk/" + fs.resolve(file)); + if(!srcPath.toFile().exists()) { + srcPath = Path.of("disk" + fs.resolve("/bin/" + file)); + } + Executable exec = Executable.load(srcPath); + computer.load(exec.code, exec.data, exec.graphics, exec.debugInfo); + tokens.consume(); + String arg = command.substring(file.length()).strip(); + computer.getOS().applyBreakpoints(); + computer.setArg(arg); + computer.run(); + } + + public static void execCommand(String command, MonTanaMiniComputer computer) { + MTMCTokenizer tokens = new MTMCTokenizer(command, "#"); + try { + MTMCToken identifier = tokens.matchAndConsume(IDENTIFIER); + String cmd; + if (identifier == null) { + MTMCToken question = tokens.matchAndConsume(QUESTION_MARK); + String executable = tokens.collapseTokensAsString(); + + // alias ? to help + if (question != null) { + cmd = "help"; + } else if (findExecutable(executable, computer.getFileSystem())) { + cmd = executable; + } else { + printShellHelp(computer); + return; + } + } else { + cmd = identifier.stringValue(); + } + if (isCommand(cmd)) { + COMMANDS.get(cmd.toLowerCase()).exec(tokens, computer); + } else { + tokens.reset(); + LinkedList asm = new LinkedList<>(tokens.stream().toList()); + LinkedList updatedAsm = Assembler.Companion.transformSyntheticInstructions(asm); + MTMCToken firstToken = updatedAsm.peekFirst(); + String firstTokenStr = firstToken.stringValue(); + if (!updatedAsm.isEmpty() && Instruction.isInstruction(firstTokenStr)) { + Assembler assembler = new Assembler(); + AssemblyResult result = assembler.assemble(command); + if (result.errors.isEmpty()) { + byte[] code = result.code; + if (code.length == 4) { + int data = (code[2] << 8) | code[3]; + computer.setRegisterValue(Register.DR, data); + } + int lower = code[1] & 0xFF; + int higher = code[0] & 0xFF; + int inst = (higher << 8) | lower; + DebugInfo originalDebugInfo = computer.getDebugInfo(); + computer.setDebugInfo(result.debugInfo); + computer.execInstruction((short) inst); + computer.setDebugInfo(originalDebugInfo); + } else { + computer.getConsole().println(result.printErrors()); + } + } else { + if (findExecutable(cmd, computer.getFileSystem())) { + runExecutable(cmd, command, tokens, computer); + } else { + printShellHelp(computer); + } + } + } + } catch (NoSuchFileException e) { + computer.getConsole().println("No such file: " + e.getFile()); + } catch (Exception e) { + computer.getConsole().println(e.getMessage()); + e.printStackTrace(); + } + } + + public static void printShellHelp(MonTanaMiniComputer computer) { + computer.getConsole().println("Shell BuiltIns: \n"); + for (ShellCommand value : COMMANDS.values()) { + computer.getConsole().println(value.help); + } + computer.getConsole().println("Also: "); + computer.getConsole().println(" "); + computer.getConsole().println("or \n" + + " \n\n"); + } + + public static void printShellWelcome(MonTanaMiniComputer computer) { + computer.getConsole().println("Welcome to MtOS! Type ? for help"); + } +} diff --git a/src/jvmMain/kotlin/mtmc/os/shell/ShellCommand.kt b/src/jvmMain/kotlin/mtmc/os/shell/ShellCommand.kt new file mode 100644 index 0000000..c4bdca7 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/os/shell/ShellCommand.kt @@ -0,0 +1,13 @@ +package mtmc.os.shell + +import mtmc.emulator.MonTanaMiniComputer +import mtmc.tokenizer.MTMCTokenizer + +abstract class ShellCommand { + @Throws(Exception::class) + abstract fun exec(tokens: MTMCTokenizer, computer: MonTanaMiniComputer) + abstract val help: String? + fun usageException() { + throw UsageException(this) + } +} diff --git a/src/jvmMain/kotlin/mtmc/os/shell/UsageException.java b/src/jvmMain/kotlin/mtmc/os/shell/UsageException.java new file mode 100644 index 0000000..69e1bb2 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/os/shell/UsageException.java @@ -0,0 +1,12 @@ +package mtmc.os.shell; + +public class UsageException extends RuntimeException { + private final ShellCommand cmd; + public UsageException(ShellCommand shellCommand) { + super("Usage:\n\n" + shellCommand.help); + this.cmd = shellCommand; + } + public ShellCommand getCmd() { + return cmd; + } +} diff --git a/src/jvmMain/kotlin/mtmc/os/shell/builtins/AssembleCommand.kt b/src/jvmMain/kotlin/mtmc/os/shell/builtins/AssembleCommand.kt new file mode 100644 index 0000000..007cdd4 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/os/shell/builtins/AssembleCommand.kt @@ -0,0 +1,45 @@ +package mtmc.os.shell.builtins + +import mtmc.asm.Assembler +import mtmc.emulator.MonTanaMiniComputer +import mtmc.os.fs.FileSystem +import mtmc.os.shell.ShellCommand +import mtmc.tokenizer.MTMCTokenizer +import java.nio.file.Files +import java.nio.file.Path + +class AssembleCommand : ShellCommand() { + @Throws(Exception::class) + public override fun exec(tokens: MTMCTokenizer, computer: MonTanaMiniComputer) { + val fs = computer.fileSystem + val src = tokens.collapseTokensAsString() + require(!(src == null || src.isBlank())) { "missing or required argument 'src'" } + val srcPath: Path = getDiskPath(src, fs) + + val dst = tokens.collapseTokensAsString() + require(!(dst == null || dst.isBlank())) { "missing required argument 'dst'" } + val dstPath: Path = getDiskPath(dst, fs) + + val contents = Files.readString(srcPath) + val assembler = Assembler() + val file_name = + fs.resolve(src) // srcPath.toString().substring(DISK_PATH.toString().length()).replaceAll("\\\\", "/"); + val executable = assembler.assembleExecutable(file_name, contents) + executable.dump(dstPath) + computer.notifyOfFileSystemUpdate() + } + + override val help: String + get() = """ + asm + - src : path to a .asm file + - dst : path to a target output binary + """.trimIndent() + + companion object { + fun getDiskPath(pathString: String, fs: FileSystem): Path { + val path = Path.of("disk" + fs.resolve(pathString)) + return path.toAbsolutePath() + } + } +} diff --git a/src/jvmMain/kotlin/mtmc/os/shell/builtins/DisplayCommand.kt b/src/jvmMain/kotlin/mtmc/os/shell/builtins/DisplayCommand.kt new file mode 100644 index 0000000..7b7512d --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/os/shell/builtins/DisplayCommand.kt @@ -0,0 +1,85 @@ +package mtmc.os.shell.builtins + +import mtmc.emulator.MTMCDisplay +import mtmc.emulator.MonTanaMiniComputer +import mtmc.os.shell.ShellCommand +import mtmc.tokenizer.MTMCToken +import mtmc.tokenizer.MTMCTokenizer +import java.io.File +import java.util.* +import javax.imageio.ImageIO + +class DisplayCommand : ShellCommand() { + var random: Random = Random() + + @Throws(Exception::class) + public override fun exec(tokens: MTMCTokenizer, computer: MonTanaMiniComputer) { + if (tokens.match(MTMCToken.TokenType.IDENTIFIER)) { + val option = tokens.consumeAsString() + when (option) { + "fuzz" -> { + for (row in 0.. { + computer.display.reset() + } + + "invert" -> { + for (row in 0.. { + if (tokens.more()) { + val imagePath = tokens.collapseTokensAsString() + val file = computer.oS.loadFile(imagePath) + val img = ImageIO.read(file) + computer.display.loadScaledImage(img) + } else { + usageException() + } + } + + else -> usageException() + } + } else if (tokens.match(MTMCToken.TokenType.INTEGER)) { + val row = tokens.consumeAsInteger() + val col = tokens.require( + mtmc.tokenizer.MTMCToken.TokenType.INTEGER, + java.lang.Runnable { this.usageException() })!!.intValue() + val color = tokens.require( + mtmc.tokenizer.MTMCToken.TokenType.INTEGER, + java.lang.Runnable { this.usageException() })!!.intValue() + computer.display.setPixel(row, col, color) + } else { + usageException() + } + computer.display.sync() + } + + override val help: String + get() = """ +disp [options] - updates the display + fuzz - displays random colors + reset - resets the display + invert - inverts the display + image - loads the given image into the display + - sets the given pixel to the given color [0-3] + """.trimIndent() + + companion object { + private fun loadFile(imagePath: String?): File { + val file = File("disk/" + imagePath) + return file + } + } +} diff --git a/src/jvmMain/kotlin/mtmc/os/shell/builtins/ExitCommand.kt b/src/jvmMain/kotlin/mtmc/os/shell/builtins/ExitCommand.kt new file mode 100644 index 0000000..7ad6b06 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/os/shell/builtins/ExitCommand.kt @@ -0,0 +1,15 @@ +package mtmc.os.shell.builtins + +import mtmc.emulator.MonTanaMiniComputer +import mtmc.os.shell.ShellCommand +import mtmc.tokenizer.MTMCTokenizer + +class ExitCommand : ShellCommand() { + public override fun exec(tokens: MTMCTokenizer, computer: MonTanaMiniComputer) { + computer.console.println("Goodbye!") + System.exit(1) + } + + override val help: String + get() = "exit - exits the system" +} diff --git a/src/jvmMain/kotlin/mtmc/os/shell/builtins/GetCommand.kt b/src/jvmMain/kotlin/mtmc/os/shell/builtins/GetCommand.kt new file mode 100644 index 0000000..b5e36dc --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/os/shell/builtins/GetCommand.kt @@ -0,0 +1,43 @@ +package mtmc.os.shell.builtins + +import mtmc.emulator.MonTanaMiniComputer +import mtmc.emulator.Register +import mtmc.os.shell.ShellCommand +import mtmc.tokenizer.MTMCToken +import mtmc.tokenizer.MTMCTokenizer + +class GetCommand : ShellCommand() { + @Throws(Exception::class) + public override fun exec(tokens: MTMCTokenizer, computer: MonTanaMiniComputer) { + val memLocation = tokens.matchAndConsume( + MTMCToken.TokenType.INTEGER, + MTMCToken.TokenType.HEX, + MTMCToken.TokenType.BINARY + ) + if (memLocation == null) { + val register = tokens.matchAndConsume(MTMCToken.TokenType.IDENTIFIER) + if (register == null) usageException() + val reg = Register.toInteger(register!!.stringValue()) + if (reg >= 0) { + computer.console.println(register.stringValue() + ": " + computer.getRegisterValue(reg)) + } else { + throw IllegalArgumentException("Bad register: " + register.stringValue()) + } + } else { + if (memLocation.type === MTMCToken.TokenType.INTEGER || memLocation.type === MTMCToken.TokenType.BINARY || memLocation.type === MTMCToken.TokenType.HEX) { + computer.console.println( + "MEMORY " + memLocation.intValue() + ": " + computer.fetchWordFromMemory( + memLocation.intValue() + ) + ) + } + } + } + + override val help: String + get() = """ + get - gets a memory location value + loc: a register name or memory location + + """.trimIndent() +} diff --git a/src/jvmMain/kotlin/mtmc/os/shell/builtins/HelpCommand.kt b/src/jvmMain/kotlin/mtmc/os/shell/builtins/HelpCommand.kt new file mode 100644 index 0000000..9dbba2c --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/os/shell/builtins/HelpCommand.kt @@ -0,0 +1,16 @@ +package mtmc.os.shell.builtins + +import mtmc.emulator.MonTanaMiniComputer +import mtmc.os.shell.Shell +import mtmc.os.shell.ShellCommand +import mtmc.tokenizer.MTMCTokenizer + +class HelpCommand : ShellCommand() { + @Throws(Exception::class) + public override fun exec(tokens: MTMCTokenizer, computer: MonTanaMiniComputer) { + Shell.printShellHelp(computer) + } + + override val help: String + get() = "? or help - print help" +} diff --git a/src/jvmMain/kotlin/mtmc/os/shell/builtins/LoadCommand.kt b/src/jvmMain/kotlin/mtmc/os/shell/builtins/LoadCommand.kt new file mode 100644 index 0000000..789fe95 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/os/shell/builtins/LoadCommand.kt @@ -0,0 +1,46 @@ +package mtmc.os.shell.builtins + +import mtmc.emulator.MonTanaMiniComputer +import mtmc.os.exec.Executable.Companion.load +import mtmc.os.fs.FileSystem +import mtmc.os.shell.ShellCommand +import mtmc.tokenizer.MTMCTokenizer +import java.nio.file.Path + +class LoadCommand : ShellCommand() { + @Throws(Exception::class) + public override fun exec(tokens: MTMCTokenizer, computer: MonTanaMiniComputer) { + val fs = computer.fileSystem + val program = tokens.collapseTokensAsString() + require(!(program == null || program.isBlank())) { "missing or required argument 'src'" } + val srcPath: Path = getDiskPath(program, fs) + + val exec = load(srcPath) + computer.load(exec!!.code, exec.data, exec.graphics, exec.debugInfo) + val source = tokens.source + + // set up an argument if given + if (tokens.more()) { + val firstArgToken = tokens.consume() + val startChar: Int = firstArgToken.start + val arg = source.substring(startChar) + val strippedArg = arg.trim() + if (!strippedArg.isEmpty()) { + computer.setArg(strippedArg) + } + } + } + + override val help: String + get() = """ + load + - exec : path to an executable file + """.trimIndent() + + companion object { + fun getDiskPath(pathString: String, fs: FileSystem): Path { + val path = Path.of("disk" + fs.resolve(pathString)) + return path.toAbsolutePath() + } + } +} diff --git a/src/jvmMain/kotlin/mtmc/os/shell/builtins/PauseCommand.kt b/src/jvmMain/kotlin/mtmc/os/shell/builtins/PauseCommand.kt new file mode 100644 index 0000000..2fd296e --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/os/shell/builtins/PauseCommand.kt @@ -0,0 +1,15 @@ +package mtmc.os.shell.builtins + +import mtmc.emulator.MonTanaMiniComputer +import mtmc.os.shell.ShellCommand +import mtmc.tokenizer.MTMCTokenizer + +class PauseCommand : ShellCommand() { + override fun exec(tokens: MTMCTokenizer, computer: MonTanaMiniComputer) { + computer.pause() + } + override val help: String + + get() = "pause - pauses the computer" +} + diff --git a/src/jvmMain/kotlin/mtmc/os/shell/builtins/RunCommand.kt b/src/jvmMain/kotlin/mtmc/os/shell/builtins/RunCommand.kt new file mode 100644 index 0000000..a51f260 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/os/shell/builtins/RunCommand.kt @@ -0,0 +1,18 @@ +package mtmc.os.shell.builtins + +import mtmc.emulator.MonTanaMiniComputer +import mtmc.emulator.MonTanaMiniComputer.ComputerStatus +import mtmc.os.shell.ShellCommand +import mtmc.tokenizer.MTMCTokenizer + +class RunCommand : ShellCommand() { + override fun exec(tokens: MTMCTokenizer, computer: MonTanaMiniComputer) { + when (computer.getStatus()) { + ComputerStatus.READY, ComputerStatus.EXECUTING -> computer.run() + else -> {} + } + } + + override val help: String + get() = "run - runs the program until it halts" +} diff --git a/src/jvmMain/kotlin/mtmc/os/shell/builtins/SeacCommand.kt b/src/jvmMain/kotlin/mtmc/os/shell/builtins/SeacCommand.kt new file mode 100644 index 0000000..fc6bd34 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/os/shell/builtins/SeacCommand.kt @@ -0,0 +1,42 @@ +package mtmc.os.shell.builtins + +import mtmc.emulator.MonTanaMiniComputer +import mtmc.lang.sea.SeaLanguage +import mtmc.os.shell.ShellCommand +import mtmc.tokenizer.MTMCTokenizer +import mtmc.util.StringEscapeUtils.escapeString + +class SeacCommand : ShellCommand() { + @Throws(Exception::class) + override fun exec(tokens: MTMCTokenizer, computer: MonTanaMiniComputer) { + var output = "a.out" + var filename: String? = null + val fs = computer.fileSystem + while (tokens.more()) { + val token = tokens.collapseTokensAsString() + if (token == "-o") { + require(tokens.more()) { "expected filename after '-o'" } + output = tokens.collapseTokensAsString() + } else { + filename = token + } + } + + requireNotNull(filename) { "expected source file" } + + require(fs.exists(filename)) { "file " + escapeString(filename) + " does not exist" } + println(fs.resolve(filename)) + val lang = SeaLanguage() + val content = fs.readFile(filename) + val exec = lang.compileExecutable(fs.resolve(filename), content) + + val bin = exec.dump() + computer.fileSystem.writeFile(output, bin) + computer.notifyOfFileSystemUpdate() + } + + override val help: String + get() = """ + + """.trimIndent() +} diff --git a/src/jvmMain/kotlin/mtmc/os/shell/builtins/SetCommand.kt b/src/jvmMain/kotlin/mtmc/os/shell/builtins/SetCommand.kt new file mode 100644 index 0000000..bb93b3b --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/os/shell/builtins/SetCommand.kt @@ -0,0 +1,54 @@ +package mtmc.os.shell.builtins + +import mtmc.emulator.MonTanaMiniComputer +import mtmc.emulator.Register +import mtmc.os.shell.ShellCommand +import mtmc.tokenizer.MTMCToken +import mtmc.tokenizer.MTMCTokenizer + +class SetCommand : ShellCommand() { + @Throws(Exception::class) + public override fun exec(tokens: MTMCTokenizer, computer: MonTanaMiniComputer) { + val memLocation = tokens.matchAndConsume( + MTMCToken.TokenType.INTEGER, + MTMCToken.TokenType.HEX, + MTMCToken.TokenType.BINARY + ) + if (memLocation == null) { + val register = tokens.matchAndConsume(MTMCToken.TokenType.IDENTIFIER) + if (register == null) usageException() + val value = tokens.matchAndConsume( + MTMCToken.TokenType.INTEGER, + MTMCToken.TokenType.HEX, + MTMCToken.TokenType.BINARY + ) + if (value == null) usageException() + val reg = Register.toInteger(register!!.stringValue()) + if (reg >= 0) { + computer.setRegisterValue(reg, value!!.intValue()) + } else { + throw IllegalArgumentException("Bad register: " + register.stringValue()) + } + } else { + val value = tokens.matchAndConsume( + MTMCToken.TokenType.INTEGER, + MTMCToken.TokenType.HEX, + MTMCToken.TokenType.BINARY, + MTMCToken.TokenType.STRING + ) + if (value == null) usageException() + if (value?.type === MTMCToken.TokenType.INTEGER || value?.type === MTMCToken.TokenType.BINARY || value?.type === MTMCToken.TokenType.HEX) { + computer.writeWordToMemory(memLocation.intValue(), value.intValue()) + } else { + computer.writeStringToMemory(memLocation.intValue(), value!!.stringValue().toByteArray()) + } + } + } + + override val help: String + get() = """ + set - sets a memory location value + loc: a register name or memory location + value: an integer, hex or binary value, or, for memory locations, a quoted string + """.trimIndent() +} diff --git a/src/jvmMain/kotlin/mtmc/os/shell/builtins/SpeedCommand.kt b/src/jvmMain/kotlin/mtmc/os/shell/builtins/SpeedCommand.kt new file mode 100644 index 0000000..210dff7 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/os/shell/builtins/SpeedCommand.kt @@ -0,0 +1,41 @@ +package mtmc.os.shell.builtins + +import mtmc.emulator.MonTanaMiniComputer +import mtmc.os.shell.ShellCommand +import mtmc.tokenizer.MTMCToken +import mtmc.tokenizer.MTMCTokenizer +import java.util.* + +class SpeedCommand : ShellCommand() { + private val speeds: MutableList = Arrays.asList( + *arrayOf( + 1, 10, 100, 1000, 10000, 100000, 1000000 + ) + ) + + @Throws(Exception::class) + override fun exec(tokens: MTMCTokenizer, computer: MonTanaMiniComputer) { + if (tokens.match(MTMCToken.TokenType.IDENTIFIER)) { + computer.speed = 0 + } else if (tokens.match(MTMCToken.TokenType.INTEGER)) { + val speed = tokens.consumeAsInteger() + if (!speeds.contains(speed)) { + usageException() + } + computer.speed = speed + } else { + usageException() + } + } + + override val help: String + get() = """ +speed - set the speed of the computer + where is one of: + raw - run with no simulated speed delay + 1 - run the computer at 1hz + 10 - run the computer at 10hz + 100 - run the computer at 100hz + 1000 - run the computer at 1khz + """.trimIndent() +} diff --git a/src/jvmMain/kotlin/mtmc/os/shell/builtins/StepCommand.kt b/src/jvmMain/kotlin/mtmc/os/shell/builtins/StepCommand.kt new file mode 100644 index 0000000..1e32404 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/os/shell/builtins/StepCommand.kt @@ -0,0 +1,24 @@ +package mtmc.os.shell.builtins + +import mtmc.emulator.MonTanaMiniComputer +import mtmc.emulator.MonTanaMiniComputer.ComputerStatus +import mtmc.os.shell.ShellCommand +import mtmc.tokenizer.MTMCTokenizer + +class StepCommand : ShellCommand() { + override fun exec(tokens: MTMCTokenizer, computer: MonTanaMiniComputer) { + when (computer.getStatus()) { + ComputerStatus.READY -> { + computer.setStatus(ComputerStatus.EXECUTING) + computer.fetchAndExecute() + } + + ComputerStatus.EXECUTING -> computer.fetchAndExecute() + else -> {} + } + } + + override val help: String + get() = "step - runs the next instruction" +} + diff --git a/src/jvmMain/kotlin/mtmc/os/shell/builtins/WebCommand.kt b/src/jvmMain/kotlin/mtmc/os/shell/builtins/WebCommand.kt new file mode 100644 index 0000000..981535e --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/os/shell/builtins/WebCommand.kt @@ -0,0 +1,16 @@ +package mtmc.os.shell.builtins + +import mtmc.emulator.MonTanaMiniComputer +import mtmc.os.shell.ShellCommand +import mtmc.tokenizer.MTMCTokenizer + +class WebCommand : ShellCommand() { + @Throws(Exception::class) + public override fun exec(tokens: MTMCTokenizer, computer: MonTanaMiniComputer) { + //val server: WebServer = WebServer.getInstance(computer) + //Desktop.getDesktop().browse(server.getURL()) + } + + override val help: String + get() = "web - starts the web UI" +} diff --git a/src/jvmMain/kotlin/mtmc/tokenizer/MTMCScanner.kt b/src/jvmMain/kotlin/mtmc/tokenizer/MTMCScanner.kt new file mode 100644 index 0000000..27c4fae --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/tokenizer/MTMCScanner.kt @@ -0,0 +1,356 @@ +package mtmc.tokenizer + +import java.util.* + +class MTMCScanner(private val src: String, private val lineCommentStart: String) { + var position: Int = 0 + var line: Int = 1 + var lineOffset: Int = 0 + var tokens: LinkedList = LinkedList() + + fun tokenize(): LinkedList { + consumeWhitespace() + while (!scanEnd()) { + scanToken() + consumeWhitespace() + } + tokens.add(makeToken(MTMCToken.TokenType.EOF, "", position)) + return tokens + } + + private fun scanToken() { + if (scanLineComment()) { + return + } + if (scanNumber()) { + return + } + if (scanChar()) { + return + } + if (scanString()) { + return + } + if (scanIdentifier()) { + return + } + scanSyntax() + } + + private fun scanLineComment(): Boolean { + val bytes = lineCommentStart.toByteArray() + for (i in bytes.indices) { + val aChar = bytes[i] + if (peek(i).code.toByte() != aChar) { + return false + } + } + // consume line comment + while (peek() != '\n' && moreChars()) { + takeChar() + } + return true + } + + private fun moreChars(): Boolean { + return !scanEnd() + } + + private fun scanChar(): Boolean { + val sb = StringBuilder() + if (peek() != '\'') return false + + val start = position + takeChar() + var c: Char + if (scanEnd()) { + tokens.add(makeToken(MTMCToken.TokenType.ERROR, strValueFrom(start + 1), start)) + return true + } + c = takeChar() + if (c == '\\') { + if (moreChars()) { + val nextChar = takeChar() + if (nextChar == 'n') { + sb.append('\n') + } else if (nextChar == 't') { + sb.append('\t') + } else { + sb.append(nextChar) + } + } + } else { + sb.append(c) + } + if (scanEnd()) { + tokens.add(makeToken(MTMCToken.TokenType.ERROR, strValueFrom(start + 1), start)) + return true + } + c = takeChar() + if (c != '\'') { + tokens.add(makeToken(MTMCToken.TokenType.ERROR, strValueFrom(start + 1), start)) + return true + } + + tokens.add(makeToken(MTMCToken.TokenType.CHAR, sb.toString(), start)) + return true + } + + private fun scanString(): Boolean { + val sb = StringBuilder() + if (peek() == '"') { + val start = position + takeChar() + var c: Char + do { + if (scanEnd()) { + tokens.add(makeToken(MTMCToken.TokenType.ERROR, strValueFrom(start + 1), start)) + return true + } + c = takeChar() + if (c == '\\') { + if (moreChars()) { + val nextChar = takeChar() + if (nextChar == 'n') { + sb.append('\n') + } else if (nextChar == 't') { + sb.append('\t') + } else { + sb.append(nextChar) + } + } + } else if (c != '"') { + sb.append(c) + } + } while (c != '"') + tokens.add(makeToken(MTMCToken.TokenType.STRING, sb.toString(), start)) + return true + } else { + return false + } + } + + private fun scanIdentifier(): Boolean { + if (isAlpha(peek())) { + val start = position + while (isAlphaNumeric(peek())) { + takeChar() + } + if (peek() == ':') { + takeChar() + tokens.add(makeToken(MTMCToken.TokenType.LABEL, strValueFrom(start), start)) + } else { + tokens.add(makeToken(MTMCToken.TokenType.IDENTIFIER, strValueFrom(start), start)) + } + return true + } else { + return false + } + } + + private fun scanNumber(): Boolean { + if (isDigit(peek()) || (peek() == '-' && isDigit(peek(1)))) { + val negative = peek() == '-' + val start = position + if (negative) { + takeChar() + } else if (peek() == '0') { + if (peek(1) == 'x') { + return scanHex(start) + } + if (peek(1) == 'b') { + return scanBinary(start) + } + } + + return scanDecimal(start) + } else { + return false + } + } + + private fun scanDecimal(start: Int): Boolean { + while (isDigit(peek())) { + takeChar() + } + if (peek() == '.') { + //decimal + while (isDigit(peek())) { + takeChar() + } + tokens.add(makeToken(MTMCToken.TokenType.DECIMAL, strValueFrom(start), start)) + } else { + tokens.add(makeToken(MTMCToken.TokenType.INTEGER, strValueFrom(start), start)) + } + return true + } + + private fun scanHex(start: Int): Boolean { + takeChar() // take leading zero + takeChar() // take 'x' + while (HexFormat.isHexDigit(peek().code)) { + takeChar() + } + tokens.add(makeToken(MTMCToken.TokenType.HEX, strValueFrom(start), start)) + return true + } + + private fun scanBinary(start: Int): Boolean { + takeChar() // take leading zero + takeChar() // take 'b' + while (peek() == '1' || peek() == '0' || peek() == '_') { + takeChar() + } + tokens.add(makeToken(MTMCToken.TokenType.BINARY, strValueFrom(start), start)) + return true + } + + private fun makeToken( + type: MTMCToken.TokenType, + stringValue: String, + startPosition: Int + ): MTMCToken { + return MTMCToken(startPosition, position, line, lineOffset, stringValue, type) + } + + private fun strValueFrom(start: Int): String { + return src.substring(start, position) + } + + private fun scanSyntax() { + val start = position + if (consumeIf('(')) { + tokens.add(makeToken(MTMCToken.TokenType.LEFT_PAREN, "(", start)) + } else if (consumeIf(')')) { + tokens.add(makeToken(MTMCToken.TokenType.RIGHT_PAREN, ")", start)) + } else if (consumeIf('?')) { + tokens.add(makeToken(MTMCToken.TokenType.QUESTION_MARK, "?", start)) + } else if (consumeIf('[')) { + tokens.add(makeToken(MTMCToken.TokenType.LEFT_BRACKET, "[", start)) + } else if (consumeIf(']')) { + tokens.add(makeToken(MTMCToken.TokenType.RIGHT_BRACKET, "]", start)) + } else if (consumeIf('{')) { + tokens.add(makeToken(MTMCToken.TokenType.LEFT_BRACE, "{", start)) + } else if (consumeIf('}')) { + tokens.add(makeToken(MTMCToken.TokenType.RIGHT_BRACE, "}", start)) + } else if (consumeIf(':')) { + tokens.add(makeToken(MTMCToken.TokenType.COLON, ":", start)) + } else if (consumeIf(',')) { + tokens.add(makeToken(MTMCToken.TokenType.COMMA, ",", start)) + } else if (consumeIf('.')) { + tokens.add(makeToken(MTMCToken.TokenType.DOT, ".", start)) + } else if (consumeIf('+')) { + tokens.add(makeToken(MTMCToken.TokenType.PLUS, "+", start)) + } else if (consumeIf('-')) { + tokens.add(makeToken(MTMCToken.TokenType.MINUS, "-", start)) + } else if (consumeIf('*')) { + tokens.add(makeToken(MTMCToken.TokenType.STAR, "*", start)) + } else if (consumeIf('/')) { + tokens.add(makeToken(MTMCToken.TokenType.SLASH, "/", start)) + } else if (consumeIf('@')) { + tokens.add(makeToken(MTMCToken.TokenType.AT, "@", start)) + } else if (consumeIf('!')) { + if (consumeIf('=')) { + tokens.add(makeToken(MTMCToken.TokenType.BANG_EQUAL, "!=", start)) + } else { + tokens.add(makeToken(MTMCToken.TokenType.BANG, "!", start)) + } + } else if (consumeIf('=')) { + if (consumeIf('=')) { + tokens.add(makeToken(MTMCToken.TokenType.EQUAL_EQUAL, "==", start)) + } else { + tokens.add(makeToken(MTMCToken.TokenType.EQUAL, "=", start)) + } + } else if (consumeIf('<')) { + if (consumeIf('=')) { + tokens.add(makeToken(MTMCToken.TokenType.LESS_EQUAL, "<=", start)) + } else { + tokens.add(makeToken(MTMCToken.TokenType.LESS, "<", start)) + } + } else if (consumeIf('>')) { + if (consumeIf('=')) { + tokens.add(makeToken(MTMCToken.TokenType.GREATER_EQUAL, ">=", start)) + } else { + tokens.add(makeToken(MTMCToken.TokenType.GREATER, ">", start)) + } + } else { + tokens.add( + makeToken( + MTMCToken.TokenType.ERROR, + "", + start + ) + ) + } + } + + private fun consumeWhitespace() { + while (!scanEnd()) { + val c = peek() + if (c == ' ' || c == '\r' || c == '\t') { + position++ + lineOffset++ + continue + } else if (c == '\n') { + position++ + line++ + lineOffset = 0 + continue + } + break + } + } + + //=============================================================== + // Utility functions + //=============================================================== + private fun peek(offset: Int = 0): Char { + return charAt(position + offset) + } + + private fun charAt(index: Int): Char { + if (0 > index || src.length <= index) return '\u0000' + return src.get(index) + } + + private fun isAlpha(c: Char): Boolean { + return (c >= 'a' && c <= 'z') || + (c >= 'A' && c <= 'Z') || c == '_' + } + + private fun isAlphaNumeric(c: Char): Boolean { + return isAlpha(c) || isDigit(c) + } + + private fun isDigit(c: Char): Boolean { + return c >= '0' && c <= '9' + } + + private fun takeChar(): Char { + val c = src.get(position) + position++ + lineOffset++ + return c + } + + private fun scanEnd(): Boolean { + return position >= src.length + } + + private fun consumeIf(c: Char): Boolean { + if (peek() == c) { + takeChar() + return true + } + return false + } + + override fun toString(): String { + if (scanEnd()) { + return src + "-->[]<--" + } else { + return src.substring(0, position) + "-->[" + peek() + "]<--" + + (if (position == src.length - 1) "" else src.substring(position + 1, src.length - 1)) + } + } +} diff --git a/src/jvmMain/kotlin/mtmc/tokenizer/MTMCToken.kt b/src/jvmMain/kotlin/mtmc/tokenizer/MTMCToken.kt new file mode 100644 index 0000000..35efb74 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/tokenizer/MTMCToken.kt @@ -0,0 +1,97 @@ +package mtmc.tokenizer + +@JvmRecord +data class MTMCToken( + @JvmField val start: Int, + @JvmField val end: Int, + val line: Int, + val lineOffset: Int, + @JvmField val stringValue: String, + @JvmField val type: TokenType? +) { + override fun toString(): String { + return stringValue + } + + fun stringValue(): String { + return stringValue + } + + fun charValue(): Char { + return stringValue.get(0) + } + + fun intValue(): Int { + if (type == TokenType.INTEGER) { + return stringValue.toInt() + } else if (type == TokenType.HEX) { + val stripped = stringValue.substring(2) + val i = stripped.toShort(16) + return i.toInt() + } else if (type == TokenType.BINARY) { + val stripped = stringValue.substring(2) + val i = stripped.replace("_".toRegex(), "").toShort(2) + return i.toInt() + } else { + throw UnsupportedOperationException("Cannot return int for type " + this.type) + } + } + + fun labelValue(): String { + return stringValue.substring(0, stringValue.length - 1) + } + + fun cloneWithVal(`val`: String): MTMCToken { + return MTMCToken(start, end, line, lineOffset, `val`, type) + } + + enum class TokenType { + LEFT_PAREN, + RIGHT_PAREN, + LEFT_BRACE, + RIGHT_BRACE, + LEFT_BRACKET, + RIGHT_BRACKET, + COLON, + COMMA, + DOT, + MINUS, + PLUS, + SLASH, + AT, + STAR, + QUESTION_MARK, + BANG, + BANG_EQUAL, + EQUAL, + EQUAL_EQUAL, + GREATER, + GREATER_EQUAL, + LESS, LESS_EQUAL, + IDENTIFIER, + LABEL, + STRING, + CHAR, + INTEGER, + DECIMAL, + HEX, + BINARY, + ERROR, + SOF, + EOF + } + + companion object { + fun join(a: MTMCToken, b: MTMCToken, type: TokenType?): MTMCToken { + require(a.end == b.start) { "tokens must be joint!" } + return MTMCToken( + a.start, + b.end, + a.line, + a.lineOffset, + a.stringValue + b.stringValue, + type + ) + } + } +} diff --git a/src/jvmMain/kotlin/mtmc/tokenizer/MTMCTokenizer.kt b/src/jvmMain/kotlin/mtmc/tokenizer/MTMCTokenizer.kt new file mode 100644 index 0000000..575a7cc --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/tokenizer/MTMCTokenizer.kt @@ -0,0 +1,111 @@ +package mtmc.tokenizer + +import java.util.* +import java.util.stream.Stream +import kotlin.math.max + +class MTMCTokenizer(var source: String, lineCommentStart: String) { + var tokens: LinkedList = MTMCScanner(source, lineCommentStart).tokenize() + var currentToken: Int = 0 + + fun currentToken(): MTMCToken { + return tokens.get(currentToken) + } + + fun consume(): MTMCToken { + return tokens.get(currentToken++) + } + + fun consumeAsString(): String { + return tokens.get(currentToken++).stringValue + } + + fun matchAndConsume(vararg type: MTMCToken.TokenType?): MTMCToken? { + if (match(*type)) { + return consume() + } else { + return null + } + } + + fun matchAndConsume(identifier: String?): Boolean { + if (currentToken().type == MTMCToken.TokenType.IDENTIFIER && + currentToken().stringValue == identifier + ) { + return true + } else { + return false + } + } + + fun match(vararg type: MTMCToken.TokenType?): Boolean { + for (tokenType in type) { + if (currentToken().type == tokenType) { + return true + } + } + return false + } + + fun reset() { + currentToken = 0 + } + + fun more(): Boolean { + return currentToken().type != MTMCToken.TokenType.EOF + } + + fun previousToken(): MTMCToken? { + return tokens.get(max(0, currentToken - 1)) + } + + fun stream(): Stream { + return tokens.stream() + } + + override fun toString(): String { + val sb = StringBuilder() + for (i in tokens.indices) { + val token = tokens.get(i) + if (i == currentToken) { + sb.append("-->[") + } + sb.append(token.stringValue) + if (i == currentToken) { + sb.append("]<--") + } + sb.append(" ") + } + return sb.toString() + } + + // collapse all adjacent tokens into a string + fun collapseTokensAsString(): String { + if (more()) { + val sb = StringBuilder() + var last: MTMCToken + var next: MTMCToken + do { + last = consume() + sb.append(last.stringValue) + next = currentToken() + } while (more() && last.end == next.start) + return sb.toString() + } else { + return "" + } + } + + fun consumeAsInteger(): Int { + return consume().intValue() + } + + fun require(tokenType: MTMCToken.TokenType, notFound: Runnable): MTMCToken? { + if (match(tokenType)) { + return consume() + } else { + notFound.run() + return null + } + } +} \ No newline at end of file diff --git a/src/jvmMain/kotlin/mtmc/util/BinaryUtils.kt b/src/jvmMain/kotlin/mtmc/util/BinaryUtils.kt new file mode 100644 index 0000000..01745ee --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/util/BinaryUtils.kt @@ -0,0 +1,50 @@ +package mtmc.util + +object BinaryUtils { + /** + * @param start - a base 1 index to start at (16 is the most significant, 1 is the least) + * @param totalBits - the total bits to get + * @param value - the value to get the bits from + */ + fun getBits(start: Int, totalBits: Int, value: Short): Short { + if (totalBits <= 0) { + return 0 + } + val returnValue = (value.toInt() and 0xffff) ushr (start - totalBits) + var mask = 0 + var toShift = totalBits + while (toShift > 0) { + toShift-- + mask = mask shl 1 + mask = mask + 1 + } + return (returnValue and mask).toShort() + } + + fun toBinary(aByte: Byte): String { + val binaryString = Integer.toBinaryString(aByte.toInt()) + val formatted = String.format("%8s", binaryString) + val zeroed = formatted.replace(" ".toRegex(), "0") + val underScored = zeroed.replace("....".toRegex(), "$0_") + val noTrailingUnderscore = underScored.substring(0, underScored.length - 1) + return "0b" + noTrailingUnderscore + } + + fun toBinary(aShort: Short): String { + val binaryString = Integer.toBinaryString(aShort.toInt()) + val formatted = String.format("%16s", binaryString) + val zeroed = formatted.replace(" ".toRegex(), "0") + val underScored = zeroed.replace("....".toRegex(), "$0_") + val noTrailingUnderscore = underScored.substring(0, underScored.length - 1) + return "0b" + noTrailingUnderscore + } + + fun toBinary(anInt: Int): String { + val binaryString = Integer.toBinaryString(anInt) + val formatted = String.format("%32s", binaryString) + val zeroed = formatted.replace(" ".toRegex(), "0") + val underScored = zeroed.replace("....".toRegex(), "$0_") + val noTrailingUnderscore = underScored.substring(0, underScored.length - 1) + return "0b" + noTrailingUnderscore + } +} diff --git a/src/jvmMain/kotlin/mtmc/util/SafeClosable.kt b/src/jvmMain/kotlin/mtmc/util/SafeClosable.kt new file mode 100644 index 0000000..d5aa578 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/util/SafeClosable.kt @@ -0,0 +1,7 @@ +package mtmc.util + +import java.io.Closeable + +interface SafeClosable : Closeable { + override fun close() +} diff --git a/src/jvmMain/kotlin/mtmc/util/StringEscapeUtils.kt b/src/jvmMain/kotlin/mtmc/util/StringEscapeUtils.kt new file mode 100644 index 0000000..8413e79 --- /dev/null +++ b/src/jvmMain/kotlin/mtmc/util/StringEscapeUtils.kt @@ -0,0 +1,31 @@ +package mtmc.util + +import java.util.function.IntConsumer + +object StringEscapeUtils { + @JvmStatic + fun escapeString(s: String): String { + var s = s + s = s + .replace("\"", "\\\"") + .replace("\\", "\\\\") + .replace("\b", "\\b") + .replace("\n", "\\n") + .replace("\t", "\\t") + .replace("\u000c", "\\f") + .replace("\r", "\\r") + + val out = StringBuilder() + out.append('"') + s.codePoints() + .forEach(IntConsumer { c: Int -> + if (c !in 32..0x7f) { + out.append("\\u{").append(Integer.toHexString(c)).append("}") + } else { + out.append(c.toChar()) + } + }) + out.append('"') + return out.toString() + } +} diff --git a/src/jvmMain/kotlin/nl/astraeus/tmpl/web/GenerateId.kt b/src/jvmMain/kotlin/mtmc/web/GenerateId.kt similarity index 91% rename from src/jvmMain/kotlin/nl/astraeus/tmpl/web/GenerateId.kt rename to src/jvmMain/kotlin/mtmc/web/GenerateId.kt index 72ed436..67ee3b2 100644 --- a/src/jvmMain/kotlin/nl/astraeus/tmpl/web/GenerateId.kt +++ b/src/jvmMain/kotlin/mtmc/web/GenerateId.kt @@ -1,4 +1,4 @@ -package nl.astraeus.tmpl.web +package mtmc.web import java.security.SecureRandom diff --git a/src/jvmMain/kotlin/nl/astraeus/tmpl/web/Index.kt b/src/jvmMain/kotlin/mtmc/web/Index.kt similarity index 83% rename from src/jvmMain/kotlin/nl/astraeus/tmpl/web/Index.kt rename to src/jvmMain/kotlin/mtmc/web/Index.kt index 3bb4897..f735f3f 100644 --- a/src/jvmMain/kotlin/nl/astraeus/tmpl/web/Index.kt +++ b/src/jvmMain/kotlin/mtmc/web/Index.kt @@ -1,4 +1,4 @@ -package nl.astraeus.tmpl.web +package mtmc.web import kotlinx.html.body import kotlinx.html.head @@ -7,10 +7,9 @@ import kotlinx.html.meta import kotlinx.html.script import kotlinx.html.stream.appendHTML import kotlinx.html.title -import nl.astraeus.tmpl.REPO_NAME -import nl.astraeus.tmpl.itemUrl -import nl.astraeus.tmpl.pageTitle -import nl.astraeus.tmpl.repoName +import mtmc.itemUrl +import mtmc.pageTitle +import mtmc.repoName fun generateIndex(patch: String?): String { val result = StringBuilder(); diff --git a/src/jvmMain/kotlin/nl/astraeus/tmpl/web/RequestHandler.kt b/src/jvmMain/kotlin/mtmc/web/RequestHandler.kt similarity index 94% rename from src/jvmMain/kotlin/nl/astraeus/tmpl/web/RequestHandler.kt rename to src/jvmMain/kotlin/mtmc/web/RequestHandler.kt index 04f514d..4ea2833 100644 --- a/src/jvmMain/kotlin/nl/astraeus/tmpl/web/RequestHandler.kt +++ b/src/jvmMain/kotlin/mtmc/web/RequestHandler.kt @@ -1,11 +1,11 @@ -package nl.astraeus.tmpl.web +package mtmc.web import io.undertow.server.HttpHandler import io.undertow.server.HttpServerExchange import io.undertow.server.handlers.PathHandler import io.undertow.server.handlers.resource.PathResourceManager import io.undertow.server.handlers.resource.ResourceHandler -import nl.astraeus.tmpl.itemUrl +import mtmc.itemUrl import java.nio.file.Paths import kotlin.text.startsWith diff --git a/src/jvmMain/kotlin/nl/astraeus/tmpl/web/WebsockerHandler.kt b/src/jvmMain/kotlin/mtmc/web/WebsockerHandler.kt similarity index 98% rename from src/jvmMain/kotlin/nl/astraeus/tmpl/web/WebsockerHandler.kt rename to src/jvmMain/kotlin/mtmc/web/WebsockerHandler.kt index d9ce8a5..991c879 100644 --- a/src/jvmMain/kotlin/nl/astraeus/tmpl/web/WebsockerHandler.kt +++ b/src/jvmMain/kotlin/mtmc/web/WebsockerHandler.kt @@ -1,4 +1,4 @@ -package nl.astraeus.tmpl.web +package mtmc.web import io.undertow.Handlers.websocket import io.undertow.server.HttpHandler diff --git a/src/jvmMain/kotlin/nl/astraeus/tmpl/Placeholders.kt b/src/jvmMain/kotlin/nl/astraeus/tmpl/Placeholders.kt deleted file mode 100644 index c8b8250..0000000 --- a/src/jvmMain/kotlin/nl/astraeus/tmpl/Placeholders.kt +++ /dev/null @@ -1,10 +0,0 @@ -package nl.astraeus.tmpl - -val REPO_NAME = "dummy so the gitea template compiles, please remove" - -val SERVER_PORT = 7001 -val JDBC_PORT = 8001 - -val pageTitle = "mtmc-web" -val itemUrl = "mtmc-web" -val repoName = "mtmc-web" diff --git a/web/web.txt b/web/web.txt deleted file mode 100644 index efdc935..0000000 --- a/web/web.txt +++ /dev/null @@ -1 +0,0 @@ -Directory where the web content (html + css + resources) will be placed.