Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions crates/openjd-expr/src/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1253,8 +1253,13 @@ impl ExprValue {
(Self::Float(a), Self::Int(b)) => int_float_eq(*b, a.value),
(Self::String(a), Self::String(b)) => a == b,
(Self::Path { value: a, .. }, Self::Path { value: b, .. }) => a == b,
(Self::String(a), Self::Path { value: b, .. })
| (Self::Path { value: b, .. }, Self::String(a)) => a == b,
// Split into two arms with position-consistent bindings so `a` is
// always the left operand. Equality is symmetric so a combined
// or-pattern with swapped bindings gives the same answer here, but
// the identical pattern in an ordering context silently reverses
// the comparison (see `compare`).
(Self::String(a), Self::Path { value: b, .. }) => a == b,
(Self::Path { value: a, .. }, Self::String(b)) => a == b,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The comment added here correctly identifies a real bug — but the bug it describes is still live in compare and was not fixed by this PR.

At value.rs:1341-1342:

(Self::String(a), Self::Path { value: b, .. })
| (Self::Path { value: b, .. }, Self::String(a)) => Ok(a.cmp(b)),

In the second alternative, self is the Path (binding its value to b) and other is the String (binding to a), so a.cmp(b) evaluates other.cmp(self) — the ordering is reversed whenever the Path is the left operand. So:

  • String("a").compare(Path("b"))Less
  • Path("a").compare(String("b"))Greater ❌ (should be Less)

This leaks into the evaluator: $(path) < "b" and "b" > $(path) will disagree, and any sort/min/max over a mixed String/Path list becomes order-dependent and non-transitive.

Suggested fix, mirroring the split applied to equals_charged in this PR:

(Self::String(a), Self::Path { value: b, .. }) => Ok(a.cmp(b)),
(Self::Path { value: a, .. }, Self::String(b)) => Ok(a.cmp(b)),

Worth adding a test asserting Path(x).compare(String(y)) is the reverse of String(y).compare(Path(x)) for x != y, since the existing coverage evidently only exercises the String-on-the-left direction.

// Same-variant typed lists: primitive element comparison (no
// per-element ExprValue construction), charged per comparison
// actually performed after the O(1) length check — a mismatch
Expand Down
Loading