Skip to content

Execute delayed lowering in a sandbox#155337

Draft
aerooneqq wants to merge 10 commits intorust-lang:mainfrom
aerooneqq:delayed-lowering-in-sandbox
Draft

Execute delayed lowering in a sandbox#155337
aerooneqq wants to merge 10 commits intorust-lang:mainfrom
aerooneqq:delayed-lowering-in-sandbox

Conversation

@aerooneqq
Copy link
Copy Markdown
Contributor

@aerooneqq aerooneqq commented Apr 15, 2026

Code is not yet ready to review.

Motivation

This PR researches the possibility to execute delayed lowering in a sandbox: a special "untracked" mode of the compiler which allows to correctly obtain all information (which is the result of ty-level queries execution) for lowering of delayed owners.
Without the sandbox we will eventually run into query cycles which results in non-relevant error messages, and what's more important we simply can't execute needed workflows, i.e., resolving inherent methods in delegations, for this resolution we need hir_crate_items to complete, however if we insert ids of delegations into hir_crate_items then we will certainly run into query cycle for example when computing associated_item which requires to fill has_self field which in turn requires the resolution of currently lowered delegation. We may try not to include ids of delayed owners into hir_crate_items, however that will break all compilation as we can't update the result of the query.

This problem is solved by a sandbox: the core idea that we execute some code to lower delayed owners, then we restore the state of the compiler as it was before sandbox execution, and continue compilation but with correctly lowered delayed owners. During sandbox we may fill queries like hir_crate_items with invalid (from without sandbox compilation point of view) values and execute lowering in a "forget that delayed owners exist in the codebase" mode. This should help to eliminate query cycles from delayed owners lowering.

Implementation details

Queries caches

The first thing to think about are query caches: when sandbox execution is over we need to invalidate caches that were filled during sandbox. The approach that is implemented in this PR is simply dropping all keys that were filled during sandbox, for this purpose we remember keys that queries had before sandbox, and after it we drop all keys that are not present in this remembered set.

However, this approach is not optimal, as some queries may contain valid values for keys filled during sandbox, consider generics_of, to obtain generics of some function we do not need to interact with unit queries that were filled with invalid values like hir_crate_items, so we should not invalidate it for performance reasons (at lest in non-incremental mode).

The invalidation approach should allow skipping modifications of almost all caches, thus it should not affect stable part of the compiler, as we invalidate them only if sandbox was executed.

Untracked

In addition to query caches we have untracked data, the most obvious example of which are definitions that are created during resolve or AST -> HIR lowering stage (or even later). The problem here is that untracked data can be modified during sandbox execution and in theory we also need to invalidate it. Why? Because definitions created during sandbox can be recreated during after-sandbox stages of compilation (i.e., impl Traits) and this will cause a correct panic.

One way of solving it as it was mentioned above is invalidation of all definitions that were created during sandbox, BUT definitions that were created during delayed AST -> HIR lowering are valid and they will not be recreated after sandbox. So we need to preserve them. Taking into account those factors now the implementation is as follows:

  • We track all definitions created during sandbox and store them in allow_overwrite set,
  • Next, when getting out of sandbox we allow recreation of definitions that are inside of allow_overwrite but only once,
  • During sandbox allow_overwrite is not considered, as during sandbox we may also try to create same definitions, in this case we need to panic.

DepGraph

We can compile the program in incremental mode that involves creation of DepNodes. DepNode can not be created twice, and by the nature of sandbox (it can be thought just as regular non-tracked computation which reuses some of the compiler's logic and infrastructure) we should not in any sense interact with dependencies. That is why we pretend that there is no DepGraph during sandbox and execute all queries in non-incremental mode. When sandbox is over we continue compilation with the DepGraph as usual.

Integration of delayed lowering into the query system

There is a separate problem of integrating sandboxed delayed lowering in the compiler flow for the following reason: we must drop ResolverAstLowering and ast::Crate after we lowered all AST owners, otherwise there will be perf regressions, as the users of compiler API implicitly rely on this. This fact blocks us from creating force_delayed_owners_lowering function and calling it in the beginning of analysis(()) query, as in rustdoc analysis is not called.
The obvious option is to call force_delayed_owners_lowering in the beginning of the hir_crate_items query, and that is how it is implemented now, but if we invoke sandboxed lowering from hir_crate_items, which requires hir_crate_items to be run before lowering (see second pseudocode in the end), we will encounter a query cycle.

  • The first option is to adjust query cycle detection for sandbox, but it is the least preferred thing to do.
  • The next option which is implemented in this PR is to create a callfront (it is like opposition for callback) which will be executed by the query infrastructure before the selected query (in our case hir_crate_items). With this approach user of the compiler API do not have to manually drop AST and as far as I understand this approach will work even if the query_execute_fn is overridden.

TyCtxt untracked caches

There are some untracked caches in TyCtxt itself (i.e., clauses_cache, evaluation_cache), we just clear them after sandbox, I did not dive too deep in researching a question whether we can not invalidate some of them, that's a future work.

Trimmed def paths

There is an assert in trimmed def paths query that ensures that it is executed only once so it is turned off in a sandbox.

Parallel frontend interaction

Now sandbox is executed in a place where no other queries are run, so synchronization issues were not considered. Basically it is a continuation of hir_crate where we finish AST -> HIR lowering, all other queries require hir_crate(()).

Diagnostics

During sandbox we should not emit anything (i.e., tcx.dcx().make_silent()), maybe the only exception are delayed-owners-related diagnostics, that is also a future work.

Pseusocode

Here is an approximate routine for execution of an operation in a sandbox.

pub fn with_sandbox(self, op: impl FnOnce()) {
    self.is_in_sandbox.store(true, Ordering::Relaxed);
    // Tell queries to save all keys before sandbox.
    self.enter_query_sandbox();

    // Execute with TasksDeps::Forbidden to panic on any dep node created during sandbox.
    self.dep_graph.with_query_deserialization(|| {
        self.dep_graph.is_in_sandbox.store(true, Ordering::Relaxed);
        // Disable trimmed paths as it has an assert that it is executed at most once.
        with_no_trimmed_paths!({
            op();
        });

        self.dep_graph.is_in_sandbox.store(false, Ordering::Relaxed);
    });

    // Tell queries to invalidate all keys that were not seen before the sandbox.
    self.leave_query_sandbox();
    self.is_in_sandbox.store(false, Ordering::Relaxed);
    // Invalidate TyCtxt caches
    self.highest_var_in_clauses_cache.borrow_mut().clear();
    self.canonical_param_env_cache.clear();
    self.new_solver_canonical_param_env_cache.borrow_mut().clear();
    self.new_solver_evaluation_cache.borrow_mut().clear();
    self.evaluation_cache.clear();
    self.selection_cache.clear();
    self.ty_rcache.borrow_mut().clear();
}

And that's what we want to execute:

self.with_sandbox(|| {
    // hir_crate_items WITHOUT delegations
    self.ensure_done().hir_crate_items(());
    // Crate inherent impls for inherent methods resolution
    self.ensure_done().crate_inherent_impls(());

    for &id in &krate.delayed_ids {
        self.ensure_done().lower_delayed_owner(id);
    }
});

Why sandbox?

  • First, another option was recently tried: filling hir_crate_items before delayed lowering without sandbox in delegation: fix cycles during delayed lowering #154368. It means that hir_crate_items contained ids of delayed owners and it did not solved the problem: delegation: OOM #155060 was immediately reported. It happened because during trimmed def paths the identifier is obtained through accessing HIR of items, and as we are now lowering delayed owner this causes a cycle. In earlier versions of delegation: fix cycles during delayed lowering #154368 there was an attempt to store information about delayed owners before delayed lowering and then access it bypassing HIR. However this approach leads to manually inserting if tcx.is_delayed_owner(def_id) in possibly many queries. So when new features are added to the compiler author or those features should know about delayed owners and think how to correctly obtain needed data. That is not good approach. Sandbox solves those issues by removing the ids of delayed owners from hir_crate_items and hir_module_items (and thus hopefully from all compilation), moreover we may adjust rustc_hir::intravisit::Visitor to not to visit delayed owners (now this check is only in ItemCollector)
  • Query cycles mentioned in previous item happen, when there is an error in other part of the code (not in delayed owners), because diagnostics want to scan all items or something like that. Now consider a valid code, there are no errors and diagnostics will not be triggered. In this case let's take a small glance into future where we need to support inherent methods resolutions for delayed lowering. This resolution is done through ProbeContext which requires crate_inherent_impls to run, which in turn requires hir_crate_items. So without hir_crate_items we can't perform inh. methods resolution but now we force delayed lowering before hir_crate_items. In this case there are only two options: either to execute hir_crate_items as in delegation: fix cycles during delayed lowering #154368 which is not scalable approach or to create a sandbox. Note that inh. methods resolution requires collecting candidates which in turn requires executing associated_item query, and AssocItem struct has a has_self field that requires HIR of delayed owner. So we can not execute resolution of inh. methods without setting hir_crate_items and in this aspect sandbox helps us a lot: as hir_crate_items will be available and will not contain ids of delayed owners.
    Honestly speaking, the algorithm for inh. methods resolution will possibly be iterative: as an input you have a set of default HIR (called default) and a set of delayed owners (called delayed), next you execute a loop: if there there are some resolved elements from delayed against default you replace default with those resolved elements and continue until !delayed.is_empty(). If at some iteration there are no resolutions and delayed is not empty then there are cycle in recursive delegations. With such an iterative algorithm sandbox should help as we may update hir_crate_items on every iteration,
  • There is one interesting idea on zulip which proposes try_query function that can return None if there is a query cycle. It may help with lowering of delegations without inh. methods, but once again it is not guaranteed as query cycles may appear some time after when someone who is not aware of delayed owners will implement his features,
  • Finally, sandbox should help integrate currently not supported workflows in the compiler mainly on infrastructural level without altering many queries with if statements.

r? @petrochenkov

@rustbot rustbot added A-query-system Area: The rustc query system (https://rustc-dev-guide.rust-lang.org/query.html) S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Apr 15, 2026
@rust-log-analyzer

This comment has been minimized.

@aerooneqq aerooneqq force-pushed the delayed-lowering-in-sandbox branch from 9028f44 to 297f41a Compare April 15, 2026 13:09
@petrochenkov
Copy link
Copy Markdown
Contributor

@bors try @rust-timer queue

@rust-timer

This comment has been minimized.

@rust-bors

This comment has been minimized.

rust-bors bot pushed a commit that referenced this pull request Apr 15, 2026
@rustbot rustbot added the S-waiting-on-perf Status: Waiting on a perf run to be completed. label Apr 15, 2026
@rust-bors
Copy link
Copy Markdown
Contributor

rust-bors bot commented Apr 15, 2026

☀️ Try build successful (CI)
Build commit: 65d1474 (65d1474bf3612fdd40cb69ec815cea30b41877a2, parent: 57cb10ae1e27f54f72eb2968f6ddc8fe88b83ab5)

@rust-timer

This comment has been minimized.

@rust-timer
Copy link
Copy Markdown
Collaborator

Finished benchmarking commit (65d1474): comparison URL.

Overall result: ❌ regressions - please read:

Benchmarking means the PR may be perf-sensitive. It's automatically marked not fit for rolling up. Overriding is possible but disadvised: it risks changing compiler perf.

Next, please: If you can, justify the regressions found in this try perf run in writing along with @rustbot label: +perf-regression-triaged. If not, fix the regressions and do another perf run. Neutral or positive results will clear the label automatically.

@bors rollup=never
@rustbot label: -S-waiting-on-perf +perf-regression

Instruction count

Our most reliable metric. Used to determine the overall result above. However, even this metric can be noisy.

mean range count
Regressions ❌
(primary)
1.1% [0.2%, 3.9%] 211
Regressions ❌
(secondary)
1.3% [0.0%, 7.5%] 217
Improvements ✅
(primary)
- - 0
Improvements ✅
(secondary)
-0.2% [-0.4%, -0.0%] 5
All ❌✅ (primary) 1.1% [0.2%, 3.9%] 211

Max RSS (memory usage)

Results (primary 7.7%, secondary 4.8%)

A less reliable metric. May be of interest, but not used to determine the overall result above.

mean range count
Regressions ❌
(primary)
7.7% [1.4%, 27.2%] 26
Regressions ❌
(secondary)
6.0% [2.0%, 24.7%] 19
Improvements ✅
(primary)
- - 0
Improvements ✅
(secondary)
-2.8% [-3.1%, -2.6%] 3
All ❌✅ (primary) 7.7% [1.4%, 27.2%] 26

Cycles

Results (primary 2.2%, secondary -0.0%)

A less reliable metric. May be of interest, but not used to determine the overall result above.

mean range count
Regressions ❌
(primary)
2.2% [2.2%, 2.2%] 1
Regressions ❌
(secondary)
2.5% [1.5%, 3.5%] 7
Improvements ✅
(primary)
- - 0
Improvements ✅
(secondary)
-5.8% [-7.8%, -3.0%] 3
All ❌✅ (primary) 2.2% [2.2%, 2.2%] 1

Binary size

This perf run didn't have relevant results for this metric.

Bootstrap: 489.055s -> 495.053s (1.23%)
Artifact size: 394.13 MiB -> 396.82 MiB (0.68%)

@rustbot rustbot added perf-regression Performance regression. and removed S-waiting-on-perf Status: Waiting on a perf run to be completed. labels Apr 15, 2026
@rust-log-analyzer

This comment has been minimized.

@petrochenkov
Copy link
Copy Markdown
Contributor

@bors try @rust-timer queue

@rust-timer

This comment has been minimized.

@rust-bors

This comment has been minimized.

rust-bors bot pushed a commit that referenced this pull request Apr 16, 2026
@rustbot rustbot added the S-waiting-on-perf Status: Waiting on a perf run to be completed. label Apr 16, 2026
@rust-bors
Copy link
Copy Markdown
Contributor

rust-bors bot commented Apr 16, 2026

☀️ Try build successful (CI)
Build commit: 244e060 (244e0605b23ae6d675ed68716fa2b042282dbfb0, parent: e8e4541ff19649d95afab52fdde2c2eaa6829965)

@rust-timer

This comment has been minimized.

@rust-timer
Copy link
Copy Markdown
Collaborator

Finished benchmarking commit (244e060): comparison URL.

Overall result: ❌✅ regressions and improvements - please read:

Benchmarking means the PR may be perf-sensitive. It's automatically marked not fit for rolling up. Overriding is possible but disadvised: it risks changing compiler perf.

Next, please: If you can, justify the regressions found in this try perf run in writing along with @rustbot label: +perf-regression-triaged. If not, fix the regressions and do another perf run. Neutral or positive results will clear the label automatically.

@bors rollup=never
@rustbot label: -S-waiting-on-perf +perf-regression

Instruction count

Our most reliable metric. Used to determine the overall result above. However, even this metric can be noisy.

mean range count
Regressions ❌
(primary)
1.0% [0.2%, 3.3%] 209
Regressions ❌
(secondary)
1.1% [0.0%, 6.6%] 217
Improvements ✅
(primary)
- - 0
Improvements ✅
(secondary)
-0.2% [-0.3%, -0.0%] 6
All ❌✅ (primary) 1.0% [0.2%, 3.3%] 209

Max RSS (memory usage)

Results (primary 2.5%, secondary 3.2%)

A less reliable metric. May be of interest, but not used to determine the overall result above.

mean range count
Regressions ❌
(primary)
2.5% [2.5%, 2.5%] 1
Regressions ❌
(secondary)
5.7% [3.7%, 7.3%] 3
Improvements ✅
(primary)
- - 0
Improvements ✅
(secondary)
-4.1% [-4.1%, -4.1%] 1
All ❌✅ (primary) 2.5% [2.5%, 2.5%] 1

Cycles

Results (secondary 0.7%)

A less reliable metric. May be of interest, but not used to determine the overall result above.

mean range count
Regressions ❌
(primary)
- - 0
Regressions ❌
(secondary)
2.9% [1.8%, 5.0%] 13
Improvements ✅
(primary)
- - 0
Improvements ✅
(secondary)
-4.9% [-7.5%, -2.5%] 5
All ❌✅ (primary) - - 0

Binary size

This perf run didn't have relevant results for this metric.

Bootstrap: 490.756s -> 493.643s (0.59%)
Artifact size: 394.16 MiB -> 396.98 MiB (0.72%)

@rustbot rustbot removed the S-waiting-on-perf Status: Waiting on a perf run to be completed. label Apr 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-query-system Area: The rustc query system (https://rustc-dev-guide.rust-lang.org/query.html) perf-regression Performance regression. S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants