Working on Pharo Smalltalk: BPatterns: Rewrite Engine with Smalltalk Style

2026-02-27T10:30:33.000Z·★ 82·5 min read
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...

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...


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?

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:`
[![Image 1](https://blogger.googleusercontent.com/img/a/AVvXsEgqrlmb6B1CSsrfYvcOgAj_E9BM11DwvkTX20JdzCIE_j1vdd-u0DWIwzAxfYBxdTZeo5APM1R4zpRV-YndmCkjeFdQc7q9Vf-6jmfxLLBB3sJeBVXITjCD5J-LoxxYFPw1Bg-cXHdVrJxzOAI6UEu_0beXzTlAnbpKggYnnRqxzW3vUcgNwj9Yue76ZNA=w640-h284)](https://blogger.googleusercontent.com/img/a/AVvXsEgqrlmb6B1CSsrfYvcOgAj_E9BM11DwvkTX20JdzCIE_j1vdd-u0DWIwzAxfYBxdTZeo5APM1R4zpRV-YndmCkjeFdQc7q9Vf-6jmfxLLBB3sJeBVXITjCD5J-LoxxYFPw1Bg-cXHdVrJxzOAI6UEu_0beXzTlAnbpKggYnnRqxzW3vUcgNwj9Yue76ZNA)
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`
[![Image 2](https://blogger.googleusercontent.com/img/a/AVvXsEi-VhHhPRtOjw9OUFVuqr5t4EnWooIZAJ7HMAygKgFPXJX0olDatk8SDAqefLThh4m5z_uATzkP0KeTWIuh87PUm-oXCe1Oka594mQCyv35Op_CtoOAS084uZn-k42uEV_m4A_-IMWa24ABNsCl35KLiVdjCW65wXEq2WzxQPYlXVbrscJy06hZRtos2p8=w640-h194)](https://blogger.googleusercontent.com/img/a/AVvXsEi-VhHhPRtOjw9OUFVuqr5t4EnWooIZAJ7HMAygKgFPXJX0olDatk8SDAqefLThh4m5z_uATzkP0KeTWIuh87PUm-oXCe1Oka594mQCyv35Op_CtoOAS084uZn-k42uEV_m4A_-IMWa24ABNsCl35KLiVdjCW65wXEq2WzxQPYlXVbrscJy06hZRtos2p8)
To rewrite them:
`[[ anyRcv isNil ifTrue: anyBlock ] ->  [ anyRcv ifNil: anyBlock ]] brewrite preview`
[![Image 3](https://blogger.googleusercontent.com/img/a/AVvXsEhi0sA6qbim9xKSX8GpNrUmBi_ZbWntFRQvu20sGAyrQnJPUHLC15EZhPE7zfeddvZprMDMeMbjRcO1rvvv72_9XqJ1y3HbzH33GmaoKLpsRsHgneXgta43qaZbwZUsz5a-c2wuqwyJy-oLe-kYdfCHYeHkYejwxEFvRm5qO1o2xnV03oj-o0c0JFIn0B0=w640-h458)](https://blogger.googleusercontent.com/img/a/AVvXsEhi0sA6qbim9xKSX8GpNrUmBi_ZbWntFRQvu20sGAyrQnJPUHLC15EZhPE7zfeddvZprMDMeMbjRcO1rvvv72_9XqJ1y3HbzH33GmaoKLpsRsHgneXgta43qaZbwZUsz5a-c2wuqwyJy-oLe-kYdfCHYeHkYejwxEFvRm5qO1o2xnV03oj-o0c0JFIn0B0)
### Refining Patterns Explicitly
You can narrow patterns explicitly using`#with: message`:
`[ anyVar isNil ifTrue: anyBlock ] bpattern with: [ anyVar ] -> [:pattern | pattern beVariable ]`
B ecause this is regular Smalltalk code, all standard development tools work out of the box: syntax highlighting, code completion, navigation, and refactorings:
[![Image 4](https://blogger.googleusercontent.com/img/a/AVvXsEh082gg8BOYisG4tlS353GAg3yIxhSmupWGQhYD1PDc-5vbBokMFvfVFy8kno9fgCGO5IZTl1W_YpP45G9K9JZwp2Bn2FAN4F-zDFglwK2rEgGL0-GjUaHBJQBPPEe7mJnE35gM49wa69loPeplTO0OpESVCE0SJ5O0yFMuyGqamESt2ykzzl0saFEeZPE=w400-h145)](https://blogger.googleusercontent.com/img/a/AVvXsEh082gg8BOYisG4tlS353GAg3yIxhSmupWGQhYD1PDc-5vbBokMFvfVFy8kno9fgCGO5IZTl1W_YpP45G9K9JZwp2Bn2FAN4F-zDFglwK2rEgGL0-GjUaHBJQBPPEe7mJnE35gM49wa69loPeplTO0OpESVCE0SJ5O0yFMuyGqamESt2ykzzl0saFEeZPE)
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:
[![Image 5](https://blogger.googleusercontent.com/img/a/AVvXsEinWaccD-GOJapKHAcDTrmFJKDvxZPHilAN1AhVRD21KhJMFU8DtC_BnUvfsCAzQZ9_xdOVXZhlvnOJZBnAxpeLfgM-YcGeTORhdt7sMsIUYYQnTNA6s0iZ6f4Zmk1dtS_PSna13uU4DbF158JLCVdvsqh0VrHdRVjvW8cp3LPI8uulY6fyz3YKv1vVzhA=w320-h173)](https://blogger.googleusercontent.com/img/a/AVvXsEinWaccD-GOJapKHAcDTrmFJKDvxZPHilAN1AhVRD21KhJMFU8DtC_BnUvfsCAzQZ9_xdOVXZhlvnOJZBnAxpeLfgM-YcGeTORhdt7sMsIUYYQnTNA6s0iZ6f4Zmk1dtS_PSna13uU4DbF158JLCVdvsqh0VrHdRVjvW8cp3LPI8uulY6fyz3YKv1vVzhA)
### 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:
[![Image 6](https://blogger.googleusercontent.com/img/a/AVvXsEipms6ylOclK4TDQxJgZ_faOqNwHcesdae8u0VArsbQQSTkv2ll_OAD9x1-uZX1J-1BBhwoNOSxUjrh-H4JN2KWaM7E2xxNI2eKTTznJVNcLxSZiPsdosDOUuJtAiZcsirwtUjvftg66cXN8FM6l4gNCqO-cIG2O1fejJABKYwtMAgb_IAZb62iomZFK9Q=w640-h300)](https://blogger.googleusercontent.com/img/a/AVvXsEipms6ylOclK4TDQxJgZ_faOqNwHcesdae8u0VArsbQQSTkv2ll_OAD9x1-uZX1J-1BBhwoNOSxUjrh-H4JN2KWaM7E2xxNI2eKTTznJVNcLxSZiPsdosDOUuJtAiZcsirwtUjvftg66cXN8FM6l4gNCqO-cIG2O1fejJABKYwtMAgb_IAZb62iomZFK9Q)
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`
[![Image 7](https://blogger.googleusercontent.com/img/a/AVvXsEjvkfFa8HXM-sDb-He-fP64IGAaPwF2hSlurTWzQaQ_TZHzHZtZL5Rmhds6PI0_dBzrXvSgkLBVFX67sWOGuIAu0-2Jw6dKCHHbmjfKitO1HbdF22_qLJ68bUjiS3MEhyTYF6zOl_k23v5LbRtv09gIALSCTyZJiWHr_aVBq2yME_zuTyMfkjzbK7w0u2k=w640-h261)](https://blogger.googleusercontent.com/img/a/AVvXsEjvkfFa8HXM-sDb-He-fP64IGAaPwF2hSlurTWzQaQ_TZHzHZtZL5Rmhds6PI0_dBzrXvSgkLBVFX67sWOGuIAu0-2Jw6dKCHHbmjfKitO1HbdF22_qLJ68bUjiS3MEhyTYF6zOl_k23v5LbRtv09gIALSCTyZJiWHr_aVBq2yME_zuTyMfkjzbK7w0u2k)
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:
*   [https://github.com/dionisiydk/BPatterns](https://github.com/dionisiydk/BPatterns)
And check the next blog post about a simplified deprecation API built on top of BPatterns:
*   [https://dionisiydk.blogspot.com/2026/02/deprecations-as-they-should-be.html](https://dionisiydk.blogspot.com/2026/02/deprecations-as-they-should-be.html)

↗ Original source
← Previous: Parakeet.cpp – Parakeet ASR inference in pure C++ with Metal GPU accelerationNext: Perplexity announces "Computer," an AI agent that assigns work to other AI agents →
Comments0