|
|
|
/**
|
|
|
|
* 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_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.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];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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}`];
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Coming soon.
|
|
|
|
*/
|
|
|
|
static parse(code) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export default { ButtonMachine };
|