You might assume that computers evaluate function arguments from left to right. However, that is not always the case.
In fact, many programming languages—such as C++ and RnRS Scheme—leave the evaluation order of arguments unspecified.
As shown in this post, ignoring this detail can trigger undefined behavior, leading to a Wrong Answer (WA) or Runtime Error (RE).
The Behavior in C++ and Scheme
Consider the following C++ code:
| |
If you compile and run this using GCC 15.2 with -O2, you might notice the output is 2 1. The compiler chose to evaluate the arguments from right to left.
Similarly, in Scheme:
| |
If you type A then B into the console, you might find y gets A and x gets B, because the order in which (read) is called for each binding is unspecified.
Why the Ambiguity?
This is not a bug, but a deliberate trade-off. Standards leave the order unspecified to grant compilers freedom for optimization. By not forcing a strict left-to-right order, compilers can better leverage instruction reordering, register allocation, and parallelization.
In older x86 C environments, evaluating from right to left was also a matter of efficiency. Since the stack grows downwards, pushing arguments from right to left ensures the leftmost argument ends up at the “top” of the stack, simplifying how functions like printf locate their first fixed argument.
Precise Semantics: C++17 and Scheme
In C++ (pre-C++17), these evaluations were unsequenced. Modifying the same object twice resulted in Undefined Behavior (UB). Since C++17, they are indeterminately sequenced—the result is unspecified, but the behavior is no longer “undefined” (no nasal demons).
The RnRS Scheme standard goes a step further, stating:
“When a procedure call is evaluated, the operator and operand expressions are evaluated (in an unspecified order) and the resulting procedure is passed the resulting arguments.”
This means even the function itself (the operator) could be evaluated after its arguments.
The “Nesting” Escape Hatch: Why let* Still Works
If the order is so chaotic, you might wonder: How can Scheme implement let* (which guarantees sequential binding) if it’s just “syntactic sugar” for nested lambdas?
The answer lies in the distinction between evaluation order within a call and the execution flow of a closure.
A let* expression:
| |
expands into nested lambdas:
| |
In this nested structure, the inner expression (+ x 1) is part of the body of the outer lambda. According to Scheme semantics, the body of a procedure can only begin execution after the procedure has been called and its arguments have been fully evaluated.
Even if the compiler evaluates the outer operator (lambda (x)...) after the outer operand (1), the inner call cannot even be “seen” by the evaluator until the outer call happens. Nesting transforms a “horizontal” ordering problem (where order is unspecified) into a “vertical” dependency (where order is guaranteed by the flow of data).
Conclusion
When writing code that relies on side effects (like read, set!, or counter++), never assume a left-to-right order unless the language explicitly guarantees it (like Java or Python). If you need a specific sequence in Scheme or C++, use explicit sequencing constructs like let*, begin, or separate statements to ensure your code remains portable and predictable.