Metaprogramming for Maintenance at Philips
This is a condensed version of the article “Large-scale semi-automated migration of legacy C/C++ test code” by Mathijs Schuts, Rodin Aarssen, Paul Tielemans and Jurgen Vinju, published in volume 2022 of Software: Practice and Experience (Wiley).
Philips joined forces with Centrum Wiskunde & Informatica and Eindhoven University of Technology to automatically maintain a key component, written in C++, of one of its successful software systems. Without automation, and for good reasons, such large-scale maintenance is often considered to be too complex and too risky. The partners applied the Rascal metaprogramming solution to script approximately 5,840 source code transformations. The resulting C++ code passed the quality assurance processes and is, in fact, of higher quality than before.
The price of the success of any high-tech system is that it gets increasingly complex over time. The code growth in volume and complexity raises the costs of maintenance (perfective, corrective or adaptive). When the code grows, then so do the costs. Code practically always grows exponentially because it’s rarely thrown away and often multiplied (“cloned”). And so maintenance costs also grow exponentially. The (unverified) story at the coffee machine is that companies and governmental institutes in the Netherlands yearly spend around 15 percent of the total cumulative cost-of-ownership of their source code on maintenance. Perhaps surprisingly, this number could easily be higher than the initial development costs.
Change is a force of nature, so what kind of code evolution is essential for survival? Firstly, we have to adapt to new market opportunities. For example, Arm processors and Linux have economical advantages, so we should be able to port our code to Linux on Arm. Secondly, we have to react to changing technical circumstances: programming languages and libraries we depend on disappear and new ones with better features appear. This is also a major part of remaining secure: security flaws in dependencies are a hazard. Thirdly, we have to remove arbitrary complexity to be able to focus on the essential complexity: source code grows more complex under the pressure of quick features and quick bug fixes (aka technical debt). We have to shrink the code back to a humane understandable size. In short: without continuous source code maintenance, businesses get stuck in the past, eventually losing their competitive edge.
Software maintenance paradox
The reality of source code maintenance is that it’s avoided. The reason is simple and completely reasonable: to do source code maintenance and do it without too much risk, you need more time and more budget than you have. We all know that programmers make mistakes sometimes. If we start to rapidly maintain thousands of lines of code, we’ll make arbitrary mistakes. It doesn’t matter that we’re doing essential maintenance; mistakes are risky for us and our customers’ business. If you have to understand an exponential amount of source code lines, it’s certainly going to take a long time before you can change them and test them without risk. Therefore, it seems to be good economical software engineering practice to never change a working component.
So we arrive at the maintenance strategy known as “clone and adapt.” A new component based on the working code of another, with minor adaptations, is the most trusted way of adding new features to a successful codebase. Now we’re doubling down on exponential growth, and exponential costs.
It’s certainly good to avoid maintaining code, right now. At the same time, it’s certainly bad for businesses to avoid maintaining code, forever. Contradiction – we’re stuck between the long-term and the short-term perspective on software maintenance. Therefore, the price of success seems unavoidably the eventual discontinuation of the codebase. Or is there a way out of this paradox?
A case of code renovation
Thousands of lines of C++ code that execute essential unit and regression tests are a key software asset at Philips. The existing proprietary testing library needed to be cleaned up and replaced by a modern unit testing library. This code is essential for the quality assurance of the system-under-test, and so new accidents in existing tests are unacceptable. However, the code was too large to maintain manually within a reasonable time and energy budget. It seems like a typical case of the software maintenance paradox: there’s no time to do it by hand.
Instead, we automated the maintenance steps using a dedicated programming language for analyzing and manipulating source code – Rascal and its Clair library for C++ analysis. The metaprogram created for carrying out the mass maintenance eventually produced 5,840 surgical source code changes in C++ source files and CMake files. The Rascal program reads all the input source files, parses them into a tree structure, locates patterns in those trees where code changes have to be made, extracts the necessary information, then removes old code and places new code at the detected locations.
Automated mass maintenance isn’t new in the Netherlands. There’s experience, for example, by Niels Veerman of Vrije Universiteit Amsterdam on maintaining enormous Cobol systems of banks with components of hundreds of thousands of lines of code. These systems were stuck for the same reasons, but the budget for their maintenance is much higher than the system we’re looking at now. Arjan Mooij and colleagues at ESI (TNO) also have experience with “industrial rejuvenation,” where source code is maintained by first extracting state machine models, then restructuring those models and then generating new code. In our case, we stayed on the C++ code level and rewrote the code almost horizontally from the source to the new target.
Key enablers
The key enablers of effective automated maintenance aren’t only in the Rascal metaprogramming language but also in the software engineering process and practice of Philips. The Rascal technology was accepted within the technology stack in the front of the quality assurance pipeline. This means that normal checks and balances, such as critical code review and automatic and manual testing in different levels of technological readiness, come after the automated maintenance, thus providing a safe environment for any code maintenance, automated or otherwise. We wouldn’t have tried this automated maintenance without the standard and trusted QA process of Philips.
The maintenance itself, as a Rascal script, was developed in fast iterations. By easily experimenting and reviewing the proposed transformations, we learned where more analysis was required, where a few outliers of otherwise standard coding patterns were and which code was so unique that manual maintenance was just as easy. The automation factor, depending on how we count, is around 40 – we wrote a single line of Rascal for transforming approximately 40 lines of C++.
During the scripting process, we ran into the requirement of extracting information from build files. The author of the script quickly wrote a context-free grammar (directly in Rascal), specific for the dialect of CMake used by Philips. The opportunity to add new languages and dialects to the existing Rascal infrastructure is a key enabler. With the information from the CMake files, the transformation could compute the effective ‘include path’ for running the Clair C++ parsing front-end, making sure that the pattern matching for the source code rewrites was exact.
Another interesting issue that popped up was the C++ preprocessor. We parse the C++ code after running the preprocessor, but we want to rewrite the code as-is before running the preprocessor. To solve this conundrum, we changed the maintenance script to collect file patches, to be applied to the original C++ file, instead of rewriting the syntax trees and printing them back. This solution was later added to the standard library of Rascal for future reuse. It’s enabled by meticulously tracing source code locations using universal resource identifiers (URI) and file offsets.