commit
1cf15d3062
@ -0,0 +1,79 @@ |
||||
== Synacor Challenge == |
||||
In this challenge, your job is to use this architecture spec to create a |
||||
virtual machine capable of running the included binary. Along the way, |
||||
you will find codes; submit these to the challenge website to track |
||||
your progress. Good luck! |
||||
|
||||
|
||||
== architecture == |
||||
- three storage regions |
||||
- memory with 15-bit address space storing 16-bit values |
||||
- eight registers |
||||
- an unbounded stack which holds individual 16-bit values |
||||
- all numbers are unsigned integers 0..32767 (15-bit) |
||||
- all math is modulo 32768; 32758 + 15 => 5 |
||||
|
||||
== binary format == |
||||
- each number is stored as a 16-bit little-endian pair (low byte, high byte) |
||||
- numbers 0..32767 mean a literal value |
||||
- numbers 32768..32775 instead mean registers 0..7 |
||||
- numbers 32776..65535 are invalid |
||||
- programs are loaded into memory starting at address 0 |
||||
- address 0 is the first 16-bit value, address 1 is the second 16-bit value, etc |
||||
|
||||
== execution == |
||||
- After an operation is executed, the next instruction to read is immediately after the last argument of the current operation. If a jump was performed, the next operation is instead the exact destination of the jump. |
||||
- Encountering a register as an operation argument should be taken as reading from the register or setting into the register as appropriate. |
||||
|
||||
== hints == |
||||
- Start with operations 0, 19, and 21. |
||||
- Here's a code for the challenge website: nZEQuhtfeqcb |
||||
- The program "9,32768,32769,4,19,32768" occupies six memory addresses and should: |
||||
- Store into register 0 the sum of 4 and the value contained in register 1. |
||||
- Output to the terminal the character with the ascii code contained in register 0. |
||||
|
||||
== opcode listing == |
||||
halt: 0 |
||||
stop execution and terminate the program |
||||
set: 1 a b |
||||
set register <a> to the value of <b> |
||||
push: 2 a |
||||
push <a> onto the stack |
||||
pop: 3 a |
||||
remove the top element from the stack and write it into <a>; empty stack = error |
||||
eq: 4 a b c |
||||
set <a> to 1 if <b> is equal to <c>; set it to 0 otherwise |
||||
gt: 5 a b c |
||||
set <a> to 1 if <b> is greater than <c>; set it to 0 otherwise |
||||
jmp: 6 a |
||||
jump to <a> |
||||
jt: 7 a b |
||||
if <a> is nonzero, jump to <b> |
||||
jf: 8 a b |
||||
if <a> is zero, jump to <b> |
||||
add: 9 a b c |
||||
assign into <a> the sum of <b> and <c> (modulo 32768) |
||||
mult: 10 a b c |
||||
store into <a> the product of <b> and <c> (modulo 32768) |
||||
mod: 11 a b c |
||||
store into <a> the remainder of <b> divided by <c> |
||||
and: 12 a b c |
||||
stores into <a> the bitwise and of <b> and <c> |
||||
or: 13 a b c |
||||
stores into <a> the bitwise or of <b> and <c> |
||||
not: 14 a b |
||||
stores 15-bit bitwise inverse of <b> in <a> |
||||
rmem: 15 a b |
||||
read memory at address <b> and write it to <a> |
||||
wmem: 16 a b |
||||
write the value from <b> into memory at address <a> |
||||
call: 17 a |
||||
write the address of the next instruction to the stack and jump to <a> |
||||
ret: 18 |
||||
remove the top element from the stack and jump to it; empty stack = halt |
||||
out: 19 a |
||||
write the character represented by ascii code <a> to the terminal |
||||
in: 20 a |
||||
read a character from the terminal and write its ascii code to <a>; it can be assumed that once input starts, it will continue until a newline is encountered; this means that you can safely read whole lines from the keyboard and trust that they will be fully read |
||||
noop: 21 |
||||
no operation |
Binary file not shown.
@ -0,0 +1,333 @@ |
||||
package main |
||||
|
||||
import ( |
||||
"errors" |
||||
"fmt" |
||||
"github.com/eiannone/keyboard" |
||||
) |
||||
|
||||
type Instruction struct { |
||||
opcode Opcode |
||||
arguments []Number |
||||
} |
||||
|
||||
func fetchInstruction(state *State) Instruction { |
||||
if state.programCounter > uint16(32767) { |
||||
return Instruction { |
||||
opcode: Halt, |
||||
arguments: []Number{}, |
||||
} |
||||
} |
||||
|
||||
instruction := Instruction { |
||||
opcode: Opcode(state.memory[state.programCounter]), |
||||
arguments: []Number{}, |
||||
} |
||||
programCounterIncrement := uint16(2) |
||||
|
||||
switch instruction.opcode { |
||||
case EqualTo: |
||||
fallthrough |
||||
case GreaterThan: |
||||
fallthrough |
||||
case Add: |
||||
fallthrough |
||||
case Multiply: |
||||
fallthrough |
||||
case Modulous: |
||||
fallthrough |
||||
case And: |
||||
fallthrough |
||||
case Or:
|
||||
instruction.arguments = append([]Number { |
||||
parseNumber(state.memory[state.programCounter + 6:state.programCounter + 8], state), |
||||
}, instruction.arguments...) |
||||
programCounterIncrement += uint16(2) |
||||
fallthrough
|
||||
|
||||
case Set: |
||||
fallthrough
|
||||
case JumpToIfNotZero: |
||||
fallthrough
|
||||
case JumpToIfZero: |
||||
fallthrough
|
||||
case Not: |
||||
fallthrough
|
||||
case ReadMemory: |
||||
fallthrough
|
||||
case WriteMemory: |
||||
instruction.arguments = append([]Number { |
||||
parseNumber(state.memory[state.programCounter + 4:state.programCounter + 6], state), |
||||
}, instruction.arguments...) |
||||
programCounterIncrement += uint16(2) |
||||
fallthrough
|
||||
|
||||
case Push: |
||||
fallthrough
|
||||
case Pop: |
||||
fallthrough
|
||||
case JumpTo: |
||||
fallthrough
|
||||
case Call: |
||||
fallthrough
|
||||
case Output: |
||||
fallthrough
|
||||
case Input: |
||||
instruction.arguments = append([]Number { |
||||
parseNumber(state.memory[state.programCounter + 2:state.programCounter + 4], state), |
||||
}, instruction.arguments...) |
||||
programCounterIncrement += uint16(2) |
||||
break
|
||||
} |
||||
|
||||
state.programCounter += programCounterIncrement |
||||
|
||||
return instruction |
||||
} |
||||
|
||||
func executeInstruction(state *State, instruction Instruction) { |
||||
switch instruction.opcode { |
||||
// HALT
|
||||
// Stop execution and terminate the program.
|
||||
case Halt: |
||||
state.shouldHalt = true |
||||
break |
||||
|
||||
// SET a b
|
||||
// set register <a> to the value of <b>.
|
||||
case Set: |
||||
a := instruction.arguments[0] |
||||
b := instruction.arguments[1] |
||||
|
||||
if a.isRegister { |
||||
state.registers[a.registerIndex] = b.value |
||||
} |
||||
break |
||||
|
||||
// PUSH a
|
||||
// Push <a> onto the stack.
|
||||
case Push: |
||||
a := instruction.arguments[0] |
||||
|
||||
state.stack.push(a.value) |
||||
break |
||||
|
||||
// POP a
|
||||
// Remove the top element from the stack and write it into <a>; empty stack = error.
|
||||
case Pop: |
||||
a := instruction.arguments[0] |
||||
|
||||
if a.isRegister && state.stack.length() != 0 { |
||||
state.registers[a.registerIndex] = state.stack.pop().value |
||||
} |
||||
break |
||||
|
||||
// EQ
|
||||
// Set <a> to 1 if <b> is equal to <c>; set it to 0 otherwise.
|
||||
case EqualTo: |
||||
a := instruction.arguments[0] |
||||
b := instruction.arguments[1] |
||||
c := instruction.arguments[2] |
||||
|
||||
if a.isRegister { |
||||
if b.value == c.value { |
||||
state.registers[a.registerIndex] = 1 |
||||
} else { |
||||
state.registers[a.registerIndex] = 0 |
||||
} |
||||
} |
||||
break |
||||
|
||||
// GT a b c
|
||||
// Set <a> to 1 if <b> is greater than <c>; set it to 0 otherwise.
|
||||
case GreaterThan: |
||||
a := instruction.arguments[0] |
||||
b := instruction.arguments[1] |
||||
c := instruction.arguments[2] |
||||
|
||||
if a.isRegister { |
||||
if b.value > c.value { |
||||
state.registers[a.registerIndex] = 1 |
||||
} else { |
||||
state.registers[a.registerIndex] = 0 |
||||
} |
||||
} |
||||
break |
||||
|
||||
// JMP a
|
||||
// Jump to <a>.
|
||||
case JumpTo: |
||||
a := instruction.arguments[0] |
||||
|
||||
state.programCounter = a.toAddress() |
||||
break |
||||
|
||||
// JT a b
|
||||
// If <a> is nonzero, jump to <b>.
|
||||
case JumpToIfNotZero: |
||||
a := instruction.arguments[0] |
||||
b := instruction.arguments[1] |
||||
|
||||
if a.value != 0 { |
||||
state.programCounter = b.toAddress() |
||||
}
|
||||
break |
||||
|
||||
// JF a b
|
||||
// If <a> is zero, jump to <b>.
|
||||
case JumpToIfZero: |
||||
a := instruction.arguments[0] |
||||
b := instruction.arguments[1] |
||||
|
||||
if a.value == 0 { |
||||
state.programCounter = b.toAddress() |
||||
}
|
||||
break |
||||
|
||||
// ADD a b c
|
||||
// Assign into <a> the sum of <b> and <c>. (Modulo 32768)
|
||||
case Add: |
||||
a := instruction.arguments[0] |
||||
b := instruction.arguments[1] |
||||
c := instruction.arguments[2] |
||||
|
||||
if a.isRegister { |
||||
state.registers[a.registerIndex] = (b.value + c.value) % 32768 |
||||
} |
||||
break |
||||
|
||||
// MULT a b c
|
||||
// Store into <a> the product of <b> and <c>. (Modulo 32768)
|
||||
case Multiply: |
||||
a := instruction.arguments[0] |
||||
b := instruction.arguments[1] |
||||
c := instruction.arguments[2] |
||||
|
||||
if a.isRegister { |
||||
state.registers[a.registerIndex] = (b.value * c.value) % 32768 |
||||
} |
||||
break |
||||
|
||||
// MOD a b c
|
||||
// Store into <a> the remainder of <b> divided by <c>.
|
||||
case Modulous: |
||||
a := instruction.arguments[0] |
||||
b := instruction.arguments[1] |
||||
c := instruction.arguments[2] |
||||
|
||||
if a.isRegister { |
||||
state.registers[a.registerIndex] = b.value % c.value |
||||
} |
||||
break |
||||
|
||||
// AND a b c
|
||||
// Stores into <a> the bitwise and of <b> and <c>.
|
||||
case And: |
||||
a := instruction.arguments[0] |
||||
b := instruction.arguments[1] |
||||
c := instruction.arguments[2] |
||||
|
||||
if a.isRegister { |
||||
state.registers[a.registerIndex] = b.value & c.value |
||||
} |
||||
break |
||||
|
||||
// OR a b c
|
||||
// Stores into <a> the bitwise or of <b> and <c>.
|
||||
case Or: |
||||
a := instruction.arguments[0] |
||||
b := instruction.arguments[1] |
||||
c := instruction.arguments[2] |
||||
|
||||
if a.isRegister { |
||||
state.registers[a.registerIndex] = b.value | c.value |
||||
} |
||||
break |
||||
|
||||
// NOT a b
|
||||
// Stores 15-bit bitwise inverse of <b> in <a>.
|
||||
case Not: |
||||
a := instruction.arguments[0] |
||||
b := instruction.arguments[1] |
||||
|
||||
if a.isRegister { |
||||
state.registers[a.registerIndex] = 0x7FFF ^ b.value |
||||
} |
||||
break |
||||
|
||||
// RMEM a b
|
||||
// Read memory at address <b> and write it to <a>.
|
||||
case ReadMemory: |
||||
a := instruction.arguments[0] |
||||
b := instruction.arguments[1] |
||||
|
||||
if a.isRegister { |
||||
address := b.toAddress() |
||||
state.registers[a.registerIndex] = parseNumber(state.memory[address:address + 2], state).value |
||||
} |
||||
break |
||||
|
||||
// WMEM a b
|
||||
// Write the value from <b> into memory at address <a>.
|
||||
case WriteMemory: |
||||
a := instruction.arguments[0] |
||||
b := instruction.arguments[1] |
||||
|
||||
address := a.toAddress() |
||||
bytes := b.toBytes() |
||||
state.memory[address] = bytes[0] |
||||
state.memory[address + 1] = bytes[1] |
||||
break |
||||
|
||||
// CALL a
|
||||
// Write the address of the next instruction to the stack and jump to <a>.
|
||||
case Call: |
||||
a := instruction.arguments[0] |
||||
|
||||
state.stack.push(state.programCounter / 2) |
||||
state.programCounter = a.toAddress() |
||||
break |
||||
|
||||
// RET
|
||||
// Remove the top element from the stack and jump to it; empty stack = halt.
|
||||
case Return: |
||||
if state.stack.length() == 0 { |
||||
state.shouldHalt = true |
||||
} else { |
||||
state.programCounter = state.stack.pop().toAddress() |
||||
} |
||||
break |
||||
|
||||
// OUT a
|
||||
// Write the character represented by ascii code <a> to the terminal.
|
||||
case Output: |
||||
fmt.Print(string(instruction.arguments[0].value)) |
||||
break |
||||
|
||||
// IN a
|
||||
// Read a character from the terminal and write its ascii code to <a>; it can be assumed that once input starts, it will continue until a newline is encountered; this means that you can safely read whole lines from the keyboard and trust that they will be fully read.
|
||||
case Input: |
||||
a := instruction.arguments[0] |
||||
|
||||
char, key, _ := keyboard.GetSingleKey() |
||||
fmt.Print(string(char)) |
||||
|
||||
if key == keyboard.KeyEnter { |
||||
state.registers[a.registerIndex] = 0x0A |
||||
} else if key == keyboard.KeySpace { |
||||
state.registers[a.registerIndex] = 0x20 |
||||
} else { |
||||
state.registers[a.registerIndex] = uint16(char) |
||||
} |
||||
break |
||||
|
||||
// NOOP
|
||||
// No operation.
|
||||
case NoOperation: |
||||
break |
||||
|
||||
default: |
||||
state.emulationError = errors.New("Error: Invalid opcode!") |
||||
break |
||||
} |
||||
} |
@ -0,0 +1,33 @@ |
||||
package main |
||||
|
||||
import ( |
||||
"fmt" |
||||
"os" |
||||
) |
||||
|
||||
func main() { |
||||
if len(os.Args[1:]) == 0 { |
||||
fmt.Println("./emu rom_path") |
||||
return |
||||
} |
||||
|
||||
state := setupState(os.Args) |
||||
for !state.shouldHalt && state.emulationError == nil { |
||||
emulationCycle(&state) |
||||
} |
||||
|
||||
if state.emulationError != nil { |
||||
fmt.Print(state.emulationError) |
||||
} |
||||
} |
||||
|
||||
func emulationCycle(state *State) { |
||||
instruction := fetchInstruction(state) |
||||
if (state.debug && instruction.opcode != Output) { |
||||
fmt.Println(instruction.opcode, instruction.arguments) |
||||
fmt.Println("Registers: ", state.registers) |
||||
fmt.Println("Stack: ", state.stack) |
||||
fmt.Println() |
||||
} |
||||
executeInstruction(state, instruction) |
||||
} |
@ -0,0 +1,57 @@ |
||||
package main |
||||
|
||||
import ( |
||||
"encoding/binary" |
||||
"errors" |
||||
"fmt" |
||||
) |
||||
|
||||
type Number struct { |
||||
isRegister bool |
||||
value uint16 |
||||
registerIndex uint16 |
||||
} |
||||
|
||||
func (n Number) String() string { |
||||
if n.isRegister { |
||||
return fmt.Sprintf("Register #%d - %d", n.registerIndex, n.value) |
||||
} else { |
||||
return fmt.Sprintf("%d", n.value) |
||||
} |
||||
} |
||||
|
||||
func parseNumber(memory []byte, state *State) Number { |
||||
value := binary.LittleEndian.Uint16(memory) |
||||
|
||||
if value <= 32767 { |
||||
return Number { |
||||
isRegister: false, |
||||
value: value, |
||||
registerIndex: 0, |
||||
} |
||||
} else if value >= 32768 && value <= 32775 { |
||||
index := value - 32768 |
||||
return Number { |
||||
isRegister: true, |
||||
value: state.registers[index], |
||||
registerIndex: index, |
||||
} |
||||
} else { |
||||
state.emulationError = errors.New("Error: Invalid Number") |
||||
return Number { |
||||
isRegister: false, |
||||
value: 0, |
||||
registerIndex: 0, |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (n Number) toAddress() uint16 { |
||||
return n.value * 2 |
||||
} |
||||
|
||||
func (n Number) toBytes() []byte { |
||||
bytes := make([]byte, 2) |
||||
binary.LittleEndian.PutUint16(bytes, n.value) |
||||
return bytes |
||||
} |
@ -0,0 +1,54 @@ |
||||
package main |
||||
|
||||
type Opcode int |
||||
const ( |
||||
Halt Opcode = iota |
||||
Set |
||||
Push |
||||
Pop |
||||
EqualTo |
||||
GreaterThan |
||||
JumpTo |
||||
JumpToIfNotZero |
||||
JumpToIfZero |
||||
Add |
||||
Multiply |
||||
Modulous |
||||
And |
||||
Or |
||||
Not |
||||
ReadMemory |
||||
WriteMemory |
||||
Call |
||||
Return |
||||
Output |
||||
Input |
||||
NoOperation |
||||
) |
||||
|
||||
func (o Opcode) String() string { |
||||
return [...]string { |
||||
"HALT", |
||||
"SET", |
||||
"PUSH", |
||||
"POP", |
||||
"EQ", |
||||
"GT", |
||||
"JMP", |
||||
"JT", |
||||
"JF", |
||||
"ADD", |
||||
"MULT", |
||||
"MOD", |
||||
"AND", |
||||
"OR", |
||||
"NOT", |
||||
"RMEM", |
||||
"WMEM", |
||||
"CALL", |
||||
"RET", |
||||
"OUT", |
||||
"IN", |
||||
"NOOP", |
||||
}[o] |
||||
} |
@ -0,0 +1,31 @@ |
||||
package main |
||||
|
||||
import ( |
||||
"fmt" |
||||
) |
||||
|
||||
type Stack struct { |
||||
stack []uint16 |
||||
} |
||||
|
||||
func (s Stack) String() string { |
||||
return fmt.Sprintf("%v", s.stack) |
||||
} |
||||
|
||||
func (s *Stack) push(data uint16) { |
||||
s.stack = append([]uint16{ data }, s.stack...) |
||||
} |
||||
|
||||
func (s *Stack) pop() Number { |
||||
data := Number { |
||||
isRegister: false, |
||||
value: s.stack[0], |
||||
registerIndex: 0, |
||||
} |
||||
s.stack = s.stack[1:] |
||||
return data |
||||
} |
||||
|
||||
func (s Stack) length() uint16 { |
||||
return uint16(len(s.stack)) |
||||
} |
@ -0,0 +1,56 @@ |
||||
package main |
||||
|
||||
import ( |
||||
"errors" |
||||
"io/ioutil" |
||||
"os" |
||||
) |
||||
|
||||
type State struct { |
||||
debug bool |
||||
emulationError error |
||||
memory []byte |
||||
programCounter uint16 |
||||
registers [8]uint16 |
||||
shouldHalt bool |
||||
stack Stack |
||||
} |
||||
|
||||
func setupState(args []string) State { |
||||
state := State { |
||||
debug: false, |
||||
emulationError: nil, |
||||
programCounter: 0, |
||||
shouldHalt: false, |
||||
stack: Stack { |
||||
stack: []uint16{}, |
||||
}, |
||||
} |
||||
|
||||
romPath := "" |
||||
for _, arg := range os.Args[1:] { |
||||
if arg == "-d" { |
||||
state.debug = true |
||||
} else if _, err := os.Stat(arg); !os.IsNotExist(err) { |
||||
romPath = arg |
||||
} |
||||
} |
||||
|
||||
if romPath == "" { |
||||
state.emulationError = errors.New("Error: Rom does not exists.") |
||||
state.shouldHalt = true |
||||
return state |
||||
} |
||||
|
||||
rom, err := ioutil.ReadFile(romPath) |
||||
if err != nil { |
||||
state.emulationError = errors.New("Error: Unable to load rom.") |
||||
state.shouldHalt = true |
||||
return state |
||||
} |
||||
|
||||
freeMemory := make([]byte, 0xFFFE - len(rom)) |
||||
state.memory = append(rom, freeMemory...) |
||||
|
||||
return state |
||||
} |
Loading…
Reference in new issue