Mat wrote
How about evaluating the right side of the let statement *before* calculating the address on the left side?
Reusing the same example as above:
// Evaluate the right side of the let statement
push constant 8 
// Evaluate the array offset
push local 2    // i
// Pop the previously-evaluated right-side into array[offset]
push local 0    // a
add            
pop pointer 1   // that -> a[i]
pop that 0      // a[i] = expression value
 
 
This is a functional way to generate code for array assignment.
Basically, it comes down to trade-offs between complexity of parsing versus complexity of code generation.
a[index-expression] = source-expression;
If you are using the simple recursive descent design presented in the book, then you have parsed and compiled the code for 
index-expression before you have encountered 
source-expression. The simple code generator has already written 
index-expression's code to the VM file.
One way to handle this is to compile 
index-expression to a memory buffer instead of the VM file. Then you can copy 
index-expression's code into the VM file at the point where you need it after parsing/compiling 
source-expression.
Another choice is to separate parsing and compiling so that your parser can return 
index-expression (or pointers to it in the source stream) so that your code generator can compile 
index-expression when it gets to the point where it needs to be evaluated.
And thanks for this amazing book, I've been completely hooked since I started!
 
Yes, it can be quite addicting; I've been hooked since I first encountered the course 6 years ago!
--Mark