Skip to content

Conversation

@ChrisPenner
Copy link
Member

@ChrisPenner ChrisPenner commented Dec 5, 2025

Overview

We've noticed that since let-rec bindings, constructors, and ability lists are all ordered by hash, they sometimes jump around when you make changes; this means the diffs also get much worse since the bindings might have completely re-ordered in-between changes.

closes #5893

internal ticket

There's no real need for these to be printed in hash-order, since the parser will re-order them according to hash on ingestion anyways.

This changes the printer to put things in a consistent order by name rather than hash.

Implementation approach and notes

Add some sorts in the printer modules.

Test coverage

  • See the new print-ordering.md transcript tests; as well as all the re-rendered existing transcripts

@ChrisPenner ChrisPenner changed the title Cp/ordered printing Human-centric let-rec, constructor and ability orderings Dec 5, 2025
@ChrisPenner ChrisPenner marked this pull request as ready for review December 5, 2025 20:42
@ChrisPenner
Copy link
Member Author

Q before merging, @aryairani @mitchellwrosen ,
If I re-order a structural type on printing from structural type Bool = True | False into structural type Bool = False | True is that going to swap every True and False 😓 ?

Maybe I should not do this for structural types just to be sure?
Or does it still apply for non-structural types? I've gotta think about it a bit.

@ChrisPenner ChrisPenner requested a review from hojberg December 5, 2025 20:46
@ChrisPenner ChrisPenner marked this pull request as draft December 5, 2025 20:46
@pchiusano
Copy link
Member

@ChrisPenner did you see #5893 it gives the safe criteria you can use for sorting

@aryairani
Copy link
Contributor

Q before merging, @aryairani @mitchellwrosen , If I re-order a structural type on printing from structural type Bool = True | False into structural type Bool = False | True is that going to swap every True and False 😓 ?

Maybe I should not do this for structural types just to be sure? Or does it still apply for non-structural types? I've gotta think about it a bit.

I'm going to do a quick transcript for this, but one answer is that structural type Bool = True | False would be better of an error anyway, since it's a structural type and the constructors are not structurally distinct.

Now what about for unique types? Do we not have the same problem?

@aryairani
Copy link
Contributor

aryairani commented Dec 5, 2025

@ChrisPenner
Okay, so it's basically the worst possible outcome:

structural type SBool = SFalse | STrue
> add

  Done.

> names STrue SFalse

  'STrue':
  Hash            Kind   Names
  #6kbe32g06n#1   Term   SBool.STrue

  'SFalse':
  Hash            Kind   Names
  #6kbe32g06n#0   Term   SBool.SFalse

0 = False; 1 = True

structural type SBool = STrue | SFalse
  Loading changes detected in scratch.u.

  No changes found.

Oh good, nothing's changed.

> add

  Done.

> names STrue SFalse

  'STrue':
  Hash            Kind   Names
  #6kbe32g06n#0   Term   SBool.STrue

  'SFalse':
  Hash            Kind   Names
  #6kbe32g06n#1   Term   SBool.SFalse

Oh just kidding, it actually did change. 0 is True and 1 is False. It doesn't break any code, but it may break your mind.

unique type UBool = UFalse | UTrue
> add

  Done.

> names UFalse UTrue

  'UFalse':
  Hash            Kind   Names
  #5l0gnirl3n#0   Term   UBool.UFalse

  'UTrue':
  Hash            Kind   Names
  #5l0gnirl3n#1   Term   UBool.UTrue
unique type UBool = UTrue | UFalse
  Loading changes detected in scratch.u.

  No changes found.
> add

  Done.

> names UFalse UTrue

  'UFalse':
  Hash            Kind   Names
  #5l0gnirl3n#1   Term   UBool.UFalse

  'UTrue':
  Hash            Kind   Names
  #5l0gnirl3n#0   Term   UBool.UTrue

Same issue here, possibly less surprising.

@ceedubs
Copy link
Contributor

ceedubs commented Dec 8, 2025

#3615 describes the (an?) issue with structural types and constructors of the same type.

@ChrisPenner
Copy link
Member Author

ChrisPenner commented Dec 8, 2025

@aryairani Is add still doing something even though it says nothing has changed? Oof.

Yeah this seems to be an issue with identical constructors in general, but of course this change would exacerbate it a lot.

I think this means we should just leave constructors out of it this iteration and proceed with the effect lists and let-recs for now.

EDIT: Looking into what Paul suggested now, sounds like a good plan if it's easy enough to implement.

@ChrisPenner
Copy link
Member Author

ChrisPenner commented Dec 8, 2025

Okay I've implemented the constructor ordering approach suggested by @pchiusano in #5893 which appears to do the trick, it's a clever idea :)

& List.groupOn fst
-- head is okay since groupOn only returns populated lists.
<&> \grp -> (fst . head $ grp, snd <$> grp)
<&> \grp -> (fst . head $ grp, NEL.fromList (snd <$> grp))
Copy link
Member Author

Choose a reason for hiding this comment

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

NEL.fromList is partial, but this list is guaranteed to be non-empty.

Copy link
Member Author

Choose a reason for hiding this comment

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

Tweaked a few helpers to return NonEmpty because 'make invalid state unrepresentable' and all that :P

Comment on lines +512 to +515
groupActionsByLocation xs =
xs
& List.groupMap (\(p, act) -> (pathLocation p, (p, act)))
<&> second Foldable.toList
Copy link
Member Author

Choose a reason for hiding this comment

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

Just a tweak to work with the new NonEmpty

Comment on lines +139 to +150
& List.sortOn (\(_n, (_a, _v, typ)) -> typ)
-- Now we group by type, we need to leave identical types in their constructor order to avoid things like
-- swapping identical constructors, e.g. False turning into True and vice versa.
& List.groupMap (\con@(_, (_, _, typ)) -> (typ, con))
-- Then we can sort those _groups_ by the name of the first constructor in the group.
& List.sortOn
( \(_typ, group) ->
group
& NEL.sortOn fst
& \case
(n, (_, _, _)) :| _rest ->
PPE.termName ppe (Referent.Con (ConstructorReference r n) ctype)
Copy link
Member Author

Choose a reason for hiding this comment

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

Here's the tweak Paul suggested :)

@ChrisPenner ChrisPenner marked this pull request as ready for review December 8, 2025 21:40
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.

Idea: alphabetical ordering of constructors

5 participants