Babel Plugin for the TC39 Pattern Maching Proposal
Learning Babel
Follow the tutorial https://github.com/ULL-ESIT-PL/babel-learning (opens in a new tab) to learn how to create a Babel plugin.
The TC39 Proposal Pattern Matching
The TC39 proposal Pattern Matching (opens in a new tab) was approved for Stage 1 in the May 2018 TC39 meeting, and slides for that presentation are available here (opens in a new tab). Its current form was presented to TC39 in the April 2021 meeting (slides (opens in a new tab)). Our comments about the slides are available here (opens in a new tab)
It introduces three new concepts to Javascript:
-
the "matcher pattern", a new DSL closely based in part on the existing Destructuring Binding Patterns (opens in a new tab) which allows recursively testing the structure and contents of a value in multiple ways at once, and extracting some of that structure into local bindings at the same time.
Matcher patterns can be divided into three general varieties:
- Value patterns, which test that the subject matches some criteria, like "is the string
"foo"
" or "matches the variablebar
". - Structure patterns, which test the subject matches some structural criteria like "has the property
foo
" or "is at least length 3", and also let you recursively apply additional matchers to parts of that structure. - Combinator patterns, which let you match several patterns in parallel on the same subject, with simple boolean
and
/or
logic.
- Value patterns, which test that the subject matches some criteria, like "is the string
-
the
match(){}
expression, a general replacement for theswitch
statement that uses matcher patterns to resolve to one of several values,match(<subject-expression>) { when <pattern>: <value-expression>; when <pattern>: <value-expression>; ... default: <value-expression>; }
For instance:
match(val) { when < 10: console.log("small"); when >= 10 and < 20: console.log("mid"); default: "large"; }
-
the
is
boolean operator, which allows for one-off testing of a value against a matcher pattern, potentially also introducing bindings from that test into the local environment. The code below checks the types of the values in the array:var json = { 'user': ['Lily', 13] }; if( json is {user: [String and let name, Number and let age]} ) { // let name = 'Lily', let age = 13 print(`User ${name} is ${age} years old.`); }
Here is another example of the DSL. Notice how
a let
, const
, or var
keyword followed by a valid variable name
(identical to binding statements anywhere else) is valid in a pattern.
Binding patterns always match,
and additionally introduce a binding,
binding the subject to the given name
with the given binding semantics.
match (res) {
when { status: 200 or 201, let pages, let data } and if (pages > 1):
handlePagedData(pages, data);
when { status: 200 or 201, let pages, let data } and if (pages === 1):
handleSinglePage(data);
default: handleError(res);
}
The Babel Plugin for the TC39 Pattern Matching Proposal
The plugin https://github.com/iptop/babel-plugin-proposal-pattern-matching (opens in a new tab) implements
- A zero JS grammar modification of the proposal,
- It offers a good performance
- Once the Babel parser will be extended, it provides the support for a straightforward implementation of https://github.com/tc39/proposal-pattern-matching (opens in a new tab).
The expression Zero Grammar used here means that the plugin does not introduce new syntax to the JavaScript language. The match
keyword is substituted by a function call to a function with name match
. Also, the when <pattern>: <value-expression>;
disappears and is replaced by function parameters of match
:
const fib = n=>match(n)( // match(n) {
(v=1)=>1, // when == 1: 1:
(v=2)=>1, // when == 2: 1;
_=>fib(_-1)+fib(_-2) // default: fib(n-1)+fib(n-2);
) // }
console.log(fib(10))
This strongly simplifies the implementation of the plugin avoiding to modify the lexer and parser of the Babel compiler. An analysis and transformation of the AST is enough to implement the plugin.
Installation and Setup of babel-plugin-proposal-pattern-matching
To use this Babel plugin, you need to install it and configure Babel to use the plugin:
-
Install the Plugin:
npm install babel-plugin-proposal-pattern-matching
-
Configure Babel: Add the plugin to your Babel configuration (
.babelrc
orbabel.config.js
):{ "plugins": ["babel-plugin-proposal-pattern-matching"] }
With this setup, you can use pattern matching in your JavaScript code, as demonstrated in the example.
How it works
Consider the example ULL-ESIT-PL/babel-learning/fib-easy.js (opens in a new tab):
➜ tc39-pattern-matching git:(main) ✗ cat fib-easy.js
➜ tc39-pattern-matching git:(main) ✗ cat -n fib-easy.js
1 import match from 'babel-plugin-proposal-pattern-matching/match.js'
2 const fib = n=>match(n)(
3 (v=1)=>1,
4 (v=2)=>1,
5 _=>fib(_-1)+fib(_-2)
6 )
7
8 console.log(fib(10))
9 // -> 55
This defines the Fibonacci function fib
using an arrow function and pattern matching.
Matcher patterns are a new DSL, closely inspired by destructuring patterns, for recursively testing the structure and contents of a value while simultaneously extracting some parts of that value as local bindings for use by other code.
Matcher patterns can be divided into three general varieties:
- Value patterns, which test that the subject matches some criteria, like "is the string
"foo"
" or "matches the variablebar
". - Structure patterns, which test the subject matches some structural criteria like "has the property
foo
" or "is at least length 3", and also let you recursively apply additional matchers to parts of that structure. - Combinator patterns, which let you match several patterns in parallel on the same subject, with simple boolean
and
/or
logic.
The plugin visits the CallExpression
nodes corresponding to calls match(n)(f1, f2, ..., fn)
replacing the AST node
by a function that uses a sequence of if
statements to match the patterns that will be built
based on the actual value of n
.
See the entry point of the plugin at src/index.js (opens in a new tab):
module.exports = (babel) => {
return {
visitor: {
ImportDeclaration (path, state) {
resolveImportDeclaration(babel, path, state)
},
CallExpression (path, state) {
resolveCallExpression(babel, path, state)
}
}
}
}
The manipulation of the AST is done in the src/visitor/call-expression.js (opens in a new tab) module. When the identifier used in the callee
of the CallExpression
node is match
, the plugin transforms the AST using the transformMatch (opens in a new tab) function:
const transformMatch = require('../transform/match')
const transformFnMatch = require('../transform/fnmatch')
module.exports = (babel, path, state) => {
const $callee = path.get('callee')
const $$callee = $callee.node
if ($$callee.type == 'Identifier') {
/* Extract AST tokens from NodePath */
const astTag = $callee.getData('pattern-matching')
if (astTag == 'match') {
transformMatch(babel, $callee)
}
if (astTag == 'fnmatch') {
transformFnMatch(babel, $callee)
}
}
}
The transformMatch
function builds the replacement AST node using this Babel template
(v=> {
const UID = EXP
BLOCKS
throw new Error("No matching pattern");
})()
that template can be clearly seen in the shape of the generated code fib-easy.mjs:
const fib = n => (v => {
const _uid = n; // const UID = EXP
if (_uid === 1) { // BLOCKS
return 1;
}
if (_uid === 2) {
return 1;
}
return fib(_uid - 1) + fib(_uid - 2); // END BLOCKS
throw new Error("No matching pattern");
})();
this is the actual code of the transformMatch
function:
module.exports = function transformMatch (babel, referencePath) {
const $root = referencePath.parentPath.parentPath
const $$uid = $root.scope.generateUidIdentifier('uid')
const $matching = getMatching($root)
const $$matching = $matching.node
const $patterns = getPatterns($root)
const $$blocks = transformPatterns(babel, $patterns, $$uid).filter(item => item)
const $$IIFE = babel.template(`
(v=> {
const UID = EXP
BLOCKS
throw new Error("No matching pattern");
})()
`)({
UID: $$uid,
EXP: $$matching,
BLOCKS: $$blocks
})
$root.replaceWith($$IIFE)
}
So what remains to understand is how the transformPatterns (opens in a new tab) function works.
Four cases are considered: assign patterns, identifier patterns, array patterns and object patterns:
function transformPatterns (babel, $patterns, $$uid) {
return $patterns.map($pattern => {
const $param = $pattern.get('params.0')
const $$param = $param.node
switch ($$param.type) {
case 'AssignmentPattern':
return createIFBlock(babel, $pattern, $param, $$uid)
case 'Identifier':
return createReturnBlock(babel, $pattern, $param, $$uid)
case 'ArrayPattern':
return createDeconstruction(babel, $pattern, $param, $$uid, 'ArrayPattern')
case 'ObjectPattern':
return createDeconstruction(babel, $pattern, $param, $$uid, 'ObjectPattern')
}
})
}
Our example uses the Identifier
case. The createReturnBlock
function is used to build the AST node for the pattern matching:
function createReturnBlock (babel, $pattern, $param, $$uid) {
const paramName = $param.get('name').node
const $body = $pattern.get('body')
const $$body = $body.node
$body.scope.rename(paramName, $$uid.name)
const $$block = babel.template(`
return RET
`)({
RET: resolveBody(babel, $$body)
})
return $$block
}
This way the initial code is transformed to:
➜ tc39-pattern-matching git:(main) ✗ npx babel fib-easy.js -o fib-easy.mjs
➜ tc39-pattern-matching git:(main) ✗ cat -n fib-easy.mjs
1 import match from 'babel-plugin-proposal-pattern-matching/match.js';
2 const fib = n => (v => {
3 const _uid = n;
4 if (_uid === 1) {
5 return 1;
6 }
7 if (_uid === 2) {
8 return 1;
9 }
10 return fib(_uid - 1) + fib(_uid - 2);
11 throw new Error("No matching pattern");
12 })();
13 console.log(fib(10));
14 // -> 55
Notice that v
is not used in the transformed code. The _uid
variable is used to store the value of n
and the patterns are matched against it.
That when executed gives:
➜ tc39-pattern-matching git:(main) ✗ node fib-easy.mjs
55
The Task
Following the chapter at https://github.com/ULL-ESIT-PL/babel-learning/tree/main/src/awesome/tc39-pattern-matching (opens in a new tab) re-create your own Babel plugin that implements as much as you can of the TC39 pattern matching proposal.
Videos
The last one is in Chinese. It is a talk by iptop (opens in a new tab) the author of the plugin.
Rubric
References
-
See the Svelte maintainer Tan Liu Hau (陈立豪) article "Creating custom JavaScript syntax with Babel" (September 25, 2019) available at https://lihautan.com/creating-custom-javascript-syntax-with-babel (opens in a new tab)
-
Babel ast-explorer (opens in a new tab): https://lihautan.com/babel-ast-explorer/ (opens in a new tab)
-
Step-by-step guide for writing a custom babel transformation (opens in a new tab) September 12, 2019
-
Babel macros (opens in a new tab) by Tan Liu Hau
-
I Can Babel Macros (and So Can You!) (opens in a new tab) by Shawn "swyx" Wang in JSConf Hawaii 2019 (Growing a Language)
-
Learning Babel Macros: ULL-ESIT-PL/learning-babel-macros (opens in a new tab) by Casiano
-
babel-plugin-macros Usage for macros authors (opens in a new tab). A tutorial on how to write Babel macros by Kent C. Dodds
-
List of Babel Youtube videos by Tan Liu Hau (opens in a new tab)
-
List of Q&A Youtube videos by Tan Liu Hau (opens in a new tab)
-
Babel flow pragma bug (opens in a new tab) not finished
-
babel-plugin-transform-rename-properties (opens in a new tab)
-
My PL notes on Babel are here: https://ull-pl.vercel.app/topics/tree-transformations/babel (opens in a new tab)
-
StackOverflow: How to create a babel plugin for internal use (opens in a new tab)
-
See this introduction by Matt Zeunert and the associated GitHub repository (opens in a new tab) for a simple example of how to create a Babel plugin.
-
Babel Handbook at jamiebuilds/babel-handbook (opens in a new tab) This document covers how to create Babel plugins.
-
Awesome Babel (opens in a new tab) A list of awesome Babel plugins, presets, etc.
-
babel-plugin-transform-remove-debugger at GitHub (opens in a new tab)
-
A boilerplate monorepo for people writing babel plugins in normal plugin form as well as babel-plugin-macros form (opens in a new tab) by Shawn "swyx" Wang
-
JavaScript engines - how do they even? (opens in a new tab) by Franziska Hinkelmann from the V8 Team. Youtubes. JSConf EU 2017
-
Parsing JavaScript - better lazy than eager? (opens in a new tab) by Marja Hölttä from the V8 Team. JSConf EU 2017
-
Parsing Javascript - Programming Languages (opens in a new tab) a non maintained Udacity course. 2012. The Youtube list of videos for this course (opens in a new tab)
-
Babel repo: issues with label help wanted (opens in a new tab)
-
ECMA TC39, TC53 presentations (opens in a new tab) at Youtube
-
TC39: From the Proposal to ECMAScript, Step by Step (opens in a new tab). Romulo Cintra. Feb 2024. Youtube [Slides}(https://es.slideshare.net/slideshow/from-the-proposal-to-ecmascript-step-by-step/264286515 (opens in a new tab))
Same contents and title but in Porto: https://youtu.be/ZheFIz4FhXo?si=ZQqgUWdEm5EU6iAj (opens in a new tab) TC39 – From the proposal to ECMAScript - Step by Step - Romulo Cintra - Fest.dev Porto, 2023
-
So how does Babel even work? (opens in a new tab) by Henry Zhu. 2017. Youtube
-
On BabelJS (opens in a new tab) by James Kyle. 2016. Youtube. (Slides (opens in a new tab))
-
Nicolo's talk
@babel/howto
at HolyJS in Youtube: https://youtu.be/UeVq_U5obnE?si=Vl_A49__5zgITvjx (opens in a new tab)- See the associated repo at GitHub: https://github.com/nicolo-ribaudo/conf-holyjs-moscow-2019 (opens in a new tab),
- Nicolo slides
- The plugin babel-plugin-transform-optional-chaining (opens in a new tab) at GitHub Babel repo and the way it is used (opens in a new tab)
- Web site of the HolyJS 2019 conference: https://holyjs.ru/en/archive/2019%20Moscow/ (opens in a new tab)
- babel-plugin-tester (opens in a new tab)
-
Babel Tutorial Part 3 - Source Maps (opens in a new tab) 2020
-
Babel: Beyond the Basics (opens in a new tab). Sebastian McKenzie, Creator of Babel. 2015. Youtube. Slides (opens in a new tab)
-
Google slides of the presentation at ÚLTIMOS AVANCES EN INFORMÁTICA 2004 XXVIII ed. (opens in a new tab) "Function Expressions on the Left Side of Assignments"