Global side-effects cannot be used for constant evaluation during compile time because they do not have a fixed execution order nor a known point in time. Reading from global constants such as tables however, can be used because reading write-protected data is not a side-effect. You can also allocate memory on the heap and stack in a virtual machine, which I did for Steamroller.
The "purity" feature exists in the D language and I use something similar in my language.
https://dlang.org/spec/function.html#pure-functions
Purity analysis/labeling allows knowing in compile time that the function will return the same output for the same input, which allow executing in compile time as well. Explicit purity labels can isolate modules so that you don't need full program analysis every time. A random generator would then have to be passed as a reference and have its state as the input when claiming functional determinism. Purity allow evaluating
if and
loop conditions without having quirky rules about which side-effect is evaluated first and therefore eliminating undefined or hard to understand behavior in placed where side-effects are confusing and dangerous.
A deterministic and well defined language should not allow things like:
| // Anyone unfamiliar with our time's languages would struggle to port this
if (x++==methodWithSideEffect(moreSideEffects()) && areTheseSideEffectsEvaluatedLater()) { ... }
|
When one can simply place side-effects before conditions;
| x++;
p = methodWithSideEffect(moreSideEffects()); // Inner goes first of course
q = areTheseSideEffectsEvaluatedLater(); // On a separate line without dangerous lazy evaluation
if ((x == p) && q) { ... }
|
The purity is useful for static analysis by knowing that only the supplied parameters (including pointer targets as children) can be affected, so that other variables passed by reference from outside will have the same conditions met before and after the call. The most common use case is to find out if run-time bound checks are needed for an array element access by propagating which range of values each variable may hold after each assignment based on all possible input paths combined.
| uint8_t myArray[512];
void foo(uint8_t x) {
// Know that 0 <= x <= 255 from the type
int y = x * 2 + 4; // For linear formulas, we only have to evaluate for the extreme ends, x==0 and x==255 in this case
// Know that 4 <= y <= 514 (no overflow for signed 32-bit integers as we expect as a minimum size)
z = pureFunction(y);
// Only z is affected because we passed y by value to a pure function
myArray[y] = x; // x is within the range, but not y! 514 might be above 511. Adding runtime checks and warning the developer.
}
|