class Spitewaste::SpitewasteParser

Constants

INSTRUCTIONS
NameError
SPECIAL_INSN

Attributes

error[R]
instructions[R]
src[R]
symbol_table[R]

Public Class Methods

new(program, **options) click to toggle source
# File lib/spitewaste/parsers/spitewaste.rb, line 16
def initialize program, **options
  @src = program.dup
  @macros = {}
  @symbol_file = options['symbol_file']

  preprocess!
  eliminate_dead_code! unless options[:keep_dead_code]
end

Public Instance Methods

parse() click to toggle source
# File lib/spitewaste/parsers/spitewaste.rb, line 25
def parse
  @instructions = []

  # first-come, first-served mapping from label names to auto-incrementing
  # indices starting from 0; it would be nice if names could round-trip,
  # but even encoding short ones would result in unpleasantly wide code.
  @symbol_table = Hash.new { |h, k| h[k] = h.size }
  @src.scan(/(\S+):/) { @symbol_table[$1] } # populate label indices
  File.write @symbol_file, @symbol_table.to_json if @symbol_file

  special = @src.scan SPECIAL_INSN
  @src.scan INSTRUCTIONS do |label, _, operator, arg|
    next @instructions << [:label, @symbol_table[label]] if label

    if %i[call jump jz jn].include? operator = operator.to_sym
      arg = @symbol_table[special.shift[1]]
    else
      if OPERATORS_M2T.keys.index(operator) < 8 && !arg
        raise "missing arg for #{operator}"
      elsif arg
        begin
          arg = Integer arg
        rescue ArgumentError
          raise "invalid argument '#{arg}' for #{operator}"
        end
      end
    end

    @instructions << [operator, arg]
  end
end

Private Instance Methods

add_sugar() click to toggle source
# File lib/spitewaste/parsers/spitewaste.rb, line 113
def add_sugar
  # `:foo` = `call foo`
  @src.gsub!(/:(\S+)/, 'call \1')
  # character literals
  @src.gsub!(/'(.)'/) { $1.ord }
  # quick push (`push 1,2,3` desugars to individual pushes)
  @src.gsub!(/push \S+/) { |m| m.split(?,) * ' push ' }
  # quick store (`^2` = `push 2 swap store`)
  @src.gsub!(/\^(\S+)/, 'push \1 swap store')
  # quick load (`@2` = `push 2 load`)
  @src.gsub!(/@(\S+)/, 'push \1 load')
end
eliminate_dead_code!() click to toggle source
# File lib/spitewaste/parsers/spitewaste.rb, line 160
def eliminate_dead_code!
  tokens = @src.split

  # We need an entry point whence to begin determining which routines
  # are never invoked, but Whitespace programs aren't required to start
  # with a label. Here, we add an implcit "main" to the beginning of the
  # source unless it already contains an explicit entry point. TODO: better?
  start = tokens[0][/(\S+):/, 1] || 'main'
  tokens.prepend "#{start}:" unless $1

  # Group the whole program into subroutines to facilitate the discovery
  # of code which can be safely removed without affecting its behavior.
  subroutines = {}
  while label = tokens.shift
    sublen = tokens.index { |t| t[/:$/] } || tokens.size
    subroutines[label.chop] ||= tokens.shift sublen
  end

  # A subroutine may indirectly depend on the one immediately after by
  # "flowing" into it; we assume this is the case if the subroutine's final
  # instruction isn't one of {jump, jz, jn, exit, ret}.
  flow = subroutines.each_cons(2).reject { |(_, tokens), _|
    tokens.last(2).any? { |t| %w[jump jz jn exit ret].include? t }
  }.map{ |pair| pair.map &:first }.to_h

  alive = Set.new
  queue = [start]
  until queue.empty?
    # Bail early if the queue is empty or we've already handled this label.
    next unless label = queue.shift and alive.add? label

    unless subroutines[label]
      raise NameError, "can't branch to '#{label}', no such label"
    end

    # Naively(?) assume that subroutines hit all of their branch targets.
    branches = subroutines[label].each_cons(2).select { |insn, _|
      %w[jump jz jn call].include? insn
    }.map &:last

    # Check dependencies for further dependencies.
    queue.concat branches, [flow[label]]
  end

  # warn alive.grep_v(/^_/).sort_by(&:upcase).inspect
  @src = subroutines.filter_map { |label, instructions|
    "#{label}: #{instructions * ' '}" if alive.include? label
  } * ?\n + ' ' # trailing space required for regex reasons!
end
fucktionalize() click to toggle source
# File lib/spitewaste/parsers/spitewaste.rb, line 130
def fucktionalize
  # Iteratively remove pseudo-fp calls until we can't to allow nesting.
  1 while @src.gsub!(/(#{FUCKTIONAL.keys * ?|})\s*\((.+?)\)/m) do
    FUCKTIONAL[$1] % [gensym, $2]
  end
end
gensym() click to toggle source
# File lib/spitewaste/parsers/spitewaste.rb, line 126
def gensym
  (@sym ||= ?`).succ!
end
preprocess!() click to toggle source
# File lib/spitewaste/parsers/spitewaste.rb, line 59
def preprocess!
  @src << "\nimport syntax"
  resolve_imports
  seed_prng if @seen.include? 'random'
  resolve_strings
  add_sugar
  remove_comments
  propagate_macros
  fucktionalize
end
propagate_macros() click to toggle source
# File lib/spitewaste/parsers/spitewaste.rb, line 137
def propagate_macros
  # Macros are write-once, allowing user code to customize the special
  # values that get used to drive certain behavior in the standard library.

  parse = -> s { s.split(?,).map &:strip }

  # Macro "functions" get handled first.
  @src.gsub!(/(\$\S+?)\(([^)]*)\)\s*{(.+?)}/m) {
    @macros[$1] ||= [$2, $3]; ''
  }
  @src.gsub!(/(\$\S+?)\(([^)]*)\)/) {
    params, body = @macros[$1]
    raise "no macro function '#$1'" unless body
    map = parse[params].zip(parse[$2]).to_h
    body
      .gsub(/`(.+?)`/) { map[$1] }
      .gsub(/#(\S+)/) { "push #{Spitewaste.strpack map[$1]}" }
  }

  @src.gsub!(/(\$\S+)\s*=\s*(.+)/) { @macros[$1] ||= $2; '' }
  @src.gsub!(/(\$[^)\s]+)/) { @macros[$1] || raise("no macro '#$1'") }
end
remove_comments() click to toggle source
# File lib/spitewaste/parsers/spitewaste.rb, line 109
def remove_comments
  @src.gsub!(/;.*/, '')
end
resolve(path) click to toggle source
# File lib/spitewaste/parsers/spitewaste.rb, line 94
def resolve path
  library = path[?/] ? path : File.join(LIBSPW, path)
  File.read "#{library}.spw"
end
resolve_imports() click to toggle source
# File lib/spitewaste/parsers/spitewaste.rb, line 70
def resolve_imports
  @seen = Set.new

  # "Recursively" resolve `import L (...)` statements with implicit include
  # guarding. Each chunk of imports is appended to the end of the current
  # source all at once to prevent having to explicitly jump to the actual
  # start of the program. Some care must be taken to ensure that the final
  # routine of one library won't inadvertently "flow" into the next.
  while @src['import']
    imports = []
    @src.gsub!(/import\s+(\S+).*/) {
      if $1 == ?*
        imports = Dir[LIBSPW + '/*.spw'].map {
          File.read(_1) if @seen.add? File.basename(_1, '.spw')
        }
      else
        imports << resolve($1) if @seen.add? $1
      end
      '' # remove import statement
    }
    @src << imports.join(?\n)
  end
end
resolve_strings() click to toggle source
# File lib/spitewaste/parsers/spitewaste.rb, line 103
def resolve_strings
  @src.gsub!(/"([^"]*)"/m) {
    [0, *$1.reverse.bytes] * ' push ' + ' :strpack'
  }
end
seed_prng() click to toggle source
# File lib/spitewaste/parsers/spitewaste.rb, line 99
def seed_prng
  @src.prepend "push $seed,#{rand 2**31} store $seed = -9001\n"
end