diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9d4873c --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +*.egg-info/ +.hypothesis/ +.mypy_cache/ +.pytest_cache/ +.vscode/ +__pycache__/ +build/ +dist/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..e22d5ed --- /dev/null +++ b/.travis.yml @@ -0,0 +1,70 @@ +# https://github.com/brandtbucher/travis-python-matrix + +# CPython 3.7.4 / CPython 3.6.8 +# Ubuntu 16.04 (Xenial Xerus) / Windows 10 (64-bit) / macOS 10.14 (Mojave) + +matrix: + + include: + + - name: CPython 3.7.4 on Ubuntu 16.04 (Xenial Xerus) + language: python + os: linux + dist: xenial + python: 3.7.4 + + - name: CPython 3.6.8 on Ubuntu 16.04 (Xenial Xerus) + language: python + os: linux + dist: xenial + python: 3.6.8 + + - name: CPython 3.7.4 on Windows 10 (64-bit) + language: shell + os: windows + before_install: + - export PATH=/c/Python37:/c/Python37/Scripts:$PATH + - choco install python --version 3.7.4 + + - name: CPython 3.6.8 on Windows 10 (64-bit) + language: shell + os: windows + before_install: + - export PATH=/c/Python36:/c/Python36/Scripts:$PATH + - choco install python --version 3.6.8 + + - name: CPython 3.7.4 on macOS 10.14 (Mojave) + language: shell + os: osx + osx_image: xcode10.2 + before_install: + - export PATH=/Users/travis/.pyenv/shims:$PATH PYENV_VERSION=3.7.4 + - travis_wait brew upgrade pyenv && pyenv install $PYENV_VERSION + + - name: CPython 3.6.8 on macOS 10.14 (Mojave) + language: shell + os: osx + osx_image: xcode10.2 + before_install: + - export PATH=/Users/travis/.pyenv/shims:$PATH PYENV_VERSION=3.6.8 + - CFLAGS="-I$(xcrun --show-sdk-path)/usr/include" pyenv install $PYENV_VERSION + + - name: mypy + language: python + os: linux + dist: xenial + python: 3.8 + script: mypy + + - name: Black + language: python + os: linux + dist: xenial + python: 3.8 + script: black --check . + +# Python and pip exectuables are just named "python" and "pip" on all platforms! + +install: pip install -r requirements.txt + +script: pytest \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..2154299 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,30 @@ +
+ +The MIT License +=============== + +### Copyright © 2019 Gary Brandt Bucher, II + +
+ +
+ +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +
diff --git a/README.md b/README.md new file mode 100644 index 0000000..78c8664 --- /dev/null +++ b/README.md @@ -0,0 +1,99 @@ +
+ +HAX +=== + +[![latest version](https://img.shields.io/github/release-pre/brandtbucher/hax.svg?style=for-the-badge&label=latest)![latest release date](https://img.shields.io/github/release-date-pre/brandtbucher/hax.svg?style=for-the-badge&label=released)](https://github.com/brandtbucher/hax/releases)[![build status](https://img.shields.io/travis/com/brandtbucher/hax/master.svg?style=for-the-badge)](https://travis-ci.com/brandtbucher/hax/branches)[![issues](https://img.shields.io/github/issues-raw/brandtbucher/hax.svg?label=issues&style=for-the-badge)](https://github.com/brandtbucher/hax/issues) + +
+ +
+ +
+ +HAX lets you write compiled bytecode inline with standard Python syntax. + +Installation +------------ + +HAX supports CPython 3.6–3.9. + +To install, just run: + +```sh +$ pip install hax +``` + +Example +------- + +Consider the following function; it accepts a sequence of items, and returns a +list with each item repeated twice: + +```py +from typing import List, Sequence, TypeVar + +T = TypeVar("T") + +def double(items: Sequence[T]) -> List[T]: + out = [] + for item in items: + out += item, item + return out +``` + +For example, `(0, 1, 2)` becomes `[0, 0, 1, 1, 2, 2]`. + +We can make this function faster by keeping `out` on the stack (instead of in a +local variable) and using the `LIST_APPEND` op to build it. HAX makes it +simple to inline these instructions: + +```py +from hax import * + +@hax +def double(items: Sequence[T]) -> List[T]: + + BUILD_LIST(0) + + for item in items: + + LOAD_FAST("item") + DUP_TOP() + LIST_APPEND(3) + LIST_APPEND(2) + + RETURN_VALUE() +``` + +If you're up to the challenge of computing jump targets, the function can be +further sped up by rewriting the for-loop in bytecode, removing _all_ temporary +variables, and operating **entirely on the stack**: + +```py +@hax +def double(items: Sequence[T]) -> List[T]: + + BUILD_LIST(0) + + LOAD_FAST("items") + GET_ITER() + FOR_ITER(34) # When done, jump forward to RETURN_VALUE(). + + DUP_TOP() + LIST_APPEND(3) + LIST_APPEND(2) + JUMP_ABSOLUTE(28) # Jump back to FOR_ITER(34). + + RETURN_VALUE() +``` + +It's important to realize that the functions HAX provides (`BUILD_LIST`, +`LOAD_FAST`, ...) aren't just "emulating" their respective bytecode +instructions; the `@hax` decorator detects them, and completely recompiles +`double`'s code to use the _actual_ ops that we've specified here! + +These performance improvements are impossible to get from CPython's compiler and +optimizer alone. + +
diff --git a/hax.py b/hax.py new file mode 100644 index 0000000..89955e6 --- /dev/null +++ b/hax.py @@ -0,0 +1,771 @@ +import dis +import os +import sys +import types +import typing + + +if sys.version_info < (3, 6): + raise RuntimeError("HAX only supports Python 3.6+!") + + +if sys.implementation.name != "cpython": + raise RuntimeError("HAX only supports CPython!") + + +__version__ = "0.0.0" + + +_F = typing.TypeVar("_F", bound=typing.Callable[..., typing.Any]) + + +_USAGE_MESSAGE = "HAX inline bytecode functions are not meant to be used directly; you must decorate any functions that use them with @hax." + + +_CALL_FUNCTION = dis.opmap["CALL_FUNCTION"] +_EXTENDED_ARG = dis.opmap["EXTENDED_ARG"] +_LOAD_CONST = dis.opmap["LOAD_CONST"] +_NOP = dis.opmap["NOP"] +_POP_TOP = dis.opmap["POP_TOP"] + + +class HaxCompileError(SyntaxError): + pass + + +class HaxUsageError(RuntimeError): + pass + + +def _raise_hax_error( + message: str, filename: str, line: int, op: dis.Instruction +) -> None: + + if os.path.isfile(filename): + with open(filename) as file: + for line_number, text in enumerate(file, 1): + if line_number != line: + continue + source: typing.Optional[str] = text + if op.starts_line: + column: typing.Optional[int] = text.find(str(op.argval)) + if column == -1: + column = None + break + else: + source = column = None + + raise HaxCompileError(message, (filename, line, column, source)) + + +def _instructions_with_lines( + code: types.CodeType +) -> typing.Iterator[typing.Tuple[dis.Instruction, int]]: + line = code.co_firstlineno + for instruction in dis.get_instructions(code): + line = instruction.starts_line or line + yield instruction, line + + +def hax(function: _F) -> _F: + + ops = _instructions_with_lines(function.__code__) + + code = bytearray(function.__code__.co_code) + names = list(function.__code__.co_names) + stacksize = function.__code__.co_stacksize + varnames = list(function.__code__.co_varnames) + + start = 0 + + for _ in range(len(code) // 2 + 1): + + for op, line in ops: + + if op.opcode != _EXTENDED_ARG: + break + + else: + + break + + if op.argval not in dis.opmap: + start = op.offset + 2 + continue + + if op.opname not in { + "LOAD_CLASSDEREF", + "LOAD_CLOSURE", + "LOAD_DEREF", + "LOAD_FAST", + "LOAD_GLOBAL", + "LOAD_NAME", + }: + message = "Ops must consist of a simple call." + _raise_hax_error(message, function.__code__.co_filename, line, op) + + new_op = dis.opmap[op.argval] + + has_arg = dis.HAVE_ARGUMENT <= new_op + + args = 0 + + for following, _ in ops: + + if following.opcode == _EXTENDED_ARG: + continue + + if following.opcode == _LOAD_CONST: + arg = following.argval + args += 1 + continue + + break + + else: + message = "Ops must consist of a simple call." + _raise_hax_error(message, function.__code__.co_filename, line, op) + + if following.opcode != _CALL_FUNCTION: + message = "Ops must consist of a simple call." + _raise_hax_error(message, function.__code__.co_filename, line, op) + + if args != has_arg: + message = ( + f"Number of arguments is wrong (expected {int(has_arg)}, got {args})." + ) + _raise_hax_error(message, function.__code__.co_filename, line, op) + + following, _ = next(ops) + + if following.opcode != _POP_TOP: + message = "Ops must be standalone statements." + _raise_hax_error(message, function.__code__.co_filename, line, op) + + line = following.starts_line or line + + if new_op in dis.hasname: + try: + arg = names.index(arg) + except ValueError: + names.append(arg) + arg = len(names) - 1 + elif new_op in dis.hasconst: + arg = function.__code__.co_consts.index(arg) + elif new_op in dis.hascompare: + try: + arg = dis.cmp_op.index(arg) + except ValueError: + message = f"Bad comparision operator {arg!r}; expected one of {' / '.join(map(repr, dis.cmp_op))}!" + _raise_hax_error( + message, function.__code__.co_filename, line, following + ) + elif new_op in dis.haslocal: + try: + arg = varnames.index(arg) + except ValueError: + varnames.append(arg) + arg = len(varnames) - 1 + elif new_op in dis.hasfree: + try: + arg = ( + function.__code__.co_cellvars + function.__code__.co_freevars + ).index(arg) + except ValueError: + message = f'No free/cell variable {arg!r}; maybe use "nonlocal" in the inner scope to compile correctly?' + _raise_hax_error( + message, function.__code__.co_filename, line, following + ) + elif not isinstance(arg, int): + message = f"Expected integer argument, got {arg!r}." + _raise_hax_error(message, function.__code__.co_filename, line, following) + + if arg > (1 << 32) - 1: + message = ( + f"Args greater than {(1 << 32) - 1:,} aren't supported (got {arg:,})!" + ) + _raise_hax_error(message, function.__code__.co_filename, line, following) + + if arg < 0: + message = f"Args less than 0 aren't supported (got {arg:,})!" + _raise_hax_error(message, function.__code__.co_filename, line, following) + + for offset in range(start, following.offset, 2): + code[offset : offset + 2] = _NOP, 0 + + start = following.offset + 2 + + code[following.offset : following.offset + 2] = new_op, arg & 255 + arg >>= 8 + + for lookback in range(2, 8, 2): + if not arg: + break + code[following.offset - lookback : following.offset - lookback + 2] = ( + _EXTENDED_ARG, + arg & 255, + ) + arg >>= 8 + + assert not arg, f"Leftover bytes in arg ({arg:,})!" + + # This is the worst-case stack size... but we can probably be more exact. + stacksize = max( + stacksize, stacksize + dis.stack_effect(new_op, arg if has_arg else None) + ) + + else: + + assert False, "Main loop exited prematurely!" + + assert len(function.__code__.co_code) == len(code), "Code changed size!" + + function.__code__ = types.CodeType( + function.__code__.co_argcount, + function.__code__.co_kwonlyargcount, + len(varnames), + stacksize, + function.__code__.co_flags, + bytes(code), + function.__code__.co_consts, + tuple(names), + tuple(varnames), + function.__code__.co_filename, + function.__code__.co_name, + function.__code__.co_firstlineno, + function.__code__.co_lnotab, + function.__code__.co_freevars, + function.__code__.co_cellvars, + ) + + return function + + +def BEFORE_ASYNC_WITH() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +if (3, 8) <= sys.version_info: + + def BEGIN_FINALLY() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def BINARY_ADD() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def BINARY_AND() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def BINARY_FLOOR_DIVIDE() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def BINARY_LSHIFT() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def BINARY_MATRIX_MULTIPLY() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def BINARY_MODULO() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def BINARY_MULTIPLY() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def BINARY_OR() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def BINARY_POWER() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def BINARY_RSHIFT() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def BINARY_SUBSCR() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def BINARY_SUBTRACT() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def BINARY_TRUE_DIVIDE() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def BINARY_XOR() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +if sys.version_info < (3, 8): + + def BREAK_LOOP() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def BUILD_CONST_KEY_MAP(arg: int) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def BUILD_LIST(arg: int) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def BUILD_LIST_UNPACK(arg: int) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def BUILD_MAP(arg: int) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def BUILD_MAP_UNPACK(arg: int) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def BUILD_MAP_UNPACK_WITH_CALL(arg: int) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def BUILD_SET(arg: int) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def BUILD_SET_UNPACK(arg: int) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def BUILD_SLICE(arg: int) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def BUILD_STRING(arg: int) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def BUILD_TUPLE(arg: int) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def BUILD_TUPLE_UNPACK(arg: int) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def BUILD_TUPLE_UNPACK_WITH_CALL(arg: int) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +if (3, 8) <= sys.version_info: + + def CALL_FINALLY(arg: int) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def CALL_FUNCTION(arg: int) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def CALL_FUNCTION_EX(arg: int) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def CALL_FUNCTION_KW(arg: int) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +if (3, 7) <= sys.version_info: + + def CALL_METHOD(arg: int) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def COMPARE_OP(arg: str) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +if sys.version_info < (3, 8): + + def CONTINUE_LOOP(arg: int) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def DELETE_ATTR(arg: str) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def DELETE_DEREF(arg: str) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def DELETE_FAST(arg: str) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def DELETE_GLOBAL(arg: str) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def DELETE_NAME(arg: str) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def DELETE_SUBSCR() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def DUP_TOP() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def DUP_TOP_TWO() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +if (3, 8) <= sys.version_info: + + def END_ASYNC_FOR() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def END_FINALLY() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def EXTENDED_ARG(arg: int) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def FORMAT_VALUE(arg: int) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def FOR_ITER(arg: int) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def GET_AITER() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def GET_ANEXT() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def GET_AWAITABLE() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def GET_ITER() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def GET_YIELD_FROM_ITER() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def IMPORT_FROM(arg: str) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def IMPORT_NAME(arg: str) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def IMPORT_STAR() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def INPLACE_ADD() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def INPLACE_AND() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def INPLACE_FLOOR_DIVIDE() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def INPLACE_LSHIFT() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def INPLACE_MATRIX_MULTIPLY() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def INPLACE_MODULO() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def INPLACE_MULTIPLY() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def INPLACE_OR() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def INPLACE_POWER() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def INPLACE_RSHIFT() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def INPLACE_SUBTRACT() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def INPLACE_TRUE_DIVIDE() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def INPLACE_XOR() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def JUMP_ABSOLUTE(arg: int) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def JUMP_FORWARD(arg: int) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def JUMP_IF_FALSE_OR_POP(arg: int) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def JUMP_IF_TRUE_OR_POP(arg: int) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def LIST_APPEND(arg: int) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +if (3, 9) <= sys.version_info: + + def LOAD_ASSERTION_ERROR() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def LOAD_ATTR(arg: str) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def LOAD_BUILD_CLASS() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def LOAD_CLASSDEREF(arg: str) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def LOAD_CLOSURE(arg: str) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def LOAD_CONST(arg: typing.Hashable) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def LOAD_DEREF(arg: str) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def LOAD_FAST(arg: str) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def LOAD_GLOBAL(arg: str) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +if (3, 7) <= sys.version_info: + + def LOAD_METHOD(arg: str) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def LOAD_NAME(arg: str) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def MAKE_FUNCTION(arg: int) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def MAP_ADD(arg: int) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def NOP() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def POP_BLOCK() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def POP_EXCEPT() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +if (3, 8) <= sys.version_info: + + def POP_FINALLY(arg: bool) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def POP_JUMP_IF_FALSE(arg: int) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def POP_JUMP_IF_TRUE(arg: int) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def POP_TOP() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def PRINT_EXPR() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def RAISE_VARARGS(arg: int) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def RETURN_VALUE() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +if (3, 8) <= sys.version_info: + + def ROT_FOUR() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def ROT_THREE() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def ROT_TWO() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def SETUP_ANNOTATIONS() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def SETUP_ASYNC_WITH(arg: int) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +if sys.version_info < (3, 8): + + def SETUP_EXCEPT(arg: int) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def SETUP_FINALLY(arg: int) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +if sys.version_info < (3, 8): + + def SETUP_LOOP(arg: int) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def SETUP_WITH(arg: int) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def SET_ADD(arg: int) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def STORE_ATTR(arg: str) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +if sys.version_info < (3, 7): + + def STORE_ANNOTATION(arg: str) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def STORE_DEREF(arg: str) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def STORE_FAST(arg: str) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def STORE_GLOBAL(arg: str) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def STORE_NAME(arg: str) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def STORE_SUBSCR() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def UNARY_INVERT() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def UNARY_NEGATIVE() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def UNARY_NOT() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def UNARY_POSITIVE() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def UNPACK_EX(arg: int) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def UNPACK_SEQUENCE(arg: int) -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def WITH_CLEANUP_FINISH() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def WITH_CLEANUP_START() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def YIELD_FROM() -> None: + raise HaxUsageError(_USAGE_MESSAGE) + + +def YIELD_VALUE() -> None: + raise HaxUsageError(_USAGE_MESSAGE) diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..5857706 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,26 @@ +[mypy] + +files = **/*.py + +check_untyped_defs = True +disallow_any_generics = True +disallow_incomplete_defs = True +disallow_subclassing_any = True +disallow_untyped_calls = True +disallow_untyped_decorators = True +disallow_untyped_defs = True +no_implicit_optional = True +no_implicit_reexport = True +warn_redundant_casts = True +warn_return_any = True +warn_unused_configs = True +warn_unused_ignores = True + +[mypy-pytest] + +ignore_missing_imports = True + +[mypy-test] + +allow_untyped_decorators = True + diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..606a6f4 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,4 @@ +[pytest] + +addopts = --forked --verbose --numprocesses auto +python_files = test.py \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f9fb0f8 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +black +hypothesis +mypy +pytest \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..ed527fa --- /dev/null +++ b/setup.py @@ -0,0 +1,22 @@ +import setuptools # type: ignore + +import hax + + +with open("README.md") as readme: + long_description = readme.read() + + +setuptools.setup( + author="Brandt Bucher", + author_email="brandtbucher@gmail.com", + description="Write compiled bytecode inline with standard Python syntax.", + keywords="bytecode", + license="MIT", + long_description=long_description, + long_description_content_type="text/markdown", + name="hax", + py_modules=["hax"], + url="https://github.com/brandtbucher/hax", + version=hax.__version__, +) diff --git a/test.py b/test.py new file mode 100644 index 0000000..12d0d52 --- /dev/null +++ b/test.py @@ -0,0 +1,45 @@ +import dis +import itertools +import re +import typing + +import hypothesis +import pytest + +import hax + + +def get_examples() -> typing.Iterator[str]: + + with open("README.md") as readme: + examples = re.findall(r"\n```py(\n[^`]+\n)```\n", readme.read()) + + for i, example in enumerate(itertools.accumulate(examples)): + yield pytest.param(example, id=f"{i}") + + +@hypothesis.given(items=hypothesis.infer) +@pytest.mark.parametrize("code", get_examples()) +def test_readme(code: str, items: typing.Sequence[object]) -> None: + + namespace: typing.Dict[str, typing.Any] = {"__name__": "__main__"} + exec(code, namespace) + + actual = namespace["double"](items) + expected = [*itertools.chain.from_iterable(zip(items, items))] + + assert actual == expected + + +@pytest.mark.parametrize("opname, opcode", dis.opmap.items()) +def test_opcodes(opname: str, opcode: int) -> None: + + arg = dis.HAVE_ARGUMENT <= opcode + + assert hasattr(hax, opname) + + with pytest.raises(hax.HaxUsageError if arg else TypeError): + getattr(hax, opname)(...) + + with pytest.raises(TypeError if arg else hax.HaxUsageError): + getattr(hax, opname)()