Three months have passed since my last post. A lot has happened in the meantime, so I figured I’d write another post to share some recent progress and what’s been on my mind lately.

New Progress

First, an update on the project. Overall, clice has reached an initially usable state. All core features are implemented, and the server architecture is stable enough to run on projects the size of LLVM without crashing, with reasonable performance. Compared to clangd, clice introduces a number of major architectural innovations that solve many long-standing problems in the C++ toolchain:

  • Multi-process architecture: Isolates memory leaks and crashes caused by bugs in Clang itself. Compilation processes (worker processes) are separated from the query process (main process), so even if a worker crashes, the main process can gracefully respawn it while continuing to serve symbol queries without interruption. Using worker processes also gives us much more flexibility in scheduling compilation task priorities.
  • Compilation context: Handles cases where a single file in a project corresponds to multiple compile commands, or where files are not self-contained (non self contained). This has been an issue in clangd since day one and has never had a good solution. clice finally solves it completely, with extensive work built around this concept (for example, solving this requires the project’s dependency graph, but existing dependency scanners weren’t fast enough for a good user experience, so we built a custom dependency scanner tailored to our needs that is 10–100x faster).
  • Module compilation graph and shared compilation cache: Implements a dynamic module compilation graph where module dependencies can change dynamically, different modules can share compilation dependencies, and modules can be compiled concurrently. It includes a reference-counted cancellation mechanism so that when a module is no longer needed, its compilation can be gracefully canceled without burning any extra CPU. Compilation caches such as PCHs and PCMs are also persisted to disk for fast recovery on the next startup, greatly speeding up startup times.
  • Pseudo-template instantiator based on DFS pruning, used to provide smarter inference inside template contexts.

There are many other architectural innovations that I won’t list one by one here. I’ve written detailed documentation at https://docs.clice.io/clice/design/overview for anyone interested in reading further. In a way, this fulfills a promise I made to myself two years ago: those vague initial ideas have finally materialized in full, after a long process of exploration and tearing things down to start over—many of my earlier assumptions turned out not to be entirely right. Along the way, I also fixed a number of long-standing minor issues in clangd, which were easy enough to take care of in passing. I’ve also been fixing upstream Clang issues as needed—for instance, fixing a decade-old issue where Clang lacked source location information for explicit template instantiations.

Now that the architectural problems are solved, the next step is implementing individual features. Gathering user feedback is crucial at this stage, and I felt the timing was right, so I published a new pre-release version on 2026-07-19. You can search for clice VSCode directly in VSCode to download and try it out. It works out of the box with no extra configuration or downloads required—unlike clangd’s packaging strategy, I bundled the clice binary directly with the VSCode extension. It supports 6 platforms in total (Windows/Linux/macOS x arm64/x64). Feel free to download it and give it a spin!

Our release strategy is that every new commit on the main branch uploads build artifacts in CI. A scheduled job then publishes a nightly release with an odd version number every day. When the timing is right, I manually cut a stable release with an even version number.

That said, I want to emphasize that I’ve spent the past stretch focused on these core architectural problems and haven’t put much effort into polishing individual features yet, so plenty of bugs are to be expected (e.g., incorrect semantic highlighting, wrong code completion results, and so on). In fact, several people from the community chat have already tried it out and filed a lot of related issues. Over the next few months, my main focus will be fixing common issues based on early user feedback, alongside continuous performance optimization. Performance right now is reasonable, but there is still noticeable latency compared to clangd. This might be due to the extra overhead of our multi-process architecture, though in theory that overhead should be minimal, so it needs deeper investigation and optimization. Once things are ready, I’ll release the first official stable version. By then, I think clice will truly be able to replace clangd as the default choice for most users.

This stage is also a great time to get involved and contribute. Fixing or implementing a specific feature is relatively self-contained and straightforward, and doesn’t require a deep understanding of the entire codebase. After my ongoing improvements, the build and development environment can now be set up in a single step, keeping the barrier to entry very low. The relevant documentation is also fairly complete.

New Vision

The idea of writing a new C++ language server first came to me around this time two years ago. You can still find the comment from when I tried to write a pseudo-template instantiator for clangd. That experience was how I discovered that clangd was effectively unmaintained. Honestly, that completely upended my assumptions at the time—how could an open-source tool used by so many developers have no one maintaining it? From there, I started digging into how open-source software actually works and the complex community dynamics behind it. Half a year later, I published Design and Implementation of a New C++ Language Server, which received a lot of attention and encouragement—it seemed like everyone wanted a new language server. At the time, I estimated it would be mostly done and ready for real use sometime in 2025, but progress turned out to be much slower than I expected. The biggest reason was simply time: from the second semester of my junior year through senior graduation, I was juggling internships and school obligations, leaving only my spare time for clice. My original estimate assumed I’d be working on it full-time seven days a week, whereas in reality I only had parts of my weekends. At a rough estimate, taking about 3x longer than planned makes sense.

Recently, however, this problem has been largely alleviated. Back in February of this year, I started experimenting with models like opus4.6 to help me write clice. At first, it was still very much human-in-the-loop: I would constantly tweak the code written by the agent until it met my expectations. As I got deeper into using them and the models kept getting stronger—especially with the arrival of top-tier models like fable5—I found that most of the time I no longer needed to intervene in the agent’s coding at all. I just describe my requirements in detail and wait quietly for it to finish. In fact, all recent code in clice was written entirely by fable5. At this point, readers will naturally have some questions:

  • Can even a model like fable5 really handle a complex project like clice? How do you ensure the code written by the agent is correct?

A: We don’t need to guarantee that the agent’s code is “correct”—absolute correctness is impossible in the first place. We only need the accuracy to be within an acceptable range. What counts as acceptable? For example, if someone is working on a production system where a bug could cause massive losses, their bar for code correctness is obviously extremely high. Never mind agent-written code—they’re going to be super cautious even about code written by other humans that might affect their system. Unsurprisingly, programmers in those domains still largely stick to “artisanal” hand-coding, and the only person they truly trust may be themselves. For clice, as an offline tool, having some bugs is completely acceptable; we can just fix them later. In fact, even a project like Clang (which historically was written almost entirely by humans) constantly receives a flood of bug reports—being completely bug-free is simply impossible for any complex project. So clice can easily tolerate bugs introduced by agent-written code and iteratively improve and fix them based on user feedback. Furthermore, we can use various techniques to push the agent’s overall code accuracy as high as possible. For instance, code written directly by opus4.6 might only be right 50% of the time, while switching to fable bumps that to 70%. Then we layer on automated workflows: in my current setup, after fable finishes writing the code, it spins up three local opus subagents for review before opening a PR. On the PR, GPT5.6 and Coderabbit run multiple rounds of review; fable5 periodically pulls the review comments, resolves them, pushes again, and re-triggers the review pipeline for several interactive rounds. Combined with a carefully designed, comprehensive CI pipeline, this workflow brings accuracy up to 80%–90%. Think about it: if I were writing the code myself, coming home exhausted after a full day at work, would I hit that level of accuracy? Probably not. For clice, that level of accuracy is more than enough.

  • If fable writes all the code, what are you still needed for?

A: I actually tried letting fable work completely autonomously on this codebase—specifically, having a planner fable orchestrate other fable instances to code and complete requirements autonomously over long stretches of time. The codebase quickly turned into a mess. Even a frontier model like fable, constrained by its context window, still lags behind a human who has two years of thought and context invested in the project when it comes to understanding the whole picture. There is a vast amount of tacit knowledge that simply cannot be fully captured in code comments or project documentation. Keeping documentation from going stale is already a hard problem in itself, so I only maintain a small set of core architecture docs to give the AI extra context, making sure those stay accurate and up to date. My job, then, is to ensure the project remains long-term maintainable. How do I do that? Through a number of strategies—such as carefully designing a sound, multi-layered, verifiable testing pipeline. Right now we have five levels of testing: unit tests, integration tests, snapshot tests, smoke tests, and end-to-end tests. Each serves a distinct purpose and gives the AI clear feedback. From watching fable work, this test suite catches a huge number of mistakes, feeding them back to the model so it can course-correct. Moreover, a model like fable resorts to hacking the tests far less often than other models, so the overall result works remarkably well. I’m also responsible for guarding the overall architecture. If you look at clice’s commit history, you’ll see a lot of refactor commits—I regularly refactor and clean up the codebase to maintain code quality and strip out redundancy (in fact, I’ve found I really enjoy doing this). My typical workflow from Monday to Friday is to hand fable a few tasks before I leave for work; it works on them on its own, which usually takes quite a while because of the multi-round CI and review pipeline mentioned earlier—typically around 6 hours—and when I get home after work, I briefly check the key parts and merge if everything looks fine. Even though fable’s code style is already much better than other models’, there are still larger, more fundamental refactorings that can clean up and deduplicate substantial amounts of code, which it won’t do unless you explicitly prompt it to. I spend my weekends doing this kind of cleanup to keep the project sustainable over the long run, and the resulting code ends up being even cleaner and less prone to accumulating technical debt than human-written code.

This automated agent workflow has completely solved my time shortage—with time to spare, even, since I only need to spend a little time on weekends cleaning up the code. Yet while liberating productivity, it also brings a new challenge: if humans aren’t writing code anymore, what’s the point of a language server? Even though, as mentioned earlier, people in high-stakes scenarios will still do “artisanal” programming—writing and reading code themselves—those scenarios will keep shrinking as models continue to improve, and the long-term value of a language server is steadily declining. Put more broadly, anything designed primarily for human convenience in the past—web UIs, IDEs, and so on—is losing value. That is the inevitable trend of our time. In the early days of the project, sheer curiosity and the joy of learning new things were enough to keep me going, but the project is now in its later stages. I’m someone who really needs a sense of meaning in what I do; if a project doesn’t mean much in the long run, there’s not much point in continuing.

I’ve actually been mulling over this question for the past few months. I touched on it briefly in clice in the Agent Era, without going into much detail. Recently, my thinking on this has gone a bit deeper. First, we can make a few basic assumptions—for example, that software like compilers will still be valuable in the short to medium term. Let’s set aside any “long-term” conclusions: AI progress is completely non-linear, and human imagination is simply too limited to picture what future modes of production will look like. Honestly, we can’t even predict a year out—this time last year, I never could have imagined that I would barely need to participate in coding clice myself. Judging purely from where things stand today, software like compilers still holds value, and by extension, the infrastructure surrounding compilers—static analysis tools, documentation extraction and generation tools, code indexing and refactoring tools, and the like—still holds value as well.

Beyond that, I have a few more observations:

One thing the C++ community particularly loves is reinventing the wheel, and it happens across every layer of the stack: compilers, standard libraries, build systems, compilation caching tools, third-party libraries, and user code. A direct consequence is that the exact same logic often gets rewritten at multiple different layers. A classic example is locating the MSVC toolchain environment on Windows: Clang wrote its own implementation (for clang-cl), build systems wrote their own (e.g., xmake), users’ build scripts wrote it again (because sometimes it still fails to find MSVC), and even Rust’s cc-rs wrote its own version just to compile C++ dependencies… All this wheel-reinvention is completely unnecessary—it only degrades the user experience and wastes the community’s energy. Yet for C++, due to all sorts of reasons (mostly historical baggage), unifying things completely has become extraordinarily difficult.

Another manifestation of this is C++20 modules support, which requires tight coordination across the entire stack—compilers, build systems, and compilation caching tools. As things stand, the surrounding tooling is still fairly immature… and progress has been glacial. “Modules are a solved problem in the compiler frontend, and an unsolved problem everywhere else.”

  1. As a compiler project, LLVM/Clang’s greatest success lies in its modular design. That might not seem remarkable from today’s perspective, but rewind 20 years and it was way ahead of its time. This modularity made it possible to expose the compiler as an API, spawning a rich ecosystem of downstream tools such as clang-tidy, clangd, and clang-doc.
  2. However, there is a catch: Clang only provides an abstraction for the compiler, not for the “compilation” process itself. What does that mean? A compiler on its own is just a driver; actually compiling a project generally requires a build system. Yet existing build systems only serve tools like compilers and linkers. As mentioned earlier, there are many other Clang-based tools—index, tidy, doc, and so on—that behave very much like a compiler, but existing build tools have no way to schedule them. Clang itself also lacks an abstraction for this, which leads to similar scheduling logic being duplicated across multiple codebases: clangd has its own multi-threaded indexing scheduler, clang-tidy has no official scheduler at all (relying on Python scripts for multiprocessing), and clang-doc reinvented the wheel yet again. Since LLVM is a compiler community with little investment in asynchronous scheduling, this outcome isn’t surprising. In fact, in more modern programming languages, the compiler and the build system are no longer separate at all, so this situation never arises in the first place.
  3. On the surface, clice is a new C++ language server, but at its core it is a real-time, high-efficiency compilation scheduler driven by an input CDB file, with the language server being just one consumer of this system. This is precisely the abstraction the Clang community has been missing for so long! Once we recognize its true nature, we can easily extend this core with a suite of new capabilities:
  • clice flags: Get compile commands and related information for files
  • clice lint: Run lint checks across the entire project based on Clang/clang-tidy, suggesting and applying possible fixes
  • clice format: Format the entire project, either based on clang-format (token stream based) or combined with the semantic information we collect for more accurate formatting
  • clice index: Index the entire project’s code into distributable binary files for targeted queries
  • clice query: Provide rich query results on top of the index—such as call graphs, reference relations, include relations, inheritance graphs, and so on—returning structured output in txt/markdown/json for different consumers, whether for an agent to read or for various renderers to turn into images
  • clice doc: Extract documentation across the entire project into a structured format for external use, such as rendering a documentation website
  • clice refactor: Provide convenient utilities for large-scale refactoring, such as reordering functions or renaming symbols
  • clice modulize: Automatically migrate source code to C++20 named modules through appropriate source rewrites
  • ...

As mentioned earlier, the C++ tooling ecosystem is deeply fragmented—you have to cobble together a pile of separate tools just to get a passable experience. clice sits at a sweet spot: on one hand, it integrates Clang at the source level to provide semantic analysis; on the other, it implements a compilation scheduler that takes on part of the role of a build system (backend), enabling a rich set of features right out of the box. And it’s not just richer in features—it can also take performance to the next level. According to my profiling, clang-tidy spends roughly 10% of its time parsing and 90% running checks. Because header files work by copy-and-paste inclusion, the code inside headers gets checked redundantly across many different source files. Neither clang-tidy nor the build system can do anything about this on its own, because solving it fundamentally requires real-time cross-TU analysis where different analysis processes can communicate with each other—something that is very hard to pull off in the traditional, fully decoupled model. For clice, however, it’s straightforward: because it already uses a master-worker architecture, worker processes simply send checked declaration information back to the main process to be cached, allowing other workers to skip those cached declarations and dramatically speeding up clang-tidy—a 3–5x speedup is by no means a pipe dream. clice will also be a small step forward toward unifying the C++ toolchain ecosystem.

This is the new vision for clice. Under this model, even if humans are no longer the ones writing code, clice can still deliver real value as a tool used by agents. As for the more distant future, it’s impossible to say—I can’t even begin to imagine what things will look like if AGI truly arrives…

New Domain

Looking back, this time last year I was still torn over what career direction to pursue. My undergraduate major was actually Chemistry, so everything I know about programming is self-taught. For various reasons—mainly because I had also self-studied for the Chemistry Olympiad in high school without getting the results I hoped for—I had pretty much lost interest in chemistry. Switching majors was tough since my coursework grades were too poor, so I just followed my own interests and taught myself programming. My self-study path went roughly like this:

  • Freshman year: I started with C, initially following tutorials to download Visual Studio and write simple command-line programs. That got boring fast—before learning to code, most people picture programming as building graphical interfaces. Later I found out I could draw graphics using a library called EasyX, so I tried building some small games with it. That ended in failure because C lacks data structure abstractions, which meant I had to follow tutorials to implement my own data structures like vector and list; I think I got them working, though they were full of bugs. After that, I started learning Qt, since Qt provided much richer drawing capabilities and came with a standard library so I didn’t have to reinvent everything from scratch. I wrote a few small games in Qt, such as a Chinese chess game. But how do you play chess by yourself? I wanted to add online multiplayer, which led me to study socket programming, UDP/TCP, and multithreading. I even went through a lot of trouble to rent a cloud server and got a friend to help test it—we actually got remote multiplayer working, though I never finished the rest of the game.
  • Sophomore fall: I kept following my interests deeper down the rabbit hole. Around that time (roughly 2023), computer graphics and game engines were hot topics on Zhihu, and I heard learning them made it easy to land a job, so I jumped in too. I started working through the classic Learn OpenGL tutorial, calling APIs to draw simple scenes. I didn’t finish that tutorial either—I only covered the earlier sections on basic rendering and never dug into the more advanced graphics algorithms later on. Instead, I started thinking about how to build a game engine on top of those APIs. I wanted to implement Qt’s signal-slot mechanism in C++ without relying on MOC; with an event system and basic drawing in place, I’d have enough to build simple games. That led me into C++ template metaprogramming, and after spending a lot of time on it, I finally managed to replicate Qt’s signals and slots in pure C++. Using what I’d learned, I tried building a “game engine” and writing some small games with it—as I recall, I ended up making a simple platformer, mostly drawing character animations and handling basic interaction logic. Digging further, I learned that building a game engine requires reflection. What even is reflection? At the time, the concept was completely baffling to me—terms like static typing versus dynamic typing were totally foreign—so I went on another detour to study how various other programming languages are implemented. Later I realized reflection was connected to ASTs, which led me to study compiler frontend fundamentals. I also dabbled in programming language theory (PL) and operating systems. At one point I wanted to build my own compiler and OS from scratch: for the compiler I only ever wrote a bit of the frontend, and for the OS I only got as far as writing an x86 bootloader (Bootloader) with MinGW and booting it up in VMware. Both were built from scratch, and I didn’t stick with either of them to the end ovo. After that huge detour, I hadn’t finished a single project, yet I had picked up a ton of new knowledge and sharpened my C++ skills considerably—implementing those kinds of “frameworks” forces you to develop a deep understanding of C++’s language mechanics.
  • Sophomore spring: Having learned a lot about C++ templates, I had published a few blog posts on Zhihu. By coincidence, someone online—blueloveTH, the author of an open-source Python interpreter called pocketpy—wanted to build an ergonomic pybind11-style binding interface for his project. Seeing that I had plenty of relevant experience, he asked if I’d be interested in implementing it. It sounded fun, so I said yes right away and was super excited to dive in. He warned me upfront that this wasn’t something that could be knocked out in a few days, and to encourage me and make sure the project made it across the finish line, he planned to apply for GSoC with it—meaning there would be an $1800 stipend once it was completed. I didn’t even know what GSoC was at the time, but getting paid sounded great, so I followed his lead and my application succeeded. He turned out to be right: the project took me about three months. Even though I was already very familiar with C++ templates, templates were just the means of implementation—I still had to learn the “domain knowledge,” which for cross-language bindings meant understanding the distinct mechanics of both languages: how the Python VM works, GC timing, variable lifetimes, as well as C++ mechanics like RAII, exceptions, and so on. And just like that, I took another step down the path of becoming a C++ language lawyer Σ(゚Д゚). I was thrilled when the stipend came through—it was the first time I’d ever made money from programming, and it wasn’t a small amount either. Up to that point I’d always coded on a laptop, so I went straight out and bought two 4K monitors, one for my dorm and one for home, which made for a vastly better coding experience. I also switched my development environment from Visual Studio to VSCode + clangd, a setup that is much friendlier for C++ open-source projects.
  • Junior year: My junior year was mostly the story of Clang and clice, which you can read about in Design and Implementation of a New C++ Language Server, along with an internship along the way where I picked up some knowledge of the LLVM middle-end.

As junior year wrapped up, it was time to start thinking about job hunting, and I was honestly pretty worried. On one hand, my major was Chemistry; on the other, I basically “only knew C++” plus some familiarity with Clang’s codebase. That’s actually a tough spot to be in on the job market, mainly because of the lack of domain knowledge. Compiler roles were an obvious thought, but most of those positions look for middle-end or backend experience, which I didn’t really have, and many of them filtered out my resume on degree requirements alone. Since applying directly was rough going for me, I ended up writing a post on Zhihu to see if anyone online could help refer me ovo. With a bit of luck, I landed an internship working on LLM inference infra, and eventually converted to a full-time offer and stayed on here.

LLM inference infra was completely uncharted territory for me. What did I have to learn? Deep learning, LLMs, HPC, CUDA, computer architecture. Looking back at my self-study journey above, almost none of it overlapped with these areas. On one hand, it was pretty stressful—I was essentially starting from zero, and very little of my prior knowledge carried over. On the other hand, I found it genuinely fascinating; diving into a new field and learning new things always excites me. Now, about a year into interning here, I’ve built up a decent grasp of these areas and can handle my day-to-day work without much trouble, though on the whole I’m still pretty green (;゚Д゚). I just have to keep learning—honestly, there is so much to learn. Beyond inference itself, I can branch out horizontally into training and RL infra, or go deeper vertically into hardware and computer architecture (compute, communication, storage), and so on.

Right now, I’m mostly familiar with Attention kernels. Historically, writing CUDA kernels has been considered high-barrier work, and the standard solution has been to lower that barrier with DSL abstractions. But now agents have completely upended this problem—how could any barrier be lower than natural language? Although current agents still can’t complete complex kernel designs completely on their own due to model limitations, that won’t necessarily stay true in the future. In fact, I think DSLs suffer from another problem: choosing the right frontend abstraction is hard, especially when programming paradigms are changing so rapidly. If the abstraction is too high-level, it’s hard to control low-level details and too little information is exposed to the compiler, forcing it to be conservative and leaving many optimizations on the table. If the abstraction is too low-level, what’s the point of having a DSL at all? Personally, though, if we view a compiler as a search-and-optimization process, its optimization capability is inherently bounded because it operates over a closed set. If we can expose optimization decisions to an agent at the right level of abstraction and tap into the agent’s strong exploration capabilities, that might work better than having the agent write kernels directly as we do today. In that world, the compiler’s role shifts to guidance and verification.

New Beginning

While I was interning before graduation, the office was very close to campus—just a ten-minute subway ride away—so I kept living at school. Now that I’ve graduated, I had to rent my own place. It took me a week to find an apartment I’m really happy with, and I’ve been moved in for almost a month now. Everything happened so naturally, and just like that, college life came to an end. Honestly, I don’t feel all that sentimental about it—my college life was pretty monotonous. Before I started interning outside, my entire world was confined to campus; I barely even stepped outside the school gates, and when I did, it was just to eat at the McDonald’s right outside the entrance. Most of the time I was alone in my dorm room writing code and chatting with people in online group chats. Once I started interning, my radius expanded slightly to a few shopping areas near campus, and I also made some good friends in person.

Whenever I got bored alone at school, I’d watch anime. I only started watching anime in college, and the show that got me into it was Bocchi the Rock!. It was around the spring of my freshman year, and I was feeling pretty lost at the time. On one hand, I had no desire to study my major’s coursework and was struggling mentally; on the other, I had only just started learning to code and had no idea what I was going to do in the future. By chance I watched Bocchi the Rock! and was deeply moved by it—even after rewatching it many times, it still left me with a lingering sense of melancholy. That got me looking for similar shows, and I went on to watch many other great works. Still, whenever I feel a bit lost, I find myself rewatching Bocchi the Rock!—I’ve seen it seven or eight times now.

Originally, my vision for the future was to live like many of the open-source veterans I admired—sustaining a relatively free lifestyle driven by passion. But when it’s an open question whether the meaning of code itself will even exist in the future, that kind of open-source lifestyle may well disappear too. My feelings toward AI are complicated:

  • On one hand, it has boosted my productivity tremendously and freed me from all kinds of tedious chores—whether mundane school errands or the boring parts of writing code. At the same time, new changes bring new problems, new opportunities, and new possibilities; as a technology enthusiast, living through an era like this makes it hard not to want to be part of it.
  • On the other hand, it relentlessly forces you to question the meaning of what you do, triggering a pervasive crisis of meaning. When every concrete skill can be handed off to an agent to execute, what are we supposed to do as humans? What should we do right now? What about as agents keep getting more capable? And what about in the future?

Perhaps only time can answer many of these questions. No one knows where the future is headed; all we can do is stay curious, keep learning, and keep thinking.

That’s everything for this post, which also serves as a wrap-up of my student years—meaningful to me in different ways on different levels. Thanks for reading!