Repository navigation
be-cpp: evaluate call operands left to right - #601
Open
notactuallytreyanastasio wants to merge 1 commit into
Open
notactuallytreyanastasio wants to merge 1 commit into
notactuallytreyanastasio wants to merge 1 commit into
Conversation
C++ does not fix the order of a call's arguments, of the object a method
is called on relative to its arguments, or of an operator's operands,
and x86-64 g++ 14 evaluates arguments right to left. be-cpp emitted
every Temper call, operator and interpolation as one C++ call, so a
read after a call saw the state from before it. A probe under x86-64
g++ 14 in Docker, before and after (js prints the "after" column):
bump=1 n=0 bump=1 n=1
args 2 1 args 2 2
plus 32 plus 33
inside 4 4 inside 3 4
before 5 5 before 4 5
receiver 606 receiver 606 ab
captured 6 6 1 0 captured 0 5 6 6
Apple clang happens to go left to right, which is why this did not show
on macOS.
When one operand may act (call user code, a getter, a cast, or support
code that writes or takes a callback) and another operand is anything
but a constant or an unassignable local, the operands are copied to
locals in order inside a lambda called in place, and the call is built
from those locals:
temper::core::Console::log(console_0, temper::core::cat("args ", [&]() -> auto {
auto arg_3 = c->bump();
auto arg_4 = c->get_n();
return show(arg_3, arg_4);
}()));
Statement-level hoisting would read better, but it would move an operand
out from under `&&`, `||` and the conditionals the frontend lowers to, so
it would run code the source says may not run. The lambda keeps each
operand where it was. The locals are copies, not `auto&&`: a reference
to `this_1->n` would see the write that the next operand makes to it.
Everything else is unchanged, which is most code: no golden output in
CppBackendTest changed, and the generated std modules contain none of
these lambdas. Telling the cases apart needs to know what an operand can
do, so:
- CppInlineSupportCode gets a SupportEffect. Support code is Acts unless
marked Pure (arithmetic, comparisons, cat, toString, constructors of
empty collections) or Reads (List::get, String::get, Int::div_wrap and
others that only read or can fail). Two Reads can run in either order;
a failure is a failure whichever comes first.
- A `var` local counts as changeable only if a closure uses it, since
closures are the only way a call can assign it. Module-level vars,
imported names and properties that are not assign-once count too.
- A getter call counts as a plain read when the subject's static type
is a class and the property is backed, since classes cannot be
extended and the generated getter only returns the field.
The grammar gains InOrderExpr and InOrderTemp (Cpp.kt is regenerated),
and the formatter keeps the calling `(` on the line of the lambda's `}`.
A static property's initializer is defined at namespace scope, where a
lambda may not have a capture default (g++: "non-local lambda expression
cannot have a capture-default"), so there the lambda is `[]`:
int32_t S::t = []() -> auto {
auto arg_3 = temper::core::Int::mul(Counter::make()->bump(), 10);
auto arg_4 = Counter::make()->bump();
return temper::core::Int::add(arg_3, arg_4);
}();
CppBackendTest.operandsThatCanAffectEachOtherAreBoundInOrder and
operandsOfAStaticInitializerAreBoundWithoutCaptures fail before this
change and pass after it. The cpp functional tests pass
with clang; regexZeroAdvance fails on origin/main and still does. Not
changed: be-cppv, which has its own translator and was not checked.
Fixes temperlang#539.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Bobby Grayson <bobbbygrayson+github@gmail.com>
Contributor
|
@ShawSumma tagging you on this since you're most familiar with be-cpp. I thought we had at least some coverage of OoO in functional tests. Something like |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
C++ does not fix the order of a call's arguments, of the object a method
is called on relative to its arguments, or of an operator's operands,
and x86-64 g++ 14 evaluates arguments right to left. be-cpp emitted
every Temper call, operator and interpolation as one C++ call, so a
read after a call saw the state from before it. A probe under x86-64
g++ 14 in Docker, before and after (js prints the "after" column):
Apple clang happens to go left to right, which is why this did not show
on macOS.
When one operand may act (call user code, a getter, a cast, or support
code that writes or takes a callback) and another operand is anything
but a constant or an unassignable local, the operands are copied to
locals in order inside a lambda called in place, and the call is built
from those locals:
Statement-level hoisting would read better, but it would move an operand
out from under
&&,||and the conditionals the frontend lowers to, soit would run code the source says may not run. The lambda keeps each
operand where it was. The locals are copies, not
auto&&: a referenceto
this_1->nwould see the write that the next operand makes to it.Everything else is unchanged, which is most code: no golden output in
CppBackendTest changed, and the generated std modules contain none of
these lambdas. Telling the cases apart needs to know what an operand can
do, so:
marked Pure (arithmetic, comparisons, cat, toString, constructors of
empty collections) or Reads (List::get, String::get, Int::div_wrap and
others that only read or can fail). Two Reads can run in either order;
a failure is a failure whichever comes first.
varlocal counts as changeable only if a closure uses it, sinceclosures are the only way a call can assign it. Module-level vars,
imported names and properties that are not assign-once count too.
is a class and the property is backed, since classes cannot be
extended and the generated getter only returns the field.
The grammar gains InOrderExpr and InOrderTemp (Cpp.kt is regenerated),
and the formatter keeps the calling
(on the line of the lambda's}.A static property's initializer is defined at namespace scope, where a
lambda may not have a capture default (g++: "non-local lambda expression
cannot have a capture-default"), so there the lambda is
[]:CppBackendTest.operandsThatCanAffectEachOtherAreBoundInOrder and
operandsOfAStaticInitializerAreBoundWithoutCaptures fail before this
change and pass after it. The cpp functional tests pass
with clang; regexZeroAdvance fails on origin/main and still does. Not
changed: be-cppv, which has its own translator and was not checked.
Fixes #539.
Repro: https://github.com/notactuallytreyanastasio/temper-issue-repros/tree/main/18-cpp-argument-order
🤖 Generated with Claude Code