File Warmup.html    Author McKeeman    Copyright © 2007    index

Warm-up Exercise for Changing xcom

It is faster to make a 4-inch mirrror
and then a 6-inch mirror
than it is to make a 6-inch mirror
-- Allyn J. Thompson Making your own Telescope 1947

Different Styles

Some students want to understand everything before hacking into a piece of complicated software like xcom. If that is you, then wait awhile before doing this exercise, but do read through the description all the way in any case.

If you like to live dangerously, and learn a lot real fast, forge ahead. The idea is to shoot a bullet right through xcom, missing almost everything, but adding just a bit of new functionality. Don't forget, you are getting help with your aim. You can always get another xcom if you mess this one up.

When it comes time to make a serious change to xcom, there is a systematic method. Each module has a unit test and a unit trial After any change to a module, the unit test is also changed to test the new functionality. The idea in this case is to proceed through xcom, checking each step before moving on to the next. More than likely, you will want to enhance the trial as a natural consequence of seeing what is going on with your changes.

Add abs to X

Consulting page 2 of the reference manual for X, one notes that there are four builtin functions. One generates a random number, the other three convert between types. All of them are represented by reserved words. I propose a fifth builtin
signatures op meaning
r(r) i(i) abs(x) absolute value (real or integer)

Steps

  1. Add a line to context-free grammar X.cfg.

    The new line

        abs factor
      
    conveniently comes right before the line for rand. The new line looks like the line for function r2i. Here is how the end of the X.cfg file should look:
          r2i factor
          abs factor
          rand
        

    Note that there are no parentheses in the grammar for abs. Since parentheses are allowed in general, then both forms

        abs 2.1
      
    and
        abs(-7)
      
    will be syntactically correct. That is, function abs is to be a unary prefix operator like '-'. Because abs appears in the grammar, and not as a rule name, it is automatically classified as a new reserved word by Cfg.m. Of course, you have to remember to run makeCfg (below) for your changes to X.cfg to be noticed by xcom.

  2. Make new cfg tables

            >> makeCfg -saveMat X.cfg
          
    or, if you are in a hurry and are not using the LR tables,
            >> makeCfg -noLR -saveMat X.cfg
          

  3. Run xcom

          >> xcom x:=1
          >> xcom x:=y+1
          >> xcom x:=abs(-1)
          >> xcom x:=abs(-1.0)
        
    What you discover is the first two trials work, the second two fail. This result should not be too surprising. Except for the new unknown builtin abs, nothing has changed. Cfg has built new tables but all the old values are still available. Note that the diagnostic has come from the Parser, so apparently the Lexer has not been damaged by your bullet. In fact the Cfg processor has recognized that abs is a new reserved word and added it to the reserved word table. It would do the same for a new operator identifier (say &&). Moving right along...

  4. Modify Parser.m.

    Find an M function factor. It has a switch on the current token tok. What you must do is add the line

        case rw.abs,    step(); factor(); reduce(rule.factor_absfactor);
      
    right after the line for r2i. What you are doing is telling the parser that when it encounters this new reserved token abs, it is to stop complaining (as it did on your earlier test) and instead step past the token, call function factor (recursively) and, finally, report that it has reduced a rule named
        rule.factor_absfactor
      
    The new funny name is constructed by Cfg.m by mangling the whole grammar rule. Look at the grammar to see the correspondence between funny names and rule texts. The funny names are unique and do not change when new rules are added.

  5. Rerun the four xcom tests (above).

    The two abs tests still cause errors, but this time from the symbol table. xcom object Tree has built a tree with a new kind of node in it, and the xcom object Symbols does not know what to do with that node. But, this is progress. Apparently Cfg.m, Lexer.m, Parser.m and Tree.m can handle the new builtin.

  6. Modify Symbols.m.

    Find the (very big) function exprWalk. It contains a case containing a funny node name for our next-door X.cfg neighbor r2i:

        case rn.factor_r2ifactor                 % r2i x
        if bitand(mustBe, intType) == 0
          symErr(nd, 'r2i returns type real');
        end
        exprWalk(tree.getKid(nd, 2), realType);
        res = intType;
      
    The first test makes sure that an intType result is allowed by the context (variable mustBe). If not, then r2i has discovered a type error. The symbol module gives a diagnostic and quits.

    Supposing intType is allowed by the context, then the tree representing the argument to r2i is walked (recursion). In this case, the new value of mustBe is passed down the tree as realType because r2i demands a real argument. Finally, the resulting type is set to intType (as demanded by r2i).

    The r2i case is a template for the new case you need to add.

        case rn.factor_absfactor                 % abs x
          if bitand(mustBe, arithType) == 0      % arith is OK
            symErr(nd, 'arithmetic type needed for abs');
          end
          res = exprWalk(tree.getKid(nd, 2), bitand(mustBe, arithType));
      
    Since abs can be overloaded, it is has a more relaxed initial type test that r2i. In this case, when the function exprWalk recurs, it makes sure it does not further relax constraint mustBe. When exprWalk returns, it may have tightened the type of the result. Or maybe not.

  7. Rerun the four xcom tests (above).

    Now the diagnostic comes from xcom object Generator. The missing code is in the generator's exprWalk function.

  8. Modify Generator.m

    Add a case after factor_r2ifactor

        rn.factor_absfactor
      
    Don't forget the semicolon separating cases.

  9. Rerun the four xcom tests.

    Now the diagnostic comes from object EmitX86. We have a missing case in M function expr1 (expressions with a single argument). Actually, we have two missing cases, one for integer and one for real.

  10. Modify EmitX86.m

    Since there are two cases for abs, abs needs two new code sequences.

    The case for real is analogous to, and in the same switch as, unary '-'. The new code is:

        case rn.factor_absfactor               % abs(real)
          toR(opd);                            % to FPS
          asm.fabs();                          % take float abs
          toX(opd);                            % off FPS into temp
      

    Code is emitted to push the operand onto the hardware floating point stack, the (very convenient) Intel absolute value instruction is emitted, and then code is emitted to pop the FPS operand into temporary memory. It would be more efficient to leave the result on the floating point stack, but then one would have to deal with potential FPS stack overflow.

    The integer case is a little more complicated since there is no integer absolute value instruction. The new code is:

        case rn.factor_absfactor               % abs(int)
          toR(opd);                            % into reg
          asm.cmpRC(getVal(opd),0);            % compare with zero
          fixuploc = asm.jge32(0);             % jmp if nonneg
          asm.negR(getVal(opd));               % negate
          asm.fixup32(fixuploc);               % fill in destination
      

    The operand is forced into a general register (it may already be in one). Then the register is compared with zero. If it is non-negative, nothing needs to be done so some code is skipped. Otherwise, the register is negated. Finally the conditional branch address needs to be filled in.

  11. Modify AsmX86.m

    Actually, the fabs instruction is already implemented. What you should do is look at AsmX86.m to make sure.

  12. Run the four xcom tests above again.

    It works!

  13. Do more thorough testing. Run testAll() and tryAll().

    The unit tests and trials also pass. You are done! As mentioned above, if making a serious change, each unit test needs new code to confirm that the change did the expected thing.