diff --git a/Lib/test/test_generated_cases.py b/Lib/test/test_generated_cases.py index 57991b5b6b859c..ce34aefaf72828 100644 --- a/Lib/test/test_generated_cases.py +++ b/Lib/test/test_generated_cases.py @@ -1,2642 +1,2711 @@ -import contextlib -import os -import sys -import tempfile -import unittest - -from test import support -from test import test_tools - - -def skip_if_different_mount_drives(): - if sys.platform != "win32": - return - ROOT = os.path.dirname(os.path.dirname(__file__)) - root_drive = os.path.splitroot(ROOT)[0] - cwd_drive = os.path.splitroot(os.getcwd())[0] - if root_drive != cwd_drive: - # May raise ValueError if ROOT and the current working - # different have different mount drives (on Windows). - raise unittest.SkipTest( - f"the current working directory and the Python source code " - f"directory have different mount drives " - f"({cwd_drive} and {root_drive})" - ) - - -skip_if_different_mount_drives() - - -test_tools.skip_if_missing("cases_generator") -with test_tools.imports_under_tool("cases_generator"): - from analyzer import StackItem - from cwriter import CWriter - import parser - from stack import Local, Stack - import tier1_generator - import optimizer_generator - - -def handle_stderr(): - if support.verbose > 1: - return contextlib.nullcontext() - else: - return support.captured_stderr() - - -def parse_src(src): - p = parser.Parser(src, "test.c") - nodes = [] - while node := p.definition(): - nodes.append(node) - return nodes - - -class TestEffects(unittest.TestCase): - def test_effect_sizes(self): - stack = Stack() - inputs = [ - x := StackItem("x", "1"), - y := StackItem("y", "oparg"), - z := StackItem("z", "oparg*2"), - ] - outputs = [ - StackItem("x", "1"), - StackItem("b", "oparg*4"), - StackItem("c", "1"), - ] - null = CWriter.null() - stack.pop(z, null) - stack.pop(y, null) - stack.pop(x, null) - for out in outputs: - stack.push(Local.undefined(out)) - self.assertEqual(stack.base_offset.to_c(), "-1 - oparg - oparg*2") - self.assertEqual(stack.physical_sp.to_c(), "0") - self.assertEqual(stack.logical_sp.to_c(), "1 - oparg - oparg*2 + oparg*4") - - -class TestGeneratedCases(unittest.TestCase): - def setUp(self) -> None: - super().setUp() - self.maxDiff = None - - self.temp_dir = tempfile.gettempdir() - self.temp_input_filename = os.path.join(self.temp_dir, "input.txt") - self.temp_output_filename = os.path.join(self.temp_dir, "output.txt") - self.temp_metadata_filename = os.path.join(self.temp_dir, "metadata.txt") - self.temp_pymetadata_filename = os.path.join(self.temp_dir, "pymetadata.txt") - self.temp_executor_filename = os.path.join(self.temp_dir, "executor.txt") - - def tearDown(self) -> None: - for filename in [ - self.temp_input_filename, - self.temp_output_filename, - self.temp_metadata_filename, - self.temp_pymetadata_filename, - self.temp_executor_filename, - ]: - try: - os.remove(filename) - except: - pass - super().tearDown() - - def run_cases_test(self, input: str, expected: str): - with open(self.temp_input_filename, "w+") as temp_input: - temp_input.write(parser.BEGIN_MARKER) - temp_input.write(input) - temp_input.write(parser.END_MARKER) - temp_input.flush() - - with handle_stderr(): - tier1_generator.generate_tier1_from_files( - [self.temp_input_filename], self.temp_output_filename, False - ) - - with open(self.temp_output_filename) as temp_output: - lines = temp_output.read() - _, rest = lines.split(tier1_generator.INSTRUCTION_START_MARKER) - instructions, labels_with_prelude_and_postlude = rest.split(tier1_generator.INSTRUCTION_END_MARKER) - _, labels_with_postlude = labels_with_prelude_and_postlude.split(tier1_generator.LABEL_START_MARKER) - labels, _ = labels_with_postlude.split(tier1_generator.LABEL_END_MARKER) - actual = instructions.strip() + "\n\n " + labels.strip() - - self.assertEqual(actual.strip(), expected.strip()) - - def test_inst_no_args(self): - input = """ - inst(OP, (--)) { - SPAM(); - } - """ - output = """ - TARGET(OP) { - #if _Py_TAIL_CALL_INTERP - int opcode = OP; - (void)(opcode); - #endif - frame->instr_ptr = next_instr; - next_instr += 1; - INSTRUCTION_STATS(OP); - SPAM(); - DISPATCH(); - } - """ - self.run_cases_test(input, output) - - def test_inst_one_pop(self): - input = """ - inst(OP, (value --)) { - SPAM(value); - DEAD(value); - } - """ - output = """ - TARGET(OP) { - #if _Py_TAIL_CALL_INTERP - int opcode = OP; - (void)(opcode); - #endif - frame->instr_ptr = next_instr; - next_instr += 1; - INSTRUCTION_STATS(OP); - _PyStackRef value; - value = stack_pointer[-1]; - SPAM(value); - stack_pointer += -1; - ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); - DISPATCH(); - } - """ - self.run_cases_test(input, output) - - def test_inst_one_push(self): - input = """ - inst(OP, (-- res)) { - res = SPAM(); - } - """ - output = """ - TARGET(OP) { - #if _Py_TAIL_CALL_INTERP - int opcode = OP; - (void)(opcode); - #endif - frame->instr_ptr = next_instr; - next_instr += 1; - INSTRUCTION_STATS(OP); - _PyStackRef res; - res = SPAM(); - stack_pointer[0] = res; - stack_pointer += 1; - ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); - DISPATCH(); - } - """ - self.run_cases_test(input, output) - - def test_inst_one_push_one_pop(self): - input = """ - inst(OP, (value -- res)) { - res = SPAM(value); - DEAD(value); - } - """ - output = """ - TARGET(OP) { - #if _Py_TAIL_CALL_INTERP - int opcode = OP; - (void)(opcode); - #endif - frame->instr_ptr = next_instr; - next_instr += 1; - INSTRUCTION_STATS(OP); - _PyStackRef value; - _PyStackRef res; - value = stack_pointer[-1]; - res = SPAM(value); - stack_pointer[-1] = res; - DISPATCH(); - } - """ - self.run_cases_test(input, output) - - def test_binary_op(self): - input = """ - inst(OP, (left, right -- res)) { - res = SPAM(left, right); - INPUTS_DEAD(); - - } - """ - output = """ - TARGET(OP) { - #if _Py_TAIL_CALL_INTERP - int opcode = OP; - (void)(opcode); - #endif - frame->instr_ptr = next_instr; - next_instr += 1; - INSTRUCTION_STATS(OP); - _PyStackRef left; - _PyStackRef right; - _PyStackRef res; - right = stack_pointer[-1]; - left = stack_pointer[-2]; - res = SPAM(left, right); - stack_pointer[-2] = res; - stack_pointer += -1; - ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); - DISPATCH(); - } - """ - self.run_cases_test(input, output) - - def test_overlap(self): - input = """ - inst(OP, (left, right -- left, result)) { - result = SPAM(left, right); - INPUTS_DEAD(); - } - """ - output = """ - TARGET(OP) { - #if _Py_TAIL_CALL_INTERP - int opcode = OP; - (void)(opcode); - #endif - frame->instr_ptr = next_instr; - next_instr += 1; - INSTRUCTION_STATS(OP); - _PyStackRef left; - _PyStackRef right; - _PyStackRef result; - right = stack_pointer[-1]; - left = stack_pointer[-2]; - result = SPAM(left, right); - stack_pointer[-1] = result; - DISPATCH(); - } - """ - self.run_cases_test(input, output) - - def test_predictions(self): - input = """ - inst(OP1, (arg -- res)) { - DEAD(arg); - res = Py_None; - } - inst(OP3, (arg -- res)) { - DEAD(arg); - DEOPT_IF(xxx); - res = Py_None; - } - family(OP1, INLINE_CACHE_ENTRIES_OP1) = { OP3 }; - """ - output = """ - TARGET(OP1) { - #if _Py_TAIL_CALL_INTERP - int opcode = OP1; - (void)(opcode); - #endif - frame->instr_ptr = next_instr; - next_instr += 1; - INSTRUCTION_STATS(OP1); - PREDICTED_OP1:; - _PyStackRef arg; - _PyStackRef res; - arg = stack_pointer[-1]; - res = Py_None; - stack_pointer[-1] = res; - DISPATCH(); - } - - TARGET(OP3) { - #if _Py_TAIL_CALL_INTERP - int opcode = OP3; - (void)(opcode); - #endif - _Py_CODEUNIT* const this_instr = next_instr; - (void)this_instr; - frame->instr_ptr = next_instr; - next_instr += 1; - INSTRUCTION_STATS(OP3); - static_assert(INLINE_CACHE_ENTRIES_OP1 == 0, "incorrect cache size"); - _PyStackRef arg; - _PyStackRef res; - arg = stack_pointer[-1]; - if (xxx) { - UPDATE_MISS_STATS(OP1); - assert(_PyOpcode_Deopt[opcode] == (OP1)); - JUMP_TO_PREDICTED(OP1); - } - res = Py_None; - stack_pointer[-1] = res; - DISPATCH(); - } - """ - self.run_cases_test(input, output) - - def test_sync_sp(self): - input = """ - inst(A, (arg -- res)) { - DEAD(arg); - SYNC_SP(); - escaping_call(); - res = Py_None; - } - inst(B, (arg -- res)) { - DEAD(arg); - res = Py_None; - SYNC_SP(); - escaping_call(); - } - """ - output = """ - TARGET(A) { - #if _Py_TAIL_CALL_INTERP - int opcode = A; - (void)(opcode); - #endif - frame->instr_ptr = next_instr; - next_instr += 1; - INSTRUCTION_STATS(A); - _PyStackRef arg; - _PyStackRef res; - arg = stack_pointer[-1]; - stack_pointer += -1; - ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); - _PyFrame_SetStackPointer(frame, stack_pointer); - escaping_call(); - stack_pointer = _PyFrame_GetStackPointer(frame); - res = Py_None; - stack_pointer[0] = res; - stack_pointer += 1; - ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); - DISPATCH(); - } - - TARGET(B) { - #if _Py_TAIL_CALL_INTERP - int opcode = B; - (void)(opcode); - #endif - frame->instr_ptr = next_instr; - next_instr += 1; - INSTRUCTION_STATS(B); - _PyStackRef arg; - _PyStackRef res; - arg = stack_pointer[-1]; - res = Py_None; - stack_pointer[-1] = res; - _PyFrame_SetStackPointer(frame, stack_pointer); - escaping_call(); - stack_pointer = _PyFrame_GetStackPointer(frame); - DISPATCH(); - } - """ - self.run_cases_test(input, output) - - - def test_pep7_condition(self): - input = """ - inst(OP, (arg1 -- out)) { - if (arg1) - out = 0; - else { - out = 1; - } - } - """ - output = "" - with self.assertRaises(SyntaxError): - self.run_cases_test(input, output) - - def test_error_if_plain(self): - input = """ - inst(OP, (--)) { - ERROR_IF(cond); - } - """ - output = """ - TARGET(OP) { - #if _Py_TAIL_CALL_INTERP - int opcode = OP; - (void)(opcode); - #endif - frame->instr_ptr = next_instr; - next_instr += 1; - INSTRUCTION_STATS(OP); - if (cond) { - JUMP_TO_LABEL(error); - } - DISPATCH(); - } - """ - self.run_cases_test(input, output) - - def test_error_if_plain_with_comment(self): - input = """ - inst(OP, (--)) { - ERROR_IF(cond); // Comment is ok - } - """ - output = """ - TARGET(OP) { - #if _Py_TAIL_CALL_INTERP - int opcode = OP; - (void)(opcode); - #endif - frame->instr_ptr = next_instr; - next_instr += 1; - INSTRUCTION_STATS(OP); - if (cond) { - JUMP_TO_LABEL(error); - } - DISPATCH(); - } - """ - self.run_cases_test(input, output) - - def test_error_if_pop(self): - input = """ - inst(OP, (left, right -- res)) { - SPAM(left, right); - INPUTS_DEAD(); - ERROR_IF(cond); - res = 0; - } - """ - output = """ - TARGET(OP) { - #if _Py_TAIL_CALL_INTERP - int opcode = OP; - (void)(opcode); - #endif - frame->instr_ptr = next_instr; - next_instr += 1; - INSTRUCTION_STATS(OP); - _PyStackRef left; - _PyStackRef right; - _PyStackRef res; - right = stack_pointer[-1]; - left = stack_pointer[-2]; - SPAM(left, right); - if (cond) { - JUMP_TO_LABEL(pop_2_error); - } - res = 0; - stack_pointer[-2] = res; - stack_pointer += -1; - ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); - DISPATCH(); - } - """ - self.run_cases_test(input, output) - - def test_error_if_pop_with_result(self): - input = """ - inst(OP, (left, right -- res)) { - res = SPAM(left, right); - INPUTS_DEAD(); - ERROR_IF(cond); - } - """ - output = """ - TARGET(OP) { - #if _Py_TAIL_CALL_INTERP - int opcode = OP; - (void)(opcode); - #endif - frame->instr_ptr = next_instr; - next_instr += 1; - INSTRUCTION_STATS(OP); - _PyStackRef left; - _PyStackRef right; - _PyStackRef res; - right = stack_pointer[-1]; - left = stack_pointer[-2]; - res = SPAM(left, right); - if (cond) { - JUMP_TO_LABEL(pop_2_error); - } - stack_pointer[-2] = res; - stack_pointer += -1; - ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); - DISPATCH(); - } - """ - self.run_cases_test(input, output) - - def test_cache_effect(self): - input = """ - inst(OP, (counter/1, extra/2, value --)) { - DEAD(value); - } - """ - output = """ - TARGET(OP) { - #if _Py_TAIL_CALL_INTERP - int opcode = OP; - (void)(opcode); - #endif - _Py_CODEUNIT* const this_instr = next_instr; - (void)this_instr; - frame->instr_ptr = next_instr; - next_instr += 4; - INSTRUCTION_STATS(OP); - _PyStackRef value; - value = stack_pointer[-1]; - uint16_t counter = read_u16(&this_instr[1].cache); - (void)counter; - uint32_t extra = read_u32(&this_instr[2].cache); - (void)extra; - stack_pointer += -1; - ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); - DISPATCH(); - } - """ - self.run_cases_test(input, output) - - def test_suppress_dispatch(self): - input = """ - label(somewhere) { - } - - inst(OP, (--)) { - goto somewhere; - } - """ - output = """ - TARGET(OP) { - #if _Py_TAIL_CALL_INTERP - int opcode = OP; - (void)(opcode); - #endif - frame->instr_ptr = next_instr; - next_instr += 1; - INSTRUCTION_STATS(OP); - JUMP_TO_LABEL(somewhere); - } - - LABEL(somewhere) - { - } - """ - self.run_cases_test(input, output) - - def test_macro_instruction(self): - input = """ - inst(OP1, (counter/1, left, right -- left, right)) { - op1(left, right); - } - op(OP2, (extra/2, arg2, left, right -- res)) { - res = op2(arg2, left, right); - INPUTS_DEAD(); - } - macro(OP) = OP1 + cache/2 + OP2; - inst(OP3, (unused/5, arg2, left, right -- res)) { - res = op3(arg2, left, right); - INPUTS_DEAD(); - } - family(OP, INLINE_CACHE_ENTRIES_OP) = { OP3 }; - """ - output = """ - TARGET(OP) { - #if _Py_TAIL_CALL_INTERP - int opcode = OP; - (void)(opcode); - #endif - frame->instr_ptr = next_instr; - next_instr += 6; - INSTRUCTION_STATS(OP); - PREDICTED_OP:; - _Py_CODEUNIT* const this_instr = next_instr - 6; - (void)this_instr; - _PyStackRef left; - _PyStackRef right; - _PyStackRef arg2; - _PyStackRef res; - // _OP1 - { - right = stack_pointer[-1]; - left = stack_pointer[-2]; - uint16_t counter = read_u16(&this_instr[1].cache); - (void)counter; - _PyFrame_SetStackPointer(frame, stack_pointer); - op1(left, right); - stack_pointer = _PyFrame_GetStackPointer(frame); - } - /* Skip 2 cache entries */ - // OP2 - { - arg2 = stack_pointer[-3]; - uint32_t extra = read_u32(&this_instr[4].cache); - (void)extra; - _PyFrame_SetStackPointer(frame, stack_pointer); - res = op2(arg2, left, right); - stack_pointer = _PyFrame_GetStackPointer(frame); - } - stack_pointer[-3] = res; - stack_pointer += -2; - ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); - DISPATCH(); - } - - TARGET(OP1) { - #if _Py_TAIL_CALL_INTERP - int opcode = OP1; - (void)(opcode); - #endif - _Py_CODEUNIT* const this_instr = next_instr; - (void)this_instr; - frame->instr_ptr = next_instr; - next_instr += 2; - INSTRUCTION_STATS(OP1); - _PyStackRef left; - _PyStackRef right; - right = stack_pointer[-1]; - left = stack_pointer[-2]; - uint16_t counter = read_u16(&this_instr[1].cache); - (void)counter; - _PyFrame_SetStackPointer(frame, stack_pointer); - op1(left, right); - stack_pointer = _PyFrame_GetStackPointer(frame); - DISPATCH(); - } - - TARGET(OP3) { - #if _Py_TAIL_CALL_INTERP - int opcode = OP3; - (void)(opcode); - #endif - frame->instr_ptr = next_instr; - next_instr += 6; - INSTRUCTION_STATS(OP3); - static_assert(INLINE_CACHE_ENTRIES_OP == 5, "incorrect cache size"); - _PyStackRef arg2; - _PyStackRef left; - _PyStackRef right; - _PyStackRef res; - /* Skip 5 cache entries */ - right = stack_pointer[-1]; - left = stack_pointer[-2]; - arg2 = stack_pointer[-3]; - _PyFrame_SetStackPointer(frame, stack_pointer); - res = op3(arg2, left, right); - stack_pointer = _PyFrame_GetStackPointer(frame); - stack_pointer[-3] = res; - stack_pointer += -2; - ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); - DISPATCH(); - } - """ - self.run_cases_test(input, output) - - def test_unused_caches(self): - input = """ - inst(OP, (unused/1, unused/2 --)) { - body; - } - """ - output = """ - TARGET(OP) { - #if _Py_TAIL_CALL_INTERP - int opcode = OP; - (void)(opcode); - #endif - frame->instr_ptr = next_instr; - next_instr += 4; - INSTRUCTION_STATS(OP); - /* Skip 1 cache entry */ - /* Skip 2 cache entries */ - body; - DISPATCH(); - } - """ - self.run_cases_test(input, output) - - def test_pseudo_instruction_no_flags(self): - input = """ - pseudo(OP, (in -- out1, out2)) = { - OP1, - }; - - inst(OP1, (--)) { - } - """ - output = """ - TARGET(OP1) { - #if _Py_TAIL_CALL_INTERP - int opcode = OP1; - (void)(opcode); - #endif - frame->instr_ptr = next_instr; - next_instr += 1; - INSTRUCTION_STATS(OP1); - DISPATCH(); - } - """ - self.run_cases_test(input, output) - - def test_pseudo_instruction_with_flags(self): - input = """ - pseudo(OP, (in1, in2 --), (HAS_ARG, HAS_JUMP)) = { - OP1, - }; - - inst(OP1, (--)) { - } - """ - output = """ - TARGET(OP1) { - #if _Py_TAIL_CALL_INTERP - int opcode = OP1; - (void)(opcode); - #endif - frame->instr_ptr = next_instr; - next_instr += 1; - INSTRUCTION_STATS(OP1); - DISPATCH(); - } - """ - self.run_cases_test(input, output) - - def test_pseudo_instruction_as_sequence(self): - input = """ - pseudo(OP, (in -- out1, out2)) = [ - OP1, OP2 - ]; - - inst(OP1, (--)) { - } - - inst(OP2, (--)) { - } - """ - output = """ - TARGET(OP1) { - #if _Py_TAIL_CALL_INTERP - int opcode = OP1; - (void)(opcode); - #endif - frame->instr_ptr = next_instr; - next_instr += 1; - INSTRUCTION_STATS(OP1); - DISPATCH(); - } - - TARGET(OP2) { - #if _Py_TAIL_CALL_INTERP - int opcode = OP2; - (void)(opcode); - #endif - frame->instr_ptr = next_instr; - next_instr += 1; - INSTRUCTION_STATS(OP2); - DISPATCH(); - } - """ - self.run_cases_test(input, output) - - - def test_array_input(self): - input = """ - inst(OP, (below, values[oparg*2], above --)) { - SPAM(values, oparg); - DEAD(below); - DEAD(values); - DEAD(above); - } - """ - output = """ - TARGET(OP) { - #if _Py_TAIL_CALL_INTERP - int opcode = OP; - (void)(opcode); - #endif - frame->instr_ptr = next_instr; - next_instr += 1; - INSTRUCTION_STATS(OP); - _PyStackRef below; - _PyStackRef *values; - _PyStackRef above; - above = stack_pointer[-1]; - values = &stack_pointer[-1 - oparg*2]; - below = stack_pointer[-2 - oparg*2]; - SPAM(values, oparg); - stack_pointer += -2 - oparg*2; - ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); - DISPATCH(); - } - """ - self.run_cases_test(input, output) - - def test_array_output(self): - input = """ - inst(OP, (unused, unused -- below, values[oparg*3], above)) { - SPAM(values, oparg); - below = 0; - above = 0; - } - """ - output = """ - TARGET(OP) { - #if _Py_TAIL_CALL_INTERP - int opcode = OP; - (void)(opcode); - #endif - frame->instr_ptr = next_instr; - next_instr += 1; - INSTRUCTION_STATS(OP); - _PyStackRef below; - _PyStackRef *values; - _PyStackRef above; - values = &stack_pointer[-1]; - SPAM(values, oparg); - below = 0; - above = 0; - stack_pointer[-2] = below; - stack_pointer[-1 + oparg*3] = above; - stack_pointer += oparg*3; - ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); - DISPATCH(); - } - """ - self.run_cases_test(input, output) - - def test_array_input_output(self): - input = """ - inst(OP, (values[oparg] -- values[oparg], above)) { - SPAM(values, oparg); - above = 0; - } - """ - output = """ - TARGET(OP) { - #if _Py_TAIL_CALL_INTERP - int opcode = OP; - (void)(opcode); - #endif - frame->instr_ptr = next_instr; - next_instr += 1; - INSTRUCTION_STATS(OP); - _PyStackRef *values; - _PyStackRef above; - values = &stack_pointer[-oparg]; - SPAM(values, oparg); - above = 0; - stack_pointer[0] = above; - stack_pointer += 1; - ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); - DISPATCH(); - } - """ - self.run_cases_test(input, output) - - def test_array_error_if(self): - input = """ - inst(OP, (extra, values[oparg] --)) { - DEAD(extra); - DEAD(values); - ERROR_IF(oparg == 0); - } - """ - output = """ - TARGET(OP) { - #if _Py_TAIL_CALL_INTERP - int opcode = OP; - (void)(opcode); - #endif - frame->instr_ptr = next_instr; - next_instr += 1; - INSTRUCTION_STATS(OP); - _PyStackRef extra; - _PyStackRef *values; - values = &stack_pointer[-oparg]; - extra = stack_pointer[-1 - oparg]; - if (oparg == 0) { - stack_pointer += -1 - oparg; - ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); - JUMP_TO_LABEL(error); - } - stack_pointer += -1 - oparg; - ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); - DISPATCH(); - } - """ - self.run_cases_test(input, output) - - def test_macro_push_push(self): - input = """ - op(A, (-- val1)) { - val1 = SPAM(); - } - op(B, (-- val2)) { - val2 = SPAM(); - } - macro(M) = A + B; - """ - output = """ - TARGET(M) { - #if _Py_TAIL_CALL_INTERP - int opcode = M; - (void)(opcode); - #endif - frame->instr_ptr = next_instr; - next_instr += 1; - INSTRUCTION_STATS(M); - _PyStackRef val1; - _PyStackRef val2; - // A - { - val1 = SPAM(); - } - // B - { - val2 = SPAM(); - } - stack_pointer[0] = val1; - stack_pointer[1] = val2; - stack_pointer += 2; - ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); - DISPATCH(); - } - """ - self.run_cases_test(input, output) - - def test_override_inst(self): - input = """ - inst(OP, (--)) { - spam; - } - override inst(OP, (--)) { - ham; - } - """ - output = """ - TARGET(OP) { - #if _Py_TAIL_CALL_INTERP - int opcode = OP; - (void)(opcode); - #endif - frame->instr_ptr = next_instr; - next_instr += 1; - INSTRUCTION_STATS(OP); - ham; - DISPATCH(); - } - """ - self.run_cases_test(input, output) - - def test_override_op(self): - input = """ - op(OP, (--)) { - spam; - } - macro(M) = OP; - override op(OP, (--)) { - ham; - } - """ - output = """ - TARGET(M) { - #if _Py_TAIL_CALL_INTERP - int opcode = M; - (void)(opcode); - #endif - frame->instr_ptr = next_instr; - next_instr += 1; - INSTRUCTION_STATS(M); - ham; - DISPATCH(); - } - """ - self.run_cases_test(input, output) - - def test_annotated_inst(self): - input = """ - pure inst(OP, (--)) { - ham; - } - """ - output = """ - TARGET(OP) { - #if _Py_TAIL_CALL_INTERP - int opcode = OP; - (void)(opcode); - #endif - frame->instr_ptr = next_instr; - next_instr += 1; - INSTRUCTION_STATS(OP); - ham; - DISPATCH(); - } - """ - self.run_cases_test(input, output) - - def test_annotated_op(self): - input = """ - pure op(OP, (--)) { - SPAM(); - } - macro(M) = OP; - """ - output = """ - TARGET(M) { - #if _Py_TAIL_CALL_INTERP - int opcode = M; - (void)(opcode); - #endif - frame->instr_ptr = next_instr; - next_instr += 1; - INSTRUCTION_STATS(M); - SPAM(); - DISPATCH(); - } - """ - self.run_cases_test(input, output) - - input = """ - pure register specializing op(OP, (--)) { - SPAM(); - } - macro(M) = OP; - """ - self.run_cases_test(input, output) - - def test_deopt_and_exit(self): - input = """ - pure op(OP, (arg1 -- out)) { - DEOPT_IF(1); - EXIT_IF(1); - } - """ - output = "" - with self.assertRaises(SyntaxError): - self.run_cases_test(input, output) - - def test_array_of_one(self): - input = """ - inst(OP, (arg[1] -- out[1])) { - out[0] = arg[0]; - DEAD(arg); - } - """ - output = """ - TARGET(OP) { - #if _Py_TAIL_CALL_INTERP - int opcode = OP; - (void)(opcode); - #endif - frame->instr_ptr = next_instr; - next_instr += 1; - INSTRUCTION_STATS(OP); - _PyStackRef *arg; - _PyStackRef *out; - arg = &stack_pointer[-1]; - out = &stack_pointer[-1]; - out[0] = arg[0]; - DISPATCH(); - } - """ - self.run_cases_test(input, output) - - def test_unused_cached_value(self): - input = """ - op(FIRST, (arg1 -- out)) { - out = arg1; - } - - op(SECOND, (unused -- unused)) { - } - - macro(BOTH) = FIRST + SECOND; - """ - output = """ - """ - with self.assertRaises(SyntaxError): - self.run_cases_test(input, output) - - def test_unused_named_values(self): - input = """ - op(OP, (named -- named)) { - } - - macro(INST) = OP; - """ - output = """ - TARGET(INST) { - #if _Py_TAIL_CALL_INTERP - int opcode = INST; - (void)(opcode); - #endif - frame->instr_ptr = next_instr; - next_instr += 1; - INSTRUCTION_STATS(INST); - DISPATCH(); - } - - """ - self.run_cases_test(input, output) - - def test_used_unused_used(self): - input = """ - op(FIRST, (w -- w)) { - USE(w); - } - - op(SECOND, (x -- x)) { - } - - op(THIRD, (y -- y)) { - USE(y); - } - - macro(TEST) = FIRST + SECOND + THIRD; - """ - output = """ - TARGET(TEST) { - #if _Py_TAIL_CALL_INTERP - int opcode = TEST; - (void)(opcode); - #endif - frame->instr_ptr = next_instr; - next_instr += 1; - INSTRUCTION_STATS(TEST); - _PyStackRef w; - _PyStackRef y; - // FIRST - { - w = stack_pointer[-1]; - USE(w); - } - // SECOND - { - } - // THIRD - { - y = w; - USE(y); - } - DISPATCH(); - } - """ - self.run_cases_test(input, output) - - def test_unused_used_used(self): - input = """ - op(FIRST, (w -- w)) { - } - - op(SECOND, (x -- x)) { - USE(x); - } - - op(THIRD, (y -- y)) { - USE(y); - } - - macro(TEST) = FIRST + SECOND + THIRD; - """ - output = """ - TARGET(TEST) { - #if _Py_TAIL_CALL_INTERP - int opcode = TEST; - (void)(opcode); - #endif - frame->instr_ptr = next_instr; - next_instr += 1; - INSTRUCTION_STATS(TEST); - _PyStackRef x; - _PyStackRef y; - // FIRST - { - } - // SECOND - { - x = stack_pointer[-1]; - USE(x); - } - // THIRD - { - y = x; - USE(y); - } - DISPATCH(); - } - """ - self.run_cases_test(input, output) - - def test_flush(self): - input = """ - op(FIRST, ( -- a, b)) { - a = 0; - b = 1; - } - - op(SECOND, (a, b -- )) { - USE(a, b); - INPUTS_DEAD(); - } - - macro(TEST) = FIRST + flush + SECOND; - """ - output = """ - TARGET(TEST) { - #if _Py_TAIL_CALL_INTERP - int opcode = TEST; - (void)(opcode); - #endif - frame->instr_ptr = next_instr; - next_instr += 1; - INSTRUCTION_STATS(TEST); - _PyStackRef a; - _PyStackRef b; - // FIRST - { - a = 0; - b = 1; - } - // flush - stack_pointer[0] = a; - stack_pointer[1] = b; - stack_pointer += 2; - ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); - // SECOND - { - USE(a, b); - } - stack_pointer += -2; - ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); - DISPATCH(); - } - """ - self.run_cases_test(input, output) - - def test_pop_on_error_peeks(self): - - input = """ - op(FIRST, (x, y -- a, b)) { - a = x; - DEAD(x); - b = y; - DEAD(y); - } - - op(SECOND, (a, b -- a, b)) { - } - - op(THIRD, (j, k --)) { - INPUTS_DEAD(); // Mark j and k as used - ERROR_IF(cond); - } - - macro(TEST) = FIRST + SECOND + THIRD; - """ - output = """ - TARGET(TEST) { - #if _Py_TAIL_CALL_INTERP - int opcode = TEST; - (void)(opcode); - #endif - frame->instr_ptr = next_instr; - next_instr += 1; - INSTRUCTION_STATS(TEST); - _PyStackRef x; - _PyStackRef y; - _PyStackRef a; - _PyStackRef b; - // FIRST - { - y = stack_pointer[-1]; - x = stack_pointer[-2]; - a = x; - b = y; - } - // SECOND - { - } - // THIRD - { - if (cond) { - JUMP_TO_LABEL(pop_2_error); - } - } - stack_pointer += -2; - ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); - DISPATCH(); - } - """ - self.run_cases_test(input, output) - - def test_push_then_error(self): - - input = """ - op(FIRST, ( -- a)) { - a = 1; - } - - op(SECOND, (a -- a, b)) { - b = 1; - ERROR_IF(cond); - } - - macro(TEST) = FIRST + SECOND; - """ - - output = """ - TARGET(TEST) { - #if _Py_TAIL_CALL_INTERP - int opcode = TEST; - (void)(opcode); - #endif - frame->instr_ptr = next_instr; - next_instr += 1; - INSTRUCTION_STATS(TEST); - _PyStackRef a; - _PyStackRef b; - // FIRST - { - a = 1; - } - // SECOND - { - b = 1; - if (cond) { - stack_pointer[0] = a; - stack_pointer[1] = b; - stack_pointer += 2; - ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); - JUMP_TO_LABEL(error); - } - } - stack_pointer[0] = a; - stack_pointer[1] = b; - stack_pointer += 2; - ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); - DISPATCH(); - } - """ - self.run_cases_test(input, output) - - def test_error_if_true(self): - - input = """ - inst(OP1, ( --)) { - ERROR_IF(true); - } - inst(OP2, ( --)) { - ERROR_IF(1); - } - """ - output = """ - TARGET(OP1) { - #if _Py_TAIL_CALL_INTERP - int opcode = OP1; - (void)(opcode); - #endif - frame->instr_ptr = next_instr; - next_instr += 1; - INSTRUCTION_STATS(OP1); - JUMP_TO_LABEL(error); - } - - TARGET(OP2) { - #if _Py_TAIL_CALL_INTERP - int opcode = OP2; - (void)(opcode); - #endif - frame->instr_ptr = next_instr; - next_instr += 1; - INSTRUCTION_STATS(OP2); - JUMP_TO_LABEL(error); - } - """ - self.run_cases_test(input, output) - - def test_scalar_array_inconsistency(self): - - input = """ - op(FIRST, ( -- a)) { - a = 1; - } - - op(SECOND, (a[1] -- b)) { - b = 1; - } - - macro(TEST) = FIRST + SECOND; - """ - - output = """ - """ - with self.assertRaises(SyntaxError): - self.run_cases_test(input, output) - - def test_array_size_inconsistency(self): - - input = """ - op(FIRST, ( -- a[2])) { - a[0] = 1; - } - - op(SECOND, (a[1] -- b)) { - b = 1; - } - - macro(TEST) = FIRST + SECOND; - """ - - output = """ - """ - with self.assertRaises(SyntaxError): - self.run_cases_test(input, output) - - def test_stack_save_reload(self): - - input = """ - inst(BALANCED, ( -- )) { - SAVE_STACK(); - code(); - RELOAD_STACK(); - } - """ - - output = """ - TARGET(BALANCED) { - #if _Py_TAIL_CALL_INTERP - int opcode = BALANCED; - (void)(opcode); - #endif - frame->instr_ptr = next_instr; - next_instr += 1; - INSTRUCTION_STATS(BALANCED); - _PyFrame_SetStackPointer(frame, stack_pointer); - code(); - stack_pointer = _PyFrame_GetStackPointer(frame); - DISPATCH(); - } - """ - self.run_cases_test(input, output) - - def test_stack_save_reload_paired(self): - - input = """ - inst(BALANCED, ( -- )) { - SAVE_STACK(); - RELOAD_STACK(); - } - """ - - output = """ - TARGET(BALANCED) { - #if _Py_TAIL_CALL_INTERP - int opcode = BALANCED; - (void)(opcode); - #endif - frame->instr_ptr = next_instr; - next_instr += 1; - INSTRUCTION_STATS(BALANCED); - DISPATCH(); - } - """ - self.run_cases_test(input, output) - - def test_stack_reload_only(self): - - input = """ - inst(BALANCED, ( -- )) { - RELOAD_STACK(); - } - """ - - output = """ - TARGET(BALANCED) { - #if _Py_TAIL_CALL_INTERP - int opcode = BALANCED; - (void)(opcode); - #endif - frame->instr_ptr = next_instr; - next_instr += 1; - INSTRUCTION_STATS(BALANCED); - _PyFrame_SetStackPointer(frame, stack_pointer); - stack_pointer = _PyFrame_GetStackPointer(frame); - DISPATCH(); - } - """ - with self.assertRaises(SyntaxError): - self.run_cases_test(input, output) - - def test_stack_save_only(self): - - input = """ - inst(BALANCED, ( -- )) { - SAVE_STACK(); - } - """ - - output = """ - TARGET(BALANCED) { - #if _Py_TAIL_CALL_INTERP - int opcode = BALANCED; - (void)(opcode); - #endif - _Py_CODEUNIT* const this_instr = next_instr; - (void)this_instr; - frame->instr_ptr = next_instr; - next_instr += 1; - INSTRUCTION_STATS(BALANCED); - _PyFrame_SetStackPointer(frame, stack_pointer); - stack_pointer = _PyFrame_GetStackPointer(frame); - DISPATCH(); - } - """ - with self.assertRaises(SyntaxError): - self.run_cases_test(input, output) - - def test_instruction_size_macro(self): - input = """ - inst(OP, (--)) { - frame->return_offset = INSTRUCTION_SIZE; - } - """ - - output = """ - TARGET(OP) { - #if _Py_TAIL_CALL_INTERP - int opcode = OP; - (void)(opcode); - #endif - frame->instr_ptr = next_instr; - next_instr += 1; - INSTRUCTION_STATS(OP); - frame->return_offset = 1u ; - DISPATCH(); - } - """ - self.run_cases_test(input, output) - - # Two instructions of different sizes referencing the same - # uop containing the `INSTRUCTION_SIZE` macro is not allowed. - input = """ - inst(OP, (--)) { - frame->return_offset = INSTRUCTION_SIZE; - } - macro(OP2) = unused/1 + OP; - """ - - output = "" # No output needed as this should raise an error. - with self.assertRaisesRegex(SyntaxError, "All instructions containing a uop"): - self.run_cases_test(input, output) - - def test_escaping_call_next_to_cmacro(self): - input = """ - inst(OP, (--)) { - #ifdef Py_GIL_DISABLED - escaping_call(); - #else - another_escaping_call(); - #endif - yet_another_escaping_call(); - } - """ - output = """ - TARGET(OP) { - #if _Py_TAIL_CALL_INTERP - int opcode = OP; - (void)(opcode); - #endif - frame->instr_ptr = next_instr; - next_instr += 1; - INSTRUCTION_STATS(OP); - #ifdef Py_GIL_DISABLED - _PyFrame_SetStackPointer(frame, stack_pointer); - escaping_call(); - stack_pointer = _PyFrame_GetStackPointer(frame); - #else - _PyFrame_SetStackPointer(frame, stack_pointer); - another_escaping_call(); - stack_pointer = _PyFrame_GetStackPointer(frame); - #endif - _PyFrame_SetStackPointer(frame, stack_pointer); - yet_another_escaping_call(); - stack_pointer = _PyFrame_GetStackPointer(frame); - DISPATCH(); - } - """ - self.run_cases_test(input, output) - - def test_pystackref_frompyobject_new_next_to_cmacro(self): - input = """ - inst(OP, (-- out1, out2)) { - PyObject *obj = SPAM(); - #ifdef Py_GIL_DISABLED - out1 = PyStackRef_FromPyObjectNew(obj); - #else - out1 = PyStackRef_FromPyObjectNew(obj); - #endif - out2 = PyStackRef_FromPyObjectNew(obj); - } - """ - output = """ - TARGET(OP) { - #if _Py_TAIL_CALL_INTERP - int opcode = OP; - (void)(opcode); - #endif - frame->instr_ptr = next_instr; - next_instr += 1; - INSTRUCTION_STATS(OP); - _PyStackRef out1; - _PyStackRef out2; - PyObject *obj = SPAM(); - #ifdef Py_GIL_DISABLED - out1 = PyStackRef_FromPyObjectNew(obj); - #else - out1 = PyStackRef_FromPyObjectNew(obj); - #endif - out2 = PyStackRef_FromPyObjectNew(obj); - stack_pointer[0] = out1; - stack_pointer[1] = out2; - stack_pointer += 2; - ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); - DISPATCH(); - } - """ - self.run_cases_test(input, output) - - def test_no_escaping_calls_in_branching_macros(self): - - input = """ - inst(OP, ( -- )) { - DEOPT_IF(escaping_call()); - } - """ - with self.assertRaises(SyntaxError): - self.run_cases_test(input, "") - - input = """ - inst(OP, ( -- )) { - EXIT_IF(escaping_call()); - } - """ - with self.assertRaises(SyntaxError): - self.run_cases_test(input, "") - - input = """ - inst(OP, ( -- )) { - ERROR_IF(escaping_call()); - } - """ - with self.assertRaises(SyntaxError): - self.run_cases_test(input, "") - - def test_kill_in_wrong_order(self): - input = """ - inst(OP, (a, b -- c)) { - c = b; - PyStackRef_CLOSE(a); - PyStackRef_CLOSE(b); - } - """ - with self.assertRaises(SyntaxError): - self.run_cases_test(input, "") - - def test_complex_label(self): - input = """ - label(other_label) { - } - - label(other_label2) { - } - - label(my_label) { - // Comment - do_thing(); - if (complex) { - goto other_label; - } - goto other_label2; - } - """ - - output = """ - LABEL(other_label) - { - } - - LABEL(other_label2) - { - } - - LABEL(my_label) - { - _PyFrame_SetStackPointer(frame, stack_pointer); - do_thing(); - stack_pointer = _PyFrame_GetStackPointer(frame); - if (complex) { - JUMP_TO_LABEL(other_label); - } - JUMP_TO_LABEL(other_label2); - } - """ - self.run_cases_test(input, output) - - def test_spilled_label(self): - input = """ - spilled label(one) { - RELOAD_STACK(); - goto two; - } - - label(two) { - SAVE_STACK(); - goto one; - } - """ - - output = """ - LABEL(one) - { - stack_pointer = _PyFrame_GetStackPointer(frame); - JUMP_TO_LABEL(two); - } - - LABEL(two) - { - _PyFrame_SetStackPointer(frame, stack_pointer); - JUMP_TO_LABEL(one); - } - """ - self.run_cases_test(input, output) - - - def test_incorrect_spills(self): - input1 = """ - spilled label(one) { - goto two; - } - - label(two) { - } - """ - - input2 = """ - spilled label(one) { - } - - label(two) { - goto one; - } - """ - with self.assertRaisesRegex(SyntaxError, ".*reload.*"): - self.run_cases_test(input1, "") - with self.assertRaisesRegex(SyntaxError, ".*spill.*"): - self.run_cases_test(input2, "") - - - def test_multiple_labels(self): - input = """ - label(my_label_1) { - // Comment - do_thing1(); - goto my_label_2; - } - - label(my_label_2) { - // Comment - do_thing2(); - goto my_label_1; - } - """ - - output = """ - LABEL(my_label_1) - { - _PyFrame_SetStackPointer(frame, stack_pointer); - do_thing1(); - stack_pointer = _PyFrame_GetStackPointer(frame); - JUMP_TO_LABEL(my_label_2); - } - - LABEL(my_label_2) - { - _PyFrame_SetStackPointer(frame, stack_pointer); - do_thing2(); - stack_pointer = _PyFrame_GetStackPointer(frame); - JUMP_TO_LABEL(my_label_1); - } - """ - self.run_cases_test(input, output) - - def test_reassigning_live_inputs(self): - input = """ - inst(OP, (in -- in)) { - in = 0; - } - """ - - output = """ - TARGET(OP) { - #if _Py_TAIL_CALL_INTERP - int opcode = OP; - (void)(opcode); - #endif - frame->instr_ptr = next_instr; - next_instr += 1; - INSTRUCTION_STATS(OP); - _PyStackRef in; - in = stack_pointer[-1]; - in = 0; - stack_pointer[-1] = in; - DISPATCH(); - } - """ - self.run_cases_test(input, output) - - def test_reassigning_dead_inputs(self): - input = """ - inst(OP, (in -- )) { - temp = use(in); - DEAD(in); - in = temp; - PyStackRef_CLOSE(in); - } - """ - output = """ - TARGET(OP) { - #if _Py_TAIL_CALL_INTERP - int opcode = OP; - (void)(opcode); - #endif - frame->instr_ptr = next_instr; - next_instr += 1; - INSTRUCTION_STATS(OP); - _PyStackRef in; - in = stack_pointer[-1]; - _PyFrame_SetStackPointer(frame, stack_pointer); - temp = use(in); - stack_pointer = _PyFrame_GetStackPointer(frame); - in = temp; - stack_pointer += -1; - ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); - _PyFrame_SetStackPointer(frame, stack_pointer); - PyStackRef_CLOSE(in); - stack_pointer = _PyFrame_GetStackPointer(frame); - DISPATCH(); - } - """ - self.run_cases_test(input, output) - - -class TestGeneratedAbstractCases(unittest.TestCase): - def setUp(self) -> None: - super().setUp() - self.maxDiff = None - - self.temp_dir = tempfile.gettempdir() - self.temp_input_filename = os.path.join(self.temp_dir, "input.txt") - self.temp_input2_filename = os.path.join(self.temp_dir, "input2.txt") - self.temp_output_filename = os.path.join(self.temp_dir, "output.txt") - - def tearDown(self) -> None: - for filename in [ - self.temp_input_filename, - self.temp_input2_filename, - self.temp_output_filename, - ]: - try: - os.remove(filename) - except: - pass - super().tearDown() - - def run_cases_test(self, input: str, input2: str, expected: str): - with open(self.temp_input_filename, "w+") as temp_input: - temp_input.write(parser.BEGIN_MARKER) - temp_input.write(input) - temp_input.write(parser.END_MARKER) - temp_input.flush() - - with open(self.temp_input2_filename, "w+") as temp_input: - temp_input.write(parser.BEGIN_MARKER) - temp_input.write(input2) - temp_input.write(parser.END_MARKER) - temp_input.flush() - - with handle_stderr(): - optimizer_generator.generate_tier2_abstract_from_files( - [self.temp_input_filename, self.temp_input2_filename], - self.temp_output_filename - ) - - with open(self.temp_output_filename) as temp_output: - lines = temp_output.readlines() - while lines and lines[0].startswith(("// ", "#", " #", "\n")): - lines.pop(0) - while lines and lines[-1].startswith(("#", "\n")): - lines.pop(-1) - actual = "".join(lines) - self.assertEqual(actual.strip(), expected.strip()) - - def test_overridden_abstract(self): - input = """ - pure op(OP, (--)) { - SPAM(); - } - """ - input2 = """ - pure op(OP, (--)) { - eggs(); - } - """ - output = """ - case OP: { - eggs(); - break; - } - """ - self.run_cases_test(input, input2, output) - - def test_overridden_abstract_args(self): - input = """ - pure op(OP, (arg1 -- out)) { - out = SPAM(arg1); - } - op(OP2, (arg1 -- out)) { - out = EGGS(arg1); - } - """ - input2 = """ - op(OP, (arg1 -- out)) { - out = EGGS(arg1); - } - """ - output = """ - case OP: { - JitOptRef arg1; - JitOptRef out; - arg1 = stack_pointer[-1]; - out = EGGS(arg1); - stack_pointer[-1] = out; - break; - } - - case OP2: { - JitOptRef out; - out = sym_new_not_null(ctx); - stack_pointer[-1] = out; - break; - } - """ - self.run_cases_test(input, input2, output) - - def test_no_overridden_case(self): - input = """ - pure op(OP, (arg1 -- out)) { - out = SPAM(arg1); - } - - pure op(OP2, (arg1 -- out)) { - } - - """ - input2 = """ - pure op(OP2, (arg1 -- out)) { - out = NULL; - } - """ - output = """ - case OP: { - JitOptRef out; - out = sym_new_not_null(ctx); - stack_pointer[-1] = out; - break; - } - - case OP2: { - JitOptRef out; - out = NULL; - stack_pointer[-1] = out; - break; - } - """ - self.run_cases_test(input, input2, output) - - def test_missing_override_failure(self): - input = """ - pure op(OP, (arg1 -- out)) { - SPAM(); - } - """ - input2 = """ - pure op(OTHER, (arg1 -- out)) { - } - """ - output = """ - """ - with self.assertRaisesRegex(ValueError, "All abstract uops"): - self.run_cases_test(input, input2, output) - - def test_validate_uop_input_length_mismatch(self): - input = """ - op(OP, (arg1 -- out)) { - SPAM(); - } - """ - input2 = """ - op(OP, (arg1, arg2 -- out)) { - } - """ - output = """ - """ - with self.assertRaisesRegex(SyntaxError, - "Must have the same number of inputs"): - self.run_cases_test(input, input2, output) - - def test_validate_uop_output_length_mismatch(self): - input = """ - op(OP, (arg1 -- out)) { - SPAM(); - } - """ - input2 = """ - op(OP, (arg1 -- out1, out2)) { - } - """ - output = """ - """ - with self.assertRaisesRegex(SyntaxError, - "Must have the same number of outputs"): - self.run_cases_test(input, input2, output) - - def test_validate_uop_input_name_mismatch(self): - input = """ - op(OP, (foo -- out)) { - SPAM(); - } - """ - input2 = """ - op(OP, (bar -- out)) { - } - """ - output = """ - """ - with self.assertRaisesRegex(SyntaxError, - "Inputs must have equal names"): - self.run_cases_test(input, input2, output) - - def test_validate_uop_output_name_mismatch(self): - input = """ - op(OP, (arg1 -- foo)) { - SPAM(); - } - """ - input2 = """ - op(OP, (arg1 -- bar)) { - } - """ - output = """ - """ - with self.assertRaisesRegex(SyntaxError, - "Outputs must have equal names"): - self.run_cases_test(input, input2, output) - - def test_validate_uop_unused_input(self): - input = """ - op(OP, (unused -- )) { - } - """ - input2 = """ - op(OP, (foo -- )) { - } - """ - output = """ - case OP: { - CHECK_STACK_BOUNDS(-1); - stack_pointer += -1; - ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); - break; - } - """ - self.run_cases_test(input, input2, output) - - input = """ - op(OP, (foo -- )) { - } - """ - input2 = """ - op(OP, (unused -- )) { - } - """ - output = """ - case OP: { - CHECK_STACK_BOUNDS(-1); - stack_pointer += -1; - ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); - break; - } - """ - self.run_cases_test(input, input2, output) - - def test_validate_uop_unused_output(self): - input = """ - op(OP, ( -- unused)) { - } - """ - input2 = """ - op(OP, ( -- foo)) { - foo = NULL; - } - """ - output = """ - case OP: { - JitOptRef foo; - foo = NULL; - CHECK_STACK_BOUNDS(1); - stack_pointer[0] = foo; - stack_pointer += 1; - ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); - break; - } - """ - self.run_cases_test(input, input2, output) - - input = """ - op(OP, ( -- foo)) { - foo = NULL; - } - """ - input2 = """ - op(OP, ( -- unused)) { - } - """ - output = """ - case OP: { - CHECK_STACK_BOUNDS(1); - stack_pointer += 1; - ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); - break; - } - """ - self.run_cases_test(input, input2, output) - - def test_validate_uop_input_size_mismatch(self): - input = """ - op(OP, (arg1[2] -- )) { - } - """ - input2 = """ - op(OP, (arg1[4] -- )) { - } - """ - output = """ - """ - with self.assertRaisesRegex(SyntaxError, - "Inputs must have equal sizes"): - self.run_cases_test(input, input2, output) - - def test_validate_uop_output_size_mismatch(self): - input = """ - op(OP, ( -- out[2])) { - } - """ - input2 = """ - op(OP, ( -- out[4])) { - } - """ - output = """ - """ - with self.assertRaisesRegex(SyntaxError, - "Outputs must have equal sizes"): - self.run_cases_test(input, input2, output) - - def test_validate_uop_unused_size_mismatch(self): - input = """ - op(OP, (foo[2] -- )) { - } - """ - input2 = """ - op(OP, (unused[4] -- )) { - } - """ - output = """ - """ - with self.assertRaisesRegex(SyntaxError, - "Inputs must have equal sizes"): - self.run_cases_test(input, input2, output) - - def test_pure_uop_body_copied_in(self): - # Note: any non-escaping call works. - # In this case, we use PyStackRef_IsNone. - input = """ - pure op(OP, (foo -- res)) { - res = PyStackRef_IsNone(foo); - } - """ - input2 = """ - op(OP, (foo -- res)) { - REPLACE_OPCODE_IF_EVALUATES_PURE(foo, res); - res = sym_new_known(ctx, foo); - } - """ - output = """ - case OP: { - JitOptRef foo; - JitOptRef res; - foo = stack_pointer[-1]; - if ( - sym_is_safe_const(ctx, foo) - ) { - JitOptRef foo_sym = foo; - _PyStackRef foo = sym_get_const_as_stackref(ctx, foo_sym); - _PyStackRef res_stackref; - /* Start of uop copied from bytecodes for constant evaluation */ - res_stackref = PyStackRef_IsNone(foo); - /* End of uop copied from bytecodes for constant evaluation */ - res = sym_new_const_steal(ctx, PyStackRef_AsPyObjectSteal(res_stackref)); - stack_pointer[-1] = res; - break; - } - res = sym_new_known(ctx, foo); - stack_pointer[-1] = res; - break; - } - """ - self.run_cases_test(input, input2, output) - - def test_pure_uop_body_copied_in_deopt(self): - # Note: any non-escaping call works. - # In this case, we use PyStackRef_IsNone. - input = """ - pure op(OP, (foo -- res)) { - DEOPT_IF(PyStackRef_IsNull(foo)); - res = foo; - } - """ - input2 = """ - op(OP, (foo -- res)) { - REPLACE_OPCODE_IF_EVALUATES_PURE(foo, res); - res = foo; - } - """ - output = """ - case OP: { - JitOptRef foo; - JitOptRef res; - foo = stack_pointer[-1]; - if ( - sym_is_safe_const(ctx, foo) - ) { - JitOptRef foo_sym = foo; - _PyStackRef foo = sym_get_const_as_stackref(ctx, foo_sym); - _PyStackRef res_stackref; - /* Start of uop copied from bytecodes for constant evaluation */ - if (PyStackRef_IsNull(foo)) { - ctx->done = true; - break; - } - res_stackref = foo; - /* End of uop copied from bytecodes for constant evaluation */ - res = sym_new_const_steal(ctx, PyStackRef_AsPyObjectSteal(res_stackref)); - stack_pointer[-1] = res; - break; - } - res = foo; - stack_pointer[-1] = res; - break; - } - """ - self.run_cases_test(input, input2, output) - - def test_pure_uop_body_copied_in_error_if(self): - # Note: any non-escaping call works. - # In this case, we use PyStackRef_IsNone. - input = """ - pure op(OP, (foo -- res)) { - ERROR_IF(PyStackRef_IsNull(foo)); - res = foo; - } - """ - input2 = """ - op(OP, (foo -- res)) { - REPLACE_OPCODE_IF_EVALUATES_PURE(foo, res); - res = foo; - } - """ - output = """ - case OP: { - JitOptRef foo; - JitOptRef res; - foo = stack_pointer[-1]; - if ( - sym_is_safe_const(ctx, foo) - ) { - JitOptRef foo_sym = foo; - _PyStackRef foo = sym_get_const_as_stackref(ctx, foo_sym); - _PyStackRef res_stackref; - /* Start of uop copied from bytecodes for constant evaluation */ - if (PyStackRef_IsNull(foo)) { - goto error; - } - res_stackref = foo; - /* End of uop copied from bytecodes for constant evaluation */ - res = sym_new_const_steal(ctx, PyStackRef_AsPyObjectSteal(res_stackref)); - stack_pointer[-1] = res; - break; - } - res = foo; - stack_pointer[-1] = res; - break; - } - """ - self.run_cases_test(input, input2, output) - - - def test_replace_opcode_uop_body_copied_in_complex(self): - input = """ - pure op(OP, (foo -- res)) { - if (foo) { - res = PyStackRef_IsNone(foo); - } - else { - res = 1; - } - } - """ - input2 = """ - op(OP, (foo -- res)) { - REPLACE_OPCODE_IF_EVALUATES_PURE(foo, res); - res = sym_new_known(ctx, foo); - } - """ - output = """ - case OP: { - JitOptRef foo; - JitOptRef res; - foo = stack_pointer[-1]; - if ( - sym_is_safe_const(ctx, foo) - ) { - JitOptRef foo_sym = foo; - _PyStackRef foo = sym_get_const_as_stackref(ctx, foo_sym); - _PyStackRef res_stackref; - /* Start of uop copied from bytecodes for constant evaluation */ - if (foo) { - res_stackref = PyStackRef_IsNone(foo); - } - else { - res_stackref = 1; - } - /* End of uop copied from bytecodes for constant evaluation */ - res = sym_new_const_steal(ctx, PyStackRef_AsPyObjectSteal(res_stackref)); - stack_pointer[-1] = res; - break; - } - res = sym_new_known(ctx, foo); - stack_pointer[-1] = res; - break; - } - """ - self.run_cases_test(input, input2, output) - - def test_replace_opcode_escaping_uop_body_copied_in_complex(self): - input = """ - pure op(OP, (foo -- res)) { - if (foo) { - res = ESCAPING_CODE(foo); - } - else { - res = 1; - } - } - """ - input2 = """ - op(OP, (foo -- res)) { - REPLACE_OPCODE_IF_EVALUATES_PURE(foo, res); - res = sym_new_known(ctx, foo); - } - """ - output = """ - case OP: { - JitOptRef foo; - JitOptRef res; - foo = stack_pointer[-1]; - if ( - sym_is_safe_const(ctx, foo) - ) { - JitOptRef foo_sym = foo; - _PyStackRef foo = sym_get_const_as_stackref(ctx, foo_sym); - _PyStackRef res_stackref; - /* Start of uop copied from bytecodes for constant evaluation */ - if (foo) { - res_stackref = ESCAPING_CODE(foo); - } - else { - res_stackref = 1; - } - /* End of uop copied from bytecodes for constant evaluation */ - res = sym_new_const_steal(ctx, PyStackRef_AsPyObjectSteal(res_stackref)); - stack_pointer[-1] = res; - break; - } - res = sym_new_known(ctx, foo); - stack_pointer[-1] = res; - break; - } - """ - self.run_cases_test(input, input2, output) - - def test_replace_opcode_binop_one_output(self): - input = """ - pure op(OP, (left, right -- res)) { - res = foo(left, right); - } - """ - input2 = """ - op(OP, (left, right -- res)) { - res = sym_new_non_null(ctx, foo); - REPLACE_OPCODE_IF_EVALUATES_PURE(left, right, res); - } - """ - output = """ - case OP: { - JitOptRef right; - JitOptRef left; - JitOptRef res; - right = stack_pointer[-1]; - left = stack_pointer[-2]; - res = sym_new_non_null(ctx, foo); - if ( - sym_is_safe_const(ctx, left) && - sym_is_safe_const(ctx, right) - ) { - JitOptRef left_sym = left; - JitOptRef right_sym = right; - _PyStackRef left = sym_get_const_as_stackref(ctx, left_sym); - _PyStackRef right = sym_get_const_as_stackref(ctx, right_sym); - _PyStackRef res_stackref; - /* Start of uop copied from bytecodes for constant evaluation */ - res_stackref = foo(left, right); - /* End of uop copied from bytecodes for constant evaluation */ - res = sym_new_const_steal(ctx, PyStackRef_AsPyObjectSteal(res_stackref)); - CHECK_STACK_BOUNDS(-1); - stack_pointer[-2] = res; - stack_pointer += -1; - ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); - break; - } - CHECK_STACK_BOUNDS(-1); - stack_pointer[-2] = res; - stack_pointer += -1; - ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); - break; - } - """ - self.run_cases_test(input, input2, output) - - def test_replace_opcode_binop_one_output_insert(self): - input = """ - pure op(OP, (left, right -- res, l, r)) { - res = foo(left, right); - l = left; - r = right; - } - """ - input2 = """ - op(OP, (left, right -- res, l, r)) { - res = sym_new_non_null(ctx, foo); - l = left; - r = right; - REPLACE_OPCODE_IF_EVALUATES_PURE(left, right, res); - } - """ - output = """ - case OP: { - JitOptRef right; - JitOptRef left; - JitOptRef res; - JitOptRef l; - JitOptRef r; - right = stack_pointer[-1]; - left = stack_pointer[-2]; - res = sym_new_non_null(ctx, foo); - l = left; - r = right; - if ( - sym_is_safe_const(ctx, left) && - sym_is_safe_const(ctx, right) - ) { - JitOptRef left_sym = left; - JitOptRef right_sym = right; - _PyStackRef left = sym_get_const_as_stackref(ctx, left_sym); - _PyStackRef right = sym_get_const_as_stackref(ctx, right_sym); - _PyStackRef res_stackref; - _PyStackRef l_stackref; - _PyStackRef r_stackref; - /* Start of uop copied from bytecodes for constant evaluation */ - res_stackref = foo(left, right); - l_stackref = left; - r_stackref = right; - /* End of uop copied from bytecodes for constant evaluation */ - (void)l_stackref; - (void)r_stackref; - res = sym_new_const_steal(ctx, PyStackRef_AsPyObjectSteal(res_stackref)); - CHECK_STACK_BOUNDS(1); - stack_pointer[-2] = res; - stack_pointer[-1] = l; - stack_pointer[0] = r; - stack_pointer += 1; - ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); - break; - } - CHECK_STACK_BOUNDS(1); - stack_pointer[-2] = res; - stack_pointer[-1] = l; - stack_pointer[0] = r; - stack_pointer += 1; - ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); - break; - } - """ - self.run_cases_test(input, input2, output) - - def test_replace_opcode_unaryop_one_output_insert(self): - input = """ - pure op(OP, (left -- res, l)) { - res = foo(left); - l = left; - } - """ - input2 = """ - op(OP, (left -- res, l)) { - res = sym_new_non_null(ctx, foo); - l = left; - REPLACE_OPCODE_IF_EVALUATES_PURE(left, res); - } - """ - output = """ - case OP: { - JitOptRef left; - JitOptRef res; - JitOptRef l; - left = stack_pointer[-1]; - res = sym_new_non_null(ctx, foo); - l = left; - if ( - sym_is_safe_const(ctx, left) - ) { - JitOptRef left_sym = left; - _PyStackRef left = sym_get_const_as_stackref(ctx, left_sym); - _PyStackRef res_stackref; - _PyStackRef l_stackref; - /* Start of uop copied from bytecodes for constant evaluation */ - res_stackref = foo(left); - l_stackref = left; - /* End of uop copied from bytecodes for constant evaluation */ - (void)l_stackref; - res = sym_new_const_steal(ctx, PyStackRef_AsPyObjectSteal(res_stackref)); - CHECK_STACK_BOUNDS(1); - stack_pointer[-1] = res; - stack_pointer[0] = l; - stack_pointer += 1; - ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); - break; - } - CHECK_STACK_BOUNDS(1); - stack_pointer[-1] = res; - stack_pointer[0] = l; - stack_pointer += 1; - ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); - break; - } - """ - self.run_cases_test(input, input2, output) - - def test_replace_opocode_uop_reject_array_effects(self): - input = """ - pure op(OP, (foo[2] -- res)) { - if (foo) { - res = PyStackRef_IsNone(foo); - } - else { - res = 1; - } - } - """ - input2 = """ - op(OP, (foo[2] -- res)) { - REPLACE_OPCODE_IF_EVALUATES_PURE(foo, res); - res = sym_new_unknown(ctx); - } - """ - output = """ - """ - with self.assertRaisesRegex(SyntaxError, - "Pure evaluation cannot take array-like inputs"): - self.run_cases_test(input, input2, output) - -if __name__ == "__main__": - unittest.main() +import contextlib +import os +import sys +import tempfile +import unittest + +from test import support +from test import test_tools + + +def skip_if_different_mount_drives(): + if sys.platform != "win32": + return + ROOT = os.path.dirname(os.path.dirname(__file__)) + root_drive = os.path.splitroot(ROOT)[0] + cwd_drive = os.path.splitroot(os.getcwd())[0] + if root_drive != cwd_drive: + # May raise ValueError if ROOT and the current working + # different have different mount drives (on Windows). + raise unittest.SkipTest( + f"the current working directory and the Python source code " + f"directory have different mount drives " + f"({cwd_drive} and {root_drive})" + ) + + +skip_if_different_mount_drives() + + +test_tools.skip_if_missing("cases_generator") +with test_tools.imports_under_tool("cases_generator"): + from analyzer import StackItem, analyze_forest + from cwriter import CWriter + import parser + from stack import Local, Stack + import tier1_generator + import optimizer_generator + + +def handle_stderr(): + if support.verbose > 1: + return contextlib.nullcontext() + else: + return support.captured_stderr() + + +def parse_src(src): + p = parser.Parser(src, "test.c") + nodes = [] + while node := p.definition(): + nodes.append(node) + return nodes + + +class TestEffects(unittest.TestCase): + def test_effect_sizes(self): + stack = Stack() + inputs = [ + x := StackItem("x", "1"), + y := StackItem("y", "oparg"), + z := StackItem("z", "oparg*2"), + ] + outputs = [ + StackItem("x", "1"), + StackItem("b", "oparg*4"), + StackItem("c", "1"), + ] + null = CWriter.null() + stack.pop(z, null) + stack.pop(y, null) + stack.pop(x, null) + for out in outputs: + stack.push(Local.undefined(out)) + self.assertEqual(stack.base_offset.to_c(), "-1 - oparg - oparg*2") + self.assertEqual(stack.physical_sp.to_c(), "0") + self.assertEqual(stack.logical_sp.to_c(), "1 - oparg - oparg*2 + oparg*4") + + +class TestGeneratedCases(unittest.TestCase): + def setUp(self) -> None: + super().setUp() + self.maxDiff = None + + self.temp_dir = tempfile.gettempdir() + self.temp_input_filename = os.path.join(self.temp_dir, "input.txt") + self.temp_output_filename = os.path.join(self.temp_dir, "output.txt") + self.temp_metadata_filename = os.path.join(self.temp_dir, "metadata.txt") + self.temp_pymetadata_filename = os.path.join(self.temp_dir, "pymetadata.txt") + self.temp_executor_filename = os.path.join(self.temp_dir, "executor.txt") + + def tearDown(self) -> None: + for filename in [ + self.temp_input_filename, + self.temp_output_filename, + self.temp_metadata_filename, + self.temp_pymetadata_filename, + self.temp_executor_filename, + ]: + try: + os.remove(filename) + except: + pass + super().tearDown() + + def run_cases_test(self, input: str, expected: str): + with open(self.temp_input_filename, "w+") as temp_input: + temp_input.write(parser.BEGIN_MARKER) + temp_input.write(input) + temp_input.write(parser.END_MARKER) + temp_input.flush() + + with handle_stderr(): + tier1_generator.generate_tier1_from_files( + [self.temp_input_filename], self.temp_output_filename, False + ) + + with open(self.temp_output_filename) as temp_output: + lines = temp_output.read() + _, rest = lines.split(tier1_generator.INSTRUCTION_START_MARKER) + instructions, labels_with_prelude_and_postlude = rest.split(tier1_generator.INSTRUCTION_END_MARKER) + _, labels_with_postlude = labels_with_prelude_and_postlude.split(tier1_generator.LABEL_START_MARKER) + labels, _ = labels_with_postlude.split(tier1_generator.LABEL_END_MARKER) + actual = instructions.strip() + "\n\n " + labels.strip() + + self.assertEqual(actual.strip(), expected.strip()) + + def test_inst_no_args(self): + input = """ + inst(OP, (--)) { + SPAM(); + } + """ + output = """ + TARGET(OP) { + #if _Py_TAIL_CALL_INTERP + int opcode = OP; + (void)(opcode); + #endif + frame->instr_ptr = next_instr; + next_instr += 1; + INSTRUCTION_STATS(OP); + SPAM(); + DISPATCH(); + } + """ + self.run_cases_test(input, output) + + def test_inst_one_pop(self): + input = """ + inst(OP, (value --)) { + SPAM(value); + DEAD(value); + } + """ + output = """ + TARGET(OP) { + #if _Py_TAIL_CALL_INTERP + int opcode = OP; + (void)(opcode); + #endif + frame->instr_ptr = next_instr; + next_instr += 1; + INSTRUCTION_STATS(OP); + _PyStackRef value; + value = stack_pointer[-1]; + SPAM(value); + stack_pointer += -1; + ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); + DISPATCH(); + } + """ + self.run_cases_test(input, output) + + def test_inst_one_push(self): + input = """ + inst(OP, (-- res)) { + res = SPAM(); + } + """ + output = """ + TARGET(OP) { + #if _Py_TAIL_CALL_INTERP + int opcode = OP; + (void)(opcode); + #endif + frame->instr_ptr = next_instr; + next_instr += 1; + INSTRUCTION_STATS(OP); + _PyStackRef res; + res = SPAM(); + stack_pointer[0] = res; + stack_pointer += 1; + ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); + DISPATCH(); + } + """ + self.run_cases_test(input, output) + + def test_inst_one_push_one_pop(self): + input = """ + inst(OP, (value -- res)) { + res = SPAM(value); + DEAD(value); + } + """ + output = """ + TARGET(OP) { + #if _Py_TAIL_CALL_INTERP + int opcode = OP; + (void)(opcode); + #endif + frame->instr_ptr = next_instr; + next_instr += 1; + INSTRUCTION_STATS(OP); + _PyStackRef value; + _PyStackRef res; + value = stack_pointer[-1]; + res = SPAM(value); + stack_pointer[-1] = res; + DISPATCH(); + } + """ + self.run_cases_test(input, output) + + def test_binary_op(self): + input = """ + inst(OP, (left, right -- res)) { + res = SPAM(left, right); + INPUTS_DEAD(); + + } + """ + output = """ + TARGET(OP) { + #if _Py_TAIL_CALL_INTERP + int opcode = OP; + (void)(opcode); + #endif + frame->instr_ptr = next_instr; + next_instr += 1; + INSTRUCTION_STATS(OP); + _PyStackRef left; + _PyStackRef right; + _PyStackRef res; + right = stack_pointer[-1]; + left = stack_pointer[-2]; + res = SPAM(left, right); + stack_pointer[-2] = res; + stack_pointer += -1; + ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); + DISPATCH(); + } + """ + self.run_cases_test(input, output) + + def test_overlap(self): + input = """ + inst(OP, (left, right -- left, result)) { + result = SPAM(left, right); + INPUTS_DEAD(); + } + """ + output = """ + TARGET(OP) { + #if _Py_TAIL_CALL_INTERP + int opcode = OP; + (void)(opcode); + #endif + frame->instr_ptr = next_instr; + next_instr += 1; + INSTRUCTION_STATS(OP); + _PyStackRef left; + _PyStackRef right; + _PyStackRef result; + right = stack_pointer[-1]; + left = stack_pointer[-2]; + result = SPAM(left, right); + stack_pointer[-1] = result; + DISPATCH(); + } + """ + self.run_cases_test(input, output) + + def test_predictions(self): + input = """ + inst(OP1, (arg -- res)) { + DEAD(arg); + res = Py_None; + } + inst(OP3, (arg -- res)) { + DEAD(arg); + DEOPT_IF(xxx); + res = Py_None; + } + family(OP1, INLINE_CACHE_ENTRIES_OP1) = { OP3 }; + """ + output = """ + TARGET(OP1) { + #if _Py_TAIL_CALL_INTERP + int opcode = OP1; + (void)(opcode); + #endif + frame->instr_ptr = next_instr; + next_instr += 1; + INSTRUCTION_STATS(OP1); + PREDICTED_OP1:; + _PyStackRef arg; + _PyStackRef res; + arg = stack_pointer[-1]; + res = Py_None; + stack_pointer[-1] = res; + DISPATCH(); + } + + TARGET(OP3) { + #if _Py_TAIL_CALL_INTERP + int opcode = OP3; + (void)(opcode); + #endif + _Py_CODEUNIT* const this_instr = next_instr; + (void)this_instr; + frame->instr_ptr = next_instr; + next_instr += 1; + INSTRUCTION_STATS(OP3); + static_assert(INLINE_CACHE_ENTRIES_OP1 == 0, "incorrect cache size"); + _PyStackRef arg; + _PyStackRef res; + arg = stack_pointer[-1]; + if (xxx) { + UPDATE_MISS_STATS(OP1); + assert(_PyOpcode_Deopt[opcode] == (OP1)); + JUMP_TO_PREDICTED(OP1); + } + res = Py_None; + stack_pointer[-1] = res; + DISPATCH(); + } + """ + self.run_cases_test(input, output) + + def test_sync_sp(self): + input = """ + inst(A, (arg -- res)) { + DEAD(arg); + SYNC_SP(); + escaping_call(); + res = Py_None; + } + inst(B, (arg -- res)) { + DEAD(arg); + res = Py_None; + SYNC_SP(); + escaping_call(); + } + """ + output = """ + TARGET(A) { + #if _Py_TAIL_CALL_INTERP + int opcode = A; + (void)(opcode); + #endif + frame->instr_ptr = next_instr; + next_instr += 1; + INSTRUCTION_STATS(A); + _PyStackRef arg; + _PyStackRef res; + arg = stack_pointer[-1]; + stack_pointer += -1; + ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); + _PyFrame_SetStackPointer(frame, stack_pointer); + escaping_call(); + stack_pointer = _PyFrame_GetStackPointer(frame); + res = Py_None; + stack_pointer[0] = res; + stack_pointer += 1; + ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); + DISPATCH(); + } + + TARGET(B) { + #if _Py_TAIL_CALL_INTERP + int opcode = B; + (void)(opcode); + #endif + frame->instr_ptr = next_instr; + next_instr += 1; + INSTRUCTION_STATS(B); + _PyStackRef arg; + _PyStackRef res; + arg = stack_pointer[-1]; + res = Py_None; + stack_pointer[-1] = res; + _PyFrame_SetStackPointer(frame, stack_pointer); + escaping_call(); + stack_pointer = _PyFrame_GetStackPointer(frame); + DISPATCH(); + } + """ + self.run_cases_test(input, output) + + + def test_pep7_condition(self): + input = """ + inst(OP, (arg1 -- out)) { + if (arg1) + out = 0; + else { + out = 1; + } + } + """ + output = "" + with self.assertRaises(SyntaxError): + self.run_cases_test(input, output) + + def test_error_if_plain(self): + input = """ + inst(OP, (--)) { + ERROR_IF(cond); + } + """ + output = """ + TARGET(OP) { + #if _Py_TAIL_CALL_INTERP + int opcode = OP; + (void)(opcode); + #endif + frame->instr_ptr = next_instr; + next_instr += 1; + INSTRUCTION_STATS(OP); + if (cond) { + JUMP_TO_LABEL(error); + } + DISPATCH(); + } + """ + self.run_cases_test(input, output) + + def test_error_if_plain_with_comment(self): + input = """ + inst(OP, (--)) { + ERROR_IF(cond); // Comment is ok + } + """ + output = """ + TARGET(OP) { + #if _Py_TAIL_CALL_INTERP + int opcode = OP; + (void)(opcode); + #endif + frame->instr_ptr = next_instr; + next_instr += 1; + INSTRUCTION_STATS(OP); + if (cond) { + JUMP_TO_LABEL(error); + } + DISPATCH(); + } + """ + self.run_cases_test(input, output) + + def test_error_if_pop(self): + input = """ + inst(OP, (left, right -- res)) { + SPAM(left, right); + INPUTS_DEAD(); + ERROR_IF(cond); + res = 0; + } + """ + output = """ + TARGET(OP) { + #if _Py_TAIL_CALL_INTERP + int opcode = OP; + (void)(opcode); + #endif + frame->instr_ptr = next_instr; + next_instr += 1; + INSTRUCTION_STATS(OP); + _PyStackRef left; + _PyStackRef right; + _PyStackRef res; + right = stack_pointer[-1]; + left = stack_pointer[-2]; + SPAM(left, right); + if (cond) { + JUMP_TO_LABEL(pop_2_error); + } + res = 0; + stack_pointer[-2] = res; + stack_pointer += -1; + ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); + DISPATCH(); + } + """ + self.run_cases_test(input, output) + + def test_error_if_pop_with_result(self): + input = """ + inst(OP, (left, right -- res)) { + res = SPAM(left, right); + INPUTS_DEAD(); + ERROR_IF(cond); + } + """ + output = """ + TARGET(OP) { + #if _Py_TAIL_CALL_INTERP + int opcode = OP; + (void)(opcode); + #endif + frame->instr_ptr = next_instr; + next_instr += 1; + INSTRUCTION_STATS(OP); + _PyStackRef left; + _PyStackRef right; + _PyStackRef res; + right = stack_pointer[-1]; + left = stack_pointer[-2]; + res = SPAM(left, right); + if (cond) { + JUMP_TO_LABEL(pop_2_error); + } + stack_pointer[-2] = res; + stack_pointer += -1; + ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); + DISPATCH(); + } + """ + self.run_cases_test(input, output) + + def test_cache_effect(self): + input = """ + inst(OP, (counter/1, extra/2, value --)) { + DEAD(value); + } + """ + output = """ + TARGET(OP) { + #if _Py_TAIL_CALL_INTERP + int opcode = OP; + (void)(opcode); + #endif + _Py_CODEUNIT* const this_instr = next_instr; + (void)this_instr; + frame->instr_ptr = next_instr; + next_instr += 4; + INSTRUCTION_STATS(OP); + _PyStackRef value; + value = stack_pointer[-1]; + uint16_t counter = read_u16(&this_instr[1].cache); + (void)counter; + uint32_t extra = read_u32(&this_instr[2].cache); + (void)extra; + stack_pointer += -1; + ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); + DISPATCH(); + } + """ + self.run_cases_test(input, output) + + def test_suppress_dispatch(self): + input = """ + label(somewhere) { + } + + inst(OP, (--)) { + goto somewhere; + } + """ + output = """ + TARGET(OP) { + #if _Py_TAIL_CALL_INTERP + int opcode = OP; + (void)(opcode); + #endif + frame->instr_ptr = next_instr; + next_instr += 1; + INSTRUCTION_STATS(OP); + JUMP_TO_LABEL(somewhere); + } + + LABEL(somewhere) + { + } + """ + self.run_cases_test(input, output) + + def test_macro_instruction(self): + input = """ + inst(OP1, (counter/1, left, right -- left, right)) { + op1(left, right); + } + op(OP2, (extra/2, arg2, left, right -- res)) { + res = op2(arg2, left, right); + INPUTS_DEAD(); + } + macro(OP) = OP1 + cache/2 + OP2; + inst(OP3, (unused/5, arg2, left, right -- res)) { + res = op3(arg2, left, right); + INPUTS_DEAD(); + } + family(OP, INLINE_CACHE_ENTRIES_OP) = { OP3 }; + """ + output = """ + TARGET(OP) { + #if _Py_TAIL_CALL_INTERP + int opcode = OP; + (void)(opcode); + #endif + frame->instr_ptr = next_instr; + next_instr += 6; + INSTRUCTION_STATS(OP); + PREDICTED_OP:; + _Py_CODEUNIT* const this_instr = next_instr - 6; + (void)this_instr; + _PyStackRef left; + _PyStackRef right; + _PyStackRef arg2; + _PyStackRef res; + // _OP1 + { + right = stack_pointer[-1]; + left = stack_pointer[-2]; + uint16_t counter = read_u16(&this_instr[1].cache); + (void)counter; + _PyFrame_SetStackPointer(frame, stack_pointer); + op1(left, right); + stack_pointer = _PyFrame_GetStackPointer(frame); + } + /* Skip 2 cache entries */ + // OP2 + { + arg2 = stack_pointer[-3]; + uint32_t extra = read_u32(&this_instr[4].cache); + (void)extra; + _PyFrame_SetStackPointer(frame, stack_pointer); + res = op2(arg2, left, right); + stack_pointer = _PyFrame_GetStackPointer(frame); + } + stack_pointer[-3] = res; + stack_pointer += -2; + ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); + DISPATCH(); + } + + TARGET(OP1) { + #if _Py_TAIL_CALL_INTERP + int opcode = OP1; + (void)(opcode); + #endif + _Py_CODEUNIT* const this_instr = next_instr; + (void)this_instr; + frame->instr_ptr = next_instr; + next_instr += 2; + INSTRUCTION_STATS(OP1); + _PyStackRef left; + _PyStackRef right; + right = stack_pointer[-1]; + left = stack_pointer[-2]; + uint16_t counter = read_u16(&this_instr[1].cache); + (void)counter; + _PyFrame_SetStackPointer(frame, stack_pointer); + op1(left, right); + stack_pointer = _PyFrame_GetStackPointer(frame); + DISPATCH(); + } + + TARGET(OP3) { + #if _Py_TAIL_CALL_INTERP + int opcode = OP3; + (void)(opcode); + #endif + frame->instr_ptr = next_instr; + next_instr += 6; + INSTRUCTION_STATS(OP3); + static_assert(INLINE_CACHE_ENTRIES_OP == 5, "incorrect cache size"); + _PyStackRef arg2; + _PyStackRef left; + _PyStackRef right; + _PyStackRef res; + /* Skip 5 cache entries */ + right = stack_pointer[-1]; + left = stack_pointer[-2]; + arg2 = stack_pointer[-3]; + _PyFrame_SetStackPointer(frame, stack_pointer); + res = op3(arg2, left, right); + stack_pointer = _PyFrame_GetStackPointer(frame); + stack_pointer[-3] = res; + stack_pointer += -2; + ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); + DISPATCH(); + } + """ + self.run_cases_test(input, output) + + def test_unused_caches(self): + input = """ + inst(OP, (unused/1, unused/2 --)) { + body; + } + """ + output = """ + TARGET(OP) { + #if _Py_TAIL_CALL_INTERP + int opcode = OP; + (void)(opcode); + #endif + frame->instr_ptr = next_instr; + next_instr += 4; + INSTRUCTION_STATS(OP); + /* Skip 1 cache entry */ + /* Skip 2 cache entries */ + body; + DISPATCH(); + } + """ + self.run_cases_test(input, output) + + def test_pseudo_instruction_no_flags(self): + input = """ + pseudo(OP, (in -- out1, out2)) = { + OP1, + }; + + inst(OP1, (--)) { + } + """ + output = """ + TARGET(OP1) { + #if _Py_TAIL_CALL_INTERP + int opcode = OP1; + (void)(opcode); + #endif + frame->instr_ptr = next_instr; + next_instr += 1; + INSTRUCTION_STATS(OP1); + DISPATCH(); + } + """ + self.run_cases_test(input, output) + + def test_pseudo_instruction_with_flags(self): + input = """ + pseudo(OP, (in1, in2 --), (HAS_ARG, HAS_JUMP)) = { + OP1, + }; + + inst(OP1, (--)) { + } + """ + output = """ + TARGET(OP1) { + #if _Py_TAIL_CALL_INTERP + int opcode = OP1; + (void)(opcode); + #endif + frame->instr_ptr = next_instr; + next_instr += 1; + INSTRUCTION_STATS(OP1); + DISPATCH(); + } + """ + self.run_cases_test(input, output) + + def test_pseudo_instruction_as_sequence(self): + input = """ + pseudo(OP, (in -- out1, out2)) = [ + OP1, OP2 + ]; + + inst(OP1, (--)) { + } + + inst(OP2, (--)) { + } + """ + output = """ + TARGET(OP1) { + #if _Py_TAIL_CALL_INTERP + int opcode = OP1; + (void)(opcode); + #endif + frame->instr_ptr = next_instr; + next_instr += 1; + INSTRUCTION_STATS(OP1); + DISPATCH(); + } + + TARGET(OP2) { + #if _Py_TAIL_CALL_INTERP + int opcode = OP2; + (void)(opcode); + #endif + frame->instr_ptr = next_instr; + next_instr += 1; + INSTRUCTION_STATS(OP2); + DISPATCH(); + } + """ + self.run_cases_test(input, output) + + + def test_array_input(self): + input = """ + inst(OP, (below, values[oparg*2], above --)) { + SPAM(values, oparg); + DEAD(below); + DEAD(values); + DEAD(above); + } + """ + output = """ + TARGET(OP) { + #if _Py_TAIL_CALL_INTERP + int opcode = OP; + (void)(opcode); + #endif + frame->instr_ptr = next_instr; + next_instr += 1; + INSTRUCTION_STATS(OP); + _PyStackRef below; + _PyStackRef *values; + _PyStackRef above; + above = stack_pointer[-1]; + values = &stack_pointer[-1 - oparg*2]; + below = stack_pointer[-2 - oparg*2]; + SPAM(values, oparg); + stack_pointer += -2 - oparg*2; + ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); + DISPATCH(); + } + """ + self.run_cases_test(input, output) + + def test_array_output(self): + input = """ + inst(OP, (unused, unused -- below, values[oparg*3], above)) { + SPAM(values, oparg); + below = 0; + above = 0; + } + """ + output = """ + TARGET(OP) { + #if _Py_TAIL_CALL_INTERP + int opcode = OP; + (void)(opcode); + #endif + frame->instr_ptr = next_instr; + next_instr += 1; + INSTRUCTION_STATS(OP); + _PyStackRef below; + _PyStackRef *values; + _PyStackRef above; + values = &stack_pointer[-1]; + SPAM(values, oparg); + below = 0; + above = 0; + stack_pointer[-2] = below; + stack_pointer[-1 + oparg*3] = above; + stack_pointer += oparg*3; + ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); + DISPATCH(); + } + """ + self.run_cases_test(input, output) + + def test_array_input_output(self): + input = """ + inst(OP, (values[oparg] -- values[oparg], above)) { + SPAM(values, oparg); + above = 0; + } + """ + output = """ + TARGET(OP) { + #if _Py_TAIL_CALL_INTERP + int opcode = OP; + (void)(opcode); + #endif + frame->instr_ptr = next_instr; + next_instr += 1; + INSTRUCTION_STATS(OP); + _PyStackRef *values; + _PyStackRef above; + values = &stack_pointer[-oparg]; + SPAM(values, oparg); + above = 0; + stack_pointer[0] = above; + stack_pointer += 1; + ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); + DISPATCH(); + } + """ + self.run_cases_test(input, output) + + def test_array_error_if(self): + input = """ + inst(OP, (extra, values[oparg] --)) { + DEAD(extra); + DEAD(values); + ERROR_IF(oparg == 0); + } + """ + output = """ + TARGET(OP) { + #if _Py_TAIL_CALL_INTERP + int opcode = OP; + (void)(opcode); + #endif + frame->instr_ptr = next_instr; + next_instr += 1; + INSTRUCTION_STATS(OP); + _PyStackRef extra; + _PyStackRef *values; + values = &stack_pointer[-oparg]; + extra = stack_pointer[-1 - oparg]; + if (oparg == 0) { + stack_pointer += -1 - oparg; + ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); + JUMP_TO_LABEL(error); + } + stack_pointer += -1 - oparg; + ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); + DISPATCH(); + } + """ + self.run_cases_test(input, output) + + def test_macro_push_push(self): + input = """ + op(A, (-- val1)) { + val1 = SPAM(); + } + op(B, (-- val2)) { + val2 = SPAM(); + } + macro(M) = A + B; + """ + output = """ + TARGET(M) { + #if _Py_TAIL_CALL_INTERP + int opcode = M; + (void)(opcode); + #endif + frame->instr_ptr = next_instr; + next_instr += 1; + INSTRUCTION_STATS(M); + _PyStackRef val1; + _PyStackRef val2; + // A + { + val1 = SPAM(); + } + // B + { + val2 = SPAM(); + } + stack_pointer[0] = val1; + stack_pointer[1] = val2; + stack_pointer += 2; + ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); + DISPATCH(); + } + """ + self.run_cases_test(input, output) + + def test_override_inst(self): + input = """ + inst(OP, (--)) { + spam; + } + override inst(OP, (--)) { + ham; + } + """ + output = """ + TARGET(OP) { + #if _Py_TAIL_CALL_INTERP + int opcode = OP; + (void)(opcode); + #endif + frame->instr_ptr = next_instr; + next_instr += 1; + INSTRUCTION_STATS(OP); + ham; + DISPATCH(); + } + """ + self.run_cases_test(input, output) + + def test_override_op(self): + input = """ + op(OP, (--)) { + spam; + } + macro(M) = OP; + override op(OP, (--)) { + ham; + } + """ + output = """ + TARGET(M) { + #if _Py_TAIL_CALL_INTERP + int opcode = M; + (void)(opcode); + #endif + frame->instr_ptr = next_instr; + next_instr += 1; + INSTRUCTION_STATS(M); + ham; + DISPATCH(); + } + """ + self.run_cases_test(input, output) + + def test_annotated_inst(self): + input = """ + pure inst(OP, (--)) { + ham; + } + """ + output = """ + TARGET(OP) { + #if _Py_TAIL_CALL_INTERP + int opcode = OP; + (void)(opcode); + #endif + frame->instr_ptr = next_instr; + next_instr += 1; + INSTRUCTION_STATS(OP); + ham; + DISPATCH(); + } + """ + self.run_cases_test(input, output) + + def test_annotated_op(self): + input = """ + pure op(OP, (--)) { + SPAM(); + } + macro(M) = OP; + """ + output = """ + TARGET(M) { + #if _Py_TAIL_CALL_INTERP + int opcode = M; + (void)(opcode); + #endif + frame->instr_ptr = next_instr; + next_instr += 1; + INSTRUCTION_STATS(M); + SPAM(); + DISPATCH(); + } + """ + self.run_cases_test(input, output) + + input = """ + pure register specializing op(OP, (--)) { + SPAM(); + } + macro(M) = OP; + """ + self.run_cases_test(input, output) + + def test_deopt_and_exit(self): + input = """ + pure op(OP, (arg1 -- out)) { + DEOPT_IF(1); + EXIT_IF(1); + } + """ + output = "" + with self.assertRaises(SyntaxError): + self.run_cases_test(input, output) + + def test_array_of_one(self): + input = """ + inst(OP, (arg[1] -- out[1])) { + out[0] = arg[0]; + DEAD(arg); + } + """ + output = """ + TARGET(OP) { + #if _Py_TAIL_CALL_INTERP + int opcode = OP; + (void)(opcode); + #endif + frame->instr_ptr = next_instr; + next_instr += 1; + INSTRUCTION_STATS(OP); + _PyStackRef *arg; + _PyStackRef *out; + arg = &stack_pointer[-1]; + out = &stack_pointer[-1]; + out[0] = arg[0]; + DISPATCH(); + } + """ + self.run_cases_test(input, output) + + def test_unused_cached_value(self): + input = """ + op(FIRST, (arg1 -- out)) { + out = arg1; + } + + op(SECOND, (unused -- unused)) { + } + + macro(BOTH) = FIRST + SECOND; + """ + output = """ + """ + with self.assertRaises(SyntaxError): + self.run_cases_test(input, output) + + def test_unused_named_values(self): + input = """ + op(OP, (named -- named)) { + } + + macro(INST) = OP; + """ + output = """ + TARGET(INST) { + #if _Py_TAIL_CALL_INTERP + int opcode = INST; + (void)(opcode); + #endif + frame->instr_ptr = next_instr; + next_instr += 1; + INSTRUCTION_STATS(INST); + DISPATCH(); + } + + """ + self.run_cases_test(input, output) + + def test_used_unused_used(self): + input = """ + op(FIRST, (w -- w)) { + USE(w); + } + + op(SECOND, (x -- x)) { + } + + op(THIRD, (y -- y)) { + USE(y); + } + + macro(TEST) = FIRST + SECOND + THIRD; + """ + output = """ + TARGET(TEST) { + #if _Py_TAIL_CALL_INTERP + int opcode = TEST; + (void)(opcode); + #endif + frame->instr_ptr = next_instr; + next_instr += 1; + INSTRUCTION_STATS(TEST); + _PyStackRef w; + _PyStackRef y; + // FIRST + { + w = stack_pointer[-1]; + USE(w); + } + // SECOND + { + } + // THIRD + { + y = w; + USE(y); + } + DISPATCH(); + } + """ + self.run_cases_test(input, output) + + def test_unused_used_used(self): + input = """ + op(FIRST, (w -- w)) { + } + + op(SECOND, (x -- x)) { + USE(x); + } + + op(THIRD, (y -- y)) { + USE(y); + } + + macro(TEST) = FIRST + SECOND + THIRD; + """ + output = """ + TARGET(TEST) { + #if _Py_TAIL_CALL_INTERP + int opcode = TEST; + (void)(opcode); + #endif + frame->instr_ptr = next_instr; + next_instr += 1; + INSTRUCTION_STATS(TEST); + _PyStackRef x; + _PyStackRef y; + // FIRST + { + } + // SECOND + { + x = stack_pointer[-1]; + USE(x); + } + // THIRD + { + y = x; + USE(y); + } + DISPATCH(); + } + """ + self.run_cases_test(input, output) + + def test_flush(self): + input = """ + op(FIRST, ( -- a, b)) { + a = 0; + b = 1; + } + + op(SECOND, (a, b -- )) { + USE(a, b); + INPUTS_DEAD(); + } + + macro(TEST) = FIRST + flush + SECOND; + """ + output = """ + TARGET(TEST) { + #if _Py_TAIL_CALL_INTERP + int opcode = TEST; + (void)(opcode); + #endif + frame->instr_ptr = next_instr; + next_instr += 1; + INSTRUCTION_STATS(TEST); + _PyStackRef a; + _PyStackRef b; + // FIRST + { + a = 0; + b = 1; + } + // flush + stack_pointer[0] = a; + stack_pointer[1] = b; + stack_pointer += 2; + ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); + // SECOND + { + USE(a, b); + } + stack_pointer += -2; + ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); + DISPATCH(); + } + """ + self.run_cases_test(input, output) + + def test_pop_on_error_peeks(self): + + input = """ + op(FIRST, (x, y -- a, b)) { + a = x; + DEAD(x); + b = y; + DEAD(y); + } + + op(SECOND, (a, b -- a, b)) { + } + + op(THIRD, (j, k --)) { + INPUTS_DEAD(); // Mark j and k as used + ERROR_IF(cond); + } + + macro(TEST) = FIRST + SECOND + THIRD; + """ + output = """ + TARGET(TEST) { + #if _Py_TAIL_CALL_INTERP + int opcode = TEST; + (void)(opcode); + #endif + frame->instr_ptr = next_instr; + next_instr += 1; + INSTRUCTION_STATS(TEST); + _PyStackRef x; + _PyStackRef y; + _PyStackRef a; + _PyStackRef b; + // FIRST + { + y = stack_pointer[-1]; + x = stack_pointer[-2]; + a = x; + b = y; + } + // SECOND + { + } + // THIRD + { + if (cond) { + JUMP_TO_LABEL(pop_2_error); + } + } + stack_pointer += -2; + ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); + DISPATCH(); + } + """ + self.run_cases_test(input, output) + + def test_push_then_error(self): + + input = """ + op(FIRST, ( -- a)) { + a = 1; + } + + op(SECOND, (a -- a, b)) { + b = 1; + ERROR_IF(cond); + } + + macro(TEST) = FIRST + SECOND; + """ + + output = """ + TARGET(TEST) { + #if _Py_TAIL_CALL_INTERP + int opcode = TEST; + (void)(opcode); + #endif + frame->instr_ptr = next_instr; + next_instr += 1; + INSTRUCTION_STATS(TEST); + _PyStackRef a; + _PyStackRef b; + // FIRST + { + a = 1; + } + // SECOND + { + b = 1; + if (cond) { + stack_pointer[0] = a; + stack_pointer[1] = b; + stack_pointer += 2; + ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); + JUMP_TO_LABEL(error); + } + } + stack_pointer[0] = a; + stack_pointer[1] = b; + stack_pointer += 2; + ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); + DISPATCH(); + } + """ + self.run_cases_test(input, output) + + def test_error_if_true(self): + + input = """ + inst(OP1, ( --)) { + ERROR_IF(true); + } + inst(OP2, ( --)) { + ERROR_IF(1); + } + """ + output = """ + TARGET(OP1) { + #if _Py_TAIL_CALL_INTERP + int opcode = OP1; + (void)(opcode); + #endif + frame->instr_ptr = next_instr; + next_instr += 1; + INSTRUCTION_STATS(OP1); + JUMP_TO_LABEL(error); + } + + TARGET(OP2) { + #if _Py_TAIL_CALL_INTERP + int opcode = OP2; + (void)(opcode); + #endif + frame->instr_ptr = next_instr; + next_instr += 1; + INSTRUCTION_STATS(OP2); + JUMP_TO_LABEL(error); + } + """ + self.run_cases_test(input, output) + + def test_scalar_array_inconsistency(self): + + input = """ + op(FIRST, ( -- a)) { + a = 1; + } + + op(SECOND, (a[1] -- b)) { + b = 1; + } + + macro(TEST) = FIRST + SECOND; + """ + + output = """ + """ + with self.assertRaises(SyntaxError): + self.run_cases_test(input, output) + + def test_array_size_inconsistency(self): + + input = """ + op(FIRST, ( -- a[2])) { + a[0] = 1; + } + + op(SECOND, (a[1] -- b)) { + b = 1; + } + + macro(TEST) = FIRST + SECOND; + """ + + output = """ + """ + with self.assertRaises(SyntaxError): + self.run_cases_test(input, output) + + def test_stack_save_reload(self): + + input = """ + inst(BALANCED, ( -- )) { + SAVE_STACK(); + code(); + RELOAD_STACK(); + } + """ + + output = """ + TARGET(BALANCED) { + #if _Py_TAIL_CALL_INTERP + int opcode = BALANCED; + (void)(opcode); + #endif + frame->instr_ptr = next_instr; + next_instr += 1; + INSTRUCTION_STATS(BALANCED); + _PyFrame_SetStackPointer(frame, stack_pointer); + code(); + stack_pointer = _PyFrame_GetStackPointer(frame); + DISPATCH(); + } + """ + self.run_cases_test(input, output) + + def test_stack_save_reload_paired(self): + + input = """ + inst(BALANCED, ( -- )) { + SAVE_STACK(); + RELOAD_STACK(); + } + """ + + output = """ + TARGET(BALANCED) { + #if _Py_TAIL_CALL_INTERP + int opcode = BALANCED; + (void)(opcode); + #endif + frame->instr_ptr = next_instr; + next_instr += 1; + INSTRUCTION_STATS(BALANCED); + DISPATCH(); + } + """ + self.run_cases_test(input, output) + + def test_stack_reload_only(self): + + input = """ + inst(BALANCED, ( -- )) { + RELOAD_STACK(); + } + """ + + output = """ + TARGET(BALANCED) { + #if _Py_TAIL_CALL_INTERP + int opcode = BALANCED; + (void)(opcode); + #endif + frame->instr_ptr = next_instr; + next_instr += 1; + INSTRUCTION_STATS(BALANCED); + _PyFrame_SetStackPointer(frame, stack_pointer); + stack_pointer = _PyFrame_GetStackPointer(frame); + DISPATCH(); + } + """ + with self.assertRaises(SyntaxError): + self.run_cases_test(input, output) + + def test_stack_save_only(self): + + input = """ + inst(BALANCED, ( -- )) { + SAVE_STACK(); + } + """ + + output = """ + TARGET(BALANCED) { + #if _Py_TAIL_CALL_INTERP + int opcode = BALANCED; + (void)(opcode); + #endif + _Py_CODEUNIT* const this_instr = next_instr; + (void)this_instr; + frame->instr_ptr = next_instr; + next_instr += 1; + INSTRUCTION_STATS(BALANCED); + _PyFrame_SetStackPointer(frame, stack_pointer); + stack_pointer = _PyFrame_GetStackPointer(frame); + DISPATCH(); + } + """ + with self.assertRaises(SyntaxError): + self.run_cases_test(input, output) + + def test_instruction_size_macro(self): + input = """ + inst(OP, (--)) { + frame->return_offset = INSTRUCTION_SIZE; + } + """ + + output = """ + TARGET(OP) { + #if _Py_TAIL_CALL_INTERP + int opcode = OP; + (void)(opcode); + #endif + frame->instr_ptr = next_instr; + next_instr += 1; + INSTRUCTION_STATS(OP); + frame->return_offset = 1u ; + DISPATCH(); + } + """ + self.run_cases_test(input, output) + + # Two instructions of different sizes referencing the same + # uop containing the `INSTRUCTION_SIZE` macro is not allowed. + input = """ + inst(OP, (--)) { + frame->return_offset = INSTRUCTION_SIZE; + } + macro(OP2) = unused/1 + OP; + """ + + output = "" # No output needed as this should raise an error. + with self.assertRaisesRegex(SyntaxError, "All instructions containing a uop"): + self.run_cases_test(input, output) + + def test_escaping_call_next_to_cmacro(self): + input = """ + inst(OP, (--)) { + #ifdef Py_GIL_DISABLED + escaping_call(); + #else + another_escaping_call(); + #endif + yet_another_escaping_call(); + } + """ + output = """ + TARGET(OP) { + #if _Py_TAIL_CALL_INTERP + int opcode = OP; + (void)(opcode); + #endif + frame->instr_ptr = next_instr; + next_instr += 1; + INSTRUCTION_STATS(OP); + #ifdef Py_GIL_DISABLED + _PyFrame_SetStackPointer(frame, stack_pointer); + escaping_call(); + stack_pointer = _PyFrame_GetStackPointer(frame); + #else + _PyFrame_SetStackPointer(frame, stack_pointer); + another_escaping_call(); + stack_pointer = _PyFrame_GetStackPointer(frame); + #endif + _PyFrame_SetStackPointer(frame, stack_pointer); + yet_another_escaping_call(); + stack_pointer = _PyFrame_GetStackPointer(frame); + DISPATCH(); + } + """ + self.run_cases_test(input, output) + + def test_pystackref_frompyobject_new_next_to_cmacro(self): + input = """ + inst(OP, (-- out1, out2)) { + PyObject *obj = SPAM(); + #ifdef Py_GIL_DISABLED + out1 = PyStackRef_FromPyObjectNew(obj); + #else + out1 = PyStackRef_FromPyObjectNew(obj); + #endif + out2 = PyStackRef_FromPyObjectNew(obj); + } + """ + output = """ + TARGET(OP) { + #if _Py_TAIL_CALL_INTERP + int opcode = OP; + (void)(opcode); + #endif + frame->instr_ptr = next_instr; + next_instr += 1; + INSTRUCTION_STATS(OP); + _PyStackRef out1; + _PyStackRef out2; + PyObject *obj = SPAM(); + #ifdef Py_GIL_DISABLED + out1 = PyStackRef_FromPyObjectNew(obj); + #else + out1 = PyStackRef_FromPyObjectNew(obj); + #endif + out2 = PyStackRef_FromPyObjectNew(obj); + stack_pointer[0] = out1; + stack_pointer[1] = out2; + stack_pointer += 2; + ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); + DISPATCH(); + } + """ + self.run_cases_test(input, output) + + def test_no_escaping_calls_in_branching_macros(self): + + input = """ + inst(OP, ( -- )) { + DEOPT_IF(escaping_call()); + } + """ + with self.assertRaises(SyntaxError): + self.run_cases_test(input, "") + + input = """ + inst(OP, ( -- )) { + EXIT_IF(escaping_call()); + } + """ + with self.assertRaises(SyntaxError): + self.run_cases_test(input, "") + + input = """ + inst(OP, ( -- )) { + ERROR_IF(escaping_call()); + } + """ + with self.assertRaises(SyntaxError): + self.run_cases_test(input, "") + + def test_kill_in_wrong_order(self): + input = """ + inst(OP, (a, b -- c)) { + c = b; + PyStackRef_CLOSE(a); + PyStackRef_CLOSE(b); + } + """ + with self.assertRaises(SyntaxError): + self.run_cases_test(input, "") + + def test_complex_label(self): + input = """ + label(other_label) { + } + + label(other_label2) { + } + + label(my_label) { + // Comment + do_thing(); + if (complex) { + goto other_label; + } + goto other_label2; + } + """ + + output = """ + LABEL(other_label) + { + } + + LABEL(other_label2) + { + } + + LABEL(my_label) + { + _PyFrame_SetStackPointer(frame, stack_pointer); + do_thing(); + stack_pointer = _PyFrame_GetStackPointer(frame); + if (complex) { + JUMP_TO_LABEL(other_label); + } + JUMP_TO_LABEL(other_label2); + } + """ + self.run_cases_test(input, output) + + def test_spilled_label(self): + input = """ + spilled label(one) { + RELOAD_STACK(); + goto two; + } + + label(two) { + SAVE_STACK(); + goto one; + } + """ + + output = """ + LABEL(one) + { + stack_pointer = _PyFrame_GetStackPointer(frame); + JUMP_TO_LABEL(two); + } + + LABEL(two) + { + _PyFrame_SetStackPointer(frame, stack_pointer); + JUMP_TO_LABEL(one); + } + """ + self.run_cases_test(input, output) + + + def test_incorrect_spills(self): + input1 = """ + spilled label(one) { + goto two; + } + + label(two) { + } + """ + + input2 = """ + spilled label(one) { + } + + label(two) { + goto one; + } + """ + with self.assertRaisesRegex(SyntaxError, ".*reload.*"): + self.run_cases_test(input1, "") + with self.assertRaisesRegex(SyntaxError, ".*spill.*"): + self.run_cases_test(input2, "") + + + def test_multiple_labels(self): + input = """ + label(my_label_1) { + // Comment + do_thing1(); + goto my_label_2; + } + + label(my_label_2) { + // Comment + do_thing2(); + goto my_label_1; + } + """ + + output = """ + LABEL(my_label_1) + { + _PyFrame_SetStackPointer(frame, stack_pointer); + do_thing1(); + stack_pointer = _PyFrame_GetStackPointer(frame); + JUMP_TO_LABEL(my_label_2); + } + + LABEL(my_label_2) + { + _PyFrame_SetStackPointer(frame, stack_pointer); + do_thing2(); + stack_pointer = _PyFrame_GetStackPointer(frame); + JUMP_TO_LABEL(my_label_1); + } + """ + self.run_cases_test(input, output) + + def test_reassigning_live_inputs(self): + input = """ + inst(OP, (in -- in)) { + in = 0; + } + """ + + output = """ + TARGET(OP) { + #if _Py_TAIL_CALL_INTERP + int opcode = OP; + (void)(opcode); + #endif + frame->instr_ptr = next_instr; + next_instr += 1; + INSTRUCTION_STATS(OP); + _PyStackRef in; + in = stack_pointer[-1]; + in = 0; + stack_pointer[-1] = in; + DISPATCH(); + } + """ + self.run_cases_test(input, output) + + def test_reassigning_dead_inputs(self): + input = """ + inst(OP, (in -- )) { + temp = use(in); + DEAD(in); + in = temp; + PyStackRef_CLOSE(in); + } + """ + output = """ + TARGET(OP) { + #if _Py_TAIL_CALL_INTERP + int opcode = OP; + (void)(opcode); + #endif + frame->instr_ptr = next_instr; + next_instr += 1; + INSTRUCTION_STATS(OP); + _PyStackRef in; + in = stack_pointer[-1]; + _PyFrame_SetStackPointer(frame, stack_pointer); + temp = use(in); + stack_pointer = _PyFrame_GetStackPointer(frame); + in = temp; + stack_pointer += -1; + ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); + _PyFrame_SetStackPointer(frame, stack_pointer); + PyStackRef_CLOSE(in); + stack_pointer = _PyFrame_GetStackPointer(frame); + DISPATCH(); + } + """ + self.run_cases_test(input, output) + + +class TestGeneratedAbstractCases(unittest.TestCase): + def setUp(self) -> None: + super().setUp() + self.maxDiff = None + + self.temp_dir = tempfile.gettempdir() + self.temp_input_filename = os.path.join(self.temp_dir, "input.txt") + self.temp_input2_filename = os.path.join(self.temp_dir, "input2.txt") + self.temp_output_filename = os.path.join(self.temp_dir, "output.txt") + + def tearDown(self) -> None: + for filename in [ + self.temp_input_filename, + self.temp_input2_filename, + self.temp_output_filename, + ]: + try: + os.remove(filename) + except: + pass + super().tearDown() + + def run_cases_test(self, input: str, input2: str, expected: str): + with open(self.temp_input_filename, "w+") as temp_input: + temp_input.write(parser.BEGIN_MARKER) + temp_input.write(input) + temp_input.write(parser.END_MARKER) + temp_input.flush() + + with open(self.temp_input2_filename, "w+") as temp_input: + temp_input.write(parser.BEGIN_MARKER) + temp_input.write(input2) + temp_input.write(parser.END_MARKER) + temp_input.flush() + + with handle_stderr(): + optimizer_generator.generate_tier2_abstract_from_files( + [self.temp_input_filename, self.temp_input2_filename], + self.temp_output_filename + ) + + with open(self.temp_output_filename) as temp_output: + lines = temp_output.readlines() + while lines and lines[0].startswith(("// ", "#", " #", "\n")): + lines.pop(0) + while lines and lines[-1].startswith(("#", "\n")): + lines.pop(-1) + actual = "".join(lines) + self.assertEqual(actual.strip(), expected.strip()) + + def test_overridden_abstract(self): + input = """ + pure op(OP, (--)) { + SPAM(); + } + """ + input2 = """ + pure op(OP, (--)) { + eggs(); + } + """ + output = """ + case OP: { + eggs(); + break; + } + """ + self.run_cases_test(input, input2, output) + + def test_overridden_abstract_args(self): + input = """ + pure op(OP, (arg1 -- out)) { + out = SPAM(arg1); + } + op(OP2, (arg1 -- out)) { + out = EGGS(arg1); + } + """ + input2 = """ + op(OP, (arg1 -- out)) { + out = EGGS(arg1); + } + """ + output = """ + case OP: { + JitOptRef arg1; + JitOptRef out; + arg1 = stack_pointer[-1]; + out = EGGS(arg1); + stack_pointer[-1] = out; + break; + } + + case OP2: { + JitOptRef out; + out = sym_new_not_null(ctx); + stack_pointer[-1] = out; + break; + } + """ + self.run_cases_test(input, input2, output) + + def test_no_overridden_case(self): + input = """ + pure op(OP, (arg1 -- out)) { + out = SPAM(arg1); + } + + pure op(OP2, (arg1 -- out)) { + } + + """ + input2 = """ + pure op(OP2, (arg1 -- out)) { + out = NULL; + } + """ + output = """ + case OP: { + JitOptRef out; + out = sym_new_not_null(ctx); + stack_pointer[-1] = out; + break; + } + + case OP2: { + JitOptRef out; + out = NULL; + stack_pointer[-1] = out; + break; + } + """ + self.run_cases_test(input, input2, output) + + def test_missing_override_failure(self): + input = """ + pure op(OP, (arg1 -- out)) { + SPAM(); + } + """ + input2 = """ + pure op(OTHER, (arg1 -- out)) { + } + """ + output = """ + """ + with self.assertRaisesRegex(ValueError, "All abstract uops"): + self.run_cases_test(input, input2, output) + + def test_validate_uop_input_length_mismatch(self): + input = """ + op(OP, (arg1 -- out)) { + SPAM(); + } + """ + input2 = """ + op(OP, (arg1, arg2 -- out)) { + } + """ + output = """ + """ + with self.assertRaisesRegex(SyntaxError, + "Must have the same number of inputs"): + self.run_cases_test(input, input2, output) + + def test_validate_uop_output_length_mismatch(self): + input = """ + op(OP, (arg1 -- out)) { + SPAM(); + } + """ + input2 = """ + op(OP, (arg1 -- out1, out2)) { + } + """ + output = """ + """ + with self.assertRaisesRegex(SyntaxError, + "Must have the same number of outputs"): + self.run_cases_test(input, input2, output) + + def test_validate_uop_input_name_mismatch(self): + input = """ + op(OP, (foo -- out)) { + SPAM(); + } + """ + input2 = """ + op(OP, (bar -- out)) { + } + """ + output = """ + """ + with self.assertRaisesRegex(SyntaxError, + "Inputs must have equal names"): + self.run_cases_test(input, input2, output) + + def test_validate_uop_output_name_mismatch(self): + input = """ + op(OP, (arg1 -- foo)) { + SPAM(); + } + """ + input2 = """ + op(OP, (arg1 -- bar)) { + } + """ + output = """ + """ + with self.assertRaisesRegex(SyntaxError, + "Outputs must have equal names"): + self.run_cases_test(input, input2, output) + + def test_validate_uop_unused_input(self): + input = """ + op(OP, (unused -- )) { + } + """ + input2 = """ + op(OP, (foo -- )) { + } + """ + output = """ + case OP: { + CHECK_STACK_BOUNDS(-1); + stack_pointer += -1; + ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); + break; + } + """ + self.run_cases_test(input, input2, output) + + input = """ + op(OP, (foo -- )) { + } + """ + input2 = """ + op(OP, (unused -- )) { + } + """ + output = """ + case OP: { + CHECK_STACK_BOUNDS(-1); + stack_pointer += -1; + ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); + break; + } + """ + self.run_cases_test(input, input2, output) + + def test_validate_uop_unused_output(self): + input = """ + op(OP, ( -- unused)) { + } + """ + input2 = """ + op(OP, ( -- foo)) { + foo = NULL; + } + """ + output = """ + case OP: { + JitOptRef foo; + foo = NULL; + CHECK_STACK_BOUNDS(1); + stack_pointer[0] = foo; + stack_pointer += 1; + ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); + break; + } + """ + self.run_cases_test(input, input2, output) + + input = """ + op(OP, ( -- foo)) { + foo = NULL; + } + """ + input2 = """ + op(OP, ( -- unused)) { + } + """ + output = """ + case OP: { + CHECK_STACK_BOUNDS(1); + stack_pointer += 1; + ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); + break; + } + """ + self.run_cases_test(input, input2, output) + + def test_validate_uop_input_size_mismatch(self): + input = """ + op(OP, (arg1[2] -- )) { + } + """ + input2 = """ + op(OP, (arg1[4] -- )) { + } + """ + output = """ + """ + with self.assertRaisesRegex(SyntaxError, + "Inputs must have equal sizes"): + self.run_cases_test(input, input2, output) + + def test_validate_uop_output_size_mismatch(self): + input = """ + op(OP, ( -- out[2])) { + } + """ + input2 = """ + op(OP, ( -- out[4])) { + } + """ + output = """ + """ + with self.assertRaisesRegex(SyntaxError, + "Outputs must have equal sizes"): + self.run_cases_test(input, input2, output) + + def test_validate_uop_unused_size_mismatch(self): + input = """ + op(OP, (foo[2] -- )) { + } + """ + input2 = """ + op(OP, (unused[4] -- )) { + } + """ + output = """ + """ + with self.assertRaisesRegex(SyntaxError, + "Inputs must have equal sizes"): + self.run_cases_test(input, input2, output) + + def test_pure_uop_body_copied_in(self): + # Note: any non-escaping call works. + # In this case, we use PyStackRef_IsNone. + input = """ + pure op(OP, (foo -- res)) { + res = PyStackRef_IsNone(foo); + } + """ + input2 = """ + op(OP, (foo -- res)) { + REPLACE_OPCODE_IF_EVALUATES_PURE(foo, res); + res = sym_new_known(ctx, foo); + } + """ + output = """ + case OP: { + JitOptRef foo; + JitOptRef res; + foo = stack_pointer[-1]; + if ( + sym_is_safe_const(ctx, foo) + ) { + JitOptRef foo_sym = foo; + _PyStackRef foo = sym_get_const_as_stackref(ctx, foo_sym); + _PyStackRef res_stackref; + /* Start of uop copied from bytecodes for constant evaluation */ + res_stackref = PyStackRef_IsNone(foo); + /* End of uop copied from bytecodes for constant evaluation */ + res = sym_new_const_steal(ctx, PyStackRef_AsPyObjectSteal(res_stackref)); + stack_pointer[-1] = res; + break; + } + res = sym_new_known(ctx, foo); + stack_pointer[-1] = res; + break; + } + """ + self.run_cases_test(input, input2, output) + + def test_pure_uop_body_copied_in_deopt(self): + # Note: any non-escaping call works. + # In this case, we use PyStackRef_IsNone. + input = """ + pure op(OP, (foo -- res)) { + DEOPT_IF(PyStackRef_IsNull(foo)); + res = foo; + } + """ + input2 = """ + op(OP, (foo -- res)) { + REPLACE_OPCODE_IF_EVALUATES_PURE(foo, res); + res = foo; + } + """ + output = """ + case OP: { + JitOptRef foo; + JitOptRef res; + foo = stack_pointer[-1]; + if ( + sym_is_safe_const(ctx, foo) + ) { + JitOptRef foo_sym = foo; + _PyStackRef foo = sym_get_const_as_stackref(ctx, foo_sym); + _PyStackRef res_stackref; + /* Start of uop copied from bytecodes for constant evaluation */ + if (PyStackRef_IsNull(foo)) { + ctx->done = true; + break; + } + res_stackref = foo; + /* End of uop copied from bytecodes for constant evaluation */ + res = sym_new_const_steal(ctx, PyStackRef_AsPyObjectSteal(res_stackref)); + stack_pointer[-1] = res; + break; + } + res = foo; + stack_pointer[-1] = res; + break; + } + """ + self.run_cases_test(input, input2, output) + + def test_pure_uop_body_copied_in_error_if(self): + # Note: any non-escaping call works. + # In this case, we use PyStackRef_IsNone. + input = """ + pure op(OP, (foo -- res)) { + ERROR_IF(PyStackRef_IsNull(foo)); + res = foo; + } + """ + input2 = """ + op(OP, (foo -- res)) { + REPLACE_OPCODE_IF_EVALUATES_PURE(foo, res); + res = foo; + } + """ + output = """ + case OP: { + JitOptRef foo; + JitOptRef res; + foo = stack_pointer[-1]; + if ( + sym_is_safe_const(ctx, foo) + ) { + JitOptRef foo_sym = foo; + _PyStackRef foo = sym_get_const_as_stackref(ctx, foo_sym); + _PyStackRef res_stackref; + /* Start of uop copied from bytecodes for constant evaluation */ + if (PyStackRef_IsNull(foo)) { + goto error; + } + res_stackref = foo; + /* End of uop copied from bytecodes for constant evaluation */ + res = sym_new_const_steal(ctx, PyStackRef_AsPyObjectSteal(res_stackref)); + stack_pointer[-1] = res; + break; + } + res = foo; + stack_pointer[-1] = res; + break; + } + """ + self.run_cases_test(input, input2, output) + + + def test_replace_opcode_uop_body_copied_in_complex(self): + input = """ + pure op(OP, (foo -- res)) { + if (foo) { + res = PyStackRef_IsNone(foo); + } + else { + res = 1; + } + } + """ + input2 = """ + op(OP, (foo -- res)) { + REPLACE_OPCODE_IF_EVALUATES_PURE(foo, res); + res = sym_new_known(ctx, foo); + } + """ + output = """ + case OP: { + JitOptRef foo; + JitOptRef res; + foo = stack_pointer[-1]; + if ( + sym_is_safe_const(ctx, foo) + ) { + JitOptRef foo_sym = foo; + _PyStackRef foo = sym_get_const_as_stackref(ctx, foo_sym); + _PyStackRef res_stackref; + /* Start of uop copied from bytecodes for constant evaluation */ + if (foo) { + res_stackref = PyStackRef_IsNone(foo); + } + else { + res_stackref = 1; + } + /* End of uop copied from bytecodes for constant evaluation */ + res = sym_new_const_steal(ctx, PyStackRef_AsPyObjectSteal(res_stackref)); + stack_pointer[-1] = res; + break; + } + res = sym_new_known(ctx, foo); + stack_pointer[-1] = res; + break; + } + """ + self.run_cases_test(input, input2, output) + + def test_replace_opcode_escaping_uop_body_copied_in_complex(self): + input = """ + pure op(OP, (foo -- res)) { + if (foo) { + res = ESCAPING_CODE(foo); + } + else { + res = 1; + } + } + """ + input2 = """ + op(OP, (foo -- res)) { + REPLACE_OPCODE_IF_EVALUATES_PURE(foo, res); + res = sym_new_known(ctx, foo); + } + """ + output = """ + case OP: { + JitOptRef foo; + JitOptRef res; + foo = stack_pointer[-1]; + if ( + sym_is_safe_const(ctx, foo) + ) { + JitOptRef foo_sym = foo; + _PyStackRef foo = sym_get_const_as_stackref(ctx, foo_sym); + _PyStackRef res_stackref; + /* Start of uop copied from bytecodes for constant evaluation */ + if (foo) { + res_stackref = ESCAPING_CODE(foo); + } + else { + res_stackref = 1; + } + /* End of uop copied from bytecodes for constant evaluation */ + res = sym_new_const_steal(ctx, PyStackRef_AsPyObjectSteal(res_stackref)); + stack_pointer[-1] = res; + break; + } + res = sym_new_known(ctx, foo); + stack_pointer[-1] = res; + break; + } + """ + self.run_cases_test(input, input2, output) + + def test_replace_opcode_binop_one_output(self): + input = """ + pure op(OP, (left, right -- res)) { + res = foo(left, right); + } + """ + input2 = """ + op(OP, (left, right -- res)) { + res = sym_new_non_null(ctx, foo); + REPLACE_OPCODE_IF_EVALUATES_PURE(left, right, res); + } + """ + output = """ + case OP: { + JitOptRef right; + JitOptRef left; + JitOptRef res; + right = stack_pointer[-1]; + left = stack_pointer[-2]; + res = sym_new_non_null(ctx, foo); + if ( + sym_is_safe_const(ctx, left) && + sym_is_safe_const(ctx, right) + ) { + JitOptRef left_sym = left; + JitOptRef right_sym = right; + _PyStackRef left = sym_get_const_as_stackref(ctx, left_sym); + _PyStackRef right = sym_get_const_as_stackref(ctx, right_sym); + _PyStackRef res_stackref; + /* Start of uop copied from bytecodes for constant evaluation */ + res_stackref = foo(left, right); + /* End of uop copied from bytecodes for constant evaluation */ + res = sym_new_const_steal(ctx, PyStackRef_AsPyObjectSteal(res_stackref)); + CHECK_STACK_BOUNDS(-1); + stack_pointer[-2] = res; + stack_pointer += -1; + ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); + break; + } + CHECK_STACK_BOUNDS(-1); + stack_pointer[-2] = res; + stack_pointer += -1; + ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); + break; + } + """ + self.run_cases_test(input, input2, output) + + def test_replace_opcode_binop_one_output_insert(self): + input = """ + pure op(OP, (left, right -- res, l, r)) { + res = foo(left, right); + l = left; + r = right; + } + """ + input2 = """ + op(OP, (left, right -- res, l, r)) { + res = sym_new_non_null(ctx, foo); + l = left; + r = right; + REPLACE_OPCODE_IF_EVALUATES_PURE(left, right, res); + } + """ + output = """ + case OP: { + JitOptRef right; + JitOptRef left; + JitOptRef res; + JitOptRef l; + JitOptRef r; + right = stack_pointer[-1]; + left = stack_pointer[-2]; + res = sym_new_non_null(ctx, foo); + l = left; + r = right; + if ( + sym_is_safe_const(ctx, left) && + sym_is_safe_const(ctx, right) + ) { + JitOptRef left_sym = left; + JitOptRef right_sym = right; + _PyStackRef left = sym_get_const_as_stackref(ctx, left_sym); + _PyStackRef right = sym_get_const_as_stackref(ctx, right_sym); + _PyStackRef res_stackref; + _PyStackRef l_stackref; + _PyStackRef r_stackref; + /* Start of uop copied from bytecodes for constant evaluation */ + res_stackref = foo(left, right); + l_stackref = left; + r_stackref = right; + /* End of uop copied from bytecodes for constant evaluation */ + (void)l_stackref; + (void)r_stackref; + res = sym_new_const_steal(ctx, PyStackRef_AsPyObjectSteal(res_stackref)); + CHECK_STACK_BOUNDS(1); + stack_pointer[-2] = res; + stack_pointer[-1] = l; + stack_pointer[0] = r; + stack_pointer += 1; + ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); + break; + } + CHECK_STACK_BOUNDS(1); + stack_pointer[-2] = res; + stack_pointer[-1] = l; + stack_pointer[0] = r; + stack_pointer += 1; + ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); + break; + } + """ + self.run_cases_test(input, input2, output) + + def test_replace_opcode_unaryop_one_output_insert(self): + input = """ + pure op(OP, (left -- res, l)) { + res = foo(left); + l = left; + } + """ + input2 = """ + op(OP, (left -- res, l)) { + res = sym_new_non_null(ctx, foo); + l = left; + REPLACE_OPCODE_IF_EVALUATES_PURE(left, res); + } + """ + output = """ + case OP: { + JitOptRef left; + JitOptRef res; + JitOptRef l; + left = stack_pointer[-1]; + res = sym_new_non_null(ctx, foo); + l = left; + if ( + sym_is_safe_const(ctx, left) + ) { + JitOptRef left_sym = left; + _PyStackRef left = sym_get_const_as_stackref(ctx, left_sym); + _PyStackRef res_stackref; + _PyStackRef l_stackref; + /* Start of uop copied from bytecodes for constant evaluation */ + res_stackref = foo(left); + l_stackref = left; + /* End of uop copied from bytecodes for constant evaluation */ + (void)l_stackref; + res = sym_new_const_steal(ctx, PyStackRef_AsPyObjectSteal(res_stackref)); + CHECK_STACK_BOUNDS(1); + stack_pointer[-1] = res; + stack_pointer[0] = l; + stack_pointer += 1; + ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); + break; + } + CHECK_STACK_BOUNDS(1); + stack_pointer[-1] = res; + stack_pointer[0] = l; + stack_pointer += 1; + ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); + break; + } + """ + self.run_cases_test(input, input2, output) + + def test_replace_opocode_uop_reject_array_effects(self): + input = """ + pure op(OP, (foo[2] -- res)) { + if (foo) { + res = PyStackRef_IsNone(foo); + } + else { + res = 1; + } + } + """ + input2 = """ + op(OP, (foo[2] -- res)) { + REPLACE_OPCODE_IF_EVALUATES_PURE(foo, res); + res = sym_new_unknown(ctx); + } + """ + output = """ + """ + with self.assertRaisesRegex(SyntaxError, + "Pure evaluation cannot take array-like inputs"): + self.run_cases_test(input, input2, output) + +class TestAnalyzer(unittest.TestCase): + """Tests for analyzer.add_macro() recording-uop placement rules (gh-148285).""" + + def _parse_and_analyze(self, src: str) -> None: + """Parse a raw DSL fragment and run analyze_forest() on it.""" + nodes = parse_src(src) + analyze_forest(nodes) + + # ---- shared DSL fragments ----------------------------------------------- + + _SPECIALIZE_OP = """\ +specializing op(_SPECIALIZE_DUMMY, (counter/1, value -- value)) { +} +""" + _RECORD_OP = """\ +op(_RECORD_DUMMY, (value -- value)) { + RECORD_VALUE(PyStackRef_AsPyObjectBorrow(value)); +} +""" + _WORKER_OP = """\ +op(_WORKER_DUMMY, (value -- res)) { + res = value; +} +""" + + # ---- test cases --------------------------------------------------------- + + def test_recording_uop_after_specializing(self): + """Valid: recording uop directly follows a specializing uop.""" + src = ( + self._SPECIALIZE_OP + + self._RECORD_OP + + self._WORKER_OP + + "macro(VALID_DIRECT) = _SPECIALIZE_DUMMY + _RECORD_DUMMY + _WORKER_DUMMY;\n" + ) + # Must not raise. + self._parse_and_analyze(src) + + def test_recording_uop_after_specializing_with_cache(self): + """Valid: recording uop follows a specializing uop with a cache effect between them. + + CacheEffect entries are transparent — they must not close the gate that + allows a recording uop to follow a specializing uop. + """ + src = ( + self._SPECIALIZE_OP + + self._RECORD_OP + + self._WORKER_OP + + "macro(VALID_CACHE) = _SPECIALIZE_DUMMY + unused/1 + _RECORD_DUMMY + _WORKER_DUMMY;\n" + ) + # Must not raise. + self._parse_and_analyze(src) + + def test_recording_uop_after_non_specializing_raises(self): + """Invalid: recording uop after a plain (non-specializing) uop must be rejected.""" + src = ( + self._SPECIALIZE_OP + + self._RECORD_OP + + self._WORKER_OP + + "macro(INVALID) = _WORKER_DUMMY + _RECORD_DUMMY;\n" + ) + with self.assertRaisesRegex( + SyntaxError, + r"Recording uop _RECORD_DUMMY must be first in macro " + r"or immediately follow a specializing uop", + ): + self._parse_and_analyze(src) + + +if __name__ == "__main__": + unittest.main() diff --git a/Tools/cases_generator/analyzer.py b/Tools/cases_generator/analyzer.py index 6ba9c43ef1f0c3..27854c362c33ec 100644 --- a/Tools/cases_generator/analyzer.py +++ b/Tools/cases_generator/analyzer.py @@ -1,1446 +1,1455 @@ -from dataclasses import dataclass -import itertools -import lexer -import parser -import re -from typing import Optional, Callable, Iterator - -from parser import Stmt, SimpleStmt, BlockStmt, IfStmt, WhileStmt, ForStmt, MacroIfStmt - -MAX_CACHED_REGISTER = 3 - -@dataclass -class EscapingCall: - stmt: SimpleStmt - call: lexer.Token - kills: lexer.Token | None - -@dataclass -class Properties: - escaping_calls: dict[SimpleStmt, EscapingCall] - escapes: bool - error_with_pop: bool - error_without_pop: bool - deopts: bool - deopts_periodic: bool - oparg: bool - jumps: bool - eval_breaker: bool - needs_this: bool - always_exits: bool - sync_sp: bool - uses_co_consts: bool - uses_co_names: bool - uses_locals: bool - has_free: bool - side_exit: bool - side_exit_at_end: bool - pure: bool - uses_opcode: bool - needs_guard_ip: bool - unpredictable_jump: bool - records_value: bool - tier: int | None = None - const_oparg: int = -1 - needs_prev: bool = False - no_save_ip: bool = False - - def dump(self, indent: str) -> None: - simple_properties = self.__dict__.copy() - del simple_properties["escaping_calls"] - text = "escaping_calls:\n" - for tkns in self.escaping_calls.values(): - text += f"{indent} {tkns}\n" - text += ", ".join([f"{key}: {value}" for (key, value) in simple_properties.items()]) - print(indent, text, sep="") - - @staticmethod - def from_list(properties: list["Properties"]) -> "Properties": - escaping_calls: dict[SimpleStmt, EscapingCall] = {} - for p in properties: - escaping_calls.update(p.escaping_calls) - return Properties( - escaping_calls=escaping_calls, - escapes = any(p.escapes for p in properties), - error_with_pop=any(p.error_with_pop for p in properties), - error_without_pop=any(p.error_without_pop for p in properties), - deopts=any(p.deopts for p in properties), - deopts_periodic=any(p.deopts_periodic for p in properties), - oparg=any(p.oparg for p in properties), - jumps=any(p.jumps for p in properties), - eval_breaker=any(p.eval_breaker for p in properties), - needs_this=any(p.needs_this for p in properties), - always_exits=any(p.always_exits for p in properties), - sync_sp=any(p.sync_sp for p in properties), - uses_co_consts=any(p.uses_co_consts for p in properties), - uses_co_names=any(p.uses_co_names for p in properties), - uses_locals=any(p.uses_locals for p in properties), - uses_opcode=any(p.uses_opcode for p in properties), - has_free=any(p.has_free for p in properties), - side_exit=any(p.side_exit for p in properties), - side_exit_at_end=any(p.side_exit_at_end for p in properties), - pure=all(p.pure for p in properties), - needs_prev=any(p.needs_prev for p in properties), - no_save_ip=all(p.no_save_ip for p in properties), - needs_guard_ip=any(p.needs_guard_ip for p in properties), - unpredictable_jump=any(p.unpredictable_jump for p in properties), - records_value=any(p.records_value for p in properties), - ) - - @property - def infallible(self) -> bool: - return not self.error_with_pop and not self.error_without_pop - -SKIP_PROPERTIES = Properties( - escaping_calls={}, - escapes=False, - error_with_pop=False, - error_without_pop=False, - deopts=False, - deopts_periodic=False, - oparg=False, - jumps=False, - eval_breaker=False, - needs_this=False, - always_exits=False, - sync_sp=False, - uses_co_consts=False, - uses_co_names=False, - uses_locals=False, - uses_opcode=False, - has_free=False, - side_exit=False, - side_exit_at_end=False, - pure=True, - no_save_ip=False, - needs_guard_ip=False, - unpredictable_jump=False, - records_value=False, -) - - -@dataclass -class Skip: - "Unused cache entry" - size: int - - @property - def name(self) -> str: - return f"unused/{self.size}" - - @property - def properties(self) -> Properties: - return SKIP_PROPERTIES - - -class Flush: - @property - def properties(self) -> Properties: - return SKIP_PROPERTIES - - @property - def name(self) -> str: - return "flush" - - @property - def size(self) -> int: - return 0 - - - - -@dataclass -class StackItem: - name: str - size: str - peek: bool = False - used: bool = False - - def __str__(self) -> str: - size = f"[{self.size}]" if self.size else "" - return f"{self.name}{size} {self.peek}" - - def is_array(self) -> bool: - return self.size != "" - - def get_size(self) -> str: - return self.size if self.size else "1" - - -@dataclass -class StackEffect: - inputs: list[StackItem] - outputs: list[StackItem] - - def __str__(self) -> str: - return f"({', '.join([str(i) for i in self.inputs])} -- {', '.join([str(i) for i in self.outputs])})" - - -@dataclass -class CacheEntry: - name: str - size: int - - def __str__(self) -> str: - return f"{self.name}/{self.size}" - - -@dataclass -class Uop: - name: str - context: parser.Context | None - annotations: list[str] - stack: StackEffect - caches: list[CacheEntry] - local_stores: list[lexer.Token] - body: BlockStmt - properties: Properties - _size: int = -1 - implicitly_created: bool = False - replicated = range(0) - replicates: "Uop | None" = None - # Size of the instruction(s), only set for uops containing the INSTRUCTION_SIZE macro - instruction_size: int | None = None - - def dump(self, indent: str) -> None: - print( - indent, self.name, ", ".join(self.annotations) if self.annotations else "" - ) - print(indent, self.stack, ", ".join([str(c) for c in self.caches])) - self.properties.dump(" " + indent) - - @property - def size(self) -> int: - if self._size < 0: - self._size = sum(c.size for c in self.caches) - return self._size - - def why_not_viable(self) -> str | None: - if self.name == "_SAVE_RETURN_OFFSET": - return None # Adjusts next_instr, but only in tier 1 code - if "INSTRUMENTED" in self.name: - return "is instrumented" - if "replaced" in self.annotations: - return "is replaced" - if self.name in ("INTERPRETER_EXIT", "JUMP_BACKWARD"): - return "has tier 1 control flow" - if self.properties.needs_this: - return "uses the 'this_instr' variable" - if len([c for c in self.caches if c.name != "unused"]) > 2: - return "has too many cache entries" - if self.properties.error_with_pop and self.properties.error_without_pop: - return "has both popping and not-popping errors" - return None - - def is_viable(self) -> bool: - return self.why_not_viable() is None - - def is_super(self) -> bool: - for tkn in self.body.tokens(): - if tkn.kind == "IDENTIFIER" and tkn.text == "oparg1": - return True - return False - - -class Label: - - def __init__(self, name: str, spilled: bool, body: BlockStmt, properties: Properties): - self.name = name - self.spilled = spilled - self.body = body - self.properties = properties - - size:int = 0 - local_stores: list[lexer.Token] = [] - instruction_size = None - - def __str__(self) -> str: - return f"label({self.name})" - - -Part = Uop | Skip | Flush -CodeSection = Uop | Label - - -@dataclass -class Instruction: - where: lexer.Token - name: str - parts: list[Part] - _properties: Properties | None - is_target: bool = False - family: Optional["Family"] = None - opcode: int = -1 - - @property - def properties(self) -> Properties: - if self._properties is None: - self._properties = self._compute_properties() - return self._properties - - def _compute_properties(self) -> Properties: - return Properties.from_list([part.properties for part in self.parts]) - - def dump(self, indent: str) -> None: - print(indent, self.name, "=", ", ".join([part.name for part in self.parts])) - self.properties.dump(" " + indent) - - @property - def size(self) -> int: - return 1 + sum(part.size for part in self.parts) - - def is_super(self) -> bool: - if len(self.parts) != 1: - return False - uop = self.parts[0] - if isinstance(uop, Uop): - return uop.is_super() - else: - return False - - -@dataclass -class PseudoInstruction: - name: str - stack: StackEffect - targets: list[Instruction] - as_sequence: bool - flags: list[str] - opcode: int = -1 - - def dump(self, indent: str) -> None: - print(indent, self.name, "->", " or ".join([t.name for t in self.targets])) - - @property - def properties(self) -> Properties: - return Properties.from_list([i.properties for i in self.targets]) - - -@dataclass -class Family: - name: str - size: str - members: list[Instruction] - - def dump(self, indent: str) -> None: - print(indent, self.name, "= ", ", ".join([m.name for m in self.members])) - - -@dataclass -class Analysis: - instructions: dict[str, Instruction] - uops: dict[str, Uop] - families: dict[str, Family] - pseudos: dict[str, PseudoInstruction] - labels: dict[str, Label] - opmap: dict[str, int] - have_arg: int - min_instrumented: int - - -def analysis_error(message: str, tkn: lexer.Token) -> SyntaxError: - # To do -- support file and line output - # Construct a SyntaxError instance from message and token - return lexer.make_syntax_error(message, tkn.filename, tkn.line, tkn.column, "") - - -def override_error( - name: str, - context: parser.Context | None, - prev_context: parser.Context | None, - token: lexer.Token, -) -> SyntaxError: - return analysis_error( - f"Duplicate definition of '{name}' @ {context} " - f"previous definition @ {prev_context}", - token, - ) - - -def convert_stack_item( - item: parser.StackEffect, replace_op_arg_1: str | None -) -> StackItem: - return StackItem(item.name, item.size) - -def check_unused(stack: list[StackItem], input_names: dict[str, lexer.Token]) -> None: - "Unused items cannot be on the stack above used, non-peek items" - seen_unused = False - for item in reversed(stack): - if item.name == "unused": - seen_unused = True - elif item.peek: - break - elif seen_unused: - raise analysis_error(f"Cannot have used input '{item.name}' below an unused value on the stack", input_names[item.name]) - - -def analyze_stack( - op: parser.InstDef | parser.Pseudo, replace_op_arg_1: str | None = None -) -> StackEffect: - inputs: list[StackItem] = [ - convert_stack_item(i, replace_op_arg_1) - for i in op.inputs - if isinstance(i, parser.StackEffect) - ] - outputs: list[StackItem] = [ - convert_stack_item(i, replace_op_arg_1) for i in op.outputs - ] - # Mark variables with matching names at the base of the stack as "peek" - modified = False - input_names: dict[str, lexer.Token] = { i.name : i.first_token for i in op.inputs if i.name != "unused" } - for input, output in itertools.zip_longest(inputs, outputs): - if output is None: - pass - elif input is None: - if output.name in input_names: - raise analysis_error( - f"Reuse of variable '{output.name}' at different stack location", - input_names[output.name]) - elif input.name == output.name: - if not modified: - input.peek = output.peek = True - else: - modified = True - if output.name in input_names: - raise analysis_error( - f"Reuse of variable '{output.name}' at different stack location", - input_names[output.name]) - if isinstance(op, parser.InstDef): - output_names = [out.name for out in outputs] - for input in inputs: - if ( - variable_used(op, input.name) - or variable_used(op, "DECREF_INPUTS") - or (not input.peek and input.name in output_names) - ): - input.used = True - for output in outputs: - if variable_used(op, output.name): - output.used = True - check_unused(inputs, input_names) - return StackEffect(inputs, outputs) - - -def analyze_caches(inputs: list[parser.InputEffect]) -> list[CacheEntry]: - caches: list[parser.CacheEffect] = [ - i for i in inputs if isinstance(i, parser.CacheEffect) - ] - if caches: - # Middle entries are allowed to be unused. Check first and last caches. - for index in (0, -1): - cache = caches[index] - if cache.name == "unused": - position = "First" if index == 0 else "Last" - msg = f"{position} cache entry in op is unused. Move to enclosing macro." - raise analysis_error(msg, cache.tokens[0]) - return [CacheEntry(i.name, int(i.size)) for i in caches] - - -def find_variable_stores(node: parser.InstDef) -> list[lexer.Token]: - res: list[lexer.Token] = [] - outnames = { out.name for out in node.outputs } - innames = { out.name for out in node.inputs } - - def find_stores_in_tokens(tokens: list[lexer.Token], callback: Callable[[lexer.Token], None]) -> None: - while tokens and tokens[0].kind == "COMMENT": - tokens = tokens[1:] - if len(tokens) < 4: - return - if tokens[1].kind == "EQUALS": - if tokens[0].kind == "IDENTIFIER": - name = tokens[0].text - if name in outnames or name in innames: - callback(tokens[0]) - #Passing the address of a local is also a definition - for idx, tkn in enumerate(tokens): - if tkn.kind == "AND": - name_tkn = tokens[idx+1] - if name_tkn.text in outnames: - callback(name_tkn) - - def visit(stmt: Stmt) -> None: - if isinstance(stmt, IfStmt): - def error(tkn: lexer.Token) -> None: - raise analysis_error("Cannot define variable in 'if' condition", tkn) - find_stores_in_tokens(stmt.condition, error) - elif isinstance(stmt, SimpleStmt): - find_stores_in_tokens(stmt.contents, res.append) - - node.block.accept(visit) - return res - - -#def analyze_deferred_refs(node: parser.InstDef) -> dict[lexer.Token, str | None]: - #"""Look for PyStackRef_FromPyObjectNew() calls""" - - #def in_frame_push(idx: int) -> bool: - #for tkn in reversed(node.block.tokens[: idx - 1]): - #if tkn.kind in {"SEMI", "LBRACE", "RBRACE"}: - #return False - #if tkn.kind == "IDENTIFIER" and tkn.text == "_PyFrame_PushUnchecked": - #return True - #return False - - #refs: dict[lexer.Token, str | None] = {} - #for idx, tkn in enumerate(node.block.tokens): - #if tkn.kind != "IDENTIFIER" or tkn.text != "PyStackRef_FromPyObjectNew": - #continue - - #if idx == 0 or node.block.tokens[idx - 1].kind != "EQUALS": - #if in_frame_push(idx): - ## PyStackRef_FromPyObjectNew() is called in _PyFrame_PushUnchecked() - #refs[tkn] = None - #continue - #raise analysis_error("Expected '=' before PyStackRef_FromPyObjectNew", tkn) - - #lhs = find_assignment_target(node, idx - 1) - #if len(lhs) == 0: - #raise analysis_error( - #"PyStackRef_FromPyObjectNew() must be assigned to an output", tkn - #) - - #if lhs[0].kind == "TIMES" or any( - #t.kind == "ARROW" or t.kind == "LBRACKET" for t in lhs[1:] - #): - ## Don't handle: *ptr = ..., ptr->field = ..., or ptr[field] = ... - ## Assume that they are visible to the GC. - #refs[tkn] = None - #continue - - #if len(lhs) != 1 or lhs[0].kind != "IDENTIFIER": - #raise analysis_error( - #"PyStackRef_FromPyObjectNew() must be assigned to an output", tkn - #) - - #name = lhs[0].text - #match = ( - #any(var.name == name for var in node.inputs) - #or any(var.name == name for var in node.outputs) - #) - #if not match: - #raise analysis_error( - #f"PyStackRef_FromPyObjectNew() must be assigned to an input or output, not '{name}'", - #tkn, - #) - - #refs[tkn] = name - - #return refs - - -def variable_used(node: parser.CodeDef, name: str) -> bool: - """Determine whether a variable with a given name is used in a node.""" - return any( - token.kind == "IDENTIFIER" and token.text == name for token in node.block.tokens() - ) - - -def oparg_used(node: parser.CodeDef) -> bool: - """Determine whether `oparg` is used in a node.""" - return any( - token.kind == "IDENTIFIER" and token.text == "oparg" for token in node.tokens - ) - - -def tier_variable(node: parser.CodeDef) -> int | None: - """Determine whether a tier variable is used in a node.""" - if isinstance(node, parser.LabelDef): - return None - for token in node.tokens: - if token.kind == "ANNOTATION": - if token.text == "specializing": - return 1 - if re.fullmatch(r"tier\d", token.text): - return int(token.text[-1]) - return None - - -def has_error_with_pop(op: parser.CodeDef) -> bool: - return ( - variable_used(op, "ERROR_IF") - or variable_used(op, "exception_unwind") - ) - - -def has_error_without_pop(op: parser.CodeDef) -> bool: - return ( - variable_used(op, "ERROR_NO_POP") - or variable_used(op, "exception_unwind") - ) - - -NON_ESCAPING_FUNCTIONS = ( - "PyCFunction_GET_FLAGS", - "PyCFunction_GET_FUNCTION", - "PyCFunction_GET_SELF", - "PyCell_GetRef", - "PyCell_New", - "PyCell_SwapTakeRef", - "PyExceptionInstance_Class", - "PyException_GetCause", - "PyException_GetContext", - "PyException_GetTraceback", - "PyFloat_AS_DOUBLE", - "PyFloat_FromDouble", - "PyFunction_GET_CODE", - "PyFunction_GET_GLOBALS", - "PyList_GET_ITEM", - "PyList_GET_SIZE", - "PyList_SET_ITEM", - "PyLong_AsLong", - "PyLong_FromLong", - "PyLong_FromSsize_t", - "PySlice_New", - "PyStackRef_AsPyObjectBorrow", - "PyStackRef_AsPyObjectNew", - "PyStackRef_FromPyObjectNewMortal", - "PyStackRef_AsPyObjectSteal", - "PyStackRef_Borrow", - "PyStackRef_CLEAR", - "PyStackRef_CLOSE_SPECIALIZED", - "PyStackRef_DUP", - "PyStackRef_False", - "PyStackRef_FromPyObjectBorrow", - "PyStackRef_FromPyObjectNew", - "PyStackRef_FromPyObjectSteal", - "PyStackRef_IsExactly", - "PyStackRef_FromPyObjectStealMortal", - "PyStackRef_IsNone", - "PyStackRef_Is", - "PyStackRef_IsHeapSafe", - "PyStackRef_IsTrue", - "PyStackRef_IsFalse", - "PyStackRef_IsNull", - "PyStackRef_MakeHeapSafe", - "PyStackRef_None", - "PyStackRef_RefcountOnObject", - "PyStackRef_TYPE", - "PyStackRef_True", - "PyTuple_GET_ITEM", - "PyTuple_GET_SIZE", - "PyType_HasFeature", - "PyUnicode_Concat", - "PyUnicode_GET_LENGTH", - "PyUnicode_READ_CHAR", - "PyUnicode_IS_COMPACT_ASCII", - "PyUnicode_1BYTE_DATA", - "Py_ARRAY_LENGTH", - "Py_FatalError", - "Py_INCREF", - "Py_IS_TYPE", - "Py_NewRef", - "Py_REFCNT", - "Py_SIZE", - "Py_TYPE", - "Py_UNREACHABLE", - "Py_Unicode_GET_LENGTH", - "_PyCode_CODE", - "_PyDictValues_AddToInsertionOrder", - "_PyErr_Occurred", - "_PyFrame_GetBytecode", - "_PyFrame_GetCode", - "_PyFrame_IsIncomplete", - "_PyFrame_PushUnchecked", - "_PyFrame_SetStackPointer", - "_PyFrame_StackPush", - "_PyFunction_SetVersion", - "_PyGen_GetGeneratorFromFrame", - "gen_try_set_executing", - "_PyInterpreterState_GET", - "_PyList_AppendTakeRef", - "_PyList_ITEMS", - "_PyLong_CompactValue", - "_PyLong_DigitCount", - "_PyLong_IsCompact", - "_PyLong_IsNegative", - "_PyLong_IsNonNegativeCompact", - "_PyLong_IsZero", - "_PyLong_BothAreCompact", - "_PyCompactLong_Add", - "_PyCompactLong_Multiply", - "_PyCompactLong_Subtract", - "_PyManagedDictPointer_IsValues", - "_PyObject_GC_IS_SHARED", - "_PyObject_GC_IS_TRACKED", - "_PyObject_GC_MAY_BE_TRACKED", - "_PyObject_GC_TRACK", - "_PyObject_GetManagedDict", - "_PyObject_InlineValues", - "_PyObject_IsUniquelyReferenced", - "_PyObject_ManagedDictPointer", - "_PyThreadState_HasStackSpace", - "_PyTuple_FromStackRefStealOnSuccess", - "_PyTuple_ITEMS", - "_PyType_HasFeature", - "_PyType_NewManagedObject", - "_PyUnicode_Equal", - "_PyUnicode_JoinArray", - "_Py_CHECK_EMSCRIPTEN_SIGNALS_PERIODICALLY", - "_Py_ID", - "_Py_IsImmortal", - "_Py_IsOwnedByCurrentThread", - "_Py_LeaveRecursiveCallPy", - "_Py_LeaveRecursiveCallTstate", - "_Py_NewRef", - "_Py_SINGLETON", - "_Py_STR", - "_Py_TryIncrefCompare", - "_Py_TryIncrefCompareStackRef", - "_Py_atomic_compare_exchange_uint8", - "_Py_atomic_load_ptr_acquire", - "_Py_atomic_load_uintptr_relaxed", - "_Py_set_eval_breaker_bit", - "advance_backoff_counter", - "assert", - "backoff_counter_triggers", - "initial_temperature_backoff_counter", - "JUMP_TO_LABEL", - "restart_backoff_counter", - "_Py_ReachedRecursionLimit", - "PyStackRef_IsTaggedInt", - "PyStackRef_TagInt", - "PyStackRef_UntagInt", - "PyStackRef_IncrementTaggedIntNoOverflow", - "PyStackRef_IsNullOrInt", - "PyStackRef_IsError", - "PyStackRef_IsValid", - "PyStackRef_Wrap", - "PyStackRef_Unwrap", - "_PyLong_CheckExactAndCompact", - "_PyExecutor_FromExit", - "_PyJit_TryInitializeTracing", - "_Py_unset_eval_breaker_bit", - "_Py_set_eval_breaker_bit", - "trigger_backoff_counter", - "_PyThreadState_PopCStackRefSteal", - "doesnt_escape", - "_Py_GatherStats_GetIter", - "_PyStolenTuple_Free", - "PyObject_GC_UnTrack", -) - - -def check_escaping_calls(instr: parser.CodeDef, escapes: dict[SimpleStmt, EscapingCall]) -> None: - error: lexer.Token | None = None - calls = {e.call for e in escapes.values()} - - def visit(stmt: Stmt) -> None: - nonlocal error - if isinstance(stmt, IfStmt) or isinstance(stmt, WhileStmt): - for tkn in stmt.condition: - if tkn in calls: - error = tkn - elif isinstance(stmt, SimpleStmt): - in_if = 0 - tkn_iter = iter(stmt.contents) - for tkn in tkn_iter: - if tkn.kind == "IDENTIFIER" and tkn.text in ("DEOPT_IF", "ERROR_IF", "EXIT_IF", "HANDLE_PENDING_AND_DEOPT_IF", "AT_END_EXIT_IF"): - in_if = 1 - next(tkn_iter) - elif tkn.kind == "LPAREN": - if in_if: - in_if += 1 - elif tkn.kind == "RPAREN": - if in_if: - in_if -= 1 - elif tkn in calls and in_if: - error = tkn - - - instr.block.accept(visit) - if error is not None: - raise analysis_error(f"Escaping call '{error.text} in condition", error) - -def escaping_call_in_simple_stmt(stmt: SimpleStmt, result: dict[SimpleStmt, EscapingCall]) -> None: - tokens = stmt.contents - for idx, tkn in enumerate(tokens): - try: - next_tkn = tokens[idx+1] - except IndexError: - break - if next_tkn.kind != lexer.LPAREN: - continue - if tkn.kind == lexer.IDENTIFIER: - if tkn.text.upper() == tkn.text: - # simple macro - continue - #if not tkn.text.startswith(("Py", "_Py", "monitor")): - # continue - if tkn.text.startswith(("sym_", "optimize_", "PyJitRef")): - # Optimize functions - continue - if tkn.text.endswith("Check"): - continue - if tkn.text.startswith("Py_Is"): - continue - if tkn.text.endswith("CheckExact"): - continue - if tkn.text in NON_ESCAPING_FUNCTIONS: - continue - elif tkn.kind == "RPAREN": - prev = tokens[idx-1] - if prev.text.endswith("_t") or prev.text == "*" or prev.text == "int": - #cast - continue - elif tkn.kind != "RBRACKET": - continue - if tkn.text in ("PyStackRef_CLOSE", "PyStackRef_XCLOSE"): - if len(tokens) <= idx+2: - raise analysis_error("Unexpected end of file", next_tkn) - kills = tokens[idx+2] - if kills.kind != "IDENTIFIER": - raise analysis_error(f"Expected identifier, got '{kills.text}'", kills) - else: - kills = None - result[stmt] = EscapingCall(stmt, tkn, kills) - - -def find_escaping_api_calls(instr: parser.CodeDef) -> dict[SimpleStmt, EscapingCall]: - result: dict[SimpleStmt, EscapingCall] = {} - - def visit(stmt: Stmt) -> None: - if not isinstance(stmt, SimpleStmt): - return - escaping_call_in_simple_stmt(stmt, result) - - instr.block.accept(visit) - check_escaping_calls(instr, result) - return result - - -EXITS = { - "DISPATCH", - "Py_UNREACHABLE", - "DISPATCH_INLINED", - "DISPATCH_GOTO", -} - - -def always_exits(op: parser.CodeDef) -> bool: - depth = 0 - tkn_iter = iter(op.tokens) - for tkn in tkn_iter: - if tkn.kind == "LBRACE": - depth += 1 - elif tkn.kind == "RBRACE": - depth -= 1 - elif depth > 1: - continue - elif tkn.kind == "GOTO" or tkn.kind == "RETURN": - return True - elif tkn.kind == "KEYWORD": - if tkn.text in EXITS: - return True - elif tkn.kind == "IDENTIFIER": - if tkn.text in EXITS: - return True - if tkn.text == "DEOPT_IF" or tkn.text == "ERROR_IF": - next(tkn_iter) # '(' - t = next(tkn_iter) - if t.text in ("true", "1"): - return True - return False - - -def stack_effect_only_peeks(instr: parser.InstDef) -> bool: - stack_inputs = [s for s in instr.inputs if not isinstance(s, parser.CacheEffect)] - if len(stack_inputs) != len(instr.outputs): - return False - if len(stack_inputs) == 0: - return False - return all( - (s.name == other.name and s.size == other.size) - for s, other in zip(stack_inputs, instr.outputs) - ) - - -def stmt_is_simple_exit(stmt: Stmt) -> bool: - if not isinstance(stmt, SimpleStmt): - return False - tokens = stmt.contents - if len(tokens) < 4: - return False - return ( - tokens[0].text in ("ERROR_IF", "DEOPT_IF", "EXIT_IF", "AT_END_EXIT_IF") - and - tokens[1].text == "(" - and - tokens[2].text in ("true", "1") - and - tokens[3].text == ")" - ) - - -def stmt_list_escapes(stmts: list[Stmt]) -> bool: - if not stmts: - return False - if stmt_is_simple_exit(stmts[-1]): - return False - for stmt in stmts: - if stmt_escapes(stmt): - return True - return False - - -def stmt_escapes(stmt: Stmt) -> bool: - if isinstance(stmt, BlockStmt): - return stmt_list_escapes(stmt.body) - elif isinstance(stmt, SimpleStmt): - for tkn in stmt.contents: - if tkn.text == "DECREF_INPUTS": - return True - d: dict[SimpleStmt, EscapingCall] = {} - escaping_call_in_simple_stmt(stmt, d) - return bool(d) - elif isinstance(stmt, IfStmt): - if stmt.else_body and stmt_escapes(stmt.else_body): - return True - return stmt_escapes(stmt.body) - elif isinstance(stmt, MacroIfStmt): - if stmt.else_body and stmt_list_escapes(stmt.else_body): - return True - return stmt_list_escapes(stmt.body) - elif isinstance(stmt, ForStmt): - return stmt_escapes(stmt.body) - elif isinstance(stmt, WhileStmt): - return stmt_escapes(stmt.body) - else: - assert False, "Unexpected statement type" - -def stmt_has_jump_on_unpredictable_path_body(stmts: list[Stmt] | None, branches_seen: int) -> tuple[bool, int]: - if not stmts: - return False, branches_seen - predict = False - seen = 0 - for st in stmts: - predict_body, seen_body = stmt_has_jump_on_unpredictable_path(st, branches_seen) - predict = predict or predict_body - seen += seen_body - return predict, seen - -def stmt_has_jump_on_unpredictable_path(stmt: Stmt, branches_seen: int) -> tuple[bool, int]: - if isinstance(stmt, BlockStmt): - return stmt_has_jump_on_unpredictable_path_body(stmt.body, branches_seen) - elif isinstance(stmt, SimpleStmt): - for tkn in stmt.contents: - if tkn.text == "JUMPBY": - return True, branches_seen - return False, branches_seen - elif isinstance(stmt, IfStmt): - predict, seen = stmt_has_jump_on_unpredictable_path(stmt.body, branches_seen) - if stmt.else_body: - predict_else, seen_else = stmt_has_jump_on_unpredictable_path(stmt.else_body, branches_seen) - return predict != predict_else, seen + seen_else + 1 - return predict, seen + 1 - elif isinstance(stmt, MacroIfStmt): - predict, seen = stmt_has_jump_on_unpredictable_path_body(stmt.body, branches_seen) - if stmt.else_body: - predict_else, seen_else = stmt_has_jump_on_unpredictable_path_body(stmt.else_body, branches_seen) - return predict != predict_else, seen + seen_else - return predict, seen - elif isinstance(stmt, ForStmt): - unpredictable, branches_seen = stmt_has_jump_on_unpredictable_path(stmt.body, branches_seen) - return unpredictable, branches_seen + 1 - elif isinstance(stmt, WhileStmt): - unpredictable, branches_seen = stmt_has_jump_on_unpredictable_path(stmt.body, branches_seen) - return unpredictable, branches_seen + 1 - else: - assert False, f"Unexpected statement type {stmt}" - - -def compute_properties(op: parser.CodeDef) -> Properties: - escaping_calls = find_escaping_api_calls(op) - has_free = ( - variable_used(op, "PyCell_New") - or variable_used(op, "PyCell_GetRef") - or variable_used(op, "PyCell_SetTakeRef") - or variable_used(op, "PyCell_SwapTakeRef") - ) - deopts_if = variable_used(op, "DEOPT_IF") - exits_if = variable_used(op, "EXIT_IF") - exit_if_at_end = variable_used(op, "AT_END_EXIT_IF") - deopts_periodic = variable_used(op, "HANDLE_PENDING_AND_DEOPT_IF") - exits_and_deopts = sum((deopts_if, exits_if, deopts_periodic)) - if exits_and_deopts > 1: - tkn = op.tokens[0] - raise lexer.make_syntax_error( - "Op cannot contain more than one of EXIT_IF, DEOPT_IF and HANDLE_PENDING_AND_DEOPT_IF", - tkn.filename, - tkn.line, - tkn.column, - op.name, - ) - error_with_pop = has_error_with_pop(op) - error_without_pop = has_error_without_pop(op) - escapes = stmt_escapes(op.block) - pure = False if isinstance(op, parser.LabelDef) else "pure" in op.annotations - no_save_ip = False if isinstance(op, parser.LabelDef) else "no_save_ip" in op.annotations - unpredictable, branches_seen = stmt_has_jump_on_unpredictable_path(op.block, 0) - unpredictable_jump = False if isinstance(op, parser.LabelDef) else (unpredictable and branches_seen > 0) - return Properties( - escaping_calls=escaping_calls, - escapes=escapes, - error_with_pop=error_with_pop, - error_without_pop=error_without_pop, - deopts=deopts_if, - deopts_periodic=deopts_periodic, - side_exit=exits_if, - side_exit_at_end=exit_if_at_end, - oparg=oparg_used(op), - jumps=variable_used(op, "JUMPBY"), - eval_breaker="CHECK_PERIODIC" in op.name, - needs_this=variable_used(op, "this_instr"), - always_exits=always_exits(op), - sync_sp=variable_used(op, "SYNC_SP"), - uses_co_consts=variable_used(op, "FRAME_CO_CONSTS"), - uses_co_names=variable_used(op, "FRAME_CO_NAMES"), - uses_locals=variable_used(op, "GETLOCAL") and not has_free, - uses_opcode=variable_used(op, "opcode"), - has_free=has_free, - pure=pure, - no_save_ip=no_save_ip, - tier=tier_variable(op), - needs_prev=variable_used(op, "prev_instr"), - needs_guard_ip=(isinstance(op, parser.InstDef) - and (unpredictable_jump and "replaced" not in op.annotations)) - or variable_used(op, "LOAD_IP") - or variable_used(op, "DISPATCH_INLINED"), - unpredictable_jump=unpredictable_jump, - records_value=variable_used(op, "RECORD_VALUE") - ) - -def expand(items: list[StackItem], oparg: int) -> list[StackItem]: - # Only replace array item with scalar if no more than one item is an array - index = -1 - for i, item in enumerate(items): - if "oparg" in item.size: - if index >= 0: - return items - index = i - if index < 0: - return items - try: - count = int(eval(items[index].size.replace("oparg", str(oparg)))) - except ValueError: - return items - return items[:index] + [ - StackItem(items[index].name + f"_{i}", "", items[index].peek, items[index].used) for i in range(count) - ] + items[index+1:] - -def scalarize_stack(stack: StackEffect, oparg: int) -> StackEffect: - stack.inputs = expand(stack.inputs, oparg) - stack.outputs = expand(stack.outputs, oparg) - return stack - -def make_uop( - name: str, - op: parser.InstDef, - inputs: list[parser.InputEffect], - uops: dict[str, Uop], -) -> Uop: - result = Uop( - name=name, - context=op.context, - annotations=op.annotations, - stack=analyze_stack(op), - caches=analyze_caches(inputs), - local_stores=find_variable_stores(op), - body=op.block, - properties=compute_properties(op), - ) - for anno in op.annotations: - if anno.startswith("replicate"): - text = anno[10:-1] - start, stop = text.split(":") - result.replicated = range(int(start), int(stop)) - break - else: - return result - for oparg in result.replicated: - name_x = name + "_" + str(oparg) - properties = compute_properties(op) - properties.oparg = False - stack = analyze_stack(op) - if not variable_used(op, "oparg"): - stack = scalarize_stack(stack, oparg) - else: - properties.const_oparg = oparg - rep = Uop( - name=name_x, - context=op.context, - annotations=op.annotations, - stack=stack, - caches=analyze_caches(inputs), - local_stores=find_variable_stores(op), - body=op.block, - properties=properties, - ) - rep.replicates = result - uops[name_x] = rep - - return result - - -def add_op(op: parser.InstDef, uops: dict[str, Uop]) -> None: - assert op.kind == "op" - if op.name in uops: - if "override" not in op.annotations: - raise override_error( - op.name, op.context, uops[op.name].context, op.tokens[0] - ) - uops[op.name] = make_uop(op.name, op, op.inputs, uops) - - -def add_instruction( - where: lexer.Token, - name: str, - parts: list[Part], - instructions: dict[str, Instruction], -) -> None: - instructions[name] = Instruction(where, name, parts, None) - - -def desugar_inst( - inst: parser.InstDef, instructions: dict[str, Instruction], uops: dict[str, Uop] -) -> None: - assert inst.kind == "inst" - name = inst.name - op_inputs: list[parser.InputEffect] = [] - parts: list[Part] = [] - uop_index = -1 - # Move unused cache entries to the Instruction, removing them from the Uop. - for input in inst.inputs: - if isinstance(input, parser.CacheEffect) and input.name == "unused": - parts.append(Skip(input.size)) - else: - op_inputs.append(input) - if uop_index < 0: - uop_index = len(parts) - # Place holder for the uop. - parts.append(Skip(0)) - uop = make_uop("_" + inst.name, inst, op_inputs, uops) - uop.implicitly_created = True - uops[inst.name] = uop - if uop_index < 0: - parts.append(uop) - else: - parts[uop_index] = uop - add_instruction(inst.first_token, name, parts, instructions) - - -def add_macro( - macro: parser.Macro, instructions: dict[str, Instruction], uops: dict[str, Uop] -) -> None: - parts: list[Part] = [] - first = True - for part in macro.uops: - match part: - case parser.OpName(): - if part.name == "flush": - parts.append(Flush()) - else: - if part.name not in uops: - raise analysis_error( - f"No Uop named {part.name}", macro.tokens[0] - ) - uop = uops[part.name] - if uop.properties.records_value and not first: - raise analysis_error( - f"Recording uop {part.name} must be first in macro", - macro.tokens[0]) - parts.append(uop) - first = False - case parser.CacheEffect(): - parts.append(Skip(part.size)) - case _: - assert False - assert parts - add_instruction(macro.first_token, macro.name, parts, instructions) - - -def add_family( - pfamily: parser.Family, - instructions: dict[str, Instruction], - families: dict[str, Family], -) -> None: - family = Family( - pfamily.name, - pfamily.size, - [instructions[member_name] for member_name in pfamily.members], - ) - for member in family.members: - member.family = family - # The head of the family is an implicit jump target for DEOPTs - instructions[family.name].is_target = True - families[family.name] = family - - -def add_pseudo( - pseudo: parser.Pseudo, - instructions: dict[str, Instruction], - pseudos: dict[str, PseudoInstruction], -) -> None: - pseudos[pseudo.name] = PseudoInstruction( - pseudo.name, - analyze_stack(pseudo), - [instructions[target] for target in pseudo.targets], - pseudo.as_sequence, - pseudo.flags, - ) - - -def add_label( - label: parser.LabelDef, - labels: dict[str, Label], -) -> None: - properties = compute_properties(label) - labels[label.name] = Label(label.name, label.spilled, label.block, properties) - - -def assign_opcodes( - instructions: dict[str, Instruction], - families: dict[str, Family], - pseudos: dict[str, PseudoInstruction], -) -> tuple[dict[str, int], int, int]: - """Assigns opcodes, then returns the opmap, - have_arg and min_instrumented values""" - instmap: dict[str, int] = {} - - # 0 is reserved for cache entries. This helps debugging. - instmap["CACHE"] = 0 - - # 17 is reserved as it is the initial value for the specializing counter. - # This helps catch cases where we attempt to execute a cache. - instmap["RESERVED"] = 17 - - # 128 is RESUME - it is hard coded as such in Tools/build/deepfreeze.py - instmap["RESUME"] = 128 - - # This is an historical oddity. - instmap["BINARY_OP_INPLACE_ADD_UNICODE"] = 3 - - instmap["INSTRUMENTED_LINE"] = 253 - instmap["ENTER_EXECUTOR"] = 254 - instmap["TRACE_RECORD"] = 255 - - instrumented = [name for name in instructions if name.startswith("INSTRUMENTED")] - - specialized: set[str] = set() - no_arg: list[str] = [] - has_arg: list[str] = [] - - for family in families.values(): - specialized.update(inst.name for inst in family.members) - - for inst in instructions.values(): - name = inst.name - if name in specialized: - continue - if name in instrumented: - continue - if inst.properties.oparg: - has_arg.append(name) - else: - no_arg.append(name) - - # Specialized ops appear in their own section - # Instrumented opcodes are at the end of the valid range - min_internal = instmap["RESUME"] + 1 - min_instrumented = 254 - len(instrumented) - assert min_internal + len(specialized) < min_instrumented - - next_opcode = 1 - - def add_instruction(name: str) -> None: - nonlocal next_opcode - if name in instmap: - return # Pre-defined name - while next_opcode in instmap.values(): - next_opcode += 1 - instmap[name] = next_opcode - next_opcode += 1 - - for name in sorted(no_arg): - add_instruction(name) - for name in sorted(has_arg): - add_instruction(name) - # For compatibility - next_opcode = min_internal - for name in sorted(specialized): - add_instruction(name) - next_opcode = min_instrumented - for name in instrumented: - add_instruction(name) - - for name in instructions: - instructions[name].opcode = instmap[name] - - for op, name in enumerate(sorted(pseudos), 256): - instmap[name] = op - pseudos[name].opcode = op - - return instmap, len(no_arg), min_instrumented - - -def get_instruction_size_for_uop(instructions: dict[str, Instruction], uop: Uop) -> int | None: - """Return the size of the instruction that contains the given uop or - `None` if the uop does not contains the `INSTRUCTION_SIZE` macro. - - If there is more than one instruction that contains the uop, - ensure that they all have the same size. - """ - for tkn in uop.body.tokens(): - if tkn.text == "INSTRUCTION_SIZE": - break - else: - return None - - size = None - for inst in instructions.values(): - if uop in inst.parts: - if size is None: - size = inst.size - if size != inst.size: - raise analysis_error( - "All instructions containing a uop with the `INSTRUCTION_SIZE` macro " - f"must have the same size: {size} != {inst.size}", - tkn - ) - if size is None: - raise analysis_error(f"No instruction containing the uop '{uop.name}' was found", tkn) - return size - - -def analyze_forest(forest: list[parser.AstNode]) -> Analysis: - instructions: dict[str, Instruction] = {} - uops: dict[str, Uop] = {} - families: dict[str, Family] = {} - pseudos: dict[str, PseudoInstruction] = {} - labels: dict[str, Label] = {} - for node in forest: - match node: - case parser.InstDef(name): - if node.kind == "inst": - desugar_inst(node, instructions, uops) - else: - assert node.kind == "op" - add_op(node, uops) - case parser.Macro(): - pass - case parser.Family(): - pass - case parser.Pseudo(): - pass - case parser.LabelDef(): - pass - case _: - assert False - for node in forest: - if isinstance(node, parser.Macro): - add_macro(node, instructions, uops) - for node in forest: - match node: - case parser.Family(): - add_family(node, instructions, families) - case parser.Pseudo(): - add_pseudo(node, instructions, pseudos) - case parser.LabelDef(): - add_label(node, labels) - case _: - pass - for uop in uops.values(): - uop.instruction_size = get_instruction_size_for_uop(instructions, uop) - # Special case BINARY_OP_INPLACE_ADD_UNICODE - # BINARY_OP_INPLACE_ADD_UNICODE is not a normal family member, - # as it is the wrong size, but we need it to maintain an - # historical optimization. - if "BINARY_OP_INPLACE_ADD_UNICODE" in instructions: - inst = instructions["BINARY_OP_INPLACE_ADD_UNICODE"] - inst.family = families["BINARY_OP"] - families["BINARY_OP"].members.append(inst) - opmap, first_arg, min_instrumented = assign_opcodes(instructions, families, pseudos) - return Analysis( - instructions, uops, families, pseudos, labels, opmap, first_arg, min_instrumented - ) - - -#Simple heuristic for size to avoid too much stencil duplication -def is_large(uop: Uop) -> bool: - return len(list(uop.body.tokens())) > 120 - - -def get_uop_cache_depths(uop: Uop) -> Iterator[tuple[int, int, int]]: - if uop.name == "_SPILL_OR_RELOAD": - for inputs in range(MAX_CACHED_REGISTER+1): - for outputs in range(MAX_CACHED_REGISTER+1): - if inputs != outputs: - yield inputs, outputs, inputs - return - if uop.name in ("_DEOPT", "_HANDLE_PENDING_AND_DEOPT", "_EXIT_TRACE", "_DYNAMIC_EXIT"): - for i in range(MAX_CACHED_REGISTER+1): - yield i, 0, 0 - return - if uop.name in ("_START_EXECUTOR", "_JUMP_TO_TOP", "_COLD_EXIT"): - yield 0, 0, 0 - return - if uop.name == "_ERROR_POP_N": - yield 0, 0, 0 - return - ideal_inputs = 0 - has_array = False - for item in reversed(uop.stack.inputs): - if item.size: - has_array = True - break - ideal_inputs += 1 - ideal_outputs = 0 - for item in reversed(uop.stack.outputs): - if item.size: - has_array = True - break - ideal_outputs += 1 - if ideal_inputs > MAX_CACHED_REGISTER: - ideal_inputs = MAX_CACHED_REGISTER - if ideal_outputs > MAX_CACHED_REGISTER: - ideal_outputs = MAX_CACHED_REGISTER - at_end = uop.properties.sync_sp or uop.properties.side_exit_at_end - exit_depth = ideal_outputs if at_end else ideal_inputs - if uop.properties.escapes or uop.properties.sync_sp or has_array or is_large(uop): - yield ideal_inputs, ideal_outputs, exit_depth - return - for inputs in range(MAX_CACHED_REGISTER + 1): - outputs = ideal_outputs - ideal_inputs + inputs - if outputs < ideal_outputs: - outputs = ideal_outputs - elif outputs > MAX_CACHED_REGISTER: - continue - yield inputs, outputs, outputs if at_end else inputs - - -def analyze_files(filenames: list[str]) -> Analysis: - return analyze_forest(parser.parse_files(filenames)) - - -def dump_analysis(analysis: Analysis) -> None: - print("Uops:") - for u in analysis.uops.values(): - u.dump(" ") - print("Instructions:") - for i in analysis.instructions.values(): - i.dump(" ") - print("Families:") - for f in analysis.families.values(): - f.dump(" ") - print("Pseudos:") - for p in analysis.pseudos.values(): - p.dump(" ") - - -if __name__ == "__main__": - import sys - - if len(sys.argv) < 2: - print("No input") - else: - filenames = sys.argv[1:] - dump_analysis(analyze_files(filenames)) +from dataclasses import dataclass +import itertools +import lexer +import parser +import re +from typing import Optional, Callable, Iterator + +from parser import Stmt, SimpleStmt, BlockStmt, IfStmt, WhileStmt, ForStmt, MacroIfStmt + +MAX_CACHED_REGISTER = 3 + +@dataclass +class EscapingCall: + stmt: SimpleStmt + call: lexer.Token + kills: lexer.Token | None + +@dataclass +class Properties: + escaping_calls: dict[SimpleStmt, EscapingCall] + escapes: bool + error_with_pop: bool + error_without_pop: bool + deopts: bool + deopts_periodic: bool + oparg: bool + jumps: bool + eval_breaker: bool + needs_this: bool + always_exits: bool + sync_sp: bool + uses_co_consts: bool + uses_co_names: bool + uses_locals: bool + has_free: bool + side_exit: bool + side_exit_at_end: bool + pure: bool + uses_opcode: bool + needs_guard_ip: bool + unpredictable_jump: bool + records_value: bool + tier: int | None = None + const_oparg: int = -1 + needs_prev: bool = False + no_save_ip: bool = False + + def dump(self, indent: str) -> None: + simple_properties = self.__dict__.copy() + del simple_properties["escaping_calls"] + text = "escaping_calls:\n" + for tkns in self.escaping_calls.values(): + text += f"{indent} {tkns}\n" + text += ", ".join([f"{key}: {value}" for (key, value) in simple_properties.items()]) + print(indent, text, sep="") + + @staticmethod + def from_list(properties: list["Properties"]) -> "Properties": + escaping_calls: dict[SimpleStmt, EscapingCall] = {} + for p in properties: + escaping_calls.update(p.escaping_calls) + return Properties( + escaping_calls=escaping_calls, + escapes = any(p.escapes for p in properties), + error_with_pop=any(p.error_with_pop for p in properties), + error_without_pop=any(p.error_without_pop for p in properties), + deopts=any(p.deopts for p in properties), + deopts_periodic=any(p.deopts_periodic for p in properties), + oparg=any(p.oparg for p in properties), + jumps=any(p.jumps for p in properties), + eval_breaker=any(p.eval_breaker for p in properties), + needs_this=any(p.needs_this for p in properties), + always_exits=any(p.always_exits for p in properties), + sync_sp=any(p.sync_sp for p in properties), + uses_co_consts=any(p.uses_co_consts for p in properties), + uses_co_names=any(p.uses_co_names for p in properties), + uses_locals=any(p.uses_locals for p in properties), + uses_opcode=any(p.uses_opcode for p in properties), + has_free=any(p.has_free for p in properties), + side_exit=any(p.side_exit for p in properties), + side_exit_at_end=any(p.side_exit_at_end for p in properties), + pure=all(p.pure for p in properties), + needs_prev=any(p.needs_prev for p in properties), + no_save_ip=all(p.no_save_ip for p in properties), + needs_guard_ip=any(p.needs_guard_ip for p in properties), + unpredictable_jump=any(p.unpredictable_jump for p in properties), + records_value=any(p.records_value for p in properties), + ) + + @property + def infallible(self) -> bool: + return not self.error_with_pop and not self.error_without_pop + +SKIP_PROPERTIES = Properties( + escaping_calls={}, + escapes=False, + error_with_pop=False, + error_without_pop=False, + deopts=False, + deopts_periodic=False, + oparg=False, + jumps=False, + eval_breaker=False, + needs_this=False, + always_exits=False, + sync_sp=False, + uses_co_consts=False, + uses_co_names=False, + uses_locals=False, + uses_opcode=False, + has_free=False, + side_exit=False, + side_exit_at_end=False, + pure=True, + no_save_ip=False, + needs_guard_ip=False, + unpredictable_jump=False, + records_value=False, +) + + +@dataclass +class Skip: + "Unused cache entry" + size: int + + @property + def name(self) -> str: + return f"unused/{self.size}" + + @property + def properties(self) -> Properties: + return SKIP_PROPERTIES + + +class Flush: + @property + def properties(self) -> Properties: + return SKIP_PROPERTIES + + @property + def name(self) -> str: + return "flush" + + @property + def size(self) -> int: + return 0 + + + + +@dataclass +class StackItem: + name: str + size: str + peek: bool = False + used: bool = False + + def __str__(self) -> str: + size = f"[{self.size}]" if self.size else "" + return f"{self.name}{size} {self.peek}" + + def is_array(self) -> bool: + return self.size != "" + + def get_size(self) -> str: + return self.size if self.size else "1" + + +@dataclass +class StackEffect: + inputs: list[StackItem] + outputs: list[StackItem] + + def __str__(self) -> str: + return f"({', '.join([str(i) for i in self.inputs])} -- {', '.join([str(i) for i in self.outputs])})" + + +@dataclass +class CacheEntry: + name: str + size: int + + def __str__(self) -> str: + return f"{self.name}/{self.size}" + + +@dataclass +class Uop: + name: str + context: parser.Context | None + annotations: list[str] + stack: StackEffect + caches: list[CacheEntry] + local_stores: list[lexer.Token] + body: BlockStmt + properties: Properties + _size: int = -1 + implicitly_created: bool = False + replicated = range(0) + replicates: "Uop | None" = None + # Size of the instruction(s), only set for uops containing the INSTRUCTION_SIZE macro + instruction_size: int | None = None + + def dump(self, indent: str) -> None: + print( + indent, self.name, ", ".join(self.annotations) if self.annotations else "" + ) + print(indent, self.stack, ", ".join([str(c) for c in self.caches])) + self.properties.dump(" " + indent) + + @property + def size(self) -> int: + if self._size < 0: + self._size = sum(c.size for c in self.caches) + return self._size + + def why_not_viable(self) -> str | None: + if self.name == "_SAVE_RETURN_OFFSET": + return None # Adjusts next_instr, but only in tier 1 code + if "INSTRUMENTED" in self.name: + return "is instrumented" + if "replaced" in self.annotations: + return "is replaced" + if self.name in ("INTERPRETER_EXIT", "JUMP_BACKWARD"): + return "has tier 1 control flow" + if self.properties.needs_this: + return "uses the 'this_instr' variable" + if len([c for c in self.caches if c.name != "unused"]) > 2: + return "has too many cache entries" + if self.properties.error_with_pop and self.properties.error_without_pop: + return "has both popping and not-popping errors" + return None + + def is_viable(self) -> bool: + return self.why_not_viable() is None + + def is_super(self) -> bool: + for tkn in self.body.tokens(): + if tkn.kind == "IDENTIFIER" and tkn.text == "oparg1": + return True + return False + + +class Label: + + def __init__(self, name: str, spilled: bool, body: BlockStmt, properties: Properties): + self.name = name + self.spilled = spilled + self.body = body + self.properties = properties + + size:int = 0 + local_stores: list[lexer.Token] = [] + instruction_size = None + + def __str__(self) -> str: + return f"label({self.name})" + + +Part = Uop | Skip | Flush +CodeSection = Uop | Label + + +@dataclass +class Instruction: + where: lexer.Token + name: str + parts: list[Part] + _properties: Properties | None + is_target: bool = False + family: Optional["Family"] = None + opcode: int = -1 + + @property + def properties(self) -> Properties: + if self._properties is None: + self._properties = self._compute_properties() + return self._properties + + def _compute_properties(self) -> Properties: + return Properties.from_list([part.properties for part in self.parts]) + + def dump(self, indent: str) -> None: + print(indent, self.name, "=", ", ".join([part.name for part in self.parts])) + self.properties.dump(" " + indent) + + @property + def size(self) -> int: + return 1 + sum(part.size for part in self.parts) + + def is_super(self) -> bool: + if len(self.parts) != 1: + return False + uop = self.parts[0] + if isinstance(uop, Uop): + return uop.is_super() + else: + return False + + +@dataclass +class PseudoInstruction: + name: str + stack: StackEffect + targets: list[Instruction] + as_sequence: bool + flags: list[str] + opcode: int = -1 + + def dump(self, indent: str) -> None: + print(indent, self.name, "->", " or ".join([t.name for t in self.targets])) + + @property + def properties(self) -> Properties: + return Properties.from_list([i.properties for i in self.targets]) + + +@dataclass +class Family: + name: str + size: str + members: list[Instruction] + + def dump(self, indent: str) -> None: + print(indent, self.name, "= ", ", ".join([m.name for m in self.members])) + + +@dataclass +class Analysis: + instructions: dict[str, Instruction] + uops: dict[str, Uop] + families: dict[str, Family] + pseudos: dict[str, PseudoInstruction] + labels: dict[str, Label] + opmap: dict[str, int] + have_arg: int + min_instrumented: int + + +def analysis_error(message: str, tkn: lexer.Token) -> SyntaxError: + # To do -- support file and line output + # Construct a SyntaxError instance from message and token + return lexer.make_syntax_error(message, tkn.filename, tkn.line, tkn.column, "") + + +def override_error( + name: str, + context: parser.Context | None, + prev_context: parser.Context | None, + token: lexer.Token, +) -> SyntaxError: + return analysis_error( + f"Duplicate definition of '{name}' @ {context} " + f"previous definition @ {prev_context}", + token, + ) + + +def convert_stack_item( + item: parser.StackEffect, replace_op_arg_1: str | None +) -> StackItem: + return StackItem(item.name, item.size) + +def check_unused(stack: list[StackItem], input_names: dict[str, lexer.Token]) -> None: + "Unused items cannot be on the stack above used, non-peek items" + seen_unused = False + for item in reversed(stack): + if item.name == "unused": + seen_unused = True + elif item.peek: + break + elif seen_unused: + raise analysis_error(f"Cannot have used input '{item.name}' below an unused value on the stack", input_names[item.name]) + + +def analyze_stack( + op: parser.InstDef | parser.Pseudo, replace_op_arg_1: str | None = None +) -> StackEffect: + inputs: list[StackItem] = [ + convert_stack_item(i, replace_op_arg_1) + for i in op.inputs + if isinstance(i, parser.StackEffect) + ] + outputs: list[StackItem] = [ + convert_stack_item(i, replace_op_arg_1) for i in op.outputs + ] + # Mark variables with matching names at the base of the stack as "peek" + modified = False + input_names: dict[str, lexer.Token] = { i.name : i.first_token for i in op.inputs if i.name != "unused" } + for input, output in itertools.zip_longest(inputs, outputs): + if output is None: + pass + elif input is None: + if output.name in input_names: + raise analysis_error( + f"Reuse of variable '{output.name}' at different stack location", + input_names[output.name]) + elif input.name == output.name: + if not modified: + input.peek = output.peek = True + else: + modified = True + if output.name in input_names: + raise analysis_error( + f"Reuse of variable '{output.name}' at different stack location", + input_names[output.name]) + if isinstance(op, parser.InstDef): + output_names = [out.name for out in outputs] + for input in inputs: + if ( + variable_used(op, input.name) + or variable_used(op, "DECREF_INPUTS") + or (not input.peek and input.name in output_names) + ): + input.used = True + for output in outputs: + if variable_used(op, output.name): + output.used = True + check_unused(inputs, input_names) + return StackEffect(inputs, outputs) + + +def analyze_caches(inputs: list[parser.InputEffect]) -> list[CacheEntry]: + caches: list[parser.CacheEffect] = [ + i for i in inputs if isinstance(i, parser.CacheEffect) + ] + if caches: + # Middle entries are allowed to be unused. Check first and last caches. + for index in (0, -1): + cache = caches[index] + if cache.name == "unused": + position = "First" if index == 0 else "Last" + msg = f"{position} cache entry in op is unused. Move to enclosing macro." + raise analysis_error(msg, cache.tokens[0]) + return [CacheEntry(i.name, int(i.size)) for i in caches] + + +def find_variable_stores(node: parser.InstDef) -> list[lexer.Token]: + res: list[lexer.Token] = [] + outnames = { out.name for out in node.outputs } + innames = { out.name for out in node.inputs } + + def find_stores_in_tokens(tokens: list[lexer.Token], callback: Callable[[lexer.Token], None]) -> None: + while tokens and tokens[0].kind == "COMMENT": + tokens = tokens[1:] + if len(tokens) < 4: + return + if tokens[1].kind == "EQUALS": + if tokens[0].kind == "IDENTIFIER": + name = tokens[0].text + if name in outnames or name in innames: + callback(tokens[0]) + #Passing the address of a local is also a definition + for idx, tkn in enumerate(tokens): + if tkn.kind == "AND": + name_tkn = tokens[idx+1] + if name_tkn.text in outnames: + callback(name_tkn) + + def visit(stmt: Stmt) -> None: + if isinstance(stmt, IfStmt): + def error(tkn: lexer.Token) -> None: + raise analysis_error("Cannot define variable in 'if' condition", tkn) + find_stores_in_tokens(stmt.condition, error) + elif isinstance(stmt, SimpleStmt): + find_stores_in_tokens(stmt.contents, res.append) + + node.block.accept(visit) + return res + + +#def analyze_deferred_refs(node: parser.InstDef) -> dict[lexer.Token, str | None]: + #"""Look for PyStackRef_FromPyObjectNew() calls""" + + #def in_frame_push(idx: int) -> bool: + #for tkn in reversed(node.block.tokens[: idx - 1]): + #if tkn.kind in {"SEMI", "LBRACE", "RBRACE"}: + #return False + #if tkn.kind == "IDENTIFIER" and tkn.text == "_PyFrame_PushUnchecked": + #return True + #return False + + #refs: dict[lexer.Token, str | None] = {} + #for idx, tkn in enumerate(node.block.tokens): + #if tkn.kind != "IDENTIFIER" or tkn.text != "PyStackRef_FromPyObjectNew": + #continue + + #if idx == 0 or node.block.tokens[idx - 1].kind != "EQUALS": + #if in_frame_push(idx): + ## PyStackRef_FromPyObjectNew() is called in _PyFrame_PushUnchecked() + #refs[tkn] = None + #continue + #raise analysis_error("Expected '=' before PyStackRef_FromPyObjectNew", tkn) + + #lhs = find_assignment_target(node, idx - 1) + #if len(lhs) == 0: + #raise analysis_error( + #"PyStackRef_FromPyObjectNew() must be assigned to an output", tkn + #) + + #if lhs[0].kind == "TIMES" or any( + #t.kind == "ARROW" or t.kind == "LBRACKET" for t in lhs[1:] + #): + ## Don't handle: *ptr = ..., ptr->field = ..., or ptr[field] = ... + ## Assume that they are visible to the GC. + #refs[tkn] = None + #continue + + #if len(lhs) != 1 or lhs[0].kind != "IDENTIFIER": + #raise analysis_error( + #"PyStackRef_FromPyObjectNew() must be assigned to an output", tkn + #) + + #name = lhs[0].text + #match = ( + #any(var.name == name for var in node.inputs) + #or any(var.name == name for var in node.outputs) + #) + #if not match: + #raise analysis_error( + #f"PyStackRef_FromPyObjectNew() must be assigned to an input or output, not '{name}'", + #tkn, + #) + + #refs[tkn] = name + + #return refs + + +def variable_used(node: parser.CodeDef, name: str) -> bool: + """Determine whether a variable with a given name is used in a node.""" + return any( + token.kind == "IDENTIFIER" and token.text == name for token in node.block.tokens() + ) + + +def oparg_used(node: parser.CodeDef) -> bool: + """Determine whether `oparg` is used in a node.""" + return any( + token.kind == "IDENTIFIER" and token.text == "oparg" for token in node.tokens + ) + + +def tier_variable(node: parser.CodeDef) -> int | None: + """Determine whether a tier variable is used in a node.""" + if isinstance(node, parser.LabelDef): + return None + for token in node.tokens: + if token.kind == "ANNOTATION": + if token.text == "specializing": + return 1 + if re.fullmatch(r"tier\d", token.text): + return int(token.text[-1]) + return None + + +def has_error_with_pop(op: parser.CodeDef) -> bool: + return ( + variable_used(op, "ERROR_IF") + or variable_used(op, "exception_unwind") + ) + + +def has_error_without_pop(op: parser.CodeDef) -> bool: + return ( + variable_used(op, "ERROR_NO_POP") + or variable_used(op, "exception_unwind") + ) + + +NON_ESCAPING_FUNCTIONS = ( + "PyCFunction_GET_FLAGS", + "PyCFunction_GET_FUNCTION", + "PyCFunction_GET_SELF", + "PyCell_GetRef", + "PyCell_New", + "PyCell_SwapTakeRef", + "PyExceptionInstance_Class", + "PyException_GetCause", + "PyException_GetContext", + "PyException_GetTraceback", + "PyFloat_AS_DOUBLE", + "PyFloat_FromDouble", + "PyFunction_GET_CODE", + "PyFunction_GET_GLOBALS", + "PyList_GET_ITEM", + "PyList_GET_SIZE", + "PyList_SET_ITEM", + "PyLong_AsLong", + "PyLong_FromLong", + "PyLong_FromSsize_t", + "PySlice_New", + "PyStackRef_AsPyObjectBorrow", + "PyStackRef_AsPyObjectNew", + "PyStackRef_FromPyObjectNewMortal", + "PyStackRef_AsPyObjectSteal", + "PyStackRef_Borrow", + "PyStackRef_CLEAR", + "PyStackRef_CLOSE_SPECIALIZED", + "PyStackRef_DUP", + "PyStackRef_False", + "PyStackRef_FromPyObjectBorrow", + "PyStackRef_FromPyObjectNew", + "PyStackRef_FromPyObjectSteal", + "PyStackRef_IsExactly", + "PyStackRef_FromPyObjectStealMortal", + "PyStackRef_IsNone", + "PyStackRef_Is", + "PyStackRef_IsHeapSafe", + "PyStackRef_IsTrue", + "PyStackRef_IsFalse", + "PyStackRef_IsNull", + "PyStackRef_MakeHeapSafe", + "PyStackRef_None", + "PyStackRef_RefcountOnObject", + "PyStackRef_TYPE", + "PyStackRef_True", + "PyTuple_GET_ITEM", + "PyTuple_GET_SIZE", + "PyType_HasFeature", + "PyUnicode_Concat", + "PyUnicode_GET_LENGTH", + "PyUnicode_READ_CHAR", + "PyUnicode_IS_COMPACT_ASCII", + "PyUnicode_1BYTE_DATA", + "Py_ARRAY_LENGTH", + "Py_FatalError", + "Py_INCREF", + "Py_IS_TYPE", + "Py_NewRef", + "Py_REFCNT", + "Py_SIZE", + "Py_TYPE", + "Py_UNREACHABLE", + "Py_Unicode_GET_LENGTH", + "_PyCode_CODE", + "_PyDictValues_AddToInsertionOrder", + "_PyErr_Occurred", + "_PyFrame_GetBytecode", + "_PyFrame_GetCode", + "_PyFrame_IsIncomplete", + "_PyFrame_PushUnchecked", + "_PyFrame_SetStackPointer", + "_PyFrame_StackPush", + "_PyFunction_SetVersion", + "_PyGen_GetGeneratorFromFrame", + "gen_try_set_executing", + "_PyInterpreterState_GET", + "_PyList_AppendTakeRef", + "_PyList_ITEMS", + "_PyLong_CompactValue", + "_PyLong_DigitCount", + "_PyLong_IsCompact", + "_PyLong_IsNegative", + "_PyLong_IsNonNegativeCompact", + "_PyLong_IsZero", + "_PyLong_BothAreCompact", + "_PyCompactLong_Add", + "_PyCompactLong_Multiply", + "_PyCompactLong_Subtract", + "_PyManagedDictPointer_IsValues", + "_PyObject_GC_IS_SHARED", + "_PyObject_GC_IS_TRACKED", + "_PyObject_GC_MAY_BE_TRACKED", + "_PyObject_GC_TRACK", + "_PyObject_GetManagedDict", + "_PyObject_InlineValues", + "_PyObject_IsUniquelyReferenced", + "_PyObject_ManagedDictPointer", + "_PyThreadState_HasStackSpace", + "_PyTuple_FromStackRefStealOnSuccess", + "_PyTuple_ITEMS", + "_PyType_HasFeature", + "_PyType_NewManagedObject", + "_PyUnicode_Equal", + "_PyUnicode_JoinArray", + "_Py_CHECK_EMSCRIPTEN_SIGNALS_PERIODICALLY", + "_Py_ID", + "_Py_IsImmortal", + "_Py_IsOwnedByCurrentThread", + "_Py_LeaveRecursiveCallPy", + "_Py_LeaveRecursiveCallTstate", + "_Py_NewRef", + "_Py_SINGLETON", + "_Py_STR", + "_Py_TryIncrefCompare", + "_Py_TryIncrefCompareStackRef", + "_Py_atomic_compare_exchange_uint8", + "_Py_atomic_load_ptr_acquire", + "_Py_atomic_load_uintptr_relaxed", + "_Py_set_eval_breaker_bit", + "advance_backoff_counter", + "assert", + "backoff_counter_triggers", + "initial_temperature_backoff_counter", + "JUMP_TO_LABEL", + "restart_backoff_counter", + "_Py_ReachedRecursionLimit", + "PyStackRef_IsTaggedInt", + "PyStackRef_TagInt", + "PyStackRef_UntagInt", + "PyStackRef_IncrementTaggedIntNoOverflow", + "PyStackRef_IsNullOrInt", + "PyStackRef_IsError", + "PyStackRef_IsValid", + "PyStackRef_Wrap", + "PyStackRef_Unwrap", + "_PyLong_CheckExactAndCompact", + "_PyExecutor_FromExit", + "_PyJit_TryInitializeTracing", + "_Py_unset_eval_breaker_bit", + "_Py_set_eval_breaker_bit", + "trigger_backoff_counter", + "_PyThreadState_PopCStackRefSteal", + "doesnt_escape", + "_Py_GatherStats_GetIter", + "_PyStolenTuple_Free", + "PyObject_GC_UnTrack", +) + + +def check_escaping_calls(instr: parser.CodeDef, escapes: dict[SimpleStmt, EscapingCall]) -> None: + error: lexer.Token | None = None + calls = {e.call for e in escapes.values()} + + def visit(stmt: Stmt) -> None: + nonlocal error + if isinstance(stmt, IfStmt) or isinstance(stmt, WhileStmt): + for tkn in stmt.condition: + if tkn in calls: + error = tkn + elif isinstance(stmt, SimpleStmt): + in_if = 0 + tkn_iter = iter(stmt.contents) + for tkn in tkn_iter: + if tkn.kind == "IDENTIFIER" and tkn.text in ("DEOPT_IF", "ERROR_IF", "EXIT_IF", "HANDLE_PENDING_AND_DEOPT_IF", "AT_END_EXIT_IF"): + in_if = 1 + next(tkn_iter) + elif tkn.kind == "LPAREN": + if in_if: + in_if += 1 + elif tkn.kind == "RPAREN": + if in_if: + in_if -= 1 + elif tkn in calls and in_if: + error = tkn + + + instr.block.accept(visit) + if error is not None: + raise analysis_error(f"Escaping call '{error.text} in condition", error) + +def escaping_call_in_simple_stmt(stmt: SimpleStmt, result: dict[SimpleStmt, EscapingCall]) -> None: + tokens = stmt.contents + for idx, tkn in enumerate(tokens): + try: + next_tkn = tokens[idx+1] + except IndexError: + break + if next_tkn.kind != lexer.LPAREN: + continue + if tkn.kind == lexer.IDENTIFIER: + if tkn.text.upper() == tkn.text: + # simple macro + continue + #if not tkn.text.startswith(("Py", "_Py", "monitor")): + # continue + if tkn.text.startswith(("sym_", "optimize_", "PyJitRef")): + # Optimize functions + continue + if tkn.text.endswith("Check"): + continue + if tkn.text.startswith("Py_Is"): + continue + if tkn.text.endswith("CheckExact"): + continue + if tkn.text in NON_ESCAPING_FUNCTIONS: + continue + elif tkn.kind == "RPAREN": + prev = tokens[idx-1] + if prev.text.endswith("_t") or prev.text == "*" or prev.text == "int": + #cast + continue + elif tkn.kind != "RBRACKET": + continue + if tkn.text in ("PyStackRef_CLOSE", "PyStackRef_XCLOSE"): + if len(tokens) <= idx+2: + raise analysis_error("Unexpected end of file", next_tkn) + kills = tokens[idx+2] + if kills.kind != "IDENTIFIER": + raise analysis_error(f"Expected identifier, got '{kills.text}'", kills) + else: + kills = None + result[stmt] = EscapingCall(stmt, tkn, kills) + + +def find_escaping_api_calls(instr: parser.CodeDef) -> dict[SimpleStmt, EscapingCall]: + result: dict[SimpleStmt, EscapingCall] = {} + + def visit(stmt: Stmt) -> None: + if not isinstance(stmt, SimpleStmt): + return + escaping_call_in_simple_stmt(stmt, result) + + instr.block.accept(visit) + check_escaping_calls(instr, result) + return result + + +EXITS = { + "DISPATCH", + "Py_UNREACHABLE", + "DISPATCH_INLINED", + "DISPATCH_GOTO", +} + + +def always_exits(op: parser.CodeDef) -> bool: + depth = 0 + tkn_iter = iter(op.tokens) + for tkn in tkn_iter: + if tkn.kind == "LBRACE": + depth += 1 + elif tkn.kind == "RBRACE": + depth -= 1 + elif depth > 1: + continue + elif tkn.kind == "GOTO" or tkn.kind == "RETURN": + return True + elif tkn.kind == "KEYWORD": + if tkn.text in EXITS: + return True + elif tkn.kind == "IDENTIFIER": + if tkn.text in EXITS: + return True + if tkn.text == "DEOPT_IF" or tkn.text == "ERROR_IF": + next(tkn_iter) # '(' + t = next(tkn_iter) + if t.text in ("true", "1"): + return True + return False + + +def stack_effect_only_peeks(instr: parser.InstDef) -> bool: + stack_inputs = [s for s in instr.inputs if not isinstance(s, parser.CacheEffect)] + if len(stack_inputs) != len(instr.outputs): + return False + if len(stack_inputs) == 0: + return False + return all( + (s.name == other.name and s.size == other.size) + for s, other in zip(stack_inputs, instr.outputs) + ) + + +def stmt_is_simple_exit(stmt: Stmt) -> bool: + if not isinstance(stmt, SimpleStmt): + return False + tokens = stmt.contents + if len(tokens) < 4: + return False + return ( + tokens[0].text in ("ERROR_IF", "DEOPT_IF", "EXIT_IF", "AT_END_EXIT_IF") + and + tokens[1].text == "(" + and + tokens[2].text in ("true", "1") + and + tokens[3].text == ")" + ) + + +def stmt_list_escapes(stmts: list[Stmt]) -> bool: + if not stmts: + return False + if stmt_is_simple_exit(stmts[-1]): + return False + for stmt in stmts: + if stmt_escapes(stmt): + return True + return False + + +def stmt_escapes(stmt: Stmt) -> bool: + if isinstance(stmt, BlockStmt): + return stmt_list_escapes(stmt.body) + elif isinstance(stmt, SimpleStmt): + for tkn in stmt.contents: + if tkn.text == "DECREF_INPUTS": + return True + d: dict[SimpleStmt, EscapingCall] = {} + escaping_call_in_simple_stmt(stmt, d) + return bool(d) + elif isinstance(stmt, IfStmt): + if stmt.else_body and stmt_escapes(stmt.else_body): + return True + return stmt_escapes(stmt.body) + elif isinstance(stmt, MacroIfStmt): + if stmt.else_body and stmt_list_escapes(stmt.else_body): + return True + return stmt_list_escapes(stmt.body) + elif isinstance(stmt, ForStmt): + return stmt_escapes(stmt.body) + elif isinstance(stmt, WhileStmt): + return stmt_escapes(stmt.body) + else: + assert False, "Unexpected statement type" + +def stmt_has_jump_on_unpredictable_path_body(stmts: list[Stmt] | None, branches_seen: int) -> tuple[bool, int]: + if not stmts: + return False, branches_seen + predict = False + seen = 0 + for st in stmts: + predict_body, seen_body = stmt_has_jump_on_unpredictable_path(st, branches_seen) + predict = predict or predict_body + seen += seen_body + return predict, seen + +def stmt_has_jump_on_unpredictable_path(stmt: Stmt, branches_seen: int) -> tuple[bool, int]: + if isinstance(stmt, BlockStmt): + return stmt_has_jump_on_unpredictable_path_body(stmt.body, branches_seen) + elif isinstance(stmt, SimpleStmt): + for tkn in stmt.contents: + if tkn.text == "JUMPBY": + return True, branches_seen + return False, branches_seen + elif isinstance(stmt, IfStmt): + predict, seen = stmt_has_jump_on_unpredictable_path(stmt.body, branches_seen) + if stmt.else_body: + predict_else, seen_else = stmt_has_jump_on_unpredictable_path(stmt.else_body, branches_seen) + return predict != predict_else, seen + seen_else + 1 + return predict, seen + 1 + elif isinstance(stmt, MacroIfStmt): + predict, seen = stmt_has_jump_on_unpredictable_path_body(stmt.body, branches_seen) + if stmt.else_body: + predict_else, seen_else = stmt_has_jump_on_unpredictable_path_body(stmt.else_body, branches_seen) + return predict != predict_else, seen + seen_else + return predict, seen + elif isinstance(stmt, ForStmt): + unpredictable, branches_seen = stmt_has_jump_on_unpredictable_path(stmt.body, branches_seen) + return unpredictable, branches_seen + 1 + elif isinstance(stmt, WhileStmt): + unpredictable, branches_seen = stmt_has_jump_on_unpredictable_path(stmt.body, branches_seen) + return unpredictable, branches_seen + 1 + else: + assert False, f"Unexpected statement type {stmt}" + + +def compute_properties(op: parser.CodeDef) -> Properties: + escaping_calls = find_escaping_api_calls(op) + has_free = ( + variable_used(op, "PyCell_New") + or variable_used(op, "PyCell_GetRef") + or variable_used(op, "PyCell_SetTakeRef") + or variable_used(op, "PyCell_SwapTakeRef") + ) + deopts_if = variable_used(op, "DEOPT_IF") + exits_if = variable_used(op, "EXIT_IF") + exit_if_at_end = variable_used(op, "AT_END_EXIT_IF") + deopts_periodic = variable_used(op, "HANDLE_PENDING_AND_DEOPT_IF") + exits_and_deopts = sum((deopts_if, exits_if, deopts_periodic)) + if exits_and_deopts > 1: + tkn = op.tokens[0] + raise lexer.make_syntax_error( + "Op cannot contain more than one of EXIT_IF, DEOPT_IF and HANDLE_PENDING_AND_DEOPT_IF", + tkn.filename, + tkn.line, + tkn.column, + op.name, + ) + error_with_pop = has_error_with_pop(op) + error_without_pop = has_error_without_pop(op) + escapes = stmt_escapes(op.block) + pure = False if isinstance(op, parser.LabelDef) else "pure" in op.annotations + no_save_ip = False if isinstance(op, parser.LabelDef) else "no_save_ip" in op.annotations + unpredictable, branches_seen = stmt_has_jump_on_unpredictable_path(op.block, 0) + unpredictable_jump = False if isinstance(op, parser.LabelDef) else (unpredictable and branches_seen > 0) + return Properties( + escaping_calls=escaping_calls, + escapes=escapes, + error_with_pop=error_with_pop, + error_without_pop=error_without_pop, + deopts=deopts_if, + deopts_periodic=deopts_periodic, + side_exit=exits_if, + side_exit_at_end=exit_if_at_end, + oparg=oparg_used(op), + jumps=variable_used(op, "JUMPBY"), + eval_breaker="CHECK_PERIODIC" in op.name, + needs_this=variable_used(op, "this_instr"), + always_exits=always_exits(op), + sync_sp=variable_used(op, "SYNC_SP"), + uses_co_consts=variable_used(op, "FRAME_CO_CONSTS"), + uses_co_names=variable_used(op, "FRAME_CO_NAMES"), + uses_locals=variable_used(op, "GETLOCAL") and not has_free, + uses_opcode=variable_used(op, "opcode"), + has_free=has_free, + pure=pure, + no_save_ip=no_save_ip, + tier=tier_variable(op), + needs_prev=variable_used(op, "prev_instr"), + needs_guard_ip=(isinstance(op, parser.InstDef) + and (unpredictable_jump and "replaced" not in op.annotations)) + or variable_used(op, "LOAD_IP") + or variable_used(op, "DISPATCH_INLINED"), + unpredictable_jump=unpredictable_jump, + records_value=variable_used(op, "RECORD_VALUE") + ) + +def expand(items: list[StackItem], oparg: int) -> list[StackItem]: + # Only replace array item with scalar if no more than one item is an array + index = -1 + for i, item in enumerate(items): + if "oparg" in item.size: + if index >= 0: + return items + index = i + if index < 0: + return items + try: + count = int(eval(items[index].size.replace("oparg", str(oparg)))) + except ValueError: + return items + return items[:index] + [ + StackItem(items[index].name + f"_{i}", "", items[index].peek, items[index].used) for i in range(count) + ] + items[index+1:] + +def scalarize_stack(stack: StackEffect, oparg: int) -> StackEffect: + stack.inputs = expand(stack.inputs, oparg) + stack.outputs = expand(stack.outputs, oparg) + return stack + +def make_uop( + name: str, + op: parser.InstDef, + inputs: list[parser.InputEffect], + uops: dict[str, Uop], +) -> Uop: + result = Uop( + name=name, + context=op.context, + annotations=op.annotations, + stack=analyze_stack(op), + caches=analyze_caches(inputs), + local_stores=find_variable_stores(op), + body=op.block, + properties=compute_properties(op), + ) + for anno in op.annotations: + if anno.startswith("replicate"): + text = anno[10:-1] + start, stop = text.split(":") + result.replicated = range(int(start), int(stop)) + break + else: + return result + for oparg in result.replicated: + name_x = name + "_" + str(oparg) + properties = compute_properties(op) + properties.oparg = False + stack = analyze_stack(op) + if not variable_used(op, "oparg"): + stack = scalarize_stack(stack, oparg) + else: + properties.const_oparg = oparg + rep = Uop( + name=name_x, + context=op.context, + annotations=op.annotations, + stack=stack, + caches=analyze_caches(inputs), + local_stores=find_variable_stores(op), + body=op.block, + properties=properties, + ) + rep.replicates = result + uops[name_x] = rep + + return result + + +def add_op(op: parser.InstDef, uops: dict[str, Uop]) -> None: + assert op.kind == "op" + if op.name in uops: + if "override" not in op.annotations: + raise override_error( + op.name, op.context, uops[op.name].context, op.tokens[0] + ) + uops[op.name] = make_uop(op.name, op, op.inputs, uops) + + +def add_instruction( + where: lexer.Token, + name: str, + parts: list[Part], + instructions: dict[str, Instruction], +) -> None: + instructions[name] = Instruction(where, name, parts, None) + + +def desugar_inst( + inst: parser.InstDef, instructions: dict[str, Instruction], uops: dict[str, Uop] +) -> None: + assert inst.kind == "inst" + name = inst.name + op_inputs: list[parser.InputEffect] = [] + parts: list[Part] = [] + uop_index = -1 + # Move unused cache entries to the Instruction, removing them from the Uop. + for input in inst.inputs: + if isinstance(input, parser.CacheEffect) and input.name == "unused": + parts.append(Skip(input.size)) + else: + op_inputs.append(input) + if uop_index < 0: + uop_index = len(parts) + # Place holder for the uop. + parts.append(Skip(0)) + uop = make_uop("_" + inst.name, inst, op_inputs, uops) + uop.implicitly_created = True + uops[inst.name] = uop + if uop_index < 0: + parts.append(uop) + else: + parts[uop_index] = uop + add_instruction(inst.first_token, name, parts, instructions) + + +def add_macro( + macro: parser.Macro, instructions: dict[str, Instruction], uops: dict[str, Uop] +) -> None: + parts: list[Part] = [] + # Tracks the last "plain" uop (neither specializing nor recording). + # CacheEffect, flush, specializing, and recording uops all leave it + # unchanged, so prev_uop being non-None is sufficient to mean + # "a disqualifying uop has been seen before this recording uop". + prev_uop: Uop | None = None + for part in macro.uops: + match part: + case parser.OpName(): + if part.name == "flush": + parts.append(Flush()) + else: + if part.name not in uops: + raise analysis_error( + f"No Uop named {part.name}", macro.tokens[0] + ) + uop = uops[part.name] + if uop.properties.records_value and prev_uop is not None: + raise analysis_error( + f"Recording uop {part.name} must be first in macro " + f"or immediately follow a specializing uop", + macro.tokens[0]) + parts.append(uop) + # Only plain worker uops set prev_uop; specializing and + # recording uops are excluded so the check above stays simple. + if not uop.properties.records_value and "specializing" not in uop.annotations: + prev_uop = uop + case parser.CacheEffect(): + parts.append(Skip(part.size)) + case _: + assert False + assert parts + add_instruction(macro.first_token, macro.name, parts, instructions) + + + +def add_family( + pfamily: parser.Family, + instructions: dict[str, Instruction], + families: dict[str, Family], +) -> None: + family = Family( + pfamily.name, + pfamily.size, + [instructions[member_name] for member_name in pfamily.members], + ) + for member in family.members: + member.family = family + # The head of the family is an implicit jump target for DEOPTs + instructions[family.name].is_target = True + families[family.name] = family + + +def add_pseudo( + pseudo: parser.Pseudo, + instructions: dict[str, Instruction], + pseudos: dict[str, PseudoInstruction], +) -> None: + pseudos[pseudo.name] = PseudoInstruction( + pseudo.name, + analyze_stack(pseudo), + [instructions[target] for target in pseudo.targets], + pseudo.as_sequence, + pseudo.flags, + ) + + +def add_label( + label: parser.LabelDef, + labels: dict[str, Label], +) -> None: + properties = compute_properties(label) + labels[label.name] = Label(label.name, label.spilled, label.block, properties) + + +def assign_opcodes( + instructions: dict[str, Instruction], + families: dict[str, Family], + pseudos: dict[str, PseudoInstruction], +) -> tuple[dict[str, int], int, int]: + """Assigns opcodes, then returns the opmap, + have_arg and min_instrumented values""" + instmap: dict[str, int] = {} + + # 0 is reserved for cache entries. This helps debugging. + instmap["CACHE"] = 0 + + # 17 is reserved as it is the initial value for the specializing counter. + # This helps catch cases where we attempt to execute a cache. + instmap["RESERVED"] = 17 + + # 128 is RESUME - it is hard coded as such in Tools/build/deepfreeze.py + instmap["RESUME"] = 128 + + # This is an historical oddity. + instmap["BINARY_OP_INPLACE_ADD_UNICODE"] = 3 + + instmap["INSTRUMENTED_LINE"] = 253 + instmap["ENTER_EXECUTOR"] = 254 + instmap["TRACE_RECORD"] = 255 + + instrumented = [name for name in instructions if name.startswith("INSTRUMENTED")] + + specialized: set[str] = set() + no_arg: list[str] = [] + has_arg: list[str] = [] + + for family in families.values(): + specialized.update(inst.name for inst in family.members) + + for inst in instructions.values(): + name = inst.name + if name in specialized: + continue + if name in instrumented: + continue + if inst.properties.oparg: + has_arg.append(name) + else: + no_arg.append(name) + + # Specialized ops appear in their own section + # Instrumented opcodes are at the end of the valid range + min_internal = instmap["RESUME"] + 1 + min_instrumented = 254 - len(instrumented) + assert min_internal + len(specialized) < min_instrumented + + next_opcode = 1 + + def add_instruction(name: str) -> None: + nonlocal next_opcode + if name in instmap: + return # Pre-defined name + while next_opcode in instmap.values(): + next_opcode += 1 + instmap[name] = next_opcode + next_opcode += 1 + + for name in sorted(no_arg): + add_instruction(name) + for name in sorted(has_arg): + add_instruction(name) + # For compatibility + next_opcode = min_internal + for name in sorted(specialized): + add_instruction(name) + next_opcode = min_instrumented + for name in instrumented: + add_instruction(name) + + for name in instructions: + instructions[name].opcode = instmap[name] + + for op, name in enumerate(sorted(pseudos), 256): + instmap[name] = op + pseudos[name].opcode = op + + return instmap, len(no_arg), min_instrumented + + +def get_instruction_size_for_uop(instructions: dict[str, Instruction], uop: Uop) -> int | None: + """Return the size of the instruction that contains the given uop or + `None` if the uop does not contains the `INSTRUCTION_SIZE` macro. + + If there is more than one instruction that contains the uop, + ensure that they all have the same size. + """ + for tkn in uop.body.tokens(): + if tkn.text == "INSTRUCTION_SIZE": + break + else: + return None + + size = None + for inst in instructions.values(): + if uop in inst.parts: + if size is None: + size = inst.size + if size != inst.size: + raise analysis_error( + "All instructions containing a uop with the `INSTRUCTION_SIZE` macro " + f"must have the same size: {size} != {inst.size}", + tkn + ) + if size is None: + raise analysis_error(f"No instruction containing the uop '{uop.name}' was found", tkn) + return size + + +def analyze_forest(forest: list[parser.AstNode]) -> Analysis: + instructions: dict[str, Instruction] = {} + uops: dict[str, Uop] = {} + families: dict[str, Family] = {} + pseudos: dict[str, PseudoInstruction] = {} + labels: dict[str, Label] = {} + for node in forest: + match node: + case parser.InstDef(name): + if node.kind == "inst": + desugar_inst(node, instructions, uops) + else: + assert node.kind == "op" + add_op(node, uops) + case parser.Macro(): + pass + case parser.Family(): + pass + case parser.Pseudo(): + pass + case parser.LabelDef(): + pass + case _: + assert False + for node in forest: + if isinstance(node, parser.Macro): + add_macro(node, instructions, uops) + for node in forest: + match node: + case parser.Family(): + add_family(node, instructions, families) + case parser.Pseudo(): + add_pseudo(node, instructions, pseudos) + case parser.LabelDef(): + add_label(node, labels) + case _: + pass + for uop in uops.values(): + uop.instruction_size = get_instruction_size_for_uop(instructions, uop) + # Special case BINARY_OP_INPLACE_ADD_UNICODE + # BINARY_OP_INPLACE_ADD_UNICODE is not a normal family member, + # as it is the wrong size, but we need it to maintain an + # historical optimization. + if "BINARY_OP_INPLACE_ADD_UNICODE" in instructions: + inst = instructions["BINARY_OP_INPLACE_ADD_UNICODE"] + inst.family = families["BINARY_OP"] + families["BINARY_OP"].members.append(inst) + opmap, first_arg, min_instrumented = assign_opcodes(instructions, families, pseudos) + return Analysis( + instructions, uops, families, pseudos, labels, opmap, first_arg, min_instrumented + ) + + +#Simple heuristic for size to avoid too much stencil duplication +def is_large(uop: Uop) -> bool: + return len(list(uop.body.tokens())) > 120 + + +def get_uop_cache_depths(uop: Uop) -> Iterator[tuple[int, int, int]]: + if uop.name == "_SPILL_OR_RELOAD": + for inputs in range(MAX_CACHED_REGISTER+1): + for outputs in range(MAX_CACHED_REGISTER+1): + if inputs != outputs: + yield inputs, outputs, inputs + return + if uop.name in ("_DEOPT", "_HANDLE_PENDING_AND_DEOPT", "_EXIT_TRACE", "_DYNAMIC_EXIT"): + for i in range(MAX_CACHED_REGISTER+1): + yield i, 0, 0 + return + if uop.name in ("_START_EXECUTOR", "_JUMP_TO_TOP", "_COLD_EXIT"): + yield 0, 0, 0 + return + if uop.name == "_ERROR_POP_N": + yield 0, 0, 0 + return + ideal_inputs = 0 + has_array = False + for item in reversed(uop.stack.inputs): + if item.size: + has_array = True + break + ideal_inputs += 1 + ideal_outputs = 0 + for item in reversed(uop.stack.outputs): + if item.size: + has_array = True + break + ideal_outputs += 1 + if ideal_inputs > MAX_CACHED_REGISTER: + ideal_inputs = MAX_CACHED_REGISTER + if ideal_outputs > MAX_CACHED_REGISTER: + ideal_outputs = MAX_CACHED_REGISTER + at_end = uop.properties.sync_sp or uop.properties.side_exit_at_end + exit_depth = ideal_outputs if at_end else ideal_inputs + if uop.properties.escapes or uop.properties.sync_sp or has_array or is_large(uop): + yield ideal_inputs, ideal_outputs, exit_depth + return + for inputs in range(MAX_CACHED_REGISTER + 1): + outputs = ideal_outputs - ideal_inputs + inputs + if outputs < ideal_outputs: + outputs = ideal_outputs + elif outputs > MAX_CACHED_REGISTER: + continue + yield inputs, outputs, outputs if at_end else inputs + + +def analyze_files(filenames: list[str]) -> Analysis: + return analyze_forest(parser.parse_files(filenames)) + + +def dump_analysis(analysis: Analysis) -> None: + print("Uops:") + for u in analysis.uops.values(): + u.dump(" ") + print("Instructions:") + for i in analysis.instructions.values(): + i.dump(" ") + print("Families:") + for f in analysis.families.values(): + f.dump(" ") + print("Pseudos:") + for p in analysis.pseudos.values(): + p.dump(" ") + + +if __name__ == "__main__": + import sys + + if len(sys.argv) < 2: + print("No input") + else: + filenames = sys.argv[1:] + dump_analysis(analyze_files(filenames)) diff --git a/Tools/cases_generator/test_analyzer.py b/Tools/cases_generator/test_analyzer.py new file mode 100644 index 00000000000000..e49ecf416d1cb5 --- /dev/null +++ b/Tools/cases_generator/test_analyzer.py @@ -0,0 +1,142 @@ +"""Tests for analyzer.py — specifically the add_macro() recording-uop placement rules. + +Run with: + cd Tools/cases_generator + python -m pytest test_analyzer.py -v +or: + python test_analyzer.py +""" + +import sys +import os +import unittest +from typing import Any + +# The cases_generator directory is not on sys.path when invoked from the repo +# root, so add it explicitly. +sys.path.insert(0, os.path.dirname(__file__)) + +import parsing +from analyzer import analyze_forest + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _parse(src: str) -> list[parsing.AstNode]: + """Parse a raw DSL string (no BEGIN/END markers needed) into an AST forest.""" + psr = parsing.Parser(src, filename="") + nodes: list[parsing.AstNode] = [] + while node := psr.definition(): + nodes.append(node) # type: ignore[arg-type] + return nodes + + +def _analyze(src: str) -> Any: + """Parse *src* and run analyze_forest(); return the Analysis object.""" + return analyze_forest(_parse(src)) + + +# --------------------------------------------------------------------------- +# Shared DSL fragments +# --------------------------------------------------------------------------- + +# A minimal specializing op (tier == 1 because of the "specializing" annotation). +_SPECIALIZE_OP = """\ +specializing op(_SPECIALIZE_DUMMY, (counter/1, value -- value)) { +} +""" + +# A minimal recording op: uses RECORD_VALUE → records_value == True. +_RECORD_OP = """\ +op(_RECORD_DUMMY, (value -- value)) { + RECORD_VALUE(PyStackRef_AsPyObjectBorrow(value)); +} +""" + +# A plain (non-specializing, non-recording) worker op. +_WORKER_OP = """\ +op(_WORKER_DUMMY, (value -- res)) { + res = value; +} +""" + + +# --------------------------------------------------------------------------- +# Test class +# --------------------------------------------------------------------------- + +class TestAnalyzer(unittest.TestCase): + + def test_recording_uop_position(self) -> None: + """Recording uops must be first, or immediately follow a specializing uop. + + Case 1 — VALID: recording uop directly after specializing uop. + Case 2 — VALID: recording uop after specializing uop with a cache effect + (unused/1) between them; cache effects are transparent. + Case 3 — INVALID: recording uop after a plain (non-specializing) worker uop. + """ + + # ------------------------------------------------------------------ + # Case 1: _SPECIALIZE_DUMMY + _RECORD_DUMMY (no cache between them) + # ------------------------------------------------------------------ + src_valid_direct = ( + _SPECIALIZE_OP + + _RECORD_OP + + _WORKER_OP + + "macro(VALID_DIRECT) = _SPECIALIZE_DUMMY + _RECORD_DUMMY + _WORKER_DUMMY;\n" + ) + # Must not raise — the recording uop follows the specializing uop directly. + try: + _analyze(src_valid_direct) + except SyntaxError as exc: + self.fail( + f"Case 1 (valid: recording after specializing) raised unexpectedly: {exc}" + ) + + # ------------------------------------------------------------------ + # Case 2: _SPECIALIZE_DUMMY + unused/1 + _RECORD_DUMMY + # A CacheEffect between them must be transparent. + # ------------------------------------------------------------------ + src_valid_with_cache = ( + _SPECIALIZE_OP + + _RECORD_OP + + _WORKER_OP + + "macro(VALID_CACHE) = _SPECIALIZE_DUMMY + unused/1 + _RECORD_DUMMY + _WORKER_DUMMY;\n" + ) + try: + _analyze(src_valid_with_cache) + except SyntaxError as exc: + self.fail( + f"Case 2 (valid: recording after specializing + cache) raised unexpectedly: {exc}" + ) + + # ------------------------------------------------------------------ + # Case 3: _WORKER_DUMMY + _RECORD_DUMMY + # A recording uop after a non-specializing uop must be rejected. + # ------------------------------------------------------------------ + src_invalid = ( + _SPECIALIZE_OP + + _RECORD_OP + + _WORKER_OP + + "macro(INVALID) = _WORKER_DUMMY + _RECORD_DUMMY;\n" + ) + with self.assertRaises(SyntaxError) as ctx: + _analyze(src_invalid) + + # Confirm the error message is the one we emit, not some unrelated error. + self.assertIn( + "Recording uop", + str(ctx.exception), + msg="Case 3: SyntaxError message should mention 'Recording uop'", + ) + self.assertIn( + "_RECORD_DUMMY", + str(ctx.exception), + msg="Case 3: SyntaxError message should name the offending uop", + ) + + +if __name__ == "__main__": + unittest.main()