zkas.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  1. import argparse
  2. import sys
  3. from zkas.types import *
  4. class CompileException(Exception):
  5. def __init__(self, error_message, line):
  6. super().__init__(error_message)
  7. self.error_message = error_message
  8. self.line = line
  9. class Constants:
  10. def __init__(self):
  11. self.table = []
  12. self.map = {}
  13. def add(self, variable, type_id):
  14. idx = len(self.table)
  15. self.table.append(type_id)
  16. self.map[variable] = idx
  17. def lookup(self, variable):
  18. idx = self.map[variable]
  19. return self.table[idx]
  20. def variables(self):
  21. return self.map.keys()
  22. class SyntaxStruct:
  23. def __init__(self):
  24. self.contracts = {}
  25. self.circuits = {}
  26. self.constants = Constants()
  27. def parse_contract(self, line, it):
  28. assert line.tokens[0] == "contract"
  29. if len(line.tokens) != 3 or line.tokens[2] != "{":
  30. raise CompileException("malformed contract opening", line)
  31. name = line.tokens[1]
  32. if name in self.contracts:
  33. raise CompileException(f"duplicate contract {name}", line)
  34. lines = []
  35. while True:
  36. try:
  37. line = next(it)
  38. except StopIteration:
  39. raise CompileException(
  40. f"premature end of file while parsing {name} contract", line)
  41. assert len(line.tokens) > 0
  42. if line.tokens[0] == "}":
  43. break
  44. lines.append(line)
  45. self.contracts[name] = lines
  46. def parse_circuit(self, line, it):
  47. assert line.tokens[0] == "circuit"
  48. if len(line.tokens) != 3 or line.tokens[2] != "{":
  49. raise CompileException("malformed circuit opening", line)
  50. name = line.tokens[1]
  51. if name in self.circuits:
  52. raise CompileException(f"duplicate contract {name}", line)
  53. lines = []
  54. while True:
  55. try:
  56. line = next(it)
  57. except StopIteration:
  58. raise CompileException(
  59. f"premature end of file while parsing {name} circuit", line)
  60. assert len(line.tokens) > 0
  61. if line.tokens[0] == "}":
  62. break
  63. lines.append(line)
  64. self.circuits[name] = lines
  65. def parse_constant(self, line):
  66. assert line.tokens[0] == "constant"
  67. if len(line.tokens) != 3:
  68. raise CompileException("malformed constant line", line)
  69. _, type_name, variable = line.tokens
  70. if type_name not in allowed_types:
  71. raise CompileException("unknown type '{type}'", line)
  72. type_id = allowed_types[type_name]
  73. self.constants.add(variable, type_id)
  74. def verify(self):
  75. self.static_checks()
  76. schema = self.format_data()
  77. self.trace_circuits(schema)
  78. return schema
  79. def static_checks(self):
  80. for name, lines in self.contracts.items():
  81. for line in lines:
  82. if len(line.tokens) != 2:
  83. raise CompileException("incorrect number of tokens", line)
  84. type, variable = line.tokens
  85. if type not in allowed_types:
  86. raise CompileException(
  87. f"unknown type specifier for variable {variable}", line)
  88. for name, lines in self.circuits.items():
  89. for line in lines:
  90. assert len(line.tokens) > 0
  91. func_name, args = line.tokens[0], line.tokens[1:]
  92. if func_name not in function_formats:
  93. raise CompileException(f"unknown function call {func_name}",
  94. line)
  95. func_format = function_formats[func_name]
  96. if len(args) != func_format.total_arguments():
  97. raise CompileException(
  98. f"incorrect number of arguments for function call {func_name}", line)
  99. # Finally check there are matching circuits and contracts
  100. all_names = set(self.circuits.keys()) | set(self.contracts.keys())
  101. for name in all_names:
  102. if name not in self.contracts:
  103. raise CompileException(f"missing contract for {name}", None)
  104. if name not in self.circuits:
  105. raise CompileException(f"missing circuit for {name}", None)
  106. def format_data(self):
  107. schema = []
  108. for name, circuit in self.circuits.items():
  109. assert name in self.contracts
  110. contract = self.contracts[name]
  111. witness = []
  112. for line in contract:
  113. assert len(line.tokens) == 2
  114. type_name, variable = line.tokens
  115. assert type_name in allowed_types
  116. type_id = allowed_types[type_name]
  117. witness.append((type_id, variable, line))
  118. code = []
  119. for line in circuit:
  120. assert len(line.tokens) > 0
  121. func_name, args = line.tokens[0], line.tokens[1:]
  122. assert func_name in function_formats
  123. func_format = function_formats[func_name]
  124. assert len(args) == func_format.total_arguments()
  125. return_values = []
  126. if func_format.return_type_ids:
  127. rv_len = len(func_format.return_type_ids)
  128. return_values, args = args[:rv_len], args[rv_len:]
  129. func_id = func_format.func_id
  130. code.append((func_format, return_values, args, line))
  131. schema.append((name, witness, code))
  132. return schema
  133. def trace_circuits(self, schema):
  134. for name, witness, code in schema:
  135. tracer = DynamicTracer(name, witness, code, self.constants)
  136. tracer.execute()
  137. class DynamicTracer:
  138. def __init__(self, name, contract_witness, circuit_code, constants):
  139. self.name = name
  140. self.witness = contract_witness
  141. self.code = circuit_code
  142. self.constants = constants
  143. def execute(self):
  144. stack = {}
  145. # Load constants
  146. for variable in self.constants.variables():
  147. stack[variable] = self.constants.lookup(variable)
  148. # Preload stack with our witness values
  149. for type_id, variable, line in self.witness:
  150. stack[variable] = type_id
  151. for i, (func_format, return_values, args, code_line) \
  152. in enumerate(self.code):
  153. assert len(args) == len(func_format.param_types)
  154. for variable, type_id in zip(args, func_format.param_types):
  155. if variable not in stack:
  156. raise CompileException(
  157. f"variable '{variable}' is not defined", code_line)
  158. stack_type_id = stack[variable]
  159. if stack_type_id != type_id:
  160. type_name = type_id_to_name[type_id]
  161. stack_type_name = type_id_to_name[stack_type_id]
  162. raise CompileException(
  163. f"variable '{variable}' has incorrect type. "
  164. f"Found {type_name} but expected variable of "
  165. f"type {stack_type_name}", code_line)
  166. assert len(return_values) == len(func_format.return_type_ids)
  167. for return_variable, return_type_id \
  168. in zip(return_values, func_format.return_type_ids):
  169. # Note that later variables shadow earlier ones.
  170. # We accept this.
  171. stack[return_variable] = return_type_id
  172. class CodeLine:
  173. def __init__(self, func_format, return_values, args, arg_idxs, code_line):
  174. self.func_format = func_format
  175. self.return_values = return_values
  176. self.args = args
  177. self.arg_idxs = arg_idxs
  178. self.code_line = code_line
  179. def func_name(self):
  180. return func_id_to_name[self.func_format.func_id]
  181. class CompiledContract:
  182. def __init__(self, name, witness, code):
  183. self.name = name
  184. self.witness = witness
  185. self.code = code
  186. class Compiler:
  187. def __init__(self, witness, uncompiled_code, constants):
  188. self.witness = witness
  189. self.uncompiled_code = uncompiled_code
  190. self.constants = constants
  191. def compile(self):
  192. code = []
  193. # Each unique type_id has its own stack
  194. stacks = [[] for i in range(TYPE_ID_LAST)]
  195. # Map from variable name to stacks above
  196. stack_vars = {}
  197. def alloc(variable, type_id):
  198. assert type_id <= len(stacks)
  199. idx = len(stacks[type_id])
  200. # Add variable to the stack for its type_id
  201. stacks[type_id].append(variable)
  202. # Create mapping from variable name
  203. stack_vars[variable] = (type_id, idx)
  204. # Load constants
  205. for variable in self.constants.variables():
  206. type_id = self.constants.lookup(variable)
  207. alloc(variable, type_id)
  208. # Preload stack with our witness values
  209. for type_id, variable, line in self.witness:
  210. alloc(variable, type_id)
  211. for i, (func_format, return_values, args, code_line) \
  212. in enumerate(self.uncompiled_code):
  213. assert len(args) == len(func_format.param_types)
  214. arg_idxs = []
  215. # Loop through all arguments
  216. for variable, type_id in zip(args, func_format.param_types):
  217. assert type_id <= len(stacks)
  218. assert variable in stack_vars
  219. # Find the index for the M by N matrix of our variable
  220. loc_type_id, loc_idx = stack_vars[variable]
  221. assert type_id == loc_type_id
  222. assert stacks[loc_type_id][loc_idx] == variable
  223. # This is the info to be serialized, not the variable names
  224. arg_idxs.append(loc_idx)
  225. assert len(return_values) == len(func_format.return_type_ids)
  226. for return_variable, return_type_id \
  227. in zip(return_values, func_format.return_type_ids):
  228. # Allocate returned values so they can be used by
  229. # subsequent function calls.
  230. alloc(return_variable, return_type_id)
  231. code.append(CodeLine(func_format, return_values, args,
  232. arg_idxs, code_line))
  233. return code
  234. class Line:
  235. def __init__(self, tokens, original_line, number):
  236. self.tokens = tokens
  237. self.orig = original_line
  238. self.number = number
  239. def __repr__(self):
  240. return f"Line({self.number}: {str(self.tokens)})"
  241. def load(src_file):
  242. source = []
  243. for i, original_line in enumerate(src_file):
  244. # Remove whitespace on both sides
  245. line = original_line.strip()
  246. # Strip out comments
  247. line = line.split("#")[0]
  248. # Split at whitespace
  249. line = line.split()
  250. if not line:
  251. continue
  252. line_number = i + 1
  253. source.append(Line(line, original_line, line_number))
  254. return source
  255. def parse(source):
  256. syntax = SyntaxStruct()
  257. it = iter(source)
  258. while True:
  259. try:
  260. line = next(it)
  261. except StopIteration:
  262. break
  263. assert len(line.tokens) > 0
  264. if line.tokens[0] == "contract":
  265. syntax.parse_contract(line, it)
  266. elif line.tokens[0] == "circuit":
  267. syntax.parse_circuit(line, it)
  268. elif line.tokens[0] == "constant":
  269. syntax.parse_constant(line)
  270. elif line.tokens[0] == "}":
  271. raise CompileException("unmatched delimiter '}'", line)
  272. return syntax
  273. def main():
  274. parser = argparse.ArgumentParser()
  275. parser.add_argument("SOURCE", help="ZK script to compile")
  276. parser.add_argument("--output", default=None, help="output file")
  277. group = parser.add_mutually_exclusive_group()
  278. group.add_argument('--display', action='store_true',
  279. help="show the compiled code in human readable format")
  280. group.add_argument('--bincode', action='store_true',
  281. help="output compiled code to zkvm supervisor")
  282. args = parser.parse_args()
  283. with open(args.SOURCE, "r") as src_file:
  284. source = load(src_file)
  285. try:
  286. syntax = parse(source)
  287. schema = syntax.verify()
  288. contracts = []
  289. for name, witness, uncompiled_code in schema:
  290. compiler = Compiler(witness, uncompiled_code, syntax.constants)
  291. code = compiler.compile()
  292. contracts.append(CompiledContract(name, witness, code))
  293. constants = syntax.constants
  294. if args.display:
  295. from zkas.text_output import output
  296. if args.output is None:
  297. output(sys.stdout, contracts, constants)
  298. else:
  299. with open(outpath, "w") as file:
  300. output(file, contracts, constants)
  301. elif args.bincode:
  302. from zkas.bincode_output import output
  303. outpath = args.output
  304. if args.output is None:
  305. outpath = args.SOURCE + ".bin"
  306. with open(outpath, "wb") as file:
  307. output(file, contracts, constants)
  308. else:
  309. from zkas.text_output import output
  310. if args.output is None:
  311. output(sys.stdout, contracts, constants)
  312. else:
  313. with open(outpath, "w") as file:
  314. output(file, contracts, constants)
  315. except CompileException as ex:
  316. print(f"Error: {ex.error_message}", file=sys.stderr)
  317. if ex.line is not None:
  318. print(f"Line {ex.line.number}: {ex.line.orig}", file=sys.stderr)
  319. #return -1
  320. raise
  321. return 0
  322. if __name__ == "__main__":
  323. sys.exit(main())
  324. # todo: think about extendable payment scheme which
  325. # is like bitcoin soft forks