Skip to content

fix: #3621 give range lower precedence than conditional expressions (breaking) - #3680

Open
mvanhorn wants to merge 1 commit into
josdejong:v16from
mvanhorn:fix/3621-range-precedence
Open

fix: #3621 give range lower precedence than conditional expressions (breaking)#3680
mvanhorn wants to merge 1 commit into
josdejong:v16from
mvanhorn:fix/3621-range-precedence

Conversation

@mvanhorn

@mvanhorn mvanhorn commented Jul 8, 2026

Copy link
Copy Markdown

Summary

Range : now has a precedence between assignment and conditional, so the parser matches a strict precedence hierarchy. This targets v16 because it changes how a few expressions parse.

Why this matters

The documented precedence table lists conditional ?: < or < range ::, but the parser didn't follow it: true ? 3 : -1 : 2 : 5 bound the conditional tighter than range, while false or 1:3 bound range tighter than or. That relation is non-transitive, so the parser wasn't strictly precedence-based, and the docs never said so (#3621).

In the issue thread you worked through the options and settled the direction: range can't be higher than conditional (that breaks a ? b : c, since b : c would parse as a range), so range moves below conditional and above assignment. This PR implements that, with the consequences you called out.

Changes

  • src/expression/parse.js: reordered the recursive-descent chain to parseAssignment -> parseRange -> parseConditional -> parseLogicalOr -> .... parseConversion now calls parseAddSubtract (range left that slot). Removed the conditionalLevel bookkeeping entirely (all 8 uses) — a : at range level is now unambiguously a range separator because conditional branches parse at a higher level that never consumes :. Net −27 lines in the parser.
  • src/expression/operators.js: moved the range band directly above conditional (and below assignment) so toString() / toTex() parenthesization stays consistent with parse precedence and round-tripping remains lossless.
  • docs/expressions/syntax.md and HISTORY.md: updated the precedence table and documented the breaking behavior changes.

Accepted behavior changes:

  • true ? 3 : -1 : 2 : 5 -> (true ? 3 : -1) : 2 : 5
  • a:b==c:d -> a:(b==c):d
  • true ? a = 1 : 7 is now a syntax error; use true ? (a=1) : 7
  • a = 1:5 still works (range stays above assignment)

Testing

Added #3621 regression tests and updated the tests that encoded the old ordering:

  • true ? 3 : -1 : 2 : 5 parses as a RangeNode whose start is true ? 3 : (-1), evaluating to [3, 5]
  • false or true ? 1 : 2 still returns 1 (conditional below or)
  • false or 1:3 parses as a range with start false or 1 -> [1, 2, 3]
  • a:b==c:d -> a:(b == c):d; true ? a = 1 : 7 throws; true ? (a=1) : 7 works; a = 1:5, A[1:3], f(1:3), 1:2:10 unchanged
  • toString() round-trip and toTex() parenthesization for the reordered operators

Full expression suite (npx mocha test/unit-tests/expression/**/*.test.js): 2213 passing, no regressions.

Closes #3621

…essions (breaking)

The documented precedence table lists conditional < or < range, but the parser
did not follow it: 'true ? 3 : -1 : 2 : 5' bound the conditional tighter than
range while 'false or 1:3' bound range tighter than or, a non-transitive
relation the docs never described.

Per the owner's analysis in josdejong#3621, range cannot sit above conditional (that
breaks 'a ? b : c', since 'b : c' would parse as a range), so range moves to a
precedence between assignment and conditional. Reorder the recursive-descent
chain to parseAssignment -> parseRange -> parseConditional -> ..., remove the
now-redundant conditionalLevel bookkeeping (all 8 uses), and move the range
band in operators.js so toString()/toTex() parenthesization matches.

Accepted behavior changes (documented in HISTORY and syntax.md):
- 'true ? 3 : -1 : 2 : 5' parses as '(true ? 3 : -1) : 2 : 5'
- 'a:b==c:d' parses as 'a:(b==c):d'
- 'true ? a = 1 : 7' now requires parentheses: 'true ? (a=1) : 7'
- 'a = 1:5' still works (range stays above assignment)

Targets v16 (breaking).

Closes josdejong#3621
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant