From eb9c011c3b72bbe77ad00f045e8a7d54ced1d2a2 Mon Sep 17 00:00:00 2001 From: Jack Firth Date: Tue, 7 Jul 2026 01:45:59 -0700 Subject: [PATCH] Add rule replacing quasiquoted make-immutable-hash with hash Adds a make-immutable-hash-with-quasiquote-to-hash rule to the hash-shortcuts suite. A make-immutable-hash call on a quasiquoted association list with statically known keys is rewritten to a flat hash call. Keys and non-unquoted values may be symbols or self-quoting literals, and at least one value must be unquoted. Quoted datums like (b . 'c) are excluded because quasiquote treats them as plain data rather than expressions. Fixes #122. Co-Authored-By: Claude Fable 5 --- .../hash-shortcuts-test.rkt | 68 +++++++++++++++++++ default-recommendations/hash-shortcuts.rkt | 41 +++++++++++ 2 files changed, 109 insertions(+) diff --git a/default-recommendations/hash-shortcuts-test.rkt b/default-recommendations/hash-shortcuts-test.rkt index bdefad27..8c55640f 100644 --- a/default-recommendations/hash-shortcuts-test.rkt +++ b/default-recommendations/hash-shortcuts-test.rkt @@ -295,3 +295,71 @@ no-change-test: (define sum (hash-ref! h1 (cadr term) 0)) (hash-set! h2 (cadr term) (+ (car term) sum))) ------------------------------ + + +test: "make-immutable-hash with quasiquoted alist refactorable to hash" +------------------------------ +(define (f body event) + (make-immutable-hash + `((body . ,body) + (event . ,event)))) +============================== +(define (f body event) + (hash 'body body 'event event)) +------------------------------ + + +test: "make-immutable-hash with self-quoting keys and values refactorable to hash" +------------------------------ +(define (f x) + (make-immutable-hash + `(("a" . ,x) + (b . 5)))) +============================== +(define (f x) + (hash "a" x 'b 5)) +------------------------------ + + +no-change-test: "make-immutable-hash with fully quoted alist not refactorable to hash" +------------------------------ +(make-immutable-hash `((a . 1) (b . 2))) +------------------------------ + + +no-change-test: "make-immutable-hash with dynamic keys not refactorable to hash" +------------------------------ +(define (f k v) + (make-immutable-hash `((,k . ,v)))) +------------------------------ + + +no-change-test: "make-immutable-hash with unquote-splicing not refactorable to hash" +------------------------------ +(define (f pairs) + (make-immutable-hash `(,@pairs (a . 1)))) +------------------------------ + + +no-change-test: "make-immutable-hash with quoted datum values not refactorable to hash" +------------------------------ +(define (f x) + (make-immutable-hash `((a . ,x) (b . 'c)))) +------------------------------ + + +test: "make-immutable-hash with symbol values refactorable to hash" +------------------------------ +(define (f x) + (make-immutable-hash `((a . ,x) (b . c)))) +============================== +(define (f x) + (hash 'a x 'b 'c)) +------------------------------ + + +no-change-test: "make-immutable-hash with list keys not refactorable to hash" +------------------------------ +(define (f x) + (make-immutable-hash `(((a b) . ,x)))) +------------------------------ diff --git a/default-recommendations/hash-shortcuts.rkt b/default-recommendations/hash-shortcuts.rkt index 797669e4..6b5a7017 100644 --- a/default-recommendations/hash-shortcuts.rkt +++ b/default-recommendations/hash-shortcuts.rkt @@ -143,6 +143,46 @@ #:original-splice (orig-definition orig-hash-set!))))) +(define-syntax-class self-quoting-literal + (pattern (~or _:boolean _:character _:number _:regexp _:byte-regexp _:string _:bytes))) + + +;; A datum within a quasiquoted expression that can be lifted directly into expression position, +;; either because it's self-quoting or because it's a symbol that can be wrapped in a quote. Quoted +;; datums like (quote x) are excluded because quasiquote treats them as plain data: the quasiquoted +;; pair (a . 'x) maps the key a to the two-element list (quote x), not to the symbol x. +(define-syntax-class liftable-quasiquoted-datum + #:attributes (as-expr) + (pattern sym:id + #:when (not (memq (syntax-e #'sym) '(unquote unquote-splicing quasiquote))) + #:with as-expr #'(quote sym)) + (pattern datum:self-quoting-literal + #:with as-expr #'datum)) + + +(define-syntax-class quasiquoted-hash-pair + #:attributes (key-expr value-expr unquoted-value?) + #:literals (unquote) + (pattern (key:liftable-quasiquoted-datum . (unquote value-expr:expr)) + #:with key-expr #'key.as-expr + #:attr unquoted-value? #true) + (pattern (key:liftable-quasiquoted-datum . value:liftable-quasiquoted-datum) + #:with key-expr #'key.as-expr + #:with value-expr #'value.as-expr + #:attr unquoted-value? #false)) + + +(define-refactoring-rule make-immutable-hash-with-quasiquote-to-hash + #:description + "This `make-immutable-hash` call with statically known keys can be replaced with a simpler, +equivalent `hash` call." + #:literals (make-immutable-hash quasiquote) + (make-immutable-hash (quasiquote (pair:quasiquoted-hash-pair ...))) + #:when (for/or ([unquoted? (in-list (attribute pair.unquoted-value?))]) + unquoted?) + (hash (~@ pair.key-expr pair.value-expr) ...)) + + (define-refactoring-rule hash-map-to-hash-keys #:description "This `hash-map` expression is equivalent to the `hash-keys` function." #:literals (hash-map) @@ -169,4 +209,5 @@ hash-ref-with-constant-lambda-to-hash-ref-without-lambda hash-ref!-with-constant-lambda-to-hash-ref!-without-lambda hash-set!-ref-to-hash-update! + make-immutable-hash-with-quasiquote-to-hash or-hash-ref-set!-to-hash-ref!))