pism.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. import sys
  2. def eprint(*args):
  3. print(*args, file=sys.stderr)
  4. class Line:
  5. def __init__(self, text, line_number):
  6. self.text = text
  7. self.orig = text
  8. self.lineno = line_number
  9. self.clean()
  10. def clean(self):
  11. # Remove the comments
  12. self.text = self.text.split("#", 1)[0]
  13. # Remove whitespace
  14. self.text = self.text.strip()
  15. def is_empty(self):
  16. return bool(self.text)
  17. def __repr__(self):
  18. return "Line %s: %s" % (self.lineno, self.orig)
  19. def command(self):
  20. if not self.is_empty():
  21. return None
  22. return self.text.split(" ")[0]
  23. def args(self):
  24. if not self.is_empty():
  25. return None
  26. return self.text.split(" ")[1:]
  27. def clean(contents):
  28. # Split input into lines
  29. contents = contents.split("\n")
  30. contents = [Line(line, i) for i, line in enumerate(contents)]
  31. # Remove empty blank lines
  32. contents = [line for line in contents if line.is_empty()]
  33. return contents
  34. def make_segments(contents):
  35. constants = [line for line in contents if line.command() == "constant"]
  36. segments = []
  37. current_segment = []
  38. for line in contents:
  39. if line.command() == "contract":
  40. current_segment = []
  41. current_segment.append(line)
  42. if line.command() == "end":
  43. segments.append(current_segment)
  44. current_segment = []
  45. return constants, segments
  46. def build_constants_table(constants):
  47. table = {}
  48. for line in constants:
  49. args = line.args()
  50. if len(args) != 2:
  51. eprint("error: wrong number of args")
  52. eprint(line)
  53. return None
  54. name, type = args
  55. table[name] = type
  56. return table
  57. symbol_table = {
  58. "contract": 1,
  59. "param": 2,
  60. "start": 0,
  61. "end": 0,
  62. "witness": 2,
  63. }
  64. def extract(segment):
  65. assert segment
  66. # Does it have a declaration?
  67. if not segment[0].command() == "contract":
  68. eprint("error: missing contract declaration")
  69. eprint(segment[0])
  70. return None
  71. # Does it have an end?
  72. if not segment[-1].command() == "end":
  73. eprint("error: missing contract end")
  74. eprint(segment[-1])
  75. return None
  76. # Does it have a start?
  77. if not [line for line in segment if line.command() == "start"]:
  78. eprint("error: missing contract start")
  79. eprint(segment[0])
  80. return None
  81. for line in segment:
  82. command, args = line.command(), line.args()
  83. if symbol_table[command] != len(args):
  84. eprint("error: wrong number of args for command '%s'" % command)
  85. eprint(line)
  86. return None
  87. contract_name = segment[0].args()[0]
  88. start_index = [index for index, line in enumerate(segment)
  89. if line.command() == "start"]
  90. if len(start_index) > 1:
  91. eprint("error: multiple start statements in contract '%s'" %
  92. contract_name)
  93. for index in start_index:
  94. eprint(segment[index])
  95. eprint("Aborting.")
  96. return None
  97. assert len(start_index) == 1
  98. start_index = start_index[0]
  99. header = segment[1:start_index]
  100. code = segment[start_index + 1:-1]
  101. params = {}
  102. for param_decl in header:
  103. args = param_decl.args()
  104. assert len(args) == 2
  105. name, type = args
  106. params[name] = type
  107. program = []
  108. for line in code:
  109. command, args = line.command(), line.args()
  110. program.append((command, args, line))
  111. return Contract(contract_name, params, program)
  112. def to_initial_caps(snake_str):
  113. components = snake_str.split("_")
  114. return "".join(x.title() for x in components)
  115. types_map = {
  116. "U64": "u64",
  117. "Fr": "jubjub::Fr",
  118. "Point": "jubjub::SubgroupPoint",
  119. "Scalar": "bls12_381::Scalar",
  120. "Bool": "bool"
  121. }
  122. command_desc = {
  123. "witness": (("EdwardsPoint", True), ("Point", False))
  124. }
  125. class Contract:
  126. def __init__(self, name, params, program):
  127. self.name = name
  128. self.params = params
  129. self.program = program
  130. def _compile_header(self):
  131. code = "pub struct %s {\n" % to_initial_caps(self.name)
  132. for param_name, param_type in self.params.items():
  133. try:
  134. mapped_type = types_map[param_type]
  135. except KeyError:
  136. return None
  137. code += " pub %s: Option<%s>,\n" % (param_name, mapped_type)
  138. code += "}\n"
  139. return code
  140. def _compile_body(self):
  141. self.stack = {}
  142. code = "\n"
  143. #indent = " " * 8
  144. for command, args, line in self.program:
  145. if (code_text := self._compile_line(command, args, line)) is None:
  146. return None
  147. code += code_text + "\n"
  148. return code
  149. def _preprocess_args(self, args, line):
  150. nargs = []
  151. for arg in args:
  152. if not arg.startswith("param:"):
  153. nargs.append((arg, False))
  154. continue
  155. _, argname = arg.split(":", 1)
  156. if argname not in self.params:
  157. eprint("error: non-existant param referenced")
  158. eprint(line)
  159. return None
  160. nargs.append((argname, True))
  161. return nargs
  162. def type_checking(self, command, args, line):
  163. assert command in command_desc
  164. type_list = command_desc[command]
  165. if len(type_list) != len(args):
  166. eprint("error: wrong number of arguments!")
  167. eprint(line)
  168. return False
  169. for (expected_type, new_val), (argname, is_param) in \
  170. zip(type_list, args):
  171. # Only type check input arguments, not output values
  172. if new_val:
  173. continue
  174. if is_param:
  175. actual_type = self.params[argname]
  176. else:
  177. # Check the stack here
  178. if argname not in self.stack:
  179. eprint("error: cannot find value '%s' on the stack!" %
  180. argname)
  181. eprint(line)
  182. return False
  183. actual_type = self.stack[argname]
  184. return True
  185. def _compile_line(self, command, args, line):
  186. if (args := self._preprocess_args(args, line)) is None:
  187. return None
  188. if not self.type_checking(command, args, line):
  189. return None
  190. self.modify_stack(command, args)
  191. args = [self.carg(arg) for arg in args]
  192. if command == "witness":
  193. out, point = args
  194. return \
  195. r"""let %s = ecc::EdwardsPoint::witness(
  196. cs.namespace(|| "%s"),
  197. %s.map(jubjub::ExtendedPoint::from))?;""" % (out, line, point)
  198. def carg(self, arg):
  199. argname, is_param = arg
  200. if is_param:
  201. return "self.%s" % argname
  202. return argname
  203. def modify_stack(self, command, args):
  204. type_list = command_desc[command]
  205. assert len(type_list) == len(args)
  206. for (expected_type, new_val), (argname, is_param) in \
  207. zip(type_list, args):
  208. if is_param:
  209. assert not new_val
  210. continue
  211. # Now apply the new values to the stack
  212. if new_val:
  213. self.stack[argname] = expected_type
  214. def compile(self):
  215. code = ""
  216. if (header := self._compile_header()) is None:
  217. return None
  218. code += header
  219. code += \
  220. r"""impl Circuit<bls12_381::Scalar> for %s {
  221. fn synthesize<CS: ConstraintSystem<bls12_381::Scalar>>(
  222. self,
  223. cs: &mut CS,
  224. ) -> Result<(), SynthesisError> {
  225. """ % to_initial_caps(self.name)
  226. if (body := self._compile_body()) is None:
  227. return None
  228. code += body
  229. code += "}\n"
  230. return code
  231. def process(contents):
  232. contents = clean(contents)
  233. constants, segments = make_segments(contents)
  234. if (constants := build_constants_table(constants)) is None:
  235. return False
  236. codes = []
  237. for segment in segments:
  238. contract = extract(segment)
  239. if (code := contract.compile()) is None:
  240. return False
  241. codes.append(code)
  242. # Success! Output finished product.
  243. [print(code) for code in codes]
  244. return True
  245. def main(argv):
  246. if len(argv) != 2:
  247. eprint("pism FILENAME")
  248. return -1
  249. contents = open(argv[1]).read()
  250. if not process(contents):
  251. return -2
  252. return 0
  253. if __name__ == "__main__":
  254. sys.exit(main(sys.argv))