|
|
|
@ -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 }; |
|
|
|
|