From 7b48ce6504f7e7517b2ccd81e7a155834cdbf895 Mon Sep 17 00:00:00 2001 From: Jack Firth Date: Thu, 9 Jul 2026 23:36:58 -0700 Subject: [PATCH 1/4] Move syntax-property-bundle module into resyntax/grimoire Co-Authored-By: Claude Fable 5 --- base.rkt | 2 +- .../analyzers/function-expression-analyzer.rkt | 2 +- default-recommendations/analyzers/identifier-usage.rkt | 2 +- default-recommendations/analyzers/ignored-result-values.rkt | 2 +- default-recommendations/analyzers/variable-mutability.rkt | 2 +- {private => grimoire}/syntax-property-bundle.rkt | 0 main.rkt | 2 +- private/analysis.rkt | 2 +- private/analyzer.rkt | 2 +- test/private/rackunit.rkt | 2 +- 10 files changed, 9 insertions(+), 9 deletions(-) rename {private => grimoire}/syntax-property-bundle.rkt (100%) diff --git a/base.rkt b/base.rkt index bc86caaf..8192c09e 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 e5d935fe..6c264468 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 5d990dcc..396d0aef 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 c2944ecf..56e26c10 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 1647ca9b..35a71dae 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/private/syntax-property-bundle.rkt b/grimoire/syntax-property-bundle.rkt similarity index 100% rename from private/syntax-property-bundle.rkt rename to grimoire/syntax-property-bundle.rkt diff --git a/main.rkt b/main.rkt index 7cafb32d..853b5f4a 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 2b9af382..e723a5be 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 2d1041e7..3dba14dd 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 1a8323a8..5f37d69e 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 From b6b4ecc3451ad130cf9dfe2a4c02a4c32b8da60c Mon Sep 17 00:00:00 2001 From: Jack Firth Date: Thu, 9 Jul 2026 23:36:58 -0700 Subject: [PATCH 2/4] Draft API reference for syntax property bundles Co-Authored-By: Claude Fable 5 --- grimoire.scrbl | 1 + grimoire/syntax-property-bundle.scrbl | 161 ++++++++++++++++++++++++++ 2 files changed, 162 insertions(+) create mode 100644 grimoire/syntax-property-bundle.scrbl diff --git a/grimoire.scrbl b/grimoire.scrbl index d6ff83e5..a2072763 100644 --- a/grimoire.scrbl +++ b/grimoire.scrbl @@ -15,4 +15,5 @@ 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/string-replacement.scrbl")] diff --git a/grimoire/syntax-property-bundle.scrbl b/grimoire/syntax-property-bundle.scrbl new file mode 100644 index 00000000..9863e37e --- /dev/null +++ b/grimoire/syntax-property-bundle.scrbl @@ -0,0 +1,161 @@ +#lang scribble/manual + + +@(require (for-label racket/base + racket/contract/base + racket/mutability + racket/sequence + 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 collection of +@tech[#:doc '(lib "scribblings/reference/reference.scrbl")]{syntax properties} that has been +detached from any particular syntax object. Each property in a bundle is addressed by the +combination of a @tech{syntax path}, identifying the subform the property belongs to, and a property +key. A bundle contains at most one value for each path and key combination. + +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, translated +from one syntax object's paths to another's, and inspected without ever touching a syntax object. +Resyntax's expansion analyzers work this way --- each analyzer examines a fully expanded program and +reports its findings as a syntax property bundle whose paths refer to subforms of the expanded +program. Resyntax then translates those paths back to the corresponding locations in the original +unexpanded program and grafts the translated bundle onto the unexpanded syntax object with +@racket[syntax-add-all-properties], making the analyzers' findings visible to +@tech{refactoring rules}. + +Note that while a bundle may hold entries with arbitrary property keys, the functions that extract +bundles from syntax objects (@racket[syntax-immediate-properties] and +@racket[syntax-all-properties]) only discover properties whose keys are interned symbols, as that is +the only part of a syntax object's property table that Racket exposes via +@racket[syntax-property-symbol-keys]. + + +@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 any/c] [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? immutable-hash?) 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.} + + +@section{Querying Syntax Property Bundles} + + +@defproc[(syntax-property-bundle-get-property [bundle syntax-property-bundle?] + [path syntax-path?] + [key any/c] + [failure-result failure-result/c #false]) + 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 + or @racket[#false], a contract error is raised instead. Note that this means @racket[#false] + cannot be used directly as a failure result --- use @racket[(λ () #false)] to make a missing + property produce @racket[#false].} + + +@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 descendants of @racket[path] are not + included. Returns an empty hash 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. 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.} From 9938042314f42ed3718f4712b7940e606e5cd6c5 Mon Sep 17 00:00:00 2001 From: Jack Firth Date: Thu, 9 Jul 2026 23:51:44 -0700 Subject: [PATCH 3/4] Add expansion analyzers stub section to the grimoire Co-Authored-By: Claude Fable 5 --- grimoire.scrbl | 1 + grimoire/expansion-analyzers.scrbl | 12 ++++++++++++ grimoire/syntax-property-bundle.scrbl | 2 +- 3 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 grimoire/expansion-analyzers.scrbl diff --git a/grimoire.scrbl b/grimoire.scrbl index a2072763..02bf46d0 100644 --- a/grimoire.scrbl +++ b/grimoire.scrbl @@ -16,4 +16,5 @@ programmatically on anything found here. @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 00000000..9a7ae7eb --- /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/grimoire/syntax-property-bundle.scrbl b/grimoire/syntax-property-bundle.scrbl index 9863e37e..b952d63f 100644 --- a/grimoire/syntax-property-bundle.scrbl +++ b/grimoire/syntax-property-bundle.scrbl @@ -24,7 +24,7 @@ key. A bundle contains at most one value for each path and key combination. 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, translated from one syntax object's paths to another's, and inspected without ever touching a syntax object. -Resyntax's expansion analyzers work this way --- each analyzer examines a fully expanded program and +Resyntax's @tech{expansion analyzers} work this way --- each analyzer examines a fully expanded program and reports its findings as a syntax property bundle whose paths refer to subforms of the expanded program. Resyntax then translates those paths back to the corresponding locations in the original unexpanded program and grafts the translated bundle onto the unexpanded syntax object with From 37304494a64682a65a26d98cad81aa0d38aaa605 Mon Sep 17 00:00:00 2001 From: Jack Firth Date: Fri, 10 Jul 2026 00:41:50 -0700 Subject: [PATCH 4/4] Require interned symbols as syntax property bundle keys Co-Authored-By: Claude Fable 5 --- grimoire/syntax-property-bundle.rkt | 8 +++++--- grimoire/syntax-property-bundle.scrbl | 17 +++++++++-------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/grimoire/syntax-property-bundle.rkt b/grimoire/syntax-property-bundle.rkt index 6f012695..9c4b43eb 100644 --- a/grimoire/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) diff --git a/grimoire/syntax-property-bundle.scrbl b/grimoire/syntax-property-bundle.scrbl index b952d63f..2ae2cc11 100644 --- a/grimoire/syntax-property-bundle.scrbl +++ b/grimoire/syntax-property-bundle.scrbl @@ -5,6 +5,7 @@ racket/contract/base racket/mutability racket/sequence + rebellion/base/symbol rebellion/collection/entry rebellion/collection/sorted-map rebellion/streaming/reducer @@ -31,11 +32,10 @@ unexpanded program and grafts the translated bundle onto the unexpanded syntax o @racket[syntax-add-all-properties], making the analyzers' findings visible to @tech{refactoring rules}. -Note that while a bundle may hold entries with arbitrary property keys, the functions that extract -bundles from syntax objects (@racket[syntax-immediate-properties] and -@racket[syntax-all-properties]) only discover properties whose keys are interned symbols, as that is -the only part of a syntax object's property table that Racket exposes via -@racket[syntax-property-symbol-keys]. +Although syntax objects themselves permit arbitrary property keys, property keys in a bundle are +required to be interned symbols. The interned-symbol-keyed portion of a syntax object's property +table is the only portion that Racket exposes via @racket[syntax-property-symbol-keys], so it is +the only portion that bundles can faithfully represent. @section{Constructing Syntax Property Bundles} @@ -45,7 +45,7 @@ the only part of a syntax object's property table that Racket exposes via A predicate that recognizes @tech{syntax property bundles}.} -@defstruct*[syntax-property-entry ([path syntax-path?] [key any/c] [value any/c]) +@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 @@ -71,7 +71,8 @@ the only part of a syntax object's property table that Racket exposes via @defthing[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)) + 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 @@ -85,7 +86,7 @@ the only part of a syntax object's property table that Racket exposes via @defproc[(syntax-property-bundle-get-property [bundle syntax-property-bundle?] [path syntax-path?] - [key any/c] + [key interned-symbol?] [failure-result failure-result/c #false]) any/c]{ Returns the value of the property with key @racket[key] at @racket[path] within @racket[bundle].