Given this code...
proc getInt(): int =
raise new Exception
var x:seq[int]
try:
x = @[getInt(), getInt()]
except:
if isNil(x):
echo "nil"
else:
echo len(x)
My assumption was that it would echo "nil" but instead it does the echo len(x) with a result of 2.
The Exceptions section of the manual states: "The statements after the try are executed in sequential order unless an exception e is raised."
So to verify that this is actually the expected order, I tried this test without the exception handling...
var x:seq[int]
proc test(): int =
if isNil(x):
echo "nil"
else:
echo len(x)
result = 123
x = @[test(), test()]
...and indeed both calls to test() show an already allocated x with a len() of 2.
Is it expected that the assignment to x would take place before the function calls? If so, is there a document, or a rule of thumb, that describes order of execution?
Thanks!
Here's a different example.
var x = 0
proc test1(n: int): int =
x += n
return x
proc test2(): int =
x = 1
return x
x = x + test1(test2())
echo x
The echo x produces 4 instead of 2, so it seems that the function calls are made before the value of the left operand of the + is evaluated.
So it becomes this:
x = 2 + 2
instead of this:
x = 0 + 2
I guess it's actually, like this:
x = `+`(x, test1(test2()))
...but I'd still expect the first argument to have its value established before the function calls.
If these are indeed bugs, I can file a report unless someone with more knowledge of the underlying issue wants to do it.
Not sure if they're the same bug, but they would seem to fall into the broader category of execution order.
x = x + test1(test2())
Is just:
let tempA = test2()
let tempB = test1(tempA)
x = x + tempB
Evaluation order is just "innermost first" here, nothing surprising. I think it's overly expensive to generate code that is really bullet proof against all sorts of dirty aliasing issues.
Isn't this exactly why side effects are such a pain?
But:
var x:seq[int]
proc test(): int =
if isNil(x):
echo "nil"
else:
echo len(x)
result = 123
x = @[test(), test()]
Why is it a bug if both calls show that x is allocated?
I find it quite logical. I may be buggy but my "feeling" for this says: Make an x with two slots and assign something into those slots. I can't assign it before I have the slots. I would need to create everything "in secret" before I can assign it otherwise, or I had to hide the change to "x" until it is finished.
If I wanted to change this behavior I could create a constructor proc, which then should evaluate the parameters first (at least if the evaluation is not lazy.. which could be a good thing though :)
So what is really the expected behavior for Nim? And 'can' this be freely chosen or is this something which has to work in a way to prevent a can of bugs nobody expects?
@Araq
And that's fine as long as we have a well-defined set of rules for the evaluation.
Though I'm not sure what you mean by "innermost". I can simplify the example to get rid of the call in the argument position, and get the same result.
var x = 0
proc test1(): int =
x = 2
return x
x = x + test()
The result is again 4.
Same if I call the + explicitly.
x = `+`(x, test1())
Is it that function calls will always have the first evaluation of operands/arguments? If there are function calls for each operand, will it be left-to-right evaluation?
Again, I'm not suggesting a "correct" behavior. Only that it's well defined.
Thanks.
@OderWat
Using Araq's example, it would seem to expand like this:
let tempA = test()
let tempB = test()
x = @[tempA, tempB]
The difference of course is that we have a literal initialization syntax instead of an operator (although the @ is an operator I think). Hopefully there'll be a simple rule to follow.
@pwd Why should that expand like that? Hey have this example for an expression. And your example is what I meant with what needs to be done to "force the order" you expect.
And here something I think which is related:
C++ There is no concept of left-to-right or right-to-left evaluation in C++, which is not to be confused with left-to-right and right-to-left associativity of operators: the expression f1() + f2() + f3() is parsed as (f1() + f2()) + f3() due to left-to-right associativity of operator+, but the function call to f3 may be evaluated first, last, or between f1() or f2() at run time.
@OderWat
I'm not saying it should. I said it would seem to expand that way based on Araq's example above of how the other code expands.
So overall, I'm not trying to make any statement about how the language should be designed. Only trying to understand its design so that I can make valid predictions.
It is the evaluation (not the associativity) I'm trying to comprehend.
This
var x = 0
proc test1(n: int): int =
x += n
return x
proc test2(): int =
x = 1
return x
x = x + test1(test2())
echo x
expands to this (comments mine)
...
// x is initialized to 0, though the initial value really doesn't matter, since
// it's overwritten by test2
x_88003 = ((NI) 0);
nimln(11, "test.nim");
nimln(11, "test.nim");
nimln(11, "test.nim");
nimln(11, "test.nim");
LOC1 = 0;
LOC1 = test2_88026(); // x_88003 set to 1, LOC1 set to 1
LOC2 = 0;
LOC2 = test1_88007(LOC1); // x_88003 set to 2, LOC2 set to 2
TMP134 = addInt(x_88003, LOC2); // TMP134 set to 2 + 2
x_88003 = (NI64)(TMP134); // x_88003 set to 4
...
So it does expand the way that Araq said; I do find it surprising that x = x + test1(test2()) isn't evaluated strictly left-to-right (preserving the original value of x), but it does make more sense (inner-to-outer) if you think of it as a procedure call of the system.+ operator.
I think there may be a little confusion. Just to clarify my points, as Araq stated, this...
x = x + test1(test2())
expands to this...
let tempA = test2()
let tempB = test1(tempA)
x = x + tempB
And so I assumed that this...
x = @[test(), test()]
would expand to this...
let tempA = test()
let tempB = test()
x = @[tempA, tempB]
But it apparently does not. Clearly my expectations are misplaced. Perhaps it's because it's able to assign the result of the test() calls directly to the array, and so it doesn't need the temp variables.
Just want to get it all worked out in my head.
@pdw Araq made his example about an expression. You project this into a "literal sequence construction".
I would expect this to expand to something like this:
var x = 0
proc test(): int =
x = x + 1
return x
let a = @[test(),test()]
echo a # @[1, 2]
x = 0
var b: seq[int] = @[]
b.add(test())
b.add(test())
echo b # @[1, 2]
x = 0
var c = newSeq[int](2)
c[0] = test()
c[1] = test()
echo c # @[1, 2]
You're forgetting the fact that an empty sequence has to be created first.
x = @[test(), test()]
Actually expands to
var x = newSeq[int](2)
x[0] = test()
x[1] = test()
In this case, It might be a simple matter of re-arranging the codegen code.
@OderWat, Varriount:
Thanks. I had sort of alluded to that before, but wasn't sure if that was the case.
So then it just comes down to whether this is going to be the expected behavior WRT the sequence example. I honestly don't have a preference, but I infer from Araq and Varriount that it may be changed.
Either way, I can easily use temporary variables before the sequence allocation when needed.