gh-98831: Support conditional effects; use for LOAD_ATTR (#101333)

This commit is contained in:
Guido van Rossum 2023-01-29 17:28:39 -08:00 committed by GitHub
parent c4170c36b0
commit f5a3d91b6c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 194 additions and 120 deletions

View File

@ -51,7 +51,7 @@
// Dummy variables for stack effects. // Dummy variables for stack effects.
static PyObject *value, *value1, *value2, *left, *right, *res, *sum, *prod, *sub; static PyObject *value, *value1, *value2, *left, *right, *res, *sum, *prod, *sub;
static PyObject *container, *start, *stop, *v, *lhs, *rhs; static PyObject *container, *start, *stop, *v, *lhs, *rhs, *res2;
static PyObject *list, *tuple, *dict, *owner, *set, *str, *tup, *map, *keys; static PyObject *list, *tuple, *dict, *owner, *set, *str, *tup, *map, *keys;
static PyObject *exit_func, *lasti, *val, *retval, *obj, *iter; static PyObject *exit_func, *lasti, *val, *retval, *obj, *iter;
static PyObject *aiter, *awaitable, *iterable, *w, *exc_value, *bc; static PyObject *aiter, *awaitable, *iterable, *w, *exc_value, *bc;
@ -1438,13 +1438,11 @@ dummy_func(
PREDICT(JUMP_BACKWARD); PREDICT(JUMP_BACKWARD);
} }
// error: LOAD_ATTR has irregular stack effect inst(LOAD_ATTR, (unused/9, owner -- res2 if (oparg & 1), res)) {
inst(LOAD_ATTR) {
#if ENABLE_SPECIALIZATION #if ENABLE_SPECIALIZATION
_PyAttrCache *cache = (_PyAttrCache *)next_instr; _PyAttrCache *cache = (_PyAttrCache *)next_instr;
if (ADAPTIVE_COUNTER_IS_ZERO(cache->counter)) { if (ADAPTIVE_COUNTER_IS_ZERO(cache->counter)) {
assert(cframe.use_tracing == 0); assert(cframe.use_tracing == 0);
PyObject *owner = TOP();
PyObject *name = GETITEM(names, oparg>>1); PyObject *name = GETITEM(names, oparg>>1);
next_instr--; next_instr--;
_Py_Specialize_LoadAttr(owner, next_instr, name); _Py_Specialize_LoadAttr(owner, next_instr, name);
@ -1454,26 +1452,18 @@ dummy_func(
DECREMENT_ADAPTIVE_COUNTER(cache->counter); DECREMENT_ADAPTIVE_COUNTER(cache->counter);
#endif /* ENABLE_SPECIALIZATION */ #endif /* ENABLE_SPECIALIZATION */
PyObject *name = GETITEM(names, oparg >> 1); PyObject *name = GETITEM(names, oparg >> 1);
PyObject *owner = TOP();
if (oparg & 1) { if (oparg & 1) {
/* Designed to work in tandem with CALL. */ /* Designed to work in tandem with CALL, pushes two values. */
PyObject* meth = NULL; PyObject* meth = NULL;
if (_PyObject_GetMethod(owner, name, &meth)) {
int meth_found = _PyObject_GetMethod(owner, name, &meth);
if (meth == NULL) {
/* Most likely attribute wasn't found. */
goto error;
}
if (meth_found) {
/* We can bypass temporary bound method object. /* We can bypass temporary bound method object.
meth is unbound method and obj is self. meth is unbound method and obj is self.
meth | self | arg1 | ... | argN meth | self | arg1 | ... | argN
*/ */
SET_TOP(meth); assert(meth != NULL); // No errors on this branch
PUSH(owner); // self res2 = meth;
res = owner; // Transfer ownership
} }
else { else {
/* meth is not an unbound method (but a regular attr, or /* meth is not an unbound method (but a regular attr, or
@ -1483,20 +1473,18 @@ dummy_func(
NULL | meth | arg1 | ... | argN NULL | meth | arg1 | ... | argN
*/ */
SET_TOP(NULL);
Py_DECREF(owner); Py_DECREF(owner);
PUSH(meth); ERROR_IF(meth == NULL, error);
res2 = NULL;
res = meth;
} }
} }
else { else {
PyObject *res = PyObject_GetAttr(owner, name); /* Classic, pushes one value. */
if (res == NULL) { res = PyObject_GetAttr(owner, name);
goto error;
}
Py_DECREF(owner); Py_DECREF(owner);
SET_TOP(res); ERROR_IF(res == NULL, error);
} }
JUMPBY(INLINE_CACHE_ENTRIES_LOAD_ATTR);
} }
// error: LOAD_ATTR has irregular stack effect // error: LOAD_ATTR has irregular stack effect

View File

@ -1745,11 +1745,13 @@
TARGET(LOAD_ATTR) { TARGET(LOAD_ATTR) {
PREDICTED(LOAD_ATTR); PREDICTED(LOAD_ATTR);
PyObject *owner = PEEK(1);
PyObject *res2 = NULL;
PyObject *res;
#if ENABLE_SPECIALIZATION #if ENABLE_SPECIALIZATION
_PyAttrCache *cache = (_PyAttrCache *)next_instr; _PyAttrCache *cache = (_PyAttrCache *)next_instr;
if (ADAPTIVE_COUNTER_IS_ZERO(cache->counter)) { if (ADAPTIVE_COUNTER_IS_ZERO(cache->counter)) {
assert(cframe.use_tracing == 0); assert(cframe.use_tracing == 0);
PyObject *owner = TOP();
PyObject *name = GETITEM(names, oparg>>1); PyObject *name = GETITEM(names, oparg>>1);
next_instr--; next_instr--;
_Py_Specialize_LoadAttr(owner, next_instr, name); _Py_Specialize_LoadAttr(owner, next_instr, name);
@ -1759,26 +1761,18 @@
DECREMENT_ADAPTIVE_COUNTER(cache->counter); DECREMENT_ADAPTIVE_COUNTER(cache->counter);
#endif /* ENABLE_SPECIALIZATION */ #endif /* ENABLE_SPECIALIZATION */
PyObject *name = GETITEM(names, oparg >> 1); PyObject *name = GETITEM(names, oparg >> 1);
PyObject *owner = TOP();
if (oparg & 1) { if (oparg & 1) {
/* Designed to work in tandem with CALL. */ /* Designed to work in tandem with CALL, pushes two values. */
PyObject* meth = NULL; PyObject* meth = NULL;
if (_PyObject_GetMethod(owner, name, &meth)) {
int meth_found = _PyObject_GetMethod(owner, name, &meth);
if (meth == NULL) {
/* Most likely attribute wasn't found. */
goto error;
}
if (meth_found) {
/* We can bypass temporary bound method object. /* We can bypass temporary bound method object.
meth is unbound method and obj is self. meth is unbound method and obj is self.
meth | self | arg1 | ... | argN meth | self | arg1 | ... | argN
*/ */
SET_TOP(meth); assert(meth != NULL); // No errors on this branch
PUSH(owner); // self res2 = meth;
res = owner; // Transfer ownership
} }
else { else {
/* meth is not an unbound method (but a regular attr, or /* meth is not an unbound method (but a regular attr, or
@ -1788,20 +1782,22 @@
NULL | meth | arg1 | ... | argN NULL | meth | arg1 | ... | argN
*/ */
SET_TOP(NULL);
Py_DECREF(owner); Py_DECREF(owner);
PUSH(meth); if (meth == NULL) goto pop_1_error;
res2 = NULL;
res = meth;
} }
} }
else { else {
PyObject *res = PyObject_GetAttr(owner, name); /* Classic, pushes one value. */
if (res == NULL) { res = PyObject_GetAttr(owner, name);
goto error;
}
Py_DECREF(owner); Py_DECREF(owner);
SET_TOP(res); if (res == NULL) goto pop_1_error;
} }
JUMPBY(INLINE_CACHE_ENTRIES_LOAD_ATTR); STACK_GROW(((oparg & 1) ? 1 : 0));
POKE(1, res);
if (oparg & 1) { POKE(1 + ((oparg & 1) ? 1 : 0), res2); }
JUMPBY(9);
DISPATCH(); DISPATCH();
} }

View File

@ -185,7 +185,7 @@ _PyOpcode_num_popped(int opcode, int oparg) {
case MAP_ADD: case MAP_ADD:
return 2; return 2;
case LOAD_ATTR: case LOAD_ATTR:
return -1; return 1;
case LOAD_ATTR_INSTANCE_VALUE: case LOAD_ATTR_INSTANCE_VALUE:
return -1; return -1;
case LOAD_ATTR_MODULE: case LOAD_ATTR_MODULE:
@ -531,7 +531,7 @@ _PyOpcode_num_pushed(int opcode, int oparg) {
case MAP_ADD: case MAP_ADD:
return 0; return 0;
case LOAD_ATTR: case LOAD_ATTR:
return -1; return ((oparg & 1) ? 1 : 0) + 1;
case LOAD_ATTR_INSTANCE_VALUE: case LOAD_ATTR_INSTANCE_VALUE:
return -1; return -1;
case LOAD_ATTR_MODULE: case LOAD_ATTR_MODULE:
@ -694,7 +694,7 @@ _PyOpcode_num_pushed(int opcode, int oparg) {
} }
#endif #endif
enum Direction { DIR_NONE, DIR_READ, DIR_WRITE }; enum Direction { DIR_NONE, DIR_READ, DIR_WRITE };
enum InstructionFormat { INSTR_FMT_IB, INSTR_FMT_IBC, INSTR_FMT_IBC0, INSTR_FMT_IBC000, INSTR_FMT_IBIB, INSTR_FMT_IX, INSTR_FMT_IXC, INSTR_FMT_IXC000 }; enum InstructionFormat { INSTR_FMT_IB, INSTR_FMT_IBC, INSTR_FMT_IBC0, INSTR_FMT_IBC000, INSTR_FMT_IBC00000000, INSTR_FMT_IBIB, INSTR_FMT_IX, INSTR_FMT_IXC, INSTR_FMT_IXC000 };
struct opcode_metadata { struct opcode_metadata {
enum Direction dir_op1; enum Direction dir_op1;
enum Direction dir_op2; enum Direction dir_op2;
@ -791,7 +791,7 @@ struct opcode_metadata {
[DICT_UPDATE] = { DIR_NONE, DIR_NONE, DIR_NONE, true, INSTR_FMT_IB }, [DICT_UPDATE] = { DIR_NONE, DIR_NONE, DIR_NONE, true, INSTR_FMT_IB },
[DICT_MERGE] = { DIR_NONE, DIR_NONE, DIR_NONE, true, INSTR_FMT_IB }, [DICT_MERGE] = { DIR_NONE, DIR_NONE, DIR_NONE, true, INSTR_FMT_IB },
[MAP_ADD] = { DIR_NONE, DIR_NONE, DIR_NONE, true, INSTR_FMT_IB }, [MAP_ADD] = { DIR_NONE, DIR_NONE, DIR_NONE, true, INSTR_FMT_IB },
[LOAD_ATTR] = { DIR_NONE, DIR_NONE, DIR_NONE, true, INSTR_FMT_IB }, [LOAD_ATTR] = { DIR_NONE, DIR_NONE, DIR_NONE, true, INSTR_FMT_IBC00000000 },
[LOAD_ATTR_INSTANCE_VALUE] = { DIR_NONE, DIR_NONE, DIR_NONE, true, INSTR_FMT_IB }, [LOAD_ATTR_INSTANCE_VALUE] = { DIR_NONE, DIR_NONE, DIR_NONE, true, INSTR_FMT_IB },
[LOAD_ATTR_MODULE] = { DIR_NONE, DIR_NONE, DIR_NONE, true, INSTR_FMT_IB }, [LOAD_ATTR_MODULE] = { DIR_NONE, DIR_NONE, DIR_NONE, true, INSTR_FMT_IB },
[LOAD_ATTR_WITH_HINT] = { DIR_NONE, DIR_NONE, DIR_NONE, true, INSTR_FMT_IB }, [LOAD_ATTR_WITH_HINT] = { DIR_NONE, DIR_NONE, DIR_NONE, true, INSTR_FMT_IB },

View File

@ -26,7 +26,9 @@ DEFAULT_METADATA_OUTPUT = os.path.relpath(
) )
BEGIN_MARKER = "// BEGIN BYTECODES //" BEGIN_MARKER = "// BEGIN BYTECODES //"
END_MARKER = "// END BYTECODES //" END_MARKER = "// END BYTECODES //"
RE_PREDICTED = r"^\s*(?:PREDICT\(|GO_TO_INSTRUCTION\(|DEOPT_IF\(.*?,\s*)(\w+)\);\s*(?://.*)?$" RE_PREDICTED = (
r"^\s*(?:PREDICT\(|GO_TO_INSTRUCTION\(|DEOPT_IF\(.*?,\s*)(\w+)\);\s*(?://.*)?$"
)
UNUSED = "unused" UNUSED = "unused"
BITS_PER_CODE_UNIT = 16 BITS_PER_CODE_UNIT = 16
@ -59,7 +61,10 @@ def effect_size(effect: StackEffect) -> tuple[int, str]:
At most one of these will be non-zero / non-empty. At most one of these will be non-zero / non-empty.
""" """
if effect.size: if effect.size:
assert not effect.cond, "Array effects cannot have a condition"
return 0, effect.size return 0, effect.size
elif effect.cond:
return 0, f"{maybe_parenthesize(effect.cond)} ? 1 : 0"
else: else:
return 1, "" return 1, ""
@ -132,7 +137,12 @@ class Formatter:
yield yield
self.emit("}") self.emit("}")
def stack_adjust(self, diff: int, input_effects: list[StackEffect], output_effects: list[StackEffect]): def stack_adjust(
self,
diff: int,
input_effects: list[StackEffect],
output_effects: list[StackEffect],
):
# TODO: Get rid of 'diff' parameter # TODO: Get rid of 'diff' parameter
shrink, isym = list_effect_size(input_effects) shrink, isym = list_effect_size(input_effects)
grow, osym = list_effect_size(output_effects) grow, osym = list_effect_size(output_effects)
@ -150,10 +160,13 @@ class Formatter:
if dst.name == UNUSED: if dst.name == UNUSED:
return return
typ = f"{dst.type}" if dst.type else "PyObject *" typ = f"{dst.type}" if dst.type else "PyObject *"
init = ""
if src: if src:
cast = self.cast(dst, src) cast = self.cast(dst, src)
init = f" = {cast}{src.name}" init = f" = {cast}{src.name}"
elif dst.cond:
init = " = NULL"
else:
init = ""
sepa = "" if typ.endswith("*") else " " sepa = "" if typ.endswith("*") else " "
self.emit(f"{typ}{sepa}{dst.name}{init};") self.emit(f"{typ}{sepa}{dst.name}{init};")
@ -162,7 +175,10 @@ class Formatter:
return return
cast = self.cast(dst, src) cast = self.cast(dst, src)
if m := re.match(r"^PEEK\((.*)\)$", dst.name): if m := re.match(r"^PEEK\((.*)\)$", dst.name):
self.emit(f"POKE({m.group(1)}, {cast}{src.name});") stmt = f"POKE({m.group(1)}, {cast}{src.name});"
if src.cond:
stmt = f"if ({src.cond}) {{ {stmt} }}"
self.emit(stmt)
elif m := re.match(r"^&PEEK\(.*\)$", dst.name): elif m := re.match(r"^&PEEK\(.*\)$", dst.name):
# NOTE: MOVE_ITEMS() does not actually exist. # NOTE: MOVE_ITEMS() does not actually exist.
# The only supported output array forms are: # The only supported output array forms are:
@ -234,7 +250,7 @@ class Instruction:
if self.register: if self.register:
num_regs = len(self.input_effects) + len(self.output_effects) num_regs = len(self.input_effects) + len(self.output_effects)
num_dummies = (num_regs // 2) * 2 + 1 - num_regs num_dummies = (num_regs // 2) * 2 + 1 - num_regs
fmt = "I" + "B"*num_regs + "X"*num_dummies fmt = "I" + "B" * num_regs + "X" * num_dummies
else: else:
if variable_used(inst, "oparg"): if variable_used(inst, "oparg"):
fmt = "IB" fmt = "IB"
@ -276,9 +292,13 @@ class Instruction:
# Write input stack effect variable declarations and initializations # Write input stack effect variable declarations and initializations
ieffects = list(reversed(self.input_effects)) ieffects = list(reversed(self.input_effects))
for i, ieffect in enumerate(ieffects): for i, ieffect in enumerate(ieffects):
isize = string_effect_size(list_effect_size(ieffects[:i+1])) isize = string_effect_size(
list_effect_size([ieff for ieff in ieffects[: i + 1]])
)
if ieffect.size: if ieffect.size:
src = StackEffect(f"&PEEK({isize})", "PyObject **") src = StackEffect(f"&PEEK({isize})", "PyObject **")
elif ieffect.cond:
src = StackEffect(f"({ieffect.cond}) ? PEEK({isize}) : NULL", "")
else: else:
src = StackEffect(f"PEEK({isize})", "") src = StackEffect(f"PEEK({isize})", "")
out.declare(ieffect, src) out.declare(ieffect, src)
@ -304,14 +324,20 @@ class Instruction:
if not self.register: if not self.register:
# Write net stack growth/shrinkage # Write net stack growth/shrinkage
out.stack_adjust(0, self.input_effects, self.output_effects) out.stack_adjust(
0,
[ieff for ieff in self.input_effects],
[oeff for oeff in self.output_effects],
)
# Write output stack effect assignments # Write output stack effect assignments
oeffects = list(reversed(self.output_effects)) oeffects = list(reversed(self.output_effects))
for i, oeffect in enumerate(oeffects): for i, oeffect in enumerate(oeffects):
if oeffect.name in self.unmoved_names: if oeffect.name in self.unmoved_names:
continue continue
osize = string_effect_size(list_effect_size(oeffects[:i+1])) osize = string_effect_size(
list_effect_size([oeff for oeff in oeffects[: i + 1]])
)
if oeffect.size: if oeffect.size:
dst = StackEffect(f"&PEEK({osize})", "PyObject **") dst = StackEffect(f"&PEEK({osize})", "PyObject **")
else: else:
@ -438,6 +464,7 @@ class MacroInstruction(SuperOrMacroInstruction):
parts: list[Component | parser.CacheEffect] parts: list[Component | parser.CacheEffect]
AnyInstruction = Instruction | SuperInstruction | MacroInstruction
INSTR_FMT_PREFIX = "INSTR_FMT_" INSTR_FMT_PREFIX = "INSTR_FMT_"
@ -506,6 +533,7 @@ class Analyzer:
self.supers = {} self.supers = {}
self.macros = {} self.macros = {}
self.families = {} self.families = {}
thing: parser.InstDef | parser.Super | parser.Macro | parser.Family | None
while thing := psr.definition(): while thing := psr.definition():
match thing: match thing:
case parser.InstDef(name=name): case parser.InstDef(name=name):
@ -631,7 +659,9 @@ class Analyzer:
parts.append(part) parts.append(part)
format += instr.instr_fmt format += instr.instr_fmt
final_sp = sp final_sp = sp
return SuperInstruction(super.name, stack, initial_sp, final_sp, format, super, parts) return SuperInstruction(
super.name, stack, initial_sp, final_sp, format, super, parts
)
def analyze_macro(self, macro: parser.Macro) -> MacroInstruction: def analyze_macro(self, macro: parser.Macro) -> MacroInstruction:
components = self.check_macro_components(macro) components = self.check_macro_components(macro)
@ -657,7 +687,9 @@ class Analyzer:
case _: case _:
typing.assert_never(component) typing.assert_never(component)
final_sp = sp final_sp = sp
return MacroInstruction(macro.name, stack, initial_sp, final_sp, format, macro, parts) return MacroInstruction(
macro.name, stack, initial_sp, final_sp, format, macro, parts
)
def analyze_instruction( def analyze_instruction(
self, instr: Instruction, stack: list[StackEffect], sp: int self, instr: Instruction, stack: list[StackEffect], sp: int
@ -710,7 +742,9 @@ class Analyzer:
for thing in components: for thing in components:
match thing: match thing:
case Instruction() as instr: case Instruction() as instr:
if any(eff.size for eff in instr.input_effects + instr.output_effects): if any(
eff.size for eff in instr.input_effects + instr.output_effects
):
# TODO: Eventually this will be needed, at least for macros. # TODO: Eventually this will be needed, at least for macros.
self.error( self.error(
f"Instruction {instr.name!r} has variable-sized stack effect, " f"Instruction {instr.name!r} has variable-sized stack effect, "
@ -736,16 +770,16 @@ class Analyzer:
def get_stack_effect_info( def get_stack_effect_info(
self, thing: parser.InstDef | parser.Super | parser.Macro self, thing: parser.InstDef | parser.Super | parser.Macro
) -> tuple[Instruction|None, str, str]: ) -> tuple[AnyInstruction | None, str, str]:
def effect_str(effects: list[StackEffect]) -> str:
def effect_str(effect: list[StackEffect]) -> str: if getattr(thing, "kind", None) == "legacy":
if getattr(thing, 'kind', None) == 'legacy':
return str(-1) return str(-1)
n_effect, sym_effect = list_effect_size(effect) n_effect, sym_effect = list_effect_size(effects)
if sym_effect: if sym_effect:
return f"{sym_effect} + {n_effect}" if n_effect else sym_effect return f"{sym_effect} + {n_effect}" if n_effect else sym_effect
return str(n_effect) return str(n_effect)
instr: AnyInstruction | None
match thing: match thing:
case parser.InstDef(): case parser.InstDef():
if thing.kind != "op": if thing.kind != "op":
@ -754,34 +788,43 @@ class Analyzer:
pushed = effect_str(instr.output_effects) pushed = effect_str(instr.output_effects)
else: else:
instr = None instr = None
popped = pushed = "", "" popped = ""
pushed = ""
case parser.Super(): case parser.Super():
instr = self.super_instrs[thing.name] instr = self.super_instrs[thing.name]
popped = '+'.join(effect_str(comp.instr.input_effects) for comp in instr.parts) popped = "+".join(
pushed = '+'.join(effect_str(comp.instr.output_effects) for comp in instr.parts) effect_str(comp.instr.input_effects) for comp in instr.parts
)
pushed = "+".join(
effect_str(comp.instr.output_effects) for comp in instr.parts
)
case parser.Macro(): case parser.Macro():
instr = self.macro_instrs[thing.name] instr = self.macro_instrs[thing.name]
parts = [comp for comp in instr.parts if isinstance(comp, Component)] parts = [comp for comp in instr.parts if isinstance(comp, Component)]
popped = '+'.join(effect_str(comp.instr.input_effects) for comp in parts) popped = "+".join(
pushed = '+'.join(effect_str(comp.instr.output_effects) for comp in parts) effect_str(comp.instr.input_effects) for comp in parts
)
pushed = "+".join(
effect_str(comp.instr.output_effects) for comp in parts
)
case _: case _:
typing.assert_never(thing) typing.assert_never(thing)
return instr, popped, pushed return instr, popped, pushed
def write_stack_effect_functions(self) -> None: def write_stack_effect_functions(self) -> None:
popped_data = [] popped_data: list[tuple[AnyInstruction, str]] = []
pushed_data = [] pushed_data: list[tuple[AnyInstruction, str]] = []
for thing in self.everything: for thing in self.everything:
instr, popped, pushed = self.get_stack_effect_info(thing) instr, popped, pushed = self.get_stack_effect_info(thing)
if instr is not None: if instr is not None:
popped_data.append( (instr, popped) ) popped_data.append((instr, popped))
pushed_data.append( (instr, pushed) ) pushed_data.append((instr, pushed))
def write_function(direction: str, data: list[tuple[Instruction, str]]) -> None: def write_function(direction: str, data: list[tuple[AnyInstruction, str]]) -> None:
self.out.emit("\n#ifndef NDEBUG"); self.out.emit("\n#ifndef NDEBUG")
self.out.emit("static int"); self.out.emit("static int")
self.out.emit(f"_PyOpcode_num_{direction}(int opcode, int oparg) {{") self.out.emit(f"_PyOpcode_num_{direction}(int opcode, int oparg) {{")
self.out.emit(" switch(opcode) {"); self.out.emit(" switch(opcode) {")
for instr, effect in data: for instr, effect in data:
self.out.emit(f" case {instr.name}:") self.out.emit(f" case {instr.name}:")
self.out.emit(f" return {effect};") self.out.emit(f" return {effect};")
@ -789,10 +832,10 @@ class Analyzer:
self.out.emit(" Py_UNREACHABLE();") self.out.emit(" Py_UNREACHABLE();")
self.out.emit(" }") self.out.emit(" }")
self.out.emit("}") self.out.emit("}")
self.out.emit("#endif"); self.out.emit("#endif")
write_function('popped', popped_data) write_function("popped", popped_data)
write_function('pushed', pushed_data) write_function("pushed", pushed_data)
def write_metadata(self) -> None: def write_metadata(self) -> None:
"""Write instruction metadata to output file.""" """Write instruction metadata to output file."""
@ -865,21 +908,21 @@ class Analyzer:
directions.extend("DIR_NONE" for _ in range(3)) directions.extend("DIR_NONE" for _ in range(3))
dir_op1, dir_op2, dir_op3 = directions[:3] dir_op1, dir_op2, dir_op3 = directions[:3]
self.out.emit( self.out.emit(
f' [{instr.name}] = {{ {dir_op1}, {dir_op2}, {dir_op3}, true, {INSTR_FMT_PREFIX}{instr.instr_fmt} }},' f" [{instr.name}] = {{ {dir_op1}, {dir_op2}, {dir_op3}, true, {INSTR_FMT_PREFIX}{instr.instr_fmt} }},"
) )
def write_metadata_for_super(self, sup: SuperInstruction) -> None: def write_metadata_for_super(self, sup: SuperInstruction) -> None:
"""Write metadata for a super-instruction.""" """Write metadata for a super-instruction."""
dir_op1 = dir_op2 = dir_op3 = "DIR_NONE" dir_op1 = dir_op2 = dir_op3 = "DIR_NONE"
self.out.emit( self.out.emit(
f' [{sup.name}] = {{ {dir_op1}, {dir_op2}, {dir_op3}, true, {INSTR_FMT_PREFIX}{sup.instr_fmt} }},' f" [{sup.name}] = {{ {dir_op1}, {dir_op2}, {dir_op3}, true, {INSTR_FMT_PREFIX}{sup.instr_fmt} }},"
) )
def write_metadata_for_macro(self, mac: MacroInstruction) -> None: def write_metadata_for_macro(self, mac: MacroInstruction) -> None:
"""Write metadata for a macro-instruction.""" """Write metadata for a macro-instruction."""
dir_op1 = dir_op2 = dir_op3 = "DIR_NONE" dir_op1 = dir_op2 = dir_op3 = "DIR_NONE"
self.out.emit( self.out.emit(
f' [{mac.name}] = {{ {dir_op1}, {dir_op2}, {dir_op3}, true, {INSTR_FMT_PREFIX}{mac.instr_fmt} }},' f" [{mac.name}] = {{ {dir_op1}, {dir_op2}, {dir_op3}, true, {INSTR_FMT_PREFIX}{mac.instr_fmt} }},"
) )
def write_instructions(self) -> None: def write_instructions(self) -> None:
@ -1012,7 +1055,9 @@ def extract_block_text(block: parser.Block) -> tuple[list[str], list[str]]:
# Separate PREDICT(...) macros from end # Separate PREDICT(...) macros from end
predictions: list[str] = [] predictions: list[str] = []
while blocklines and (m := re.match(r"^\s*PREDICT\((\w+)\);\s*(?://.*)?$", blocklines[-1])): while blocklines and (
m := re.match(r"^\s*PREDICT\((\w+)\);\s*(?://.*)?$", blocklines[-1])
):
predictions.insert(0, m.group(1)) predictions.insert(0, m.group(1))
blocklines.pop() blocklines.pop()
@ -1029,13 +1074,22 @@ def always_exits(lines: list[str]) -> bool:
return False return False
line = line[12:] line = line[12:]
return line.startswith( return line.startswith(
("goto ", "return ", "DISPATCH", "GO_TO_", "Py_UNREACHABLE()", "ERROR_IF(true, ") (
"goto ",
"return ",
"DISPATCH",
"GO_TO_",
"Py_UNREACHABLE()",
"ERROR_IF(true, ",
)
) )
def variable_used(node: parser.Node, name: str) -> bool: def variable_used(node: parser.Node, name: str) -> bool:
"""Determine whether a variable with a given name is used in a node.""" """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.tokens) return any(
token.kind == "IDENTIFIER" and token.text == name for token in node.tokens
)
def main(): def main():

View File

@ -48,9 +48,6 @@ class Node:
context = self.context context = self.context
if not context: if not context:
return "" return ""
tokens = context.owner.tokens
begin = context.begin
end = context.end
return lx.to_text(self.tokens, dedent) return lx.to_text(self.tokens, dedent)
@property @property
@ -74,13 +71,13 @@ class Block(Node):
class StackEffect(Node): class StackEffect(Node):
name: str name: str
type: str = "" # Optional `:type` type: str = "" # Optional `:type`
cond: str = "" # Optional `if (cond)`
size: str = "" # Optional `[size]` size: str = "" # Optional `[size]`
# Note: we can have type or size but not both # Note: size cannot be combined with type or cond
# TODO: condition (can be combined with type but not size)
@dataclass @dataclass
class Dimension(Node): class Expression(Node):
size: str size: str
@ -239,31 +236,39 @@ class Parser(PLexer):
@contextual @contextual
def stack_effect(self) -> StackEffect | None: def stack_effect(self) -> StackEffect | None:
# IDENTIFIER # IDENTIFIER [':' IDENTIFIER] ['if' '(' expression ')']
# | IDENTIFIER ':' IDENTIFIER # | IDENTIFIER '[' expression ']'
# | IDENTIFIER '[' dimension ']'
# TODO: Conditions
if tkn := self.expect(lx.IDENTIFIER): if tkn := self.expect(lx.IDENTIFIER):
type_text = ""
if self.expect(lx.COLON): if self.expect(lx.COLON):
typ = self.require(lx.IDENTIFIER) type_text = self.require(lx.IDENTIFIER).text.strip()
return StackEffect(tkn.text, typ.text) cond_text = ""
elif self.expect(lx.LBRACKET): if self.expect(lx.IF):
if not (dim := self.dimension()): self.require(lx.LPAREN)
raise self.make_syntax_error("Expected dimension") if not (cond := self.expression()):
raise self.make_syntax_error("Expected condition")
self.require(lx.RPAREN)
cond_text = cond.text.strip()
size_text = ""
if self.expect(lx.LBRACKET):
if type_text or cond_text:
raise self.make_syntax_error("Unexpected [")
if not (size := self.expression()):
raise self.make_syntax_error("Expected expression")
self.require(lx.RBRACKET) self.require(lx.RBRACKET)
return StackEffect(tkn.text, "PyObject **", dim.text.strip()) type_text = "PyObject **"
else: size_text = size.text.strip()
return StackEffect(tkn.text) return StackEffect(tkn.text, type_text, cond_text, size_text)
@contextual @contextual
def dimension(self) -> Dimension | None: def expression(self) -> Expression | None:
tokens: list[lx.Token] = [] tokens: list[lx.Token] = []
while (tkn := self.peek()) and tkn.kind != lx.RBRACKET: while (tkn := self.peek()) and tkn.kind not in (lx.RBRACKET, lx.RPAREN):
tokens.append(tkn) tokens.append(tkn)
self.next() self.next()
if not tokens: if not tokens:
return None return None
return Dimension(lx.to_text(tokens).strip()) return Expression(lx.to_text(tokens).strip())
@contextual @contextual
def super_def(self) -> Super | None: def super_def(self) -> Super | None:
@ -366,7 +371,7 @@ class Parser(PLexer):
return None return None
@contextual @contextual
def block(self) -> Block: def block(self) -> Block | None:
if self.c_blob(): if self.c_blob():
return Block() return Block()

View File

@ -9,19 +9,19 @@ from parser import StackEffect
def test_effect_sizes(): def test_effect_sizes():
input_effects = [ input_effects = [
x := StackEffect("x", "", ""), x := StackEffect("x", "", "", ""),
y := StackEffect("y", "", "oparg"), y := StackEffect("y", "", "", "oparg"),
z := StackEffect("z", "", "oparg*2"), z := StackEffect("z", "", "", "oparg*2"),
] ]
output_effects = [ output_effects = [
a := StackEffect("a", "", ""), StackEffect("a", "", "", ""),
b := StackEffect("b", "", "oparg*4"), StackEffect("b", "", "", "oparg*4"),
c := StackEffect("c", "", ""), StackEffect("c", "", "", ""),
] ]
other_effects = [ other_effects = [
p := StackEffect("p", "", "oparg<<1"), StackEffect("p", "", "", "oparg<<1"),
q := StackEffect("q", "", ""), StackEffect("q", "", "", ""),
r := StackEffect("r", "", ""), StackEffect("r", "", "", ""),
] ]
assert generate_cases.effect_size(x) == (1, "") assert generate_cases.effect_size(x) == (1, "")
assert generate_cases.effect_size(y) == (0, "oparg") assert generate_cases.effect_size(y) == (0, "oparg")
@ -54,6 +54,12 @@ def run_cases_test(input: str, expected: str):
while lines and lines[0].startswith("// "): while lines and lines[0].startswith("// "):
lines.pop(0) lines.pop(0)
actual = "".join(lines) actual = "".join(lines)
# if actual.rstrip() != expected.rstrip():
# print("Actual:")
# print(actual)
# print("Expected:")
# print(expected)
# print("End")
assert actual.rstrip() == expected.rstrip() assert actual.rstrip() == expected.rstrip()
def test_legacy(): def test_legacy():
@ -475,3 +481,28 @@ def test_register():
} }
""" """
run_cases_test(input, output) run_cases_test(input, output)
def test_cond_effect():
input = """
inst(OP, (aa, input if (oparg & 1), cc -- xx, output if (oparg & 2), zz)) {
output = spam(oparg, input);
}
"""
output = """
TARGET(OP) {
PyObject *cc = PEEK(1);
PyObject *input = (oparg & 1) ? PEEK(1 + ((oparg & 1) ? 1 : 0)) : NULL;
PyObject *aa = PEEK(2 + ((oparg & 1) ? 1 : 0));
PyObject *xx;
PyObject *output = NULL;
PyObject *zz;
output = spam(oparg, input);
STACK_SHRINK(((oparg & 1) ? 1 : 0));
STACK_GROW(((oparg & 2) ? 1 : 0));
POKE(1, zz);
if (oparg & 2) { POKE(1 + ((oparg & 2) ? 1 : 0), output); }
POKE(2 + ((oparg & 2) ? 1 : 0), xx);
DISPATCH();
}
"""
run_cases_test(input, output)