Latest Posts (18 found)
baby steps 2 days 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 1 weeks 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 1 weeks 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 3 weeks 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 2 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 2 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 4 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 5 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 6 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 6 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 7 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
baby steps 7 months ago

Rust in 2025: Targeting foundational software

Rust turns 10 this year. It’s a good time to take a look at where we are and where I think we need to be going. This post is the first in a series I’m calling “Rust in 2025”. This first post describes my general vision for how Rust fits into the computing landscape. The remaining posts will outline major focus areas that I think are needed to make this vision come to pass. Oh, and fair warning, I’m expecting some controversy along the way—at least I hope so, since otherwise I’m just repeating things everyone knows. I see Rust’s mission as making it dramatically more accessible to author and maintain foundational software. By foundational I mean the software that underlies everything else . You can already see this in the areas where Rust is highly successful: CLI and development tools that everybody uses to do their work and which are often embedded into other tools 1 ; cloud platforms that people use to run their applications 2 ; embedded devices that are in the things around (and above ) us; and, increasingly, the kernels that run everything else (both Windows and Linux !). The needs of foundational software have a lot in common with all software, but everything is extra important. Reliability is paramount, because when the foundations fail, everything on top fails also. Performance overhead is to be avoided because it becomes a floor on the performance achievable by the layers above you. Traditionally, achieving the extra-strong requirements of foundational software has meant that you can’t do it with “normal” code. You had two choices. You could use C or C++ 3 , which give great power but demand perfection in response 4 . Or, you could use a higher-level language like Java or Go, but in a very particular way designed to keep performance high. You have to avoid abstractions and conveniences and minimizing allocations so as not to trigger the garbage collector. Rust changed the balance by combining C++’s innovations in zero-cost abstractions with a type system that can guarantee memory safety. The result is a pretty cool tool, one that (often, at least) lets you write high-level code with low-level performance and without fear of memory safety errors. In my Rust talks, I often say that type systems and static checks sound to most developers like “spinach”, something their parents forced them to eat because it was “good for them”, but not something anybody wants. The truth is that type systems are like spinach—popeye spinach. Having a type system to structure your thinking makes you more effective, regardless of your experience level. If you are a beginner, learning the type system helps you learn how to structure software for success. If you are an expert, the type system helps you create structures that will catch your mistakes faster (as well as those of your less experienced colleagues). Yehuda Katz sometimes says, “When I’m feeling alert, I build abstractions that will help tired Yehuda be more effective”, which I’ve always thought was a great way of putting it. When I say that Rust’s mission is to target foundational software, I don’t mean that’s all it’s good for. Projects like Dioxus , Tauri , and Leptos are doing fascinating, pioneering work pushing the boundaries of Rust into higher-level applications like GUIs and Webpages. I don’t believe this kind of high-level development will ever be Rust’s sweet spot . But that doesn’t mean I think we should ignore them—in fact, quite the opposite. The traditional thinking goes that, because foundational software often needs control over low-level details, it’s not as important to focus on accessibility and ergonomics . In my view, though, the fact that foundational software needs control over low-level details only makes it more important to try and achieve good ergonomics. Anything you can do to help the developer focus on the details that matter most will make them more productive. I think projects that stretch Rust to higher-level areas, like Dioxus , Tauri , and Leptos , are a great way to identify opportunities to make Rust programming more convenient. These opportunities then trickle down to make Rust easier to use for everyone. The trick is to avoid losing the control and reliability that foundational applications need along the way (and it ain’t always easy). There’s another reason to make sure that higher-level applications are pleasant in Rust: it means that people can build their entire stack using one technology. I’ve talked to a number of people who expected just to use Rust for one thing, say a tail-latency-sensitive data plane service , but they wound up using it for everything. Why? Because it turned out that, once they learned it, Rust was quite productive and using one language meant they could share libraries and support code. Put another way, simple code is simple no matter what language you build it in. 5 The other lesson I’ve learned is that you want to enable what I think of as smooth, iterative deepening . This rather odd phrase is the one that always comes to my mind, somehow. The idea is that a user’s first experience should be simple –they should be able to get up and going quickly. As they get further into their project, the user will find places where it’s not doing what they want, and they’ll need to take control. They should be able to do this in a localized way, changing one part of their project without disturbing everything else. Smooth, iterative deepening sounds easy but is in fact very hard. Many projects fail either because the initial experience is hard or because the step from simple-to-control is in fact more like scaling a cliff, requiring users to learn a lot of background material. Rust certainly doesn’t always succeed–but we succeed enough, and I like to think we’re always working to do better. This is the first post of the series. My current plan 6 is to post four follow-ups that cover what I see as the core investments we need to make to improve Rust’s fit for foundational software. In my mind, the first three talk about how we should double down on some of Rust’s core values: After that, I’ll talk about the Rust open-source organization and what I think we should be doing there to make contributing to and maintaining Rust as accessible and, dare I say it, joyful as we can. Plenty of people use ripgrep, but did you know that when you do full text search in VSCode, you are also using ripgrep ? And of course Deno makes heavy use of Rust, as does a lot of Python tooling, like the uv package manager. The list goes on and on.  ↩︎ What do AWS, Azure, CloudFlare, and Fastly all have in common? They’re all big Rust users.  ↩︎ Rod Chapman tells me I should include Ada. He’s not wrong, particularly if you are able to use SPARK to prove strong memory safety (and stronger properties, like panic freedom or even functional correctness). But Ada’s never really caught on broadly, although it’s very successful in certain spaces.  ↩︎ Alas, we are but human.  ↩︎ Well, that’s true if the language meets a certain base bar. I’d say that even “simple” code in C isn’t all that simple, given that you don’t even have basic types like vectors and hashmaps available.  ↩︎ I reserve the right to change it as I go!  ↩︎

0 views
baby steps 7 months ago

View types redux and abstract fields

A few years back I proposed view types as an extension to Rust’s type system to let us address the problem of (false) inter-procedural borrow conflicts. The basic idea is to introduce a “view type” 1 , meaning “an instance of where you can only access the fields or ”. The main purpose is to let you write function signatures like or that define what fields a given type might access. I was thinking about this idea again and I wanted to try and explore it a bit more deeply, to see how it could actually work, and to address the common question of how to have places in types without exposing the names of private fields. The type is going to be our running example. The type collects experiments, each of which has a name and a set of values. In addition to the experimental data, it has a counter, , which indicates how many measurements were successful. There are some helper functions you can use to iterate over the list of experiments and read their data. All of these return data borrowed from self. Today in Rust I would typically leverage lifetime elision, where the in the return type is automatically linked to the argument: Now imagine that has methods for reading and modifying the counter of successful experiments: The type as presented thus far is pretty sensible, but it can actually be a pain to use. Suppose you wanted to iterate over the experiments, analyze their data, and adjust the successful counter as a result. You might try writing the following: Experienced Rustaceans are likely shaking their head at this point—in fact, the previous code will not compile. What’s wrong? Well, the problem is that returns data borrowed from which then persists for the duration of the loop. Invoking then requires an argument, which causes a conflict. The compiler is indeed flagging a reasonable concern here. The risk is that could mutate the map while is still iterating over it. Now, we as code authors know that this is unlikely — but let’s be honest, it may be unlikely now , but it’s not impossible that as evolves somebody might add some kind of logic into that would mutate the map. This is precisely the kind of subtle interdependency that can make an innocuous “but it’s just one line!” PR cause a massive security breach. That’s all well and good, but it’s also very annoying that I can’t write this code. The right fix here is to have a way to express what fields may be accessed in the type system. If we do this, then we can get the code to compile today and prevent future PRs from introducing bugs. This is hard to do with Rust’s current system, though, as types do not have any way of talking about fields, only spans of execution-time (“lifetimes”). With view types, though, we can change the signature from to . Just as is shorthand for , this is actually shorthand for . We would also modify the method to flag what field it needs: The idea of this post was to sketch out how view types could work in a slightly more detailed way. The basic idea is to extend Rust’s type grammar with a new type… We would also have some kind of expression for defining a view onto a place. This would be a place expression. For now I will write to define this expression, but that’s obviously ambiguous with Rust blocks. So for example you could write… …to get a reference that can only access the field of the tuple and a reference that can only access field . Note the difference between , which creates a reference to the entire tuple but with limited access, and , which creates a reference to the field itself. Both have their place. Consider this function from our example: How would we type check the statement? Today, without view types, typing an expression like begins by getting the type of , which is something like . We then “auto-deref”, looking for the struct type within. That would bring us to , at which point we would check to see if defines a field . To integrate view types, we have to track both the type of data being accessed and the set of allowed fields. Initially we have variable with type and allow set . The deref would bring us to (allow-set remains ). Traversing a view type modifies the allow-set, so we go from to (to be legal, every field in the view must be allowed). We now have the type . We would then identify the field as both a member of and a member of the allow-set, and so this code would be successful. If however you tried to modify a function to access a field not declared as part of its view, e.g., the type-checking would now fail, because the field would not be a member of the allow-set. A more interesting problem comes when we type-check a call to . We had the following code: Consider the call to . In the compiler today, method lookup begins by examining , of type , auto-deref’ing by one step to yield , and then auto-ref’ing to yield . The result is this method call is desugared to a call like . With view types, when introducing the auto-ref, we would also introduce a view operation. So we would get . What is this ? That indicates that the set of allowed fields has to be inferred. A place-set variable can be inferred to a set of fields or to (all fields). We would integrate these place-set variables into inference, so that if is a subset of and (e.g., ). We would also for dropping view types from subtypes, e.g., if . Place-set variables only appear as an internal inference detail, so users can’t (e.g.) write a function that is generic over a place-set, and the only kind of constraints you can get are subset ( ) and inclusion ( ). I think it should be relatively straightforward to integrate these into HIR type check inference. When generalizing, we can replace each specific view set with a variable, just as we do for lifetimes. When we go to construct MIR, we would always know the precise set of fields we wish to include in the view. In the case where the set of fields is we can also omit the view from the MIR. So, view types allow us to address these sorts of conflicts by making it more explicit what sets of types we are going to access, but they introduce a new problem — does this mean that the names of our private fields become part of our interface? That seems obviously undesirable. The solution is to introduce the idea of abstract 2 fields. An abstract field is a kind of pretend field, one that doesn’t really exist, but which you can talk about “as if” it existed. It lets us give symbolic names to data. Abstract fields would be defined as aliases for a set of fields, like . An alias defines a public symbolic names for a set of fields. We could therefore define two aliases for , one for the set of experiments and one for the count of successful experiments. I think it be useful to allow these names to alias actual field names, as I think that in practice the compiler can always tell which set to use, but I would require that if there is an alias, then the abstract field is aliased to the actual field with the same name. Now the view types we wrote earlier ( , etc) are legal but they refer to the abstract fields and not the actual fields. One nice property of abstract fields is that they permit refactoring. Imagine that we decide to change so that instead of storing experiments as a , we put all the experimental data in one big vector and store a range of indices in the map, like . We can do that no problem: We would still declare methods like , but the compiler now understands that the abstract field can be expanded to the set of private fields. Yes, I think it should be possible to define to indicate the empty set of fields. Good question. There is no necessary interaction, we could leave view types as simply a kind of type. You might do interesting things like implement for a view on your struct: Yes! And it’d be interesting. You could imagine declaring abstract fields as trait members that can appear in its interface: You could then define those fields in an impl. You can even map some of them to real fields and leave some as purely abstract: Although I wouldn’t want to at first, I think you could permit something like and then, given something like , you’d get the type , but I’ve not really thought it more deeply than that. Yes! You should be able to do something like I described the view type subtyping rules as two rules: In principle we could have a rule like if — this rule would allow “introducing” a view type into the supertype. We may wind up needing such a rule but I didn’t want it because it meant that code like this really ought to compile (using the type from the previous question): I would expect this to compile because but I kind of don’t want it to compile. Yes! I think abstract fields would also be useful in two other ways (though we have to stretch their definition a bit). I believe it’s important for Rust to grow stronger integration with theorem provers; I don’t expect these to be widely used, but for certain key libraries (stdlib, zerocopy, maybe even tokio) it’d be great to be able to mathematically prove type safety. But mathematical proof systems often require a notion of ghost fields — basically logical state that doesn’t really exist at runtime but which you can talk about in a proof. A ghost field is essentially an abstract field that is mapped to an empty set of fields and which has a type. For example you might declare a struct with two abstract fields ( , ) and one real field that stores their sum: then when you create you would specify a value for those fields. The value would perhaps be written using something like an abstract block, indicating that in fact the code within will not be executed (but must still be type checkable): Providing abstract values is useful because it lets the theorem prover act “as if” the code was there for the purpose of checking pre- and post-conditions and other kinds of contracts. Yes! I imagine that instead of you could do , but that would mean we’d have to have some abstract initializer. So perhaps we permit an anonymous field , in which case you wouldn’t be required to provide an initializer, but you also couldn’t name it in contracts. I would start with just the simplest form of abstract fields, which is an alias for a set of real fields. But to extend to cover ghost fields or , you want to support the ability to declare a type for abstract fields (we could say that the default if ). For fields with non- types, you would be expected to provide an abstract value in the struct constructor. To conveniently handle , we could add anonymous abstract fields where no type is needed. I’ve shown view types attached to structs and tuples. Conceivably we could permit them elsewhere, e.g., might be equivalent to . I don’t think that’s needed for now and I’d make it ill-formed, but it could be reasonable to support at some point. This concludes my exploration through view types. The post actually changed as I wrote it — initially I expected to include place-based borrows, but it turns out we didn’t really need those. I also initially expected view types to be a special case of struct types, and that indeed might simplify things, but I wound up concluding that they are a useful type constructor on their own. In particular if we want to integrate them into traits it will be necessary for them to be applied to generics and the rest.≈g In terms of next steps, I’m not sure, I want to think about this idea, but I do feel we need to address this gap in Rust, and so far view types seem like the most natural. I think what could be interesting is to prototype them in a-mir-formality as it evolves to see if there are other surprises that arise. I’m not really proposing this syntax—among other things, it is ambiguous in expression position. I’m not sure what the best syntax is, though! It’s an important question, but not one I will think hard about here.  ↩︎ I prefer the name ghost fields, because it’s spooky, but abstract is already a reserved keyword.  ↩︎

0 views
baby steps 7 months ago

Rust 2024 Is Coming

So, a little bird told me that Rust 2024 is going to become stable today, along with Rust 1.85.0. In honor of this momentous event, I have penned a little ditty that I’d like to share with you all. Unfortunately, for those of you who remember Rust 2021’s “Edition: The song” , in the 3 years between Rust 2021 and now, my daughter has realized that her father is deeply uncool 1 and so I had to take this one on solo 2 . Anyway, enjoy! Or, you know, suffer. As the case may be. Watch the movie embedded here, or watch it on YouTube : In ChordPro format, for those of you who are inspired to play along. It was bound to happen eventually.  ↩︎ Actually, I had a plan to make this a duet with somebody who shall remain nameless (they know who they are). But I was too lame to get everything done on time. In fact, I may or may not have realized “Oh, shit, I need to finish this recording!” while in the midst of a beer with Florian Gilcher last night. Anyway, sorry, would-be-collaborator-I -was-really-looking-forward-to-playing-with! Next time!  ↩︎

0 views
baby steps 8 months ago

How I learned to stop worrying and love the LLM

I believe that AI-powered development tools can be a game changer for Rust—and vice versa. At its core, my argument is simple: AI’s ability to explain and diagnose problems with rich context can help people get over the initial bump of learning Rust in a way that canned diagnostics never could, no matter how hard we try. At the same time, rich type systems like Rust’s give AIs a lot to work with, which could be used to help them avoid hallucinations and validate their output. This post elaborates on this premise and sketches out some of the places where I think AI could be a powerful boost. Is Rust good for every project? No, of course not. But it’s absolutely great for some things—specifically, building reliable, robust software that performs well at scale. This is no accident. Rust’s design is intended to surface important design questions (often in the form of type errors) and to give users the control to fix them in whatever way is best. But this same strength is also Rust’s biggest challenge. Talking to people within Amazon about adopting Rust, perceived complexity and fear of its learning curve is the biggest hurdle. Most people will say, “Rust seems interesting, but I don’t need it for this problem” . And you know, they’re right! They don’t need it. But that doesn’t mean they wouldn’t benefit from it. One of Rust’s big surprises is that, once you get used to it, it’s “surprisingly decent” at very large number of things beyond what it was designed for. Simple business logic and scripts can be very pleasant in Rust. But the phase “once you get used to it” in that sentence is key, since most people’s initial experience with Rust is confusion and frustration . Some languages are geared to say yes —that is, given any program, they aim to run it and do something . JavaScript is of course the most extreme example (no semicolons? no problem!) but every language does this to some degree. It’s often quite elegant. Consider how, in Python, you write to get the last element in the list: super handy! Rust is not (usually) like this. Rust is geared to say no . The compiler is just itching for a reason to reject your program. It’s not that Rust is mean: Rust just wants your program to be as good as it can be. So we try to make sure that your program will do what you want (and not just what you asked for). This is why , in Rust, will panic: sure, giving you the last element might be convenient, but how do we know you didn’t have an off-by-one bug that resulted in that negative index? 1 But that tendency to say no means that early learning can be pretty frustrating. For most people, the reward from programming comes from seeing their program run—and with Rust, there’s a lot of niggling details to get right before your program will run. What’s worse, while those details are often motivated by deep properties of your program (like data races), the way they are presented is as the violation of obscure rules, and the solution (“add a ”) can feel random. Once you get the hang of it, Rust feels great, but getting there can be a pain. I heard a great phrase from someone at Amazon to describe this: “Rust: the language where you get the hangover first”. 3 My favorite thing about working at Amazon is getting the chance to talk to developers early in their Rust journey. Lately I’ve noticed an increasing trend—most are using Q Developer. Over the last year, Amazon has been doing a lot of internal promotion of Q Developer, so that in and of itself is no surprise, but what did surprise me a bit is hearing from developers the way that they use it. For most of them, the most valuable part of Q Dev is authoring code but rather explaining it. They ask it questions like “why does this function take an and not an ?” or “what happens when I move a value from one place to another?”. Effectively, the LLM becomes an ever-present, ever-patient teacher. 4 Some time back I sat down with an engineer learning Rust at Amazon. They asked me about an error they were getting that they didn’t understand. “The compiler is telling me something about , what does that mean?” Their code looked something like this: And the compiler was telling them : This is a pretty good error message! And yet it requires significant context to understand it (not to mention scrolling horizontally, sheesh). For example, what is “borrowed data”? What does it mean for said data to “escape”? What is a “lifetime” and what does it mean that “ must outlive ”? Even assuming you get the basic point of the message, what should you do about it? Ultimately, the answer to the engineer’s problem was just to insert a call to 5 . But deciding on that fix requires a surprisingly large amount of context. In order to figure out the right next step, I first explained to the engineer that this confusing error is, in fact, what it feels like when Rust saves your bacon , and talked them through how the ownership model works and what it means to free memory. We then discussed why they were spawning a task in the first place (the answer: to avoid the latency of logging)—after all, the right fix might be to just not spawn at all, or to use something like rayon to block the function until the work is done. Once we established that the task needed to run asynchronously from its parent, and hence had to own the data, we looked into changing the function to take an so that it could avoid a deep clone. This would be more efficient, but only if the caller themselves could cache the somewhere. It turned out that the origin of this string was in another team’s code and that this code only returned an . Refactoring that code would probably be the best long term fix, but given that the strings were expected to be quite short, we opted to just clone the string. An error message is often your first and best chance to teach somebody something.—Esteban Küber (paraphrased) Working through this error was valuable. It gave me a chance to teach this engineer a number of concepts. I think it demonstrates a bit of Rust’s promise—the idea that learning Rust will make you a better programmer overall, regardless of whether you are using Rust or not. Despite all the work we have put into our compiler error messages, this kind of detailed discussion is clearly something that we could never achieve. It’s not because we don’t want to! The original concept for , for example, was to present a customized explanation of each error was tailored to the user’s code. But we could never figure out how to implement that. And yet tailored, in-depth explanation is absolutely something an LLM could do. In fact, it’s something they already do, at least some of the time—though in my experience the existing code assistants don’t do nearly as good a job with Rust as they could. Emery Berger is a professor at UMass Amherst who has been exploring how LLMs can improve the software development experience. Emery emphasizes how AI can help close the gap from “tool to goal”. In short, today’s tools (error messages, debuggers, profilers) tell us things about our program, but they stop there. Except in simple cases, they can’t help us figure out what to do about it—and this is where AI comes in. When I say AI, I am not talking (just) about chatbots. I am talking about programs that weave LLMs into the process, using them to make heuristic choices or proffer explanations and guidance to the user. Modern LLMs can also do more than just rely on their training and the prompt: they can be given access to APIs that let them query and get up-to-date data. I think AI will be most useful in cases where solving the problem requires external context not available within the program itself. Think back to my explanation of the error, where knowing the right answer depended on how easy/hard it would be to change other APIs. I’ve thought about a lot of places I think AI could help make working in Rust more pleasant. Here is a selection. Consider this code: This function will give a type error, because the signature (thanks to lifetime elision) promises to return a string borrowed from but actually returns a string borrowed from . Now…what is the right fix? It’s very hard to tell in isolation! It may be that in fact the code was meant to be (in which case the current signature is correct). Or perhaps it was meant to be something that sometimes returns and sometimes returns , in which case the signature of the function was wrong. Today, we take our best guess. But AI could help us offer more nuanced guidance. People often ask me questions like “how do I make a visitor in Rust?” The answer, of course, is “it depends on what you are trying to do”. Much of the time, a Java visitor is better implemented as a Rust enum and match statements, but there is a time and a place for something more like a visitor. Guiding folks through the decision tree for how to do non-trivial mappings is a great place for LLMs. When I start writing a Rust program, I start by authoring type declarations. As I do this, I tend to think ahead to how I expect the data to be accessed. Am I going to need to iterate over one data structure while writing to another? Will I want to move this data to another thread? The setup of my structures will depend on the answer to these questions. I think a lot of the frustration beginners feel comes from not having a “feel” yet for the right way to structure their programs. The structure they would use in Java or some other language often won’t work in Rust. I think an LLM-based assistant could help here by asking them some questions about the kinds of data they need and how it will be accessed. Based on this it could generate type definitions, or alter the definitions that exist. A follow-on to the previous point is that, in Rust, when your data access patterns change as a result of refactorings, it often means you need to do more wholesale updates to your code. 6 A common example for me is that I want to split out some of the fields of a struct into a substruct, so that they can be borrowed separately. 7 This can be quite non-local and sometimes involves some heuristic choices, like “should I move this method to be defined on the new substruct or keep it where it is?”. When you run the command today it will automatically apply various code suggestions to cleanup your code. With the upcoming Rust 2024 edition , will do the same but for edition-related changes. All of the logic for these changes is hardcoded in the compiler and it can get a bit tricky. For editions, we intentionally limit ourselves to local changes, so the coding for these migrations is usually not too bad, but there are some edge cases where it’d be really useful to have heuristics. For example, one of the changes we are making in Rust 2024 affects “temporary lifetimes”. It can affect when destructors run. This almost never matters (your vector will get freed a bit earlier or whatever) but it can matter quite a bit, if the destructor happens to be a lock guard or something with side effects. In practice when I as a human work with changes like this, I can usually tell at a glance whether something is likely to be a problem—but the heuristics I use to make that judgment are a combination of knowing the name of the types involved, knowing something about the way the program works, and perhaps skimming the destructor code itself. We could hand-code these heuristics, but an LLM could do it and better, and if could ask questions if it was feeling unsure. Now imagine you are releasing the 2.x version of your library. Maybe your API has changed in significant ways. Maybe one API call has been broken into two, and the right one to use depends a bit on what you are trying to do. Well, an LLM can help here, just like it can help in translating idioms from Java to Rust. I imagine the idea of having an LLM help you migrate makes some folks uncomfortable. I get that. There’s no reason it has to be mandatory—I expect we could always have a more limited, precise migration available. 8 Premature optimization is the root of all evil, or so Donald Knuth is said to have said. I’m not sure about all evil, but I have definitely seen people rathole on microoptimizing a piece of code before they know if it’s even expensive (or, for that matter, correct). This is doubly true in Rust, where cloning a small data structure (or reference counting it) can often make your life a lot simpler. Llogiq’s great talks on Easy Mode Rust make exactly this point. But here’s a question, suppose you’ve been taking this advice to heart, inserting clones and the like, and you find that your program is running kind of slow? How do you make it faster? Or, even worse, suppose that you are trying to turn our network service. You are looking at the blizzard of available metrics and trying to figure out what changes to make. What do you do? To get some idea of what is possible, check out Scalene , a Python profiler that is also able to offer suggestions as well (from Emery Berger’s group at UMass, the professor I talked about earlier). Let’s look a bit to the future. I want us to get to a place where the “minimum bar” for writing unsafe code is that you test that unsafe code with some kind of sanitizer that checks for both C and Rust UB—something like miri today, except one that works “at scale” for code that invokes FFI or does other arbitrary things. I expect a smaller set of people will go further, leveraging automated reasoning tools like Kani or Verus to prove statically that their unsafe code is correct 9 . From my experience using miri today, I can tell you two things. (1) Every bit of unsafe code I write has some trivial bug or other. (2) If you enjoy puzzling out the occasionally inscrutable error messages you get from Rust, you’re gonna love miri! To be fair, miri has a much harder job—the (still experimental) rules that govern Rust aliasing are intended to be flexible enough to allow all the things people want to do that the borrow checker doesn’t permit. This means they are much more complex. It also means that explaining why you violated them (or may violate them) is that much more complicated. Just as an AI can help novices understand the borrow checker, it can help advanced Rustaceans understand tree borrows (or whatever aliasing model we wind up adopting). And just as it can make smarter suggestions for whether to modify the function body or its signature, it can likely help you puzzle out a good fix. Anyone who has used an LLM-based tool has encountered hallucinations, where the AI just makes up APIs that “seem like they ought to exist”. 10 And yet anyone who has used Rust knows that “if it compiles, it works” is true may more often than it has a right to be. 11 This suggests to me that any attempt to use the Rust compiler to validate AI-generated code or solutions is going to also help ensure that the code is correct. AI-based code assistants right now don’t really have this property. I’ve noticed that I kind of have to pick between “shallow but correct” or “deep but hallucinating”. A good example is statements. I can use rust-analyzer to fill in the match arms and it will do a perfect job, but the body of each arm is . Or I can let the LLM fill them in and it tends to cover most-but-not-all of the arms but it generates bodies. I would love to see us doing deeper integration, so that the tool is talking to the compiler to get perfect answers to questions like “what variants does this enum have” while leveraging the LLM for open-ended questions like “what is the body of this arm”. 12 Overall AI reminds me a lot of the web around the year 2000. It’s clearly overhyped. It’s clearly being used for all kinds of things where it is not needed. And it’s clearly going to change everything. If you want to see examples of what is possible, take a look at the ChatDBG videos published by Emery Berger’s group. You can see how the AI sends commands to the debugger to explore the program state before explaining the root cause. I love the video debugging bootstrap.py , as it shows the AI applying domain knowledge about statistics to debug and explain the problem. My expectation is that compilers of the future will not contain nearly so much code geared around authoring diagnostics. They’ll present the basic error, sure, but for more detailed explanations they’ll turn to AI. It won’t be just a plain old foundation model, they’ll use RAG techniques and APIs to let the AI query the compiler state, digest what it finds, and explain it to users. Like a good human tutor, the AI will tailor its explanations to the user, leveraging the user’s past experience and intuitions (oh, and in the user’s chosen language). I am aware that AI has some serious downsides. The most serious to me is its prodigous energy use, but there are also good questions to be asked about the way that training works and the possibility of not respecting licenses. The issues are real but avoiding AI is not the way to solve them. Just in the course of writing this post, DeepSeek was announced, demonstrating that there is a lot of potential to lower the costs of training. As far as the ethics and legality, that is a very complex space. Agents are already doing a lot to get better there, but note also that most of the applications I am excited about do not involve writing code so much as helping people understand and alter the code they’ve written. We don’t always get this right. For example, I find the combinator of iterators annoying because it takes the shortest of the two iterators, which is occasionally nice but far more often hides bugs.  ↩︎ The irony, of course, is that AI can help you to improve your woeful lack of tests by auto-generating them based on code coverage and current behavior.  ↩︎ I think they told me they heard it somewhere on the internet? Not sure the original source.  ↩︎ Personally, the thing I find most annoying about LLMs is the way they are trained to respond like groveling serveants. “Oh, that’s a good idea! Let me help you with that” or “I’m sorry, you’re right I did make a mistake, here is a version that is better”. Come on, I don’t need flattery. The idea is fine but I’m aware it’s not earth-shattering. Just help me already.  ↩︎ Inserting a call to is actually a bit more subtle than you might think, given the interaction of the future here.  ↩︎ Garbage Collection allows you to make all kinds of refactorings in ownership structure without changing your interface at all. This is convenient, but—as we discussed early on—it can hide bugs. Overall I prefer having that information be explicit in the interface, but that comes with the downside that changes have to be refactored.  ↩︎ I also think we should add a feature like View Types to make this less necessary. In this case instead of refactoring the type structure, AI could help by generating the correct type annotations, which might be non-obvious.  ↩︎ My hot take here is that if the idea of an LLM doing migrations in your code makes you uncomfortable, you are likely (a) overestimating the quality of your code and (b) underinvesting in tests and QA infrastructure 2 . I tend to view an LLM like a “inconsistently talented contributor”, and I am perfectly happy having contributors hack away on projects I own.  ↩︎ The student asks, “When unsafe code is proven free of UB, does that make it safe?” The master says, “Yes.” The student asks, “And is it then still unsafe?” The master says, “Yes.” Then, a minute later, “Well, sort of.” (We may need new vocabulary.)  ↩︎ My personal favorite story of this is when I asked ChatGPT to generate me a list of “real words and their true definition along with 2 or 3 humorous fake definitions” for use in a birthday party game. I told it that “I know you like to hallucinate so please include links where I can verify the real definition”. It generated a great list of words along with plausible looking URLs for merriamwebster.com and so forth—but when I clicked the URLs, they turned out to all be 404s (the words, it turned out, were real—just not the URLs).  ↩︎ This is not a unique property of Rust, it is shared by other languages with rich type systems, like Haskell or ML. Rust happens to be the most widespread such language.  ↩︎ I’d also like it if the LLM could be a bit less interrupt-y sometimes. Especially when I’m writing type-system code or similar things, it can be distracting when it keeps trying to author stuff it clearly doesn’t understand. I expect this too will improve over time—and I’ve noticed that while, in the beginning, it tends to guess very wrong, over time it tends to guess better. I’m not sure what inputs and context are being fed by the LLM in the background but it’s evident that it can come to see patterns even for relatively subtle things.  ↩︎

0 views
baby steps 8 months ago

Preview crates

This post lays out the idea of preview crates . 1 Preview crates would be special crates released by the rust-lang org. Like the standard library, preview crates would have access to compiler internals but would still be usable from stable Rust. They would be used in cases where we know we want to give users the ability to do X but we don’t yet know precisely how we want to expose it in the language or stdlib. In git terms, preview crates would let us stabilize the plumbing while retaining the ability to iterate on the final shape of the porcelain . Developing large language features is a tricky business. Because everything builds on the language, stability is very important, but at the same time, there are some questions that are very hard to answer without experience. Our main tool for getting this experience has been the nightly toolchain, which lets us develop, iterate, and test features before committing to them. Because the nightly toolchain comes with no guarantees at all, however, most users who experiment with it do so lightly, just using it for toy projects and the like. For some features, this is perfectly fine, particularly syntactic features like , where you can learn everything you need to know about how it feels from a single crate. Where nightly really fails us though is the ability to estimate the impact of a feature on a larger ecosystem. Sometimes you would like to expose a capability and see what people build with it. How do they use it? What patterns emerge? Often, we can predict those patterns in advance, but sometimes there are surprises, and we find that what we thought would be the default mode of operation is actually kind of a niche case. For these cases, it would be cool if there were a way to issue a feature in “preview” mode, where people can build on it, but it is not yet released in its final form. The challenge is that if we want people to use this to build up an ecosystem, we don’t want to disturb all those crates when we iterate on the feature. We want a way to make changes that lets those crates keep working until the maintainers have time to port to the latest syntax, naming, or whatever. The other tool we have for correct mistakes is editions . Editions let us change what syntax means and, because they are opt-in, all existing code continues to work. Editions let us fix a great many things to make Rust more self-consistent, but they carry a heavy cost. They force people to relearn how things in Rust work. The make books oudated. This price is typically too high for us to ship a feature knowing that we are going to change it in a future edition. To make this concrete, let’s take a specific example. The const generics team has been hard at work iterating on the meaning of and in fact there is a pending RFC that describes their work. There’s just one problem: it’s not yet clear how it should be exposed to users. I won’t go into the rationale for each choice, but suffice to say that there are a number of options under current consideration. All of these examples have been proposed, for example, as the way to say “a function that can be executed at compilation time which will call ”: At the moment, I personally have a preference between these (I’ll let you guess), but I figure I have about… hmm… 80-90% confidence in that choice. And what’s worse, to really decide between them, I think we have to see how the work on async proceeds, and perhaps also what kinds of patterns turn out to be common in practice for . This stuff is difficult to gauge accurately in advance. So what if we released a crate . In my dream world, this is released on crates.io, using the namespaces described in [RFC #3243][https://rust-lang.github.io/rfcs/3243-packages-as-optional-namespaces.html]. Like any crate, can be versioned. It would expose exactly one item, a macro that can be used to write const functions that have const trait bounds: Interally, this macro can make use of internal APIs in the compiler to parse the contents and deploy the special semantics. Now, maybe we use this for a while, and we find that people really don’t like the , so we decide to change the syntax. Perhaps we opt to write instead of . No problem, we release a 2.0 version of the crate and we also rewrite 1.0 to take in the tokens and invoke 2.0 using the semver trick . Once we decide we are happy with we can merge it into the language proper. The preview crates are deprecated and simply desugar to the true language syntax. We all go home, drink non-fat flat whites, and pat ourselves on the back. One thing I like about the preview crates is that then others can begin to do their own experiments. Perhaps somebody wants to try out what it would be like it meant by default–they can readily write a wrapper that desugars to and try it out. And people can build on it. And all that code keeps working once we integrate const functions into the language “for real”, it just looks kinda dated. Even if we know the semantics, we could use previews to stabilize features where the user experience is not great. I’m thinking of Generic Associated Types as one example, where the stabilization was slowed because of usability concerns. The previous answers hints at one of my fears… if preview crates become a widespread way for us to stabilize features with usability gaps, we may accumulate a very large number of them and then never move those features into Rust proper. That seems bad. I mean…maybe? I do think we are sometimes very cautious. I would like us to get better at leaning on our judgment. But I also seem that sometimes there is a tension between “getting something out the door” and “taking the time to evaluate a generalization”, and it’s not clear to me that this tension is an inherent complexity or an artificial artifact of the way we do business. One very special thing about libstd is that it is released together with the compiler and hence it is able to co-evolve, making use of internal APIs that are unstable and change from release to release. If we want to put this crate on crates.io, it will not be able to co-evolve in the same way. Bah. That’s annoying! But I figure we still handle it by actually having the preview functionality exposed by crates in sysroot that are shipping along the compiler. These crates would not be directly usable except by our blessed crates.io crates, but they would basically just be shims that expose the underlying stuff. We could of course cut out the middleman and just have people use those preview crates directly– but I don’t like that as much because it’s less obvious and because we can’t as easily track reverse dependencies on crates.io to evaluate usage. I also considered the idea of having keywords (“preview”), so e.g. Using a keyword would fire off a lint ( ) that you would probably want to . This is less intrusive, but I like the crate idea better because it allows us to release a v2.0 of the keyword. Good question. I’m not entirely sure. It seems like APIs that require us to define new traits and other things would be a bit tricky to maintain the total interoperability I think we want. Tools like trait aliases etc (which we need for other reasons) would help. Ember has formalized this “plumbing first” approach in their version of editions . In Ember, from what I understand, an edition is not a “time-based thing”, like in Rust. Instead, it indicates a big shift in paradigms, and it comes out when that new paradigm is ready. But part of the process to reaching an edition is to start by shipping core APIs (plumbing APIs) that create the new capabilities. The community can then create wrappers and experiment with the “porcelain” before the Ember crate enshrines a best practice set of APIs and declares the new Edition ready. Java has a notion of preview features, but they are not semver guaranteed to stick around. I’m not sure who else! Usability of decorators like is better, particularly in rust-analyzer. The tricky bit there is that decorates can only be applied to valid Rust syntax, so it implies we’d need to extend the parser to include things like forever, whereas I might prefer to have that complexity isolated to the crate. I don’t know! People often think that because I write a blog post about something it will happen, but this is currently just in “early ideation” stage. As I’ve written before, though, I continue to feel that we need something kind of “middle state” for our release process (see e.g. this blog post, Stability without stressing the !@#! out ), and I think preview crates could be a good tool to have in our toolbox. Hat tip to Yehuda Katz and the Ember community, Tyler Mandry, Jack Huey, Josh Triplett, Oli Scherer, and probably a few others I’ve forgotten with whom I discussed this idea. Of course anything you like, they came up with, everything you hate was my addition.  ↩︎

0 views
baby steps 11 months ago

MinPin: yet another pin proposal

This post floats a variation of boats’ UnpinCell proposal that I’m calling MinPin . 1 MinPin’s goal is to integrate into the language in a “minimally disruptive” way 2 – and in particular a way that is fully backwards compatible. Unlike , MinPin does not attempt to make and “play nicely” together. It does however leave the door open to add in the future, and I think helps to clarify the positives and negatives that would bring. Here is a brief summary of MinPin’s rules Before I go further I want to layout some of my design axioms (beliefs that motivate and justify my design). For the rest of the post I’m just going to go into FAQ mode. Yes. I think the rule of thumb would be this. For any given type, you should decide whether your type cares about pinning or not. Most types do not care about pinning. They just go on using and as normal. Everything works as today (this is the “zero-conceptual-cost” goal). But some types do care about pinning. These are typically future implementations but they could be other special case things. In that case, you should explicitly implement to declare yourself as pinnable. When you declare your methods, you have to make a choice This design works well so long as all mutating methods can be categorized into before-or-after pinning. If you have methods that need to be used in both settings, you have to start using workarounds – in the limit, you make two copies. Those of you who have been following the various posts in this area will recognize many elements from boats’ recent UnpinCell . While the proposals share many elements, there is also one big difference between them that makes a big difference in how they would feel when used. Which is overall better is not yet clear to me. Let’s start with what they have in common. Both propose syntax for pinned references/borrows (albeit slightly different syntax) and both include a type for “opting out” from pinning (the eponymous in UnpinCell , in MinPin). Both also have a similar “special case” around in which writing a drop impl with disables safe pin-projection. Where they differ is how they manage generic structs like , where it is not known whether or not they are . The , the question is whether we can project the field : There is a specific danger case that both sets of rules are trying to avoid. Imagine that implements but does not – e.g., imagine that you have a . In that case, the referent of the reference is not actually pinned, because the type is unpinnable. If we permitted the creation of a , where , we would be under the (mistaken) impression that is pinned. Bad. UnpinCell handles this case by saying that projecting from a is only allowed so long as there is no explicit impl of for (“if [WrapFuture<F>] implements , it does so using the auto-trait mechanism, not a manually written impl”). Basically: if the user doesn’t say whether the type is or not, then you can do pin-projection. The idea is that if the self type is , that will only be because all fields are unpin (in which case it is fine to make references to them); if the self type is not , then the field is pinned, so it is safe. In contrast, in MinPin, this case is only allowed if there is an explicit impl for : Explicit negative impls are not allowed on stable, but they were included in the original auto trait RFC. The idea is that a negative impl is an explicit, semver-binding commitment not to implement a trait. This is different from simply not including an impl at all, which allows for impls to be added later. I’m not totally sure which of these is better. I came to the impl based on my axiom that pin is its own world – the idea was that it was better to push types to be explicitly unpin all the time than to have “dual-mode” types that masquerade as sometimes pinned and sometimes not. In general I feel like it’s better to justify language rules by the presence of a declaration than the absence of one. So I don’t like the idea of saying “the absence of an impl allows for pin-projection” – after all, adding impls is supposed to be semver-compliant. Of course, that’s much lesss true for auto traits, but it can still be true. In fact, has had some unsoundness in the past based on unsafe reasoning that was justified by the lack of an impl. We assumed that could never implemented , but it turned out to be possible to add weird impls of in very specific cases. We fixed this by adding an explicit impl . On the other hand, I can imagine that many explicitly implemented futures might benefit from being able to be ambiguous about whether they are . The way I see it is that, in Rust today (and in MinPin, pinned places, UnpinCell, etc), if you have a type (that is, a type that is pinnable), it lives a double life. Initially, it is unpinned, and you interact can move it, -ref it, or -ref it, just like any other Rust value. But once a value becomes pinned to a place, it enters a different state, in which you can no longer move it or use , you have to use : One-way transitions like this limit the amount of interop and composability you get in the language. For example, if my type has methods, I can’t use them once the type is pinned, and I have to use some workaround, such as duplicating the method with . 3 In this specific case, however, I don’t think this transition is so painful, and that’s because of the specifics of the domain: futures go through a pretty hard state change where they start in “preparation mode” and then eventually start executing. The set of methods you need at these two phases are quite distinct. So this is what I meant by “pin is its own world”: pin is not very interopable with Rust, but this is not as bad as it sounds, because you don’t often need that kind of interoperability. With , when you pin a value in place, you just gain the ability to use , you don’t give up the ability to use : Making pinning into a “superset” of the capabilities of pinned means that can be coerced into an (it could even be a “true subtype”, in Rust terms). This in turn means that a method can invoke methods, which helps to make pin feel like a smoothly integrated part of the language. 3 Not exactly, but I do think that if is justified, it is not on the basis of , it is on the basis of immutable fields . If you just look at , then does make work better, but it does that by limiting the capabilities of to those that are compatible with . There is no free lunch! As Eric Holk memorably put it to me in privmsg: It seems like there’s a fixed amount of inherent complexity to pinning, but it’s up to us how we distribute it. Pin keeps it concentrated in a small area which makes it seem absolutely terrible, because you have to face the whole horror at once. 4 I think as designed is a “zero-conceptual-cost” abstraction, meaning that if you are not trying to use it, you don’t really have to care about it. That’s worth maintaining, if we can. If we are going to limit what can do, the reason to do it is primarily to get other benefits, not to benefit pin code specifically. To be clear, this is largely a function of where we are in Rust’s evolution. If we were still in the early days of Rust, I would say is the correct call. It reminds me very much of the IMHTWAMA , the core “mutability xor sharing” rule at the heart of Rust’s borrow checker. When we decided to adopt the current borrow checker rules, the code was about 85-95% in conformance. That is, although there was plenty of aliased mutation, it was clear that “mutability xor sharing” was capturing a rule that we already mostly followed, but not completely. Because combining aliased state with memory safety is more complicated, that meant that a small minority of code was pushing complexity onto the entire language. Confining shared mutation to types like and made most code simpler at the cost of more complexity around shared state in particular. There’s a similar dynamic around replace and swap. Replace and swap are only used in a few isolated places and in a few particular ways, but the all code has to be more conservative to account for that possibility. If we could go back, I think limiting to some kind of type would be a good move, because it would mean that the more common case can enjoy the benefits: fewer borrow check errors and more precise programs due to immutable fields and the ability to pass an and be sure that your callee is not swapping the value under your feet (useful for the “scope pattern” and also enables to be a subtype of ). The main reason was that I wanted a syntax that scaled to . But also the macro exists, making the keyword somewhat awkward (though not impossible). One thing I was wondering about is the phrase “pinned reference” or “pinned pointer”. On the one hand, it is really a reference to a pinned value (which suggests ). On the other hand, I think this kind of ambiguity is pretty common. The main thing I have found is that my brain has trouble with because it wants to think of as a “smart pointer” versus a modifier on another smart pointer. feels much better this way. Yeah, totally. So boats pinned places post introduced two futures, and . Here is how would look in MinPin, along with some inline comments: Yep! Here is : Drop’s current signature takes . But recall that once a type is pinned, it is only safe to use . This is a combustible combination. It means that, for example, I can write a that uses or swap to move values out from my fields, even though they have been pinned. For types that are always , this is no problem, because and are equivalent. For types that are always , I’m not too worried, because Drop as is is a poor fit for them, and will be beter. The tricky bit is types that are conditionally . Consider something like this: At least today, whether or not is depends on whether , so we can’t know it for sure. The solution that boats and I both landed on effectively creates three categories of types: 5 The idea is that using puts you in this purgatory category of being “unsafely pinnable” (it might be more accurate to say being “maybe unsafely pinnable”, since often at compilation time with generics we won’t know if there is an impl or not). You don’t get access to safe pin projection or other goodies, but you can do projection with unsafe code (e.g., the way the crate does it today). Yes, it does, but in fact any method whose trait uses can be implemented safely with so long as . So we could just allow that in general. This would be cool because many hand-written futures are in fact , and so they could implement the method with . Well, it’s true that an type can use in place of , but in fact we don’t always know when types are . Moreover, per the zero-conceptual-cost axiom, we don’t want people to have to know anything about to use . The obvious approaches I could think of all either violated that axiom or just… well… seemed weird: These considerations let me to conclude that actually the current design kind of puts in a place where we want three categories. I think in retrospect it’d be better if were implemented by default but not as an auto trait (i.e., all types were unconditionally unless they declare otherwise), but oh well. I mentioned early on that MinPin could be seen as a first step that can later be extended with if we choose. How would that work? Basically, if we did the change, then we would These changes mean that is pin-preserving. If , then may be pinned, but then won’t allow it to be overwritten, replaced, or swapped, and so pinning guarantees are preserved (and then some, since technically overwrites are ok, just not replacing or swapping). As a result, we can simplify the MinPin rules for pin-projection to the following: Given a reference , the rules for projection of the field are as follows: We actually got a bit of a preview when we talked about . Remember how we had to introduce around the final value so that we could swap it out? If we adopted , I think the TL;DR of how code would be different is that most any code that today uses or would probably wind up using an explicit -like wrapper. I’ll cover this later. This goes a bit to show what I meant about there being a certain amount of inherent complexity that we can choose to distibute: in MinPin, this pattern of wrapping “swappable” data is isolated to methods in types. With , it would be more widespread (but you would get more widespread benefits, as well). My conclusion is that this is a fascinating space to think about! 6 So fun. Hat tip to Tyler Mandry and Eric Holk who discussed these ideas with me in detail.  ↩︎ MinPin is the “minimal” proposal that I feel meets my desiderata; I think you could devise a maximally minimal proposal is even smaller if you truly wanted.  ↩︎ It’s worth noting that coercions and subtyping though only go so far. For example, can be coerced to , but we often need methods that return “the same kind of reference they took in”, which can’t be managed with coercions. That’s why you see things like and .  ↩︎   ↩︎ I would say that the current complexity of pinning is, in no small part, due to accidental complexity , as demonstrated by the recent round of exploration, but Eric’s wider point stands.  ↩︎ Here I am talking about the category of a particular monomorphized type in a particular version of the crate. At that point, every type either implements or it doesn’t. Note that at compilation time there is more grey area, as they can be types that may or may not be pinnable, etc.  ↩︎ Also that I spent way too much time iterating on this post. JUST GONNA POST IT.  ↩︎

0 views
baby steps 1 years ago

The `Overwrite` trait and `Pin`

In July, boats presented a compelling vision in their post pinned places . With the trait that I introduced in my previous post, however, I think we can get somewhere even more compelling, albeit at the cost of a tricky transition. As I will argue in this post, the trait effectively becomes a better version of the existing trait, one that effects not only pinned references but also regular references. Through this it’s able to make fit much more seamlessly with the rest of Rust. Before I dive into the details, let’s start by reviewing a few examples to show you what we are aiming at (you can also skip to the TL;DR , in the FAQ). I’m assuming a few changes here: The first change is “mildly” backwards incompatible. I’m not going to worry about that in this post, but I’ll cover the ways I think we can make the transition in a follow up post. We would really like to add a generator syntax that lets you write an iterator more conveniently. 1 For example, given some slice , we should be able to define a generator that iterates over the string lengths like so: But there is a catch here! To permit the borrow of , which is owned by the generator, the generator will have to be pinned. 2 That means that generators cannot directly implement , because generators need a signature for their methods. It is possible, however, to implement for where is a generator. 3 In today’s Rust, that means that using a generator as an iterator would require explicit pinning: With pinned places , this feels more builtin, but it still requires users to actively think about pinning for even the most basic use case: Under this proposal, users would simply be able to ignore pinning altogether: Pinning is still happening: once a user has called , they would not be able to move after that point. If they tried to do so, the borrow checker (which now understands pinning natively) would give an error like: As noted, it is possible to move after pinning, but only if you pin it into a heap-allocated box. So we can advise users how to do that. The pinned places post included an example future called . I’m going to implement that same future in the system I describe here. There are some comments in the example comparing it to the version from the pinned places post . Let’s complete the journey by implementing a future: OK, now that I’ve lured you in with code examples, let me drive you away by diving into the details of . I’m going to cover the way that I think about . It is similar to but different from how is presented in the pinned places post – in particular, I prefer to think about places that pin their values and not pinned places . In any case, is surprisingly subtle, and I recommend that if you want to go deeper, you read boat’s history of post and/or the stdlib documentation for . The type is unusual in Rust. It looks similar to a “smart pointer” type, like , but it functions differently. is not a pointer, it is a modifier on another pointer, so and so forth. You can think of a type as being a pointer of type that refers to a place (Rust jargon for a location in memory that stores a value) whose value has been pinned . A pinned value can never be moved to another place in memory. Moreover, must be dropped before its place can be reassigned to another value. The way I think about, every place in memory has a lifecycle: When first allocated, a place is uninitialized – that is, has no value at all. An uninitialized place can be freed . This corresponds to e.g. popping a stack frame or invoking . may at some point become initialized by an assignment like . At that point, there are three ways to transition back to uninitialized: Alternatively, the value can be pinned in place : Once a value is pinned, moving or forgetting the value is not allowed. These actions are “undefined behavior”, and safe Rust must not permit them to occur. As most folks know, Rust does not guarantee that destructors run. If you have a value whose destructor never runs, we say that value is leaked . There are however two ways to leak a value, and they are quite different in their impact: In retrospect, I wish that Option A did not exist – I wish that we had not added . We did so as part of working through the impact of ref-count cycles. It seemed equivalent at the time (“the dtor doesn’t run anyway, why not make it easy to do”) but I think this diagram shows why it adding forget made things permanently more complicated for relatively little gain. 4 Oh well! Can’t win ’em all. There is one subtle aspect here: not all values can be pinned. If a type implements , then values of type cannot be pinned. When you have a pinned reference to them, they can still squirm out from under you via or other techniques. Another way to say the same thing is to say that values can only be pinned if their type is (“does not implement ”). Types that are can be called address sensitive , meaning that once they pinned, there can be pointers to the internals of that value that will be invalidated if the address changes. Types that implement would therefore be address insensitive . Traditionally, all Rust types have been address insensitive, and therefore is an auto trait, implemented by most types by default. Looking at the state machine as I describe it here, we can see that possessing a isn’t really a pinned mutable reference, in the sense that it doesn’t always refer to a place that is pinning its value. If , then it’s just a regular reference. But if , then a pinned reference guarantees that the value it refers to is pinned in place. This fits with the name , which I believe was meant to convey that idea that, even if you have a pinned reference to a value of type , that value can become unpinned. I’ve heard the metaphor of “if , you can left out the pin, swap in a different value, and put the pin back”. Everyone agrees that is confusing and a pain to use. But what makes it such a pain? If you are attempting to author a Pin-based API, there are two primary problems: If you attempting to consume a Pin-based API, the primary annoyance is that getting a pinned reference is hard. You can’t just call methods normally, you have to remember to use or first. (We saw this in Example 1 from this post.) This post is focused on a proposal with two parts: I’m going to walk through those in turn. The first part of my proposalis a change I call . The idea is to introduce and then change the “place lifecycle” to reference instead of : For to work well, we have to make all types also be . This is not, strictly speaking, backwards compatible, since today types (like all types) can be overwritten and swapped. I think eventually we want every type to be by default, but I don’t think we can change that default in a general way without an edition. But for types in particular I suspect we can get away with it, because types are pretty rare, and the simplification we get from doing so is pretty large. (And, as I argued in the previous post, there is no loss of expressiveness ; code today that overwrites or swaps values can be locally rewritten.) Today, cannot be converted into an reference unless . 5 This because it would allow safe Rust code to create Undefined Behavior by swapping the referent of the reference and hence moving the pinned value. By requiring that , the impl is effectively limiting itself to references that are not, in fact, in the “pinned” state, but just in the “initialized” state. This leads directly to our first two pain points. To start, from a method, you can only invoke methods (via the impl) or other methods. This schism separates out the “regular” methods of a type from its pinned methods; it also means that methods doing field assignments don’t compile: This errors because compiling a field assignment requires a impl and doesn’t have one. allows us to implement for all pinned types. This is because, unlike , affects how works, and hence would preserve the pinned state for the place it references. Consider the two possibilities for the value of type referred to by the : This implies that is in fact a generalized version of . Every keeps the value pinned for the duration of its lifetime , but a ensures the value stays pinned for the lifetime of the underlying storage. If we have a impl, then methods can freely call methods. Big win! The other pain point today with is that we have no native support for “pin projection” 6 . That is, you cannot safely go from a reference to a method that referring to some field without relying on unsafe code. The most common practice today is to use a custom crate like pin-project-lite . Even then, you also have to make a choice for each field between whether you want to be able to get a reference or a normal reference. Fields for which you can get a pinned reference are called structurally pinned and the criteria for which one you should use is rather subtle. Ultimately this choice is required because and don’t play nicely together. With , we can scrap the idea of structural pinning. Instead, if we have a field owner , pinned projection is allowed so long as . That is, if , then I can always get a reference to some field of type . How is that possible? Actually, the full explanation relies on borrow checker extensions I haven’t introduced yet. But let’s see how far we get without them, so that we can see the gap that the borrow checker has to close. Assume we are creating a reference to some field , where : There are three ways to move a value out of : Today, getting a requires using the macro, going through , or some similar explicit action. This adds “syntactic salt” to calling a some other abstraction rooted in unsafe (e.g., ). There is no built-in way to safely create a pinned reference. This is fine but introduces ergonomic hurdles We want to make calling a method as easy as calling an method. To do this, we need to extra the compiler’s notion of “auto-ref” to include the option of “auto-pin-ref”: Just as a typical method call like expands to , the compiler would be expanding to something like so: This expansion though includes a new piece of syntax that doesn’t exist today, the operation. (I’m lifting this syntax from boats’ pinned places proposal.) Whereas results in an reference (assuming ), borrow would result in a . It would also make the borrow checker consider the value in to be pinned . That means that it is illegal to move out from . The pinned state continues indefinitely until goes out of scope or is overwritten by an assignment like (which drops the heretofore pinned value). This is a fairly straightforward extension to the borrow checker’s existing logic. It’s worth noting that we don’t actually need the syntax (which means we don’t need the keyword). We could make it so that the only way to get the compiler to do a pinned borrow is via auto-ref. We could even add a silly trait to make it explicit, like so: Now you can write , which the compiler would desugar to . Here I am using to denote an “internal keyword” that users can’t type. 7 The shortest version of this post I can manage is 8 Indeed the trait as I defined it is overkill for pinning. The more precise, we might imagine two special traits that affect how and when we can drop or move values: Today, every type is . What I argued in the previous post is that we should make the default be that user-defined types implement neither of these two traits (over an edition, etc etc). Instead, you could opt-in to both of them at once by implementing . But we could get all the pin benefits by making a weaker change. Instead of having types opt out from both traits by default, they could only opt out of , but continue to implement . This is enough to make pinning work smoothly. To see why, recall the pinning state diagram : dropping the value in (permitted by ) will exit the “pinned” state and return to the “uninitialized” state. This is valid. Swapping, in contrast, is UB. Two subtle observations here worth calling out: EDIT: An earlier draft of this post named the trait . This was wrong, as described in the FAQ on subtle reasoning . Opting out of overwrites (i.e., making the default be neither nor ) gives us the additional benefit of truly immutable fields. This will make cross-function borrows less of an issue, as I described in my previous post, and make some other things (e.g., variance) less relevant. Moreover, I don’t think overwriting an entire reference like is that common, versus accessing individual fields. And in the cases where people do do it, it is easy to make a dummy struct with a single field, and then overwrite instead of . To me, therefore, distinguishing between and doesn’t obviously carry its weight. All the trait names I’ve given so far ( , , ) answer the question of “what operation does this trait allow”. That’s pretty common for traits (e.g., or, for that matter, ) but it is sometimes useful to think instead about “what kinds of types should implement this trait” (or not implement it, as the case may be). My current favorite “semantic style name” is , which corresponds to implementing . A mobile type is one that, while borrowed, can move to a new place. This name doesn’t convey that it’s also ok to drop the value, but that follows, since if you can swap the value to a new place, you can presumably drop that new place. I don’t have a “semantic” name for . As I said, I’m hard pressed to characterize the type that would want to implement but not . These traits pertain to whether an owner who lends out a local variable (i.e., executes ) can rely on that local variable to store the same value after the borrow completes. Under this model, the answer depends on the type of the local variable: Let’s use an analogy. Suppose I own a house and I lease it out to someone else to use. I expect that they will make changes on the inside, such as hanging up a new picture. But I don’t expect them to tear down the house and build a new one on the same lot. I also don’t expect them to drive up a flatbed truck, load my house onto it, and move it somewhere else (while proving me with a new one in return). In Rust today, a reference reference allows all of these things: EDIT: Wording refined based on feedback. One question I received was what it meant for two structs to have the “same value”? Imagine a struct with all public fields – can we make any sense of it having an identity ? The way I think of it, every struct has a “ghost” private field (one that doesn’t exist at runtime) that contains its identity. Every expression has an implicit that assigns the identity a distinct value from every other struct that has been created thus far. If two struct values have the same , then they are the same value. Admittedly, if a struct has all public fields, then it doesn’t really matter whether it’s identity is the same, except perhaps to philosophers . But most structs don’t. An example that can help clarify this is what I call the “scope pattern”. Imagine I have a type that has some private fields and which can be “installed” in some way and later “deinstalled” (perhaps it modifies thread-local values): And the only way for users to get their hands on a “scope” is to use , which ensures it is installed and deinstalled properly: It may appear that this code enforces a “stack discipline”, where nested scopes will be installed and deinstalled in a stack-like fashion. But in fact, thanks to , this is not guaranteed: This could easily cause logic bugs or, in unsafe is involved, something worse. This is why lending out scopes requires some extra step to be safe, such as using a -reference or adding a “fresh” lifetime paramteer of some kind to ensure that each scope has a unique type. In principle you could also use a type like , because the compiler disallows overwriting or swapping values: but I think it’s ambiguous today whether unsafe code could validly do such a swap. EDIT: Question added based on feedback. I am pretty sure! But not 100%. I’m definitely scared that people will point out some obvious flaw in my reasoning. But of course, if there’s a flaw I want to know. To help people analyze, let me recap the two subtle arguments that I made in this post and recap the reasoning. Lemma. Given some local variable where mutably borrowed by a reference , the value in cannot be dropped, moved, or forgotten for the lifetime . During , the variable cannot be accessed directly (per the borrow checker’s usual rules). Therefore, any drops/moves/forgets must take place to : Theorem A. If we replace and , then is a safe subtype of . The argument proceeds by cases: Theorem B. Given some field owner where with a field , it is safe to pin-project from to a reference referring to . The argument proceeds by cases: EDIT: It was pointed out to me that this last theorem isn’t quite proving what it needs to prove. It shows that will not be disturbed for the duration of the borrow, but to meet the pin rules, we need to ensure that the value is not swapped even after the borrow ends. We can do this by committing to never permit swaps of values unless , regardless of whether they are borrowed. I meant to clarify this in the post but forgot about it, and then I made a mistake and talked about – but is the right name. Geez, I’m so glad you asked! Such a thoughtful question. To be honest, the part of this post that I am happiest with is the state diagram for places, which I’ve found very useful in helping me to understand : Obviously this question was just an excuse to reproduce it again. Some of the key insights that it helped me to crystallize: In thinking through the stuff I wrote in this post, I’ve found it very useful to go back to this diagram and trace through it with my finger. Maybe? The question does not have a simple answer. I will address in a future blog post in this series. Let me say a few points here though: First, the proposal is not backwards compatible as I described. It would mean for example that all futures returned by are no longer . It is quite possible we simply can’t get away with it. That’s not fatal, but it makes things more annoying. It would mean there exist types that are but which can be overwritten. This in turn means that is not a subtype of for all types. Pinned mutable references would be a subtype for almost all types, but not those that are . Second, a naive, conservative transition would definitely be rough. My current thinking is that, in older editions, we add bounds by default on type parameters and, when you have a bound, we would expand that to include a bound on associated types in , like . When you move to a newer edition I think we would just not add those bounds. This is kind of a mess, though, because if you call code from an older edition, you are still going to need those bounds to be present. That all sounds painful enough that I think we might have to do something smarter, where we don’t always add bounds, but instead use some kind of inference in older editions to avoid it most of the time. My takeaway from authoring this post is that something like has the potential to turn from wizard level Rust into mere “advanced Rust”, somewhat akin to knowing the borrow checker really well. If we had no backwards compatibility constraints to work with, it seems clear that this would be a better design than as it is today. Of course, we do have backwards compatibility constraints, so the real question is how we can make the transition. I don’t know the answer yet! I’m planning on thinking more deeply about it (and talking to folks) once this post is out. My hope was first to make the case for the value of (and to be sure my reasoning is sound) before I invest too much into thinking how we can make the transition. Assuming we can make the transition, I’m wondering two things. First, is the right name? Second, should we take the time to re-evaluate the default bounds on generic types in a more complete way? For example, to truly have a nice async story, and for myraid other reasons, I think we need must move types . How does that fit in? The precise design of generators is of course an ongoing topic of some controversy. I am not trying to flesh out a true design here or take a position. Mostly I want to show that we can create ergonomic bridges between “must pin” types like generators and “non pin” interfaces like in an ergonomic way without explicit mentioning of pinning.  ↩︎ Boats has argued that, since no existing iterator can support borrows over a yield point, generators might not need to do so either. I don’t agree. I think supporting borrows over yield points is necessary for ergonomics just as it was in futures .  ↩︎ Actually for .  ↩︎ I will say, I use quite regularly, but mostly to make up for a shortcoming in . I would like it if had a separate method, , and we invoked that method when unwinding. Most of the time, it would be the same as regular drop, but in some cases it’s useful to have cleanup logic that only runs in the case of unwinding.  ↩︎ In contrast, a reference can be safely converted into an reference, as evidenced by Pin’s impl . This is because, even if , a reference cannot do anything that is invalid for a pinned value. You can’t swap the underlying value or read from it.  ↩︎ Projection is the wonky PL term for “accessing a field”. It’s never made much sense to me, but I don’t have a better term to use, so I’m sticking with it.  ↩︎ We have a syntax for explicitly referred to a keyword . It is meant to be used only for keywords that will be added in future Rust editions. However, I sometimes think it’d be neat to internal-ish keywords (like ) that are used in desugaring but rarely need to be typed explicitly; you would still be able to write if for whatever reason you wanted to. And of course we could later opt to stabilize it as (no prefix required) in a future edition.  ↩︎ I tried asking ChatGPT to summarize the post but, when I pasted in my post, it replied, “The message you submitted was too long, please reload the conversation and submit something shorter.” Dang ChatGPT, that’s rude! Gemini at least gave it the old college try . Score one for Google. Plus, it called my post “thought-provoking!” Aww, I’m blushing!  ↩︎

0 views