bfc.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  1. #!/usr/bin/env python
  2. #
  3. # Optimizing brainfuck compiler
  4. #
  5. # Copyright (c) 2014 Project Nayuki
  6. # All rights reserved. Contact Nayuki for licensing.
  7. # http://www.nayuki.io/page/optimizing-brainfuck-compiler
  8. #
  9. # This script translates brainfuck source code into C/Java/Python source code.
  10. # Usage: python bfc.py BrainfuckFile OutputFile.c/java/py
  11. #
  12. import os, re, sys
  13. # ---- Main ----
  14. def main(args):
  15. # Handle command-line arguments
  16. if len(args) != 2:
  17. return "Usage: python bfc.py BrainfuckFile OutputFile.c/java/py"
  18. inname = args[0]
  19. if not os.path.exists(inname):
  20. return inname + ": File does not exist"
  21. if not os.path.isfile(inname):
  22. return inname + ": Not a file"
  23. outname = args[1]
  24. if outname.endswith(".c" ): outfunc = commands_to_c
  25. elif outname.endswith(".java"): outfunc = commands_to_java
  26. elif outname.endswith(".py" ): outfunc = commands_to_python
  27. else: return outname + ": Unknown output type"
  28. # Read input
  29. with open(inname, "r") as fin:
  30. incode = fin.read()
  31. # Parse and optimize Brainfuck code
  32. commands = parse(incode)
  33. #commands = optimize(commands)
  34. #commands = optimize(commands)
  35. #commands = optimize(commands)
  36. # Write output
  37. tempname = os.path.splitext(os.path.basename(outname))[0]
  38. outcode = outfunc(commands, tempname)
  39. with open(outname, "w") as fout:
  40. fout.write(outcode)
  41. # ---- Parser ----
  42. # Parses the given raw code string, returning a list of Command objects.
  43. def parse(codestr):
  44. codestr = re.sub(r"[^+\-<>.,\[\]]", "", codestr) # Keep only the 8 Brainfuck characters
  45. def chargen():
  46. for c in codestr:
  47. yield c
  48. while True: # At end of stream
  49. yield ""
  50. return _parse(chargen(), True)
  51. def _parse(chargen, maincall):
  52. result = []
  53. for c in chargen:
  54. if c == "+": result.append(Add(0, +1))
  55. elif c == "-": result.append(Add(0, -1))
  56. elif c == "<": result.append(Right(-1))
  57. elif c == ">": result.append(Right(+1))
  58. elif c == ",": result.append(Input (0))
  59. elif c == ".": result.append(Output(0))
  60. elif c == "[": result.append(Loop(_parse(chargen, False)))
  61. elif c == "]":
  62. if maincall: raise ValueError("Extra loop closing")
  63. else: return result
  64. elif c == "":
  65. if maincall: return result
  66. else: raise ValueError("Unclosed loop")
  67. else:
  68. raise AssertionError("Illegal code character")
  69. # ---- Optimizers ----
  70. # Optimizes the given list of Commands, returning a new list of Commands.
  71. def optimize(commands):
  72. result = []
  73. offset = 0 # How much the memory pointer has moved without being updated
  74. for cmd in commands:
  75. if isinstance(cmd, Assign):
  76. # Try to fuse into previous command
  77. off = cmd.offset + offset
  78. prev = result[-1] if len(result) >= 1 else None
  79. if isinstance(prev, (Add,Assign)) and prev.offset == off \
  80. or isinstance(prev, (MultAdd,MultAssign)) and prev.destOff == off:
  81. del result[-1]
  82. result.append(Assign(off, cmd.value))
  83. elif isinstance(cmd, MultAssign):
  84. result.append(MultAssign(cmd.srcOff + offset, cmd.destOff + offset, cmd.value))
  85. elif isinstance(cmd, Add):
  86. # Try to fuse into previous command
  87. off = cmd.offset + offset
  88. prev = result[-1] if len(result) >= 1 else None
  89. if isinstance(prev, Add) and prev.offset == off:
  90. prev.value = (prev.value + cmd.value) & 0xFF
  91. elif isinstance(prev, Assign) and prev.offset == off:
  92. prev.value = (prev.value + cmd.value) & 0xFF
  93. else:
  94. result.append(Add(off, cmd.value))
  95. elif isinstance(cmd, MultAdd):
  96. # Try to fuse into previous command
  97. off = cmd.destOff + offset
  98. prev = result[-1] if len(result) >= 1 else None
  99. if isinstance(prev, Assign) and prev.offset == off and prev.value == 0:
  100. result[-1] = MultAssign(cmd.srcOff + offset, off, cmd.value)
  101. else:
  102. result.append(MultAdd(cmd.srcOff + offset, off, cmd.value))
  103. elif isinstance(cmd, Right):
  104. offset += cmd.offset
  105. elif isinstance(cmd, Input):
  106. result.append(Input(cmd.offset + offset))
  107. elif isinstance(cmd, Output):
  108. result.append(Output(cmd.offset + offset))
  109. else:
  110. # Commit the pointer movement before starting a loop/if
  111. if offset != 0:
  112. result.append(Right(offset))
  113. offset = 0
  114. if isinstance(cmd, Loop):
  115. temp = optimize_simple_loop(cmd.commands)
  116. if temp is not None:
  117. result.extend(temp)
  118. else:
  119. temp = optimize_complex_loop(cmd.commands)
  120. if temp is not None:
  121. result.append(temp)
  122. else:
  123. result.append(Loop(optimize(cmd.commands)))
  124. elif isinstance(cmd, If):
  125. result.append(If(optimize(cmd.commands)))
  126. else:
  127. raise AssertionError("Unknown command")
  128. # Commit the pointer movement before exiting this block
  129. if offset != 0:
  130. result.append(Right(offset))
  131. return result
  132. # Tries to optimize the given list of looped commands into a list that would be executed without looping. Returns None if not possible.
  133. def optimize_simple_loop(commands):
  134. deltas = {} # delta[i] = v means that in each loop iteration, mem[p + i] is added by the amount v
  135. offset = 0
  136. for cmd in commands:
  137. # This implementation can only optimize loops that consist of only Add and Right
  138. if isinstance(cmd, Add):
  139. off = cmd.offset + offset
  140. deltas[off] = deltas.get(off, 0) + cmd.value
  141. elif isinstance(cmd, Right):
  142. offset += cmd.offset
  143. else:
  144. return None
  145. # Can't optimize if a loop iteration has a net pointer movement, or if the cell being tested isn't decremented by 1
  146. if offset != 0 or deltas.get(0, 0) != -1:
  147. return None
  148. # Convert the loop into a list of multiply-add commands that source from the cell being tested
  149. del deltas[0]
  150. result = []
  151. for off in sorted(deltas.keys()):
  152. result.append(MultAdd(0, off, deltas[off]))
  153. result.append(Assign(0, 0))
  154. return result
  155. # Attempts to convert the body of a while-loop into an if-statement. This is possible if roughly all these conditions are met:
  156. # - There are no commands other than Add/Assign/MultAdd/MultAssign (in particular, no net movement, I/O, or embedded loops)
  157. # - The value at offset 0 is decremented by 1
  158. # - All MultAdd and MultAssign commands read from {an offset other than 0 whose value is cleared before the end in the loop}
  159. def optimize_complex_loop(commands):
  160. result = []
  161. origindelta = 0
  162. clears = set([0])
  163. for cmd in commands:
  164. if isinstance(cmd, Add):
  165. if cmd.offset == 0:
  166. origindelta += cmd.value
  167. else:
  168. clears.discard(cmd.offset)
  169. result.append(MultAdd(0, cmd.offset, cmd.value))
  170. elif isinstance(cmd, (MultAdd,MultAssign)):
  171. if cmd.destOff == 0:
  172. return None
  173. clears.discard(cmd.destOff)
  174. result.append(cmd)
  175. elif isinstance(cmd, Assign):
  176. if cmd.offset == 0:
  177. return None
  178. else:
  179. if cmd.value == 0:
  180. clears.add(cmd.offset)
  181. else:
  182. clears.discard(cmd.offset)
  183. result.append(cmd)
  184. else:
  185. return None
  186. if origindelta != -1:
  187. return None
  188. for cmd in result:
  189. if isinstance(cmd, (MultAdd,MultAssign)) and cmd.srcOff not in clears:
  190. return None
  191. result.append(Assign(0, 0))
  192. return If(result)
  193. # ---- Output formatters ----
  194. def commands_to_c(commands, name, maincall=True, indentlevel=1):
  195. def indent(line, level=indentlevel):
  196. return "\t" * level + line + "\n"
  197. result = ""
  198. if maincall:
  199. result += indent("#include <stdint.h>", 0)
  200. result += indent("#include <stdio.h>", 0)
  201. result += indent("", 0)
  202. result += indent("static uint8_t read() {", 0)
  203. result += indent("int temp = getchar();", 1)
  204. result += indent("return (uint8_t)(temp != EOF ? temp : 0);", 1)
  205. result += indent("}", 0)
  206. result += indent("", 0)
  207. result += indent("int main(int argc, char **argv) {", 0)
  208. result += indent("uint8_t mem[1000000] = {};")
  209. result += indent("uint8_t *p = &mem[1000];")
  210. result += indent("")
  211. for cmd in commands:
  212. if isinstance(cmd, Assign):
  213. result += indent("p[{}] = {};".format(cmd.offset, cmd.value))
  214. elif isinstance(cmd, Add):
  215. s = "p[{}]".format(cmd.offset)
  216. if cmd.value == 1:
  217. s += "++;"
  218. elif cmd.value == -1:
  219. s += "--;"
  220. else:
  221. s += " {}= {};".format("+" if cmd.value >= 0 else "-", abs(cmd.value))
  222. result += indent(s)
  223. elif isinstance(cmd, MultAssign):
  224. if cmd.value == 1:
  225. result += indent("p[{}] = p[{}];".format(cmd.destOff, cmd.srcOff))
  226. else:
  227. result += indent("p[{}] = p[{}] * {};".format(cmd.destOff, cmd.srcOff, cmd.value))
  228. elif isinstance(cmd, MultAdd):
  229. if abs(cmd.value) == 1:
  230. result += indent("p[{}] {}= p[{}];".format(cmd.destOff, "+" if cmd.value >= 0 else "-", cmd.srcOff))
  231. else:
  232. result += indent("p[{}] {}= p[{}] * {};".format(cmd.destOff, "+" if cmd.value >= 0 else "-", cmd.srcOff, abs(cmd.value)))
  233. elif isinstance(cmd, Right):
  234. if cmd.offset == 1:
  235. result += indent("p++;")
  236. elif cmd.offset == -1:
  237. result += indent("p--;")
  238. else:
  239. result += indent("p {}= {};".format("+" if cmd.offset >= 0 else "-", abs(cmd.offset)))
  240. elif isinstance(cmd, Input):
  241. result += indent("p[{}] = read();".format(cmd.offset))
  242. elif isinstance(cmd, Output):
  243. result += indent("putchar(p[{}]);".format(cmd.offset))
  244. elif isinstance(cmd, If):
  245. result += indent("if (*p != 0) {")
  246. result += commands_to_c(cmd.commands, name, False, indentlevel + 1)
  247. result += indent("}")
  248. elif isinstance(cmd, Loop):
  249. result += indent("while (*p != 0) {")
  250. result += commands_to_c(cmd.commands, name, False, indentlevel + 1)
  251. result += indent("}")
  252. else: raise AssertionError("Unknown command")
  253. if maincall:
  254. result += indent("")
  255. result += indent("return 0;")
  256. result += indent("}", 0)
  257. return result
  258. def commands_to_java(commands, name, maincall=True, indentlevel=2):
  259. def indent(line, level=indentlevel):
  260. return "\t" * level + line + "\n"
  261. result = ""
  262. if maincall:
  263. result += indent("import java.io.IOException;", 0)
  264. result += indent("", 0)
  265. result += indent("public class " + name + " {", 0)
  266. result += indent("public static void main(String[] args) throws IOException {", 1)
  267. result += indent("byte[] mem = new byte[1000000];")
  268. result += indent("int i = 1000;")
  269. result += indent("")
  270. def format_memory(off):
  271. if off == 0:
  272. return "mem[i]"
  273. else:
  274. return "mem[i {} {}]".format("+" if off >= 0 else "-", abs(off))
  275. for cmd in commands:
  276. if isinstance(cmd, Assign):
  277. result += indent("{} = {};".format(format_memory(cmd.offset), (cmd.value & 0xFF) - ((cmd.value & 0x80) << 1)))
  278. elif isinstance(cmd, Add):
  279. if cmd.value == 1:
  280. result += indent("{}++;".format(format_memory(cmd.offset)))
  281. elif cmd.value == -1:
  282. result += indent("{}--;".format(format_memory(cmd.offset)))
  283. else:
  284. result += indent("{} {}= {};".format(format_memory(cmd.offset), "+" if cmd.value >= 0 else "-", abs(cmd.value)))
  285. elif isinstance(cmd, MultAssign):
  286. if cmd.value == 1:
  287. result += indent("{} = {};".format(format_memory(cmd.destOff), format_memory(cmd.srcOff)))
  288. else:
  289. result += indent("{} = (byte)({} * {});".format(format_memory(cmd.destOff), format_memory(cmd.srcOff), cmd.value))
  290. elif isinstance(cmd, MultAdd):
  291. if abs(cmd.value) == 1:
  292. result += indent("{} {}= {};".format(format_memory(cmd.destOff), "+" if cmd.value >= 0 else "-", format_memory(cmd.srcOff)))
  293. else:
  294. result += indent("{} {}= {} * {};".format(format_memory(cmd.destOff), "+" if cmd.value >= 0 else "-", format_memory(cmd.srcOff), abs(cmd.value)))
  295. elif isinstance(cmd, Right):
  296. if cmd.offset == 1:
  297. result += indent("i++;")
  298. elif cmd.offset == -1:
  299. result += indent("i--;")
  300. else:
  301. result += indent("i {}= {};".format("+" if cmd.offset >= 0 else "-", abs(cmd.offset)))
  302. elif isinstance(cmd, Input):
  303. result += indent("{} = (byte)Math.max(System.in.read(), 0);".format(format_memory(cmd.offset)))
  304. elif isinstance(cmd, Output):
  305. result += indent("System.out.write({});".format(format_memory(cmd.offset))) + indent("System.out.flush();")
  306. elif isinstance(cmd, If):
  307. result += indent("if (mem[i] != 0) {")
  308. result += commands_to_java(cmd.commands, name, False, indentlevel + 1)
  309. result += indent("}")
  310. elif isinstance(cmd, Loop):
  311. result += indent("while (mem[i] != 0) {")
  312. result += commands_to_java(cmd.commands, name, False, indentlevel + 1)
  313. result += indent("}")
  314. else: raise AssertionError("Unknown command")
  315. if maincall:
  316. result += indent("}", 1)
  317. result += indent("}", 0)
  318. return result
  319. def commands_to_python(commands, name, maincall=True, indentlevel=0):
  320. def indent(line, level=indentlevel):
  321. return "\t" * level + line + "\n"
  322. result = ""
  323. if maincall:
  324. result += indent("import sys")
  325. result += indent("")
  326. result += indent("mem = [0] * 1000000")
  327. result += indent("i = 1000")
  328. result += indent("")
  329. def format_memory(off):
  330. if off == 0:
  331. return "mem[i]"
  332. else:
  333. return "mem[i {} {}]".format("+" if off >= 0 else "-", abs(off))
  334. for cmd in commands:
  335. if isinstance(cmd, Assign):
  336. result += indent("{} = {}".format(format_memory(cmd.offset), cmd.value))
  337. elif isinstance(cmd, Add):
  338. result += indent("{} = ({} {} {}) & 0xFF".format(format_memory(cmd.offset), format_memory(cmd.offset), "+" if cmd.value >= 0 else "-", abs(cmd.value)))
  339. elif isinstance(cmd, MultAssign):
  340. if cmd.value == 1:
  341. result += indent("{} = {}".format(format_memory(cmd.destOff), format_memory(cmd.srcOff)))
  342. else:
  343. result += indent("{} = ({} * {}) & 0xFF".format(format_memory(cmd.destOff), format_memory(cmd.srcOff), cmd.value))
  344. elif isinstance(cmd, MultAdd):
  345. result += indent("{} = ({} + {} * {}) & 0xFF".format(format_memory(cmd.destOff), format_memory(cmd.destOff), format_memory(cmd.srcOff), cmd.value))
  346. elif isinstance(cmd, Right):
  347. result += indent("i {}= {}".format("+" if cmd.offset >= 0 else "-", abs(cmd.offset)))
  348. elif isinstance(cmd, Input):
  349. result += indent("{} = ord((sys.stdin.read(1) + chr(0))[0])".format(format_memory(cmd.offset)))
  350. elif isinstance(cmd, Output):
  351. result += indent("sys.stdout.write(chr({}))".format(format_memory(cmd.offset)))
  352. elif isinstance(cmd, If):
  353. result += indent("if mem[i] != 0:")
  354. result += commands_to_python(cmd.commands, name, False, indentlevel + 1)
  355. elif isinstance(cmd, Loop):
  356. result += indent("while mem[i] != 0:")
  357. result += commands_to_python(cmd.commands, name, False, indentlevel + 1)
  358. else: raise AssertionError("Unknown command")
  359. return result
  360. # ---- Intermediate representation (IR) ----
  361. class Command(object): # Common superclass
  362. pass
  363. class Assign(Command):
  364. def __init__(self, offset, value):
  365. self.offset = offset
  366. self.value = value
  367. class Add(Command):
  368. def __init__(self, offset, value):
  369. self.offset = offset
  370. self.value = value
  371. class MultAssign(Command):
  372. def __init__(self, srcOff, destOff, value):
  373. self.srcOff = srcOff
  374. self.destOff = destOff
  375. self.value = value
  376. class MultAdd(Command):
  377. def __init__(self, srcOff, destOff, value):
  378. self.srcOff = srcOff
  379. self.destOff = destOff
  380. self.value = value
  381. class Right(Command):
  382. def __init__(self, offset):
  383. self.offset = offset
  384. class Input(Command):
  385. def __init__(self, offset):
  386. self.offset = offset
  387. class Output(Command):
  388. def __init__(self, offset):
  389. self.offset = offset
  390. class If(Command):
  391. def __init__(self, commands):
  392. self.commands = commands
  393. class Loop(Command):
  394. def __init__(self, commands):
  395. self.commands = commands
  396. # ---- Miscellaneous ----
  397. if __name__ == "__main__":
  398. errmsg = main(sys.argv[1:])
  399. if errmsg is not None:
  400. print >>sys.stderr, errmsg
  401. sys.exit(1)