diff --git a/aheui/_argparse.py b/aheui/_argparse.py index df196f9..b1056b1 100644 --- a/aheui/_argparse.py +++ b/aheui/_argparse.py @@ -25,12 +25,14 @@ class ArgumentNotInChoicesError(ParserError): __description__ = 'argument is not in choices: ' -class InformationException(ParserError): - __description__ = '' +class InformationException(Exception): + def __init__(self, desc=''): + self.desc = desc + class HelpException(InformationException): - __description__ = '' + pass class ArgumentParser(object): @@ -88,7 +90,10 @@ def _parse_args(self, args): done = True elif name.startswith('-'): if name == arg: - arg = args[idx + 1] + try: + arg = args[idx + 1] + except IndexError: + raise TooFewArgumentError(name) parsed[dest] = arg idx += 2 else: @@ -113,7 +118,7 @@ def parse_args(self, args): try: return self._parse_args(args) except HelpException: - os.write(2, 'usage: %s [option] ... file\n\n' % self.kwargs.get('prog', args[0])) + os.write(2, 'usage: %s [option] ... file\n\n' % get_prog(args[0])) for names, opt in self.arguments: name = names[0] if names[0] == names[1] else ('%s,%s' % names[0:2]) os.write(2, '%s%s: %s' % (name, ' ' * (12 - len(name)), opt['description'])) @@ -125,9 +130,11 @@ def parse_args(self, args): os.write(2, '\n') os.write(2, opt['full_description']) os.write(2, '\n') + raise except InformationException as e: os.write(2, '%s\n' % e.desc) - except ParserError as e: - prog = self.kwargs.get('prog', args[0]) - os.write(2, '%s: error: %s\n' % (prog, e.message())) - return {}, [] + raise + + +def get_prog(arg0): + return arg0.rsplit('/', 1)[-1] diff --git a/aheui/aheui.py b/aheui/aheui.py index e399dda..f04962c 100644 --- a/aheui/aheui.py +++ b/aheui/aheui.py @@ -6,10 +6,11 @@ import os from aheui import const as c +from aheui._argparse import InformationException, get_prog from aheui._compat import jit, unichr, ord, _unicode, bigint, PYR from aheui import compile -from aheui.option import process_options -from aheui.warning import WarningPool +from aheui.option import process_options, OptionError +from aheui.warning import NoRpythonWarning, WriteUtf8RangeWarning, warnings def get_location(pc, stackok, is_queue, program): @@ -34,7 +35,7 @@ def get_location(pc, stackok, is_queue, program): MINUS1 = bigint.fromlong(-1) -class Link(object): +class Node(object): """Element unit for stack and queue.""" def __init__(self, next, value=MINUS1): @@ -102,7 +103,7 @@ def __init__(self): def push(self, value): # assert(isinstance(value, bigint.Int)) - node = Link(self.head, value) + node = Node(self.head, value) self.head = node self.size += 1 @@ -121,7 +122,7 @@ def _put_value(self, value): class Queue(LinkedList): def __init__(self): - self.tail = Link(None) + self.tail = Node(None) self.head = self.tail self.size = 0 @@ -129,14 +130,14 @@ def push(self, value): # assert(isinstance(value, bigint.Int)) tail = self.tail tail.value = value - new = Link(None) + new = Node(None) tail.next = new self.tail = new self.size += 1 def dup(self): head = self.head - node = Link(head, head.value) + node = Node(head, head.value) self.head = node self.size += 1 @@ -156,7 +157,7 @@ def __init__(self): def push(self, value): # assert(isinstance(value, bigint.Int)) - node = Link(self.head, value) + node = Node(self.head, value) self.head = node self.size += 1 self.last_push = value @@ -282,7 +283,7 @@ def write_number(value_str): os.write(outfp, value_str) -def write_utf8(warnings, value): +def write_utf8(value): REPLACE_CHAR = unichr(0xfffd).encode('utf-8') if bigint.is_unicodepoint(value): @@ -295,8 +296,8 @@ def write_utf8(warnings, value): os.write(outfp, bytes) -def warn_utf8_range(warnings, value): - warnings.warn(b'write-utf8-range', value) +def warn_utf8_range(value): + warnings.warn(WriteUtf8RangeWarning, value) os.write(outfp, unichr(0xfffd).encode('utf-8')) class Program(object): @@ -327,7 +328,6 @@ def get_label(self, pc): outfp = 1 errfp = 2 -warnings = WarningPool() def mainloop(program, debug): @@ -421,7 +421,7 @@ def mainloop(program, debug): write_number(bigint.str(r)) elif op == c.OP_POPCHAR: r = selected.pop() - write_utf8(warnings, r) + write_utf8(r) elif op == c.OP_PUSHNUM: num = read_number() selected.push(num) @@ -483,7 +483,10 @@ def prepare_compiler(contents, opt_level=2, source='code', aheuic_output=None, a def entry_point(argv): try: cmd, source, contents, str_opt_level, target, aheuic_output, comment_aheuis, output, warning_limit, trace_limit = process_options(argv, os.environ) - except SystemExit: + except InformationException: + return 0 + except OptionError as e: + os.write(errfp, b"%s: error: %s\n" % (get_prog(argv[0]), e.message())) return 1 warnings.limit = warning_limit @@ -495,7 +498,7 @@ def entry_point(argv): outfp = 1 if output == '-' else open_w(output) if target == 'run': if not PYR: - warnings.warn(b'no-rpython') + warnings.warn(NoRpythonWarning) program = Program(compiler.lines, compiler.label_map) exitcode = mainloop(program, compiler.debug) elif target in ['asm', 'asm+comment']: diff --git a/aheui/option.py b/aheui/option.py index aff6e5f..3eae681 100644 --- a/aheui/option.py +++ b/aheui/option.py @@ -3,9 +3,10 @@ from __future__ import absolute_import import os -from aheui._argparse import ArgumentParser +from aheui._argparse import ArgumentParser, ParserError from aheui._compat import bigint, PY3 from aheui.version import VERSION +from aheui.warning import CommandLineArgumentWarning, warnings from aheui import compile @@ -37,6 +38,39 @@ parser.add_argument('--help', '-h', narg='-1', default='no', description='Show this help text') +class OptionError(Exception): + pass + + +class ParsingError(OptionError): + def __init__(self, msg): + self.args = (msg,) + + def message(self): + return self.args[0] + + +class IntOptionParsingError(OptionError): + def __init__(self, key, value): + self.args = (key, value) + def message(self): + return 'The value of %s="%s" is not a valid integer' % self.args + + +class SourceError(OptionError): + pass + + +class NoInputError(SourceError): + def message(self): + return "no input files" + + +class CommandConflictInputFileError(SourceError): + def message(self): + return "--cmd,-c and input file cannot be used together" + + def kwarg_or_environ(kwargs, environ, arg_key, env_key): if arg_key in kwargs and kwargs[arg_key] != '': return (1, kwargs[arg_key]) @@ -54,41 +88,39 @@ def kwarg_or_environ_int(kwargs, environ, arg_key, env_key, default): value = int(arg) except ValueError: if source == 1: - msg = b'The value of --%s="%s" is not a valid integer\n' % (arg_key, arg) + raise IntOptionParsingError('--' + arg_key, arg) elif source == 2: - msg = b'The value %s="%s" is not a valid integer\n' % (env_key, arg) + raise IntOptionParsingError(env_key, arg) else: assert False - os.write(2, msg) - raise return value -def process_options(argv, environ): - def open_r(filename): - return os.open(filename, os.O_RDONLY, 0o777) +def open_input(filename): + return os.open(filename, os.O_RDONLY, 0o777) - kwargs, args = parser.parse_args(argv) - if not args: - raise SystemExit() + +def process_options(argv, environ): + try: + kwargs, args = parser.parse_args(argv) + except ParserError as e: + raise ParsingError(e.message()) cmd = kwargs['cmd'] if cmd == '': if len(args) != 2: - os.write(2, b'aheui: error: no input files\n') - raise SystemExit() + raise NoInputError() filename = args[1] if filename == '-': fp = 0 contents = compile.read(fp) else: - fp = open_r(filename) + fp = open_input(filename) contents = compile.read(fp) os.close(fp) else: if len(args) != 1: - os.write(2, b'aheui: error: --cmd,-c but input file found\n') - raise SystemExit() + raise CommandConflictInputFileError if PY3: cmd = cmd.encode('utf-8') contents = cmd @@ -115,10 +147,12 @@ def open_r(filename): if need_aheuic: aheuic_output = filename - if aheuic_output.endswith('.aheui'): - aheuic_output += 'c' + if filename.endswith('.aheui'): + aheuic_output = filename + 'c' + elif filename.endswith('.aheuis'): + aheuic_output = filename[:-1] + 'c' else: - aheuic_output += '.aheuic' + aheuic_output = filename + '.aheuic' else: aheuic_output = None @@ -142,8 +176,11 @@ def open_r(filename): elif target == 'run': output = '-' else: - os.write(2, b'aheui: error: --target,-t must be one of "bytecode", "asm", "asm+comment", "run"\n') # noqa: E501 + assert False # must be handled by argparse raise SystemExit() + else: + if target == 'run': + warnings.warn(CommandLineArgumentWarning, b'--target=run always ignores --output') warning_limit = kwarg_or_environ_int(kwargs, environ, 'warning-limit', 'RPAHEUI_WARNING_LIMIT', 3) trace_limit = kwarg_or_environ_int(kwargs, environ, 'trace-limit', 'RPAHEUI_TRACE_LIMIT', -1) diff --git a/aheui/warning.py b/aheui/warning.py index e8f7265..0060f63 100644 --- a/aheui/warning.py +++ b/aheui/warning.py @@ -5,36 +5,46 @@ class Warning(object): - def __init__(self, name, message): - self.name = name - self.message = message - def format(self, *args): - return self.message % args + return self.MESSAGE % args + + +class NoRpythonWarning(Warning): + MESSAGE = b"[Warning:VirtualMachine] Running without rlib/jit." + + +class CommandLineArgumentWarning(Warning): + MESSAGE = b"[Warning:CommandLine] Invalid command line argument is ignored: %s." + + +class WriteUtf8RangeWarning(Warning): + MESSAGE = b'[Warning:UndefinedBehavior:write-utf8-range] value %x is out of unicode codepoint range.' WARNING_LIST = [ - Warning(b'no-rpython', b"[Warning:VirtualMachine] Running without rlib/jit.\n"), - Warning(b'write-utf8-range', b'[Warning:UndefinedBehavior:write-utf8-range] value %x is out of unicode codepoint range.'), + NoRpythonWarning(), + CommandLineArgumentWarning(), + WriteUtf8RangeWarning(), ] class WarningPool(object): def __init__(self): self.limit = -1 - self.warnings = {} self.counters = {} for w in WARNING_LIST: - self.warnings[w.name] = w - self.counters[w.name] = 0 + self.counters[type(w).__name__] = 0 @jit.dont_look_inside - def warn(self, name, *args): - warning = self.warnings[name] + def warn(self, warning, *args): + name = warning.__name__ if self.limit != -1 and self.limit <= self.counters[name]: return self.counters[name] = self.counters[name] + 1 - os.write(2, warning.format(*args)) + os.write(2, warning().format(*args)) os.write(2, b'\n') if self.limit != -1 and self.limit <= self.counters[name]: - os.write(2, b"[Warning:Meta] The warning '%s' has reached the limit %d and will be suppressed\n" % (warning.name, self.limit)) + os.write(2, b"[Warning:Meta] The warning '%s' has reached the limit %d and will be suppressed\n" % (name, self.limit)) + + +warnings = WarningPool() diff --git a/setup.py b/setup.py index 7c22fc7..29de84c 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ def get_readme(): tests_require = [ - 'ruff', 'tox', 'pytest>=3.0.1', + 'ruff', 'tox', 'pytest>=3.0.1', 'pytest-mock' ] setup( diff --git a/tests/test_option.py b/tests/test_option.py new file mode 100644 index 0000000..a0bfcd5 --- /dev/null +++ b/tests/test_option.py @@ -0,0 +1,36 @@ +import pytest +from aheui import option +from aheui.option import process_options + + +def test_option_filename(mocker): + mocker.patch('os.open', return_value=0) + mocker.patch('os.close', return_value=None) + mocker.patch('aheui.compile.read', return_value=b'') + + assert ('', 'text', b'', '1', 'run', None, False, '-', 3, -1) == process_options(['aheui-c', '-'], {}) + assert ('', 'text', b'', '1', 'run', 'x.aheuic', False, '-', 3, -1) == process_options(['aheui-c', 'x'], {}) + assert ('', 'text', b'', '1', 'run', 'x.aheuic', False, '-', 3, -1) == process_options(['aheui-c', 'x.aheui'], {}) + assert ('', 'asm', b'', '1', 'run', 'x.aheuic', False, '-', 3, -1) == process_options(['aheui-c', 'x.aheuis'], {}) + assert ('', 'bytecode', b'', '1', 'run', None, False, '-', 3, -1) == process_options(['aheui-c', 'x.aheuic'], {}) + + assert ('', 'text', b'', '1', 'run', None, False, '-', 3, -1) == process_options(['aheui-c', '-', '--output=-'], {}) + assert ('', 'text', b'', '1', 'run', None, False, 'out', 3, -1) == process_options(['aheui-c', '-', '--output=out'], {}) + + +def test_option_cmd(mocker): + mocker.patch('os.open', return_value=0) + mocker.patch('os.close', return_value=None) + mocker.patch('aheui.compile.read', return_value=b'') + + heui = '희'.encode('utf-8') + assert (heui, 'text', heui, '1', 'run', None, False, '-', 3, -1) == process_options(['aheui-c', '-c', '희'], {}) + assert (heui, 'text', heui, '1', 'run', None, False, '-', 3, -1) == process_options(['aheui-c', '-c', '희', '--output=-'], {}) + assert (heui, 'text', heui, '1', 'run', None, False, 'out', 3, -1) == process_options(['aheui-c', '-c', '희', '--output=out'], {}) + with pytest.raises(option.ParsingError): + process_options(['aheui-c', '-c'], {}) + with pytest.raises(option.CommandConflictInputFileError): + process_options(['aheui-c', '-c', '희', 'x'], {}) + assert (heui, 'text', heui, '1', 'asm', None, False, '-', 3, -1) == process_options(['aheui-c', '-c', '희', '--target=asm'], {}) + assert (heui, 'text', heui, '1', 'asm', None, False, '-', 3, -1) == process_options(['aheui-c', '-c', '희', '--target=asm', '--output=-'], {}) + assert (heui, 'text', heui, '1', 'asm', None, False, 'out', 3, -1) == process_options(['aheui-c', '-c', '희', '--target=asm', '--output=out'], {})