Yes, it is fine to use different data structures and call different methods, as long as the branches behave identically from the point of view of an outside observer. So if those methods have visible side-effects, it should be the same side-effects. And the final return value should be the same.
So what you’d require is that the effects in both branches would be the same, right? So not even a difference in task-local state, or other effects?
I mean, that sounds like a very hard requirement that’s extremely difficult to check in practice.
So no rand
s then, that was in OP’s code example earlier. And we’d have to convert the different array types to the same one before a return. This actually seems stricter than the identical behavior suggestion for optionally generated functions.
Using rand
s is fine. To be truly identical both branches should consume the same number of rands form the given RNG, and the result should depend on them in the same way, so that the return value is the same.
However, what I probably should have written is “side effects and return value should behave identically, for whatever meaning of `identical behaviour` suits the user”. Most users will be fine with getting a different random sequence when upgrading Julia, and that may happen as a result of upgrading the Random
package anyways.
That’s not really a tenable definition for how @constant
is intended to be used though. It’s basically the same as “Do what I mean”, which really isn’t something you can build such a primitive on.
I don’t follow. To me, “do what I mean” is a different concept entirely.
Of course the user is not forced to write code that behaves identically in both branches. Only if the code is expected to behave identically under different versions of Julia is this a requirement.
I can also imagine situations were a user would explicitly violate this principle. For example, let’s say that a piece of code slows down when upgrading Julia, because constant propagation no longer works as expected. Then a test script containing the snippet
if @constant x
exit(0)
else
exit(1)
end
might be used in a git bisect
operation to find the offending commit to the Julia codebase.
I appreciate all the comments. Thank you.
As has been stated by others, in my particular use case the two branches would have identical return types and outcomes/side-effects. From a program perspective, it wouldn’t matter which branch was taken. It would only impact performance. This is the intention of the macro - that it would generally be used to improve performance, not change behaviour.
We cannot, and I believe need not, enforce this. Like so many other things in the language (abusing if @generated
, @inbounds
, etc., as already mentioned), it’s up to the user to do the right thing.
This is a nice example of how the macro might be sensibly “abused” to achieve something else useful. In the GitHub feature request I also mentioned using the macro in unit tests to test for regressions in constant propagation inference - this seems to be very unstable at the moment. Indeed, @Per 's git bisect suggestion could be used to find the causes of such regressions.
Perhaps we should call this “optionally propagated constant branches” by this point?
Since this is along the same lines as optionally generated functions with respect to version compilation differences, maybe this should lead to us formally laying out what “same behavior” means in either or both cases. I’m still a little unclear on that here; in most cases you’d want the same return value, but that’s not possible with side effects like rand
. You might not want to say “same algorithm” either, if we’re allocating different intermediate structures and doing different methods for efficiency. But “same return type and side effects” could easily mean too different a behavior; in the case where there are no side effects, this is just “same return type” which can do completely different things.
Though if you’re testing for regressions, you don’t want “same behavior” there, which should also be documented as acceptable for version testing. Though I would rather a function call profiler tell me what constants are propagated, instead of me manually designing @constant
branches to test.
Yeah I think if @constant
should behave in direct analogy to optionally generated functions.
This point also raises the possibility of mimicking non-optionally generated functions. We could have something like
function f(x)
@constant x begin
# Generated code using the static value of x
end
end
which lifts x
to a Core.Const
regardless of whether its runtime value is known ahead of time or not, just like using Val
, but without you needing to write wholly new methods or dispatch chains. So if x
isn’t constant propagated into that block, you need to do runtime codegen
If it’s a direct analogy, then that non-optional @constant
annotation should really be in the header for clarity. function f(x)
really shouldn’t indicate a function that generates code depending on the value of x
. I’m not even comfortable with function f(@constant x)
, and that doesn’t cover intermediate variables in the body. Maybe we do put something in front like @generatedconstant function f(x)
.
This idea does seem a lot stranger than the optional version though. The optional version is more intended to be an optimization for callees, but now this can force a non-constant value to be treated as constant in a branch, it’ll happen even with f(x)
in the global scope. And it seems like the branch has to be dynamically dispatched to in that case, like a inlined function barrier.