|
@@ -0,0 +1,388 @@
|
|
|
+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
|
|
|
+
|
|
|
+
|
|
|
+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
|
|
|
+ Operands(
|
|
|
+ get_value(pc+1, lm)?,
|
|
|
+ get_value(pc+2, rm)?,
|
|
|
+ read_storage(pc+3)?
|
|
|
+ )
|
|
|
+
|
|
|
+ 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): 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.
|
|
|
+ """
|
|
|
+
|
|
|
+ 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
|
|
|
+ output
|
|
|
+
|
|
|
+ fun ref clone(): Program =>
|
|
|
+ Program(storage, input)
|
|
|
+
|
|
|
+ fun ref set_input(i: Array[I64]): Program =>
|
|
|
+ input = i
|
|
|
+ this
|
|
|
+
|
|
|
+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, [0;1])
|
|
|
+ var max: (I64 | None) = None
|
|
|
+ var choice = Array[I64]()
|
|
|
+ for p in permutations([0;1;2;3;4]).values() do
|
|
|
+ env.out.print("\ntesting")
|
|
|
+ let res = test_phases(env, program, p)
|
|
|
+ if try (res as I64) > (max as I64) else true end then
|
|
|
+ max = res
|
|
|
+ choice = p
|
|
|
+ end
|
|
|
+ end
|
|
|
+ env.out.print("maximum is " + max.string())
|
|
|
+ env.out.write("permutation was")
|
|
|
+ for i in choice.values() do
|
|
|
+ env.out.write(" " + i.string())
|
|
|
+ end
|
|
|
+ env.out.print("")
|
|
|
+ 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
|
|
|
+
|
|
|
+ fun test_phases(env: Env, program: Program, phases: Array[I64]): (I64 | None) =>
|
|
|
+ var input: I64 = 0
|
|
|
+ for i in phases.values() do
|
|
|
+ let output = program.clone().set_input([i; input]).run(env)
|
|
|
+ input = try output(0)? else
|
|
|
+ env.out.print("Not enough output returned")
|
|
|
+ return None
|
|
|
+ end
|
|
|
+ None
|
|
|
+ end
|
|
|
+ input
|
|
|
+
|
|
|
+ fun permutations(array: Array[I64]): Array[Array[I64]] =>
|
|
|
+ if array.size() == 0 then
|
|
|
+ [[]]
|
|
|
+ else
|
|
|
+ let res = Array[Array[I64]]()
|
|
|
+ for a in array.values() do
|
|
|
+ let sm = Array[I64]()
|
|
|
+ Iter[I64](array.values()).filter({(x: I64) => x != a}).collect(sm)
|
|
|
+ let ps = permutations(sm)
|
|
|
+ for p in ps.values() do
|
|
|
+ p.unshift(a)
|
|
|
+ res.push(p)
|
|
|
+ end
|
|
|
+ end
|
|
|
+ res
|
|
|
+ end
|