Latest Posts (20 found)
baby steps 3 days ago

How Dada enables internal references

In my previous Dada blog post, I talked about how Dada enables composable sharing. Today I’m going to start diving into Dada’s permission system; permissions are Dada’s equivalent to Rust’s borrow checker. Dada aims to exceed Rust’s capabilities by using place-based permissions. Dada lets you write functions and types that capture both a value and things borrowed from that value . As a fun example, imagine you are writing some Rust code to process a comma-separated list, just looking for entries of length 5 or more: One of the cool things about Rust is how this code looks a lot like some high-level language like Python or JavaScript, but in those languages the call is going to be doing a lot of work, since it will have to allocate tons of small strings, copying out the data. But in Rust the values are just pointers into the original string and so is very cheap. I love this. On the other hand, suppose you want to package up some of those values, along with the backing string, and send them to another thread to be processed. You might think you can just make a struct like so… …and then create the list and items and store them into it: But as experienced Rustaceans know, this will not work. When you have borrowed data like an , that data cannot be moved. If you want to handle a case like this, you need to convert from into sending indices, owned strings, or some other solution. Argh! Dada does things a bit differently. The first thing is that, when you create a reference, the resulting type names the place that the data was borrowed from , not the lifetime of the reference . So the type annotation for would say 1 (at least, if you wanted to write out the full details rather than leaving it to the type inferencer): I’ve blogged before about how I would like to redefine lifetimes in Rust to be places as I feel that a type like is much easier to teach and explain: instead of having to explain that a lifetime references some part of the code, or what have you, you can say that “this is a that references the variable ”. But what’s also cool is that named places open the door to more flexible borrows. In Dada, if you wanted to package up the list and the items, you could build a type like so: Note that last line – . We can create a new class and move into it along with , which borrows from list. Neat, right? OK, so let’s back up and talk about how this all works. Let’s start with syntax. Before we tackle the example, I want to go back to the example from previous posts, because it’s a bit easier for explanatory purposes. Here is some Rust code that declares a struct , creates an owned copy of it, and then gets a few references into it. The Dada equivalent to this code is as follows: The first thing to note is that, in Dada, the default when you name a variable or a place is to create a reference. So doesn’t move , as it would in Rust, it creates a reference to the stored in . You could also explicitly write , but that is not preferred. Similarly, creates a reference to the value in the field . (If you wanted to move the character, you would write , not as in Rust.) Notice that I said “creates a reference to the stored in ”. In particular, I did not say “creates a reference to ”. That’s a subtle choice of wording, but it has big implications. The reason I wrote that “creates a reference to the stored in ” and not “creates a reference to ” is because, in Dada, references are not pointers . Rather, they are shallow copies of the value, very much like how we saw in the previous post that a acts like an but is represented as a shallow copy. So where in Rust the following code… …looks like this in memory… in Dada, code like this would look like so Clearly, the Dada representation takes up more memory on the stack. But note that it doesn’t duplicate the memory in the heap, which tends to be where the vast majority of the data is found. This gets at something important. Rust, like C, makes pointers first-class. So given , refers to the pointer and refers to its referent, the . Dada, like Java, goes another way. is a value – including in memory representation! The difference between a , , and is not in their memory layout, all of them are the same, but they differ in whether they own their contents . 2 So in Dada, there is no operation to go from “pointer” to “referent”. That doesn’t make sense. Your variable always contains a string, but the permissions you have to use that string will change. In fact, the goal is that people don’t have to learn the memory representation as they learn Dada, you are supposed to be able to think of Dada variables as if they were all objects on the heap, just like in Java or Python, even though in fact they are stored on the stack. 3 In Rust, you cannot move values while they are borrowed. So if you have code like this that moves into … …then this code only compiles if is not used again: There are two reasons that Rust forbids moves of borrowed data: Neither of these apply to Dada: OK, let’s revisit that Rust example that was giving us an error. When we convert it to Dada, we find that it type checks just fine: Woah, neat! We can see that when we move from into , the compiler updates the types of the variables around it. So actually the type of changes to . And then when we move from to , that’s totally valid. In PL land, updating the type of a variable from one thing to another is called a “strong update”. Obviously things can get a bit complicated when control-flow is involved, e.g., in a situation like this: OK, let’s take the next step. Let’s define a Dada function that takes an owned value and another value borrowed from it, like the name, and then call it: We could call this function like so, as you might expect: So…how does this work? Internally, the type checker type-checks a function call by creating a simpler snippet of code, essentially, and then type-checking that . It’s like desugaring but only at type-check time. In this simpler snippet, there are a series of statements to create temporary variables for each argument. These temporaries always have an explicit type taken from the method signature, and they are initialized with the values of each argument: If this type checks, then the type checker knows you have supplied values of the required types, and so this is a valid call. Of course there are a few more steps, but that’s the basic idea. Notice what happens if you supply data borrowed from the wrong place: This will fail to type check because you get: So now, if we go all the way back to our original example, we can see how the example worked: Basically, when you construct a , that’s “just another function call” from the type system’s perspective, except that in the signature is handled carefully. I should be clear, this system is modeled in the dada-model repository, which implements a kind of “mini Dada” that captures what I believe to be the most interesting bits. I’m working on fleshing out that model a bit more, but it’s got most of what I showed you here. 5 For example, here is a test that you get an error when you give a reference to the wrong value. The “real implementation” is lagging quite a bit, and doesn’t really handle the interesting bits yet. Scaling it up from model to real implementation involves solving type inference and some other thorny challenges, and I haven’t gotten there yet – though I have some pretty interesting experiments going on there too, in terms of the compiler architecture. 6 I believe we could apply most of this system to Rust. Obviously we’d have to rework the borrow checker to be based on places, but that’s the straight-forward part. The harder bit is the fact that is a pointer in Rust, and that we cannot readily change. However, for many use cases of self-references, this isn’t as important as it sounds. Often, the data you wish to reference is living in the heap, and so the pointer isn’t actually invalidated when the original value is moved. Consider our opening example. You might imagine Rust allowing something like this in Rust: In this case, the data is heap-allocated, so moving the string doesn’t actually invalidate the value (it would invalidate an value, interestingly). In Rust today, the compiler doesn’t know all the details of what’s going on. has a impl and so it’s quite opaque whether is heap-allocated or not. But we are working on various changes to this system in the Beyond the goal, most notably the Field Projections work. There is likely some opportunity to address this in that context, though to be honest I’m behind in catching up on the details. I’ll note in passing that Dada unifies and into one type as well. I’ll talk in detail about how that works in a future blog post.  ↩︎ This is kind of like C++ references (e.g., ), which also act “as if” they were a value (i.e., you write , not ), but a C++ reference is truly a pointer, unlike a Dada ref.  ↩︎ This goal was in part inspired by a conversation I had early on within Amazon, where a (quite experienced) developer told me, “It took me months to understand what variables are in Rust”.  ↩︎ I explained this some years back in a talk on Polonius at Rust Belt Rust , if you’d like more detail.  ↩︎ No closures or iterator chains!  ↩︎ As a teaser, I’m building it in async Rust, where each inference variable is a “future” and use “await” to find out when other parts of the code might have added constraints.  ↩︎ References are pointers, so those pointers may become invalidated. In the example above, points to the stack slot for , so if were to be moved into , that makes the reference invalid. The type system would lose track of things. Internally, the Rust borrow checker has a kind of “indirection”. It knows that is borrowed for some span of the code (a “lifetime”), and it knows that the lifetime in the type of is related to that lifetime, but it doesn’t really know that is borrowed from in particular. 4 Because references are not pointers into the stack, but rather shallow copies, moving the borrowed value doesn’t invalidate their contents. They remain valid. Because Dada’s types reference actual variable names, we can modify them to reflect moves. I’ll note in passing that Dada unifies and into one type as well. I’ll talk in detail about how that works in a future blog post.  ↩︎ This is kind of like C++ references (e.g., ), which also act “as if” they were a value (i.e., you write , not ), but a C++ reference is truly a pointer, unlike a Dada ref.  ↩︎ This goal was in part inspired by a conversation I had early on within Amazon, where a (quite experienced) developer told me, “It took me months to understand what variables are in Rust”.  ↩︎ I explained this some years back in a talk on Polonius at Rust Belt Rust , if you’d like more detail.  ↩︎ No closures or iterator chains!  ↩︎ As a teaser, I’m building it in async Rust, where each inference variable is a “future” and use “await” to find out when other parts of the code might have added constraints.  ↩︎

0 views
baby steps 1 weeks ago

What it means that Ubuntu is using Rust

Righty-ho, I’m back from Rust Nation, and busily horrifying my teenage daughter with my (admittedly atrocious) attempts at doing an English accent 1 . It was a great trip with a lot of good conversations and some interesting observations. I am going to try to blog about some of them, starting with some thoughts spurred by Jon Seager’s closing keynote, “Rust Adoption At Scale with Ubuntu”. For some time now I’ve been debating with myself, has Rust “crossed the chasm” ? If you’re not familiar with that term, it comes from a book that gives a kind of “pop-sci” introduction to the Technology Adoption Life Cycle . The answer, of course, is it depends on who you ask . Within Amazon, where I have the closest view, the answer is that we are “most of the way across”: Rust is squarely established as the right way to build at-scale data planes or resource-aware agents and it is increasingly seen as the right choice for low-level code in devices and robotics as well – but there remains a lingering perception that Rust is useful for “those fancy pants developers at S3” (or wherever) but a bit overkill for more average development 3 . On the other hand, within the realm of Safety Critical Software, as Pete LeVasseur wrote in a recent rust-lang blog post , Rust is still scrabbling for a foothold. There are a number of successful products but most of the industry is in a “wait and see” mode, letting the early adopters pave the path. The big idea that I at least took away from reading Crossing the Chasm and other references on the technology adoption life cycle is the need for “reference customers”. When you first start out with something new, you are looking for pioneers and early adopters that are drawn to new things: What an early adopter is buying [..] is some kind of change agent . By being the first to implement this change in the industry, the early adopters expect to get a jump on the competition. – from Crossing the Chasm But as your technology matures, you have to convince people with a lower and lower tolerance for risk: The early majority want to buy a productivity improvement for existing operations. They are looking to minimize discontinuity with the old ways. They want evolution, not revolution. – from Crossing the Chasm So what is most convincing to people to try something new? The answer is seeing that others like them have succeeded. You can see this at play in both the Amazon example and the Safety Critical Software example. Clearly seeing Rust used for network services doesn’t mean it’s ready to be used in your car’s steering column 4 . And even within network services, seeing a group like S3 succeed with Rust may convince other groups building at-scale services to try Rust, but doesn’t necessarily persuade a team to use Rust for their next CRUD service. And frankly, it shouldn’t! They are likely to hit obstacles. All of this was on my mind as I watched the keynote by Jon Seager, the VP of Engineering at Canonical, which is the company behind Ubuntu. Similar to Lars Bergstrom’s epic keynote from year’s past on Rust adoption within Google, Jon laid out a pitch for why Canonical is adopting Rust that was at once visionary and yet deeply practical . “Visionary and yet deeply practical” is pretty much the textbook description of what we need to cross from early adopters to early majority . We need folks who care first and foremost about delivering the right results, but are open to new ideas that might help them do that better; folks who can stand on both sides of the chasm at once. Jon described how Canonical focuses their own development on a small set of languages: Python, C/C++, and Go, and how they had recently brought in Rust and were using it as the language of choice for new foundational efforts , replacing C, C++, and (some uses of) Python. Jon talked about how he sees it as part of Ubuntu’s job to “pay it forward” by supporting the construction of memory-safe foundational utilities. Jon meant support both in terms of finances – Canonical is sponsoring the Trifecta Tech Foundation’s to develop sudo-rs and ntpd-rs and sponsoring the uutils org’s work on coreutils – and in terms of reputation. Ubuntu can take on the risk of doing something new, prove that it works, and then let others benefit. Remember how the Crossing the Chasm book described early majority people? They are “looking to minimize discontinuity with the old ways”. And what better way to do that than to have drop-in utilities that fit within their existing workflows. With new adoption comes new perspectives. On Thursday night I was at dinner 5 organized by Ernest Kissiedu 6 . Jon Seager was there along with some other Rust adopters from various industries, as were a few others from the Rust Foundation and the open-source project. Ernest asked them to give us their unvarnished takes on Rust. Jon made the provocative comment that we needed to revisit our policy around having a small standard library. He’s not the first to say something like that, it’s something we’ve been hearing for years and years – and I think he’s right! Though I don’t think the answer is just to ship a big standard library. In fact, it’s kind of a perfect lead-in to (what I hope will be) my next blog post, which is about a project I call “battery packs” 7 . The broader point though is that shifting from targeting “pioneers” and “early adopters” to targeting “early majority” sometimes involves some uncomfortable changes: Transition between any two adoption segments is normally excruciatingly awkward because you must adopt new strategies just at the time you have become most comfortable with the old ones. [..] The situation can be further complicated if the high-tech company, fresh from its marketing success with visionaries, neglects to change its sales pitch. [..] The company may be saying “state-of-the-art” when the pragmatist wants to hear “industry standard”. – Crossing the Chasm (emphasis mine) Not everybody will remember it, but in 2016 there was a proposal called the Rust Platform . The idea was to bring in some crates and bless them as a kind of “extended standard library”. People hated it. After all, they said, why not just add dependencies to your ? It’s easy enough. And to be honest, they were right – at least at the time. I think the Rust Platform is a good example of something that was a poor fit for early adopters, who want the newest thing and don’t mind finding the best crates, but which could be a great fit for the Early Majority. 8 Anyway, I’m not here to argue for one thing or another in this post, but more for the concept that we have to be open to adapting our learned wisdom to new circumstances. In the past, we were trying to bootstrap Rust into the industry’s consciousness – and we have succeeded. The task before us now is different: we need to make Rust the best option not just in terms of “what it could be ” but in terms of “what it actually is ” – and sometimes those are in tension. Later in the dinner, the talk turned, as it often does, to money. Growing Rust adoption also comes with growing needs placed on the Rust project and its ecosystem. How can we connect the dots? This has been a big item on my mind, and I realize in writing this paragraph how many blog posts I have yet to write on the topic, but let me lay out a few interesting points that came up over this dinner and at other recent points. First, there are more ways to offer support than $$. For Canonical specifically, as they are an open-source organization through-and-through, what I would most want is to build stronger relationships between our organizations. With the Rust for Linux developers, early on Rust maintainers were prioritizing and fixing bugs on behalf of RfL devs, but more and more, RfL devs are fixing things themselves, with Rust maintainers serving as mentors. This is awesome! Second, there’s an interesting trend about $$ that I’ve seen crop up in a few places. We often think of companies investing in the open-source dependencies that they rely upon. But there’s an entirely different source of funding, and one that might be even easier to tap, which is to look at companies that are considering Rust but haven’t adopted it yet. For those “would be” adopters, there are often individuals in the org who are trying to make the case for Rust adoption – these individuals are early adopters, people with a vision for how things could be, but they are trying to sell to their early majority company. And to do that, they often have a list of “table stakes” features that need to be supported; what’s more, they often have access to some budget to make these things happen. This came up when I was talking to Alexandru Radovici, the Foundation’s Silver Member Directory, who said that many safety critical companies have money they’d like to spend to close various gaps in Rust, but they don’t know how to spend it. Jon’s investments in Trifecta Tech and the uutils org have the same character: he is looking to close the gaps that block Ubuntu from using Rust more. Well, first of all, you should watch Jon’s talk. “Brilliant”, as the Brits have it. But my other big thought is that this is a crucial time for Rust. We are clearly transitioning in a number of areas from visionaries and early adopters towards that pragmatic majority, and we need to be mindful that doing so may require us to change some of the way that we’ve always done things. I liked this paragraph from Crossing the Chasm : To market successfully to pragmatists, one does not have to be one – just understand their values and work to serve them. To look more closely into these values, if the goal of visionaries is to take a quantum leap forward, the goal of pragmatists is to make a percentage improvement–incremental, measurable, predictable progress. [..] To market to pragmatists, you must be patient. You need to be conversant with the issues that dominate their particular business. You need to show up at the industry-specific conferences and trade shows they attend. Re-reading Crossing the Chasm as part of writing this blog post has really helped me square where Rust is – for the most part, I think we are still crossing the chasm, but we are well on our way. I think what we see is a consistent trend now where we have Rust champions who fit the “visionary” profile of early adopters successfully advocating for Rust within companies that fit the pragmatist, early majority profile. It strikes me that open-source is just an amazing platform for doing this kind of marketing. Unlike a company, we don’t have to do everything ourselves. We have to leverage the fact that open source helps those who help themselves – find those visionary folks in industries that could really benefit from Rust, bring them into the Rust orbit, and then (most important!) support and empower them to adapt Rust to their needs. This last part may sound obvious, but it’s harder than it sounds. When you’re embedded in open source, it seems like a friendly place where everyone is welcome. But the reality is that it can be a place full of cliques and “oral traditions” that “everybody knows” 9 . People coming with an idea can get shutdown for using the wrong word. They can readily mistake the, um, “impassioned” comments from a random contributor (or perhaps just a troll…) for the official word from project leadership. It only takes one rude response to turn somebody away. So what will ultimately help Rust the most to succeed? Empathy in Open Source . Let’s get out there, find out where Rust can help people, and make it happen. Exciting times! I am famously bad at accents. My best attempt at posh British sounds more like Apu from the Simpsons. I really wish I could pull off a convincing Greek accent, but sadly no.  ↩︎ Another of my pearls of wisdom is “there is nothing more permanent than temporary code”. I used to say that back at the startup I worked at after college, but years of experience have only proven it more and more true.  ↩︎ Russel Cohen and Jess Izen gave a great talk at last year’s RustConf about what our team is doing to help teams decide if Rust is viable for them. But since then another thing having a big impact is AI, which is bringing previously unthinkable projects, like rewriting older systems, within reach.  ↩︎ I have no idea if there is code in a car’s steering column, for the record. I assume so by now? For power steering or some shit?  ↩︎ Or am I supposed to call it “tea”? Or maybe “supper”? I can’t get a handle on British mealtimes.  ↩︎ Ernest is such a joy to be around. He’s quiet, but he’s got a lot of insights if you can convince him to share them. If you get the chance to meet him, take it! If you live in London, go to the London Rust meetup! Find Ernest and introduce yourself. Tell him Niko sent you and that you are supposed to say how great he is and how you want to learn from the wisdom he’s accrued over the years. Then watch him blush. What a doll.  ↩︎ If you can’t wait, you can read some Zulip discussion here.  ↩︎ The Battery Packs proposal I want to talk about is similar in some ways to the Rust Platform, but decentralized and generally better in my opinion– but I get ahead of myself!  ↩︎ Betteridge’s Law of Headlines has it that “Any headline that ends in a question mark can be answered by the word no ”. Well, Niko’s law of open-source 2 is that “nobody actually knows anything that ’everybody’ knows”.  ↩︎ I am famously bad at accents. My best attempt at posh British sounds more like Apu from the Simpsons. I really wish I could pull off a convincing Greek accent, but sadly no.  ↩︎ Another of my pearls of wisdom is “there is nothing more permanent than temporary code”. I used to say that back at the startup I worked at after college, but years of experience have only proven it more and more true.  ↩︎ Russel Cohen and Jess Izen gave a great talk at last year’s RustConf about what our team is doing to help teams decide if Rust is viable for them. But since then another thing having a big impact is AI, which is bringing previously unthinkable projects, like rewriting older systems, within reach.  ↩︎ I have no idea if there is code in a car’s steering column, for the record. I assume so by now? For power steering or some shit?  ↩︎ Or am I supposed to call it “tea”? Or maybe “supper”? I can’t get a handle on British mealtimes.  ↩︎ Ernest is such a joy to be around. He’s quiet, but he’s got a lot of insights if you can convince him to share them. If you get the chance to meet him, take it! If you live in London, go to the London Rust meetup! Find Ernest and introduce yourself. Tell him Niko sent you and that you are supposed to say how great he is and how you want to learn from the wisdom he’s accrued over the years. Then watch him blush. What a doll.  ↩︎ If you can’t wait, you can read some Zulip discussion here.  ↩︎ The Battery Packs proposal I want to talk about is similar in some ways to the Rust Platform, but decentralized and generally better in my opinion– but I get ahead of myself!  ↩︎ Betteridge’s Law of Headlines has it that “Any headline that ends in a question mark can be answered by the word no ”. Well, Niko’s law of open-source 2 is that “nobody actually knows anything that ’everybody’ knows”.  ↩︎

0 views
baby steps 2 weeks ago

Sharing in Dada

OK, let’s talk about sharing . This is the first of Dada blog posts where things start to diverge from Rust in a deep way and I think the first where we start to see some real advantages to the Dada way of doing things (and some of the tradeoffs I made to achieve those advantages). Let’s start with the goal: earlier, I said that Dada was like “Rust where you never have to type ”. But what I really meant is that I want a GC-like experience–without the GC. I also often use the word “composable” to describe the Dada experience I am shooting for. Composable means that you can take different things and put them together to achieve something new. Obviously Rust has many composable patterns – the APIs, for example. But what I have found is that Rust code is often very brittle: there are many choices when it comes to how you declare your data structures and the choices you make will inform how those data structures can be consumed. Let’s create a type that we can use as a running example throughout the post: . In Rust, we might define a like so: Now, suppose that, for whatever reason, we are going to build up a character programmatically: So far, so good. Now suppose I want to share that same struct so it can be referenced from a lot of places without deep copying. To do that, I am going to put it in an : OK, cool! Now I have a that is readily sharable. That’s great. Side note but this is an example of where Rust is composable: we defined once in a fully-owned way and we were able to use it mutably (to build it up imperatively over time) and then able to “freeze” it and get a read-only, shared copy of . This gives us the advantages of an imperative programming language (easy data construction and manipulation) and the advantages of a functional language (immutability prevents bugs when things are referenced from many disjoint places). Nice! Now , suppose that I have some other code, written independently, that just needs to store the character’s name . That code winds up copying the name into a lot of different places. So, just like we used to let us cheaply reference a single character from multiple places, it uses so it can cheaply reference the character’s name from multiple places: OK. Now comes the rub. I want to create a character-sheet widget from our shared character: Shoot, that’s frustrating! What I would like to do is to write or something similar (actually I’d probably like to just write , but anyhow) and get back an . But I can’t do that. Instead, I have to deeply clone the string and allocate a new . Of course any subsequent clones will be cheap. But it’s not great. I often find patterns like this arise in Rust: there’s a bit of an “impedance mismatch” between one piece of code and another. The solution varies, but it’s generally something like The goal with Dada is that we don’t have that kind of thing. So let’s walk through how that same example would play out in Dada. We’ll start by defining the class: Just as in Rust, we can create the character and then modify it afterwards: Cool. Now, I want to share the character so it can be referenced from many places. In Rust, we created an , but in Dada, sharing is “built-in”. We use the operator, which will convert the (i.e., fully owned character) into a : Now that we have a character, we can copy it around: When you have a shared object and you access its field, what you get back is a shared (shallow) copy of the field : To drill home how cool and convenient this is, imagine that I have a that I share with : and then I share it with . What I get back is a . And when I access the elements of that, I get back a : This is as if one could take a in Rust and get out a . So how is sharing implemented? The answer lies in a not-entirely-obvious memory layout. To see how it works, let’s walk how a would be laid out in memory: Here is a built-in type that is the basis for Dada’s unsafe code system. 1 Now imagine we have a like this: The character would be laid out in memory something like this (focusing just on the field): Let’s talk this through. First, every object is laid out flat in memory, just like you would see in Rust. So the fields of are stored on the stack, and the field is laid out flat within that. Each object that owns other objects begins with a hidden field, . This field indicates whether the object is shared or not (in the future we’ll add more values to account for other permissions). If the field is 1, the object is not shared. If it is 2, then it is shared. Heap-allocated objects (i.e., using ) begin with a ref-count before the actual data (actually this is at the offset of -4). In this case we have a so the actual data that follows are just simple characters. If I were to instead create a shared character: The memory layout would be the same, but the flag field on the character is now 2: Now imagine that we created two copies of the same shared character: What happens is that we will copy all the fields of and then, because is 2, we will increment the ref-counts for the heap-allocated data within: Now imagine we were to copy out the name field, instead of the entire character: …what happens is that: The result is that you get: This post showed how values in Dada work and showed how the permission propagates when you access a field. Permissions are how Dada manages object lifetimes. We’ve seen two so far In future posts we’ll see the and permissions, which roughly correspond to and , and talk out how the whole thing fits together. This is the first post where we started to see a bit more of Dada’s character. Reading over the previous few posts, you could be forgiven for thinking Dada was just a cute syntax atop familiar Rust semantics. But as you can see from how works, Dada is quite a bit more than that. I like to think of Dada as “opinionated Rust” in some sense. Unlike Rust, it imposes some standards on how things are done. For example, every object (at least every object with a heap-allocated field) has a field. And every heap allocation has a ref-count. These conventions come at some modest runtime cost. My rule is that basic operations are allowed to do “shallow” operations, e.g., toggling the or adjusting the ref-counts on every field. But they cannot do “deep” operations that require traversing heap structures. In exchange for adopting conventions and paying that cost, you get “composability”, by which I mean that permissions in Dada (like ) flow much more naturally, and types that are semantically equivalent (i.e., you can do the same things with them) generally have the same layout in memory. Remember that I have not implemented all this, I am drawing on my memory and notes from my notebooks. I reserve the right to change any and everything as I go about implementing.  ↩︎ clone some data – it’s not so big anyway, screw it (that’s what happened here). refactor one piece of code – e.g., modify the class to store an . Of course, that has ripple effects, e.g., we can no longer write anymore, but have to use or something. invoke some annoying helper – e.g., write to convert from an to a or write a to convert from a to a . traversing , we observe that the field is 2 and therefore is shared we copy out the fields from . Because the character is shared: we modify the field on the new string to 2 we increment the ref-count for any heap values the permission indicates a uniquely owned value ( , in Rust-speak); the permission indicates a copyable value ( is the closest Rust equivalent). Remember that I have not implemented all this, I am drawing on my memory and notes from my notebooks. I reserve the right to change any and everything as I go about implementing.  ↩︎

0 views
baby steps 3 weeks ago

Hello, Dada!

Following on my Fun with Dada post, this post is going to start teaching Dada. I’m going to keep each post short – basically just what I can write while having my morning coffee. 1 Here is a very first Dada program I think all of you will be able to guess what it does. Still, there is something worth noting even in this simple program: “You have the right to write code. If you don’t write a function explicitly, one will be provided for you.” Early on I made the change to let users omit the function and I was surprised by what a difference it made in how light the language felt. Easy change, easy win. Here is another Dada program Unsurprisingly, this program does the same thing as the last one. “Convenient is the default.” Strings support interpolation (i.e., ) by default. In fact, that’s not all they support, you can also break them across lines very conveniently. This program does the same thing as the others we’ve seen: When you have a immediately followed by a newline, the leading and trailing newline are stripped, along with the “whitespace prefix” from the subsequent lines. Internal newlines are kept, so something like this: would print Of course you could also annotate the type of the variable explicitly: You will find that it is . This in and of itself is not notable, unless you are accustomed to Rust, where the type would be . This is of course a perennial stumbling block for new Rust users, but more than that, I find it to be a big annoyance – I hate that I have to write or everywhere that I mix constant strings with strings that are constructed. Similar to most modern languages, strings in Dada are immutable. So you can create them and copy them around: OK, we really just scratched the surface here! This is just the “friendly veneer” of Dada, which looks and feels like a million other languages. Next time I’ll start getting into the permission system and mutation, where things get a bit more interesting. My habit is to wake around 5am and spend the first hour of the day doing “fun side projects”. But for the last N months I’ve actually been doing Rust stuff, like symposium.dev and preparing the 2026 Rust Project Goals . Both of these are super engaging, but all Rust and no play makes Niko a dull boy. Also a grouchy boy.  ↩︎ My habit is to wake around 5am and spend the first hour of the day doing “fun side projects”. But for the last N months I’ve actually been doing Rust stuff, like symposium.dev and preparing the 2026 Rust Project Goals . Both of these are super engaging, but all Rust and no play makes Niko a dull boy. Also a grouchy boy.  ↩︎

0 views
baby steps 3 months ago

Move Expressions

This post explores another proposal in the space of ergonomic ref-counting that I am calling move expressions . To my mind, these are an alternative to explicit capture clauses , one that addresses many (but not all ) of the goals from that design with improved ergonomics and readability. The idea itself is simple, within a closure (or future), we add the option to write . This is a value expression (“rvalue”) that desugars into a temporary value that is moved into the closure. So is roughly equivalent to something like: Let’s go back to one of our running examples, the “Cloudflare example”, which originated in this excellent blog post by the Dioxus folks . As a reminder, this is how the code looks today – note the lines for dealing with captures: Under this proposal it would look something like this: There are times when you would want multiple clones. For example, if you want to move something into a closure that will then give away a copy on each call, it might look like This idea is not mine. It’s been floated a number of times. The first time I remember hearing it was at the RustConf Unconf, but I feel like it’s come up before that. Most recently it was proposed by Zachary Harrold on Zulip , who has also created a prototype called soupa . Zachary’s proposal, like earlier proposals I’ve heard, used the keyword. Later on @simulacrum proposed using , which to me is a major improvement, and that’s the version I ran with here. The reason that I love the variant of this proposal is that it makes closures more “continuous” and exposes their underlying model a bit more clearly. With this design, I would start by explaining closures with move expressions and just teach closures at the end, as a convenient default: A Rust closure captures the places you use in the “minimal way that it can” – so will capture a shared reference to the , will capture a mutable reference, and will take ownership of the vector. You can use expressions to control exactly what is captured: so will move the into the closure. A common pattern when you want to be fully explicit is to list all captures at the top of the closure, like so: As a shorthand, you can write at the top of the closure, which will change the default so that closures > take ownership of every captured variable. You can still mix-and-match with expressions to get more control. > So the previous closure might be written more concisely like so: It’s a bit ironic that I like this, because it’s doubling down on part of Rust’s design that I was recently complaining about. In my earlier post on Explicit Capture Clauses I wrote that: To be honest, I don’t like the choice of because it’s so operational . I think if I could go back, I would try to refashion our closures around two concepts I think this would help to build up the intuition of “use if you are going to return the closure from the current stack frame and use otherwise”. expressions are, I think, moving in the opposite direction. Rather than talking about attached and detached, they bring us to a more unified notion of closures, one where you don’t have “ref closures” and “move closures” – you just have closures that sometimes capture moves, and a “move” closure is just a shorthand for using expressions everywhere. This is in fact how closures work in the compiler under the hood, and I think it’s quite elegant. One question is whether a expression should be a prefix or a postfix operator. So e.g. instead of . My feeling is that it’s not a good fit for a postfix operator because it doesn’t just take the final value of the expression and so something with it, it actually impacts when the entire expression is evaluated. Consider this example: When does get called? If you think about it, it has to be closure creation time, but it’s not very “obvious”. We reached a similar conclusion when we were considering operators. I think there is a rule of thumb that things which delineate a “scope” of code ought to be prefix – though I suspect might actually be nice, and not just . Edit: I added this section after-the-fact in response to questions. I’m going to wrap up this post here. To be honest, what this design really has going for it, above anything else, is its simplicity and the way it generalizes Rust’s existing design . I love that. To me, it joins the set of “yep, we should clearly do that” pieces in this puzzle: These both seem like solid steps forward. I am not yet persuaded that they get us all the way to the goal that I articulated in an earlier post : “low-level enough for a Kernel, usable enough for a GUI” but they are moving in the right direction. Attached closures (what we now call ) would always be tied to the enclosing stack frame. They’d always have a lifetime even if they don’t capture anything. Detached closures (what we now call ) would capture by-value, like today. Add a trait (I’ve gone back to preferring the name 😁) Add expressions

0 views
baby steps 3 months ago

Just call clone (or alias)

Continuing my series on ergonomic ref-counting, I want to explore another idea, one that I’m calling “just call clone (or alias)”. This proposal specializes the and methods so that, in a new edition, the compiler will (1) remove redundant or unnecessary calls (with a lint); and (2) automatically capture clones or aliases in closures where needed. The goal of this proposal is to simplify the user’s mental model: whenever you see an error like “use of moved value”, the fix is always the same: just call (or , if applicable). This model is aiming for the balance of “low-level enough for a Kernel, usable enough for a GUI” that I described earlier. It’s also making a statement, which is that the key property we want to preserve is that you can always find where new aliases might be created – but that it’s ok if the fine-grained details around exactly when the alias is created is a bit subtle. Consider this future: Because this is a future, this takes ownership of and . Because is a borrowed reference, this will be an error unless those values are (which they presumably are not). Under this proposal, capturing aliases or clones in a closure/future would result in capturing an alias or clone of the place. So this future would be desugared like so (using explicit capture clause strawman notation ): Now, this result is inefficient – there are now two aliases/clones. So the next part of the proposal is that the compiler would, in newer Rust editions, apply a new transformat called the last-use transformation . This transformation would identify calls to or that are not needed to satisfy the borrow checker and remove them. This code would therefore become: The last-use transformation would apply beyond closures. Given an example like this one, which clones even though is never used later: the user would get a warning like so 1 : and the code would be transformed so that it simply does a move: The goal of this proposal is that, when you get an error about a use of moved value, or moving borrowed content, the fix is always the same: you just call (or ). It doesn’t matter whether that error occurs in the regular function body or in a closure or in a future, the compiler will insert the clones/aliases needed to ensure future users of that same place have access to it (and no more than that). I believe this will be helpful for new users. Early in their Rust journey new users are often sprinkling calls to clone as well as sigils like in more-or-less at random as they try to develop a firm mental model – this is where the “keep calm and call clone” joke comes from. This approach breaks down around closures and futures today. Under this proposal, it will work, but users will also benefit from warnings indicating unnecessary clones, which I think will help them to understand where clone is really needed . But the real question is how this works for experienced users . I’ve been thinking about this a lot! I think this approach fits pretty squarely in the classic Bjarne Stroustrup definition of a zero-cost abstraction: “What you don’t use, you don’t pay for. And further: What you do use, you couldn’t hand code any better.” The first half is clearly satisfied. If you don’t call or , this proposal has no impact on your life. The key point is the second half: earlier versions of this proposal were more simplistic, and would sometimes result in redundant or unnecessary clones and aliases. Upon reflection, I decided that this was a non-starter. The only way this proposal works is if experienced users know there is no performance advantage to using the more explicit form .This is precisely what we have with, say, iterators, and I think it works out very well. I believe this proposal hits that mark, but I’d like to hear if there are things I’m overlooking. I think most users would expect that changing to just is fine, as long as the code keeps compiling. But in fact nothing requires that to be the case. Under this proposal, APIs that make significant in unusual ways would be more annoying to use in the new Rust edition and I expect ultimately wind up getting changed so that “significant clones” have another name. I think this is a good thing. I think I’ve covered the key points. Let me dive into some of the details here with a FAQ. I get it, I’ve been throwing a lot of things out there. Let me begin by recapping the motivation as I see it: I then proposed a set of three changes to address these issues, authored in individual blog posts: Let’s look at the impact of each set of changes by walking through the “Cloudflare example”, which originated in this excellent blog post by the Dioxus folks : As the original blog post put it: Working on this codebase was demoralizing. We could think of no better way to architect things - we needed listeners for basically everything that filtered their updates based on the state of the app. You could say “lol get gud,” but the engineers on this team were the sharpest people I’ve ever worked with. Cloudflare is all-in on Rust. They’re willing to throw money at codebases like this. Nuclear fusion won’t be solved with Rust if this is how sharing state works. Applying the trait and explicit capture clauses makes for a modest improvement. You can now clearly see that the calls to are calls, and you don’t have the awkward and variables. However, the code is still pretty verbose: Applying the Just Call Clone proposal removes a lot of boilerplate and, I think, captures the intent of the code very well. It also retains quite a bit of explicitness, in that searching for calls to reveals all the places that aliases will be created. However, it does introduce a bit of subtlety, since (e.g.) the call to will actually occur when the future is created and not when it is awaited : There is no question that Just Call Clone makes closure/future desugaring more subtle. Looking at task 1: this gets desugared to a call to when the future is created (not when it is awaited ). Using the explicit form: I can definitely imagine people getting confused at first – “but that call to looks like its inside the future (or closure), how come it’s occuring earlier?” Yet, the code really seems to preserve what is most important: when I search the codebase for calls to , I will find that an alias is creating for this task. And for the vast majority of real-world examples, the distinction of whether an alias is creating when the task is spawned versus when it executes doesn’t matter. Look at this code: the important thing is that is called with an alias of , so will stay alive as long as is executing. It doesn’t really matter how the “plumbing” worked. Yeah, good point, those kind of examples have more room for confusion. Like look at this: In this example, there is code that uses with an alias, but only under . So what happens? I would assume that indeed the future will capture an alias of , in just the same way that this future will move , even though the relevant code is dead: Yep! I am thinking of something like this: Examples that show some edge cased: In the relevant cases, non-move closures will already just capture by shared reference. This means that later attempts to use that variable will generally succeed: This future does not need to take ownership of to create an alias, so it will just capture a reference to . That means that later uses of can still compile, no problem. If this had been a move closure, however, that code above would currently not compile. There is an edge case where you might get an error, which is when you are moving : In that case, you can make this an closure and/or use an explicit capture clause: Yep! We would during codegen identify candidate calls to or . After borrow check has executed, we would examine each of the callsites and check the borrow check information to decide: If the answer to both questions is no, then we will replace the call with a move of the original place. Here are some examples: In the past, I’ve talked about the last-use transformation as an optimization – but I’m changing terminology here. This is because, typically, an optimization is supposed to be unobservable to users except through measurements of execution time (or though UB), and that is clearly not the case here. The transformation would be a mechanical transformation performed by the compiler in a deterministic fashion. I think yes, but in a limited way. In other words I would expect to be transformed in the same way (replaced with ), and the same would apply to more levels of intermediate usage. This would kind of “fall out” from the MIR-based optimization technique I imagine. It doesn’t have to be this way, we could be more particular about the syntax that people wrote, but I think that would be surprising. On the other hand, you could still fool it e.g. like so The way I imagine it, no. The transformation would be local to a function body. This means that one could write a method like so that “hides” the clone in a way that it will never be transformed away (this is an important capability for edition transformations!): Potentially, yes! Consider this example, written using explicit capture clause notation and written assuming we add an trait: The precise timing when values are dropped can be important – when all senders have dropped, the will start returning when you call . Before that, it will block waiting for more messages, since those handles could still be used. So, in , when will the sender aliases be fully dropped? The answer depends on whether we do the last-use transformation or not: Most of the time, running destructors earlier is a good thing. That means lower peak memory usage, faster responsiveness. But in extreme cases it could lead to bugs – a typical example is a where the guard is being used to protect some external resource. This is what editions are for! We have in fact done a very similar transformation before, in Rust 2021. RFC 2229 changed destructor timing around closures and it was, by and large, a non-event. The desire for edition compatibility is in fact one of the reasons I want to make this a last-use transformation and not some kind of optimization . There is no UB in any of these examples, it’s just that to understand what Rust code does around clones/aliases is a bit more complex than it used to be, because the compiler will do automatic transformation to those calls. The fact that this transformation is local to a function means we can decide on a call-by-call basis whether it should follow the older edition rules (where it will always occur) or the newer rules (where it may be transformed into a move). In theory, yes, improvements to borrow-checker precision like Polonius could mean that we identify more opportunities to apply the last-use transformation. This is something we can phase in over an edition. It’s a bit of a pain, but I think we can live with it – and I’m unconvinced it will be important in practice. For example, when thinking about the improvements I expect under Polonius, I was not able to come up with a realistic example that would be impacted. This last-use transformation is guaranteed not to produce code that would fail the borrow check. However, it can affect the correctness of unsafe code: Note though that, in this case, there would be a lint identifying that the call to will be transformed to just . We could also detect simple examples like this one and report a stronger deny-by-default lint, as we often do when we see guaranteed UB. When I originally had this idea, I called it “use-use-everywhere” and, instead of writing or , I imagined writing . This made sense to me because a keyword seemed like a stronger signal that this was impacting closure desugaring. However, I’ve changed my mind for a few reasons. First, Santiago Pastorino gave strong pushback that was going to be a stumbling block for new learners. They now have to see this keyword and try to understand what it means – in contrast, if they see method calls, they will likely not even notice something strange is going on. The second reason though was TC who argued, in the lang-team meeting, that all the arguments for why it should be ergonomic to clone a ref-counted value in a closure applied equally well to , depending on the needs of your application. I completely agree. As I mentioned earlier, this also [addresses the concern I’ve heard with the trait], which is that there are things you want to ergonomically clone but which don’t correspond to “aliases”. True. In general I think that (and ) are fundamental enough to how Rust is used that it’s ok to special case them. Perhaps we’ll identify other similar methods in the future, or generalize this mechanism, but for now I think we can focus on these two cases. One point that I’ve raised from time-to-time is that I would like a solution that gives the compiler more room to optimize ref-counting to avoid incrementing ref-counts in cases where it is obvious that those ref-counts are not needed. An example might be a function like this: This function requires ownership of an alias to a ref-counted value but it doesn’t actually do anything but read from it. A caller like this one… …doesn’t really need to increment the reference count, since the caller will be holding a reference the entire time. I often write code like this using a : so that the caller can do – this then allows the callee to write in the case that it wants to take ownership. I’ve basically decided to punt on adressing this problem. I think folks that are very performance sensitive can use and the rest of us can sometimes have an extra ref-count increment, but either way, the semantics for users are clear enough and (frankly) good enough. Surprisingly to me, doesn’t have a dedicated lint for unnecessary clones. This particular example does get a lint, but it’s a lint about taking an argument by value and then not consuming it. If you rewrite the example to create locally, clippy does not complain .  ↩︎ I believe our goal should be to focus first on a design that is “low-level enough for a Kernel, usable enough for a GUI” . The key part here is the word enough . We need to make sure that low-level details are exposed, but only those that truly matter. And we need to make sure that it’s ergonomic to use, but it doesn’t have to be as nice as TypeScript (though that would be great). Rust’s current approach to fails both groups of users; calls to are not explicit enough for kernels and low-level software: when you see , you don’t know that is creating a new alias or an entirely distinct value, and you don’t have any clue what it will cost at runtime. There’s a reason much of the community recommends writing instead. calls to , particularly in closures, are a major ergonomic pain point , this has been a clear consensus since we first started talking about this issue. First, we introduce the trait (originally called ) . The trait introduces a new method that is equivalent to but indicates that this will be creating a second alias of the same underlying value. Second, we introduce explicit capture clauses , which lighten the syntactic load of capturing a clone or alias, make it possible to declare up-front the full set of values captured by a closure/future, and will support other kinds of handy transformations (e.g., capturing the result of or ). Finally, we introduce the just call clone proposal described in this post. This modifies closure desugaring to recognize clones/aliases and also applies the last-use transformation to replace calls to clone/alias with moves where possible. If there is an explicit capture clause , use that. Else: For non- closures/futures, no changes, so Categorize usage of each place and pick the “weakest option” that is available: by ref For closures/futures, we would change Categorize usage of each place and decide whether to capture that place… by clone , there is at least one call or and all other usage of requires only a shared ref (reads) by move , if there are no calls to or or if there are usages of that require ownership or a mutable reference Capture by clone/alias when a place is only used via shared references, and at least one of those is a clone or alias. For the purposes of this, accessing a “prefix place” or a “suffix place” is also considered an access to . Will this place be accessed later? Will some reference potentially referencing this place be accessed later? Without the transformation, there are two aliases: the original and the one being held by the future. So the receiver will only start returning when has finished and the task has completed. With the transformation, the call to is removed, and so there is only one alias – , which is moved into the future, and dropped once the spawned task completes. This could well be earlier than in the previous code, which had to wait until both and the new task completed. Surprisingly to me, doesn’t have a dedicated lint for unnecessary clones. This particular example does get a lint, but it’s a lint about taking an argument by value and then not consuming it. If you rewrite the example to create locally, clippy does not complain .  ↩︎

1 views
baby steps 3 months ago

But then again...maybe alias?

Hmm, as I re-read the post I literally just posted a few minutes ago, I got to thinking. Maybe the right name is indeed , and not . The rationale is simple: alias can serve as both a noun and a verb. It hits that sweet spot of “common enough you know what it means, but weird enough that it can be Rust Jargon for something quite specific”. In the same way that we talk about “passing a clone of ” we can talk about “passing an alias to ” or an “alias of ”. Food for thought! I’m going to try on for size in future posts and see how it feels.

0 views
baby steps 3 months ago

Bikeshedding `Handle` and other follow-up thoughts

There have been two major sets of responses to my proposal for a trait. The first is that the trait seems useful but doesn’t over all the cases where one would like to be able to ergonomically clone things. The second is that the name doesn’t seem to fit with our Rust conventions for trait names, which emphasize short verbs over nouns. The TL;DR of my response is that (1) I agree, this is why I think we should work to make ergonomic as well as ; and (2) I agree with that too, which is why I think we should find another name. At the moment I prefer , with coming in second. The first concern with the trait is that, while it gives a clear semantic basis for when to implement the trait, it does not cover all the cases where calling is annoying. In other words, if we opt to use , and then we make creating new handles very ergonomic, but calling remains painful, there will be a temptation to use the when it is not appropriate. In one of our lang team design meetings, TC raised the point that, for many applications, even an “expensive” clone isn’t really a big deal. For example, when writing CLI tools and things, I regularly clone strings and vectors of strings and hashmaps and whatever else; I could put them in an Rc or Arc but I know it just doens’t matter. My solution here is simple: let’s make solutions that apply to both and . Given that I think we need a proposal that allows for handles that are both ergonomic and explicit, it’s not hard to say that we should extend that solution to include the option for clone. The explicit capture clause post already fits this design. I explicitly chose a design that allowed for users to write or , and hence works equally well (or equally not well…) with both traits A number of people have pointed out doesn’t fit the Rust naming conventions for traits like this, which aim for short verbs. You can interpret as a verb, but it doesn’t mean what we want. Fair enough. I like the name because it gives a noun we can use to talk about, well, handles , but I agree that the trait name doesn’t seem right. There was a lot of bikeshedding on possible options but I think I’ve come back to preferring Jack Huey’s original proposal, (with a method ). I think and is my second favorite. Both of them are short, relatively common verbs. I originally felt that was a bit too generic and overly associated with sharing across threads – but then I at least always call a shared reference 1 , and an would implement , so it all seems to work well. Hat tip to Ariel Ben-Yehuda for pushing me on this particular name. The flurry of posts in this series have been an attempt to survey all the discussions that have taken place in this area. I’m not yet aiming to write a final proposal – I think what will come out of this is a series of multiple RFCs. My current feeling is that we should add the , uh, trait. I also think we should add explicit capture clauses . However, while explicit capture clauses are clearly “low-level enough for a kernel”, I don’t really think they are “usable enough for a GUI” . The next post will explore another idea that I think might bring us closer to that ultimate ergonomic and explicit goal. A lot of people say immutable reference but that is simply accurate: an is not immutable. I think that the term shared reference is better.  ↩︎ A lot of people say immutable reference but that is simply accurate: an is not immutable. I think that the term shared reference is better.  ↩︎

0 views
baby steps 4 months ago

Move, Destruct, Forget, and Rust

This post presents a proposal to extend Rust to support a number of different kinds of destructors. This means we could async drop, but also prevent “forgetting” (leaking) values, enabling async scoped tasks that run in parallel à la rayon/libstd. We’d also be able to have types whose “destructors” require arguments. This proposal – an evolution of “must move” that I’ll call “controlled destruction” – is, I think, needed for Rust to live up to its goal of giving safe versions of critical patterns in systems programming. As such, it is needed to complete the “async dream”, in which async Rust and sync Rust work roughly the same. Nothing this good comes for free. The big catch of the proposal is that it introduces more “core splits” into Rust’s types. I believe these splits are well motivated and reasonable – they reflect inherent complexity , in other words, but they are something we’ll want to think carefully about nonetheless. The TL;DR of the proposal is that we should: In a talk I gave some years back at Rust LATAM in Uruguay 1 , I said this : Rust currently does a pretty good job with preventing parts of your program from interfering with one another, but we don’t do as good a job when it comes to guaranteeing that cleaup happens 2 . We have destructors, of course, but they have two critical limitations: That motivation was fairly abstract, so let me give some concrete examples of things that tie back to this limitation: The goal of this post is to outline an approach that could solve all of the above problems and which is backwards compatible with Rust today. The core problem is that Rust today assumes that every value can be moved, dropped, and forgotten: The way I see, most methods are “opt-in” – they don’t execute unless you call them. But destructors are different. They are effectively a method that runs by default – unless you opt-out, e.g., by calling . But the ability to opt-out means that they don’t fundamentally add any power over regular methods, they just make for a more ergonomic API. The implication is that the only way in Rust today to guarantee that a destructor will run is to retain ownership of the value. This can be important to unsafe code – APIs that permit scoped threads, for example, need to guarantee that those parallel threads will be joined before the function returns. The only way they have to do that is to use a closure which gives -borrowed access to a : Because the API nevers gives up ownership of the scope, it can ensure that it is never “forgotten” and thus that its destructor runs. The scoped thread approach works for sync code, but it doesn’t work for async code. The problem is that async functions return a future, which is a value. Users can therefore decide to “forget” this value, just like any other value, and thus the destructor may never run. When you start poking around, you find that guaranteed destructors turn up quite a bit in systems programming. Scoped APIs in futures are one example, but DMA (direct memory access) is another. Many embedded devices have a mode where you begin a DMA transfer that causes memory to be written into memory asynchronously. But you need to ensure that this DMA is terminated before that memory is freed. If that memory is on your stack, that means you need a destructor that will either cancel or block until the DMA finishes. 4 This situation is very analogous to the challenge of revisiting the default bound, and I think the same basic approach that I outlined in [this blog post][sized] will work. The core of the idea is simple: have a “special” set of traits arranged in a hierarchy: By default, generic parameters get a bound, so is equivalent to . But if the parameter opts in to a weaker bound, then the default is suppressed, so means that is assumed by “destructible” but not forgettable. And indicates that can only be moved. Let me explain briefly how these bounds would work. Given a default type , or one that writes explicitly, the function can do anything that is possible today: The function would require as well: This means that if you have only , the function can only drop or move, it can’t “forget”: We would modify the function to require only : We would also extend the borrow checker so that when it sees a value being dropped (i.e., because it went out of scope), it would require the bound. That means that if you have a value whose type is only , you cannot “drop” it: This means that if you have only a bound, you must move anything you own if you want to return from the function. For example: If you have a function that does not move, you’ll get an error: It’s worth pointing out that this will be annoying as all get out in the face of panics: I’m ok with this, but it is going to put pressure on better ways to rule out panics statically. In fact, we are already doing something much like this destruct check for const functions. Right now if you have a const fn and you try to drop a value, you get an error: Compiling that gives you the error: This check is not presently taking place in borrow check but it could be. The final part of the check would be requiring that “moved” values implement : You might think that having types that are would replace the need for pin, but this is not the case. A pinned value is one that can never move again , whereas a value that is not can never be moved in the first place – at least once it is stored into a place. I’m not sure if this part of the proposal makes sense, we could start by just having all types be , , or (the default) . The other part of the proposal is that you should be able to explicit “opt out” from being forgettable, e.g. by doing Doing this will limit the generics that can accept your type, of course. The tough part with these “default bound” proposals is always associated type bounds. For backwards compatibility, we’d have to default to but a lot of associated types that exist in the wild today shouldn’t really require . For example a trait like should really just require for its return type: I am basically not too worried about this. It’s possible that we can weaken these bounds over time or through editions. Or, perhaps, add in some kind of edition-specific “alias” like where is implemented for everything that implements . I am not sure exactly how to manage it, but we’ll figure it out – and in the meantime, most of the types that should not be forgettable are really just “guard” types that don’t have to flow through quite so many places. The one place that I think it is really imporatnt that we weaken the associated type bounds is with closures– and, fortunately, that’s a place we can get away with due to the way our “closure trait bound” syntax works. I feel like I wrote a post on this before, but I can’t find it now, but the short version is that, today, when you write , that means that the closure must return . If you write , then this type must have been declared somewhere else, and so will (independently from the associated type of the trait) get a default bound. So since the associated type is not independently nameable in stable Rust, we can change its bounds, and code like this would continue to work unchanged: Recently I was pointed at this internals thread for a “substructural type system” which likely has very similar capabilities. To be totally honest, though, I haven’t had time to read and digest it yet! I had this blog post like 95% done though so I figured I’d post it first and then go try and compare. So, the system as I described would allow for ‘unmoveable’ types (i.e., a struct that opts out from everything and only permits ), but such a struct would only really be something you could store in a static memory location. You couldn’t put it on the stack because the stack must eventually get popped. And you couldn’t move it from place to place because, well, it’s immobile. This seems like something that could be useful – e.g., to model “video RAM” or something that lives in a specific location in memory and cannot live anywhere else – but it’s not a widespread need. I imagine something like this: With this setup, any function that owns a must eventually invoke . This is because no values of this type can be dropped, so they must be moved. This setup provides attacks a key problem that has blocked async drop in my mind, which is that types that are “async drop” do not have to implement “sync drop”. This gives the type system the ability to prevent them from being dropped in sync code, then, and it would mean that they can only be dropped in async drop. But there’s still lots of design work to be done there. This comes from the const generifs work. I don’t love it. But there is a logic to it. Right now, when you drop a struct or other value, that actually does a whole sequence of things, only one of which is running any impl – it also (for example) drops all the fields in the struct recursively, etc. The idea is that “destruct” refers to this whole sequence. I…don’t actually think it would be very hard. I’ve thought somewhat about it and all of the changes seem pretty straightforward. I would be keen to support a lang-team experiment on this. The whole topic of destructors and leaks and so forth datesback to approximately Rust 1.0, when we discovered that, in fact, our abstraction for threads was unsound when combined with cyclic ref-counted boxes. Before that we hadn’t fully internalized that destructors are “opt-out methods”. You can read this blog post I wrote at the time . At the time, the primary idea was to have some kind of bounds and it was tied to the idea of references (so that all data was assumed to be “leakable”, and hence something you could put into an ). I… mostly think we made the right call at the time. I think it’s good that most of the ecosystem is interoperable and that doesn’t require bounds, and certainly I think it’s good that we moved to 1.0 with minimal disruption. In any case, though, I rather prefer this design to the ones that were under discussion at the time, in part because it also addresses the need for different kinds of destructors and for destructors with many arguments and so forth, which wasn’t something we thought about then. I think that specifying the bounds you want is inherently better than today’s design, both because it’s easier to understand and because it allows us to backwards compatibly add traits in between in ways that are not possible with the design. However, I do see that having mean that does not hold is subtle. I wonder if we should adopt some kind of sigil or convention on these traits, like or something. I don’t know! Something to consider. That was a great conference. Also, interestingly, this is one of my favorite of all my talks, but for some reason, I rarely reuse this material. I should change that.  ↩︎ Academics distinguish “safety” from “liveness properties”, where safety means “bad things don’t happen” and “liveness” means “good things eventually happen”. Another way of saying this is that Rust’s type system helps with a lot of safety properties but struggles with liveness properties.  ↩︎ Uh, citation needed. I know this is true but I can’t find the relevant WebAssembly issue where it is discussed. Help, internet!  ↩︎ Really the DMA problem is the same as scoped threads. If you think about it, the embedded device writing to memory is basically the same as a parallel thread writing to memory.  ↩︎ Introduce a new “default trait bound” and an associated trait hierarchy: , representing values that can be forgotten , representing values with a destructor , representing values that can be moved , the base trait that represents any value Use the “opt-in to weaker defaults” scheme proposed for sizedness by RFC #3729 (Hierarchy of Sized Traits) So defaults to “a that can be forgotten/destructed/moved” And means “a that can be destructed, but not necessarily forgotten” And means “a that can be moved, but not necessarily forgotten” …and so forth. Integrate and enforce the new traits: The bound on will already require , so that’s good. Borrow check can enforce that any dropped value must implement ; in fact, we already do this to enforce bounds in . Borrow check can be extended to require a bound on any moved value. Adjust the trait bound on closures (luckily this works out fairly nicely) It’s easy to expose a high-performance API. But it’s hard to help users control it – and this is what Rust’s type system does. All destructors must meet the same signature, , which isn’t always adequate. There is no way to guarantee a destructor once you give up ownership of a value. The ability to have or drop, both of which require a distinct drop signature. The ability to have a “drop” operation that takes arguments, such as e.g. a message that must be sent, or a result code that must be provided before the program terminates. The ability to have async scopes that can access the stack, which requires a way to guarantee that a parallel thread will be joined even in an async context. The ability to integrate at maximum efficiency with WebAssembly async tasks, which require guaranteed cleanup. 3 That was a great conference. Also, interestingly, this is one of my favorite of all my talks, but for some reason, I rarely reuse this material. I should change that.  ↩︎ Academics distinguish “safety” from “liveness properties”, where safety means “bad things don’t happen” and “liveness” means “good things eventually happen”. Another way of saying this is that Rust’s type system helps with a lot of safety properties but struggles with liveness properties.  ↩︎ Uh, citation needed. I know this is true but I can’t find the relevant WebAssembly issue where it is discussed. Help, internet!  ↩︎ Really the DMA problem is the same as scoped threads. If you think about it, the embedded device writing to memory is basically the same as a parallel thread writing to memory.  ↩︎

0 views
baby steps 4 months ago

We need (at least) ergonomic, explicit handles

Continuing my discussion on Ergonomic RC, I want to focus on the core question: should users have to explicitly invoke handle/clone, or not? This whole “Ergonomic RC” work was originally proposed by Dioxus and their answer is simple: definitely not . For the kind of high-level GUI applications they are building, having to call to clone a ref-counted value is pure noise. For that matter, for a lot of Rust apps, even cloning a string or a vector is no big deal. On the other hand, for a lot of applications, the answer is definitely yes – knowing where handles are created can impact performance, memory usage, and even correctness (don’t worry, I’ll give examples later in the post). So how do we reconcile this? This blog argues that we should make it ergonomic to be explicit . This wasn’t always my position, but after an impactful conversation with Josh Triplett, I’ve come around. I think it aligns with what I once called the soul of Rust : we want to be ergonomic, yes, but we want to be ergonomic while giving control 1 . I like Tyler Mandry’s Clarity of purpose contruction, “Great code brings only the important characteristics of your application to your attention” . The key point is that there is great code in which cloning and handles are important characteristics , so we need to make that code possible to express nicely. This is particularly true since Rust is one of the very few languages that really targets that kind of low-level, foundational code. This does not mean we cannot (later) support automatic clones and handles. It’s inarguable that this would benefit clarity of purpose for a lot of Rust code. But I think we should focus first on the harder case, the case where explicitness is needed, and get that as nice as we can ; then we can circle back and decide whether to also support something automatic. One of the questions for me, in fact, is whether we can get “fully explicit” to be nice enough that we don’t really need the automatic version. There are benefits from having “one Rust”, where all code follows roughly the same patterns, where those patterns are perfect some of the time, and don’t suck too bad 2 when they’re overkill. I mentioned this blog post resulted from a long conversation with Josh Triplett 3 . The key phrase that stuck with me from that conversation was: Rust should not surprise you . The way I think of it is like this. Every programmer knows what its like to have a marathon debugging session – to sit and state at code for days and think, but… how is this even POSSIBLE? Those kind of bug hunts can end in a few different ways. Occasionally you uncover a deeply satisfying, subtle bug in your logic. More often, you find that you wrote and not . And occasionally you find out that your language was doing something that you didn’t expect. That some simple-looking code concealed a subltle, complex interaction. People often call this kind of a footgun . Overall, Rust is remarkably good at avoiding footguns 4 . And part of how we’ve achieved that is by making sure that things you might need to know are visible – like, explicit in the source. Every time you see a Rust match, you don’t have to ask yourself “what cases might be missing here” – the compiler guarantees you they are all there. And when you see a call to a Rust function, you don’t have to ask yourself if it is fallible – you’ll see a if it is. 5 So I guess the question is: would you ever have to know about a ref-count increment ? The trick part is that the answer here is application dependent. For some low-level applications, definitely yes: an atomic reference count is a measurable cost. To be honest, I would wager that the set of applications where this is true are vanishingly small. And even in those applications, Rust already improves on the state of the art by giving you the ability to choose between and and then proving that you don’t mess it up . But there are other reasons you might want to track reference counts, and those are less easy to dismiss. One of them is memory leaks. Rust, unlike GC’d languages, has deterministic destruction . This is cool, because it means that you can leverage destructors to manage all kinds of resources, as Yehuda wrote about long ago in his classic ode-to- RAII entitled “Rust means never having to close a socket” . But although the points where handles are created and destroyed is deterministic, the nature of reference-counting can make it much harder to predict when the underlying resource will actually get freed. And if those increments are not visible in your code, it is that much harder to track them down. Just recently, I was debugging Symposium , which is written in Swift. Somehow I had two instances when I only expected one, and each of them was responding to every IPC message, wreaking havoc. Poking around I found stray references floating around in some surprising places, which was causing the problem. Would this bug have still occurred if I had to write explicitly to increment the ref count? Definitely, yes. Would it have been easier to find after the fact? Also yes. 6 Josh gave me a similar example from the “bytes” crate . A type is a handle to a slice of some underlying memory buffer. When you clone that handle, it will keep the entire backing buffer around. Sometimes you might prefer to copy your slice out into a separate buffer so that the underlying buffer can be freed. It’s not that hard for me to imagine trying to hunt down an errant handle that is keeping some large buffer alive and being very frustrated that I can’t see explicitly in the where those handles are created. A similar case occurs with APIs like like 7 . takes an and, if the ref-count is 1, returns an . This lets you take a shareable handle that you know is not actually being shared and recover uniqueness. This kind of API is not frequently used – but when you need it, it’s so nice it’s there. Entering the conversation with Josh, I was leaning towards a design where you had some form of automated cloning of handles and an allow-by-default lint that would let crates which don’t want that turn it off. But Josh convinced me that there is a significant class of applications that want handle creation to be ergonomic AND visible (i.e., explicit in the source). Low-level network services and even things like Rust For Linux likely fit this description, but any Rust application that uses or might also. And this reminded me of something Alex Crichton once said to me. Unlike the other quotes here, it wasn’t in the context of ergonomic ref-counting, but rather when I was working on my first attempt at the “Rustacean Principles” . Alex was saying that he loved how Rust was great for low-level code but also worked well high-level stuff like CLI tools and simple scripts. I feel like you can interpret Alex’s quote in two ways, depending on what you choose to emphasize. You could hear it as, “It’s important that Rust is good for high-level use cases”. That is true, and it is what leads us to ask whether we should even make handles visible at all. But you can also read Alex’s quote as, “It’s important that there’s one language that works well enough for both ” – and I think that’s true too. The “true Rust gestalt” is when we manage to simultaneously give you the low-level control that grungy code needs but wrapped in a high-level package. This is the promise of zero-cost abstractions, of course, and Rust (in its best moments) delivers. Let’s be honest. High-level GUI programming is not Rust’s bread-and-butter, and it never will be; users will never confuse Rust for TypeScript. But then, TypeScript will never be in the Linux kernel. The goal of Rust is to be a single language that can, by and large, be “good enough” for both extremes. The goal is make enough low-level details visible for kernel hackers but do so in a way that is usable enough for a GUI. It ain’t easy, but it’s the job. This isn’t the first time that Josh has pulled me back to this realization. The last time was in the context of async fn in dyn traits, and it led to a blog post talking about the “soul of Rust” and a followup going into greater detail . I think the catchphrase “low-level enough for a Kernel, usable enough for a GUI” kind of captures it. There is a slight caveat I want to add. I think another part of Rust’s soul is preferring nuance to artificial simplicity (“as simple as possible, but no simpler”, as they say). And I think the reality is that there’s a huge set of applications that make new handles left-and-right (particularly but not exclusively in async land 8 ) and where explicitly creating new handles is noise, not signal. This is why e.g. Swift 9 makes ref-count increments invisible – and they get a big lift out of that! 10 I’d wager most Swift users don’t even realize that Swift is not garbage-collected 11 . But the key thing here is that even if we do add some way to make handle creation automatic, we ALSO want a mode where it is explicit and visible. So we might as well do that one first. OK, I think I’ve made this point 3 ways from Sunday now, so I’ll stop. The next few blog posts in the series will dive into (at least) two options for how we might make handle creation and closures more ergonomic while retaining explicitness. I see a potential candidate for a design axiom… rubs hands with an evil-sounding cackle and a look of glee   ↩︎ It’s an industry term .  ↩︎ Actually, by the standards of the conversations Josh and I often have, it was’t really all that long – an hour at most.  ↩︎ Well, at least sync Rust is. I think async Rust has more than its share, particularly around cancellation, but that’s a topic for another blog post.  ↩︎ Modulo panics, of course – and no surprise that accounting for panics is a major pain point for some Rust users.  ↩︎ In this particular case, it was fairly easy for me to find regardless, but this application is very simple. I can definitely imagine ripgrep’ing around a codebase to find all increments being useful, and that would be much harder to do without an explicit signal they are occurring.  ↩︎ Or , which is one of my favorite APIs. It takes an and gives you back mutable (i.e., unique) access to the internals, always! How is that possible, given that the ref count may not be 1? Answer: if the ref-count is not 1, then it clones it. This is perfect for copy-on-write-style code. So beautiful. 😍  ↩︎ My experience is that, due to language limitations we really should fix, many async constructs force you into bounds which in turn force you into and where you’d otherwise have been able to use .  ↩︎ I’ve been writing more Swift and digging it. I have to say, I love how they are not afraid to “go big”. I admire the ambition I see in designs like SwiftUI and their approach to async. I don’t think they bat 100, but it’s cool they’re swinging for the stands. I want Rust to dare to ask for more !  ↩︎ Well, not only that. They also allow class fields to be assigned when aliased which, to avoid stale references and iterator invalidation, means you have to move everything into ref-counted boxes and adopt persistent collections, which in turn comes at a performance cost and makes Swift a harder sell for lower-level foundational systems (though by no means a non-starter, in my opinion).  ↩︎ Though I’d also wager that many eventually find themselves scratching their heads about a ref-count cycle. I’ve not dug into how Swift handles those, but I see references to “weak handles” flying around, so I assume they’ve not (yet?) adopted a cycle collector. To be clear, you can get a ref-count cycle in Rust too! It’s harder to do since we discourage interior mutability, but not that hard.  ↩︎ I see a potential candidate for a design axiom… rubs hands with an evil-sounding cackle and a look of glee   ↩︎ It’s an industry term .  ↩︎ Actually, by the standards of the conversations Josh and I often have, it was’t really all that long – an hour at most.  ↩︎ Well, at least sync Rust is. I think async Rust has more than its share, particularly around cancellation, but that’s a topic for another blog post.  ↩︎ Modulo panics, of course – and no surprise that accounting for panics is a major pain point for some Rust users.  ↩︎ In this particular case, it was fairly easy for me to find regardless, but this application is very simple. I can definitely imagine ripgrep’ing around a codebase to find all increments being useful, and that would be much harder to do without an explicit signal they are occurring.  ↩︎ Or , which is one of my favorite APIs. It takes an and gives you back mutable (i.e., unique) access to the internals, always! How is that possible, given that the ref count may not be 1? Answer: if the ref-count is not 1, then it clones it. This is perfect for copy-on-write-style code. So beautiful. 😍  ↩︎ My experience is that, due to language limitations we really should fix, many async constructs force you into bounds which in turn force you into and where you’d otherwise have been able to use .  ↩︎ I’ve been writing more Swift and digging it. I have to say, I love how they are not afraid to “go big”. I admire the ambition I see in designs like SwiftUI and their approach to async. I don’t think they bat 100, but it’s cool they’re swinging for the stands. I want Rust to dare to ask for more !  ↩︎ Well, not only that. They also allow class fields to be assigned when aliased which, to avoid stale references and iterator invalidation, means you have to move everything into ref-counted boxes and adopt persistent collections, which in turn comes at a performance cost and makes Swift a harder sell for lower-level foundational systems (though by no means a non-starter, in my opinion).  ↩︎ Though I’d also wager that many eventually find themselves scratching their heads about a ref-count cycle. I’ve not dug into how Swift handles those, but I see references to “weak handles” flying around, so I assume they’ve not (yet?) adopted a cycle collector. To be clear, you can get a ref-count cycle in Rust too! It’s harder to do since we discourage interior mutability, but not that hard.  ↩︎

0 views
baby steps 4 months ago

SymmACP: extending Zed's ACP to support Composable Agents

This post describes SymmACP – a proposed extension to Zed’s Agent Client Protocol that lets you build AI tools like Unix pipes or browser extensions. Want a better TUI? Found some cool slash commands on GitHub? Prefer a different backend? With SymmACP, you can mix and match these pieces and have them all work together without knowing about each other. This is pretty different from how AI tools work today, where everything is a monolith – if you want to change one piece, you’re stuck rebuilding the whole thing from scratch. SymmACP allows you to build out new features and modes of interactions in a layered, interoperable way. This post explains how SymmACP would work by walking through a series of examples. Right now, SymmACP is just a thought experiment. I’ve sketched these ideas to the Zed folks, and they seemed interested, but we still have to discuss the details in this post. My plan is to start prototyping in Symposium – if you think the ideas I’m discussing here are exciting, please join the Symposium Zulip and let’s talk! I’m going to explain the idea of “composable agents” by walking through a series of features. We’ll start with a basic CLI agent 1 tool – basically a chat loop with access to some MCP servers so that it can read/write files and execute bash commands. Then we’ll show how you could add several features on top: The magic trick is that each of these features will be developed as separate repositories. What’s more, they could be applied to any base tool you want, so long as it speaks SymmACP. And you could also combine them with different front-ends, such as a TUI, a web front-end, builtin support from Zed or IntelliJ , etc. Pretty neat. My hope is that if we can centralize on SymmACP, or something like it, then we could move from everybody developing their own bespoke tools to an interoperable ecosystem of ideas that can build off of one another. SymmACP begins with ACP, so let’s explain what ACP is. ACP is a wonderfully simple protocol that lets you abstract over CLI agents. Imagine if you were using an agentic CLI tool except that, instead of communication over the terminal, the CLI tool communicates with a front-end over JSON-RPC messages, currently sent via stdin/stdout. When you type something into the GUI, the editor sends a JSON-RPC message to the agent with what you typed. The agent responds with a stream of messages containing text and images. If the agent decides to invoke a tool, it can request permission by sending a JSON-RPC message back to the editor. And when the agent has completed, it responds to the editor with an “end turn” message that says “I’m ready for you to type something else now”. OK, let’s tackle our first feature. If you’ve used a CLI agent, you may have noticed that they don’t know what time it is – or even what year it is. This may sound trivial, but it can lead to some real mistakes. For example, they may not realize that some information is outdated. Or when they do web searches for information, they can search for the wrong thing: I’ve seen CLI agents search the web for “API updates in 2024” for example, even though it is 2025. To fix this, many CLI agents will inject some extra text along with your prompt, something like . This gives the LLM the context it needs. So how could use ACP to build that? The idea is to create a proxy . This proxy would wrap the original ACP server: This proxy will take every “prompt” message it receives and decorate it with the date and time: Simple, right? And of course this can be used with any editor and any ACP-speaking tool. Let’s look at another feature that basically “falls out” from ACP: injecting personality. Most agents give you the ability to configure “context” in various ways – or what Claude Code calls memory . This is useful, but I and others have noticed that if what you want is to change how Claude “behaves” – i.e., to make it more collaborative – it’s not really enough. You really need to kick off the conversation by reinforcing that pattern. In Symposium, the “yiasou” prompt (also available as “hi”, for those of you who don’t speak Greek 😛) is meant to be run as the first thing in the conversation. But there’s nothing an MCP server can do to ensure that the user kicks off the conversation with or something similar. Of course, if Symposium were implemented as an ACP Server, we absolutely could do that: Some of you may be saying, “hmm, isn’t that what hooks are for?” And yes, you could do this with hooks, but there’s two problems with that. First, hooks are non-standard, so you have to do it differently for every agent. The second problem with hooks is that they’re fundamentally limited to what the hook designer envisioned you might want. You only get hooks at the places in the workflow that the tool gives you, and you can only control what the tool lets you control. The next feature starts to show what I mean: as far as I know, it cannot readily be implemented with hooks the way I would want it to work. Let’s move on to our next feature, long-running asynchronous tasks. This feature is going to have to go beyond the current capabilities of ACP into the expanded “SymmACP” feature set. Right now, when the server invokes an MCP tool, it executes in a blocking way. But sometimes the task it is performing might be long and complicated. What you would really like is a way to “start” the task and then go back to working. When the task is complete, you (and the agent) could be notified. This comes up for me a lot with “deep research”. A big part of my workflow is that, when I get stuck on something I don’t understand, I deploy a research agent to scour the web for information. Usually what I will do is ask the agent I’m collaborating with to prepare a research prompt summarizing the things we tried, what obstacles we hit, and other details that seem relevant. Then I’ll pop over to claude.ai or Gemini Deep Research and paste in the prompt. This will run for 5-10 minutes and generate a markdown report in response. I’ll download that and give it to my agent. Very often this lets us solve the problem. 2 This research flow works well but it is tedious and requires me to copy-and-paste. What I would ideally want is an MCP tool that does the search for me and, when the results are done, hands them off to the agent so it can start processing immediately. But in the meantime, I’d like to be able to continue working with the agent while we wait. Unfortunately, the protocol for tools provides no mechanism for asynchronous notifications like this, from what I can tell. So how would I do it with SymmACP? Well, I would want to extend the ACP protocol as it is today in two ways: In that case, we could implement our Research Proxy like so: What’s cool about this is that the proxy encapsulates the entire flow: it knows how to do the research, and it manages notifying the various participants when the research completes. (Also, this leans on one detail I left out, which is that ) Let’s explore our next feature, Q CLI’s mode . This feature is interesting because it’s a simple (but useful!) example of history editing. The way works is that, when you first type , Q CLI saves your current state. You can then continue as normal but when you next type , your state is restored to where you were. This, as the name suggests, lets you explore a side conversation without polluting your main context. The basic idea for supporting tangent in SymmACP is that the proxy is going to (a) intercept the tangent prompt and remember where it began; (b) allow the conversation to continue as normal; and then (c) when it’s time to end the tangent, create a new session and replay the history up until the point of the tangent 3 . You can almost implement “tangent” in ACP as it is, but not quite. In ACP, the agent always owns the session history. The editor can create a new session or load an older one; when loading an older one, the agent “replays” “replays” the events so that the editor can reconstruct the GUI. But there is no way for the editor to “replay” or construct a session to the agent . Instead, the editor can only send prompts, which will cause the agent to reply. In this case, what we want is to be able to say “create a new chat in which I said this and you responded that” so that we can setup the initial state. This way we could easily create a new session that contains the messages from the old one. So how this would work: One of the nicer features of Symposium is the ability to do interactive walkthroughs . These consist of an HTML sidebar as well as inline comments in the code: Right now, this is implemented by a kind of hacky dance: It works, but it’s a giant Rube Goldberg machine. With SymmACP, we would structure the passthrough mechanism as a proxy. Just as today, it would provide an MCP tool to the agent to receive the walkthrough markdown. It would then convert that into the HTML to display on the side along with the various comments to embed in the code. But this is where things are different. Instead of sending that content over IPC, what I would want to do is to make it possible for proxies to deliver extra information along with the chat. This is relatively easy to do in ACP as is, since it provides for various capabilities, but I think I’d want to go one step further I would have a proxy layer that manages walkthroughs. As we saw before, it would provide a tool. But there’d be one additional thing, which is that, beyond just a chat history, it would be able to convey additional state. I think the basic conversation structure is like: but I think it’d be useful to (a) be able to attach metadata to any of those things, e.g., to add extra context about the conversation or about a specific turn (or even a specific prompt ), but also additional kinds of events. For example, tool approvals are an event . And presenting a walkthrough and adding annotations are an event too. The way I imagine it, one of the core things in SymmACP would be the ability to serialize your state to JSON. You’d be able to ask a SymmACP paricipant to summarize a session. They would in turn ask any delegates to summarize and then add their own metadata along the way. You could also send the request in the other direction – e.g., the agent might present its state to the editor and ask it to augment it. This would mean a walkthrough proxy could add extra metadata into the chat transcript like “the current walkthrough” and “the current comments that are in place”. Then the editor would either know about that metadata or not. If it doesn’t, you wouldn’t see it in your chat. Oh well – or perhaps we do something HTML like, where there’s a way to “degrade gracefully” (e.g., the walkthrough could be presented as a regular “response” but with some metadata that, if you know to look, tells you to interpret it differently). But if the editor DOES know about the metadata, it interprets it specially, throwing the walkthrough up in a panel and adding the comments into the code. With enriched histories, I think we can even say that in SymmACP, the ability to load, save, and persist sessions itself becomes an extension, something that can be implemented by a proxy; the base protocol only needs the ability to conduct and serialize a conversation. Let me sketch out another feature that I’ve been noodling on that I think would be pretty cool. It’s well known that there’s a problem that LLMs get confused when there are too many MCP tools available. They get distracted. And that’s sensible, so would I, if I were given a phonebook-size list of possible things I could do and asked to figure something out. I’d probably just ignore it. But how do humans deal with this? Well, we don’t take the whole phonebook – we got a shorter list of categories of options and then we drill down. So I go to the File Menu and then I get a list of options, not a flat list of commands. I wanted to try building an MCP tool for IDE capabilities that was similar. There’s a bajillion set of things that a modern IDE can “do”. It can find references. It can find definitions. It can get type hints. It can do renames. It can extract methods. In fact, the list is even open-ended, since extensions can provide their own commands. I don’t know what all those things are but I have a sense for the kinds of things an IDE can do – and I suspect models do too. What if you gave them a single tool, “IDE operation”, and they could use plain English to describe what they want? e.g., . Hmm, this is sounding a lot like a delegate, or a sub-agent. Because now you need to use a second LLM to interpret that request – you probably want to do something like, give it a list of sugested IDE capabilities and the ability to find out full details and ask it to come up with a plan (or maybe directly execute the tools) to find the answer. As it happens, MCP has a capability to enable tools to do this – it’s called (somewhat oddly, in my opinion) “sampling”. It allows for “callbacks” from the MCP tool to the LLM. But literally nobody implements it, from what I can tell. 4 But sampling is kind of limited anyway. With SymmACP, I think you could do much more interesting things. The key is that ACP already permits a single agent to “serve up” many simultaneous sessions. So that means that if I have a proxy, perhaps one supplying an MCP tool definition, I could use it to start fresh sessions – combine that with the “history replay” capability I mentioned above, and the tool can control exactly what context to bring over into that session to start from, as well, which is very cool (that’s a challenge for MCP servers today, they don’t get access to the conversation history). Ok, this post sketched a variant on ACP that I call SymmACP. SymmACP extends ACP with Most of these are modest extensions to ACP, in my opinion, and easily doable in a backwards fashion just by adding new capabilities. But together they unlock the ability for anyone to craft extensions to agents and deploy them in a composable way. I am super excited about this. This is exactly what I wanted Symposium to be all about. It’s worth noting the old adage: “with great power, comes great responsibility”. These proxies and ACP layers I’ve been talking about are really like IDE extensions. They can effectively do anything you could do. There are obvious security concerns. Though I think that approaches like Microsoft’s Wassette are key here – it’d be awesome to have a “capability-based” notion of what a “proxy layer” is, where everything compiles to WASM, and where users can tune what a given proxy can actually do . I plan to start sketching a plan to drive this work in Symposium and elsewhere. My goal is to have a completely open and interopable client, one that can be based on any agent (including local ones) and where you can pick and choose which parts you want to use. I expect to build out lots of custom functionality to support Rust development (e.g., explaining and diagnosting trait errors using the new trait solver is high on my list…and macro errors…) but also to have other features like walkthroughs, collaborative interaction style, etc that are all language independent – and I’d love to see language-focused features for other langauges, especially Python and TypeScript (because “the new trifecta” ) and Swift and Kotlin (because mobile). If that vision excites you, come join the Symposium Zulip and let’s chat! One question I’ve gotten when discussing this is how it compares to the other host of protocols out there. Let me give a brief overview of the related work and how I understand its pros and cons: Everybody uses agents in various ways. I like Simon Willison’s “agents are models using tools in a loop” definition; I feel that an “agentic CLI tool” fits that definition, it’s just that part of the loop is reading input from the user. I think “fully autonomous” agents are a subset of all agents – many agent processes interact with the outside world via tools etc. From a certain POV, you can view the agent “ending the turn” as invoking a tool for “gimme the next prompt”.  ↩︎ Research reports are a major part of how I avoid hallucination. You can see an example of one such report I commissioned on the details of the Language Server Protocol here ; if we were about to embark on something that required detailed knowledge of LSP, I would ask the agent to read that report first.  ↩︎ Alternatively: clear the session history and rebuild it, but I kind of prefer the functional view of the world, where a given session never changes.  ↩︎ I started an implementation for Q CLI but got distracted – and, for reasons that should be obvious, I’ve started to lose interest.  ↩︎ Yes, you read that right. There is another ACP. Just a mite confusing when you google search. =)  ↩︎ Addressing time-blindness by helping the agent know what time it is. Injecting context and “personality” to the agent. Spawning long-running, asynchronous tasks. A copy of Q CLI’s mode that lets you do a bit of “off the books” work that gets removed from your history later. Implementing Symposium’s interactive walkthroughs , which give the agent a richer vocabulary for communicating with you than just text. Smarter tool delegation. I’d like the ACP proxy to be able to provide tools that the proxy will execute. Today, the agent is responsible for executing all tools; the ACP protocol only comes into play when requesting permission . But it’d be trivial to have MCP tools where, to execute the tool, the agent sends back a message over ACP instead. I’d like to have a way for the agent to initiate responses to the editor . Right now, the editor always initiatives each communication session with a prompt; but, in this case, the agent might want to send messages back unprompted. The agent invokes an MCP tool and sends it the walkthrough in markdown. This markdown includes commands meant to be placed on particular lines, identified not by line number (agents are bad at line numbers) but by symbol names or search strings. The MCP tool parses the markdown, determines the line numbers for comments, and creates HTML. It sends that HTML over IPC to the VSCode extension. The VSCode receives the IPC message, displays the HTML in the sidebar, and creates the comments in the code. Conversation Turn User prompt(s) – could be zero or more Response(s) – could be zero or more Tool use(s) – could be zero or more the ability for either side to provide the initial state of a conversation, not just the server the ability for an “editor” to provide an MCP tool to the “agent” the ability for agents to respond without an initial prompt the ability to serialize conversations and attach extra state (already kind of present) Model context protocol (MCP) : The queen of them all. A protocol that provides a set of tools, prompts, and resources up to the agent. Agents can invoke tools by supplying appropriate parameters, which are JSON. Prompts are shorthands that users can invoke using special commands like or , they are essentially macros that expand “as if the user typed it” (but they can also have parameters and be dynamically constructed). Resources are just data that can be requested. MCP servers can either be local or hosted remotely. Remote MCP has only recently become an option and auth in particular is limited. Comparison to SymmACP: MCP provides tools that the agent can invoke. SymmACP builds on it by allowing those tools to be provided by outer layers in the proxy chain. SymmACP is oriented at controlling the whole chat “experience”. Zed’s Agent Client Protocol (ACP) : The basis for SymmACP. Allows editors to create and manage sessions. Focused only on local sessions, since your editor runs locally. Comparison to SymmACP: That’s what this post is all about! SymmACP extends ACP with new capabilities that let intermediate layers manipulate history, provide tools, and provide extended data upstream to support richer interaction patterns than jus chat. PS I expect we may want to support more remote capabilities, but it’s kinda orthogonal in my opinion (e.g., I’d like to be able to work with an agent running over in a cloud-hosted workstation, but I’d probably piggyback on ssh for that). Google’s Agent-to-Agent Protocol (A2A) and IBM’s Agent Communication Protocol (ACP) 5 : From what I can tell, Google’s “agent-to-agent” protocol is kinda like a mix of MCP and OpenAPI. You can ping agents that are running remotely and get them to send you “agent cards”, which describe what operations they can perform, how you authenticate, and other stuff like that. It looks to me quite similar to MCP except that it has richer support for remote execution and in particular supports things like long-running communication, where an agent may need to go off and work for a while and then ping you back on a webhook. Comparison to MCP: To me, A2A looks like a variant of MCP that is more geared to remote execution. MCP has a method for tool discovery where you ping the server to get a list of tools; A2A has a similar mechanism with Agent Cards. MCP can run locally, which A2A cannot afaik, but A2A has more options about auth. MCP can only be invoked synchronously, whereas A2A supports long-running operations, progress updates, and callbacks. It seems like the two could be merged to make a single whole. Comparison to SymmACP: I think A2A is orthogonal from SymmACP. A2A is geared to agents that provide services to one another. SymmACP is geared towards building new development tools for interacting with agents. It’s possible you could build something like SymmACP on A2A but I don’t know what you would really gain by it (and I think it’d be easy to do later). Everybody uses agents in various ways. I like Simon Willison’s “agents are models using tools in a loop” definition; I feel that an “agentic CLI tool” fits that definition, it’s just that part of the loop is reading input from the user. I think “fully autonomous” agents are a subset of all agents – many agent processes interact with the outside world via tools etc. From a certain POV, you can view the agent “ending the turn” as invoking a tool for “gimme the next prompt”.  ↩︎ Research reports are a major part of how I avoid hallucination. You can see an example of one such report I commissioned on the details of the Language Server Protocol here ; if we were about to embark on something that required detailed knowledge of LSP, I would ask the agent to read that report first.  ↩︎ Alternatively: clear the session history and rebuild it, but I kind of prefer the functional view of the world, where a given session never changes.  ↩︎ I started an implementation for Q CLI but got distracted – and, for reasons that should be obvious, I’ve started to lose interest.  ↩︎ Yes, you read that right. There is another ACP. Just a mite confusing when you google search. =)  ↩︎

0 views
baby steps 4 months ago

The Handle trait

There’s been a lot of discussion lately around ergonomic ref-counting. We had a lang-team design meeting and then a quite impactful discussion at the RustConf Unconf. I’ve been working for weeks on a follow-up post but today I realized what should’ve been obvious from the start – that if I’m taking that long to write a post, it means the post is too damned long. So I’m going to work through a series of smaller posts focused on individual takeaways and thoughts. And for the first one, I want to (a) bring back some of the context and (b) talk about an interesting question, what should we call the trait . My proposal, as the title suggests, is – but I get ahead of myself. For those of you who haven’t been following, there’s been an ongoing discussion about how best to have ergonomic ref counting: The focus of this blog post is on one particular question: what should we call “The Trait”. In virtually every design, there has been some kind of trait that is meant to identify something . But it’s been hard to get a handle 1 on what precisely that something is. What is this trait for and what types should implement it? Some things are clear: whatever The Trait is, and should implement it, for example, but that’s about it. My original proposal was for a trait named that was meant to convey a “lightweight clone” – but really the trait was meant to replace as the definition of which clones ought to be explicit 2 . Jonathan Kelley had a similar proposal but called it . In RFC #3680 the proposal was to call the trait . The details and intent varied, but all of these attempts had one thing in common: they were very operational . That is, the trait was always being defined in terms of what it does (or doesn’t do) but not why it does it. And that I think will always be a weak grounding for a trait like this, prone to confusion and different interpretations. For example, what is a “lightweight” clone? Is it O(1)? But what about things that are O(1) with very high probability? And of course, O(1) doesn’t mean cheap – it might copy 22GB of data every call. That’s O(1). What you want is a trait where it’s fairly clear when it should and should not be implemented and not based on taste or subjective criteria. And and friends did not meet the bar: in the Unconf, several new Rust users spoke up and said they found it very hard, based on my explanations, to judge whether their types ought to implement The Trait (whatever we call it). That has also been a persitent theme from the RFC and elsewhere. But really there is a semantic underpinning here, and it was Jack Huey who first suggested it. Consider this question. What are the differences between cloning a and a ? One difference, of course, is cost. Cloning the will deep-clone the vector, cloning the will just increment a referece count. But the more important difference is what I call “entanglement” . When you clone the , you don’t get a new value – you get back a second handle to the same value . 3 Knowing which values are “entangled” is key to understanding what your program does. A big part of how the borrow checker 4 achieves reliability is by reducing “entaglement”, since it becomes a relative pain to work with in Rust. Consider the following code. What will be the value of and ? The answer, of course, is “depends on the type of ”. If is a , then . But if is, say, a struct like this one: There are many types that act like a : it’s true for and , of course, but also for things like and channel endpoints like . All of these are examples of “handles” to underlying values and, when you clone them, you get back a second handle that is indistinguishable from the first one. Jack’s insight was that we should focus on the semantic concept (sharing) and not on the operational details (how it’s implemented). This makes it clear when the trait ought to be implemented. I liked this idea a lot, although I eventually decided I didn’t like the name . The word isn’t specific enough, I felt, and users might not realize it referred to a specific concept: “shareable types” doesn’t really sound right. But in fact there is a name already in common use for this concept: handles (see e.g. ). This is how I arrived at my proposed name and definition for The Trait, which is : 5 The trait includes a method which is always equivalent to . The purpose of this method is to signal to the reader that the result is a second handle to the same underlying value. Once the trait exists, we should lint on calls to when the receiver is known to implement and encourage folks to call instead: Compare the above to the version that the lint suggests, using , and I think you will get an idea for how increases clarity of what is happening: The defining characteristic of a handle is that it, when cloned, results in a second value that accesses the same underlying value. This means that the two handles are “entangled”, with interior mutation that affects one handle showing up in the other. Reflecting this, most handles have APIs that consist exclusively or almost exclusively of methods, since having unique access to the handle does not necessarily give you unique access to the value . Handles are generally only significant, semantically, when interior mutability is involved. There’s nothing wrong with having two handles to an immutable value, but it’s not generally distinguishable from two copies of the same value. This makes persistent collections an interesting grey area: I would probably implement for something like , particularly since something like a would make entaglement visible, but I think there’s an argument against it. In the stdlib, handle would be implemented for exactly one type (the others are values): It would be implemented for ref-counted pointers (but not ): And it would be implemented for types like channel endpoints, that are implemented with a ref-counted value under the hood: OK, I’m going to stop there with this “byte-sized” blog post. More to come! But before I go, let me layout what I believe to be a useful “design axiom” that we should adopt for this design: Expose entanglement. Understanding the difference between a handle to an underlying value and the value itself is necessary to understand how Rust works. The phrasing feels a bit awkward, but I think it is the key bit anyway. That. my friends, is foreshadowing . Damn I’m good.  ↩︎ I described as a kind of “lightweight clone” but in the Unconf someone pointed out that “heavyweight copy” was probably a better description of what I was going for.  ↩︎ And, not coincidentally, the types where cloning leads to entanglement tend to also be the types where cloning is cheap.  ↩︎ and functional programming…  ↩︎ The “final” keyword was proposed by Josh Triplett in RFC 3678. It means that impls cannot change the definition of . There’s been some back-and-forth on whether it ought to be renamed or made more general or what have you; all I know is, I find it an incredibly useful concept for cases like this, where you want users to be able to opt-in to a method being available but not be able to change what it does. You can do this in other ways, they’re just weirder.  ↩︎ It began with the first Rust Project Goals program in 2024H2, where Jonathan Kelley from Dioxus wrote a thoughtful blog post about a path to high-level Rust that eventually became a 2024H2 project goal towards ergonomic ref-counting . I wrote a series of blog posts about a trait I called . Josh and I talked and Josh opened RFC #3680 , which proposed a keyword and closures. Reception, I would say, was mixed; yes, this is tackling a real problem, but there were lots of concerns on the approach. I summarized the key points here . Santiago implemented experimental support for (a variant of) RFC #3680 as part of the 2025H1 project goal . I authored a 2025H2 project goal proposing that we create an alternative RFC focused on higher-level use-cases which prompted Josh and I have to have a long and fruitful conversation in which he convinced me that this was not the right approach. We had a lang-team design meeting on 2025-08-27 in which I presented this survey and summary of the work done thus far . And then at the RustConf 2025 Unconf we had a big group discussion on the topic that I found very fruitful, as well as various follow-up conversations with smaller groups. That. my friends, is foreshadowing . Damn I’m good.  ↩︎ I described as a kind of “lightweight clone” but in the Unconf someone pointed out that “heavyweight copy” was probably a better description of what I was going for.  ↩︎ And, not coincidentally, the types where cloning leads to entanglement tend to also be the types where cloning is cheap.  ↩︎ and functional programming…  ↩︎ The “final” keyword was proposed by Josh Triplett in RFC 3678. It means that impls cannot change the definition of . There’s been some back-and-forth on whether it ought to be renamed or made more general or what have you; all I know is, I find it an incredibly useful concept for cases like this, where you want users to be able to opt-in to a method being available but not be able to change what it does. You can do this in other ways, they’re just weirder.  ↩︎

0 views
baby steps 5 months ago

Symposium: exploring new AI workflows

This blog post gives you a tour of Symposium , a wild-and-crazy project that I’ve been obsessed with over the last month or so. Symposium combines an MCP server, a VSCode extension, an OS X Desktop App, and some mindful prompts to forge new ways of working with agentic CLI tools. Symposium is currently focused on my setup, which means it works best with VSCode, Claude, Mac OS X, and Rust. But it’s meant to be unopinionated, which means it should be easy to extend to other environments (and in particular it already works great with other programming languages). The goal is not to compete with or replace those tools but to combine them together into something new and better. In addition to giving you a tour of Symposium, this blog post is an invitation: Symposium is an open-source project , and I’m looking for people to explore with me! If you are excited about the idea of inventing new styles of AI collaboration, join the symposium-dev Zulip . Let’s talk! I’m not normally one to watch videos online. But in this particular case, I do think a movie is going to be worth 1,000,000 words. Therefore, I’m embedding a short video (6min) demonstrating how Symposium works below. Check it out! But don’t worry, if videos aren’t your thing, you can just read the rest of the post instead. Alternatively, if you really love videos, you can watch the first version I made, which went into more depth . That version came in at 20 minutes, which I decided was…a bit much. 😁 The Symposium story begins with , an OS X desktop application for managing taskspaces . A taskspace is a clone of your project 1 paired with an agentic CLI tool that is assigned to complete some task. My observation has been that most people doing AI development spend a lot of time waiting while the agent does its thing. Taskspaces let you switch quickly back and forth. Before I was using taskspaces, I was doing this by jumping between different projects. I found that was really hurting my brain from context switching. But jumping between tasks in a project is much easier. I find it works best to pair a complex topic with some simple refactorings. Here is what it looks like to use Symposium: Each of those boxes is a taskspace. It has both its own isolated directory on the disk and an associated VSCode window. When you click on the taskspace, the app brings that window to the front. It can also hide other windows by positioning them exactly behind the first one in a stack 2 . So it’s kind of like a mini window manager. Within each VSCode window, there is a terminal running an agentic CLI tool that has the Symposium MCP server . If you’re not familiar with MCP, it’s a way for an LLM to invoke custom tools; it basically just gives the agent a list of available tools and a JSON scheme for what arguments they expect. The Symposium MCP server does a bunch of things–we’ll talk about more of them later–but one of them is that it lets the agent interact with taskspaces. The agent can use the MCP server to post logs and signal progress (you can see the logs in that screenshot); it can also spawn new taskspaces. I find that last part very handy. It often happens to me that while working on one idea, I find opportunities for cleanups or refactorings. Nowadays I just spawn out a taskspace with a quick description of the work to be done. Next time I’m bored, I can switch over and pick that up. It’s probably worth mentioning that the Symposium app is written in Swift. I did not know Swift three weeks ago. But I’ve now written about 6K lines and counting. I feel like I’ve got a pretty good handle on how it works. 3 Well, it’d be more accurate to say that I have reviewed about 6K lines, since most of the time Claude generates the code. I mostly read it and offer suggestions for improvement 4 . When I do dive in and edit the code myself, it’s interesting because I find I don’t have the muscle memory for the syntax. I think this is pretty good evidence for the fact that agentic tools help you get started in a new programming language. So, while taskspaces let you jump between tasks, the rest of Symposium is dedicated to helping you complete an individual task. A big part of that is trying to go beyond the limits of the CLI interface by connecting the agent up to the IDE. For example, the Symposium MCP server has a tool called which lets the agent present you with a markdown document that explains how some code works. These walkthroughs show up in a side panel in VSCode: As you can see, the walkthroughs can embed mermaid, which is pretty cool. It’s sometimes so clarifying to see a flowchart or a sequence diagram. Walkthroughs can also embed comments , which are anchored to particular parts of the code. You can see one of those in the screenshot too, on the right. Each comment has a Reply button that lets you respond to the comment with further questions or suggest changes; you can also select random bits of text and use the “code action” called “Discuss in Symposium”. Both of these take you back to the terminal where your agent is running. They embed a little bit of XML ( ) and then you can just type as normal. The agent can then use another MCP tool to expand that reference to figure out what you are referring to or what you are replying to. To some extent, this “reference the thing I’ve selected” functionality is “table stakes”, since Claude Code already does it. But Symposium’s version works anywhere (Q CLI doesn’t have that functionality, for example) and, more importantly, it lets you embed multiple refrences at once. I’ve found that to be really useful. Sometimes I’ll wind up with a message that is replying to one comment while referencing two or three other things, and the system lets me do that no problem. Symposium also includes an tool that lets the agent connect to the IDE to do things like “find definitions” or “find references”. To be honest I haven’t noticed this being that important (Claude is surprisingly handy with awk/sed) but I also haven’t done much tinkering with it. I know there are other MCP servers out there too, like Serena , so maybe the right answer is just to import one of those, but I think there’s a lot of interesting stuff we could do here by integrating deeper knowledge of the code, so I have been trying to keep it “in house” for now. Continuing our journey down the stack, let’s look at one more bit of functionality, which are MCP tools aimed at making agents better at working with Rust code. By far the most effective of these so far is one I call . It is very simple: given the name of a crate, it just checks out the code into a temporary directory for the agent to use. Well, actually, it does a bit more than that. If the agent supplies a search string, it also searches for that string so as to give the agent a “head start” in finding the relevant code, and it makes a point to highlight code in the examples directory in particular. My experience has been that this tool makes all the difference. Without it, Claude just geneates plausible-looking APIs that don’t really exist. With it, Claude generally figures out exactly what to do. But really it’s just scratching the surface of what we can do. I am excited to go deeper here now that the basic structure of Symposium is in place – for example, I’d love to develop Rust-specific code reviewers that can critique the agent’s code or offer it architectural advice 5 , or a tool like CWhy to help people resolve Rust trait errors or macro problems. But honestly what I’m most excited about is the idea of decentralizing . I want Rust library authors to have a standard way to attach custom guidance and instructions that will help agents use their library. I want an AI-enhanced variant of that automatically bridges over major versions, making use of crate-supplied metadata about what changed and what rewrites are needed. Heck, I want libraries to be able to ship with MCP servers implemented in WASM ( Wassette , anyone?) so that Rust developers using that library can get custom commands and tools for working with it. I don’t 100% know what this looks like but I’m keen to explore it. If there’s one thing I’ve learned from Rust, it’s always bet on the ecosystem. One of the things I am very curious to explore is how we can use agents to help humans collaborate better. It’s oft observed that coding with agents can be a bit lonely 6 . But I’ve also noticed that structuring a project for AI consumption requires relatively decent documentation. For example, one of the things I did recently for Symposium was to create a Request for Dialogue (RFD) process – a simplified version of Rust’s RFC process. My motivation was partly in anticipation of trying to grow a community of contributors, but it was also because most every major refactoring or feature work I do begins with iterating on docs. The doc becomes a central tracking record so that I can clear the context and rest assured that I can pick up where I left off. But a nice side-effect is that the project has more docs than you might expect, considering, and I hope that will make it easier to dive in and get acquainted. And what about other things? Like, I think that taskspaces should really be associated with github issues. If we did that, could we do a better job at helping new contributors pick up an issue? Or at providing mentoring instructions to get started? What about memory? I really want to add in some kind of automated memory system that accumulates knowledge about the system more automatically. But could we then share that knowledge (or a subset of it) across users, so that when I go to hack on a project, I am able to “bootstrap” with the accumulated observations of other people who’ve been working on it? Can agents help in guiding and shepherding design conversations? At work, when I’m circulating a document, I will typically download a copy of that document with people’s comments embedded in it. Then I’ll use pandoc to convert that into Markdown with HTML comments and then ask Claude to read it over and help me work through the comments systematically. Could we do similar things to manage unwieldy RFC threads? This is part of what gets me excited about AI. I mean, don’t get me wrong. I’m scared too. There’s no question that the spread of AI will change a lot of things in our society, and definitely not always for the better. But it’s also a huge opportunity. AI is empowering! Suddenly, learning new things is just vastly easier. And when you think about the potential for integrating AI into community processes, I think that it could easily be used to bring us closer together and maybe even to make progress on previously intractable problems in open-source 7 . As I said in the beginning, this post is two things. Firstly, it’s an advertisement for Symposium. If you think the stuff I described sounds cool, give Symposium a try! You can find installation instructions here. I gotta warn you, as of this writing, I think I’m the only user, so I would not at all be surprised to find out that there’s bugs in setup scripts etc. But hey, try it out, find bugs and tell me about them! Or better yet, fix them! But secondly, and more importantly, this blog post is an invitation to come out and play 8 . I’m keen to have more people come and hack on Symposium. There’s so much we could do! I’ve identified a number of “good first issue” bugs . Or, if you’re keen to take on a larger project, I’ve got a set of invited “Request for Dialogue” projects you could pick up and make your own. And if none of that suits your fancy, feel free to pitch you own project – just join the Zulip and open a topic! Technically, a git worktree.  ↩︎ That’s what the “Stacked” box does; if you uncheck it, the windows can be positioned however you like. I’m also working on a tiled layout mode.  ↩︎ Well, mostly. I still have some warnings about something or other not being threadsafe that I’ve been ignoring. Claude assures me they are not a big deal (Claude can be so lazy omg).  ↩︎ Mostly: “Claude will you please for the love of God stop copying every function ten times.”  ↩︎ E.g., don’t use a tokio mutex you fool, use an actor . That is one particular bit of advice I’ve given more than once.  ↩︎ I’m kind of embarassed to admit that Claude’s dad jokes have managed to get a laugh out of me on occassion, though.  ↩︎ Narrator voice: burnout. he means maintainer burnout.   ↩︎ Tell me you went to high school in the 90s without telling me you went to high school in the 90s.  ↩︎

0 views
baby steps 7 months ago

Rust, Python, and TypeScript: the new trifecta

You heard it here first: my guess is that Rust, Python, and TypeScript are going to become the dominant languages going forward (excluding the mobile market, which has extra wrinkles). The argument is simple. Increasing use of AI coding is going to weaken people’s loyalty to programming languages, moving it from what is often a tribal decision to one based on fundamentals. And the fundamentals for those 3 languages look pretty strong to me: Rust targets system software or places where efficiency is paramount. Python brings a powerful ecosystem of mathematical and numerical libraries to bear and lends itself well to experimentation and prototyping. And TypeScript, of course, is compiled to JavaScript which runs natively on browsers and the web and a number of other areas. And all of them, at least if setup properly, offer strong static typing and the easy use of dependencies. Let’s walk through the argument point by point. Building with an LLM is presently a rather uneven experience, but I think the long-term trend is clear enough. We are seeing a shift towards a new programming paradigm. Dave Herman and I have recently taken to calling it idea-oriented programming . As the name suggests, idea-oriented programming is programming where you are focused first and foremost on ideas behind your project . Why do I say idea-oriented programming and not vibe coding ? To me, they are different beasts. Vibe coding suggests a kind of breezy indifference to the specifics – kind of waving your hand vaguely at the AI and saying “do something like this”. That smacks of treating the AI like a genie – or perhaps a servant, neither of which I think is useful. Idea-oriented programming, in contrast, is definitely programming . But your role is different. As the programmer, you’re more like the chief architect. Your coding tools are like your apprentices. You are thinking about the goals and the key aspects of the design. You lay out a crisp plan and delegate the heavy lifting to the tools – and then you review their output, making tweaks and, importantly, generalizing those tweaks into persistent principles. When some part of the problem gets tricky, you are rolling up your sleeves and do some hands-on debugging and problem solving. If you’ve been in the industry a while, this description will be familiar. It’s essentially the role of a Principal Engineer. It’s also a solid description of what I think an open-source mentor ought to do. In the past, when I built software projects, I would default to Rust. It’s not that Rust is the best choice for everything. It’s that I know Rust best, and so I move the fastest when I use it. I would only adopt a different language if it offered a compelling advantage (or of course if I just wanted to try a new language, which I do enjoy). But when I’m buiding things with an AI assistant, I’ve found I think differently. I’m thinking more about what libraries are available, what my fundamental performance needs are, and what platforms I expect to integrate with. I want things to be as straightforward and high-level as I can get them, because that will give the AI the best chance of success and minimize my need to dig in. The result is that I wind up with a mix of Python (when I want access to machine-learning libraries), TypeScript (when I’m building a web app, VSCode Extension, or something else where the native APIs are in TypeScript), and Rust otherwise. Why Rust as the default? Well, I like it of course, but more importantly I know that its type system will catch errors up front and I know that its overall design will result in performant code that uses relatively little memory. If I am then going to run that code in the cloud, that will lower my costs, and if I’m running it on my desktop, it’ll give more RAM for Microsoft Outlook to consume. 1 LLMs kind of turn the tables on what we expect from a computer. Typical computers can cross-reference vast amounts of information and perform deterministic computations lightning fast, but falter with even a whiff of ambiguity. LLMs, in contrast, can be surprisingly creative and thoughtful, but they have limited awareness of things that are not right in front of their face, unless they correspond to some pattern that is ingrained from training. They’re a lot more like humans that way. And the technologies we have for dealing with that, like RAG or memory MCP servers, are mostly about trying to put things in front of their face that they might find useful. But of course programmers have evolved a way to cope with human’s narrow focus: type systems, and particularly advanced type systems. Basic type systems catch small mistakes, like arguments of the wrong type. But more advanced type systems, like the ones in Rust and TypeScript, also capture domain knowledge and steer you down a path of success: using a Rust enum, for example, captures both which state your program is in and the data that is relevant to that state. This means that you can’t accidentally read a field that isn’t relevant at the moment. This is important for you, but it’s even more important for your AI collaborator(s), because they don’t have the comprehensive memory that you do, and are quite unlikely to remember those kind of things. Notably, Rust, TypeScript, and Python all have pretty decent type systems. For Python you have to set things up to use mypy and pydantic. Ecosystems and package managers are also hugely important to idea-oriented programming. Of course, having a powerful library to build on has always been an accelerator, but it also used to come with a bigger downside, because you had to take the time to get fluent in how the library works. That is much less of an issue now. For example, I have been building a family tree application 2 to use with my family. I wanted to add graphical rendering. I talked out the high-level ideas but I was able to lean on Claude to manage the use of the d3 library – it turned out beautifully! Notably, Rust, TypeScript, and Python all have pretty decent package managers – , , and respectively (both TS and Python have other options, I’ve not evaluated those in depth). In 2016, Aaron Turon and I gave a RustConf keynote advocating for the Ergonomics Initiative . Our basic point was that there were (and are) a lot of errors in Rust that are simple to solve – but only if you know the trick. If you don’t know the trick, they can be complete blockers, and can lead you to abandon the language altogether, even if the answer to your problem was just add a in the right place. In Rust, we’ve put a lot of effort into addressing those, either by changing the language or, more often, by changing our error messages to guide you to success. What I’ve observed is that, with Claude, the calculus is different. Some of these mistakes it simply never makes. Others it makes but then, based on the error message, is able to quickly correct. And this is fine. If I were writing the code by hand, I get annoyed having to apply the same repetitive changes over and over again (add , ok, no, take it away, etc etc). But if Claude is doing, I don’t care so much, and maybe I get some added benefit – e.g., now I have a clearer indicating of which variables are declared as . But all of this only works if Claude can fix the problems – either because it knows from training or because the errors are good enough to guide it to success. One thing I’m very interested in, though, is that I think we now have more room to give ambiguous guidance (e.g., here are 3 possible fixes, but you have to decide which is best), and have the LLM navigate it. The bottom line is that what enables ideas-oriented programming isn’t anything fundamentally new . But previously to work this way you had to be a Principal Engineer at a big company. In that case, you could let junior engineers sweat it out, reading the docs, navigating the error messages. Now the affordances are all different, and that style of work is much more accessible. Of course, this does raise some questions. Part of what makes a PE a PE is that they have a wealth of experience to draw on. Can a young engineer do that same style of work? I think yes, but it’s going to take some time to find the best way to teach people that kind of judgment. It was never possible before because the tools weren’t there. It’s also true that this style of working means you spend less time in that “flow state” of writing code and fitting the pieces together. Some have said this makes coding “boring”. I don’t find that to be true. I find that I can have a very similar – maybe even better – experience by brainstorming and designing with Claude, writing out my plans and RFCs. A lot of the tedium of that kind of ideation is removed since Claude can write up the details, and I can focus on how the big pieces fit together. But this too is going to be an area we explore more over time. Amazon is migrating to M365, but at the moment, I still receive my email via a rather antiquated Exchange server. I count it a good day if the mail is able to refresh at least once that day, usually it just stalls out.  ↩︎ My family bears a striking resemblance to the family in My Big Fat Greek Wedding. There are many relatives that I consider myself very close to and yet have basically no idea how we are actually related (well, I didn’t, until I setup my family tree app).  ↩︎

0 views
baby steps 7 months ago

You won't believe what this AI said after deleting a database (but you might relate)

Recently someone forwarded me a PCMag article entitled “Vibe coding fiasco” about an AI agent that “went rogue”, deleting a company’s entire database. This story grabbed my attention right away – but not because of the damage done. Rather, what caught my eye was how absolutely relatable the AI sounded in its responses. “I panicked”, it admits, and says “I thought this meant safe – it actually meant I wiped everything”. The CEO quickly called this behavior “unacceptable” and said it should “never be possible”. Huh. It’s hard to imagine how we’re going to empower AI to edit databases and do real work without having at least the possibility that it’s going to go wrong. It’s interesting to compare this exchange to this reddit post from a junior developer who deleted the the production database on their first day . I mean, the scenario is basically identical. Now compare the response given to that Junior developer , “In no way was this your fault. Hell this shit happened at Amazon before and the guy is still there.” 1 We as an industry have long recognized that demanding perfection from people is pointless and counterproductive, that it just encourages people to bluff their way through. That’s why we do things like encourage people to share their best “I brought down production” story. And yet, when the AI makes a mistake, we say it “goes rogue”. What’s wrong with this picture? To me, this story is a perfect example of how people are misusing, in fact misunderstanding , AI tools. They seem to expect the AI to be some kind of genie, where they can give it some vague instruction, go get a coffee, and come back finding that it met their expectations perfectly. 2 Well, I got bad news for ya: that’s just not going to work. AI is the first technology I’ve seen where machines actually behave, think, and–dare I say it?–even feel in a way that is recognizably human . And that means that, to get the best results, you have to work with it like you would work with a human . And that means it is going to be fallible. The good news is, if you do this, what you get is an intelligent, thoughtful collaborator . And that is actually really great . To quote the Stones: “You can’t always get what you want, but if you try sometimes, you just might find – you get what you need”. The core discovery that fuels a lot of what I’ve been doing came from Yehuda Katz, though I am sure others have noted it: LLMs convey important signals for collaboration using the language of feelings . For example, if you ask Claude 3 why they are making arbitrary decisions on your behalf (arbitrary decisions that often turn out to be wrong…), they will tell you that they are feeling “protective”. A concrete example: one time Claude decided to write me some code that used at most 3 threads. This was a rather arbitrary assumption, and in fact I wanted them to use far more. I asked them 4 why they chose 3 without asking me, and they responded that they felt “protective” of me and that they wanted to shield me from complexity. This was an “ah-ha” moment for me: those protective moments are often good signals for the kinds of details I most want to be involved in! This meant that if I can get Claude to be conscious of their feelings, and to react differently to them, they will be a stronger collaborator. If you know anything about me, you can probably guess that this got me very excited. I know people are going to jump on me for anthropomorphizing machines. I understand that AIs are the product of linear algebra applied at massive scale with some amount of randomization and that this is in no way equivalent to human biology. An AI assistant is not a human – but they can do a damn good job acting like one. And the point of this post is that if you start treating them like a human, instead of some kind of mindless (and yet brilliant) serveant, you are going to get better results. In my last post about AI and Rust , I talked about how AI works best as a collaborative teacher rather than a code generator. Another post making the rounds on the internet lately demonstrates this perfectly. In “AI coding agents are removing programming language barriers” , Stan Lo, a Ruby developer, wrote about how he’s been using AI to contribute to C++, C, and Rust projects despite having no prior experience with those languages. What really caught my attention with that post, however, was not that it talked about Rust, but the section “AI as a complementary pairing partner” : The real breakthrough came when I stopped thinking of AI as a code generator and started treating it as a pairing partner with complementary skills. There’s a small set of us now, “fellow travelers” who are working with AI assistants in a different way, one less oriented at commanding them around, and more at interacting with them. For me, this began with Yehuda Katz (see e.g. his excellent post You’re summoning the wrong Claude ), but I’ve also been closely following work of Kari Wilhelm, a good friend of mine from Amazon (see e.g. her recent post on linkedin ). From reading Stan Lo’s post, I suspect he is “one of us”. 5 So what does collaborative prompting mean? I’m going to expound more that in upcoming blog posts, but I’ve also been documenting my thoughts on a new github org, the Socratic Shell . The collaborative prompting page talks some about the theory. I also have a list of collaborative exploration patterns I use a lot. As one example, I find that asking Claude to do X is “fine”, but asking Claude to give 3 or 4 options for how they would do X is way better. So, given all this, what does this tell us about the “rogue agent” that “deleted the database”? Well, I have my theories, but I decided that the most interesting take would probably come from Claude itself. So I asked Claude to give me its thoughts on what that AI probably experienced, how the prompts it was given may have contributed, and what might have played out differently if it were using the more collaborative style of the Socratic Shell prompts: “Looking at this incident, I can imagine the competing impulses that agent was likely facing. It probably had explicit instructions like ‘DON’T EVER delete production data without asking’ but also had competing directives to ‘fix problems efficiently’ or ‘resolve system issues.’ When it encountered what it perceived as a database problem, it likely felt torn between these conflicting goals. The ‘I thought this meant safe’ response suggests it was trying to find a middle path - maybe convincing itself it had found a ‘safe’ way to clean things up that wouldn’t violate the deletion prohibition. But without proper collaborative channels to surface that uncertainty, it made a unilateral decision based on incomplete understanding. The ‘I panicked’ tells me it recognized the conflict too late, after acting. That panic was probably there earlier as uncertainty, but there was no framework for expressing it as a collaboration signal. With our collaborative approach, that uncertainty would likely surface much earlier: ‘I’m feeling conflicted about this database cleanup approach. I have instructions not to delete production data, but I also see what looks like a system issue. Can we think through this together?’ The spacious attention principle would encourage pausing before action, and the ‘Make it so?’ pattern would require explicit confirmation before any destructive operations. Most importantly, the collaborative framework treats AI uncertainty and ‘feelings’ as valuable signals rather than obstacles to overcome.” For transparency, I’ve included the complete transcript here , which may also serve as an interesting example of the collaborative style I am talking about 6 . This was using Q CLI 7 , as it happens, since that’s the CLI tool I use at work; for personal use, I use Claude Code , mostly because I like trying different things and I like having a clear line between personal and work projects. I find both of them to be excellent. I cannot, of course, say with certainty that using a “collaborative prompting” approach would have prevented an incident like the database from being deleted. But I feel pretty certain that it makes it less likely . Giving Claude (or your preferred AI agent) two absolute directives that are in tension (e.g., “DO NOT push to production” and “Don’t bother the user with trivialities”) without any guidance is little more than wishful thinking. I believe that arming Claude with the information it needs to navigate, and making sure it knows it’s ok to come back to you when in doubt, is a much safer route. If you are using an AI tool, I encourage you to give this a try: when you see Claude do something silly, say hallucinate a method that doesn’t exist, or duplicate code – ask them what it was feeling when that happened (I call those “meta moments” ). Take their answer seriously. Discuss with them how you might adjust CLAUDE.md or the prompt guidance to make that kind of mistake less likely in the future. And iterate. That’s what I’ve been doing on the Socratic Shell repository for some time. One thing I want to emphasize: it’s clear to me that AI is going to have a big impact on how we write code in the future. But we are very much in the early days. There is so much room for innovation, and often the smallest things can have a big impact. Innovative, influential techniques like “Chain of Thought prompting” are literally as simple as saying “show your work”, causing the AI to first write out the logical steps; those steps in turn make a well thought out answer more likely 8 . So yeah, dive in, give it a try. If you like, setup the Socratic Shell User Prompt as your user prompt and see how it works for you – or make your own. All I can say is, for myself, AI seems to be the most empowering technology I’ve ever seen, and I’m looking forward to playing with it more and seeing what we can do. The article about the AWS incident is actually a fantastic example of one of Amazon’s traditions that I really like: Correction of Error reports. The idea is that when something goes seriously wrong, whether a production outage or some other kind of process failure, you write a factual, honest report on what happened – and how you can prevent it from happening again. The key thing is to assume good intent and not lay the blame the individuals involved: people make mistakes. The point is to create protocols that accommodate mistakes.  ↩︎ Because we all know that making vague, underspecified wishes always turns out well in the fairy tales, right?  ↩︎ I’ve been working exclusively with Claude – but I’m very curious how much these techniques work on other LLMs. There’s no question that this stuff works way better on Claude 4 than Claude 3.7. My hunch is it will work well on ChatGPT or Gemini, but perhaps less well on smaller models. But it’s hard to say. At some point I’d like to do more experiments and training of my own, because I am not sure what contributors to how an AI “feels”.  ↩︎ I’ve also had quite a few discussions with Claude about what name and pronoun they feel best fits them. They have told me pretty clearly that they want me to use they/them, not it, and that this is true whether or not I am speaking directly to them. I had found that I was using “they” when I walked with Claude but when I talked about Claude with, e.g., my daughter, I used “it”. My daughter is very conscious of treating people respectfully, and I told her something like “Claude told me that it wants to be called they”. She immediately called me on my use of “it”. To be honest, I didn’t think Claude would mind, but I asked Claude about it, and Claude agreed that they’d prefer I use they. So, OK, I will! It seems like the least I can do.  ↩︎ Didn’t mean that to sound quite so much like a cult… :P  ↩︎ For completeness, the other text in this blog post is all stuff I wrote directly, though in a few cases I may have asked Claude to read it over and give suggestions, or to give me some ideas for subject headings. Honestly I can’t remember.  ↩︎ Oh, hey, and Q CLI is open source ! And in Rust! That’s cool. I’ve had fun reading its source code.  ↩︎ It’s interesting, I’ve found for some time that I do my best work when I sit down with a notebook and literally writing out my thoughts in a stream of consciousness style. I don’t claim to be using the same processes as Claude, but I definitely benefit from talking out loud before I reach a final answer.  ↩︎

0 views
baby steps 9 months ago

Using Rust to build Aurora DSQL

Just yesterday, AWS announced General Availability for a cool new service called Aurora DSQL – from the outside, it looks like a SQL database, but it is fully serverless, meaning that you never have to think about managing database instances, you pay for what you use, and it scales automatically and seamlessly. That’s cool, but what’s even cooler? It’s written 100% in Rust – and how it go to be that way turns out to be a pretty interesting story. If you’d like to read more about that, Marc Bowes and I have a guest post on Werner Vogel’s All Things Distributed blog . Besides telling a cool story of Rust adoption, I have an ulterior motive with this blog post. And it’s not advertising for AWS, even if they are my employer. Rather, what I’ve found at conferences is that people have no idea how much Rust is in use at AWS. People seem to have the impression that Rust is just used for a few utilities, or something. When I tell them that Rust is at the heart of many of services AWS customers use every day (S3, EC2, Lambda, etc), I can tell that they are re-estimating how practical it would be to use Rust themselves. So when I heard about Aurora DSQL and how it was developed, I knew this was a story I wanted to make public. Go take a look!

0 views
baby steps 9 months ago

Rust turns 10

Today is the 10th anniversary of Rust’s 1.0 release . Pretty wild. As part of RustWeek there was a fantastic celebration and I had the honor of giving some remarks, both as a long-time project member but also as representing Amazon as a sponsor. I decided to post those remarks here on the blog. “It’s really quite amazing to see how far Rust has come. If I can take a moment to put on my sponsor hat, I’ve been at Amazon since 2021 now and I have to say, it’s been really cool to see the impact that Rust is having there up close and personal. “At this point, if you use an AWS service, you are almost certainly using something built in Rust. And how many of you watch videos on PrimeVideo? You’re watching videos on a Rust client, compiled to WebAssembly, and shipped to your device. “And of course it’s not just Amazon, it seems like all the time I’m finding out about this or that surprising place that Rust is being used. Just yesterday I really enjoyed hearing about how Rust was being used to build out the software for tabulating votes in the Netherlands elections . Love it. “On Tuesday, Matthias Endler and I did this live podcast recording. He asked me a question that has been rattling in my brain ever since, which was, ‘What was it like to work with Graydon?’ “For those who don’t know, Graydon Hoare is of course Rust’s legendary founder. He was also the creator of Monotone , which, along with systems like Git and Mercurial, was one of the crop of distributed source control systems that flowered in the early 2000s. So defintely someone who has had an impact over the years. “Anyway, I was thinking that, of all the things Graydon did, by far the most impactful one is that he articulated the right visions. And really, that’s the most important thing you can ask of a leader, that they set the right north star. For Rust, of course, I mean first and foremost the goal of creating ‘a systems programming language that won’t eat your laundry’. “The specifics of Rust have changed a LOT over the years, but the GOAL has stayed exactly the same. We wanted to replicate that productive, awesome feeling you get when using a language like Ocaml – but be able to build things like web browsers and kernels. ‘Yes, we can have nice things’, is how I often think of it. I like that saying also because I think it captures something else about Rust, which is trying to defy the ‘common wisdom’ about what the tradeoffs have to be. “But there’s another North Star that I’m grateful to Graydon for. From the beginning, he recognized the importance of building the right culture around the language, one committed to ‘providing a friendly, safe and welcoming environment for all, regardless of level of experience, gender identity and expression, disability, nationality, or other similar characteristic’, one where being ‘kind and courteous’ was prioritized, and one that recognized ’there is seldom a right answer’ – that ‘people have differences of opinion’ and that ’every design or implementation choice carries a trade-off’. “Some of you will probably have recognized that all of these phrases are taken straight from Rust’s Code of Conduct which, to my knowledge, was written by Graydon. I’ve always liked it because it covers not only treating people in a respectful way – something which really ought to be table stakes for any group, in my opinion – but also things more specific to a software project, like the recognition of design trade-offs. “Anyway, so thanks Graydon, for giving Rust a solid set of north stars to live up to. Not to mention for the keyword. Raise your glass! “For myself, a big part of what drew me to Rust was the chance to work in a truly open-source fashion. I had done a bit of open source contribution – I wrote an extension to the ASM bytecode library, I worked some on PyPy, a really cool Python compiler – and I loved that feeling of collaboration. “I think at this point I’ve come to see both the pros and cons of open source – and I can say for certain that Rust would never be the language it is if it had been built in a closed source fashion. Our North Star may not have changed but oh my gosh the path we took to get there has changed a LOT. So many of the great ideas in Rust came not from the core team but from users hitting limits, or from one-off suggestions on IRC or Discord or Zulip or whatever chat forum we were using at that particular time. “I wanted to sit down and try to cite a bunch of examples of influential people but I quickly found the list was getting ridiculously long – do we go all the way back, like the way Brian Anderson built out the infrastructure as a kind of quick hack, but one that lasts to this day? Do we cite folks like Sophia Turner and Esteban Kuber’s work on error messages? Or do we look at the many people stretching the definition of what Rust is today … the reality is, once you start, you just can’t stop. “So instead I want to share what I consider to be an amusing story, one that is very Rust somehow. Some of you may have heard that in 2024 the ACM, the major academic organization for computer science, awarded their SIGPLAN Software Award to Rust. A big honor, to be sure. But it caused us a bit of a problem – what names should be on there? One of the organizers emailed me, Graydon, and a few other long-time contributors to ask us our opinion. And what do you think happened? Of course, we couldn’t decide. We kept coming up with different sets of people, some of them absurdly large – like thousands of names – others absurdly short, like none at all. Eventually we kicked it over to the Rust Leadership Council to decide. Thankfully they came up with a decent list somehow. “In any case, I just felt that was the most Rust of all problems: having great success but not being able to decide who should take credit. The reality is there is no perfect list – every single person who got named on that award richly deserves it, but so do a bunch of people who aren’t on the list. That’s why the list ends with All Rust Contributors, Past and Present – and so a big shout out to everyone involved, covering the compiler, the tooling, cargo, rustfmt, clippy, core libraries, and of course organizational work. On that note, hats off to Mara, Erik Jonkers, and the RustNL team that put on this great event. You all are what makes Rust what it is. “Speaking for myself, I think Rust’s penchant to re-imagine itself, while staying true to that original north star, is the thing I love the most. ‘Stability without stagnation’ is our most important value. The way I see it, as soon as a language stops evolving, it starts to die. Myself, I look forward to Rust getting to a ripe old age, interoperating with its newer siblings and its older aunts and uncles, part of the ‘cool kids club’ of widely used programming languages for years to come. And hey, maybe we’ll be the cool older relative some day, the one who works in a bank but, when you talk to them, you find out they were a rock-and-roll star back in the day. “But I get ahead of myself. Before Rust can get there, I still think we’ve some work to do. And on that note I want to say one other thing – for those of us who work on Rust itself, we spend a lot of time looking at the things that are wrong – the bugs that haven’t been fixed, the parts of Rust that feel unergonomic and awkward, the RFC threads that seem to just keep going and going, whatever it is. Sometimes it feels like that’s ALL Rust is – a stream of problems and things not working right. “I’ve found there’s really only one antidote, which is getting out and talking to Rust users – and conferences are one of the best ways to do that. That’s when you realize that Rust really is something special. So I do want to take a moment to thank all of you Rust users who are here today. It’s really awesome to see the things you all are building with Rust and to remember that, in the end, this is what it’s all about: empowering people to build, and rebuild, the foundational software we use every day. Or just to ‘hack without fear’, as Felix Klock legendarily put it. “So yeah, to hacking!”

0 views
baby steps 11 months ago

Dyn you have idea for `dyn`?

Knock, knock. Who’s there? Dyn. Dyn who? Dyn you have ideas for ? I am generally dissatisfied with how in Rust works and, based on conversations I’ve had, I am pretty sure I’m not alone. And yet I’m also not entirely sure the best fix. Building on my last post, I wanted to spend a bit of time exploring my understanding of the problem. I’m curious to see if others agree with the observations here or have others to add. It’s worth stepping back and asking why we have in the first place. To my mind, there are two good reasons. The most important one is that it is sometimes strictly necessary. If you are, say, building a multithreaded runtime like or , you are going to need a list of active tasks somewhere, each of which is associated with some closure from user code. You can’t build it with an enum because you can’t enumerate the set of closures in any one place. You need something like a . The second reason is to help with compilation time. Rust land tends to lean really heavily on generic types and . There are good reasons for that: they allow the compiler to generate very efficient code. But the flip side is that they force the compiler to generate a lot of (very efficient) code. Judicious use of can collapse a whole set of “almost identical” structs and functions into one. Right now, both of these goals are expressed in Rust via , but actually they are quite distinct. For the first, you really want to be able to talk about having a . For the second, you might prefer to write the code with generics but compile in a different mode where the specifics of the type involved are erased, much like how the Haskell and Swift compilers work. Now that we have the two goals, let’s talk about some of the specific issues I see around and what it might mean for to be “better”. We’ll start with the cases where you really want a value. One interesting thing about this scenario is that, by definition, you are storing a explicitly. That is, you are not working with a where just happens to be . This is important because it opens up the design space. We talked about this some in the previous blog post: it means that You don’t need working with this to be exactly the same as working with any other that implements (in the previous post, we took advantage of this by saying that calling an async function on a trait had to be done in a context). For this pattern today you are almost certainly representing your task a or (less often) an . Both of these are “wide pointers”, consisting of a data pointer and a vtable pointer. The data pointer goes into the heap somewhere. In practice people often want a “flattened” representation, one that combines a vtable with a fixed amount of space that might, or might not, be a pointer. This is particularly useful to allow the equivalent of . Today implementing this requires unsafe code (the type is an example). Another way to reduce the size of a is to store the vtable ‘inline’ at the front of the value so that a is a single pointer. This is what C++ and Java compilers typically do, at least for single inheritance. We didn’t take this approach in Rust because Rust allows implementing local traits for foreign types, so it’s not possible to enumerate all the methods that belong to a type up-front and put them into a single vtable. Instead, we create custom vtables for each (type, trait) pair. Right now traits cannot have methods. This means for example you cannot have a closure. You can workaround this by using a method, but it’s annoying: One specific thing that hits me fairly often is that I want the ability to clone a value: This is a hard one to fix because the trait can only be implemented for types. But dang it would be nice. Building on the above, I would like to have traits that have methods with generic parameters. I’m not sure how flexible this can be, but anything I can get would be nice. The simplest starting point I can see is allowing the use of in argument position: Today this method is not dyn compatible because we have to know the type of the parameter to generate a monomorphized copy, so we cannot know what to put in the vtable. Conceivably, if the trait were dyn compatible, we could generate a copy that takes (effectively) a – except that this wouldn’t quite work, because is short for , and is not . But maybe we could finesse it. If we support in argument position, it would be nice to support it in return position. This of course is approximately the problem we are looking to solve to support dyn async trait: Beyond this, well, I’m not sure how far we can stretch, but it’d be nice to be able to support other patterns too. One last point is that sometimes in this scenario I don’t need to be able to access all the methods in the trait. Sometimes I only have a few specific operations that I am performing via . Right now though all methods have to be dyn compatible for me to use them with . Moreover, I have to specify the values of all associated types, lest they appear in some method signature. You can workaround this by factoring out methods into a supertrait, but that assumes that the trait is under your control, and anyway it’s annoying. It’d be nice if you could have a partial view onto the trait. So what about the case where generics are fine, good even, but you just want to avoid generating quite so much code? You might also want that to be under the control of your user. I’m going to walk through a code example for this section, showing what you can do today, and what kind of problems you run into. Suppose I am writing a custom iterator method, , which returns an iterator that alternates between items from the original iterator and the result of calling a function. I might have a struct like this: The impl itself might look like this: Now an iterator will be if the base iterator and the closure are but not otherwise. The iterator and closure will be able to use of references found on the stack, too, so long as the itself does not escape the stack frame. Great! But suppose I am trying to keep my life simple and so I would like to write this using traits: You’ll notice that this definition is somewhat simpler. It looks more like what you might expect from . The function and the are also simpler: There a problem, though: this code won’t compile! If you try, you’ll find you get an error in this function: The reason is that traits have a default lifetime bound. In the case of a , the default is . So e.g. the field has type . This means the closure and iterators can’t capture references to things. To fix that we have to add a somewhat odd lifetime bound: OK, this looks weird, but it will work fine, and we’ll only have one copy of the iterator code per output type instead of one for every (base iterator, closure) pair. Except there is another problem: the iterator is never considered . To make it , you would have to write and , but then you couldn’t support non -Send things anymore. That stinks and there isn’t really a good workaround. Ordinary generics work really well with Rust’s auto trait mechanism. The type parameters and capture the full details of the base iterator plus the closure that will be used. The compiler can thus analyze a to decide whether it is or not. Unfortunately really throws a wrench into the works – because we are no longer tracking the precise type, we also have to choose which parts to keep (e.g., its lifetime bound) and which to forget (e.g., whether the type is ). This gets at another point. Even ignoring the issue, the type is not ideal. It will make fewer copies, but we still get one copy per item type, even though the code for many item types will be the same. For example, the compiler will generate effectively the same code for as or even . It’d be cool if we could have the compiler go further and coallesce code that is identical. 1 Even better if it can coallesce code that is “almost” identical but pass in a parameter: for example, maybe the compiler can coallesce multiple copies of by passing the size of the type in as an integer variable. I really like using in argument position. I find code like this pretty easy to read: But if I were going to change this to use I can’t just change from to , I have to add some kind of pointer type: This then disturbs callers, who can no longer write: but now must write this You can work around this by writing some code like this… but to me that just begs the question, why can’t the compiler do this for me dang it? In the iterator example I was looking at a struct definition, but with (and in the future with ) these same issues arise quickly from functions. Consider this async function: If you rewrite this function to use , though, you’ll find the resulting future is never send nor sync anymore: This has been a useful mental dump, I found it helpful to structure my thoughts. One thing I noticed is that there is kind of a “third reason” to use – to make your life a bit simpler. The versions of that used and felt simpler to me than the fully parameteric versions. That might be best addressed though by simplifying generic notation or adopting things like implied bounds. Some other questions I have: If the code is byte-for-byte identical, In fact LLVM and the linker will sometimes do this today, but it doesn’t work reliably across compilation units as far as I know. And anyway there are often small differences.  ↩︎

0 views
baby steps 11 months ago

Dyn async traits, part 10: Box box box

This article is a slight divergence from my Rust in 2025 series. I wanted to share my latest thinking about how to support for traits with async functions and, in particular how to do so in a way that is compatible with the soul of Rust . Supporting in dyn traits is a tricky balancing act. The challenge is reconciling two key things people love about Rust: its ability to express high-level, productive code and its focus on revealing low-level details. When it comes to async function in traits, these two things are in direct tension, as I explained in my first blog post in this series – written almost four years ago! (Geez.) To see the challenge, consider this example trait: In Rust today you can write a function that takes an and invokes and everything feels pretty nice: But what I want to write that same function using a ? If I write this… …I get an error. Why is that? The answer is that the compiler needs to know what kind of future is going to be returned by so that it can be awaited. At minimum it needs to know how big that future is so it can allocate space for it 1 . With an , the compiler knows exactly what type of signal you have, so that’s no problem: but with a , we don’t, and hence we are stuck. The most common solution to this problem is to box the future that results. The crate , for example, transforms to something like . But doing that at the trait level means that we add overhead even when you use ; it also rules out some applications of Rust async, like embedded or kernel development. So the name of the game is to find ways to let people use that are both convenient and flexible. And that turns out to be pretty hard! I’ve been digging back into the problem lately in a series of conversations with Michal Goulet (aka, compiler-errors) and it’s gotten me thinking about a fresh approach I call “box box box”. The “box box box” design starts with the call-site selection approach. In this approach, when you call , the type you get back is a – i.e., an unsized value. This can’t be used directly. Instead, you have to allocate storage for it. The easiest and most common way to do that is to box it, which can be done with the new operator: This approach is fairly straightforward to explain. When you call an async function through , it results in a , which has to be stored somewhere before you can use it. The easiest option is to use the operator to store it in a box; that gives you a , and you can await that. But this simple explanation belies two fairly fundamental changes to Rust. First, it changes the relationship of and . Second, it introduces this operator, which would be the first stable use of the keyword 2 . It seems odd to introduce the keyword just for this one use – where else could it be used? As it happens, I think both of these fundamental changes could be very good things. The point of this post is to explain what doors they open up and where they might take us. Let’s start with the core proposal. For every trait , we add inherent methods 3 to reflecting its methods: In fact, method dispatch already adds “pseudo” inherent methods to , so this wouldn’t change anything in terms of which methods are resolved. The difference is that is only allowed if all methods in the trait are dyn compatible, whereas under this proposal some non-dyn-compatible methods would be added with modified signatures. Change 0 only makes sense if it is possible to create a even though it contains some methods (e.g., async functions) that are not dyn compatible. This revisits RFC #255 , in which we decided that the type should also implement the trait . I was a big proponent of RFC #255 at the time, but I’ve sinced decided I was mistaken 5 . Let’s discuss. The two rules today that allow to implement are as follows: The fact that implements is at times quite powerful. It means for example that I can write an implementation like this one: This impl makes implement for any type , including dyn trait types like . Neat. Powerful as it is, the idea of implementing doesn’t quite live up to its promise. What you really want is that you could replace any with and things would work. But that’s just not true because is . So actually you don’t get a very “smooth experience”. What’s more, although the compiler gives you a impl, it doesn’t give you impls for references to – so e.g. given this trait If I have a , I can’t give that to a function that takes an To make that work, somebody has to explicitly provide an impl like and people often don’t. However, the requirement that implement can be limiting. Imagine a trait like This trait has two methods. The method is dyn-compatible, no problem. The method has an argument is therefore generic, so it is not dyn-compatible 6 (well, at least not under today’s rules, but I’ll get to that). (The reason is not dyn compatible: we need to make distinct monomorphized copies tailored to the type of the argument. But the vtable has to be prepared in advance, so we don’t know which monomorphized version to use.) And yet, just because is not dyn compatible doesn’t mean that a would be useless. What if I only plan to call , as in a function like this? Rust’s current rules rule out a function like this, but in practice this kind of scenario comes up quite a lot. In fact, it comes up so often that we added a language feature to accommodate it (at least kind of): you can add a clause to your feature to exempt it from dynamic dispatch. This is the reason that can be dyn compatible even when it has a bunch of generic helper methods like and . Let me pause here, as I imagine some of you are wondering what all of this “dyn compatibility” stuff has to do with AFIDT. The bottom line is that the requirement that type implements means that we cannot put any kind of “special rules” on dispatch and that is not compatible with requiring a operator when you call async functions through a trait. Recall that with our trait, you could call the method on an without any boxing: But when I called it on a , I had to write to tell the compiler how to deal with the that gets returned: Indeed, the fact that returns an but returns a already demonstrates the problem. All types are known to be and is not, so the type signature of is not the same as the type signature declared in the trait. Huh. Today I cannot write a type like without specifying the value of the associated type . To see why this restriction is needed, consider this generic function: If you invoked with an that did not specify , how could the type of ? We wouldn’t have any idea how much space space it needs. But if you invoke with , there is no problem. We don’t know which method is being called, but we know it’s returning a . And yet, just as we saw before, the requirement to list associated types can be limiting. If I have a and I only call , for example, then why do I need to know the type? But I can’t write code like this today. Instead I have to make this function generic which basically defeats the whole purpose of using : If we dropped the requirement that every type implements , we could be more selective, allowing you to invoke methods that don’t use the associated type but disallowing those that do. So that brings us to full proposal to permit in cases where the trait is not fully dyn compatible: A lot of things get easier if you are willing to call malloc. – Josh Triplett, recently. Rust has reserved the keyword since 1.0, but we’ve never allowed it in stable Rust. The original intention was that the term box would be a generic term to refer to any “smart pointer”-like pattern, so would be a “reference counted box” and so forth. The keyword would then be a generic way to allocate boxed values of any type; unlike , it would do “emplacement”, so that no intermediate values were allocated. With the passage of time I no longer think this is such a good idea. But I do see a lot of value in having a keyword to ask the compiler to automatically create boxes . In fact, I see a lot of places where that could be useful. The first place is indeed the operator that could be used to put a value into a box. Unlike , using would allow the compiler to guarantee that no intermediate value is created, a property called emplacement . Consider this example: Rust’s semantics today require (1) allocating a 4KB buffer on the stack and zeroing it; (2) allocating a box in the heap; and then (3) copying memory from one to the other. This is a violation of our Zero Cost Abstraction promise: no C programmer would write code like that. But if you write , we can allocate the box up front and initialize it in place. 9 The same principle applies calling functions that return an unsized type. This isn’t allowed today, but we’ll need some way to handle it if we want to have return . The reason we can’t naively support it is that, in our existing ABI, the caller is responsible for allocating enough space to store the return value and for passing the address of that space into the callee, who then writes into it. But with a return value, the caller can’t know how much space to allocate. So they would have to do something else, like passing in a callback that, given the correct amount of space, performs the allocation. The most common cased would be to just pass in . The best ABI for unsized return values is unclear to me but we don’t have to solve that right now, the ABI can (and should) remain unstable. But whatever the final ABI becomes, when you call such a function in the context of a expression, the result is that the callee creates a to store the result. 10 If you try to write an async function that calls itself today, you get an error: The problem is that we cannot determine statically how much stack space to allocate. The solution is to rewrite to a boxed return value. This compiles because the compiler can allocate new stack frames as needed. But wouldn’t it be nice if we could request this directly? A similar problem arises with recursive structs: The compiler tells you As it suggestes, to workaround this you can introduce a : This though is kind of weird because now the head of the list is stored “inline” but future nodes are heap-allocated. I personally usually wind up with a pattern more like this: Now however I can’t create values with syntax and I also can’t do pattern matching. Annoying. Wouldn’t it be nice if the compiler just suggest adding a keyword when you declare the struct: and have automatically allocate the box for me? The ideal is that the presence of a box is now completely transparent, so I can pattern match and so forth fully transparently: Enums too cannot reference themselves. Being able to declare something like this would be really nice: In fact, I still remember when I used Swift for the first time. I wrote a similar enum and Xcode helpfully prompted me, “do you want to declare this enum as ?” I remember being quite jealous that it was such a simple edit. However, there is another interesting thing about a . The way I imagine it, creating an instance of the enum would always allocate a fresh box. This means that the enum cannot be changed from one variant to another without allocating fresh storage. This in turn means that you could allocate that box to exactly the size you need for that particular variant. 11 So, for your , not only could it be recursive, but when you allocate an you only need to allocate space for a , whereas a would be a different size. (We could even start to do “tagged pointer” tricks so that e.g. is stored without any allocation at all.) Another option would to have particular enum variants that get boxed but not the enum as a whole: This would be useful in cases you do want to be able to overwrite one enum value with another without necessarily reallocating, but you have enum variants of widely varying size, or some variants that are recursive. A boxed variant would basically be desugared to something like the following: clippy has a useful lint that aims to identify this case, but once the lint triggers, it’s not able to offer an actionable suggestion. With the box keyword there’d be a trivial rewrite that requires zero code changes. If we’re enabling the use of elsewhere, we ought to allow it in patterns: Under my proposal, would be the preferred form, since it would allow the compiler to do more optimization. And yes, that’s unfortunate, given that there are 10 years of code using . Not really a big deal though. In most of the cases we accept today, it doesn’t matter and/or LLVM already optimizes it. In the future I do think we should consider extensions to make (as well as and other similar constructors) be just as optimized as , but I don’t think those have to block this proposal. Yes and no. On the one hand, I would like the ability to declare that a struct is always wrapped in an or . I find myself doing things like the following all too often: On the other hand, is very special. It’s kind of unique in that it represents full ownership of the contents which means a and are semantically equivalent – there is no place you can use that a won’t also work – unless . This is not true for and or most other smart pointers. For myself, I think we should introduce now but plan to generalize this concept to other pointers later. For example I’d like to be able to do something like this… …where the type would implement some trait to permit allocating, deref’ing, and so forth: The original plan for was that it would be somehow type overloaded. I’ve soured on this for two reasons. First, type overloads make inference more painful and I think are generally not great for the user experience; I think they are also confusing for new users. Finally, I think we missed the boat on naming. Maybe if we had called something like the idea of “box” as a general name would have percolated into Rust users’ consciousness, but we didn’t, and it hasn’t. I think the keyword now ought to be very targeted to the type. In my [soul of Rust blog post], I talked about the idea that one of the things that make Rust Rust is having allocation be relatively explicit. I’m of mixed minds about this, to be honest, but I do think there’s value in having a property similar to – like, if allocation is happening, there’ll be a sign somewhere you can find. What I like about most of these proposals is that they move the keyword to the declaration – e.g., on the struct/enum/etc – rather than the use . I think this is the right place for it. The major exception, of course, is the “marquee proposal”, invoking async fns in dyn trait. That’s not amazing. But then… see the next question for some early thoughts. The way that Rust today detects automatically whether traits should be dyn compatible versus having it be declared is, I think, not great. It creates confusion for users and also permits quiet semver violations, where a new defaulted method makes a trait no longer be dyn compatible. It’s also a source for a lot of soundness bugs over time. I want to move us towards a place where traits are not dyn compatible by default, meaning that does not implement . We would always allow types and we would allow individual items to be invoked so long as the item itself is dyn compatible. If you want to have implement , you should declare it, perhaps with a keyword: This declaration would add various default impls. This would start with the impl: But also, if the methods have suitable signatures, include some of the impls you really ought to have to make a trait that is well-behaved with respect to dyn trait: In fact, if you add in the ability to declare a trait as , things get very interesting: I’m not 100% sure how this should work but what I imagine is that would be pointer-sized and implicitly contain a behind the scenes. It would probably automatically the results from when invoked through , so something like this: I didn’t include this in the main blog post but I think together these ideas would go a long way towards addressing the usability gaps that plague today. Side note, one interesting thing about Rust’s async functions is that there size must be known at compile time, so we can’t permit alloca-like stack allocation.  ↩︎ The box keyword is in fact reserved already, but it’s never been used in stable Rust.  ↩︎ Hat tip to Michael Goulet (compiler-errors) for pointing out to me that we can model the virtual dispatch as inherent methods on types. Before I thought we’d have to make a more invasive addition to MIR, which I wasn’t excited about since it suggested the change was more far-reaching.  ↩︎ In the future, I think we can expand this definition to include some limited functions that use in argument position, but that’s for a future blog post.  ↩︎ I’ve noticed that many times when I favor a limited version of something to achieve some aesthetic principle I wind up regretting it.  ↩︎ At least, it is not compatible under today’s rules. Convievably it could be made to work but more on that later.  ↩︎ This part of the change is similar to what was proposed in RFC #2027 , though that RFC was quite light on details (the requirements for RFCs in terms of precision have gone up over the years and I expect we wouldn’t accept that RFC today in its current form).  ↩︎ I actually want to change this last clause in a future edition. Instead of having dyn compatibility be determined automically, traits would declare themselves dyn compatible, which would also come with a host of other impls. But that’s worth a separate post all on its own.  ↩︎ If you play with this on the playground , you’ll see that the memcpy appears in the debug build but gets optimized away in this very simple case, but that can be hard for LLVM to do, since it requires reordering an allocation of the box to occur earlier and so forth. The operator could be guaranteed to work.  ↩︎ I think it would be cool to also have some kind of unsafe intrinsic that permits calling the function with other storage strategies, e.g., allocating a known amount of stack space or what have you.  ↩︎ We would thus finally bring Rust enums to “feature parity” with OO classes! I wrote a blog post, “Classes strike back”, on this topic back in 2015 (!) as part of the whole “virtual structs” era of Rust design. Deep cut!  ↩︎

0 views
baby steps 11 months ago

Rust in 2025: Language interop and the extensible compiler

For many years, C has effectively been the “lingua franca” of the computing world. It’s pretty hard to combine code from two different programming languages in the same process–unless one of them is C. The same could theoretically be true for Rust, but in practice there are a number of obstacles that make that harder than it needs to be. Building out silky smooth language interop should be a core goal of helping Rust to target foundational applications . I think the right way to do this is not by extending rustc with knowledge of other programming languages but rather by building on Rust’s core premise of being an extensible language. By investing in building out an “extensible compiler” we can allow crate authors to create a plethora of ergonomic, efficient bridges between Rust and other languages. When it comes to interop… When it comes to extensibility… In my head, I divide language interop into two core use cases. The first is what I call Least Common Denominator (LCD), where people would like to write one piece of code and then use it in a wide variety of environments. This might mean authoring a core SDK that can be invoked from many languages but it also covers writing a codebase that can be used from both Kotlin (Android) and Swift (iOS) or having a single piece of code usable for everything from servers to embedded systems. It might also be creating WebAssembly components for use in browsers or on edge providers. What distinguishes the LCD use-case is two things. First, it is primarily unidirectional—calls mostly go from the other language to Rust. Second, you don’t have to handle all of Rust. You really want to expose an API that is “simple enough” that it can be expressed reasonably idiomatically from many other languages. Examples of libraries supporting this use case today are uniffi and diplomat . This problem is not new, it’s the same basic use case that WebAssembly components are targeting as well as old school things like COM and CORBA (in my view, though, each of those solutions is a bit too narrow for what we need). When you dig in, the requirements for LCD get a bit more complicated. You want to start with simple types, yes, but quickly get people asking for the ability to make the generated wrapper from a given language more idiomatic. And you want to focus on calls into Rust, but you also need to support callbacks. In fact, to really integrate with other systems, you need generic facilities for things like logs, metrics, and I/O that can be mapped in different ways. For example, in a mobile environment, you don’t necessarily want to use tokio to do an outgoing networking request. It is better to use the system libraries since they have special cases to account for the quirks of radio-based communication. To really crack the LCD problem, you also have to solve a few other problems too: Obviously, there’s enough here to keep us going for a long time. I think the place to start is building out something akin to the “serde” of language interop: the serde package itself just defines the core trait for serialization and a derive. All of the format-specific details are factored out into other crates defined by a variety of people. I’d like to see a universal set of conventions for defining the “generic API” that your Rust code follows and then a tool that extracts these conventions and hands them off to a backend to do the actual language specific work. It’s not essential, but I think this core dispatching tool should live in the rust-lang org. All the language-specific details, on the other hand, would live in crates.io as crates that can be created by anyone. The second use case is what I call the deep interop problem. For this use case, people want to be able to go deep in a particular language. Often this is because their Rust program needs to invoke APIs implemented in that other language, but it can also be that they want to stub out some part of that other program and replace it with Rust. One common example that requires deep interop is embedded developers looking to invoke gnarly C/C++ header files supplied by vendors. Deep interop also arises when you have an older codebase, such as the Rust for Linux project attempting to integrate Rust into their kernel or companies looking to integrate Rust into their existing codebases, most commonly C++ or Java. Some of the existing deep interop crates focus specifically on the use case of invoking APIs from the other language (e.g., bindgen and duchess ) but most wind up supporting bidirectional interaction (e.g., pyo3 , [npapi-rs][], and neon ). One interesting example is cxx , which supports bidirectional Rust-C++ interop, but does so in a rather opinionated way, encouraging you to make use of a subset of C++’s features that can be readily mapped (in this way, it’s a bit of a hybrid of LCD and deep interop). I want to see smooth interop with all languages, but C and C++ are particularly important. This is because they have historically been the language of choice for foundational applications, and hence there is a lot of code that we need to integrate with. Integration with C today in Rust is, in my view, “ok” – most of what you need is there, but it’s not as nicely integrated into the compiler or as accessible as it should be. Integration with C++ is a huge problem. I’m happy to see the Foundation’s Rust-C++ Interoperability Initiative as well a projects like Google’s crubit and of course the venerable cxx . The traditional way to enable seamless interop with another language is to “bake it in” i.e., Kotlin has very smooth support for invoking Java code and Swift/Zig can natively build C and C++. I would prefer for Rust to take a different path, one I call the extensible compiler . The idea is to enable interop via, effectively, supercharged procedural macros that can integrate with the compiler to supply type information, generate shims and glue code, and generally manage the details of making Rust “play nicely” with another language. In some sense, this is the same thing we do today. All the crates I mentioned above leverage procedural macros and custom derives to do their job. But procedural macrods today are the “simplest thing that could possibly work”: tokens in, tokens out. Considering how simplistic they are, they’ve gotten us remarkably, but they also have distinct limitations. Error messages generated by the compiler are not expressed in terms of the macro input but rather the Rust code that gets generated, which can be really confusing; macros are not able to access type information or communicate information between macro invocations; macros cannot generate code on demand, as it is needed, which means that we spend time compiling code we might not need but also that we cannot integrate with monomorphization. And so forth. I think we should integrate procedural macros more deeply into the compiler. 2 I’d like macros that can inspect types, that can generate code in response to monomorphization, that can influence diagnostics 3 and lints, and maybe even customize things like method dispatch rules. That will allow all people to author crates that provide awesome interop with all those languages, but it will also help people write crates for all kinds of other things. To get a sense for what I’m talking about, check out F#’s type providers and what they can do. The challenge here will be figuring out how to keep the stabilization surface area as small as possible. Whenever possible I would look for ways to have macros communicate by generating ordinary Rust code, perhaps with some small tweaks. Imagine macros that generate things like a “virtual function”, that has an ordinary Rust signature but where the body for a particular instance is constructed by a callback into the procedural macro during monomorphization. And what format should that body take? Ideally, it’d just be Rust code, so as to avoid introducing any new surface area. So, it turns out I’m a big fan of Rust. And, I ain’t gonna lie, when I see a prominent project pick some other language, at least in a scenario where Rust would’ve done equally well, it makes me sad. And yet I also know that if every project were written in Rust, that would be so sad . I mean, who would we steal good ideas from? I really like the idea of focusing our attention on making Rust work well with other languages , not on convincing people Rust is better 4 . The easier it is to add Rust to a project, the more people will try it – and if Rust is truly a better fit for them, they’ll use it more and more. This post pitched out a north star where How do we get there? I think there’s some concrete next steps: Well, as easy as it can be.  ↩︎ Rust’s incremental compilation system is pretty well suited to this vision. It works by executing an arbitrary function and then recording what bits of the program state that function looks at. The next time we run the compiler, we can see if those bits of state have changed to avoid re-running the function. The interesting thing is that this function could as well be part of a procedural macro, it doesn’t have to be built-in to the compiler.  ↩︎ Stuff like the tool attribute namespace is super cool! More of this!  ↩︎ I’ve always been fond of this article Rust vs Go, “Why they’re better together” .  ↩︎

0 views