LightPhotos

Learning Rust while the agents write it

I picked Rust knowing only the fundamentals.

What I learned

I took on two problems at once

A few days ago I wrote about choosing Rust for the whole app. What I skipped in that post is how much Rust I actually knew when I chose it, which was the basics. I'd shipped Rust before, so I wasn't starting from zero. But knowing the basics is a long way from writing image code that keeps up as you drag a slider, and I knew that gap was going to hurt.

The second problem is the goal itself. I want a photo editor where you move a slider and the image has already changed, even in a folder with thousands of RAW files in it. That's a speed target, and in my experience speed is very hard to add at the end.

Taking on both at once is how a lot of side projects stall. I think the reason mine hasn't is that I'm not the one doing the typing.

Where my time actually goes

WHO DOES THE WORK, BY STAGE Plan me, and I don't hand this one over Build the agents write essentially all of it Test me, running the real app Review me, reading the diff Release scripted, and safe to run twice BLUE IS MY OWN HANDS ON THE KEYBOARD
The agents do nearly all of the typing. I keep planning for myself, because everything after it depends on getting it right.

Planning is the part I won't hand over

Every change starts as a short written plan, and I write it myself. It's usually just a paragraph and a list. It says what the change is, which parts of the app it touches, what I'm deliberately leaving out, and how I'll know it worked.

That last item does most of the work. If I can't say what would show the change is done, I don't really have a plan yet. "Make thumbnails faster" is too vague to check. "A folder of 3,000 photos fills the first two screens without the scroll stuttering, and memory doesn't climb while I scroll" is something I can actually test.

Planning first is also the cheapest place to be wrong. An agent will sometimes confidently build the wrong thing. Catching that in a paragraph costs me a minute. Catching it in finished code costs an afternoon, and honestly I probably won't throw that code away, because by then it compiles and I'm attached to it.

This is the same habit as the seven-item list in the first post. I've found that writing the list up front matters even more when something can build the whole list in twenty minutes.

Building one thing at a time

The rule I keep coming back to is that every change has to leave the app in a working state. One task, then build, then run the app, then the next task. I don't let five tasks pile up and hope the build works at the end.

I do this because when two changes land together and the app misbehaves, I can't tell which one caused it, and that's the thing I most need to know. Agents can write a lot of reasonable-looking code very quickly, so if I'm not careful they bundle changes together and I lose that clue.

I don't take the agent's word for it

Agents are quick to announce success: "The thumbnail loading should now be much faster." I've lost more time to that word "should" than to any bug in this codebase.

The pixel maths is the easy part to test, so it has real tests. Exposure, the tone curve, white balance and the rest are pure functions, meaning the same input always gives the same output, so I can check them against fixed numbers.

Everything else I check by running the app myself, and this is where I'm weakest right now: scrolling, decoding, panning, memory. Clicking around a window tells me whether something is broken. It doesn't reliably tell me whether a change made things faster or slower, because I never click the same way twice.

I don't have a good answer for that yet. It's the next thing I want to fix, and I expect it'll be its own post. For now my rule is that something works when I've run it and seen it work, whatever the agent says.

Getting a second agent to review

When a change is done, a fresh agent reviews it. It has no memory of the conversation that produced the code, and I ask it to find what's wrong with the diff, which is the list of lines the change adds and removes.

This helped more than I expected. The agent that just wrote four hundred lines is a poor judge of them, and it'll happily agree with whatever I suggest.

WHO SHOULD REVIEW THE DIFF The agent that wrote it Agrees with you, warmly It just wrote four hundred lines. It is the worst possible judge of whether they are any good. A fresh agent, no memory Clone to dodge the borrow checker Allocation in a per-pixel loop An unwrap on user input A lock held during slow work An unasked-for refactor
The fresh reviewer finds the same kinds of problems again and again. Most of them are about wasted work, and few are outright bugs.

Those five come up over and over. Rust's compiler has a set of rules about who owns each piece of memory, and making a copy (a "clone") is the easiest way to get it to stop complaining. On a 60 MB image in a function that runs every frame, that copy is a real problem, and it still compiles without a warning. Asking for new memory inside a loop that runs once per pixel looks reasonable until it runs twenty million times. And holding a lock, which stops other threads from touching some data, while doing slow work is how a background task ends up freezing the interface.

Then I read the diff myself. I'm mostly not going to catch what the reviewer missed. I do it because reading those diffs is how I ended up learning Rust.

Keeping releases dull on purpose

A release is a version bump, a build for each platform, and the site updated with the new build. It's scripted, and it's safe to run twice by accident. I care about keeping it that easy. If a release takes concentration, I'll put it off, and then I'm sitting on a month of work nobody can use.

Stage What I hand over Done when
Plan A paragraph and a list I can state what would prove it
Build One task, with its scope It compiles and the app still runs
Test The proof from the plan I saw it, on a real folder
Review The diff, to an agent that didn't write it Findings are fixed or consciously declined
Release A version number The build on the site is the build I tested

Four ways this goes wrong

I don't want to make this sound smoother than it is. Here's where it goes wrong for me.

Fixing symptoms. One symptom gets fixed, another appears, and the agent cheerfully patches that one too. Four rounds in, the code is full of special-case checks and nobody has said out loud what's actually wrong.

WHEN AN AGENT FIXES SYMPTOMS Symptom chasing Symptom Guard Symptom Guard Symptom Guard Four rounds in, nobody has said what is actually wrong. What works instead Reproduce it Name the cause One fix
What works for me is to stop, reproduce the problem, and have the agent tell me the cause before it changes any code.

Confident numbers that were never measured. An agent once told me a change made something "roughly three times faster" when it hadn't run anything at all. If I didn't see a number get measured, I treat it as a guess.

Code that is correct and wasteful. Rust's compiler is strict, and that made me more confident than I should have been. It checks that your code isn't doing anything unsafe with memory. It doesn't care at all whether you just copied fifty megabytes for no reason. The whole point of this app is speed, so I have to watch for that myself, and it's why a change has to pass a speed check before I accept it.

The change keeps growing. Left alone, a change picks up extra work nobody asked for. Writing down what's out of scope in the plan stops most of it, and reading the diff catches the rest.

So how did I actually learn Rust?

I didn't learn it from a book or by writing an app from scratch. Four things did it, roughly in order of how much they taught me.

A few weeks in, I can read all of the Rust in LightPhotos and write most of it. Reading is the part that matters most for the way I work.

An agent can always produce more code than I can write. What it can't do is tell me whether that code is any good. I have to be able to judge that myself if I want the app I'm trying to build, and not just one that works.