use "collections" use "files" use "format" use "itertools" interface Value """ Values referred to by opcodes in instructions should all be things that implement this interface """ fun value(): I64 interface Target fun value(): I64 fun addr(): USize 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(): USize => _addr.usize() 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 Relative """ A realtive operand (i.e. one whose value is fetched from memory relative to a register) """ let _addr: USize let _value: I64 new create(__addr: USize, __value: I64) => _addr = __addr _value = __value fun value(): I64 => _value fun addr(): USize => _addr 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: Target new create(__lhs: Value, __rhs: Value, __tgt: Target) => _lhs = __lhs _rhs = __rhs _tgt = __tgt fun val1(): I64 => _lhs.value() fun val2(): I64 => _rhs.value() fun tgt(): USize => _tgt.addr() 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 (Mode, Mode, Mode) primitive ModeImm fun sigil(): String => "" primitive ModePos fun sigil(): String => "*" primitive ModeRel fun sigil(): String => "~" type Mode is (ModeImm | ModePos | ModeRel) class val OutputRequest let _o: I64 new val create(_o': I64) => _o = _o' fun output(): I64 => _o fun string(): String => "OutputRequest(" + _o.string() + ")" primitive InputRequest fun string(): String => "InputRequest" primitive Halted fun string(): String => "Halted" type ProgramResult is (OutputRequest | InputRequest | Halted) 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 // var input: Array[I64] = Array[I64]() // var rel_base: I64 = 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]() // a queue of debug messages var debug: Array[String] = Array[String]() new create(s: Array[I64]) => """ Create a Program from a verbatim storage and input array """ storage = s new from_file(file: File) => """ 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 fun ref send_input(i: I64) => input.push(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 """ while addr > (storage.size() - 1) do storage.push(0) end 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 """ while addr > (storage.size() - 1) do storage.push(0) end 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_relative(addr_loc: USize): Relative ? => """ Read a positional value whose address is stored at the provided loc """ let offset = read_storage(addr_loc)? let addr = offset + rel_base let value = read_storage(addr.usize())? dbg(" (rel: base " + rel_base.string() + " delta " + offset.string() + ")") Relative(addr.usize(), value) fun ref get_value(addr_loc: USize, mode: Mode): 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 """ match mode | ModeImm => get_immediate(addr_loc)? | ModePos => get_position(addr_loc)? | ModeRel => get_relative(addr_loc)? end fun ref get_target(addr_loc: USize, mode: Mode): Target ? => """ 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 """ match mode | ModeImm => error | ModePos => get_position(addr_loc)? | ModeRel => get_relative(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, let tm) = modes Operands( get_value(pc+1, lm)?, get_value(pc+2, rm)?, get_target(pc+3, tm)? ) fun ref mk_mode(n: I64): Mode => match n | 0 => ModePos | 1 => ModeImm | 2 => ModeRel else ModeImm end 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, (mk_mode(mode1), mk_mode(mode2), mk_mode(mode3))) fun ref do_binop(modes: Modes, op: {(I64, I64): I64}, op_name: String) ? => """ 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)? dbg(" writing " + data.val1().string() + op_name + data.val2().string() + " = " + op(data.val1(), data.val2()).string() + " to *" + data.tgt().string()) write_storage(data.tgt(), op(data.val1(), data.val2()))? pc = pc + 4 fun ref do_adjust_base(modes: Modes) ? => (let m, _, _) = modes let delta = get_value(pc+1, m)?.value() rel_base = rel_base + delta.i64() pc = pc + 2 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 (let m, _, _) = modes let addr = match get_value(pc+1, m)? | let r: Relative => r.addr().usize() | let p: Position => p.addr().usize() else error end dbg(" writing " + i.string() + " to *" + addr.string()) write_storage(addr, i)? pc = pc + 2 fun ref do_output(modes: Modes): OutputRequest ? => """ Write the value indicated according to the mode to the output queue. """ (let m, _, _) = modes let r = get_value(pc+1, m)?.value() pc = pc + 2 OutputRequest(r) fun ref at_pc(): USize => pc fun ref fmt_op(opcode: I64, modes: Modes): String => (let a, let b, let c) = modes let op1 = try read_storage(pc+1)? else -99 end let op2 = try read_storage(pc+2)? else -99 end let op3 = try read_storage(pc+3)? else -99 end let arg1 = a.sigil() + op1.string() let arg2 = b.sigil() + op2.string() let arg3 = c.sigil() + op3.string() match opcode | 1 => "add " + arg1 + " " + arg2 + " -> " + arg3 | 2 => "mul " + arg1 + " " + arg2 + " -> " + arg3 | 3 => "input " + arg1 | 4 => "output " + arg1 | 5 => "jnz " + arg1 + " to " + arg2 | 6 => "jez " + arg1 + " to " + arg2 | 7 => "lt " + arg1 + " " + arg2 + " -> " + arg3 | 8 => "eq " + arg1 + " " + arg2 + " -> " + arg3 | 9 => "adj_base " + arg1 | 99 => "halt" else "unknown op(" + opcode.string() + ")" end fun ref run_loop(env: Env): ProgramResult ? => """ 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)?) env.out.print(" " + pc.string() + "/" + rel_base.string() + " -> " + fmt_op(opcode, modes)) match opcode | 1 => do_binop(modes, {(x, y) => x + y}, "+")? | 2 => do_binop(modes, {(x, y) => x * y}, "*")? | 3 => try do_input(modes)? else return InputRequest end | 4 => return 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}, "==")? | 9 => do_adjust_base(modes)? | 99 => return Halted | let x: I64 => errors.push(UnknownOperand(x)) error end end Halted fun ref run_with(env: Env, i: Array[I64]): Array[I64] => """ 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. """ input.append(i) var output = Array[I64]() var is_done = false try repeat env.out.print("...") match run_loop(env)? | Halted => is_done = true | InputRequest => env.err.print("Not enough input for program"); is_done = true | let o: OutputRequest => output.push(o.output()) end for n in debug.values() do env.out.print("[debug] " + n) end until is_done end 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 output fun ref run_step(env: Env): ProgramResult => try env.out.print("restarting from " + pc.string()) let r = run_loop(env)? for msg in debug.values() do env.out.print(msg) end debug.clear() r else for err in errors.values() do env.out.print(err.message()) end Halted end fun ref clone(): Program => Program(storage.clone()) actor Main new create(env: Env) => let caps = recover val FileCaps.>set(FileRead).>set(FileStat) end let target_file = try env.args(1)? else "input.txt" end try with file = OpenFile(FilePath(env.root as AmbientAuth, target_file, caps)?) as File do let program = Program.from_file(file) program.run_with(env, [2]) end else // if something failed, then print an error message of some kind and exit env.err.print("Couldn't read expected file `" + target_file + "`") env.exitcode(99) end