"use strict"; let systemName = "bedrock-js"; let systemVersion = "1.0.0"; let systemAuthors = "Ben Bridle"; let initialScreenScale = 2; // Upgrade every pre.bedrock element to a full assembler, and every // bedrock to a full emulator. window.addEventListener('DOMContentLoaded', function() { injectStyles(); for (let element of document.querySelectorAll('pre.bedrock')) { // Click on the element to upgrade to an assembler. element.style.cursor = 'pointer'; element.addEventListener('click', function(e) { e.preventDefault(); upgradeToAssembler(element); }) // Prevent text selection on the element. element.addEventListener('selectstart', function(e) { e.preventDefault(); }) } for (let element of document.querySelectorAll('bedrock')) { upgradeToEmulator(element); } }) // Insert the styles for the assembler and emulator into the page. function injectStyles() { let css = document.createElement('style'); css.textContent = bedrockStyles; document.head.appendChild(css); } // Upgrade a text element to a full assembler. function upgradeToAssembler(element) { let assembler = new AssemblerElement(element); // HACK 2: Track the top of the original element relative to the viewport. let topStart = element.getBoundingClientRect().top; let scrollStart = window.scrollY; // HACK 1: Insert a fake spacer element following the original element to // prevent the page from jumping when the original element is replaced. // Spacer ensures that there is at least element height below the // viewport, filling the gap created while element is being replaced. let windowHeight = document.documentElement.clientHeight; let documentHeight = document.documentElement.scrollHeight; let belowViewport = documentHeight - (window.scrollY + windowHeight); let elementHeight = element.scrollHeight; let spacerHeight = Math.max(0, elementHeight - belowViewport); let spacer = document.createElement('div'); spacer.style.height = spacerHeight + 'px'; element.parentElement.insertBefore(spacer, element.nextSibling); element.replaceWith(assembler); // HACK 1: Remove the spacer after a short delay. setTimeout(()=>spacer.remove()); // HACK 2: Check if the page jumped as the assembler was inserted. let topEnd = assembler.getBoundingClientRect().top; if (topStart != topEnd) { window.scrollTo({top: scrollStart, behavior: 'instant'}); // Wait for the document layout to settle. setTimeout(() => { topEnd = assembler.getBoundingClientRect().top; let topTarget = (topEnd - topStart) + window.scrollY; window.scrollTo({top: topTarget, behavior: 'instant'}); }); } return assembler; } // Upgrade a bedrock element with attributes to a full emulator. function upgradeToEmulator(element) { let attr = element.attributes; if (!attr.src || !attr.src.value) { console.error("No valid src attribute provided for element"); return; } let scale = 1; try { scale = clamp(parseInt(attr.scale.value), 1, 10) } catch {} let options = { scale, controls: 'controls' in attr, autoplay: true, }; let url = URL.parse(attr.src.value, window.location.href); if (!url) { console.error(`Invalid src attribute ${attr.src.value} provided for element`); return; } let request = new XMLHttpRequest(); request.open("GET", url, true); request.responseType = "arraybuffer"; request.onreadystatechange = function() { if (request.readyState == 4) { if (request.status == 200) { options.bytecode = new Uint8Array(request.response); let emulator = new EmulatorElement(options); element.replaceWith(emulator); } else { console.error(`Could not load Bedrock program from ${url.href}`); } } } request.send(); } // ----------------------------------------------------------------------------------------------- + // :::::: ELEMENTS :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + // ----------------------------------------------------------------------------------------------- + // Constructor for an interactive assembler element. function AssemblerElement(element) { let assembler = elementFromHTML(assemblerTemplate); let textarea = assembler.querySelector('textarea'); let viewer = assembler.querySelector('.viewer'); let editor = assembler.querySelector('.editor'); let errorPanel = assembler.querySelector('.panel.errors'); let bytecodePanel = assembler.querySelector('.panel.bytecode'); let status = assembler.querySelector('.status'); let checkButton = assembler.querySelector('button[name="check"]'); let runButton = assembler.querySelector('button[name="run"]'); let fullscreenButton = assembler.querySelector('button[name="fullscreen"]'); assembler.autoGrow = true; assembler.emulator = null; // Show and hide the error and bytecode panels. assembler.showErrorPanel = function() { errorPanel.classList.remove('hidden'); } assembler.hideErrorPanel = function() { errorPanel.classList.add('hidden'); } assembler.showBytecodePanel = function() { bytecodePanel.classList.remove('hidden'); } assembler.hideBytecodePanel = function() { bytecodePanel.classList.add('hidden'); } // Place focus on the editor. assembler.focus = function() { textarea.focus(); } // Lock the viewer to the textarea by syncing their scroll positions. assembler.syncScroll = function() { viewer.scrollTop = textarea.scrollTop; viewer.scrollLeft = textarea.scrollLeft; } // Lock the viewer to the textarea by syncing their sizes. assembler.syncSize = function() { // Disable auto-grow if editor was made smaller by the user. if (textarea.clientHeight < viewer.clientHeight) { assembler.autoGrow = false; } viewer.style.height = textarea.clientHeight + 'px'; editor.style.height = textarea.clientHeight + 'px'; } // Copy text from textarea to viewer with syntax highlighting. assembler.renderText = function() { // Force viewer to show trailing newlines. let text = textarea.value; if (text.slice(-1) == "\n") text += ' '; // Replace contents of viewer. viewer.innerHTML = ''; for (let child of highlightSource(text)) { viewer.appendChild(child); } // Auto-grow the text area as text is entered. if (assembler.autoGrow && textarea.scrollHeight > textarea.clientHeight) { textarea.style.height = textarea.scrollHeight + 'px'; } // Lock viewer to textarea. assembler.syncScroll(); } // Assemble the program and show bytecode or errors. assembler.checkProgram = function() { let { bytecode, symbols, errors } = assembleProgram(textarea.value); errorPanel.innerHTML = ''; bytecodePanel.innerHTML = ''; if (errors.length) { let list = document.createElement('ul'); for (let error of errors) { let item = document.createElement('li'); let line = error.start.line + 1; let column = error.start.column + 1; item.textContent = `[${line}:${column}] ${error.text}`; list.appendChild(item); } errorPanel.appendChild(list); let unit = errors.length == 1 ? 'error' : 'errors'; status.textContent = `${errors.length} ${unit}`; assembler.hideBytecodePanel(); assembler.showErrorPanel(); } else { let unit = bytecode.length == 1 ? 'byte' : 'bytes'; status.textContent = `no errors (${bytecode.length} ${unit})`; let hexString = ''; for (let byte of bytecode) hexString += hex(byte, 2) + ' '; let programListing = document.createElement('div'); programListing.textContent = hexString; bytecodePanel.appendChild(programListing); assembler.hideErrorPanel(); assembler.hideBytecodePanel(); // if (bytecode.length) assembler.showBytecodePanel(); return { bytecode, symbols }; } } assembler.runProgram = function() { // Create an emulator if none exists. if (!assembler.emulator) { assembler.emulator = new EmulatorElement(); assembler.emulator.assembler = assembler; assembler.parentElement.insertBefore(assembler.emulator, assembler.nextSibling); } let program = assembler.checkProgram(); if (program) { let { bytecode, symbols } = program; assembler.emulator.showStatePanel(); assembler.emulator.startProgram(bytecode, symbols); } } // Change the fullscreen state of the whole assembler. assembler.toggleFullscreen = function() { if (document.fullscreenElement != assembler) { assembler.requestFullscreen(); } else { document.exitFullscreen(); } } // Handle keypresses on the whole assembler. assembler.onKeyPress = function(event) { if (event.key == "Escape") { event.preventDefault(); assembler.hideErrorPanel(); assembler.hideBytecodePanel(); } else if (event.key == "s" && event.ctrlKey) { event.preventDefault(); assembler.checkProgram(); } } // Handle keypresses on just the editor. assembler.onEditorKeyPress = function(event) { let size = 2; let indent = ' '; let text = textarea.value; let selectionStart = textarea.selectionStart; let selectionEnd = textarea.selectionEnd; // Find the character index following the previous newline character. function findLineStart(i) { while (i>0 && text[i-1] != '\n') i -= 1; return i; } if (event.key == "Enter" && event.ctrlKey) { event.preventDefault(); assembler.runProgram(); } else if (event.key == "Tab") { event.preventDefault(); // Find the start index of every line in the selection. let i = findLineStart(selectionStart); let lineStarts = [i]; for (; i=0; i--) { let start = lineStarts[i]; let trim = Math.min(size, whitespaceLengths[i]); text = text.slice(0, start) + text.slice(start+trim); selectionEnd -= trim; } } else { // Indent each line in reverse order. selectionStart += size; for (let i=lineStarts.length-1; i>=0; i--) { let start = lineStarts[i]; text = text.slice(0, start) + indent + text.slice(start); selectionEnd += size; } } // Update textarea content and viewer. textarea.value = text; textarea.selectionStart = selectionStart; textarea.selectionEnd = selectionEnd; assembler.renderText(); } } textarea.addEventListener('input', assembler.renderText); textarea.addEventListener('scroll', assembler.syncScroll); textarea.addEventListener('keydown', assembler.onEditorKeyPress); assembler.addEventListener('keydown', assembler.onKeyPress); checkButton.addEventListener('click', assembler.checkProgram); runButton.addEventListener('click', assembler.runProgram); fullscreenButton.addEventListener('click', assembler.toggleFullscreen); assembler.resizeObserver = new ResizeObserver(assembler.syncSize).observe(textarea); textarea.value = element.textContent.trim(); textarea.selectionStart = 0; textarea.selectionEnd = 0; textarea.scrollLeft = 0; textarea.style.height = element.clientHeight + 'px'; assembler.renderText(); return assembler; } /// Constructor for a interactive emulator element. function EmulatorElement(options) { let emulator = elementFromHTML(emulatorTemplate); // Menu bar let menuBar = emulator.querySelector('.menubar'); let fullscreenButton = menuBar.querySelector('button[name="fullscreen"]'); let stateButton = menuBar.querySelector('button[name="state"]'); let runButton = menuBar.querySelector('button[name="run"]'); let pauseButton = menuBar.querySelector('button[name="pause"]'); let stopButton = menuBar.querySelector('button[name="stop"]'); let stepButton = menuBar.querySelector('button[name="step"]'); let status = menuBar.querySelector('.status'); // Screen panel let screenPanel = emulator.querySelector('.screen'); let canvas = screenPanel.querySelector('canvas'); // Stream panel let streamPanel = emulator.querySelector('.stream'); let transmissions = emulator.querySelector('.transmissions'); // State panel let statePanel = emulator.querySelector('.panel.state'); let pcState = statePanel.querySelector('.pc'); let labelState = statePanel.querySelector('.label'); let wstState = statePanel.querySelector('.wst'); let rstState = statePanel.querySelector('.rst'); let loadedBytecode = options ? options.bytecode : new Uint8Array(0); let loadedSymbols = new SymbolTable(); let loadedMetadata = parseMetadata(loadedBytecode); let currentTransmission = null; let transmissionParser = new Utf8StreamParser(); emulator.screen = screenPanel; emulator.canvas = canvas; let br = new Bedrock(emulator); br.reset(); br.onUpdate = () => emulator.updateStatePanel(); emulator.showStatePanel = function() { statePanel.classList.remove('hidden'); } emulator.toggleStatePanel = function() { statePanel.classList.toggle('hidden'); } emulator.showStreamPanel = function() { streamPanel.classList.remove('hidden'); } emulator.hideStreamPanel = function() { streamPanel.classList.add('hidden'); } emulator.showScreenPanel = function() { screenPanel.classList.remove('hidden'); } emulator.hideScreenPanel = function() { screenPanel.classList.add('hidden'); } emulator.hideMenuBar = function() { menuBar.classList.add('hidden'); } // Reset the emulator and load a new program. emulator.startProgram = function(bytecode, symbols) { emulator.stopProgram(); loadedBytecode = bytecode; loadedSymbols = symbols; loadedMetadata = parseMetadata(bytecode); br.loadProgram(bytecode); br.run(); } // Fires when the play button is pressed. emulator.runProgram = function() { br.blank ? emulator.startProgram(loadedBytecode, loadedSymbols) : br.run(); } // Fires when the pause button is pressed. emulator.pauseProgram = function() { br.pause(); } // Fires when the stop button is pressed. emulator.stopProgram = function() { br.stop(); emulator.updateStatePanel(); transmissions.innerHTML = ''; emulator.hideStreamPanel(); emulator.hideScreenPanel(); currentTransmission = null; } emulator.updateStatePanel = function() { function renderStack(stack) { let string = ''; for (let i=0; i{e.preventDefault()}); canvas.addEventListener('keydown', (e)=>emulator.keyInput(e, true)); canvas.addEventListener('keyup', (e)=>emulator.keyInput(e, false)); // Make canvas focusable to be able to receive keydown and keyup events. canvas.tabIndex = 0; emulator.resizeObserver = new ResizeObserver(emulator.updateScreenSize).observe(screenPanel); if (options && !options.controls) emulator.hideMenuBar(); if (options && options.autoplay) emulator.runProgram(); return emulator; } // Create an element from an HTML string. function elementFromHTML(html) { let template = document.createElement('template'); template.innerHTML = html.trim(); return template.content.firstChild; } // Convert an integer to a hexadecimal string. function hex(value, pad) { return value.toString(16).toUpperCase().padStart(pad, '0'); } function parseMetadata(bytecode) { // Test identifier to see if program has metadata. let id = [0xE8,0x00,0x18,0x42,0x45,0x44,0x52,0x4F,0x43,0x4B]; for (let i=0; i<10; i++) if (bytecode[i] != id[i]) return; // Parse each metadata item. let name = getString(0x000A); let authors = getString(0x000C); let description = getString(0x000E); let bgColour = getColour(0x0010); let fgColour = getColour(0x0012); let smallIcon = getIcon(0x0014, 24); let largeIcon = getIcon(0x0016, 64); return { name, authors, description, bgColour, fgColour, smallIcon, largeIcon }; function getByte(addr) { return bytecode[addr] || 0; } function getDouble(addr) { return getByte(addr) << 8 | getByte(addr+1); } function getString(addr) { addr = getDouble(addr); if (addr) { let parser = new Utf8StreamParser(); while (true) { let byte = bytecode[addr++]; if (!byte) break; parser.push(byte); } return parser.read(); } } function getColour(addr) { addr = getDouble(addr); if (addr) { let colour = getDouble(addr); let r = (colour >> 8 & 0xF) * 17; let g = (colour >> 8 & 0xF) * 17; let b = (colour >> 8 & 0xF) * 17; return [r, g, b]; } } function getIcon(addr, size) { // TODO: Return a custom type containing the dimensions and raw // pixel data, and a colourise method to generate a coloured ImageData. } } // Copy a string to the clipboard. function copyText(text) { navigator.clipboard.writeText(text); } let encoder = new TextEncoder(); function encodeUtf8(text) { return encoder.encode(text); } // Incrementally parse a UTF-8 string. function Utf8StreamParser() { // Full characters parsed so far. this.string = ''; // Current partial code point. this.codePoint; // True if the current code point is being dropped. this.dropBytes = false; // Number of bytes received of the current code point. this.i = 0; // Expected byte length of the current code point. this.len = 0; // Push a code point to the string. let pushCodePoint = (codePoint) => { this.string += String.fromCodePoint(codePoint); } // Drop the current partial code point, if any. let dropCodePoint = () => { // Push a 'replacement character' if there is a partial code point // that hasn't already been dropped. if (this.i && !this.dropBytes) pushCodePoint(0xFFFD); this.codePoint = 0; this.dropBytes = false; this.i = 0; this.len = 0; } // Parse the start of an encoded code point. let parseStartByte = (byte, length) => { dropCodePoint(); this.len = length; parseContinuationByte(byte); } // Parse the remaining bytes of an encoded code point. let parseContinuationByte = (byte) => { if (!this.dropBytes) { if (this.len) { this.codePoint = this.codePoint << 6 | (byte & 0x3F); this.i += 1; if (this.i == this.len) { pushCodePoint(this.codePoint); this.len = 0; } } else { this.dropBytes = true; pushCodePoint(0xFFFD); } } } // Parse a UTF-8 encoded byte. this.push = (byte) => { if (byte < 0x80) { // start of a 1-byte code point dropCodePoint(); pushCodePoint(byte); } else if (byte < 0xC0) { // not a start byte parseContinuationByte(byte); } else if (byte < 0xE0) { // start of a 2-byte code point parseStartByte(byte, 2); } else if (byte < 0xF0) { // start of a 3-byte code point parseStartByte(byte, 3); } else if (byte < 0xF8) { // start of a 4-byte code point parseStartByte(byte, 4); } } // Parse an array of bytes. this.pushList = (list) => { for (let byte of list) this.push(byte); } // Take the characters that have been parsed so far. this.read = () => { let output = this.string; this.string = ''; return output; } } // ----------------------------------------------------------------------------------------------- + // :::::: ASSEMBLER:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + // ----------------------------------------------------------------------------------------------- + // Convert a string of Bedrock source code into syntax-highlighted nodes. function highlightSource(source) { let currentText = '', currentClass = ''; let items = []; let typeLookup = { 'comment': 'comment', 'markOpen': 'comment', 'markClose': 'comment', 'blockOpen': '', 'blockClose': '', 'rawString': 'value', 'terminatedString': 'value', 'macroDefinition': 'definition', 'globalLabel': 'definition', 'localLabel': 'definition', 'macroTerminator': 'definition', 'spacer': '', 'symbol': 'reference', 'byteLiteral': 'value', 'doubleLiteral': 'value', 'instruction': '', 'whitespace': '', }; function pushItem(text, className) { if (currentClass != className) { if (currentClass) { let span = document.createElement('span'); span.textContent = currentText; span.className = currentClass; items.push(span); } else { items.push(document.createTextNode(currentText)); } currentClass = className; currentText = text; } else { currentText += text; } } for (let token of tokeniseProgram(source).tokens) { pushItem(token.text, typeLookup[token.type]); } pushItem(' '); return items; } // Tokenise a string of Bedrock source code. function tokeniseProgram(source) { let tokens = [], errors = []; let currentText = '', endChar = '', label = ''; let currentLine = 0, currentColumn = 0; // position of next character let prevLine = 0, prevColumn = 0; // position before current let rewindLine = 0, rewindColumn = 0; // position before prev let startLine = 0, startColumn = 0; // position of token start // Rewind the prev position by one character. function rewind() { prevLine = rewindLine; prevColumn = rewindColumn; } // Attach a start and end location to a string. function trackText(text) { let start = { line: startLine, column: startColumn }; let end = { line: prevLine, column: prevColumn }; return { text, start, end }; } // Report an error. function error(text) { errors.push(trackText(text)); } // Parse a literal string. function parseLiteral(string) { if (string.length == 2 || string.length == 4) { const hexDigits = "0123456789ABCDEFabcdef"; for (let c of string) if (!hexDigits.includes(c)) return; let value = parseInt(string, 16); if (!isNaN(value)) return value; } }; // Commit the current token. function commit() { if (currentText) { let firstChar = currentText[0]; let token = trackText(currentText); if ('()'.includes(firstChar)) { token.type = 'comment'; } else if (firstChar == '[') { token.type = 'markOpen'; } else if (firstChar == ']') { token.type = 'markClose'; } else if (firstChar == '{') { token.type = 'blockOpen'; } else if (firstChar == '}') { token.type = 'blockClose'; } else if (firstChar == '\'') { token.type = 'rawString'; token.value = currentText.slice(1,-1); } else if (firstChar == '"') { token.type = 'terminatedString'; token.value = currentText.slice(1,-1); } else if (firstChar == '%') { token.type = 'macroDefinition'; token.name = currentText.slice(1); } else if (firstChar == '@') { token.type = 'globalLabel'; label = currentText.slice(1); token.name = label; } else if (firstChar == '&') { token.type = 'localLabel'; token.name = label + '/' + currentText.slice(1); } else if (firstChar == ';') { token.type = 'macroTerminator'; } else if (firstChar == '#') { token.type = 'spacer'; token.value = parseLiteral(currentText.slice(1)); if (token.value == undefined) { error(`Invalid value for spacer: ${currentText.slice(1)}`) } } else if (firstChar == '~') { token.type = 'symbol'; token.name = label + '/' + currentText.slice(1); } else { let value = parseLiteral(currentText); if (value != undefined) { token.type = currentText.length == 2 ? 'byteLiteral' : 'doubleLiteral'; token.value = value; } else { let value = instructionTable[currentText]; if (value != undefined) { token.type = 'instruction'; token.value = value; } else { token.type = 'symbol'; token.name = currentText; } } } tokens.push(token); currentText = ''; startLine = currentLine; startColumn = currentColumn; } } for (let c of source) { // Advance position. rewindLine = prevLine; rewindColumn = prevColumn; prevLine = currentLine; prevColumn = currentColumn; currentColumn += 1; if (c == '\n') { currentLine += 1; currentColumn = 0; } if (endChar) { currentText += c; if (c == endChar) { commit(); endChar = ''; } } else if ('()'.includes(c)) { commit(); currentText += c; if (c == '(') { endChar = ')'; } else { error("Unmatched ')', no comment was in progress"); commit(); } } else if (!currentText && '"\''.includes(c)) { currentText += c; endChar = c; } else if ('\r\n\t '.includes(c)) { let token = { text: c, type: 'whitespace', start: { line: prevLine, column: prevColumn }, end: { line: prevLine, column: prevColumn }, }; rewind(); commit(); tokens.push(token); } else if ('[]{};'.includes(c)) { rewind(); commit(); currentText += c; commit(); } else if (c == ':') { currentText += c; commit(); } else { currentText += c; } } // Report unclosed token. if (endChar == ')') { error("Unclosed comment"); } else if (endChar) { error("Unclosed string"); } // Commit final token. commit(); return { tokens, errors }; } // Assemble a string of Bedrock source code. function assembleProgram(source) { let { tokens, errors } = tokeniseProgram(source); let program = new Uint8Array(65536); // Populated up front with all names. let macros = {}, labels = {}, address = 0; for (let token of tokens) { if (['globalLabel', 'localLabel'].includes(token.type)) { labels[token.name] = { refs:[], address:0 }; } else if ('macroDefinition' == token.type) { macros[token.name] = []; } } // Populated gradually as each definition is reached. let macroNames = {}, labelNames = {}; // Assemble a byte value. function pushByte(byte) { program[address++] = byte; } // Assemble a double value. function pushDouble(double) { program[address++] = double >> 8; program[address++] = double & 0xFF; } // Report an error message. function error(text, token) { let start = { line: token.start.line, column: token.start.column }; let end = { line: token.end.line, column: token.end.column }; errors.push({ text, start, end }); } // Returns true if the name has been defined at this point in the program. function nameTaken(name) { return name in macroNames || name in labelNames || name in instructionTable; } // Recursively assemble an array of tokens. function assembleTokens(tokens) { let stack = []; for (let i=0; i 0xFFFF) { error(`Block address too high`, token); } } else { error("Unmatched '}', no block was in progress", token); } address = stash; } else if (['globalLabel', 'localLabel'].includes(token.type)) { if (nameTaken(token.name)) { error(`Label name already taken: ${token.name}`, token); } else { labelNames[token.name] = true; labels[token.name].address = address; if (address > 0xFFFF) { error(`Label address too high: ${token.name}`, token); } } } else if (token.type == 'macroDefinition') { if (nameTaken(token.name)) { error(`Macro name already taken: ${token.name}`, token); } else { let body = [], terminated = false; i += 1; while (i 1) { let half = size >> 1; let mid = base + half; base = this.addresses[mid] > address ? base : mid; size -= half; } if (this.addresses[base] == address) { return '@' + this.names[base]; } else { if (address >= this.addresses[0]) { let i = base + (this.addresses[base] > address); return '@' + this.names[i]; } else { return ''; } } } } // ----------------------------------------------------------------------------------------------- + // :::::: EMULATOR::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + // ----------------------------------------------------------------------------------------------- + // The core Bedrock emulator. Receives an EmulatorElement. function Bedrock(e) { this.e = e; this.mem = new Uint8Array(65536); this.wst = new Stack(); this.rst = new Stack(); this.dev = new DeviceBus(this); // Callback functions. this.onUpdate; // Reset the emulator, preserving the contents of program memory. this.reset = () => { this.p = 0; this.wst.reset(); this.rst.reset(); this.dev.reset(); this.cycle = 0; this.blank = true; // true when emulator has been reset. this.paused = true; // true when program is paused for the user. this.asleep = false; // true when program is waiting for input. this.halted = false; // true when program has halted. } // Reset the emulator and load in a new program. this.loadProgram = (bytecode) => { this.reset(); this.mem.fill(0); this.mem.set(bytecode.slice(0, 65536)); this.blank = false; } this.run = () => { this.paused = false; this.update(); this.runLoop(); } this.step = () => { if (this.paused && !this.halted) { if (this.asleep) { this.sleepLoop() } else { this.blank = false; switch (this.evaluate(1)) { case 5: this.halt(); break; // HALT case 4: this.reset(); break; // RESET case 3: this.reset(); break; // FORK case 2: this.sleep(); break; // SLEEP case 1: this.pause(); break; // DEBUG default: break; // NORMAL } this.update(); this.render(); } } } this.pause = () => { this.paused = true; this.update(); this.render(); } this.stop = () => { this.reset(); this.update(); this.render(); } this.halt = () => { this.halted = true; this.update(); this.render(); } this.sleep = () => { this.asleep = true; this.update(); this.render(); this.sleepLoop(); } this.update = () => { if (this.onUpdate) this.onUpdate(this); } // Render the screen contents to the canvas. this.render = () => { if (this.dev.input.accessed || this.dev.screen.accessed) { e.showScreenPanel(); } let scr = this.dev.screen; if (scr.dirty()) { scr.render(); let image = new ImageData(scr.pixels, scr.width, scr.height); scr.ctx.putImageData(image, 0, 0, scr.dx0, scr.dy0, scr.dx1, scr.dy1); scr.unmark(); } } this.runLoop = () => { if (!this.halted && !this.paused) { if (this.asleep) { setTimeout(this.sleepLoop.bind(this)); } else { // Tail-recursive with timeout to allow other code to run. switch (this.evaluate(100000)) { case 5: this.halt(); return; // HALT case 4: this.reset(); break; // RESET case 3: this.reset(); break; // FORK case 2: this.sleep(); return; // SLEEP case 1: this.pause(); break; // DEBUG default: // NORMAL } this.update(); setTimeout(this.runLoop.bind(this), 0); } } } this.sleepLoop = () => { if (!this.halted) { if (!this.asleep) { setTimeout(this.runLoop.bind(this), 0); } else { let queue = this.dev.queue.get(this.dev.system.wakeMask); for (let slot of queue) { if (this.dev.devices[slot].wake()) { this.asleep = false; this.dev.system.wakeSlot = slot; setTimeout(this.runLoop.bind(this)); return; } } setTimeout(this.sleepLoop.bind(this), 0); } } } // Evaluate at most n processor cycles, returning a signal value. this.evaluate = (n) => { for (let i=0; i { return this.mem[this.p++]; } let mpop2 = ( ) => { return mpop1() << 8 | mpop1(); } let mpopx = ( ) => { return wMode ? mpop2() : mpop1(); } let mpsh1 = (v) => { this.mem[this.p++] = v; } let mpsh2 = (v) => { this.mpsh1(v >> 8); this.mpsh1(v); } let mpshx = (v) => { wMode ? mpsh2(v) : mpsh1(v); } let mget1 = (a) => { return this.mem[a]; } let mget2 = (a) => { return mget1(a) << 8 | mget1(a+1); } let mgetx = (a) => { return wMode ? mget2(a) : mget1(a); } let mset1 = (a,v) => { this.mem[a] = v; } let mset2 = (a,v) => { mset1(a, v >> 8); mset1(a+1, v); } let msetx = (a,v) => { wMode ? mset2(a,v) : mset1(a,v); } // Device access functions. let dget1 = (p) => { return this.dev.read(p); } let dget2 = (p) => { return dget1(p) << 8 | dget1(p+1); } let dgetx = (p) => { return wMode ? dget2(p) : dget1(p); } let dset1 = (p,v) => { return this.dev.write(p,v); } let dset2 = (p,v) => { let s1 = dset1(p, v >> 8); let s2 = dset1(p+1, v & 0xFF); return s1 || s2; } let dsetx = (p,v) => { if (wMode) { return dset2(p,v) } else { return dset1(p,v) } } // Stack access functions. let wpop1 = ( ) => { return iMode ? (iMode=0, mpop1()) : w.pop1(); } let wpop2 = ( ) => { return iMode ? (iMode=0, mpop2()) : w.pop2(); } let wpopx = ( ) => { return wMode ? wpop2() : wpop1(); } let rpop1 = ( ) => { return iMode ? (iMode=0, mpop1()) : r.pop1(); } let rpop2 = ( ) => { return iMode ? (iMode=0, mpop2()) : r.pop2(); } let rpopx = ( ) => { return wMode ? rpop2() : rpop1(); } let wpsh1 = (v) => { w.psh1(v); } let wpsh2 = (v) => { w.psh2(v); } let wpshx = (v) => { wMode ? w.psh2(v) : w.psh1(v); } let rpsh1 = (v) => { r.psh1(v); } let rpsh2 = (v) => { r.psh2(v); } let rpshx = (v) => { wMode ? r.psh2(v) : r.psh1(v); } // Shifting operations. let shl1 = (v,d) => { return d < 8 ? v << d & 0xFF : 0; } let shl2 = (v,d) => { return d < 16 ? v << d & 0xFFFF : 0; } let shlx = (v,d) => { return wMode ? shl2(v,d) : shl1(v,d); } let shr1 = (v,d) => { return d < 8 ? v >> d & 0xFF : 0; } let shr2 = (v,d) => { return d < 16 ? v >> d & 0xFFFF : 0; } let shrx = (v,d) => { return wMode ? shr2(v,d) : shr1(v,d); } let rol1 = (v,d) => { d %= 8; return shl1(v, d) | shr1(v, 8-d); } let rol2 = (v,d) => { d %= 16; return shl2(v, d) | shr2(v, 16-d); } let rolx = (v,d) => { return wMode ? rol2(v,d) : rol1(v,d); } let ror1 = (v,d) => { d %= 8; return shr1(v, d) | shl1(v, 8-d); } let ror2 = (v,d) => { d %= 16; return shr2(v, d) | shl2(v, 16-d); } let rorx = (v,d) => { return wMode ? ror2(v,d) : ror1(v,d); } let a, p, s, t, v, x, y, z; switch (instr & 0x1F) { /* HLT */ case 0x00: switch(instr) { case 0x00: return 5; case 0x40: return 1; default: } break; /* PSH */ case 0x01: x=rpopx(); wpshx(x); break; /* POP */ case 0x02: wpopx(); break; /* CPY */ case 0x03: x=rpopx(); rpshx(x); wpshx(x); break; /* DUP */ case 0x04: x=wpopx(); wpshx(x); wpshx(x); break; /* OVR */ case 0x05: y=wpopx(); x=wpopx(); wpshx(x); wpshx(y); wpshx(x); break; /* SWP */ case 0x06: y=wpopx(); x=wpopx(); wpshx(y); wpshx(x); break; /* ROT */ case 0x07: z=wpopx(); y=wpopx(); x=wpopx(); wpshx(y); wpshx(z); wpshx(x); break; /* JMP */ case 0x08: a=wpop2(); this.p = a; break; /* JMS */ case 0x09: a=wpop2(); rpsh2(this.p); this.p = a; break; /* JCN */ case 0x0A: a=wpop2(); t=wpop1(); if (t) { this.p = a; } break; /* JCS */ case 0x0B: a=wpop2(); t=wpop1(); if (t) { rpsh2(this.p); this.p = a; } break; /* LDA */ case 0x0C: a=wpop2(); v=mgetx(a); wpshx(v); break; /* STA */ case 0x0D: a=wpop2(); v=wpopx(); msetx(a,v); break; /* LDD */ case 0x0E: p=wpop1(); v=dgetx(p); wpshx(v); break; /* STD */ case 0x0F: p=wpop1(); v=wpopx(); s=dsetx(p,v); if (s) { return s; } break; /* ADD */ case 0x10: y=wpopx(); x=wpopx(); wpshx(x+y); break; /* SUB */ case 0x11: y=wpopx(); x=wpopx(); wpshx(x-y); break; /* INC */ case 0x12: x=wpopx(); wpshx(x+1); break; /* DEC */ case 0x13: x=wpopx(); wpshx(x-1); break; /* LTH */ case 0x14: y=wpopx(); x=wpopx(); wpsh1(0-(x y)); break; /* EQU */ case 0x16: y=wpopx(); x=wpopx(); wpsh1(0-(x==y)); break; /* NQK */ case 0x17: y=wpopx(); x=wpopx(); wpshx(x); wpshx(y); wpsh1(0-(x!=y)); break; /* SHL */ case 0x18: y=wpop1(); x=wpopx(); wpshx(shlx(x,y)); break; /* SHR */ case 0x19: y=wpop1(); x=wpopx(); wpshx(shrx(x,y)); break; /* ROL */ case 0x1A: y=wpop1(); x=wpopx(); wpshx(rolx(x,y)); break; /* ROR */ case 0x1B: y=wpop1(); x=wpopx(); wpshx(rorx(x,y)); break; /* IOR */ case 0x1C: y=wpopx(); x=wpopx(); wpshx(x|y); break; /* XOR */ case 0x1D: y=wpopx(); x=wpopx(); wpshx(x^y); break; /* AND */ case 0x1E: y=wpopx(); x=wpopx(); wpshx(x&y); break; /* NOT */ case 0x1F: x=wpopx(); wpshx(~x ); break; } } }; } // Stack for the core system. function Stack() { this.mem = new Uint8Array(256); this.p = 0; this.reset = ( ) => { this.mem.fill(0); this.p = 0; } this.psh1 = (v) => { this.mem[this.p++] = v; } this.psh2 = (v) => { this.psh1(v >> 8); this.psh1(v); } this.pop1 = ( ) => { return this.mem[--this.p]; } this.pop2 = ( ) => { return this.pop1() | this.pop1() << 8; } } // Device bus for the core system. function DeviceBus(br) { this.system = new SystemDevice(br); this.memory = new MemoryDevice(br); this.math = new MathDevice(br); this.clock = new ClockDevice(br); this.input = new InputDevice(br); this.screen = new ScreenDevice(br); this.tone = new NullDevice(); this.sampler = new NullDevice(); this.stream = new StreamDevice(br); this.file = new NullDevice(); this.clipboard = new ClipboardDevice(br); this.registry = new NullDevice(); this.custom1 = new NullDevice(); this.custom2 = new NullDevice(); this.custom3 = new NullDevice(); this.custom4 = new NullDevice(); this.devices = [ this.system, this.memory, this.math, this.clock, this.input, this.screen, this.tone, this.sampler, this.stream, this.file, this.clipboard, this.registry, this.custom1, this.custom2, this.custom3, this.custom4, ]; for (let device of this.devices) { if (device.init) device.init(); } this.queue = new WakeQueue(); this.reset = function() { for (let d of this.devices) d.reset(); } this.read = function(p) { return this.devices[p >> 4].read(p & 0x0F); } this.write = function(p,v) { return this.devices[p >> 4].write(p & 0x0F, v); } } function WakeQueue() { this.queue = [0x1,0x2,0x3,0x4,0x5,0x6,0x7,0x8,0x9,0xA,0xB,0xC,0xD,0xE,0xF]; // Return the queue after applying a wake mask. this.get = function(wakeMask) { let masked = []; for (let slot of this.queue) if (wakeMask & 0x8000 >> slot) masked.push(slot); // Add slot zero last. if (wakeMask & 0x8000) masked.push(0); return masked; } // Move slot to the back of the queue. this.wake = function(slot) { let i = this.queue.indexOf(slot); if (i > -1) this.queue.splice(i, 1); this.queue.push(slot); } } // Disconnected device. function NullDevice() { this.reset = function() { } this.read = function(p) { return 0; } this.write = function(p,v) { return; } this.wake = function() { return false; } } function SystemDevice(br) { this.reset = function() { this.wakeMask = 0; this.wakeSlot = 0; this.nameBuffer = new TextBuffer(`${systemName}/${systemVersion}`); this.authorsBuffer = new TextBuffer(systemAuthors); } this.read = function(p) { switch (p) { case 0x2: return this.wakeSlot; case 0x8: return this.nameBuffer.read(); case 0x9: return this.authorsBuffer.read(); case 0xE: return 0b1111_1100; case 0xF: return 0b1010_0000; default: return 0x00; } } this.write = function(p,v) { switch (p) { case 0x0: this.wakeMask = setH(this.wakeMask, v); break; case 0x1: this.wakeMask = setL(this.wakeMask, v); return 2; case 0x3: let s = v ? 3 : 4; return s; case 0x8: this.nameBuffer.reset(); break; case 0x9: this.authorsBuffer.reset(); break; default: return; } } this.wake = function() { return true; } } function MemoryDevice(br) { this.reset = function() { this.mem = new Uint8Array(); this.count = 0; this.countW = 0; this.copyW = 0; this.page1 = 0; this.addr1 = 0; this.page2 = 0; this.addr2 = 0; } this.read = function(p) { switch (p) { case 0x0: return getH(this.count); case 0x1: return getL(this.count); case 0x2: return getH(this.page1); case 0x3: return getL(this.page1); case 0x4: return getH(this.addr1); case 0x5: return getL(this.addr1); case 0x6: case 0x7: return this.read1(); case 0xA: return getH(this.page2); case 0xB: return getL(this.page2); case 0xC: return getH(this.addr2); case 0xD: return getL(this.addr2); case 0xE: case 0xF: return this.read2(); default: return 0x00; } } this.write = function(p,v) { switch (p) { case 0x0: this.countW = setH(this.countW, v); return; case 0x1: this.countW = setL(this.countW, v); this.allocate(); return; case 0x2: this.page1 = setH(this.page1, v); return; case 0x3: this.page1 = setL(this.page1, v); return; case 0x4: this.addr1 = setH(this.addr1, v); return; case 0x5: this.addr1 = setL(this.addr1, v); return; case 0x6: case 0x7: this.write1(v); return; case 0x8: this.copyW = setH(this.copyW, v); return; case 0x9: this.copyW = setL(this.copyW, v); this.copy(); return; case 0xA: this.page2 = setH(this.page2, v); return; case 0xB: this.page2 = setL(this.page2, v); return; case 0xC: this.addr2 = setH(this.addr2, v); return; case 0xD: this.addr2 = setL(this.addr2, v); return; case 0xE: case 0xF: this.write2(v); return; default: return; } } this.wake = function() { return false; } // Update number of allocated pages to equal count. this.allocate = function() { // Allocate memory. if (this.countW > this.count) { let old = this.mem; this.mem = new Uint8Array(this.countW * 256); this.mem.set(old); // Deallocate memory. } else if (this.countW < this.count) { this.mem = this.mem.slice(0, this.countW); } this.count = this.countW; } // Read a byte from head 1. this.read1 = function() { let i = (this.page1 * 256) + this.addr1; this.addr1 = (this.addr1 + 1) & 0xFFFF; return this.mem.length > i ? this.mem[i] : 0; } // Read a byte from head 2. this.read2 = function() { let i = (this.page2 * 256) + this.addr2; this.addr2 = (this.addr2 + 1) & 0xFFFF; return this.mem.length > i ? this.mem[i] : 0; } // Write a byte to head 1. this.write1 = function(v) { let i = (this.page1 * 256) + this.addr1; this.addr1 = (this.addr1 + 1) & 0xFFFF; if (this.mem.length > i) this.mem[i] = v; } // Write a byte to head 2. this.write2 = function(v) { let i = (this.page2 * 256) + this.addr2; this.addr2 = (this.addr2 + 1) & 0xFFFF; if (this.mem.length > i) this.mem[i] = v; } // Copy pages from head 2 to head 1. this.copy = function() { let src = this.page2; let dst = this.page1; let remaining = this.copyW while (remaining && this.count > src && this.count > dst) { let index = dst++ * 256; let start = src++ * 256; let end = start + 256; this.mem.copyWithin(index, start, end); } } } function MathDevice() { this.reset = function() { this.x = 0; this.y = 0; this.r = 0; this.t = 0; } this.read = function(p) { switch (p) { case 0x0: return getH(this.px()); case 0x1: return getL(this.px()); case 0x2: return getH(this.py()); case 0x3: return getL(this.py()); case 0x4: return getH(this.cr()); case 0x5: return getL(this.cr()); case 0x6: return getH(this.ct()); case 0x7: return getL(this.ct()); case 0x8: return getHH(this.prod()); case 0x9: return getHL(this.prod()); case 0xA: return getH(this.prod()); case 0xB: return getL(this.prod()); case 0xC: return getH(this.quot()); case 0xD: return getL(this.quot()); case 0xE: return getH(this.rem()); case 0xF: return getL(this.rem()); default: return 0x00; } } this.write = function(p,v) { switch (p) { case 0x0: this.x = setH(this.x, v); return; case 0x1: this.x = setL(this.x, v); return; case 0x2: this.y = setH(this.y, v); return; case 0x3: this.y = setL(this.y, v); return; case 0x4: this.r = setH(this.r, v); return; case 0x5: this.r = setL(this.r, v); return; case 0x6: this.t = setH(this.t, v); return; case 0x7: this.t = setL(this.t, v); return; default: return; } } this.wake = function() { return false; } // Return x coordinate of polar point. this.px = function() { let x = Math.trunc(Math.cos((2 * Math.PI * this.t) / 65536) * this.r); return x <= 32767 && x >= -32768 ? x : 0; }; // Return y coordinate of polar point. this.py = function() { let y = Math.trunc(Math.sin((2 * Math.PI * this.t) / 65536) * this.r); return y <= 32767 && y >= -32768 ? y : 0; }; // Return r coordinate of cartesian point. this.cr = function() { return Math.trunc(Math.sqrt(Math.pow(signed(this.x), 2) + Math.pow(signed(this.y), 2))); }; // Return t coordinate of cartesian point. this.ct = function() { return Math.trunc(Math.atan2(signed(this.y), signed(this.x)) * 65536 / (2 * Math.PI)); }; // Return product of x and y operands. this.prod = function() { return this.x * this.y; }; this.quot = function() { return this.y ? this.x / this.y : 0; }; this.rem = function() { return this.y ? this.x % this.y : 0; }; } function ClockDevice(br) { this.reset = function() { this.start = performance.now(); this.uptime = 0; this.t1 = new CountdownTimer(); this.t2 = new CountdownTimer(); this.t3 = new CountdownTimer(); this.t4 = new CountdownTimer(); } this.read = function(p) { switch (p) { case 0x0: return new Date().getFullYear()-2000; case 0x1: return new Date().getMonth(); case 0x2: return new Date().getDate()-1; case 0x3: return new Date().getHours(); case 0x4: return new Date().getMinutes(); case 0x5: return new Date().getSeconds(); case 0x6: this.updateUptime(); return getH(this.uptime); case 0x7: return getL(this.uptime); case 0x8: this.t1.update(); return getH(this.t1.read); case 0x9: return getL(this.t1.read); case 0xA: this.t2.update(); return getH(this.t2.read); case 0xB: return getL(this.t2.read); case 0xC: this.t3.update(); return getH(this.t3.read); case 0xD: return getL(this.t3.read); case 0xE: this.t4.update(); return getH(this.t4.read); case 0xF: return getL(this.t4.read); default: return 0x00; } } this.write = function(p,v) { switch (p) { case 0x8: this.t1.write = setH(this.t1.write, v); return; case 0x9: this.t1.write = setL(this.t1.write, v); this.t1.commit(); return; case 0xA: this.t2.write = setH(this.t2.write, v); return; case 0xB: this.t2.write = setL(this.t2.write, v); this.t2.commit(); return; case 0xC: this.t3.write = setH(this.t3.write, v); return; case 0xD: this.t3.write = setL(this.t3.write, v); this.t3.commit(); return; case 0xE: this.t4.write = setH(this.t4.write, v); return; case 0xF: this.t4.write = setL(this.t4.write, v); this.t4.commit(); return; default: return; } } this.wake = function() { let t1 = this.t1.wake(); let t2 = this.t2.wake(); let t3 = this.t3.wake(); let t4 = this.t4.wake(); return t1 || t2 || t3 || t4; } this.updateUptime = function() { let delta = performance.now() - this.start; this.uptime = delta * 0.256 & 0xFFFF; } } function InputDevice(br) { this.reset = function() { this.x = 0; this.y = 0; this.xR = 0; this.yR = 0; this.hScroll = 0; this.vScroll = 0; this.hScrollR = 0; this.vScrollR = 0; this.pointerButtons = 0; this.pointerActive = false; // Navigation state this.navigation = 0; this.navUp = false; this.navDown = false; this.navLeft = false; this.navRight = false; this.navConfirm = false; this.navCancel = false; this.navTab = false; this.navShift = false; // this.modifiers = 0; this.characterBytes = new ByteQueue(1024); // Flags this.wakeFlag = false; this.accessed = false; } this.read = function(p) { this.accessed = true; switch (p) { case 0x0: this.xR = this.x; return getH(this.xR); case 0x1: return getL(this.xR); case 0x2: this.yR = this.y; return getH(this.yR); case 0x3: return getL(this.yR); case 0x4: this.updateHScroll(); return getH(this.hScrollR); case 0x5: return getL(this.hScrollR); case 0x6: this.updateVScroll(); return getH(this.vScrollR); case 0x7: return getL(this.vScrollR); case 0x8: return bool(this.pointerActive); case 0x9: return this.pointerButtons; case 0xA: return this.characterBytes.pop(); case 0xB: return this.modifiers; case 0xC: return this.navigation; default: return 0x00; } } this.write = function(p,v) { this.accessed = true; switch (p) { case 0xA: this.characterBytes = []; return; default: return; } } this.wake = function() { let wake = this.wakeFlag; this.wakeFlag = false; return wake; } this.updateHScroll = function() { let delta = Math.trunc(this.hScroll); this.hScroll -= delta; this.hScrollR = delta; } this.updateVScroll = function() { let delta = Math.trunc(this.vScroll); this.vScroll -= delta; this.vScrollR = delta; } this.applyPosition = function(x, y) { let wake = x != this.x || y != this.y; this.x = x; this.y = y; if (wake) this.wakeFlag = true; } this.applyButtons = function(jsButtons) { let pointerButtons = 0; pointerButtons |= jsButtons & 0x01 ? 0x80 : 0x00; pointerButtons |= jsButtons & 0x02 ? 0x40 : 0x00; pointerButtons |= jsButtons & 0x04 ? 0x20 : 0x00; let wake = pointerButtons != this.pointerButtons; this.pointerButtons = pointerButtons; if (wake) this.wakeFlag = true; } this.applyActive = function(active) { let wake = active != this.pointerActive; this.pointerActive = active; if (wake) this.wakeFlag = true; } this.applyKey = function(event, pressed) { // Handle navigation keys. switch (event.key) { case "ArrowUp": this.navUp = pressed; break; case "ArrowDown": this.navDown = pressed; break; case "ArrowLeft": this.navLeft = pressed; break; case "ArrowRight": this.navRight = pressed; break; case "Enter": this.navConfirm = pressed; break; case "Escape": this.navCancel = pressed; break; case "Tab": this.navTab = pressed; break; case "Shift": this.navShift = pressed; break; } let navigation = 0; navigation |= this.navUp ? 0x80 : 0x00; navigation |= this.navDown ? 0x40 : 0x00; navigation |= this.navLeft ? 0x20 : 0x00; navigation |= this.navRight ? 0x10 : 0x00; navigation |= this.navConfirm ? 0x08 : 0x00; navigation |= this.navCancel ? 0x04 : 0x00; navigation |= this.navTab && !this.navShift ? 0x02 : 0x00; navigation |= this.navTab && this.navShift ? 0x01 : 0x00; if (navigation != this.navigation) { this.wakeFlag = true; this.navigation = navigation; } // Handle character input. if (pressed) { if (event.key.length == 1) { let byteList = encodeUtf8(event.key); this.characterBytes.pushList(byteList); this.wakeFlag = true; } else { switch (event.key) { case "Backspace": this.characterBytes.push(0x08); break; case "Tab": this.characterBytes.push(0x09); break; case "Enter": this.characterBytes.push(0x0A); break; case "Spacebar": this.characterBytes.push(0x20); break; default: return; } this.wakeFlag = true; } } } this.applyModifiers = function(event) { let modifiers = 0; modifiers |= event.ctrlKey ? 0x80 : 0x00; modifiers |= event.shiftKey ? 0x40 : 0x00; modifiers |= event.altKey ? 0x20 : 0x00; modifiers |= event.metaKey ? 0x10 : 0x00; let wake = modifiers != this.modifiers; this.modifiers = modifiers; if (wake) this.wakeFlag = true; } this.applyHScroll = function(delta) { if (delta) { this.wakeFlag = true; this.hScroll += delta; this.hScroll = clamp(this.hScroll, -32768, 32767); } } this.applyVScroll = function(delta) { if (delta) { this.wakeFlag = true; this.vScroll += delta; this.vScroll = clamp(this.vScroll, -32768, 32767); } } } function ScreenDevice(br) { this.init = function() { this.ctx = br.e.canvas.getContext("2d", { alpha: false, willReadFrequently: false, }); } this.reset = function() { this.x = 0; this.y = 0; this.px = 0; // previous x this.py = 0; // previous y this.widthW = 0; this.heightW = 0; this.palette = [ [0x00,0x00,0x00],[0xFF,0xFF,0xFF],[0x55,0x55,0x55],[0xAA,0xAA,0xAA], [0x00,0x00,0x00],[0xFF,0xFF,0xFF],[0x55,0x55,0x55],[0xAA,0xAA,0xAA], [0x00,0x00,0x00],[0xFF,0xFF,0xFF],[0x55,0x55,0x55],[0xAA,0xAA,0xAA], [0x00,0x00,0x00],[0xFF,0xFF,0xFF],[0x55,0x55,0x55],[0xAA,0xAA,0xAA], ]; this.paletteW = 0; this.selection = [0, 0, 0, 0]; this.sprite = new SpriteBuffer(); let initialWidth = Math.trunc(br.e.clientWidth / initialScreenScale); let initialHeight = Math.trunc(initialWidth * 9/16); this.width = -1; // screen won't resize if dimensions aren't different this.resize(initialWidth, initialHeight, initialScreenScale); this.wakeFlag = false; this.accessed = false; } this.read = function(p) { this.accessed = true; switch (p) { case 0x0: return getH(this.x); case 0x1: return getL(this.x); case 0x2: return getH(this.y); case 0x3: return getL(this.y); case 0x4: return getH(this.width); case 0x5: return getL(this.width); case 0x6: return getH(this.height); case 0x7: return getL(this.height); default: return 0x00; } } this.write = function(p,v) { this.accessed = true; switch (p) { case 0x0: this.x = setH(this.x, v); return; case 0x1: this.x = setL(this.x, v); return; case 0x2: this.y = setH(this.y, v); return; case 0x3: this.y = setL(this.y, v); return; case 0x4: this.widthW = setH(this.widthW, v); return; case 0x5: this.widthW = setL(this.widthW, v); this.commitWidth(); return; case 0x6: this.heightW = setH(this.heightW, v); return; case 0x7: this.heightW = setL(this.heightW, v); this.commitHeight(); return; case 0x8: this.paletteW = setH(this.paletteW, v); return; case 0x9: this.paletteW = setL(this.paletteW, v); this.commitPalette(); return; case 0xA: this.selection[0] = v >> 4; this.selection[1] = v & 0xF; return; case 0xB: this.selection[2] = v >> 4; this.selection[3] = v & 0xF; return; case 0xC: case 0xD: this.sprite.push(v); return; case 0xE: this.draw(v); return; case 0xF: this.move(v); return; default: return; } } this.wake = function() { let wake = this.wakeFlag; this.wakeFlag = false; return wake; } // Mark a region as dirty, with 0 inclusive and 1 exclusive. this.mark = function(x0, y0, x1, y1) { if(x0 < this.dx0) this.dx0 = x0; if(y0 < this.dy0) this.dy0 = y0; if(x1 > this.dx1) this.dx1 = x1; if(y1 > this.dy1) this.dy1 = y1; } // Unmark the dirty region. this.unmark = function() { this.dx0 = 32767; this.dy0 = 32767; this.dx1 = -32768; this.dy1 = -32768; } // Check if the screen has a visible dirty region. this.dirty = function() { // TODO: Does clamp pull a distant dirty region into the viewport? this.dx0 = clamp(this.dx0, 0, this.width); this.dx1 = clamp(this.dx1, 0, this.width); this.dy0 = clamp(this.dy0, 0, this.height); this.dy1 = clamp(this.dy1, 0, this.height); return this.dx1 > this.dx0 && this.dy1 > this.dy0; } // Update the dirty region of this.pixels from fg and bg. this.render = function() { let dw = this.dx1 - this.dx0; for (let y=this.dy0; y> 8 & 0x0F) * 17; let g = (this.paletteW >> 4 & 0x0F) * 17; let b = (this.paletteW & 0x0F) * 17; this.palette[this.paletteW>>12] = [r,g,b]; this.mark(0, 0, this.width, this.height); } this.draw = function(v) { let layer = v & 0x80 ? this.fg : this.bg; let lower = v & 0x0F; switch (v >> 4 & 0x07) { case 0: this.drawPixel(layer, this.x, this.y, lower); break; case 1: this.draw1BitSprite(layer, lower); break; case 2: this.fillLayer(layer, lower); break; case 3: this.draw2BitSprite(layer, lower); break; case 4: this.drawSolidLine(layer, lower); break; case 5: this.drawTexturedLine(layer, lower); break; case 6: this.drawSolidRect(layer, lower); break; case 7: this.drawTexturedRect(layer, lower); break; } this.px = this.x; this.py = this.y; } this.move = function(v) { let dist = v & 0x3F; switch (v & 0xC0) { case 0x00: this.x += dist; this.x &= 0xFFFF; return; case 0x40: this.y += dist; this.y &= 0xFFFF; return; case 0x80: this.x -= dist; this.x &= 0xFFFF; return; case 0xC0: this.y -= dist; this.y &= 0xFFFF; return; } } this.drawPixel = function(layer, x, y, colour) { x = x & 0xFFFF; y = y & 0xFFFF; if (x < this.width && y < this.height) { layer[x + (y * this.width)] = colour; this.mark(x, y, x+1, y+1); } } this.fillLayer = function(layer, colour) { layer.fill(colour); this.mark(0, 0, this.width, this.height); } this.draw1BitSprite = function(layer, transform) { let sprite = this.sprite.get1Bit(transform); this.drawSprite(layer, sprite, transform); } this.draw2BitSprite = function(layer, transform) { let sprite = this.sprite.get2Bit(transform); this.drawSprite(layer, sprite, transform); } this.drawSprite = function(layer, sprite, transform) { let opaque = !(transform & 0x8); for (let y=0; y<8; y++) { for (let x=0; x<8; x++) { let index = sprite[y*8+x]; if (opaque || index) { let colour = this.selection[index]; this.drawPixel(layer, this.x+x, this.y+y, colour) }; } } } this.drawSolidLine = function(layer, colour) { this.drawLine((x, y) => this.drawPixel(layer, x, y, colour)); } this.drawSolidRect = function(layer, colour) { this.drawRect((x, y) => this.drawPixel(layer, x, y, colour)); } this.drawTexturedLine = function(layer, transform) { let sprite = this.sprite.get1Bit(transform); let opaque = !(transform & 0x8); this.drawLine((x, y) => { let index = sprite[(y%8)*8+(x%8)]; if (opaque || index) { let colour = this.selection[index]; this.drawPixel(layer, x, y, colour); } }) } this.drawTexturedRect = function(layer, transform) { let sprite = this.sprite.get1Bit(transform); let opaque = !(transform & 0x8); this.drawRect((x, y) => { let index = sprite[(y%8)*8+(x%8)]; if (opaque || index) { let colour = this.selection[index]; this.drawPixel(layer, x, y, colour); } }) } this.drawLine = function(drawFn) { let xEnd = signed(this.x); let yEnd = signed(this.y); let x = signed(this.px); let y = signed(this.py); let dx = Math.abs(xEnd - x); let dy = -Math.abs(yEnd - y); let sx = x < xEnd ? 1 : -1; let sy = y < yEnd ? 1 : -1; let e1 = dx + dy; while (true) { drawFn(x, y); if (x == xEnd && y == yEnd) break; let e2 = e1 << 1; if (e2 >= dy) { e1 += dy; x += sx; } if (e2 <= dx) { e1 += dx; y += sy; } } } this.drawRect = function(drawFn) { let x = signed(this.x); let px = signed(this.px); let y = signed(this.y); let py = signed(this.py); let x0 = Math.min(x, px); let x1 = Math.max(x, px); let y0 = Math.min(y, py); let y1 = Math.max(y, py); if (x1 >= 0 && y1 >= 0 && x0 < this.width && y0 < this.height) { for (let y=y0; y<=y1; y++) { for (let x=x0; x<=x1; x++) { drawFn(x, y); } } } } } function StreamDevice(br) { this.reset = function() { } this.read = function(p) { switch (p) { case 0x1: return 0xFF; case 0x3: return 0xFF; case 0x5: return 0xFF; default: return 0x00; } } this.write = function(p,v) { switch (p) { case 0x3: br.e.endTransmission(); break; case 0x6: case 0x7: br.e.receiveTransmissionByte(v); break; default: return; } } } function ClipboardDevice(br) { this.reset = function() { this.readQueue = []; // reverse order this.writeQueue = []; // normal order } this.read = function(p) { switch (p) { case 0x4: return clamp(this.readQueue.length, 0, 0xFF); case 0x5: return clamp(this.writeQueue.length, 0, 0xFF); case 0x6: case 0x7: return this.readQueue.pop() || 0x00; default: return 0x00; } } this.write = function(p,v) { // HACK: The -1 is to break the run loop to allow the br.pause() to take hold. switch (p) { case 0x2: this.readEntry(v); return -1; case 0x3: this.writeEntry(v); return -1; case 0x5: this.writeQueue = []; break; case 0x6: case 0x7: this.writeQueue.push(v); break; default: return; } } this.readEntry = function(v) { this.readQueue = []; if (v) { // TODO: Figure out how to make this synchronous. // HACK: Pause the emulator until promise resolves. if (navigator.clipboard.readText) { br.pause(); navigator.clipboard.readText().then( (text) => { this.readQueue = Array.from(encodeUtf8(text)); this.readQueue.reverse(); br.run(); }, () => { br.run(); }, ); } } else { // TODO: Look into valid clipboard binary formats. } } this.writeEntry = function(v) { if (v) { // TODO: Figure out how to make this synchronous. // HACK: Pause the emulator until promise resolves. if (navigator.clipboard.writeText) { br.pause(); let parser = new Utf8StreamParser(); parser.pushList(this.writeQueue); navigator.clipboard.writeText(parser.read()).then( () => { br.run(); }, () => { br.run(); } ); } } else { // TODO: Look into valid clipboard binary formats. // let array = new Uint8Array(this.writeQueue); // let type = "application/octet-stream"; // let blob = new Blob([array.buffer], {type}); // let items = [new ClipboardItem({ [type]: blob })]; // navigator.clipboard.write(items); } this.writeQueue = 0; } } // Read-only buffer for reading a string from a device. function TextBuffer(text) { this.mem = new Uint8Array(text.length); this.p = 0; // Populate the buffer. for (let i=0; i 0) { this.read = remaining * 0.256; } else { this.read = 0; this.end = 0; this.wakeFlag = true; } } else { this.read = 0; } } this.commit = function() { this.end = performance.now() + (this.write / 0.256); } } // Byte queue implemented as a circular buffer. function ByteQueue(capacity) { this.mem = new Uint8Array(capacity); this.length = 0; this.head = 0; this.tail = 0; this.clear = function() { this.length = 0; this.head = 0; this.tail = 0; } this.push = function(v) { // Only push if there is sufficient remaining capacity. if (this.length < this.mem.length) { this.mem[this.head] = v; this.head = (this.head + 1) % this.mem.length; this.length += 1; } } this.pushList = function(list) { // Only push if there is sufficient remaining capacity. if (this.mem.length - this.length >= list.length) { for (let byte of list) this.push(byte); } } this.pop = function() { if (this.length) { let byte = this.mem[this.tail]; this.tail = (this.tail + 1) % this.mem.length; this.length -= 1; return byte; } else { return 0x00; } } } // Circular buffer that holds sprite data for the screen device. function SpriteBuffer() { this.mem = new Uint8Array(16); this.p = 0; this.cache1 = new Uint8Array(64); // cached 1-bit sprite this.cache2 = new Uint8Array(64); // cached 2-bit sprite this.dirty1 = true; // dirty flag for 1-bit sprite this.dirty2 = true; // dirty flag for 2-bit sprite this.push = function(v) { this.mem[this.p] = v; this.p = (this.p + 1) & 0xF; this.dirty1 = true; this.dirty2 = true; } this.get1Bit = function(transform) { if (this.dirty1) { for (let i=0; i<8; i++) { let rowL = this.mem[this.p+i+8 & 0xF]; for (let j=0; j<8; j++) { let bit = rowL >> (7-j) & 0x1; this.cache1[i*8+j] = bit; } } this.dirty1 = false; } return this.transform(this.cache1, transform); } this.get2Bit = function(transform) { if (this.dirty2) { for (let i=0; i<8; i++) { let rowH = this.mem[this.p+i & 0xF]; let rowL = this.mem[this.p+i+8 & 0xF]; for (let j=0; j<8; j++) { let bitH = rowH >> (7-j) & 0x1; let bitL = rowL >> (7-j) & 0x1; this.cache2[i*8+j] = bitH << 1 | bitL; } } this.dirty2 = false; } return this.transform(this.cache2, transform); } this.transform = function(sprite, transform) { let x, y; let t = new Uint8Array(64); switch (transform & 0x7) { case 0: for (y=0; y<8; y++) { for (x=0; x<8; x++) { t[y*8+x] = sprite[ y *8+ x ] } }; break; case 1: for (y=0; y<8; y++) { for (x=0; x<8; x++) { t[y*8+x] = sprite[ y *8+(7-x)] } }; break; case 2: for (y=0; y<8; y++) { for (x=0; x<8; x++) { t[y*8+x] = sprite[(7-y)*8+ x ] } }; break; case 3: for (y=0; y<8; y++) { for (x=0; x<8; x++) { t[y*8+x] = sprite[(7-y)*8+(7-x)] } }; break; case 4: for (y=0; y<8; y++) { for (x=0; x<8; x++) { t[y*8+x] = sprite[ x *8+ y ] } }; break; case 5: for (y=0; y<8; y++) { for (x=0; x<8; x++) { t[y*8+x] = sprite[ x *8+(7-y)] } }; break; case 6: for (y=0; y<8; y++) { for (x=0; x<8; x++) { t[y*8+x] = sprite[(7-x)*8+ y ] } }; break; case 7: for (y=0; y<8; y++) { for (x=0; x<8; x++) { t[y*8+x] = sprite[(7-x)*8+(7-y)] } }; break; } return t; } } function setHH(v, byte) { return byte << 24 | v & 0x00FFFFFF; } function setHL(v, byte) { return byte << 16 | v & 0xFF00FFFF; } function setH (v, byte) { return byte << 8 | v & 0xFFFF00FF; } function setL (v, byte) { return byte | v & 0xFFFFFF00; } function getHH(v) { return v >> 24 & 0x000000FF; } function getHL(v) { return v >> 16 & 0x000000FF; } function getH (v) { return v >> 8 & 0x000000FF; } function getL (v) { return v & 0x000000FF; } function bool (b) { return b ? 0xFF : 0x00; } function signed(v) { return v << 16 >> 16; } function clamp(v,a,b) { return v>=a ? v bedrock-js `; const fullscreenIcon = ` `; const runIcon = ` `; const pauseIcon = ` `; const stopIcon = ` `; const stepIcon = ` `; const assemblerTemplate = `
`; const emulatorTemplate = `
`; const bedrockStyles = ` div.bedrock { --br-fg:var(--fg,#222); --br-bg:var(--bg,#eed); --br-accent:var(--accent,#467); } div.bedrock * { font-family:var(--font-family),monospace; font-weight:var(--font-weight); font-size:var(--font-size); box-sizing:border-box; color:var(--br-bg); } div.bedrock { display:flex; flex-direction:column; overflow:hidden; border-radius:2px; } div.bedrock .hidden { display:none } .bedrock.assembler .editor { position:relative; flex-grow:1; } .bedrock.assembler textarea, .bedrock.assembler .viewer { position:absolute; top:0; left:0; width:100%; height:100%; margin:0; padding:0.5rem 0.7rem; max-height:calc(100vh); overflow:auto; border:none; white-space:pre; tab-size:2; } .bedrock.assembler textarea { z-index:1; background:transparent; color:transparent; caret-color:var(--br-fg); resize:vertical } .bedrock.assembler textarea:focus { outline:none; } .bedrock:fullscreen textarea, .bedrock:fullscreen .screen { resize:none; height:100% !important; } .bedrock.assembler .viewer { z-index:0; background:var(--br-bg); color:var(--br-fg) } .bedrock.assembler .viewer .comment { color:var(--comment, #998) } .bedrock.assembler .viewer .value { color:var(--value, #532) } .bedrock.assembler .viewer .definition { color:var(--definition, #245) } .bedrock.assembler .viewer .reference { color:var(--reference ) } .bedrock .menubar { background:var(--br-accent); display:flex; padding:3px; gap:3px } .bedrock button { display:flex; align-items:center; background:#0004; border:none; border-radius:2px; cursor:pointer; } .bedrock .menubar button:hover { background:#0006; } .bedrock .menubar button:active { background:#000c; } .bedrock .menubar .status { font-size:85%; } .bedrock .menubar a { text-decoration:none; white-space:nowrap; padding-left:0.4em } .bedrock .menubar a:hover { text-decoration:underline } .bedrock .menubar .status { flex-grow:1; white-space:nowrap; overflow-x:hidden; align-self:center; padding-left:0.4rem; } .bedrock .panel { color:var(--br-bg); background:var(--br-accent); } .bedrock .panel > * { background:#0006; padding:0.3rem 0.5rem; } .bedrock .panel ul { overflow:scroll; margin:0; list-style:none; } .bedrock .panel li { white-space:pre; } .bedrock .panel .label { opacity:60%; } .bedrock .stream { overflow:auto; background:var(--br-bg); padding:0.3rem; flex-grow:1; max-height:10rem; } .bedrock .stream ul { margin:0; padding:0; padding-left:0.5rem } .bedrock .stream li { color:var(--br-fg); padding:0 0.3rem; cursor:pointer; white-space:break-spaces } .bedrock .stream li::marker { content:'>'; } .bedrock .stream li:hover { text-decoration:underline } .bedrock .screen { background:#111; } .bedrock .screen canvas { display:block; margin:0 auto; image-rendering:pixelated; touch-action:none; } .bedrock:fullscreen .stream { max-height:none; } `; const instructionTable = { 'HLT': 0x00, 'NOP' : 0x20, 'DB1' : 0x40, 'DB2' : 0x60, 'DB3' : 0x80, 'DB4' : 0xA0, 'DB5' : 0xC0, 'DB6' : 0xE0, 'PSH': 0x01, 'PSH:': 0x21, 'PSH*': 0x41, 'PSH*:': 0x61, 'PSHr': 0x81, 'PSHr:': 0xA1, 'PSHr*': 0xC1, 'PSHr*:': 0xE1, ':': 0x21, '*:': 0x61, 'r:': 0xA1, 'r*:': 0xE1, 'POP': 0x02, 'POP:': 0x22, 'POP*': 0x42, 'POP*:': 0x62, 'POPr': 0x82, 'POPr:': 0xA2, 'POPr*': 0xC2, 'POPr*:': 0xE2, 'CPY': 0x03, 'CPY:': 0x23, 'CPY*': 0x43, 'CPY*:': 0x63, 'CPYr': 0x83, 'CPYr:': 0xA3, 'CPYr*': 0xC3, 'CPYr*:': 0xE3, 'DUP': 0x04, 'DUP:': 0x24, 'DUP*': 0x44, 'DUP*:': 0x64, 'DUPr': 0x84, 'DUPr:': 0xA4, 'DUPr*': 0xC4, 'DUPr*:': 0xE4, 'OVR': 0x05, 'OVR:': 0x25, 'OVR*': 0x45, 'OVR*:': 0x65, 'OVRr': 0x85, 'OVRr:': 0xA5, 'OVRr*': 0xC5, 'OVRr*:': 0xE5, 'SWP': 0x06, 'SWP:': 0x26, 'SWP*': 0x46, 'SWP*:': 0x66, 'SWPr': 0x86, 'SWPr:': 0xA6, 'SWPr*': 0xC6, 'SWPr*:': 0xE6, 'ROT': 0x07, 'ROT:': 0x27, 'ROT*': 0x47, 'ROT*:': 0x67, 'ROTr': 0x87, 'ROTr:': 0xA7, 'ROTr*': 0xC7, 'ROTr*:': 0xE7, 'JMP': 0x08, 'JMP:': 0x28, 'JMP*': 0x48, 'JMP*:': 0x68, 'JMPr': 0x88, 'JMPr:': 0xA8, 'JMPr*': 0xC8, 'JMPr*:': 0xE8, 'JMS': 0x09, 'JMS:': 0x29, 'JMS*': 0x49, 'JMS*:': 0x69, 'JMSr': 0x89, 'JMSr:': 0xA9, 'JMSr*': 0xC9, 'JMSr*:': 0xE9, 'JCN': 0x0A, 'JCN:': 0x2A, 'JCN*': 0x4A, 'JCN*:': 0x6A, 'JCNr': 0x8A, 'JCNr:': 0xAA, 'JCNr*': 0xCA, 'JCNr*:': 0xEA, 'JCS': 0x0B, 'JCS:': 0x2B, 'JCS*': 0x4B, 'JCS*:': 0x6B, 'JCSr': 0x8B, 'JCSr:': 0xAB, 'JCSr*': 0xCB, 'JCSr*:': 0xEB, 'LDA': 0x0C, 'LDA:': 0x2C, 'LDA*': 0x4C, 'LDA*:': 0x6C, 'LDAr': 0x8C, 'LDAr:': 0xAC, 'LDAr*': 0xCC, 'LDAr*:': 0xEC, 'STA': 0x0D, 'STA:': 0x2D, 'STA*': 0x4D, 'STA*:': 0x6D, 'STAr': 0x8D, 'STAr:': 0xAD, 'STAr*': 0xCD, 'STAr*:': 0xED, 'LDD': 0x0E, 'LDD:': 0x2E, 'LDD*': 0x4E, 'LDD*:': 0x6E, 'LDDr': 0x8E, 'LDDr:': 0xAE, 'LDDr*': 0xCE, 'LDDr*:': 0xEE, 'STD': 0x0F, 'STD:': 0x2F, 'STD*': 0x4F, 'STD*:': 0x6F, 'STDr': 0x8F, 'STDr:': 0xAF, 'STDr*': 0xCF, 'STDr*:': 0xEF, 'ADD': 0x10, 'ADD:': 0x30, 'ADD*': 0x50, 'ADD*:': 0x70, 'ADDr': 0x90, 'ADDr:': 0xB0, 'ADDr*': 0xD0, 'ADDr*:': 0xF0, 'SUB': 0x11, 'SUB:': 0x31, 'SUB*': 0x51, 'SUB*:': 0x71, 'SUBr': 0x91, 'SUBr:': 0xB1, 'SUBr*': 0xD1, 'SUBr*:': 0xF1, 'INC': 0x12, 'INC:': 0x32, 'INC*': 0x52, 'INC*:': 0x72, 'INCr': 0x92, 'INCr:': 0xB2, 'INCr*': 0xD2, 'INCr*:': 0xF2, 'DEC': 0x13, 'DEC:': 0x33, 'DEC*': 0x53, 'DEC*:': 0x73, 'DECr': 0x93, 'DECr:': 0xB3, 'DECr*': 0xD3, 'DECr*:': 0xF3, 'LTH': 0x14, 'LTH:': 0x34, 'LTH*': 0x54, 'LTH*:': 0x74, 'LTHr': 0x94, 'LTHr:': 0xB4, 'LTHr*': 0xD4, 'LTHr*:': 0xF4, 'GTH': 0x15, 'GTH:': 0x35, 'GTH*': 0x55, 'GTH*:': 0x75, 'GTHr': 0x95, 'GTHr:': 0xB5, 'GTHr*': 0xD5, 'GTHr*:': 0xF5, 'EQU': 0x16, 'EQU:': 0x36, 'EQU*': 0x56, 'EQU*:': 0x76, 'EQUr': 0x96, 'EQUr:': 0xB6, 'EQUr*': 0xD6, 'EQUr*:': 0xF6, 'NQK': 0x17, 'NQK:': 0x37, 'NQK*': 0x57, 'NQK*:': 0x77, 'NQKr': 0x97, 'NQKr:': 0xB7, 'NQKr*': 0xD7, 'NQKr*:': 0xF7, 'SHL': 0x18, 'SHL:': 0x38, 'SHL*': 0x58, 'SHL*:': 0x78, 'SHLr': 0x98, 'SHLr:': 0xB8, 'SHLr*': 0xD8, 'SHLr*:': 0xF8, 'SHR': 0x19, 'SHR:': 0x39, 'SHR*': 0x59, 'SHR*:': 0x79, 'SHRr': 0x99, 'SHRr:': 0xB9, 'SHRr*': 0xD9, 'SHRr*:': 0xF9, 'ROL': 0x1A, 'ROL:': 0x3A, 'ROL*': 0x5A, 'ROL*:': 0x7A, 'ROLr': 0x9A, 'ROLr:': 0xBA, 'ROLr*': 0xDA, 'ROLr*:': 0xFA, 'ROR': 0x1B, 'ROR:': 0x3B, 'ROR*': 0x5B, 'ROR*:': 0x7B, 'RORr': 0x9B, 'RORr:': 0xBB, 'RORr*': 0xDB, 'RORr*:': 0xFB, 'IOR': 0x1C, 'IOR:': 0x3C, 'IOR*': 0x5C, 'IOR*:': 0x7C, 'IORr': 0x9C, 'IORr:': 0xBC, 'IORr*': 0xDC, 'IORr*:': 0xFC, 'XOR': 0x1D, 'XOR:': 0x3D, 'XOR*': 0x5D, 'XOR*:': 0x7D, 'XORr': 0x9D, 'XORr:': 0xBD, 'XORr*': 0xDD, 'XORr*:': 0xFD, 'AND': 0x1E, 'AND:': 0x3E, 'AND*': 0x5E, 'AND*:': 0x7E, 'ANDr': 0x9E, 'ANDr:': 0xBE, 'ANDr*': 0xDE, 'ANDr*:': 0xFE, 'NOT': 0x1F, 'NOT:': 0x3F, 'NOT*': 0x5F, 'NOT*:': 0x7F, 'NOTr': 0x9F, 'NOTr:': 0xBF, 'NOTr*': 0xDF, 'NOTr*:': 0xFF, // TODO: Remove. '?':0x40, };