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_full_self_host,
run_type_check_with_loaded, run_type_check_with_loaded_self_host,
};
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum PipelineStage {
Tco,
Typecheck,
InterpLower,
BufferBuild,
Resolve,
LastUse,
Analyze,
Escape,
RefinementLower,
IntervalAnalyze,
ContractLower,
LawLower,
BuildSymbols,
NameResolve,
}
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",
Self::RefinementLower => "refinement_lower",
Self::IntervalAnalyze => "interval_analyze",
Self::ContractLower => "contract_lower",
Self::LawLower => "law_lower",
Self::BuildSymbols => "build_symbols",
Self::NameResolve => "name_resolve",
}
}
}
pub type AfterPassHook<'a> = Box<dyn FnMut(PipelineStage, &[TopLevel]) + 'a>;
pub enum TypecheckMode<'a> {
Full { base_dir: Option<&'a str> },
WithLoaded(&'a [LoadedModule]),
FullSelfHost { base_dir: Option<&'a str> },
WithLoadedSelfHost(&'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 run_refinement_lower: bool,
pub run_interval_analyze: bool,
pub run_contract_lower: bool,
pub run_law_lower: bool,
pub run_build_symbols: bool,
pub dep_modules: &'a [crate::codegen::ModuleInfo],
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,
run_refinement_lower: false,
run_interval_analyze: false,
run_contract_lower: false,
run_law_lower: false,
run_build_symbols: false,
dep_modules: &[],
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,
},
RefinementLower {
refined_types: usize,
},
IntervalAnalyze {
types_analyzed: usize,
two_sided_bounded: usize,
ops_overflow_free: usize,
ops_needs_wider: usize,
ops_unbounded: usize,
raw_i64_eligible: usize,
},
ContractLower {
fn_contracts: usize,
},
LawLower {
law_theorems: usize,
},
BuildSymbols {
fns: usize,
types: usize,
ctors: usize,
modules: Vec<BuildSymbolsModule>,
fn_name_collisions: usize,
type_name_collisions: usize,
},
NameResolve {
promoted_fns: usize,
passthrough_items: usize,
unresolved_count: usize,
},
}
#[derive(Debug, Clone)]
pub struct BuildSymbolsModule {
pub prefix: String,
pub fns: usize,
pub types: usize,
pub ctors: 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 proof_ir: Option<crate::ir::ProofIR>,
pub interval_analysis: Option<crate::ir::IntervalAnalysisResult>,
pub symbol_table: crate::ir::SymbolTable,
pub resolved_items: Vec<crate::ir::hir::ResolvedTopLevel>,
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),
TypecheckMode::FullSelfHost { base_dir } => run_type_check_full_self_host(items, *base_dir),
TypecheckMode::WithLoadedSelfHost(loaded) => {
run_type_check_with_loaded_self_host(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);
}
{
let symbol_table = crate::ir::SymbolTable::build(items, cfg.dep_modules);
result
.pass_diagnostics
.push(diag_for_build_symbols(&symbol_table));
result.symbol_table = symbol_table;
if cfg.run_build_symbols {
fire(&mut cfg, PipelineStage::BuildSymbols, items);
}
}
{
let resolved = crate::ir::hir::resolve_program(&result.symbol_table, items);
result
.pass_diagnostics
.push(diag_for_name_resolve(&resolved));
result.resolved_items = resolved;
fire(&mut cfg, PipelineStage::NameResolve, items);
}
if cfg.run_refinement_lower
|| cfg.run_interval_analyze
|| cfg.run_contract_lower
|| cfg.run_law_lower
{
let symbols = &result.symbol_table;
let recursive_fns_owned: std::collections::HashSet<crate::ir::FnId> = {
let mut set = std::collections::HashSet::new();
if let Some(a) = result.analysis.as_ref() {
set.extend(crate::codegen::scc::analysis_set_to_fn_ids(
&a.recursive_fns,
symbols,
None,
));
}
for m in cfg.dep_modules {
if let Some(a) = m.analysis.as_ref() {
set.extend(crate::codegen::scc::analysis_set_to_fn_ids(
&a.recursive_fns,
symbols,
Some(&m.prefix),
));
}
}
set
};
let module_prefixes: std::collections::HashSet<String> =
cfg.dep_modules.iter().map(|m| m.prefix.clone()).collect();
let entry_resolved_fns: Vec<&crate::ir::hir::ResolvedFnDef> = result
.resolved_items
.iter()
.filter_map(|t| match t {
crate::ir::hir::ResolvedTopLevel::FnDef(fd) => Some(fd),
_ => None,
})
.collect();
let program_shape = crate::analysis::shape::analyze_program_with_modules(
&entry_resolved_fns,
items,
cfg.dep_modules,
);
let inputs = crate::codegen::proof_lower::ProofLowerInputs {
entry_items: items,
dep_modules: cfg.dep_modules,
module_prefixes: &module_prefixes,
recursive_fns: &recursive_fns_owned,
symbol_table: symbols,
program_shape: Some(&program_shape),
};
let mut ir = result.proof_ir.take().unwrap_or_default();
if cfg.run_refinement_lower {
crate::codegen::proof_lower::populate_refined_types(&inputs, &mut ir);
result.pass_diagnostics.push(diag_for_refinement_lower(&ir));
fire(&mut cfg, PipelineStage::RefinementLower, items);
}
if cfg.run_interval_analyze {
let analysis = crate::ir::interval::analyze(&ir.refined_types, &inputs);
result
.pass_diagnostics
.push(diag_for_interval_analyze(&analysis));
result.interval_analysis = Some(analysis);
fire(&mut cfg, PipelineStage::IntervalAnalyze, items);
}
if cfg.run_contract_lower {
crate::codegen::proof_lower::populate_fn_contracts(&inputs, &mut ir);
result.pass_diagnostics.push(diag_for_contract_lower(&ir));
fire(&mut cfg, PipelineStage::ContractLower, items);
}
if cfg.run_law_lower {
crate::codegen::proof_lower::populate_law_theorems(&inputs, &mut ir);
result.pass_diagnostics.push(diag_for_law_lower(&ir));
fire(&mut cfg, PipelineStage::LawLower, items);
}
result.proof_ir = Some(ir);
}
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_refinement_lower(ir: &crate::ir::ProofIR) -> PassDiagnostic {
PassDiagnostic {
stage: PipelineStage::RefinementLower,
report: PassReport::RefinementLower {
refined_types: ir.refined_types.len(),
},
}
}
fn diag_for_interval_analyze(analysis: &crate::ir::IntervalAnalysisResult) -> PassDiagnostic {
PassDiagnostic {
stage: PipelineStage::IntervalAnalyze,
report: PassReport::IntervalAnalyze {
types_analyzed: analysis.types_analyzed(),
two_sided_bounded: analysis.two_sided_bounded(),
ops_overflow_free: analysis.ops_overflow_free(),
ops_needs_wider: analysis.ops_needs_wider(),
ops_unbounded: analysis.ops_unbounded(),
raw_i64_eligible: analysis.raw_i64_eligible(),
},
}
}
fn diag_for_contract_lower(ir: &crate::ir::ProofIR) -> PassDiagnostic {
PassDiagnostic {
stage: PipelineStage::ContractLower,
report: PassReport::ContractLower {
fn_contracts: ir.fn_contracts.len(),
},
}
}
fn diag_for_law_lower(ir: &crate::ir::ProofIR) -> PassDiagnostic {
PassDiagnostic {
stage: PipelineStage::LawLower,
report: PassReport::LawLower {
law_theorems: ir.law_theorems.len(),
},
}
}
fn diag_for_build_symbols(table: &crate::ir::SymbolTable) -> PassDiagnostic {
let mut per_module: Vec<BuildSymbolsModule> = table
.modules
.iter()
.map(|m| BuildSymbolsModule {
prefix: m.prefix.clone().unwrap_or_default(),
fns: 0,
types: 0,
ctors: 0,
})
.collect();
for fe in &table.fns {
per_module[fe.module.0 as usize].fns += 1;
}
for te in &table.types {
per_module[te.module.0 as usize].types += 1;
}
for ce in &table.ctors {
let owning_module = table.types[ce.owning_type.0 as usize].module;
per_module[owning_module.0 as usize].ctors += 1;
}
use std::collections::HashMap;
let mut fn_buckets: HashMap<&str, usize> = HashMap::new();
for fe in &table.fns {
*fn_buckets.entry(fe.key.name.as_str()).or_default() += 1;
}
let fn_name_collisions = fn_buckets.values().filter(|c| **c >= 2).count();
let mut type_buckets: HashMap<&str, usize> = HashMap::new();
for te in &table.types {
*type_buckets.entry(te.key.name.as_str()).or_default() += 1;
}
let type_name_collisions = type_buckets.values().filter(|c| **c >= 2).count();
PassDiagnostic {
stage: PipelineStage::BuildSymbols,
report: PassReport::BuildSymbols {
fns: table.fns.len(),
types: table.types.len(),
ctors: table.ctors.len(),
modules: per_module,
fn_name_collisions,
type_name_collisions,
},
}
}
fn diag_for_name_resolve(items: &[crate::ir::hir::ResolvedTopLevel]) -> PassDiagnostic {
use crate::ir::hir::ResolvedTopLevel;
let mut promoted_fns = 0;
let mut passthrough_items = 0;
let mut unresolved_count = 0;
for item in items {
match item {
ResolvedTopLevel::FnDef(fd) => {
promoted_fns += 1;
unresolved_count += crate::ir::hir::count_unresolved_in_fn(fd);
}
ResolvedTopLevel::Passthrough(_) => passthrough_items += 1,
ResolvedTopLevel::Module(_) => {}
}
}
PassDiagnostic {
stage: PipelineStage::NameResolve,
report: PassReport::NameResolve {
promoted_fns,
passthrough_items,
unresolved_count,
},
}
}
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,
PipelineStage::NameResolve,
]
);
}
#[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,
PipelineStage::NameResolve,
]
);
}
#[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, PipelineStage::NameResolve,
]
);
}
#[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,
PipelineStage::BuildSymbols,
PipelineStage::NameResolve,
]
);
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:?}"),
}
}
#[test]
fn name_resolve_runs_unconditionally() {
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()
},
);
assert!(
!result.resolved_items.is_empty(),
"NameResolve runs unconditionally; resolved_items must be populated"
);
let saw_name_resolve = result
.pass_diagnostics
.iter()
.any(|d| d.stage == PipelineStage::NameResolve);
assert!(
saw_name_resolve,
"NameResolve stage must always fire its diagnostic"
);
}
#[test]
fn name_resolve_populates_resolved_items() {
let mut items = parse(
r#"
module M
intent = "test"
depends []
fn helper(n: Int) -> Int
n + 1
fn main() -> Int
helper(7)
"#,
);
let result = run(
&mut items,
PipelineConfig {
typecheck: Some(TypecheckMode::Full { base_dir: None }),
..Default::default()
},
);
let resolved = result.resolved_items;
use crate::ir::hir::ResolvedTopLevel;
let promoted = resolved
.iter()
.filter(|t| matches!(t, ResolvedTopLevel::FnDef(_)))
.count();
let passthrough = resolved
.iter()
.filter(|t| matches!(t, ResolvedTopLevel::Passthrough(_)))
.count();
assert_eq!(promoted, 2, "helper + main should both promote");
assert_eq!(passthrough, 0, "no non-fn items in this fixture");
let diag = result
.pass_diagnostics
.iter()
.find(|d| d.stage == PipelineStage::NameResolve)
.expect("NameResolve diagnostic missing");
match &diag.report {
PassReport::NameResolve {
promoted_fns,
passthrough_items,
unresolved_count,
} => {
assert_eq!(*promoted_fns, 2);
assert_eq!(*passthrough_items, 0);
assert_eq!(
*unresolved_count, 0,
"well-typed program must produce zero unresolved nodes"
);
}
other => panic!("expected NameResolve report, got {other:?}"),
}
}
#[test]
fn name_resolve_invariant_zero_unresolved_for_well_typed_programs() {
let fixtures: &[&str] = &[
r#"
fn count(n: Int, acc: Int) -> Int
match n
0 -> acc
_ -> count(n - 1, acc + Int.abs(-1))
fn main() -> Int
x: Int = count(5, 0)
x
"#,
r#"
type Shape
Circle(Float)
Square(Float)
fn area(s: Shape) -> Float
match s
Shape.Circle(r) -> r * r
Shape.Square(s) -> s * s
fn main() -> Float
area(Shape.Circle(3.0))
"#,
r#"
record Point
x: Int
y: Int
fn origin() -> Point
Point(x = 0, y = 0)
fn shift(p: Point) -> Result<Point, String>
Result.Ok(Point.update(p, x = p.x + 1))
"#,
r#"
fn greet(n: Int) -> String
"hi ${n}!"
fn main() -> String
greet(7)
"#,
];
for (i, src) in fixtures.iter().enumerate() {
let mut items = parse(src);
let result = run(
&mut items,
PipelineConfig {
typecheck: Some(TypecheckMode::Full { base_dir: None }),
..Default::default()
},
);
if let Some(tc) = result.typecheck.as_ref() {
assert!(
tc.errors.is_empty(),
"fixture #{i} did not typecheck: {:?}",
tc.errors
);
}
let diag = result
.pass_diagnostics
.iter()
.find(|d| d.stage == PipelineStage::NameResolve)
.expect("NameResolve diagnostic missing");
match &diag.report {
PassReport::NameResolve {
unresolved_count, ..
} => {
assert_eq!(
*unresolved_count, 0,
"fixture #{i}: well-typed program produced {} unresolved nodes",
*unresolved_count
);
}
other => panic!("expected NameResolve report, got {other:?}"),
}
}
}
#[test]
fn name_resolve_runs_after_build_symbols() {
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_build_symbols: true,
on_after_pass: Some(Box::new(|stage, _| fired.push(stage))),
..Default::default()
},
);
let build_pos = fired
.iter()
.position(|s| *s == PipelineStage::BuildSymbols)
.expect("BuildSymbols fired");
let resolve_pos = fired
.iter()
.position(|s| *s == PipelineStage::NameResolve)
.expect("NameResolve fired");
assert!(
resolve_pos > build_pos,
"NameResolve must fire after BuildSymbols (saw {fired:?})"
);
}
}