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