use "collections" use "files" use "format" interface Value """ Values referred to by opcodes in instructions should all be things that implement this interface """ fun value(): I64 class Position """ A positional operand (i.e. one whose value is fetched from a memory cell """ let _addr: I64 let _value: I64 new create(__addr: I64, __value: I64) => _addr = __addr _value = __value fun addr(): I64 => _addr fun value(): I64 => _value class Immediate """ An immediate operand (i.e. one whose value is a verbatim argument) """ let _value: I64 new create(__value: I64) => _value = __value fun value(): I64 => _value class Operands """ The operands to a typical binary operator: two values and a target that will always be in positional (i.e. non-immediate) mode """ let _lhs: Value let _rhs: Value let _tgt: I64 new create(__lhs: Value, __rhs: Value, __tgt: I64) => _lhs = __lhs _rhs = __rhs _tgt = __tgt fun val1(): I64 => _lhs.value() fun val2(): I64 => _rhs.value() fun tgt(): USize => _tgt.usize() trait Error """ The errors we can raise will all implement this so we can give nice informative error messages """ fun message(): String class val OutOfBounds is Error let _loc: USize new val create(loc: USize) => _loc = loc fun message(): String => "Memory access out of bounds: " + Format.int[USize](_loc) class val UnknownOperand is Error let _op: I64 new val create(op: I64) => _op = op fun message(): String => "Unknown operation: " + Format.int[I64](_op) class val NotEnoughInput is Error let _pc: USize new val create(pc: USize) => _pc = pc fun message(): String => "Not enough input when running instruction at " + Format.int[USize](_pc) // I should wrap this but I haven't, so, uh, yeah type Opcode is I64 // a Mode is true if immediate, false if positional type Modes is (Bool, Bool, Bool) class Program """ All the logic we need for running programs """ // an array of memory cells var storage: Array[I64] // the current instruction we're running var pc: USize = 0 // a list of errors that may or may not have happened. (right now, // we'll only ever have one, but it's worth planning ahead, eh?) var errors: Array[Error val] = Array[Error val]() // the queue of input values from the user var input: Array[I64] // the queue of lines we intend to output var output: Array[I64] = Array[I64]() // a queue of debug messages var debug: Array[String] = Array[String]() new create(s: Array[I64], i: Array[I64] = Array[I64]()) => """ Create a Program from a verbatim storage and input array """ storage = s input = i new from_file(file: File, i: Array[I64] = Array[I64]()) => """ Create a Program by walking input from a file. This just skips any errors if they happen (e.g. if there are non-numbers), which might not be what we want! """ storage = Array[I64](0) for line in file.lines() do for chunk in line.split(",").values() do try storage.push(chunk.i64()?) end end end input = i fun print(env: Env) => """ Print out the current state of memory """ env.out.write("[") for x in storage.values() do env.out.write(Format.int[I64](x)) env.out.write(" ") end env.out.print("]") fun ref dbg(msg: String) => """ Log a debug message """ debug.push(msg) fun ref read_storage(addr: USize): I64 ? => """ Attempt to read a word from storage, logging an error message if we fail to do so """ try storage(addr)? else errors.push(OutOfBounds(addr)) error end fun ref write_storage(addr: USize, value: I64) ? => """ Attempt to write a word to storage, logging an error message if we fail to do so """ try storage(addr)? = value else errors.push(OutOfBounds(addr)) error end fun ref get_immediate(addr_loc: USize): Immediate ? => """ Read a single immediate value at the provided loc """ let value = read_storage(addr_loc)? Immediate(value) fun ref get_position(addr_loc: USize): Position ? => """ Read a positional value whose address is stored at the provided loc """ let addr = read_storage(addr_loc)? let value = read_storage(addr.usize())? Position(addr, value) fun ref get_value(addr_loc: USize, mode: Bool): Value ? => """ Read a value at the provided loc based on the mode: if true, then treat it as an immediate, otherwise treat it as a positional """ if mode then get_immediate(addr_loc)? else get_position(addr_loc)? end fun ref get_op_data(modes: Modes): Operands ? => """ Get the data that a binary op needs based on the provided modes """ (let lm, let rm, _) = modes let lhs = get_value(pc+1, lm)? let rhs = get_value(pc+2, rm)? let tgt = read_storage(pc+3)? Operands(lhs, rhs, tgt) fun ref parse_op(n: I64): (Opcode, Modes) => """ Parse out an opcode and set of modes. This hard-codes modes for three operands; it might be worth it in the future to generalize to any N operands """ let opcode = n % 100 let mode1 = (n / 100) % 10 let mode2 = (n / 1000) % 10 let mode3 = (n / 10000) % 10 (opcode, (mode1 == 1, mode2 == 1, mode3 == 1)) fun ref do_binop(modes: Modes, op: {(I64, I64): I64}) ? => """ Get the values of the first two operands by the provided modes, run the function `op` on them, and store the result of that at the provided location """ let data = get_op_data(modes)? write_storage(data.tgt(), op(data.val1(), data.val2()))? pc = pc + 4 fun ref do_jump(modes: Modes, jump_cond: {(I64): Bool}) ? => """ Get the value indicated by the modes at [pc+1], and the function `jump_cond` returns true on that value, then jump to the location indicated by [pc+2], otherwise move ahead as usual """ (let cond, let tgt, _) = modes if jump_cond(get_value(pc+1, cond)?.value()) then pc = get_value(pc+2, tgt)?.value().usize() else pc = pc + 3 end fun ref do_input(modes: Modes) ? => """ Read a value from the input and put it into storage. This logs an error if there are not enough values in the input queue. """ let i = try input.shift()? else errors.push(NotEnoughInput(pc)) error end write_storage(read_storage(pc+1)?.usize(), i)? pc = pc + 2 fun ref do_output(modes: Modes) ? => """ Write the value indicated according to the mode to the output queue. """ (let m, _, _) = modes output.push(get_value(pc+1, m)?.value()) pc = pc + 2 fun ref run_loop() ? => """ This continues running the VM until it encounters a halt instruction or it encounters some other error """ while true do (let opcode, let modes) = parse_op(read_storage(pc)?) dbg("running op " + Format.int[I64](opcode)) match opcode | 1 => do_binop(modes, {(x, y) => x + y})? | 2 => do_binop(modes, {(x, y) => x * y})? | 3 => do_input(modes)? | 4 => do_output(modes)? | 5 => do_jump(modes, {(x) => x != 0})? | 6 => do_jump(modes, {(x) => x == 0})? | 7 => do_binop(modes, {(x, y) => if x < y then 1 else 0 end})? | 8 => do_binop(modes, {(x, y) => if x == y then 1 else 0 end})? | 99 => return None | let x: I64 => errors.push(UnknownOperand(x)) error end end fun ref run(env: Env) => """ This runs the VM and prints output: if an error occurs, it prints the error message(s) and any debug messages that happened during program execution, and then it prints everything printed via an output instruction. """ try run_loop()? else for err in errors.values() do env.out.print(err.message()) end for n in debug.values() do env.out.print("[debug] " + n) end end for n in output.values() do env.out.print("> " + Format.int[I64](n)) end actor Main new create(env: Env) => let caps = recover val FileCaps.>set(FileRead).>set(FileStat) end try with file = OpenFile(FilePath(env.root as AmbientAuth, "input.txt", caps)?) as File do let program = Program.from_file(file, [5]) program.run(env) program.print(env) end else // if something failed, then print an error message of some kind and exit env.err.print("Couldn't read expected file `input.txt'") env.exitcode(99) end