ADVANCED CODE GENERATION (with some runtime system issues) - Parameter passing - Short-circuit boolean evaluation - Arrays: a[i + k] + Computing address with non-constant offset - Classes: + obj.foo(x) + field (data member) reference within method (function member) Parameter Passing - Logically, you want to put arguments on the stack using the stack pointer, which points to the same location that the frame pointer for the called function will point to. + Example w := foo(x + y, z) + Change in stack: before/after args/during call before: ------------- <- sp + T1 + ------------- This is the caller of foo. + saved r's + T1 is the temp/local for x + y result ------------- + args + ------------- <- fp after: ------------- + 2nd arg + + 1st arg + + ret-val + ------------- <- sp + T1 + ------------- The caller has put the arguments on + saved r's + the stack where foo expects to find ------------- them, left space for return val + args + ------------- <- fp in call: ------------- <- sp + locals + ------------- + saved r's + ------------- + 2nd arg + Now foo has been called, so rest + 1st arg + of frame has been built, state saved + ret-val + and sp/fp updated ------------- <- fp + T1 + ------------- + saved r's + ------------- + args + ------------- + Rules (R = Reference, in your grammar probably Designator) R : R ( A ) A : A , E | E + Assembly ; E (no code for `z') ld T1, r1 ; A:E st r1, [sp + -4] ; A:E ld z, r1 ; A:E st r1, [sp + -8] ; A:E call foo ; R:R(A) ld [sp + -0], r1 ; R:R(A) st r1, T2 ; R:R(A) ld T2, r1 ; S : R := E /E st r1, w ; S : R := E /R := - Code is pretty simple + Rule A returns a linked list of VarSTO's + Do an emitLoad, emitPutArg on each VarSTO String r1 = Machine.getReg() foreach a := STO in argument list, p := STO in parameter list do Machine.emitLoad(a, r1); Machine.emitPutArg(r1, p); end - emitPutArg + Takes a paramter STO and a register + Emits code to store the appropriate value at the parameter's offset in the frame + In many ways like an emitStore - major peculiarity is offseting from sp rather than fp - could enhance STO's `base' method to make this work - then would not need emitPutArg, just emitStore; less code! - but how does `base' know if accessing from outside (sp) or inside (fp)? + procedure that compiler is parsing is inherited attribute (i.e., compiler is storing parameter but not inside proc that declared it) + only applies to parameter STO's + only applies within a call rule, only when storing + what about recursion? Recall that quicksort(A, low, high) pass A to recursive call + would need a stack and flags to keep track of this nesting; a mess! + such a complication with using emitStore implies having emitPutArg - emitPutArg encodes all above information concisely - anomaly only occurs in one place - complicating base with all these inherited attributes sounds messy, especially with recursion - Of course, we also need emitGetResult, which deals with similar issues - Note that in the SPARC the first six args are actually passed in registers, and results are returned in registers. + THE ABSTRACTIONS DISCUSSED HERE STILL APPLY, BUT THEIR IMPLEMENTATIONS INVOLVE MOVES BETWEEN REGISTERS RATHER THAN STORES ONTO THE STACK. + How would you implement it? + The fact that we still want emitPutArg and emitGetResult in both cases is an example of ``design resiliency'': the property that the design accommodates change. Lesson: Reuse and Conceptual Integrity don't Go Hand in Hand - Looked like similarity of sp vs. fp store provided a unifying concept that could save us some code - Exploiting that similarity, however, requires big change to other code + `Base' method is made substantially more complicated + Rather than base being simple check of scope, now becomes very dynamic + Recursion is royal pain - Potential Benefits Small + Reuse was narrow: only appears in single place + Minimize impact of redundance - put both emitStore and emitPutArg in same module - near each other - document similarities - 80/20 rules in action: cost outweighs benefit Short-Circuit Evaluation: Boolean AND and OR in C - Short-circuit evaluation is a feature of convenience, which we derisively call ``syntactic sugar'' if (x != 0 && y/x > 2) % when first fails, don't test second S1 else if (y == 0 || x/y < 0) % when first succs, don't test second S2 - Any ideas how to compile for this? + Like nested if-then/if-then, perhaps? if (x != 0) if (y/x > 2) S1 else if (y == 0) S2 else if (x/y < 0) S2 + Well, the AND (&&)looks easy, but OR (||) doesn't quite work. - Code for S2 appears twice! + How about these alternatives: 1. Apply DeMorgan's law: X or Y = ~(~X and ~Y), but a lot of code 2. Reduce code by inverting operators: "=" becomes "not=", but that requires that when see "E" in parser, knowing that will be for a jump test 3. Just replicate S2: sounds complicated (e.g., whether to dup code is an inherited attribute, need a duplicate code buffer) and a little wasteful 4. Hey, since we're generating assembly, why not just have both pieces of code jump to the same single piece of code? + Note that we have been ignoring our rules *and* the actual assembly; let's return to our usual 'model-driven' process - Let's look at rules: E : E T_AND E | E T_OR E - Now the code: < store y = 0 in T1 > ; E : E = E ld T1, r1 ; E OR {} E if r1 goto L1 ; < store x/y < 0 into T2 > ; E : E < E ld T2, r1 ; E OR E {} if r1 goto L1 ; st 0, T3 ; goto L2 ; L1: st 1, T3 ; L2: ... ; + The ordering looks perfect. + Code emitted has to make sure that code generated under E:e2 is conditionally executed. E ::= E:e1 T_OR {: orTrueStack.push(newlabel()); orFalseStack.push(newlabel()); RESULT = new VarSTO(TypeValues.boolean); Machine.emitCond(e1, orTrueStack.top()); // if true, jump // to set true :} E:e2 {: Machine.emitCond(e2, orTrueStack.top()); // if true, jump // to set true Machine.emitSetFalse(RESULT); // fall through to set false Machine.emitGoto(orFalseStack.top()); Machine.emitLabel(orTrueStack.top()); Machine.emitSetTrue(RESULT); Machine.emitLabel(orFalseStack.pop()); Machine.emitLabel(orTrueStack.pop()); :} + Might not actually be wise to reuse IF label stacks. + Code for AND is nearly identical, just need to invert EmitCond, emitSetFalse, and emitSetTrue. That's DeMorgan's Law again! Lesson: Idea of Reducing New Construct to Existing Construct - Good idea because it reduces number of ideas and reuses implementation - However, led us to ignore our model matching problem (Oberon --> Sparc) + Sparc provides gotos, which simplify problem + Unlike in C/C++ reduction, && and || are nearly identical in Sparc reduction, reducing number of ideas and reusing implementation + Analogy did cause us to reuse If label stacks - Again, when investigating ``match'' of problem and solution structure, need to consider multiple dimensions (or-if in C/C++ vs. and-or in Sparc) + similarity to Sparc (and-or) more imporant than C (or-if) - Could have ended up in same place if had aggressively pursued DeMorgan's law at beginning, but this was unnatural. Bringing in target model was more straightforward. Code Gen of Arrays: Harder Than It Looks - To figure out how to compile for arrays, we first look at the associated grammar rules, since we are committed to a syntax-directed translation driven by a bottom-up parse of these rules. Model-driven! Des : ID | Des [ E ] E : Des S : Des := E - How does the Designator rule work? + 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* (i.e., pointer) to an object, not the object itself. - To make sense out of this, we better construct an example that works all these cases, meaning we look at both a read and set of an array variable, since they are different: VAR i, k, x : INTEGER; A : ARRAY 17 OF INTEGER; ... BEGIN x := A[i]; ... A[k] := 2 * x; ... - So here's the deal: Ref rules signify that we may want the *address* of an object, not just it's value. So that means when we ``load'' from a Des, we want the object's address, not it's value. The Des rule tells us when to do this; that's what syntax-directed translation is all about. - We already dealt with this for simple variable assignment. In that case, the solution was simple, since the store rule treats the base+offset as an address to store to: Machine.emitStore(String valReg, VarSTO *tArg) { eprint("st %R, [%R + %D]\n", valReg, tArg->base(), tArg->offset()); } - With arrays, though, we do not know the offset at compile-time. + What is the value of i? What is the value of k? + ``offset'' is a variable rather than constant value + This complicates our normal base + offset computations + What's the ``base + offset'' formula? &A[i] = base(A) + offset(A) + ((i-lowerbound(A)) * size(elemtype(A))) + We can treat this as a two-stage base + offset to fit our addressing model, which is designed for typical 2 and 3 address architectures: arraybase = base(A) + offset(A) &A[i] = arraybase + ((i-lowerbound(A)) * size(elemtype(A))) For Oberon, the lower bound is always 0, so we can drop that term: &A[i] = arraybase + (i * size(elemtype(A))) Think of the index part as the ``index offset''. In reality, it is accessing the i-th variable in the array, and hence computing the offset by summing the size of the previous i-1 array elements. + The multidimensional case is unbelievably hairy. Look it up! - How should we generate code for this? (focus on store case in class) + As before, write out example of Oberon code and matching assembler ; x ; R : ID ld [gp+0], r1 ; i ; R : R [E] mul r1, 4, r2 ; i as offset for int ; add gp, 12, r3 ; address of A ; add r3, r2, r4 ; &A[i] -> reg ; ld [r4], r5 ; A[i] -> reg ; st r5, [fp+80] ; store in T1 ; ld [fp+80], r1 ; load T1 ; S : R := E st r1, [fp+8] ; x := T1 ; ... ld [gp+4], r1 ; k ; R : R [E] mul r1, 4, r2 ; k as offset for int ; st r2, [gp+84] ; T2 ; ; T3 == [gp+88] ; E : E * E add gp, 12, r3 ; address of A ; S : R := E ld [gp+84], r4 ; T2 ; add r3, r4, r5 ; &A[i] -> reg ; ld [gp+88], r0 ; T3 ; st r0, [r5] ; A[k] := ; + Does this align with grammar? + Problem: load and store of array is different than regular variable - extra offset (r2 in these examples), plus address of A (in r3) + both must be passed across evaluation of 2 * x (E rule) - but standard ``interface'' between R and S rule is just an STO - could extend a STO to have an additional offset value (r2 here) + can't use A's STO for this, since it has many uses + looks like a hack - note also that [E] generates different code in two places! That's not very syntax directed! (depends on inheritied attribute telling which side of := subscript appears on). Assign rule varies, too. + top case also has early actions for combining R and [E] + bottom case does it in assignment rule! - not looking good, so try a different code gen scheme, one that + sticks to the rule of a basic STO communicating between rules + generates same code every time for subscript, assignment + Better Solution: store sum of r3 and r2 to memory (a temp) that is normal STO. - more in line with current system's approach, and consistent with idea of computing base+offset in two steps (where arraybase is the intermediate result, and hence our new temp variable). - however, our stored value is a *pointer* to a variable - this means we have new kind of value, ``pointer to X'', although we will not actually change the type (see below). ld [gp+0], r1 ; R : R [ E ] mul r1, 4, r2 ; add gp, 12, r3 ; add r3, r2, r4 ; st r4, [fp+80] ; (store computed address as temp) ld [fp+80], r1 ; S : R := E ld [r1], r2 ; st r2, x ; ... ld [gp+4] r1 ; R : R [ E ] mul r1, 4, r2 ; add gp, 12, r3 ; add r3, r2, r4 ; st r4, [fp+84] ; (store computed address as temp) ; E : E * E ld [gp+88], r0 ; S : R := E ld [fp+84], r1 ; st r0, [r1] ; - R and S rules are now very similar for both cases - *computation* of address is in different rule from *use* of address + in store case, 2 * x comes in between + this implies the following code (should be in ArrayRef.action()) R ::= R:r '[' E:e ']' {: RESULT = new VarSTO(); /* ... check ... */ ; ArrayRef.emit(RESULT, r, e); :} void ArrayRef::emit(VarSTO t, VarSTO a, STO e) { String indexReg = Machine.getReg(); String addrReg = Machine.getReg(); String resReg = Machine.getReg(); Machine.emitLoad(e, indexReg); // but see next section // WGG - this is if we had lower bounds other than 0 // eprint("sub %R, %D, %R", indexReg, a.lowerBound(), indexReg); // not typical multiply, so can't call emitadd, etc. eprint("mul %R, %D, %R", indexReg, a.elemSize(), indexReg); Machine.emitLoadAddress(a, addrReg); /* ld addr, not 1st val of array! */ eprint("add %R, %R, %R", addrReg, indexReg, resReg); // WGG - these should really happen in the type check // set type/mode of t so that emitStore, etc., knows what to do // t.setType(a.type().elemType()); // t.setReferenceMode(); // see comments below Machine.emitStore(resReg, t); Machine.freeReg(indexReg); Machine.freeReg(addrReg); Machine.freeReg(resReg); } - array reference really loads address of array, not value. + consistent with notion of Ref rule - arrays now have a two-stage ``load'' now + load address from memory + load value (off of register) - even a store to an array has a load address (but no load value) + load address + store value (off of register) - basic rule is that: + E implies load value + R implies ``load'' address - note that when loading "A" above, we only loaded its address - additional problem now is that we have STO's floating around that are *pointing* to int. Since these are converted at ``load time'' to actual ints, that means that we may be ``adding'' pointer to int values in the add rule: E ::= E:e1 '+' E:e2 /* e1 or e2 could be pointer to int */ - so we really want the VarSTO to be of type "int", but *represent* it as a *pointer* to an int. So this means that the *type* of our array reference should be "int", but that it's *mode* should be "reference". This reference is just like a Pascal var parameter or a C++ ref parameter. By the mode being set to "reference", the compiler knows to do dereference the pointer. + Does all this ruin our emitload and emitStore abstractions? Almost! - need both emitLoad (emitLoadValue?) and emitLoadAddress + use emitLoadAddress in R rule - emitLoadValue needs to look at type of variable (is it a pointer?) to determine how loads are done. + normal variables behave as before + pointer 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 /* * emitLoadValue - emit code for loading a value, taking into account whether * ``variable'' is a normal variable, reference variable, or constant. * * emitStore has a similar look. * * 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 STO/variable. Of course, this would only work if each special * type of variable had its own class. * */ void emitLoadValue(VarSTO var, String reg) { if (var.isConstant()) // Constants are a "mode", too. eprint("set %D, %R", var.value(), reg); // value() rets Value w/toString() else if (var.isReference()) { String r = getReg(); emitLoad(var, r); // load addr eprint("ld [%R], %R", r, reg); // deref freeReg(r); } else if (var.isVariable()) emitLoad(var, reg); 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(VarSTO var, String reg) { if (var.isReference()) // includes array elem and class field refs emitLoad(var, reg); // get the address out of memory else if (var.isVariable()) // for arrays and passing arg by "var" eprint("add %R, %D, %R", var.baseName(), var.offset(), reg); else /* error */ } Lesson 1: Matching Structure of Solution to Structure of Problem is Hard! - Working with several structures complicates coming up with simple 1-1 match + Oberon language structure + Grammar structure and parsing order (bottom-up, left-to-right) + Rule communication structure (single STO) + Assembly code (this is our supposed problem structure) - Except for Oberon itself and assembly syntax, all are ``solution structure'' + Oberon and SPARC syntax are ``non-negotiable'' + Grammar structure and communication by STO's hard to change, though! - not encapsulated in any module, permeates system - part of paradigm: provides expressive power, but with limitations + Turns out that order of assembly code and how computations broken up was fungible - change order of R and E rules to match grammar structure - take second base + offset and put in R rule rather than S rule. - distinguish loading address from loading value - In general, when trying to find a solution, figure out what structures can be changed and which are impossible or hard to change. This delimits your search space and helps prioritize options. Lesson 2: Conceptual Integrity Helps Resolve Design Problems - 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 - Agreed early on to pass a single STO as the primary synthesized attribute + Matches type system + Result of every computation is a variable - We decided later on to do load-load-compute-store + Fits single-STO decision + Allows having 1 emit per syntax rule + Simplifies register allocation - Even played a minor role in loop issues (adding loops, exit, else) + Notion of stack for inherited attributes + One stack/attribute per ``fact'' - To keep system structure consistent (preserve integrity), cannot deviate + Avoid letting unique properties of new features lead to compromise of earlier decisions - quirks in arrays, loops, etc. challenge system's architectural concepts - if change approach, then there are more approaches to remember + Having so many choices, let concepts behind previous decisions guide you - success of this depends on quality of previous decisions - if early decisions faithful to problem structure, more likely to be OK + i.e., concepts expressed in system correspond to concepts in domain, such as type equivalence, assignability, method, etc. - do not necessarily get what you would if started from scratch knowing what you know now Compiling Classes: Records + Functions = Objects TYPE Stack = RECORD stk : ARRAY 100 OF INTEGER; size : INTEGER; ... END; PROCEDURE (S : POINTER TO Stack) pop(); S.size := S.size - 1; END pop; PROCEDURE (S : POINTER TO Stack) init(); S.size := 0; END init; VAR mystack : POINTER TO Stack; BEGIN new(Stack); mystack.init(); mystack.push(3); mystack.push(7); IF (mystack.size > 5) & mystack.top() < 10 THEN mystack.pop(); END; END. - What types are similar to records in Oberon? How are they different? - Several challenges to compiling class objects + Unlike arrays, each component can be a different type, different size + Objects are dynamically allocated + Class functions (methods, function members) take an implicit parameter, the object itself - Accessing fields of a record: a) mystack.size % from outside stack b) S.size := S.size - 1 % from inside mystack.pop + For now assume that the stack itself is an (implicit) parameter to pop + Any ideas about how we should approach this? - objects are stored in dynamic area, since can be allocated or freed at any time; pointer to object points to object in dynamic area - in this picture, we see variable mystack in the global area pointing to some storage (the sequences of o's): ------------- + + + +-+ + +-----+-| + global | + +-+ + | ------------- | + + | + + text | + + | ------------- | + + +---->ooooooo + dynamic + + ------------- + + ------------- + + + + stack + + ------------- - In C++, mystack would be a Stack*, and it would get an object assigned to it by new: Stack *mystack = new Stack(); - for now, let's assume we have a pointer to object for accesses - better hope that base + offset works - objects similar to arrays, except that we start from object pointer rather than object itself + emitLoadAddress loads from memory rather than adds - base is address stored in "mystack", offset is sum of sizes of object components before "size": &(mystack.size) = *(base(mystack) + offset(mystack)) + offset(size) ^ note dereference here or in "assembler normal form": obj_ptr = base(mystack) + offset(mystack) ; addr of mystack obj_base = *obj_ptr ; addr stored in mystack &(mystack.size) = obj_base + offset(size) Note that if the pointer is treated like a reference, then the first two terms can be generated by emitLoadAddress! - a field reference is a "Ref" rule, so we keep it in address form (``reference to int'') until the program actually needs the value. + Grammar makes actions syntax directed (R ::= R:r '.' ID:id): RESULT = new VarSTO(); /* type check */ FieldRef.emit(RESULT, r, id); + Code for case b: ; value of mystack (pointer to Stack) has ; been pushed on stack as a parameter to pop ; R : ID ld [fp - 0], r1 ; load pointer to stack (1st parm on call stack) add r1, 400, r2 ; point to size within mystack (skip array) st r2, [fp - 64] ; store to T1 (64 is made up) ; R : ID (and then E : R) ld [fp - 0], r1 ; load pointer to stack add r1, 400, r2 ; point to size within stack (skip array) st r2, [fp - 68] ; store to T2 ; E : E - E ld [fp - 68], r1 ; load address size from T2 ld [r1], r2 ; load value size set 1, r3 ; load constant sub r2, r3, r4 ; subtract st r4, [fp - 72] ; store to T3 ; S : R := E ld [fp - 64], r1 ; load address size from T1 ld [fp - 72], r2 ; load T3 st r2, [r1] ; store T3 to size + This looks surprisingly like array code! - can make use of emitLoadAddress, emitStoreAddress, and enhanced emitLoadValue + But since we have a *pointer* to Stack, we have to do a load in order to do compute initial offset. For arrays, we just added base and offset, because array value is not a pointer. + Also, offsets are computed differently - 400 is determined by size of variables that come before size in Stack declaration (100 ints are 400 bytes) - this is the same way we compute offsets for locals and params in functions - so like functions: + keep running tally of sizes of all var decls seen so far + when new var declared, take current tally as its offset +---------+ |Stack 404| Number is total size of object; |Class G| useful for malloc +-- +---------+ | | | V | +--------+ +--------+ | |stack 0 |-->|size 400| Numbers here are offsets | |var | |var | | +--------+ +--------+ | | +-----+ +-> |pop |--> ... |proc | +-----+ - remember that we have to keep exported elements around after the downscope/closescope. - Method Invocation mystack.pop() + invoking a method is like procedure call, except that there is an implicit parameter ``mystack'': pop(mystack) ------------- <- sp + locals + ------------- + saved r's + ------------- Pop's frame + ... + + 1st arg + + mystack + ------------- <- fp + + + + Main's frame + + ------------- - This allows the "size" reference to be really treated as "mystack.size" in the body of pop by accessing the first arg of pop (which contains the pointer to mystack). - Note that this means that ID references have to be handled differently if the ID comes from a Class scope rather than a normal (local or global) scope. A class ID has to get its ``base'' from the Class, not another scope (see earlier pictures of class symbol table). + The special dot syntax is used to signal to the compiler that the pop defined in Stack should be used, as opposed to some other stack. It also makes it clear that it is not a normal function invocation. A pure OO language could do without the special syntax. + However, note that the "R . ID" syntax does not distinguish variable reference and type-bound procedure call, which are handled differently. You need to have an explicit test in the rule to distinguish the two cases and generate different code for them: R ::== R:r '.' ID:id {: FieldSTO field = r.lookup(id); if (field.isProc()) // do something for call here else FieldRef.emit(RESULT, r, id); :} + Using the pop(mystack) analogy, we realize that we should probably try to pass an extra argument to pop. R : R . ID // R = mystack VarSTO, ID = pop ProcSTO | R ( AL ) // R = pop ProcSTO, how to access mystack VarSTO? AL : AL E | E + Unfortunately, the grammar and our ``single VarSTO result per rule'' convention does not allow passing both VarSTO's to the method invocation; moreover, we can't put mystack on the call stack early, because the compiler hasn't generated the function calls that are likely to appear in the argument list: mystack.push(theirstack.top()) the theirstack call has to be emited completely before the compiler can start emiting code for the mystack.push call. + As usual, we can use a stack to handle the nested/recursive nature of the constructs. - When a method name (e.g., pop) is seen, then the associated object STO is pushed on the stack - When it is time to emit the method call code, the top of the stack is accessed. When the call code emit is completed, the stack is popped: R : R . ID // push R on object stack | R ( AL ) // emit call with extra argument from top obj stack // and then pop stack - This is less tidy than previous use of attribute stacks, because we don't have a nice balanced-parentheses syntax to drive pushes and pops, e.g., BEGIN ... END. It is more semantics driven. + Note that the offsets stored for method STO's need to account for the fact that an extra argument has been stuck at the front. You could put it at the end, too, to avoid the problem. + Another way to fix this problem is to change the grammar: R : R . ID | R . ID ( AL ) + This introduces a shift/reduce conflict, but CUP/yacc correctly handles this by shifting for the longer case if a "(" is seen. - New: dynamic memory allocation and initialization + For us, this is easy: call C's cmalloc, don't do init - write C program that does a cmalloc, mimic the code - pass to it the size of your object in bytes (varSTO->type()->size()) - store result in object variable pointer. + Two intellectually interesting issues - initialization - how does cmalloc work? - Initialization of classes (in C++) + all variables in class declaration can have static initializers - but object is dynamically allocated, vars should get initialized on allocation + constants, too - we'd like one set of constants for all objects, though, to save space and creation time (time an issue for contants in functions, too) + ideas for variables? - declaration for class creates special initializer function - this may be a static or dynamic initializer in general Oberon - as each initializer is encountered, write out initializer code to separate file (like we recommend for main). Then assemble into a function after processing of class is completed. Lesson: Matching Solution Structure to Problem and Conceptual Integrity are Not Formulaic - Each problem we have encountered has had a different approach to solution + Force to stay in model: new operators, large constants + Tweak model: arrays, complex conditionals, small constants + Change grammar: method invocation + Replicate code in different context: setting parameters in call - Came from complex management of tradeoffs + Different problem structures are not mutually consistent in view of new additions, so must conflict in final solution (at least for reasonable cost: 80/20 rules) + emitStore/emitPutArg versus complex base calculation + Conceptual integrity is relative - ease of understanding what is there - ease of adding what hasn't been added yet - how do you decide which to take care of? - spend more time to achieve both, but then deadline can slip + In the end, our choices have to be made based on estimates of complexity and cost - can't know (exact) cost of future changes - can't even cost-effectively enumerate all present tradeoffs quantitatively