forth.rb
# frozen_string_literal: true

class Forth < Array
  Call = Struct.new :addr
  class Return end
  class ReturnFalse end
  Branch = Struct.new :offset
  BranchFalse = Struct.new :offset
  Branch2 = Struct.new :offset
  class Case end
  Of = Struct.new :offset
  class EndCase end
  class Do end
  class CountUp end
  class Loop end

  def initialize
    super
    # ループカウンタスタック(I/J/Kを記憶)
    @lcstack = []

    # ループ先頭ワードスタック do/?do - loop/+loop で使用
    @lwstack = []

    # 中間コード列
    @codespace = []

    # Rスタック
    @rstack = []

    # 定数辞書
    @constants = {}

    # 変数辞書
    @variables = {}
    @varnames = []

    @words = {
      '.' => ->(x) { print "#{x} " }, # Print the top value of the stack
      '.s' => -> { p self }, # Print the entire stack
      'clear' => -> { clear },
      'cr' => -> { puts },
      'depth' => -> { push size },
      'drop' => ->(x) {},
      'dup' => -> { push peek },
      'nip' => ->(_x1, x2) { push(x2) }, # ( x1 x2 -- x2 ) Drop the second item on the stack
      'over' => ->(x1, x2) { push(x1, x2, x1) }, # ( x1 x2 -- x1 x2 x1 )Copy the second item on the stack to the top
      'rot' => lambda { |x1, x2, x3|
                 push(x2, x3, x1)
               }, # ( x1 x2 x3 -- x2 x3 x1 ) Rotate the top three values on the stack
      '-rot' => lambda { |x1, x2, x3|
                  push(x3, x1, x2)
                }, # ( x1 x2 x3 -- x3 x1 x2 ) Reverse rotate the top three values on the stack
      'swap' => ->(x1, x2) { push(x2, x1) }, # ( x1 x2 -- x2 x1) Swap the top two values of the stack
      'tuck' => lambda { |x1, x2|
                  push(x2, x1, x2)
                }, # ( x1 x2 -- x2 x1 x2 ) Insert a copy of the top item below the second item on the stack
      '2drop' => ->(x1, x2) {}, # ( x1 x2 -- ) スタックの上位2つの値を削除します。
      '2dup' => ->(x1, x2) { push(x1, x2, x1, x2) }, # ( x1 x2 -- x1 x2 x1 x2 ) スタックの上位2つの値を複製します。
      '2nip' => ->(_x1, _x2, x3) { push(x3) }, # ( x1 x2 x3 -- x3 ) スタックの上位2つの値を削除し、3番目の値を残します。
      '2over' => lambda { |x1, x2, x3, x4| # ( x1 x2 x3 x4 -- x1 x2 x3 x4 x1 x2 ) スタックの3番目と4番目の値をコピーして最上位に置きます。
                   push(x1, x2, x3, x4, x1, x2)
                 },
      '2rot' => lambda { |x1, x2, x3, x4, x5, x6| # ( x1 x2 x3 x4 x5 x6 -- x3 x4 x5 x6 x1 x2 ) スタックの上位6つの値を回転させます。
                  push(x3, x4, x5, x6, x1, x2)
                },
      '2swap' => lambda { |x1, x2, x3, x4| # ( x1 x2 x3 x4 -- x3 x4 x1 x2 ) スタックの上位4つの値を交換します。
                   push(x3, x4, x1, x2)
                 },
      '2tuck' => lambda { |x1, x2, x3, x4| # ( x1 x2 x3 x4 -- x3 x4 x1 x2 x3 x4 ) スタックの3番目と4番目の値を2番目と1番目の値の間に挿入します。
                   push(x3, x4, x1, x2, x3, x4)
                 },
      'min' => ->(x1, x2) { push [x1, x2].min },
      'max' => ->(x1, x2) { push [x1, x2].max },
      'nan?' => ->(x) { push x.nan? },
      'words' => lambda do
        @words.select { |k| k.instance_of? String }.keys.sort.each do |k|
          v = @words[k]
          case v
          in Proc
            if nil
              puts "#{k}: ( #{v.parameters.map { |x|
                                case x
                                in [:req, Symbol => x]
                                  x.to_s
                                else nil
                                end
                              } * ' ' } )"
            end
          else
            puts "#{k}: ( #{v.class} )"
          end
        end
        @words.select { |k| k.instance_of? Proc }.keys.each_with_index do |k, index|
          v = @words[k]
          puts "<lamda #{index}>: ( #{v.parameters.map { |x|
                                        case x
                                        in [:req, Symbol => x]
                                          x.to_s
                                        else nil
                                        end
                                      } * ' ' } )"
        end
      end,
      'I' => -> { push @lcstack[-1][:count] },
      'J' => -> { push @lcstack[-2][:count] },
      'K' => -> { push @lcstack[-3][:count] },
      'INFINITY' => -> { push Float::INFINITY },
      '-INFINITY' => -> { push(-Float::INFINITY) },
      'NAN' => -> { push Float::NAN },
      'EPSILON' => -> { push Float::EPSILON },
      'PI' => -> { push Math::PI },

      'constant' => lambda {
        @evalstack.push(lambda { |word|
          if @constants.include? word
            puts "duplicate define #{word}"
          else
            @constants[word] = pop
          end
          @evalstack.pop
        })
      },
      'variable' => lambda {
        @evalstack.push(lambda { |word|
          @variables[word] = nil
          @evalstack.pop
        })
      },
      ':' => lambda { # ワード定義
        push ':'
        define_word
      }

    }

    # Import from Numeric
    %w[negative? zero? [] abs rationalize class
       to_c to_f to_i to_r
       ! != == < <= > >= -@ ~
       + - * / % divmod **].each do |word|
      m = word.to_sym
      ary_ = [1.0, 1, 1r, 1i]
      q = ary_.each { |n| break n if n.respond_to? m }
      next if q == ary_

      case q.method(m).parameters
      in [] then @words[word] = ->(x) { push x.send(m) }
      in [[:rest]] => a then @words[word] = ->(x1, x2) { push x1.send(m, x2) }
      in [[:req]] then  @words[word] = ->(x1, x2) { push x1.send(m, x2) }
      else
      end
    end

    # Import from Math module
    %w[sin cos tan sqrt cbrt].sort.each do |word|
      m = word.to_sym
      case Math.method(m).parameters
      in [[:req]] then @words[word] = ->(x) { push Math.send(m, x) }
      in [[:rest]] then @words[word] = ->(x) { push Math.send(m, x) }
      in [[:req], [:req]] then @words[word] = ->(x1, x2) { push Math.send(node, x1, x2) }
      else
      end
    end

    # ワードに別名を定義
    {
      'negate' => '-@',
      'invert' => '~',
      '=' => '==',
      '<>' => '!='
    }.each { |key, value| @words[key] = @words[value] }

    # ワード定義中に有効なワード
    @keywords = {
      ';' => lambda {
        name, *body = slice!(rindex(':')..-1).drop(1)

        start = @codespace.size
        body.each { |word| @codespace.push word }
        @codespace.push Return.new
        @words[name] = Call.new(start)

        enddef_word
      },
      'if' => lambda {
        push 'if'
        define_word
      },
      'then' => lambda {
        body = slice!(rindex('if')..-1).drop(1)
        _then = body.take_while { |word| word != 'else' }
        _else = body.drop_while { |word| word != 'else' }.drop_while { |word| word == 'else' }

        start = @codespace.size
        if _else.size.positive?
          _then.push Return.new
          @codespace.push BranchFalse.new _then.size
          _then.each { |word| @codespace.push word }
          _else.each { |word| @codespace.push word }
        else
          @codespace.push ReturnFalse.new
          _then.each { |word| @codespace.push word }
        end
        @codespace.push Return.new

        name = Call.new(start)
        push(name)
        @words[name] = name # XXX
        enddef_word
      },
      'case' => lambda {
        push 'case'
        @casestack ||= []
        @casestack.push []
        define_word
      },
      'of' => lambda {
        stmt = @casestack.last.empty? ? 'case' : 'endof'
        _when = slice!(rindex(stmt)..-1).drop(1)
        @casestack.last << _when
        push 'of'
      },
      'endof' => lambda {
        body = slice!(rindex('of')..-1).drop(1)
        @casestack.last.last[1] = body
        push 'endof'
      },
      'endcase' => lambda {
        default_ = slice!(rindex('endof')..-1).drop(1)
        tbl = @casestack.pop

        start = @codespace.size

        @codespace.push Case.new
        tbl.each do |w, body|
          @codespace.push w
          body.push EndCase.new
          @codespace.push Of.new body.size
          body.each { |word| @codespace.push word }
        end
        default_.each { |word| @codespace.push(word) }
        @codespace.push EndCase.new

        word = Call.new start

        push(word)
        @words[word] = word
        enddef_word
      },
      'do' => lambda {
        push 'do'
        @lwstack.push 'do'
        define_word
      },
      '?do' => lambda {
        push '?do'
        @lwstack.push '?do'
        define_word
      },
      'loop' => lambda {
        stmt = @lwstack.pop
        body = slice!(rindex(stmt)..-1).drop(1)

        start = @codespace.size

        @codespace.push Do.new
        case stmt
        in 'do'
          body.push '1'
          body.push CountUp.new
          body.push Branch2.new(-body.size - 1)
        in '?do'
          body.push '1'
          body.push CountUp.new
          body.unshift Branch body.size + 1
          body.push Branch2.new(-body.size - 1)
        end
        body.each { |word| @codespace.push word }
        @codespace.push Loop.new
        # warn [__method__, @codespace.inspect]
        word = Call.new start

        push(word)
        @words[word] = word
        enddef_word
      },
      '+loop' => lambda {
        stmt = @lwstack.pop
        body = slice!(rindex(stmt)..-1).drop(1)

        start = @codespace.size

        @codespace.push Do.new
        case stmt
        in 'do'
          body.push CountUp.new
          body.push Branch2.new(-body.size - 1)
        in '?do'
          body.push CountUp.new
          body.unshift Branch body.size + 1
          body.push Branch2.new(-body.size - 1)
        end
        body.each { |word| @codespace.push word }
        @codespace.push Loop.new
        word = Call.new start

        push(word)
        @words[word] = word
        enddef_word
      },
      '."' => lambda {
        push '."'
        @evalstack.push(lambda do |word|
          case word
          when /(.*)"$/
            push ::Regexp.last_match(1)
            body = slice!(rindex('."')..-1).drop(1)
            word = -> { push body * ' ' }
            push(word)
            @words[word] = word
            @evalstack.pop
          else
            push(word)
          end
        end)
      },
      '(' => lambda {
        push '('
        @evalstack.push(lambda do |word|
          case word
          when ')'
            slice!(rindex('(')..-1).drop(1)
            @evalstack.pop
          else
            push(word)
          end
        end)
      }
    }

    # キーワードに別名を定義
    {
      'endif' => 'then'
    }.each { |key, value| @keywords[key] = @keywords[value] }

    # evaluator stack
    @evalstack = [method(:eval)]
  end
  alias peek last

  def eval(word)
    ip = nil
    lc = 0
    while word
      # $stderr.puts "word #{word}"
      next_word = nil
      case word
      in /\A-?\d+\z/ # Decimal integer
        push Integer(word)
      in /\A[+-]?0[Bb][01]+\z/ # Binary integer
        push Integer(word)
      in /\A[+-]?0[Oo][0-7]+\z/ # Octal integer
        push Integer(word)
      in /\A[+-]?0[Xx][0-9A-Fa-f]+\z/ # Hexadecimal integer
        push Integer(word)
      in /\A[+-]?\d+(\.\d+)?([eE][-+]?\d+)?\z/ # Floating point number
        push Float(word)
      in String
        if @constants.include? word
          push @constants[word]
        elsif @variables.include? word
          @varnames.push word
          @evalstack.push(lambda { |word|
            case word
            in '!'
              @variables[@varnames.pop] = pop
            in '@'
              push @variables[@varnames.pop]
            end
            @evalstack.pop
          })
        else
          next_word = @words[word]
        end
      in Proc => proc then
        n = proc.parameters.reduce(0) do |result, el|
          el.first == :req ? result + 1 : result
        end
        proc[*pop(n)]
      in Call => call then
        @rstack.push ip
        ip = call.addr
      in Return then
        ip = @rstack.pop
      in ReturnFalse then
        ip = @rstack.pop unless pop
      in BranchFalse => bf then
        ip += bf.offset unless pop
      in Case => _case then
        n = pop
        # warn [:case, n].inspect
        @rstack.push n
      in Of => of then
        n = pop
        # warn [:of, n].inspect
        ip += of.offset unless @rstack.last == n
      in EndCase => endcase then
        @rstack.pop
        ip = @rstack.pop
        # break if @rstack.empty?

        if ip.nil?
          p [EndCase, word, ip]
          warn [ip, word, self].inspect
          @codespace.each_with_index do |word, i|
            p [EndCase, i, word]
          end
          # exit
        end
      in Do => _do then
        limit, count = *pop(2)
        @lcstack.push({limit: limit, count: count}) 
      in Branch2 => branch2
        limit, count = @lcstack.last.fetch_values(:limit, :count)
        ip += branch2.offset if limit > count
      in CountUp => countup
        @lcstack.last[:count] += pop
      in Loop then
        pair = @lcstack.pop
        ip = @rstack.pop
      else
        raise "Unknown word #{word}(#{wird.class})"
      end
      word = nil
      if next_word
        word = next_word
      elsif ip
        word = @codespace[ip]
        ip += 1
      end
      #      break if !@rstack.empty?
      lc += 1
      if lc > 9999
        [[:inf - loop]]
        exit
      end
      #      p self # XXX
    end
  end

  def eval_line(line)
    line.split.each { |word| @evalstack.last[word] }
    self
  end

  def repl
    while (line = gets)
      eval_line(line)
    end
  end

  private

  def define_word
    @evalstack.push(lambda do |word|
      @keywords[word] ? @keywords[word][] : push(word)
    end)
  end

  def enddef_word = @evalstack.pop
end

require 'minitest/spec'

# 標準出力をキャプチャして文字列として返す
def capture_stdout
  original_stdout = $stdout
  $stdout = StringIO.new
  yield
  $stdout.string
ensure
  $stdout = original_stdout
end

describe Forth do
  before do
    @forth = Forth.new
  end

  describe 'basic operation' do
    it 'initialize' do
      _(@forth.eval_line('')).must_equal []
    end

    it '.' do
      actual_output = capture_stdout { @forth.eval_line('123 .') }
      _(actual_output).must_equal '123 '
    end

    it '.s' do
      actual_output = capture_stdout { @forth.eval_line('123 .s') }
      _(actual_output).must_equal "[123]\n"
    end

    it 'words' do
      skip
      @forth.eval_line(': cube dup dup * * ;')
      @forth.eval_line(%(: sign dup 0 < if -1 else dup 0 > if 1 else 0 then then ;))
      @forth.eval_line(%(: d34 3 1 do 4 1 do I J loop loop ; d34))
      actual_output = capture_stdout { @forth.eval_line('words') }
      _(actual_output).must_equal ''
    end

    it 'stack op.' do
      _(@forth.eval_line('1')).must_equal [1]
      _(@forth.eval_line('2')).must_equal [1, 2]
      _(@forth.eval_line('+')).must_equal [3]
    end

    it 'to_i' do
      _(@forth.eval_line(%(3.1415926536))).must_equal [3.1415926536]
      _(@forth.eval_line(%(to_i))).must_equal [3]
    end

    it 'to_f' do
      _(@forth.eval_line(%(3))).must_equal [3]
      _(@forth.eval_line(%(to_f))).must_equal [3.0]
    end

    it 'to_c' do
      _(@forth.eval_line(%(-2))).must_equal [-2]
      _(@forth.eval_line(%(to_c))).must_equal [-2]
      _(@forth.eval_line(%(0.5 **))).must_equal [(0.0 + 1.4142135623730951i)]
    end

    it 'to_r' do
      _(@forth.eval_line(%(12))).must_equal [12]
      _(@forth.eval_line(%(to_r))).must_equal [12]
      _(@forth.eval_line(%(18 /))).must_equal [Rational(2, 3)]
      _(@forth.eval_line(%(0.0 /))).must_equal [Float::INFINITY]
      _(@forth.eval_line(%(0.0 to_r 0.0 / nan?))).must_equal [Float::INFINITY, true]
      _ { @forth.eval_line(%(12 to_r 0 /)) }.must_raise ZeroDivisionError
    end

    it 'extensive func.' do
      _(@forth.eval_line(%(9 5 divmod))).must_equal [[1, 4]]
      _(@forth.eval_line(%(clear 27 cbrt))).must_equal [3]
      _(@forth.eval_line(%(clear PI 4 / sin))).must_equal [0.7071067811865475]
      _(@forth.eval_line(%(clear 0.0 zero?))).must_equal [true]
      _(@forth.eval_line(%(clear 0b1111 0 []))).must_equal [1]
      _(@forth.eval_line(%(clear 0b1111 1 []))).must_equal [1]
      _(@forth.eval_line(%(clear 0b1111 2 []))).must_equal [1]
      _(@forth.eval_line(%(clear 0b1111 3 []))).must_equal [1]
      _(@forth.eval_line(%(clear 0b1111 4 []))).must_equal [0]
      _(@forth.eval_line(%(clear 73 42 rationalize class))).must_equal [Rational]
    end
  end

  describe 'Basic words' do
    it 'should execute . correctly' do
      output = capture_stdout { @forth.eval_line(%(123 .)) }
      _(output).must_equal '123 '
    end

    it 'should execute .s correctly' do
      output = capture_stdout { @forth.eval_line(%(1 2 3 .s)) }
      _(output).must_equal "[1, 2, 3]\n"
    end

    it 'should execute clear correctly' do
      @forth.eval_line(%(1 2 3 clear))
      _(capture_stdout { @forth.eval_line('.s') }).must_equal "[]\n"
    end

    it 'should execute depth correctly' do
      @forth.eval_line(%(1 2 3 depth))
      _(capture_stdout { @forth.eval_line('.s') }).must_equal "[1, 2, 3, 3]\n"
    end

    it 'should execute drop correctly' do
      @forth.eval_line(%(1 2 3 drop))
      _(capture_stdout { @forth.eval_line('.s') }).must_equal "[1, 2]\n"
    end

    it 'should execute dup correctly' do
      @forth.eval_line(%(1 2 dup))
      _(capture_stdout { @forth.eval_line('.s') }).must_equal "[1, 2, 2]\n"
    end

    it 'should execute nip correctly' do
      @forth.eval_line(%(1 2 3 nip))
      _(capture_stdout { @forth.eval_line('.s') }).must_equal "[1, 3]\n"
    end

    it 'should execute over correctly' do
      @forth.eval_line(%(1 2 over))
      _(capture_stdout { @forth.eval_line('.s') }).must_equal "[1, 2, 1]\n"
    end

    it 'should execute rot correctly' do
      @forth.eval_line(%(1 2 3 rot))
      _(capture_stdout { @forth.eval_line('.s') }).must_equal "[2, 3, 1]\n"
    end

    it 'should execute -rot correctly' do
      @forth.eval_line(%(1 2 3 -rot))
      _(capture_stdout { @forth.eval_line('.s') }).must_equal "[3, 1, 2]\n"
    end

    it 'should execute swap correctly' do
      @forth.eval_line(%(1 2 swap))
      _(capture_stdout { @forth.eval_line('.s') }).must_equal "[2, 1]\n"
    end

    it 'should execute tuck correctly' do
      @forth.eval_line(%(1 2 tuck))
      _(capture_stdout { @forth.eval_line('.s') }).must_equal "[2, 1, 2]\n"
    end

    it 'should execute 2drop correctly' do
      @forth.eval_line(%(1 2 3 4 2drop))
      _(capture_stdout { @forth.eval_line('.s') }).must_equal "[1, 2]\n"
    end

    it 'should execute 2dup correctly' do
      @forth.eval_line(%(1 2 2dup))
      _(capture_stdout { @forth.eval_line('.s') }).must_equal "[1, 2, 1, 2]\n"
    end

    it 'should execute 2nip correctly' do
      @forth.eval_line(%(1 2 3 4 2nip))
      _(capture_stdout { @forth.eval_line('.s') }).must_equal "[1, 4]\n"
    end

    it 'should execute 2over correctly' do
      @forth.eval_line(%(1 2 3 4 2over))
      _(capture_stdout { @forth.eval_line('.s') }).must_equal "[1, 2, 3, 4, 1, 2]\n"
    end

    it 'should execute 2rot correctly' do
      @forth.eval_line(%(1 2 3 4 5 6 2rot))
      _(capture_stdout { @forth.eval_line('.s') }).must_equal "[3, 4, 5, 6, 1, 2]\n"
    end

    it 'should execute 2swap correctly' do
      @forth.eval_line(%(1 2 3 4 2swap))
      _(capture_stdout { @forth.eval_line('.s') }).must_equal "[3, 4, 1, 2]\n"
    end

    it 'should execute 2tuck correctly' do
      @forth.eval_line(%(1 2 3 4 2tuck))
      _(capture_stdout { @forth.eval_line('.s') }).must_equal "[3, 4, 1, 2, 3, 4]\n"
    end
  end

  describe 'arithmetic operations' do
    it 'should perform arithmetic operations correctly' do
      _(@forth.eval_line(%(5 3 + 2 *))).must_equal [16]
    end

    it '!' do
      _(@forth.eval_line(%(clear 1 !))).must_equal [false]
      _(@forth.eval_line(%(clear 0 !))).must_equal [false]
      _(@forth.eval_line(%(clear 0 1 < !))).must_equal [false]
    end

    it '!= ==' do
      _(@forth.eval_line(%(clear 1 1 !=))).must_equal [false]
      _(@forth.eval_line(%(clear 1 1 ==))).must_equal [true]
      _(@forth.eval_line(%(clear 0 1 !=))).must_equal [true]
      _(@forth.eval_line(%(clear 0 1 ==))).must_equal [false]
      _(@forth.eval_line(%(clear 1 1.0 !=))).must_equal [false]
      _(@forth.eval_line(%(clear 1 1.0 ==))).must_equal [true]
      _(@forth.eval_line(%(clear 0 1.0 !=))).must_equal [true]
      _(@forth.eval_line(%(clear 0 1.0 ==))).must_equal [false]
      _(@forth.eval_line(%(clear 1.0 1 !=))).must_equal [false]
      _(@forth.eval_line(%(clear 1.0 1 ==))).must_equal [true]
      _(@forth.eval_line(%(clear 0.0 1 !=))).must_equal [true]
      _(@forth.eval_line(%(clear 0.0 1 ==))).must_equal [false]
      _(@forth.eval_line(%(clear NAN 1 ==))).must_equal [false]
      _(@forth.eval_line(%(clear NAN 1 !=))).must_equal [true]
      _(@forth.eval_line(%(clear NAN NAN ==))).must_equal [false]
      _(@forth.eval_line(%(clear NAN NAN !=))).must_equal [true]
    end

    it 'abs' do
      _(@forth.eval_line(%(4 abs))).must_equal [4]
      _(@forth.eval_line(%(clear -4 abs))).must_equal [4]
      _(@forth.eval_line(%(clear -9 to_c 0.5 ** 4 + abs))).must_equal [5]
    end

    it 'min max' do
      _(@forth.eval_line(%(4 1 min))).must_equal [1]
      _(@forth.eval_line(%(4 max))).must_equal [4]
    end

    it 'negate invert' do
      _(@forth.eval_line(%(1 negate))).must_equal [-1]
      _(@forth.eval_line(%(negate))).must_equal [1]
      _(@forth.eval_line(%(4 invert))).must_equal [1, -5]
    end

    it 'negative?' do
      _(@forth.eval_line(%(clear -1 negative?))).must_equal [true]
      _(@forth.eval_line(%(clear 0 negative?))).must_equal [false]
      _(@forth.eval_line(%(clear 1 negative?))).must_equal [false]
      _(@forth.eval_line(%(clear -1.0 negative?))).must_equal [true]
      _(@forth.eval_line(%(clear 0.0 negative?))).must_equal [false]
      _(@forth.eval_line(%(clear 1.0 negative?))).must_equal [false]
      _(@forth.eval_line(%(clear -0.0 negative?))).must_equal [false]
    end
  end
  describe 'comparison operations' do
    it 'should compare values correctly' do
      _(@forth.eval_line(%(5 3 < 5 3 > 5 5 ==))).must_equal [false, true, true]
    end
  end

  describe 'dup' do
    it 'should duplicate the top value of the stack' do
      _(@forth.eval_line(%(5 dup))).must_equal [5, 5]
    end
  end

  describe 'drop' do
    it 'should remove the top value from the stack' do
      _(@forth.eval_line(%(5 drop))).must_equal []
    end
  end

  describe 'swap' do
    it '( x1 x2 -- x2 x1 ) should swap the top two values of the stack' do
      _(@forth.eval_line(%(5 10 swap))).must_equal [10, 5]
    end
  end

  describe 'over' do
    it 'should copy the second item on the stack to the top' do
      _(@forth.eval_line(%(5 10 over))).must_equal [5, 10, 5]
    end
  end

  describe 'rot' do
    it 'should rotate the top three values on the stack' do
      _(@forth.eval_line(%(5 10 15 rot))).must_equal [10, 15, 5]
    end
  end

  describe '-rot' do
    it 'should reverse rotate the top three values on the stack' do
      _(@forth.eval_line(%(5 10 15 -rot))).must_equal [15, 5, 10]
    end
  end

  describe 'nip' do
    it '( x1 x2 -- x2 ) should drop the second item on the stack' do
      _(@forth.eval_line(%(5 10 nip))).must_equal [10]
    end
  end

  describe 'tuck' do
    it 'should insert a copy of the top item below the second item on the stack' do
      _(@forth.eval_line(%(5 10 tuck))).must_equal [10, 5, 10]
    end
  end

  describe 'constant' do
    it 'define constant' do
      _(@forth.eval_line(%(42 constant C1))).must_equal []
      _(@forth.eval_line(%(12 C1))).must_equal [12, 42]
      actual_output = capture_stdout do
        _(@forth.eval_line(%(34 constant C1))).must_equal [12, 42, 34]
      end
      _(actual_output).must_equal "duplicate define C1\n"
    end
  end

  describe 'variable' do
    it 'define variable' do
      _(@forth.eval_line(%(variable v1))).must_equal []
      _(@forth.eval_line(%(12 v1 !))).must_equal []
      _(@forth.eval_line(%(v1 @ ))).must_equal [12]
    end
  end

  describe 'if-then-else' do
    it 'should execute if-then blocks correctly' do
      _(@forth.eval_line(%(: xxx if 2 + then ;))).must_equal []
      _(@forth.eval_line(%(0 5 3 > xxx))).must_equal [2]
      _(@forth.eval_line(%(5 3 < xxx))).must_equal [2]
    end

    it 'should execute if-else-then blocks correctly' do
      _(@forth.eval_line(%(: ttt 5 3 > if 2 2 + else 3 3 + then ;))).must_equal []
      _(@forth.eval_line(%(ttt))).must_equal [4]
      _(@forth.eval_line(%[clear : sss ( n -- ) if ." true" else ." false" then ;])).must_equal []
      _(@forth.eval_line(%(1 2 < sss))).must_equal ['true']
    end

    it 'should execute if-else-endif blocks correctly' do
      _(@forth.eval_line(%(: ttt 5 3 > if 2 2 + else 3 3 + endif ;))).must_equal []
      _(@forth.eval_line(%(ttt))).must_equal [4]
      _(@forth.eval_line(%[clear : sss ( n -- ) if ." true" else ." false" endif ;])).must_equal []
      _(@forth.eval_line(%(1 2 < sss))).must_equal ['true']
    end

    it 'should execute if-then-else nested blocks correctly' do
      _(@forth.eval_line(%(: c dup dup * * ; 3 c))).must_equal [27]
      _(@forth.eval_line(%(clear))).must_equal []
      _(@forth.eval_line(%(: sign dup 0 < if -1 else dup 0 > if 1 else 0 then then ;))).must_equal []
      _(@forth.eval_line(%(123 sign))).must_equal [123, 1]
      _(@forth.eval_line(%(clear -123 sign))).must_equal [-123, -1]
      _(@forth.eval_line(%(clear 0.0 sign))).must_equal [0.0, 0]
      _(@forth.eval_line(%(clear 0 sign))).must_equal [0.0, 0]
    end
  end

  describe 'case _ of ... endof _ of ... endof default endcase' do
    it 'should execute case-endcase correctly' do
      _(@forth.eval_line(%(: t case 1 of 111 endof 2 of 222 endof 3 of 333 endof 999 endcase ;))).must_equal []

      _(@forth.eval_line(%(clear 2 t ))).must_equal [222]
      _(@forth.eval_line(%(clear 1 t ))).must_equal [111]
      _(@forth.eval_line(%(clear 3 t ))).must_equal [333]
      _(@forth.eval_line(%(clear 0 t ))).must_equal [999]
      _(@forth.eval_line(%(clear 2.0 t ))).must_equal [222]
      _(@forth.eval_line(%(clear 1.0 t ))).must_equal [111]
      _(@forth.eval_line(%(clear 3.0 t ))).must_equal [333]
      _(@forth.eval_line(%(clear 0.0 t ))).must_equal [999]
    end
  end

  describe 'do loop' do
    it 'should execute do loops correctly' do
      _(@forth.eval_line(%(: d3 3 1 do I loop ; d3))).must_equal [1, 2]
    end

    it 'should execute do +loops correctly' do
      _(@forth.eval_line(%(: d15 15 1 do I 5 +loop ;))).must_equal []
    end

    it 'should execute do loops nested correctly' do
      _(@forth.eval_line(%(: d34 3 1 do 4 1 do I J loop
                                      loop ; d34))).must_equal [1, 1, 2, 1, 3, 1, 1, 2, 2, 2, 3, 2]
    end

    it 'should execute do loops nested correctly w/ stdout' do
      actual_output = capture_stdout do
        @forth.eval_line(%(: d34 3 1 do 4 1 do I . J . ." ," . loop
                                      cr loop ; d34))
      end
      _(actual_output).must_equal <<~EOS
        1 1 , 2 1 , 3 1 ,#{' '}
        1 2 , 2 2 , 3 2 ,#{' '}
      EOS
    end

    it 'should execute do do +loop loop nested correctly' do
      _(@forth.eval_line(%(: d34x 3 1 do 5 1 do I J 99 2 +loop loop ; d34x ))).must_equal [1, 1, 99, 3, 1, 99, 1, 2, 99, 3, 2, 99]
    end
  end
end

Minitest.run
# Forth.new.repl