/** * ButtonMachine is a simple fantasy computer that has a * simple stack/register machine instruction set with a * limited number of "ticks" of execution. It's intended * to be a study project for people learning to code, and * also a challenge to implement small code in a limited * amount of time. * * You can alter the number of ticks allowed by setting * this.max_ticks before you run the code. */ export class ButtonMachine { /** * Takes an array, each element of the array is another * array with the "lines" of code to execute. A line is * in the form of [OP, DATA?, ...]. Some operations (see * their docs) have different numbers of data arguments, * from 0 to 4. * * @constructor * @param { Array } code -- The array of arrays for the code. */ constructor() { this.stack = []; this.ram = new Array(64).fill(0); this.ip = 0; this.code = []; this.max_ticks = 256; this.tick = 0; this.registers = { 'IX': 0, 'AX': 0, 'BX': 0, 'CX': 0, 'DX': 0 }; this.error = ''; this.error_line = 0; this.halted = false; } /** * Used internally to print out error messages if a test * doesn't pass. It will print the message and source * location if test is false. * * @param { boolean } test -- If false then error. * @param { string } message -- The message to display to the user including the source locations. * * @return { boolean } -- Just whatever test was. */ assert(test, message) { // I should use exceptions but not sure if I want to do that too early in the course if(!test) { let display_op = this.cur_op ? this.cur_op.join(' ') : 'NONE'; this.error_line = this.ip; console.log(`HALT[FIRE]: ${message} at line #${this.ip}: ${display_op}`); this.error = message; this.halted = true; } return test; } /** * Returns this machine's availalbe register names. * * @return { Array[String] } -- List of registers. */ register_names() { return Object.getOwnPropertyNames(this.registers); } /** * Statis method that returns the available operations. * * @return { Array[String] } -- List of operation names. */ static operations() { return Object.getOwnPropertyNames(ButtonMachine.prototype) .filter(x => x.startsWith('op_')) .map(x => x.slice(3)); } /** * Property function (don't call it) that gives the * current stack top. * * @return { Number? } -- Whatever is on top. */ get stack_top() { return this.stack[this.stack.length - 1]; } /** * Property function to get the current "line" of code, * which is an array of the [OP, DATA?, ...]. * * @return { Array[String|Numbre]] } -- Current code line. */ get cur_op() { return this.code[this.ip]; } /** * Many operations are "infix", like a+b for ADD, so this * does the very common operation of: * * 1. pop both operands off the stack as a, b * 2. give them to cb(a, b) to do it * 3. take the result and push it on the stack * 4. next() * * @param { String } op_name -- Name of the operation for debugging/assert messages. * @param { function } cb -- callback cb(a, b) that should do the operation and return the result. */ infix(op_name, cb) { let b = this.stack.pop(); this.assert(b !== undefined, `${op_name} right operand POP empty stack`); let a = this.stack.pop(); this.assert(b !== undefined, `${op_name} left operand POP empty stack`); let res = cb(a, b); this.assert(res != NaN, `${op_name} results in NaN value`); this.assert(res !== undefined, `${op_name} results in undefined value`); this.stack.push(res); this.next(); } /** * ADD operation, expects 2 operands on the stack: * * PUSH 1 * PUSH 2 * ADD * * Top of stack is now 3. */ op_ADD() { this.infix('ADD', (a,b) => a + b); } /** * SUB operation, expects 2 operands on the stack: * * PUSH 1 * PUSH 2 * SUB * * Top of stack is now -1. */ op_SUB() { this.infix('SUB', (a,b) => a - b); } /** * DIVide operation, expects 2 operands on the stack: * * PUSH 1 * PUSH 2 * DIV * * Top of stack is now 0.5. You'll notice that, since * this is a simple "fantasy" machine, it doesn't really * do binary numbers and just uses JavaScript default * numerics. */ op_DIV() { this.infix('DIV', (a,b) => a / b); } /** * MULiply operation, expects 2 operands on the stack: * * PUSH 1 * PUSH 2 * MUL * * Top of stack is now 2. */ op_MUL() { this.infix('MUL', (a,b) => a * b); } /** * MODulus operation, expects 2 operands on the stack: * * PUSH 9 * PUSH 2 * MOD * * Top of stack is now 1. */ op_MOD() { this.infix('MOD', (a,b) => a % b); } /** * Takes the top of the stack off and returns it. * * @return { Number? } -- top of stack result. */ op_POP() { let val = this.stack.pop(); this.next(); return val; } /** * Pushes a value onto the stack at the top. If you do * op_PUSH(1) then op_POP() should return 1. */ op_PUSH(value) { this.stack.push(value); this.next(); } /** * Crashes Buttons and it catches on fire. */ op_HALT(message) { this.halted = true; this.error = message; } /** * Jumps to a given line number (starting at 0) then * continues execution there. * * @param { Integer } -- line number to jump to */ op_JUMP(line) { if(!this.assert(line !== undefined, `Invalid jump! You need to give a line number.`)) return; if(!this.assert(line <= this.code.length, `Invalid jump to line ${line} last line is ${this.code.length}`)) return; this.ip = line; } /** * JZ (Jump Zero) Jumps to the given line ONLY if the current stack top * is 0. If it's not 0 then this operation does nothing. * This implements branching as in: * * 0: PUSH 10 * 1: PUSH 1 * 2: SUB * 3: JZ 5 * 4: JUMP 1 * 5: HALT * * This will count down from 10, pushing each SUB onto the stack until the top is 0, then jump to line 5: HALT and exit. * * @param { Integer } -- line */ op_JZ(line) { if(!this.assert(line !== undefined, `Invalid jump! You need to give a line number.`)) return; if(!this.assert(line <= this.code.length, `Invalid jump to line ${line} last line is ${this.code.length}`)) return; if(this.stack_top == 0) { this.op_JUMP(line); } else { this.next(); } } /** * Exactly the same as JZ but it jumps if the stack top is * NOT zero. Thus Jump Not Zero. If the stack top IS zero * then the instruction does nothing. * * @param { Integer } -- line to jump to */ op_JNZ(line) { if(!this.assert(line !== undefined, `Invalid jump! You need to give a line number.`)) return; if(!this.assert(line <= this.code.length, `Invalid jump to line ${line} last line is ${this.code.length}`)) return; if(this.stack_top != 0) { this.op_JUMP(line); } else { this.next(); } } /** * CLeaRs the machine, kind of a soft reset. * It kills the stack, sets all registers to 0, and resets * line but *not* code. Use this to reset everything and * start running again. */ op_CLR() { Object.keys(this.registers). forEach(key => this.registers[key] = 0); // clears register this.stack.splice(0, this.stack.length); // clears the stack contents this.ip = 0; this.tick = 0; this.error = ''; this.error_line = 0; this.halted = false; } /** * Stores the current stack top in the requested register. * Registers in this fantasy computer are dynamic, so you * can change them by altering this.registers. By default * it starts with: * * IX -- Current memory index for PEEK/POKE. * AX -- Open use for anything. * BX -- Open use for anything. * CX -- Open use for anything. * DX -- Open use for anything. * * BUG: This does not *consume* the stack which is a * debatable design decision. * * @param { String } reg -- register name to store */ op_STOR(reg) { const reg_names = this.register_names(); if(!this.assert(reg_names.includes(reg), `Register "${reg}" is not valid. Use ${reg_names}`)) return; this.registers[reg] = this.stack_top; this.next(); } /** * The Reverse of STOR (thus RSTOR) it takes the * given register's value and pushes it onto the stack. * * See STOR for the list of registers. * * @param { String } reg -- register name */ op_RSTOR(reg) { const reg_names = this.register_names(); if(!this.assert(reg_names.includes(reg), `Register "${reg}" is not valid. Use ${reg_names}`)) return; let val = this.registers[reg]; this.assert(val !== undefined, `Invalid register ${reg} or register empty.`); this.stack.push(val); this.next(); } op_PEEK(inc) { let index = Math.abs(this.registers.IX) % this.ram.length; this.stack.push(this.ram[index]); if(inc) this.registers.IX = index + inc; this.next(); } op_POKE(inc) { let index = Math.abs(this.registers.IX) % this.ram.length; this.ram[index] = this.stack_top; if(inc) this.registers.IX = index + inc; this.next(); } /** * Takes the top 2 stack values and swaps them. This is * common in languages like FORTH where you might use just * 2 stack levels to perform a sequence of calculations. * */ op_SWAP() { if(!this.assert(this.stack.length >= 2, "Not enough elements on the stack to swap.")) return; let [top, n] = [this.stack.pop(), this.stack.pop()]; this.stack.push(top); this.stack.push(n); this.next(); } /** * Determines if the machine is running or not. * The interesting thing is ButtonsComputer has a * this.max_ticks which defaults to 256 and will * exit the machine if it processes more than 256 * instructions. This is done as part of a game * idea where you have to solve a puzzle in a limited * amount of ticks, but also to prevent running forever. * * @return { boolean } -- if it's running. */ get running() { return this.halted === false && this.tick < this.max_ticks && this.ip < this.code.length && this.cur_op !== undefined; } /** * Moves the instruction pointer up by 1, thus moving to * the next line of code. */ next() { this.ip++; } /* * Dumps debugging information, with a leading set of * text to help tracing. * * @param { string } leader -- Test displayed at the beginning of the line. */ dump(leader) { console.log(leader, "TICK", this.tick, "CUR", this.cur_op, "IP", this.ip, "STACK", this.stack); } /** * Process the next line of code. */ step() { if(this.running) { let [op, data] = this.cur_op; let op_func = this[`op_${op}`]; if(this.assert(op_func !== undefined, `Invalid operation ${op}`)) { op_func.call(this, data); this.tick++; } } } /** * Executes all lines of code until the end. You can * add a debug parameter to get dump() lines before/after * each line execution. * * @param { boolean } debug -- debug execution. */ run(debug=false) { while(this.running === true) { if(debug) this.dump(">>>"); this.step(); if(debug) this.dump("<<<"); // this.tick is managed by this.step } } load(code) { this.code = code; } /** * Coming soon. */ parse(code) { const lines = code.split("\n"); const result = lines.map(l => { let t = l.split(" "); let inst = t.slice(0,1); let data = t.slice(1).map(i => { let num = parseInt(i, 10); return isNaN(num) ? i : num; }); return inst.concat(data); }); // remove empty operations/lines return result.filter(t => t[0] !== ""); } } export default { ButtonMachine };