Code for the littler Buttons the Computer used in the Turing Machine portion of the book.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
buttons-computer/src/buttons.js

465 lines
12 KiB

/**
* 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 };