lib/syntax_tree/yarv/bf.rb



# frozen_string_literal: true

module SyntaxTree
  module YARV
    # Parses the given source code into a syntax tree, compiles that syntax tree
    # into YARV bytecode.
    class Bf
      attr_reader :source

      def initialize(source)
        @source = source
      end

      def compile
        # Set up the top-level instruction sequence that will be returned.
        iseq = InstructionSequence.new("<compiled>", "<compiled>", 1, :top)

        # Set up the $tape global variable that will hold our state.
        iseq.duphash({ 0 => 0 })
        iseq.setglobal(:$tape)
        iseq.getglobal(:$tape)
        iseq.putobject(0)
        iseq.send(YARV.calldata(:default=, 1))

        # Set up the $cursor global variable that will hold the current position
        # in the tape.
        iseq.putobject(0)
        iseq.setglobal(:$cursor)

        stack = []
        source
          .each_char
          .chunk do |char|
            # For each character, we're going to assign a type to it. This
            # allows a couple of optimizations to be made by combining multiple
            # instructions into single instructions, e.g., +++ becomes a single
            # change_by(3) instruction.
            case char
            when "+", "-"
              :change
            when ">", "<"
              :shift
            when "."
              :output
            when ","
              :input
            when "[", "]"
              :loop
            else
              :ignored
            end
          end
          .each do |type, chunk|
            # For each chunk, we're going to emit the appropriate instruction.
            case type
            when :change
              change_by(iseq, chunk.count("+") - chunk.count("-"))
            when :shift
              shift_by(iseq, chunk.count(">") - chunk.count("<"))
            when :output
              chunk.length.times { output_char(iseq) }
            when :input
              chunk.length.times { input_char(iseq) }
            when :loop
              chunk.each do |char|
                case char
                when "["
                  stack << loop_start(iseq)
                when "]"
                  loop_end(iseq, *stack.pop)
                end
              end
            end
          end

        iseq.leave
        iseq.compile!
        iseq
      end

      private

      # $tape[$cursor] += value
      def change_by(iseq, value)
        iseq.getglobal(:$tape)
        iseq.getglobal(:$cursor)

        iseq.getglobal(:$tape)
        iseq.getglobal(:$cursor)
        iseq.send(YARV.calldata(:[], 1))

        if value < 0
          iseq.putobject(-value)
          iseq.send(YARV.calldata(:-, 1))
        else
          iseq.putobject(value)
          iseq.send(YARV.calldata(:+, 1))
        end

        iseq.send(YARV.calldata(:[]=, 2))
        iseq.pop
      end

      # $cursor += value
      def shift_by(iseq, value)
        iseq.getglobal(:$cursor)

        if value < 0
          iseq.putobject(-value)
          iseq.send(YARV.calldata(:-, 1))
        else
          iseq.putobject(value)
          iseq.send(YARV.calldata(:+, 1))
        end

        iseq.setglobal(:$cursor)
      end

      # $stdout.putc($tape[$cursor].chr)
      def output_char(iseq)
        iseq.getglobal(:$stdout)

        iseq.getglobal(:$tape)
        iseq.getglobal(:$cursor)
        iseq.send(YARV.calldata(:[], 1))
        iseq.send(YARV.calldata(:chr))

        iseq.send(YARV.calldata(:putc, 1))
        iseq.pop
      end

      # $tape[$cursor] = $stdin.getc.ord
      def input_char(iseq)
        iseq.getglobal(:$tape)
        iseq.getglobal(:$cursor)

        iseq.getglobal(:$stdin)
        iseq.send(YARV.calldata(:getc))
        iseq.send(YARV.calldata(:ord))

        iseq.send(YARV.calldata(:[]=, 2))
        iseq.pop
      end

      # unless $tape[$cursor] == 0
      def loop_start(iseq)
        start_label = iseq.label
        end_label = iseq.label

        iseq.push(start_label)
        iseq.getglobal(:$tape)
        iseq.getglobal(:$cursor)
        iseq.send(YARV.calldata(:[], 1))

        iseq.putobject(0)
        iseq.send(YARV.calldata(:==, 1))
        iseq.branchif(end_label)

        [start_label, end_label]
      end

      # Jump back to the start of the loop.
      def loop_end(iseq, start_label, end_label)
        iseq.getglobal(:$tape)
        iseq.getglobal(:$cursor)
        iseq.send(YARV.calldata(:[], 1))

        iseq.putobject(0)
        iseq.send(YARV.calldata(:==, 1))
        iseq.branchunless(start_label)

        iseq.push(end_label)
      end
    end
  end
end