Document all of the functions in ButtonComputer.

master
Zed A. Shaw 3 years ago
parent 2902842fcc
commit 1c655b9b53
  1. 253
      src/buttons.js
  2. 4
      tests/basic_tests.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 { 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) { constructor(code) {
this.stack = []; this.stack = [];
this.ram = new Array(64).fill(0); this.ram = new Array(64).fill(0);
this.ip = 0; this.ip = 0;
this.code = code; this.code = code;
this.max_clicks = 256; this.max_ticks = 256;
this.tick = 0; this.tick = 0;
this.registers = {'IX': 0}; this.registers = {
'IX': 0, 'AX': 0, 'BX': 0,
'CX': 0, 'DX': 0
};
this.error = ''; this.error = '';
this.error_line = 0; this.error_line = 0;
this.halted = false; 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) { assert(test, message) {
// I should use exceptions but not sure if I want to do that too early in the course // I should use exceptions but not sure if I want to do that too early in the course
if(!test) { if(!test) {
@ -26,29 +60,58 @@ export class ButtonMachine {
return test; return test;
} }
/* Need to use a function because @babel/plugin-proposal-class-properties */ /**
static register_names() { * Returns this machine's availalbe register names.
return ['AX', 'BX', 'CX', 'DX', 'IX']; *
* @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() { static operations() {
return Object.getOwnPropertyNames(ButtonMachine.prototype) return Object.getOwnPropertyNames(ButtonMachine.prototype)
.filter(x => x.startsWith('op_')) .filter(x => x.startsWith('op_'))
.map(x => x.slice(3)); .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() { get stack_top() {
return this.stack[this.stack.length - 1]; 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() { get cur_op() {
return this.code[this.ip]; 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) { infix(op_name, cb) {
let b = this.stack.pop(); let b = this.stack.pop();
this.assert(b !== undefined, `${op_name} right operand POP empty stack`); this.assert(b !== undefined, `${op_name} right operand POP empty stack`);
@ -64,42 +127,108 @@ export class ButtonMachine {
this.next(); this.next();
} }
/**
* ADD operation, expects 2 operands on the stack:
*
* PUSH 1
* PUSH 2
* ADD
*
* Top of stack is now 3.
*/
op_ADD() { op_ADD() {
this.infix('ADD', (a,b) => a + b); 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() { op_SUB() {
this.infix('SUB', (a,b) => a - b); 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() { op_DIV() {
this.infix('DIV', (a,b) => a / b); 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() { op_MUL() {
this.infix('MUL', (a,b) => a * b); 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() { op_MOD() {
this.infix('MOD', (a,b) => a % b); 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() { op_POP() {
let val = this.stack.pop(); let val = this.stack.pop();
this.next(); this.next();
return val; 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) { op_PUSH(value) {
this.stack.push(value); this.stack.push(value);
this.next(); this.next();
} }
/**
* Crashes Buttons and it catches on fire.
*/
op_HALT(message) { op_HALT(message) {
this.halted = true; this.halted = true;
this.error = message; 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) { op_JUMP(line) {
if(!this.assert(line !== undefined, `Invalid jump! You need to give a line number.`)) return; 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.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; 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) { op_JZ(line) {
if(!this.assert(line !== undefined, `Invalid jump! You need to give a line number.`)) return; 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.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) { op_JNZ(line) {
if(!this.assert(line !== undefined, `Invalid jump! You need to give a line number.`)) return; 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.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() { 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.stack.splice(0, this.stack.length); // clears the stack contents
this.ip = 0; this.ip = 0;
@ -140,16 +299,41 @@ export class ButtonMachine {
this.halted = false; 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) { 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; if(!this.assert(reg_names.includes(reg), `Register "${reg}" is not valid. Use ${reg_names}`)) return;
this.registers[reg] = this.stack_top; this.registers[reg] = this.stack_top;
this.next(); 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) { 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; if(!this.assert(reg_names.includes(reg), `Register "${reg}" is not valid. Use ${reg_names}`)) return;
let val = this.registers[reg]; let val = this.registers[reg];
@ -173,6 +357,12 @@ export class ButtonMachine {
this.next(); 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() { op_SWAP() {
if(!this.assert(this.stack.length >= 2, "Not enough elements on the stack to swap.")) return; 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(); 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() { 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() { next() {
this.ip++; 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) { dump(leader) {
console.log(leader, "TICK", this.tick, "CUR", this.cur_op, "IP", this.ip, "STACK", this.stack); console.log(leader, "TICK", this.tick, "CUR", this.cur_op, "IP", this.ip, "STACK", this.stack);
} }
/**
* Process the next line of code.
*/
step() { step() {
if(this.running) { if(this.running) {
let [op, data] = this.cur_op; 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) { run(debug=false) {
while(this.running === true) { while(this.running === true) {
if(debug) this.dump(">>>"); if(debug) this.dump(">>>");
@ -212,6 +433,12 @@ export class ButtonMachine {
// this.tick is managed by this.step // this.tick is managed by this.step
} }
} }
/**
* Coming soon.
*/
static parse(code) {
}
} }
export default { ButtonMachine }; export default { ButtonMachine };

@ -15,11 +15,11 @@ let code = [
let machine = new ButtonMachine(code); let machine = new ButtonMachine(code);
const ops = ButtonMachine.operations(); const ops = ButtonMachine.operations();
const registers = ButtonMachine.register_names(); const registers = machine.register_names();
machine.run(); machine.run();
console.log("STACK TOP", machine.stack_top); console.log("STACK TOP", machine.stack_top);
console.log("REGISTER", machine.register_entries); console.log("REGISTERS", registers);
console.log("STACK", machine.stack); console.log("STACK", machine.stack);
console.log("RAM", machine.ram); console.log("RAM", machine.ram);

Loading…
Cancel
Save