Fix NullPointerExceptions and contract violations in RecordValue - #1925
Open
slisson wants to merge 3 commits into
Open
Fix NullPointerExceptions and contract violations in RecordValue#1925slisson wants to merge 3 commits into
slisson wants to merge 3 commits into
Conversation
Comparing member values used Object.equals, which throws when a member is present but mapped to null. Use the null-safe :ne: operator instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
hashCode hashed memberData and recordDeclaration, while equals compared only memberData, so two record values with equal members but different record declarations were equal with differing hash codes. Compare the record declaration in equals as well. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
compareTo threw a NullPointerException for a record value without a
record declaration -- what the interpreter builds for inline records --
because effectiveMembers() returns null for a null node. Such a record
now compares by member name instead.
Member values that were null were skipped entirely, which made the
ordering intransitive: {y:1} vs {y:null} and {y:null} vs {y:2} both
compared equal while {y:1} vs {y:2} did not, so a TreeSet of the three
held two elements. A member without a value now sorts before one that
has a value.
Adds test cases for both, plus a regression test for the equals()
NullPointerException fixed in 7ec5d2e.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
RecordValue(org.iets3.core.expr.toplevel.plugin) threwNullPointerExceptionfrom bothequalsandcompareTowhenever a record member had no value, and itsequals/hashCode/compareTotrio was mutually inconsistent. This fixes all three, with test cases intest.ts.expr.os.records.What changed
1.
equalsthrew an NPE on null member values (7ec5d2e4)!rv.memberData[e.key].equals(e.value)dereferenced the other record's member value, which isnullfor a member that is present in the map but has no value. Replaced with the null-safe:ne:operator, so it generates asObjects.equals(...).2.
equalsandhashCodedisagreed (030cab47)hashCodewasObjects.hash(memberData, recordDeclaration)whileequalscompared onlymemberData. Two record values with equal members but different record declarations were thereforeequalswith differing hash codes — broken forHashMap/HashSet.equalsnow compares the record declaration as well.The declaration is compared by node identity, which is what
IRecordDeclaration.equals(thisNode :eq: declaration) already does for KernelF's own==. Since records are nominal, both operands of a well-typed comparison carry the same declaration, so this changes nothing for well-typed programs; it fixes the contract for values that reach a hash-based collection across record types.3.
compareTothrew an NPE, and was not a validComparable(934b968f)new RecordValue(null)for inline/projected records.SLinkOperations.getChildren(null, …)returns empty, so control fell into theeffectiveMembers()branch, which returnsnullfor anullnode, and the for-loop NPE'd. Such a record now compares by member name, over the sorted union of both sides' member names.{y:1}vs{y:null}and{y:null}vs{y:2}both compared equal while{y:1}vs{y:2}did not — aTreeSetof the three held two elements. A new privatecompareMemberValuessorts a member without a value before one that has a value; two present values still go throughOH.compareunchanged.The
comparisonOrderbranch is unchanged in intent — it still compares only the declared sort key, which is the feature, socompareTo == 0remains deliberately weaker thanequals.Tests
Three test methods added to
RecordValueTestintest.ts.expr.os.records. Against the unfixed code the twocompareTotests reproduce the defects — one with theNullPointerExceptionatRecordValue.compareTo, one with anAssertionErrorbecausewithNull.compareTo(one)was0:equalsIsNullSafeForNullMemberValues— regression test for (1)compareToWorksWithoutRecordDeclaration— inline records: ordering, reflexivity, and differing key setscompareToOrdersNullMemberValuesConsistently—withNull < one < two, i.e. transitivityAll 4 tests in
RecordValueTestpass, and the existingrecordstest case (10 tests) is unaffected.For the reviewer
==operator uses neither method:EqualsHelper.equalshas its ownRecordValuebranch.equals/hashCodematter for Java-side collections,compareTofor ordering (OH.compare, sorting,TreeSet/TreeMap). So the risk surface here is Java-side, not language semantics.test.in.expr.osthat touch record ordering were run:enums,enum_sort,EnumsWithValuesOfSortableTypes,references,records,path,groupingpass.projectionfails, but it fails identically withcompareToreverted (same four items,Type system returned null type for childand aninvalid null value detectedinSumOp) — pre-existing and unrelated.EqualsHelper.equalsreturnsfalsefor two structurally identical inline records, because it invokes theIRecordDeclaration.equalsbehavior method on anullnode and gets the boolean default back. So==between two projected records is alwaysfalse. Worth its own issue.🤖 Generated with Claude Code