Created
May 16, 2024 04:58
-
-
Save mendes5/99115912c4d14b81e531aa495b71fe87 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const REG = { | |
RAX: 0, | |
RBX: 1, | |
RCX: 2, | |
RDX: 3, | |
} as const; | |
const ADDRESSING_MODES = { | |
AUTO: 0b00, | |
REGISTER: 0b01, | |
IMMEDIATE: 0b10, | |
MEMORY: 0b11, | |
}; | |
const a = (mode = 'AAAA') => { | |
if (mode.length > 4) { | |
throw new Error('Invalid adressing mode shorthand'); | |
} | |
const modeNumber = [...mode].map((value, index) => { | |
if (value === 'A') return ADDRESSING_MODES.AUTO; | |
if (value === 'I') return ADDRESSING_MODES.IMMEDIATE; | |
if (value === 'R') return ADDRESSING_MODES.REGISTER; | |
if (value === 'M') return ADDRESSING_MODES.MEMORY; | |
throw new Error(`${value} at position ${index} is not a valid adressing mode shorthand`); | |
}); | |
return encodeAdressingModes(modeNumber); | |
} | |
const encodeAdressingModes = (arr: number[]) => { | |
let prefix = 0b00000000; | |
if (arr.length > 4) { | |
throw new Error('Cannot encode more than 4 operands adressing modes'); | |
} | |
let position = 0; | |
for (const mode of arr) { | |
prefix |= (mode << position++ * 2); | |
} | |
return prefix; | |
}; | |
const getAddressMode = (addressingMode: number, position: 0 | 1 | 2 | 3): number => { | |
return (addressingMode >> position * 2) & 0b00000011; | |
}; | |
const read = (computer: Computer, value: number, adressingMode: number, operand: 0 | 1 | 2 | 3) => { | |
const mode = getAddressMode(adressingMode, operand); | |
switch (mode) { | |
case ADDRESSING_MODES.AUTO: | |
throw new Error('Tried to read an adress on a instruction which considers the addressing mode AUTO for this operand, the read operation should be implemented on the instruction itself, or this instruction as no flexible adressing modes'); | |
case ADDRESSING_MODES.IMMEDIATE: | |
return value; | |
case ADDRESSING_MODES.MEMORY: | |
return computer.memory[value]; | |
case ADDRESSING_MODES.REGISTER: | |
return computer.registers[value]; | |
default: | |
throw new Error('Invalid addressing mode'); | |
} | |
}; | |
const write = (computer: Computer, data: number, value: number, adressingMode: number, operand: 0 | 1 | 2 | 3) => { | |
const mode = getAddressMode(adressingMode, operand); | |
switch (mode) { | |
case ADDRESSING_MODES.AUTO: | |
throw new Error('Tried to write to an adress on a instruction which considers the addressing mode AUTO for this operand, the read operation should be implemented on the instruction itself, or this instruction as no flexible adressing modes'); | |
case ADDRESSING_MODES.IMMEDIATE: | |
throw new Error('IMMEDIATE is not a valid adressing mode for a write operation'); | |
case ADDRESSING_MODES.MEMORY: | |
computer.memory[value] = data; | |
case ADDRESSING_MODES.REGISTER: | |
computer.registers[value] = data; | |
} | |
}; | |
let iota = 0; | |
const I = { | |
/** | |
* SRC, DST | |
*/ | |
LOAD: iota++, | |
/** | |
* SRC, DST | |
*/ | |
STORE: iota++, | |
/** | |
* L, R, OUT | |
*/ | |
ADD: iota++, | |
/** | |
* L, R, OUT | |
*/ | |
MULTIPLY: iota++, | |
/** | |
* L, R, OUT | |
*/ | |
SUB: iota++, | |
/** | |
* IN, OUT | |
*/ | |
NEGATE: iota++, | |
/** | |
* L, R, OUT | |
*/ | |
AND: iota++, | |
/** | |
* L, R, OUT | |
*/ | |
OR: iota++, | |
/** | |
* IN, OUT | |
*/ | |
NOT: iota++, | |
/** | |
* LOC | |
*/ | |
JMP: iota++, | |
/** | |
* COND, LOC | |
*/ | |
JMP_IF_ZERO: iota++, | |
/** | |
* | |
*/ | |
EXIT: 255, | |
} as const; | |
const NUM_ARGS = { | |
[I.LOAD]: 2, | |
[I.STORE]: 2, | |
[I.ADD]: 3, | |
[I.SUB]: 3, | |
[I.MULTIPLY]: 3, | |
[I.NEGATE]: 2, | |
[I.AND]: 3, | |
[I.OR]: 3, | |
[I.NOT]: 2, | |
[I.JMP]: 1, | |
[I.JMP_IF_ZERO]: 2, | |
[I.EXIT]: 0, | |
}; | |
const NAMES = { | |
[I.LOAD]: 'LOAD', | |
[I.STORE]: 'STORE', | |
[I.ADD]: 'ADD', | |
[I.SUB]: 'SUB', | |
[I.MULTIPLY]: 'MULTIPLY', | |
[I.NEGATE]: 'NEGATE', | |
[I.AND]: 'AND', | |
[I.OR]: 'OR', | |
[I.NOT]: 'NOT', | |
[I.JMP]: 'JMP', | |
[I.JMP_IF_ZERO]: 'JMP_IF_ZERO', | |
[I.EXIT]: 'EXIT', | |
}; | |
const NEEDS_ADDRESSING_MODE_BYTE = { | |
[I.LOAD]: false, | |
[I.STORE]: false, | |
[I.ADD]: true, | |
[I.SUB]: true, | |
[I.MULTIPLY]: true, | |
[I.NEGATE]: true, | |
[I.AND]: true, | |
[I.OR]: true, | |
[I.NOT]: true, | |
[I.JMP]: true, | |
[I.JMP_IF_ZERO]: true, | |
[I.EXIT]: false, | |
}; | |
type Computer = { | |
programCounter: number; | |
memory: number[]; | |
registers: number[]; | |
} | |
/** | |
* Fetches the current instruction | |
*/ | |
const cpuFetch = (computer: Computer): [number, number] => { | |
const instruction = computer.memory[computer.programCounter]; | |
computer.programCounter += 1; | |
if (NEEDS_ADDRESSING_MODE_BYTE[instruction]) { | |
const adressingMode = computer.memory[computer.programCounter]; | |
computer.programCounter += 1; | |
return [instruction, adressingMode]; | |
} | |
return [instruction, 0]; | |
}; | |
/** | |
* Decodes the current instruction while | |
* also incrementing the program counter | |
*/ | |
const cpuDecode = (computer: Computer, instruction: number): number[] => { | |
const num_arguments = NUM_ARGS[instruction]; | |
const args = computer.memory.slice(computer.programCounter, computer.programCounter + num_arguments); | |
computer.programCounter += num_arguments; | |
return args; | |
}; | |
// Full auto | |
const DEFAULT_ADDRESS_MODE = | |
ADDRESSING_MODES.AUTO & | |
(ADDRESSING_MODES.AUTO << 2) & | |
(ADDRESSING_MODES.AUTO << 4) & | |
(ADDRESSING_MODES.AUTO << 6); | |
/** | |
* Executes the given instruction | |
*/ | |
const cpuExecute = (computer: Computer, instruction: number, operands: number[], adressMode: number) => { | |
if (instruction === I.LOAD) { | |
const [srcMem, dstReg] = operands; | |
computer.registers[dstReg] = computer.memory[srcMem] | |
} else if (instruction === I.STORE) { | |
const [srcReg, dstMem] = operands; | |
computer.memory[dstMem] = computer.registers[srcReg] | |
} else if (instruction === I.ADD) { | |
const [lReg, rReg, outReg] = operands; | |
const result = read(computer, lReg, adressMode, 0) + read(computer, rReg, adressMode, 1); | |
write(computer, result, outReg, adressMode, 2); | |
} else if (instruction === I.MULTIPLY) { | |
const [lReg, rReg, outReg] = operands; | |
const result = read(computer, lReg, adressMode, 0) * read(computer, rReg, adressMode, 1); | |
write(computer, result, outReg, adressMode, 2); | |
} else if (instruction === I.NEGATE) { | |
const [inReg, outReg] = operands; | |
const result = -read(computer, inReg, adressMode, 0); | |
write(computer, result, outReg, adressMode, 1); | |
} else if (instruction === I.AND) { | |
const [lReg, rReg, outReg] = operands; | |
const result = read(computer, lReg, adressMode, 0) & read(computer, rReg, adressMode, 1); | |
write(computer, result, outReg, adressMode, 2); | |
} else if (instruction === I.SUB) { | |
const [lReg, rReg, outReg] = operands; | |
const result = read(computer, lReg, adressMode, 0) - read(computer, rReg, adressMode, 1); | |
write(computer, result, outReg, adressMode, 2); | |
} else if (instruction === I.OR) { | |
const [lReg, rReg, outReg] = operands; | |
const result = read(computer, lReg, adressMode, 0) | read(computer, rReg, adressMode, 1); | |
write(computer, result, outReg, adressMode, 2); | |
} else if (instruction === I.NOT) { | |
const [inReg, outReg] = operands; | |
const result = ~read(computer, inReg, adressMode, 0); | |
write(computer, result, outReg, adressMode, 1); | |
} else if (instruction === I.JMP) { | |
const [locReg] = operands; | |
computer.programCounter = read(computer, locReg, adressMode, 0); | |
} else if (instruction === I.JMP_IF_ZERO) { | |
const [condReg, locReg] = operands; | |
if (read(computer, condReg, adressMode, 0) === 0) { | |
computer.programCounter = read(computer, locReg, adressMode, 1); | |
} | |
} else if (instruction === I.EXIT) { | |
return false; | |
} else { | |
throw new Error(`Invalid op_code: ${instruction}`); | |
} | |
return true; | |
} | |
const debug = false; | |
/** | |
* Does a full cpu cicle. | |
* @returns a boolean indicating whenever we should keep executing. | |
*/ | |
const cpuCycle = (computer: Computer) => { | |
const [instruction, addressMode] = cpuFetch(computer); | |
if (debug) { | |
console.log(`@${computer.programCounter} ${NAMES[instruction]}`); | |
} | |
const operands = cpuDecode(computer, instruction); | |
return cpuExecute(computer, instruction, operands, addressMode); | |
} | |
/** | |
* Executes a program | |
* @param tickCallback optional tick | |
*/ | |
const runComputer = (computer: Computer, tickCallback?: () => void) => { | |
while (cpuCycle(computer)) tickCallback?.(); | |
} | |
/** | |
* Just to better illustrate | |
* that those are computer.memory adresses | |
*/ | |
const MEM = <T>(i: T) => i; | |
/** | |
* Sample loop increment a number from 0 to 1_000 | |
```c | |
void main() { | |
int state = 0; | |
int limit = 1000; | |
while (state != limit) { | |
state++ | |
} | |
} | |
``` | |
But a more direct translation | |
of the bytecode is this: | |
```c | |
void main() { | |
int state = 0; | |
int increment = 1; | |
int limit = 1000; | |
prog: | |
int a = state; | |
int b = increment; | |
int c = a + b; | |
state = c; | |
int difference = state - limit; | |
if (difference === 0) | |
goto end; | |
else | |
goto prog; | |
end: | |
return; | |
} | |
``` | |
*/ | |
const PROGRAM = [ | |
I.LOAD, MEM(132), REG.RDX, | |
I.LOAD, MEM(130), REG.RAX, | |
I.LOAD, MEM(131), REG.RBX, | |
I.ADD, a('RRR'), REG.RAX, REG.RBX, REG.RCX, | |
I.STORE, REG.RCX, MEM(130), | |
I.SUB, a('RRR'), REG.RCX, REG.RDX, REG.RAX, | |
I.LOAD, MEM(133), REG.RBX, | |
I.JMP_IF_ZERO, a('RR'), REG.RAX, REG.RBX, | |
I.LOAD, MEM(134), REG.RBX, | |
I.JMP, a('R'), REG.RBX, | |
I.EXIT, | |
] as const; | |
/** | |
* How to write computer programs: | |
* * Supported instructions are in the `I` constant | |
* * You can manually look at `NEEDS_ADDRESSING_MODE_BYTE` to check if you need to specify the adressing modes of the operands | |
* - If you need you use the `a` function to specify, you put a string in the order of the arguments, so for example | |
* - `I.LOAD, a('IR'), 100, REG.EAX,` the `IR` stands for: "First operand immediate, second operand register" | |
* - Available modes are `A`: Auto, `I`: Immediate, `R`: Register, `M`: Memory | |
* - If you don't need just skip the adressing byte | |
* * Then you can use either `REG` to specify registers as operands, or `MEM` to specify memory locations | |
*/ | |
const computer: Computer = { | |
programCounter: 0, | |
memory: new Array(250).fill(0), | |
registers: new Array(4).fill(0), | |
}; | |
// usually this goes before the actual program | |
computer.memory[130] = 0; // state | |
computer.memory[131] = 1; // constant 1 | |
computer.memory[132] = 1000; // constant 1000 | |
computer.memory[133] = 35; // location of end of program | |
computer.memory[134] = 0; // location of start of program | |
computer.memory.splice(0, PROGRAM.length, ...PROGRAM); | |
runComputer(computer); | |
// Result should be stored here: | |
console.log(computer.memory[130]); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Based of https://gist.github.com/mikex86/36e6eeef5a5c53de9792ec224e25c45c | |
Just converted to typescript, overenginered adding adressing modes, and made a program that counts from 0 to 1000 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment