Skip to content

ScheduledMerges: allow empty levels#823

Draft
mheinzel wants to merge 2 commits intomainfrom
mheinzel/prototype-empty-levels
Draft

ScheduledMerges: allow empty levels#823
mheinzel wants to merge 2 commits intomainfrom
mheinzel/prototype-empty-levels

Conversation

@mheinzel
Copy link
Collaborator

@mheinzel mheinzel commented Mar 3, 2026

Builds on top of #814.

This can generally be useful, but the main motivation now is to allow migration of the union level into the regular levels as soon as the merging tree is completed.

So far, we always required an IncomingRun to be present on each level, but already had a special case for where this could be a plain Run that doesn't require any merging. Instead, we can make the presence of the IncomingRun itself optional, which allows us to have no runs on a level at all. The resulting changes are minor, but we should perhaps think about more suitable terms for IncomingRun and/or the Maybe IncomingRun (which might deserve its own data type), at least in the real implementation).

Copy link
Collaborator

@jorisdral jorisdral left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a useful change 😄

We talked over other channels about this PR, so 'm just leaving some comments to keep track of what we talked about

We also considered how empty levels would influence the implementation, in particular backwards compatibility for snapshots, and we agreed that it's probably going to be straightforward to support backwards compatibility. We're roughly going from a levels structure analogous to [NonEmpty Run] to a structure analogous to [Maybe (NonEmpty Run)], and the former maps easily into the latter

data IncomingRun s = Merging !MergePolicyForLevel
!NominalDebt !(STRef s NominalCredit)
!(MergingRun LevelMergeType s)
| Single !Run
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We talked about keeping Single, but I'm just leaving a summary of my thoughts on it here for posterity:

I'd argue in favour of keeping Single to encode invariants statically, because we are now replacing that with extra assertions in levelsInvariant & co. In my mind it's analogous to using NonEmpty over [] when you know a list should be non-empty (though the prototype is obviously more complex and therefore the choice of representation is a more nuanced tradeoff). Yes, there are more case-expressions you need to write in levelsInvariant, but being forced to be explicit about these cases is in my mind clearer and less error-prone.

-- to see statically that it's a single run without having to read the 'STRef',
-- and secondly to make it easier to avoid supplying merge credits. It's not
-- essential, but simplifies things somewhat.
data Level s = Level !(Maybe (IncomingRun s)) ![Run]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we agreed we would try out a representation where the level can be either empty, or non-empty. Something along the lines of

data Level = EmptyLevel | Level ...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done now. We still need to merge #814 first and rebase, so I'll leave it as a draft PR for now. But apart from that, this branch should be ready.

@mheinzel mheinzel force-pushed the mheinzel/prototype-tests-nested-union branch from 9e43f0e to b7bc5c2 Compare March 17, 2026 16:59
It makes more sense for it to take PreExistingRuns directly, instead of
first turning each level into IncomingRuns and then turning those into
PreExistingRuns.
@mheinzel mheinzel force-pushed the mheinzel/prototype-empty-levels branch from f90b172 to 2591aff Compare March 23, 2026 11:59
Copy link
Collaborator

@jorisdral jorisdral left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me! I only have a minor comment about a use of mergePolicyForLevel. Once that is resolved and we have rebased on top of the #814 merge, then we can go ahead and merge this too

Comment on lines +362 to +363
levelsInvariant !_ (EmptyLevel : ls) = do
assertST $ not (null ls) -- last level shouldn't be empty
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 makes sense, we don't want "trailing empty levels" so to speak


go !ln incoming (EmptyLevel : ls) = do
let mergePolicy = mergePolicyForLevel ln [] ul
traceWith tr' $ LevelIsNotFullEvent mergePolicy
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: it might nice to have a separate trace constructor for this

tr' = contramap (EventAt sc ln) tr

go !ln incoming (EmptyLevel : ls) = do
let mergePolicy = mergePolicyForLevel ln [] ul
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should take into account later levels, no?

Suggested change
let mergePolicy = mergePolicyForLevel ln [] ul
let mergePolicy = mergePolicyForLevel ln ls ul

Base automatically changed from mheinzel/prototype-tests-nested-union to main March 24, 2026 09:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants