const TXBUF = 0x000; const RXBUF = 0x080; const TXHEAD = 0x100; const TXTAIL = 0x104; const RXHEAD = 0x108; const RXTAIL = 0x10c; const SYSREADY = 0x110; const DOT_INTERVAL_MS = 120; const PERIPHS_SIZE = 0x200; const POLL_INTERVAL_MS = 5; const COLS = 80; const TAB_WIDTH = 8; const CURSOR_IDLE_TIME_MS = 1000; class Emulator { constructor() { this.mem = new WebAssembly.Memory({ initial: 1, maximum: 1, shared: true }); this.mem_u8 = new Uint8Array(this.mem.buffer); for (let i = 0; i < PERIPHS_SIZE; ++i) this.mem_u8[i] = 0; this.mem_i32 = new Int32Array(this.mem.buffer); this.decoder = new TextDecoder('utf-8'); this.encoder = new TextEncoder('utf-8'); this.output = document.getElementById('output'); this.rx_queue = []; this.timer = setInterval(() => this.poll(), POLL_INTERVAL_MS); this.rows = this.max_rows(); this.grid = Array.from( { length: this.rows }, () => new Array(COLS).fill(' ')); this.cursor = { x: 0, y: 0 }; this.range = { start: { x: 0, y: 0 }, end: { x: 0, y: 0 } }; this.idle_timer = null; this.input_enable = false; document.addEventListener('keydown', (e) => this.handle_keydown(e)); window.addEventListener('resize', () => this.handle_resize()); this.print("Assembling kernel "); const dots = setInterval(() => this.print("."), DOT_INTERVAL_MS); this.worker = new Worker('boot.js', { type: 'module' }); this.worker.postMessage({ type: "load", mem: this.mem }); this.worker.onmessage = (e) => { clearInterval(dots); this.print(" done\n"); this.worker.postMessage({ type: "boot" }); }; fetch('prelude.f') .then(res => res.text()) .then(text => { for (const cu of this.encoder.encode(text)) this.rx_queue.push(cu); }); } poll() { const txhead = Atomics.load(this.mem_u8, TXHEAD); const txtail = Atomics.load(this.mem_u8, TXTAIL); if (txhead != txtail) this.handle_tx_data(txhead, txtail); if (this.rx_queue.length != 0) { const rxhead = Atomics.load(this.mem_u8, RXHEAD); const rxtail = Atomics.load(this.mem_u8, RXTAIL); if (this.fifo_next(rxtail) != rxhead) this.handle_rx_data(rxhead, rxtail); } if (!this.input_enable) { const sysready = Atomics.load(this.mem_u8, SYSREADY); if (sysready != 0) { this.input_enable = true; this.flush_output(); document.getElementById('cursor').classList.add('blinking'); } } } fifo_next(idx) { return (idx + 1) & 0x7f; } handle_tx_data(head, tail) { const data = []; do { data.push(this.mem_u8[TXBUF + head]); head = this.fifo_next(head); } while (head != tail); Atomics.store(this.mem_u8, TXHEAD, head); this.print(this.decoder.decode(new Uint8Array(data))); } handle_rx_data(head, tail) { do { this.mem_u8[RXBUF + tail] = this.rx_queue.shift(); tail = this.fifo_next(tail); } while (this.fifo_next(tail) != head && this.rx_queue.length != 0); Atomics.store(this.mem_u8, RXTAIL, tail); Atomics.notify(this.mem_i32, RXTAIL / 4); } print(str) { for (const cp of str) { switch (cp) { case '\n': do this.print_character(' '); while (this.range.start.x != 0); break; case '\t': do this.print_character(' '); while (this.range.start.x % TAB_WIDTH != 0); break; default: this.print_character(cp); break; } } this.flush_output(); } print_character(c) { this.shift_up(this.range.start); this.grid[this.range.start.y][this.range.start.x] = c; this.cursor = this.next_cell(this.cursor); this.range.start = this.next_cell(this.range.start); } handle_keydown(e) { if (!this.input_enable) return; if (e.key.length == 1 && !e.ctrlKey && !e.altKey && !e.metaKey) { this.shift_up(this.cursor); this.grid[this.cursor.y][this.cursor.x] = e.key; this.cursor_move(1, 0); } else if (e.key == 'Enter') { this.cursor.y = this.range.end.y + 1; while (this.cursor.y >= this.rows) this.scroll(); this.cursor.x = 0; this.submit_line(); Object.assign(this.range.start, this.cursor); Object.assign(this.range.end, this.cursor); } else if (e.key == 'ArrowLeft' || e.key == 'b' && e.ctrlKey) { this.cursor_move(-1, 0); } else if (e.key == 'ArrowRight' || e.key == 'f' && e.ctrlKey) { this.cursor_move(1, 0); } else if (e.key == 'ArrowUp' || e.key == 'p' && e.ctrlKey) { this.cursor_move(0, -1); } else if (e.key == 'ArrowDown' || e.key == 'n' && e.ctrlKey) { this.cursor_move(0, 1); } else if (e.key == 'Backspace') { if (this.cursor_move(-1, 0)) this.shift_down(this.cursor); } else if (e.key == 'Delete' || e.key == 'd' && e.ctrlKey) { this.shift_down(this.cursor); } else if (e.key == 'Home' || e.key == 'a' && e.ctrlKey) { Object.assign(this.cursor, this.range.start); } else if (e.key == 'End' || e.key == 'e' && e.ctrlKey) { Object.assign(this.cursor, this.range.end); } else { return; } e.preventDefault(); this.flush_output(); document.getElementById('cursor').classList.remove('blinking'); if (this.idle_timer) clearTimeout(this.idle_timer); this.idle_timer = setTimeout(() => { document.getElementById('cursor').classList.add('blinking'); }, CURSOR_IDLE_TIME_MS); } submit_line() { let line = ''; let cell = this.range.start; while (cell.x != this.range.end.x || cell.y != this.range.end.y) { line += this.grid[cell.y][cell.x]; cell = this.next_cell(cell); } line += '\n'; for (let cu of this.encoder.encode(line)) { this.rx_queue.push(cu); } } cursor_move(dx, dy) { let x = this.cursor.x + dx; let y = this.cursor.y + dy; if (x < 0) { --y; x += COLS; } else if (x >= COLS) { ++y; x -= COLS; } if (this.range.start.y > y || this.range.end.y < y) return false; if (y == this.range.start.y && this.range.start.x > x) return false; if (y == this.range.end.y && this.range.end.x < x) return false; this.cursor.x = x; this.cursor.y = y; return true; } prev_cell(cell) { if (cell.x > 0) return { x: cell.x - 1, y: cell.y }; else if (cell.y > 0) return { x: COLS - 1, y: cell.y - 1 }; else return null; } next_cell(cell) { if (cell.x < COLS - 1) return { x: cell.x + 1, y: cell.y }; else if (cell.y < this.rows - 1) return { x: 0, y: cell.y + 1 }; else return null; } shift_up(start) { let cell = { x: this.range.end.x, y: this.range.end.y }; while (cell.x != start.x || cell.y != start.y) { let prev = this.prev_cell(cell); this.grid[cell.y][cell.x] = this.grid[prev.y][prev.x]; cell = prev; } if (this.next_cell(this.range.end) == null) this.scroll(); this.range.end = this.next_cell(this.range.end); } shift_down(start) { let cell = { x: start.x, y: start.y }; while (cell.x != this.range.end.x || cell.y != this.range.end.y) { let next = this.next_cell(cell); this.grid[cell.y][cell.x] = this.grid[next.y][next.x]; cell = next; } this.grid[this.range.end.y][this.range.end.x] = ' '; this.range.end = this.prev_cell(this.range.end); } flush_output() { const html = this.grid.map((row, y) => { return row.map((c, x) => { const ec = this.html_escape(c); if (this.input_enable && x == this.cursor.x && y == this.cursor.y) return '' + ec + ''; else return ec; }).join('').trimEnd(); }).join('\n'); this.output.innerHTML = html; } html_escape(c) { switch (c) { case '"': return '"'; case "'": return '''; case '&': return '&'; case '<': return '<'; case '>': return '>'; default: return c; } } scroll() { this.grid.shift() this.grid.push(new Array(COLS).fill(' ')); this.cursor.y -= 1; this.range.start.y -= 1; this.range.end.y -= 1; } max_rows() { const style = getComputedStyle(this.output); const line_height = parseFloat(style.lineHeight); const margin_top = parseFloat(style.marginTop); const margin_bottom = parseFloat(style.marginBottom); const viewport_height = window.innerHeight; const output_height = viewport_height - margin_top - margin_bottom; return Math.floor(output_height / line_height) - 1; } handle_resize() { this.rows = this.max_rows(); while (this.grid.length < this.rows) this.grid.push(new Array(COLS).fill(' ')); while (this.grid.length > this.rows) { this.grid.shift() this.cursor.y -= 1; this.range.start.y -= 1; this.range.end.y -= 1; } this.flush_output(); } } window.addEventListener('DOMContentLoaded', () => { document.getElementById('output').innerText = ''; window.emu = new Emulator(); });