From d088b0dedaf6885996af681cea75c5afc2389857 Mon Sep 17 00:00:00 2001 From: Jack Firth Date: Thu, 9 Jul 2026 23:36:58 -0700 Subject: [PATCH 1/7] 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 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/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 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 From ec444da3cdda7f39b58574aa9946e7fa3ad410fa Mon Sep 17 00:00:00 2001 From: Jack Firth Date: Thu, 9 Jul 2026 23:36:58 -0700 Subject: [PATCH 2/7] 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 d6ff83e..a207276 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 0000000..9863e37 --- /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 97271a79b59b23f16fbe3d511bc96d51f3a09596 Mon Sep 17 00:00:00 2001 From: Jack Firth Date: Thu, 9 Jul 2026 23:51:44 -0700 Subject: [PATCH 3/7] 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 a207276..02bf46d 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 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/grimoire/syntax-property-bundle.scrbl b/grimoire/syntax-property-bundle.scrbl index 9863e37..b952d63 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 c1b3dff53d11c0e67202fd216fe2d502cdc31b86 Mon Sep 17 00:00:00 2001 From: Jack Firth Date: Fri, 10 Jul 2026 00:41:50 -0700 Subject: [PATCH 4/7] 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 6f01269..9c4b43e 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 b952d63..2ae2cc1 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]. From d6358113878d2dc6bbb006fb19255b12d7889041 Mon Sep 17 00:00:00 2001 From: Jack Firth Date: Fri, 10 Jul 2026 23:19:14 -0700 Subject: [PATCH 5/7] Review and edit property bundle docs --- grimoire/syntax-property-bundle.scrbl | 61 +++++++++++++++++++-------- 1 file changed, 44 insertions(+), 17 deletions(-) diff --git a/grimoire/syntax-property-bundle.scrbl b/grimoire/syntax-property-bundle.scrbl index 2ae2cc1..f6829fc 100644 --- a/grimoire/syntax-property-bundle.scrbl +++ b/grimoire/syntax-property-bundle.scrbl @@ -16,26 +16,40 @@ @title[#:tag "syntax-property-bundle"]{Syntax Property Bundles} @defmodule[resyntax/grimoire/syntax-property-bundle] -A @deftech{syntax property bundle} is an immutable collection of +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. 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. +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, translated -from one syntax object's paths to another's, and inspected without ever touching a syntax object. -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 -@racket[syntax-add-all-properties], making the analyzers' findings visible to +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 -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. +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 uncontrollable access to them.}] @section{Constructing Syntax Property Bundles} @@ -78,12 +92,17 @@ the only portion that bundles can faithfully represent. 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.} + 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} +@;TODO: claude, that failure-result behavior is definitely not what I intended. Please fix +@; it with a hidden sentinel value (i.e. a gensym). @defproc[(syntax-property-bundle-get-property [bundle syntax-property-bundle?] [path syntax-path?] [key interned-symbol?] @@ -98,14 +117,19 @@ the only portion that bundles can faithfully represent. property produce @racket[#false].} +@;TODO: claude, what does this do if the bundle doesn't contain path? I think it should return an +@; empty hash rather than error, similar to how I handle this case in Rebellion's multimaps. That +@; should be documented explicitly too. @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].} + property keys to property values. Properties located at @bold{descendants} of @racket[path] are + @bold{not} included. Returns an empty hash if @racket[bundle] contains no properties at + @racket[path].} +@;TODO: claude, same question as above. @defproc[(syntax-property-bundle-get-all-properties [bundle syntax-property-bundle?] [path syntax-path?]) syntax-property-bundle?]{ @@ -116,6 +140,9 @@ the only portion that bundles can faithfully represent. @racket[root-syntax-path] returns @racket[bundle] unchanged.} +@;TODO: claude, since keys are always interned symbols, should they maybe be represented with a sorted +@; map instead of a hash? Would there be a point to that? I don't think I have any use cases where I +@; care about "all syntax properties with key names less than or greater than key 'foo" @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 From 1c37722e996621e1580c393e5534099c2bc9a0f1 Mon Sep 17 00:00:00 2001 From: Jack Firth Date: Fri, 10 Jul 2026 23:24:44 -0700 Subject: [PATCH 6/7] Resolve review TODOs on property bundle lookups Make #false usable as a failure result in syntax-property-bundle-get-property via a hidden sentinel default, document that absent-path lookups return empty results rather than raising, and replace the empty get-all-properties test stub with real test cases. Co-Authored-By: Claude Fable 5 --- grimoire/syntax-property-bundle.rkt | 41 ++++++++++++++++++++++++--- grimoire/syntax-property-bundle.scrbl | 31 +++++++++----------- 2 files changed, 50 insertions(+), 22 deletions(-) diff --git a/grimoire/syntax-property-bundle.rkt b/grimoire/syntax-property-bundle.rkt index 9c4b43e..830ebbe 100644 --- a/grimoire/syntax-property-bundle.rkt +++ b/grimoire/syntax-property-bundle.rkt @@ -132,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) @@ -143,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 @@ -177,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) @@ -222,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 index f6829fc..90af658 100644 --- a/grimoire/syntax-property-bundle.scrbl +++ b/grimoire/syntax-property-bundle.scrbl @@ -101,43 +101,38 @@ one specific and one general: @section{Querying Syntax Property Bundles} -@;TODO: claude, that failure-result behavior is definitely not what I intended. Please fix -@; it with a hidden sentinel value (i.e. a gensym). -@defproc[(syntax-property-bundle-get-property [bundle syntax-property-bundle?] - [path syntax-path?] - [key interned-symbol?] - [failure-result failure-result/c #false]) +@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 - 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].} + directly, following the same protocol as @racket[hash-ref]. If @racket[failure-result] is omitted, + a contract error is raised instead.} -@;TODO: claude, what does this do if the bundle doesn't contain path? I think it should return an -@; empty hash rather than error, similar to how I handle this case in Rebellion's multimaps. That -@; should be documented explicitly too. @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 if @racket[bundle] contains no properties at - @racket[path].} + @bold{not} included. Returns an empty hash, rather than raising an error, if @racket[bundle] + contains no properties at @racket[path].} -@;TODO: claude, same question as above. @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.} + 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.} @;TODO: claude, since keys are always interned symbols, should they maybe be represented with a sorted From ca67dc0998f2382f94ed3b20ea73972db254a2ae Mon Sep 17 00:00:00 2001 From: Jack Firth Date: Fri, 10 Jul 2026 23:51:56 -0700 Subject: [PATCH 7/7] Update syntax-property-bundle.scrbl --- grimoire/syntax-property-bundle.scrbl | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/grimoire/syntax-property-bundle.scrbl b/grimoire/syntax-property-bundle.scrbl index 90af658..f04dce9 100644 --- a/grimoire/syntax-property-bundle.scrbl +++ b/grimoire/syntax-property-bundle.scrbl @@ -49,7 +49,7 @@ one specific and one general: 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 uncontrollable access to them.}] + system takes care to make sure no API exists that provides uncontrolled access to them.}] @section{Constructing Syntax Property Bundles} @@ -135,9 +135,6 @@ one specific and one general: @racket[path]. Passing @racket[root-syntax-path] returns @racket[bundle] unchanged.} -@;TODO: claude, since keys are always interned symbols, should they maybe be represented with a sorted -@; map instead of a hash? Would there be a point to that? I don't think I have any use cases where I -@; care about "all syntax properties with key names less than or greater than key 'foo" @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