Is legacy code hard to read or do you just not get it yet?

Programmers often complain that code they inherited, their colleagues wrote or even they themselves wrote in the past is “hard to read”, with the implied understanding that “the code is bad”. I used to nod my head, thinking “yeah, I know that experience” and not give it any more thought. However, at some point I realized that my belief that a lot of code I encountered was “hard to read” was just plain false.

I now believe that any unfamiliar code is initially “hard to understand”. Understanding doesn’t come through just reading code: it requires thinking about the code. Complaining that code is “hard to read” is like complaining that a different language you haven’t yet mastered is “hard to read”. The problem is your lack of fluency, not the text.

Reading through unfamiliar code feels comparable to reading through e.g. a book on Analysis to me. I may have the background to understand what’s there and can follow along, but by just reading it I haven’t understood it and I need to do the exercises to understand the consequences of what I read.

While investigating code, what usually happens is that I:

  • discover what problem it actually intends to solve, as opposed to the problem I thought it solved
  • figure out some complexities involved in solving that problem and stop underestimating it
  • start understanding how the code addresses that complexity
  • start understanding how a lot of the seemingly unnecessary complexity deals with relevant use cases and edge cases
  • understand that the structure of the code makes sense, solves the problem and cannot be improved in obvious ways

So the code wasn’t actually intrinsically hard to understand: it was just hard to understand for me, because I understood neither the problem nor the structure of the solution.

Having realized this, what nevertheless still happened was that the initial “this code is incomprehensible” unfairly lingered in the back of my mind, contributing to a general sense of “all code out there sucks; we’re doomed”. I think that’s unfortunate and unnecessary and I’m happy that I’ve since been able to immediately follow up with “wait until you actually understand it before concluding anything” when I find myself thinking the code I’m reading is incomprehensible. Perhaps that would make you happier too?

Edit: retitled in response to feedback from lobste.rs. Thanks!

It’s Turing machines all the way down

I don’t have a formal CS background and for a long time the whole idea of Turing machines seemed a bit abstract to me, having little to do with actual computers or programming. At some point things ‘clicked’ for me, but not because it was clearly explained somewhere, so I thought I’d give it a go and explain the relation between Turing machines and actual computers and programming.

If this is obvious to you, you are not the target audience of this post. Perhaps there is no target audience and I’m basically explaining to myself what’s obvious to everyone else. Nevertheless, here goes:

The relation between Turing machines, computers and programming

One of the first things I was confused about after studying Turing machines: are they computers or are they programs? On the one hand, it’s clearly a machine, with a pretty ‘physical’ description. That seems to come closest to a computer. On the other hand, it has this transition function, which encodes the rules according to which the machine operates. It’s unclear where it’s stored, how it is represented and how it performs its operations, but it definitely seems more like a program than like a computer. So which is it?

The answer is: both. In the end almost every machine consists of both the physical parts and a ‘program’ that guides its operation. Even if the program consists only of “pressing this button turns the machine on”, then logically and physically there is a state transition. I think the easiest way to look at it is from the other side: an actual CPU with its firmware is a Turing machine, where the physical CPU is the machine and the firmware is the transition function.

A major difference between automata and Turing machines is that a Turing machine can generate output. A bunch of proofs around Turing machines rely on the fact that the output of a Turing machine can be (the transition function for) another Turing machine and that’s what made it click for me. That initially seems like just a neat trick, until I realized that that is exactly what’s going on in practice: a computer contains layers upon layers of Turing machines, defining each other, defining programs for each other and rewriting input for each other.

An example

The Ruby interpreter is a Turing machine that accepts valid Ruby programs as input and outputs bytecode that is input for its internal bytecode interpreter, which is another Turing machine, which outputs machine code for the CPU microcode Turing machine which finally outputs the instructions for the hardware Turing machine, the CPU.

At the same time, the representation of the Ruby interpreter Turing machine on disk is itself a direct input for the CPU microcode Turing machine (I’m hand waving some details here). But it could also have been an input for, e.g. the Java virtual machine bytecode interpreter. Which is of course another Turing machine.

Updating firmware using FreeDOS

A common problem for Linux users is that firmware updates for some devices are only distributed as Windows or DOS executables. For an experienced Linux user it may seem easy: download FreeDOS, write it to a USB flash drive, add the DOS executable, boot from the flash drive, done.

Unfortunately, things aren’t that simple, primarily because after writing the FreeDOS iso or img to the drive there is readonly filesystem, or no space left in the partition, so the DOS executable can’t be written to the drive.

A solution I puzzled together from various sources is:

    • Download the freeDOS iso (standard installer) from their website
    • Prepare a file to hold the disk image you are creating:
      $ dd if=/dev/zero of=mydos12.img count=1024 bs=1M
    • Install freeDOS into the disk image using
      $ qemu-system-x86_64 -cdrom FD12CD.iso mydos12.img -boot d
    • Verify the image boots correctly via
      $ qemu-system-x86_64 -hda mydos12.img -boot c
    • Mount the image with a specific offset to be able to add files:
      $ mkdir mnt
      $ sudo mount -t msdos -o loop,offset=32256 mydos12.img mnt
      $ sudo cp installer.exe mnt
      $ sudo umount mnt
    • Write the image to a flash drive with
      $ sudo dd if=dos12.img of=/dev/sdc bs=1M
    • Test it by figuring out the device id of the flash drive using
      $ lsusb
      and inserting the value you find into
      $ sudo qemu-system-x86_64 -usb -usbdevice host:0781:5581 -m 1024 -vga std

Rescuing an SSD using ddrescue

Some things I learned while rescuing the SSD from my sister’s macbook, which would no longer boot correctly (the progress bar would show, proceed until roughly halfway and the machine would then shut down):

  • SSD’s can get bad blocks and fail to do something about it. Possibly the firmware is defective, but unfortunately Crucial does not have changelogs for their firmware available, so there is no way of knowing whether a firmware update could have prevented the problem. Also updating the firmware is non-trivial, as their ISO’s won’t boot from USB and their management software only runs in Windows. Back to Intel/Samsung. You get what you pay for I guess.
  • almost none of the posts about fixing the filesystem mention that you should try running fsck -fry / if running fsck -fy / fails. It will then try to rebuild the catalog. Without it, the filesystem cannot be unmounted cleanly, the filesystem cannot be mounted read/write in e.g. a Linux machine and you generally cannot try to fix problems on the disk
  • ddrescue is a great tool and plenty of helpful posts explain how to use it, but almost none mention that it runs much faster if you set

    $ echo -n "2" >/sys/block/sdf/device/eh_timeout
    $ echo -n "3" >/sys/block/sdf/device/timeout

    to lower the time before ddrescue concludes it cannot read a block from 180 seconds to 18 seconds. The more bad blocks there are, the more time this saves you. For modern hardware there does not seem to be a good reason to have the timeouts as high as they are by default. [1]

[1] Thanks to newspaint, which links to an answer on superuser.com.

Ruby, fork and copy-on-write

The Linux copy-on-write feature sounds like it should make forking a Ruby process cheap in terms of the additional memory that would be used. In practice, it turns out it often doesn’t help much at all.

When you call fork, the new process shares most of its memory with the parent process and as long as neither the parent nor the child changes the memory, they make do with the single shared copy. As soon as one of the two writes to a memory page, the contents of that page is duplicated and each of the copies is assigned to one of the two processes, after which the write can be safely performed.

Before Ruby 2.0 this feature was of little use to Ruby users, because the garbage collection code used by Ruby wrote to a lot of memory on every run and most of the memory would be duplicated the first time the GC ran in either of the two processes.

After Ruby 2.0 things became a bit better, as the garbage collection code no longer touched memory. However, as https://brandur.org/ruby-memory explains very well: during normal use, Ruby still touches memory all over the place and quickly most pages are still duplicated.

A solution is in the works: before forking, compact the heap, by stuffing all live objects into as few memory pages as possible, such that those won’t be touched by object (de)allocations for a while. Unfortunately, it is unclear when, or even if, these patches will land MRI.

An LLVM JIT in Swift

Alex Denisov wrote an excellent post, with a follow up, about how to create an LLVM JIT compiler in Swift. There’s also a presentation that covers the same material (but there are some tiny differences).

I attempted to follow his guide, but ran into the problem that I’m using

  • Linux
  • Swift 3.1.1
  • LLVM 4.0

and that there were a few small bugs in his code. So to not let my work go to waste, I’ve uploaded my updated version of his code to Github. On my Ubuntu 17.04 system I can compile it with
swiftc -O $1 \
-I /usr/include/llvm-c \
-I /usr/lib/llvm-4.0/build/include/llvm-c \
-L /usr/lib/llvm-4.0/lib \
-lc++ \
-lncurses \
-lz \
-lLLVMX86Disassembler -lLLVMX86AsmParser -lLLVMX86CodeGen -lLLVMGlobalISel -lLLVMSelectionDAG -lLLVMAsmPrinter -lLLVMDebugInfoCodeView -lLLVMDebugInfoMSF -lLLVMCodeGen -lLLVMScalarOpts -lLLVMInstCombine -lLLVMTransformUtils -lLLVMBitWriter -lLLVMX86Desc -lLLVMMCDisassembler -lLLVMX86Info -lLLVMX86AsmPrinter -lLLVMX86Utils -lLLVMMCJIT -lLLVMExecutionEngine -lLLVMTarget -lLLVMAnalysis -lLLVMProfileData -lLLVMRuntimeDyld -lLLVMObject -lLLVMMCParser -lLLVMBitReader -lLLVMMC -lLLVMCore -lLLVMSupport -lLLVMDemangle

I’m probably including a few libraries that don’t need to be in there, but it gets the job done.

Program development by stepwise refinement

The second paper on Barbara Liskov’s reading list for computer scientists is titled ‘Program development by stepwise refinement’. It was written by Niklaus Wirth, of Pascal, Modula and Oberon fame, in 1971. The inimitable Adrian Colyer of the morning paper already wrote a great summary.

The key takeaway: programmers should (be taught to) solve problems in multiple steps, incrementally refining a solution through a set of small design decisions, instead of trying to solve the complete problem at once.

Among these criteria [used to make incremental design decisions] are efficiency, storage economy, clarity, and regularity of structure. Students must be taught to be conscious of the involved decisions and to critically examine and to reject solutions, sometimes even if they are correct as far as the result is concerned; they must learn to weigh the various aspects of design alternatives in the light of these criteria. In particular, they must be taught to revoke earlier decisions and to back up, if necessary, even to the top.

And I would like to add: students should be taught to assume their solution is deficient and should be shown how reviews of theirs designs and code by peers usually leads to better solutions.