Monday, February 2, 2026

Deprecations as They Should Be

With BPatterns, you don’t need a special syntax to search or rewrite code. Any block or any AST node can become a matching pattern.

This opens up interesting opportunities to simplify existing users of the rewrite engine. One particularly good candidate is the deprecation mechanism in Pharo.

Let’s look at a real example.

Deprecations in Pharo Today

Consider this recently introduced deprecation in Object:

Object>>confirm: queryString
        self 
                deprecated: 'Use the ConfirmationRequest instead.' 
                transformWith: '`@rcv confirm: `@arg' -> 'ConfirmationRequest signal: `@arg'.
        ^ConfirmationRequest signal: queryString

This method will be removed in an upcoming Pharo version. Users are expected to migrate from #confirm: to raising a ConfirmationRequest.

The interesting part is the transformation rule: any sender of #confirm: is automatically rewritten on the fly using the provided rewrite pattern.

Run your test suite with good coverage, and your codebase is upgraded automatically. No manual work required. That’s powerful.

But Let’s Look Closer

Even in this relatively simple example, a lot is duplicated:

  • The deprecated message is duplicated twice in the transformation rule and the actual method name:
    • '`@rcv confirm: `@arg'
  • The replacement API appears three times:
    • in deprecation description: 
      • 'Use the ConfirmationRequest instead'
    • in the transformation rule: 
      • 'ConfirmationRequest signal: `@arg'
    • at the end of the method to do the real API call: 
      • ^ConfirmationRequest signal: queryString
  • The transformation rule duplicates symbolic pattern variables: 
    • `@arg 
If you are already familiar with Pharo’s deprecation API, this is manageable. But think about the first time you deprecated a method. How easy was it to write this from scratch?

Personally, I always start by searching for an existing example to copy. I never try to write it cold.

There is a “Deprecate Method” refactoring in Pharo, but today it is very basic and does not help with transformations. It could be improved—but that’s where we currently are.

Now let’s look at the same idea through the lens of BPatterns.

The Same Rewrite with BPatterns

The core transformation rule can be written as a plain block:

[[ anyRcv confirm: anyArg ] -> [ ConfirmationRequest signal: anyArg ]] brewrite.

No strings. No backticks. No special syntax.

Now imagine embedding this rewrite directly into the deprecated method:

Object>>confirm: queryString
        | brewrite anyRcv anyArg newAST |
        brewrite := [[ anyRcv confirm: anyArg ] -> [ ConfirmationRequest signal: anyArg ]] brewrite.

        "some code to actually rewrite the sender"
        newAST := brewrite rewriteMethod: thisContext sender home method.
        thisContext sender home receiver class compile: newAST formattedCode.

        "And call new API at the end"
        ^ConfirmationRequest signal: queryString

This already removes the string-based rewrite syntax, but we can do better.

In BPatterns, variables starting with any are wildcards by default—but you can explicitly configure any variable, including self and method arguments.

That means we can reuse the actual method variables directly in the rewrite rule:

Object>>confirm: queryString
        | brewrite newAST |
        brewrite := [[ self confirm: queryString ] -> [ ConfirmationRequest signal: queryString ]] 
                                        brewrite with: [ self. queryString ].

        "some code to actually rewrite the sender"
        newAST := brewrite rewriteMethod: thisContext sender home method.
        thisContext sender home receiver class compile: newAST formattedCode.

        "And call new API at the end"
        ^ConfirmationRequest signal: queryString

Now the transformation rule and the method body speak the same language. And we can easily refactor it like any other method to avoid the duplication of the new API call:

Object>>confirm: queryString
        | brewrite newAST |
        newAPI := [ ConfirmationRequest signal: queryString ].
        brewrite := [[ self confirm: queryString ] -> newAPI ] brewrite with: [ self. queryString ].

        "some code to actually rewrite the sender"
        newAST := brewrite rewriteMethod: thisContext sender home method.
        thisContext sender home receiver class compile: newAST formattedCode.

        ^ newAPI value

At this point, most of the code above is mechanical. BPatterns only needs an AST of the deprecated API call for the matching pattern. 

Normally it comes from a block - [ self confirm: queryString ] but here it can be easily derived from thisContext method ast since it is equivalent to the deprecated method header. 

That means the whole deprecation can collapse into this: 

Object>>confirm: queryString
        ^ self deprecatedBy:  [ ConfirmationRequest signal: queryString ]

That’s it.

No duplicated selectors.
No duplicated replacement calls.
No rewrite strings.
No boilerplate.

This is the deprecation without stress.

Variations Are Easy

Once the transformation is automatic, it’s natural to allow variations:

Disable auto-transformation:

Object>>confirm: queryString
        ^self 
                deprecatedBy: [ ConfirmationRequest signal: queryString ] 
                autoTransform: false

Add a custom deprecation message:

Object>>confirm: queryString
        ^ self 
                deprecated: 'self confirm: is a bad style , use ConfirmationRequest'
                by:  [ ConfirmationRequest signal: queryString ] 

The basic version generates a default title automatically.

Summary

With the new API, the deprecation becomes a seamless, fluent process. You no longer need to think about transformation rules at all—they are simply part of the system, enabled by default. Deprecation is expressed in the same language as the code itself, not in a separate, string-based mini-language.

Once this foundation is in place, building higher-level tooling becomes straightforward. Wrapping deprecations into proper refactoring commands is trivial, and integrating them into existing refactorings—such as Rename Method or Add/Remove Argument—becomes a natural next step.

That’s all for now.

You can load BPatterns from GitHub and try new deprecations yourself:

👉 https://github.com/dionisiydk/BPatterns

BPatterns: Rewrite Engine with Smalltalk style

The rewrite engine is an absolutely brilliant invention by John Brant and Don Roberts, introduced with the Refactoring Browser (see “A Refactoring Tool for Smalltalk”, 1997). It gives us AST-level matching and rewriting with astonishing power.

But let’s be honest: how many people actually remember its syntax?

Even the simplest rewrite rule—say, replacing a deprecated message with a new one—usually sends me hunting for examples. During this project I spent a lot of time deep inside the rewrite engine, and even now I cannot reliably recall the exact syntax.

Is it something like this?

``@receiver isNil ifTrue: ``@nilBlock -> ``@receiver ifNil: ``@nilBlock

Or maybe with single backticks?

`@receiver isNil ifTrue: `@nilBlock -> `@receiver ifNil: `@nilBlock

In fact, both versions work—but they apply different filters to the target node. Try to remember which one.

And that’s only the beginning. 

Do you know you can wildcard parts of selectors?

`@receiver `anyKeywordPart: `@arg1 staticPart: `@arg2

You can rename keywords using it:

`@receiver newKeywordPart: `@arg1 staticPart: `@arg2

Or even swap them:

`@receiver staticPart: `@arg2 `anyKeywordPart: `@arg1

It’s incredibly powerful. But how do you remember all of this?

With normal Smalltalk code, I would explore the system using senders, implementors, inspectors— gradually rebuilding my understanding. Here, that breaks down. The matching syntax lives inside strings, invisible to standard navigation tools. No code completion. No refactorings. No help from the environment.

So how do we keep the power without the syntax tax?

That is where BPatterns come in:

[ any isNil ifTrue: anyBlock ] bpattern

BPatterns

BPatterns provide a fluent, Smalltalk-native API on top of the rewrite engine, using ordinary Smalltalk blocks as patterns.

You create a BPattern instance by sending the #bpattern message to a block. The variables and selectors inside the block define the pattern to be matched against target AST nodes. By convention anything started with any word acts as a wildcard. Everything else must match structurally.

Under the hood, BPattern builds a pattern AST using the same pattern node classes as the rewrite engine. All the original matching and rewriting machinery is still there — just wrapped in a more approachable, scriptable interface.

You can think of BPatterns as a Smalltalk DSL for the rewrite engine.

Pharo already provides dedicated tools for the rewrite engine, such as StRewriterMatchToolPresenter:


Glamorous Toolkit adds its own powerful helpers.

With BPatterns, none of that is required. A pattern is just a block. Add one more message and simple DoIt will do the job.

To find all matching methods:
[ anyRcv isNil ifTrue: anyBlock ] bpattern browseUsers

To rewrite them:

[[ anyRcv isNil ifTrue: anyBlock ] ->  [ anyRcv ifNil: anyBlock ]] brewrite preview

 

Refining Patterns Explicitly


You can narrow patterns explicitly using #with: message:

[ anyVar isNil ifTrue: anyBlock ] bpattern with: [ anyVar ] -> [:pattern | pattern beVariable ]

Because this is regular Smalltalk code, all standard development tools work out of the box: syntax highlighting, code completion, navigation, and refactorings:


Browse the implementors of #beVariable message and you will find other filters under BPatternVariableNode class, such as #beInstVar or #beLocalVar. If you miss something, just add a method. No new syntax required.

You can also use an arbitrary block as a filter: 

[ anyVar isNil ifTrue: anyBlock ] bpattern 

         with: [ anyVar ] -> [:pattern | 

                           pattern beInstVar where: [:var | var name beginsWith: 'somePrefix' ]]

Notice the block [anyVar] is used to reference variables where the configuration block should be applied. This avoids raw strings for variable names and keeps these configs friendly to development tools:



Message Patterns Revisited


Now let’s revisit the selector wildcard examples from the beginning using BPatterns.


Renaming a keyword:

[

         anyRcv anyKeywordPart: anyArg1 staticPart: anyArg2 ] 

                  -> [ anyRcv newKeywordPart: anyArg1 staticPart: anyArg2 ]

] brewrite.

Swapping keywords:

[

         anyRcv anyKeywordPart: anyArg1 staticPart: anyArg2 ] 

                  -> [ anyRcv staticPart: anyArg2 anyKeywordPart: anyArg1 ]

] brewrite.

Message patterns can also be refined using #with: message:

[ any anyMessage: any2 ] bpattern 

with: #anyMessage: -> [:pattern | pattern beBinary ];

browseUsers.

This finds all methods containing binary messages:


Add another filter to keep only binaries between literals:

[ any anyMessage: any2 ] bpattern 

with: #anyMessage: -> [:pattern | pattern beBinary ];

with: [ any. any2 ] -> [ :pattern | pattern beLiteral ]; 

browseUsers


The old syntax also supports literal patterns but good luck finding an example.

Message patterns can also be configured with arbitrary conditions:

[ any anyMessage ] bpattern 

with: #anyMessage -> [:pattern | pattern where: [:node | node selector beginsWith: 'prim' ]];

browseUsers


Status and What’s Next

BPatterns don’t expose every feature of the rewrite engine yet, but many are already supported, including full method patterns via #bmethod.

For full details, see the GitHub repository:

And check the next blog post about a simplified deprecation API built on top of BPatterns: