diff options
| author | LLLL Colonq <llll@colonq> | 2026-08-17 20:14:16 -0400 |
|---|---|---|
| committer | LLLL Colonq <llll@colonq> | 2026-08-17 20:14:16 -0400 |
| commit | de99c5f7636a952025ea13de02bafd25bcb6bdec (patch) | |
| tree | be359424af4f6717103e3b667b6529ae5a4b4e6a /2026/games_submissions/liquidcake1/interpreter.mjs | |
| parent | 4d4d1f24721f27fbf37e1070be6414de130ace61 (diff) | |
Fix broken
Diffstat (limited to '2026/games_submissions/liquidcake1/interpreter.mjs')
| -rw-r--r-- | 2026/games_submissions/liquidcake1/interpreter.mjs | 449 |
1 files changed, 449 insertions, 0 deletions
diff --git a/2026/games_submissions/liquidcake1/interpreter.mjs b/2026/games_submissions/liquidcake1/interpreter.mjs new file mode 100644 index 0000000..703b2d7 --- /dev/null +++ b/2026/games_submissions/liquidcake1/interpreter.mjs @@ -0,0 +1,449 @@ +// JIT TODO: +// * OK, it's probably about 6x faster with JIT than without, but we can go another 4-5x faster by loop unrolling. +// * We don't bounds-check the stack before executing a JITted routine. +// * We can probably do a lot better in speed by unrolling and aliasing the stack. Name top N stack vars a1 to aN, +// then unpack the top N into those, then run code, then pack aN to a?. +// * We don't have support for branches, even just the dumb "primary only, bail otherwise" variety. +// * We don't ever remove JIT. We should destroy JIT when a cell is modified. +// * If a cell is modified _DURING_ a JITted sequence, what do? If external, we can just claim it doesn't matter. +// If the thread self-modified, we'll need a means to abort the JITted sequence early. +// * There is no JIT visualisation. + +import { instructions, instructions_raw } from "./instructions.mjs"; +import { Queue } from "./queue.mjs"; +import { Jit } from "./jit.mjs"; + +function gen_fingerprint(s) { + let x = 0; + for(let c of Array.from(s).map(x => x.charCodeAt(0))) { + x = x * 256 + c; + } + return x; +} + +function deep_copy(obj) { + return JSON.parse(JSON.stringify(obj)); +} + +function shallow_copy(obj) { + return Object.fromEntries(Object.entries(obj)); +} + +function shallowish_copy(obj, i) { + if (i > 0) { + return shallowish_copy(obj, i - 1); + } else { + return shallow_copy(obj); + } +} + +class Thread { + running = true; + mode = "normal"; + jit = null; + tick_count = 0; + + constructor(interpreter, col, row, cold, rowd, stack, overlays) { + this.interpreter = interpreter; + this.col = col; + this.row = row; + this.cold = cold; + this.rowd = rowd; + this.stack = stack; + this.overlays = overlays || {}; + } + + split_thread() { + let thread = new Thread( + this.interpreter, + this.col, + this.row, + -this.cold, + -this.rowd, + Array.from(this.stack), + shallow_copy(this.overlays), + ); + thread.tick_forwards(); + thread.tick_count = this.tick_count; // Ensure new thread has equal priority to old thread. + return thread; + } + + tick_forwards() { + this.col += this.cold; + this.row += this.rowd; + if (this.row == this.interpreter.field.length && this.rowd > 0) this.row = 0; + else if (this.row == -1 && this.rowd < 0) this.row = this.interpreter.field.length - 1; + if (this.col == this.interpreter.field[this.row].length && this.cold > 0) this.col = 0; + else if (this.col == -1 && this.cold < 0) this.col = this.interpreter.field[this.row].length - 1; + } + + pop() { + if (this.stack.length > 0) { + return this.stack.pop(); + } else { + this.interpreter.error(this, "Stack underflow"); + return 0; + } + } +} + +export class Interpreter { + max_loops = 1000000000000; + ticks = 0; + paused_wake = null; + paused_event = null; + slice_loops = 1; + slice_sleep = 20; + running = false; + threads = []; + events = {}; + awake_sleep = null; + stats = []; + field = []; + instructions = shallow_copy(instructions); + instructions_raw = shallow_copy(instructions_raw); + overlays = {}; + input_queue = new Queue(); // Queue things like: change speed, input, etc. + stdin_queue = new Queue(); // Chars from stdin. + stdin_waiters = []; + handlers = {}; + + constructor() { + this.jit = new Jit(this); + } + + load_overlay(fingerprint, overlay) { + this.overlays[gen_fingerprint(fingerprint)] = overlay; + } + + add_handler(event_name, f) { + if (this.handlers[event_name] === undefined) { + this.handlers[event_name] = []; + } + this.handlers[event_name].push(f); + } + + trigger_event(event_name, ...args) { + //console.log(`Event: ${event_name} -- ${args}`); + let handlers = this.handlers[event_name]; + if (handlers === undefined) { + //console.log(`Unhandled event: ${event_name} -- ${args}`); + } else { + handlers.forEach(f => f(...args)); + } + } + + set_speed(raw_speed) { + if (raw_speed > 0) { + this.slice_sleep = 1; + this.slice_loops = Math.floor(1.1 ** raw_speed); + } else { + this.slice_sleep = Math.floor(1.1 ** -raw_speed); + this.slice_loops = 1; + } + this.trigger_event("speed_changed", raw_speed, this.slice_sleep, this.slice_loops); + } + + toggle_pause(e) { + if (this.paused_wake === null) { + this.pause(); + } else { + this.unpause(); + } + } + + pause() { + let thisthis = this; + this.paused_event = new Promise(function (r) { + thisthis.paused_wake = r; + // We shouldn't be in the sleep loop if paused. + if (thisthis.awake_sleep != null) thisthis.awake_sleep(); + }); + // These really need outputting from the main loop, not here! + this.trigger_event("paused", true); + this.threads.forEach(function (thread) { + thisthis.trigger_event("thread_paused", thread, true); + }); + } + + unpause() { + let old_wake = this.paused_wake; + let thisthis = this; + this.paused_wake = null; + this.trigger_event("paused", false); + this.threads.forEach(function (thread) { + thisthis.trigger_event("thread_paused", thread, false) + }); + if (old_wake) + old_wake(); + } + + async sleep_until_unpaused(all_blocked) { // TODO??? + if (!this.paused_event) + await this.sleep(all_blocked ? 50 : this.slice_sleep); + if (this.paused_event) { + let paused_event = this.paused_event; + this.paused_event = null; + let unpause_data = await paused_event; + return unpause_data; + } + } + + step(e) { + let old_wake = this.paused_wake; + this.pause(); + if (old_wake !== null) { + old_wake({count: 1}); + } + } + + step_thread(thread) { + let old_wake = this.paused_wake; + this.pause(); + if (old_wake !== null) { + old_wake({threads: [thread], count: 1}); + } + } + + async go() { + //console.log(this.threads); + this.stop(); + if (this.running_promise != null) { + console.log("Waiting for last thread to exit"); + await this.running_promise; + console.log("Last thread has exited"); + } + let thread = new Thread(this, 0, 0, 1, 0, []); + this.new_thread(thread); + this.trigger_event("started"); + this.main_loop(); + } + + new_thread(thread) { + this.threads.push(thread); + this.trigger_event("thread_created", thread); + } + + stop() { + if (this.threads.length > 0) { + this.trigger_event("stopped"); + } + this.threads.forEach(function (thread) { thread.running = false; }); + this.unpause(); + if (this.awake_sleep != null) this.awake_sleep(); + } + + check(state, col, row) { + if (row < 0 || row >= this.field.length) this.error(state, `Row ${row} is out of bounds`); + //else if (col < 0 || col >= this.field[row].length) this.error(state, `Col ${col} is out of bounds`); + else return true; + } + + set_field(state, col, row, val) { + if (this.check(state, col, row)) { + this.set_cell(col, row, val); + } else { + console.log(`out of bounds access ${state} ${col} ${row} ${val}`); + } + } + + set_cell(col, row, val) { + let val_str = String.fromCharCode(val); + this.field[row][col] = val; + let title = `(${col},${row})=${val} (${val_str})`; + if (this.instructions_raw[val_str] !== undefined) + title += ": " + this.instructions_raw[val_str].desc; + this.jit.cell_changed(row, col); + this.trigger_event("cell_changed", col, row, val); + } + + get_field(state, col, row, val) { + if (this.check(state, col, row)) { + let val = this.field[row][col]; + return val === undefined ? 0 : val; + } + } + + out(s) { + this.trigger_event("char_out", s); + //console.log("Out: " + s); + } + oute(s) { + this.trigger_event("error_occurred", s); + //console.log("Err: " + s); + } + + error(thread, message) { + thread.running = false; + this.output_error(thread, message); + } + + output_error(thread, message) { + this.oute(`ERROR: ${message}`); + this.oute(`State was: row=${thread.row} col=${thread.col} stack=${thread.stack}`); + this.output_field(); + } + + output_field() { + let lines = []; + for(let row=0; row<this.field.length; row++) { + let s = ""; + for(let col=0; col<this.field[row].length; col++) { + let c = this.field[row][col]; + s += (c > 31 && c < 127 ? String.fromCharCode(this.field[row][col]) : "?"); + } + lines.push(s); + } + this.trigger_event("output_field", lines); + } + + async sleep(ms) { + await new Promise(r => { this.awake_sleep = r; setTimeout(r, ms) }); + this.awake_sleep = null; + } + + process_events() { + while (this.input_queue.length > 0) { + let item = this.input_queue.pop(); + if (item[0] == "set_speed") { + this.set_speed(item[1]); + } else if (item[0] == "pause") { + this.pause(); + } else if (item[0] == "unpause") { + this.unpause(); + } else if (item[0] == "stdin") { + if (this.stdin_waiters.length > 0) { + this.stdin_waiters.shift()(item[1]); + } else { + this.stdin_queue.push(item[1]); + } + } + } + } + + async main_loop() { + let ticks = 0; + this.running = true; + let running_wake = null; + this.running_promise = new Promise(r => { running_wake = r; }); + let highlighted_cells = []; + let start_time = new Date().getTime(); + let all_blocked = false; + while(this.threads.length > 0) { + this.process_events(); + let slice_loops = this.slice_loops; + let limit_threads = null; + let need_sleep = true; + + this.threads.forEach(thread => this.trigger_event("thread_state_updated", thread)); + this.trigger_event("thread_state_synced", this.threads); + + if (ticks > this.max_loops) { + this.ticks += ticks; + ticks = 0; + let end_time = new Date().getTime(); + this.oute(`Ran out of ticks! (${ticks} > ${this.max_loops})`); + console.log(`${ticks} in ${end_time - start_time} is ${ticks / (end_time - start_time) / 1000} MHz`); + this.pause(); + this.output_field(); + } + + let unpause_data = await this.sleep_until_unpaused(all_blocked); + if (unpause_data) { + console.log(`Unpaused; resetting ticks to 0 from ${ticks}`); + start_time = new Date().getTime(); + if (unpause_data.count !== undefined) { + need_sleep = false; + slice_loops = unpause_data.count; + } + if (unpause_data.threads !== undefined) + limit_threads = unpause_data.threads; + } + + let count = 0; + let dead_threads = []; + while(count < slice_loops && this.threads.length > 0) { + // This will _not_ iterate over newly-added threads. + all_blocked = true; + let to_iter = limit_threads !== null ? limit_threads : this.threads; + to_iter = to_iter.filter(thread => !thread.blocked); + if (to_iter.length == 0) { + break; + } + to_iter.forEach(function (thread) { if (!thread.waiter) { all_blocked = false; } }); + let min_count = Math.min(...to_iter.map(x => x.tick_count)); + to_iter.forEach((thread, i) => { + if (thread.tick_count == min_count) { + let tick_count = this.tick(thread, slice_loops - count, i); + count += tick_count; + thread.tick_count += tick_count; + } + }); + let new_dead_threads = this.threads.filter(t => !t.running); + if (new_dead_threads) { + dead_threads.push(...new_dead_threads); + this.threads = this.threads.filter(t => t.running); + } + } + + dead_threads.forEach( + dead_thread => this.trigger_event("thread_dead", dead_thread)); + + ticks += count; + } + running_wake(); + //console.log(`exited main_loop with ticks ${ticks}`); + this.ticks += ticks; + let end_time = new Date().getTime(); + //console.log(`${ticks} in ${end_time - start_time} is ${ticks / (end_time - start_time) / 1000} MHz`); + this.output_field(); + this.trigger_event("terminate"); + } + + tick(thread, target_ticks, thread_num) { + if (thread.waiter) { + try { + if (thread.waiter(thread)) { + thread.waiter = null; + } + } catch (err) { + this.error(thread, "Exception caught during waiter: " + err.message); + //console.log(err.stack); + } + return 1; + } + if (thread.mode == "normal") { + let symbol = this.field[thread.row][thread.col] || 32; + let instruction = thread.overlays[symbol] || this.instructions[symbol]; + //console.log(`Executing ${String.fromCharCode(symbol)} at ${thread.col},${thread.row} stack_length=${thread.stack.length} stack_tail=${thread.stack.slice(-10)}`); + //console.log("Executing " + String.fromCharCode(symbol)); + let jit_return = this.jit.step_jit(thread, target_ticks); + if (jit_return > 0) { + return jit_return; + } + if (instruction == null) { + this.error(thread, "Invalid instruction: " + symbol); + } else { + try { + let ret = instruction(thread, thread_num); + if (ret && ret.constructor === Promise) { + thread.blocked = true; + } + } catch (err) { + this.error(thread, "Exception caught during " + symbol + ": " + err.message); + //console.log(err.stack); + } + } + } else if (thread.mode == "string") { + let symbol = this.field[thread.row][thread.col]; + if (symbol == '"'.charCodeAt(0)) { + thread.mode = "normal"; + } else { + thread.stack.push(symbol); + } + } + thread.tick_count += 1; + thread.tick_forwards(); + return 1; + } +} |
