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;
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;
108Constant BLK_BVBITMAP_TEXT = $20;
109Constant BLK_BVBITMAP_CONSTANT = $40;
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
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
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
637 kovs = KOVSupportFunction(BlkValueWeakKind(bv), "impossible deallocation");
638 BlkValueDestroyPrimitive(bv, kovs);
639
640
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
735
736 }
737
738 kovs(COPY_KOVS, to_bv, from_bv, k);
739
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];
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];