[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.