diff --git a/2019/ruby/computer.rb b/2019/ruby/computer.rb index cbec2d2..c0d0984 100644 --- a/2019/ruby/computer.rb +++ b/2019/ruby/computer.rb @@ -1,60 +1,185 @@ OPCODES = { - 1 => ->(m, a, b, c) { m[c] = m[a] + m[b] }, - 2 => ->(m, a, b, c) { m[c] = m[a] * m[b] }, - 99 => ->(*) { throw :halt }, + 1 => ->(m, _, _, a, b, c) { m[c] = m[a] + m[b] ; nil }, # add + 2 => ->(m, _, _, a, b, c) { m[c] = m[a] * m[b] ; nil }, # multiply + 3 => ->(m, i, _, a) { m[a] = i.gets.to_i ; nil }, # input + 4 => ->(m, _, o, a) { o.puts(m[a]) ; nil }, # output + 5 => ->(m, _, _, a, b) { m[a].nonzero? ? m[b] : nil }, # jump-if-true + 6 => ->(m, _, _, a, b) { m[a].zero? ? m[b] : nil }, # jump-if-false + 7 => ->(m, _, _, a, b, c) { m[c] = (m[a] < m[b]) ? 1 : 0; nil }, # less than + 8 => ->(m, _, _, a, b, c) { m[c] = (m[a] == m[b]) ? 1 : 0; nil }, # equals + 99 => ->(*) { throw :halt }, } +class Parameter + def self.from(value) + case value + when Parameter + value + when Integer + new(0, value) + else + raise "unexpected value: #{value}" + end + end + + attr_reader :value + + def initialize(mode, value) + raise "unexpected mode: #{mode.inspect}" unless [0, 1].include?(mode) + + @mode, @value = mode, value + end + + def position_mode? + @mode.zero? + end + + def immediate_mode? + @mode.nonzero? + end +end + class Computer def self.from(input) new(input.split(?,).map(&:to_i)) end - def initialize(program) - @memory = program + attr_reader :input, :output + + def initialize(program, input=STDIN, output=STDOUT) + @memory, @input, @output = program.dup, input, output @pc = 0 end - def run - each.inject(nil) {|_,i| i } + def run(input=STDIN, output=STDOUT) + each(input, output).inject(nil) {|_,i| i } end - def each - return enum_for(__method__) unless block_given? + def each(input, output) + return enum_for(__method__, input, output) unless block_given? catch(:halt) do loop do - opcode = OPCODES.fetch(@memory.fetch(@pc)) + instruction = @memory[@pc].to_s.rjust(5, ?0) + opcode = OPCODES.fetch(instruction[-2..-1].to_i) @pc += 1 - n = opcode.arity - 1 - args = (0...n).map {|i| @memory.fetch(@pc + i) } + n = opcode.arity - 3 + args = (0...n).zip(instruction[0..2].reverse.chars.map(&:to_i)).map {|i, mode| + value = @memory[@pc + i] + Parameter.new(mode, value) + } @pc += n - opcode.call(@memory, *args) + @pc = opcode.call(self, input, output, *args) || @pc yield @memory end end end + + def [](parameter) + parameter = Parameter.from(parameter) + parameter.position_mode? ? @memory.fetch(parameter.value) : parameter.value + end + alias_method :fetch, :[] + + def []=(parameter, value) + raise "writes should never be in immediate mode" if parameter.immediate_mode? + + @memory[parameter.value] = value + end end require "minitest" class TestComputer < Minitest::Test def test_samples - c = Computer.from("1,9,10,3,2,3,11,0,99,30,40,50").each + c = Computer.from("1,9,10,3,2,3,11,0,99,30,40,50").each(StringIO.new, StringIO.new) - assert_equal 70, c.next.fetch(3) - assert_equal 3500, c.next.fetch(0) + assert_equal 70, c.next[3] + assert_equal 3500, c.next[0] end def test_more_samples - run = ->(p) { Computer.from(p).run } + assert_equal 2, run_program("1,0,0,0,99")[0] + assert_equal 6, run_program("2,3,0,3,99")[3] + assert_equal 9801, run_program("2,4,4,5,99,0")[5] + assert_equal 30, run_program("1,1,1,4,99,5,6,0,99")[0] + end + + def test_parameter_modes + assert_equal 99, run_program("1002,4,3,4,33")[4] + assert_equal 99, run_program("1101,100,-1,4,0")[4] + end + + def test_input_output + assert_equal 1, run_program("3,50,99", input: "1\n")[50] + + output = StringIO.new + run_program("4,3,99,50", output: output) + assert_equal "50\n", output.string + end + + def test_comparisons + # Using position mode, consider whether the input is equal to 8; output 1 + # (if it is) or 0 (if it is not). + { 7 => 0, 8 => 1, 9 => 0 }.each do |i, o| + c = Computer.from("3,9,8,9,10,9,4,9,99,-1,8") + output = StringIO.new + c.run(StringIO.new(i.to_s), output) + assert_equal "#{o}\n", output.string + end + + # Using position mode, consider whether the input is less than 8; output 1 + # (if it is) or 0 (if it is not). + { 7 => 1, 8 => 0, 9 => 0 }.each do |i, o| + c = Computer.from("3,9,7,9,10,9,4,9,99,-1,8") + output = StringIO.new + c.run(StringIO.new(i.to_s), output) + assert_equal "#{o}\n", output.string + end + + # Using immediate mode, consider whether the input is equal to 8; output 1 + # (if it is) or 0 (if it is not). + { 7 => 0, 8 => 1, 9 => 0 }.each do |i, o| + c = Computer.from("3,3,1108,-1,8,3,4,3,99") + output = StringIO.new + c.run(StringIO.new(i.to_s), output) + assert_equal "#{o}\n", output.string + end + + # Using immediate mode, consider whether the input is less than 8; output 1 + # (if it is) or 0 (if it is not). + { 7 => 1, 8 => 0, 9 => 0 }.each do |i, o| + c = Computer.from("3,3,1107,-1,8,3,4,3,99") + output = StringIO.new + c.run(StringIO.new(i.to_s), output) + assert_equal "#{o}\n", output.string + end + end + + def test_jumps + { -1 => 1, 0 => 0, 1 => 1 }.each do |i, o| + c = Computer.from("3,12,6,12,15,1,13,14,13,4,13,99,-1,0,1,9") + output = StringIO.new + c.run(StringIO.new(i.to_s), output) + assert_equal "#{o}\n", output.string + + c = Computer.from("3,3,1105,-1,9,1101,0,0,12,4,12,99,1") + output = StringIO.new + c.run(StringIO.new(i.to_s), output) + assert_equal "#{o}\n", output.string + end + end + + private - assert_equal 2, run.call("1,0,0,0,99").fetch(0) - assert_equal 6, run.call("2,3,0,3,99").fetch(3) - assert_equal 9801, run.call("2,4,4,5,99,0").fetch(5) - assert_equal 30, run.call("1,1,1,4,99,5,6,0,99").fetch(0) + def run_program(p, input: "", output: StringIO.new) + Computer.from(p).run( + StringIO.new(input), + output, + ) end end diff --git a/2019/ruby/day_05.rb b/2019/ruby/day_05.rb index b6681a9..c1353b2 100644 --- a/2019/ruby/day_05.rb +++ b/2019/ruby/day_05.rb @@ -1,52 +1,5 @@ -OPCODES = ARGF.read.split(?,).map(&:to_i) +require_relative "computer" -def run(memory) - pc = 0 - loop do - instruction = memory[pc] - return if instruction == 99 - - p pc - a, b, c, opcode = instruction.to_s.rjust(5, ?0).scan(/^(0|1)(0|1)(0|1)(\d\d)$/)[0].map(&:to_i) - c = c.zero? ? memory[memory[pc+1]] : memory[pc+1] - b = b.zero? ? memory[memory[pc+2]] : memory[pc+2] - a = a.zero? ? memory[memory[pc+3]] : memory[pc+3] - - case opcode - when 1 - memory[memory[pc+3]] = c + b - pc += 4 - when 2 - memory[memory[pc+3]] = c * b - pc += 4 - when 3 - memory[memory[pc+1]] = 5 - pc += 2 - when 4 - puts c - pc += 2 - when 5 - if c.nonzero? - pc = b - else - pc += 3 - end - when 6 - if c.zero? - pc = b - else - pc += 3 - end - when 7 - memory[memory[pc+3]] = (c < b) ? 1 : 0 - pc += 4 - when 8 - memory[memory[pc+3]] = (c == b) ? 1 : 0 - pc += 4 - else - fail "unrecognized instruction: #{instruction}" - end - end -end - -run(OPCODES) +program = ARGF.read.split(?,).map(&:to_i) +computer = Computer.new(program) +computer.run(StringIO.new("5\n"))