From 1c655b9b53b0cb17a6005d7b669f45a6ec064e26 Mon Sep 17 00:00:00 2001 From: "Zed A. Shaw" Date: Mon, 18 Apr 2022 23:02:52 -0400 Subject: [PATCH] Document all of the functions in ButtonComputer. --- src/buttons.js | 253 ++++++++++++++++++++++++++++++++++++++++--- tests/basic_tests.js | 4 +- 2 files changed, 242 insertions(+), 15 deletions(-) diff --git a/src/buttons.js b/src/buttons.js index 1640178..70c34e3 100644 --- a/src/buttons.js +++ b/src/buttons.js @@ -1,18 +1,52 @@ +/** + * 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(code) { this.stack = []; this.ram = new Array(64).fill(0); this.ip = 0; this.code = code; - this.max_clicks = 256; + this.max_ticks = 256; this.tick = 0; - this.registers = {'IX': 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) { @@ -26,29 +60,58 @@ export class ButtonMachine { return test; } - /* Need to use a function because @babel/plugin-proposal-class-properties */ - static register_names() { - return ['AX', 'BX', 'CX', 'DX', 'IX']; + /** + * Returns this machine's availalbe register names. + * + * @return { Array[String] } -- List of registers. + */ + register_names() { + return Object.entries(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]; } - get register_entries() { - return Object.entries(this.registers); - } - + /** + * 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`); @@ -64,42 +127,108 @@ export class ButtonMachine { 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; @@ -107,6 +236,22 @@ export class ButtonMachine { 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; @@ -118,6 +263,13 @@ export class ButtonMachine { } } + /** + * 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; @@ -129,8 +281,15 @@ export class ButtonMachine { } } + /** + * 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 => delete this.registers[key]); // clears register + 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; @@ -140,16 +299,41 @@ export class ButtonMachine { 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 = ButtonMachine.register_names(); + 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 = ButtonMachine.register_names(); + 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]; @@ -173,6 +357,12 @@ export class ButtonMachine { 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; @@ -182,18 +372,42 @@ export class ButtonMachine { 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_clicks && this.ip < this.code.length && this.cur_op !== undefined; + 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; @@ -204,6 +418,13 @@ export class ButtonMachine { } } + /** + * 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(">>>"); @@ -212,6 +433,12 @@ export class ButtonMachine { // this.tick is managed by this.step } } + + /** + * Coming soon. + */ + static parse(code) { + } } export default { ButtonMachine }; diff --git a/tests/basic_tests.js b/tests/basic_tests.js index 3b4a988..b0c44e2 100644 --- a/tests/basic_tests.js +++ b/tests/basic_tests.js @@ -15,11 +15,11 @@ let code = [ let machine = new ButtonMachine(code); const ops = ButtonMachine.operations(); -const registers = ButtonMachine.register_names(); +const registers = machine.register_names(); machine.run(); console.log("STACK TOP", machine.stack_top); -console.log("REGISTER", machine.register_entries); +console.log("REGISTERS", registers); console.log("STACK", machine.stack); console.log("RAM", machine.ram);