I6 Template Layer

Inform 7 6M62ContentsIntroductionFunction IndexRules Index

BlockValues.i6t

BlockValues contents

Overview.

Each I7 value is represented at run-time by an I6 word: on the Z-machine, a 16-bit number, and on Glulx, a 32-bit number. The correspondence between these numbers and the original values depends on the kind of value: "number" comes out as a signed twos-complement number, but "time" as an integer number of minutes since midnight, "rulebook" as the index of the rulebook in order of creation, and so on.

Even if a 32-bit number is available, this is not enough to represent the full range of values we might want: consider all the possible hundred-word essays of text, for instance. So for a whole range of kinds – "text", "list of K", "stored action" and so on – the I6 value at run-time is only a pointer to what is called a "short block". This is typically only a few words long, and often only a single word: hence the term "short". It has no header or other overhead, and its contents depend on the kind of value.

If we know that a given kind of value can be stored in, say, exactly 128 bits, then it's possible simply to store the whole thing in the short block. More often, though, the data needs to be flexible in size, or needs to be large. In that case, the short block will include (and sometimes, will consist only of) a pointer to data stored in a "long block". Unlike the short block, the long block is a chunk of memory stored using the Flex system, and thus is genuinely a "block" in the sense of the Flex documentation.

It's possible to have several different short blocks each pointing to the same long block of underlying data: for example, the result of the I7 code

let L1 be { 2, 3, 5, 7, 11 }; let L2 be L1;

is to create L1 and L2 as pointers to two different short blocks, but the two SBs each point to the same long block, which contains the data for the list 2, 3, 5, 7, 11. Note that this makes it very fast to copy L1's contents into L2, because only L2's short block needs to change.

The rules for customers who want to deal with values like this are much like the rules for allocating memory with Flex. Calling BlkValueCreate creates a new value, but this must always, and only once, later be disposed of using BlkValueFree.

So if the short blocks of L1 and L2 both point to the same long block of actual data, what happens when only one of them is freed? The answer is that every long block has a reference count attached, which counts the number of short blocks pointing to it. In our example, this count is 2. If list L1 is freed, the long block's reference count is decremented to 1, but it remains in memory, and only L1's short block is given up; when list L2 is subsequently freed, both its short block and the now unwanted long block are given up.

The harder case to handle is what happens when L1 and L2 share a long block containing 2, 3, 5, 7, 11, but when the source text asks to "add 13 to L1". If we simply changed the long block, that would affect L2 as well. So we must first make L1 "mutable". This means copying the long block to make a new unique copy with reference count 1; assigning that to L1 in place of the original; and decrementing the reference count of the original from 2 to 1. L1 and L2 now point to two different long blocks, so it's safe to modify L1's.

Subtle and beautiful bugs can occur as a result of making a value mutable at the wrong moment. Beware in particular of reading data out of a long block, then writing it back again, because the act of writing may force the value owning the long block to become mutable; this will make a new copy of the data; but you will be left holding the old copy. Since these are functionally identical, you may not even notice, but calamities will occur later because the version of the value you're holding really belongs to somebody else and may be freed at any point.

Finally, note that the I7 compiler also creates block values representing constants. For example, the source text

let L1 be { 2, 3, 5, 7, 11 };

causes a block value representing this list to be stored in memory. The long block for a constant needs to be immortal, since this memory must never be freed: it's therefore given a reference count of "infinity".

86Constant RC_INFINITY = MAX_POSITIVE_NUMBER;

Short Block Format.

A short block begins with a word which is usually, but not always, a pointer to the long block. There are three possibilities:

(a) 0 means the short block has length 1 and the long block begins at the very next word in memory. This makes it more convenient for I7 to compile BV constants, but isn't otherwise used.

(b) 1 to 255 means the short block has length 2 or more. The value is expected to be a bitmap in bits 4 to 8 together with a nonzero ID in bits 1 to 4. If the BLK_BVBITMAP_LONGBLOCK bit is set, a pointer to the long block is stored in the second word of the short block.

(c) Otherwise the short block has length 1 and contains only a pointer to the long block.

105Constant BLK_BVBITMAP = $ff; 106 107Constant BLK_BVBITMAP_LONGBLOCK = $10; ! Word 1 of SB is pointer to LB 108Constant BLK_BVBITMAP_TEXT = $20; ! BV holds a TEXT_TY value 109Constant BLK_BVBITMAP_CONSTANT = $40; ! BV holds a TEXT_TY value 110 111#IFTRUE WORDSIZE == 4; 112Constant BLK_BVBITMAP_LONGBLOCKMASK = $ffffff10; 113Constant BLK_BVBITMAP_TEXTMASK = $ffffff20; 114Constant BLK_BVBITMAP_CONSTANTMASK = $ffffff40; 115#IFNOT; 116Constant BLK_BVBITMAP_LONGBLOCKMASK = $ff10; 117Constant BLK_BVBITMAP_TEXTMASK = $ff20; 118Constant BLK_BVBITMAP_CONSTANTMASK = $ff40; 119#ENDIF;

Long Block Access.

Illustrating this:

125[ BlkValueGetLongBlock bv o; 126    if (bv) { 127        o = bv-->0; 128        if (o == 0) return bv + WORDSIZE; 129        if (o & BLK_BVBITMAP == o) { 130            if (o & BLK_BVBITMAP_LONGBLOCK) return bv-->1; 131            return 0; 132        } 133        return o; 134    } 135    return bv; 136];

Weak Kind.

This returns the weak kind ID of a block value. Most of the time this information is stored in the long block, but that poses a problem for BVs which have no long block: we must use the bitmap instead.

144[ BlkValueWeakKind bv o; 145    if (bv) { 146        o = bv-->0; 147        if (o == 0) return bv-->(BLK_HEADER_KOV+1); 148        if (o & BLK_BVBITMAP == o) { 149            if (o & BLK_BVBITMAP_TEXT) return TEXT_TY; 150            o = bv-->1; 151        } 152        return o-->BLK_HEADER_KOV; 153    } 154    return NIL_TY; 155];

Reference counting.

Reference counts lie in a word at a fixed offset from the start of the long block: doctrinally, any block value with no long block at all (such as a piece of packed text) has reference count infinity.

163[ BlkValueGetRefCountPrimitive bv long_block; 164    long_block = BlkValueGetLongBlock(bv); 165    if (long_block) return long_block-->BLK_HEADER_RCOUNT; 166    return RC_INFINITY; 167];

Changing Reference Counts.

When incrementing, infinity's the limit; when decrementing, it never reduces. Note that the decrement function returns the new reference count, but the increment function returns nothing. It's only when reference counts go downwards that we have to worry about whether something happens.

176[ BlkValueIncRefCountPrimitive bv long_block refc; 177    long_block = BlkValueGetLongBlock(bv); 178    if (long_block) { 179        refc = long_block-->BLK_HEADER_RCOUNT; 180        if (refc < RC_INFINITY) long_block-->BLK_HEADER_RCOUNT = refc + 1; 181    } 182]; 183 184[ BlkValueDecRefCountPrimitive bv long_block refc; 185    long_block = BlkValueGetLongBlock(bv); 186    if (long_block) { 187        refc = long_block-->BLK_HEADER_RCOUNT; 188        if (refc < RC_INFINITY) { 189            refc--; 190            if (refc < 0) BlkValueError("reference count negative"); 191            long_block-->BLK_HEADER_RCOUNT = refc; 192        } 193        return refc; 194    } 195    return RC_INFINITY; 196];

Long Block Capacity.

As we've seen, the long block has some metadata in a header, but otherwise it's organised as if it were an array, with entries 8, 16 or 32 bits wide. At any given time, the "capacity" of the LB is the number of entries in this array: that doesn't mean that the BV is using them all at any given moment.

205[ BlkValueLBCapacity bv long_block array_size_in_bytes entry_size_in_bytes flags; 206    long_block = BlkValueGetLongBlock(bv); 207    if (long_block == 0) return 0; 208 209    array_size_in_bytes = FlexTotalSize(long_block); 210 211    flags = long_block->BLK_HEADER_FLAGS; 212    entry_size_in_bytes = 1; 213    if (flags & BLK_FLAG_16_BIT) entry_size_in_bytes = 2; 214    else if (flags & BLK_FLAG_WORD) entry_size_in_bytes = WORDSIZE; 215 216    return array_size_in_bytes / entry_size_in_bytes; 217]; 218 219[ BlkValueSetLBCapacity bv new_capacity long_block flags entry_size_in_bytes; 220    if (bv == 0) rfalse; 221    BlkMakeMutable(bv); 222    long_block = BlkValueGetLongBlock(bv); 223    if (long_block == 0) rfalse; 224 225    flags = long_block->BLK_HEADER_FLAGS; 226    entry_size_in_bytes = 1; 227    if (flags & BLK_FLAG_16_BIT) entry_size_in_bytes = 2; 228    else if (flags & BLK_FLAG_WORD) entry_size_in_bytes = WORDSIZE; 229 230    FlexResize(long_block, new_capacity*entry_size_in_bytes); 231    rtrue; 232];

Long Block Array Access.

Though the customer thinks he's getting an array, in fact the storage in the LB is not necessarily contiguous, since it can span multiple Flex blocks. We abstract that with two routines to read and write entries.

BlkValueRead takes two compulsory arguments and one optional one. Thus:

BlkValueRead(bv, n)

reads the nth entry in the long block for bv, whereas

BlkValueRead(long_block, n, true)

read it from the given long block directly. BlkValueWrite is similar.

250[ BlkValueRead from pos do_not_indirect 251    long_block chunk_size_in_bytes header_size_in_bytes flags entry_size_in_bytes seek_byte_position; 252    if (from == 0) rfalse; 253    if (do_not_indirect) 254        long_block = from; 255    else 256        long_block = BlkValueGetLongBlock(from); 257 258    flags = long_block->BLK_HEADER_FLAGS; 259    entry_size_in_bytes = 1; 260    if (flags & BLK_FLAG_16_BIT) entry_size_in_bytes = 2; 261    else if (flags & BLK_FLAG_WORD) entry_size_in_bytes = WORDSIZE; 262 263    if (flags & BLK_FLAG_MULTIPLE) header_size_in_bytes = BLK_DATA_MULTI_OFFSET; 264    else header_size_in_bytes = BLK_DATA_OFFSET; 265 266    seek_byte_position = pos*entry_size_in_bytes; 267    for (: long_block~=NULL: long_block=long_block-->BLK_NEXT) { 268        chunk_size_in_bytes = FlexSize(long_block) - header_size_in_bytes; 269        if ((seek_byte_position >= 0) && (seek_byte_position<chunk_size_in_bytes)) { 270            long_block = long_block + header_size_in_bytes + seek_byte_position; 271            switch(entry_size_in_bytes) { 272                1: return long_block->0; 273                2: #Iftrue (WORDSIZE == 2); return long_block-->0; 274                    #ifnot; return (long_block->0)*256 + (long_block->1); 275                    #endif; 276                4: return long_block-->0; 277            } 278        } 279        seek_byte_position = seek_byte_position - chunk_size_in_bytes; 280    } 281    "*** BlkValueRead: reading from index out of range: ", pos, " in ", from, " ***"; 282]; 283 284[ BlkValueWrite to pos val do_not_indirect 285    long_block chunk_size_in_bytes header_size_in_bytes flags entry_size_in_bytes seek_byte_position; 286    if (to == 0) rfalse; 287    if (do_not_indirect) 288        long_block = to; 289    else { 290        BlkMakeMutable(to); 291        long_block = BlkValueGetLongBlock(to); 292    } 293 294    flags = long_block->BLK_HEADER_FLAGS; 295    entry_size_in_bytes = 1; 296    if (flags & BLK_FLAG_16_BIT) entry_size_in_bytes = 2; 297    else if (flags & BLK_FLAG_WORD) entry_size_in_bytes = WORDSIZE; 298 299    if (flags & BLK_FLAG_MULTIPLE) header_size_in_bytes = BLK_DATA_MULTI_OFFSET; 300    else header_size_in_bytes = BLK_DATA_OFFSET; 301 302    seek_byte_position = pos*entry_size_in_bytes; 303    for (:long_block~=NULL:long_block=long_block-->BLK_NEXT) { 304        chunk_size_in_bytes = FlexSize(long_block) - header_size_in_bytes; 305        if ((seek_byte_position >= 0) && (seek_byte_position<chunk_size_in_bytes)) { 306            long_block = long_block + header_size_in_bytes + seek_byte_position; 307            switch(entry_size_in_bytes) { 308                1: long_block->0 = val; 309                2: #Iftrue (WORDSIZE == 2); long_block-->0 = val; 310                    #ifnot; long_block->0 = (val/256)%256; long_block->1 = val%256; 311                    #endif; 312                4: long_block-->0 = val; 313            } 314            return; 315        } 316        seek_byte_position = seek_byte_position - chunk_size_in_bytes; 317    } 318    "*** BlkValueWrite: writing to index out of range: ", pos, " in ", to, " ***"; 319];

First Zero Entry.

This returns the entry index of the first zero entry in the long block's array, or -1 if it has no zeros.

326[ BlkValueSeekZeroEntry from 327    long_block chunk_size_in_bytes header_size_in_bytes flags entry_size_in_bytes 328    byte_position addr from_addr to_addr; 329    if (from == 0) return -1; 330    long_block = BlkValueGetLongBlock(from); 331 332    flags = long_block->BLK_HEADER_FLAGS; 333    entry_size_in_bytes = 1; 334    if (flags & BLK_FLAG_16_BIT) entry_size_in_bytes = 2; 335    else if (flags & BLK_FLAG_WORD) entry_size_in_bytes = WORDSIZE; 336 337    if (flags & BLK_FLAG_MULTIPLE) header_size_in_bytes = BLK_DATA_MULTI_OFFSET; 338    else header_size_in_bytes = BLK_DATA_OFFSET; 339 340    byte_position = 0; 341    for (: long_block~=NULL: long_block=long_block-->BLK_NEXT) { 342        chunk_size_in_bytes = FlexSize(long_block) - header_size_in_bytes; 343        from_addr = long_block + header_size_in_bytes; 344        to_addr = from_addr + chunk_size_in_bytes; 345        switch(entry_size_in_bytes) { 346            1: 347                for (addr = from_addr: addr < to_addr: addr++) 348                    if (addr->0 == 0) 349                        return byte_position + addr - from_addr; 350            2: 351                #iftrue (WORDSIZE == 2); 352                for (addr = from_addr: addr < to_addr: addr=addr+2) 353                    if (addr-->0 == 0) 354                        return (byte_position + addr - from_addr)/2; 355                #ifnot; 356                for (addr = from_addr: addr < to_addr: addr=addr+2) 357                    if ((addr->0 == 0) && (addr->1 == 0)) 358                        return (byte_position + addr - from_addr)/2; 359                #endif; 360            4: 361                for (addr = from_addr: addr < to_addr: addr=addr+4) 362                    if (addr-->0 == 0) 363                        return (byte_position + addr - from_addr)/4; 364        } 365        byte_position = byte_position + chunk_size_in_bytes; 366    } 367    return -1; 368];

Mass Copy Entries.

This copies a given number of entries from one BV's long block to another; they must both be of the same word size but can differ in header size. Functionally, it's identical to

for (n=0: n<no_entries_to_copy: n++)
BlkValueWrite(to_bv, n, BlkValueRead(from_bv, n));

but it's much, much faster, and runs in a reasonably small number of cycles given what it needs to do.

382[ BlkValueMassCopyEntries to_bv from_bv no_entries_to_copy 383    from_long_block from_addr from_bytes_left from_header_size_in_bytes 384    to_long_block to_addr to_bytes_left to_header_size_in_bytes 385    bytes_to_copy flags entry_size_in_bytes min; 386 387    BlkMakeMutable(to_bv); 388 389    from_long_block = BlkValueGetLongBlock(from_bv); 390    to_long_block = BlkValueGetLongBlock(to_bv); 391 392    flags = from_long_block->BLK_HEADER_FLAGS; 393    entry_size_in_bytes = 1; 394    if (flags & BLK_FLAG_16_BIT) entry_size_in_bytes = 2; 395    else if (flags & BLK_FLAG_WORD) entry_size_in_bytes = WORDSIZE; 396 397    if ((flags & (BLK_FLAG_MULTIPLE + BLK_FLAG_TRUNCMULT)) && 398        (BlkValueSetLBCapacity(to_bv, no_entries_to_copy) == false)) 399        BlkValueError("copy resizing failed"); 400 401    if (flags & BLK_FLAG_MULTIPLE) from_header_size_in_bytes = BLK_DATA_MULTI_OFFSET; 402    else from_header_size_in_bytes = BLK_DATA_OFFSET; 403    flags = to_long_block->BLK_HEADER_FLAGS; 404    if (flags & BLK_FLAG_MULTIPLE) to_header_size_in_bytes = BLK_DATA_MULTI_OFFSET; 405    else to_header_size_in_bytes = BLK_DATA_OFFSET; 406 407    from_addr = from_long_block + from_header_size_in_bytes; 408    from_bytes_left = FlexSize(from_long_block) - from_header_size_in_bytes; 409    to_addr = to_long_block + to_header_size_in_bytes; 410    to_bytes_left = FlexSize(to_long_block) - to_header_size_in_bytes; 411 412    bytes_to_copy = entry_size_in_bytes*no_entries_to_copy; 413    while (true) { 414        if (from_bytes_left == 0) { 415            from_long_block = from_long_block-->BLK_NEXT; 416            if (from_long_block == 0) BlkValueError("copy destination exhausted"); 417            from_addr = from_long_block + from_header_size_in_bytes; 418            from_bytes_left = FlexSize(from_long_block) - from_header_size_in_bytes; 419        } else if (to_bytes_left == 0) { 420            to_long_block = to_long_block-->BLK_NEXT; 421            if (to_long_block == 0) BlkValueError("copy source exhausted"); 422            to_addr = to_long_block + to_header_size_in_bytes; 423            to_bytes_left = FlexSize(to_long_block) - to_header_size_in_bytes; 424        } else { 425            min = from_bytes_left; if (to_bytes_left < min) min = to_bytes_left; 426            if (bytes_to_copy <= min) { 427                Memcpy(to_addr, from_addr, bytes_to_copy); 428                return; 429            } 430            Memcpy(to_addr, from_addr, min); 431            bytes_to_copy = bytes_to_copy - min; 432            from_addr = from_addr + min; 433            from_bytes_left = from_bytes_left - min; 434            to_addr = to_addr + min; 435            to_bytes_left = to_bytes_left - min; 436        } 437    } 438];

Mass Copy From Array.

The following is helpful when reading an array of characters into a text:

444[ BlkValueMassCopyFromArray to_bv from_array from_entry_size no_entries_to_copy 445    to_long_block to_addr to_entries_left to_header_size to_entry_size 446    flags; 447 448    BlkMakeMutable(to_bv); 449 450    to_long_block = BlkValueGetLongBlock(to_bv); 451 452    flags = to_long_block->BLK_HEADER_FLAGS; 453    to_entry_size = 1; 454    if (flags & BLK_FLAG_16_BIT) to_entry_size = 2; 455    else if (flags & BLK_FLAG_WORD) to_entry_size = WORDSIZE; 456 457    if ((flags & (BLK_FLAG_MULTIPLE + BLK_FLAG_TRUNCMULT)) && 458        (BlkValueSetLBCapacity(to_bv, no_entries_to_copy) == false)) 459        BlkValueError("copy resizing failed"); 460 461    if (flags & BLK_FLAG_MULTIPLE) to_header_size = BLK_DATA_MULTI_OFFSET; 462    else to_header_size = BLK_DATA_OFFSET; 463 464    to_addr = to_long_block + to_header_size; 465    to_entries_left = (FlexSize(to_long_block) - to_header_size)/to_entry_size; 466 467    while (no_entries_to_copy > to_entries_left) { 468        Arrcpy(to_addr, to_entry_size, from_array, from_entry_size, to_entries_left); 469        no_entries_to_copy = no_entries_to_copy - to_entries_left; 470        from_array = from_array + to_entries_left*from_entry_size; 471        to_long_block = to_long_block-->BLK_NEXT; 472        if (to_long_block == 0) BlkValueError("copy source exhausted"); 473        to_addr = to_long_block + to_header_size; 474        to_entries_left = (FlexSize(to_long_block) - to_header_size)/to_entry_size; 475    } 476    if (no_entries_to_copy > 0) { 477        Arrcpy(to_addr, to_entry_size, from_array, from_entry_size, no_entries_to_copy); 478    } 479];

KOVS Routines.

Different kinds of value use different data formats for both their short and long blocks, so it follows that each kind needs its own routines to carry out the fundamental operations of creating, destroying, copying and comparing. This is organised at run-time by giving each kind of block value a "KOVS", a "kind of value support" routine. These are named systematically by suffixing _Support: that is, the support function for TEXT_TY is called TEXT_TY_Support and so on.

I7 automatically compiles a function called KOVSupportFunction which returns the KOVS for a given kind. Note that this depends only on the weak kind, not the strong one: so "list of numbers" and "list of texts", for example, share a common KOVS which handles all list support.

The support function can be called with any of the following task constants as its first argument: it then has a further one to three arguments depending on the task in hand.

500Constant CREATE_KOVS = 1; 501Constant CAST_KOVS = 2; 502Constant DESTROY_KOVS = 3; 503Constant MAKEMUTABLE_KOVS = 4; 504Constant COPYKIND_KOVS = 5; 505Constant EXTENT_KOVS = 6; 506Constant COPYQUICK_KOVS = 7; 507Constant COPYSB_KOVS = 8; 508Constant KINDDATA_KOVS = 9; 509Constant COPY_KOVS = 10; 510Constant COMPARE_KOVS = 11; 511Constant READ_FILE_KOVS = 12; 512Constant WRITE_FILE_KOVS = 13; 513Constant HASH_KOVS = 14; 514Constant DEBUG_KOVS = 15; 515 516! Constant BLKVALUE_TRACE; ! Uncomment this to expose masses of tracery

Creation.

To create a block value, call:

BlkValueCreate(kind)

where K is its (strong) kind ID. Optionally, call:

BlkValueCreate(K, short_block)

to mandate that the short block needs to be located at the given address outside the heap: but don't do this unless you can guarantee that space of the necessary length will be available there for as long as the lifetime of the value; and please note, it really does matter that this address lies outside the heap, for reasons to be seen below.

These work by delegating to:

kovs(CREATE_KOVS, strong_kind, short_block)

which returns the address of the short block for the new value.

540[ BlkValueCreate strong_kind short_block kovs; 541 542    kovs = KOVSupportFunction(strong_kind, "impossible allocation"); 543    short_block = kovs(CREATE_KOVS, strong_kind, short_block); 544 545    #ifdef BLKVALUE_TRACE; print "Created: ", (BlkValueDebug) short_block, "^"; #endif; 546 547    ! The new value is represented in I6 as the pointer to its short block: 548    return short_block; 549];

Errors.

No I7 source text should ever result in a call to this, unless it does unpleasant things at the I6 level.

556[ BlkValueError reason; 557    print "*** Value handling failed: ", (string) reason, " ***^"; 558    RunTimeProblem(RTP_HEAPERROR); 559    @quit; 560];

Short Block Allocation.

The first thing a KOVS does when creating a value is to initialise its short block. The following routines provide for a one-word and a two-word short block respectively. The KOVS should pass these routines the same short_block value it was called with. As can be seen, if this is zero then we need to conjure up memory from somewhere: we do this using Flex. That incurs a fair amount of overhead in time and memory, though. The SB data is stored in the data portion of the Flex block, which is why we get its address from by adding the data offset to the block address.

573[ BlkValueCreateSB1 short_block val; 574    if (short_block == 0) 575        short_block = FlexAllocate(WORDSIZE, 0, BLK_FLAG_WORD) + BLK_DATA_OFFSET; 576    short_block-->0 = val; 577    return short_block; 578]; 579 580[ BlkValueCreateSB2 short_block val1 val2; 581    if (short_block == 0) 582        short_block = FlexAllocate(2*WORDSIZE, 0, BLK_FLAG_WORD) + BLK_DATA_OFFSET; 583    short_block-->0 = val1; short_block-->1 = val2; 584    return short_block; 585];

Block Values On Stack.

As noted above, it's wasteful to keep allocating short blocks using Flex. For the short blocks of block values in local variables, we store them on a stack instead. This is a top-down stack, so the current stack frame starts out just after the end of the stack area in memory (and therefore points to an empty stack frame); it drops down as new frames are created.

BlkValueCreateOnStack acts exactly like BlkValueCreate, but stores the short block at the given word offset in the current stack frame. (I7 compiles calls to these routines when compiling code to manage local variables.)

600[ StackFramingInitialise; 601    I7SFRAME = blockv_stack + WORDSIZE*BLOCKV_STACK_SIZE; 602]; 603 604[ StackFrameCreate size new; 605    new = I7SFRAME - WORDSIZE*size; 606    if (new < blockv_stack) { RunTimeProblem(RTP_HEAPERROR); @quit; } 607    I7SFRAME = new; 608]; 609 610[ BlkValueCreateOnStack offset strong_kind; 611    BlkValueCreate(strong_kind, I7SFRAME + WORDSIZE*offset); 612]; 613 614[ BlkValueFreeOnStack offset; 615    BlkValueFree(I7SFRAME + WORDSIZE*offset); 616];

Freeing.

As noted above, every value returned by BlkValueCreate must later be freed by calling the following routine exactly once:

BlkValueFree(value)

In particular, if a block value is stored in any I6 location which is about to be overwritten with a new value, it's essential to call this in order properly to dispose of the old value.

As noted above, short blocks are sometimes created within Flex blocks on the heap, using FlexAllocate; and if this is one of those, we need to free the relevant Flex block.

633[ BlkValueFree bv kovs d; 634    if (bv == 0) return; 635 636    ! Dispose of any data in the long block 637    kovs = KOVSupportFunction(BlkValueWeakKind(bv), "impossible deallocation"); 638    BlkValueDestroyPrimitive(bv, kovs); 639 640    ! Free any heap memory occupied by the short block 641    d = bv - Flex_Heap; 642    if ((d >= 0) && (d < MEMORY_HEAP_SIZE + 16)) 643        FlexFree(bv - BLK_DATA_OFFSET); 644];

Quick Copy.

The basic method of copying block value B into block value A is to destroy the old contents of A, which are about to be overwritten; then copy the short block of A into the short block of B, a quick process; and increment the reference count of B.

The support function should respond to:

kovs(COPYSB_KOVS, to_bv, from_bv)

by copying the short blocks alone.

659[ BlkValueQuickCopyPrimitive to_bv from_bv kovs; 660    BlkValueDestroyPrimitive(to_bv, kovs); 661    kovs(COPYSB_KOVS, to_bv, from_bv); 662    BlkValueIncRefCountPrimitive(from_bv); 663];

Short Block Copy.

In fact, most of the work of copying a short block is standard. If a SB consists only a single word pointing to the LB, the first routine below handles all the work needed to handle the COPYSB_KOVS task. The surprising line in this routine is to deal with the convention that a pointer value of 0 means the LB immediately follows the SB: if that's true in from_bv, it won't be true in to_bv, so we must correct it.

674[ BlkValueCopySB1 to_bv from_bv; 675    to_bv-->0 = from_bv-->0; 676    if (to_bv-->0 == 0) to_bv-->0 = from_bv + WORDSIZE; 677]; 678 679[ BlkValueCopySB2 to_bv from_bv; 680    to_bv-->0 = from_bv-->0; 681    to_bv-->1 = from_bv-->1; 682    if (to_bv-->1 == 0) to_bv-->1 = from_bv + 2*WORDSIZE; 683];

Slow Copy.

Why don't we always do this? Consider the case where B is a list of rooms, and A is a list of objects. If we give A's short block a pointer to the long block of B, A will suddenly change its kind as well as its contents, because the strong kind of a list is stored inside the long block. So there are a few cases where it's not safe to make a quick copy. In any case, sooner or later you have to duplicate actual data, not just rearrange pointers to it, and here's where.

We first call:

kovs(KINDDATA_KOVS, to_bv)

which asks for an ID for the kinds stored in the BV: for example, for a list of rooms it would return the kind ID for "room". We ask for this because it's information stored in the long block, which is about to be overwritten.

As with the quick copy, we must now make sure any data currently in the destination is properly destroyed. We could do so by making the destination mutable and then destroying its contents, but that would be inefficient, in that it might create a whole lot of temporary copies and then delete them again. So if the long block has a high reference count, we decrement it and then replace the short block (in place) with a fresh one pointing to empty data; we only destroy the contents if the long block has reference count 1.

All of which finally means we can scribble over the destination without spoiling anybody else's day. We resize it to make room for the incoming data; we copy the raw data of the long block; and finally we:

kovs(COPY_KOVS, to_bv, from_bv, k)

This is where the KOVS should make a proper copy, using BlkValueCopy and thus perhaps recursing, if any of that data contained block values in turn: as for instance it will if we're copying a list of texts. Note that k is the value given us by KINDDATA_KOVS.

724[ BlkValueSlowCopyPrimitive to_bv from_bv kovs recycling 725    k from_long_block no_entries_to_copy; 726    k = kovs(KINDDATA_KOVS, to_bv, from_bv); 727 728    from_long_block = BlkValueGetLongBlock(from_bv); 729    if (from_long_block) { 730        if (recycling) BlkValueRecyclePrimitive(to_bv, kovs); 731        no_entries_to_copy = kovs(EXTENT_KOVS, from_bv); 732        if (no_entries_to_copy == -1) no_entries_to_copy = BlkValueLBCapacity(from_bv); 733        BlkValueMassCopyEntries(to_bv, from_bv, no_entries_to_copy); 734!print "So to: "; BlkValueDebug(to_bv); print "^"; 735 736    } 737 738    kovs(COPY_KOVS, to_bv, from_bv, k); 739!print "Whence to: "; BlkValueDebug(to_bv); print "^"; 740];

Copy.

As noted above, some copies are quick, and some are slow. We decide by asking the kind's support function:

kovs(COPYQUICK_KOVS, to_bv, from_bv)

which should return true if a quick copy is okay, or false if not.

751[ BlkValueCopy to_bv from_bv to_kind from_kind kovs; 752    if (to_bv == 0) BlkValueError("copy to null value"); 753    if (from_bv == 0) BlkValueError("copy from null value"); 754    if (to_bv == from_bv) return; 755 756    #ifdef BLKVALUE_TRACE; 757    print "Copy: ", (BlkValueDebug) to_bv, " to equal ", (BlkValueDebug) from_bv, "^"; 758    #endif; 759 760    to_kind = BlkValueWeakKind(to_bv); 761    from_kind = BlkValueWeakKind(from_bv); 762    if (to_kind ~= from_kind) BlkValueError("copy incompatible kinds"); 763 764    kovs = KOVSupportFunction(to_kind, "impossible copy"); 765     766    if (kovs(COPYQUICK_KOVS, to_bv, from_bv)) 767        BlkValueQuickCopyPrimitive(to_bv, from_bv, kovs); 768    else 769        BlkValueSlowCopyPrimitive(to_bv, from_bv, kovs, true); 770 771    return to_bv; 772]; 773 774[ BlkValueCopyAZ to_bv from_bv; 775    if (from_bv) return BlkValueCopy(to_bv, from_bv); 776    return to_bv; 777];

Destruction.

We will also need primitives for two different forms of destruction. This is something which should happen whenever a block value is thrown away, not to be used again: either because it's being freed, or because new contents are being copied into it.

The idea of destruction is that any data stored in the long block should safely be disposed of. If the reference count of the long block is 2 or more, there's no problem, because we can simply decrement the count and let other people worry about the data from now on. But if it's only 1, then destroying the data is on us. Since we don't know what's in the long block, we have to ask the KOVS to do this by means of:

kovs(DESTROY_KOVS, bv)

Note that all of this frequently causes recursion: destruction leads to freeing of some of the data, which in turn means that that data must be destroyed, and so on. So it's essential that block values be well-founded: a list must not, for example, contain itself.

800[ BlkValueDestroyPrimitive bv kovs long_block; 801    #ifdef BLKVALUE_TRACE; print "Destroying ", (BlkValueDebug) bv, "^"; #endif; 802    if (BlkValueDecRefCountPrimitive(bv) == 0) { 803        kovs(DESTROY_KOVS, bv); 804        long_block = BlkValueGetLongBlock(bv); 805        if (long_block) FlexFree(long_block); 806    } 807];

Recycling.

This is like destruction in that it disposes of the value safely, but it tries to keep the long block for reuse, rather than deallocating it. This won't work if other people are still using it, so in the case where its reference count is 2 or more, we simply reduce the count by 1 and then replace the small block with a new one (at the same address).

817[ BlkValueRecyclePrimitive bv kovs; 818    #ifdef BLKVALUE_TRACE; print "Recycling ", (BlkValueDebug) bv, "^"; #endif; 819    if (BlkValueDecRefCountPrimitive(bv) == 0) { 820        kovs(DESTROY_KOVS, bv); 821        BlkValueIncRefCountPrimitive(bv); 822    } else { 823        BlkValueCreate(BlkValueWeakKind(bv), bv); 824    } 825];

Mutability.

A block value is by definition mutable if it has a long block with reference count 1, because then the data in the long block can freely be changed without corrupting other block values.

We offer the KOVS a chance to handle this for us:

kovs(MAKEMUTABLE_KOVS, bv)

should return 0 to say that it has done so, or else return the size of the short block in words to ask us to handle it. The way we do this is to create a temporary value to make a safe copy into; it would be unnecessarily slow to allocate the short block for this safe copy on the heap and then free it again moments later, so instead we put the short block on the stack, making a temporary one-value stack frame instead to hold it.

844[ BlkMakeMutable bv block bv_kind kovs sb_size; 845    if (bv == 0) BlkValueError("tried to make null block mutable"); 846 847    if (BlkValueGetRefCountPrimitive(bv) > 1) { 848        #ifdef BLKVALUE_TRACE; print "Make mutable: ", (BlkValueDebug) bv, "^"; #endif; 849 850        BlkValueDecRefCountPrimitive(bv); 851 852        bv_kind = BlkValueWeakKind(bv); 853        kovs = KOVSupportFunction(bv_kind, "impossible mutability"); 854 855        sb_size = kovs(MAKEMUTABLE_KOVS, bv); 856        if (sb_size > 0) { 857            @push I7SFRAME; 858            StackFrameCreate(sb_size); 859            BlkValueCreateOnStack(0, bv_kind); 860            kovs(COPYKIND_KOVS, I7SFRAME, bv); 861            BlkValueSlowCopyPrimitive(I7SFRAME, bv, kovs, false); 862            kovs(COPYSB_KOVS, bv, I7SFRAME); 863            @pull I7SFRAME; 864        } 865    } 866];

Casting.

We can also perform an assignment to an already-created block value in the form of a cast, that is, a conversion of data from one kind to another: or at least, for some kinds of value we can.

kovs(CAST_KOVS, to_bv, original_kind, original_value)

casts from the given value, with the given kind, into the existing block value to_bv. Note that the source value doesn't need to be a BV itself. This mechanism is used, for example, to cast snippets to text.

880[ BlkValueCast to_bv original_kind original_value kovs; 881    kovs = KOVSupportFunction(BlkValueWeakKind(to_bv), "impossible cast"); 882    kovs(CAST_KOVS, to_bv, original_kind, original_value); 883    return to_bv; 884];

Comparison.

And it's a similar story with comparison:

kovs(COMPARE_KOVS, bv_left, bv_right)

looks at the data in the two BVs and returns 0 if they are equal, a positive number if bv_right is "greater than" bv_left, and a negative number if not. The interpretation of "greater than" depends on the kind, but should be something which the user would find natural.

897[ BlkValueCompare bv_left bv_right kind_left kind_right kovs; 898    if ((bv_left == 0) && (bv_right == 0)) return 0; 899    if (bv_left == 0) return 1; 900    if (bv_right == 0) return -1; 901 902    kind_left = BlkValueWeakKind(bv_left); 903    kind_right = BlkValueWeakKind(bv_right); 904    if (kind_left ~= kind_right) return kind_left - kind_right; 905 906    kovs = KOVSupportFunction(kind_left, "impossible comparison"); 907    return kovs(COMPARE_KOVS, bv_left, bv_right); 908];

Hashing.

Given a value of any kind, assign it a hash code which fits in a single virtual machine word, maximizing the chances that two different values will have different hash codes.

If the value can be stored in a single word already, it can be its own hash code. Otherwise, we ask:

kovs(HASH_KOVS, bv)

to return one for us. Whatever this does, it must at minimum have the property that two equivalent blocks (for which COMPARE_KOVS returns 0) produce the same hash value.

925[ GetHashValue kind value; 926    if (KOVIsBlockValue(kind)) return BlkValueHash(value); 927    return value; 928]; 929 930[ BlkValueHash bv bv_kind kovs; 931    if (bv == 0) return 0; 932    bv_kind = BlkValueWeakKind(bv); 933    kovs = KOVSupportFunction(bv_kind, "impossible hashing"); 934    return kovs(HASH_KOVS, bv); 935];

Serialisation.

Some block values can be written to external files (on Glulx): others cannot. The following routines abstract that.

If ch is -1, then:

kovs(READ_FILE_KOVS, bv, auxf, ch)

returns true or false according to whether it is possible to read data from an auxiliary file auxf into the block value bv. If ch is any other value, then the routine should do exactly that, taking ch to be the first character of the text read from the file which makes up the serialised form of the data.

kovs(WRITE_FILE_KOVS, bv)

is simpler because, strictly speaking, it doesn't write to a file at all: it simply prints a serialised form of the data in bv to the output stream. Since it is called only when that output stream has been redirected to an auxiliary file, and since the serialised form would often be illegible on screen, it seems reasonable to call it a file input-output function just the same. The WRITE_FILE_KOVS should return true or false according to whether it was able to write the data.

962[ BlkValueReadFromFile bv auxf ch bv_kind kovs; 963    kovs = KOVSupportFunction(bv_kind); 964    if (kovs) return kovs(READ_FILE_KOVS, bv, auxf, ch); 965    rfalse; 966]; 967 968[ BlkValueWriteToFile bv bv_kind kovs; 969    kovs = KOVSupportFunction(bv_kind); 970    if (kovs) return kovs(WRITE_FILE_KOVS, bv); 971    rfalse; 972];

Debugging.

Surprisingly, the above system of reference-counted double indirection didn't work first time, so it turned out to be useful to have these routines on hand.

979[ BlkValueDebug bv flag refc long_block kovs; 980    print "(BV"; 981    if (bv) { 982        BlkDebugAddress(bv, flag); 983        long_block = BlkValueGetLongBlock(bv); 984        if (long_block) { 985            if (bv-->0 == 0) print "..."; else print "-->"; 986            print "L"; BlkDebugAddress(long_block, flag); 987            print " 2**", long_block->BLK_HEADER_N; 988            refc = BlkValueGetRefCountPrimitive(bv); 989            if (refc == RC_INFINITY) print " resident"; 990            else { print " ", refc, " ref"; if (refc ~= 1) print "s"; } 991        } 992        kovs = KOVSupportFunction(BlkValueWeakKind(bv)); 993        if (kovs) kovs(DEBUG_KOVS, bv); 994    } 995    print ")"; 996];

Printing Memory Addresses.

The point of the anonymity flag is that, with this set, the output can be used as the required output in an Inform test case without tiny movements in memory between builds invalidating this required output.

1004[ BlkDebugAddress addr flag d; 1005    if (flag) { print "###"; return; } 1006 1007    d = addr - blockv_stack; 1008    if ((d >= 0) && (d <= WORDSIZE*BLOCKV_STACK_SIZE)) { 1009        print "s+", (BlkPrintHexadecimal) d; 1010        d = addr - I7SFRAME; 1011        print "=f"; if (d >= 0) print "+"; print d; 1012        return; 1013    } 1014     1015    d = addr - Flex_Heap; 1016    if ((d >= 0) && (d < MEMORY_HEAP_SIZE + 16)) { 1017        print "h+", (BlkPrintHexadecimal) d; 1018        return; 1019    } 1020 1021    print (BlkPrintHexadecimal) addr; 1022];

Hexadecimal Printing.

1027[ BlkPrintHexadecimal v; 1028    #iftrue WORDSIZE == 4; 1029    if (v & $ffff0000) { 1030        if (v & $ff000000) { 1031            BlkPrintHexDigit(v / $10000000); 1032            BlkPrintHexDigit(v / $1000000); 1033        } 1034        BlkPrintHexDigit(v / $100000); 1035        BlkPrintHexDigit(v / $10000); 1036    } 1037    #endif; 1038    BlkPrintHexDigit(v / $1000); 1039    BlkPrintHexDigit(v / $100); 1040    BlkPrintHexDigit(v / $10); 1041    BlkPrintHexDigit(v); 1042]; 1043 1044[ BlkPrintHexDigit v; 1045    v = v & $F; 1046    if (v < 10) print v; else print (char) 'A' + v - 10; 1047];