Monday, October 19, 2026

No imageStatic Memory Planner for MLIR: A GSoC 2026 JourneyHi, I am Krish Gupta. Over the summer I worked with the LLVM Foundation as partof Google Summer of Code 2026 on building a static memory planner for MLIR , mentored by Matthias Springer and Javed Absar . This post walks through what webuilt, why it matters, and what the iterative process of landing it upstreamactually looked like. The Problem After bufferization, buffer lifetimes in MLIR IR are fully explicit: thecompiler knows exactly when each allocation starts and ends (for staticshapes; runtime-dependent allocations are handled conservatively). We use thisinformation to eliminate redundant heap allocations through a static memoryplanner. This matters for accelerator-oriented compilation. On many targets (embeddedCPUs, DSPs, custom accelerators) heap allocation is either not allowed orslow, because access to main memory is expensive. Even where heap allocationis available, individual small allocations hurt performance throughfragmentation and cache pressure. What you want instead is a single staticarena whose layout is computed at compile time: // Before: three separate heap allocations %a = memref .alloc() : memref 1024x f32 > // 4096 bytes %b = memref .alloc() : memref 512x f32 > // 2048 bytes memref .dealloc %a : memref 1024x f32 > memref .dealloc %b : memref 512x f32 > // After: one arena, two views %arena = memref .alloc() : memref 6144x i8 > %a = memref .view %arena[ 0 ][] : memref 6144x i8 > to memref 1024x f32 > %b = memref .view %arena[ 4096 ][]: memref 6144x i8 > to memref 512x f32 > // ... uses of %a and %b ... memref .dealloc %arena : memref 6144x i8 > The arena is memref so it can hold buffers of different datatypes. memref.view reinterprets slices back to their original types with zerooverhead. This is the core transformation the planner performs. How the Pass Works The pass is a FunctionOpInterface pass: it runs per function and processesthe entire function body in six steps: Step 1: Collect candidates: walk all memref.alloc ops, find their deallocs via alias analysis, apply eligibility checks.Step 2: Build descriptors: compute (size, alignment, timeStart, timeEnd) for each candidate using a single O(n+m) block scan.Step 3: Run the planner: feed descriptors into the chosen algorithm (trivial sequential packing or best-fit with lifetime overlap).Step 4: Assign offsets: compute total arena size, assign byte offsets to each candidate.Step 5: Create arena: allocate one memref (or use a passed-in arg).Step 6: Rewrite: replace each alloc with a memref.view into the arena, erase all associated deallocs. The core data structure is AllocationCandidate , which groups a memref.alloc , its set of potential deallocs (there can be more than one,reachable through alias chains), the computed byte offset, size, and alignment.The entire alias reasoning goes through BufferViewFlowAnalysis : a unifiedanalysis that models how buffers flow through arith.select , scf.if / scf.for results, cf branches, and view ops without anyop-type-specific code in the pass itself. The rest of the blog explains how wegot there. PR 1: The Foundation ( #205125 ) The first PR landed in late June and introduced the core pass: static-memory-planner-analysis . It handled the straightforward case (allocsand deallocs in the same basic block with a direct one-to-one relationship)and put in place the architecture everything else builds on: trivialMemoryPlanner : a pure C++ function that takes a list of (size, alignment, timeStart, timeEnd) descriptors and returns offsets. Beinga pure function means it can be unit tested independently and is easy to swap out. bestFitMemoryPlanner : a lifetime-aware algorithm that tries to reusearena slots for allocations whose lifetimes do not overlap. Arena modes : allocate (create the arena inside the function) and arg (the arena is passed as a function argument, useful for external memorymanagement). Alignment : the arena alignment is the LCM of all individual alignments,ensuring every view is correctly aligned regardless of datatype. The review process surfaced an important point early: upstream contributionculture expects every design decision to be justified and every edge caseexplicitly tested. The javedabsar1 feedback on adding timeStart / timeEnd fields even before they were wired up was a preview of what was coming. PR 2: arith.select Deallocs ( #209106 ) After the first PR landed, we hit the first real-world blocker. The ownership-based-buffer-deallocation pipeline routinely produces patterns likethis: %a = memref .alloc() : memref 1024x f32 > %b = memref .alloc() : memref 1024x f32 > %sel = arith.select %cond, %a, %b : memref 1024x f32 > memref .dealloc %sel : memref 1024x f32 > Here neither %a nor %b has a direct memref.dealloc user. The originalpass just incremented a skip counter for both and moved on. The fix was aforward DFS over the use-def graph, following BufferViewFlowOpInterface (which arith.select implements) to find reachable deallocs. This also required a reverse-alias safety guard : since dealloc %sel mayfree either %a or %b , erasing it after replacing both with arena views isonly safe if all allocs it might free are managed by the arena. If any is not,the dealloc cannot be erased. The review cycle on this PR shaped the finalapproach: drop the group-constraint fixpoint in favour of the guard, emit harderrors (not silent skips) for missing deallocs, and improve the buildAllocInfos scan from O(n x m) to O(n+m) using a single upfront DenseMap . PR 3: scf.if and the Architectural Shift ( #213634 ) The deallocation pipeline also produces structured-control-flow patterns thatthe DFS could not follow: // Pattern 1: dealloc on the scf.if result %a = memref .alloc() : memref 1024x f32 > %0 = scf.if %c -> memref 1024x f32 > { scf.yield %a } else { scf.yield %b } memref .dealloc %0 // dealloc is on %0, not %a // Pattern 2: dealloc inside the scf.if body %a = memref .alloc() : memref 1024x f32 > scf.if %c { memref .dealloc %a } The mentor pointed to BufferViewFlowAnalysis ( Transforms/BufferViewFlowAnalysis.h ), a unified alias analysis already usedelsewhere in the bufferization pipeline. It models buffer flow through allrelevant op types in one graph: BufferViewFlowOpInterface for arith.select , RegionBranchOpInterface for scf.if / scf.for , BranchOpInterface for cf branches, and ViewLikeOpInterface for memref.view . One analysis covers all cases. We deleted the hand-rolled DFS entirely and replaced it with two calls: analysis.resolve(alloc) gives the forward alias set: every SSA value the allocmay flow into. Walking users of each alias finds all potential deallocs. analysis.resolveReverse(dealloc.getMemref()) gives the reverse alias set: everyalloc that flows into the dealloc’s operand. Used by the safety guard. Two design decisions made this correct: Lifetime anchoring via findAncestorOpInBlock . A dealloc nested inside an scf.if body is in a different block. Block::findAncestorOpInBlock walks upthe parent chain and returns the enclosing scf.if op in the plan block. Thatop’s index becomes timeEnd (conservative, but correct). The buffer isconsidered live until the entire scf.if completes. The reverse-alias guard (again, but stronger). For pattern 2 above, thedealloc inside the scf.if body is attributed to %a and correctly anchored.For the scf.if-sharing-a-nested-alloc case: %a = memref .alloc() // entry block %0 = scf.if %c -> memref 1024x f32 > { memref .dealloc %a %b = memref .alloc() // NESTED -- not arena-managed scf.yield %b } else { scf.yield %a } memref .dealloc %0 // may free EITHER %a OR %b resolveReverse(%0) traces back to both %a and %b . Since %b is nested(not in the plan block), the guard fires: %a is conservatively skipped. Nomiscompile, no leak. The else vs then branch distinction turned out to be irrelevant to theanalysis: it works on SSA alias sets, not branch structure. PR 4: scf.for Test Cases ( #215221 ) After the BufferViewFlowAnalysis switch, scf.for patterns were handledcorrectly with no additional pass changes. This PR documented the results: Allocs nested inside an scf.for body are skipped (same rule as nested scf.if ). scf.for with iter_args passing buffers across iterations hits the guardin all three mentor patterns: the per-iteration nested alloc makes erasingany shared dealloc unsafe. The canonical valid case (entry-block allocs used inside the loop body withdeallocs in the entry block) transforms cleanly. The loop body gets arenaviews instead of raw alloc pointers. This PR was deliberately tests-only. Having a clear separation between “thiswas a correct transformation that just needed to be documented” and “thisrequired a new design decision” kept the review simple. PR 5: Edge and Error Cases ( #216610 ) The final PR stress-tested the pass with patterns existing tests did not coverand documented the results. Eight new analysis tests and four new error tests: Static and dynamic shapes coexisting in one function (static transforms,dynamic is skipped without interference). Both branches of an scf.if deallocating the same alloc (duplicate deallocops in the same arena slot, correctly deduplicated). Dealloc at depth 3: findAncestorOpInBlock walks up any depth, not just one level. Multi-hop alias chain: %a/%b -> %0 -> %1 -> dealloc . The resolve() BFSfollows the full chain. Cross-interface chain: arith.select -> scf.if -> dealloc . Two differentinterface implementations in one alias path, exercised by a single resolve() call. cf.cond_br to a sibling block rejected as unstructured control flow (sameerror as cf.br ). Walk-stops-on-first-error: WalkResult::interrupt() points the diagnostic atthe problematic alloc, not at subsequent valid ones. One test was removed during review: a dynamic alloc with no dealloc placed inthe errors file. It had no expected-error annotation since dynamic shapes aresilently skipped, not errored. A test without a defined diagnostic output doesnot belong in a -verify-diagnostics file. Lesson learned. Challenges The group constraint dead-end. The first implementation of arith.select support used a fixpoint iteration to enforce that all allocs sharing a deallocmust either all go into the arena or all be skipped. This was correct butcomplicated and fragile. Switching to the reverse-alias guard removed thefixpoint entirely: the guard is a simpler, more direct statement of the sameinvariant. Understanding what already exists. The biggest conceptual shift wasrealizing that BufferViewFlowAnalysis already did everything the hand-rolledDFS was trying to do, and more. The lesson: before implementing a bespoketraversal, read the existing analysis infrastructure. You will often find thatthe right tool exists and using it both simplifies your code and gives youcorrectness for free on patterns you had not thought about. Upstream review culture. Every error message, every test comment, everyvariable name is scrutinized. Rephrasing “unstructured control flow is notsupported” replaced a longer, less precise message. Swapping the order of twobranches in a test to make the alias chain more obvious. These details matterbecause the tests and messages are the documentation that future contributorswill read. What Was Built PR What landed #205125 Core pass, trivial + best-fit planners, alignment, arena modes #209106 arith.select dealloc chains, reverse-alias guard, O(n+m) lifetime scan #213634 BufferViewFlowAnalysis integration, scf.if support (nested deallocs + result aliases) #215221 scf.for test coverage, canonical loop case documented #216610 Edge and error case test suite (26 analysis tests, 4 error tests) The final pass lives in StaticMemoryPlannerAnalysis.cpp . Future Work The immediate open question is lifting the entry-block restriction: only allocsin the function’s entry block are planned today. Two concrete directions wediscussed: Path-sensitive lifetime tightening. The current [timeStart, timeEnd] interval model treats branches conservatively: a buffer freed inside an scf.if then-branch is considered live until the entire scf.if completes,even though the else path never held it. Adding dominance-based non-overlapdetection (using MLIR’s existing DominanceInfo ) would let mutually exclusivebranches share arena slots, unlocking patterns like: %0 = alloc() %1 = scf.if %c { %2 = alloc() // could share %0's slot -- they never co-exist dealloc(%0) yield %2 } else { yield %0 } dealloc(%1) Plugging in stronger planning algorithms. The planner function is pure andseparately testable, so dropping in a better algorithm is straightforward.Best-fit is already there. Beyond that, interval graph / graph-coloringapproaches and algorithms like minimalloc map directly onto the (size, alignment, timeStart, timeEnd) interface the planner already expects. Memory space awareness. Heterogeneous targets have multiple distinct memoryregions with different capacity constraints. Extending the planner to assignbuffers to specific memory spaces while respecting per-space limits is a naturalfollow-on. Acknowledgements Huge thanks to my mentors Matthias Springer and Javed Absar for the patient andprecise review feedback, for pointing at BufferViewFlowAnalysis at exactlythe right moment, and for treating every PR as a teaching opportunity. The LLVMcommunity’s review culture is demanding in the best possible way. The project GSoC page is at summerofcode.withgoogle.com/programs/2026/projects/XsjxBQ9o .Feel free to reach out with questions or to build on this work at krishgupta2832@gmail.com .📝The LLVM Project Blog
No imageReworking the prime path cacheI just reworked how the caching of prime paths works in zcov. Computing the prime paths can be quite expensive in both time and memory, so GCC limits to 250 000 paths by default. This limit was chosen because at around 250k the later passes slow down significantly (verifying the newly emitted instructions seem to be the worst offender, but I don’t have hard data here), but there are functions out there where just finding the prime paths is infeasible on my workstation. The prime paths are given by the structure of the function (specifically, the control flow graph), which makes it a prime candidate for caching. This is especially useful for zcov as it is an interactive application where every microsecond matters. Even more so since code tends to change very little between coverage runs (and most of those changes are in the test suite and not the code being tested), and so the same functions will be analyzed again and again. Since the same project will be tested over a long time with reasonably few and slow changes we have an perfect use case for a persistent cache. The caching feature was implemented a long time ago, and was quite quickly put together. It was for all intents and purposes the just-computed suffix tree dumped to disk. For non-trivial functions this was an obvious improvement, but still had a large space- and time overhead it meant storing all subpaths, too. I tried a few different things and put together a new bespoke representation for a set of prime paths optimized to reduce memory usage, which I then benchmarked on 485 functions from GNU Coreutils. In the benchmark I measured the time (in microseconds) it too to: Compute the prime paths Look up the prime paths in the old cache Look up the prime paths in the new cache Here are some aggregate statistics from a pandas dataframe: mean median min max path count 181698 15 1 84197299 compute µs 4636.26 6.00 1.00 1572546.00 new-cache-hit µs 44.79 7.00 4.00 13678.00 old-cache-hit µs 1716.59 10.00 5.00 525424.00 compute µs/path 0.53 0.45 0.01 1.73 new-cache µs/path 0.60 0.44 0.00 3.50 old-cache µs/path 0.86 0.64 0.00 3.00 What the median and min/max path count tells us is that most functions are small and simple, but once they go beyond that the path count explodes. That we apparently have higher throughput (lower elapsed per-path time) suggest that for very simple functions the cost of looking up the cache is higher than just computing the number of paths. This is confirmed by filtering out any function with less than 30 paths: mean median min max path count 530842 124 31 84197299 compute µs 13536.43 101.00 3.00 1572546.00 new-cache-hit µs 118.52 9.00 4.00 13678.00 old-cache-hit µs 4998.68 39.00 10.00 525424.00 compute µs/path 0.77 0.74 0.01 1.73 new-cache µs/path 0.08 0.06 0.00 0.35 old-cache µs/path 0.33 0.33 0.00 0.58 new-cache/compute 0.18 0.06 0.00 2.67 compute/new-cache 39.58 17.19 0.38 254.53 old-cache/compute 0.63 0.41 0.12 10.25 new-cache/old-cache 0.22 0.16 0.01 0.73 old-cache/new-cache 15.91 6.14 1.36 115.83 So on average the new cache takes 0.08µs/path while computing takes 0.77µs/path, a 10x on average as opposed to the approx 2x of the old cache. This is in practice skewed by the cost of the cache being high for very small functions (sub-50 paths tend to take less than 5 µs to compute). If we look at the fastest/slowest functions while still filtering out those with less than 30 paths: function path count compute µs new-cache-hit µs old-cache-hit µs compute µs/path new-cache µs/path three_arguments 31 13 7 15 0.42 0.23 main 31 5 11 15 0.16 0.35 value_to_baud 32 32 8 14 1.00 0.25 apply_mode 32 13 8 14 0.41 0.25 write_to_file 32 21 8 13 0.66 0.25 … … … … … … … string_to_baud 61 46 8 21 0.75 0.13 sane_mode 62 58 8 17 0.94 0.13 set_suffix_length 64 97 8 32 1.52 0.12 parse_symbols 64 55 7 20 0.86 0.11 set_file_security_ctx 65 45 7 23 0.69 0.11 … … … … … … … main 63790 24282 144 15101 0.38 0.00 lines_rr 74899 22539 100 4256 0.30 0.00 ng_pollard_rho2 1416562 101036 627 39961 0.07 0.00 ine_bytes_split 1425449 147657 633 27143 0.10 0.00 parents_private 84197299 1572546 13678 525424 0.02 0.00 From this table we can see that even for very complex functions the cache hits are fast; we fetch 84 million paths in about 13.5ms which is decently quick and should be fine. For comparison, computing the same function takes about 1.6s. And finally, the relative improvement: function path count compute/new-cache old-cache/new-cache three_arguments 31 1.86 2.14 main 31 0.45 1.36 value_to_baud 32 4.00 1.75 apply_mode 32 1.62 1.75 write_to_file 32 2.62 1.62 … … … … string_to_baud 61 5.75 2.62 sane_mode 62 7.25 2.12 set_suffix_length 64 12.12 4.00 parse_symbols 64 7.86 2.86 set_file_security_ctx 65 6.43 3.29 … … … … main 63790 168.62 104.87 lines_rr 74899 225.39 42.56 ng_pollard_rho2 1416562 161.14 63.73 ine_bytes_split 1425449 233.27 42.88 parents_private 84197299 114.97 38.41 As the number of path goes past 40–50 into moderately complex functions we are looking at from 5–10 speed improvement over computing and 2–4x speed improvement over the old cache. For complex functions, more than 50 000 paths, the new cache is more than 100x faster than computing, and more than 40 times faster than the old cache. One of the ways this has been made quick is by significantly compressing the representation, which also reduces memory pressure. The old cache needs roughly 460 MB to store all the prime paths, while the new cache needs 93 MB to store the same set, about 20%. Speed matters. This translates into making the experience using zcov much nicer and smoother.📝patch – Blog

If this page is useful, please consider donating a coffee

Sunday, October 18, 2026

Composite objects and C++26 the std::polymorphic typeIn a previous article, I discussed the C++ std::indirect type and how it helps implementing the PImpl idiom. However, the std::indirect type was inducted in the standard alongside another type called std::polymorphic. This helps implementing composite classes, where composite means a class composed of member objects in an has-a relation, where the composite own the ... Read more The post Composite objects and C++26 the std::polymorphic type first appeared on Marius Bancila's Blog .📝Marius Bancila's Blog

Saturday, October 17, 2026

_Paradise Lost_, diffedOne of my book clubs will be starting Milton's _Paradise Lost_ soon. Preparing for that reading, I learned that there are actually two different versions of _Paradise Lost_: the “original” 1667 edition in ten books, and the 1674 revised edition in twelve. Are the differences between the two editions as significant as, say, the differences between the 1818 and 1831 editions of Mary Shelley's _Frankenstein_? To find out, I wanted a website similar to Eduardo Villuendas' [_Frankendiff.com_](https://frankendiff.com/). So, I made one. Presenting [Paradise Lost, Diffed](https://quuxplusone.github.io/Wikisource/Milton/paradise-lost-diffed.html)!📝Arthur O’Dwyer

Friday, October 16, 2026

Thursday, October 15, 2026

MSVC Build Tools Version 14.52 Release Candidate Now AvailableWe are pleased to announce the availability of the release candidate for Microsoft C++ (MSVC) Build Tools version 14.52. With this release, C++23 is ready for production use and available through the /std:c++23 compiler option; see below for details. This is the last MSVC v14.52 preview before it moves to stable status and full support. […] The post MSVC Build Tools Version 14.52 Release Candidate Now Available appeared first on C++ Team Blog .📝C++ Team Blog
Implementing a simple version of ThreadPoolExecutor in PystdOne of my favourite things in the Python standard library is the ThreadPoolExecutor . It is a very simple way to run a bunch of tasks in parallel, such as converting or repacking a whole bunch of image files from one format to another. You write a pure function that takes two arguments, the input and output file names, and does the conversion. Then you submit a bunch of tasks (basically function and argument pairs) and the executor runs them with maximal parallelism. I decided to implement this in Pystd . To make things simpler it does not yet support futures for return values. Writing a thread pool executor needs some fundamental building blocks such as threads, mutexes and condition variables. Once those were implemented (and working), writing the actual "business logic" turned out to be fairly simple. In fact, it took less time than writing the PNG processing code with libpng. First you need to write this helper class to register you task for running. It also stores all arguments etc that the task needs to do its thing. Yes, this has a bit of that "icky 90s Java" feeling to it but OTOH it is fairly simple, has unambiguous ownership semantics and compiles massively faster than your typical template monstrosity. Using this is just a matter of globbing for png files in the input dir and submitting tasks for them. The UX is a bit verbose. It could probably be made nicer with a helper method or two. Still, not too shabby for something written by hand in a single afternoon. For those interested in details, the code for this program is in the pngtest branch.📝Nibble Stew
Conan Workspaces: a monolithic super-build of your packages, on demandThere is a long-running argument in C and C++ organizations: should a large codebase be a monorepo with a single monolithic build, or a set of independently versioned components, each in its own repository, consumed as packages? Each camp has good reasons. The component approach gives you per-component versioning and releases, pre-built binaries and builds that only touch what changed. The monorepo approach gives you “live at head” development: change a header in a low-level library, and the next build recompiles every consumer, in one build tree. The component approach usually loses that developer experience when needing to work simultaneously on multiple different packages. But the component approach can be better for large organizations with different teams working at different paces, using package releases to work more independently. This post is about not having to choose. Conan workspaces let you keep your components as versioned packages in different git repositories, with precompiled binaries for efficient consumption and, at the same time, bring back multiple components and collapse them into a single monolithic super-build for development UX and convenience. The problem: changing a library deep in the graph Let’s use a small example graph. game is an application that depends on engine , which depends on ai and graphics , which both depend on mathlib . All of them are static libraries, each one lives in its own git repository, and each is a Conan package in our server. The repositories are conanci_mathlib , conanci_ai , conanci_graphics , conanci_engine and conanci_game . The workspace itself is in conanci_workspace . The dependencies are declared with version ranges, for example in engine : class EngineRecipe ( ConanFile ): name = "engine" version = "1.0" requires = "ai/[>=1.0 , "graphics/[>=1.0 I want to add a feature to ai , an API-compatible change that will become ai/1.1 , and verify that game still builds and works correctly with it. engine sits between the two. Its code won’t change, but it’s a static library compiled against ai ’s headers, so it has to be rebuilt. With plain packages, that means conan create in ai , then rebuilding engine and game against it, and repeating the whole loop through the Conan cache for every iteration. Editable packages ( conan editable add ) improve this, since engine can consume ai directly from its source folder. But they’re still independent projects: you build ai , then engine , then game , each with its own build tree, its own conan_toolchain.cmake , and often its own IDE window. Editables are also global to the Conan cache, so it’s easy to forget one is active and have an unrelated project pick it up. What a workspace is A workspace is a set of locally editable packages, defined by a conanws.yml and optionally a conanws.py at the root of a folder. Unlike conan editable add , these editables are not global. They only apply to Conan commands running inside that folder. A workspace can be used in two ways: Orchestrated : Conan builds each package of the workspace separately, in the correct order ( conan workspace build ). This works even when the packages have different build systems, and it can also work when there are multiple final “consumer” packages in the workspace, like different final executable applications belonging to a system. Monolithic (super-build) : all the workspace packages are collapsed into a single node of the dependency graph. Conan installs only the external dependencies and generates one toolchain and one set of find_package() files. You then build everything as one project. The monolithic mode is where the paradigm changes, and it’s what the rest of this post is about. This is the flow we are going to follow: Creating the packages Everything in this post is reproducible. You need Conan 2.31 or newer, CMake 3.25 or newer, git and a C++ compiler. We start from the conanci_workspace repository: $ git clone https://github.com/conan-io/conanci_workspace.git game-dev $ cd game-dev $ python bootstrap.py ... Packages created: mathlib/1.0, ai/1.0, graphics/1.0, engine/1.0, game/1.0 In a real organization, the packages would already be in your server, built by CI. bootstrap.py plays that role. It clones each component repository into a temporary folder and runs conan create on them in dependency order. Any extra arguments are forwarded to conan create , for example python bootstrap.py -s build_type=Debug . Adding packages to a workspace Packages know where their sources are, so a workspace can be built from package references instead of repository URLs. I want to work on ai and check the result in game . engine sits between them and has to be part of the workspace too, because its binary in the Conan cache was compiled against the old ai : $ conan workspace add --ref ai/1.0 Cloning git repo Checkout: 8595303a211ba4556f257017e789e45a515adf50 Reference 'ai/1.0' added to workspace $ conan workspace add --ref engine/1.0 Cloning git repo Checkout: 98aee101ef132b2f7a63fbe99686991955da8817 Reference 'engine/1.0' added to workspace $ conan workspace add --ref game/1.0 Cloning git repo Checkout: 88df8ca649eda1e2f30fa599fdcefc8f7bea4e4c Reference 'game/1.0' added to workspace Conan could clone each repository at the exact commit that produced the package, not some branch head that may have moved on, because the packages capture their git origin url and commit when they are created. After these commands, the workspace folder looks like this. Each package folder is still its own git repository: game-dev/ ├── conanws.yml # now lists ai, engine and game ├── conanws.py # super-build definition, see below ├── CMakeLists.txt # super-project, see below ├── bootstrap.py ├── ai/ # clone of conanci_ai at 8595303 ├── engine/ # clone of conanci_engine at 98aee10 └── game/ # clone of conanci_game at 88df8ca Note: Adding engine by hand is not strictly necessary. After adding ai and game , conan workspace complete finds the packages in the Conan cache that depend on workspace packages and adds them too, cloning them at their recorded commit. If one is missing, conan workspace super-install fails and suggests running it. The super-build definition Besides conanws.yml , the workspace repository contains two files that define the super-build. The first is conanws.py , which defines a minimal ConanFile with the CMakeDeps and CMakeToolchain generators for the super-project, and a Workspace class. The interesting part of the latter is build_order() : class MyWorkspace ( Workspace ): def build_order ( self , order ): pkgs = [] for level in order : for pkg in level : pkgs . append ( f ' { pkg [ "ref" ]. name } : { pkg [ "folder" ] } ' ) content = "set(CONAN_WS_BUILD_ORDER {})" . format ( " " . join ( pkgs )) save ( self , "build/conanws_build_order.cmake" , content ) build_order() receives the topologically sorted list of workspace packages. Translating it into something the build system understands is up to the workspace. Here we write a small CMake conanws_build_order.cmake file, and the second file, the super-project CMakeLists.txt at the workspace root, consumes it: cmake_minimum_required ( VERSION 3.25 ) project ( superbuild CXX ) include ( FetchContent ) # Generated by "conan workspace super-install", see conanws.py build_order() include ( build/conanws_build_order.cmake ) foreach ( pair ${ CONAN_WS_BUILD_ORDER } ) string ( REPLACE ":" ";" parts " ${ pair } " ) list ( GET parts 0 pkg ) list ( GET parts 1 folder ) FetchContent_Declare ( ${ pkg } SOURCE_DIR ${ CMAKE_CURRENT_LIST_DIR } / ${ folder } SYSTEM OVERRIDE_FIND_PACKAGE ) FetchContent_MakeAvailable ( ${ pkg } ) get_target_property ( type ${ pkg } TYPE ) if ( NOT type STREQUAL "EXECUTABLE" ) # Just because the original packages didn't properly do it # but can be fixed here add_library ( ${ pkg } :: ${ pkg } ALIAS ${ pkg } ) endif () endforeach () The key is OVERRIDE_FIND_PACKAGE . When engine/CMakeLists.txt calls find_package(ai CONFIG REQUIRED) , CMake doesn’t look for an installed ai-config.cmake . It resolves to the ai subproject in the same build. The ALIAS provides the namespaced ai::ai target that the package’s find_package() would have defined. None of the packages’ own CMakeLists.txt files needed any change. Their build scripts are identical whether they build standalone, as a package in the cache, or inside the super-build. Neither of these two files is specific to this workspace. They work for any set of CMake-based packages, so they can live in a shared repository, like this one, or be generated by your own conan new template. Installing and building the super-build Now we install the workspace: $ conan workspace super-install ... Requirements ai/1.0 - Editable engine/1.0 - Editable game/1.0 - Editable graphics/1.0#77561e5ec993160f3103f3300ea01102 - Cache mathlib/1.0#03ead0629a968a8daa7c5ed84418a895 - Cache ======== Collapsing workspace packages ======== -------- Collapsed graph -------- Graph root conanws.py base project Conanfile: /home/user/game-dev Requirements graphics/1.0#77561e5ec993160f3103f3300ea01102 - Cache mathlib/1.0#03ead0629a968a8daa7c5ed84418a895 - Cache This is what monolithic means in practice. ai , engine and game are now a single project, and graphics and mathlib , the parts of the graph that are not in the workspace, remain pre-built binaries in the Conan cache. I get the monorepo experience for 3 packages without building the other 2, or the other 300 in a real organization. The build_order() method of our conanws.py has written the build order of the workspace packages to a CMake file, which the super-project CMakeLists.txt includes: $ cat build/conanws_build_order.cmake set ( CONAN_WS_BUILD_ORDER ai:ai engine:engine game:game ) Now we can build the whole super-project and run the application as usual: $ cmake --preset conan-release # conan-default on Windows $ cmake --build --preset conan-release [ 28%] Built target ai [ 57%] Built target engine [ 100%] Built target game $ ./build/Release/_deps/game-build/game # PATH is different in Win: build\_deps\game-build\Release\game.exe mathlib/1.0: mathlib maths ( Release )! ai/ws: Some Artificial Intelligence for aliens ( Release )! ai/ws: Intelligence level = 0 graphics/1.0: Checking if things collide ( Release )! engine/ws: Computing some game things ( Release )! game/ws:fun game ( Release )! The output tells the two worlds apart: mathlib/1.0 and graphics/1.0 are binaries from the cache, while ai , engine and game were compiled from the workspace sources, which the example marks by printing ws instead of a version. On Windows and other multi-config generators, the executable is at build/_deps/game-build/Release/game.exe . Making the change Now the actual work. ai gets a new feature, a configurable number of aliens, as a new parameter with a default value. Existing callers don’t need any change, so this is an API-compatible, minor change: --- a/include/ai.h +++ b/include/ai.h -AI_EXPORT void ai(int intelligence=0); +AI_EXPORT void ai(int intelligence=0, int aliens=3); --- a/src/ai.cpp +++ b/src/ai.cpp -void ai(int intelligence){ +void ai(int intelligence, int aliens){ ... - std::cout + std::cout One build command, from the workspace root: $ cmake --build --preset conan-release [ 14%] Building CXX object _deps/ai-build/CMakeFiles/ai.dir/src/ai.cpp.o [ 28%] Linking CXX static library libai.a [ 42%] Building CXX object _deps/engine-build/CMakeFiles/engine.dir/src/engine.cpp.o [ 57%] Linking CXX static library libengine.a [ 71%] Linking CXX executable game $ ./build/Release/_deps/game-build/game # PATH is different in Win: build\_deps\game-build\Release\game.exe ... ai/ws: Intelligence level = 0, aliens = 3 ... Look at the third line of the build: engine.cpp was recompiled, although nobody touched it, and git status in the engine repository is clean. It’s recompiled because it includes ai.h , and the build system tracks that dependency across what are, in reality, different repositories. This is exactly the monorepo experience: the build system decides what has to be rebuilt, not the developer. That recompilation is also necessary for correctness. engine was compiled against the old ai.h , so as the header changed it has to be rebuilt, even if the change is API-compatible. In an IDE, you open the workspace root and get a single project. “Go to definition” on ai() in engine.cpp jumps into ai ’s source rather than into a header in the Conan cache, and the debugger steps across all three components. What to keep in mind for monolithic super-builds A monolithic build is one build, and that has implications: One toolchain for everything. When using monolithic super-builds, all workspace packages share a single conan_toolchain.cmake and a single set of dependency files. What packages do in their own generate() doesn’t apply automatically. The same goes for options like shared : the super-build ConanFile must define its own options or aggregate the packages’ ones explicitly. Subprojects need to be well-behaved. The FetchContent approach works best when each package’s CMakeLists.txt is at the root of its repository and defines the same targets its find_package() would provide. The ALIAS trick covers the most common gap. One node per dependency. A dependency can’t appear twice, for example once as a tool_requires in the build context and once in the host context. Since Conan 2.33, Conan resolves this kind of ambiguity in favor of the host context and visible requirements, and prints a warning when it does. CMake is the paved road, not the only one. conan new workspace generates a CMake super-project, but nothing in the model is CMake-specific. An MSBuild solution aggregating .vcxproj subprojects follows the same principle. Longer term, consuming dependencies from a source or build tree through the Common Package Specification (CPS) would make this kind of integration possible without build-system-specific glue. If you’re interested in other build systems or IDEs, please let us know. There is more to workspaces than the super-build The super-build exists only on the developer’s machine. To ship the change, we go back to components: bump ai to 1.1 , commit and push, and let CI build the new binaries. engine and game keep their versions, because ai/[>=1.0 already accepts ai/1.1 , but they will get new binaries built against it. The workspace gave the developer the same answer locally, before pushing anything. The CI side of the story is covered in the Conan CI tutorial . The super-build is the most visible part of workspaces, but not the only one. The conan workspace command family covers the whole development flow: conan workspace add , open and complete populate the workspace, from folders, from package references or from the packages already listed in conanws.yml . conan workspace super-install computes the monolithic super-build. conan workspace install and build work in the orchestrated mode, building each package separately in the right order, can orchestrate builds when packages have different build systems. conan workspace create builds proper packages from the workspace sources in the Conan cache, in dependency order, which is how a change goes back to being packages. conan workspace info and remove inspect and edit the workspace. Workspaces can also be shared and dynamic: Because a workspace is defined by files, a team can commit a conanws.yml listing the packages they usually work on, and a bare conan workspace open will clone every package that is missing, each at its recorded commit. The list of packages doesn’t have to be static. conanws.py can compute it, for example from the subfolders of the workspace, so cloning or removing a repository is all it takes to change what is in the super-build. For large workspaces, conan workspace super-install --pkg=engine --pkg=game installs just those packages and their dependencies. conan new workspace generates a complete template, ready to start from. The workspace reference and the conan workspace command reference have the details. The same flow, at scale Our example has five packages, but real organizations have hundreds. Imagine a game studio that needs to verify a change in nav-mesh , deep in its graph, in game-client . Adding both to the workspace and running conan workspace complete brings in only the packages in between, and everything else stays a pre-built binary: Conclusion Workspaces don’t make you pick between components and a monorepo. The components keep doing what they’re good at: versioning, decoupled releases, binaries, efficient CI, etc. When a change needs to be developed and verified across component boundaries, you collapse just the packages involved into one super-build with a workspace for development convenience. The full example is in conanci_workspace . The workspace tutorial and the conan workspace command reference cover everything here in more detail. The feature is still experimental, which means your feedback can still change it, so please open issues in github.com/conan-io/conan .📝Conan C/C++ Package Manager Blog

Wednesday, October 14, 2026

Lauterbach and JetBrains Announce TRACE32 Integration in CLion IDEHoehenkirchen, Germany/Amsterdam, Netherlands — October 14, 2026 — Lauterbach and JetBrains have integrated TRACE32® debugging tools with CLion, JetBrains’ professional C and C++ tool. The integration enables embedded developers to debug applications on target hardware directly from CLion, bringing code editing, navigation, and hardware debugging into one development workflow. Using the Debug Adapter Protocol (DAP), […]📝CLion : A Cross-Platform IDE for C and C++ | The JetBrains Blog

Tuesday, October 13, 2026

Benchmarking any2bazel - towards more efficient migrationsBenchmarking any2bazel - towards more efficient migrations We’re fond of Bazel, and particularly fond of the capabilities it offers developers. Faster builds, made more efficient through remote execution and caching, made more understandable through declarative configuration, queryability, and precise logging – these deliver benefits and solutions to developers once Bazel’s all set up and integrated into their daily processes. But it’s a rare developer who enjoys spending their time and effort in build system configuration. Wouldn’t we rather be solving our customers’ problems? And the decision to migrate from an existing build system to Bazel is one that demands consideration of huge trade-offs; the labor and opportunity costs of engineering time spent on migration and adapting internal workflows to a new build system discourage the pursuit of its advantages. In early 2026 I became interested in AI-guided Bazel migration because the capabilities of models like Opus 4.5 and agentic tools like Claude Code seemed to offer a way of realizing those benefits without so much toil – that is, if AI-guided Bazel migration worked. I believed it would work eventually, if not at that point in time, given the rapid pace of capability growth in AI models and their harnesses. I also believed it would be possible to distill lessons learned from open-source projects’ Bazel migrations and AI-guided migration attempts into a skill. If done right, such a skill could elevate those capabilities, and the process of improving the skill with better distillation from more and more migration attempts could augment the "natural" capability growth coming from the models getting smarter. I was particularly struck by the question, "how would we know when we arrived?" It seemed answerable, if only we had an experimental laboratory that evaluated AI-guided Bazel migration attempts. Many months later, around the time any2bazel launched, I began work on any2bazel-lab , to bring experimental results to bear on the question of how well current AI models performed at automated Bazel migration, both with and without the any2bazel skill. Anthropic’s frontier models – a mix of Opus 5, 5.5, Fable 5.1, and a bit of Haiku 4.5 – extensively contributed to the lab’s development, and coordinated the execution of its migration experiments. Our experiments are ongoing. Here’s a summary of what we’re learning, and how.📝EngFlow Blog