On this page:
4.3.1 Original Syntax Paths and Formatting Preservation
4.3.2 Basic Syntax Path Operations
syntax-path?
root-syntax-path?
child-syntax-path?
root-syntax-path
syntax-path
syntax-path-elements
syntax-path-add
syntax-path-parent
syntax-path-last-element
syntax-path-next-neighbor
syntax-path-neighbors?
syntax-path-remove-prefix
syntax-path<=>
syntax-path->string
string->syntax-path
4.3.3 Operating on Syntax Objects with Syntax Paths
syntax-ref
syntax-contains-path?
syntax-set
syntax-remove-splice
syntax-insert-splice
syntax-label-paths
in-syntax-paths
9.2

4.3 Syntax Paths🔗ℹ

 (require resyntax/grimoire/syntax-path) package: resyntax

A syntax path identifies the location of a subform within a syntax object. Syntax paths are sequences of zero-based indices: the root path, which contains no indices, refers to an entire syntax object, and a path whose first element is i refers to a location within the syntax object’s i-th child. Syntax paths are similar in spirit to filesystem paths, and they print in a filesystem-like notation — the path (syntax-path (list 1 2 3)) prints as #<syntax-path:/1/2/3>. Resyntax uses syntax paths to refer to subforms of a program in a way that doesn’t depend on exact source locations or syntax object identity, and which is relatively stable when structure-preserving edits are made to source text.

The children of a syntax object are determined by the shape of its datum:

  • The children of a pair-based form are the elements of its normalized shape: the proper list obtained by treating the trailing atom of an improper list, if any, as a final element. Only the form’s own pair structure is normalized — nested forms remain distinct children — but how the underlying pairs and syntax objects nest has no effect on paths: #(a b c), #(a . (b . (c . ()))), #(a . (b c)), #(a b . c), and #(a . (b . c)) all have three children, and in each case the child at index 2 is #'c.

  • The children of a vector are its elements, in order.

  • A box has a single child, its contents, at index 0.

  • The children of a prefab struct are its fields, in order.

  • Hash datums cannot be traversed by syntax paths. Operations that would need to traverse a hash raise contract errors instead. More about this constraint is explained in Original Syntax Paths and Formatting Preservation.

  • All other datums have no children.

4.3.1 Original Syntax Paths and Formatting Preservation🔗ℹ

When Resyntax calls the Racket reader to turn source code into unexpanded syntax objects using source-read-syntax, Resyntax recursively labels each syntax object with its original syntax path. This label is attached to each syntax object via a syntax property named 'original-syntax-path.

As these syntax objects are expanded by the Racket macro expander and further manipulated by refactoring rules, subforms get transformed and moved, but they preserve their original syntax properties. Resyntax then inspects these properties on the refactored syntax objects produced by rules in order to determine where each syntax object originated structurally, in addition to inspecting source locations to determine where it originated textually. Resyntax uses this information to determine when refactored code contains unchanged subexpressions whose text should be copied from the input rather than reproduced wholesale and reformatted. This mechanism is what powers the automatic comment preservation described in Exercising Fine Control Over Comments, and the template metafunctions described in that section give rule authors a way to guide it.

Unchanged syntax objects have their original text copied by Resyntax, and if two unchanged syntax objects start and end adjacent to each other (and without swapping their order) then the original text between them is also copied. This allows Resyntax to minimize the amount of text that a refactoring has to change. However, in order for this process to work, Resyntax makes a few assumptions about the relationship between syntax paths and the source locations of reader-produced syntax objects. These assumptions are:

  • If one syntax path is a prefix of another, then the source location of a syntax object originally at the first path (the prefix path) should fully contain the source location of a syntax object originally at the second path. This need not be a proper subset relation: it’s acceptable for the two syntax objects to have the exact same source location. That allowance doesn’t affect code written in #lang racket, but it matters for other languages which sometimes use "invisible" wrappers around syntax objects to reflect grouping implied by how the text was originally parsed by the language’s reader.

  • If two syntax paths are disjoint, meaning neither is a prefix of the other, then the source locations of two syntax objects originally at those paths should contain no overlap (but they may be adjacent). Furthermore, the lesser syntax path (in the sense of syntax-path<=>) should correspond to the source location that occurs earlier in the source code (by character position).

It is the second of these two requirements that prevents Resyntax from traversing literal hash datums. A hash datum such as #hash((b . 3) (a . 1) ("foo" . 2)), when parsed by the Racket reader, does not preserve source ordering in the resulting hash datum’s iteration order. Two literal hash datums with the same keys will always iterate in the same order when inspected with syntax-e. As a result, Resyntax cannot uphold the assumptions it makes about source locations and syntax paths, and cannot preserve text between adjacent hash entries correctly. For this reason, Resyntax does not allow editing expressions inside hash datums.

4.3.2 Basic Syntax Path Operations🔗ℹ

procedure

(syntax-path? v)  boolean?

  v : any/c
A predicate that recognizes syntax paths.

procedure

(root-syntax-path? v)  boolean?

  v : any/c
A predicate that recognizes the root syntax path. Implies syntax-path?.

procedure

(child-syntax-path? v)  boolean?

  v : any/c
A predicate that recognizes syntax paths that refer to a child of some enclosing form — that is, any path except the root. Implies syntax-path?.

The root syntax path, which contains no indices and refers to an entire syntax object rather than to any subform within it.

procedure

(syntax-path elements)  syntax-path?

  elements : (sequence/c exact-nonnegative-integer?)
Constructs a syntax path from a sequence of zero-based child indices.

Returns the child indices that make up path, as a treelist.

procedure

(syntax-path-add path element)  syntax-path?

  path : syntax-path?
  element : exact-nonnegative-integer?
Extends path with one additional child index. The resulting path refers to the element-th child of the subform that path refers to.

procedure

(syntax-path-parent path)  syntax-path?

  path : child-syntax-path?
Returns the path to the parent of path, which refers to the form that immediately encloses the form referred to by path.

Returns the final child index of path, which is the position of the subform that path refers to within its enclosing form.

procedure

(syntax-path-next-neighbor path)  (or/c syntax-path? #false)

  path : syntax-path?
Returns the path to the sibling immediately following path within its enclosing form, or #false if path is the root path (the root of a syntax object has no siblings). Note that this is pure path arithmetic: the returned path is not guaranteed to actually exist within any particular syntax object.

procedure

(syntax-path-neighbors? leading-path    
  trailing-path)  boolean?
  leading-path : syntax-path?
  trailing-path : syntax-path?
Returns #true if leading-path and trailing-path refer to immediately adjacent siblings, meaning they share the same parent path and trailing-path’s final child index is one greater than leading-path’s. Warning: the order of leading-path and trailing-path is significant — this operation returns #false if leading-path and trailing-path are adjacent siblings where the trailing path comes first.

procedure

(syntax-path-remove-prefix path prefix)  syntax-path?

  path : syntax-path?
  prefix : syntax-path?
Returns path with the leading elements of prefix removed, producing a path relative to the subform that prefix refers to. Raises a contract error if path does not start with prefix.

A comparator that orders syntax paths lexicographically by their child indices. A path that is a prefix of another path sorts before it, so ancestors precede their descendants and sorting a collection of paths produces a depth-first preorder traversal.

procedure

(syntax-path->string path)  immutable-string?

  path : syntax-path?
Returns a string notation for path in which each child index is preceded by a slash, like a filesystem path. The root path is rendered as "/".

procedure

(string->syntax-path str)  syntax-path?

  str : string?
Parses str as a syntax path. This is the inverse of syntax-path->string. The string must start with a slash, must not end with a slash (except for the root path "/"), and must contain only slash-separated nonnegative integers. Raises a contract error otherwise.

4.3.3 Operating on Syntax Objects with Syntax Paths🔗ℹ

procedure

(syntax-ref stx path)  syntax?

  stx : syntax?
  path : syntax-path?
Returns the subform of stx that path refers to. The root path returns stx itself. Raises a contract error if path is inconsistent with the shape of stx, which can occur if path refers to children of an atomic subform that has no children, or if path contains a child index that’s too large for the number of children actually contained by the parent subform of that index.

procedure

(syntax-contains-path? stx path)  boolean?

  stx : syntax?
  path : syntax-path?
Returns #true if path refers to a subform of stx, meaning syntax-ref would succeed.

procedure

(syntax-set stx path new-subform)  syntax?

  stx : syntax?
  path : syntax-path?
  new-subform : syntax?
Returns a copy of stx in which the subform that path refers to is replaced with new-subform. Passing the root path returns new-subform itself. The lexical context, source locations, and syntax properties of the enclosing forms are preserved.

procedure

(syntax-remove-splice stx    
  path    
  children-count)  syntax?
  stx : syntax?
  path : child-syntax-path?
  children-count : exact-nonnegative-integer?
Returns a copy of stx with children-count consecutive children removed from the form enclosing path, starting with the child that path refers to. The enclosing form must be a proper list. Removing zero children returns stx unchanged. Removing one child removes only the subform referred to by path. If stx does not contain path (in the sense of syntax-contains-path?) a contract error is raised.

procedure

(syntax-insert-splice stx path new-children)  syntax?

  stx : syntax?
  path : child-syntax-path?
  new-children : (sequence/c syntax?)
Returns a copy of stx with each syntax object in new-children inserted as consecutive children of the form enclosing path, such that the first inserted child is located at path. Existing children at or after path are shifted over — new-children are inserted just before path. The enclosing form must be a proper list. Inserting an empty sequence returns stx unchanged.

procedure

(syntax-label-paths stx property-name)  syntax?

  stx : syntax?
  property-name : symbol?
Returns a copy of stx in which every subform, including stx itself, has a syntax property named property-name whose value is that subform’s syntax path within stx. Subforms of hash datums are not labeled, as syntax paths cannot refer to them — see Original Syntax Paths and Formatting Preservation for further explanation.

procedure

(in-syntax-paths stx [#:base-path base-path])

  (sequence/c syntax-path?)
  stx : syntax?
  base-path : syntax-path? = root-syntax-path
Returns a sequence of the syntax paths of every subform in stx, in depth-first preorder. Each returned path is prefixed with base-path, so the first path in the sequence is always base-path itself. The base-path argument is useful when stx is itself a subform of some larger syntax object, such as the root syntax object for the entire source file that stx originates from.