-
Notifications
You must be signed in to change notification settings - Fork 72
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(marshal)!: Update compareRank to terminate comparability at the first remotable #2597
Conversation
e502d10
to
4e39e58
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Flushing this review before I get to the end, to keep the conversation lively.
export const makeComparatorKit = (compareRemotables = (_x, _y) => 0) => { | ||
/** @type {RankCompare} */ | ||
export const makeComparatorKit = (compareRemotables = (_x, _y) => NaN) => { | ||
/** @type {PartialCompare} */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Confirm or deny:
Previously:
- values from dispirit ranks are compared by the total order of ranks
- all values within atom ranks, e.g., numbers and strings, are mutually comparable
- all values within composite ranks, e.g., arrays, are mutually comparable provided
- that their respective entries are all comparable, or
- if they contain any respective pairs that are incomparable, those arrays have non-equivalent prefixes
- and are otherwise incomparable
- within the rank of promises, all values are incomparable
- within the rank of remotables, all values are equivalent
- comparing incomparable values produces 0
After:
- within the rank of remotables, all values are incomparable
- comparing incomparable values produces NaN
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That seems correct, although I would rephrase a bit:
Previously:
- …
- within the rank of remotables, all values are equivalent in the default comparison (used by
compareRank
) comparing incomparable values produces 0within the respective ranks of errors and promises, all values are equivalent (and encountering an unacceptable pass style would result in a thrown exception)
After:
- within the rank of remotables, all values are incomparable
- comparing incomparable values produces NaN in the inner comparator and short-circuits the outer comparator result to 0
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, both before and after this change, comparing any pair of promises would by default return NaN?
Consequently, both before and after this change, the range of comparison was -1, 0, 1, and NaN?
Consequently, both before and after this change, rank compare is and was a partial order?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’m far enough along in my read to see with my own eyes that, both before and after this change:
- all exported comparators produce values in the range -1, 0, +1 for the default comparator for remotables.
- all promises are equivalent, not incoparable
Just skimming, but saw the q at end. rankCompare is a full order. It cannot answer NaN. (Unless this PR is making a more radical change than I thought. For all promises p,q, compareRank(p,q) === 0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’m convinced about the correctness and practical forward-compatibility of the fix.
I note that the fix occurs almost entirely outside the diff as an emergent behavior of having compareRemoatables
return NaN and coercing NaN back down to 0 at the border. The relevant behavior appears to be stopping early and propagating NaN on:
endo/packages/marshal/src/rankOrder.js
Lines 199 to 206 in bf8b1f8
case 'copyArray': { | |
// Lexicographic | |
const len = Math.min(left.length, right.length); | |
for (let i = 0; i < len; i += 1) { | |
const result = comparator(left[i], right[i]); | |
if (result !== 0) { | |
return result; | |
} |
I am not qualified to read the property based tests yet.
let i = 0; | ||
for (;;) { | ||
const { value: char1 } = iter1.next(); | ||
const { value: char2 } = iter2.next(); | ||
if (char1 !== char2 || char1 === undefined) { | ||
return str1.substring(0, i); | ||
} | ||
i += char1.length; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’m sure you’re cognizant of the discrepancy here between iterating code points and indexing code units, but that might not be obvious under maintenance of these tests. Anything that compensates for that would be great, albeit accumulating common code points by concatenation or documenting a limitation to correctness under strings where code unit and code point are the same, but if documenting, consider using the dumber algorithm that co-iterates code units to be consistent with the substring call.
const sorted = unsorted.sort(compare); | ||
// For reverse comparison, move `undefined` values from the end to the start. | ||
// Note that passStylePrefixes (@see {@link ./encodePassable.js}) MUST NOT | ||
// sort any category after `undefined`. | ||
if (compare(true, undefined) > 0) { | ||
let i = sorted.length - 1; | ||
while (i >= 0 && sorted[i] === undefined) i -= 1; | ||
const n = sorted.length - i - 1; | ||
if (n > 0 && n < sorted.length) { | ||
sorted.copyWithin(n, 0); | ||
sorted.fill(/** @type {T} */ (undefined), 0, n); | ||
} | ||
} | ||
harden(sorted); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✅ the undefined fix
packages/marshal/src/rankOrder.js
Outdated
const outerComparator = (x, y) => | ||
/** @type {Exclude<PartialComparison, NaN>} */ (comparator(x, y) || 0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✅ narrowing the range in case of a compareRemotables that returns NaN, as it now will by default.
This does break any existing program that expects to distinguish NaN and provided a compareRemotables to produce that effect, but as you state you’re convinced nobody does that in practice, I’m fine with this. It may be worth noting in NEWS.md if only to build trust.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be worth explicitly reminding reviewers that NaN || 0 === 0
because it’s not entirely obvious which JavaScript operators implicitly coerce down from the float to integer domain.
export const makeComparatorKit = (compareRemotables = (_x, _y) => 0) => { | ||
/** @type {RankCompare} */ | ||
export const makeComparatorKit = (compareRemotables = (_x, _y) => NaN) => { | ||
/** @type {PartialCompare} */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’m far enough along in my read to see with my own eyes that, both before and after this change:
- all exported comparators produce values in the range -1, 0, +1 for the default comparator for remotables.
- all promises are equivalent, not incoparable
await fc.assert( | ||
fc.property( | ||
fc.uniqueArray(arbKey, { comparator: keyEQ }), | ||
fc.infiniteStream(fc.boolean()), | ||
(arr, keepSeq) => { | ||
const sub = arr.filter(() => keepSeq.next().value); | ||
setIsSuperset(makeCopySet(arr), makeCopySet(sub)) || | ||
Fail`${q(sub)} of ${q(arr)}`; | ||
}, | ||
), | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’m not a qualified reviewer for property tests yet.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I read this as: For an arbitrary array of distinct keys arr
and subsequence sub
of arr
, setIsSuperset(makeCopySet(arr), makeCopySet(sub))
holds, where a subsequence is formed by filtering using elements of an arbitrary infinite stream of booleans.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought it would be illustrative to show the output when it fails, before the fix is applied. But even when I check out 14fca7b that has the test and not the fix, this test seems to pass. I suppose the failing case is pretty obscure and the generator doesn't get that far?
When I crank numRuns
up to 4000, I get a failure that might be showing another problem? Got error: Error: ["[undefined]"] of ["[Symbol()]",null,"[undefined]"]
with seed: -1238147821
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oic... it's finding cases of the undefined
problem.
6094bcb
to
85ff875
Compare
@erights Right, and that remains true... the default Do you have further comments or reason(s) to delay merging now that @kriskowal has approved? |
@@ -143,61 +143,17 @@ export {}; | |||
*/ | |||
|
|||
/** | |||
* @typedef {RankCompare} FullCompare |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are static types considered part of the API? This FullCompare
type is exported, right? So is removing it from this package a breaking change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, that's a good question. I've updated to reëxport FullCompare to preserve any remaining imports, on the assumption that dropping it would be considered breaking.
I have not found time to review this in enough depth to approve it. But I saw no red flags and am satisfied from the conversations above that all the issues I thought to worry about have been extremely well thought through and reviewed. So please consider your existing approval adequate and do not wait for me. In this case, I'm happy to complete my review post merge. Really looks great. A clever simple solution that I would not have thought of. Thanks much!! |
…irst remotable To be refinable into a total order that distinguishes remotables, `compareRank` must consider `[r1, 'x']` vs. `[r2, 'y']` as a tie rather than as equivalent to `[0, 'x']` vs. `[0, 'y']`, in case r1 vs. r2 ends up comparing differently than 'x' vs. 'y'. Fixes #2588
85ff875
to
95c427a
Compare
Fixes #2588
Description
To be refinable into a total order that distinguishes remotables,
compareRank
must consider[r1, 'x']
vs.[r2, 'y']
as a tie rather than as equivalent to[0, 'x']
vs.[0, 'y']
, in case r1 vs. r2 ends up comparing differently than 'x' vs. 'y'.Security Considerations
None known.
Scaling Considerations
None known.
Documentation Considerations
This includes implicit fixup in types.js, but there may be some Markdown that needs updating as well.
Testing Considerations
This expands property-based testing, but only narrowly. Expansion is expected in the future.
Compatibility Considerations
This affects storage of compound Keys in e.g. CopySets/CopyBags/CopyMaps, but any ordering written by the old
compareRank
should be accepted by the new one (which now reclassifies as ties what its predecessor considered to be distinct ranks). As such, we should be fine, but I welcome challenges to this assertion.Upgrade Considerations
This is breaking in the sense of changing
compareRank
output, but as far as I know, nothing depends upon that except in combination withcompareKeys
(which is fixed by this PR). I am not aware of any concerns that should hinder upgrade to a version of endo including this PR.But I am wondering what if anything to put in NEWS.md file(s).