use crate::ast::TopLevel;
use crate::ir::buffer_build::BufferBuildPassReport;
use crate::ir::pass_diag::{self, CountsByFn};
use crate::ir::{AllocPolicy, AnalysisResult, CallLowerCtx};
use crate::source::LoadedModule;
use crate::types::checker::{TypeCheckResult, run_type_check_full, run_type_check_with_loaded};
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum PipelineStage {
Tco,
Typecheck,
InterpLower,
BufferBuild,
Resolve,
LastUse,
Analyze,
Escape,
}
impl PipelineStage {
pub const fn name(self) -> &'static str {
match self {
Self::Tco => "tco",
Self::Typecheck => "typecheck",
Self::InterpLower => "interp_lower",
Self::BufferBuild => "buffer_build",
Self::Resolve => "resolve",
Self::LastUse => "last_use",
Self::Analyze => "analyze",
Self::Escape => "escape",
}
}
}
pub type AfterPassHook<'a> = Box<dyn FnMut(PipelineStage, &[TopLevel]) + 'a>;
pub enum TypecheckMode<'a> {
Full { base_dir: Option<&'a str> },
WithLoaded(&'a [LoadedModule]),
}
pub struct PipelineConfig<'a> {
pub run_tco: bool,
pub typecheck: Option<TypecheckMode<'a>>,
pub run_interp_lower: bool,
pub run_buffer_build: bool,
pub run_resolve: bool,
pub run_last_use: bool,
pub run_analyze: bool,
pub run_escape: bool,
pub alloc_policy: Option<&'a dyn AllocPolicy>,
pub call_ctx: Option<&'a dyn CallLowerCtx>,
pub on_after_pass: Option<AfterPassHook<'a>>,
}
impl<'a> Default for PipelineConfig<'a> {
fn default() -> Self {
Self {
run_tco: true,
typecheck: None,
run_interp_lower: true,
run_buffer_build: true,
run_resolve: true,
run_last_use: true,
run_analyze: true,
run_escape: true,
alloc_policy: None,
call_ctx: None,
on_after_pass: None,
}
}
}
#[derive(Debug, Clone)]
pub enum PassReport {
Tco {
tail_calls_added: usize,
fns_changed: Vec<FnCountChange>,
non_tail_recursive: Vec<NonTailEntry>,
},
Typecheck {
items_checked: usize,
errors: usize,
error_messages: Vec<String>,
},
InterpLower {
interpolations_lowered: usize,
fns_changed: Vec<FnCountChange>,
},
BufferBuild(BufferBuildPassReport),
Resolve {
slots_resolved: usize,
fns_with_slots: usize,
slot_types_total: usize,
slot_types_invalid: usize,
},
LastUse {
last_use_marked: usize,
total_resolved: usize,
},
Analyze {
total_fns: usize,
no_alloc_fns: usize,
recursive_fns: usize,
mutual_tco_members: usize,
unknown_alloc: usize,
},
Escape {
rewrites: usize,
},
}
#[derive(Debug, Clone)]
pub struct FnCountChange {
pub name: String,
pub before: usize,
pub after: usize,
}
#[derive(Debug, Clone)]
pub struct NonTailEntry {
pub fn_name: String,
pub recursive_calls: usize,
pub line: usize,
}
#[derive(Debug, Clone)]
pub struct PassDiagnostic {
pub stage: PipelineStage,
pub report: PassReport,
}
#[derive(Default)]
pub struct PipelineResult {
pub typecheck: Option<TypeCheckResult>,
pub buffer_build: Option<BufferBuildPassReport>,
pub analysis: Option<AnalysisResult>,
pub pass_diagnostics: Vec<PassDiagnostic>,
}
pub fn tco(items: &mut [TopLevel]) {
crate::tco::transform_program(items);
}
pub fn typecheck(items: &[TopLevel], mode: &TypecheckMode<'_>) -> TypeCheckResult {
match mode {
TypecheckMode::Full { base_dir } => run_type_check_full(items, *base_dir),
TypecheckMode::WithLoaded(loaded) => run_type_check_with_loaded(items, loaded),
}
}
pub fn interp_lower(items: &mut [TopLevel]) {
crate::ir::lower_interpolation_pass(items);
}
pub fn buffer_build(items: &mut Vec<TopLevel>) -> BufferBuildPassReport {
crate::ir::run_buffer_build_pass(items)
}
pub fn resolve(items: &mut [TopLevel]) {
crate::resolver::resolve_program(items);
}
pub fn last_use(items: &mut [TopLevel]) {
crate::ir::last_use::annotate_program_last_use(items);
}
pub fn run(items: &mut Vec<TopLevel>, mut cfg: PipelineConfig<'_>) -> PipelineResult {
let mut result = PipelineResult::default();
if cfg.run_tco {
let pre = pass_diag::collect(items);
tco(items);
let post = pass_diag::collect(items);
result
.pass_diagnostics
.push(diag_for_tco(&pre, &post, items));
fire(&mut cfg, PipelineStage::Tco, items);
}
if let Some(mode) = cfg.typecheck.as_ref() {
let tc = typecheck(items, mode);
let has_errors = !tc.errors.is_empty();
result
.pass_diagnostics
.push(diag_for_typecheck(&tc, items.len()));
result.typecheck = Some(tc);
fire(&mut cfg, PipelineStage::Typecheck, items);
if has_errors {
return result;
}
}
if cfg.run_interp_lower {
let pre = pass_diag::collect(items);
interp_lower(items);
let post = pass_diag::collect(items);
result
.pass_diagnostics
.push(diag_for_interp_lower(&pre, &post));
fire(&mut cfg, PipelineStage::InterpLower, items);
}
if cfg.run_buffer_build {
let report = buffer_build(items);
result.pass_diagnostics.push(diag_for_buffer_build(&report));
result.buffer_build = Some(report);
fire(&mut cfg, PipelineStage::BufferBuild, items);
}
if cfg.run_resolve {
resolve(items);
let post = pass_diag::collect(items);
result.pass_diagnostics.push(diag_for_resolve(&post, items));
fire(&mut cfg, PipelineStage::Resolve, items);
}
if cfg.run_analyze {
let adapter = CallCtxAdapter(cfg.call_ctx);
let analysis = crate::ir::analyze(items, cfg.alloc_policy, &adapter);
result.pass_diagnostics.push(diag_for_analyze(&analysis));
result.analysis = Some(analysis);
fire(&mut cfg, PipelineStage::Analyze, items);
}
if cfg.run_escape {
let rewrites = crate::ir::escape::run(items);
result.pass_diagnostics.push(diag_for_escape(rewrites));
fire(&mut cfg, PipelineStage::Escape, items);
}
if cfg.run_last_use {
last_use(items);
let post = pass_diag::collect(items);
result.pass_diagnostics.push(diag_for_last_use(&post));
fire(&mut cfg, PipelineStage::LastUse, items);
crate::ir::alias::annotate_program_alias_slots(items);
}
result
}
struct CallCtxAdapter<'a>(Option<&'a dyn CallLowerCtx>);
impl<'a> CallLowerCtx for CallCtxAdapter<'a> {
fn is_local_value(&self, name: &str) -> bool {
self.0.is_some_and(|c| c.is_local_value(name))
}
fn is_user_type(&self, name: &str) -> bool {
self.0.is_some_and(|c| c.is_user_type(name))
}
fn resolve_module_call<'b>(&self, dotted: &'b str) -> Option<(&'b str, &'b str)> {
self.0.and_then(|c| c.resolve_module_call(dotted))
}
}
fn fire(cfg: &mut PipelineConfig<'_>, stage: PipelineStage, items: &[TopLevel]) {
if let Some(cb) = cfg.on_after_pass.as_mut() {
cb(stage, items);
}
}
fn diag_for_tco(pre: &CountsByFn, post: &CountsByFn, items: &[TopLevel]) -> PassDiagnostic {
let pre_total = pass_diag::total(pre);
let post_total = pass_diag::total(post);
let tail_calls_added = post_total.tail_calls.saturating_sub(pre_total.tail_calls);
let fns_changed: Vec<FnCountChange> = pass_diag::fns_that_grew(pre, post, |c| c.tail_calls)
.into_iter()
.map(|name| {
let before = pre.get(&name).copied().unwrap_or_default().tail_calls;
let after = post.get(&name).copied().unwrap_or_default().tail_calls;
FnCountChange {
name,
before,
after,
}
})
.collect();
let non_tail_recursive: Vec<NonTailEntry> =
crate::tail_check::collect_non_tail_recursion_warnings(items)
.into_iter()
.map(|w| NonTailEntry {
fn_name: w.fn_name,
recursive_calls: w.recursive_calls,
line: w.line,
})
.collect();
PassDiagnostic {
stage: PipelineStage::Tco,
report: PassReport::Tco {
tail_calls_added,
fns_changed,
non_tail_recursive,
},
}
}
fn diag_for_typecheck(tc: &TypeCheckResult, item_count: usize) -> PassDiagnostic {
let error_messages = if tc.errors.is_empty() {
Vec::new()
} else {
tc.errors
.iter()
.take(5)
.map(|e| e.message.clone())
.collect()
};
PassDiagnostic {
stage: PipelineStage::Typecheck,
report: PassReport::Typecheck {
items_checked: item_count,
errors: tc.errors.len(),
error_messages,
},
}
}
fn diag_for_interp_lower(pre: &CountsByFn, post: &CountsByFn) -> PassDiagnostic {
let interpolations_lowered = pass_diag::total(pre)
.interpolations
.saturating_sub(pass_diag::total(post).interpolations);
let fns_changed: Vec<FnCountChange> = pass_diag::fns_that_grew(post, pre, |c| c.interpolations)
.into_iter()
.map(|name| {
let before = pre.get(&name).copied().unwrap_or_default().interpolations;
let after = post.get(&name).copied().unwrap_or_default().interpolations;
FnCountChange {
name,
before,
after,
}
})
.collect();
PassDiagnostic {
stage: PipelineStage::InterpLower,
report: PassReport::InterpLower {
interpolations_lowered,
fns_changed,
},
}
}
fn diag_for_buffer_build(report: &BufferBuildPassReport) -> PassDiagnostic {
PassDiagnostic {
stage: PipelineStage::BufferBuild,
report: PassReport::BufferBuild(report.clone()),
}
}
fn diag_for_resolve(post: &CountsByFn, items: &[TopLevel]) -> PassDiagnostic {
let slots_resolved = pass_diag::total(post).resolved;
let fns_with_slots = post.values().filter(|c| c.resolved > 0).count();
let mut slot_types_total = 0usize;
let mut slot_types_invalid = 0usize;
for item in items {
if let TopLevel::FnDef(fd) = item
&& let Some(res) = fd.resolution.as_ref()
{
slot_types_total += res.local_slot_types.len();
slot_types_invalid += res
.local_slot_types
.iter()
.filter(|t| matches!(t, crate::ast::Type::Invalid))
.count();
}
}
PassDiagnostic {
stage: PipelineStage::Resolve,
report: PassReport::Resolve {
slots_resolved,
fns_with_slots,
slot_types_total,
slot_types_invalid,
},
}
}
fn diag_for_last_use(post: &CountsByFn) -> PassDiagnostic {
let totals = pass_diag::total(post);
PassDiagnostic {
stage: PipelineStage::LastUse,
report: PassReport::LastUse {
last_use_marked: totals.last_use_resolved,
total_resolved: totals.resolved,
},
}
}
fn diag_for_escape(rewrites: usize) -> PassDiagnostic {
PassDiagnostic {
stage: PipelineStage::Escape,
report: PassReport::Escape { rewrites },
}
}
fn diag_for_analyze(analysis: &AnalysisResult) -> PassDiagnostic {
let total_fns = analysis.fn_analyses.len();
let no_alloc_fns = analysis
.fn_analyses
.values()
.filter(|fa| fa.allocates == Some(false))
.count();
let unknown_alloc = analysis
.fn_analyses
.values()
.filter(|fa| fa.allocates.is_none())
.count();
PassDiagnostic {
stage: PipelineStage::Analyze,
report: PassReport::Analyze {
total_fns,
no_alloc_fns,
recursive_fns: analysis.recursive_fns.len(),
mutual_tco_members: analysis.mutual_tco_members.len(),
unknown_alloc,
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::source::parse_source;
fn parse(src: &str) -> Vec<TopLevel> {
parse_source(src).expect("parse failed")
}
#[test]
fn default_config_fires_every_stage_in_order() {
let mut items = parse(
r#"
module M
intent = "test"
depends []
fn id(n: Int) -> Int
n
"#,
);
let mut fired: Vec<PipelineStage> = Vec::new();
run(
&mut items,
PipelineConfig {
typecheck: Some(TypecheckMode::Full { base_dir: None }),
on_after_pass: Some(Box::new(|stage, _| fired.push(stage))),
..Default::default()
},
);
assert_eq!(
fired,
vec![
PipelineStage::Tco,
PipelineStage::Typecheck,
PipelineStage::InterpLower,
PipelineStage::BufferBuild,
PipelineStage::Resolve,
PipelineStage::Analyze,
PipelineStage::Escape,
PipelineStage::LastUse,
]
);
}
#[test]
fn disabled_stages_dont_fire() {
let mut items = parse(
r#"
module M
intent = "test"
depends []
fn id(n: Int) -> Int
n
"#,
);
let mut fired: Vec<PipelineStage> = Vec::new();
run(
&mut items,
PipelineConfig {
typecheck: None,
run_interp_lower: false,
run_buffer_build: false,
run_last_use: false,
run_analyze: false,
run_escape: false,
on_after_pass: Some(Box::new(|stage, _| fired.push(stage))),
..Default::default()
},
);
assert_eq!(fired, vec![PipelineStage::Tco, PipelineStage::Resolve]);
}
#[test]
fn typecheck_errors_skip_later_stages() {
let mut items = parse(
r#"
module M
intent = "test"
depends []
fn broken() -> Int
undefined_thing
"#,
);
let mut fired: Vec<PipelineStage> = Vec::new();
let result = run(
&mut items,
PipelineConfig {
typecheck: Some(TypecheckMode::Full { base_dir: None }),
on_after_pass: Some(Box::new(|stage, _| fired.push(stage))),
..Default::default()
},
);
assert!(
!result.typecheck.unwrap().errors.is_empty(),
"typecheck must surface the undefined identifier"
);
assert_eq!(fired, vec![PipelineStage::Tco, PipelineStage::Typecheck]);
}
#[test]
fn analyze_populates_result_when_enabled() {
let mut items = parse(
r#"
module M
intent = "test"
depends []
fn id(n: Int) -> Int
n
fn dub(n: Int) -> Int
n + n
"#,
);
let result = run(
&mut items,
PipelineConfig {
typecheck: Some(TypecheckMode::Full { base_dir: None }),
..Default::default()
},
);
let analysis = result
.analysis
.expect("analyze runs by default and must populate result");
assert!(
analysis.fn_analyses.contains_key("id"),
"every user fn shows up in fn_analyses, got keys: {:?}",
analysis.fn_analyses.keys().collect::<Vec<_>>()
);
assert!(analysis.fn_analyses.contains_key("dub"));
}
#[test]
fn analyze_skipped_when_disabled() {
let mut items = parse(
r#"
module M
intent = "test"
depends []
fn id(n: Int) -> Int
n
"#,
);
let result = run(
&mut items,
PipelineConfig {
typecheck: Some(TypecheckMode::Full { base_dir: None }),
run_analyze: false,
..Default::default()
},
);
assert!(
result.analysis.is_none(),
"run_analyze=false must leave PipelineResult.analysis as None"
);
}
#[test]
fn alloc_policy_populates_per_fn_allocates() {
let mut items = parse(
r#"
module M
intent = "test"
depends []
fn pure_one() -> Int
1
fn allocates_list(n: Int) -> List<Int>
[n, n, n]
"#,
);
let policy = crate::ir::NeutralAllocPolicy;
let result = run(
&mut items,
PipelineConfig {
typecheck: Some(TypecheckMode::Full { base_dir: None }),
alloc_policy: Some(&policy),
..Default::default()
},
);
let analysis = result.analysis.expect("analyze ran");
assert_eq!(
analysis
.fn_analyses
.get("pure_one")
.and_then(|fa| fa.allocates),
Some(false),
"pure_one returns a literal — proven not to allocate"
);
assert_eq!(
analysis
.fn_analyses
.get("allocates_list")
.and_then(|fa| fa.allocates),
Some(true),
"list literal allocates under the neutral policy"
);
}
#[test]
fn analyze_without_policy_leaves_allocates_unset() {
let mut items = parse(
r#"
module M
intent = "test"
depends []
fn id(n: Int) -> Int
n
"#,
);
let result = run(
&mut items,
PipelineConfig {
typecheck: Some(TypecheckMode::Full { base_dir: None }),
..Default::default()
},
);
let analysis = result.analysis.expect("analyze ran");
let fa = analysis
.fn_analyses
.get("id")
.expect("id is in the analysis");
assert!(
fa.allocates.is_none(),
"without an alloc_policy, allocates stays None (every other field still set)"
);
}
#[test]
fn last_use_runs_only_after_resolve() {
let mut items = parse(
r#"
module M
intent = "test"
depends []
fn id(n: Int) -> Int
n
"#,
);
let mut fired: Vec<PipelineStage> = Vec::new();
run(
&mut items,
PipelineConfig {
typecheck: Some(TypecheckMode::Full { base_dir: None }),
run_resolve: false,
run_analyze: false,
on_after_pass: Some(Box::new(|stage, _| fired.push(stage))),
..Default::default()
},
);
assert_eq!(
fired,
vec![
PipelineStage::Tco,
PipelineStage::Typecheck,
PipelineStage::InterpLower,
PipelineStage::BufferBuild,
PipelineStage::Escape,
PipelineStage::LastUse, ]
);
}
#[test]
fn pass_diagnostics_recorded_for_each_stage_that_ran() {
let mut items = parse(
r#"
module M
intent = "test"
depends []
fn factorial(n: Int, acc: Int) -> Int
match n
0 -> acc
_ -> factorial(n - 1, acc * n)
"#,
);
let result = run(
&mut items,
PipelineConfig {
typecheck: Some(TypecheckMode::Full { base_dir: None }),
..Default::default()
},
);
let stages: Vec<PipelineStage> = result.pass_diagnostics.iter().map(|d| d.stage).collect();
assert_eq!(
stages,
vec![
PipelineStage::Tco,
PipelineStage::Typecheck,
PipelineStage::InterpLower,
PipelineStage::BufferBuild,
PipelineStage::Resolve,
PipelineStage::Analyze,
PipelineStage::Escape,
PipelineStage::LastUse,
]
);
let tco_diag = &result.pass_diagnostics[0];
match &tco_diag.report {
PassReport::Tco {
tail_calls_added,
fns_changed,
..
} => {
assert!(*tail_calls_added >= 1, "factorial got at least one TCO");
assert!(
fns_changed.iter().any(|c| c.name == "factorial"),
"fns_changed must list factorial: {fns_changed:?}"
);
}
other => panic!("expected Tco report, got {other:?}"),
}
let bb_diag = result
.pass_diagnostics
.iter()
.find(|d| d.stage == PipelineStage::BufferBuild)
.unwrap();
match &bb_diag.report {
PassReport::BufferBuild(r) => {
assert_eq!(r.rewrites, 0, "factorial-only program has no fusion sites")
}
other => panic!("expected BufferBuild report, got {other:?}"),
}
let resolve_diag = result
.pass_diagnostics
.iter()
.find(|d| d.stage == PipelineStage::Resolve)
.unwrap();
match &resolve_diag.report {
PassReport::Resolve { slots_resolved, .. } => assert!(
*slots_resolved > 0,
"factorial body resolves at least one ident"
),
other => panic!("expected Resolve report, got {other:?}"),
}
}
}