[RFC] [LTO] [lld] LTO with linker scripts, implemented

,

Draft PR: [WIP] LTO with linker scripts, end-to-end. by efriedma-quic · Pull Request #204282 · llvm/llvm-project · GitHub

LTO with linker scripts, as discussed previously ([llvm-dev] [RFC] (Thin)LTO with Linker Scripts etc.), is a set of LTO features designed to make linker script behavior with LTO enabled align more closely to the behavior without LTO enabled. Among other things, it:

  • Allows naming specific filenames in the linker script, even if those files are bitcode.
  • Disables inlining, and similar transforms, across linker output sections.
  • Makes KEEP work correctly with symbols defined in bitcode.

This is broadly along the same lines as previous proposals, although a few of the details are slightly different.

Along with the RFC, I’m posting a draft patch which implements this proposal in LLVM and LLD: [WIP] LTO with linker scripts, end-to-end. by efriedma-quic · Pull Request #204282 · llvm/llvm-project · GitHub. This is based heavily on the approach Qualcomm has used for a long time internally. The approach is adapted a bit for upstream, and the lld implementation is new.

The initial implementation is guarded under flags: a flag -flto-linker-scripts for clang, and --lto-linker-scripts for lld. This ensures that code which expects existing LTO semantics is not affected by these changes.

An architectural outline follows.

Annotating bitcode files before LTO

Making these features work requires the linker to know section names before LTO runs. To accomplish this, the existing “section” field on LLVM IR symbols, and in the symbol table, is reused. The user passes a flag -flto-linker-scripts to the compiler to tell it to add these annotations. A pass runs before bitcode emission to add these annotations, querying the backend for the appropriate mapping for symbols.

(Alternatively, we could delay this computation to the LTO step, but that’s awkward to layer correctly because we only have the symbol table loaded at the point where we first need the section names.)

Running linker rules before LTO

To allow the mapping to work correctly, the linker needs to use linker script rules: it needs to compute the compute the correct output section for each symbol, and it needs to compute whether each symbol is subject to a KEEP rule. This uses the section information from the symbol table. This information is then passed to the LTO API for each symbol, using the SymbolResolution struct.

There are a couple other minor changes to how symbol processing works before LTO: the linker now process symbols which are not externally visible (“internal” or “private”), to compute additional information, and computes whether each symbol is the target of a KEEP directive.

Changes to IR passes

During LTO, there’s one significant change to IR optimizations: a new “output_section” metadata on global variables and functions. This metadata modifies the behavior of a few passes. Most importantly, it prevents passes like inlining from changing the output section of code. This is important for allowing the user to ensure that certain functions are emitted in a specific section.

Passing symbol information back to the linker

After optimizations run, the backend emits an ELF file. This ELF file needs to contain information about the original, pre-LTO files. The LTO step takes care of this by modifying the section names: each section name is annotated with a marker at the end indicating the original source file. The syntax is appending “^^” followed by the name of the file to the section name. The linker then uses this to map symbols to source files when performing final processing of linker script rules. (This mangling is only done when the linker is using the new additions to the LTO interface.)

After LTO, linking works mostly the same as usual, except for the modified processing of linker script rules.

3 Likes

Thanks for picking this up, I know it has been discussed for a long time.

It looks like the draft patch is regular LTO only - is that correct? If so, is there a plan to do this for ThinLTO as well?

The architecture is designed so that it should be straightforward to add ThinLTO; the pre-LTO processing don’t rely on loading anything that ThinLTO doesn’t normally load, and post-LTO linker script rule changes don’t rely on symbols being emitted into a specific object. But initially, we’re mostly focused on getting this working for regular LTO.

Hey Eli. Thanks for putting this RFC together; I’m particularly fond of having and end-to-end draft PR to look at. This really cements the ideas and makes it easy to talk about them concretely. A lot came to mind while reading through this RFC and skimming through the PR; I’ll break out my thoughts by topic. I’ve wanted this to work for a long time, and I do think that a version of this patch is very desirable, but some details need to be sorted still I think.

Linker

Section spilling

The RFC and patch assume that each input section is assigned to at most one output section. For LD and LLD, the --enable-non-contiguous-regions flag makes this not the case. When this flag is enabled, an input section is matched with all output sections with matching wildcards. If an earlier match for a section would cause a MEMORY region to become oversize, the section is spilled to the next matching output section. In LLD, this functionality is available using the SECTION linker script syntax, even without the flag.

While these cases could be made mutually exclusive with LTO linker script support via a linker error, this would be very unfortunate, since they are of primary utility to the embedded space that most relies on linker scripts. To make them compatible, we would need to include some information about the spilling semantics in the model of output sections maintained by LTO. The output section would be a list of names instead of a name, but I think the simplest usable semantics would be just to condition inlining on list identity.

Linker/LTO interface

The output section information is recorded as part of the symbol table… this feels like an odd choice. Correct me if I’m wrong, but the purpose of this is to annotate an IR entity with a tag that says: “This entity will emit into this output section, and that fact is an invariant”. (What exactly this means I think will be tricky; more on that later.)

It doesn’t seem that symbols entered anywhere into this picture, at least as far as the linker is concerned. The relationship is between IR objects and their resulting output sections, and symbols are being used as a proxy for the former. But the mapping between IR objects and symbols is loose, and this patch does a lot of work to make it stronger, e.g., by making the linker aware of private and internal symbols. Module inline assembly can be used to make sections with no symbols associated, but arguably we don’t need that here. Still, this is a constraint on the IR and its development, and what I’m not sure about is whether this is a constraint that was already present.

An alternative would be to provide a callback interface to allow the LTO subsystem to query the output sections for a given input section. Then, the initial LTO pass could scan the IR, determine for each entity what input sections it could emit to, and query the output sections for each. It seems prima facie that this would be more general and simplify the patch, but I’m not sure if that’s kosher in the embedding API.

From the above, there’s also the question of what to do if a construct can emit into multiple input sections (only thing that comes to mind is… uh, exception handling metadata?). I don’t know how far this guarantee actually needs to extend.

^^ semantics

It appears that this patch adds an independent, unconditional feature to LLD: whenever a section name includes ^^, the portion before is taken as the section name, and the portion after overrides the filename. Taken by itself, this seems… an odd feature. At the very least something that feels like it should be conditionalized on a LTO link, but as is, it adds universal magic to section names writ large, which have previously only had some magic about how they were used. (Some section name string parts are treated specially (e.g., numeric sorting of .init.N), but AFAIK the ELF section name was actually always just the section name.)

In past discussions, I’ve always been in favor of separately tracking filename information and using it to actually emit separate object files into a temporary directory. This has a few (potential) advantages:

  • The linker could easily maintain a mapping from input file to output file just by the filename. This would keep the mapping semantics a bit more contained to LTO: that mapping would be empty outside of LTO.
  • It decreases the divergence somewhat between regular and thin LTO, which I think is something that’s seen as desirable
  • It might present opportunities for parallelization, e.g. at some point a regular LTO module could be split, and code for each TU could be emitted in separate processes.

Failing this, there may be a cleaner encoding than ^^ into ELF, or a way to conditionalize it better on LTO, but this is an avenue where I haven’t spent much mental effort yet, so I have nothing to add.

Inlining semantics

I’ll take a stab at a broad statement of principles WRT inlining across translation units. We want LTO to make user code better by inlining across TUs when it’s safe. We also shouldn’t regress from the separate compilation behavior: optimizations done fully within a TU should still be done in LTO.

The latter is particularly interesting, because within a TU, both clang and GCC already happily inline across output sections, and thus, across linker script boundaries. Preventing this would also be very difficult, since much of this inlining occurs in the pre-link pipeline of a LTO, where there is no linker script to be seen.

Accordingly, I believe it is only cross-TU inlining that should be constrained by these rules. This would prevent the worst instances of spooky action at a distance, but still allow users a modicum of control by providing them the TU as a unit where inlining is declared safe, as per the standard set today. If I understand correctly, this information is already tracked to set the ^^ labels, so we could allow post-LTO inlining to follow these rules as well.

Anyway, thanks for reading, and thanks again for the RFC and patch!

We haven’t been using --enable-non-contiguous-regions internally… but I think whatever we want to do here doesn’t really impact the overall architecture. “Output section” markings are basically opaque identifiers; we don’t do anything with them except compare them for equality. If the linker wants to treat multiple output sections as a group, the linker can use different names.

I don’t want to propose a specific partitioning rule without actual codebases to reference. We might need new linker script directives to make this work well.

The input section (the section the symbol would have had in the input object file if we weren’t doing LTO) is recorded into the symbol table. The output section is computed by the linker, and written to the IR as metadata on each global. The IR has two sections for each symbol: an input section, and an output section. The output section is discarded after IR optimizations, and the input section information is emitted to the ELF.

We don’t need output_section markings for data defined in module inline asm because we can’t optimize it anyway. (Output sections are not emitted into the ELF object.)

That said, there is a problem with module inline asm: once we emit the LTO’ed object file, we can’t tell which bitcode input it came from, for resolving linker rules. I don’t want to go too deeply into it here, but we’re considering proposing some sort of metadata markings for module inline asm, which would also solve other problems we have with the current representation.

Currently, I don’t think there are any callbacks from LTO into the linker. I guess there isn’t any fundamental reason we can’t add callbacks. It’s a little complicated to do with a C API, but not impossible. I’m not sure the end result is really simpler, though. Linker script rule resolution needs to run at the beginning of LTO in any case, and callbacks are sort of hard to follow.

I’ll think about it a bit more.

I don’t think there are any constructs where this is actually a problem.

I can conditionalize the LinkerScript::computeInputSections change in lld on ctx.arg.ltoLinkerScripts, sure.

There are reasons to split LTO modules before codegen, but I’m not sure we want to drive the split based on source translation unit names. Most projects have a few big translation units, which significantly hurts parallelism if you go this route. (See also [RFC] [ThinLTO]: Multi-Thread Parallel Compilation for Large Modules .)

I’m not against this, but the lack of this kind of inlining at LTO-time hasn’t been a practical issue for us, as far as I know.

Ah, okay. So, the linker is in complete control of the optimization domains; then yeah, it could come up with an appropriate link-wide identifier system. I guess that it leads to a nit on naming; this is kind of defining a new notion of “output section” for LTO then… maybe it would be worth inventing a new concept, “LTO domain” or something? I don’t feel that strongly either way, surprisingly.

Yeah, I think the core of my feelings were surprise at seeing private symbols coming across into the IR symbol table; these really feel like they should be completely private concerns on the LTO/IR side of things. This just by analogy to regular object files: private is intended to mean “doesn’t come out into the symbol table”. It’s only used during LTO in your patch… but those working on tools like llvm-nm would need to reason “ah, no, I don’t need to ever check !isPrivate(): these only ever occur during LTO… for the purposes of LTO/linker communication”.

But, intrinsically information about private symbols/entities does need to cross from LTO to the linker and back again. I guess I’m quibbling about mechanism; IRSymtab already has a kinda nebulous semantics, and even a completely separate input section list (where the linker can give domains for each) seems preferable to making it even more nebulous. In retrospect that seems better than a callback, but… if that’s too special casey, ehhhh. This is just a quibble I think.

Ah, good point! I’ll retreat to a stronger position: I’d think we should split at least on TU boundaries. But, I’ll admit, I’ve never liked the injury that regular LTO has done to the notion of a “translation unit”, and arguably this RFC is an attempt to partially rectify that. Still, you do end up with a notion of a TU in either case: with this RFC as-is, as part of an uber-link-object, and alternatively, as files in a ThinLTO-style tmp directory. I’d prefer the latter, but the former is still an improvement. It would be nice if tools could be made to work with it: e.g., llvm-nm on the uber object being able to filter or show TUs separately. The latter approach dodges such things.

Yeah, it’s admittedly subtle. IMO the vast majority of optimization in LTO happens pre-link, just as with separate compilation. The remainder is very niche, and you actually did defeat my point: you do get the full TU-internal optimization pre-link. You just don’t get some TU-internal optimization with things e.g. inlined into the TU… which you aren’t promised, because you aren’t promised any cross-TU optimization. So it’s a pretty minor missed optimization in retrospect, but still something that would be nice to have, and that should be safe.

Some thoughts on the file mapping:

As discussed in the embedded toolchains working group, could it be worth splitting out the encoding of an original filename as currently encoded by ^^ into a separate RFC as it may have some other use cases. Two I can think of that may help:

  • A “partial object” when a ld -r relocatable link is used as an input into a subsequent link. The current ld -r format is not ideal for this purpose as it merges non-comdat sections in the ld -r output. An ld -r with origina file info that kept input sections un-merged could in principle be used in place of --whole-archive without needing additional command-line args, and with comdats merged.
  • The dynamic debugging proposal is using an embedded ld -r object for the unitialized code with debug info. Not sure if the additional file info will add much in that use case as the .debug_info should have the necessary file information, but it could be useful if an embedded ld -r is used in another context.

A lot of linker scripts, even embedded ones have * as the filename pattern. If this were proving contentious it could be left out of the first implementation.

Some file patterns need the full path and library information such as archive:file ( Input Section Basics (LD) ) if we’re to support that we’ll need to capture path and archive information too.

I can remember thinking about the original file mapping in the context of partial objects but I don’t remember anything about it as nothing got implemented. If I come across any material I’ll summarise it and post.

The “filename” after the “^^” is not really intended to be a general path encoding.

The way this works in my PR, getModuleIdentifier() is used as a unique ID for the file. The linker never parses this, it’s just an ID. After LTO, the linker maps from that ID back to the actual original input. (This assumes getModuleIdentifier() is unique, which I’m actually not completely sure about off the top of my head. If it isn’t, some other identifier could be used.)

Because of that, linker script rules involving path/archive information should work as usual with my patch.


In the discussion in the embedded toochains working group, I mentioned our internal version had checks for output_section in places other than the inliner. I went back through the places we actually modified… and all of them are either internal passes, or have hasSection() checks. So actually I don’t know of any specific places we need additional checks. (The internal version has separate input_section metadata instead of reusing the “section” marking.)

I’m playing around with some alternatives to see if I can get them down to the size of this patch… So far splitting the module right before AsmPrinter seems like it ends up a bit conceptually cleaner, and it does work trivially with thin LTO. But… the actual splitting logic is hairy; I’ll see if I can trim it down, but it is simpler to just spit out one ELF module with shenanigans than K. And… the linker code ends up fairly isomorphic, but there’s just dramatically less faffing about with symbols. I’ll see if I can some up with something in between the two approaches too. Hopefully tomorrow or the day after.

One bit of results from playing around with this below:

If we want to keep the ^^ thing very uh… linker-internal, an option here would be to use the index in which the bitcode files were added to the LTO object. The linker could cross-reference this index as it came out of the ELF file (in your patch, via ^^); in practice, it would essentially always be the index in bitcodeFiles directly, and that is a local invariant in the linker, so it’s easy to maintain. This saves some mapping logic and makes the section names smaller and less noisy. But, the module identifiers might be useful for human-reading purposes too.

I’ve finished vibe-coding a prototype for splitting the module after codegen… and while it greatly simplifies the linker semantics… it’s fraught with semantic peril on the codegen side. For example, once LTO internalizes everything, it’s difficult to determine what the external symbol structure of each TU should be. A very semantically poor version of this patch does remove a lot of the complexity of your patch, especially in the linker, but the delta is still +200 lines; the splitting logic is large and complex. (My patch is almost certainly completely incorrect and busted, but it doesn’t seem likely that fixing it would make it any smaller. So I didn’t bother looking further.)

The hacked patch for the above is here: GitHub - mysterymath/llvm-project at tu-split-after-codegen · GitHub

While I do think it might be desirable to eventually go this way, I no longer thing it’s a good idea for this patch. I’m going to see if there’s a way to gain the benefits without adding any complexity; I’m looking for a large negative delta. I think there’s a there there; I’m going to spend another day on it.

Regarding section splitting, there is another approach. Rather than changing the linker’s placement behavior via the --enable-non-contiguous-regions flag, the TI linker uses a “split” operator (“>>”) in the placement instructions attached to an output section in the linker script.

For example,

SECTIONS {

.my_out_section: { *(.my_sections) } >> MEM1 | MEM2 | MEM3

}

This allows the input section to be assigned to the first output section in the linker script that it matches. The output section’s placement would then be split among the memory ranges listed on the RHS of the split operator. The order of the memory ranges in the list dictates that the linker will first fit as many input sections from .my_out_section into MEM1 as it can, then MEM2, then MEM3.

I realize that this split operator is not implemented in the GNU linker or the LLVM linker, but perhaps we should consider adding it to those linkers? I’d be willing to RFC a proposal of that nature.

~ Todd

Running linker rules before LTO

To allow the mapping to work correctly, the linker needs to use linker script rules: it needs to compute the compute the correct output section for each symbol, and it needs to compute whether each symbol is subject to a KEEP rule. This uses the section information from the symbol table. This information is then passed to the LTO API for each symbol, using the SymbolResolution struct.

There may be other kinds of information that the linker script can supply to the LTO recompile. One area that comes to mind is some description of the device memory: memory region specifications, including a characterization of the type of memory present in each region. Perhaps a distinction between very fast memory (like tightly coupled memory), onchip memory, and offchip memory. This information could be used in combination with profile information to enable a sort of placement-guided optimization during the LTO recompile.

@snidertm , I don’t think that this is quite apropos to this RFC; the splitting I was discussing wasn’t about taking output sections and splitting them across memory regions, but rather splitting a merged LTO module to preserve TU semantics.

I do think think there’s warrant to discuss the TI syntax, and whether it provides anything over the SECTION(class) syntax we’ve introduced in LLD (and cursorily had okayed for GNU LD) for this purpose. But that should probably be it’s own thread. I should also add that I’m working right now on adding profile-guided section spilling to LLD; I’ll probably have a RFC out for that shortly. We can sync up if you’d like, or I can discuss what I’ve been thinking on a separate thread along with the other issues.

As for this RFC, sorry it’s taken so long, but I’ll have a larger post later today to report the results of my hackery and investigations. I think I’ve found a few semantic correctness issues with the approach as is, and moving around where aspects of the logic are done can resolve it and simplify the overall semantics. But that’s a bit of a topic. I’ve a crappy patch on top of Eli’s (around +200 lines delta at the moment) that passes ELI’s tests, but I want to try to demonstrate the issues with new tests and clean it up; I’ll do that in the afternoon.

The TL;DR is that I think we’d need to do section accounting in AsmPrinter to account for sections that are accounted to functions, but are not function bodies, or any other sections that are generated by AsmPrinter on behalf of a TU. This would also provide the opportunity to define “this is in this TU” asm guard directives, which would provide a handy solution for module-level inline asm. And it can come out as a simple ULEB128 section-index to TU bitcodeFile index table section, which is comparatively tiny and extremely fast to generate and use in LTO and the linker, respectively.

Alright, here’s patch with the results of my explorations, with commits added on top of Eli’s for the adjustments and a test for a newly handled scenario.

Beware this code; I have only hammered it into approximate shape with AI. It passes Eli’s tests and one Eli’s patch does not, but caveat emptor.

So, the observation I was struggling with was this: the accounting mechanism in IR is at the “GlobalObject” level, and that’s what we desire to ascribe TUs to, but the actual mapping in linker scripts is in terms of output sections. That’s a fundamental tension.

For optimization purposes, we do need to know the input section, so the linker can tell us what the output section is. This can then be set as an attribute, and the input section can be frozen. I used to hate the freezing, but I actually like it after having worked with it: it keeps the output section attribute safe and up to date as manipulations occur. There may be ways to relax this, but it’s
tricky, and forcing the input section to be set early should be conservatively correct.

I did rework the interface to use a new SectionResolution data structure, which keeps the determination language entirely in terms of Section. This cleans up all of the modifications to IRSymtab.

There’s also the question of how to inform the linker of which input section in the final binary belongs to which TU. Eli’s patch does this by encoding TU information into section names, which seems okay, but that itself also seems to happen at the wrong granularity: IR object, not input section.

We have an IR object that is recorded as being as being from some given TU: this itself will produce some number of output sections: function body or data, but maybe also EH information, jump tables, etc etc. The place where this transition happens in AsmPrinter, and that seems to be the right place to handle this overall!

I added an IR attribute, ‘lto.tu’, to store an integer index indicating which bitcode file a given IR GlobalObject is from. It’s then up to AsmPrinter to lower this information into the object file; I did this by having it invoke something like a “begin TU” and “end TU” asm directive to wrap the sections that are attributable to that object. (Note that this would also supply a solution for module-level inline assembly, although I did not do so in my patch.) Sections would be prevented from being merged with sections from other TUs via the existing UniqueID mechanism, and AsmPrinter’s streamer for ELF would generate another section to tell the linker what the mapping is between input section and TU.

This section, .llvm.lto.tu, would be a tight ULEB128 table mapping section index in the final ELF file with TU index in the list of bitcodeFiles maintained by the linker. This makes small, fast to generate, and trivial to take advantage of. It also avoids any string-related ambiguities.

Since this works by the normal extension mechanisms, it’s conceivable that this mechanism could be repurposed for other object file formats, supposing they have or gain equivalent mechanisms to linker scripts that require TU names to be preserved.

Lastly, doing this in AsmPrinter ensures that supplementary sections generated on behalf of IR constructs are also associated with the correct TU. This includes jump tables, exception tables, and the like. I’ve included a test for an exception table case; this fails on the original patch, but it passes with the alterations.

All my names are crappy and all my semantics and code are rough, this is just to add fodder for further discussion in the RFC.

GitHub link to read the diffs: Comparing 50e4cb09b218577990edca5e2dd08013b6d14403..0f7069cb085d4e121035a7299b5aba65f707bf5d · llvm/llvm-project · GitHub .

Keeping the resolution in terms of the symbol table was an attempt to keep the encapsulation of the LTO APIs as much as possible. The linker gets a simplified datastructure with the relevant bits of the IR, not the raw IR. And using the symbol table ensures we don’t need to parse the IR pre-LTO; ThinLTO goes out of its way to delay parsing IR as much as possible.

If you drop those constraints, there are other solutions. The linker itself can parse the IR, like your patch. Or we could add a callback to the LTO APIs allow the linker to read/modify the IR after it’s parsed. We can even drop the clang flag, and make the linker compute the section names at link-time. But I expect LTO maintainers won’t like that.


This looks pretty clean. But we’ll need to think about how developers debugging LTO are supposed to map the indexes back to filenames.

Sorry for any misunderstanding, my comment about splitting was in response to this earlier post.

Section spilling

The RFC and patch assume that each input section is assigned to at most one output section. For LD and LLD, the --enable-non-contiguous-regions flag makes this not the case. When this flag is enabled, an input section is matched with all output sections with matching wildcards. If an earlier match for a section would cause a MEMORY region to become oversize, the section is spilled to the next matching output section. In LLD, this functionality is available using the SECTION linker script syntax, even without the flag.

You contend that the --enable-non-contiguous-regions flag invalidates the assumption that an input section can be assigned to at most one output section, but the TI linker “split” operator is another way of specifying split placement that preserves the input section assignment to one output section assumption.

This does come into play with the downstream TI implementation of LTO since we ordinarily generate functions and data objects into their own sections (-ffunction-sections, -fdata-sections).

Hmm, this is interesting, and something that I was going to ask about. The TI linker would then need to allow an individual output section to be discontiguous across memory, with holes in it? I wasn’t sure exactly what the semantics would be… I figured it might be multiple output sections, one per region, all with the same name, but I couldn’t be sure.

Yeah, I’d agree, adding another lazy parse step isn’t great; see below for thoughts on the regular LTO case. But, for thin LTO, it seems like the module summary might be a better place for this information to live? An accounting of the symbols in a given TU which the linker could read and convert into a resolution for the various sections.

I guess I’m detecting a little bit of a tension with what IRSymtab is for. Arguably its most imporant role is as a vehicle for the linker to obtain information that it needs to use link decisions as quickly as possible. In particular, if this bitcode file is in an archive, does it need to deal with it at all? SymbolResolution is a convenient place to stash verdicts about such symbols, but I don’t think it should permanently remain the only linker->LTO vehicle for communication about the ambient linker state. Otherwise, whenever there’s something else that the linker needs to comment on, we’ll have to add representation for it to the IRSymtab… which would eventually have an impact on how quickly the linker can make the extract/no-extract decision. And I just can’t escape the feeling that we’re taking the first step in that direction here.

Eh, I looked into this, and it honestly seems a bit cleaner than what I was doing for the regular LTO case. But, the callback could express only the linker’s actual responsibility: “for this input section, what output section would it be assigned to, and is it KEEP?”. LTO seems repsonsible for all other decision-making. Callbacks provide one way for LTO to comment on information like this, but maybe something else would work better? Anyway, See Use a callback for section resolutions · mysterymath/llvm-project@112e632 · GitHub

It is multiple output sections. If needed and allowed, an output section is split during the placement phase of the link, each output section piece gets its base name plus a unique id annotation.

1 Like

I’ve re-pushed the above change after doing a bunch of sketching to support ThinLTO. I wouldn’t trust the test modifications, or anything really, but it again gives a AI-fueld look at how this might look. It does appear that the module summaries are a natural place for section information about globals to live for ThinLTO, and these can be converted into real attributes in the backend naturally. It’s also possible to use the same section resolution callback for both Thin and Regular LTO.