重写 Psion CTRAN:从可用到优雅的工程重构实录
Let’s start with a quick recap for those of you who are just tuning in (and for those who have been tuned in for a while but are likely very lost):
• In the late 80s, Psion developed their own message-passing Object Oriented C dialect with its own markup language. They did this because Psion be Psion-ing. This is a recurring theme.
• CTRAN.EXE, a 16-bit DOS app, is Psion’s OO preprocessor. It takes class declaration files using this OO markup language (called “category files” by no one but Psion – see the previous point) and translates them into C and Turbo Assembler code.
• The C generator in CTRAN.EXE is rudimentary but very reliable.
• The TASM generator is, shall we say, less optimal. There are bugs. Sometimes it crashes. I’m convinced no one actually used it.
Here’s an example category file. If you’ve written parsers before, you’ll enjoy trying to wrap your head around this format!
At the beginning of 2024, after ragequitting the development of a new EPOC16 word processor because of choices Psion made in its OO implementation, I decided to recreate CTRAN.EXE for modern OSes – Linux, *BSD, Haiku, macOS, even Windows… I also wanted a drop-in replacement for the original DOS executable. I chose Free Pascal, because it’s highly portable, including having cross-compiler that targets 32-bit DOS. Plus, I like the language, and Object Pascal feels very era-appropriate.
Fast forward to today. My new CTRAN works at least as well as the original, running on every OS I wanted it to and fixing a lot of the issues with Psion’s CTRAN that caused my initial ragequit. I completed the first 90% back in June 2024. This was the “make it work” phase.
The last 90% is taking waaaaay too long.
Make It Right# (https://thelastpsion.com/posts/make-it-right-next-steps-with-ctran/#make-it-right)
While I have an engineer’s mind, I’ll be the first to admit that I’m not the best programmer in the world. I get stuff done, but it’ll take me a while and the code won’t necessarily be pretty.
The “make it right” phase is where I’m really learning things.
First, I’ve gone through the code and added tonnes of TODO and FIX comments. If something isn’t clear, or if I think I’ve missed an edge case, I’m noting it down.
Readability is key. If I can’t understand what’s going on in my own code two years on, no one else will be able to. A function that’s only ever called once but cleans up the code is a worthwhile function.
Sometimes I find that I’ve attacked the same problem from multiple angles at different times. I probably started off thinking I was solving completely different problems and ended up with a few code snippets or functions that do almost the same thing. By cleaning the code up, I’m finding these little snippets all over the place that can be further tidied, which then reveals more snippets that can be tidied, etc.
I’m flooding the code with comments. Well-written code shows the how but not necessarily the why. I’m trying to explain Psion’s choices, or at least why the code has to be this way to match what Psion did. I’m also making it clear when I’ve taken a different path and why. Every function needs a description. If I can’t describe it, I can’t test it.
The aim is to get CTRAN to a place where I can write unit tests. Currently I’m only doing manual testing, which needs to change. My plan is to tackle as many TODOs and FIXes as possible before refactoring the code into multiple files. Once I’ve done that, I’ll make a release (0.0.3? 0.1.0?) and tackle the Great Refactor.
The Past 6 Months# (https://thelastpsion.com/posts/make-it-right-next-steps-with-ctran/#the-past-6-months)
If you follow me on Mastodon (https://oldbytes.space/@thelastpsion), you’ll know I’ve mostly been dealing with questionable programming choices, both my own and Psion’s. I’ll go into more detail about specific things in future posts. For now, here are some notable moments:
• Abstract classes: In laymans terms, an abstract class is a class with no new methods and no property, they simply set up a structure for child classes to use. Psion CTRAN skips them in some of its output, and now the new CTRAN does too. I spent months trying to work out how to do this in my head, before having the revolutionary idea of writing the problem down. It took a weekend to get working.
• Turbo Assembler generator: I’m convinced that Psion (Colly Myers?) added TASM generation to CTRAN.EXE in a rush, went, “Yep, that’ll do,” and never looked at it again. I’m also convinced that no one ever used it. I think my TASM generator now works better than the original, but I don’t know 8086 assembly well enough to know for sure. In addition, I haven’t yet found a project that uses CTRAN’s TASM output, so I’ve got no way to test it. I’ve often thought about just dropping the “feature” from the new CTRAN. For now, I’m keeping it in but avoiding any work on it. Also, using it displays a massive disclaimer explaining how broken it probably is.
• Tedious but worthwhile code clean-up: Adding small helper functions like IsConcrete() that help with abstract class detection. Caching the results of function that processes a class’s methods rather than calling it multiple times. Not passing unnecessary data to functions. Calling functions as early as possible so that there’s less unnecessary data to walk through. Most of these won’t make a noticeable difference to how fast CTRAN runs on any machine from the past 20 years, but they make the code more efficient and understandable.
The Future# (https://thelastpsion.com/posts/make-it-right-next-steps-with-ctran/#the-future)
This project sits in a multi-level niche. It’s a recreation of a little-known tool in a largely forgotten SDK for a long-discontinued operating system on a selection of 16-bit portables from the 90s. But it’s a platform I deeply care about for all sorts of reasons, the least of which is nostalgia.
My next step is to rewrite the rest of the tools in the SDK. There are a few more small apps that handle resource file creation, object manipulation and executable linking. None of them are particularly complicated, but without any source code they’ll need to be reverse-engineered manually.
Eventually I’ll tackle the compiler, a project that will inevitably render CTRAN redundant. After all, there’ll be no need for a separate preprocessor if the compiler can handle OO code. That doesn’t mean CTRAN is a waste of time. It’s where I’m cutting my teeth, learning how to write better code, not to mention the basics of lexers, parsers, abstract syntax trees, etc.
At some point I might even use it to write that new word processor.