diff --git a/base.rkt b/base.rkt index bc86caa..8192c09 100644 --- a/base.rkt +++ b/base.rkt @@ -278,7 +278,7 @@ (module+ test (require rackunit resyntax/private/analyzer - resyntax/private/syntax-property-bundle) + resyntax/grimoire/syntax-property-bundle) (test-case "refactoring-rule stores analyzers" (define-refactoring-rule test-rule diff --git a/default-recommendations/analyzers/function-expression-analyzer.rkt b/default-recommendations/analyzers/function-expression-analyzer.rkt index e5d935f..6c26446 100644 --- a/default-recommendations/analyzers/function-expression-analyzer.rkt +++ b/default-recommendations/analyzers/function-expression-analyzer.rkt @@ -13,7 +13,7 @@ rebellion/streaming/transducer resyntax/private/analyzer resyntax/grimoire/syntax-path - resyntax/private/syntax-property-bundle + resyntax/grimoire/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 5d990dc..396d0ae 100644 --- a/default-recommendations/analyzers/identifier-usage.rkt +++ b/default-recommendations/analyzers/identifier-usage.rkt @@ -16,7 +16,7 @@ resyntax/default-recommendations/analyzers/private/expanded-id-table resyntax/private/analyzer resyntax/grimoire/syntax-path - resyntax/private/syntax-property-bundle + resyntax/grimoire/syntax-property-bundle resyntax/private/syntax-traversal syntax/id-table syntax/parse) diff --git a/default-recommendations/analyzers/ignored-result-values.rkt b/default-recommendations/analyzers/ignored-result-values.rkt index c2944ec..56e26c1 100644 --- a/default-recommendations/analyzers/ignored-result-values.rkt +++ b/default-recommendations/analyzers/ignored-result-values.rkt @@ -12,7 +12,7 @@ (require racket/stream resyntax/private/analyzer resyntax/grimoire/syntax-path - resyntax/private/syntax-property-bundle + resyntax/grimoire/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 1647ca9..35a71da 100644 --- a/default-recommendations/analyzers/variable-mutability.rkt +++ b/default-recommendations/analyzers/variable-mutability.rkt @@ -17,7 +17,7 @@ resyntax/default-recommendations/analyzers/private/expanded-id-table resyntax/private/analyzer resyntax/grimoire/syntax-path - resyntax/private/syntax-property-bundle + resyntax/grimoire/syntax-property-bundle resyntax/private/syntax-traversal syntax/id-table syntax/parse) diff --git a/grimoire.scrbl b/grimoire.scrbl index d6ff83e..02bf46d 100644 --- a/grimoire.scrbl +++ b/grimoire.scrbl @@ -15,4 +15,6 @@ programmatically on anything found here. @include-section[(lib "resyntax/grimoire/source.scrbl")] @include-section[(lib "resyntax/grimoire/source-group.scrbl")] @include-section[(lib "resyntax/grimoire/syntax-path.scrbl")] +@include-section[(lib "resyntax/grimoire/syntax-property-bundle.scrbl")] +@include-section[(lib "resyntax/grimoire/expansion-analyzers.scrbl")] @include-section[(lib "resyntax/grimoire/string-replacement.scrbl")] diff --git a/grimoire/expansion-analyzers.scrbl b/grimoire/expansion-analyzers.scrbl new file mode 100644 index 0000000..9a7ae7e --- /dev/null +++ b/grimoire/expansion-analyzers.scrbl @@ -0,0 +1,12 @@ +#lang scribble/manual + + +@title[#:tag "expansion-analyzers"]{Expansion Analyzers} + +An @deftech{expansion analyzer} examines the fully expanded form of a program and reports facts +about it as a @tech{syntax property bundle} whose @tech{syntax paths} refer to subforms of the +expanded program. Resyntax translates those paths back to the corresponding subforms of the +original unexpanded program and attaches the reported properties to them, making information that +is only discoverable after macro expansion available to @tech{refactoring rules}. Expansion +analyzers are an as-yet undocumented work in progress; a future edition of this grimoire will +describe them properly. diff --git a/private/syntax-property-bundle.rkt b/grimoire/syntax-property-bundle.rkt similarity index 88% rename from private/syntax-property-bundle.rkt rename to grimoire/syntax-property-bundle.rkt index 6f01269..830ebbe 100644 --- a/private/syntax-property-bundle.rkt +++ b/grimoire/syntax-property-bundle.rkt @@ -12,7 +12,7 @@ [syntax-property-bundle-as-map (-> syntax-property-bundle? immutable-sorted-map?)] [syntax-property-bundle-entries (-> syntax-property-bundle? (sequence/c syntax-property-entry?))] [syntax-property-bundle-get-property - (->* (syntax-property-bundle? syntax-path? any/c) (failure-result/c) any/c)] + (->* (syntax-property-bundle? syntax-path? interned-symbol?) (failure-result/c) any/c)] [syntax-property-bundle-get-immediate-properties (-> syntax-property-bundle? syntax-path? immutable-hash?)] [syntax-property-bundle-get-all-properties @@ -20,7 +20,8 @@ [sequence->syntax-property-bundle (-> (sequence/c syntax-property-entry?) syntax-property-bundle?)] [into-syntax-property-bundle (reducer/c syntax-property-entry? syntax-property-bundle?)] [property-hashes-into-syntax-property-bundle - (reducer/c (entry/c syntax-path? immutable-hash?) syntax-property-bundle?)] + (reducer/c (entry/c syntax-path? (hash/c interned-symbol? any/c #:immutable #true #:flat? #true)) + syntax-property-bundle?)] [syntax-add-all-properties (-> syntax? syntax-property-bundle? syntax?)] [syntax-immediate-properties (->* (syntax?) (#:base-path syntax-path?) syntax-property-bundle?)] [syntax-all-properties (->* (syntax?) (#:base-path syntax-path?) syntax-property-bundle?)])) @@ -32,6 +33,7 @@ racket/sequence racket/stream rebellion/base/range + rebellion/base/symbol rebellion/collection/entry (except-in rebellion/collection/hash mutable-hash? immutable-hash?) rebellion/collection/sorted-map @@ -55,7 +57,7 @@ #:transparent) (struct syntax-property-entry (path key value) - #:guard (struct-guard/c syntax-path? any/c any/c) + #:guard (struct-guard/c syntax-path? interned-symbol? any/c) #:transparent) @@ -130,7 +132,10 @@ (check-equal? actual expected))) -(define (syntax-property-bundle-get-property prop-bundle path key [failure-result #false]) +(define absent-property (gensym 'absent-property)) + + +(define (syntax-property-bundle-get-property prop-bundle path key [failure-result absent-property]) (define props-at-path (sorted-map-get (syntax-property-bundle-as-map prop-bundle) path (hash))) (define (fail) @@ -141,7 +146,7 @@ "property key" key "properties at path" props-at-path)) - (hash-ref props-at-path key (or failure-result fail))) + (hash-ref props-at-path key (if (eq? failure-result absent-property) fail failure-result))) (module+ test @@ -175,7 +180,12 @@ (define path (syntax-path (list 1 2 3))) (define actual (syntax-property-bundle-get-property (syntax-property-bundle) path 'foo (λ () 42))) - (check-equal? actual 42)))) + (check-equal? actual 42)) + + (test-case "empty bundle with false failure value provided" + (define path (syntax-path (list 1 2 3))) + (define actual (syntax-property-bundle-get-property (syntax-property-bundle) path 'foo #false)) + (check-equal? actual #false)))) (define (syntax-property-bundle-get-immediate-properties prop-bundle path) @@ -220,7 +230,32 @@ (module+ test (test-case "syntax-property-bundle-get-all-properties" - (void))) + (define props + (syntax-property-bundle + (syntax-property-entry root-syntax-path 'root-prop 0) + (syntax-property-entry (syntax-path (list 1)) 'list-prop 1) + (syntax-property-entry (syntax-path (list 1 0)) 'leaf-prop 2) + (syntax-property-entry (syntax-path (list 2)) 'other-prop 3))) + + (test-case "properties at and under path are re-rooted relative to it" + (define actual (syntax-property-bundle-get-all-properties props (syntax-path (list 1)))) + (define expected + (syntax-property-bundle + (syntax-property-entry root-syntax-path 'list-prop 1) + (syntax-property-entry (syntax-path (list 0)) 'leaf-prop 2))) + (check-equal? actual expected)) + + (test-case "root path returns the bundle unchanged" + (check-equal? (syntax-property-bundle-get-all-properties props root-syntax-path) props)) + + (test-case "path with no properties at or under it returns the empty bundle" + (define actual (syntax-property-bundle-get-all-properties props (syntax-path (list 5)))) + (check-equal? actual (syntax-property-bundle))) + + (test-case "empty bundle" + (define actual + (syntax-property-bundle-get-all-properties (syntax-property-bundle) (syntax-path (list 1)))) + (check-equal? actual (syntax-property-bundle))))) (define (syntax-property-bundle-entries prop-bundle) diff --git a/grimoire/syntax-property-bundle.scrbl b/grimoire/syntax-property-bundle.scrbl new file mode 100644 index 0000000..f04dce9 --- /dev/null +++ b/grimoire/syntax-property-bundle.scrbl @@ -0,0 +1,181 @@ +#lang scribble/manual + + +@(require (for-label racket/base + racket/contract/base + racket/mutability + racket/sequence + rebellion/base/symbol + rebellion/collection/entry + rebellion/collection/sorted-map + rebellion/streaming/reducer + resyntax/grimoire/syntax-path + resyntax/grimoire/syntax-property-bundle)) + + +@title[#:tag "syntax-property-bundle"]{Syntax Property Bundles} +@defmodule[resyntax/grimoire/syntax-property-bundle] + +A @deftech{syntax property bundle} is an immutable tree of +@tech[#:doc '(lib "scribblings/reference/reference.scrbl")]{syntax properties} that has been +detached from any particular syntax object tree. Each property in a bundle is addressed by the +combination of a @tech{syntax path}, identifying the subform the property belongs to, and a symbol +property key. A bundle contains at most one value for each path and key combination. Property bundles +are a plain data representation of metadata that can be attached to and extracted from arbitrary +syntax objects and their subforms. + +Ordinarily, syntax properties live directly on syntax objects. Representing them separately as plain +data lets Resyntax manipulate the properties themselves: bundles can be filtered, merged, inspected, +and rearranged without ever touching a syntax object. Resyntax's @tech{expansion analyzers} work this +way --- each analyzer examines a fully expanded module and reports its findings as a syntax property +bundle. Resyntax then combines each analyzer's output bundle together, rearranges the combined bundle, +and grafts it onto the original, unexpanded syntax object before analyzing it with +@tech{refactoring rules}. + +Although syntax objects themselves permit arbitrary property keys, property keys in a bundle are +more limited. A syntax property bundle key @emph{must} be an interned symbol. This is for two reasons, +one specific and one general: + +@itemlist[ + @item{Interned symbol keys are the only syntax property keys that can be iterated by + @racket[syntax-property-symbol-keys], so such keys are the only kind that bundles can faithfully + transfer between syntax objects. Since the entire purpose of property bundles is to transfer + properties between syntax objects, allowing uninterned symbol keys would have no purpose.} + + @item{More generally, @bold{syntax objects are not plain serializable data}. Syntax objects are not + safe to transfer from one Racket process to another. This is relied upon by various layers of the + Racket macro system and similar low-level systems for encapsulation purposes. Some syntax properties + are only used during macro expansion with opaque keys to provide private communication channels + between macros that implement some library abstraction. Exposing such keys directly would allow + intermediate macros in client code using that library to violate its abstractions. These sorts of + opaque keys are often @racket[gensym]s or similar unserializable unique values, and the Racket macro + system takes care to make sure no API exists that provides uncontrolled access to them.}] + + +@section{Constructing Syntax Property Bundles} + + +@defproc[(syntax-property-bundle? [v any/c]) boolean?]{ + A predicate that recognizes @tech{syntax property bundles}.} + + +@defstruct*[syntax-property-entry ([path syntax-path?] [key interned-symbol?] [value any/c]) + #:transparent]{ + A @deftech{syntax property entry} pairs a syntax property key and value with the + @tech{syntax path} of the subform that the property belongs to. Bundles are built out of these + entries.} + + +@defproc[(syntax-property-bundle [entry syntax-property-entry?] ...) syntax-property-bundle?]{ + Constructs a @tech{syntax property bundle} containing each @racket[entry]. The entries may be given + in any order, but no two entries may share both a path and a key --- a contract error is raised if + they do.} + + +@defproc[(sequence->syntax-property-bundle [entries (sequence/c syntax-property-entry?)]) + syntax-property-bundle?]{ + Like @racket[syntax-property-bundle], but the entries are supplied as a sequence instead of as + individual arguments.} + + +@defthing[into-syntax-property-bundle (reducer/c syntax-property-entry? syntax-property-bundle?)]{ + A @tech[#:doc '(lib "rebellion/main.scrbl")]{reducer} that collects a sequence of + @tech{syntax property entries} into a @tech{syntax property bundle}. Like + @racket[syntax-property-bundle], no two reduced entries may share both a path and a key.} + + +@defthing[property-hashes-into-syntax-property-bundle + (reducer/c (entry/c syntax-path? (hash/c interned-symbol? any/c #:immutable #true)) + syntax-property-bundle?)]{ + A @tech[#:doc '(lib "rebellion/main.scrbl")]{reducer} that collects a sequence of + @racket[entry] values, each mapping a @tech{syntax path} to a hash of the properties at that path, + into a @tech{syntax property bundle}. Entries with empty property + hashes are ignored. Each path may occur at most once in the reduced sequence --- unlike + @racket[into-syntax-property-bundle], this reducer does not merge multiple entries that refer to + the same path, and instead raises a contract error. This is because this reducer is intended to be + used when a syntax object and its subforms are iterated, and each syntax object's properties are all + extracted at once using @racket[syntax-property-symbol-keys]. Such a client would expect to never + iterate two syntax objects with the same syntax path, even if they had disjoint property keys.} + + +@section{Querying Syntax Property Bundles} + + +@defproc[(syntax-property-bundle-get-property + [bundle syntax-property-bundle?] + [path syntax-path?] + [key interned-symbol?] + [failure-result failure-result/c + (λ () (raise (make-exn:fail:contract ....)))]) + any/c]{ + Returns the value of the property with key @racket[key] at @racket[path] within @racket[bundle]. + If no such property exists, then @racket[failure-result] determines the result: if it's a + procedure, it's called with no arguments to produce the result, and otherwise it's returned + directly, following the same protocol as @racket[hash-ref]. If @racket[failure-result] is omitted, + a contract error is raised instead.} + + +@defproc[(syntax-property-bundle-get-immediate-properties [bundle syntax-property-bundle?] + [path syntax-path?]) + immutable-hash?]{ + Returns a hash of every property in @racket[bundle] located at exactly @racket[path], mapping + property keys to property values. Properties located at @bold{descendants} of @racket[path] are + @bold{not} included. Returns an empty hash, rather than raising an error, if @racket[bundle] + contains no properties at @racket[path].} + + +@defproc[(syntax-property-bundle-get-all-properties [bundle syntax-property-bundle?] + [path syntax-path?]) + syntax-property-bundle?]{ + Returns a @tech{syntax property bundle} of every property in @racket[bundle] located at + @racket[path] or at any descendant of @racket[path]. The paths of the returned bundle are made + relative to @racket[path], as though by @racket[syntax-path-remove-prefix]: properties located at + exactly @racket[path] appear at @racket[root-syntax-path] in the returned bundle. Returns the + empty bundle, rather than raising an error, if @racket[bundle] contains no properties at or under + @racket[path]. Passing @racket[root-syntax-path] returns @racket[bundle] unchanged.} + + +@defproc[(syntax-property-bundle-entries [bundle syntax-property-bundle?]) + (sequence/c syntax-property-entry?)]{ + Returns a lazy sequence of every @tech{syntax property entry} in @racket[bundle]. Entries are + produced in ascending order of their paths, in the sense of @racket[syntax-path<=>]. The relative + order of multiple entries at the same path is unspecified.} + + +@defproc[(syntax-property-bundle-as-map [bundle syntax-property-bundle?]) immutable-sorted-map?]{ + Returns a view of @racket[bundle] as a sorted map whose keys are @tech{syntax paths}, ordered by + @racket[syntax-path<=>], and whose values are hashes mapping property keys to property values. + Only paths with at least one property appear in the map.} + + +@section{Moving Properties Between Bundles and Syntax Objects} + + +@defproc[(syntax-immediate-properties [stx syntax?] + [#:base-path base-path syntax-path? root-syntax-path]) + syntax-property-bundle?]{ + Returns a @tech{syntax property bundle} of the properties attached directly to @racket[stx], + ignoring any properties attached to its subforms. Only properties whose keys are interned symbols + are extracted, as determined by @racket[syntax-property-symbol-keys]. The extracted properties are + located at @racket[base-path] in the returned bundle, which is useful when @racket[stx] is itself + a subform of some larger syntax object.} + + +@defproc[(syntax-all-properties [stx syntax?] + [#:base-path base-path syntax-path? root-syntax-path]) + syntax-property-bundle?]{ + Returns a @tech{syntax property bundle} of the properties attached to @racket[stx] and to every + subform within it. Only properties whose keys are interned symbols are extracted, as determined by + @racket[syntax-property-symbol-keys]. Each extracted property is located at the @tech{syntax path} + of the subform it was attached to, prefixed with @racket[base-path]. Subforms of hash datums are + not traversed, as syntax paths cannot refer to them --- see @secref["original-syntax-paths"] for + further explanation.} + + +@defproc[(syntax-add-all-properties [stx syntax?] [bundle syntax-property-bundle?]) syntax?]{ + Returns a copy of @racket[stx] in which, for each @tech{syntax property entry} in @racket[bundle], + the subform at the entry's path is given the entry's property key and value as though by + @racket[syntax-property]. Properties already attached to @racket[stx] and its subforms are + retained, except where an entry in @racket[bundle] overwrites them. Every path in @racket[bundle] + must refer to a subform of @racket[stx], in the sense of @racket[syntax-contains-path?] --- + otherwise a contract error is raised.} diff --git a/main.rkt b/main.rkt index 7cafb32..853b5f4 100644 --- a/main.rkt +++ b/main.rkt @@ -63,7 +63,7 @@ resyntax/grimoire/source resyntax/private/string-indent resyntax/grimoire/string-replacement - resyntax/private/syntax-property-bundle + resyntax/grimoire/syntax-property-bundle resyntax/private/syntax-range resyntax/private/syntax-replacement (except-in racket/list range) diff --git a/private/analysis.rkt b/private/analysis.rkt index 2b9af38..e723a5b 100644 --- a/private/analysis.rkt +++ b/private/analysis.rkt @@ -49,7 +49,7 @@ resyntax/private/syntax-movement resyntax/private/syntax-neighbors resyntax/grimoire/syntax-path - resyntax/private/syntax-property-bundle + resyntax/grimoire/syntax-property-bundle resyntax/private/syntax-traversal syntax/parse) diff --git a/private/analyzer.rkt b/private/analyzer.rkt index 2d1041e..3dba14d 100644 --- a/private/analyzer.rkt +++ b/private/analyzer.rkt @@ -14,7 +14,7 @@ (require rebellion/custom-write - resyntax/private/syntax-property-bundle) + resyntax/grimoire/syntax-property-bundle) ;@---------------------------------------------------------------------------------------------------- diff --git a/test/private/rackunit.rkt b/test/private/rackunit.rkt index 1a8323a..5f37d69 100644 --- a/test/private/rackunit.rkt +++ b/test/private/rackunit.rkt @@ -42,7 +42,7 @@ resyntax/private/string-indent resyntax/grimoire/string-replacement resyntax/grimoire/syntax-path - resyntax/private/syntax-property-bundle + resyntax/grimoire/syntax-property-bundle resyntax/private/syntax-traversal syntax/modread syntax/parse