Latest Posts (20 found)
Unsung Today

“Some say it sounds like an alto saxophone.”

I witnessed this Siemens locomotive depart yesterday and for a second I thought I was losing my mind. Then, I smiled so hard: = 2x) and (width >= 700px)" srcset="https://unsung.aresluna.org/_media/some-say-it-sounds-like-an-alto-saxophone/yt1.2096w.avif" type="image/avif"> = 3x) or (width >= 700px)" srcset="https://unsung.aresluna.org/_media/some-say-it-sounds-like-an-alto-saxophone/yt1.1600w.avif" type="image/avif"> Turns out, the startup melody was intentional in this particular model. The power converters have to adapt the current from the overhead line to convert it to the three-phase motors of the locomotive, and that generates a rising tone. The engineers decided to change the logic to increment the tone in precise few steps resembling a musical scale, rather than allowing it to rise continuously. I debated whether to include this on Unsung. I guess it is software, even if it’s attached to the hardest of hardware. And sure, it’s “just” delightful, but it is still kind of nice to see someone go extra, adding a human touch atop a technical process that had to happen anyway. But then, it reminded me of something. No, not the poor CSIRAC trying (and similarly struggling) to become a musician. Rather, a “musical road” built in Lancaster, California, where the engineers messed up the execution, creating a truly unpleasant, atonal melody. David Simmons-Duffin wrote a fun essay in 2008 analyzing the “bug” thoroughly, including useful visuals, and even replicating the problem. Subsequently, Tom Scott visited the road and made a video about it ten years later. It won’t surprise you that the cause of the bug was bad hand-off between designers and engineers, but there can be no software patch for grooves you cut in asphalt – and so at least as of last year , the embarrassingly sounding road was still there. I think I prefer my out-of-tune musical scale performed by a train. Given it’s easy to find compilation videos of Siemens locomotives booting up , it seems I’m not alone. #bugs #real world #sound design #youtube

0 views
Unsung Today

Shallow breathing

Turns out that the breathing light survives, sort of, not really, in an Apple product today: The AirPods Pro case does this when charging – right at the start, or when you tap it later. But it disappears after a while, the pace is now 28 breaths a second (over twice as fast as the original iteration), and the light is orange. Is it still the same thing, reflecting on how smaller organisms breathe faster? Or is it mostly an unrelated idea, with the light fading in and out indicating activity rather than lack of it? My money is on the latter – the light turns white when pairing, too, and it cycles even faster then – but it was nice to imagine the return of the old feature for a second or two… or 2.1, to be precise. #apple #details #hardware #motion design

0 views

PHP's Oddities

I've been coding in PHP at work for the last 5 years. My org's entire backend is written in PHP—a decision made in 2007 when the company first started. It's not a language I ever imagined myself using prior to working there, but life takes you in all sorts of directions you don't expect. PHP gets a bad rep in the industry, despite being a mature and commonly used language . But it's mostly based on out-of-date understanding of what PHP can do. Recent versions have caught up with most other languages in terms of features; by this point it's a pretty versatile general-purpose language. Certainly not just for serving HTML, as it was originally designed. I'm no longer working at the aforementioned company, so I'm reflecting on my experience with PHP after all these years and there's some things I've always found odd about it. And more than just odd, some of it's language features are really unintuitive and have been prone to cause bugs. This comes from personal experience and many previous headaches at work. I'll explain two of the biggest offenders in this post—in short: PHP's standard library basically only has one data structure: the . This was intentional; it was designed to be a general-purpose, flexible data structure that can cover a variety of use cases. It's technically an ordered key-value dictionary , not an array in the traditional sense . Unfortunately, with flexibility comes complexity. If you want to create a collection of fixed-size objects in an allocated memory block, you can't really do that. PHP pretends to support them, but the illusion breaks down in unexpected ways. Let's say I have a bunch of fruits. PHP let's me define a fruits "array" and I can do normal array things with it. Everything looks fine but you get into trouble whenever you perform a mutation on this "simple" array; it will be exposed as being a key-value store. When you use one of PHP's built-in functions for standard array operations like sorting or filtering, it will operate on the keys AND values of your array. If it mutates the array in-place or by a return value, the key order will likely become inconsistent. why can't I hold all these indices??? The only way to put these arrays back into a naturally indexed state is to use the function. You just have to know that, or else you end up with subtle bugs. It's just strange to me that PHP doesn't support simple collections of objects. It's annoying to have to manage these arbitrary numeric keys when all you really want is ordinal indexing like 99% of the time. It feels like a leaky abstraction. In PHP5, a native type system was added to the language. It was expanded over time and by PHP7 you could define the types for your class's properties. Although PHP is a scripting language, type declarations will help catch bugs during testing, or even during development with the use of static analysis tools like PHPStan . But PHP's type system has some quirks since it was built on an existing dynamically typed language. The rules had to be designed after the behaviour was already there. For class properties, there's a hidden uninitialized state that can pop up if you're not careful. Let's define a class with three properties: Here, I'm illustrating all the ways of declaring the type for a string property: Before PHP7, all class properties were (1): untyped. Since the type system is optional, it has to live alongside the "legacy" behaviour which has weird consequences. For example, what do you think the values of these three properties will be after we instantiate a object? Trick question! Only the untyped property will have a value, and that value is . That seems fine and is roughly in line with how I'd expect a language to use a value. But the other two properties will NOT have a value because they don't exist, or rather they could exist but haven't been initialized yet. This example exposes the "uninitialized" state that a property can be in, which is NOT the same as . This distinction frustratingly comes up when you try to do a null check on these properties: Not a warning—a FATAL error occurs if you try to access an uninitialized property. This comes up a lot in cases where you try to deserialize data into a PHP object. If a field's data isn't present you might not initialize the property at all. ahh yes, NULL...who was that by again? This lax behaviour for property definitions makes writing code around them harder. Especially when you take into account that any object can have properties dynamically added to them: So I feel like the class property type system does little to help you understand what a given object is composed of, and in some respects has made it less clear because it's introduced this new uninitialized state. As a developer, it's hard to write defensive code because you're never sure which checks to do for all these situations: , (), , ... it's not obvious which functions cover which states. I'd argue that uninitialized did not need to be a state at all. For nullable typed properties, just default them to the way untyped properties are. And for non-nullable types, require them to be be defined as constructor promoted parameters OR require a default value at declaration. Similar requirements already exist for the attribute, so it's certainly feasible for the PHP execution engine to enforce it. But there's probably some nuance or historical reason I'm missing here. Let me know in the comments if you know. Despite all the critiquing I've done in this article, I still think the amount of hate PHP gets is undeserved. Like any language, it has it's quirks and tradeoffs, but you can still accomplish any task using PHP that you could in another language. The more you know about a language, the better you can structure things to work "with the grain" and write more idiomatic code. Some things I do enjoy about PHP: Thanks for reading! Arrays are weird and overloaded The type system is clunky It's a string It's nullable string It's a scripting language, so development friction is low. Make a file change and it instantly takes effect. Laravel is a solid web framework with tons of extensible functionality. It's opinionated and definitely leans into the "auto-magical" framework style, but it was designed well so you don't mind. All the $ signs help remind you what you're doing it all for at the end of the day 🤑

0 views

May 2026 blend of links

Forgive the higher-than-usual rate of direct quotes from these links, which replace a few regular comments, but as you can see, there is a general theme in my recent readings. Even though I’m trying to avoid focusing on it in these monthly collections of links, the theme is so rich, so complex and consequential (and fascinating in many ways), that I’m still not really sure what to think about all this and what I can add to these excellent takes. Your CEO is suffering from A.I. psychosis, by Jake Handy – “ An agent without a spec is a random text generator with a budget. ” A lot of quotable and relatable parts in this excellent, insightful column. The Majority A.I. View, by Anil Dash – “ One of the reasons we don't hear about this most popular, moderate view on A.I. within the tech industry is because people are afraid to say it. Mid-level managers and individual workers who know this is the common-sense view on A.I. are concerned that simply saying that they think A.I. is a normal technology like any other, and should be subject to the same critiques and controls, and be viewed with the same skepticism and care, fear for their careers. ” The Rise of the Bullshittery, by マリウス (Marius) – “ A few thoughts on how the modern economy has stopped rewarding people who know what they are doing, and started rewarding people who know how to look like they do. ” Do I belong in tech anymore? by Ky Decker – “ What I’ve gained from A.I. is a deeper appreciation for human communication, in all its messy imperfection. ” (via Kottke ) Your A.I. Use Is Breaking My Brain, by Jason Koebler – “ Our brains are now performing untold numbers of calculations per day: Is this A.I.? Do I care if it’s A.I.? Why does this sound or look or read so weird? Does this person just write like this? Is this a person at all? ” Craft is Untouchable, by Christopher Butler – “ And that’s the risk with collapsing skills into tools. I won’t always be there to do the thing I do. Inferior designs will ship. That’s bad. But what’s worse—the thing that really stings most designers’ egos—is that most people won’t even notice. ” Software as the Product of Obsession Times Voice, by John Gruber – “ It’s one thing to make something poorly designed and shrug on the grounds that it doesn’t matter. It’s another thing to make something poorly designed and hold it up as good design. ” “The Biggest Android Update Ever”, by MKBHD – Reading the comments section of this video — which I usually avoid doing at all costs on YouTube — was very revealing of the world we live in: companies pushing A.I. everywhere to please investors, but a sizeable number of users from the general public seem to be genuinely annoyed by it. I also really like this new-ish column-like video format from MKBHD (Marques, if you’re reading this, please consider adding a good old blog next to your YouTube channels and podcast?) Hyperduck, by Sindre Sorhus – Another excellent little utility from Sindre Sorhus: this one forces me to use open tabs on my Mac as my read-later list, rather than saving the link somewhere, only to forget about it. (via Loren Stephens ) Patricia, I Went on Holiday – Every now and then, I listen to Patricia and their specific bass-deep techno music that sounds like nothing else I know (my fave being Sick Day ). This song, not featured on an album, is great, but the fan-made video is very well made and it’s a nice window into the early 90s. I do need to go on holiday.

0 views

Maintainability sensors for coding agents

In her recent article about harness engineering for coding agent users, Birgitta Böckeler laid out a mental model for expanding a coding agent harness: a system of guides and sensors that increase the probability of good agent outputs and enable self-correction before issues reach human eyes. Birgitta has now started publishing an article where she walks though her experiences using sensors to keep a codebase maintainable. This part looks at static analysis with basic code linting.

0 views
Unsung Today

“If you just ignore those pesky impossible details, the demo looks deceptively simple.”

This DOS demo called Wake Up! is astonishingly small – only 16 bytes: = 2x) and (width >= 700px)" srcset="https://unsung.aresluna.org/_media/if-you-just-ignore-those-pesky-impossible-details-the-demo-looks-deceptively-simple/yt1.2096w.avif" type="image/avif"> = 3x) or (width >= 700px)" srcset="https://unsung.aresluna.org/_media/if-you-just-ignore-those-pesky-impossible-details-the-demo-looks-deceptively-simple/yt1.1600w.avif" type="image/avif"> The demo doesn’t just make QUOD feel gargantuan. Output this one solitary emoji, “Woman Technologist with Light Skin Tone” – 👩🏻‍💻 – and you spent all your 16 bytes, too. ( Proof !) The creator’s write-up is a bit hard to follow, but there are some interesting aspects to it: “stealing” the beauty from math itself, the reliance on the environment being set up properly (to avoid wasting precious bytes on initialization), and the tight connection between the hardware, the visuals, and the sound. Oh yeah, in case you haven’t noticed, this has sound! Two out of 16 bytes are devoted to its production, using an existing BIOS function that slots nicely into the existing graphics routine. This is another recent demo that caught my attention: NINE , from about a year ago: = 2x) and (width >= 700px)" srcset="https://unsung.aresluna.org/_media/if-you-just-ignore-those-pesky-impossible-details-the-demo-looks-deceptively-simple/yt2.2096w.avif" type="image/avif"> = 3x) or (width >= 700px)" srcset="https://unsung.aresluna.org/_media/if-you-just-ignore-those-pesky-impossible-details-the-demo-looks-deceptively-simple/yt2.1600w.avif" type="image/avif"> The platform here is a computer of a similar vintage as the early DOS machines, Commodore 64. C64, like many other home microcomputers, supported special graphical entities called “ sprites ,” which were used for gaming since the rest of the graphics couldn’t move very fast. (Today, your mouse pointer is conceptually similar to a sprite, being imbued with special powers unavailable to anything else.) C64 could output up to 8 such sprites. The demo inexplicably has… nine. The NINE demo didn’t focus on absolute minimalism, but instead employed a barrage of ghostly (and ghastly!) trickery to achieve something that was thought impossible. This time around, the explainer from the author – a 22-minute YouTube video – is filled with great storytelling, and absolutely worth a watch: = 2x) and (width >= 700px)" srcset="https://unsung.aresluna.org/_media/if-you-just-ignore-those-pesky-impossible-details-the-demo-looks-deceptively-simple/yt3.2096w.avif" type="image/avif"> = 3x) or (width >= 700px)" srcset="https://unsung.aresluna.org/_media/if-you-just-ignore-those-pesky-impossible-details-the-demo-looks-deceptively-simple/yt3.1600w.avif" type="image/avif"> I think both of these showcase two things that I appreciate and that translate to great UX design as well. The first demo shows tight integration between design and the capabilities of software and hardware. Let’s pick the sound routine that needed just 2 bytes. If there wasn’t a way to output sound within this extremely tight budget, the author likely wouldn’t fight to their death to get sound… they would instead focus on what else was possible within two bytes. This is getting as close to full understanding of the medium you’re working in as possible. The second demo highlights how sometimes you can use absolutely horrid sleights of hand to achieve something beautiful – and how you can perhaps find beauty in those sleights of hand, too. It reminds me of the quote attributed to Teller (of Penn & Teller): Sometimes magic is just someone spending more time on something than anyone else might reasonably expect. Penn & Teller talk a lot about how there are only two keys to their success: going further than others would think, and not worrying about employing inelegant tricks in service of something that would appear to be of utmost elegance. Today’s computing limitations are different than the ones from the 1980s. But a lot of this attitude can still be helpful, even four decades in, and even if your work seems as far away from the demoscene as you can imagine. #graphics #hacks #youtube

0 views

AppGoblin App Ecosystem Report 2026 Q1

The 2026 Q1 App Ecosystem Report is here with a special section for those attending MAU in Vegas this week. Ad Networks were led by Verve once again after its strong Q4 2025, with other notable breakouts from Snap Inc. , TaurusX , adjoe , and Moloco . Business Tools were led by small but super fast growing Luciq . PayPal also posted strong mobile growth, while emerging companies like AppHarbr stood out. In attribution analytics, growth was broadly healthy across the category and was led by Tenjin . Open source product analytics platform Matomo also looked great heading into 2026. One notable absence from the growth list was AppsFlyer , which has historically been one of the category’s largest and most consistent performers but saw a small down tick in tracked market share. For Development Tools, Divkit posted solid growth. The framework launched in 2025 and is backed by Yandex . Report is totally free and the raw data is available as a free dataset download for the top 1000 app companies / ad domains to see their quarter-over-quarter 2026 Q1 growth: https://appgoblin.info/reports/app-ecosystem-report-Q1-2026

0 views

Alternatives for the EDIT tool of LLM agents

EDIT: of course this was already done in the past! I had little doubts but people just confirmed me about it on Twitter :) But, keep reading: the CRC32 compromise at the end is an interesting tradeoff, and this is a good discussion to have in general. Right now I'm working to an agent for my DS4 project. Local inference is token-poor, it's a battlefield where optimizations count. I was quite surprised by the fact the EDIT tool everybody is using right now forces the LLM to emit the old version of the text verbatim. This CAS (check and set) mode of operation, where I say EDIT old="foo" new="bar", is needed because there are often colliding edits (the user is editing as well, or checked out a different branch, and so forth) and because the LLM can just hallucinate that a given line had a given content. This means, basically, that just using line numbers is very fragile: to say, change line 22 with new="foobar" is not good. Yet I don't want my local LLM to throw away tokens rewriting the old text each time, also because certain times the old text has a lot of special chars and spaces that the model may get wrong; in this case the tool would fail, forcing the LLM to do the same edit again. So I (re)designed a tag-based EDIT tool that is still CAS style, but more tokens efficient. The READ and SEARCH tools return something like that: 10:Q8fA int count = 10; 11:rA3_ if (count > limit) { 12:Kq9z count = limit; 13:PX0b } So there are line numbers and tags. The tag is 4 chars, on average 2.5 LLM tokens, representing a checksum of the line. Now the LLM can edit like this: { "tool": "edit", "path": "/tmp/example.c", "line": 10, "tag": "Q8fA", "new": "int count = 11;" } Or, multi line, like this: { "tool": "edit", "path": "/tmp/example.c", "lines": "11:rA3_\n12:Kq9z\n13:PX0b", "new": "if (count > limit)\n return limit;" } The saving is significant especially when the agent is deleting big amounts of text, but also in the general case. However, there is some overhead due to the fact we have line numbers and tags. There are potential tradeoffs, maybe the tag should be 8 chars and include the line number in the hash, there is to check exactly collisions possibilities and tokenization to see how much this is a win, but I like the line:tag format as later the LLM is often able to exploit the line information in many ways, like to get ranges in successive tool calls. Maybe there are other ways to exploit the tag, too, like: is this line still dj4_? The interesting thing is that DeepSeek v4 Flash is able to use this tool in a very effective way, so apparently it is natural for it. And while I did't measure the exact savings I saw in the field that edits are much faster and even more reliable. The alternative to this is to return just the whole file CRC32 each time (basically the tag becomes a file tag, even for partial reads). So that we can only work with line numbers + CRC, the edit would just specify 11,12,13,14. Less tokens, of course. This forces, however, to recompute the CRC32 of the file each time, but for reasonably sized files this is cheap enough. But this approach has limits: we fail the edit even if *unrelated* changes happened, so there is a strong tradeoff at play. To be fair to the whole file method, there is to say that it allows to specify ranges as 10:23, which is a huge win. I have the feeling I can decide which is better only with enough practical evidence, by using ds4-agent over multiple sessions with the two systems. For now maybe a command line to switch the edit mode is the first right step to do. Comments

0 views
Unsung Today

“Accents are an opportunity, not a burden.”

The iOS 26 update introduced a bug in the Czech keyboard. Instead of the customary háček (ǍǎĚěǦǧǏǐǑǒǓǔY̌y̌) in the bottom row, another key was duplicated, removing access to the accent character (or, a diacritic ) very popular in that language. Here is the before and after of this situation: = 3x)" srcset="https://unsung.aresluna.org/_media/accents-are-an-opportunity-not-a-burden/1-framed.1600w.avif" type="image/avif"> = 3x)" srcset="https://unsung.aresluna.org/_media/accents-are-an-opportunity-not-a-burden/2-framed.1600w.avif" type="image/avif"> Ordinarily, this can be frustrating but not insurmountable; you can always copy/​paste, rely on autocorrect to help out, or even add some topical text replacements for common phrases. The problem is that this bug only appeared on the keyboard used for logging on, and at least a few people used that character in their password. There, none of these workarounds were available – and so those people were now completely locked out of their iPhones. The Register reported on this on April 12 , and a few days later suggested that Apple was working on a fix. I won’t keep you in suspense; I just verified that the fix landed with the recent May 11 update. This is, in an of itself, not a fascinating story, but with interesting things to talk about at its periphery. First of all, The Register never showed a single screenshot. This led to a lot of confusion and speculation in the comments. Turns out, screenshots are valuable not just with bug reporting, but also with bug reporting . Second, check out this Czech keyboard. Even within the limitations of the ancient QWERTY, there’s a lot of cool stuff happening here. Two new accented keys just appear on the top layer when you switch to Czech. Both have magical properties, too. They’re the modern “ dead keys ” that either stand alone, or get combined with the previous letter if that makes sense. This is the stuff typewriters, and even desktop keyboards, could only dream of. But, as always, more software means more bugs, including some with unforeseen consequences; a typewriter could never break this way. Thirdly, there is this interesting tension between us being led to believe “more interesting passwords are safer,” but then sometimes being penalized for actually making them interesting. A decade ago someone used emoji in their password without realizing they won’t be able to input it, and I’m sure there were other examples. But the most interesting, to me, part? It’s the diacritic itself. Under one of the posts, a commenter wrote: Stick with the 7-bit ASCII subset. You will never go wrong. 7-bit ASCII basically means “26 Western letters and nothing else.” I hate this. I know it’s objectively true – in the late 1980s I felt a sense of relief my name didn’t have any of Polish language’s nine diacritics, which would complicate my life. Even just yesterday in Germany, I spotted this: = 2x) and (width >= 700px)" srcset="https://unsung.aresluna.org/_media/accents-are-an-opportunity-not-a-burden/4.2096w.avif" type="image/avif"> = 3x) or (width >= 700px)" srcset="https://unsung.aresluna.org/_media/accents-are-an-opportunity-not-a-burden/4.1600w.avif" type="image/avif"> = 2x) and (width >= 700px)" srcset="https://unsung.aresluna.org/_media/accents-are-an-opportunity-not-a-burden/5.2096w.avif" type="image/avif"> = 3x) or (width >= 700px)" srcset="https://unsung.aresluna.org/_media/accents-are-an-opportunity-not-a-burden/5.1600w.avif" type="image/avif"> Software still struggles beyond ASCII. But this is why we need to keep pushing. Diacritical characters are to be found everywhere in the world. They’re detailed, and varied, and filled with histories. Umlaut is not diaeresis . Kreska is not the acute. A háček is not a breve. They’re rarely optional decoration, and often not even decoration at all; learning about Turkish dotless i might completely upend your understanding of what’s an accent and what is not. If you don’t have a favourite diacritic , you are missing out. Even the names – grave! ogonek! horn! – are beautiful. (Háček is also known as caron and a wedge depending on context, and in other regions referred to with beautiful words kvačica and strešica.) If you’re interested, here is David J. Ross’s 22-minute talk about getting to love diacritics from the perspective of a type designer. It’s filled with craft and playfulness: = 2x) and (width >= 700px)" srcset="https://unsung.aresluna.org/_media/accents-are-an-opportunity-not-a-burden/yt1.2096w.avif" type="image/avif"> = 3x) or (width >= 700px)" srcset="https://unsung.aresluna.org/_media/accents-are-an-opportunity-not-a-burden/yt1.1600w.avif" type="image/avif"> My favourite accent is, obviously, ogonek. Just looking at Adam Twardoch’s guide on how it should be drawn fills my heart with joy: = 2x) and (width >= 700px)" srcset="https://unsung.aresluna.org/_media/accents-are-an-opportunity-not-a-burden/6.2096w.avif" type="image/avif"> = 3x) or (width >= 700px)" srcset="https://unsung.aresluna.org/_media/accents-are-an-opportunity-not-a-burden/6.1600w.avif" type="image/avif"> #bugs #david jonathan ross #localization #security #typography #youtube

0 views

The last six months in LLMs in five minutes

I put together these annotated slides from my five minute lightning talk at PyCon US 2026, using the latest iteration of my annotated presentation tool . I presented this lightning talk at PyCon US 2026, attempting to summarize the last six months of developments in LLMs in five minutes. Six months is a pretty convenient time period to cover, because it captures what I've been calling the November 2025 inflection point . November was a critical month in LLMs, especially for coding. For one thing, the supposedly "best" model (depending mostly on vibes) changed hands five times between the three big providers. As always, I'm using my Generate an SVG of a pelican riding a bicycle test to help illustrate the differences between the models. Why this test? Because pelicans are hard to draw, bicycles are hard to draw, pelicans can't ride bicycles ... and there's zero chance any AI lab would train a model for such a ridiculous task. At the start of November the widely acknowledged "best" model was Claude Sonnet 4.5, released on 29th September . It drew me this pelican. In November it was overtaken by GPT-5.1 , then Gemini 3 , then GPT-5.1 Codex Max , and then Anthropic took the crown back again with Claude Opus 4.5 . I think Gemini 3 drew the best pelican out of this lot, but pelicans aren't everything. Most practitioners will agree that Opus 4.5 held the crown for the next couple of months. It took a little while for this to become clear, but the real news from November was that the coding agents got good . OpenAI and Anthropic had spent most of 2025 running Reinforcement Learning from Verifiable Rewards to increase the quality of code written by their models, especially when paired up with their Codex and Claude Code agent harnesses. In November the results of this work became apparent. Coding agents went from often-work to mostly-work, crossing a quality barrier where you could use them as a daily-driver to get real work done, without needing to spend most of your time fixing their stupid mistakes. Also in November, this happened - the first commit to an obscure (back then) repo called "Warelay" by some guy called Pete. Over the holiday period, from December to January, a whole lot of us took advantage of the break to have a poke at these new models and coding agents and see what they could do. They could do a lot! Some of us got a little bit over-excited. I had my own short-lived bout of a form of LLM psychosis as I started spinning up wildly ambitious projects to see how far I could push them. One of my projects was a vibe-coded implementation of JavaScript in Python - a loose port of MicroQuickJS - which I called micro-javascript . You can try it out in your browser in this playground . That playground demo shows JavaScript code run using my micro-javascript library, in Python, running inside Pyodide, running in WebAssembly, running in JavaScript, running in a browser! It's pretty cool! But did anyone out there need a buggy, slow, insecure half-baked implementation of JavaScript in Python? They did not. I have quite a few other projects from that holiday period that I have since quietly retired! On to February. Remember that Warelay project that had its first commit at the end of November? In December and January it had gone through quite a few name changes ... and by February it was taking the world by storm under its final name, OpenClaw . The amount of attention it got is pretty astonishing for a project that was less than three months old. OpenClaw is a "personal AI assistant", and we actually got a generic term for these, based on NanoClaw and ZeroClaw and suchlike... they're called Claws . Mac Minis started to sell out around Silicon Valley, because people were buying them to run their Claws. Drew Breunig joked to me that this is because they're the new digital pets, and a Mac Mini is the perfect aquarium for your Claw. My favourite metaphor for Claws is Alfred Molina's Doc Ock in the 2004 movie Spider-Man 2. His claws were powered by AI, and were perfectly safe provided nothing damaged his inhibitor chip... after which they turned evil and took over. Also in February: Gemini 3.1 Pro came out, and drew me a really good pelican riding a bicycle . Look at this! It's even got a fish in its basket. And then Google's Jeff Dean tweeted this video of an animated pelican riding a bicycle, plus a frog on a penny-farthing and a giraffe driving a tiny car and an ostrich on roller skates and a turtle kickflipping a skateboard and a dachshund driving a stretch limousine. So maybe the AI labs have been paying attention after all! A lot of stuff happened just in the past month. Google released the Gemma 4 series of models, which are the most capable open weight models I've seen from a US company. Also last month, Chinese AI lab GLM came out with GLM-5.1 - an open weight 1.5TB monster! This is a very effective model... if you can afford the hardware to run it. GLM-5.1 drew me this very competent pelican on a bicycle. ... though when it tried to animate it the bicycle bounced off into the top and the bicycle got warped. Charles on Bluesky suggested I try it with a North Virginia Opossum on an E-scooter And it did this! I've tried this on other models and they don't even come close. "Cruising the commonwealth since dusk" is perfect. It's animated too . The other neat Chinese open weight models in April came from Qwen. Qwen3.6-35B-A3B on my laptop drew me a better pelican than Claude Opus 4.7 . That's a 20.9GB open weights model that runs on my laptop! (I think this mainly demonstrates that the pelican on the bicycle has firmly exceeded its limits as a useful benchmark.) Here's that Claude Sonnet 4.5 pelican from September for comparison. So those were the two main themes of the past six months. The coding agents got really good... and the laptop-available models, while a lot weaker than the frontier, have started wildly outperforming expectations. You are only seeing the long-form articles from my blog. Subscribe to /atom/everything/ to get all of my posts, or take a look at my other subscription options .

0 views
Blargh Yesterday

Everything in C is undefined behavior

If he had been a programmer, Cardinal Richelieu would have said “Give me six lines written by the hand of the most expert C programmer in the world, and I will find enough in them to trigger undefined behavior”. Nobody can write correct C, or C++. And I say that as someone who’s written C and C++ on an almost daily basis for about 30 years. I listen to C++ podcasts. I watch C++ conference talks. I enjoy reading and writing C++. C++ has served us well, but it’s 2026, and the environment of 1985 (C++) or 1972 (C) is not the environment of today. I’m definitely not the first to say this. I remember reading a post by someone prominent about a decade ago saying that a good case can be made that use of C++ is a SOX violation. And while I was not onboard with the rest of their rant (nor their confusion about “its” vs “it’s”), I never disagreed about that point. With time I found it to be more and more true. WAY more things are undefined behavior (UB) than you’d expect. Everyone knows that double-free, use after free, accessing outside the bounds of an object (e.g. array), and accessing uninitialized memory is UB. After all, C/C++ is not a memory safe language. And yet we as an industry seem to be unable to stop making even those mistakes over and over. But there’s more. More subtle. More illogical. Some people seem to think that as long as they don’t compile with optimizations turned on, undefined behavior can’t hurt them. They believe that the compiler is somehow being deliberately hostile, going “AHA! UB! I can do whatever I want here!”, and without optimizations turned on it won’t. This is incorrect. UB doesn’t mean that the compiler can take advantage of your sloppiness. UB means that the compiler can assume that your code is valid. It means that the intention of your code that’s oh so obvious when read by a human, doesn’t even have a way to be expressed between compiler stages or modules. UB means that the compiler doesn’t even have to implement some special cases in its code generation, because they “can’t happen”. The compiler, and really the underlying hardware too, is playing a game of telephone with your UB intentions. It may end up with what you wanted, but there’s no guarantee for now or in the future. The following is not an attempt at enumerating all the UB in the world. It’s merely making the case that UB is everywhere, and if nobody can do it right, how is it even fair to blame the programmer? My point is that ALL nontrivial C/C++ code has UB. As an example of this, take this code: If this function is called with a pointer not correctly aligned (probably meaning on an address that’s a multiple of , but who knows), this is UB. C23 6.3.2.3. On Linux Alpha, in some cases this would merely trap to the kernel, which would software emulate what you intended. In other cases it would (probably) crash your program with a SIGBUS. On SPARC it would cause a SIGBUS. Sure, on x86/amd64 (henceforth just “x86”) this is likely fine. Hell, it’s probably even an atomic read. x86 is famously extremely forgiving about cache coherency subtleties. So here we have three cases: What about ARM, RISC-V, and others? What about future architectures? A future architecture could even have special that do not populate the lowest bits, because such pointers cannot exist. Even if it works, maybe the compiler one day changes from using one load instruction to another, and suddenly that’s no longer fixed up by the kernel. Because the compiler is not obligated to generate assembly instructions that work on unaligned pointers . Because it’s UB. Or how about this: Is this operation atomic when the object is not correctly aligned? That’s the wrong question to ask. Mu , unask the question. It’s UB. (but also yes, in practice this can easily be an atomicity problem) If you want to get even more convinced, you can try thinking about what happens if an object you thought you were reading atomically spans pages . But don’t think too much about it, or you may conclude that “it’s fine”. It’s not. It’s UB. Don’t blame the function, above. The act of dereferencing the pointer wasn’t the problem. Merely creating the pointer was enough to be a problem. That cast is the problem, not . It’s perfectly valid for the compiler to assign specific meaning, such as garbage collection or security tagging bits, to the lower bits of an . is a simple function that takes a character and returns if it’s a hex digit. 0-9 or a-f. It can also take the value . Uh, ok. What value is ? Per C23 7.4p1 we know it’s an , and we can infer that it’s not representable by . therefore takes an , not a . All values of fit inside , so we should be fine. Casting from to fits, so per section 6.3.1.3 we’re fine, right? No. Because if is called with a value other than 0-127, and on your architecture is (implementation defined, per 6.2.5, paragraph 20 in C23 ), then the integer value ends up negative. And the following is a valid implementation of , that would cause a read of who-knows-what memory. It could even be I/O mapped memory, triggering things to happen that is more than merely getting a random value or crash. It could cause the motor to start. Less likely in an application running in a desktop operating system than in an embedded system, sure. But there are user space network drivers (for performance), so even user space won’t protect you. And, by omission, it’s also UB if the float is a non-finite value. So how do you compare a float to ? Do you cast the float to ? No, that’s the UB you want to avoid. So you cast to float? How do you know it can be represented exactly? Maybe casting to rounds to a value not representable in , and your comparison becomes non-representative? Maybe the following works? You’ll miss out on representing some really high values, but maybe that’s OK? I just wanted to convert a float to an int. :-( I bet there’s lots of code out there that take a value in seconds, and convert it to integer milliseconds, by just multiplying and casting. Most programmers won’t have to deal with this, but I don’t think there’s any C standards compliant way in practice to put an object at address zero. This can come up in OS kernel and embedded coding. By 6.3.2.3 an integer constant zero (which is convertible to a pointer) and are the “null pointer constant” (which I’ll just call ). C doesn’t specify that the actual pointer points addr machine address zero , because the C standard only talks of the C abstract machine, not about hardware. All C guarantees is that if you compare to zero you’ll see them equal. But for all you know that’s because the zero is converted to the native platform’s , which happens to be . It also explicitly says that dereferencing a null pointer, no matter what the value, is undefined behavior. It’s the example of UB under 3.4.3. This also means that you can’t assume that will create a pointer! You cannot initialize your structs this way and assume member pointers are ! And this does apply to most programmers. And yes, some historic machines used non-zero NULL pointers . But let’s say you have a modern machine, where is a pointer to address zero, and you actually have an object there. Again, C 6.3.2.3 says that compares unequal to “any object or function”. So this is UB: C says “there is no function there”. For all you know the compiler has no internal way to even express your intention here. You may argue that “but surely it’ll just emit a call instruction to the bit pattern of all zeroes? Nothing else seems reasonable. What is “all zeroes”, though? On 16bit x86, is it ? Is it ? This is UB: This is not: Because the argument needs to be a pointer, and the macro may be misinterpreted as an integer zero. Similarly, this is UB: It needs to be: So how do you print an ? Well, you could cast them to and print them using . But is even unsigned? Oh well, worst case you get a nonsense value printed instead of , I guess. Sure, you probably knew this. But did you consider the security aspects of it? It’s not rare for the denominator to come from untrusted input. And there’s so much more. The C23 standard contains 283 uses of the word “undefined”. And that’s not even including the things that are undefined by omission. Nobody can find integer promotion rules and code skimming speeds. Nobody . This post is already long enough, but as a start: Point an LLM at ANY C code, asking it to find UB, and it will. And it’ll be right almost all the time, nowadays. I felt a bit bad after it correctly found ones in my code, so I thought I’d point it at the mature and pedantically written OpenBSD. I just picked the first tool I could think of, , and it spit out a bunch. I sent the project a patch for an out of bounds write (and also for a non-UB logic bug ). I didn’t send them patches for the UB that was left and right, partly because the OpenBSD project has not been very receptive in the past for bug reports, my sense of “this is probably fine, in practice”, and that if OpenBSD wants to weed out UB from their code base, then that’s a major project that should be done in a better way than me just being the middle man between the LLM and them for a patch here and there. We can’t just throw away our C/C++ code bases. But leaving them inherently broken is also not an option. We need some way of fixing UB at scale, without committing AI slop nor overwhelming human reviewers. This too is not a new opinion, nor a great revelation. But yes, writing C/C++ in 2026 without an LLM supervising you for UB should probably be seen as a SOX violation, and just plain irresponsible. If OpenBSD people can’t find these problems gives 30+ years, what chance do the rest of us have? It may not scale to large code bases, but for my own projects I’ve asked the LLM to find UB, if necessary explain it, and fix it. And then stare at the output until I can confirm the issue and the fix. A problem with this is that in order to confirm the findings, you’ll need an expert human. But generally expert humans are busy doing other things. This is janitor work, but too subtle to leave to the junior programmers who have traditionally been assigned janitor work. kernel gave a helping hand (Alpha for some loads) crash (other Alpha loads, and SPARC) not a problem (x86) No way to parse integers in C Integer handling is broken UB in the Linux kernel Integer promotion

0 views
Corrode Yesterday

Migrating from Go to Rust

Out of all the migrations I help teams with, Go to Rust is a bit of an outlier. It’s not a question of “is Rust faster?” or “does Rust have types?”, Go already gets you most of the way there. The discussion is mostly about correctness guarantees , runtime tradeoffs , and developer ergonomics . A quick disclaimer before we start: this guide is heavily backend-focused . Backend services are where Go is strongest, small static binaries, a standard library focused on networking, and an ecosystem of libraries for HTTP servers, gRPC, databases, etc. That’s also where most teams considering Rust are coming from (at least the ones who reach out to me), so I think that’s the comparison that’s actually useful in practice. If you’re writing CLI tools, embedded firmware, or game engines, some of this still applies, but to be honest, I’m afraid this is not the best resource for you. For context, I’ve written about Go and Rust before: “Go vs Rust? Choose Go.” back in 2017, and later the “Rust vs Go: A Hands-On Comparison” with the Shuttle team, which walks through a small backend service in both languages. What you will learn in this article I’ll be upfront: I’m not a fan of Go. I think it’s a badly designed language, even if a very successful one. It confuses easiness with simplicity , and several of its core design tradeoffs ( everywhere, error handling as a discipline rule rather than a type, the long absence of generics) point in a direction I disagree with. That said, success matters! Go has captured a real and persistent share of working developers, hovering around 17–19% in the JetBrains Developer Ecosystem Survey. Rust is growing steadily but is still a smaller slice: Go is clearly working for a lot of people, and a guide that pretends otherwise isn’t helpful. So I’ll do my very best to be objective in this guide rather than relitigate old arguments. But you should know my priors so you can calibrate. The other prior worth disclosing: I run a Rust consultancy; of course I’m biased! More people using Rust is good for my business. But I’ve also worked in both languages professionally and shipped Go services to production. This guide is for Go developers who want an honest, side-by-side look at what changes when you move to Rust. For a deliberately opposite take, I recommend reading “Just Fucking Use Go” by Blain Smith. Holding both views in your head at once is more useful than either one alone. If you prefer to watch rather than read, here’s a video from the Shuttle article above, read and commented by the Primeagen: Go developers already have one of the cleanest toolchains in the industry. Back in the day, it started off a trend of “batteries included” toolchains that give you a single, consistent interface for building, testing, formatting, linting, and managing dependencies. I’m glad that Rust followed suit, because it’s a great model. It’s one of my favorite parts about both ecosystems. has even more built-in: The big difference is that in Go you typically reach for third-party tools ( , , , ) to fill gaps. In Rust, the first-party ecosystem covers more out of the box. Things that do require external crates (e.g. , ) install with one command and feel native, e.g. gives you right away. Both communities have converged on the same insight about formatters: a single canonical style, even an imperfect one, is worth more than the bikeshedding it eliminates. Gofmt’s style is no one’s favorite, yet gofmt is everyone’s favorite. — Rob Pike, Go Proverbs The same is true of : not everyone likes every detail, but the absence of style debates in code review is worth far more than the occasional formatting preference you’d have made differently. The headline is that Go and Rust are both compiled, statically typed, single-binary-deploy languages with strong concurrency stories. The differences are about what guarantees you get from the compiler and how much control you have over runtime behaviour . Go developers don’t usually come to Rust because Go is “too slow.” For most backend workloads, Go is plenty fast. People are generally a bit frustrated with Go’s verbose error handling, the danger of segmentation faults from pointers, and the lack of generics (for a long time) or any sophisticated type system features, such as enums or traits. Interfaces are not a worthy replacement for traits, and the Go standard library has some weird gaps, such as the lack of a type. I call it my billion-dollar mistake. It was the invention of the null reference in 1965 … This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years. — Tony Hoare, inventor of , QCon London 2009 This is the one I hear most often. You ship a Go service, it runs fine for months, and then one Tuesday at 3 a.m. a code path runs where someone forgot to check whether a pointer was , and the goroutine panics. Go’s compiler does not force you to consider the absence case. Rust’s does: You literally cannot dereference an without acknowledging the case. Whole categories of pager-duty incidents disappear. is a great tool, but it’s a runtime detector, it only finds races that actually execute during your tests. Mutating a map from two goroutines without a lock compiles fine in Go and only blows up in production under load. In Rust, sharing mutable state across threads requires types that implement and . Try to share a plain between threads and the program does not compile . You’re forced to wrap it in an , an , or use a channel. That race condition becomes a type error. 1 is fine for a while. After a few years, you notice three things: It’s worth being honest about the counter-argument here, since it came up in the Lobste.rs thread on my Shuttle article: experienced Go developers point out that and catch most of the “forgot to handle the error” cases in practice, and that explicit is easier to read than dense chains. Both points are fair, and the explicit style is a deliberate cultural value, not an accident: I think that error handling should be explicit, this should be a core value of the language. — Peter Bourgon, GoTime #91 , quoted in Dave Cheney’s Zen of Go My take is that lints are an opt-in safety net you have to remember to set up, while Rust’s is the type signature itself, there’s no way to forget. The boilerplate-vs-readability tradeoff is more genuinely subjective. The operator handles propagation; handles wrapping; and a on is exhaustively checked . Add a new variant tomorrow and the compiler shows you every place that needs updating. Go got generics in 1.18, and they’re useful, but the implementation has constraints (no methods with type parameters, GC shape stenciling, occasional surprising performance characteristics). Rust generics monomorphize, each instantiation produces specialized code with zero runtime cost. Combined with traits, this gives you real zero-cost abstractions. This matters less in handler code and more in shared infrastructure (middleware, generic repositories, decoders, parsers), where Go often pushes you back to / plus type assertions. Go’s GC is excellent, concurrent, low-pause, well-tuned for typical service workloads. But “low-pause” is not “no-pause.” Under heavy allocation, P99 latency tails are noticeably worse than a Rust equivalent that simply doesn’t allocate on the hot path. I won’t oversell this, for the vast majority of services, Go’s GC is a non-issue. But for latency-sensitive systems (trading, real-time bidding, network proxies, high-throughput ingestion), the lack of GC pauses is a genuine selling point. Go is death by a thousand paper cuts. It is a very pragmatic language and if you are willing to glance over the above issues, you can be very productive in it. But at a certain codebase size, the problems start to compound. There is no single moment when Go loses its appeal, but teams find themselves wishing for more (more safety, more control, more expressiveness) and that’s when they start looking around for alternatives. The fastest way to feel comfortable in Rust is to map patterns you already know. For a longer, fully-worked example of building the same backend service in both languages, see the Shuttle comparison , the section below focuses on the patterns that come up most often. The operator does the dance for you, including type conversion if is implemented (idiomatic with ’s ). There is no in safe Rust. References can’t be null. Pointers can be, but you almost never use raw pointers in application code. Go’s interfaces are structural, a type satisfies an interface implicitly: Rust’s traits are nominal, you implement them explicitly: The Go style is great for ad-hoc duck typing. The Rust style is great for refactoring and discoverability, you can grep for every implementer of a trait. The closest equivalent of / in Rust is , but you almost never want it. The Go community knows the cost of reaching for too: interface{} says nothing. — Rob Pike, Go Proverbs Generic functions with trait bounds ( ) cover the vast majority of cases and give you monomorphization with no runtime dispatch. Where Go pre-1.18 would have forced you back to plus a type assertion, Rust’s traits + generics let you stay specific. When you do want runtime dispatch (e.g. heterogeneous storage of different implementers), reach for or . That’s the direct Rust analog of holding an value in Go. Go’s concurrency model is famously simple: Goroutines are cheap, the runtime schedules them across OS threads, and channels ( ) are the primary coordination primitive. The Go proverb captures the philosophy: Don’t communicate by sharing memory; share memory by communicating. — Rob Pike, Go Proverbs This is the area where Go genuinely shines, several commenters in the Lobste.rs discussion made the point that goroutines “just disappear” into normal-looking blocking code, and that’s worth giving Go credit for. Rust async is more powerful, but it’s also more visible in your code. Rust uses / on top of an executor (almost always for backend services): The shape is similar. The differences: For most backend code, the day-to-day feel is similar: spawn a task, communicate via channels, use timeouts liberally. In Go, you plumb a through every blocking call: Rust has no built-in . The closest equivalent for cancellation is : For timeouts, wraps any future. For deadlines/values, you typically pass them as explicit arguments or via spans rather than a single context object. Some Go developers miss the implicit-feel of . In practice, the explicit Rust style is easier to reason about, you always know exactly what’s cancellable and what isn’t. The deeper point is that neither language gives you cancellation for free, the discipline just shows up at different layers: Go doesn’t have a way to tell a goroutine to exit. There is no stop or kill function, for good reason. If we cannot command a goroutine to stop, we must instead ask it, politely. — Dave Cheney, The Zen of Go In Go that “asking politely” is a plumbed through every call site by convention. In Rust it’s a (or a channel) plumbed through every call site, but the compiler can actually tell you when you forgot. Both languages have channels. The translation is direct: Rust’s channels distinguish sender and receiver as separate types, which makes ownership and -ness explicit at the type level. Rust’s is the equivalent of a Go value receiver; is a pointer receiver with mutation. Owned (consuming the value) has no Go analog and is occasionally very useful (typestate, builders). Go’s is a UTF-8 byte slice with copy-on-assign semantics (the header is copied, the underlying bytes are shared and immutable). Rust splits this into two types: As a rule of thumb, take in arguments, return when you produce new data. This is mostly painless once you internalize it. The vs split is a microcosm of Rust’s broader “borrow vs own” model. Go got generics in 1.18 (March 2022), thirteen years after the language shipped. They are useful, but they feel tacked on, and in practice they have most of the downsides of a generic type system without delivering the upsides you’d expect coming from Rust, Haskell, or even modern C++. This is a strong claim, so let me back it up. The most telling signal is that three years after generics landed, Go’s own standard library still mostly avoids them. still takes a closure instead of a constraint. is still typed as / . The generic helpers that do exist live in a small handful of packages: , , , and a few entries under . Compare that to Rust, where generics permeate the standard library from day one: , , , , , / , , , every collection, every smart pointer. You cannot write idiomatic Rust without using generics, because the standard library is generic. In Go, generics are an opt-in feature for library authors who really need them. In Rust, they’re the substrate everything else is built on. Rust’s generics are tied to traits, which double as the language’s mechanism for ad-hoc polymorphism, supertraits, associated types, blanket impls, and coherence. Go’s constraints are just interfaces with an extra operator for type-set membership. There are no: The practical consequence is that the moment your abstraction needs more than “a function that works for any with these few operations,” Go pushes you back to plus type assertions, code generation, or runtime reflection. Rust uses a Hindley-Milner-style inference engine that propagates type information through entire expressions, including across closures, iterator chains, and operators. You routinely write: and the compiler figures out is from the range, and is from the target. Go’s inference is much shallower. It can usually infer type parameters from function arguments, but it cannot infer from return-position context , cannot chain inference through generic builders the way Rust does, and frequently forces explicit type arguments at call sites: In Rust this is the exception; in Go it’s still common. Rust monomorphizes: every and produces specialized machine code with zero runtime dispatch. Go uses GCShape stenciling with dictionaries , where types that share a “GC shape” share the same compiled function and dispatch through a dictionary at runtime. The result is a compile-time/runtime tradeoff that often surprises people: generic Go code can be measurably slower than the equivalent hand-written non-generic version, because every method call on a type parameter goes through an indirection. There’s a well-known PlanetScale post showing exactly this. In Rust, generic code is the fast path. Reaching for (the equivalent of Go’s interface dispatch) is a deliberate choice you make when you want runtime polymorphism. This is the part that bothers me most. A good generics system removes reasons to fall back to escape hatches. In Rust, generics + traits eliminate most of what you’d otherwise need or runtime reflection for. The type system gets stronger. In Go, generics did not remove , did not remove , did not remove code generation as the dominant pattern for things like ORMs, decoders, and mocks. still uses reflection. still uses . still generates code. The places where a real generics system would shine are the same places Go reaches for runtime mechanisms it had before 1.18. Generics in Go feel additive, a new tool in the box that’s useful in narrow cases. Generics in Rust feel foundational; remove them and the language collapses. That’s the difference, and it’s why generic Go code, in my experience, doesn’t read better than the -based code it replaced; it just reads differently, with more punctuation. If you’re already opinionated in Go, the Rust ecosystem has converged to a similar level of “default picks.” For a typical backend service: + + + + + covers 90% of what you need. I want to be straightforward here. Coming from Go, you will hit a wall . The wall has a name. Go’s runtime handles memory and aliasing for you. Rust pushes that decision into the type system. The first few weeks you’ll write code that “should obviously work” and the compiler will refuse it. The patterns that bite Go developers most often: With all of these rules, the borrow checker truly sounds like a “gatekeeper” of sorts, which keeps getting in the way and is just overall frustrating to deal with. That is not the mental mindset you should have when learning Rust. The borrow checker truly uncovers real and very existing bugs in your code, and if you don’t address them, your program will deal with safety issues. So whenever you get a compiler error from , take a step back and think how your code could break. A few questions you can ask yourself: That is the mindset you need to understand the borrow checker. Humans are genuinely bad at reasoning about memory. We forget that pointers can be null, that old references can outlive the data they point to, and that multiple threads can touch the same data at the same time. We tend to have a “linear” mental model of how data flows through a program, but in reality it’s closer to a complex graph with many paths and interactions. Every condition forces you to consider what happens in both branches. Every loop forces you to consider what happens on every iteration. That is exactly the kind of reasoning the borrow checker is designed to do for you! It enforces best practices at compile time, and it can feel annoying when your own mental model disagrees with the borrow checker’s (which is the more accurate one 99% of the time). There are cases where the borrow checker is genuinely too strict, but they are rare, and as a beginner you’ll almost never run into them. I got memory management wrong plenty of times in my early days, but I approached it with a learner’s mindset , which helped me ask “what’s wrong with my code?” instead of “what’s wrong with the compiler?”, a reaction I see a lot in trainings. The good news is that once you internalize borrowing, it stops fighting you. Most experienced Rust developers will tell you the borrow checker became an ally somewhere between weeks 4 and 12. The first month is the hardest. Be honest with your team, Rust compile times are a real downgrade from Go’s. A clean release build of a medium service can take minutes in comparison to Go’s near-instantaneous compiles. Incremental builds and are reasonable and compile times have gotten much better over the years, but you’ll feel the difference. To mitigate, use in your edit loop, split into a workspace once it pays off, and keep proc-macro-heavy crates in their own crate so they only recompile when they change. See tips for faster Rust compile times for a deeper dive. Go’s “one type of function, sync everywhere, the runtime handles concurrency” is genuinely simpler than Rust’s split between and . You’ll need to think about which of your functions are async, where you , and how that interacts with traits. Async traits (stable since Rust 1.75) help a lot, but there are still rough edges (especially around with async methods). Rust’s crate ecosystem is growing and libraries are high-quality across the board, but Go has a head start in some backend-adjacent domains: Kubernetes operators, cloud-provider SDKs, database drivers for certain niche stores. Before you commit, spend a day checking that the libraries you depend on have Rust equivalents you’re willing to use. Teams I help often have to hand-roll at least one or two core libraries themselves. For example, they might have to update an abandoned crate for XML schema validation, or write their own client for a lesser-known protocol. You don’t have to rewrite everything in one go. The strategies that work best, in order of how I usually recommend them: If one specific service in your fleet is the perpetual problem child (high CPU, latency-sensitive, or constantly hit with reliability issues), rewrite just that one in Rust, behind the same API contract. This is the lowest-risk migration. Other Go services keep talking to it via HTTP/gRPC, oblivious to the underlying language. Background workers, queue consumers, ingestion pipelines, and CPU-bound batch jobs are excellent first targets. They typically have a clear input/output boundary (a queue, a topic) and no shared in-process state with the rest of the system. You can call Rust from Go via cgo, and there are good guides on how to do it . (Reach out if you’d be interested in a guide on this from me.) In practice, I rarely recommend it for backend services. The build complexity and FFI overhead usually outweigh the benefits compared to “just stand up a Rust service and put it behind a network call.” For libraries and CLI tools, it’s more viable. If you have an API gateway or reverse proxy, you can route specific endpoints to a new Rust service while the rest stays in Go. This works particularly well when one bounded context (auth, search, billing) is the right unit to migrate. The pattern is often called “strangler fig,” because the new service grows around the old one until it eventually replaces it entirely. Start with a service that has a clear boundary. Don’t pick the most central, most-deployed service in your fleet. Pick the one where the contract with the rest of the system is well-defined and the blast radius is small. Keep the same API contract. If your Go service exposes a REST API, your Rust service should too: same paths, same JSON shapes, same error envelope. The migration is invisible to clients, and you can swap traffic incrementally with a gateway. Don’t translate idioms verbatim. Resist the urge to write Go-flavoured Rust. becomes . Goroutine-per-request becomes only when you actually need it (axum already concurrently handles requests). Interfaces with one method usually become trait bounds on a generic, not . Use the compiler as a pair programmer. Rust’s compiler errors are usually pretty good. Read them slowly. They almost always tell you the right answer. The team members who struggle longest are the ones who fight the compiler instead of treating it as a collaborator. Invest in training early. I’ve seen teams try to do a Rust migration “on the side,” learning as they go. It rarely ends well. It’s a bit like training for a marathon by signing up for the race and then trying to run it without any prior training. You can do it, but it’s going to be painful and you might not finish. Block off real time for learning: a workshop, an online course , paired sessions on real code. The upfront investment pays back many times over once the team is fluent. (Hey, if you want to talk about training options, I’m happy to chat .) Not everything should be migrated. Go is excellent for: A hybrid strategy is fine and common. Many of the teams I work with end up with a polyglot backend: Go for the “boring” services, Rust for the ones where reliability and performance pay back the extra effort. Numbers vary wildly by workload, so take these as rough guidance. Not promises! But here are some ballpark numbers, based on Go-to-Rust migrations I’ve helped with: Honestly, you’re unlikely to get a 10x throughput improvement going from Go to Rust the way you might from Python. What you get is fewer “silly errors” and flatter latency tails, plus the ability to expand into other domains like embedded development or systems programming while still using the same language. That’s often the most surprising side-effect of a migration: there’s a lot of opportunity for code-sharing across teams that previously had to use different stacks. You can use Rust for everything. Going from Go to Rust is a different kind of migration than coming from Python or TypeScript . Coming from Go, you know the benefits of a statically-typed, compiled language. So you’re not trading away dynamic typing or a slow runtime, you’re trading away in exchange for a more robust codebase with fewer footguns, and a stricter compiler that catches more mistakes at compile time. There is a steeper learning curve, however. For foundational services (services that your organization relies on, that have high uptime requirements, that are critical to your business), that trade is obviously worth it. For others, Go remains the right answer. The point of a migration is to put each problem in the language that solves it best. Ready to Make the Move to Rust? I help backend teams evaluate, plan, and execute Go-to-Rust migrations. Whether you need an architecture review, training, or hands-on help porting a critical service, let’s talk about your needs . Rust’s type system doesn’t catch all data races, but types that truly can’t be shared between threads without synchronization won’t compile. You can still have logic bugs in your synchronization, but you won’t have the kind of “oh no, I forgot to lock this” that often leads to silent data corruption. ↩ Where Go and Rust overlap, and where they diverge. How Go patterns map to Rust. What you gain from the borrow checker. Where I tell people to keep Go and where Rust is worth the migration cost. How to migrate Go services incrementally. The boilerplate dilutes the actual logic of your function. Wrapping with is a discipline rule, not a compiler rule. It’s easy to drop context on the floor. Sentinel errors via / work, but the compiler doesn’t tell you when you forgot to handle a new variant. Rust async functions return s. They don’t run until awaited or spawned. The compiler tracks / across points. If you hold a non- value across an await, you get a compile error explaining exactly why. There’s no built-in goroutine-style preemption. Long CPU-bound work in an async task starves the executor; you offload to or instead. Channels ( , , ) are first-class but live in libraries, not the language. , owned, heap-allocated, growable. Equivalent to you intend to mutate. , a borrowed view into someone else’s string data. Equivalent to a Go parameter most of the time. Supertraits / constraint hierarchies. In Rust you write , and any automatically satisfies and . Go has no equivalent; you stack interface embeddings, but the constraint solver doesn’t reason about hierarchies the way Rust’s trait system does. Associated types. Rust’s has , so is a first-class thing you can name in bounds. Go’s closest equivalent is a second type parameter, which leaks into every signature. Blanket impls. In Rust, automatically gives every type a method. Go has no way to add methods to a type from outside its defining package, generic or not. Methods with their own type parameters. This is an explicit, documented non-feature in Go. You cannot write . In Rust, generic methods on generic types are routine. Long-lived references. In Go, you’d happily hold a from a map for as long as you want. In Rust, that borrow blocks mutation of the map for its whole lifetime. The fix is usually to clone, or to scope the borrow tighter. Self-referential structs. Common in Go (a struct holding both data and an iterator over it). In Rust, this requires , , or a redesign. Almost always: redesign. Sharing mutable state across goroutines. What you’d write as becomes . Slightly more verbose, much more checked. Returning references from functions. Lifetime annotations show up. They’re not as bad as their reputation, but they’re new. If a value got moved from one place to another, what would happen if the original place tried to use it again? If a value is shared across threads, what would happen if one thread modified it while another thread is using it? If a pointer is dereferenced , what would happen if it was null or dangling? When a value goes out of scope , what would happen if it was still being used somewhere else? Kubernetes-native tooling : operators, controllers, CRDs. The ecosystem is overwhelmingly in Go. CLI utilities and dev tooling : fast compiles, easy cross-compilation, simple deployment. Glue services : thin API layers, proxies, format converters. The boilerplate ratio in Rust isn’t worth it here. Anywhere your team velocity matters more than absolute correctness guarantees . CPU usage: 20–60% reduction. Less dramatic than Python-to-Rust, because Go is already efficient. The wins come from no GC and tighter loops. Memory: 30–50% reduction, mostly from the absence of GC overhead and a smaller runtime. P99 latency: significantly more consistent. Rust services tend to flatline where Go services have visible GC-induced jitter. (This has gotten much better on the Go-side ever since they introduced their low-latency GC, but the difference is still there under heavy load.) Production incidents: this is the one teams report most enthusiastically. The classes of bugs that survive and reach production (data races, nil dereferences, missed error paths) just don’t compile in Rust. Oncall rotations are typically very boring after a Rust migration. Rust’s type system doesn’t catch all data races, but types that truly can’t be shared between threads without synchronization won’t compile. You can still have logic bugs in your synchronization, but you won’t have the kind of “oh no, I forgot to lock this” that often leads to silent data corruption. ↩

0 views
Ivan Sagalaev Yesterday

Shoppy

Meet Shoppy ! It's a helper app for my recently revived shopping list , with which I'm hoping to grow the dataset for categories prediction. In fact, even early beta tests have made Shoppy significantly more savvy about alcoholic drinks (the initial data comes from my own shopping, and my entire family happens to be non-drinkers). See if you can confuse it about something it doesn't know! But besides that, there's a few deeper philosophical and technical notes I wanted to share. It's a very, very simple Django app . When I first had the idea to build it I entertained some thoughts about trying some front-end based technology, because, you know, it's an "app"… But then after actually thinking about what it's going to be — a handful of static screens and a couple of forms — I decided to go the familiar way. Now I have a small, view-source 'able HTML app which I'm proud to offer as an example of how you can build something interactive without the layers of modern front-end technology. If you're new here, simplicity is kind of my thing in software engineering. Although it's really hard to convince people to do simple. Trying modern CSS after a long break felt really exciting! Nested blocks, variables, complete control over the box model, new useful units (like ), and niceties like — all of these made my life much simpler. I was especially impressed with which allowed me to make speech and form bubbles flexible. Without it, trying to make text of variable length look nice in a fixed-size bubble caused me a lot of frustration. For layout, I tried flexbox and grid, but they didn't really work for me. It's my own fault, really. You see, ever since I bought into the idea of separating the roles of markup and style, I dislike adding extra structure to markup purely for styling convenience. Markup needs to mean something! And the one thing that grids and flexboxes really like is having straightforward container s with stuff inside of them. But what I have is a which consists of naked , , and , in this order — and that's just not enough structure to say "this goes here, and that goes there". So I ended up with good old absolute positioning and some paddings around Shoppy's avatar. CSS variables really do shine for things like this. And! It was my first time making a responsive layout that looks nice both on mobile and desktop! Tell me if something is broken on your particular setup. The model is a mapping from "terms" to categories . I learned to build such things while working on the Search team at Shutterstock, and their simplicity still amazes me! Here's how it works: You get a search query, like "Honeycrisp apples". You split it into words, stem them and sort them, which gives you — a predictable set of keys independent of morphology and the input order (they're called unigrams). Then you generate all two-word combinations (called bigrams) from this set, which in this case gives you just , and add them to unigrams. And then you look up each of the search terms in the dataset and pick the entry that comes the earliest. In this case, there's only one: . But there's a few non-obvious tricks it lets you do: You don't need to list all the apple varieties, unknown words are simply ignored, and you just recognize any apple as produce. But what of "apple juice"? For that it has an entry , which is deliberately placed before the apples, so it gets picked up instead. In fact, what it means is that "any kind of juice is a drink, regardless of what it's made of". Same goes for "oat milk " (drink), " diced tomatoes" (canned products), etc. Now think of "apple sauce". "Apple" is produce, "sauce" is (usually) a condiment. But "apple sauce" is a snack! This is where bigrams come into play: the bigram entry comes before both and , which resolves the conundrum. (In fact, all of the bigrams must come before all the unigrams, because they're always more specific.) There's some more to it all, and there are downsides, but I won't go any deeper right now. It's 2026, so I can't not talk about it, can I? Generative AI happened to the world right in between of me first coming up with the idea of category prediction and having a chance to actually implement it. And I admit of having thoughts that may be there's no point in building your own model for such a thing now. After all, just ask any LLM "which grocery category is dill weed" and it will tell you… a lot of text with several variants, which you can't really use in a precise manner :-) So of course I went back to my own idea, because it's much, much simpler. And local. And free. And ethical. Luckily, the simpler solution doesn't really lose on feeling magical and intelligent. I've seen people play with the app and really engage with it, and be impressed! One of the testers, when trying to come up with a random grocery item for the first time, said, "There's probably a million of them!" It doesn't matter that my entire model is just around 500 entries, it still feels like it knows much more simply because people overestimate the size of the problem :-) You see, I can process photos, I can do business graphics, and I'm known to have put together a few toolbar icons in my time… but for the life of me I can't draw! And even if I could, I'm particularly hopeless at coming up with what to draw. So I commissioned the graphics from an artist , who also introduced me to the concept of "object shows" and the whole OSC fandom . Not sure I'm joining as a fan yet, but I'm definitely very happy with the original character of Shoppy! Oh, and the background. You get a search query, like "Honeycrisp apples". You split it into words, stem them and sort them, which gives you — a predictable set of keys independent of morphology and the input order (they're called unigrams). Then you generate all two-word combinations (called bigrams) from this set, which in this case gives you just , and add them to unigrams. And then you look up each of the search terms in the dataset and pick the entry that comes the earliest. In this case, there's only one: . You don't need to list all the apple varieties, unknown words are simply ignored, and you just recognize any apple as produce. But what of "apple juice"? For that it has an entry , which is deliberately placed before the apples, so it gets picked up instead. In fact, what it means is that "any kind of juice is a drink, regardless of what it's made of". Same goes for "oat milk " (drink), " diced tomatoes" (canned products), etc. Now think of "apple sauce". "Apple" is produce, "sauce" is (usually) a condiment. But "apple sauce" is a snack! This is where bigrams come into play: the bigram entry comes before both and , which resolves the conundrum. (In fact, all of the bigrams must come before all the unigrams, because they're always more specific.)

0 views

CISA Admin Leaked AWS GovCloud Keys on Github

Until this past weekend, a contractor for the Cybersecurity & Infrastructure Security Agency (CISA) maintained a public GitHub repository that exposed credentials to several highly privileged AWS GovCloud accounts and a large number of internal CISA systems. Security experts said the public archive included files detailing how CISA builds, tests and deploys software internally, and that it represents one of the most egregious government data leaks in recent history. On May 15, KrebsOnSecurity heard from Guillaume Valadon , a researcher with the security firm GitGuardian . Valadon’s company constantly scans public code repositories at GitHub and elsewhere for exposed secrets, automatically alerting the offending accounts of any apparent sensitive data exposures. Valadon said he reached out because the owner in this case wasn’t responding and the information exposed was highly sensitive. A redacted screenshot of the now-defunct “Private CISA” repository maintained by a CISA contractor. The GitHub repository that Valadon flagged was named “ Private-CISA ,” and it harbored a vast number of internal CISA/DHS credentials and files, including cloud keys, tokens, plaintext passwords, logs and other sensitive CISA assets. Valadon said the exposed CISA credentials represent a textbook example of poor security hygiene, noting that the commit logs in the offending GitHub account show that the CISA administrator disabled the default setting in GitHub that blocks users from publishing SSH keys or other secrets in public code repositories. “Passwords stored in plain text in a csv, backups in git, explicit commands to disable GitHub secrets detection feature,” Valadon wrote in an email. “I honestly believed that it was all fake before analyzing the content deeper. This is indeed the worst leak that I’ve witnessed in my career. It is obviously an individual’s mistake, but I believe that it might reveal internal practices.” One of the exposed files, titled “importantAWStokens,” included the administrative credentials to three Amazon AWS GovCloud servers. Another file exposed in their public GitHub repository — “AWS-Workspace-Firefox-Passwords.csv” — listed plaintext usernames and passwords for dozens of internal CISA systems. According to Caturegli, those systems included one called “LZ-DSO,” which appears short for “Landing Zone DevSecOps,” the agency’s secure code development environment. Philippe Caturegli , founder of the security consultancy Seralys , said he tested the AWS keys only to see whether they were still valid and to determine which internal systems the exposed accounts could access. Caturegli said the GitHub account that exposed the CISA secrets exhibits a pattern consistent with an individual operator using the repository as a working scratchpad or synchronization mechanism rather than a curated project repository. “The use of both a CISA-associated email address and a personal email address suggests the repository may have been used across differently configured environments,” Caturegli observed. “The available Git metadata alone does not prove which endpoint or device was used.” The Private CISA GitHub repo exposed dozens of plaintext credentials for important CISA GovCloud resources. Caturegli said he validated that the exposed credentials could authenticate to three AWS GovCloud accounts at a high privilege level. He said the archive also includes plain text credentials to CISA’s internal “artifactory” — essentially a repository of all the code packages they are using to build software — and that this would represent a juicy target for malicious attackers looking for ways to maintain a persistent foothold in CISA systems. “That would be a prime place to move laterally,” he said. “Backdoor in some software packages, and every time they build something new they deploy your backdoor left and right.” In response to questions, a spokesperson for CISA said the agency is aware of the reported exposure and is continuing to investigate the situation. “Currently, there is no indication that any sensitive data was compromised as a result of this incident,” the CISA spokesperson wrote. “While we hold our team members to the highest standards of integrity and operational awareness, we are working to ensure additional safeguards are implemented to prevent future occurrences.” A review of the GitHub account and its exposed passwords show the “Private CISA” repository was maintained by an employee of Nightwing , a government contractor based in Dulles, Va. Nightwing declined to comment, directing inquiries to CISA. CISA has not responded to questions about the potential duration of the data exposure, but Caturegli said the Private CISA repository was created on November 13, 2025. The contractor’s GitHub account was created back in September 2018. The GitHub account that included the Private CISA repo was taken offline shortly after both KrebsOnSecurity and Seralys notified CISA about the exposure. But Caturegli said the exposed AWS keys inexplicably continued to remain valid for another 48 hours. CISA is currently operating with only a fraction of its normal budget and staffing levels. The agency has lost nearly a third of its workforce since the beginning of the second Trump administration, which forced a series of early retirements, buyouts, and resignations across the agency’s various divisions. The now-defunct Private CISA repo showed the contractor also used easily-guessed passwords for a number of internal resources; for example, many of the credentials used a password consisting of each platform’s name followed by the current year. Caturegli said such practices would constitute a serious security threat for any organization even if those credentials were never exposed externally, noting that threat actors often use key credentials exposed on the internal network to expand their reach after establishing initial access to a targeted system. “What I suspect happened is [the CISA contractor] was using this GitHub to synchronize files between a work laptop and a home computer, because he has regularly committed to this repo since November 2025,” Caturegli said. “This would be an embarrassing leak for any company, but it’s even more so in this case because it’s CISA.”

0 views
JSLegendDev Yesterday

How I Accidentally Made a Game for No One

I initially set out to make an arcade game, then I tried turning it into an incremental. Finally, I ended up with a game that’s neither an arcade game, neither an incremental and on top of that, it looks like a puzzle game. In this video, I explain how I ended up making a game that appeals to no one!

0 views
Giles's blog Yesterday

10Gb/s Ethernet: using mini-heatsinks with a 10GBASE-T SFP+ module

In my last post I showed the somewhat-scary temperatures I was getting on the MikroTik 10GBASE-T SFP+ module I have plugged into , the 10Gb/s switch I have in my study. As I mentioned then, the plan was to try using some of the mini-heatsinks that people use on Raspberry Pis, to see if that would help. Here's how it went. I bought a 40-piece set of heatsinks made by the improbably-named VooGenzek on Amazon for €8 , and attached two of them like this -- see the bottom module, with the yellow cable: That was 24 hours ago, and here's a chart of temperatures from that module showing the 24 hours before and after: You can see the big drop-off in the middle of the chart; it even overshot a bit (I'm guessing because the heatsinks absorbed a bunch of heat initially when I put them on). The difference looks more dramatic than it is! See where the Y-axis starts. But given that the weather has been pretty much the same today as it was yesterday, that looks like a 3.5°C improvement. Not great, but not nothing either. In the copious discussion about the last post on Hacker News , one of the most popular comments -- from -- was that there are two generations of SFP+ modules for this kind of thing; an older one, using a Marvell chip, and the newer one using one from Broadcom. on the ServeTheHome forums made the same point. They both mentioned that a good indicator of which type a module is using is that the older ones tend to be rated up to 30 metres, while the newer ones are rated up to 100. This one is a MikroTik S+RJ10 , which definitely is one of the older ones -- the specific chip is mentioned in the docs . I'm not sure which chip the Protectli modules in my router are -- they're these modules -- but they say they're rated up to 30 metres, so I guess they're probably the older type too. Looking into switching those out might be a good next step! I probably won't do that in the short term, though, unless I start getting issues as we move into summer.

0 views
Jim Nielsen Yesterday

Something’s Rotten in the State of macOS Icon Design

This is an iconic observation : If you put the Apple icons in reverse it looks like the portfolio of someone getting really really good at icon design This isn’t, however, just the story of Apple’s Creator Studio icons. It’s the unfolding story of icon design across the entire macOS platform. For example, take a look at some of Apple’s other apps like iMovie : Or Remote Desktop : Apple sets the standard (and the rules) for how icons look on the Mac. Wherever they go, so goes the ecosystem — and they’re taking the entire ecosystem along down with them. It’s fast becoming the case that if you put any Mac app’s icons in reverse, it looks like the portfolio of someone getting really, really good at icon design. Even Microsoft — not exactly a bastion of design — starts to look pretty decent with their icons the further back you go. For example, with OneNote , the app icon’s progression looks like it went something like this: Some 3rd-party apps continue to fight a good fight, even as Apple’s definition of what an icon should be — or what’s even possible — shrinks all around them. Apps like Capo (remember, these are reverse chronological ): Or BBEdit : Or Fantastical : Or Cot Editor : Everyone’s being put in a box squircle. The imposition is real. I don’t blame any of the 3rd-party app makers. Their designs have to play by Apple’s rules (or end up in icon jail ). World-class designers like Matthew Skiles or The Iconfactory are still out there striving for excellence, even as they’re hamstrung by the Mac’s latest rules. When it comes to icon design on the Mac, the sky is no longer the limit: Apple’s icon design sensibilities are. They set the examples of what world-class icon design should look like, but what do you do when the examples are no longer exemplary? Reply via: Email · Mastodon · Bluesky “I made this with AI” “I tried to make the AI one, but by hand myself” “I don’t need to be constrained by this squircle” “Hey, I’m getting better at this”

0 views
Zak Knill Yesterday

Generations of AI applications: conversational, delegative, and collaborative

Walk into most product reviews, board decks, or “AI strategy” docs and the mental model on display is still the one from November 2022: a chat window, a back-and-forth, an LLM replying in prose. That model is two generations out of date, and teams building against it are solving the wrong problems. The conversational generation of AI applications came first. ChatGPT launched in November 2022, and through the first half of 2023 the Chat product category evolved. In early 2024 Google Gemini joined the race, and the Claude 3 family of models launched. These products are all part of the conversational generation of AI applications. It’s this generation of AI apps that still matches most people’s mental models. The core interaction of a conversational app is a text box at the bottom of the screen, you type a question or instruction, and the AI replies in the same window, in prose. This is also the design of most AI library examples. This is the design that uses HTTP request/response and SSE streamed responses. It’s the design that fits well into companies’ existing technologies and architectures. This mental model is closer to instant messaging than anything else, which is why some of the first areas of disruption were the areas where users were already interacting with a chat-box. Customer support, and search. In the conversational generation of AI applications, there’s no sense that the AI is doing anything for you. You are consulting the AI and it’s responding to you; answering your questions, asking you questions. Most people’s workflows operated on copy-pasting information in and out of the conversation. The AI’s response is essentially the whole product in the first generation of AI applications.

0 views
iDiallo Yesterday

Don't call yourself a Software Engineer, you are an AI Enabled Engineer.

I can only imagine what it's like learning the skill of programming in this day and age. What does an average college class look like? What is the CS professor teaching? And students, how do you reconcile what you are learning vs the current job market? I know if I was a student today, I would at least attempt to make connections on LinkedIn to prepare for a future where I would need those connections to get a job . But LinkedIn is not a real place. It might have been at one point. But it certainly isn't today. Everybody is an AI Engineer/Leverager/Prompter/Professional. If I wasn't in the industry, it would be real confusing. I see my ex-colleagues right there on LinkedIn, gainfully employed, yet still felt the need to update their titles. We've worked together, I know their current role. Or at least I thought I knew. Now, that same role leverages AI in a way that has "enabled" them. The software engineers though, their role has remained the same while the entire marketing department has switched to AI first. The software engineer is being left behind . The whole company is moving fast, yet the person writing the code using Claude, Cursor, Codex, Copilot, that person remains a mere Software Engineer. In some cases, this person even calls himself a Software Developer. It reminds me of the days we used to call ourselves programmers. We reluctantly accepted the title of software engineer, feeling that imposter syndrome knowing that our code isn't as reliable as it looks. Patrick Mckenzie wrote something short of a manifesto to convince us to market ourselves as Software Engineers . Yes, we were just building CRUD applications for our employers, maybe an expense tracker and accounting software here and there. It didn't feel very impressive. But companies didn't care about the technical elegance of our code. They were just happy to get such a valuable product that helped them save money and increase revenue. It's not hard to see how an engineer can create business value in the real world. Software is eating the world after all, and everything runs on software today. When you call yourself a programmer, you undersell your competence and the value you create. You are a problem solver, you are a business contributor, and you directly affect the company's bottom line. We don't need convincing anymore. We are Software Engineers. But I think we missed the point of Patrick's essay. Maybe we stopped reading a little too early. Maybe we didn't read anything past the title. While I think his argument for not calling ourselves programmers is convincing, I think the meat of it was in the career advice he offered. What he was trying to convince us is that the outcome of our work is what's important, not the form. To grow in this field of tech, the programming language and framework we used had little relevance. Instead, he talked about attending conferences and meetups, blogging and participating online, helping people, building professional relationships. These are things software engineers still avoid today and then wonder why they have a hard time getting the next job. He spoke on the value of communication as a skill, salary negotiation, navigating politics at work. Only those who read past the title benefitted from this advice. I know I did! But Patrick is nowhere to be found in this new landscape we find ourselves in. AI is eating the world now. The workplace is transformed and there are no signs of ever going back. What is the career advice for the year 2026? If you are on LinkedIn, you must have noticed everyone is changing their work title. Is that what we are supposed to do? Did I miss the memo? I didn't read Patrick's latest manifesto, if there are any. But let me get ahead and suggest a few AI enabled titles to get you started. If you were a backend engineer, you are now an AI Platform engineer. If you worked as DRE, you switch to Decision Intelligence Pipeline. If you ever updated the cron job, well, my friend, now you know about Autonomous Agents. If you had the word blockchain in your title, it's even easier. You can make a one to one replacement from blockchain to AI. I'm still working on that list, but I don't think it will have the same impact as going from programmer to software engineer. The title was just semantics. The real value remains in the career advice. And I think Patrick's advice remains just as effective today as it did so many years ago. If you want to grow as a software engineer, don't worry about mastering React. Instead learn the fundamentals of programming and you will have no trouble navigating the different environments. Experience comes from working anyway . Don't stop there. Meet people. Join those conferences, blog about your experience, help others, make those connections with people. In fact, send emails and reply to emails. Hop on a call. You will be surprised how much easier it is to get an interview when you aren't application #234 in a list of one thousand. In fact, you can close this tab and read Patrick's essay. Pretend that the title is "Don't call yourself a Software Engineer." Ps: Patrick was not harmed in the making of this article. He wasn’t even consulted. Ps2: I understand many won't read past this title as well, but that's just filtering at work.

0 views