Skip to content

unified: improve node locations - #22608

Draft
tausbn wants to merge 10 commits into
mainfrom
tausbn/unified-improve-node-locations
Draft

tausbn wants to merge 10 commits into
mainfrom
tausbn/unified-improve-node-locations

Conversation

@tausbn

@tausbn tausbn commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Changes the location-assigning heuristics to implement roughly the following principles:

  • A node that is translated recursively gets its location assigned first, and this location is not modified by the outer rule. (However, it can affect the outer location.)
  • If an intermediate node is synthesised (e.g. middle in (outer foo: (middle bar: {baz}))), then it gets a location that spans all of its children (that is, all of the captures that have already been translated, that are assigned as descendants of this node).
  • If an intermediate node has no children that give it a location (and the location is not otherwise set), it gets an empty location at the start of the node that led to this rule match in the first place. (Thus, it's generally in the correct area, even if there isn't a specific bit of source code we can point to as the appropriate "location".)
  • The outermost node of the rule body (i.e. outer above) gets the location of the entire node that matched that rule (unless that location has been assigned already, e.g. by recursive translation or explicit assignment).
  • Interpolated node IDs carry their location. If you write (identifier #{name}), where name is some captured node ID, then the identifier node gets the location of that capture, whereas the string value is what is actually interpolated.
  • If a node is constructed with an explicit location, that location is respected and is not subsequently widened to the entire rule match.

One consequence of the above rules is that if you have something like

(foo bar: @baz) => expr {baz}

then the location of the node emitted by the right hand side is not that of foo, but rather whatever is assigned to baz when it is recursively translated.

In cases where this heuristic is insufficient, newly added tree_at! and tree_spanning! macros may be employed. These take a second argument that specifies the node (or iterable of nodes) from which to take the location of the tree being constructed.


Should be reviewed commit-by-commit. I checked a bunch of the location changes in the last commit manually (and instructed Copilot to check the rest). In all cases, the locations are an improvement on what was there before (e.g. a call inside a tuple accidentally including the following , in its range). This does produce a fair amount of churn, but I think it's somewhat unavoidable.

@tausbn tausbn added the no-change-note-required This PR does not need a change note label Sep 17, 2026
@tausbn
tausbn force-pushed the tausbn/unified-improve-node-locations branch 3 times, most recently from 3579163 to ad29326 Compare September 17, 2026 15:37
tausbn and others added 10 commits September 18, 2026 12:45
NB: This temporarily changes the test output for bulk imports, blocks,
and things like synthesised Array and Option literals. Later commits
will fix these up again.
The location 0..0 was being treated as "no useable location", which at the beginning of a file could cause problems. We now represent this as None instead, making Some(0..0) a valid empty location.
Adds two new macros, both variants of the existing `tree!` macro.

First, `tree_at!` takes an extra argument, and sets the location of the
root of the constructed tree to be that of the argument in question.

Secondly, `tree_spanning!` does the same construction, but accepts an
iterable of nodes instead. It then makes it so that the location of the
root node of the constructed tree is the smallest span that contains all
of the locations given by the iterable.
For such nodes, we assign them the zero-length location at the beginning
of the matched input node.
Blocks are slightly awkward, since we destructure them (as codeBlock) in
the query, and then reconstruct them (as `block`) in the rule body,
meaning the location of the block is (by default) assigned to the wrong
place by the heuristic.

To get around this, I added a helper function that updates the location
appropriately, including handling cases where we only optionally match a
block. (Also, in some cases we can fix this by just not destructuring
`codeBlock`s in the first place -- there's already a rule that maps
`codeBlock` to `block`.)
Handles things like `try!` (which is represented as two separate tokens
-- we explicitly union their ranges) and "let" binding modifiers (where
we reuse the bindingSpecifier, getting its location and string value for
free).
Build arguments, tuple-pattern elements, and tuple-type elements from their meaningful child ranges so separators remain owned by their parent nodes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep external child locations visible in AST dumps without treating non-containment as a fatal source-skeleton error.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep inherited property metadata at its original source while limiting each accessor declaration to its own keyword and optional body.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@tausbn
tausbn force-pushed the tausbn/unified-improve-node-locations branch from ad29326 to 8b8e8f7 Compare September 18, 2026 14:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation no-change-note-required This PR does not need a change note

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant