From 9a2d215637f15dac7c77a8d3e6b4342503a628b3 Mon Sep 17 00:00:00 2001 From: minjaesong Date: Wed, 29 Jul 2020 18:51:06 +0900 Subject: [PATCH] v2k parser wip --- Videotron2K.md | 40 ++++-- src/net/torvald/random/HQRNG.java | 194 +++++++++++++++++++++++++ src/net/torvald/tsvm/Videotron2K.kt | 216 ++++++++++++++++++++++++---- 3 files changed, 416 insertions(+), 34 deletions(-) create mode 100644 src/net/torvald/random/HQRNG.java diff --git a/Videotron2K.md b/Videotron2K.md index 54db056..6714cbc 100644 --- a/Videotron2K.md +++ b/Videotron2K.md @@ -80,10 +80,23 @@ Commands can have "conditional postfix" used to execute the command conditionall - ge r : if r >= 0 - le r : if r <= 0 +## Programming Guidelines + +### Scene + +* A scene will always need one or more `exit` command; scenes are infini-loop object and without `exit`, there will be no way to terminate the scene. + +### Program + +* A program will not infini-loop on its own; you need to explicitly enter the `loop` command. + + ## Available Commands ### Arithmetic +NOTE : immediates and variables can substitute registers + * add rA rB rC : rC = rA + rB * sub rA rB rC : rC = rA - rB * mul rA rB rC : rC = rA * rB @@ -95,21 +108,22 @@ Commands can have "conditional postfix" used to execute the command conditionall * shr rA rB rC : rC = rA >> rB * ushr rA rB rC : rC = rA >>> rB -* inc rA rB : rC = rA + 1 -* dec rA rB : rC = rA - 1 - +* inc R : R = R + 1 +* dec R : R = R - 1 * not R : R = !R (ones' complement of R) * neg R : R = -R (twos' complement of R) ### Conditional +NOTE : immediates and variables can substitute registers + * cmp rA rB rC : compares rA and rB and stores result to rC. 1 if rA > rB, -1 if rA < rB, 0 if rA == rB. ### Data Control NOTE: Any drawing command will clobber internal memory starting from address zero. -* mov rA rB/I : assignes R with contents of rB/constant I +* mov rA rB/I : assignes rA with contents of rB/constant I * data rA/I bytes : writes bytes to the internal memory of address starting from rA/I * mcp from y x len : copies part of the internal memory to the framebuffer, from the internal memory address `from`, to scanline `y`, horizontal position `x`, with copying length of `len`. @@ -117,17 +131,19 @@ NOTE: Any drawing command will clobber internal memory starting from address zer ### Flow Control * perform scenename : gosub into the scenename +* jumpto label_name : goto the label name. JUMPTO only works when the label is within the same scope. * next : advance a frame counter (frm) and sleeps until it is time to draw next frame -* exit : terminates current scene. System will error out if this command is used outside of a scene +* loop : will jump to the beginning of the current scope (scene). @-padded line will NOT be executed. The opposite of EXIT +* exit : terminates current scene. System will error out if this command is used outside of a scene. The opposite of LOOP * exeunt : completely terminates the program ### Drawing NOTE: Any drawing command will clobber internal memory starting from address zero. -* fillin byte y x-start y-end-exclusive : fills entire scanline of `y` from the horizontal position `x-start` through +* fillin byte y x-start y-end-exclusive : fills entire scanline of `y` with `byte` from the horizontal position `x-start` through `y-end-exclusive` MINUS ONE. final (px,py) will be (scanline,x-end-exclusive) -* plot byte... : writes bytes into the framebuffer +* plot byte... : writes bytes into the framebuffer. The `px` register will auto-increment but `py` won't! * fillscr byte : fills entire screen with a given byte * goto x y : writes `x` to px and `y` to py (use `mov px ` to write to px/py only) * border r g b : sets border colour @@ -146,4 +162,12 @@ NOTE: Any drawing command will clobber internal memory starting from address zer #### Predefined Constants * RATET : framerate defined by miliseconds between each frame. Mutually exclusive with RATEF. -* RATEF : framerate defined by how many frames must be shown in one second. Mutually exclusive with RATET. \ No newline at end of file +* RATEF : framerate defined by how many frames must be shown in one second. Mutually exclusive with RATET. + +### Auto-incrementation + +Some instructions will cause a register to auto-increment. Auto-increment rule is as follows: + +* `px` : px = (px + 1 fmod width) +* `py` : py = (py + 1 fmod height) +* If an arithmetic command is used against `px` or `py` register, above rules will apply to the registers. \ No newline at end of file diff --git a/src/net/torvald/random/HQRNG.java b/src/net/torvald/random/HQRNG.java new file mode 100644 index 0000000..1ead5a0 --- /dev/null +++ b/src/net/torvald/random/HQRNG.java @@ -0,0 +1,194 @@ +package net.torvald.random; + +import java.util.Random; + +/** + * Xoroshiro128 + * + * Note: low 4 bits are considered "dirty"; avoid these bits for making random set of booleans + * + * see https://github.com/SquidPony/SquidLib/blob/master/squidlib-util/src/main/java/squidpony/squidmath/XoRoRNG.java + */ +public class HQRNG extends Random { + + private static final long DOUBLE_MASK = (1L << 53) - 1; + private static final double NORM_53 = 1. / (1L << 53); + private static final long FLOAT_MASK = (1L << 24) - 1; + private static final double NORM_24 = 1. / (1L << 24); + + private static final long serialVersionUID = 1018744536171610262L; + + private long state0, state1; + + public long getState0() { + return state0; + } + + public long getState1() { + return state1; + } + + /** + * Creates a new generator seeded using four calls to Math.random(). + */ + public HQRNG() { + this((long) ((Math.random() - 0.5) * 0x10000000000000L) + ^ (long) (((Math.random() - 0.5) * 2.0) * 0x8000000000000000L), + (long) ((Math.random() - 0.5) * 0x10000000000000L) + ^ (long) (((Math.random() - 0.5) * 2.0) * 0x8000000000000000L)); + } + /** + * Constructs this XoRoRNG by dispersing the bits of seed using {@link #setSeed(long)} across the two parts of state + * this has. + * @param seed a long that won't be used exactly, but will affect both components of state + */ + public HQRNG(final long seed) { + setSeed(seed); + } + /** + * Constructs this XoRoRNG by calling {@link #setSeed(long, long)} on the arguments as given; see that method for + * the specific details (stateA and stateB are kept as-is unless they are both 0). + * @param stateA the number to use as the first part of the state; this will be 1 instead if both seeds are 0 + * @param stateB the number to use as the second part of the state + */ + public HQRNG(final long stateA, final long stateB) { + setSeed(stateA, stateB); + } + + @Override + public final int next(int bits) { + final long s0 = state0; + long s1 = state1; + final int result = (int)(s0 + s1) >>> (32 - bits); + s1 ^= s0; + state0 = (s0 << 55 | s0 >>> 9) ^ s1 ^ (s1 << 14); // a, b + state1 = (s1 << 36 | s1 >>> 28); // c + return result; + } + + @Override + public final long nextLong() { + final long s0 = state0; + long s1 = state1; + final long result = s0 + s1; + + s1 ^= s0; + state0 = (s0 << 55 | s0 >>> 9) ^ s1 ^ (s1 << 14); // a, b + state1 = (s1 << 36 | s1 >>> 28); // c + /* + state0 = Long.rotateLeft(s0, 55) ^ s1 ^ (s1 << 14); // a, b + state1 = Long.rotateLeft(s1, 36); // c + */ + return result; + } + + /** + * Can return any int, positive or negative, of any size permissible in a 32-bit signed integer. + * @return any int, all 32 bits are random + */ + public int nextInt() { + return (int)nextLong(); + } + + /** + * Exclusive on the outer bound; the inner bound is 0. The bound may be negative, which will produce a non-positive + * result. + * @param bound the outer exclusive bound; may be positive or negative + * @return a random int between 0 (inclusive) and bound (exclusive) + */ + public int nextInt(final int bound) { + return (int) ((bound * (nextLong() >>> 33)) >> 31); + } + /** + * Inclusive lower, exclusive upper. + * @param inner the inner bound, inclusive, can be positive or negative + * @param outer the outer bound, exclusive, should be positive, should usually be greater than inner + * @return a random int that may be equal to inner and will otherwise be between inner and outer + */ + public int nextInt(final int inner, final int outer) { + return inner + nextInt(outer - inner); + } + + /** + * Exclusive on the outer bound; the inner bound is 0. The bound may be negative, which will produce a non-positive + * result. + * @param bound the outer exclusive bound; may be positive or negative + * @return a random long between 0 (inclusive) and bound (exclusive) + */ + public long nextLong(long bound) { + long rand = nextLong(); + final long randLow = rand & 0xFFFFFFFFL; + final long boundLow = bound & 0xFFFFFFFFL; + rand >>>= 32; + bound >>= 32; + final long z = (randLow * boundLow >> 32); + long t = rand * boundLow + z; + final long tLow = t & 0xFFFFFFFFL; + t >>>= 32; + return rand * bound + t + (tLow + randLow * bound >> 32) - (z >> 63) - (bound >> 63); + } + /** + * Inclusive inner, exclusive outer; both inner and outer can be positive or negative. + * @param inner the inner bound, inclusive, can be positive or negative + * @param outer the outer bound, exclusive, can be positive or negative and may be greater than or less than inner + * @return a random long that may be equal to inner and will otherwise be between inner and outer + */ + public long nextLong(final long inner, final long outer) { + return inner + nextLong(outer - inner); + } + + public double nextDouble() { + return (nextLong() & DOUBLE_MASK) * NORM_53; + } + + public float nextFloat() { + return (float) ((nextLong() & FLOAT_MASK) * NORM_24); + } + + public boolean nextBoolean() { + return nextLong() < 0L; + } + + public void nextBytes(final byte[] bytes) { + int i = bytes.length, n = 0; + while (i != 0) { + n = Math.min(i, 8); + for (long bits = nextLong(); n-- != 0; bits >>>= 8) { + bytes[--i] = (byte) bits; + } + } + } + + /** + * Sets the seed of this generator using one long, running that through LightRNG's algorithm twice to get the state. + * @param seed the number to use as the seed + */ + public void setSeed(final long seed) { + + long state = seed + 0x9E3779B97F4A7C15L, + z = state; + z = (z ^ (z >>> 30)) * 0xBF58476D1CE4E5B9L; + z = (z ^ (z >>> 27)) * 0x94D049BB133111EBL; + state0 = z ^ (z >>> 31); + state += 0x9E3779B97F4A7C15L; + z = state; + z = (z ^ (z >>> 30)) * 0xBF58476D1CE4E5B9L; + z = (z ^ (z >>> 27)) * 0x94D049BB133111EBL; + state1 = z ^ (z >>> 31); + } + + /** + * Sets the seed of this generator using two longs, using them without changes unless both are 0 (then it makes the + * state variable corresponding to stateA 1 instead). + * @param stateA the number to use as the first part of the state; this will be 1 instead if both seeds are 0 + * @param stateB the number to use as the second part of the state + */ + public void setSeed(final long stateA, final long stateB) { + + state0 = stateA; + state1 = stateB; + if((stateA | stateB) == 0L) + state0 = 1L; + } + +} diff --git a/src/net/torvald/tsvm/Videotron2K.kt b/src/net/torvald/tsvm/Videotron2K.kt index 15dfe1e..1a72023 100644 --- a/src/net/torvald/tsvm/Videotron2K.kt +++ b/src/net/torvald/tsvm/Videotron2K.kt @@ -1,6 +1,7 @@ package net.torvald.tsvm import net.torvald.UnsafeHelper +import net.torvald.random.HQRNG import net.torvald.tsvm.peripheral.GraphicsAdapter import java.lang.NumberFormatException @@ -9,49 +10,162 @@ import java.lang.NumberFormatException */ class Videotron2K(val gpu: GraphicsAdapter) { + private val screenfiller = """ + DEFINE RATEF 60 + DEFINE height 448 + DEFINE width 560 + + SCENE fill_line + @ mov 0 px + plot c1 ; will auto-increment px by one + inc c1 + cmp c1 251 r3 + movzr r3 c3 0 ; mov (-zr r3) c3 0 -- first, the comparison is made with r3 then runs 'mov c3 0' if r3 == 0 + cmp px 560 r1 + exitzr r1 + END SCENE + + SCENE loop_frame + @ mov 0 py + perform fill_line + inc py + next + ; there's no EXIT command so this scene will make the program to go loop indefinitely + END SCENE + + perform loop_frame + + """.trimIndent() + private var regs = UnsafeHelper.allocate(16 * 8) private var internalMem = UnsafeHelper.allocate(16384) - private var scenes = HashMap>() - private var varIdTable = HashMap() + private var scenes = HashMap>() + private var varIdTable = HashMap() // String is always uppercase, Long always has VARIABLE_PREFIX added + private var currentScene: Long? = null // if it's named_scene, VARIABLE_PREFIX is added; indexed_scene does not. private val reComment = Regex(""";[^\n]*""") private val reTokenizer = Regex(""" +""") - private val conditional = arrayOf("ZR", "NZ", "GT", "LS", "GE", "LE") + private val debugPrint = true + private val rng = HQRNG() fun eval(command: String) { - var command = command.replace(reComment, "") - } + val rootStatements = ArrayList() + val sceneStatements = ArrayList() - private fun translateLine(lnum: Int, line: String): VT2Statement? { - val tokens = line.split(reTokenizer) - if (tokens.isEmpty()) return null + command.replace(reComment, "").split('\n') + .mapIndexed { index, s -> index to s }.filter { it.second.isNotBlank() } + .forEach { (lnum, stmt) -> + val stmtUpper = stmt.toUpperCase() + val wordsUpper = stmtUpper.split(reTokenizer) - val isInit = tokens[0] == "@" - val cmdstr = tokens[isInit.toInt()].toUpperCase() - val cmdcond = (conditional.linearSearch { it == cmdstr.substring(cmdstr.length - 2, cmdstr.length) } ?: -1) + 1 - val realcmd = if (cmdcond > 0) cmdstr.substring(0, cmdstr.length - 2) else cmdstr + if (stmtUpper.startsWith("SCENE_")) { // indexed scene + val scenenumStr = stmt.substring(6) + try { + val scenenum = scenenumStr.toLong() - val cmd: Int = Command.dict[realcmd] ?: throw RuntimeException("Syntax Error at line $lnum") - val args = tokens.subList(1 + isInit.toInt(), tokens.size).map { parseArgString(it) } + currentScene = scenenum + } + catch (e: NumberFormatException) { + throw IllegalArgumentException("Line $lnum: Illegal scene numeral on $scenenumStr") + } + } + else if (stmtUpper.startsWith("SCENE ")) { // named scene + val sceneName = wordsUpper[1] + if (sceneName.isNullOrBlank()) { + throw IllegalArgumentException("Line $lnum: Illegal scene name on $stmt") + } + else if (hasVar(sceneName)) { + throw IllegalArgumentException("Line $lnum: Scene name or variable '$sceneName' already exists") + } - return VT2Statement(if (isInit) StatementPrefix.INIT else StatementPrefix.NONE, cmd or cmdcond, args.toLongArray()) - } + currentScene = registerNewVariable(sceneName) + } + else if (wordsUpper[0] == "END" && wordsUpper[1] == "SCENE") { // END SCENE + if (currentScene == null) { + throw IllegalArgumentException("Line $lnum: END SCENE is called without matching SCENE definition") + } - private fun parseArgString(token: String): Long { - if (token.toIntOrNull() != null) - return token.toLong().and(0xFFFFFFFF) - else if (token.endsWith('h') && token.substring(0, token.lastIndex).toIntOrNull() != null) - return token.substring(0, token.lastIndex).toInt(16).toLong().and(0xFFFFFFFF) - else if (token.startsWith('r') && token.substring(1, token.length).toIntOrNull() != null) - return REGISTER_PREFIX or token.substring(1, token.length).toLong().and(0xFFFFFFFF) - else { - TODO("variable assignation and utilisation") + scenes[currentScene!!] = sceneStatements.toTypedArray() + + sceneStatements.clear() + currentScene = null + } + else { + val cmdBuffer = if (currentScene != null) sceneStatements else rootStatements + + cmdBuffer.add(translateLine(lnum, stmt)) + } + } + + + if (debugPrint) { + scenes.forEach { id, statements -> + println("SCENE #$id") + statements.forEach { println(" $it") } + println("END SCENE\n") + } + + rootStatements.forEach { println(it) } } } - private class VT2Statement(val prefix: Int = StatementPrefix.NONE, val command: Int, val args: LongArray) + private fun translateLine(lnum: Int, line: String): VT2Statement { + val tokens = line.split(reTokenizer) + if (tokens.isEmpty()) throw InternalError("Line $lnum: empty line not filtered!") + + val isInit = tokens[0] == "@" + val cmdstr = tokens[isInit.toInt()].toUpperCase() + + val cmd: Int = Command.dict[cmdstr] ?: throw RuntimeException("Syntax Error at line $lnum") // conditional code is pre-added on dict + val args = tokens.subList(1 + isInit.toInt(), tokens.size).map { parseArgString(it) } + + return VT2Statement(if (isInit) StatementPrefix.INIT else StatementPrefix.NONE, cmd, args.toLongArray()) + } + + private fun parseArgString(token: String): Long { + if (token.toIntOrNull() != null) // number literal + return token.toLong().and(0xFFFFFFFF) + else if (token.endsWith('h') && token.substring(0, token.lastIndex).toIntOrNull() != null) // hex literal + return token.substring(0, token.lastIndex).toInt(16).toLong().and(0xFFFFFFFF) + else if (token.startsWith('r') && token.substring(1, token.length).toIntOrNull() != null) // r-registers + return REGISTER_PREFIX or token.substring(1, token.length).toLong().minus(1).and(0xFFFFFFFF) + else if (token.startsWith('c') && token.substring(1, token.length).toIntOrNull() != null) // c-registers + return REGISTER_PREFIX or token.substring(1, token.length).toLong().plus(5).and(0xFFFFFFFF) + else { + val varId = varIdTable[token.toUpperCase()] ?: throw IllegalArgumentException("Undefined variable: $token") + + return varId + } + } + + private fun registerNewVariable(varName: String): Long { + var id: Long + do { + id = VARIABLE_PREFIX or rng.nextLong().and(0xFFFFFFFFL) + } while (varIdTable.containsValue(id)) + + varIdTable[varName.toUpperCase()] = id + return id + } + + private fun hasVar(name: String) = (varIdTable.containsKey(name.toUpperCase())) + + + private class VT2Statement(val prefix: Int = StatementPrefix.NONE, val command: Int, val args: LongArray) { + override fun toString(): String { + return StatementPrefix.toString(prefix) + " " + Command.reverseDict[command] + " " + (args.map { argsToString(it) + " " }) + } + + private fun argsToString(i: Long): String { + if (i and REGISTER_PREFIX != 0L) { + val regnum = i and 0xFFFFFFFFL + return if (regnum < 6) "r${regnum + 1}" else "c${regnum - 5}" + } + else return i.toInt().toString() + } + } fun dispose() { regs.destroy() @@ -70,15 +184,54 @@ class Videotron2K(val gpu: GraphicsAdapter) { companion object { private const val REGISTER_PREFIX = 0x7FFFFFFF_00000000L private const val VARIABLE_PREFIX = 0x3FFFFFFF_00000000L + + private const val REG_PX = REGISTER_PREFIX or 12 + private const val REG_PY = REGISTER_PREFIX or 13 + private const val REG_FRM = REGISTER_PREFIX or 14 + private const val REG_TMR = REGISTER_PREFIX or 15 + + private const val REG_R1 = REGISTER_PREFIX + private const val REG_C1 = REGISTER_PREFIX + 6 + + /* + Registers internal variable ID: + + r1 = REGISTER_PREFIX + 0 + r2 = REGISTER_PREFIX + 1 + r3 = REGISTER_PREFIX + 2 + r4 = REGISTER_PREFIX + 3 + r5 = REGISTER_PREFIX + 4 + r6 = REGISTER_PREFIX + 5 + + c1 = REGISTER_PREFIX + 6 + c2 = REGISTER_PREFIX + 7 + c3 = REGISTER_PREFIX + 8 + c4 = REGISTER_PREFIX + 9 + c5 = REGISTER_PREFIX + 10 + c6 = REGISTER_PREFIX + 11 + + px = REGISTER_PREFIX + 12 + py = REGISTER_PREFIX + 13 + + frm = REGISTER_PREFIX + 14 + tmr = REGISTER_PREFIX + 15 + */ } } object StatementPrefix { const val NONE = 0 const val INIT = 1 + + fun toString(key: Int) = when(key) { + INIT -> "@" + else -> " " + } } object Command { + val conditional = arrayOf("ZR", "NZ", "GT", "LS", "GE", "LE") + const val NOP = 0 const val ADD = 0x8 const val SUB = 0x10 @@ -154,4 +307,15 @@ object Command { "DEFINE" to DEFINE ) + + // fill in conditionals to dict + init { + dict.entries.forEach { (command, opcode) -> + conditional.forEachIndexed { i, cond -> + dict[command + cond] = opcode + i + 1 + } + } + } + + val reverseDict = HashMap(dict.entries.associate { (k,v)-> v to k }) } \ No newline at end of file