[2019][ruby] typecheck computer
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