Evolving programming languages in the AI era
- AI , runtime , guarantees
This post is a collection of short ramblings on how programming languages may evolve in the AI era.
It is split into two parts: Reflections and Agentic tooling. The first raises questions about what happens to programming languages, their ecosystems, and their communities when humans are no longer writing most of the code. The second is more concrete and opinionated: how our tools should improve now that coding agents have become users of our languages.
My opinions on these topics will probably change, but this is a reasonable digest of what’s been on my mind lately.
Reflections
This section explores how programming languages and their communities change if agents write most of our code. I am not yet convinced this will happen. However:
- it is already the reality for many developers and teams; and
- I believe it is my responsibility to explore that possibility regardless of how likely I think it is.
Let’s get started.
On community
At the center of most programming languages, there is a community that rallies around a set of shared sensibilities. Python has its emphasis on an obvious way to do something. Ruby has long cultivated an appreciation for programmer happiness. Lisp communities have traditionally celebrated the ability to reshape the language itself.
But what happens when we stop writing most of the code? And how will that impact our sense of belonging? Is that something we should try to preserve or should communities find out what will be the next thing that glues them together?
Following on the same lines, we build ecosystems around languages to tackle hard problems and create shared abstractions: web frameworks, tensor libraries, data-processing pipelines, GUI toolkits, and so on. Coding agents may affect these ecosystems in two opposing ways.
First, the gap between ecosystems may get smaller. Building all of these frameworks requires a considerable amount of time and effort, much of which agents can potentially trim down. This is especially true when the problem involves implementing known algorithms, translating ideas from papers, or porting existing implementations between languages, allowing smaller communities to catch up with larger ones much more quickly.
On the other hand, if implementing something becomes cheap enough, will people still join efforts and collaborate on the same solution? If I need a library to solve problem X, I might just ask an agent to build exactly what I need.
This creates an interesting tension. Coding agents could dramatically reduce the cost of building an ecosystem while simultaneously weakening one of the forces that causes ecosystems to form in the first place.
On ergonomics
A reasonable chunk of how programming languages evolve is through adding syntactic affordances and improving ergonomics over time. However, if humans are no longer writing most of the code, how much do these changes matter?
For example, over the last decade, we saw an influx of languages adding optional chaining operators which are considerably nicer for a human to write than a sequence of explicit null checks. On the other hand, agents are not bothered by boilerplate and the difference is much less meaningful.
There is an argument that these affordances also make coding agents more token-efficient. But I’d argue that token efficiency is at the tail end of the characteristics we should optimize programming languages for, especially as models become cheaper, more efficient, and context windows grow larger.
I’d go as far as saying that any new programming language that claims to be made “for coding agents” and ultimately focuses on syntax is effectively building around today’s limitations. I have used agents to write HTML, CSS, JavaScript, Elixir, Rust, and Lean, and the differences in syntax that feel enormous to me seem considerably less important to them. From their perspective, it is all tokens-in, tokens-out.
On compilers
Whenever we discuss programming languages in this context, there is a common follow-up question: will we need programming languages in the first place? Perhaps coding agents will replace compilers and write assembly directly?
I don’t buy this version for a few reasons.
First, if you are building desktop application, you probably don’t want to maintain different assembly implementations for every architecture you support. You still want some architecture-independent representation and something capable of lowering it to the target machine. In other words, you have reinvented at least part of a compiler and a higher-level language, even if that language was never designed for humans to write.
Second, we have not found a single language or computational model that excels at everything. We have systems programming languages, theorem provers, languages for concurrent, distributed, and resilient software (e.g. Erlang/Elixir), query languages, hardware-description languages, and so on. They encode different semantics, different level of abstractions, with different guarantees. It is unreasonable to expect a single lower level language to unify all of those.
If programming languages are not going anywhere but we stop optimizing programming languages for the humans writing them, what should we optimize them for?
Agentic tooling
In the past, I have said that creating great tools for humans also leads to great tools for agents. I believe this will always be true. This has led us to build solutions that automate what we already do: agents write the same tests as us, consume the same program metadata as us, and read the same logs as us.
But what if we start using agents to perform actions we wouldn’t normally do ourselves? Perhaps because they are too tedious, have a steep learning curve, or require processing more information than is practical for a human? That’s what this section explores.
The tools in this section does not require agents to be writing most of the code. Even if you use agents for only 20% of your code, they could benefit from the tools outlined here.
Stronger guarantees over user constraints
Programming languages balance several competing goals, among them expressiveness, guarantees, and ergonomics. We want to express the programs we care about, we want the language to establish useful properties about those programs, and we want to make it accessible to developers. If agents are writing most of the code, we can revisit these trade-offs.
One such example is the inference of function signatures. This is valuable to humans because explicitly writing information the compiler can infer is tedious. Coding agents don’t care about tedium. If anything, making types and intentions explicit gives the compiler, other agents, and ourselves more information to work with. More importantly, the languages whose types can be fully inferred are generally a subset of those whose types can be checked, so optimizing for inference can ultimately limit both expressiveness and the guarantees a type system can provide. Why impose those limits on agents when we’ve already seen them capable of writing proofs in much more complex systems?
Guarantees don’t always have to be statically established either. Memory safety may be enforced statically or by the runtime, such as through garbage collection. Model checking can bridge models and implementations by using model-generated execution traces to validate the actual system. Case in point: Erlang/Elixir rely on isolated processes and message passing to constrain how concurrent programs are structured, trading some expressiveness for stronger properties around isolation and fault tolerance. Not every concurrent algorithm maps efficiently to this model, but programs that do inherit useful guarantees from it.
Overall, there has never been a better time to provide stronger guarantees about our software. We are not able to formally verify all software, but we can combine different approaches to strengthen it:
- Correct by construction: the language makes invalid states or programs hard or impossible to express.
- Statically established: types, proofs, and static analysis establish properties before execution.
- Runtime-enforced: memory management, isolation, capability boundaries, and other runtime enforced properties.
- Empirically validated: program validation through tests, property-based testing, and fuzzing.
I believe the different ways languages combine these techniques and push the boundary between expressiveness and guarantees will play an increasing role in how they differentiate themselves and are adopted. Especially if you believe agents will make it easier for ecosystems to catch up with one another, as discussed in “On community”. On a similar note, frameworks must also adopt some of those practices at their own level of abstraction.
Program databases over LSPs
The death of IDEs has been pronounced several times over the last two years. Once the obituary is finally published, I don’t expect LSPs to survive either.
The Language Server Protocol was designed primarily for IDEs, and many of its operations are biased towards documents and positions: file, line, and column, which agents do not track precisely. In our experience building Tidewave, a CLI or tool where agents can ask “where is the documentation for foo_bar?” or “where is BarBaz defined?” is far more suitable than requiring them to provide a precise reference to where those symbols appear in the source code.
Furthermore, LSPs were designed to present information for human consumption rather than exploration. You gather information one piece at a time by hopping between source files.
The good news is that many language servers already build, or have access to, much of the information coding agents need: symbols, references, call graphs, type information, and sometimes data-flow information. My suggestion is to expose this information as a program database with a query language, be it SQLite, Datalog, or a custom DSL.
It would be unreasonable to ask most developers to write a query just to find all references to a function. Coding agents, on the other hand, would gladly do so: writing a program query is the same amount of work as invoking a CLI or LSP tool. More importantly, they could compose queries that would be impractical to expose as individual IDE features: find all public functions that eventually call this function or all paths through the program where a given value can become nil. Those databases could also be used as linters to guard agents against undesired practices.
This implies that locality remains extremely important, especially in large codebases. Features such as monkey-patching, implicit hooks, dynamic rebinding, and other forms of action at a distance mean that code written in one place can affect how the entire system behaves, in ways that are hard to trace even when program databases are available.
Runtime observability over debuggers
Debuggers are another interface designed primarily for humans. We set breakpoints, step through execution one line at a time, and inspect variables as we go. However, agents can instrument code, collect traces, and correlate information much faster than we can. We should give them interfaces that take advantage of that.
Furthermore, if the assumption is that coding agents are going to write most of our code, it is reasonable to expect them to also take on more responsibilities across the software development lifecycle, including monitoring and diagnosing production systems as they run, rather than relying on logs and dashboards.
We should expose the runtime and state in our systems in ways that agents can query and explore programmatically. Runtime observability can give agents a common interface to diagnose failures, identify reliability issues, and detect bottlenecks live across all environments.
Luckily, this is an area where Elixir has always excelled, thanks to the Erlang VM. Inspecting processes, sockets, applications, supervisors, ETS tables, message queues, and much more is a built-in capability of the runtime. The remaining gap is to expose these capabilities safely to agents, whether through a collection of tools, a query language, or a sandbox.
Acknowledgements: I want to thank Quinn Wilton, Chris McCord, Ryan Lopopolo, Chad Fowler, Rob Knight, Danila Poyarkov, and the multiple folks I met at ElixirConf for discussions and sharing work that has helped shape many of the thoughts in this article. All opinions are my own.
Disclaimer: AI was used to address stylistic and grammatical issues in the article.