{"slug": "local-reasoning-for-global-properties", "title": "Local Reasoning for Global Properties", "summary": "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.", "body_md": "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.\n\nNow 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.\n\nPerhaps this struggle will soon be overcome, but if it isn’t, we might once\nagain look to programming language design for help. My aim in this post isn’t\nto try and predict the specific ways that programming languages will, or even\nshould, try to address this[1](javascript:;). Instead I want to answer a more basic question: do\nwe have a good example of programming language design that allows local\nreasoning to give us assurance about a surprising global property?\n\nBackground\n\nI’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.\n\nI 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.\n\nThe simplest argument for this is that creating software that does everything its users\nneed, in a comprehensible and reliable way, requires empathy more\nthan it does expertise in challenging programming language features.\nFor a slightly more nuanced\nview, I’ve previously tried to capture my thoughts on the [nature of\nsoftware](/laurie/blog/2024/what_factors_explain_the_nature_of_software.html).\n\nThis shouldn’t be taken as me saying that programming languages don’t make *any* difference.\nWhen I moved from programming in assembler to “high” level languages like Python and C, my\nproductivity increased substantially and I felt able to tackle much larger\npieces of software. The reason is simple: assembly forces me to deal with so\nmany low-level details that I continually forget the more important high-level\npicture. The difference in the software I could create was profound.\n\nUnfortunately, I gradually came to realise that such a huge improvement was\nunlikely to be repeated. I had, slowly and ineptly, reinvented Fred Brooks’s [no\nsilver bullet](https://www.cs.unc.edu/techreports/86-020.pdf) argument:\n\nMost 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.\n\nAn exception\n\nThat 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.\n\nLet me start with a concrete example. I wrote the software that builds the\nwebsite you’re currently reading as normal single-threaded code. Because\nI’m lazy – and my website isn’t *that* big – every time I run it, the\nentire website is rebuilt.\n\nAfter 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.\n\nIn 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!\n\nIn 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.\n\nHow can this possibly be? I like Rust a lot – it’s been my main language since\n2015 – but it is not a perfect language. Indeed, I can, and have, bored people\nby going into its flaws in detail. But when it comes to multi-threading, it\ndoes something which I would never have imagined possible: [data\nraces](https://doc.rust-lang.org/nomicon/races.html) (i.e. uncoordinated read /\nwrites, where two threads can unexpectedly interfere with each other) become\nstatic errors. That is no small thing: data races were, before, by far the\nbiggest source of errors[2](javascript:;) when I tried to write multi-threaded programs.\n\nHow Rust prevents data races\n\nRust[3](javascript:;) prevents data races through a combination of ownership types and\nthe `Send`\n\nand `Sync`\n\ntraits. If you know how Rust works, you can skip this\nsection. If you don’t know Rust, I’m going to give as brief an overview\nof these features as I know how, simplifying wherever possible.\n\nOne 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.\n\n`Send`\n\nmeans “instances of this struct can be moved from the current thread to\nanother thread” (i.e. after the move the current thread can’t access the\nobject). `Sync`\n\nmeans “multiple threads can read from instances of this struct\nsimultaneously”. For our purposes, we can assume that Rust automatically works\nout when it is safe for a struct to be `Send`\n\nand/or `Sync`\n\nand implements\nthose traits automatically for us.\n\nLet’s start with this very simple Rust code:\n\n``` js\nfn main() {\n  let x = vec![1, 2];\n  println!(\"{x:?}\");\n}\n```\n\nThe vector created by `vec!`\n\ncreates an instance of the `Vec`\n\ntype, which\nimplements `Send`\n\n. So we can send a vector to another thread and have that\nother thread print out the vector:\n\n``` js\nfn main() {\n  let x = vec![1, 2];\n  std::thread::spawn(move || println!(\"{x:?}\")).join().ok();\n}\n```\n\n`std::thread::spawn(...)`\n\nis how one creates a new thread in Rust: the `move || ...`\n\nis a “closure” (i.e. anonymous function) which the new thread will run\nwhen it starts. The `move`\n\nmeans that the new thread becomes the owner of\nany data referenced from the outer function (i.e. `x`\n\nis moved to the new thread).\n`join`\n\nmeans that the main thread waits for the new thread to\nfinish.\n\nWe can see that the main thread really has lost access to the vector because this code:\n\n``` js\nfn main() {\n  let x = vec![1, 2];\n  std::thread::spawn(move || println!(\"{x:?}\")).join().ok();\n  println!(\"{x:?}\");\n}\n```\n\nleads to this compile-time error:\n\n``` php\nerror[E0382]: borrow of moved value: `x`\n --> t.rs:4:14\n  |\n2 |   let x = vec![1, 2];\n  |       - move occurs because `x` has type `Vec<i32>`, which does not implement the `Copy` trait\n3 |   std::thread::spawn(move || println!(\"{x:?}\")).join().ok();\n  |                      -------            - variable moved due to use in closure\n  |                      |\n  |                      value moved into closure here\n4 |   println!(\"{x:?}\");\n  |              ^ value borrowed here after move\n  |\nhelp: consider cloning the value before moving it into the closure\n  |\n3 ~   let value = x.clone();\n4 ~   std::thread::spawn(move || println!(\"{value:?}\")).join().ok();\n  |\n\nerror: aborting due to 1 previous error\n\nFor more information about this error, try `rustc --explain E0382`.\n```\n\nI haven’t even got as far as introducing a full-blown data race and Rust has already prevented me from doing something naughty!\n\nThe error suggests that we should `clone`\n\nvalues: experienced Rust programmers\nare cautious about this advice as it can lead to terrible performance. Why\ndon’t we try wrapping the object in the reference counting type `Rc`\n\ninstead? That way\nwe can happily share the value across both threads:\n\n``` js\nfn main() {\n  let x = std::rc::Rc::new(vec![1, 2]);\n  std::thread::spawn(move || println!(\"{x:?}\")).join().ok();\n  println!(\"{x:?}\");\n}\n```\n\nbut unfortunately that leads to this error:\n\n```\n`Rc<Vec<i32>>` cannot be sent between threads safely\n```\n\nThe reason we can’t move an `Rc`\n\ninstance to another thread is because\nthe reference counting is not done in a thread-safe way. Fortunately there is a\nvariant which does so: the “atomic reference counting” `Arc`\n\n. For slightly boring\nreasons, I need to clone the `Arc`\n\n(which, fortunately, does not clone the vector inside it!):\n\n``` js\nfn main() {\n  let x = std::sync::Arc::new(vec![1, 2]);\n  let y = std::sync::Arc::clone(&x);\n  std::thread::spawn(move || println!(\"{y:?}\")).join().ok();\n  println!(\"{x:?}\");\n}\n```\n\nThis compiles and runs successfully: both threads read from the same vector and\nprint out the same thing. Finally let’s try to enable shared mutation across\nthose threads by introducing Rust’s standard `RefCell`\n\ntype:\n\n``` js\nlet x = std::sync::Arc::new(std::cell::RefCell::new(vec![1, 2]));\n```\n\nWe again get an error but this time it’s not about *sending* (`Send`\n\n)\nbut *sharing* (`Sync`\n\n):\n\n```\nerror[E0277]: `RefCell<Vec<i32>>` cannot be shared between threads safely\n...\n    = help: the trait `Sync` is not implemented for `RefCell<Vec<i32>>`\n```\n\nArguably this is the first time we’ve really tried to introduce a complete data\nrace: again Rust has stopped us. If I want to enable the possibility of shared mutation\nacross threads, I need to introduce a type such as `Mutex`\n\n:\n\n``` js\nfn main() {\n  let x = std::sync::Arc::new(std::sync::Mutex::new(vec![1, 2]));\n  let y = std::sync::Arc::clone(&x);\n  std::thread::spawn(move || {\n    y.lock().unwrap().push(3);\n    println!(\"{:?}\", y.lock());\n  }).join().ok();\n  println!(\"{:?}\", x.lock());\n}\n```\n\nThis compiles and runs correctly (printing `1, 2, 3`\n\ntwice).\n\nExpanding the reasoning globally\n\nAt this point, readers have, I hope, got a sense that Rust prevents me from\nintroducing data races into my program. An important thing to say is that Rust\nhasn’t really had to introduce new features for this: ownership types and the\n`Send`\n\nand `Sync`\n\ntraits are all that’s needed. In other words, I’m still\nwriting “normal” Rust programs: I’m not having to use a new sublanguage\nas I would if I was writing `async`\n\nprograms.\n\nBecause the rules that benefit multi-threaded programs in Rust are, to experienced Rust programmers, natural and obvious, it can prevent us observing a deeper truth: Rust is enforcing a global data-race-free property on my programs in a way I can reason about locally. For example, this property is enforced at the level of function signatures:\n\n```\nfn f<T: std::fmt::Debug>(x: T) {\n  std::thread::spawn(move || println!(\"{x:?}\")).join().ok();\n}\n\nfn main() {\n  f(vec![1,2]);\n}\n```\n\nBecause I haven’t constrained `T`\n\n, Rust can’t be sure that a caller to `f`\n\nhas\nmoved a `Send`\n\nable object to `f`\n\n, so the `spawn`\n\non line 2 leads to this error:\n\n```\n`T` cannot be sent between threads safely\n```\n\nFor this to work, `f`\n\nmust require of its callers that the objects passed to it\nreally are allowed to be sent to other threads. The syntax is a bit unwieldy:\n\n```\nfn f<T: std::fmt::Debug>(x: T) where T: Send + 'static {\n  std::thread::spawn(move || println!(\"{x:?}\")).join().ok();\n}\n\nfn main() {\n  f(vec![1,2]);\n}\n```\n\nThis does now compile and run! The good news is that by looking at the\nsignature of `f`\n\nI know for sure that calling it will not cause a data race on `x`\n\n.\nSo this fragment fails to compile because I’ve used `Rc`\n\n:\n\n```\nf(std::rc::Rc::new(vec![1,2]));\n```\n\nbut if I change that to:\n\n```\nf(std::sync::Arc::new(vec![1,2]));\n```\n\nit does compile.\n\nWhy global reasoning is so powerful\n\nFor those of you unfamiliar with Rust, you will be glad to know that the code fragments stop here. The reason I’ve shown so many examples is that I hope you now believe the following statement:\n\nThat might sound like a normal static typing guarantee: after all, if I write (say) a Haskell program then I’m guaranteed not to get typing errors at runtime. That’s true, but Haskell’s normal type system doesn’t, on its own, give me Rust’s guarantee that concurrent code is free from data races.\n\nPut another way, until Rust I had implicitly assumed that the only global\nproperty a standard programming language could enforce without undue pain is “there are\nno runtime type errors”[4](javascript:;). I thought one had to use\nexotic and/or experimental languages to achieve such properties, and that the\ncompromises this involved would be acceptable to few programmers. Rust’s data\nrace freedom guarantees are accurate, the errors when the rules are undermined\nare (mostly) comprehensible, and the overall result highly usable[5](javascript:;).\n\nLanguages of the future\n\nWe can now go back to the original topic. What makes programming difficult on even moderately sized programs is that each local change is a butterfly — and some of those butterflies’ wing flaps cause great storms (i.e. bugs!) faraway.\n\nThis has always been a problem: even the very best human programmers struggle to gain, and maintain, a global view of the software they’re working on. Right now, though, AI often struggles even more.\n\nAsk AI to generate a single function with a well-defined specification and it\nwill often create better code than I can, and do so more quickly. Ask AI to\ngenerate a moderately sized piece of software and then refine it, and you will\noften have unappealing results. People often talk about code bloat in this\nregard, and while that’s true, that misses a more profound problem: the\nglobal view of the system is often ineptly, and sometimes incorrectly,\ncaptured in the generated code. The easiest – though definitely not the only! –\nway to see this problem is that AI-generated code tends to contain vast numbers\nof *defensive checks*.\n\nAssertions and defensive checks are sometimes conflated, but they are very different. Assertions abort a program as soon as an unexpected situation is observed: they encode the idea that “if this condition fails to hold, the programmer misunderstood how the system works or other parts of the system have gone wrong”. Defensive checks, in contrast, do not abort the program: execution deliberately continues if the check fails. Defensive checks are thus better thought of as encoding the idea that “I’m not sure if this condition holds or not, but if it does fail, I want to have a graceful way of handling it”.\n\nDefensive checks seem like an unambiguously good thing: the current operation tends to finish early, but the program as a whole carries on. However, one can have too much of a good thing. Just as with human written code, many of the defensive checks in AI-generated code are unnecessary (i.e. they cannot fail).\n\nA common example I notice in a lot of AI-generated code are checks for “this list must be non-empty”, even though that has been checked (often multiple times) at all paths that lead to the check. Code that looks like the following is common:\n\n``` python\ndef f(x):\n  if len(x) == 0: return\n  else: ... # do something with x\n\n# This is the only place that f is called from\nfor x in ...:\n  if len(x) > 0: f(x)\n```\n\nIn this example, the defensive check in `f`\n\nis at best unnecessary[6](javascript:;)[7](javascript:;).\n\nUnnecessary checks are intrusive; they undermine performance; and they mislead those reading the code as to the program’s state at a given point in time. We often underestimate how pernicious the last of these is. If you think a program can be in states A, B, and C at the point you want to edit, then you have to consider all three states. If, in fact, two of those states cannot happen, you’ve not just wasted effort, but created a potentially exponential explosion of states for subsequent edits to consider, with all the impacts on productivity and reliability that entails.\n\nGiven how fast AI code generation has progressed in the last year, it would be\nfoolish to rule out the possibility that this problem is soon solved.\nIt is possible, though, that, short of another major breakthrough or\ntwo, AI will continue to be excellent locally and weak globally. If that is\nthe case[8](javascript:;), then we will have a much stronger incentive than in the past to\nhave our programming languages help us enforce global properties, because\neach time we do that, we remove an entire class of bugs.\n\nIn this post what I hope I’ve shown you is that a surprising global property –\ndata race freedom – can be enforced through purely local reasoning. What’s\ninteresting to me about this is that it makes a slew of important programming\ntasks more reliable whilst imposing little additional burden on the programmer. It gives\nme hope that there are other desirable global properties that can be similarly\ndealt with by programming language design[9](javascript:;).\n\nWe have to be realistic though: we won’t be able to enforce every global property we might want. These might range from guarantees about performance, isolation of subcomponents, non-interference of various kinds, resource cleanup, to state changes, and so on. Some of those will almost certainly be in conflict with each other; some will prove too onerous to be worthwhile; and some we simply won’t be able to handle at all.\n\nThe good news is that there are a number of experimental programming language\nfeatures[10](javascript:;) that might turn out to be relevant to this. For\nexample, effects systems allow us to reason about things like what parts of a\nprogram perform input/output through local reasoning. As things stand now, none\nof those has been tested at the same scale as the Rust features I’ve talked\nabout in this post: it’s difficult to know which, if any, might turn out to be\nwinners. It’s also impossible to know which features have yet to be thought of.\n\nHowever, there is enough evidence for us to realistically imagine usable future\nprogramming languages allowing us to write programs with many more guarantees\nabout global properties. Whoever, or whatever, is creating and modifying\nprograms may benefit substantially from this. Personally I hope that it enables\nus to [rethink how we structure programs\nentirely](/laurie/blog/2024/can_we_retain_the_benefits_of_transitive_dependencies_without_undermining_security.html)!\n\nIn summary, until recently, I didn’t think that AI changed the incentives we have for programming languages. Now those incentives are changing; fortunately, we have some indications that we may be able to meet those incentives. Whether anyone with sufficient resources has this level of ambition is unclear to me. But it wouldn’t surprise me if at least one large company – and, even in 2026, this will almost certainly require a company’s resources – tries to do so. I cheer them on in advance: there may be some very interesting programming languages to come!\n\n[Older](/laurie/blog/2026/test_case_reducers_are_underappreciated_debugging_tools.html)\n\n[Mastodon](https://mastodon.social/@ltratt)or\n\n[subscribe to the RSS feed](../blog.rss); or\n\n[subscribe to email updates](/laurie/newsletter/):\n\n### Footnotes\n\n[1](#3d996ad8872fuse)\n\nPartly because this is an active area of research / design in programming languages; and partly because guessing how AI code generation will change is above my pay grade.\n\nPartly because this is an active area of research / design in programming languages; and partly because guessing how AI code generation will change is above my pay grade.\n\n[2](#72f0c461525ause)\n\nThere are, of course, other kinds of errors one can encounter, notably deadlocks. Still, those are easier to debug, and in my experience happen much less often.\n\nThere are, of course, other kinds of errors one can encounter, notably deadlocks. Still, those are easier to debug, and in my experience happen much less often.\n\n[3](#d4cdfdeb636cuse)\n\nTechnically “safe Rust”.\n\nTechnically “safe Rust”.\n\n[4](#8ed9afc65b79use)\n\nOther languages, notably Pony, do offer the same guarantee, though via rather different mechanisms.\n\nOther languages, notably Pony, do offer the same guarantee, though via rather different mechanisms.\n\n[5](#f516d7057061use)\n\nWith the mild exception that one does tend to end up calling\n`Arc::clone`\n\nenough that it becomes an eyesore.\n\nWith the mild exception that one does tend to end up calling\n`Arc::clone`\n\nenough that it becomes an eyesore.\n\n[6](#7277e90890e8use)\n\nDepending on what `f`\n\nis supposed to do, the check may even\nbe incorrect. I have seen functions called things like `print_sum`\n\nwhich\nshould print the sum of the elements passed, but which print nothing for\nthe empty list!\n\nDepending on what `f`\n\nis supposed to do, the check may even\nbe incorrect. I have seen functions called things like `print_sum`\n\nwhich\nshould print the sum of the elements passed, but which print nothing for\nthe empty list!\n\n[7](#666777a756cause)\n\nA brave programmer might remove the check entirely; I would tend to encode it as an assertion.\n\nA brave programmer might remove the check entirely; I would tend to encode it as an assertion.\n\n[8](#8d5295ca523cuse)\n\nAnd perhaps even if not. Even if humans are relegated to a role where we solely review AI-generated code, anything we can do to make it easier for us to have confidence in the code we’re reading would be useful.\n\nAnd perhaps even if not. Even if humans are relegated to a role where we solely review AI-generated code, anything we can do to make it easier for us to have confidence in the code we’re reading would be useful.\n\n[9](#daea673fd253use)\n\nI also have no idea what this language design will, or won’t, look like, or whether it will even be influenced by Rust. The overall point I’m making in this post is, I believe, independent of the particular language I used as a motivating example.\n\nI also have no idea what this language design will, or won’t, look like, or whether it will even be influenced by Rust. The overall point I’m making in this post is, I believe, independent of the particular language I used as a motivating example.\n\n[10](#85235a5485d3use)\n\nTo say nothing of the growing external integration of formal methods into programming.\n\nTo say nothing of the growing external integration of formal methods into programming.", "url": "https://wpnews.pro/news/local-reasoning-for-global-properties", "canonical_source": "https://tratt.net/laurie/blog/2026/local_reasoning_for_global_properties.html", "published_at": "2026-06-30 09:58:16+00:00", "updated_at": "2026-06-30 10:22:25.583781+00:00", "lang": "en", "topics": ["artificial-intelligence", "large-language-models", "developer-tools"], "entities": ["Fred Brooks"], "alternates": {"html": "https://wpnews.pro/news/local-reasoning-for-global-properties", "markdown": "https://wpnews.pro/news/local-reasoning-for-global-properties.md", "text": "https://wpnews.pro/news/local-reasoning-for-global-properties.txt", "jsonld": "https://wpnews.pro/news/local-reasoning-for-global-properties.jsonld"}}