├── .gitignore ├── README.md ├── example-duet.py ├── example.py ├── isim-judge.py ├── judge ├── __init__.py ├── base.py ├── concurrent.py ├── diff.py ├── isim.py ├── judge.py ├── kits │ └── marsx.jar ├── logisim.py ├── mars.py └── utils.py └── logisim-judge.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | __pycache__ 3 | test* 4 | tmp 5 | *.bat 6 | *.sh 7 | *.json 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is inspired by [ 2 | BUAA-CO-Logisim-Judger](https://github.com/biopuppet/BUAA-CO-Logisim-Judger). 3 | 4 | This helps to verify MIPS CPU in Logisim circuits or Verilog HDL against MARS simulation based on given .asm programs. 5 | 6 | ## Getting started 7 | 8 | ### Logisim 9 | 10 | Set up the output pins in your `main` circuit in order of PC (32-bit by default), GRF_WRITE_ENABLED (1-bit), GRF_WRITE_ADDRESS (5-bit), GRF_WRITE_DATA (32-bit), DM_WRITE_ENABLED (1-bit), DM_WRITE_ADDRESS (32-bit by default), and DM_WRITE_DATA (32-bit). Dumped instructions for verification will be loaded into the ROM component automatically. 11 | 12 | ### Verilog (ISim) 13 | 14 | Your test bench should instantiate the CPU and provide clocks. At initialization or reset, it should `$readmemh` from `code.txt` into the instruction memory and `$display` writing accesses as the course requires. 15 | 16 | ## Usage 17 | 18 | ### CLI 19 | 20 | #### Logisim 21 | 22 | ```shell 23 | $ python logisim-judge.py mips.circ mips1.asm kits/logisim.jar --dm-address-width 5 --dm-address-by-word 24 | ``` 25 | 26 | ```shell 27 | $ python logisim-judge.py --help 28 | ``` 29 | 30 | #### Verilog (ISim) 31 | 32 | ```shell 33 | $ python isim-judge.py ise-projects/mips4 tb mips1.asm --recompile 34 | ``` 35 | Unless the switch `--recompile` is specified, latest changes on the sources may not take effect before a manual ISim simulation. 36 | 37 | ```shell 38 | $ python isim-judge.py ise-projects/mips5 tb mips1.asm --db 39 | ``` 40 | The switch `--db` enables delayed branching for MARS. 41 | 42 | ```shell 43 | $ python isim-judge.py --help 44 | ``` 45 | 46 | ### Python APIs 47 | 48 | Refer to [example.py](example.py) for a useful wheel. 49 | 50 | #### Example 51 | 52 | ```python 53 | from judge import Mars, ISim, Logisim, MarsJudge, DuetJudge, resolve_paths, INFINITE_LOOP 54 | 55 | isim = ISim('ise-projects/mips5', 'tb', appendix=INFINITE_LOOP) 56 | mars = Mars(db=True) 57 | judge = MarsJudge(isim, mars) 58 | 59 | judge('mips1.asm') 60 | judge.all(['mips1.asm', 'mips2.asm']) 61 | judge.all(resolve_paths('./cases')) 62 | judge.all(resolve_paths(['./cases', './extra-cases', 'mips1.asm'])) 63 | 64 | logisim = Logisim('mips.circ', 'kits/logisim.jar', appendix=INFINITE_LOOP) 65 | naive_mars = Mars() 66 | judge = MarsJudge(logisim, naive_mars) 67 | judge('mips1.asm') 68 | 69 | isim = ISim('ise-projects/mips7', 'tb', appendix=INFINITE_LOOP) 70 | judge = MarsJudge(isim, mars) 71 | judge.load_handler('handler.asm') 72 | judge('exceptions.asm') 73 | 74 | std = ISim('ise-projects/mips-std', 'tb', appendix=INFINITE_LOOP) 75 | judge = DuetJudge(isim, std, mars) 76 | judge('interrupts.asm') # Dui Pai 77 | ``` 78 | -------------------------------------------------------------------------------- /example-duet.py: -------------------------------------------------------------------------------- 1 | from judge import Mars, ISim, DuetJudge, resolve_paths, INFINITE_LOOP 2 | from judge.utils import CachedList 3 | 4 | project_path = 'ise-projects/mips7' 5 | module_name = 'tb' 6 | std_project_path = 'ise-projects/mips7-std' 7 | 8 | cases = [ 9 | 'cases/interrupts' 10 | ] 11 | 12 | db = True 13 | duration = 'all' 14 | appendix = INFINITE_LOOP 15 | timeout = 3 16 | recompile = True 17 | skip_passed_cases = True 18 | include_handler = True 19 | 20 | 21 | isim = ISim(project_path, module_name, duration=duration, 22 | appendix=appendix, recompile=recompile, timeout=timeout) 23 | std = ISim(std_project_path, module_name, duration=duration, 24 | appendix=appendix, recompile=recompile, timeout=timeout) 25 | mars = Mars(db=db, timeout=timeout) 26 | judge = DuetJudge(isim, std, mars) 27 | 28 | 29 | def main(): 30 | with CachedList('passes.json') as passes: 31 | blocklist = passes if skip_passed_cases else None 32 | paths = resolve_paths(cases, 33 | blocklist=blocklist, 34 | on_omit=lambda path: print('Omitting', path), 35 | ) 36 | judge.all(paths, 37 | self_handler=include_handler, 38 | on_success=passes.append, 39 | on_error=passes.close_some, 40 | ) 41 | 42 | 43 | if __name__ == '__main__': 44 | main() 45 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | from judge import Mars, ISim, MarsJudge, resolve_paths, INFINITE_LOOP 2 | from judge.utils import CachedList 3 | 4 | project_path = 'ise-projects/mips5' 5 | module_name = 'tb' 6 | 7 | cases = [ 8 | 'cases/5', 9 | 'cases/extra', 10 | 'cases/case*' 11 | ] 12 | 13 | db = True 14 | duration = 'all' 15 | appendix = INFINITE_LOOP 16 | timeout = 3 17 | recompile = False 18 | skip_passed_cases = True 19 | 20 | 21 | isim = ISim(project_path, module_name, duration=duration, 22 | appendix=appendix, recompile=recompile, timeout=timeout) 23 | mars = Mars(db=db, timeout=timeout) 24 | judge = MarsJudge(isim, mars) 25 | 26 | 27 | def main(): 28 | with CachedList('passes.json') as passes: 29 | blocklist = passes if skip_passed_cases else None 30 | paths = resolve_paths(cases, 31 | blocklist=blocklist, 32 | on_omit=lambda path: print('Omitting', path), 33 | ) 34 | # judge.load_handler(handler) 35 | judge.all(paths, 36 | on_success=passes.append, 37 | on_error=passes.close_some, 38 | ) 39 | 40 | 41 | if __name__ == '__main__': 42 | main() 43 | -------------------------------------------------------------------------------- /isim-judge.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from judge.isim import duration_default 3 | from judge.base import timeout_default 4 | from judge import ISim, Mars, Diff, MarsJudge, INFINITE_LOOP, resolve_paths 5 | 6 | if __name__ == '__main__': 7 | parser = argparse.ArgumentParser(description='Verify MIPS CPU in Verilog against MARS simulation of given .asm ' 8 | 'program.') 9 | parser.add_argument('project_path', 10 | help='path to your ISE project') 11 | parser.add_argument('module_name', 12 | help='name of your test bench module') 13 | parser.add_argument('asm_path', 14 | help='path to the .asm program to simulate, or a directory containing multiple .asm files') 15 | parser.add_argument('--ise-path', 16 | help=r'path to the ISE installation, detect automatically if not specified') 17 | parser.add_argument('--mars-path', 18 | help='path to the modified MARS .jar file, the built-in one by default', 19 | default=None) 20 | parser.add_argument('--java-path', metavar='path', 21 | default='java', help='path to your jre binary, omit this if java is in your path environment') 22 | parser.add_argument('--diff-path', metavar='path', 23 | default=None, 24 | help='path to your diff tool, "diff" for POSIX and "fc" for Windows by default') 25 | parser.add_argument('--recompile', action='store_true', 26 | help='recompile the test bench before running the simulation') 27 | parser.add_argument('--db', action='store_true', 28 | help='specify this to enable delayed branching') 29 | parser.add_argument('--duration', metavar='time', 30 | default=duration_default, 31 | help='duration for ISim simulation, "{}" by default'.format(duration_default)) 32 | parser.add_argument('--no-infinite-loop-appendix', action='store_true', 33 | help='specify this to prevent an extra infinite loop inserted at the end of simulation, ' 34 | 'where you have to call $finish manually in your project') 35 | parser.add_argument('--tb-timeout', metavar='secs', type=int, 36 | default=None, 37 | help='timeout for ISim simulation, {} by default'.format(timeout_default)) 38 | parser.add_argument('--mars-timeout', metavar='secs', type=int, 39 | default=None, 40 | help='timeout for MARS simulation, {} by default'.format(timeout_default)) 41 | 42 | args = parser.parse_args() 43 | 44 | isim = ISim(args.project_path, args.module_name, duration=args.duration, 45 | appendix=None if args.no_infinite_loop_appendix else INFINITE_LOOP, 46 | recompile=args.recompile, timeout=args.tb_timeout) 47 | mars = Mars(args.mars_path, java_path=args.java_path, db=args.db, timeout=args.mars_timeout) 48 | diff = Diff(args.diff_path) 49 | 50 | judge = MarsJudge(isim, mars, diff) 51 | judge.all(resolve_paths(args.asm_path)) 52 | -------------------------------------------------------------------------------- /judge/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import VerificationFailed, INFINITE_LOOP, DISABLE_SR 2 | from .isim import ISim 3 | from .mars import Mars 4 | from .diff import Diff 5 | from .judge import MarsJudge, DuetJudge, DummyJudge 6 | from .utils import resolve_paths 7 | 8 | try: 9 | from .logisim import Logisim 10 | except Exception: 11 | pass 12 | -------------------------------------------------------------------------------- /judge/base.py: -------------------------------------------------------------------------------- 1 | import os, subprocess 2 | from .utils import kill_im 3 | 4 | timeout_default = 3 5 | 6 | INFINITE_LOOP = '1000ffff\n00000000\n' # beq $0, $0, -1; nop; 7 | DISABLE_SR = '40806000\n' # mtc0 $0 8 | 9 | 10 | class VerificationFailed(Exception): 11 | pass 12 | 13 | 14 | def render_msg(msg): 15 | return (', ' + msg) if msg else '' 16 | 17 | 18 | def _communicate_callback(proc, fp, handler, timeout=None, ctx=None, raw_output_file=None): 19 | s = proc.communicate(timeout=timeout)[0] 20 | if raw_output_file: 21 | with open(raw_output_file, 'wb') as raw: 22 | raw.write(s) 23 | for line in s.decode(errors='ignore').splitlines(): 24 | r = handler(line.strip()) if ctx is None else handler(line.strip(), ctx) 25 | if r and fp: 26 | fp.write(r + '\n') 27 | 28 | 29 | class BaseRunner: 30 | def __init__(self, timeout=None, env=None, cwd=None, 31 | kill_on_timeout=True, permit_timeout=True, 32 | raw_output_file=None 33 | ): 34 | self.timeout = timeout_default if timeout is None else timeout 35 | self.env = env 36 | self.cwd = cwd 37 | self.permit_timeout = permit_timeout 38 | self.kill_on_timeout = kill_on_timeout 39 | self.raw_output_file = raw_output_file 40 | 41 | @staticmethod 42 | def stop(): 43 | pass 44 | 45 | def parse(self, line): 46 | raise TypeError 47 | 48 | def _communicate_fp(self, cmd, fp, timeout_msg, error_msg=None, ctx=None): 49 | name = self.__class__.__name__ 50 | with subprocess.Popen(cmd, stdout=subprocess.PIPE, cwd=self.cwd, env=self.env) as proc: 51 | try: 52 | _communicate_callback(proc, fp, self.parse, self.timeout, ctx=ctx, 53 | raw_output_file=self.raw_output_file) 54 | except subprocess.TimeoutExpired as e: 55 | proc.kill() 56 | if self.kill_on_timeout: 57 | kill_im(os.path.basename(cmd[0])) 58 | _communicate_callback(proc, fp, self.parse, ctx=ctx, 59 | raw_output_file=self.raw_output_file) 60 | msg = '{} timed out after {} secs{}'.format( 61 | name, self.timeout, render_msg(timeout_msg) 62 | ) 63 | if self.permit_timeout: 64 | print('Permitted:', msg) 65 | return 66 | raise RuntimeError(msg) from e 67 | if proc.returncode: 68 | raise RuntimeError('{} subprocess returned {}{}'.format( 69 | name, proc.returncode, render_msg(error_msg) 70 | )) 71 | 72 | def _communicate(self, cmd, out_fn, timeout_msg=None, error_msg=None, ctx=None): 73 | if out_fn: 74 | with open(out_fn, 'w', encoding='utf-8') as fp: 75 | return self._communicate_fp(cmd, fp, timeout_msg, error_msg, ctx) 76 | return self._communicate_fp(cmd, None, timeout_msg, error_msg, ctx) 77 | 78 | 79 | class BaseHexRunner(BaseRunner): 80 | def __init__(self, appendix=None, _hex_path=None, _handler_hex_path=None, **kw): 81 | super().__init__(**kw) 82 | self.appendix = appendix 83 | self._hex_path = _hex_path 84 | self._handler_hex_path = _handler_hex_path 85 | self.tmp_dir = None 86 | 87 | def _put_appendix(self, hex_path): 88 | if self.appendix: 89 | with open(hex_path, 'a', encoding='utf-8') as fp: 90 | fp.write('\n' + self.appendix) 91 | 92 | def run(self, out_path): 93 | raise TypeError 94 | 95 | # to support customized path, setter should be override and getters should return None before set 96 | def get_hex_path(self): 97 | return self._hex_path 98 | 99 | def get_handler_hex_path(self): 100 | return self._handler_hex_path 101 | 102 | def set_hex_path(self, path): 103 | raise NotImplementedError 104 | 105 | def set_handler_hex_path(self, path): 106 | raise NotImplementedError 107 | 108 | def _set_hex_path(self, path): 109 | self._hex_path = path 110 | 111 | def _set_handler_hex_path(self, path): 112 | self._handler_hex_path = path 113 | 114 | def set_tmp_dir(self, tmp_dir): 115 | self.tmp_dir = tmp_dir 116 | 117 | @staticmethod 118 | def run_loaded(out_path): 119 | raise TypeError 120 | 121 | def __call__(self, out_path): 122 | self._put_appendix(self.get_hex_path()) 123 | return self.run(out_path) 124 | -------------------------------------------------------------------------------- /judge/concurrent.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | 4 | class PropagatingThread(threading.Thread): 5 | def run(self): 6 | self.exc = None 7 | try: 8 | self.ret = self._target(*self._args, **self._kwargs) 9 | except BaseException as e: 10 | self.exc = e 11 | 12 | def join(self): 13 | super(PropagatingThread, self).join() 14 | if self.exc: 15 | raise self.exc 16 | return self.ret 17 | 18 | 19 | class Atomic: 20 | Lock = threading.Lock 21 | 22 | def __init__(self, data): 23 | self.mutex = self.Lock() 24 | self.data = data 25 | 26 | def __enter__(self): 27 | self.mutex.__enter__() 28 | return self.data 29 | 30 | def __exit__(self, t, v, tb): 31 | return self.mutex.__exit__(t, v, tb) 32 | 33 | 34 | class RAtomic(Atomic): 35 | Lock = threading.RLock 36 | 37 | 38 | class Counter(RAtomic): 39 | def __init__(self, x=0): 40 | super().__init__(x) 41 | 42 | def increase(self, x=1): 43 | with self.mutex: 44 | self.data += x 45 | 46 | def value(self): 47 | return self.data 48 | 49 | def __enter__(self): 50 | self.mutex.__enter__() 51 | 52 | def __str__(self): 53 | return str(self.data) 54 | 55 | __int__ = value 56 | __repr__ = __str__ 57 | -------------------------------------------------------------------------------- /judge/diff.py: -------------------------------------------------------------------------------- 1 | import os, subprocess 2 | from .base import VerificationFailed 3 | 4 | diff_path_default = 'fc' if os.name == 'nt' else 'diff' 5 | 6 | 7 | class InconsistentResults(VerificationFailed): 8 | pass 9 | 10 | 11 | class Diff: 12 | 13 | def __init__(self, diff_path=None, keep_output_files=False, permit_prefix=False): 14 | self.diff_path = diff_path_default if diff_path is None else diff_path 15 | self.keep_output_files = keep_output_files 16 | self.permit_prefix = permit_prefix 17 | 18 | def __call__(self, out_path, ans_path, log_path=None): 19 | def complain(): 20 | nonlocal log_path 21 | if log_path is None: 22 | log_path = out_path + '.diff' 23 | with open(log_path, 'wb') as fp: 24 | fp.write(res[0] + b'\n' + res[1]) 25 | raise InconsistentResults('output differs, see {}, {}, and {} for diff logs' 26 | .format(out_path, ans_path, log_path)) 27 | 28 | with subprocess.Popen([self.diff_path, out_path, ans_path], 29 | stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc: 30 | res = proc.communicate() 31 | if proc.returncode: 32 | # res = res[0].decode(errors='ignore') + res[1].decode(errors='ignore') 33 | # print(res, file=sys.stderr) 34 | if self.permit_prefix: 35 | with open(out_path, 'rb') as fp1, open(ans_path, 'rb') as fp2: 36 | s1 = fp1.read().strip() 37 | s2 = fp2.read().strip() 38 | l = min(len(s1), len(s2)) 39 | if s1[:l] != s2[:l]: 40 | return complain() 41 | else: 42 | return complain() 43 | 44 | if not self.keep_output_files: 45 | os.remove(out_path) 46 | os.remove(ans_path) 47 | -------------------------------------------------------------------------------- /judge/isim.py: -------------------------------------------------------------------------------- 1 | import os, subprocess 2 | from .base import VerificationFailed, BaseHexRunner 3 | from .utils import kill_im 4 | 5 | tcl_common_fn = 'judge.cmd' 6 | hex_common_fn = 'code.txt' 7 | handler_hex_common_fn = 'code_handler.txt' 8 | duration_default = '1000 us' 9 | 10 | nil = object() 11 | 12 | class ISim(BaseHexRunner): 13 | name = 'ISim' 14 | 15 | platform = None 16 | @classmethod 17 | def get_platform(cls, bin): 18 | if not cls.platform: 19 | platform = ('lin', 'nt')[os.name == 'nt'] 20 | platform_64 = platform + '64' 21 | if os.path.isdir(os.path.join(bin, platform_64)): 22 | cls.platform = platform_64 23 | else: 24 | cls.platform = platform 25 | return cls.platform 26 | 27 | @classmethod 28 | def _get_ise_path(cls): 29 | if os.name != 'nt': 30 | return None 31 | from winreg import ConnectRegistry, OpenKey, EnumValue, HKEY_CLASSES_ROOT 32 | from shlex import split 33 | reg = ConnectRegistry(None, HKEY_CLASSES_ROOT) 34 | try: 35 | key = OpenKey(reg, r'isefile\shell\open\Command') 36 | except FileNotFoundError: 37 | return None 38 | cmd = EnumValue(key, 0)[1] 39 | return os.path.dirname(split(cmd)[0]) 40 | 41 | ise_path = nil 42 | @classmethod 43 | def get_ise_path(cls): 44 | if cls.ise_path is nil: 45 | cls.ise_path = cls._get_ise_path() 46 | print('Detected ISE installation at', cls.ise_path) 47 | return cls.ise_path 48 | 49 | def __init__(self, project_path, module_name=None, 50 | duration=duration_default, 51 | recompile=False, 52 | ise_path=None, 53 | tcl_fn=tcl_common_fn, 54 | **kw 55 | ): 56 | env = os.environ.copy() 57 | platform_bin = None 58 | 59 | if 'XILINX' not in env: 60 | if ise_path: 61 | ise_path = os.path.normcase(ise_path) 62 | else: 63 | ise_path = ISim.get_ise_path() 64 | if not ise_path: 65 | raise VerificationFailed('ISE installation not found, specify it by ise_path') 66 | ise = ise_path if os.path.isdir(os.path.join(ise_path, 'bin')) else os.path.join(ise_path, 'ISE') 67 | bin = os.path.join(ise, 'bin') 68 | platform = ISim.get_platform(bin) 69 | platform_bin = os.path.join(bin, platform) 70 | 71 | env['XILINX'] = ise 72 | env['XILINX_PLATFORM'] = platform 73 | env['PATH'] = platform_bin + os.pathsep + env['PATH'] 74 | 75 | exe = '.exe' if os.name == 'nt' else '' 76 | if module_name: 77 | tb_dir = project_path 78 | tb_basename = module_name + '_isim_beh' + exe 79 | tb_path = os.path.join(tb_dir, tb_basename) 80 | else: 81 | tb_dir = os.path.dirname(project_path) 82 | tb_basename = os.path.basename(project_path) 83 | tb_path = project_path 84 | 85 | hex_path = os.path.join(tb_dir, hex_common_fn) 86 | handler_hex_path = os.path.join(tb_dir, handler_hex_common_fn) 87 | super().__init__(**dict(kw, env=env, cwd=tb_dir, 88 | _hex_path=hex_path, 89 | _handler_hex_path=handler_hex_path, 90 | kill_on_timeout=True)) 91 | 92 | if not platform_bin: 93 | bin = os.path.join(env['XILINX'], 'bin') 94 | platform_bin = os.path.join(bin, ISim.get_platform(bin)) 95 | 96 | self.exe = exe 97 | self.platform_bin = platform_bin 98 | self.recompile = recompile 99 | self.module_name = module_name 100 | self.tb_dir = tb_dir 101 | self.tb_basename = tb_basename 102 | self.tb_path = tb_path 103 | self.tcl_fn = tcl_fn 104 | self.tcl_path = os.path.join(tb_dir, tcl_fn) 105 | tcl_text = 'run {}\nexit\n'.format(duration.strip()) 106 | self._generate_tcl(self.tcl_path, tcl_text) 107 | 108 | @staticmethod 109 | def _generate_tcl(path, s): 110 | with open(path, 'w', encoding='utf-8') as fp: 111 | fp.write(s) 112 | 113 | @staticmethod 114 | def parse(s): 115 | if 'error' in s.lower(): 116 | raise VerificationFailed('ISim complained ' + s) 117 | if '$ 0' in s: 118 | return 119 | p = s.find('@') 120 | if p >= 0: 121 | return s[p:] 122 | 123 | def compile(self): 124 | self.tb_basename = tb_basename = self.module_name + '_qwqwq' + self.exe 125 | self.tb_path = os.path.join(self.tb_dir, tb_basename) 126 | subprocess.run([os.path.join(self.platform_bin, 'fuse'), 127 | '--nodebug', 128 | '-i', '.', 129 | '--prj', self.module_name + '_beh.prj', 130 | '-o', tb_basename, 131 | self.module_name 132 | ], env=self.env, cwd=self.tb_dir) 133 | 134 | def run(self, out_path): 135 | if self.recompile: 136 | self.compile() 137 | self.recompile = False 138 | self._communicate([os.path.normcase(self.tb_path), '-tclbatch', self.tcl_fn], 139 | out_path, 140 | 'see ' + out_path, 141 | 'maybe ISE path is incorrect' 142 | ) 143 | 144 | def stop(self): 145 | kill_im(self.tb_basename) 146 | -------------------------------------------------------------------------------- /judge/judge.py: -------------------------------------------------------------------------------- 1 | import os, sys, shutil 2 | from random import randint 3 | from typing import Iterable, Optional 4 | 5 | from .base import BaseHexRunner, VerificationFailed 6 | from .mars import Mars, SegmentNotFoundError 7 | from .diff import Diff 8 | from .utils import TmpDir 9 | 10 | tmp_pre = 'tmp' 11 | 12 | handler_segment = '0x4180-0x4ffc' 13 | 14 | def sync_path(src, dst): 15 | if not is_path_same(dst, src): 16 | shutil.copy(src, dst) 17 | 18 | class BaseJudge: 19 | def __init__(self, runners: Iterable[BaseHexRunner], 20 | mars: Mars, diff: Optional[Diff] = None): 21 | self.runners = runners 22 | self.mars = mars 23 | self.diff = Diff() if diff is None else diff 24 | self.id = randint(100000, 999999) 25 | self.tmp_dir = tmp_dir = TmpDir(os.path.join(tmp_pre, str(self.id))) 26 | for runner in runners: 27 | runner.set_tmp_dir(tmp_dir) 28 | 29 | def get_path(self, get, set, fn): 30 | r = get() 31 | if r is None: 32 | r = os.path.join(self.tmp_dir(), fn) 33 | set(r) 34 | return r 35 | 36 | def get_hex_path(self, runner: BaseHexRunner, asm_base): 37 | return self.get_path(runner.get_hex_path, runner.set_hex_path, 38 | asm_base + '.hex') 39 | 40 | def get_handler_hex_path(self, runner: BaseHexRunner, asm_base): 41 | return self.get_path(runner.get_handler_hex_path, runner.set_handler_hex_path, 42 | asm_base + '-h.hex') 43 | 44 | def dump_handler(self, asm_path, hex_path): 45 | try: 46 | self.mars(asm_path=asm_path, hex_path=hex_path, a=True, 47 | dump_segment=handler_segment) 48 | except SegmentNotFoundError as e: 49 | print('Warning: no handler found in', asm_path, file=sys.stderr) 50 | return False 51 | return True 52 | 53 | def load_handler(self, asm_path): 54 | source = None 55 | q = [] 56 | for runner in self.runners: 57 | path = runner.get_handler_hex_path() 58 | if path is None: 59 | q.append(runner) 60 | else: 61 | if source is None: 62 | source = path 63 | if not self.dump_handler(asm_path, path): 64 | return False 65 | else: 66 | sync_path(source, path) 67 | 68 | if q: 69 | if not source: 70 | source = os.path.join(self.tmp_dir(), os.path.basename(asm_path) + '.hex') 71 | if not self.dump_handler(asm_path, source): 72 | return False 73 | for runner in q: 74 | runner.set_handler_hex_path(source) 75 | 76 | print('Loaded handler from', asm_path) 77 | return True 78 | 79 | def stop(self): 80 | for runner in self.runners: 81 | runner.stop() 82 | 83 | @staticmethod 84 | def __call__(asm_path): 85 | raise TypeError 86 | 87 | def judge_handler(self, asm_path): 88 | self.load_handler(asm_path) 89 | self(asm_path) 90 | 91 | def all(self, asm_paths, 92 | self_handler=None, 93 | fallback_handler_keyword=None, 94 | fallback_handler_asm_path=None, 95 | on_success=None, 96 | on_error=None, 97 | stop_on_error=True, 98 | permit_missing_segment=True, 99 | reraise=False 100 | ): 101 | total = len(asm_paths) 102 | cnt = 0 103 | for path in asm_paths: 104 | try: 105 | if self_handler and not self.load_handler(path): 106 | loaded = False 107 | if fallback_handler_asm_path and os.path.exists(fallback_handler_asm_path): 108 | fallback = fallback_handler_asm_path 109 | print('Fallback to handler', fallback) 110 | loaded = self.load_handler(fallback) 111 | if not loaded and fallback_handler_keyword: 112 | dirname = os.path.dirname(os.path.abspath(path)) 113 | for fn in os.listdir(dirname): 114 | if fn.endswith('.asm') and fallback_handler_keyword in fn: 115 | fallback = os.path.join(dirname, fn) 116 | print('Fallback to handler', fallback) 117 | loaded = self.load_handler(fallback) 118 | if loaded: 119 | break 120 | if not loaded: 121 | print('No valid handlers found, keeping the previous one') 122 | self(path) 123 | except VerificationFailed as e: 124 | print('!!', path + ':', e.__class__.__name__, e, file=sys.stderr) 125 | if isinstance(e, SegmentNotFoundError) and permit_missing_segment: 126 | print('!! Permitted') 127 | else: 128 | if on_error: 129 | on_error(path) 130 | if reraise: 131 | raise e 132 | if stop_on_error: 133 | return self.stop() 134 | else: 135 | cnt += 1 136 | print('{}/{}'.format(cnt, total), path, 'ok') 137 | if on_success: 138 | on_success(path) 139 | 140 | 141 | common_tmp = TmpDir(tmp_pre) 142 | def get_paths(asm_path): 143 | base = os.path.basename(asm_path) 144 | pre = common_tmp() 145 | return base, os.path.join(pre, base + '.out'), os.path.join(pre, base + '.ans') 146 | 147 | 148 | class MarsJudge(BaseJudge): 149 | def __init__(self, runner: BaseHexRunner, mars: Mars, 150 | diff: Optional[Diff] = None): 151 | super().__init__([runner], mars, diff) 152 | self.runner = runner 153 | 154 | def __call__(self, asm_path): 155 | base, out_path, ans_path = get_paths(asm_path) 156 | hex_path = self.get_hex_path(self.runner, base) 157 | 158 | if self.mars.permit_timeout: 159 | self.mars(asm_path=asm_path, hex_path=hex_path, a=True) 160 | self.mars(asm_path=asm_path, out_path=ans_path) 161 | else: 162 | self.mars(asm_path=asm_path, out_path=ans_path, hex_path=hex_path) 163 | 164 | print('Running simulation for', asm_path, '...') 165 | self.runner(out_path) 166 | self.diff(out_path, ans_path) 167 | 168 | 169 | def is_path_same(path1, path2): 170 | return os.path.relpath(path1, path2) == '.' 171 | 172 | 173 | class DuetJudge(BaseJudge): 174 | def __init__(self, runner: BaseHexRunner, runner_std: BaseHexRunner, mars: Mars, 175 | diff: Optional[Diff] = None): 176 | super().__init__([runner, runner_std], mars, diff) 177 | self.runner = runner 178 | self.runner_std = runner_std 179 | 180 | def __call__(self, asm_path): 181 | base, out_path, ans_path = get_paths(asm_path) 182 | hex_path = self.get_hex_path(self.runner, base) 183 | hex_std_path = self.get_hex_path(self.runner_std, base) 184 | 185 | self.mars(asm_path=asm_path, hex_path=hex_path, a=True) 186 | sync_path(hex_path, hex_std_path) 187 | 188 | print('Running standard simulation for', asm_path, '...') 189 | self.runner_std(ans_path) 190 | 191 | print('Running simulation for', asm_path, '...') 192 | self.runner(out_path) 193 | 194 | self.diff(out_path, ans_path) 195 | 196 | 197 | class DummyJudge(MarsJudge): 198 | def __call__(self, asm_path): 199 | base, out_path, _ = get_paths(asm_path) 200 | hex_path = self.get_hex_path(self.runner, base) 201 | 202 | self.mars(asm_path=asm_path, hex_path=hex_path, a=True) 203 | print('Running simulation for', asm_path, '...') 204 | self.runner(out_path) 205 | print('Output to', out_path) 206 | -------------------------------------------------------------------------------- /judge/kits/marsx.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karin0/buaa-co-cpu-judge/0ab3dc0c8417be24f8034102ffce6cb315bf2fef/judge/kits/marsx.jar -------------------------------------------------------------------------------- /judge/logisim.py: -------------------------------------------------------------------------------- 1 | import os, os.path, string 2 | import xml.etree.ElementTree as ET 3 | 4 | from .base import BaseHexRunner, VerificationFailed 5 | 6 | pc_width_default = 32 7 | pc_by_word_default = False 8 | pc_start_default = 0 9 | dma_width_default = 32 10 | dma_by_word_default = False 11 | 12 | hex_chars = set(filter(lambda c: not c.isupper(), string.hexdigits)) 13 | 14 | 15 | def to_instr(line): 16 | if not line: 17 | return None 18 | for ch in line: 19 | if ch not in hex_chars: 20 | return None 21 | r = line.lstrip('0') 22 | return r if r else '0' 23 | 24 | 25 | def to_hex(x): 26 | return hex(x)[2:].zfill(8) 27 | 28 | 29 | def to_dec(x): 30 | return str(x).rjust(2) 31 | 32 | 33 | class LogLine: 34 | 35 | def __init__(self, line): 36 | self.s = ''.join(line.split()) 37 | for ch in self.s: 38 | if ch not in ('0', '1'): 39 | raise ValueError('found ' + ch) 40 | self.p = 0 41 | 42 | def take(self, n, by_word=False): 43 | self.p += n 44 | r = int(self.s[self.p - n: self.p], 2) 45 | if by_word: 46 | r *= 4 47 | return r 48 | 49 | def take_hex(self, n, by_word=False): 50 | return to_hex(self.take(n, by_word)) 51 | 52 | def parse(self, 53 | pc_width=pc_width_default, 54 | pc_by_word=pc_by_word_default, 55 | pc_start=pc_start_default, 56 | dma_width=dma_width_default, 57 | dma_by_word=dma_by_word_default 58 | ): 59 | pc = to_hex(0x3000 - pc_start + self.take(pc_width, pc_by_word)) 60 | gw = self.take(1) 61 | ga_int = self.take(5) 62 | ga = to_dec(ga_int) 63 | gd = self.take_hex(32) 64 | if gw and ga_int: 65 | return '@{}: ${} <= {}'.format(pc, ga, gd) 66 | 67 | if self.take(1): 68 | da = self.take_hex(dma_width, dma_by_word) 69 | return '@{}: *{} <= {}'.format(pc, da, self.take_hex(32)) 70 | return None 71 | 72 | 73 | def gen(circ_path, hex_path, im_circ_name, tmp_dir): 74 | tree = ET.parse(circ_path) 75 | root = tree.getroot() 76 | if im_circ_name is None: 77 | token = './circuit/comp[@name="ROM"]/a[@name="contents"]' 78 | else: 79 | token = './circuit[@name="{' + im_circ_name + '"]/comp[@name="ROM"]/a[@name="contents"]' 80 | cont = root.find(token) 81 | if cont is None: 82 | raise ValueError('no rom comp found in ' + circ_path) 83 | 84 | s = cont.text 85 | desc = s[:s.find('\n')] 86 | addr_width, data_width = map(int, desc[desc.find(':') + 1:].split()) 87 | if data_width != 32: 88 | raise ValueError('data width of the rom is ' + str(data_width) + ', 32 expected') 89 | max_ins_cnt = 2 ** addr_width 90 | 91 | with open(hex_path, 'r', encoding='utf-8') as fp: 92 | hex = fp.read() 93 | 94 | image_path = os.path.join(tmp_dir, os.path.splitext(os.path.basename(hex_path))[0] + '-image.hex') 95 | with open(image_path, 'w', encoding='utf-8') as fp: 96 | fp.write('v2.0 raw\n' + hex) 97 | 98 | instrs = [] 99 | for s in hex.splitlines(): 100 | ins = to_instr(s) 101 | if ins: 102 | instrs.append(ins) 103 | if len(instrs) > max_ins_cnt: 104 | raise ValueError('too many instructions ({}) for rom addr width {}'.format(len(instrs), addr_width)) 105 | 106 | while instrs and instrs[-1] == '0': 107 | instrs.pop() 108 | 109 | lines = [desc] 110 | line = [] 111 | for ins in instrs: 112 | line.append(ins) 113 | if len(line) == 8: 114 | lines.append(' '.join(line)) 115 | line.clear() 116 | if line: 117 | lines.append(' '.join(line)) 118 | cont.text = '\n'.join(lines) + '\n' 119 | 120 | new_circ_path = os.path.join(tmp_dir, os.path.basename(circ_path)) 121 | tree.write(new_circ_path) 122 | return new_circ_path 123 | 124 | 125 | class IllegalCircuit(VerificationFailed): 126 | pass 127 | 128 | 129 | class Logisim(BaseHexRunner): 130 | 131 | def __init__(self, circ_path, 132 | logisim_path, 133 | java_path='java', 134 | pc_width=pc_width_default, 135 | pc_by_word=pc_by_word_default, 136 | pc_start=pc_start_default, 137 | dma_width=dma_width_default, 138 | dma_by_word=dma_by_word_default, 139 | im_circuit_name=None, 140 | **kw 141 | ): 142 | super().__init__(**kw) 143 | 144 | self.circ_path = circ_path 145 | self.logisim_path = logisim_path 146 | self.java_path = java_path 147 | 148 | self.im_circ_name = im_circuit_name 149 | self.pc_width = pc_width 150 | self.pc_by_word = pc_by_word 151 | self.pc_start = pc_start 152 | self.dma_width = dma_width 153 | self.dma_by_word = dma_by_word 154 | 155 | def parse(self, s): 156 | if not s: 157 | return 158 | try: 159 | r = LogLine(s).parse( 160 | self.pc_width, self.pc_by_word, self.pc_start, 161 | self.dma_width, self.dma_by_word 162 | ) 163 | except ValueError as e: 164 | raise VerificationFailed('invalid output ({}): {}'.format(e, s)) from e 165 | return r 166 | 167 | def set_hex_path(self, path): 168 | self._set_hex_path(path) 169 | 170 | def run(self, out_path): 171 | try: 172 | circ_path = gen(self.circ_path, self.get_hex_path(), self.im_circ_name, self.tmp_dir()) 173 | except ValueError as e: 174 | raise IllegalCircuit(e) from e 175 | self._communicate([self.java_path, '-jar', self.logisim_path, circ_path, '-tty', 'table'], 176 | out_path, 177 | 'maybe the halt pin is set incorrectly, see ' + out_path 178 | ) 179 | -------------------------------------------------------------------------------- /judge/mars.py: -------------------------------------------------------------------------------- 1 | import os, subprocess 2 | from .base import BaseRunner, VerificationFailed 3 | 4 | mars_path_default = os.path.join(os.path.dirname(__file__), 'kits', 'marsx.jar') 5 | 6 | 7 | class MarsError(VerificationFailed): 8 | pass 9 | 10 | 11 | class SegmentNotFoundError(MarsError): 12 | pass 13 | 14 | 15 | def render_arg(name, value, fallback=''): 16 | return name if value else fallback 17 | 18 | 19 | class Mars(BaseRunner): 20 | name = 'MARS' 21 | 22 | def __init__(self, mars_path=None, java_path='java', db=False, np=False, a=False, **kw): 23 | super().__init__(**kw) 24 | self.mars_path = mars_path_default if mars_path is None else mars_path 25 | self.java_path = java_path 26 | self.db = render_arg('db', db) 27 | self.np = render_arg('np', np) 28 | self.a = render_arg('a', a) 29 | 30 | if not db: 31 | print('Delayed branching is disabled') 32 | 33 | def set_assemble_only(self): 34 | self.a = 'a' 35 | 36 | @staticmethod 37 | def parse(s): 38 | sl = s.lower() 39 | if 'error' in sl: 40 | raise MarsError('MARS reported ' + s) 41 | if 'nothing to dump' in sl: 42 | raise SegmentNotFoundError(sl) 43 | if '$ 0' in s: 44 | return 45 | if s.startswith('@'): 46 | return s 47 | 48 | def start(self, asm_path): 49 | subprocess.run([self.java_path, '-jar', self.mars_path, asm_path]) 50 | 51 | def __call__(self, asm_path, out_path=None, hex_path=None, a=False, dump_segment='.text'): 52 | cmd = [self.java_path, '-jar', self.mars_path, asm_path, 53 | 'nc', 54 | self.db, self.np, render_arg('a', a, self.a), 55 | 'mc', 'CompactDataAtZero'] 56 | if hex_path: 57 | cmd += ['dump', dump_segment, 'HexText', hex_path] 58 | 59 | self._communicate(cmd, out_path, 60 | 'maybe an infinite loop' + (', see ' + out_path if out_path else '') 61 | ) 62 | -------------------------------------------------------------------------------- /judge/utils.py: -------------------------------------------------------------------------------- 1 | import os, subprocess, glob, threading, json 2 | from hashlib import md5 3 | 4 | 5 | def try_mkdir(path, func=os.mkdir): 6 | if not os.path.isdir(path): 7 | func(path) 8 | 9 | 10 | class CachedList: 11 | def __init__(self, fn): 12 | self.fn = fn 13 | self.a = [] 14 | self.changed = False 15 | self.mutex = threading.Lock() 16 | self.changed_mutex = threading.Lock() 17 | 18 | def __enter__(self): 19 | try: 20 | with open(self.fn, encoding='utf-8') as fp: 21 | self.a = json.load(fp) 22 | except (FileNotFoundError, json.decoder.JSONDecodeError): 23 | self.a = [] 24 | return self 25 | 26 | def close(self): 27 | with self.changed_mutex: 28 | if self.changed: 29 | with self.mutex: 30 | with open(self.fn, 'w', encoding='utf-8') as fp: 31 | json.dump(self.a, fp, ensure_ascii=False, indent=4, separators=(',', ': ')) 32 | self.changed = False 33 | 34 | def close_some(self, _): 35 | return self.close() 36 | 37 | def __exit__(self, t, v, tb): 38 | self.close() 39 | 40 | def __iter__(self): 41 | return self.a.__iter__() 42 | 43 | def __in__(self, v): 44 | return v in self.a 45 | 46 | def append(self, v): 47 | with self.changed_mutex: 48 | self.changed = True 49 | with self.mutex: 50 | return self.a.append(v) 51 | 52 | 53 | class TmpDir: 54 | def __init__(self, path): 55 | self.path = path 56 | self.created = False 57 | 58 | def __call__(self): 59 | if not self.created: 60 | try_mkdir(self.path, func=os.makedirs) 61 | self.created = True 62 | return self.path 63 | 64 | 65 | def hash_file(fn): 66 | with open(fn, 'rb') as fp: 67 | return md5(fp.read()).hexdigest()[:10] 68 | 69 | 70 | def run(cmd, quiet=True): 71 | if quiet: 72 | subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) 73 | else: 74 | subprocess.run(cmd) 75 | 76 | 77 | def kill_pid(pid): 78 | if os.name != 'nt': 79 | raise NotImplementedError 80 | run(['taskkill', '/f', '/pid', str(pid)]) 81 | 82 | 83 | def kill_im(im): 84 | if os.name != 'nt': 85 | raise NotImplementedError 86 | if not os.path.splitext(im)[1]: 87 | im += '.exe' 88 | run(['taskkill', '/f', '/im', im]) 89 | 90 | 91 | def resolve_paths(paths, recursive=True, use_glob=True, blocklist=None, on_omit=None): 92 | if isinstance(paths, str): 93 | r = [paths] 94 | elif use_glob: 95 | r = [] 96 | for p in paths: 97 | r += glob.glob(p, recursive=recursive) 98 | else: 99 | r = paths 100 | 101 | q = [] 102 | if blocklist: 103 | ban_set = set(os.path.abspath(path) for path in blocklist) 104 | 105 | def push(path): 106 | if os.path.abspath(path) in ban_set: 107 | if on_omit: 108 | on_omit(path) 109 | else: 110 | q.append(path) 111 | else: 112 | push = q.append 113 | 114 | for path in r: 115 | if recursive and os.path.isdir(path): 116 | for file_path in glob.glob(os.path.join(path, '**', '*.asm'), recursive=True): 117 | push(file_path) 118 | else: 119 | push(path) 120 | 121 | return q 122 | -------------------------------------------------------------------------------- /logisim-judge.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from judge.logisim import * 3 | from judge.base import timeout_default 4 | from judge import Logisim, Mars, Diff, MarsJudge, INFINITE_LOOP, resolve_paths 5 | 6 | 7 | if __name__ == '__main__': 8 | parser = argparse.ArgumentParser(description='Verify MIPS CPU circuits in Logisim against MARS simulation of ' 9 | 'given .asm program.') 10 | parser.add_argument('circuit_path', 11 | help='path to the Logisim project file') 12 | parser.add_argument('asm_path', 13 | help='path to the .asm program to simulate, or a directory containing multiple .asm files') 14 | parser.add_argument('logisim_path', 15 | help='path to the Logisim .jar file') 16 | parser.add_argument('--mars-path', 17 | help='path to the modified MARS .jar file, the built-in one by default', 18 | default=None) 19 | parser.add_argument('--java-path', metavar='path', 20 | default='java', 21 | help='path to your jre binary, omit this if java is in your path environment') 22 | parser.add_argument('--diff-path', metavar='path', 23 | default=None, 24 | help='path to your diff tool, "diff" for POSIX and "fc" for Windows by default') 25 | parser.add_argument('--im-circuit-name', metavar='im', 26 | default=None, 27 | help='name of the circuit containing the ROM to load dumped instructions into, omit to look' 28 | ' for any ROM in the project') 29 | parser.add_argument('--pc-width', metavar='width', type=int, 30 | default=pc_width_default, help='width of output PC, {} by default'.format(pc_by_word_default)) 31 | parser.add_argument('--pc-start', metavar='addr', type=int, 32 | default=str(pc_start_default), 33 | help='starting address of output PC, {} by default'.format(hex(pc_start_default))) 34 | parser.add_argument('--pc-by-word', action='store_true', 35 | help='specify this if output PC is word addressing') 36 | parser.add_argument('--dm-address-width', metavar='width', type=int, 37 | default=dma_width_default, 38 | help='width of DM_WRITE_ADDRESS in output, {} by default'.format(dma_width_default)) 39 | parser.add_argument('--dm-address-by-word', action='store_true', 40 | help='specify this if output DM address is word addressing') 41 | parser.add_argument('--no-infinite-loop-appendix', action='store_true', 42 | help='specify this to prevent an extra infinite loop inserted at the end of simulation, ' 43 | 'where you have to provide a halt output pin in your project') 44 | parser.add_argument('--logisim-timeout', metavar='secs', type=int, 45 | default=None, 46 | help='timeout for Logisim simulation, {} by default'.format(timeout_default)) 47 | parser.add_argument('--mars-timeout', metavar='secs', type=int, 48 | default=None, 49 | help='timeout for MARS simulation, {} by default'.format(timeout_default)) 50 | 51 | args = parser.parse_args() 52 | logi = Logisim(args.circuit_path, args.logisim_path, args.java_path, 53 | args.pc_width, args.pc_by_word, args.pc_start, 54 | args.dm_address_width, args.dm_address_by_word, 55 | args.im_circuit_name, 56 | appendix=None if args.no_infinite_loop_appendix else INFINITE_LOOP, 57 | timeout=args.logisim_timeout 58 | ) 59 | mars = Mars(args.mars_path, java_path=args.java_path, timeout=args.mars_timeout) 60 | diff = Diff(args.diff_path) 61 | 62 | judge = MarsJudge(logi, mars, diff) 63 | judge.all(resolve_paths(args.asm_path)) 64 | --------------------------------------------------------------------------------