Adding Named Constants to Jack

classic Classic list List threaded Threaded
2 messages Options
Reply | Threaded
Open this post in threaded view
|

Adding Named Constants to Jack

cadet1620
Administrator
This post was updated on .
[See Adding "break" and "continue" to Jack's while loops for another Jack language extension.]

The Jack language as currently defined encourages the bad programming practice of using constant integers throughout the code. For example, the screen dimensions are 512 by 256 and these limits may be needed in may places when writing games and OS code.

When constants are used directly in the code, it may not be clear what they mean. For example, my game has a ball diameter that is currently 8 as well as a mirror size that is also 8. If I want to increase just the mirror size, I will need to be careful that I don't accidentally change anywhere an 8 refers to the ball diameter.

The best that can be done in Jack is to add class variables to hold these constants and initialize them in the constructor:

    class Mirror {
        static MIRROR_SIZE;

        constructor Mirror new() {
            let MIRROR_SIZE = 8;
            ...
        }
This requires a static RAM variable for each constant, which is a limited resource, and a memory reference whenever the constant is used, which increases the size of the generated code.

The proposed language extension is to add named constants to the Jack language. The named constants need to support negative values. Along with the current class variable definition commands, field and static, add a const definition with the following syntax:

    <class>      ::=  'class' <className> ( <classVarDec> | <constDec> )* <subroutineDec>* '}'
    <constDec>   ::=  'const' <constValue> ( ',' <constValue> )* ';'
    <constValue> ::=  <varName> '=' '-'? <integerConstant>
This same syntax will be added to subroutines. Normal scoping rules will be applied to named constants; a named constant defined in a subroutine with the same name as a named constant defined in the class will override the class definition.
    <subroutineBody>  ::=  '{' ( <varDec> | <constDec> )* <statements>* '}'

Implementation

Changes to the SymbolTable Module

  • The kind property needs to have two new values: CLASS_CONST and LOCAL_CONST. Although the syntax of the const command is identical, separate values are required to properly resolve identifier scope.
  • The Define method needs an optional additional parameter, value. If value is supplied, it is used instead of the running index. value should only by overridden for symbols of type *_CONST.
  • The VarCount and KindOf methods will need to support the new kind values.
By using the constant's value in place of the running index, the CompileEngine.CompileTerm method should require minimal, if any, changes to call VMWriter.WritePush(CONST,...).

Changes to the VMWriter Module

  • The WritePush method will need to be changed to deal with the possibility of negative indices for the const segment. In this case, it should push the absolute value of the index and add a "neg" VM command after the push.

Changes to the CompilationEngine Module

  • The CompileClass method will need to look for the const keyword in the loop where it is looking for field and static keywords.
  • The CompileSubroutineBody method will need to look for the const keyword in the loop where it is looking for var keywords.
  • A CompileConstDec method will need to be written to handle const commands. It needs to have a kind parameter so that it can handle either class or subroutine constants.
  • The CompileLet method will need to check the kind of the identifier being set and throw an error if an attempt is made to assign to a constant.
[The need to check for assignment to a constant was an afterthought. By adding constants to the symbol table the way I did, my CompileLet method generated "pop const 42" without any error from underlying code. I should have had an "assert(segment != SEG_CONST)" in my WritePop method!]

Example usage

I modified my Memory.jack to use named constants.

class Memory {
    // Heap location
    const HEAP_START = 2048;        // start address
    const HEAP_END = 16384;         // end address + 1
    
    // Memory block structure
    const BLOCK_SIZE = 0;           // offset to block size including 1 word header
    const NEXT_BLOCK = 1;           // offset to free list forward link
    const HEADER_SIZE_FREE = 2;     // header size for free blocks
    const HEADER_SIZE_ALLOC = 1;    // header size for allocated blocks
    
    // Free List head pointer
    static Array freeList;

    
    /** Initializes the heap. */
    
    function void init() {
        let freeList = HEAP_START;
        let freeList[BLOCK_SIZE] = HEAP_END - HEAP_START;
        let freeList[NEXT_BLOCK] = null;
        return;
    }
    
    /** Finds and allocates from the heap a memory block of the 
     *  specified size and returns a reference to its base address. */

     function int alloc(int size) {
        ...
        // Walk freeList looking for best fit.  Short-circuit the walk
        // if exact fit is found.  IMPORTANT: the fragment must be >=
        // HEADER_SIZE_FREE words long.

        let freeBlock = freeList;
        let freePrev = null;
        while (~(freeBlock = null)) {
            if (freeBlock[BLOCK_SIZE] = size) {
                // exact fit
                do Memory._setLink(freePrev, freeBlock[NEXT_BLOCK]);
                return freeBlock + HEADER_SIZE_ALLOC;
            }
            ...
            let freePrev = freeBlock;
            let freeBlock = freeBlock[NEXT_BLOCK];
        }
        ...
    }

Testing the concept

The Memory.jack using the constants passes the test. If I've correctly replaced all the integer constants with their corresponding named constants, I should be able to add block signatures to the free and allocated blocks just by changing the constants. (This will, of course, just allocate space for the signatures; new code will be needed to set and validate the signatures.)

    // Memory block structure
    const BLOCK_SIGNATURE = 0;      // offset to block signature
    const BLOCK_SIZE = 1;           // offset to block size including 1 word header
    const NEXT_BLOCK = 2;           // offset to free list forward link
    const HEADER_SIZE_FREE = 3;     // header size for free blocks
    const HEADER_SIZE_ALLOC = 2;    // header size for allocated blocks
    const SIGNATURE_FREE = -5412;   // signature for free blocks
    const SIGNATURE_ALLOC = 29554;  // signature for allocated blocks
Well, that failed! Time to debug it...

Oops, missed a "1" that should have been HEADER_SIZE_ALLOC. That's why named constants are a good thing!

With that bug fixed, Memory.jack with the modified header structure now passes the test.

Reply | Threaded
Open this post in threaded view
|

Re: Adding Named Constants to Jack

Dano
Very nice Mark! I especially like that you provided a list of proposed changes.