[2019][ruby] typecheck computer
change ronnloolmmslttoyxwprwllwrlvrntzy
commit 3b2399a83f0d9cb7c17f12c880b61c944ba8dfbc
author Alpha Chen <alpha@kejadlen.dev>
date
parent lvstlnxl
diff --git a/2019/ruby/computer.rb b/2019/ruby/computer.rb
index b75df09..6047480 100644
--- a/2019/ruby/computer.rb
+++ b/2019/ruby/computer.rb
@@ -1,5 +1,18 @@
-# typed: false
-OPCODES = {
+# typed: strict
+
+require "sorbet-runtime"
+
+AnyIO = T.type_alias { T.any(IO, StringIO) }
+Memory = T.type_alias { T::Array[T.nilable(Integer)] }
+
+class Mode < T::Enum
+  enums do
+    Position = new
+    Immediate = new
+  end
+end
+
+OPCODES = T.let({
   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
@@ -9,55 +22,70 @@ OPCODES = {
   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                        },
-}
+}, T::Hash[T.untyped, T.untyped])
 
 class Parameter
+  extend T::Sig
+
+  sig {params(value: T.any(Parameter, Integer)).returns(Parameter)}
   def self.from(value)
     case value
     when Parameter
       value
     when Integer
-      new(0, value)
+      new(Mode::Position, value)
     else
-      raise "unexpected value: #{value}"
+      T.absurd(value)
     end
   end
 
+  sig {returns(Mode)}
+  attr_reader :mode
+
+  sig {returns(Integer)}
   attr_reader :value
 
+  sig {params(mode: Mode, value: Integer).void}
   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?
+    @mode = T.let(mode, Mode)
+    @value = T.let(value, Integer)
   end
 end
 
 class Computer
+  extend T::Sig
+
+  sig {params(input: String).returns(Computer)}
   def self.from(input)
     new(input.split(?,).map(&:to_i))
   end
 
+  sig {returns(AnyIO)}
   attr_reader :input, :output
 
+  sig {params(program: Memory, input: AnyIO, output: AnyIO).void}
   def initialize(program, input=STDIN, output=STDOUT)
-    @memory, @input, @output = program.dup, input, output
-    @pc = 0
+    @memory = T.let(program.dup, Memory)
+    @input = T.let(input, AnyIO)
+    @output = T.let(output, AnyIO)
+    @pc = T.let(0, Integer)
   end
 
+  sig {params(input: AnyIO, output: AnyIO).returns(Memory)}
   def run(input=STDIN, output=STDOUT)
-    each(input, output).inject(nil) {|_,i| i }
+    each = T.cast(each(input, output), T::Enumerator[Memory])
+    each.inject(nil) {|_,i| i }
   end
 
-  def each(input, output)
-    return enum_for(__method__, input, output) unless block_given?
+  sig {
+    params(
+      input: AnyIO,
+      output: AnyIO,
+      blk: T.nilable(T.proc.params(m: Memory).returns(T.nilable(Integer)))
+    ).returns(T.any(T::Enumerator[Memory], BasicObject))
+  }
+  def each(input, output, &blk)
+    return enum_for(T.must(__method__), input, output) unless block_given?
 
     catch(:halt) do
       loop do
@@ -66,8 +94,12 @@ class Computer
         @pc += 1
 
         n = opcode.arity - 3
-        args = (0...n).zip(instruction[0..2].reverse.chars.map(&:to_i)).map {|i, mode|
-          value = @memory[@pc + i]
+        args = (0...n).zip(T.must(instruction[0..2]).reverse.chars.map(&:to_i)).map {|i, mode|
+          value = @memory.fetch(@pc + i) || 0
+          mode = case mode
+                 when 0 then Mode::Position
+                 when 1 then Mode::Immediate
+                 end
           Parameter.new(mode, value)
         }
         @pc += n
@@ -79,111 +111,22 @@ class Computer
     end
   end
 
+  sig {params(parameter: Parameter).returns(Integer)}
   def [](parameter)
     parameter = Parameter.from(parameter)
-    parameter.position_mode? ? @memory.fetch(parameter.value) : parameter.value
+    mode = parameter.mode
+    case mode
+    when Mode::Position  then @memory.fetch(parameter.value) || 0
+    when Mode::Immediate then parameter.value
+    else T.absurd(mode)
+    end
   end
   alias_method :fetch, :[]
 
+  sig {params(parameter: Parameter, value: Integer).void}
   def []=(parameter, value)
-    raise "writes should never be in immediate mode" if parameter.immediate_mode?
+    raise "writes should never be in immediate mode" if parameter.mode == Mode::Immediate
 
     @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(StringIO.new, StringIO.new)
-
-    assert_equal 70, c.next[3]
-    assert_equal 3500, c.next[0]
-  end
-
-  def test_more_samples
-    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
-
-  def run_program(p, input: "", output: StringIO.new)
-    Computer.from(p).run(
-      StringIO.new(input),
-      output,
-    )
-  end
-end
-
-if __FILE__ == $0
-  require "minitest/autorun"
-end
diff --git a/2019/ruby/test_computer.rb b/2019/ruby/test_computer.rb
new file mode 100644
index 0000000..40f0a95
--- /dev/null
+++ b/2019/ruby/test_computer.rb
@@ -0,0 +1,95 @@
+# typed: ignore
+
+require "minitest/autorun"
+
+require_relative "computer"
+
+class TestComputer < Minitest::Test
+  def test_samples
+    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[3]
+    assert_equal 3500, c.next[0]
+  end
+
+  def test_more_samples
+    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
+
+  def run_program(p, input: "", output: StringIO.new)
+    Computer.from(p).run(
+      StringIO.new(input),
+      output,
+    )
+  end
+end