From abe68c0844dd4b44ff148edfecc15a60996c78a1 Mon Sep 17 00:00:00 2001 From: Christopher Haster Date: Mon, 25 Mar 2024 01:04:59 -0500 Subject: [PATCH] rbyd-rr: Reworking rbyd range removal to try to preserve rby structure This is the start of (yet another) rework of rybd range removals, this time in an effort to preserve the rby structure that maps to a balanced 2-3-4 tree. Specifically, the property that all search paths have the same number of black edges (2-3-4 nodes). This is currently incomplete, as you can probably tell from the mess, but this commit at least gets a working altn/alta encoding in place necessary for representing empty 2-3-4 nodes. More on that below. --- First the problem: My assumption, when implementing the previous range removal algorithms, was that we only needed to maintain the existing height of the tree. The existing rbyd operations limit the height to strictly log n. And while we can't _reduce_ the height to maintain perfect balance, we can at least avoid _increasing_ the height, which means the resulting tree should have a height <= log n. Since our rbyds are bounded by the block_size b, this means worst case our rbyd can never exceed a height <= log b, right? Well, not quite. This is true the instance after the remove operation. But there is an implicit assumption that future rbyd operations will still be able to maintain height <= log n after the remove operation. This turns out to not be true. The problem is that our rbyd appends only maintain height <= log n if our rby structure is preserved. If the rby structure is broken, rbyd append assumes an rby structure that doesn't exist, which can lead to an increasingly unbalanced tree. Consider this happily balanced tree: .-------o-------. .--------o .---o---. .---o---. .---o---. | .-o-. .-o-. .-o-. .-o-. .-o-. .-o-. | .o. .o. .o. .o. .o. .o. .o. .o. .o. .o. .o. .o. | a b c d e f g h i j k l m n o p => a b c d e f g h i '------+------' remove After a range removal it looks pretty bad, but note the height is still <= log n (old n not the new n). We are still <= log b. But note what happens if we start to insert attrs into the short half of the tree: .--------o .---o---. | .-o-. .-o-. | .o. .o. .o. .o. | a b c d e f g h i .-----o .--------o .-+-r .---o---. | | | | .-o-. .-o-. | | | | .o. .o. .o. .o. | | | | a b c d e f g h i j'k'l' .-------------o .---o .---+-----r .--------o .-o .-o .-o .-+-r .---o---. | | | | | | | | | | .-o-. .-o-. | | | | | | | | | | .o. .o. .o. .o. | | | | | | | | | | a b c d e f g h i j'k'l'm'n'o'p'q'r' Our right side is generating a perfectly balanced tree as expected, but the left side is suddenly twice as far from the root! height(r')=3, height(a)=6! The problem is when we append l', we don't really know how tall the tree is. We only know l' has one black edge, which assuming rby structure is preserved, means all other attrs must have one black edge, so creating a new root is justified. In reality this just makes the tree grow increasingly unbalanced, increasing the height of the tree by worst case log n every range removal. --- It's interesting to note this was discovered while debugging test_fwrite_overwrite, specifically: test_fwrite_overwrite:1181h1g2i1gg2l15o10p11r1gg8s10 It turns out the append fragments -> delete fragments -> append/carve block + becksum loop contains the perfect sequence of attrs necessary to turn this tree inbalance into a linked-list! .-> 0 data w1 1 .-b-> 1 data w1 1 | .-> 2 data w1 1 .-b-b-> 3 data w1 1 | .-> 4 data w1 1 | .-b-> 5 data w1 1 | | .-> 6 data w1 1 .---b-b-b-> 7 data w1 1 | .-> 8 data w1 1 | .-b-> 9 data w1 1 | | .-> 10 data w1 1 | .-b-b-> 11 data w1 1 | .-b-----> 12 data w1 1 .-y-y-------> 13 data w1 1 | .-> 14 data w1 1 .-y---------y-> 15 data w1 1 | .-> 16 data w1 1 .-y-----------y-> 17 data w1 1 | .-> 18 data w1 1 .-y-------------y-> 19 data w1 1 | .-> 20 data w1 1 .-y---------------y-> 21 data w1 1 | .-> 22 data w1 1 .-y-----------------y-> 23 data w1 1 | .-> 24 data w1 1 .-y-------------------y-> 25 data w1 1 | .---> 26 data w1 1 | | .-> 27-2047 block w2021 10 b-------------------r-b-> becksum 5 Note, to reproduce this you need to step through with a breakpoint on lfsr_bshrub_commit. This only shows up in the file's intermediary btree, which at the time of writing ends up at block 0xb8: $ ./scripts/test.py \ test_fwrite_overwrite:1181h1g2i1gg2l15o10p11r1gg8s10 \ -ddisk --gdb -f $ ./scripts/watch.py -Kdisk -b \ ./scripts/dbgrbyd.py -b4096 disk 0xb8 -t (then b lfsr_bshrub_commit and continue a bunch) --- So, we need to preserve the rby structure. Note pruning red/yellow alts is not an issue. These aren't black, so we aren't changing the number of black edges in the tree. We've just effectively reduced a 3/4 node into a 2/3 node: .-> a .---b-> b .-> a <- 2 black | .---> c .-b-> b | | .-> d | .-> c b-r-b-> e <- rm => b-b-> d <- 2 black The tricky bit is pruning black alts. Naively this changes the number of black edges/2-3-4 nodes in the tree, which is bad: .-> a .-b-> b .-> a <- 2 black | .-> c .-b-> b b-b-> d <- rm => b---> c <- 1 black It's tempting to just make the alt red at this point, effectively merging the sibling 2-3-4 node. This maintains balance in the subtree, but still removes a black edge, causing problems for our parent: .-> a .-b-> b .-> a <- 3 black | .-> c .-b-> b .-b-b-> d | .-> c | .-> e .-b-b-> d | .-b-> f | .---> e | | .-> g | | .-> f b-b-b-> h <- rm => b-r-b-> g <- 2 black In theory you could propagate this all the way up to the root, and this _would_ probably give you a perfect self-balancing range removal algorithm... but it's recursive... and littlefs can't be recursive... .-> s .-b-> t .-> s | .-> u .-----b-> t .-b-b-> v | .-> u | .-> w | .---b-> v | .-b-> x | | .---> w | | | | .-> y | | | | | | | .-> x b-b- ... b-b-b-> z <- rm => r-b-r-b- ... r-b-r-b-> y So instead, an alternative solution. What if we allowed black alts that point nowhere? A sort of noop 2-3-4 node that serves only to maintain the rby structure? .-> a .-b-> b .-> a <- 2 black | .-> c .-b-> b b-b-> d <- rm => b-b-> c <- 2 black I guess that would technically make this 1-2-3-4 tree. This does add extra overhead for writing noop alts, which are otherwise useless, but it seems to solve most of our problems: 1. does not increase the height of the tree, 2. maintains the rby structure, 3. tail-recursive. And, thanks to the preserved rby structure, we can say that in the worst case our rbyds will never exceed height <= log b again, even with range removals. If we apply this strategy to our original example, you can see how the preserved rby structure sort of "absorbs" new red alts, preventing further unbalancing: .-------o-------. .--------o .---o---. .---o---. .---o---. o .-o-. .-o-. .-o-. .-o-. .-o-. .-o-. o .o. .o. .o. .o. .o. .o. .o. .o. .o. .o. .o. .o. o a b c d e f g h i j k l m n o p => a b c d e f g h i '------+------' remove Reinserting: .--------o .---o---. o .-o-. .-o-. o .o. .o. .o. .o. o a b c d e f g h i .----------------o .---o---. o .-o-. .-o-. .------o .o. .o. .o. .o. .o. .-+-r a b c d e f g h i j'k'l'm' .----------------------------o .---o---. .-------------o .-o-. .-o-. .---o .---+-----r .o. .o. .o. .o. .-o .-o .-o .-o .-+-r a b c d e f g h i j'k'l'm'n'o'p'q'r's' Much better! --- This commit makes some big steps towards this solution, mainly codifying a now-special alt-never/alt-always (altn/alta) encoding to represent these noop 1 nodes. Technically, since null (0) tags are not allowed, these already exist as altle 0/altgt 0 and don't need any extra carve-out encoding-wise: LFSR_TAG_ALT 0x4kkk v1dc kkkk -kkk kkkk LFSR_TAG_ALTN 0x4000 v10c 0000 -000 0000 LFSR_TAG_ALTA 0x6000 v11c 0000 -000 0000 We actually already used altas to terminate unreachable tags during range removals, but this behavior was implicit. Now, altns have very special treatment as a part of determining bounds during appendattr (both unreachable gt/le alts are represented as altns). For this reason I think the new names are warranted. I've also added these encodings to the dbg*.py scripts for, well, debuggability, and added a special case to dbgrby.py -j to avoid unnecessary altn jump noise. As a part of debugging, I've also extended dbgrbyd.py's tree renderer to show trivial prunable alts. Unsure about keeping this. On one hand it's useful to visualize the exact alt structure, on the other hand it likely adds quite a bit of noise to the more complex dbg scripts. The current state of things is a mess, but at least tests are passing! Though we aren't actually reclaiming any altns yet... We're definitely _not_ preserving the rby structure at the moment, and if you look at the output from the tests, the resulting tree structure is hilarious bad. But at least the path forward is clear. --- lfs.c | 105 ++++++++++++++++++++++++++++++++++++++------ scripts/dbgbmap.py | 18 ++------ scripts/dbgbtree.py | 27 +++++------- scripts/dbglfs.py | 27 +++++------- scripts/dbgmtree.py | 27 +++++------- scripts/dbgrbyd.py | 30 +++++-------- 6 files changed, 137 insertions(+), 97 deletions(-) diff --git a/lfs.c b/lfs.c index 2c3b35d0..955af912 100644 --- a/lfs.c +++ b/lfs.c @@ -884,11 +884,13 @@ static inline bool lfsr_tag_follow( if (lfsr_tag_isgt(alt)) { return rid > upper - (lfsr_srid_t)weight - 1 || (rid == upper - (lfsr_srid_t)weight - 1 - && lfsr_tag_key(tag) > lfsr_tag_key(alt)); + && (lfsr_tag_key(alt) == 0 + || lfsr_tag_key(tag) > lfsr_tag_key(alt))); } else { return rid < lower + (lfsr_srid_t)weight - 1 || (rid == lower + (lfsr_srid_t)weight - 1 - && lfsr_tag_key(tag) <= lfsr_tag_key(alt)); + && (lfsr_tag_key(alt) != 0 + && lfsr_tag_key(tag) <= lfsr_tag_key(alt))); } } @@ -946,14 +948,15 @@ static inline void lfsr_tag_trim( lfsr_tag_t alt, lfsr_rid_t weight, lfsr_srid_t *lower_rid, lfsr_srid_t *upper_rid, lfsr_tag_t *lower_tag, lfsr_tag_t *upper_tag) { + LFS_ASSERT((lfsr_srid_t)weight >= 0); if (lfsr_tag_isgt(alt)) { *upper_rid -= weight; - if (upper_tag) { + if (upper_tag && lfsr_tag_key(alt) != 0) { *upper_tag = alt + 1; } } else { *lower_rid += weight; - if (lower_tag) { + if (lower_tag && lfsr_tag_key(alt) != 0) { *lower_tag = alt + 1; } } @@ -2728,6 +2731,8 @@ static int lfsr_rbyd_appendattr(lfs_t *lfs, lfsr_rbyd_t *rbyd, return 0; } + printf("%04x->%04x: appendattr:\n", lfsr_rbyd_trunk(rbyd), rbyd->eoff); + // begin appending int err = lfsr_rbyd_prepareappend(lfs, rbyd); if (err) { @@ -2848,12 +2853,20 @@ again:; if (d < 0) { return d; } + printf("%04x->%04x: tag 0x%x w%d (%d %d)\n", + branch, + rbyd->eoff, + alt, + weight, + lower_rid, + upper_rid); // found an alt? if (lfsr_tag_isalt(alt)) { // make jump absolute jump = branch - jump; lfs_size_t branch_ = branch + d; + bool diverged_this_alt = false; // do bounds want to take different paths? begin diverging if (!lfsr_d_isdiverged(d_state) @@ -2878,9 +2891,20 @@ again:; // begin diverging } else { d_state = lfsr_d_diverge(d_state); + printf("%04x->%04x: diverging 0x%x w%d\n", + branch, + rbyd->eoff, + alt, + weight); // stitch together diverged branches if (d_state == LFSR_D_DIVERGEDUPPER && d_tag) { + printf("%04x->%04x: stitching: 0x%x w%d, 0x%x\n", + branch, + rbyd->eoff, + d_tag, + d_rid - lower_rid, + d_branch); err = lfsr_rbyd_p_push(lfs, rbyd, p_alts, p_weights, p_jumps, LFSR_TAG_ALT(LFSR_TAG_LE, LFSR_TAG_B, d_tag), @@ -2890,9 +2914,15 @@ again:; return err; } } + + diverged_this_alt = true; } } +// if (lfsr_d_isdiverged(d_state)) { +// alt &= ~LFSR_TAG_R; +// } + // prune? // b // .-'| .-'| @@ -2936,26 +2966,57 @@ again:; &lower_rid, &upper_rid, &lower_tag, &upper_tag); + // red alts we can collapse if (lfsr_tag_isred(p_alts[0])) { + printf("%04x->%04x: rprune 0x%x w%d\n", + branch, + rbyd->eoff, + alt, + weight); alt = p_alts[0] & ~LFSR_TAG_R; weight = p_weights[0]; jump = p_jumps[0]; lfsr_rbyd_p_pop(p_alts, p_weights, p_jumps); + + // black alts just become unreachable, if we pruned these + // it would break the coloring of our tree } else { - // propagate pruning to yellow splits to avoid issues - // with tail-recursive recoloring - if (lfsr_d_isdiverged(d_state)) { - d_pruned = true; + if (d_state == LFSR_D_DIVERGINGLOWER || diverged_this_alt) { + printf("%04x->%04x: dprune 0x%x w%d\n", + branch, + rbyd->eoff, + alt, + weight); + // propagate pruning to yellow splits to avoid issues + // with tail-recursive recoloring + if (lfsr_d_isdiverged(d_state)) { + d_pruned = true; + } + y_branch = branch; + branch = branch_; + continue; + } else { + printf("%04x->%04x: bprune 0x%x w%d\n", + branch, + rbyd->eoff, + alt, + weight); + alt = LFSR_TAG_ALT(LFSR_TAG_LE, LFSR_TAG_B, 0); + weight = 0; + // jump = 0; } - y_branch = branch; - branch = branch_; - continue; } } // two reds makes a yellow, split? if (lfsr_tag_isred(alt) && lfsr_tag_isred(p_alts[0])) { LFS_ASSERT(lfsr_tag_isparallel(alt, p_alts[0])); + printf("%04x->%04x: ysplit 0x%x w%d 0x%x\n", + branch, + rbyd->eoff, + alt, + weight, + y_branch); // if we take the red or yellow alt we can just point // to the black alt @@ -3016,6 +3077,14 @@ again:; lfsr_rbyd_p_recolor(p_alts, p_weights, p_jumps); } + // keep track of last alt on diverged trunk to stitch the + // trunks together with + if (d_state == LFSR_D_DIVERGEDLOWER + && lfsr_tag_key(p_alts[0]) != 0) { + d_tag = p_alts[0]; + // d_rid = lower_rid; + } + branch = branch_; continue; } @@ -3068,6 +3137,14 @@ again:; &lower_tag, &upper_tag); // no longer pruned d_pruned = false; + + // keep track of last alt on diverged trunk to stitch the + // trunks together with + if (d_state == LFSR_D_DIVERGEDLOWER + && lfsr_tag_key(alt) != 0) { + d_tag = alt; + // d_rid = lower_rid; + } } // push alt onto our queue @@ -3104,7 +3181,7 @@ again:; // keep track of last alt on diverged trunk to stitch the trunks // together with d_state = LFSR_D_DIVERGINGUPPER; - d_tag = p_alts[0]; +// d_tag = p_alts[0]; d_rid = lower_rid; // flush any pending alts @@ -3115,7 +3192,7 @@ again:; } // terminate diverged trunk with an unreachable tag - if (d_tag) { +// if (d_tag) { err = lfsr_rbyd_appendattr_(lfs, rbyd, (lfsr_rbyd_isshrub(rbyd) ? LFSR_TAG_SHRUB : 0) | LFSR_TAG_NULL, @@ -3124,7 +3201,7 @@ again:; if (err) { return err; } - } +// } // swap tag/rid and write out the upper trunk lfs_swap16(&a_tag, &b_tag); diff --git a/scripts/dbgbmap.py b/scripts/dbgbmap.py index 933b1e15..8f31ff98 100755 --- a/scripts/dbgbmap.py +++ b/scripts/dbgbmap.py @@ -813,24 +813,14 @@ class Rbyd: else: alts[j_] |= {'nf': j__, 'c': c} - # prune any alts with unreachable edges - pruned = {} + # treat unreachable alts as converging paths for j_, alt in alts.items(): if 'f' not in alt: - pruned[j_] = alt['nf'] + alt['f'] = alt['nf'] elif 'nf' not in alt: - pruned[j_] = alt['f'] - for j_ in pruned.keys(): - del alts[j_] + alt['nf'] = alt['f'] - for j_, alt in alts.items(): - while alt['f'] in pruned: - alt['f'] = pruned[alt['f']] - while alt['nf'] in pruned: - alt['nf'] = pruned[alt['nf']] - - # find the trunk and depth of each alt, assuming pruned alts - # didn't exist + # find the trunk and depth of each alt def rec_trunk(j_): if j_ not in alts: return trunks[j_] diff --git a/scripts/dbgbtree.py b/scripts/dbgbtree.py index bcc498a6..bc7685ba 100755 --- a/scripts/dbgbtree.py +++ b/scripts/dbgbtree.py @@ -230,10 +230,13 @@ def tagrepr(tag, w, size, off=None): ' w%d' % w if w > 0 else '', size) elif tag & TAG_ALT: - return 'alt%s%s 0x%x w%d %s' % ( + return 'alt%s%s%s w%d %s' % ( 'r' if tag & TAG_R else 'b', - 'gt' if tag & TAG_GT else 'le', - tag & 0x0fff, + 'a' if tag & 0x0fff == 0 and tag & TAG_GT + else 'n' if tag & 0x0fff == 0 + else 'gt' if tag & TAG_GT + else 'le', + ' 0x%x' % (tag & 0x0fff) if tag & 0x0fff != 0 else '', w, '0x%x' % (0xffffffff & (off-size)) if off is not None @@ -473,24 +476,14 @@ class Rbyd: else: alts[j_] |= {'nf': j__, 'c': c} - # prune any alts with unreachable edges - pruned = {} + # treat unreachable alts as converging paths for j_, alt in alts.items(): if 'f' not in alt: - pruned[j_] = alt['nf'] + alt['f'] = alt['nf'] elif 'nf' not in alt: - pruned[j_] = alt['f'] - for j_ in pruned.keys(): - del alts[j_] + alt['nf'] = alt['f'] - for j_, alt in alts.items(): - while alt['f'] in pruned: - alt['f'] = pruned[alt['f']] - while alt['nf'] in pruned: - alt['nf'] = pruned[alt['nf']] - - # find the trunk and depth of each alt, assuming pruned alts - # didn't exist + # find the trunk and depth of each alt def rec_trunk(j_): if j_ not in alts: return trunks[j_] diff --git a/scripts/dbglfs.py b/scripts/dbglfs.py index cd4f2cc0..2c725dcc 100755 --- a/scripts/dbglfs.py +++ b/scripts/dbglfs.py @@ -261,10 +261,13 @@ def tagrepr(tag, w, size, off=None): ' w%d' % w if w > 0 else '', size) elif tag & TAG_ALT: - return 'alt%s%s 0x%x w%d %s' % ( + return 'alt%s%s%s w%d %s' % ( 'r' if tag & TAG_R else 'b', - 'gt' if tag & TAG_GT else 'le', - tag & 0x0fff, + 'a' if tag & 0x0fff == 0 and tag & TAG_GT + else 'n' if tag & 0x0fff == 0 + else 'gt' if tag & TAG_GT + else 'le', + ' 0x%x' % (tag & 0x0fff) if tag & 0x0fff != 0 else '', w, '0x%x' % (0xffffffff & (off-size)) if off is not None @@ -504,24 +507,14 @@ class Rbyd: else: alts[j_] |= {'nf': j__, 'c': c} - # prune any alts with unreachable edges - pruned = {} + # treat unreachable alts as converging paths for j_, alt in alts.items(): if 'f' not in alt: - pruned[j_] = alt['nf'] + alt['f'] = alt['nf'] elif 'nf' not in alt: - pruned[j_] = alt['f'] - for j_ in pruned.keys(): - del alts[j_] + alt['nf'] = alt['f'] - for j_, alt in alts.items(): - while alt['f'] in pruned: - alt['f'] = pruned[alt['f']] - while alt['nf'] in pruned: - alt['nf'] = pruned[alt['nf']] - - # find the trunk and depth of each alt, assuming pruned alts - # didn't exist + # find the trunk and depth of each alt def rec_trunk(j_): if j_ not in alts: return trunks[j_] diff --git a/scripts/dbgmtree.py b/scripts/dbgmtree.py index 07ab0dd9..cce4a8aa 100755 --- a/scripts/dbgmtree.py +++ b/scripts/dbgmtree.py @@ -245,10 +245,13 @@ def tagrepr(tag, w, size, off=None): ' w%d' % w if w > 0 else '', size) elif tag & TAG_ALT: - return 'alt%s%s 0x%x w%d %s' % ( + return 'alt%s%s%s w%d %s' % ( 'r' if tag & TAG_R else 'b', - 'gt' if tag & TAG_GT else 'le', - tag & 0x0fff, + 'a' if tag & 0x0fff == 0 and tag & TAG_GT + else 'n' if tag & 0x0fff == 0 + else 'gt' if tag & TAG_GT + else 'le', + ' 0x%x' % (tag & 0x0fff) if tag & 0x0fff != 0 else '', w, '0x%x' % (0xffffffff & (off-size)) if off is not None @@ -488,24 +491,14 @@ class Rbyd: else: alts[j_] |= {'nf': j__, 'c': c} - # prune any alts with unreachable edges - pruned = {} + # treat unreachable alts as converging paths for j_, alt in alts.items(): if 'f' not in alt: - pruned[j_] = alt['nf'] + alt['f'] = alt['nf'] elif 'nf' not in alt: - pruned[j_] = alt['f'] - for j_ in pruned.keys(): - del alts[j_] + alt['nf'] = alt['f'] - for j_, alt in alts.items(): - while alt['f'] in pruned: - alt['f'] = pruned[alt['f']] - while alt['nf'] in pruned: - alt['nf'] = pruned[alt['nf']] - - # find the trunk and depth of each alt, assuming pruned alts - # didn't exist + # find the trunk and depth of each alt def rec_trunk(j_): if j_ not in alts: return trunks[j_] diff --git a/scripts/dbgrbyd.py b/scripts/dbgrbyd.py index 46d03ebb..c210ece1 100755 --- a/scripts/dbgrbyd.py +++ b/scripts/dbgrbyd.py @@ -232,10 +232,13 @@ def tagrepr(tag, w, size, off=None): ' w%d' % w if w > 0 else '', size) elif tag & TAG_ALT: - return 'alt%s%s 0x%x w%d %s' % ( + return 'alt%s%s%s w%d %s' % ( 'r' if tag & TAG_R else 'b', - 'gt' if tag & TAG_GT else 'le', - tag & 0x0fff, + 'a' if tag & 0x0fff == 0 and tag & TAG_GT + else 'n' if tag & 0x0fff == 0 + else 'gt' if tag & TAG_GT + else 'le', + ' 0x%x' % (tag & 0x0fff) if tag & 0x0fff != 0 else '', w, '0x%x' % (0xffffffff & (off-size)) if off is not None @@ -260,7 +263,8 @@ def dbg_log(data, block_size, rev, eoff, weight, *, if not tag & TAG_ALT: j_ += size - if tag & TAG_ALT: + # skip alt-nevers + if tag & TAG_ALT and tag & ~TAG_R != TAG_ALT: # figure out which alt color if tag & TAG_R: _, ntag, _, _, _ = fromtag(data[j_:]) @@ -692,24 +696,14 @@ def dbg_tree(data, block_size, rev, trunk, weight, *, else: alts[j_] |= {'nf': j__, 'c': c} - # prune any alts with unreachable edges - pruned = {} + # treat unreachable alts as converging paths for j_, alt in alts.items(): if 'f' not in alt: - pruned[j_] = alt['nf'] + alt['f'] = alt['nf'] elif 'nf' not in alt: - pruned[j_] = alt['f'] - for j_ in pruned.keys(): - del alts[j_] + alt['nf'] = alt['f'] - for j_, alt in alts.items(): - while alt['f'] in pruned: - alt['f'] = pruned[alt['f']] - while alt['nf'] in pruned: - alt['nf'] = pruned[alt['nf']] - - # find the trunk and depth of each alt, assuming pruned alts - # didn't exist + # find the trunk and depth of each alt def rec_trunk(j_): if j_ not in alts: return trunks[j_]