Why predeclared variables?

I just used x and y so I didn’t have to write a longer equivalent example that overrides some variables that don’t exist in the Interpreter’s scope, such as

(
value {
	var xx = 42;
	value {
		var yy = xx.postln, xx = 5;
	}
}
)

There’s actually still something I don’t understand with the last one. Why is xx is posted as 5 and not nil… It seems some initializers get run before others… and not necessarily in the order they were given! It looks like some constant-initializers get put into the function prologue, because they don’t show up in the byte-code for the inner function:

BYTECODES: (7)
  0   31		 PushTempZeroVar 'xx'
  1   C1 3A    SendSpecialMsg 'postln'
  3   80 00    StoreTempVar 'yy'
  5   6E       PushSpecialValue nil
  6   F2       BlockReturn

In contrast

(
value {
	var xx = 42;
	value {
		var yy = xx.postln, xx = 2 + 3;
	}
}
)

prints nil, because the initializer for xx is not a constant now; the bytecode generated for this 2+3 initializer is visible on a dump, and comes after the postln call.

So, the actual rule/algorithm presently implemented by the SC mini-hoisting seems to be:

  1. Pull all vars into a local scope table; this is use to resolve all name accesses, in preference to the outer scopes.
  2. Initialize all vars that have constant initializers via the function prologue.
  3. Generate byte code for the rest of the initializers in the function body, in the order in which these non-constant-expression initializers appear in the var statements.

(1 & 2 are probably a single step/pass.)

Since name lookups in initializers (of the form x = y) are treated as non-constant initializers , they are done in step 3, so they can access constant-initialized vars from step 2 (seemingly) “out of order”.

And sure enough

value { |yy = xx, xx = 5| yy.postln }

is a parse error, has to be written as

value { |yy = (xx), xx = 5| yy.postln }