use crate::ast::TopLevel;
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,
}
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",
}
}
}
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 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,
alloc_policy: None,
call_ctx: None,
on_after_pass: None,
}
}
}
#[derive(Default)]
pub struct PipelineResult {
pub typecheck: Option<TypeCheckResult>,
pub buffer_build_stats: Option<(usize, usize)>,
pub analysis: Option<AnalysisResult>,
}
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>) -> (usize, usize) {
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 {
tco(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.typecheck = Some(tc);
fire(&mut cfg, PipelineStage::Typecheck, items);
if has_errors {
return result;
}
}
if cfg.run_interp_lower {
interp_lower(items);
fire(&mut cfg, PipelineStage::InterpLower, items);
}
if cfg.run_buffer_build {
result.buffer_build_stats = Some(buffer_build(items));
fire(&mut cfg, PipelineStage::BufferBuild, items);
}
if cfg.run_resolve {
resolve(items);
fire(&mut cfg, PipelineStage::Resolve, items);
}
if cfg.run_last_use {
last_use(items);
fire(&mut cfg, PipelineStage::LastUse, items);
}
if cfg.run_analyze {
let adapter = CallCtxAdapter(cfg.call_ctx);
result.analysis = Some(crate::ir::analyze(items, cfg.alloc_policy, &adapter));
fire(&mut cfg, PipelineStage::Analyze, 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);
}
}
#[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::LastUse,
PipelineStage::Analyze,
]
);
}
#[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,
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::LastUse, ]
);
}
}