diff --git a/default-recommendations/analyzers/function-expression-analyzer.rkt b/default-recommendations/analyzers/function-expression-analyzer.rkt index d64254e..e5d935f 100644 --- a/default-recommendations/analyzers/function-expression-analyzer.rkt +++ b/default-recommendations/analyzers/function-expression-analyzer.rkt @@ -12,7 +12,7 @@ (require racket/stream rebellion/streaming/transducer resyntax/private/analyzer - resyntax/private/syntax-path + resyntax/grimoire/syntax-path resyntax/private/syntax-property-bundle resyntax/private/syntax-traversal syntax/parse) diff --git a/default-recommendations/analyzers/identifier-usage.rkt b/default-recommendations/analyzers/identifier-usage.rkt index de46499..5d990dc 100644 --- a/default-recommendations/analyzers/identifier-usage.rkt +++ b/default-recommendations/analyzers/identifier-usage.rkt @@ -15,7 +15,7 @@ rebellion/streaming/transducer resyntax/default-recommendations/analyzers/private/expanded-id-table resyntax/private/analyzer - resyntax/private/syntax-path + resyntax/grimoire/syntax-path resyntax/private/syntax-property-bundle resyntax/private/syntax-traversal syntax/id-table diff --git a/default-recommendations/analyzers/ignored-result-values.rkt b/default-recommendations/analyzers/ignored-result-values.rkt index 156e4cf..c2944ec 100644 --- a/default-recommendations/analyzers/ignored-result-values.rkt +++ b/default-recommendations/analyzers/ignored-result-values.rkt @@ -11,7 +11,7 @@ (require racket/stream resyntax/private/analyzer - resyntax/private/syntax-path + resyntax/grimoire/syntax-path resyntax/private/syntax-property-bundle resyntax/private/syntax-traversal syntax/parse) diff --git a/default-recommendations/analyzers/variable-mutability.rkt b/default-recommendations/analyzers/variable-mutability.rkt index 78d1dd4..1647ca9 100644 --- a/default-recommendations/analyzers/variable-mutability.rkt +++ b/default-recommendations/analyzers/variable-mutability.rkt @@ -16,7 +16,7 @@ rebellion/streaming/transducer resyntax/default-recommendations/analyzers/private/expanded-id-table resyntax/private/analyzer - resyntax/private/syntax-path + resyntax/grimoire/syntax-path resyntax/private/syntax-property-bundle resyntax/private/syntax-traversal syntax/id-table diff --git a/grimoire.scrbl b/grimoire.scrbl index 30327d3..cf2da9f 100644 --- a/grimoire.scrbl +++ b/grimoire.scrbl @@ -4,9 +4,13 @@ @(require (for-label racket/base racket/contract/base racket/path + racket/sequence + racket/treelist + rebellion/base/comparator rebellion/base/immutable-string rebellion/collection/range-set resyntax/grimoire/source + resyntax/grimoire/syntax-path syntax/modread)) @@ -30,8 +34,8 @@ In Resyntax, @deftech{source code} refers to @racket[source?] values, which come @item{@emph{Source strings}, constructed with @racket[string-source], which contain the source code directly as a string and don't exist anywhere on the local filesystem. (These are useful for - testing Resyntax, and other scenarios where Resyntax needs to operate on code that doesn't exist on - disk.)} + testing Resyntax, and other scenarios where Resyntax needs to operate on code that doesn't exist on + disk.)} @item{@emph{Modified sources}, constructed by passing another (unmodified) source to @racket[modified-source] along with a string representing what to replace the source's contents @@ -156,11 +160,15 @@ This applies to both modified and unmodified file sources. @defproc[(source-read-syntax [code source?]) syntax?]{ Reads @racket[code] as a syntax object, using the module reading parameterization to allow the - source's @hash-lang[] to control the reader.} + source's @hash-lang[] to control the reader. Every syntax object within the result is labeled with + its @tech{original syntax path} using the @racket['original-syntax-path] syntax property, as + described in @secref["original-syntax-paths"].} @defproc[(source-expand [code source?]) syntax?]{ - Reads @racket[code] and fully expands it, as in @racket[expand].} + Reads @racket[code] and fully expands it, as in @racket[expand]. Because the program is read with + @racket[source-read-syntax], its subforms are labeled with @racket['original-syntax-path] + properties before expansion occurs.} @defproc[(source-can-expand? [code source?]) boolean?]{ @@ -180,3 +188,235 @@ This applies to both modified and unmodified file sources. @racketmodname[syntax-color/module-lexer] API. @bold{Warning: the positions are zero-based}, unlike the one-based positions returned from @racket[syntax-position]. Additionally, positions are in terms of @emph{characters} and not @emph{bytes}.} + + +@section{Syntax Paths} +@defmodule[resyntax/grimoire/syntax-path] + +A @deftech{syntax path} identifies the location of a subform within a syntax object. Syntax paths +are sequences of zero-based indices: the @emph{root} path, which contains no indices, refers to an +entire syntax object, and a path whose first element is @racket[_i] refers to a location within the +syntax object's @racket[_i]-th child. Syntax paths are similar in spirit to filesystem paths, and they +print in a filesystem-like notation --- the path @racket[(syntax-path (list 1 2 3))] prints as +@racketresultfont{#}. 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: + +@itemlist[ + @item{The children of a pair-based form are the elements of its @emph{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: + @tt{#'(a b c)}, @tt{#'(a . (b . (c . ())))}, @tt{#'(a . (b c))}, @tt{#'(a b . c)}, + and @tt{#'(a . (b . c))} all have three children, and in each case the child at index + @racket[2] is @racket[#'c].} + + @item{The children of a vector are its elements, in order.} + + @item{A box has a single child, its contents, at index @racket[0].} + + @item{The children of a prefab struct are its fields, in order.} + + @item{Hash datums @emph{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 + @secref["original-syntax-paths"].} + + @item{All other datums have no children.}] + + +@subsection[#:tag "original-syntax-paths"]{Original Syntax Paths and Formatting Preservation} + +When Resyntax calls the Racket reader to turn @tech{source code} into unexpanded syntax objects using +@racket[source-read-syntax], Resyntax recursively labels each syntax object with its +@deftech{original syntax path}. This label is attached to each syntax object via a +@tech[#:doc '(lib "scribblings/reference/reference.scrbl")]{syntax property} named +@indexed-racket['original-syntax-path]. + +As these syntax objects are expanded by the Racket macro expander and further manipulated by +@tech{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 @emph{structurally}, in addition to +inspecting source locations to determine where it originated @emph{textually}. Resyntax uses this +information to determine when refactored code contains unchanged subexpressions whose text should be +@emph{copied} from the input rather than reproduced wholesale and reformatted. This mechanism is what +powers the automatic comment preservation described in @secref["comment-preservation"], 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: + +@itemlist[ + @item{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 @emph{fully contain} the source location of a + syntax object originally at the second path. This need not be a @emph{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 @hash-lang[] @racketmodname[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.} + + @item{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 @racket[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 @racket[#hash((a . 1) ("foo" . 2) (b . 3))], when parsed by the Racket reader, +@bold{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 +@racket[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. + + +@subsection{Basic Syntax Path Operations} + + +@defproc[(syntax-path? [v any/c]) boolean?]{ + A predicate that recognizes @tech{syntax paths}.} + + +@defproc[(root-syntax-path? [v any/c]) boolean?]{ + A predicate that recognizes the root @tech{syntax path}. Implies @racket[syntax-path?].} + + +@defproc[(child-syntax-path? [v any/c]) boolean?]{ + A predicate that recognizes @tech{syntax paths} that refer to a child of some enclosing form --- + that is, any path except the root. Implies @racket[syntax-path?].} + + +@defthing[root-syntax-path syntax-path?]{ + The root @tech{syntax path}, which contains no indices and refers to an entire syntax object + rather than to any subform within it.} + + +@defproc[(syntax-path [elements (sequence/c exact-nonnegative-integer?)]) syntax-path?]{ + Constructs a @tech{syntax path} from a sequence of zero-based child indices.} + + +@defproc[(syntax-path-elements [path syntax-path?]) (treelist/c exact-nonnegative-integer?)]{ + Returns the child indices that make up @racket[path], as a + @tech[#:doc '(lib "scribblings/reference/reference.scrbl")]{treelist}.} + + +@defproc[(syntax-path-add [path syntax-path?] [element exact-nonnegative-integer?]) + syntax-path?]{ + Extends @racket[path] with one additional child index. The resulting path refers to the + @racket[element]-th child of the subform that @racket[path] refers to.} + + +@defproc[(syntax-path-parent [path child-syntax-path?]) syntax-path?]{ + Returns the path to the parent of @racket[path], which refers to the form that immediately encloses + the form referred to by @racket[path].} + + +@defproc[(syntax-path-last-element [path child-syntax-path?]) exact-nonnegative-integer?]{ + Returns the final child index of @racket[path], which is the position of the subform that + @racket[path] refers to within its enclosing form.} + + +@defproc[(syntax-path-next-neighbor [path syntax-path?]) (or/c syntax-path? #false)]{ + Returns the path to the sibling immediately following @racket[path] within its enclosing form, or + @racket[#false] if @racket[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.} + + +@defproc[(syntax-path-neighbors? [leading-path syntax-path?] [trailing-path syntax-path?]) + boolean?]{ + Returns @racket[#true] if @racket[leading-path] and @racket[trailing-path] refer to immediately + adjacent siblings, meaning they share the same parent path and @racket[trailing-path]'s final + child index is one greater than @racket[leading-path]'s. @bold{Warning:} the order of + @racket[leading-path] and @racket[trailing-path] is significant --- this operation returns + @racket[#false] if @racket[leading-path] and @racket[trailing-path] are adjacent siblings where the + @emph{trailing} path comes first.} + + +@defproc[(syntax-path-remove-prefix [path syntax-path?] [prefix syntax-path?]) syntax-path?]{ + Returns @racket[path] with the leading elements of @racket[prefix] removed, producing a path + relative to the subform that @racket[prefix] refers to. Raises a contract error if @racket[path] + does not start with @racket[prefix].} + + +@defthing[syntax-path<=> (comparator/c syntax-path?)]{ + A comparator that orders @tech{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.} + + +@defproc[(syntax-path->string [path syntax-path?]) immutable-string?]{ + Returns a string notation for @racket[path] in which each child index is preceded by a slash, like + a filesystem path. The root path is rendered as @racket["/"].} + + +@defproc[(string->syntax-path [str string?]) syntax-path?]{ + Parses @racket[str] as a @tech{syntax path}. This is the inverse of @racket[syntax-path->string]. + The string must start with a slash, must not end with a slash (except for the root path + @racket["/"]), and must contain only slash-separated nonnegative integers. Raises a contract error + otherwise.} + + +@subsection{Operating on Syntax Objects with Syntax Paths} + + +@defproc[(syntax-ref [stx syntax?] [path syntax-path?]) syntax?]{ + Returns the subform of @racket[stx] that @racket[path] refers to. The root path returns + @racket[stx] itself. Raises a contract error if @racket[path] is inconsistent with the shape of + @racket[stx], which can occur if @racket[path] refers to children of an atomic subform that has no + children, or if @racket[path] contains a child index that's too large for the number of children + actually contained by the parent subform of that index.} + + +@defproc[(syntax-contains-path? [stx syntax?] [path syntax-path?]) boolean?]{ + Returns @racket[#true] if @racket[path] refers to a subform of @racket[stx], meaning + @racket[syntax-ref] would succeed.} + + +@defproc[(syntax-set [stx syntax?] [path syntax-path?] [new-subform syntax?]) syntax?]{ + Returns a copy of @racket[stx] in which the subform that @racket[path] refers to is replaced with + @racket[new-subform]. Passing the root path returns @racket[new-subform] itself. The lexical + context, source locations, and syntax properties of the enclosing forms are preserved.} + + +@defproc[(syntax-remove-splice [stx syntax?] + [path child-syntax-path?] + [children-count exact-nonnegative-integer?]) + syntax?]{ + Returns a copy of @racket[stx] with @racket[children-count] consecutive children removed from the + form enclosing @racket[path], starting with the child that @racket[path] refers to. The enclosing + form must be a proper list. Removing zero children returns @racket[stx] unchanged. Removing one child + removes @emph{only} the subform referred to by @racket[path]. If @racket[stx] does not contain + @racket[path] (in the sense of @racket[syntax-contains-path?]) a contract error is raised.} + + +@defproc[(syntax-insert-splice [stx syntax?] + [path child-syntax-path?] + [new-children (sequence/c syntax?)]) + syntax?]{ + Returns a copy of @racket[stx] with each syntax object in @racket[new-children] inserted as + consecutive children of the form enclosing @racket[path], such that the first inserted child is + located at @racket[path]. Existing children at or after @racket[path] are shifted over --- + @racket[new-children] are inserted @emph{just before} @racket[path]. The enclosing form must be a + proper list. Inserting an empty sequence returns @racket[stx] unchanged.} + + +@defproc[(syntax-label-paths [stx syntax?] [property-name symbol?]) syntax?]{ + Returns a copy of @racket[stx] in which every subform, including @racket[stx] itself, has a + @tech[#:doc '(lib "scribblings/reference/reference.scrbl")]{syntax property} named + @racket[property-name] whose value is that subform's @tech{syntax path} within @racket[stx]. + Subforms of hash datums are not labeled, as syntax paths cannot refer to them --- see + @secref["original-syntax-paths"] for further explanation.} + + +@defproc[(in-syntax-paths [stx syntax?] [#:base-path base-path syntax-path? root-syntax-path]) + (sequence/c syntax-path?)]{ + Returns a sequence of the @tech{syntax paths} of every subform in @racket[stx], in depth-first + preorder. Each returned path is prefixed with @racket[base-path], so the first path in the + sequence is always @racket[base-path] itself. The @racket[base-path] argument is useful when + @racket[stx] is itself a subform of some larger syntax object, such as the root syntax object for the + entire source file that @racket[stx] originates from.} diff --git a/private/syntax-path.rkt b/grimoire/syntax-path.rkt similarity index 93% rename from private/syntax-path.rkt rename to grimoire/syntax-path.rkt index 9a012a4..bd7eb4b 100644 --- a/private/syntax-path.rkt +++ b/grimoire/syntax-path.rkt @@ -8,14 +8,14 @@ (contract-out [syntax-path? (-> any/c boolean?)] [syntax-path<=> (comparator/c syntax-path?)] - [empty-syntax-path? (-> any/c boolean?)] - [nonempty-syntax-path? (-> any/c boolean?)] - [empty-syntax-path syntax-path?] + [root-syntax-path? (-> any/c boolean?)] + [child-syntax-path? (-> any/c boolean?)] + [root-syntax-path syntax-path?] [syntax-path (-> (sequence/c exact-nonnegative-integer?) syntax-path?)] [syntax-path-elements (-> syntax-path? (treelist/c exact-nonnegative-integer?))] - [syntax-path-parent (-> nonempty-syntax-path? syntax-path?)] + [syntax-path-parent (-> child-syntax-path? syntax-path?)] [syntax-path-next-neighbor (-> syntax-path? (or/c syntax-path? #false))] - [syntax-path-last-element (-> nonempty-syntax-path? exact-nonnegative-integer?)] + [syntax-path-last-element (-> child-syntax-path? exact-nonnegative-integer?)] [syntax-path-add (-> syntax-path? exact-nonnegative-integer? syntax-path?)] [syntax-path-remove-prefix (-> syntax-path? syntax-path? syntax-path?)] [syntax-path-neighbors? (-> syntax-path? syntax-path? boolean?)] @@ -24,8 +24,8 @@ [syntax-ref (-> syntax? syntax-path? syntax?)] [syntax-contains-path? (-> syntax? syntax-path? boolean?)] [syntax-set (-> syntax? syntax-path? syntax? syntax?)] - [syntax-remove-splice (-> syntax? nonempty-syntax-path? exact-nonnegative-integer? syntax?)] - [syntax-insert-splice (-> syntax? nonempty-syntax-path? (sequence/c syntax?) syntax?)] + [syntax-remove-splice (-> syntax? child-syntax-path? exact-nonnegative-integer? syntax?)] + [syntax-insert-splice (-> syntax? child-syntax-path? (sequence/c syntax?) syntax?)] [syntax-label-paths (-> syntax? symbol? syntax?)] [in-syntax-paths (->* (syntax?) (#:base-path syntax-path?) (sequence/c syntax-path?))])) @@ -73,16 +73,16 @@ (write-string ">" out))]) -(define empty-syntax-path (syntax-path (treelist))) +(define root-syntax-path (syntax-path (treelist))) (module+ test (test-case "syntax-path custom printing" - (test-case "empty path prints as #" - (check-equal? (~a empty-syntax-path) "#") - (check-equal? (~v empty-syntax-path) "#") - (check-equal? (~s empty-syntax-path) "#")) + (test-case "root path prints as #" + (check-equal? (~a root-syntax-path) "#") + (check-equal? (~v root-syntax-path) "#") + (check-equal? (~s root-syntax-path) "#")) (test-case "single element path prints compactly" (define path (syntax-path (list 0))) @@ -99,26 +99,26 @@ (check-equal? (~a path) "#")))) -(define (empty-syntax-path? v) +(define (root-syntax-path? v) (and (syntax-path? v) (treelist-empty? (syntax-path-elements v)))) (module+ test - (test-case "empty-syntax-path?" - (check-true (empty-syntax-path? empty-syntax-path)) - (check-false (empty-syntax-path? (syntax-path (list 0)))) - (check-false (empty-syntax-path? 42)))) + (test-case "root-syntax-path?" + (check-true (root-syntax-path? root-syntax-path)) + (check-false (root-syntax-path? (syntax-path (list 0)))) + (check-false (root-syntax-path? 42)))) -(define (nonempty-syntax-path? v) +(define (child-syntax-path? v) (and (syntax-path? v) (not (treelist-empty? (syntax-path-elements v))))) (module+ test - (test-case "nonempty-syntax-path?" - (check-false (nonempty-syntax-path? empty-syntax-path)) - (check-true (nonempty-syntax-path? (syntax-path (list 0)))) - (check-false (nonempty-syntax-path? 42)))) + (test-case "child-syntax-path?" + (check-false (child-syntax-path? root-syntax-path)) + (check-true (child-syntax-path? (syntax-path (list 0)))) + (check-false (child-syntax-path? 42)))) @@ -130,7 +130,7 @@ (module+ test (test-case "syntax-path-add" - (check-equal? (syntax-path-add empty-syntax-path 0) (syntax-path (list 0))) + (check-equal? (syntax-path-add root-syntax-path 0) (syntax-path (list 0))) (check-equal? (syntax-path-add (syntax-path (list 0)) 1) (syntax-path (list 0 1))))) @@ -141,11 +141,11 @@ (module+ test (test-case "syntax-path-parent" - (check-equal? (syntax-path-parent (syntax-path (list 0))) empty-syntax-path) + (check-equal? (syntax-path-parent (syntax-path (list 0))) root-syntax-path) (check-equal? (syntax-path-parent (syntax-path (list 0 1))) (syntax-path (list 0))) - (check-exn exn:fail:contract? (λ () (syntax-path-parent empty-syntax-path))) - (check-exn #rx"expected: nonempty-syntax-path?" (λ () (syntax-path-parent empty-syntax-path))))) + (check-exn exn:fail:contract? (λ () (syntax-path-parent root-syntax-path))) + (check-exn #rx"expected: child-syntax-path?" (λ () (syntax-path-parent root-syntax-path))))) (define/guard (syntax-path-next-neighbor path) @@ -159,8 +159,8 @@ (module+ test (test-case "syntax-path-next-neighbor" - (test-case "empty path" - (check-false (syntax-path-next-neighbor empty-syntax-path))) + (test-case "root path" + (check-false (syntax-path-next-neighbor root-syntax-path))) (test-case "first child" (define path (syntax-path (list 0))) @@ -197,9 +197,9 @@ (module+ test (test-case "syntax-path-remove-prefix" - (test-case "remove empty" + (test-case "remove root prefix" (define path (syntax-path (list 1 2 3))) - (check-equal? (syntax-path-remove-prefix path empty-syntax-path) path)) + (check-equal? (syntax-path-remove-prefix path root-syntax-path) path)) (test-case "remove one elem" (define path (syntax-path (list 1 2 3))) @@ -223,14 +223,14 @@ (check-equal? (syntax-path-last-element (syntax-path (list 0))) 0) (check-equal? (syntax-path-last-element (syntax-path (list 0 1))) 1) - (check-exn exn:fail:contract? (λ () (syntax-path-last-element empty-syntax-path))) - (check-exn #rx"expected: nonempty-syntax-path?" - (λ () (syntax-path-last-element empty-syntax-path))))) + (check-exn exn:fail:contract? (λ () (syntax-path-last-element root-syntax-path))) + (check-exn #rx"expected: child-syntax-path?" + (λ () (syntax-path-last-element root-syntax-path))))) (define (syntax-path-neighbors? leading-path trailing-path) - (and (nonempty-syntax-path? leading-path) - (nonempty-syntax-path? trailing-path) + (and (child-syntax-path? leading-path) + (child-syntax-path? trailing-path) (equal? (syntax-path-parent leading-path) (syntax-path-parent trailing-path)) (syntax-path-element-neighbors? (syntax-path-last-element leading-path) (syntax-path-last-element trailing-path)))) @@ -253,8 +253,8 @@ (module+ test (test-case "syntax-path->string" - (test-case "empty path" - (check-equal? (syntax-path->string empty-syntax-path) "/")) + (test-case "root path" + (check-equal? (syntax-path->string root-syntax-path) "/")) (test-case "single element" (check-equal? (syntax-path->string (syntax-path (list 0))) "/0")) @@ -278,7 +278,7 @@ "syntax path string must not end with / (except for root path)" "given" str)) (cond - [(equal? str "/") empty-syntax-path] + [(equal? str "/") root-syntax-path] [else (define parts (string-split (substring str 1) "/")) (define numbers @@ -298,8 +298,8 @@ (module+ test (test-case "string->syntax-path" - (test-case "empty path" - (check-equal? (string->syntax-path "/") empty-syntax-path)) + (test-case "root path" + (check-equal? (string->syntax-path "/") root-syntax-path)) (test-case "single element" (check-equal? (string->syntax-path "/0") (syntax-path (list 0)))) @@ -371,9 +371,9 @@ (module+ test (test-case "round-trip conversion" - (test-case "empty path" - (check-equal? (string->syntax-path (syntax-path->string empty-syntax-path)) - empty-syntax-path)) + (test-case "root path" + (check-equal? (string->syntax-path (syntax-path->string root-syntax-path)) + root-syntax-path)) (test-case "single element path" (define path (syntax-path (list 5))) @@ -522,9 +522,9 @@ (module+ test (test-case "syntax-ref" - (test-case "empty path" + (test-case "root path" (define stx #'a) - (define actual (syntax-ref stx empty-syntax-path)) + (define actual (syntax-ref stx root-syntax-path)) (check-equal? actual stx)) (test-case "list element path" @@ -654,9 +654,9 @@ (module+ test (test-case "syntax-contains-path?" - (test-case "empty path on any syntax" + (test-case "root path on any syntax" (define stx #'a) - (check-true (syntax-contains-path? stx empty-syntax-path))) + (check-true (syntax-contains-path? stx root-syntax-path))) (test-case "valid path in list" (define stx #'(a b c)) @@ -822,9 +822,9 @@ (define new-subform #'FOO) - (test-case "empty path" + (test-case "root path" (define stx #'a) - (define actual (syntax-set stx empty-syntax-path new-subform)) + (define actual (syntax-set stx root-syntax-path new-subform)) (check-equal? actual new-subform)) (test-case "list element path" @@ -877,6 +877,11 @@ (define/guard (syntax-remove-splice stx path children-count) + (unless (syntax-contains-path? stx path) + (raise-arguments-error 'syntax-remove-splice + "syntax path does not exist within the given syntax object" + "syntax" stx + "path" path)) (guard (positive? children-count) #:else stx) (define parent (syntax-ref stx (syntax-path-parent path))) (define updated @@ -917,6 +922,11 @@ (check-exn exn:fail? (λ () (syntax-remove-splice stx (syntax-path (list 1)) 10)))) + (test-case "remove zero children from out of bounds path - should still error" + (define stx #'(a b c)) + (define bad-path (syntax-path (list 42))) + (check-exn exn:fail? (λ () (syntax-remove-splice stx bad-path 0)))) + (test-case "nested list removal" (define stx #'(a (x y z) b)) (define actual (syntax-remove-splice stx (syntax-path (list 1 1)) 1)) @@ -927,10 +937,10 @@ (check-exn exn:fail? (λ () (syntax-remove-splice stx (syntax-path (list 0)) 1)))) - (test-case "error on empty path with non-zero count" + (test-case "error on root path with non-zero count" (define stx #'(a b c)) (check-exn exn:fail:contract? - (λ () (syntax-remove-splice stx empty-syntax-path 1)))) + (λ () (syntax-remove-splice stx root-syntax-path 1)))) (test-case "error on non-list target" (define stx #'#(a b c)) @@ -989,10 +999,10 @@ (define actual (syntax-insert-splice stx (syntax-path (list 0)) (list #'x))) (check-equal? (syntax->datum actual) '(x))) - (test-case "error on empty path" + (test-case "error on root path" (define stx #'(a b c)) (check-exn exn:fail:contract? - (λ () (syntax-insert-splice stx empty-syntax-path (list #'x))))) + (λ () (syntax-insert-splice stx root-syntax-path (list #'x))))) (test-case "error on non-list target" (define stx #'#(a b c)) @@ -1228,7 +1238,7 @@ [_ (in-list (list))])) -(define (in-syntax-paths stx #:base-path [base-path empty-syntax-path]) +(define (in-syntax-paths stx #:base-path [base-path root-syntax-path]) (in-generator (let traverse ([stx stx] [parent-elems (syntax-path-elements base-path)]) diff --git a/private/analysis.rkt b/private/analysis.rkt index dc944de..2b9af38 100644 --- a/private/analysis.rkt +++ b/private/analysis.rkt @@ -48,7 +48,7 @@ resyntax/private/string-indent resyntax/private/syntax-movement resyntax/private/syntax-neighbors - resyntax/private/syntax-path + resyntax/grimoire/syntax-path resyntax/private/syntax-property-bundle resyntax/private/syntax-traversal syntax/parse) @@ -333,7 +333,7 @@ (λ (expanded) (syntax-property-bundle ;; Valid path - the root - (syntax-property-entry empty-syntax-path 'valid-prop #true) + (syntax-property-entry root-syntax-path 'valid-prop #true) ;; Invalid path - way out of bounds (syntax-property-entry (syntax-path (list 999)) 'invalid-prop #true) ;; Another invalid path @@ -350,7 +350,7 @@ (check-true (syntax-property-bundle? props)) ;; The valid property at the root should be present - (define root-props (syntax-property-bundle-get-immediate-properties props empty-syntax-path)) + (define root-props (syntax-property-bundle-get-immediate-properties props root-syntax-path)) (check-equal? (hash-ref root-props 'valid-prop #false) #true) ;; The invalid properties should NOT be present @@ -370,7 +370,7 @@ ;; Sleep for 200ms - longer than a 100ms timeout (sleep 0.2) (syntax-property-bundle - (syntax-property-entry empty-syntax-path 'slow-prop #true))))) + (syntax-property-entry root-syntax-path 'slow-prop #true))))) ;; Run analysis with the slow analyzer and a short timeout - should timeout and not crash (define analysis (source-analyze test-source #:analyzers (list slow-analyzer) #:timeout-ms 100)) @@ -381,7 +381,7 @@ ;; Check that no properties were added from the timed-out analyzer (define props (source-code-analysis-added-syntax-properties analysis)) (check-true (syntax-property-bundle? props)) - (define root-props (syntax-property-bundle-get-immediate-properties props empty-syntax-path)) + (define root-props (syntax-property-bundle-get-immediate-properties props root-syntax-path)) ;; The slow-prop should NOT be present since the analyzer timed out (check-false (hash-has-key? root-props 'slow-prop))) @@ -395,7 +395,7 @@ (λ (expanded) ;; This should complete quickly, well within the timeout (syntax-property-bundle - (syntax-property-entry empty-syntax-path 'fast-prop #true))))) + (syntax-property-entry root-syntax-path 'fast-prop #true))))) ;; Run analysis with the fast analyzer - should complete successfully (define analysis (source-analyze test-source #:analyzers (list fast-analyzer) #:timeout-ms 10000)) @@ -406,7 +406,7 @@ ;; Check that properties were added from the successful analyzer (define props (source-code-analysis-added-syntax-properties analysis)) (check-true (syntax-property-bundle? props)) - (define root-props (syntax-property-bundle-get-immediate-properties props empty-syntax-path)) + (define root-props (syntax-property-bundle-get-immediate-properties props root-syntax-path)) ;; The fast-prop should be present since the analyzer completed successfully (check-equal? (hash-ref root-props 'fast-prop #false) #true)) diff --git a/private/syntax-delta.rkt b/private/syntax-delta.rkt index 5589928..0e203da 100644 --- a/private/syntax-delta.rkt +++ b/private/syntax-delta.rkt @@ -21,7 +21,7 @@ rebellion/collection/entry rebellion/collection/sorted-map rebellion/streaming/transducer - resyntax/private/syntax-path) + resyntax/grimoire/syntax-path) (module+ test @@ -80,8 +80,8 @@ (define disjoint? (cond [(equal? children-count 0) #true] - [(empty-syntax-path? start) #false] - [(empty-syntax-path? next-start) #false] + [(root-syntax-path? start) #false] + [(root-syntax-path? next-start) #false] [(equal? (syntax-path-parent start) (syntax-path-parent next-start)) (<= (syntax-path-last-element start) (syntax-path-last-element next-start))] [else #true])) @@ -101,7 +101,12 @@ (match added [(new-syntax new-stx) new-stx] [(copied-syntax orig-path) (syntax-ref stx orig-path)]))) - (syntax-insert-splice (syntax-remove-splice stx start children-count) start new-stxs))) + ;; Pure insertions skip the removal step: their start path may be one past the end of the + ;; enclosing form, which syntax-remove-splice rejects because it only accepts paths of children + ;; that actually exist. + (define stx-with-removals + (if (zero? children-count) stx (syntax-remove-splice stx start children-count))) + (syntax-insert-splice stx-with-removals start new-stxs))) diff --git a/private/syntax-movement.rkt b/private/syntax-movement.rkt index 340e03a..5647306 100644 --- a/private/syntax-movement.rkt +++ b/private/syntax-movement.rkt @@ -17,7 +17,7 @@ rebellion/collection/sorted-set rebellion/streaming/transducer resyntax/private/syntax-neighbors - resyntax/private/syntax-path + resyntax/grimoire/syntax-path resyntax/private/syntax-traversal syntax/parse) @@ -70,8 +70,8 @@ #:key-comparator syntax-path<=> ; (module ...) - empty-syntax-path - (sorted-set empty-syntax-path (syntax-path (list 3)) #:comparator syntax-path<=>) + root-syntax-path + (sorted-set root-syntax-path (syntax-path (list 3)) #:comparator syntax-path<=>) ; module (syntax-path (list 0)) diff --git a/private/syntax-neighbors.rkt b/private/syntax-neighbors.rkt index 953c087..9b7f940 100644 --- a/private/syntax-neighbors.rkt +++ b/private/syntax-neighbors.rkt @@ -27,7 +27,7 @@ racket/struct racket/syntax-srcloc resyntax/private/logger - resyntax/private/syntax-path + resyntax/grimoire/syntax-path syntax/parse syntax/parse/experimental/template) diff --git a/private/syntax-property-bundle.rkt b/private/syntax-property-bundle.rkt index e4549c1..6f01269 100644 --- a/private/syntax-property-bundle.rkt +++ b/private/syntax-property-bundle.rkt @@ -37,7 +37,7 @@ rebellion/collection/sorted-map rebellion/streaming/reducer rebellion/streaming/transducer - resyntax/private/syntax-path) + resyntax/grimoire/syntax-path) (module+ test @@ -204,7 +204,7 @@ (define/guard (syntax-property-bundle-get-all-properties prop-bundle path) - (guard (nonempty-syntax-path? path) #:else prop-bundle) + (guard (child-syntax-path? path) #:else prop-bundle) (define next-neighbor (syntax-path-next-neighbor path)) (define path-range (if next-neighbor @@ -246,7 +246,7 @@ (syntax-set stx path new-subform)) -(define (syntax-immediate-properties stx #:base-path [base-path empty-syntax-path]) +(define (syntax-immediate-properties stx #:base-path [base-path root-syntax-path]) (define keys (syntax-property-symbol-keys stx)) (transduce keys (mapping (λ (key) @@ -255,7 +255,7 @@ #:into into-syntax-property-bundle)) -(define (syntax-all-properties stx #:base-path [base-path empty-syntax-path]) +(define (syntax-all-properties stx #:base-path [base-path root-syntax-path]) (transduce (in-syntax-paths stx #:base-path base-path) (append-mapping (λ (path) @@ -272,7 +272,7 @@ (define stx #'(a (b c) d)) (define props (syntax-property-bundle - (syntax-property-entry empty-syntax-path 'size 3) + (syntax-property-entry root-syntax-path 'size 3) (syntax-property-entry (syntax-path (list 0)) 'headphone-shaped? #false) (syntax-property-entry (syntax-path (list 1)) 'size 2) (syntax-property-entry (syntax-path (list 1 0)) 'headphone-shaped? #true) @@ -298,14 +298,14 @@ (define stx #'(a b c)) (define props (syntax-immediate-properties stx)) (check-true (syntax-property-bundle? props)) - (check-equal? (syntax-property-bundle-get-immediate-properties props empty-syntax-path) + (check-equal? (syntax-property-bundle-get-immediate-properties props root-syntax-path) (hash))) (test-case "syntax with single property" (define stx (syntax-property #'(a b c) 'foo 42)) (define props (syntax-immediate-properties stx)) (check-true (syntax-property-bundle? props)) - (define immediate (syntax-property-bundle-get-immediate-properties props empty-syntax-path)) + (define immediate (syntax-property-bundle-get-immediate-properties props root-syntax-path)) (check-equal? (hash-ref immediate 'foo) 42)) (test-case "syntax with multiple properties" @@ -317,7 +317,7 @@ 'baz "hello")) (define props (syntax-immediate-properties stx)) (check-true (syntax-property-bundle? props)) - (define immediate (syntax-property-bundle-get-immediate-properties props empty-syntax-path)) + (define immediate (syntax-property-bundle-get-immediate-properties props root-syntax-path)) (check-equal? (hash-ref immediate 'foo) 42) (check-equal? (hash-ref immediate 'bar) #true) (check-equal? (hash-ref immediate 'baz) "hello")) @@ -329,7 +329,7 @@ (datum->syntax #f (list inner-stx #'y) #f) 'outer-prop 456)) (define props (syntax-immediate-properties outer-stx)) - (define immediate (syntax-property-bundle-get-immediate-properties props empty-syntax-path)) + (define immediate (syntax-property-bundle-get-immediate-properties props root-syntax-path)) (check-equal? (hash-ref immediate 'outer-prop #false) 456) (check-equal? (hash-ref immediate 'inner-prop #false) #false)))) @@ -347,7 +347,7 @@ (define stx (syntax-property #'(a b c) 'root-prop 42)) (define props (syntax-all-properties stx)) (check-true (syntax-property-bundle? props)) - (define root-props (syntax-property-bundle-get-immediate-properties props empty-syntax-path)) + (define root-props (syntax-property-bundle-get-immediate-properties props root-syntax-path)) (check-equal? (hash-ref root-props 'root-prop) 42)) (test-case "syntax with properties on multiple subforms" @@ -359,7 +359,7 @@ (define props (syntax-all-properties stx-with-root)) (check-true (syntax-property-bundle? props)) - (define root-props (syntax-property-bundle-get-immediate-properties props empty-syntax-path)) + (define root-props (syntax-property-bundle-get-immediate-properties props root-syntax-path)) (check-equal? (hash-ref root-props 'root-prop) 0) (define a-props (syntax-property-bundle-get-immediate-properties props (syntax-path (list 0)))) diff --git a/refactoring-rules.scrbl b/refactoring-rules.scrbl index 4c498ec..41ed603 100644 --- a/refactoring-rules.scrbl +++ b/refactoring-rules.scrbl @@ -178,7 +178,7 @@ refactoring rules. (void)))} -@section{Exercising Fine Control Over Comments} +@section[#:tag "comment-preservation"]{Exercising Fine Control Over Comments} Writing a rule with @racket[define-refactoring-rule] is usually enough for Resyntax to handle @@ -248,6 +248,8 @@ Based on this observation, Resyntax decides to preserve whatever text was origin @racket[(foo ...)] and the nested @racket[or] expression. This mechanism, exposed via @racket[~replacement] and @racket[~splicing-replacement], offers a means for refactoring rules to guide Resyntax's internal comment preservation system when the default behavior is not sufficient. +For an explanation of how that system works under the hood, see @secref["original-syntax-paths"] in +The Resyntax Grimoire. @defform[#:kind "template metafunction" (~replacement replacement-form original) diff --git a/test/private/rackunit.rkt b/test/private/rackunit.rkt index b99c4ec..3eb4dbe 100644 --- a/test/private/rackunit.rkt +++ b/test/private/rackunit.rkt @@ -41,7 +41,7 @@ resyntax/grimoire/source resyntax/private/string-indent resyntax/private/string-replacement - resyntax/private/syntax-path + resyntax/grimoire/syntax-path resyntax/private/syntax-property-bundle resyntax/private/syntax-traversal syntax/modread