Possible memory leak from DoubleHashSetRelationHandler?

I’ve been looking at RelationKind.i6t, and there is a curiosity that I don’t understand in the routine DoubleHashSetRelationHandler():

[ DoubleHashSetRelationHandler rel task X Y sym  kov kx ky at tmp v;
	    kov = BlkValueRead(rel, RRV_KIND);
	    kx = KindBaseTerm(kov, 0); ky = KindBaseTerm(kov, 1);
	    if (task == RELS_SET_VALENCY) {
	            return RELATION_TY_SetValency(rel, X);
	    } else if (task == RELS_DESTROY) {
	            ! clear
	            kx = KOVIsBlockValue(kx); ky = KOVIsBlockValue(ky);
	            if (~~(kx || ky)) return;
	            for (at = BlkValueRead(rel, RRV_STORAGE): at >= 0: at--) {	! <--- field index decremented here
	                    tmp = BlkValueRead(rel, RRV_DATA_BASE + 3*at);
	                    if (tmp & RRF_USED) {
	                            if (kx) BlkValueFree(BlkValueRead(rel, RRV_DATA_BASE + 3*at + 1));
	                            if (ky) BlkValueFree(BlkValueRead(rel, RRV_DATA_BASE + 3*at + 2));
	                    }
	                    at--;		! <-- why also decrement here?
	            }
	            return;
	    } else if (task == RELS_COPY)
		...

In short: While handling a RELS_DESTROY task, the for loop decrements the field index at twice in each cycle. Similar loops (both for and while) appear in several other places in the template, but this is the only one that decrements twice in this manner.

Wouldn’t double-decrementing each pass result in skipped entries, and wouldn’t freeing only every other entry of the relation result in orphan block values due to the skipped entries?

2 Likes

Compare the above to the handling of the RELS_EMPTY task later in the routine:

		...
	    } else if (task == RELS_EMPTY) {
	            if (BlkValueRead(rel, RRV_USED) == 0) rtrue;
	            if (X == 1) {
	                    DoubleHashSetRelationHandler(rel, RELS_DESTROY);			! <--- applies RELS_DESTROY logic
	                    for (at = BlkValueRead(rel, RRV_STORAGE): at >= 0: at--) {	! <--- field index decremented once per loop
	                            tmp = RRV_DATA_BASE + 3*at;
	                            BlkValueWrite(rel, tmp, 0);
	                            BlkValueWrite(rel, tmp + 1, 0);		! <--- assumes X block value already freed
	                            BlkValueWrite(rel, tmp + 2, 0);		! <--- assumes Y block value already freed
	                    }
	                    BlkValueWrite(rel, RRV_USED, 0);
	                    BlkValueWrite(rel, RRV_FILLED, 0);
	                    rtrue;
	            }
	            rfalse;
	    } else if (task == RELS_LOOKUP_ANY) {
		...

Also compare to the RELS_COPY clause in the same routine, which contains a while loop that handles each entry individually and creates new block values for each entry:

		...
	    } else if (task == RELS_COPY) {
	            X = KOVIsBlockValue(kx); Y = KOVIsBlockValue(ky);
	            if (~~(X || Y)) return;
	            at = BlkValueRead(rel, RRV_STORAGE);
	            while (at >= 0) {
	                    tmp = BlkValueRead(rel, RRV_DATA_BASE + 3*at);
	                    if (tmp & RRF_USED) {
	                            if (X) {
	                                    tmp = BlkValueRead(rel, RRV_DATA_BASE + 3*at + 1);
	                                    tmp = BlkValueCopy(BlkValueCreate(kx), tmp);	! <--- new block value for X
	                                    BlkValueWrite(rel, RRV_DATA_BASE + 3*at + 1, tmp);
	                            }
	                            if (Y) {
	                                    tmp = BlkValueRead(rel, RRV_DATA_BASE + 3*at + 2);
	                                    tmp = BlkValueCopy(BlkValueCreate(ky), tmp);	! <--- new block value for Y
	                                    BlkValueWrite(rel, RRV_DATA_BASE + 3*at + 2, tmp);
	                            }
	                    }
	                    at--;	! <--- field index decremented once per loop
	            }
	            return;
	    } else if (task == RELS_SHOW) {
		...

Am I correct in thinking that if relation B is copied to B’, then even if the skipped-entries logic is valid for B, it would not be valid for B’?

I worked up an example to demonstrate that the problem is real:

"Leak Demo"

Place is a room. 

 [temporary relation will be automatically destroyed at end of phrase]
After jumping:
	let R be a relation of numbers to texts;
	now R relates 4 to "score and seven days ago";
	now R relates 6 to "ways to Sunday";
	now R relates 7 to "up";
	now R relates 7 to "eleven";
	now R relates 8 to "days a week";
	show relation R;
	now R is empty. [demonstrates mismatch between loop index handling]

Test me with "showheap / jump / showheap / jump / showheap / jump / g / g / g / showheap".

Stopping the decrement of at at the end of the RELS_DESTROY loop seems to fix the issue:

[ DoubleHashSetRelationHandler rel task X Y sym  kov kx ky at tmp v;
	    kov = BlkValueRead(rel, RRV_KIND);
	    kx = KindBaseTerm(kov, 0); ky = KindBaseTerm(kov, 1);
	    if (task == RELS_SET_VALENCY) {
	            return RELATION_TY_SetValency(rel, X);
	    } else if (task == RELS_DESTROY) {
	            ! clear
	            kx = KOVIsBlockValue(kx); ky = KOVIsBlockValue(ky);
	            if (~~(kx || ky)) return;
	            for (at = BlkValueRead(rel, RRV_STORAGE): at >= 0: at--) {
	                    tmp = BlkValueRead(rel, RRV_DATA_BASE + 3*at);
	                    if (tmp & RRF_USED) {
	                            if (kx) BlkValueFree(BlkValueRead(rel, RRV_DATA_BASE + 3*at + 1));
	                            if (ky) BlkValueFree(BlkValueRead(rel, RRV_DATA_BASE + 3*at + 2));
	                    }
	                    at--; ! <--- COMMENTING OUT OR REMOVING THIS LINE FIXES LEAK
	            }
	            return;
	    } else if (task == RELS_COPY) {
		...
2 Likes

Cool spot and fix.

Although surely that should be “score and seven years ago”?

Even speaking from the old side of the pond :wink:

1 Like

Actually, it should have been “score and seven minutes ago”… I was thinking of Lincoln’s lesser-known San Dimas address.

5 Likes