Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 15 commits into from
Jan 30, 2023

Conversation

gvanrossum
Copy link
Member

@gvanrossum gvanrossum commented Jan 26, 2023

We can now write

inst(OP, (foo if (oparg & 1) -- bar if (oparg & 2)) {
    ...
}

which pops foo off the stack if oparg & 1, otherwise setting it to NULL, and pushes bar onto the stack if oparg & 2 (otherwise ignoring it).

This syntax cannot be combined with an array size on the same effect, but it can be combined with a type (untested). In addition, it is incompatible with an array size closer to the stack top, e.g. foo if (oparg&1), bar[oparg>>1] is not supported (I don't think it's used, and generating code for it would be slightly awkward, but could be done if needed).

The implementation switches to using POP() and PUSH() (from PEEK() and POKE()) for those variables that are either conditional or whose position relative to the bottom (!) of the stack depends on a condition. (See the added test for details.)

To demonstrate this, I converted LOAD_ATTR (but not its specializations).

@@ -1438,13 +1438,11 @@ dummy_func(
PREDICT(JUMP_BACKWARD);
}

// error: LOAD_ATTR has irregular stack effect
inst(LOAD_ATTR) {
inst(LOAD_ATTR, (unused/9, owner -- res, res2 if (oparg & 1))) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check out my comment in the generated code about 'res2'.

@@ -1745,11 +1745,13 @@

TARGET(LOAD_ATTR) {
PREDICTED(LOAD_ATTR);
PyObject *owner = PEEK(1);
PyObject *res;
PyObject *res2;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this initialize res2 = NULL? Right now I get compiler warnings (in GitHub) about a possibly uninitialized variable:

‘res2’ may be used uninitialized in this function [-Wmaybe-uninitialized]

presumably from the PUSH() on L1798.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the issue is that the compiler can't see that if (oparg & 1) is the same as if (_cond_res2), so it can't tell that res2 is always initialised when it is used. Not sure what to do about it, add a syntax in the DSL to access the condition from within the bytecode definition?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll just initialize it to NULL. Perhaps some later phase of optimization will recognize that for the dead value it is.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if we rename _cond_res2 to res2_exists and then in the body of the bytecode we do if(res2_exists) /* use res2 */ instead of if(oparg &1) /* use res2 */ then it would make the code clearer. It's also safer, because if you change the args to the instr then oparg&1 might not be correct anymore, but res2_exists will be automatically updated.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, let's try that. I had a commit in the queue that initializes the variable to NULL, which I think would also get rid of the problem (if not I'd be very surprised) but since I didn't push that let's first try it with your idea.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, looks like it doesn't help, the error is still there. So instead let me try this with the (much simpler) "= NULL" solution, which has to work. In fact, I may also drop the flag variable idea, and solve the warning about bool by writing X ? 1 : 0 instead of X != 0.

}
JUMPBY(INLINE_CACHE_ENTRIES_LOAD_ATTR);
POKE(1, res);
if (oparg & 1) { PUSH(res2); }
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here (I think).

- UndefinedLocalError when generating metadata for an 'op'
- Accidental newline inserted in test_generator.py
Comment on lines 494 to 496
POKE(1, xx);
if (oparg & 2) { PUSH(output); }
PUSH(zz);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we stick with PEEK and POKE and here have something like this?

Suggested change
POKE(1, xx);
if (oparg & 2) { PUSH(output); }
PUSH(zz);
POKE((oparg & 2) + 2, xx);
if (oparg & 2) { POKE((oparg & 2) + 1, output); }
POKE(1, zz);

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. This would favor cases where the conditional is at the bottom of the inputs (e.g. a if (...), b, c). But I think it would become quite horrible if you have several conditions, like we have in MAKE_FUNCTION.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there are several conditions then the concatenation of the conditions (and the unconditional 1's) gives you the index.
The generator will be simpler, the generated code will have these expressions for indices, but I think that's probably easier to follow than a mixture of pushes and pokes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll play around with that.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have a look. The generator sure is a lot simpler, I think I can live with the more complicated generated code. What made this solution so nice is that we already had the symbolic size -- the condition just gets added into that. Thanks for the nudge!

Copy link
Member Author

@gvanrossum gvanrossum left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notes to self.

@@ -59,7 +61,10 @@ def effect_size(effect: StackEffect) -> tuple[int, str]:
At most one of these will be non-zero / non-empty.
"""
if effect.size:
assert not effect.cond, "Manual effects should be conditional"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This message is incorrect.

Comment on lines 171 to 172
if "boerenkool" in dst.name or "boerenkool" in src.name:
breakpoint()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug output accidentally left in.

Comment on lines 181 to 185
# elif m := re.match(r"^POP\(\)$", dst.name):
# if src.cond:
# self.emit(f"if ({src.cond}) {{ PUSH({cast}{src.name}); }}")
# else:
# self.emit(f"PUSH({cast}{src.name});")
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delete commented-out block.

@@ -320,6 +354,7 @@ def write(self, out: Formatter) -> None:
else:
# Write output register assignments
for oeffect, reg in zip(self.output_effects, self.output_registers):
src = StackEffect(reg, "")
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't need this.

Comment on lines 1797 to 1799
STACK_GROW(((oparg & 1) != 0));
POKE(1, res);
if (oparg & 1) { POKE(1 + ((oparg & 1) != 0), res2); }
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's unfortunate that all new versions test for oparg & 1 several times, whereas the original version tested that condition exactly once.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assumed the compiler would do this, but we could assign ((oparg& i) != 0) to something. I think it’s only used as a condition in one place, in other places it’s just int arithmetic.

@iritkatriel
Copy link
Member

I don't understand what this warning is about:

comparison of constant ‘0’ with boolean expression is always true [-Wbool-compare]

@gvanrossum
Copy link
Member Author

gvanrossum commented Jan 28, 2023

I don't understand what this warning is about:

comparison of constant ‘0’ with boolean expression is always true [-Wbool-compare]

I assume it's about this line (L1797):

STACK_GROW(((oparg & 1) != 0));

The compiler probably reasons that x >= 0 where x has type bool is always true and is trying to warn you that you've got a trivial condition (the expansion of STACK_GROW(n) uses assert(n >= 0) and presumably assert(x) expands to something like if (!x) ...).

That's silly since it doesn't warn us about the equally trivial 1 >= 0 that appears in the expansion of STACK_GROW(1) -- I'm guessing it's more likely to warn based on types rather than values.

I'm not sure what to do about it.

PS. I'm still looking into your suggestion to assign the condition to a flag variable, it's a bit messy.

@gvanrossum
Copy link
Member Author

Here's the new version. Let me know if you like it enough to go ahead -- if not, I can easily roll it back. Note that I also fixed a bunch of typing issues (which caught one genuine bug).

@iritkatriel
Copy link
Member

Here's the new version. Let me know if you like it enough to go ahead -- if not, I can easily roll it back. Note that I also fixed a bunch of typing issues (which caught one genuine bug).

I think this is easier to read. The warning about the bool comparison is gone too. There's just the one about res being potentially unassigned.

@@ -51,7 +51,7 @@

// Dummy variables for stack effects.
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;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We will also need static int res2_exists, and a bunch of other things, before I land this.

This hopefully avoids a silly compiler warning.
@gvanrossum
Copy link
Member Author

Finally this has no compiler warnings on GitHub. I prefer this version, the changes to generate_cases.py are simpler than the version with flags.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants