************************************************************* NOTE: The content of this outline is no longer maintained ************************************************************* VAR Parameters: extending code gen to handle references - Special handling is required for VAR parameters, because an assignment to the parameter changes the argument: PROCEDURE incr(VAR v : INTEGER) v := v + 1; (* Increments the variable passed in *) END incr; VAR x : INTEGER; BEGIN x := 0; incr(x); WriteInt(x); (* writes out "1" *) END. - Although we'd like to handle this with a syntax-directed translation, we only get a little help: the "VAR" tag on the declaration and the fact that we're doing parameter passing. Most of the rules are ones we have emit routines for: Des : Des ( A ) Des : ID E : Des S : Des := E - How does the Designator rule work? + Basically it is an ``addressable'' object, for assignment or other computation that requires a variable's address rather than its value. - Sometimes it just becomes an E, - other times it stays a Designator, - and sometimes it turns up as something to be assigned to. In some grammars this is called a "Ref" because it is a *reference* to an object, not the object itself. C++ supports references like this: void incr(int &v) { v += 1; } // Boy, C++ is concise, huh? + Our changes/additions to code gen will focus on the A, E, and Des rules. + Without changes to our codegen, parameter passing would like like this: (THIS CODE IS WRONG) ;; code in main ld [gp + 0], r1 ; A:E st r1, [sp + -0] ; A:E (no return value, start at 0) call incr ; Des:Des(A) ld [gp + 0], r1 call WriteInt ;; code in incr incr: ... ld [fp - 0], r1 ; E set 1, r2 ; E add r1, r2, r3 ; + st r1, [fp - 68] ; E : E+E ld [fp - 68], r1 ; Des := E st r1, [fp - 0] ; + Stack during call set-up: set-up: ------------- + val of x + ============= <- sp + saved r's + ------------- + args + ============= <- fp in call: ============= <- sp + v + 1 + ------------- + saved r's + ------------- + val of x + ============= <- fp + saved r's + ------------- + args + ============= - The problem is that to assign to the variable x inside incr requires having the *address* of x (gp + 0). The first step, then, is that we need to change parameter passing to trigger off of the VAR declaration to pass x's address rather than it's value: (Draw picture of whole memory map as it should be) ;; code in main add gp, 0, r1 ; A:E ADDRESS, not VALUE! st r1, [sp + -0] ; A:E (no return value, start at 0) call incr ; Des:Des(A) ld [gp + 0], r1 call WriteInt - We decided last time to have code like this on procedure call: r1 = getreg() foreach a := STE in argument list, p := STE in parameter list do emitload(a, r1); emitputarg(r1, p); end So what's wrong here is the emitload, which is loading a VALUE, not an ADDRESS. - What we need to do is trigger an ``address load'' off of the parameter declaration: foreach ... ... if (p->isReference()) // triggered off of the VAR property of p's decl emitloadaddress(a, r1); else emitload(a, r1); ... /* * You'll see in a second why this gets its own procedure */ void emitloadaddress(STE *var, char *reg) { eprint("add %s, %d, %s", var->base(), var->offset(), reg); } - Now let's look at incr's code, which now must handle what's passed in: ;; code in incr incr: ... ld [fp - 0], r1 ; E Contains ADDRESS of x! ld [r1], r1 ; E Contains VALUE of x ld 1, r2 ; E add r1, r2, r3 ; + st r1, [fp - 68] ; E : E+E ld [fp - 68], r1 ; Des := E ld [fp - 0], r2 ; Contains ADDRESS of x st r1, [r2] ; Store to x - Uh, oh! So now we have a different load-value and store. Does all this ruin our emitload and emitstore abstractions? Almost! There's no direct syntax here to help us, but we do have the isReference() check: + need to enhance emitload (emitloadvalue?) and emitloadaddress - use emitloadaddress in Des rule and VAR parameter passing + emitloadvalue needs to look check isReference() (is it a var parameter?) to determine how loads are done. - normal variables behave as before - reference variables do a two-stage load (or two-stage store) + first stage is similar to emitloadaddress + emitstore also looks at type and does extra load for variable of pointer type - EVERYWHERE that emitload was called before, we now call emitloadvalue! foreach ... ... if (p->isReference()) // triggered off of the VAR property of p's decl emitloadaddress(a, r1); else emitloadvalue(a, r1); ... - emitload would be obsolete, except that its code occurs a lot in emitloadvalue, emitloadaddress, and emitstore, so we keep it as a low-level code-gen routine (see below). /* * emitloadvalue - emit code for loading a value, taking into account whether * ``variable'' is a normal variable, reference variable, or constant. * Replaces emitload. * * One could make this a method with var as the object and reg as the * argument. There would be three methods, one for each special type * of STE/variable. Of course, this would only work if each special * type of variable had its own class. * */ void emitloadvalue (STE *var, char *reg) { if (var->isConstant()) // or hasConstantValue() ? eprint("set %d, %s", var->intValue(), reg); else if (var->isReference()) { char *r = getreg(); emitload(var, r); // load address eprint("ld [%s], %s", r, reg); // deref address to get value freereg(r); } else if (var->isVariable()) emitload(var, reg); // maybe should appear in-line else /* error */ } /* * emitloadaddress - load only the address into a register, not the value. * Takes into account whether var is a reference or regular variable. ELA * for a normal variable would occur for VAR parameters. */ void emitloadaddress(STE *var, char *reg) { if (var->isReference()) // in case pass var param to var param! emitload(var, reg); // because a Ref, addr is in memory, must load else if (var->isVariable()) // original ELA case eprint("add %s, %d, %s", var->base(), var->offset(), reg); else /* error */ } /* * Here's a straightforward extention of emitstore */ void emitstore(char *valreg, STE *target) { char *treg = getreg(); if (target->isReference()) { emitload(target, treg); // address is in memory, so load eprint("st %s, [%s]\n", valreg, treg); } else eprint("st %s, [%s + %d]\n", valreg, target->base(), target->offset()); } /* * Less efficient but more maintainable. You decide which is better. */ void emitstore(char *valreg, STE *target) { char *treg = getreg(); emitloadaddress(target, treg); eprint("st %s, [%s]\n", valreg, treg); } - Note that emitloadaddress now has TWO cases. Why? Because I might pass a VAR param to a VAR param, and I need to make sure I'm still assigning to the original variable! Thus, I have to load the var param's *stored* address, not it's "fp - offset" value. Example: PROCEDURE double_incr(VAR v : INTEGER) incr(v); (* must increment x, so need to retrieve x's address *) incr(v) END incr; - RECAP: Des rules signify that we may want the *address* of an object, not just it's value. So that means when we ``load'' on Des, we want the object's address, not it's value. - Note that although a VAR parameter is *represented* as a pointer to another variable, its type is just INTEGER or whatever, not POINTER TO INTEGER. Lesson: Preserving Conceptual Integrity Can Require Rework - Conceptual integrity is the property of the same design rules being applied consistently throughout the system + Because designer can't be absolutely precise about every design rule, programmers need a way of quickly resolving ambiguities without involving designer. - can't write out every detail - can't e-mail on every question + By understanding and following the general design rules, when new situations arise, you can invent your own solution that will be consistent with other solutions to other design problems. + Important in multi-person projects, where ambiguities are likely to get resolved without consulting group leader - each person adopting own solution means no one else can understand - own solutions may not be compatible when integrated - applying conceptual integrity concept means effective parallel development is possible with minimal communication - We designed our original emitload/emitstore without planning for VAR params. - Although we could have planned for this, real-life extensions are + harder to anticipate + would take a lot more effort to handle - Lack of planning is manifested by inadequate abstractions + we could hack the changes in + go back and change the abstractions - Since we've been consistent with our naming conventions, at least we can find the places to change easily: find all emitload's and emitstore's. - By going back and reworking the design, we maintain conceptual integrity, making future additions and changes easier.