vm.py 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. import argparse
  2. import sys
  3. from enum import Enum
  4. alloc_commands = {
  5. "param": 1,
  6. "private": 1,
  7. "public": 1,
  8. }
  9. op_commands = {
  10. "set": 2,
  11. "mul": 2,
  12. "local": 1,
  13. }
  14. constraint_commands = {
  15. "lc0_add": 1,
  16. "lc1_add": 1,
  17. "lc2_add": 1,
  18. "lc0_add_one": 0,
  19. "lc1_add_one": 0,
  20. "lc2_add_one": 0,
  21. "enforce": 0,
  22. }
  23. def eprint(*args):
  24. print(*args, file=sys.stderr)
  25. class Line:
  26. def __init__(self, text, line_number):
  27. self.text = text
  28. self.orig = text
  29. self.lineno = line_number
  30. self.clean()
  31. def clean(self):
  32. # Remove the comments
  33. self.text = self.text.split("#", 1)[0]
  34. # Remove whitespace
  35. self.text = self.text.strip()
  36. def is_empty(self):
  37. return bool(self.text)
  38. def __repr__(self):
  39. return "Line %s: %s" % (self.lineno, self.orig.lstrip())
  40. def command(self):
  41. if not self.is_empty():
  42. return None
  43. return self.text.split(" ")[0]
  44. def args(self):
  45. if not self.is_empty():
  46. return None
  47. return self.text.split(" ")[1:]
  48. def clean(contents):
  49. # Split input into lines
  50. contents = contents.split("\n")
  51. contents = [Line(line, i) for i, line in enumerate(contents)]
  52. # Remove empty blank lines
  53. contents = [line for line in contents if line.is_empty()]
  54. return contents
  55. def divide_sections(contents):
  56. state = "NOSCOPE"
  57. segments = {}
  58. current_segment = []
  59. contract_name = None
  60. for line in contents:
  61. if line.command() == "contract":
  62. if len(line.args()) != 1:
  63. eprint("error: missing contract name")
  64. eprint(line)
  65. return None
  66. contract_name = line.args()[0]
  67. if state == "NOSCOPE":
  68. assert not current_segment
  69. state = "INSCOPE"
  70. continue
  71. else:
  72. assert state == "INSCOPE"
  73. eprint("error: double contract entry violation")
  74. eprint(line)
  75. return None
  76. elif line.command() == "end":
  77. if len(line.args()) != 0:
  78. eprint("error: end takes no args")
  79. eprint(line)
  80. return None
  81. if state == "NOSCOPE":
  82. eprint("error: missing contract start for end")
  83. eprint(line)
  84. return None
  85. else:
  86. assert state == "INSCOPE"
  87. state = "NOSCOPE"
  88. segments[contract_name] = current_segment
  89. current_segment = []
  90. continue
  91. elif state == "NOSCOPE":
  92. # Ignore lines outside any contract
  93. continue
  94. current_segment.append(line)
  95. if state != "NOSCOPE":
  96. eprint("error: reached end of file with unclosed scope")
  97. return None
  98. return segments
  99. def extract_relevant_lines(contract, commands_table):
  100. relevant_lines = []
  101. for line in contract:
  102. command = line.command()
  103. if command not in commands_table.keys():
  104. continue
  105. define = commands_table[command]
  106. if len(line.args()) != define:
  107. eprint("error: wrong number of args")
  108. return None
  109. relevant_lines.append(line)
  110. return relevant_lines
  111. class VariableType(Enum):
  112. PUBLIC = 1
  113. PRIVATE = 2
  114. class Variable:
  115. def __init__(self, symbol, index, type, is_param):
  116. self.symbol = symbol
  117. self.index = index
  118. self.type = type
  119. self.is_param = is_param
  120. def __repr__(self):
  121. return "<Variable %s:%s>" % (self.symbol, self.index)
  122. def generate_alloc_table(contract):
  123. relevant_lines = extract_relevant_lines(contract, alloc_commands)
  124. alloc_table = {}
  125. for i, line in enumerate(relevant_lines):
  126. assert len(line.args()) == 1
  127. symbol = line.args()[0]
  128. command = line.command()
  129. if command == "param":
  130. type = VariableType.PRIVATE
  131. is_param = True
  132. elif command == "private":
  133. type = VariableType.PRIVATE
  134. is_param = False
  135. elif command == "public":
  136. type = VariableType.PUBLIC
  137. is_param = False
  138. else:
  139. assert False
  140. alloc_table[symbol] = Variable(symbol, i, type, is_param)
  141. return alloc_table
  142. class Operation:
  143. def __init__(self, line, indexes):
  144. self.command = line.command()
  145. self.args = indexes
  146. self.line = line
  147. class VariableRefType(Enum):
  148. AUX = 1
  149. LOCAL = 2
  150. class VariableRef:
  151. def __init__(self, type, index):
  152. self.type = type
  153. self.index = index
  154. def __repr__(self):
  155. return "%s(%s)" % (self.type.name, self.index)
  156. def symbols_list_to_refs(line, alloc, local_vars):
  157. indexes = []
  158. for symbol in line.args():
  159. if symbol in alloc:
  160. # Lookup variable index
  161. index = alloc[symbol].index
  162. index = VariableRef(VariableRefType.AUX, index)
  163. elif symbol in local_vars:
  164. index = local_vars[symbol]
  165. index = VariableRef(VariableRefType.LOCAL, index)
  166. else:
  167. eprint("error: missing unallocated symbol")
  168. eprint(line)
  169. return None
  170. indexes.append(index)
  171. return indexes
  172. def generate_ops_table(contract, alloc):
  173. relevant_lines = extract_relevant_lines(contract, op_commands)
  174. ops = []
  175. local_vars = {}
  176. for line in relevant_lines:
  177. # This is a special case which creates a new local stack value
  178. if line.command() == "local":
  179. assert len(line.args()) == 1
  180. symbol = line.args()[0]
  181. local_vars[symbol] = len(local_vars)
  182. indexes = []
  183. else:
  184. if (indexes := symbols_list_to_refs(line, alloc,
  185. local_vars)) is None:
  186. return None
  187. ops.append(Operation(line, indexes))
  188. return ops
  189. class Constraint:
  190. def __init__(self, line, indexes):
  191. self.command = line.command()
  192. self.args = indexes
  193. self.line = line
  194. def args_comment(self):
  195. return ", ".join("%s" % symbol for symbol in self.line.args())
  196. def symbols_list_to_indexes(line, alloc):
  197. indexes = []
  198. for symbol in line.args():
  199. if symbol not in alloc:
  200. eprint("error: missing unallocated symbol")
  201. eprint(line)
  202. return None
  203. # Lookup variable index
  204. index = alloc[symbol].index
  205. indexes.append(index)
  206. return indexes
  207. def generate_constraints_table(contract, alloc):
  208. relevant_lines = extract_relevant_lines(contract, constraint_commands)
  209. constraints = []
  210. for line in relevant_lines:
  211. if (indexes := symbols_list_to_indexes(line, alloc)) is None:
  212. return None
  213. constraints.append(Constraint(line, indexes))
  214. return constraints
  215. class Contract:
  216. def __init__(self, alloc, ops, constraints):
  217. self.alloc = alloc
  218. self.ops = ops
  219. self.constraints = constraints
  220. def __repr__(self):
  221. repr_str = ""
  222. repr_str += "Alloc table:\n"
  223. for symbol, variable in self.alloc.items():
  224. repr_str += " // %s\n" % symbol
  225. repr_str += " %s %s\n" % (variable.type, variable.index)
  226. repr_str += "Operations:\n"
  227. for op in self.ops:
  228. repr_str += " // %s\n" % op.line
  229. repr_str += " %s %s\n" % (op.command, op.args)
  230. repr_str += "Constraints:\n"
  231. for constraint in self.constraints:
  232. if constraint.args:
  233. repr_str += " // %s\n" % constraint.args_comment()
  234. repr_str += " %s %s\n" % (constraint.command, constraint.args)
  235. return repr_str
  236. def compile(contract, constants):
  237. # Allocation table
  238. # symbol: Private/Public, is_param, index
  239. alloc = generate_alloc_table(contract)
  240. # Operations lines list
  241. if (ops := generate_ops_table(contract, alloc)) is None:
  242. return None
  243. # Constraint commands
  244. if (constraints := generate_constraints_table(contract, alloc)) is None:
  245. return None
  246. return Contract(alloc, ops, constraints)
  247. def process(contents):
  248. # Remove left whitespace
  249. contents = clean(contents)
  250. # Parse all constants
  251. constants = [line for line in contents if line.command() == "constant"]
  252. # Divide into contract sections
  253. if (pre_contracts := divide_sections(contents)) is None:
  254. return None
  255. # Process each contract
  256. contracts = {}
  257. for contract_name, pre_contract in pre_contracts.items():
  258. if (contract := compile(pre_contract, constants)) is None:
  259. return None
  260. contracts[contract_name] = contract
  261. return contracts
  262. def main(argv):
  263. parser = argparse.ArgumentParser()
  264. parser.add_argument("filename", help="VM PISM file: proofs/vm.pism")
  265. group = parser.add_mutually_exclusive_group()
  266. group.add_argument('--display', action='store_true',
  267. help="show the compiled code in human readable format")
  268. group.add_argument('--rust', action='store_true',
  269. help="output compiled code to rust for testing")
  270. args = parser.parse_args()
  271. src_filename = args.filename
  272. contents = open(src_filename).read()
  273. if (contracts := process(contents)) is None:
  274. return -2
  275. def default_display():
  276. for contract_name, contract in contracts.items():
  277. print("Contract:", contract_name)
  278. print(contract)
  279. if args.display:
  280. default_display()
  281. elif args.rust:
  282. import vm_export_rust
  283. for contract_name, contract in contracts.items():
  284. vm_export_rust.display(contract)
  285. else:
  286. default_display()
  287. return 0
  288. if __name__ == "__main__":
  289. sys.exit(main(sys.argv))