t: Tweaked traversal to use more states, less indirect pointers

This splits LFSR_TSTATE_BTREE into separate LFSR_TSTATE_MTREE/BTREE/
OBTREE states that indicate what to do next after traversing the btree.

This removes the need to point indirectly to file's o.next pointer,
since we can just point to the file struct itself.

I've also simplified opened-file clobbering to just move to the next
opened mdir, instead of searching for another unsynced file. This
simplifies things but does mean we now need to clobber traversals when
closing non-file objects. Implicitly calling lfsr_opened_clobber in
lfsr_opened_remove solves this with very little extra code cost,
deduplicated, and gives us a stronger invariant for traversal references
to closed objects. So win win?

Oh, and all the explicit open-file clobber checks are now deduplicated
into lfsr_opened_clobber again.

These tweaks save quite a bit of code:

           code          stack
  before: 34740           2624
  after:  34570 (-0.5%)   2624 (+0.0%)
This commit is contained in:
Christopher Haster
2024-06-18 17:40:01 -05:00
parent a8a214ec0b
commit d36abd387d
3 changed files with 94 additions and 138 deletions
+91 -135
View File
@@ -4961,9 +4961,9 @@ static lfs_scmp_t lfsr_btree_namelookup(lfs_t *lfs, const lfsr_btree_t *btree,
// note this is different from iteration, iteration should use // note this is different from iteration, iteration should use
// lfsr_btree_lookupnext, traversal includes inner btree nodes // lfsr_btree_lookupnext, traversal includes inner btree nodes
#define LFSR_BTRAVERSAL(_bid) \ #define LFSR_BTRAVERSAL() \
((lfsr_btraversal_t){ \ ((lfsr_btraversal_t){ \
.bid=_bid, \ .bid=0, \
.rid=0, \ .rid=0, \
.branch.trunk=0, \ .branch.trunk=0, \
.branch.weight=0}) .branch.weight=0})
@@ -5182,12 +5182,19 @@ static bool lfsr_opened_isopen(lfs_t *lfs, const lfsr_omdir_t *o) {
static void lfsr_opened_add(lfs_t *lfs, lfsr_omdir_t *o) { static void lfsr_opened_add(lfs_t *lfs, lfsr_omdir_t *o) {
LFS_ASSERT(!lfsr_opened_isopen(lfs, o)); LFS_ASSERT(!lfsr_opened_isopen(lfs, o));
// add to opened list
o->next = lfs->opened; o->next = lfs->opened;
lfs->opened = o; lfs->opened = o;
} }
// needed in lfsr_opened_remove
static void lfsr_opened_clobber(lfs_t *lfs, lfsr_omdir_t *o, bool dirty);
static void lfsr_opened_remove(lfs_t *lfs, lfsr_omdir_t *o) { static void lfsr_opened_remove(lfs_t *lfs, lfsr_omdir_t *o) {
LFS_ASSERT(lfsr_opened_isopen(lfs, o)); LFS_ASSERT(lfsr_opened_isopen(lfs, o));
// make sure we're not entangled in any traversals
lfsr_opened_clobber(lfs, o, false);
// remove from opened list
for (lfsr_omdir_t **o_ = &lfs->opened; *o_; o_ = &(*o_)->next) { for (lfsr_omdir_t **o_ = &lfs->opened; *o_; o_ = &(*o_)->next) {
if (*o_ == o) { if (*o_ == o) {
*o_ = (*o_)->next; *o_ = (*o_)->next;
@@ -5208,20 +5215,25 @@ static bool lfsr_mid_isopen(lfs_t *lfs, lfsr_smid_t mid) {
return false; return false;
} }
//static void lfsr_opened_clobber(lfs_t *lfs, lfsr_omdir_t *o) { // needed in lfsr_opened_clobber
// for (lfsr_omdir_t *o_ = lfs->opened; o_; o_ = o_->next) { static void lfsr_traversal_clobberopen(lfs_t *lfs, lfsr_traversal_t *t);
// if (o_->type == LFS_TYPE_TRAVERSAL
// && ((lfsr_traversal_t*)o_)->mt.ot == &o->next) { // traversal invalidation things
// lfsr_traversal_t *t = (lfsr_traversal_t*)o_; static void lfsr_opened_clobber(lfs_t *lfs, lfsr_omdir_t *o, bool dirty) {
// // move to next omdir for (lfsr_omdir_t *o_ = lfs->opened; o_; o_ = o_->next) {
// t->mt.ot = &o->next->next; if (o_->type == LFS_TYPE_TRAVERSAL) {
// // mark _all_ traversals as dirty if we're mutating the
// // and clear any pending blocks // filesystem at all
// t->blocks[0] = -1; o_->flags |= (dirty) ? LFS_F_DIRTY : 0;
// t->blocks[1] = -1;
// } // clobber any traversals referencing our mdir
// } lfsr_traversal_t *t = (lfsr_traversal_t*)o_;
//} if (t->mt.ot == o) {
lfsr_traversal_clobberopen(lfs, t);
}
}
}
}
//// find any traversals that reference our opened mdir and move them //// find any traversals that reference our opened mdir and move them
//// to the next unsync file //// to the next unsync file
@@ -6855,7 +6867,7 @@ static int lfsr_mroot_parent(lfs_t *lfs, const lfsr_mptr_t *mptr,
} }
// needed in lfsr_mdir_commit // needed in lfsr_mdir_commit
static void lfsr_traversal_clobber(lfs_t *lfs, lfsr_traversal_t *t); static void lfsr_traversal_clobbermdir(lfs_t *lfs, lfsr_traversal_t *t);
// high-level mdir commit // high-level mdir commit
// //
@@ -7431,7 +7443,7 @@ static int lfsr_mdir_commit(lfs_t *lfs, lfsr_mdir_t *mdir,
// clobber any related traversals // clobber any related traversals
if (lfsr_mdir_cmp(&o->mdir, mdir) == 0) { if (lfsr_mdir_cmp(&o->mdir, mdir) == 0) {
lfsr_traversal_clobber(lfs, (lfsr_traversal_t*)o); lfsr_traversal_clobbermdir(lfs, (lfsr_traversal_t*)o);
} }
} }
} }
@@ -7442,7 +7454,7 @@ static int lfsr_mdir_commit(lfs_t *lfs, lfsr_mdir_t *mdir,
for (lfsr_omdir_t *o = lfs->opened; o; o = o->next) { for (lfsr_omdir_t *o = lfs->opened; o; o = o->next) {
if (o->type == LFS_TYPE_TRAVERSAL if (o->type == LFS_TYPE_TRAVERSAL
&& o->mdir.mid == -1) { && o->mdir.mid == -1) {
lfsr_traversal_clobber(lfs, (lfsr_traversal_t*)o); lfsr_traversal_clobbermdir(lfs, (lfsr_traversal_t*)o);
} }
} }
} }
@@ -7811,10 +7823,12 @@ enum {
LFSR_TSTATE_MROOTANCHOR = 0, LFSR_TSTATE_MROOTANCHOR = 0,
LFSR_TSTATE_MROOTCHAIN = 1, LFSR_TSTATE_MROOTCHAIN = 1,
LFSR_TSTATE_MTREE = 2, LFSR_TSTATE_MTREE = 2,
LFSR_TSTATE_MDIR = 3, LFSR_TSTATE_MDIRS = 3,
LFSR_TSTATE_OPENED = 4, LFSR_TSTATE_MDIR = 4,
LFSR_TSTATE_BTREE = 5, LFSR_TSTATE_BTREE = 5,
LFSR_TSTATE_DONE = 6, LFSR_TSTATE_OMDIRS = 6,
LFSR_TSTATE_OBTREE = 7,
LFSR_TSTATE_DONE = 8,
}; };
#define LFSR_MTRAVERSAL(_flags) \ #define LFSR_MTRAVERSAL(_flags) \
@@ -7878,56 +7892,31 @@ static void lfsr_fs_traverserewind(lfs_t *lfs, lfsr_mtraversal_t *mt) {
mt->u.mtortoise.power = 0; mt->u.mtortoise.power = 0;
} }
static void lfsr_fs_traverseclobber(lfs_t *lfs, lfsr_mtraversal_t *mt) { static void lfsr_fs_traverseclobbermdir(lfs_t *lfs, lfsr_mtraversal_t *mt) {
// increment the mid (to make progress) and reset to the mtree (void)lfs;
mt->o.state = LFSR_TSTATE_MTREE; // increment the mid (to make progress) and reset to mdir iteration
mt->o.state = LFSR_TSTATE_MDIRS;
mt->o.mdir.mid = lfsr_mid_bid(lfs, mt->o.mdir.mid) mt->o.mdir.mid = lfsr_mid_bid(lfs, mt->o.mdir.mid)
+ (1 << lfs->mdir_bits); + (1 << lfs->mdir_bits);
// TODO
// mt->o.mdir.mid = lfs_min(
// mt->o.mdir.mid + 1,
// lfsr_fs_weight(lfs));
// TODO do something different with this maybe?
mt->ot = NULL; mt->ot = NULL;
} }
// needed in lfsr_fs_traverseclobberopen
static inline bool lfsr_f_isunsync(uint32_t flags);
static void lfsr_fs_traverseclobberopen(lfs_t *lfs, lfsr_mtraversal_t *mt) { static void lfsr_fs_traverseclobberopen(lfs_t *lfs, lfsr_mtraversal_t *mt) {
(void)lfs; (void)lfs;
// TODO really this is the best we can do? // move to next omdir
// move to next unsync opened file LFS_ASSERT(mt->o.state == LFSR_TSTATE_OMDIRS
while (true) { || mt->o.state == LFSR_TSTATE_OBTREE);
lfsr_omdir_t *o = *mt->ot; mt->o.state = LFSR_TSTATE_OMDIRS;
if (!o) { mt->ot = mt->ot->next;
mt->o.mdir.mid += 1;
mt->o.state = LFSR_TSTATE_MDIR;
break;
}
if (o->mdir.mid != mt->o.mdir.mid
|| o->type != LFS_TYPE_REG
|| !lfsr_f_isunsync(o->flags)) {
mt->ot = &o->next;
continue;
}
// TODO don't do all of this...
const lfsr_file_t *file = (const lfsr_file_t*)o;
mt->bshrub = file->bshrub;
mt->u.bt = LFSR_BTRAVERSAL(0);
mt->ot = &o->next;
mt->o.state = LFSR_TSTATE_BTREE;
break;
}
} }
// alias mtinfo=btinfo // alias mtinfo=btinfo
typedef lfsr_btinfo_t lfsr_mtinfo_t; typedef lfsr_btinfo_t lfsr_mtinfo_t;
// needed in lfsr_fs_traverse_ // needed in lfsr_fs_traverse_
static inline bool lfsr_f_isunsync(uint32_t flags);
static int lfsr_bshrub_traverse(lfs_t *lfs, const lfsr_file_t *file, static int lfsr_bshrub_traverse(lfs_t *lfs, const lfsr_file_t *file,
lfsr_btraversal_t *bt, lfsr_btraversal_t *bt,
lfsr_bid_t *bid_, lfsr_btinfo_t *btinfo); lfsr_bid_t *bid_, lfsr_btinfo_t *btinfo);
@@ -8043,8 +8032,8 @@ static int lfsr_fs_traverse_(lfs_t *lfs, lfsr_mtraversal_t *mt,
} }
// transition to traversing the mtree // transition to traversing the mtree
mt->u.bt = LFSR_BTRAVERSAL(0); mt->u.bt = LFSR_BTRAVERSAL();
mt->o.state = LFSR_TSTATE_BTREE; mt->o.state = LFSR_TSTATE_MTREE;
continue; continue;
} else { } else {
@@ -8053,7 +8042,7 @@ static int lfsr_fs_traverse_(lfs_t *lfs, lfsr_mtraversal_t *mt,
} }
// iterate over mdirs in the mtree // iterate over mdirs in the mtree
case LFSR_TSTATE_MTREE:; case LFSR_TSTATE_MDIRS:;
// TODO should we move this into lfsr_mtree_lookup? // TODO should we move this into lfsr_mtree_lookup?
// end of mtree? guess we're done // end of mtree? guess we're done
if (mt->o.mdir.mid >= (lfsr_smid_t)lfsr_fs_weight(lfs)) { if (mt->o.mdir.mid >= (lfsr_smid_t)lfsr_fs_weight(lfs)) {
@@ -8089,7 +8078,7 @@ static int lfsr_fs_traverse_(lfs_t *lfs, lfsr_mtraversal_t *mt,
|| lfsr_mid_rid(lfs, mt->o.mdir.mid) || lfsr_mid_rid(lfs, mt->o.mdir.mid)
>= (lfsr_srid_t)mt->o.mdir.rbyd.weight) { >= (lfsr_srid_t)mt->o.mdir.rbyd.weight) {
mt->o.mdir.mid = lfsr_mid_bid(lfs, mt->o.mdir.mid) + 1; mt->o.mdir.mid = lfsr_mid_bid(lfs, mt->o.mdir.mid) + 1;
mt->o.state = LFSR_TSTATE_MTREE; mt->o.state = LFSR_TSTATE_MDIRS;
continue; continue;
} }
@@ -8125,22 +8114,20 @@ static int lfsr_fs_traverse_(lfs_t *lfs, lfsr_mtraversal_t *mt,
// no? next we need to check any opened files // no? next we need to check any opened files
} else { } else {
mt->ot = &lfs->opened; mt->ot = lfs->opened;
mt->o.state = LFSR_TSTATE_OPENED; mt->o.state = LFSR_TSTATE_OMDIRS;
continue; continue;
} }
// start traversing // start traversing
mt->u.bt = LFSR_BTRAVERSAL(0); mt->u.bt = LFSR_BTRAVERSAL();
mt->ot = &lfs->opened;
mt->o.state = LFSR_TSTATE_BTREE; mt->o.state = LFSR_TSTATE_BTREE;
continue; continue;
// scan for blocks/btrees in our opened file list // scan for blocks/btrees in our opened file list
case LFSR_TSTATE_OPENED:; case LFSR_TSTATE_OMDIRS:;
// reached end of opened files? return to mdir traversal // reached end of opened files? return to mdir traversal
lfsr_omdir_t *o = *mt->ot; if (!mt->ot) {
if (!o) {
mt->o.mdir.mid += 1; mt->o.mdir.mid += 1;
mt->o.state = LFSR_TSTATE_MDIR; mt->o.state = LFSR_TSTATE_MDIR;
continue; continue;
@@ -8154,38 +8141,48 @@ static int lfsr_fs_traverse_(lfs_t *lfs, lfsr_mtraversal_t *mt,
// literally every file open, but other things grow O(n^2) with // literally every file open, but other things grow O(n^2) with
// this list anyways // this list anyways
// //
if (o->mdir.mid != mt->o.mdir.mid if (mt->ot->mdir.mid != mt->o.mdir.mid
|| o->type != LFS_TYPE_REG || mt->ot->type != LFS_TYPE_REG
|| !lfsr_f_isunsync(o->flags)) { || !lfsr_f_isunsync(mt->ot->flags)) {
mt->ot = &o->next; mt->ot = mt->ot->next;
continue; continue;
} }
// start traversing the file // start traversing the file
const lfsr_file_t *file = (const lfsr_file_t*)o; const lfsr_file_t *file = (const lfsr_file_t*)mt->ot;
mt->bshrub = file->bshrub; mt->bshrub = file->bshrub;
mt->u.bt = LFSR_BTRAVERSAL(0); mt->u.bt = LFSR_BTRAVERSAL();
mt->ot = &o->next; mt->o.state = LFSR_TSTATE_OBTREE;
mt->o.state = LFSR_TSTATE_BTREE;
continue; continue;
// traverse any btrees we see, this includes the mtree and any file // traverse any btrees we see, this includes the mtree and any file
// btrees/bshrubs // btrees/bshrubs
case LFSR_TSTATE_MTREE:;
case LFSR_TSTATE_BTREE:; case LFSR_TSTATE_BTREE:;
case LFSR_TSTATE_OBTREE:;
// traverse through our file // traverse through our file
err = lfsr_bshrub_traverse(lfs, (const lfsr_file_t*)mt, &mt->u.bt, err = lfsr_bshrub_traverse(lfs, (const lfsr_file_t*)mt, &mt->u.bt,
NULL, mtinfo); NULL, mtinfo);
if (err) { if (err) {
if (err == LFS_ERR_NOENT) { if (err == LFS_ERR_NOENT) {
// end of mtree? start iterating over mdirs // end of mtree? start iterating over mdirs
if (mt->o.mdir.mid == -1) { if (mt->o.state == LFSR_TSTATE_MTREE) {
mt->o.mdir.mid = 0; mt->o.mdir.mid = 0;
mt->o.state = LFSR_TSTATE_MTREE; mt->o.state = LFSR_TSTATE_MDIRS;
// end of btree? go to next opened file continue;
// end of mdir btree? start iterating over opened files
} else if (mt->o.state == LFSR_TSTATE_BTREE) {
mt->ot = lfs->opened;
mt->o.state = LFSR_TSTATE_OMDIRS;
continue;
// end of opened btree? go to next opened file
} else if (mt->o.state == LFSR_TSTATE_OBTREE) {
mt->ot = mt->ot->next;
mt->o.state = LFSR_TSTATE_OMDIRS;
continue;
} else { } else {
mt->o.state = LFSR_TSTATE_OPENED; LFS_UNREACHABLE();
} }
continue;
} }
return err; return err;
} }
@@ -9423,10 +9420,9 @@ failed:;
/// High-level filesystem traversal /// /// High-level filesystem traversal ///
// TODO keep this? static void lfsr_traversal_clobbermdir(lfs_t *lfs, lfsr_traversal_t *t) {
static void lfsr_traversal_clobber(lfs_t *lfs, lfsr_traversal_t *t) { // clobber low-level traversal
// clobber the low-level traversal lfsr_fs_traverseclobbermdir(lfs, &t->mt);
lfsr_fs_traverseclobber(lfs, &t->mt);
// and clear any pending blocks // and clear any pending blocks
t->blocks[0] = -1; t->blocks[0] = -1;
@@ -9434,7 +9430,7 @@ static void lfsr_traversal_clobber(lfs_t *lfs, lfsr_traversal_t *t) {
} }
static void lfsr_traversal_clobberopen(lfs_t *lfs, lfsr_traversal_t *t) { static void lfsr_traversal_clobberopen(lfs_t *lfs, lfsr_traversal_t *t) {
// clobber the low-level traversal // clobber low-level traversal
lfsr_fs_traverseclobberopen(lfs, &t->mt); lfsr_fs_traverseclobberopen(lfs, &t->mt);
// and clear any pending blocks // and clear any pending blocks
@@ -10700,18 +10696,6 @@ int lfsr_file_close(lfs_t *lfs, lfsr_file_t *file) {
err = lfsr_file_sync(lfs, file); err = lfsr_file_sync(lfs, file);
} }
// if we're unsync, we need to clobber any traversals that may be
// referencing our bshrub/memory, but we don't need to mark as dirty
if (lfsr_f_isunsync(file->o.flags)) {
for (lfsr_omdir_t *o = lfs->opened; o; o = o->next) {
if (o->type == LFS_TYPE_TRAVERSAL) {
if (((lfsr_traversal_t*)o)->mt.ot == &file->o.next) {
lfsr_traversal_clobberopen(lfs, (lfsr_traversal_t*)o);
}
}
}
}
// remove from tracked mdirs // remove from tracked mdirs
lfsr_opened_remove(lfs, &file->o); lfsr_opened_remove(lfs, &file->o);
@@ -12032,15 +12016,8 @@ lfs_ssize_t lfsr_file_write(lfs_t *lfs, lfsr_file_t *file,
// checkpoint the allocator // checkpoint the allocator
lfs_alloc_ckpoint(lfs); lfs_alloc_ckpoint(lfs);
// clobber any problematic traversals // clobber any entangled traversals
for (lfsr_omdir_t *o = lfs->opened; o; o = o->next) { lfsr_opened_clobber(lfs, &file->o, true);
if (o->type == LFS_TYPE_TRAVERSAL) {
o->flags |= LFS_F_DIRTY;
if (((lfsr_traversal_t*)o)->mt.ot == &file->o.next) {
lfsr_traversal_clobberopen(lfs, (lfsr_traversal_t*)o);
}
}
}
// mark as unsynced in case we fail // mark as unsynced in case we fail
file->o.flags |= LFS_F_UNSYNC; file->o.flags |= LFS_F_UNSYNC;
@@ -12199,15 +12176,8 @@ int lfsr_file_flush(lfs_t *lfs, lfsr_file_t *file) {
// checkpoint the allocator // checkpoint the allocator
lfs_alloc_ckpoint(lfs); lfs_alloc_ckpoint(lfs);
// clobber any problematic traversals // clobber any entangled traversals
for (lfsr_omdir_t *o = lfs->opened; o; o = o->next) { lfsr_opened_clobber(lfs, &file->o, true);
if (o->type == LFS_TYPE_TRAVERSAL) {
o->flags |= LFS_F_DIRTY;
if (((lfsr_traversal_t*)o)->mt.ot == &file->o.next) {
lfsr_traversal_clobberopen(lfs, (lfsr_traversal_t*)o);
}
}
}
// flush our buffer if it contains any unwritten data // flush our buffer if it contains any unwritten data
int err; int err;
@@ -12467,15 +12437,8 @@ int lfsr_file_truncate(lfs_t *lfs, lfsr_file_t *file, lfs_off_t size_) {
// checkpoint the allocator // checkpoint the allocator
lfs_alloc_ckpoint(lfs); lfs_alloc_ckpoint(lfs);
// clobber any problematic traversals // clobber any entangled traversals
for (lfsr_omdir_t *o = lfs->opened; o; o = o->next) { lfsr_opened_clobber(lfs, &file->o, true);
if (o->type == LFS_TYPE_TRAVERSAL) {
o->flags |= LFS_F_DIRTY;
if (((lfsr_traversal_t*)o)->mt.ot == &file->o.next) {
lfsr_traversal_clobberopen(lfs, (lfsr_traversal_t*)o);
}
}
}
// mark as unsynced in case we fail // mark as unsynced in case we fail
file->o.flags |= LFS_F_UNSYNC; file->o.flags |= LFS_F_UNSYNC;
@@ -12580,15 +12543,8 @@ int lfsr_file_fruncate(lfs_t *lfs, lfsr_file_t *file, lfs_off_t size_) {
// checkpoint the allocator // checkpoint the allocator
lfs_alloc_ckpoint(lfs); lfs_alloc_ckpoint(lfs);
// clobber any problematic traversals // clobber any entangled traversals
for (lfsr_omdir_t *o = lfs->opened; o; o = o->next) { lfsr_opened_clobber(lfs, &file->o, true);
if (o->type == LFS_TYPE_TRAVERSAL) {
o->flags |= LFS_F_DIRTY;
if (((lfsr_traversal_t*)o)->mt.ot == &file->o.next) {
lfsr_traversal_clobberopen(lfs, (lfsr_traversal_t*)o);
}
}
}
// mark as unsynced in case we fail // mark as unsynced in case we fail
file->o.flags |= LFS_F_UNSYNC; file->o.flags |= LFS_F_UNSYNC;
+1 -1
View File
@@ -613,7 +613,7 @@ typedef struct lfsr_mtraversal {
// opened file state, we use an indirect pointer here so we // opened file state, we use an indirect pointer here so we
// always point to data associated with the current mid // always point to data associated with the current mid
lfsr_omdir_t **ot; lfsr_omdir_t *ot;
union { union {
// cycle detection state, only valid when traversing the mroot chain // cycle detection state, only valid when traversing the mroot chain
struct { struct {
+2 -2
View File
@@ -4092,7 +4092,7 @@ code = '''
uint8_t *seen = malloc((BLOCK_COUNT+7)/8); uint8_t *seen = malloc((BLOCK_COUNT+7)/8);
memset(seen, 0, (BLOCK_COUNT+7)/8); memset(seen, 0, (BLOCK_COUNT+7)/8);
lfsr_btraversal_t bt = LFSR_BTRAVERSAL(0); lfsr_btraversal_t bt = LFSR_BTRAVERSAL();
for (lfs_block_t i = 0;; i++) { for (lfs_block_t i = 0;; i++) {
// a bit hacky, but this catches infinite loops // a bit hacky, but this catches infinite loops
assert(i <= 2*N); assert(i <= 2*N);
@@ -4241,7 +4241,7 @@ code = '''
uint8_t *seen = malloc((BLOCK_COUNT+7)/8); uint8_t *seen = malloc((BLOCK_COUNT+7)/8);
memset(seen, 0, (BLOCK_COUNT+7)/8); memset(seen, 0, (BLOCK_COUNT+7)/8);
lfsr_btraversal_t bt = LFSR_BTRAVERSAL(0); lfsr_btraversal_t bt = LFSR_BTRAVERSAL();
for (lfs_block_t i = 0;; i++) { for (lfs_block_t i = 0;; i++) {
// a bit hacky, but this catches infinite loops // a bit hacky, but this catches infinite loops
assert(i <= 2*N); assert(i <= 2*N);