// A Transpiler for the Brainf*** language
//=========================================
//
// This version "optimises" the code by replacing 
// for example +++ by (*ptr) += 3, instead of three
// separate (*ptr)++, (*ptr)++, (*ptr)++
// 
// Call with
//
//  amm bfc1.sc <<bf_program.bf>>
//


// generate "compound" c-instructions 
def instr2(c: Char, n: Int) : String = c match {
  case '>' => s"ptr += $n ;"
  case '<' => s"ptr -= $n ;"
  case '+' => s"(*ptr) += $n ;"
  case '-' => s"(*ptr) -= $n ;"
  case '.' => "putchar(*ptr);" * n
  case ',' => "*ptr = getchar(); " * n
  case '['  => "while(*ptr){" * n
  case ']'  => "}" * n
  case _ => ""
}

// "splicing" a BF program into "spans" 
// and counting the number of occurrences in
// each span; then generate the new intruction
// accordingly

def splice(cs: List[Char], acc: List[String]) : List[String] = cs match {
  case Nil => acc
  case hd :: _ => {
    val (hds, rest) = cs.span(_ == hd)
    splice(rest, instr2(hd, hds.length) :: acc) 
  }
}

def instrs2(prog: String) : String =
  splice(prog.toList, Nil).reverse.mkString

// adding boilerplate
def compile(prog: String) : String = 
  s"""#include <string.h> 
      #include <stdio.h> 
      int field[30000]; 
      int *ptr = &field[15000]; 
      int main() { 
      memset(field, '\\0', 30000); 
      ${instrs2(prog)} 
      return 0;}"""

def compile_to_file(name: String, prog: String) = 
  os.write.over(os.pwd / name, compile(prog))


// running the c-compiler over the transpiled
// BF program and running the resulting binary

def compile_and_run(prog: String) = {
  val tn = "tmp"
  compile_to_file(s"${tn}.c", prog)
  os.proc("gcc", "-O0", "-o", tn, s"${tn}.c").call() // call gcc
  os.proc("./tmp").call(stdout = os.Inherit)         // run binary
}

// Running Testcases
//===================

def time_needed[T](n: Int, code: => T) = {
  val start = System.nanoTime()
  for (i <- 0 until n) code
  val end = System.nanoTime()
  (end - start)/(n * 1.0e9)
}

@doc(" the argument should be a BF program ")
@main
def main(fname: String) = {
  val bf_str = os.read(os.pwd / fname)
  println(s"${time_needed(1, compile_and_run(bf_str))} secs")
}  

