Local Reasoning for Global Properties AI struggles with generating code requiring global program understanding, often producing excessive defensive checks that increase state complexity. Programming language design may offer solutions, but historical evidence suggests languages have limited impact on software quality compared to empathy and essential complexity. In the last couple of years, I’ve increasingly been asked questions that boil down to: will AI benefit from new kinds of programming languages? My answer has been “probably not” and, so far at least, that answer has held up well: AI is now able to generate large quantities of code in just about any programming language you or I can think of. Now that the technology has advanced, and its characteristics have started to become clearer, my answer has changed. My experience is that AI – at least as it stands right now – often generates high-quality local e.g. a function chunks of code, but often struggles when asked to generate code that requires a global understanding of the program. The easiest way to see this is a proliferation of unnecessary defensive checks: these seem benign, but can cause an exponential increase in the number of states later readers of the code believe can occur, with all the deleterious effects that implies. Perhaps this struggle will soon be overcome, but if it isn’t, we might once again look to programming language design for help. My aim in this post isn’t to try and predict the specific ways that programming languages will, or even should, try to address this 1 javascript:; . Instead I want to answer a more basic question: do we have a good example of programming language design that allows local reasoning to give us assurance about a surprising global property? Background I’ve made a fair chunk of my living out of programming languages, so I have a vested interest in amplifying their importance. However, while I believe that programming languages do have some influence on our productivity, and on the reliability of the software we create, there isn’t much evidence that they make a profound difference. I don’t just mean “no-one’s been able to do a good experiment which proves there are differences” — though that is true Rather, a lot of “good” software has been created in “bad” languages and a lot of “bad” software has been created in “good” languages. It seems unlikely that the particular programming language used was the main influence on such outcomes. The simplest argument for this is that creating software that does everything its users need, in a comprehensible and reliable way, requires empathy more than it does expertise in challenging programming language features. For a slightly more nuanced view, I’ve previously tried to capture my thoughts on the nature of software /laurie/blog/2024/what factors explain the nature of software.html . This shouldn’t be taken as me saying that programming languages don’t make any difference. When I moved from programming in assembler to “high” level languages like Python and C, my productivity increased substantially and I felt able to tackle much larger pieces of software. The reason is simple: assembly forces me to deal with so many low-level details that I continually forget the more important high-level picture. The difference in the software I could create was profound. Unfortunately, I gradually came to realise that such a huge improvement was unlikely to be repeated. I had, slowly and ineptly, reinvented Fred Brooks’s no silver bullet https://www.cs.unc.edu/techreports/86-020.pdf argument: Most of the big past gains in software productivity have come from removing artificial barriers that have made the accidental tasks inordinately hard, such as severe hardware constraints, awkward programming languages, lack of machine time. How much of what software engineers now do is still devoted to the accidental, as opposed to the essential? Unless it is more than 9/10 of all effort, shrinking all the accidental activities to zero time will not give an order of magnitude improvement. An exception That meant that when, in a specific context many, many years later, I experienced another profound change in productivity for a lot of software I write, I was so surprised that I almost didn’t notice. When I eventually did, and tried to explain to other people the difference, they also seemed baffled. The context? Multi-threaded programming in Rust. That experience is what informs my opinion on the best course for programming languages in the future, so I need to convince you that there is something deep in the way that Rust makes multi-threaded programming much easier. Let me start with a concrete example. I wrote the software that builds the website you’re currently reading as normal single-threaded code. Because I’m lazy – and my website isn’t that big – every time I run it, the entire website is rebuilt. After a while, I found that the pauses the software needed to rebuild the site were long enough that they made editing some pages like this post inefficient. I quickly made some single-threaded optimisations, but they weren’t enough. I then guessed that if I could rewrite this to use multi-threading I would get those pauses down to an acceptable level. In nearly any other programming language, rewriting the software to use multi-threading would have been a daunting task. Indeed, my past experiences with multi-threading showed me that I would immediately encounter difficult to debug crashes; and, almost certainly, there would be a long tail of such horrors to stumble across over weeks and months. There’s a good reason why I stopped trying to write multi-threaded programs In this particular case, though, the rewrite – which did indeed solve the performance problems – took me under 5 minutes. It ran correctly on the first try, has stayed working correctly — and I had total confidence that both things would be the case. How can this possibly be? I like Rust a lot – it’s been my main language since 2015 – but it is not a perfect language. Indeed, I can, and have, bored people by going into its flaws in detail. But when it comes to multi-threading, it does something which I would never have imagined possible: data races https://doc.rust-lang.org/nomicon/races.html i.e. uncoordinated read / writes, where two threads can unexpectedly interfere with each other become static errors. That is no small thing: data races were, before, by far the biggest source of errors 2 javascript:; when I tried to write multi-threaded programs. How Rust prevents data races Rust 3 javascript:; prevents data races through a combination of ownership types and the Send and Sync traits. If you know how Rust works, you can skip this section. If you don’t know Rust, I’m going to give as brief an overview of these features as I know how, simplifying wherever possible. One can get lost in ownership types but all we need to know is that: a given object has an owner which can read/write to it; and objects can be moved to other owners, at which point the old owner loses access to the object, and the other owner gains access to it. Send means “instances of this struct can be moved from the current thread to another thread” i.e. after the move the current thread can’t access the object . Sync means “multiple threads can read from instances of this struct simultaneously”. For our purposes, we can assume that Rust automatically works out when it is safe for a struct to be Send and/or Sync and implements those traits automatically for us. Let’s start with this very simple Rust code: js fn main { let x = vec 1, 2 ; println "{x:?}" ; } The vector created by vec creates an instance of the Vec type, which implements Send . So we can send a vector to another thread and have that other thread print out the vector: js fn main { let x = vec 1, 2 ; std::thread::spawn move || println "{x:?}" .join .ok ; } std::thread::spawn ... is how one creates a new thread in Rust: the move || ... is a “closure” i.e. anonymous function which the new thread will run when it starts. The move means that the new thread becomes the owner of any data referenced from the outer function i.e. x is moved to the new thread . join means that the main thread waits for the new thread to finish. We can see that the main thread really has lost access to the vector because this code: js fn main { let x = vec 1, 2 ; std::thread::spawn move || println "{x:?}" .join .ok ; println "{x:?}" ; } leads to this compile-time error: php error E0382 : borrow of moved value: x -- t.rs:4:14 | 2 | let x = vec 1, 2 ; | - move occurs because x has type Vec