#!/usr/bin/ruby
# c-repl -- a C read-eval-print loop.
# Copyright (C) 2006 Evan Martin <martine@danga.com>

require 'readline'
require 'gdbmi'
require 'codesnippet'

# A wrapper around the child process.
class Runner
  def initialize
    start
  end

  def start
    command_pipe = IO.pipe
    response_pipe = IO.pipe
    @pid = fork do
      command_pipe[1].close
      response_pipe[0].close
      STDIN.reopen(command_pipe[0])
      command_pipe[0].close
      exec('./child', response_pipe[1].fileno.to_s)
    end
    command_pipe[0].close
    command_pipe[1].sync = true
    response_pipe[0].sync = true
    response_pipe[1].close
    @command_pipe = command_pipe[1]
    @response_pipe = response_pipe[0]
  end

  def run_command(id, attempt_recover=true)
    @command_pipe.puts id
    resp = @response_pipe.gets
    #p resp if resp != "#{id}\n"

    pid, status = Process.wait2(@pid, Process::WNOHANG)
    return true unless pid

    # the subprocess failed.  try recovering once if asked.
    if status.signaled? and status.termsig == 11
      puts "segfault detected."
    else
      puts "??? wait finished; #{pid.inspect} #{status.inspect}"
    end
    if attempt_recover
      puts "resuming."
      start
      run_command(id-1, false) if id > 1
    end
    return false
  end

  def gdb(args)
    GDBMI.attach_run_detach(@pid, args)
  end
end

class CREPL
  def initialize
    @debug = false
    @cur_so_id = 1
    @runner = Runner.new
    @externs = []
    @headers = []
    @libraries = []
    @commands = {
      'd' => ['toggle debug mode.',
              proc { @debug = !@debug; puts "debug is #{@debug?'on':'off'}" }],
      'h' => ['"h foo.h": bring in a C header.',
              proc { |args| @headers << args }],
      'l' => ['"l m": bring in a C library.',
              proc { |args| @libraries << args }],
      't' => ['test if the repl is ok by running a printf through it.',
              proc { c_command('printf("repl is ok\n");') }],
      'g' => ['"g foobar": run an arbitrary command through gdb.',
              proc { |args| @runner.gdb args }],
      's' => ['cause a segfault to let crepl attempt to recover.',
              proc do
                puts 'attempting segfault'
                c_command '*((char*)0) = 0;'
              end],
      'help' => ['show help on commands.', proc { show_help }]
    }
  end

  # Given a number and some code.
  # generate a shared object that declares the variables as globals
  # with a function containing the code (if any).
  def generate_so(name, snippet)
    reader, writer = IO.pipe
    pid = fork do
      writer.close
      STDIN.reopen(reader)
      reader.close
      cmd = "gcc -xc -g -shared -o #{name}.so"
      # add in all libraries
      cmd += ' ' + @libraries.map{|l| "-l#{l}"}.join(' ')
      # tell it to read input through stdin
      cmd += ' -'
      puts cmd if @debug
      exec(cmd)
    end

    reader.close
    generate_code(name, snippet, writer)
    writer.close

    generate_code(name, snippet, STDERR) if @debug

    pid, status = Process.wait2(pid)
    status.success?
  end

  # Generate code from snippet, writing it to out.
  def generate_code(name, snippet, out)
    out.puts "#include <stdio.h>"
    @headers.each do |header|
      out.puts "#include \"#{header}\""
    end
    @externs.each do |extern|
      out.puts "extern #{extern}"
    end
    out.puts snippet.decl if snippet.decl
    if snippet.func
      out.puts snippet.func 
    end
    out.puts "void #{name}() {"
    if snippet.stmt
      out.puts "  #line 1"
      out.puts "  #{snippet.stmt}"
    end
    out.puts "}"
  end

  def user_command command
    args = command.gsub(/^(\S+)\s*/, '')
    cmd = $1
    unless @commands.has_key? cmd
      puts "unknown command: #{cmd}"
      return
    end
    help, func = @commands[cmd]
    func.call args
  end

  def c_command code
    snippet = CodeSnippet.parse(code)
    return unless generate_so("dl#{@cur_so_id}", snippet)
    return unless @runner.run_command @cur_so_id

    @externs << snippet.decl if snippet.decl
    @cur_so_id += 1
  end

  def show_help
    puts <<-EOT
Type C statements and declarations as you would in a normal program.
Type a variable name by itself to see its value.

Commands start with a . and are as follows:
    EOT
    cmds = @commands.keys.sort
    len = cmds.map{|c|c.length}.max
    @commands.keys.sort.each do |cmd|
      printf("  %-#{len}s   %s\n", cmd, @commands[cmd][0])
    end
  end

  def input_loop
    loop do
      line = Readline.readline('> ')
      break unless line
      line.chomp!
      next if line.empty?
      if line[0] == ?.
        user_command line[1..-1]
      elsif line =~ /^(\w+)$/
        # bare variable name -- dump it
        @runner.gdb "p #{line}"
      else
        line += ';' unless line =~ /;$/
        c_command line
      end
      Readline::HISTORY.push(line)
    end
  end
end

CREPL.new.input_loop

# vim: set ts=2 sw=2 et :
