Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1917,6 +1917,7 @@ fn deferrable_residue_preserves_local_binding_type_checks() {
&mut result,
&mut assigners,
&mut exemptions,
&[],
));
assert!(result.errors.is_empty());
assert!(!crate::run_arg_promote_stages(
Expand All @@ -1932,7 +1933,6 @@ fn deferrable_residue_preserves_local_binding_type_checks() {
&mut store,
package_id,
crate::PipelineStage::Full,
&mut result,
&[],
&exemptions,
);
Expand Down
65 changes: 29 additions & 36 deletions source/compiler/qsc_fir_transforms/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,9 +270,9 @@ pub enum PipelineStage {
/// abort the schedule before UDT erasure runs. Non-fatal defunctionalization
/// warnings are preserved on [`PipelineResult::warnings`] and the schedule
/// continues to the requested stage.
/// 4. Pinned-item validation runs before seeded item DCE and exec graph
/// rebuild. Missing or non-callable pins are fatal diagnostics because
/// pinned items are explicit preservation requests from callers.
/// 4. Pinned-item validation runs before seed-expanded UDT erasure, item DCE,
/// and exec graph rebuild. Missing or non-callable pins are fatal diagnostics
/// because pinned items are explicit preservation requests from callers.
///
/// In every fatal case the intermediate FIR intentionally violates downstream
/// invariants, so running later passes would produce misleading failures.
Expand Down Expand Up @@ -362,6 +362,7 @@ fn run_pipeline_to_impl(
&mut result,
&mut assigners,
&mut exemptions,
pinned_items,
);
if defunc_lowering_done {
return result;
Expand All @@ -378,14 +379,7 @@ fn run_pipeline_to_impl(
return result;
}

finalize_pipeline(
store,
package_id,
stage,
&mut result,
pinned_items,
&exemptions,
);
finalize_pipeline(store, package_id, stage, pinned_items, &exemptions);
result
}

Expand All @@ -403,6 +397,7 @@ fn run_defunc_and_lowering_stages(
result: &mut PipelineResult,
assigners: &mut PackageAssigners,
exemptions: &mut invariants::InvariantExemptions,
pinned_items: &[StoreItemId],
) -> bool {
let defunctionalize::DefuncOutcome {
diagnostics,
Expand Down Expand Up @@ -450,7 +445,12 @@ fn run_defunc_and_lowering_stages(
return true;
}

udt_erase::erase_udts(store, package_id, assigners);
let pinned_errors = validate_pinned_items(store, pinned_items);
if !pinned_errors.is_empty() {
result.errors = pinned_errors;
return true;
}
udt_erase::erase_udts_with_seeds(store, package_id, assigners, pinned_items);
invariants::check_with_exemptions(
store,
package_id,
Expand Down Expand Up @@ -668,28 +668,21 @@ fn assert_no_simulatable_intrinsics(store: &PackageStore) {
);
}

/// Runs the backend stages after all structural transforms: pinned-item
/// validation, item dead-code elimination, execution-graph rebuild, and the
/// final `PostAll` invariant walk.
/// Runs the backend stages after all structural transforms: item dead-code
/// elimination, execution-graph rebuild, and the final `PostAll` invariant walk.
///
/// Mutates `result` in place; a fatal pinned-item validation error stops the
/// backend early with the errors recorded on `result`.
/// Pins have already been validated before UDT erasure. Stops after the
/// requested stage.
fn finalize_pipeline(
store: &mut PackageStore,
package_id: PackageId,
stage: PipelineStage,
result: &mut PipelineResult,
pinned_items: &[StoreItemId],
exemptions: &invariants::InvariantExemptions,
) {
// Item DCE: remove unreachable callable items and dead type items.
// Callers may pin items via `pinned_items` to keep them (and their
// transitive dependencies) alive through DCE and exec-graph-rebuild.
let pinned_errors = validate_pinned_items(store, pinned_items);
if !pinned_errors.is_empty() {
result.errors = pinned_errors;
return;
}
run_item_dce_and_gc(store, package_id, pinned_items);
invariants::check_with_exemptions(
store,
Expand All @@ -711,8 +704,9 @@ fn finalize_pipeline(
return;
}

// PostAll uses entry-only reachability. Pinned items (original target kept
// for fir_to_qir_from_callable) retain pre-transform types and are not checked.
// PostAll uses entry-only reachability. Pinned-only bodies have erased UDTs
// but can retain unpromoted inputs and other forms handled by the later
// signature-preserving sub-pipeline, so full-schedule checks do not apply.
invariants::check_with_exemptions(
store,
package_id,
Expand Down Expand Up @@ -832,14 +826,13 @@ fn validate_pinned_item(store: &PackageStore, item_id: StoreItemId) -> Result<()
/// Runs item-level DCE with optional pinned-root expansion, followed by an
/// unconditional GC pass.
///
/// Item DCE runs in two forms: the entry package keeps every entry-reachable
/// callable, while each foreign (library) package keeps only its
/// entry-reachable callables (its public surface is not an entry point for a
/// closed codegen compilation). GC then runs over the entire reachable package
/// Item DCE retains entry- and pin-reachable callables in the entry package and
/// each foreign package. A library's public surface is not an entry point for a
/// closed codegen compilation. GC then runs over the entire reachable package
/// closure because upstream rewrite passes leave orphaned arena nodes behind in
/// every transformed package, regardless of whether item DCE removed any items.
///
/// Pinned items are validated by `run_pipeline_to_impl` before this helper is
/// Pinned items are validated before UDT erasure and before this helper is
/// called. They are not invariant-checked; `PostAll` uses entry-only
/// reachability. Pinning is needed when the original target ID is used
/// by `fir_to_qir_from_callable` after defunc rewrites the entry `Call`
Expand All @@ -856,9 +849,9 @@ fn run_item_dce_and_gc(
};
let _ = item_dce::eliminate_dead_items(package_id, store.get_mut(package_id), &reachable);

// Foreign packages: structural passes transformed only their entry-reachable
// callables, so each foreign package still holds entry-unreachable callables
// that reference erased UDTs and pre-promotion signatures. RCA and codegen
// Signature-changing passes transformed only entry-reachable callables, so
// foreign packages can still hold callers using pre-promotion signatures.
// UDT erasure, by contrast, rewrites whole selected packages. RCA and codegen
// analyze every item in every package, so those stale callables must be
// removed to keep each foreign package internally consistent with its
// transformed reachable callables. Pinned callable items and their
Expand Down Expand Up @@ -892,8 +885,8 @@ fn run_item_dce_and_gc(
///
/// `pinned_items` must identify existing callable items. Invalid pins are
/// reported as fatal [`PipelineError::MissingPinnedItem`] or
/// [`PipelineError::PinnedItemNotCallable`] diagnostics before seeded item
/// DCE runs.
/// [`PipelineError::PinnedItemNotCallable`] diagnostics before seed-expanded
/// UDT erasure runs.
///
/// Callers may consume the transformed FIR only when [`PipelineResult::errors`]
/// is empty; warnings do not block successful output.
Expand All @@ -905,7 +898,7 @@ fn run_item_dce_and_gc(
/// package. The mutated store is a disposable codegen artifact — pass a fresh
/// `lower_to_fir` store (or an explicit clone) and do **not** reuse it after the
/// transforms. Item DCE and GC leave each package internally consistent only for
/// the entry-rooted reachable closure, not for reuse as a general-purpose
/// the entry- and pin-rooted compilation, not for reuse as a general-purpose
/// package store. Production callers uphold this by re-lowering from HIR per
/// request; it is a caller property, not a contract this function enforces.
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
//! * [`return_unify::unify_returns`](crate::return_unify::unify_returns)
//! * [`cond_normalize::normalize_conditions`](crate::cond_normalize::normalize_conditions)
//! * [`defunctionalize::defunctionalize`](crate::defunctionalize::defunctionalize)
//! * [`udt_erase::erase_udts`](crate::udt_erase::erase_udts)
//! * [`udt_erase::erase_udts_with_seeds`](crate::udt_erase::erase_udts_with_seeds)
//! * [`tuple_compare_lower::lower_tuple_comparisons`](crate::tuple_compare_lower::lower_tuple_comparisons)
//! * [`tuple_decompose::tuple_decompose`](crate::tuple_decompose::tuple_decompose)
//! * [`arg_promote::arg_promote`](crate::arg_promote::arg_promote) (and its
Expand Down
2 changes: 1 addition & 1 deletion source/compiler/qsc_fir_transforms/src/reachability.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ pub fn collect_reachable_with_seeds(
visited
}

/// Returns the package closure induced by an entry-reachable callable set.
/// Returns the package closure induced by entry-only or seed-expanded item reachability.
///
/// The returned set always includes the root package, even when the entry
/// expression reaches no other callables.
Expand Down
35 changes: 32 additions & 3 deletions source/compiler/qsc_fir_transforms/src/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1062,21 +1062,50 @@ pub(crate) fn generate_qir(source: &str) -> String {
pub(crate) fn try_eval_fir_entry(
store: &fir::PackageStore,
pkg_id: fir::PackageId,
) -> Result<qsc_eval::val::Value, String> {
eval_fir_graph(store, pkg_id, store.get(pkg_id).entry_exec_graph.clone())
}

/// Evaluates a unit-input callable directly, without making it entry-reachable.
/// This lets pinned-root tests exercise the body retained by the pipeline.
#[cfg(test)]
pub(crate) fn try_eval_fir_callable(
store: &fir::PackageStore,
callable: fir::StoreItemId,
) -> Result<qsc_eval::val::Value, String> {
let package = store.get(callable.package);
let fir::ItemKind::Callable(decl) = &package.get_item(callable.item).kind else {
panic!("target must be callable");
};
assert_eq!(
package.get_pat(decl.input).ty,
qsc_fir::ty::Ty::UNIT,
"direct body evaluation requires unit input"
);
let fir::CallableImpl::Spec(spec) = &decl.implementation else {
panic!("target must have a body");
};
eval_fir_graph(store, callable.package, spec.body.exec_graph.clone())
}

#[cfg(test)]
fn eval_fir_graph(
store: &fir::PackageStore,
pkg_id: fir::PackageId,
graph: fir::ExecGraph,
) -> Result<qsc_eval::val::Value, String> {
use qsc_eval::backend::{SparseSim, TracingBackend};
use qsc_eval::output::GenericReceiver;
use qsc_fir::fir::ExecGraphConfig;

let package = store.get(pkg_id);
let entry_graph = package.entry_exec_graph.clone();
let mut env = qsc_eval::Env::default();
let mut sim = SparseSim::new();
let mut out = Vec::<u8>::new();
let mut receiver = GenericReceiver::new(&mut out);
qsc_eval::eval(
pkg_id,
Some(42),
entry_graph,
graph,
ExecGraphConfig::NoDebug,
store,
&mut env,
Expand Down
31 changes: 21 additions & 10 deletions source/compiler/qsc_fir_transforms/src/udt_erase.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
//! the child's source span.
//! - **Whole-package erasure across the reachable package closure.** This
//! mutates every expression and callable signature in the target package
//! and in packages reached from its entry, not just reachable callable
//! bodies. Like defunctionalization, it handles paths into library
//! and in packages reached from its entry or additional seeds, not just
//! reachable callable bodies. Like defunctionalization, it handles paths into library
//! packages. UDT definitions are resolved from the whole store via the UDT cache.
//! - **Feeds [`crate::exec_graph_rebuild`].** Structurally mutates reachable
//! callable bodies in place; the pipeline driver unconditionally rebuilds the
Expand Down Expand Up @@ -56,7 +56,7 @@ mod test_cases;
use crate::EMPTY_EXEC_RANGE;
use crate::fir_builder;
use crate::package_assigners::PackageAssigners;
use crate::reachability::{collect_reachable_from_entry, collect_reachable_package_closure};
use crate::reachability::{collect_reachable_package_closure, collect_reachable_with_seeds};
use crate::walk_utils::{
DirectChild, expr_is_safe_to_discard, for_each_direct_child, for_each_expr,
for_each_expr_in_callable_impl,
Expand All @@ -78,9 +78,19 @@ use rustc_hash::{FxHashMap, FxHashSet};
/// in the store.
type UdtCache = FxHashMap<StoreItemId, Ty>;

/// Test convenience wrapper for entry-rooted UDT erasure without extra seeds.
#[cfg(test)]
pub fn erase_udts(
store: &mut PackageStore,
package_id: PackageId,
assigners: &mut PackageAssigners,
) {
erase_udts_with_seeds(store, package_id, assigners, &[]);
}

/// Erases UDT types and UDT-shaped expressions in the target package's
/// reachable package closure, while resolving UDT definitions from the
/// whole store. Specifically, rewrites:
/// entry- and seed-reachable package closure, while resolving UDT definitions
/// from the whole store. Specifically, rewrites:
///
/// - Every `Ty::Udt` to its pure tuple or scalar type (via `get_pure_ty()`)
/// on expressions, patterns, blocks, and callable signatures.
Expand Down Expand Up @@ -117,19 +127,20 @@ type UdtCache = FxHashMap<StoreItemId, Ty>;
/// # Panics
///
/// Panics if the package has no entry expression. The reachability scans
/// in this pass go through [`collect_reachable_from_entry`], which asserts
/// in this pass go through [`collect_reachable_with_seeds`], which asserts
/// `package.entry.is_some()`.
pub fn erase_udts(
pub fn erase_udts_with_seeds(
store: &mut PackageStore,
package_id: PackageId,
assigners: &mut PackageAssigners,
seeds: &[StoreItemId],
) {
// Build a resolution cache from all UDT items across all packages.
let udt_cache = build_udt_cache(store);
let reachable = collect_reachable_from_entry(store, package_id);
let reachable = collect_reachable_with_seeds(store, package_id, seeds);

// Erase UDTs in the target package and in any package that contains an
// entry-reachable item. UDT definition lookup still spans the whole
// entry- or seed-reachable item. UDT definition lookup still spans the whole
// store so cross-package references resolve correctly.
let pkg_ids: Vec<PackageId> = collect_reachable_package_closure(package_id, &reachable)
.into_iter()
Expand All @@ -144,7 +155,7 @@ pub fn erase_udts(
/// Erases UDT types and struct expressions in a single package, rewriting
/// every expression type, pattern type, block type, callable signature,
/// and struct construction in place. Called once per package in the
/// entry-reachable closure.
/// entry- and seed-reachable closure.
///
/// # Before
/// ```text
Expand Down
Loading
Loading