buenomini

A learning framework to fight AI brain rot

How I'm learning Rust and Zig in parallel without letting AI do the learning for me: knowledge bases, project logs and a healthy amount of suffering.

10 min readlearning

I want to be a better engineer, the kind who understands what's happening underneath the code they ship. Someone like Tom.

This kind of comes from my own scare. A few months ago I realized I didn't understand almost anything in my current project, even though I had onboarded without AI like a year ago. But around then my company made a deal with an AI company and we were allowed to use as much AI for coding as we wanted (not only allowed, actually encouraged), and I got to the point where I asked AI to do config change PRs like "please enable this feature in QA". A literal one-liner.

When I realized I could not code on my own, I got scared. And this is the result of that scare.

# AI is amazing, but it's rotting our brains

AI is incredible for learning, when used properly. The problem is that "properly" is a narrow and fuzzy window. Cross the line and you're not learning anymore, you're just putting together code you don't understand. Stack Overflow copy-paste on steroids, tailored to your every need (at least with Stack Overflow you had to think about what to Google and read a bit, AI pushes the code at you and you don't even have to read it).

What AI should be, IMO:

  • A knowledge base you can query
  • A fact-checker for your understanding ("Is this how Rust's borrow checker works?"), and not the other way around
  • A rubber duck for reasoning through problems (maybe, I'm split on this one)
  • A documentation pointer ("Where do I find info about Zig's allocators?")

What AI should NOT do:

  • Write code
  • Debug your application. It's yours, debug it yourself
  • Explain documentation too much. Reading documentation is one of the most valuable skills an engineer has, maybe second only to reading code
  • Save you from suffering and struggling when something isn't working

Maybe controversial, but the suffering is kind of the point. When you're stuck on an error for an hour, reading docs and trying different approaches, that's when the learning happens, and believe me, after that you won't forget what the issue was. AI that "fixes it for you" takes that away.

At some point I wanted to stop using AI for learning entirely, but that isn't great either. It's a tool, and the best way I've found so far to use this hammer without every problem becoming a nail is to use AI as read-only. It doesn't write my code, debug my programs or solve my problems, it's just a fact-checker/knowledge-base machine. It can't access my code and it can't tell me what to do.

# Finding the right project

When you're learning something new, the project you pick matters more than you'd think.

Don't try to invent something. You (probably) won't. And even if you do, you'll spend more time on product features than on learning, weeks on UI, auth and webhooks, and you never get to the deep programming concepts you wanted to learn. Who cares about the UI if your goal is to learn how something works under the hood. I get wanting to feel like you're making something someone else could use, to justify the effort, but if you want to learn that's the wrong goal. Learning is the goal. People using your app isn't (and they probably won't).

Don't try to fill a gap either, that leads to massive feature creep. "Oh, but this other tool doesn't have X"... and now you're building features instead of learning fundamentals.

Rebuild something that already exists. It sounds counterintuitive, but build a text editor or a terminal emulator. A key-value store. Redis already exists, so does Vim, so does every terminal. But you haven't built them. Whatever you make will be worse than what exists, and that's fine, it actually takes away a lot of the distractions of building something, like deciding what it should do. If you're writing your own terminal, you know exactly what it should do.

For one of my projects I picked an encrypted in-memory key-value store (in both Rust and Zig, more on that later). I get to learn:

  • Memory management (how do hash maps work?)
  • Network and I/O multiplexing (epoll/kqueue, event loops)
  • Wire protocols (how clients and servers talk and serialize the data)
  • Encryption (how do you encrypt data at rest? Can I have a fully encrypted data store without client-side encryption and an obfuscated KV store?)
  • Systems programming concepts without the feature noise

The scope is clear: GET, SET, DELETE, EXISTS. Four commands. No lists, no pub/sub.

If you're stuck for ideas, check out CodeCrafters (I haven't used their service, but the project ideas are solid).

# The value of exploration

Learning a new language "just because" is underrated. You don't have to be that guy who wants to start every project in the most obscure, new, non-adopted, beta language that doesn't fit the use case at all. You might never use Zig professionally, or even again after this project, but you'll still take something with you.

Different languages encode different philosophies:

  • Zig says: "Keep it simple, give the programmer full control, trust them to manage memory correctly (which you probably won't)"
  • Rust says: "Fuck you, you know nothing. I'm better than you (which is also probably true)"
  • Javascript says: "i NeED a sUePErSet tO Be KIndA usEFul"

They're different approaches to the same problem, and learning them makes your brain more malleable. You stop thinking in one paradigm and start seeing tradeoffs, so when you hit a problem you have more tools to reach for: "oh, this is like Zig's defer pattern" or "this would be cleaner with Rust's Result type". Even if you never touch these languages again you carry that forward. You understand why memory safety matters, you recognize the cost of abstraction (and its power when it's done right), and when manual control is worth the hassle and the risk.

I think exploration is one of the ways to form somewhat educated opinions about different ways to solve problems.

# Learning in parallel

There's a problem with language comparisons. Ideally you build something in language A, then rebuild it in language B, but the second time you already know the solution, so you implement it better, faster and with fewer mistakes. Unfair comparison.

I wanted to compare Zig and Rust fairly, so I'm building the same encrypted key-value store in both languages, module by module, in parallel. First I built my own hash map implementation in Zig, then in Rust. After both were done I moved to the wire protocol (serialization and deserialization), first in Rust and then in Zig. Right now I'm powering through my own event loop for I/O multiplexing in Zig, which will probably be orders of magnitude worse than any library, but again, that's not the point.

It's painful, there's a lot of context switching and you're maybe pushing too much information into your brain. Slow down. Don't try to finish the project quickly, do the thing you actually set out to do, which is a fair 1:1 comparison.

What I've learned so far (2 modules in):

Module 1 in Zig: the language got out of my way, but I spent hours learning about memory. How does Zig's HashMap work internally? (It stores pointers instead of copying the data.) What is a use-after-free vulnerability, how do you test for memory leaks.

Module 1 in Rust was the opposite. I didn't think about memory management at all and focused on the type system instead. String vs &str, why do I need .clone() everywhere?

Module 2 in Zig: I found a subtle memory leak in my serializer. Some responses returned string literals ("OK"), others used allocPrint(), which allocates, and the caller didn't know which responses needed freeing. In Zig you discover and document these patterns yourself.

Module 2 in Rust: enums with data are amazing. The type system made the parser clean and safe.

So both teach systems programming, just different parts of it. Zig makes you understand memory management deeply, Rust makes you understand ownership and borrowing deeply. Doing them in parallel keeps me from saying "language A is better" without understanding why each one makes the tradeoffs it does.

Before I started I was bullish on Zig and scared of Rust. So far I haven't hit any complexity in Rust (no async stuff yet), and borrowing and ownership were actually pretty easy after everything I learned about memory while preparing for Zig. Right now I'm more bullish on Rust than Zig (I still find Zig amazing, and so far there's space for both in my heart).

# Project logs and knowledge bases

This is the core of the framework, inspired by the Zettelkasten method (see Sönke Ahrens "How to Take Smart Notes" if you want the philosophy behind it). I keep two files for each project.

knowledge-base.md has concepts explained in my own words. Nothing copy-pasted from documentation, if I can't explain it simply I don't understand it.

Example entry:

### Zig HashMap memory management

What HashMap allocates:
  - Internal structure: buckets, metadata, hash table arrays
  - Uses the allocator you pass at init()
  - map.deinit() frees only this internal structure

What HashMap does NOT allocate:
  - The actual key/value data you pass to put()
  - It stores slices (pointer + length) pointing to YOUR memory
  - Does NOT copy the string data

Memory ownership problem:
  - HashMap stores pointers to wherever you allocated keys/values
  - If that memory gets freed (e.g., arena deinit), hashmap has dangling pointers
  - Use-after-free = undefined behavior

I would've missed this if I'd just asked AI "how does HashMap work?"

project-log.md is the raw record of the learning process.

Example entry:

### 2025-11-14 - Zig's Real Difficulty: Understanding Memory, Not the Language

After implementing the Storage module, I realized Zig itself is not hard. Systems programming is hard.

The language syntax is simple and clean. But using it requires deeply understanding:
- How memory works (stack vs heap)
- What pointers actually are vs string literals
- How HashMap works internally (it doesn't copy data, just stores pointers!)
- Manual lifetime tracking (all in your head)
- Use-after-free, double-free, dangling pointer

A bit more to convince you:

  1. Writing is learning. Explaining a concept in your own words forces you to understand it.
  2. Future reference. When I forget how HashMap works I don't re-google, I read my own explanation.
  3. Blog material. The raw logs become blog posts later, with the real struggles still in them.
  4. Pattern recognition. Looking back I can see where I struggled and where things clicked.

The Zettelkasten method says notes should be written as if you're teaching someone else, "here's how X works and why it matters" instead of "I learned X today", and that shift forces you to be clear. I highly recommend the book. It's easy to read and it doesn't spoon-feed you how to take your own notes, it just tells you how some prolific academics do it, and why, and it's up to you what you take from it and what you ignore (unlike a lot of productivity and self-improvement books that sell you a magic recipe).

# Where I am now

I'm 2 modules into a 7-module project, and the framework is working. My knowledge base has grown organically, with entries on what a process actually is, IPC, memory management, wire protocols, encryption, concurrency models, different async strategies, and Zig/Rust-specific patterns. I could already start writing blog posts about it, but I don't have to, whenever I feel like it the notes will be there (the posts basically write themselves, you just pull a thread through the different notes).

The point is learning as much as possible per unit of time, finished project or not. So if you're learning something new, try this:

  1. Pick a project to rebuild (not invent)
  2. Create knowledge-base.md and project-log.md
  3. Write in your own words
  4. Use AI as a read-only resource or not at all
  5. Embrace the suffering