Browse Source

🎉 Initial Commit

main
Nichole Mattera 10 months ago
commit
1cf15d3062
  1. 79
      arch-spec
  2. BIN
      challenge.bin
  3. 333
      src/instruction.go
  4. 33
      src/main.go
  5. 57
      src/numbers.go
  6. 54
      src/opcode.go
  7. 31
      src/stack.go
  8. 56
      src/state.go

79
arch-spec

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

BIN
challenge.bin

Binary file not shown.

333
src/instruction.go

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

33
src/main.go

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

57
src/numbers.go

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

54
src/opcode.go

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

31
src/stack.go

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

56
src/state.go

@ -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…
Cancel
Save