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.
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.
The produced script does nothing more than the specific automated maintenance to the specific Philips testing component. Can’t we reuse part of this for the next maintenance task? This is a natural question. The answer is that Rascal and the Clair C++ analysis library are already reusable components. These components need to be robust, well-tested and generally applicable. The specific small script for the specific maintenance task is free of such requirements. It can be a bespoke, ad-hoc throw-away script, which depends on the specific timing, context, style and general state of the currently maintained code. Not having to think about all possible C++ code makes creating a maintenance script much more delineated and economical.
Of course, learning Rascal and learning to automate maintenance is an investment. Using a specific library like Clair requires skill and intelligence as well. We observed no bottlenecks in this regard at Philips. Rascal is relatively easy to learn for software engineers, with its familiar programming language syntax and semantics. The new concepts, such as pattern matching, context-free grammars and relational calculus, are default elements of computer science bachelor programs in the Netherlands. Most engineers will recognize them and be happy to see them integrated into their workflow.
Automating the maintenance of C++ code with Rascal enables code changes that would otherwise be too risky or uneconomical. We know maintenance represents a major time and energy investment in all high-tech software. By automating it, we can save time and reduce the risk of arbitrary human errors at the same time. Finally, metaprogramming can be challenging, interesting and highly motivating for advanced software engineers, as opposed to the labored manual reading and writing of legacy code.
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).