| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402 |
- import argparse
- import sys
- from zkas.types import *
- class CompileException(Exception):
- def __init__(self, error_message, line):
- super().__init__(error_message)
- self.error_message = error_message
- self.line = line
- class Constants:
- def __init__(self):
- self.table = []
- self.map = {}
- def add(self, variable, type_id):
- idx = len(self.table)
- self.table.append(type_id)
- self.map[variable] = idx
- def lookup(self, variable):
- idx = self.map[variable]
- return self.table[idx]
- def variables(self):
- return self.map.keys()
- class SyntaxStruct:
- def __init__(self):
- self.contracts = {}
- self.circuits = {}
- self.constants = Constants()
- def parse_contract(self, line, it):
- assert line.tokens[0] == "contract"
- if len(line.tokens) != 3 or line.tokens[2] != "{":
- raise CompileException("malformed contract opening", line)
- name = line.tokens[1]
- if name in self.contracts:
- raise CompileException(f"duplicate contract {name}", line)
- lines = []
- while True:
- try:
- line = next(it)
- except StopIteration:
- raise CompileException(
- f"premature end of file while parsing {name} contract", line)
- assert len(line.tokens) > 0
- if line.tokens[0] == "}":
- break
- lines.append(line)
- self.contracts[name] = lines
- def parse_circuit(self, line, it):
- assert line.tokens[0] == "circuit"
- if len(line.tokens) != 3 or line.tokens[2] != "{":
- raise CompileException("malformed circuit opening", line)
- name = line.tokens[1]
- if name in self.circuits:
- raise CompileException(f"duplicate contract {name}", line)
- lines = []
- while True:
- try:
- line = next(it)
- except StopIteration:
- raise CompileException(
- f"premature end of file while parsing {name} circuit", line)
- assert len(line.tokens) > 0
- if line.tokens[0] == "}":
- break
- lines.append(line)
- self.circuits[name] = lines
- def parse_constant(self, line):
- assert line.tokens[0] == "constant"
- if len(line.tokens) != 3:
- raise CompileException("malformed constant line", line)
- _, type_name, variable = line.tokens
- if type_name not in allowed_types:
- raise CompileException("unknown type '{type}'", line)
- type_id = allowed_types[type_name]
- self.constants.add(variable, type_id)
- def verify(self):
- self.static_checks()
- schema = self.format_data()
- self.trace_circuits(schema)
- return schema
- def static_checks(self):
- for name, lines in self.contracts.items():
- for line in lines:
- if len(line.tokens) != 2:
- raise CompileException("incorrect number of tokens", line)
- type, variable = line.tokens
- if type not in allowed_types:
- raise CompileException(
- f"unknown type specifier for variable {variable}", line)
- for name, lines in self.circuits.items():
- for line in lines:
- assert len(line.tokens) > 0
- func_name, args = line.tokens[0], line.tokens[1:]
- if func_name not in function_formats:
- raise CompileException(f"unknown function call {func_name}",
- line)
- func_format = function_formats[func_name]
- if len(args) != func_format.total_arguments():
- raise CompileException(
- f"incorrect number of arguments for function call {func_name}", line)
- # Finally check there are matching circuits and contracts
- all_names = set(self.circuits.keys()) | set(self.contracts.keys())
- for name in all_names:
- if name not in self.contracts:
- raise CompileException(f"missing contract for {name}", None)
- if name not in self.circuits:
- raise CompileException(f"missing circuit for {name}", None)
- def format_data(self):
- schema = []
- for name, circuit in self.circuits.items():
- assert name in self.contracts
- contract = self.contracts[name]
- witness = []
- for line in contract:
- assert len(line.tokens) == 2
- type_name, variable = line.tokens
- assert type_name in allowed_types
- type_id = allowed_types[type_name]
- witness.append((type_id, variable, line))
- code = []
- for line in circuit:
- assert len(line.tokens) > 0
- func_name, args = line.tokens[0], line.tokens[1:]
- assert func_name in function_formats
- func_format = function_formats[func_name]
- assert len(args) == func_format.total_arguments()
- return_values = []
- if func_format.return_type_ids:
- rv_len = len(func_format.return_type_ids)
- return_values, args = args[:rv_len], args[rv_len:]
- func_id = func_format.func_id
- code.append((func_format, return_values, args, line))
- schema.append((name, witness, code))
- return schema
- def trace_circuits(self, schema):
- for name, witness, code in schema:
- tracer = DynamicTracer(name, witness, code, self.constants)
- tracer.execute()
- class DynamicTracer:
- def __init__(self, name, contract_witness, circuit_code, constants):
- self.name = name
- self.witness = contract_witness
- self.code = circuit_code
- self.constants = constants
- def execute(self):
- stack = {}
- # Load constants
- for variable in self.constants.variables():
- stack[variable] = self.constants.lookup(variable)
- # Preload stack with our witness values
- for type_id, variable, line in self.witness:
- stack[variable] = type_id
- for i, (func_format, return_values, args, code_line) \
- in enumerate(self.code):
- assert len(args) == len(func_format.param_types)
- for variable, type_id in zip(args, func_format.param_types):
- if variable not in stack:
- raise CompileException(
- f"variable '{variable}' is not defined", code_line)
- stack_type_id = stack[variable]
- if stack_type_id != type_id:
- type_name = type_id_to_name[type]
- stack_type_name = type_id_to_name[stack_type]
- raise CompileException(
- f"variable '{variable}' has incorrect type. "
- f"Found {type_name} but expected variable of "
- f"type {stack_type_name}", code_line)
- assert len(return_values) == len(func_format.return_type_ids)
- for return_variable, return_type_id \
- in zip(return_values, func_format.return_type_ids):
- # Note that later variables shadow earlier ones.
- # We accept this.
- stack[return_variable] = return_type_id
- class CodeLine:
- def __init__(self, func_format, return_values, args, arg_idxs, code_line):
- self.func_format = func_format
- self.return_values = return_values
- self.args = args
- self.arg_idxs = arg_idxs
- self.code_line = code_line
- def func_name(self):
- return func_id_to_name[self.func_format.func_id]
- class CompiledContract:
- def __init__(self, name, witness, code):
- self.name = name
- self.witness = witness
- self.code = code
- class Compiler:
- def __init__(self, witness, uncompiled_code, constants):
- self.witness = witness
- self.uncompiled_code = uncompiled_code
- self.constants = constants
- def compile(self):
- code = []
- # Each unique type_id has its own stack
- stacks = [[] for i in range(TYPE_ID_LAST)]
- # Map from variable name to stacks above
- stack_vars = {}
- def alloc(variable, type_id):
- assert type_id <= len(stacks)
- idx = len(stacks[type_id])
- # Add variable to the stack for its type_id
- stacks[type_id].append(variable)
- # Create mapping from variable name
- stack_vars[variable] = (type_id, idx)
- # Load constants
- for variable in self.constants.variables():
- type_id = self.constants.lookup(variable)
- alloc(variable, type_id)
- # Preload stack with our witness values
- for type_id, variable, line in self.witness:
- alloc(variable, type_id)
- for i, (func_format, return_values, args, code_line) \
- in enumerate(self.uncompiled_code):
- assert len(args) == len(func_format.param_types)
- arg_idxs = []
- # Loop through all arguments
- for variable, type_id in zip(args, func_format.param_types):
- assert type_id <= len(stacks)
- assert variable in stack_vars
- # Find the index for the M by N matrix of our variable
- loc_type_id, loc_idx = stack_vars[variable]
- assert type_id == loc_type_id
- assert stacks[loc_type_id][loc_idx] == variable
- # This is the info to be serialized, not the variable names
- arg_idxs.append(loc_idx)
- assert len(return_values) == len(func_format.return_type_ids)
- for return_variable, return_type_id \
- in zip(return_values, func_format.return_type_ids):
- # Allocate returned values so they can be used by
- # subsequent function calls.
- alloc(return_variable, return_type_id)
- code.append(CodeLine(func_format, return_values, args,
- arg_idxs, code_line))
- return code
- class Line:
- def __init__(self, tokens, original_line, number):
- self.tokens = tokens
- self.orig = original_line
- self.number = number
- def __repr__(self):
- return f"Line({self.number}: {str(self.tokens)})"
- def load(src_file):
- source = []
- for i, original_line in enumerate(src_file):
- # Remove whitespace on both sides
- line = original_line.strip()
- # Strip out comments
- line = line.split("#")[0]
- # Split at whitespace
- line = line.split()
- if not line:
- continue
- line_number = i + 1
- source.append(Line(line, original_line, line_number))
- return source
- def parse(source):
- syntax = SyntaxStruct()
- it = iter(source)
- while True:
- try:
- line = next(it)
- except StopIteration:
- break
- assert len(line.tokens) > 0
- if line.tokens[0] == "contract":
- syntax.parse_contract(line, it)
- elif line.tokens[0] == "circuit":
- syntax.parse_circuit(line, it)
- elif line.tokens[0] == "constant":
- syntax.parse_constant(line)
- elif line.tokens[0] == "}":
- raise CompileException("unmatched delimiter '}'", line)
- return syntax
- def main():
- parser = argparse.ArgumentParser()
- parser.add_argument("SOURCE", help="ZK script to compile")
- parser.add_argument("--output", default=None, help="output file")
- group = parser.add_mutually_exclusive_group()
- group.add_argument('--display', action='store_true',
- help="show the compiled code in human readable format")
- group.add_argument('--bincode', action='store_true',
- help="output compiled code to zkvm supervisor")
- args = parser.parse_args()
- with open(args.SOURCE, "r") as src_file:
- source = load(src_file)
- try:
- syntax = parse(source)
- schema = syntax.verify()
- contracts = []
- for name, witness, uncompiled_code in schema:
- compiler = Compiler(witness, uncompiled_code, syntax.constants)
- code = compiler.compile()
- contracts.append(CompiledContract(name, witness, code))
- constants = syntax.constants
- if args.display:
- from zkas.text_output import output
- if args.output is None:
- output(sys.stdout, contracts, constants)
- else:
- with open(outpath, "w") as file:
- output(file, contracts, constants)
- elif args.bincode:
- from zkas.bincode_output import output
- outpath = args.output
- if args.output is None:
- outpath = args.SOURCE + ".bin"
- with open(outpath, "wb") as file:
- output(file, contracts, constants)
- else:
- from zkas.text_output import output
- if args.output is None:
- output(sys.stdout, contracts, constants)
- else:
- with open(outpath, "w") as file:
- output(file, contracts, constants)
- except CompileException as ex:
- print(f"Error: {ex.error_message}", file=sys.stderr)
- if ex.line is not None:
- print(f"Line {ex.line.number}: {ex.line.orig}", file=sys.stderr)
- #return -1
- raise
- return 0
- if __name__ == "__main__":
- sys.exit(main())
- # todo: think about extendable payment scheme which
- # is like bitcoin soft forks
|