diff --git a/Cargo.toml b/Cargo.toml index 8e90f78a..8f78b108 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ pharmsol-dsl = { path = "pharmsol-dsl", version = "0.28.2" } pharmsol-macros = { path = "pharmsol-macros", version = "0.28.2" } serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" +thiserror = "2.0.18" [package] name = "pharmsol" @@ -52,7 +53,7 @@ cranelift-module = { version = "0.131.0", optional = true } cranelift-native = { version = "0.131.0", optional = true } serde = { workspace = true } serde_json = { workspace = true } -thiserror = "2.0.18" +thiserror = { workspace = true } tracing = "0.1.44" wasm-encoder = { version = "0.247.0", optional = true } diff --git a/pharmsol-dsl/Cargo.toml b/pharmsol-dsl/Cargo.toml index 78fd3586..d1cc9d2b 100644 --- a/pharmsol-dsl/Cargo.toml +++ b/pharmsol-dsl/Cargo.toml @@ -15,3 +15,4 @@ bench = false [dependencies] serde = { workspace = true } serde_json = { workspace = true } +thiserror = { workspace = true } diff --git a/pharmsol-dsl/README.md b/pharmsol-dsl/README.md index 0280727b..363bbb05 100644 --- a/pharmsol-dsl/README.md +++ b/pharmsol-dsl/README.md @@ -1,37 +1,23 @@ # pharmsol-dsl -`pharmsol-dsl` is the backend-neutral frontend crate for the pharmsol DSL. +`pharmsol-dsl` compiles pharmsol DSL model source into a ready-to-run form. Use this crate when you need to work with model source as data: - parse DSL text into syntax nodes - inspect spans and diagnostics -- analyze names and types into typed IR -- lower validated models into the execution model used by runtime backends +- analyze names and types into a checked model +- compile validated models into the ready-to-run form used by runtime backends Do not use this crate for JIT compilation, native AoT export or load, WASM runtime loading, or `Subject`-based prediction helpers. Those workflows stay in `pharmsol::dsl` in the main `pharmsol` crate. ## Main Pipeline -The public pipeline is: - -1. `parse_model` or `parse_module` -2. `analyze_model` or `analyze_module` -3. `lower_typed_model` or `lower_typed_module` - -The main public modules are: - -- `ast` for syntax-level nodes -- `diagnostic` for spans, codes, and rendered reports -- `ir` for the typed intermediate representation -- `execution` for the lowered execution model shared by JIT, AoT, and WASM backends - -The parser accepts both canonical `model { ... }` source and the authoring shorthand used by the `pharmsol` examples. - -## Small Example +The one-shot pipeline is `compile_model` or `compile_module`, which fails with +the unified `DslError`: ```rust -use pharmsol_dsl::{analyze_model, lower_typed_model, parse_model}; +use pharmsol_dsl::compile_model; let source = r#" name = bimodal_ke @@ -47,19 +33,50 @@ dx(central) = -ke * central out(cp) = central / v "#; -let syntax = parse_model(source).expect("model parses"); -let typed = analyze_model(&syntax).expect("model analyzes"); -let execution = lower_typed_model(&typed).expect("model lowers"); +let execution = compile_model(source).expect("model compiles"); assert_eq!(execution.name, "bimodal_ke"); assert_eq!(execution.metadata.routes.len(), 1); assert_eq!(execution.metadata.outputs.len(), 1); ``` +The staged pipeline is available when you need the intermediate representations: + +1. `parse_model` or `parse_module` +2. `analyze_model` or `analyze_module` +3. `compile_analyzed_model` or `compile_analyzed_module` + +The main public modules are: + +- `syntax` for the syntax tree +- `diagnostic` for spans, codes, and rendered reports +- `analysis` for the analyzed, fully checked model +- `execution` for the ready-to-run model shared by JIT, AoT, and WASM backends + +The parser accepts both canonical `model { ... }` source and the authoring +shorthand used by the `pharmsol` examples. + +## Errors + +Every stage reports errors with source spans and renders an annotated report +when printed: + +```text +error[DSL2000]: unknown identifier `missing_state` + --> line 3, column 11 + | +3 | out(cp) = missing_state + | ^^^^^^^^^^^^^ unknown identifier `missing_state` +``` + +`DslError::phase` identifies the failing stage, `DslError::diagnostics` +exposes the structured diagnostics, and `DslError::diagnostic_report` produces +a JSON-serializable report for editors and tooling. + ## Boundary With `pharmsol` -`pharmsol-dsl` owns the frontend pipeline and its data structures. +`pharmsol-dsl` owns the source-to-execution compiler and its data structures. -`pharmsol::dsl` re-exports that frontend surface and adds the runtime-facing APIs for backend selection, artifact loading, and prediction execution. +`pharmsol::dsl` re-exports that compiler surface and adds the runtime-facing APIs for backend selection, artifact loading, and prediction execution. Use `pharmsol-dsl` when you are building tooling, validation, migration, or your own backend. Use `pharmsol::dsl` when you want a complete source-to-runtime workflow. diff --git a/pharmsol-dsl/browser-compile-bridge/src/lib.rs b/pharmsol-dsl/browser-compile-bridge/src/lib.rs index 466e4a07..96da392d 100644 --- a/pharmsol-dsl/browser-compile-bridge/src/lib.rs +++ b/pharmsol-dsl/browser-compile-bridge/src/lib.rs @@ -110,7 +110,7 @@ pub unsafe extern "C" fn compile_model( "metadata": { "abiVersion": compiled.metadata.abi_version, "model": compiled.metadata.model, - "kernels": compiled.metadata.kernels, + "functions": compiled.metadata.functions, } })); 0 diff --git a/pharmsol-dsl/src/ir.rs b/pharmsol-dsl/src/analysis.rs similarity index 85% rename from pharmsol-dsl/src/ir.rs rename to pharmsol-dsl/src/analysis.rs index 8909247f..a0274e01 100644 --- a/pharmsol-dsl/src/ir.rs +++ b/pharmsol-dsl/src/analysis.rs @@ -1,5 +1,9 @@ -use std::error::Error; -use std::fmt; +//! The analyzed model: a syntax tree with names, types, and structure +//! checked and resolved. +//! +//! Produced by [`analyze_model`](crate::analyze_model) or +//! [`analyze_module`](crate::analyze_module), and compiled into a +//! ready-to-run form by [`compile_analyzed_model`](crate::compile_analyzed_model). use serde::{Deserialize, Serialize}; @@ -11,31 +15,31 @@ use crate::{ModelKind, RouteKind, Span}; pub type SymbolId = usize; #[derive(Debug, Clone, PartialEq)] -pub struct TypedModule { - pub models: Vec, +pub struct AnalyzedModule { + pub models: Vec, pub span: Span, } #[derive(Debug, Clone, PartialEq)] -pub struct TypedModel { +pub struct AnalyzedModel { pub name: String, pub kind: ModelKind, pub symbols: Vec, pub parameters: Vec, - pub constants: Vec, - pub covariates: Vec, - pub states: Vec, - pub routes: Vec, + pub constants: Vec, + pub covariates: Vec, + pub states: Vec, + pub routes: Vec, pub derived: Vec, pub outputs: Vec, pub particles: Option, - pub analytical: Option, - pub derive: Option, - pub dynamics: Option, - pub outputs_block: TypedStatementBlock, - pub init: Option, - pub drift: Option, - pub diffusion: Option, + pub analytical: Option, + pub derive: Option, + pub dynamics: Option, + pub outputs_block: AnalyzedStatementBlock, + pub init: Option, + pub drift: Option, + pub diffusion: Option, pub span: Span, } @@ -105,6 +109,9 @@ impl ConstValue { } } + /// Converts to `i64` when the value is finite, integral, and exactly + /// representable; the 2^63 upper bound is exclusive because + /// `i64::MAX as f64` rounds up to it and would saturate the cast. pub fn as_i64(&self) -> Option { match self { ConstValue::Int(value) => Some(*value), @@ -112,7 +119,7 @@ impl ConstValue { if value.is_finite() && value.fract() == 0.0 && *value >= i64::MIN as f64 - && *value <= i64::MAX as f64 => + && *value < -(i64::MIN as f64) => { Some(*value as i64) } @@ -122,7 +129,7 @@ impl ConstValue { } #[derive(Debug, Clone, PartialEq)] -pub struct TypedConstant { +pub struct AnalyzedConstant { pub symbol: SymbolId, pub value: ConstValue, pub span: Span, @@ -135,32 +142,32 @@ pub enum CovariateInterpolation { } #[derive(Debug, Clone, PartialEq)] -pub struct TypedCovariate { +pub struct AnalyzedCovariate { pub symbol: SymbolId, pub interpolation: Option, pub span: Span, } #[derive(Debug, Clone, PartialEq)] -pub struct TypedState { +pub struct AnalyzedState { pub symbol: SymbolId, pub size: Option, pub span: Span, } #[derive(Debug, Clone, PartialEq)] -pub struct TypedRoute { +pub struct AnalyzedRoute { pub symbol: SymbolId, pub kind: Option, - pub destination: TypedStatePlace, - pub properties: Vec, + pub destination: AnalyzedStatePlace, + pub properties: Vec, pub span: Span, } #[derive(Debug, Clone, PartialEq)] -pub struct TypedRouteProperty { +pub struct AnalyzedRouteProperty { pub kind: RoutePropertyKind, - pub value: TypedExpr, + pub value: AnalyzedExpr, pub span: Span, } @@ -171,7 +178,7 @@ pub enum RoutePropertyKind { } #[derive(Debug, Clone, PartialEq)] -pub struct TypedAnalytical { +pub struct AnalyticalSpec { pub structure: AnalyticalKernel, pub span: Span, } @@ -297,7 +304,7 @@ pub struct AnalyticalStructureInputPlan { impl AnalyticalStructureInputPlan { pub fn for_kernel( - kernel: AnalyticalKernel, + function: AnalyticalKernel, primary_names: P, derived_names: D, ) -> Result @@ -335,8 +342,8 @@ impl AnalyticalStructureInputPlan { } } - let mut bindings = Vec::with_capacity(kernel.required_parameter_count()); - for required_name in kernel.required_parameter_names() { + let mut bindings = Vec::with_capacity(function.required_parameter_count()); + for required_name in function.required_parameter_names() { match ( primary_index_by_name.get(required_name).copied(), derived_index_by_name.get(required_name).copied(), @@ -351,7 +358,7 @@ impl AnalyticalStructureInputPlan { }), (None, None) => { return Err(AnalyticalStructureInputError::MissingRequiredName { - structure: kernel.name(), + structure: function.name(), name: (*required_name).to_string(), suggestion: best_similar_candidate( required_name, @@ -414,17 +421,18 @@ impl AnalyticalStructureInputPlan { } } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] pub enum AnalyticalStructureInputError { - DuplicatePrimary { - name: String, - }, - DuplicateDerived { - name: String, - }, - ConflictingName { - name: String, - }, + #[error("duplicate primary parameter `{name}`")] + DuplicatePrimary { name: String }, + #[error("duplicate derived parameter `{name}`")] + DuplicateDerived { name: String }, + #[error("`{name}` is declared in both `params` and `derived`")] + ConflictingName { name: String }, + #[error( + "analytical structure `{structure}` requires `{name}`; {}declare it in `params` or `derived`", + suggestion_note(.name, .suggestion) + )] MissingRequiredName { structure: &'static str, name: String, @@ -432,31 +440,13 @@ pub enum AnalyticalStructureInputError { }, } -impl fmt::Display for AnalyticalStructureInputError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::DuplicatePrimary { name } => write!(f, "duplicate primary parameter `{name}`"), - Self::DuplicateDerived { name } => write!(f, "duplicate derived parameter `{name}`"), - Self::ConflictingName { name } => { - write!(f, "`{name}` is declared in both `params` and `derived`") - } - Self::MissingRequiredName { - structure, - name, - suggestion, - } => { - write!(f, "analytical structure `{structure}` requires `{name}`; ")?; - if let Some(candidate) = suggestion { - write!(f, "did you mean `{name}` instead of `{candidate}`? ")?; - } - f.write_str("declare it in `params` or `derived`") - } - } +fn suggestion_note(name: &str, suggestion: &Option) -> String { + match suggestion { + Some(candidate) => format!("did you mean `{name}` instead of `{candidate}`? "), + None => String::new(), } } -impl Error for AnalyticalStructureInputError {} - fn best_similar_candidate<'a, I>(needle: &str, candidates: I) -> Option where I: IntoIterator, @@ -534,118 +524,118 @@ pub enum BlockContext { } #[derive(Debug, Clone, PartialEq)] -pub struct TypedStatementBlock { +pub struct AnalyzedStatementBlock { pub context: BlockContext, - pub statements: Vec, + pub statements: Vec, pub span: Span, } #[derive(Debug, Clone, PartialEq)] -pub struct TypedStmt { - pub kind: TypedStmtKind, +pub struct AnalyzedStmt { + pub kind: AnalyzedStmtKind, pub span: Span, } #[derive(Debug, Clone, PartialEq)] -pub enum TypedStmtKind { - Let(TypedLetStmt), - Assign(TypedAssignStmt), - If(TypedIfStmt), - For(TypedForStmt), +pub enum AnalyzedStmtKind { + Let(AnalyzedLetStmt), + Assign(AnalyzedAssignStmt), + If(AnalyzedIfStmt), + For(AnalyzedForStmt), } #[derive(Debug, Clone, PartialEq)] -pub struct TypedLetStmt { +pub struct AnalyzedLetStmt { pub symbol: SymbolId, - pub value: TypedExpr, + pub value: AnalyzedExpr, } #[derive(Debug, Clone, PartialEq)] -pub struct TypedAssignStmt { - pub target: TypedAssignTarget, - pub value: TypedExpr, +pub struct AnalyzedAssignStmt { + pub target: AnalyzedAssignTarget, + pub value: AnalyzedExpr, } #[derive(Debug, Clone, PartialEq)] -pub struct TypedIfStmt { - pub condition: TypedExpr, - pub then_branch: Vec, - pub else_branch: Option>, +pub struct AnalyzedIfStmt { + pub condition: AnalyzedExpr, + pub then_branch: Vec, + pub else_branch: Option>, } #[derive(Debug, Clone, PartialEq)] -pub struct TypedForStmt { +pub struct AnalyzedForStmt { pub binding: SymbolId, - pub range: TypedRangeExpr, - pub body: Vec, + pub range: AnalyzedRangeExpr, + pub body: Vec, } #[derive(Debug, Clone, PartialEq)] -pub struct TypedRangeExpr { - pub start: TypedExpr, - pub end: TypedExpr, +pub struct AnalyzedRangeExpr { + pub start: AnalyzedExpr, + pub end: AnalyzedExpr, pub span: Span, } #[derive(Debug, Clone, PartialEq)] -pub struct TypedAssignTarget { - pub kind: TypedAssignTargetKind, +pub struct AnalyzedAssignTarget { + pub kind: AnalyzedAssignTargetKind, pub span: Span, } #[derive(Debug, Clone, PartialEq)] -pub enum TypedAssignTargetKind { +pub enum AnalyzedAssignTargetKind { Derived(SymbolId), Output(SymbolId), - StateInit(TypedStatePlace), - Derivative(TypedStatePlace), - Noise(TypedStatePlace), + StateInit(AnalyzedStatePlace), + Derivative(AnalyzedStatePlace), + Noise(AnalyzedStatePlace), } #[derive(Debug, Clone, PartialEq)] -pub struct TypedStatePlace { +pub struct AnalyzedStatePlace { pub state: SymbolId, - pub index: Option>, + pub index: Option>, pub span: Span, } #[derive(Debug, Clone, PartialEq)] -pub struct TypedExpr { - pub kind: TypedExprKind, +pub struct AnalyzedExpr { + pub kind: AnalyzedExprKind, pub ty: ValueType, pub constant: Option, pub span: Span, } #[derive(Debug, Clone, PartialEq)] -pub enum TypedExprKind { +pub enum AnalyzedExprKind { Literal(ConstValue), Symbol(SymbolId), - StateValue(TypedStatePlace), + StateValue(AnalyzedStatePlace), Unary { - op: TypedUnaryOp, - expr: Box, + op: AnalyzedUnaryOp, + expr: Box, }, Binary { - op: TypedBinaryOp, - lhs: Box, - rhs: Box, + op: AnalyzedBinaryOp, + lhs: Box, + rhs: Box, }, Call { - callee: TypedCall, - args: Vec, + callee: AnalyzedCall, + args: Vec, }, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum TypedUnaryOp { +pub enum AnalyzedUnaryOp { Plus, Minus, Not, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum TypedBinaryOp { +pub enum AnalyzedBinaryOp { Or, And, Eq, @@ -662,13 +652,13 @@ pub enum TypedBinaryOp { } #[derive(Debug, Clone, PartialEq)] -pub enum TypedCall { - Math(MathIntrinsic), +pub enum AnalyzedCall { + Math(MathFunction), Rate(SymbolId), } #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum MathIntrinsic { +pub enum MathFunction { Abs, Ceil, Exp, @@ -687,7 +677,7 @@ pub enum MathIntrinsic { Sqrt, } -impl MathIntrinsic { +impl MathFunction { pub const ALL: [Self; 16] = [ Self::Abs, Self::Ceil, @@ -750,16 +740,16 @@ impl MathIntrinsic { } } - pub fn arity(self) -> IntrinsicArity { + pub fn argument_count(self) -> ArgumentCount { match self { - Self::Max | Self::Min | Self::Pow => IntrinsicArity::Exact(2), - _ => IntrinsicArity::Exact(1), + Self::Max | Self::Min | Self::Pow => ArgumentCount::Exact(2), + _ => ArgumentCount::Exact(1), } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum IntrinsicArity { +pub enum ArgumentCount { Exact(usize), } @@ -814,10 +804,10 @@ mod tests { ), ]; - for (kernel, expected) in cases { - assert_eq!(kernel.required_parameter_names(), expected); - assert_eq!(kernel.required_parameter_count(), expected.len()); - assert_eq!(AnalyticalKernel::from_name(kernel.name()), Some(kernel)); + for (function, expected) in cases { + assert_eq!(function.required_parameter_names(), expected); + assert_eq!(function.required_parameter_count(), expected.len()); + assert_eq!(AnalyticalKernel::from_name(function.name()), Some(function)); } } diff --git a/pharmsol-dsl/src/semantic.rs b/pharmsol-dsl/src/analyze.rs similarity index 83% rename from pharmsol-dsl/src/semantic.rs rename to pharmsol-dsl/src/analyze.rs index 981db69d..e2c52f9f 100644 --- a/pharmsol-dsl/src/semantic.rs +++ b/pharmsol-dsl/src/analyze.rs @@ -2,16 +2,18 @@ use std::collections::{BTreeMap, BTreeSet}; use std::fmt; use std::sync::Arc; -use crate::ast as syntax; +use crate::analysis::*; use crate::diagnostic::{ Applicability, Diagnostic, DiagnosticPhase, DiagnosticReport, DiagnosticSuggestion, Span, - TextEdit, DSL_SEMANTIC_GENERIC, + TextEdit, DSL_ANALYSIS_GENERIC, }; -use crate::ir::*; use crate::name_match::{ common_prefix_len, edit_distance, is_high_confidence_match, is_single_adjacent_transposition, }; -use crate::{ModelKind, NUMERIC_OUTPUT_PREFIX, NUMERIC_ROUTE_PREFIX, RATE_FUNCTION_NAME}; +use crate::syntax; +use crate::{ + ModelKind, MAX_CONST_USIZE, NUMERIC_OUTPUT_PREFIX, NUMERIC_ROUTE_PREFIX, RATE_FUNCTION_NAME, +}; const RESERVED_NAMES: &[&str] = &[ "abs", @@ -41,14 +43,14 @@ const RESERVED_NAMES: &[&str] = &[ ]; #[derive(Default)] -struct SemanticAssist { +struct AnalysisAssist { context_labels: Vec<(Span, String)>, secondary_labels: Vec<(Span, String)>, helps: Vec, suggestions: Vec, } -impl SemanticAssist { +impl AnalysisAssist { fn context_label(mut self, span: Span, message: impl Into) -> Self { self.context_labels.push((span, message.into())); self @@ -77,7 +79,7 @@ impl SemanticAssist { self } - fn apply(self, mut error: SemanticError) -> SemanticError { + fn apply(self, mut error: AnalysisError) -> AnalysisError { for (span, message) in self.context_labels { error = error.with_context_label(span, message); } @@ -96,11 +98,11 @@ impl SemanticAssist { struct SimilarNameCandidate { lookup_name: String, - assist: SemanticAssist, + assist: AnalysisAssist, } impl SimilarNameCandidate { - fn new(lookup_name: impl Into, assist: SemanticAssist) -> Self { + fn new(lookup_name: impl Into, assist: AnalysisAssist) -> Self { Self { lookup_name: lookup_name.into(), assist, @@ -108,33 +110,41 @@ impl SimilarNameCandidate { } } -pub fn analyze_module(module: &syntax::Module) -> Result { +/// Checks every model in a parsed module and resolves all names and types. +/// +/// This is the middle pipeline stage: after [`parse_module`](crate::parse_module), +/// before [`compile_analyzed_module`](crate::compile_analyzed_module). +pub fn analyze_module(module: &syntax::Module) -> Result { let mut models = Vec::with_capacity(module.models.len()); for model in &module.models { models.push(analyze_model(model)?); } - Ok(TypedModule { + Ok(AnalyzedModule { models, span: module.span, }) } -pub fn analyze_model(model: &syntax::Model) -> Result { +/// Checks a parsed model and resolves all names and types. +/// +/// This is the middle pipeline stage: after [`parse_model`](crate::parse_model), +/// before [`compile_analyzed_model`](crate::compile_analyzed_model). +pub fn analyze_model(model: &syntax::Model) -> Result { Analyzer::new(model).analyze() } #[derive(Clone, PartialEq, Eq)] -pub struct SemanticError { +pub struct AnalysisError { diagnostic: Box, source: Option>, } -impl SemanticError { +impl AnalysisError { pub fn new(message: impl Into, span: Span) -> Self { Self { diagnostic: Box::new(Diagnostic::error( - DSL_SEMANTIC_GENERIC, - DiagnosticPhase::Semantic, + DSL_ANALYSIS_GENERIC, + DiagnosticPhase::Analysis, message, span, )), @@ -197,13 +207,13 @@ impl SemanticError { } } -impl fmt::Debug for SemanticError { +impl fmt::Debug for AnalysisError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Display::fmt(self, f) } } -impl fmt::Display for SemanticError { +impl fmt::Display for AnalysisError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if let Some(source) = self.source() { return f.write_str(&self.render(source)); @@ -211,13 +221,13 @@ impl fmt::Display for SemanticError { let span = self.diagnostic.primary_span(); write!( f, - "{} at bytes {}..{}", - self.diagnostic.message, span.start, span.end + "error[{}]: {} (at bytes {}..{})", + self.diagnostic.code, self.diagnostic.message, span.start, span.end ) } } -impl std::error::Error for SemanticError {} +impl std::error::Error for AnalysisError {} struct Analyzer<'a> { model: &'a syntax::Model, @@ -234,7 +244,7 @@ impl<'a> Analyzer<'a> { } } - fn analyze(mut self) -> Result { + fn analyze(mut self) -> Result { let sections = ModelSections::from_model(self.model)?; let parameters = self.register_parameters(sections.parameters)?; @@ -252,7 +262,7 @@ impl<'a> Analyzer<'a> { §ions .outputs .ok_or_else(|| { - SemanticError::new( + AnalysisError::new( format!( "model `{}` is missing an `outputs` block", self.model.name.text @@ -348,7 +358,7 @@ impl<'a> Analyzer<'a> { let analytical = if let Some(block) = sections.analytical { let structure = AnalyticalKernel::from_name(&block.structure.text).ok_or_else(|| { - SemanticError::new( + AnalysisError::new( format!("unknown analytical structure `{}`", block.structure.text), block.structure.span, ) @@ -358,7 +368,7 @@ impl<'a> Analyzer<'a> { .map(|state| state.size.unwrap_or(1)) .sum::(); if state_components != structure.state_count() { - return Err(SemanticError::new( + return Err(AnalysisError::new( format!( "analytical structure `{}` expects {} state value(s), but model declares {}", block.structure.text, @@ -375,7 +385,7 @@ impl<'a> Analyzer<'a> { &derived, derive_result.as_ref(), )?; - Some(TypedAnalytical { + Some(AnalyticalSpec { structure, span: block.span, }) @@ -387,7 +397,7 @@ impl<'a> Analyzer<'a> { let model_kind = self.model.kind; let model_span = self.model.span; let symbols = self.finalize_symbols()?; - Ok(TypedModel { + Ok(AnalyzedModel { name: model_name, kind: model_kind, symbols, @@ -413,7 +423,7 @@ impl<'a> Analyzer<'a> { fn register_parameters( &mut self, block: Option<&syntax::ParametersBlock>, - ) -> Result, SemanticError> { + ) -> Result, AnalysisError> { let mut parameters = Vec::new(); if let Some(block) = block { for ident in &block.items { @@ -433,7 +443,7 @@ impl<'a> Analyzer<'a> { fn resolve_and_register_constants( &mut self, block: Option<&syntax::ConstantsBlock>, - ) -> Result, SemanticError> { + ) -> Result, AnalysisError> { let Some(block) = block else { return Ok(Vec::new()); }; @@ -441,7 +451,7 @@ impl<'a> Analyzer<'a> { let mut bindings = BTreeMap::new(); for binding in &block.items { if let Some(existing) = bindings.insert(binding.name.text.clone(), binding) { - return Err(SemanticAssist::default() + return Err(AnalysisAssist::default() .context_label( existing.name.span, format!("constant `{}` first declared here", binding.name.text), @@ -456,7 +466,7 @@ impl<'a> Analyzer<'a> { format!("rename this constant to `{}_2`", binding.name.text), Applicability::MaybeIncorrect, ) - .apply(SemanticError::new( + .apply(AnalysisError::new( format!("duplicate constant `{}`", binding.name.text), binding.name.span, ))); @@ -464,7 +474,7 @@ impl<'a> Analyzer<'a> { } let mut visiting = BTreeSet::new(); - let mut typed = Vec::new(); + let mut analyzed = Vec::new(); for binding in &block.items { let value = self.evaluate_const_expr(&binding.value, &bindings, &mut visiting)?; let id = self.insert_global_symbol( @@ -477,19 +487,19 @@ impl<'a> Analyzer<'a> { self.globals .constant_values .insert(binding.name.text.clone(), value.clone()); - typed.push(TypedConstant { + analyzed.push(AnalyzedConstant { symbol: id, value, span: binding.span, }); } - Ok(typed) + Ok(analyzed) } fn register_covariates( &mut self, block: Option<&syntax::CovariatesBlock>, - ) -> Result, SemanticError> { + ) -> Result, AnalysisError> { let mut covariates = Vec::new(); if let Some(block) = block { for covariate in &block.items { @@ -502,7 +512,7 @@ impl<'a> Analyzer<'a> { Some("linear") => Some(CovariateInterpolation::Linear), Some("locf") | Some("carry_forward") => Some(CovariateInterpolation::Locf), Some(other) => { - return Err(SemanticError::new( + return Err(AnalysisError::new( format!("unknown covariate interpolation `{other}`"), covariate.interpolation.as_ref().unwrap().span, ) @@ -518,7 +528,7 @@ impl<'a> Analyzer<'a> { self.globals .covariates .insert(covariate.name.text.clone(), id); - covariates.push(TypedCovariate { + covariates.push(AnalyzedCovariate { symbol: id, interpolation, span: covariate.span, @@ -531,9 +541,9 @@ impl<'a> Analyzer<'a> { fn register_states( &mut self, block: Option<&syntax::StatesBlock>, - ) -> Result, SemanticError> { + ) -> Result, AnalysisError> { let Some(block) = block else { - return Err(SemanticError::new( + return Err(AnalysisError::new( format!( "model `{}` is missing a `states` block", self.model.name.text @@ -564,7 +574,7 @@ impl<'a> Analyzer<'a> { self.globals .states .insert(state.name.text.clone(), StateEntry { symbol: id, size }); - states.push(TypedState { + states.push(AnalyzedState { symbol: id, size, span: state.span, @@ -576,7 +586,7 @@ impl<'a> Analyzer<'a> { fn register_routes( &mut self, block: Option<&syntax::RoutesBlock>, - ) -> Result, SemanticError> { + ) -> Result, AnalysisError> { let mut routes = Vec::new(); if let Some(block) = block { for route in &block.routes { @@ -596,7 +606,7 @@ impl<'a> Analyzer<'a> { "lag" => RoutePropertyKind::Lag, "bioavailability" => RoutePropertyKind::Bioavailability, other => { - return Err(SemanticError::new( + return Err(AnalysisError::new( format!("unknown route property `{other}`"), property.name.span, ) @@ -606,7 +616,7 @@ impl<'a> Analyzer<'a> { } }; if let Some(existing_span) = seen_props.insert(kind, property.name.span) { - return Err(SemanticAssist::default() + return Err(AnalysisAssist::default() .context_label( existing_span, format!( @@ -618,7 +628,7 @@ impl<'a> Analyzer<'a> { "each route can declare `{}` at most once", property.name.text )) - .apply(SemanticError::new( + .apply(AnalysisError::new( format!("duplicate route property `{}`", property.name.text), property.name.span, ))); @@ -626,13 +636,13 @@ impl<'a> Analyzer<'a> { let env = BlockEnv::new(BTreeSet::new()); let value = self.analyze_expr(&property.value, &env)?; self.expect_numeric(&value, "route property", property.value.span)?; - properties.push(TypedRouteProperty { + properties.push(AnalyzedRouteProperty { kind, value, span: property.span, }); } - routes.push(TypedRoute { + routes.push(AnalyzedRoute { symbol: id, kind: route.kind, destination, @@ -648,7 +658,7 @@ impl<'a> Analyzer<'a> { &mut self, statements: Option<&[syntax::Stmt]>, kind: SymbolKind, - ) -> Result, SemanticError> { + ) -> Result, AnalysisError> { let mut collected_idents = Vec::new(); let Some(statements) = statements else { return Ok(Vec::new()); @@ -663,7 +673,7 @@ impl<'a> Analyzer<'a> { } if matches!(kind, SymbolKind::Derived) { if let Some(parameter) = self.globals.parameters.get(&ident.text).copied() { - return Err(SemanticAssist::default() + return Err(AnalysisAssist::default() .context_label( self.symbol_span(parameter), self.symbol_declared_here(parameter), @@ -677,7 +687,7 @@ impl<'a> Analyzer<'a> { format!("rename this derive target to `{}_derived`", ident.text), Applicability::MaybeIncorrect, ) - .apply(SemanticError::new( + .apply(AnalysisError::new( format!( "derived name `{}` collides with primary parameter `{}`", ident.text, ident.text @@ -711,12 +721,12 @@ impl<'a> Analyzer<'a> { block: &syntax::StatementBlock, context: BlockContext, available_derived: BTreeSet, - ) -> Result { + ) -> Result { let env = BlockEnv::new(available_derived); let (statements, env, touched_states) = self.analyze_stmt_list(&block.statements, context, env)?; Ok(BlockAnalysis { - block: TypedStatementBlock { + block: AnalyzedStatementBlock { context, statements, span: block.span, @@ -732,8 +742,8 @@ impl<'a> Analyzer<'a> { statements: &[syntax::Stmt], context: BlockContext, mut env: BlockEnv, - ) -> Result<(Vec, BlockEnv, BTreeSet), SemanticError> { - let mut typed = Vec::with_capacity(statements.len()); + ) -> Result<(Vec, BlockEnv, BTreeSet), AnalysisError> { + let mut analyzed = Vec::with_capacity(statements.len()); let mut touched_states = BTreeSet::new(); for stmt in statements { @@ -746,8 +756,8 @@ impl<'a> Analyzer<'a> { value.ty, SymbolKind::Local, )?; - typed.push(TypedStmt { - kind: TypedStmtKind::Let(TypedLetStmt { symbol, value }), + analyzed.push(AnalyzedStmt { + kind: AnalyzedStmtKind::Let(AnalyzedLetStmt { symbol, value }), span: stmt.span, }); } @@ -756,23 +766,23 @@ impl<'a> Analyzer<'a> { let value = self.analyze_expr(&assign.value, &env)?; self.expect_numeric(&value, "assignment value", assign.value.span)?; match &target.kind { - TypedAssignTargetKind::Derived(symbol) => { + AnalyzedAssignTargetKind::Derived(symbol) => { self.merge_symbol_type(*symbol, value.ty, assign.value.span)?; env.available_derived.insert(*symbol); env.definite_targets.insert(*symbol); } - TypedAssignTargetKind::Output(symbol) => { + AnalyzedAssignTargetKind::Output(symbol) => { self.merge_symbol_type(*symbol, value.ty, assign.value.span)?; env.definite_targets.insert(*symbol); } - TypedAssignTargetKind::StateInit(place) - | TypedAssignTargetKind::Derivative(place) - | TypedAssignTargetKind::Noise(place) => { + AnalyzedAssignTargetKind::StateInit(place) + | AnalyzedAssignTargetKind::Derivative(place) + | AnalyzedAssignTargetKind::Noise(place) => { touched_states.insert(place.state); } } - typed.push(TypedStmt { - kind: TypedStmtKind::Assign(TypedAssignStmt { target, value }), + analyzed.push(AnalyzedStmt { + kind: AnalyzedStmtKind::Assign(AnalyzedAssignStmt { target, value }), span: stmt.span, }); } @@ -816,8 +826,8 @@ impl<'a> Analyzer<'a> { env.available_derived = next_available; env.definite_targets = next_targets; touched_states.extend(branch_states); - typed.push(TypedStmt { - kind: TypedStmtKind::If(TypedIfStmt { + analyzed.push(AnalyzedStmt { + kind: AnalyzedStmtKind::If(AnalyzedIfStmt { condition, then_branch, else_branch, @@ -841,10 +851,10 @@ impl<'a> Analyzer<'a> { let (body, _loop_env, body_states) = self.analyze_stmt_list(&for_stmt.body, context, loop_env)?; touched_states.extend(body_states); - typed.push(TypedStmt { - kind: TypedStmtKind::For(TypedForStmt { + analyzed.push(AnalyzedStmt { + kind: AnalyzedStmtKind::For(AnalyzedForStmt { binding, - range: TypedRangeExpr { + range: AnalyzedRangeExpr { start, end, span: for_stmt.range.span, @@ -857,7 +867,7 @@ impl<'a> Analyzer<'a> { } } - Ok((typed, env, touched_states)) + Ok((analyzed, env, touched_states)) } fn analyze_assign_target( @@ -865,20 +875,20 @@ impl<'a> Analyzer<'a> { target: &syntax::AssignTarget, context: BlockContext, env: &BlockEnv, - ) -> Result { + ) -> Result { let kind = match context { BlockContext::Derive => match &target.kind { syntax::AssignTargetKind::Name(name) => { let Some(symbol) = self.globals.derived.get(&name.text).copied() else { - return Err(SemanticError::new( + return Err(AnalysisError::new( format!("`{}` is not a valid derive target", name.text), name.span, )); }; - TypedAssignTargetKind::Derived(symbol) + AnalyzedAssignTargetKind::Derived(symbol) } _ => { - return Err(SemanticError::new( + return Err(AnalysisError::new( "derive assignments must target a bare identifier", target.span, )) @@ -887,35 +897,35 @@ impl<'a> Analyzer<'a> { BlockContext::Outputs => match &target.kind { syntax::AssignTargetKind::Name(name) => { let Some(symbol) = self.globals.outputs.get(&name.text).copied() else { - return Err(SemanticError::new( + return Err(AnalysisError::new( format!("`{}` is not a valid output target", name.text), name.span, )); }; - TypedAssignTargetKind::Output(symbol) + AnalyzedAssignTargetKind::Output(symbol) } _ => { - return Err(SemanticError::new( + return Err(AnalysisError::new( "outputs assignments must target a bare identifier", target.span, )) } }, BlockContext::Init => { - TypedAssignTargetKind::StateInit(self.analyze_runtime_state_place(target, env)?) + AnalyzedAssignTargetKind::StateInit(self.analyze_runtime_state_place(target, env)?) } BlockContext::Dynamics | BlockContext::Drift => { let place = self.expect_call_state_target(target, "ddt")?; - TypedAssignTargetKind::Derivative( + AnalyzedAssignTargetKind::Derivative( self.analyze_runtime_state_place_expr(&place, env)?, ) } BlockContext::Diffusion => { let place = self.expect_call_state_target(target, "noise")?; - TypedAssignTargetKind::Noise(self.analyze_runtime_state_place_expr(&place, env)?) + AnalyzedAssignTargetKind::Noise(self.analyze_runtime_state_place_expr(&place, env)?) } }; - Ok(TypedAssignTarget { + Ok(AnalyzedAssignTarget { kind, span: target.span, }) @@ -925,28 +935,28 @@ impl<'a> Analyzer<'a> { &self, target: &syntax::AssignTarget, expected: &str, - ) -> Result { + ) -> Result { match &target.kind { syntax::AssignTargetKind::Call { callee, args } if callee.text == expected && args.len() == 1 => { self.place_from_expr(&args[0]) } - syntax::AssignTargetKind::Call { callee, .. } => Err(SemanticError::new( + syntax::AssignTargetKind::Call { callee, .. } => Err(AnalysisError::new( format!( "expected `{expected}(...)` assignment target, found `{}`", callee.text ), target.span, )), - _ => Err(SemanticError::new( + _ => Err(AnalysisError::new( format!("expected `{expected}(...)` assignment target"), target.span, )), } } - fn place_from_expr(&self, expr: &syntax::Expr) -> Result { + fn place_from_expr(&self, expr: &syntax::Expr) -> Result { match &expr.kind { syntax::ExprKind::Name(name) => Ok(syntax::Place { name: name.clone(), @@ -959,12 +969,12 @@ impl<'a> Analyzer<'a> { index: Some((**index).clone()), span: expr.span, }), - _ => Err(SemanticError::new( + _ => Err(AnalysisError::new( "indexed assignment targets must index a state identifier", expr.span, )), }, - _ => Err(SemanticError::new( + _ => Err(AnalysisError::new( "expected a state reference in assignment target", expr.span, )), @@ -975,7 +985,7 @@ impl<'a> Analyzer<'a> { &self, target: &syntax::AssignTarget, env: &BlockEnv, - ) -> Result { + ) -> Result { let place = match &target.kind { syntax::AssignTargetKind::Name(name) => syntax::Place { name: name.clone(), @@ -988,7 +998,7 @@ impl<'a> Analyzer<'a> { span: target.span, }, syntax::AssignTargetKind::Call { .. } => { - return Err(SemanticError::new( + return Err(AnalysisError::new( "unexpected call target in runtime state assignment", target.span, )) @@ -1001,9 +1011,9 @@ impl<'a> Analyzer<'a> { &self, place: &syntax::Place, env: &BlockEnv, - ) -> Result { + ) -> Result { let state = self.globals.states.get(&place.name.text).ok_or_else(|| { - let error = SemanticError::new( + let error = AnalysisError::new( format!("unknown state `{}`", place.name.text), place.name.span, ); @@ -1019,13 +1029,13 @@ impl<'a> Analyzer<'a> { Some(Box::new(index)) } (Some(_), None) => { - return Err(SemanticError::new( + return Err(AnalysisError::new( format!("state array `{}` requires an index", place.name.text), place.span, )) } (None, Some(_)) => { - return Err(SemanticError::new( + return Err(AnalysisError::new( format!( "state `{}` is scalar and cannot be indexed", place.name.text @@ -1035,7 +1045,7 @@ impl<'a> Analyzer<'a> { } (None, None) => None, }; - Ok(TypedStatePlace { + Ok(AnalyzedStatePlace { state: state.symbol, index, span: place.span, @@ -1045,9 +1055,9 @@ impl<'a> Analyzer<'a> { fn analyze_state_place_const( &self, place: &syntax::Place, - ) -> Result { + ) -> Result { let state = self.globals.states.get(&place.name.text).ok_or_else(|| { - let error = SemanticError::new( + let error = AnalysisError::new( format!("unknown state `{}`", place.name.text), place.name.span, ); @@ -1059,21 +1069,21 @@ impl<'a> Analyzer<'a> { let index = match (&state.size, &place.index) { (Some(_), Some(index)) => { let value = self.expect_const_usize(index, "route destination index", false)?; - Some(Box::new(TypedExpr { - kind: TypedExprKind::Literal(ConstValue::Int(value as i64)), + Some(Box::new(AnalyzedExpr { + kind: AnalyzedExprKind::Literal(ConstValue::Int(value as i64)), ty: ValueType::Int, constant: Some(ConstValue::Int(value as i64)), span: index.span, })) } (Some(_), None) => { - return Err(SemanticError::new( + return Err(AnalysisError::new( format!("state array `{}` requires an index", place.name.text), place.span, )) } (None, Some(_)) => { - return Err(SemanticError::new( + return Err(AnalysisError::new( format!( "state `{}` is scalar and cannot be indexed", place.name.text @@ -1083,7 +1093,7 @@ impl<'a> Analyzer<'a> { } (None, None) => None, }; - Ok(TypedStatePlace { + Ok(AnalyzedStatePlace { state: state.symbol, index, span: place.span, @@ -1094,12 +1104,12 @@ impl<'a> Analyzer<'a> { &self, expr: &syntax::Expr, env: &BlockEnv, - ) -> Result { + ) -> Result { match &expr.kind { syntax::ExprKind::Number(value) => { let constant = number_to_const(*value); - Ok(TypedExpr { - kind: TypedExprKind::Literal(constant.clone()), + Ok(AnalyzedExpr { + kind: AnalyzedExprKind::Literal(constant.clone()), ty: constant.value_type(), constant: Some(constant), span: expr.span, @@ -1107,8 +1117,8 @@ impl<'a> Analyzer<'a> { } syntax::ExprKind::Bool(value) => { let constant = ConstValue::Bool(*value); - Ok(TypedExpr { - kind: TypedExprKind::Literal(constant.clone()), + Ok(AnalyzedExpr { + kind: AnalyzedExprKind::Literal(constant.clone()), ty: ValueType::Bool, constant: Some(constant), span: expr.span, @@ -1128,16 +1138,16 @@ impl<'a> Analyzer<'a> { } }; let op = match op { - syntax::UnaryOp::Plus => TypedUnaryOp::Plus, - syntax::UnaryOp::Minus => TypedUnaryOp::Minus, - syntax::UnaryOp::Not => TypedUnaryOp::Not, + syntax::UnaryOp::Plus => AnalyzedUnaryOp::Plus, + syntax::UnaryOp::Minus => AnalyzedUnaryOp::Minus, + syntax::UnaryOp::Not => AnalyzedUnaryOp::Not, }; let constant = inner .constant .as_ref() .and_then(|value| fold_unary(op, value)); - Ok(TypedExpr { - kind: TypedExprKind::Unary { + Ok(AnalyzedExpr { + kind: AnalyzedExprKind::Unary { op, expr: Box::new(inner), }, @@ -1155,8 +1165,8 @@ impl<'a> Analyzer<'a> { (Some(lhs), Some(rhs)) => fold_binary(op, lhs, rhs), _ => None, }; - Ok(TypedExpr { - kind: TypedExprKind::Binary { + Ok(AnalyzedExpr { + kind: AnalyzedExprKind::Binary { op, lhs: Box::new(lhs), rhs: Box::new(rhs), @@ -1175,7 +1185,7 @@ impl<'a> Analyzer<'a> { match &target.kind { syntax::ExprKind::Name(name) => { let state = self.globals.states.get(&name.text).ok_or_else(|| { - SemanticError::new( + AnalysisError::new( format!( "only state arrays can be indexed; `{}` is not a state", name.text @@ -1184,13 +1194,13 @@ impl<'a> Analyzer<'a> { ) })?; if state.size.is_none() { - return Err(SemanticError::new( + return Err(AnalysisError::new( format!("state `{}` is scalar and cannot be indexed", name.text), expr.span, )); } - Ok(TypedExpr { - kind: TypedExprKind::StateValue(TypedStatePlace { + Ok(AnalyzedExpr { + kind: AnalyzedExprKind::StateValue(AnalyzedStatePlace { state: state.symbol, index: Some(Box::new(index)), span: expr.span, @@ -1200,7 +1210,7 @@ impl<'a> Analyzer<'a> { span: expr.span, }) } - _ => Err(SemanticError::new( + _ => Err(AnalysisError::new( "only state arrays can be indexed", expr.span, )), @@ -1214,16 +1224,16 @@ impl<'a> Analyzer<'a> { name: &syntax::Ident, span: Span, env: &BlockEnv, - ) -> Result { + ) -> Result { if let Some(symbol) = env.lookup_local(&name.text) { let ty = self.scalar_symbol_type(symbol).ok_or_else(|| { - SemanticError::new( + AnalysisError::new( format!("local `{}` does not resolve to a scalar value", name.text), span, ) })?; - return Ok(TypedExpr { - kind: TypedExprKind::Symbol(symbol), + return Ok(AnalyzedExpr { + kind: AnalyzedExprKind::Symbol(symbol), ty, constant: None, span, @@ -1231,8 +1241,8 @@ impl<'a> Analyzer<'a> { } if let Some(symbol) = self.globals.parameters.get(&name.text).copied() { - return Ok(TypedExpr { - kind: TypedExprKind::Symbol(symbol), + return Ok(AnalyzedExpr { + kind: AnalyzedExprKind::Symbol(symbol), ty: ValueType::Real, constant: None, span, @@ -1244,8 +1254,8 @@ impl<'a> Analyzer<'a> { let ty = self .scalar_symbol_type(symbol) .expect("constant type must be known"); - return Ok(TypedExpr { - kind: TypedExprKind::Symbol(symbol), + return Ok(AnalyzedExpr { + kind: AnalyzedExprKind::Symbol(symbol), ty, constant, span, @@ -1253,8 +1263,8 @@ impl<'a> Analyzer<'a> { } if let Some(symbol) = self.globals.covariates.get(&name.text).copied() { - return Ok(TypedExpr { - kind: TypedExprKind::Symbol(symbol), + return Ok(AnalyzedExpr { + kind: AnalyzedExprKind::Symbol(symbol), ty: ValueType::Real, constant: None, span, @@ -1263,18 +1273,18 @@ impl<'a> Analyzer<'a> { if let Some(state) = self.globals.states.get(&name.text) { if state.size.is_some() { - return Err(SemanticError::new( + return Err(AnalysisError::new( format!("state array `{}` requires an index", name.text), span, )); } - let place = TypedStatePlace { + let place = AnalyzedStatePlace { state: state.symbol, index: None, span, }; - return Ok(TypedExpr { - kind: TypedExprKind::StateValue(place), + return Ok(AnalyzedExpr { + kind: AnalyzedExprKind::StateValue(place), ty: ValueType::Real, constant: None, span, @@ -1283,7 +1293,7 @@ impl<'a> Analyzer<'a> { if let Some(symbol) = self.globals.derived.get(&name.text).copied() { if !env.available_derived.contains(&symbol) { - return Err(SemanticError::new( + return Err(AnalysisError::new( format!( "derived value `{}` is not definitely assigned at this point", name.text @@ -1292,7 +1302,7 @@ impl<'a> Analyzer<'a> { )); } let ty = self.scalar_symbol_type(symbol).ok_or_else(|| { - SemanticError::new( + AnalysisError::new( format!( "derived value `{}` does not have a resolved type yet", name.text @@ -1300,8 +1310,8 @@ impl<'a> Analyzer<'a> { span, ) })?; - return Ok(TypedExpr { - kind: TypedExprKind::Symbol(symbol), + return Ok(AnalyzedExpr { + kind: AnalyzedExprKind::Symbol(symbol), ty, constant: None, span, @@ -1312,7 +1322,7 @@ impl<'a> Analyzer<'a> { let route = self.globals.routes[&name.text]; return Err(self .assist_for_route_scalar(route, span) - .apply(SemanticError::new( + .apply(AnalysisError::new( format!( "route `{}` cannot be used as a scalar value; use `rate({})`", name.text, name.text @@ -1325,13 +1335,13 @@ impl<'a> Analyzer<'a> { let output = self.globals.outputs[&name.text]; return Err(self .assist_for_output_scope(output) - .apply(SemanticError::new( + .apply(AnalysisError::new( format!("output `{}` is not in expression scope", name.text), span, ))); } - let error = SemanticError::new(format!("unknown identifier `{}`", name.text), span); + let error = AnalysisError::new(format!("unknown identifier `{}`", name.text), span); Err(match self.assist_for_unknown_identifier(name, span, env) { Some(assist) => assist.apply(error), None => error, @@ -1344,10 +1354,10 @@ impl<'a> Analyzer<'a> { args: &[syntax::Expr], span: Span, env: &BlockEnv, - ) -> Result { + ) -> Result { if callee.text == RATE_FUNCTION_NAME { if args.len() != 1 { - return Err(SemanticError::new( + return Err(AnalysisError::new( format!( "`rate` expects exactly one route argument, got {}", args.len() @@ -1361,7 +1371,7 @@ impl<'a> Analyzer<'a> { } } let syntax::ExprKind::Name(route_name) = &args[0].kind else { - return Err(SemanticError::new( + return Err(AnalysisError::new( "`rate` expects a route identifier argument", args[0].span, )); @@ -1373,7 +1383,7 @@ impl<'a> Analyzer<'a> { .get(&route_name.text) .copied() .ok_or_else(|| { - let error = SemanticError::new( + let error = AnalysisError::new( format!("unknown route `{}` in `rate(...)`", route_name.text), route_name.span, ); @@ -1382,9 +1392,9 @@ impl<'a> Analyzer<'a> { None => error, } })?; - return Ok(TypedExpr { - kind: TypedExprKind::Call { - callee: TypedCall::Rate(route), + return Ok(AnalyzedExpr { + kind: AnalyzedExprKind::Call { + callee: AnalyzedCall::Rate(route), args: Vec::new(), }, ty: ValueType::Real, @@ -1393,18 +1403,18 @@ impl<'a> Analyzer<'a> { }); } - let intrinsic = MathIntrinsic::from_name(&callee.text).ok_or_else(|| { + let intrinsic = MathFunction::from_name(&callee.text).ok_or_else(|| { let error = - SemanticError::new(format!("unknown function `{}`", callee.text), callee.span); + AnalysisError::new(format!("unknown function `{}`", callee.text), callee.span); match self.assist_for_unknown_function(callee) { Some(assist) => assist.apply(error), None => error, } })?; - let expected_arity = intrinsic.arity(); + let expected_arity = intrinsic.argument_count(); match expected_arity { - IntrinsicArity::Exact(expected) if expected != args.len() => { - return Err(SemanticError::new( + ArgumentCount::Exact(expected) if expected != args.len() => { + return Err(AnalysisError::new( format!( "function `{}` expects {} argument(s), got {}", callee.text, @@ -1419,9 +1429,9 @@ impl<'a> Analyzer<'a> { let mut typed_args = Vec::with_capacity(args.len()); for arg in args { - let typed = self.analyze_expr(arg, env)?; - self.expect_numeric(&typed, &format!("`{}` argument", callee.text), arg.span)?; - typed_args.push(typed); + let analyzed = self.analyze_expr(arg, env)?; + self.expect_numeric(&analyzed, &format!("`{}` argument", callee.text), arg.span)?; + typed_args.push(analyzed); } let ty = call_result_type(intrinsic, &typed_args); let constant = typed_args @@ -1429,9 +1439,9 @@ impl<'a> Analyzer<'a> { .map(|arg| arg.constant.clone()) .collect::>>() .and_then(|values| fold_call(intrinsic, &values)); - Ok(TypedExpr { - kind: TypedExprKind::Call { - callee: TypedCall::Math(intrinsic), + Ok(AnalyzedExpr { + kind: AnalyzedExprKind::Call { + callee: AnalyzedCall::Math(intrinsic), args: typed_args, }, ty, @@ -1442,20 +1452,20 @@ impl<'a> Analyzer<'a> { fn binary_result_type( &self, - op: TypedBinaryOp, - lhs: &TypedExpr, - rhs: &TypedExpr, + op: AnalyzedBinaryOp, + lhs: &AnalyzedExpr, + rhs: &AnalyzedExpr, span: Span, - ) -> Result { + ) -> Result { match op { - TypedBinaryOp::Or | TypedBinaryOp::And => { + AnalyzedBinaryOp::Or | AnalyzedBinaryOp::And => { self.expect_bool(lhs, "logical operand", lhs.span)?; self.expect_bool(rhs, "logical operand", rhs.span)?; Ok(ValueType::Bool) } - TypedBinaryOp::Eq | TypedBinaryOp::NotEq => { + AnalyzedBinaryOp::Eq | AnalyzedBinaryOp::NotEq => { if lhs.ty != rhs.ty { - return Err(SemanticError::new( + return Err(AnalysisError::new( format!( "equality comparison requires matching operand types, found {:?} and {:?}", lhs.ty, rhs.ty @@ -1465,17 +1475,20 @@ impl<'a> Analyzer<'a> { } Ok(ValueType::Bool) } - TypedBinaryOp::Lt | TypedBinaryOp::LtEq | TypedBinaryOp::Gt | TypedBinaryOp::GtEq => { + AnalyzedBinaryOp::Lt + | AnalyzedBinaryOp::LtEq + | AnalyzedBinaryOp::Gt + | AnalyzedBinaryOp::GtEq => { self.expect_numeric(lhs, "comparison operand", lhs.span)?; self.expect_numeric(rhs, "comparison operand", rhs.span)?; Ok(ValueType::Bool) } - TypedBinaryOp::Add | TypedBinaryOp::Sub | TypedBinaryOp::Mul => { + AnalyzedBinaryOp::Add | AnalyzedBinaryOp::Sub | AnalyzedBinaryOp::Mul => { self.expect_numeric(lhs, "arithmetic operand", lhs.span)?; self.expect_numeric(rhs, "arithmetic operand", rhs.span)?; Ok(promote_numeric(lhs.ty, rhs.ty)) } - TypedBinaryOp::Div | TypedBinaryOp::Pow => { + AnalyzedBinaryOp::Div | AnalyzedBinaryOp::Pow => { self.expect_numeric(lhs, "arithmetic operand", lhs.span)?; self.expect_numeric(rhs, "arithmetic operand", rhs.span)?; Ok(ValueType::Real) @@ -1485,14 +1498,14 @@ impl<'a> Analyzer<'a> { fn expect_numeric( &self, - expr: &TypedExpr, + expr: &AnalyzedExpr, context: &str, span: Span, - ) -> Result<(), SemanticError> { + ) -> Result<(), AnalysisError> { if expr.ty.is_numeric() { Ok(()) } else { - Err(SemanticError::new( + Err(AnalysisError::new( format!("{context} must be numeric, found {:?}", expr.ty), span, )) @@ -1501,25 +1514,30 @@ impl<'a> Analyzer<'a> { fn expect_bool( &self, - expr: &TypedExpr, + expr: &AnalyzedExpr, context: &str, span: Span, - ) -> Result<(), SemanticError> { + ) -> Result<(), AnalysisError> { if expr.ty == ValueType::Bool { Ok(()) } else { - Err(SemanticError::new( + Err(AnalysisError::new( format!("{context} must be boolean, found {:?}", expr.ty), span, )) } } - fn expect_int(&self, expr: &TypedExpr, context: &str, span: Span) -> Result<(), SemanticError> { + fn expect_int( + &self, + expr: &AnalyzedExpr, + context: &str, + span: Span, + ) -> Result<(), AnalysisError> { if expr.ty == ValueType::Int { Ok(()) } else { - Err(SemanticError::new( + Err(AnalysisError::new( format!("{context} must be integer-valued, found {:?}", expr.ty), span, )) @@ -1531,16 +1549,16 @@ impl<'a> Analyzer<'a> { expr: &syntax::Expr, context: &str, strictly_positive: bool, - ) -> Result { + ) -> Result { let value = self.evaluate_const_expr(expr, &BTreeMap::new(), &mut BTreeSet::new())?; let Some(value) = value.as_i64() else { - return Err(SemanticError::new( + return Err(AnalysisError::new( format!("{context} must be an integer constant"), expr.span, )); }; if value < 0 || (strictly_positive && value == 0) { - return Err(SemanticError::new( + return Err(AnalysisError::new( format!( "{context} must be {}", if strictly_positive { @@ -1552,6 +1570,12 @@ impl<'a> Analyzer<'a> { expr.span, )); } + if value as u64 > MAX_CONST_USIZE as u64 { + return Err(AnalysisError::new( + format!("{context} exceeds the maximum supported value of {MAX_CONST_USIZE}"), + expr.span, + )); + } Ok(value as usize) } @@ -1560,7 +1584,7 @@ impl<'a> Analyzer<'a> { expr: &syntax::Expr, bindings: &BTreeMap, visiting: &mut BTreeSet, - ) -> Result { + ) -> Result { match &expr.kind { syntax::ExprKind::Number(value) => Ok(number_to_const(*value)), syntax::ExprKind::Bool(value) => Ok(ConstValue::Bool(*value)), @@ -1569,7 +1593,7 @@ impl<'a> Analyzer<'a> { return Ok(value.clone()); } let binding = bindings.get(&name.text).ok_or_else(|| { - SemanticError::new( + AnalysisError::new( format!( "unknown constant `{}` in compile-time expression", name.text @@ -1578,7 +1602,7 @@ impl<'a> Analyzer<'a> { ) })?; if !visiting.insert(name.text.clone()) { - return Err(SemanticError::new( + return Err(AnalysisError::new( format!("constant `{}` forms a dependency cycle", name.text), name.span, )); @@ -1590,46 +1614,60 @@ impl<'a> Analyzer<'a> { syntax::ExprKind::Unary { op, expr } => { let value = self.evaluate_const_expr(expr, bindings, visiting)?; let op = match op { - syntax::UnaryOp::Plus => TypedUnaryOp::Plus, - syntax::UnaryOp::Minus => TypedUnaryOp::Minus, - syntax::UnaryOp::Not => TypedUnaryOp::Not, + syntax::UnaryOp::Plus => AnalyzedUnaryOp::Plus, + syntax::UnaryOp::Minus => AnalyzedUnaryOp::Minus, + syntax::UnaryOp::Not => AnalyzedUnaryOp::Not, }; fold_unary(op, &value).ok_or_else(|| { - SemanticError::new("invalid constant unary operation", expr.span) + AnalysisError::new("invalid constant unary operation", expr.span) }) } syntax::ExprKind::Binary { op, lhs, rhs } => { let lhs = self.evaluate_const_expr(lhs, bindings, visiting)?; let rhs = self.evaluate_const_expr(rhs, bindings, visiting)?; fold_binary(map_binary_op(*op), &lhs, &rhs).ok_or_else(|| { - SemanticError::new("invalid constant binary operation", expr.span) + AnalysisError::new("invalid constant binary operation", expr.span) }) } syntax::ExprKind::Call { callee, args } => { if callee.text == RATE_FUNCTION_NAME { - return Err(SemanticError::new( + return Err(AnalysisError::new( "`rate(...)` cannot appear in a compile-time expression", callee.span, )); } - let intrinsic = MathIntrinsic::from_name(&callee.text).ok_or_else(|| { - SemanticError::new( + let intrinsic = MathFunction::from_name(&callee.text).ok_or_else(|| { + AnalysisError::new( format!("unknown compile-time function `{}`", callee.text), callee.span, ) })?; + match intrinsic.argument_count() { + ArgumentCount::Exact(expected) if expected != args.len() => { + return Err(AnalysisError::new( + format!( + "function `{}` expects {} argument(s), got {}", + callee.text, + expected, + args.len() + ), + callee.span, + )); + } + _ => {} + } let mut values = Vec::with_capacity(args.len()); for arg in args { values.push(self.evaluate_const_expr(arg, bindings, visiting)?); } fold_call(intrinsic, &values).ok_or_else(|| { - SemanticError::new( + AnalysisError::new( format!("invalid compile-time call to `{}`", callee.text), expr.span, ) }) } - syntax::ExprKind::Index { .. } => Err(SemanticError::new( + syntax::ExprKind::Index { .. } => Err(AnalysisError::new( "indexing is not allowed in compile-time expressions", expr.span, )), @@ -1642,9 +1680,9 @@ impl<'a> Analyzer<'a> { kind: SymbolKind, ty: PendingSymbolType, span: Span, - ) -> Result { + ) -> Result { if RESERVED_NAMES.contains(&name) { - return Err(SemanticAssist::default() + return Err(AnalysisAssist::default() .help(format!( "rename `{name}` to a non-reserved identifier such as `{}_value`", name @@ -1655,7 +1693,7 @@ impl<'a> Analyzer<'a> { format!("rename `{name}` to `{}_value`", name), Applicability::MaybeIncorrect, ) - .apply(SemanticError::new( + .apply(AnalysisError::new( format!("`{name}` is reserved by the DSL and cannot be used as a symbol name"), span, ))); @@ -1663,7 +1701,7 @@ impl<'a> Analyzer<'a> { if let Some(existing) = self.globals.all_names.get(name).copied() { let existing_kind = self.symbols.get(existing).expect("valid symbol id").kind; if !allows_route_output_name_overlap(existing_kind, kind) { - return Err(SemanticAssist::default() + return Err(AnalysisAssist::default() .context_label( self.symbol_span(existing), self.symbol_declared_here(existing), @@ -1678,7 +1716,7 @@ impl<'a> Analyzer<'a> { format!("rename this declaration to `{}_2`", name), Applicability::MaybeIncorrect, ) - .apply(SemanticError::new( + .apply(AnalysisError::new( format!( "symbol name `{name}` collides with existing `{}`", self.symbol_name(existing) @@ -1699,7 +1737,7 @@ impl<'a> Analyzer<'a> { Ok(id) } - fn validate_route_label_name(&self, label: &syntax::Ident) -> Result<(), SemanticError> { + fn validate_route_label_name(&self, label: &syntax::Ident) -> Result<(), AnalysisError> { if let Some(suffix) = bare_numeric_label(&label.text) { return Err(self.bare_numeric_route_error(label.span, suffix)); } @@ -1709,7 +1747,7 @@ impl<'a> Analyzer<'a> { Ok(()) } - fn validate_output_label_name(&self, label: &syntax::Ident) -> Result<(), SemanticError> { + fn validate_output_label_name(&self, label: &syntax::Ident) -> Result<(), AnalysisError> { if let Some(suffix) = bare_numeric_label(&label.text) { return Err(self.bare_numeric_output_error(label.span, suffix)); } @@ -1719,9 +1757,9 @@ impl<'a> Analyzer<'a> { Ok(()) } - fn bare_numeric_route_error(&self, span: Span, suffix: &str) -> SemanticError { + fn bare_numeric_route_error(&self, span: Span, suffix: &str) -> AnalysisError { let replacement = format!("{NUMERIC_ROUTE_PREFIX}{suffix}"); - SemanticAssist::default() + AnalysisAssist::default() .help("numeric route labels must use the `input_` form in authored DSL") .replacement_suggestion( span, @@ -1729,7 +1767,7 @@ impl<'a> Analyzer<'a> { format!("use `{replacement}`"), Applicability::Always, ) - .apply(SemanticError::new( + .apply(AnalysisError::new( format!( "bare numeric route labels are not allowed in the DSL; use `{replacement}` instead" ), @@ -1737,9 +1775,9 @@ impl<'a> Analyzer<'a> { )) } - fn bare_numeric_output_error(&self, span: Span, suffix: &str) -> SemanticError { + fn bare_numeric_output_error(&self, span: Span, suffix: &str) -> AnalysisError { let replacement = format!("{NUMERIC_OUTPUT_PREFIX}{suffix}"); - SemanticAssist::default() + AnalysisAssist::default() .help("numeric output labels must use the `outeq_` form in authored DSL") .replacement_suggestion( span, @@ -1747,7 +1785,7 @@ impl<'a> Analyzer<'a> { format!("use `{replacement}`"), Applicability::Always, ) - .apply(SemanticError::new( + .apply(AnalysisError::new( format!( "bare numeric output labels are not allowed in the DSL; use `{replacement}` instead" ), @@ -1755,9 +1793,9 @@ impl<'a> Analyzer<'a> { )) } - fn wrong_prefix_route_error(&self, label: &syntax::Ident, suffix: &str) -> SemanticError { + fn wrong_prefix_route_error(&self, label: &syntax::Ident, suffix: &str) -> AnalysisError { let replacement = format!("{NUMERIC_ROUTE_PREFIX}{suffix}"); - SemanticAssist::default() + AnalysisAssist::default() .help("numeric route labels use the `input_` prefix") .replacement_suggestion( label.span, @@ -1765,7 +1803,7 @@ impl<'a> Analyzer<'a> { format!("use `{replacement}`"), Applicability::Always, ) - .apply(SemanticError::new( + .apply(AnalysisError::new( format!( "`{}` is an output label and cannot be used as a route; use `{replacement}` here", label.text @@ -1774,9 +1812,9 @@ impl<'a> Analyzer<'a> { )) } - fn wrong_prefix_output_error(&self, label: &syntax::Ident, suffix: &str) -> SemanticError { + fn wrong_prefix_output_error(&self, label: &syntax::Ident, suffix: &str) -> AnalysisError { let replacement = format!("{NUMERIC_OUTPUT_PREFIX}{suffix}"); - SemanticAssist::default() + AnalysisAssist::default() .help("numeric output labels use the `outeq_` prefix") .replacement_suggestion( label.span, @@ -1784,7 +1822,7 @@ impl<'a> Analyzer<'a> { format!("use `{replacement}`"), Applicability::Always, ) - .apply(SemanticError::new( + .apply(AnalysisError::new( format!( "`{}` is a route label and cannot be used as an output target; use `{replacement}` here", label.text @@ -1799,12 +1837,12 @@ impl<'a> Analyzer<'a> { ident: &syntax::Ident, ty: ValueType, kind: SymbolKind, - ) -> Result { + ) -> Result { if let Some(existing) = env .lookup_local(&ident.text) .or_else(|| self.globals.all_names.get(&ident.text).copied()) { - return Err(SemanticAssist::default() + return Err(AnalysisAssist::default() .context_label( self.symbol_span(existing), self.symbol_declared_here(existing), @@ -1819,7 +1857,7 @@ impl<'a> Analyzer<'a> { format!("rename this local binding to `{}_local`", ident.text), Applicability::MaybeIncorrect, ) - .apply(SemanticError::new( + .apply(AnalysisError::new( format!( "local symbol `{}` would shadow an existing symbol", ident.text @@ -1844,7 +1882,7 @@ impl<'a> Analyzer<'a> { symbol: SymbolId, ty: ValueType, span: Span, - ) -> Result<(), SemanticError> { + ) -> Result<(), AnalysisError> { let entry = self.symbols.get_mut(symbol).expect("valid symbol id"); match &mut entry.ty { PendingSymbolType::Scalar(slot) => match slot { @@ -1854,7 +1892,7 @@ impl<'a> Analyzer<'a> { *slot = Some(promote_numeric(*existing, ty)); } Some(existing) => { - return Err(SemanticError::new( + return Err(AnalysisError::new( format!( "symbol `{}` is assigned incompatible types {:?} and {:?}", entry.name, existing, ty @@ -1864,7 +1902,7 @@ impl<'a> Analyzer<'a> { } }, PendingSymbolType::Array { .. } | PendingSymbolType::Route => { - return Err(SemanticError::new( + return Err(AnalysisError::new( format!( "symbol `{}` is not assignable as a scalar target", entry.name @@ -1914,9 +1952,9 @@ impl<'a> Analyzer<'a> { ) } - fn assist_for_symbol_replacement(&self, symbol: SymbolId, span: Span) -> SemanticAssist { + fn assist_for_symbol_replacement(&self, symbol: SymbolId, span: Span) -> AnalysisAssist { let name = self.symbol_name(symbol).to_string(); - SemanticAssist::default() + AnalysisAssist::default() .context_label(self.symbol_span(symbol), self.symbol_declared_here(symbol)) .replacement_suggestion( span, @@ -1926,9 +1964,9 @@ impl<'a> Analyzer<'a> { ) } - fn assist_for_route_scalar(&self, route: SymbolId, span: Span) -> SemanticAssist { + fn assist_for_route_scalar(&self, route: SymbolId, span: Span) -> AnalysisAssist { let name = self.symbol_name(route).to_string(); - SemanticAssist::default() + AnalysisAssist::default() .context_label(self.symbol_span(route), self.symbol_declared_here(route)) .help(format!("route inputs are read through `rate({name})`")) .replacement_suggestion( @@ -1939,8 +1977,8 @@ impl<'a> Analyzer<'a> { ) } - fn assist_for_output_scope(&self, output: SymbolId) -> SemanticAssist { - SemanticAssist::default() + fn assist_for_output_scope(&self, output: SymbolId) -> AnalysisAssist { + AnalysisAssist::default() .context_label(self.symbol_span(output), self.symbol_declared_here(output)) .help( "outputs are assignment targets inside the `outputs` block and are not available as expression values", @@ -1952,7 +1990,7 @@ impl<'a> Analyzer<'a> { name: &syntax::Ident, span: Span, env: &BlockEnv, - ) -> Option { + ) -> Option { let mut seen = BTreeSet::new(); let mut candidates = Vec::new(); @@ -2013,7 +2051,7 @@ impl<'a> Analyzer<'a> { best_similar_name_assist(&name.text, candidates) } - fn assist_for_unknown_state(&self, state_name: &syntax::Ident) -> Option { + fn assist_for_unknown_state(&self, state_name: &syntax::Ident) -> Option { let candidates = self .globals .states @@ -2028,7 +2066,7 @@ impl<'a> Analyzer<'a> { best_similar_name_assist(&state_name.text, candidates) } - fn assist_for_unknown_route(&self, route_name: &syntax::Ident) -> Option { + fn assist_for_unknown_route(&self, route_name: &syntax::Ident) -> Option { let candidates = self .globals .routes @@ -2037,7 +2075,7 @@ impl<'a> Analyzer<'a> { let name = self.symbol_name(*symbol).to_string(); SimilarNameCandidate::new( name.clone(), - SemanticAssist::default() + AnalysisAssist::default() .context_label( self.symbol_span(*symbol), self.symbol_declared_here(*symbol), @@ -2054,14 +2092,14 @@ impl<'a> Analyzer<'a> { best_similar_name_assist(&route_name.text, candidates) } - fn assist_for_unknown_function(&self, callee: &syntax::Ident) -> Option { - let mut candidates = MathIntrinsic::ALL + fn assist_for_unknown_function(&self, callee: &syntax::Ident) -> Option { + let mut candidates = MathFunction::ALL .iter() .map(|intrinsic| { let name = intrinsic.name().to_string(); SimilarNameCandidate::new( name.clone(), - SemanticAssist::default().replacement_suggestion( + AnalysisAssist::default().replacement_suggestion( callee.span, name.clone(), format!("did you mean `{name}`?"), @@ -2072,7 +2110,7 @@ impl<'a> Analyzer<'a> { .collect::>(); candidates.push(SimilarNameCandidate::new( RATE_FUNCTION_NAME, - SemanticAssist::default() + AnalysisAssist::default() .help("`rate` reads route inputs as `rate(route)`") .replacement_suggestion( callee.span, @@ -2084,14 +2122,14 @@ impl<'a> Analyzer<'a> { best_similar_name_assist(&callee.text, candidates) } - fn finalize_symbols(self) -> Result, SemanticError> { + fn finalize_symbols(self) -> Result, AnalysisError> { self.symbols .into_iter() .map(|symbol| { let ty = match symbol.ty { PendingSymbolType::Scalar(Some(ty)) => SymbolType::Scalar(ty), PendingSymbolType::Scalar(None) => { - return Err(SemanticError::new( + return Err(AnalysisError::new( format!( "symbol `{}` does not have a resolved scalar type", symbol.name @@ -2118,10 +2156,10 @@ impl<'a> Analyzer<'a> { fn validate_kind_requirements( &self, sections: &ModelSections<'_>, - states: &[TypedState], - ) -> Result<(), SemanticError> { + states: &[AnalyzedState], + ) -> Result<(), AnalysisError> { if states.is_empty() { - return Err(SemanticError::new( + return Err(AnalysisError::new( format!( "model `{}` must declare at least one state", self.model.name.text @@ -2130,7 +2168,7 @@ impl<'a> Analyzer<'a> { )); } if sections.outputs.is_none() { - return Err(SemanticError::new( + return Err(AnalysisError::new( format!( "model `{}` is missing an `outputs` block", self.model.name.text @@ -2145,30 +2183,30 @@ impl<'a> Analyzer<'a> { &self, kind: ModelKind, blocks: ModelKindBlocks<'_>, - states: &[TypedState], - ) -> Result<(), SemanticError> { + states: &[AnalyzedState], + ) -> Result<(), AnalysisError> { match kind { ModelKind::Ode => { if blocks.dynamics.is_none() { - return Err(SemanticError::new( + return Err(AnalysisError::new( "ODE models require a `dynamics` block", self.model.span, )); } if blocks.drift.is_some() || blocks.diffusion.is_some() { - return Err(SemanticError::new( + return Err(AnalysisError::new( "ODE models cannot declare `drift` or `diffusion` blocks", self.model.span, )); } if blocks.analytical.is_some() { - return Err(SemanticError::new( + return Err(AnalysisError::new( "ODE models cannot declare an `analytical` block", self.model.span, )); } if let Some(particles_decl) = blocks.particles { - return Err(SemanticError::new( + return Err(AnalysisError::new( "ODE models cannot declare `particles`", particles_decl.span, )); @@ -2176,20 +2214,20 @@ impl<'a> Analyzer<'a> { } ModelKind::Analytical => { if blocks.analytical.is_none() { - return Err(SemanticError::new( + return Err(AnalysisError::new( "analytical models require an `analytical` block", self.model.span, )); } if blocks.dynamics.is_some() || blocks.drift.is_some() || blocks.diffusion.is_some() { - return Err(SemanticError::new( + return Err(AnalysisError::new( "analytical models cannot declare `dynamics`, `drift`, or `diffusion` blocks", self.model.span, )); } if let Some(particles_decl) = blocks.particles { - return Err(SemanticError::new( + return Err(AnalysisError::new( "analytical models cannot declare `particles`", particles_decl.span, )); @@ -2197,25 +2235,25 @@ impl<'a> Analyzer<'a> { } ModelKind::Sde => { if blocks.drift.is_none() || blocks.diffusion.is_none() { - return Err(SemanticError::new( + return Err(AnalysisError::new( "SDE models require both `drift` and `diffusion` blocks", self.model.span, )); } if blocks.dynamics.is_some() { - return Err(SemanticError::new( + return Err(AnalysisError::new( "SDE models cannot declare a `dynamics` block", self.model.span, )); } if blocks.analytical.is_some() { - return Err(SemanticError::new( + return Err(AnalysisError::new( "SDE models cannot declare an `analytical` block", self.model.span, )); } if blocks.particles.is_none() { - return Err(SemanticError::new( + return Err(AnalysisError::new( "SDE models require `particles`", self.model.span, )); @@ -2224,8 +2262,8 @@ impl<'a> Analyzer<'a> { } if states.is_empty() { - return Err(SemanticError::new( - "typed model validation requires at least one state", + return Err(AnalysisError::new( + "analyzed model validation requires at least one state", self.model.span, )); } @@ -2236,10 +2274,10 @@ impl<'a> Analyzer<'a> { &self, outputs: &[SymbolId], block: &BlockAnalysis, - ) -> Result<(), SemanticError> { + ) -> Result<(), AnalysisError> { for output in outputs { if !block.definite_targets.contains(output) { - return Err(SemanticError::new( + return Err(AnalysisError::new( format!( "output `{}` is not definitely assigned on all control-flow paths", self.symbol_name(*output) @@ -2258,13 +2296,13 @@ impl<'a> Analyzer<'a> { parameters: &[SymbolId], derived: &[SymbolId], derive_result: Option<&BlockAnalysis>, - ) -> Result<(), SemanticError> { + ) -> Result<(), AnalysisError> { let plan = AnalyticalStructureInputPlan::for_kernel( structure, parameters.iter().map(|symbol| self.symbol_name(*symbol)), derived.iter().map(|symbol| self.symbol_name(*symbol)), ) - .map_err(|error| SemanticError::new(error.to_string(), structure_span))?; + .map_err(|error| AnalysisError::new(error.to_string(), structure_span))?; let Some(derive_result) = derive_result else { return Ok(()); @@ -2297,7 +2335,7 @@ impl<'a> Analyzer<'a> { for (required_name, symbol) in required_derived_symbols { if !derive_result.available_derived.contains(&symbol) { - return Err(SemanticError::new( + return Err(AnalysisError::new( format!( "derived value `{required_name}` is not definitely assigned on all control-flow paths before analytical structure `{}` uses it", structure.name() @@ -2316,12 +2354,12 @@ impl<'a> Analyzer<'a> { fn validate_state_coverage( &self, block: &BlockAnalysis, - states: &[TypedState], + states: &[AnalyzedState], block_name: &str, - ) -> Result<(), SemanticError> { + ) -> Result<(), AnalysisError> { for state in states { if !block.touched_states.contains(&state.symbol) { - return Err(SemanticError::new( + return Err(AnalysisError::new( format!( "{block_name} block does not assign `{}`", self.symbol_name(state.symbol) @@ -2351,7 +2389,9 @@ fn canonical_numeric_suffix<'a>(src: &'a str, prefix: &str) -> Option<&'a str> { } fn numeric_label_literal_suffix(value: f64) -> Option { - (value.is_finite() && value >= 0.0 && value.fract() == 0.0 && value <= usize::MAX as f64) + // `usize::MAX as f64` rounds up to 2^64; keep the bound exclusive so the + // cast below never saturates. + (value.is_finite() && value >= 0.0 && value.fract() == 0.0 && value < usize::MAX as f64) .then(|| (value as usize).to_string()) } @@ -2412,7 +2452,7 @@ impl BlockEnv { } struct BlockAnalysis { - block: TypedStatementBlock, + block: AnalyzedStatementBlock, available_derived: BTreeSet, definite_targets: BTreeSet, touched_states: BTreeSet, @@ -2459,7 +2499,7 @@ struct ModelSections<'a> { } impl<'a> ModelSections<'a> { - fn from_model(model: &'a syntax::Model) -> Result { + fn from_model(model: &'a syntax::Model) -> Result { let mut sections = Self::default(); for item in &model.items { match item { @@ -2504,18 +2544,18 @@ impl<'a> ModelSections<'a> { } } -fn set_once<'a, T>(slot: &mut Option<&'a T>, value: &'a T, name: &str) -> Result<(), SemanticError> +fn set_once<'a, T>(slot: &mut Option<&'a T>, value: &'a T, name: &str) -> Result<(), AnalysisError> where T: HasSpan, { if let Some(existing) = *slot { - return Err(SemanticAssist::default() + return Err(AnalysisAssist::default() .context_label( existing.span(), format!("`{name}` section first declared here"), ) .help(format!("each model can declare `{name}` at most once")) - .apply(SemanticError::new( + .apply(AnalysisError::new( format!("duplicate `{name}` section in model body"), value.span(), ))); @@ -2527,10 +2567,10 @@ where fn best_similar_name_assist( needle: &str, candidates: Vec, -) -> Option { +) -> Option { let original_needle = needle; let needle = needle.to_ascii_lowercase(); - let mut best: Option<((usize, usize, usize), SemanticAssist)> = None; + let mut best: Option<((usize, usize, usize), AnalysisAssist)> = None; let mut tied = false; for candidate in candidates { @@ -2647,10 +2687,12 @@ fn collect_bare_assignment_names( } fn number_to_const(value: f64) -> ConstValue { + // `i64::MIN as f64` is exactly -2^63, but `i64::MAX as f64` rounds up to + // 2^63, so the upper bound must be exclusive to keep the cast lossless. if value.is_finite() && value.fract() == 0.0 && value >= i64::MIN as f64 - && value <= i64::MAX as f64 + && value < -(i64::MIN as f64) { ConstValue::Int(value as i64) } else { @@ -2670,137 +2712,136 @@ fn intersect_sets(set_a: &BTreeSet, set_b: &BTreeSet) -> BTr set_a.intersection(set_b).copied().collect() } -fn map_binary_op(op: syntax::BinaryOp) -> TypedBinaryOp { +fn map_binary_op(op: syntax::BinaryOp) -> AnalyzedBinaryOp { match op { - syntax::BinaryOp::Or => TypedBinaryOp::Or, - syntax::BinaryOp::And => TypedBinaryOp::And, - syntax::BinaryOp::Eq => TypedBinaryOp::Eq, - syntax::BinaryOp::NotEq => TypedBinaryOp::NotEq, - syntax::BinaryOp::Lt => TypedBinaryOp::Lt, - syntax::BinaryOp::LtEq => TypedBinaryOp::LtEq, - syntax::BinaryOp::Gt => TypedBinaryOp::Gt, - syntax::BinaryOp::GtEq => TypedBinaryOp::GtEq, - syntax::BinaryOp::Add => TypedBinaryOp::Add, - syntax::BinaryOp::Sub => TypedBinaryOp::Sub, - syntax::BinaryOp::Mul => TypedBinaryOp::Mul, - syntax::BinaryOp::Div => TypedBinaryOp::Div, - syntax::BinaryOp::Pow => TypedBinaryOp::Pow, - } -} - -fn call_result_type(intrinsic: MathIntrinsic, args: &[TypedExpr]) -> ValueType { + syntax::BinaryOp::Or => AnalyzedBinaryOp::Or, + syntax::BinaryOp::And => AnalyzedBinaryOp::And, + syntax::BinaryOp::Eq => AnalyzedBinaryOp::Eq, + syntax::BinaryOp::NotEq => AnalyzedBinaryOp::NotEq, + syntax::BinaryOp::Lt => AnalyzedBinaryOp::Lt, + syntax::BinaryOp::LtEq => AnalyzedBinaryOp::LtEq, + syntax::BinaryOp::Gt => AnalyzedBinaryOp::Gt, + syntax::BinaryOp::GtEq => AnalyzedBinaryOp::GtEq, + syntax::BinaryOp::Add => AnalyzedBinaryOp::Add, + syntax::BinaryOp::Sub => AnalyzedBinaryOp::Sub, + syntax::BinaryOp::Mul => AnalyzedBinaryOp::Mul, + syntax::BinaryOp::Div => AnalyzedBinaryOp::Div, + syntax::BinaryOp::Pow => AnalyzedBinaryOp::Pow, + } +} + +fn call_result_type(intrinsic: MathFunction, args: &[AnalyzedExpr]) -> ValueType { match intrinsic { - MathIntrinsic::Abs => args.first().map_or(ValueType::Real, |arg| arg.ty), - MathIntrinsic::Min | MathIntrinsic::Max => args + MathFunction::Abs => args.first().map_or(ValueType::Real, |arg| arg.ty), + MathFunction::Min | MathFunction::Max => args .iter() .map(|arg| arg.ty) .reduce(promote_numeric) .unwrap_or(ValueType::Real), - MathIntrinsic::Floor - | MathIntrinsic::Ceil - | MathIntrinsic::Exp - | MathIntrinsic::Ln - | MathIntrinsic::Log - | MathIntrinsic::Log10 - | MathIntrinsic::Log2 - | MathIntrinsic::Pow - | MathIntrinsic::Round - | MathIntrinsic::Sin - | MathIntrinsic::Cos - | MathIntrinsic::Tan - | MathIntrinsic::Sqrt => ValueType::Real, - } -} - -fn fold_unary(op: TypedUnaryOp, value: &ConstValue) -> Option { + MathFunction::Floor + | MathFunction::Ceil + | MathFunction::Exp + | MathFunction::Ln + | MathFunction::Log + | MathFunction::Log10 + | MathFunction::Log2 + | MathFunction::Pow + | MathFunction::Round + | MathFunction::Sin + | MathFunction::Cos + | MathFunction::Tan + | MathFunction::Sqrt => ValueType::Real, + } +} + +fn fold_unary(op: AnalyzedUnaryOp, value: &ConstValue) -> Option { match (op, value) { - (TypedUnaryOp::Plus, ConstValue::Int(value)) => Some(ConstValue::Int(*value)), - (TypedUnaryOp::Plus, ConstValue::Real(value)) => Some(ConstValue::Real(*value)), - (TypedUnaryOp::Minus, ConstValue::Int(value)) => Some(ConstValue::Int(-value)), - (TypedUnaryOp::Minus, ConstValue::Real(value)) => Some(ConstValue::Real(-value)), - (TypedUnaryOp::Not, ConstValue::Bool(value)) => Some(ConstValue::Bool(!value)), + (AnalyzedUnaryOp::Plus, ConstValue::Int(value)) => Some(ConstValue::Int(*value)), + (AnalyzedUnaryOp::Plus, ConstValue::Real(value)) => Some(ConstValue::Real(*value)), + (AnalyzedUnaryOp::Minus, ConstValue::Int(value)) => Some(match value.checked_neg() { + Some(negated) => ConstValue::Int(negated), + None => ConstValue::Real(-(*value as f64)), + }), + (AnalyzedUnaryOp::Minus, ConstValue::Real(value)) => Some(ConstValue::Real(-value)), + (AnalyzedUnaryOp::Not, ConstValue::Bool(value)) => Some(ConstValue::Bool(!value)), _ => None, } } -fn fold_binary(op: TypedBinaryOp, lhs: &ConstValue, rhs: &ConstValue) -> Option { +fn fold_binary(op: AnalyzedBinaryOp, lhs: &ConstValue, rhs: &ConstValue) -> Option { match op { - TypedBinaryOp::Or => Some(ConstValue::Bool( + AnalyzedBinaryOp::Or => Some(ConstValue::Bool( matches!(lhs, ConstValue::Bool(true)) || matches!(rhs, ConstValue::Bool(true)), )), - TypedBinaryOp::And => Some(ConstValue::Bool( + AnalyzedBinaryOp::And => Some(ConstValue::Bool( matches!(lhs, ConstValue::Bool(true)) && matches!(rhs, ConstValue::Bool(true)), )), - TypedBinaryOp::Eq => Some(ConstValue::Bool(lhs == rhs)), - TypedBinaryOp::NotEq => Some(ConstValue::Bool(lhs != rhs)), - TypedBinaryOp::Lt => Some(ConstValue::Bool(lhs.as_f64()? < rhs.as_f64()?)), - TypedBinaryOp::LtEq => Some(ConstValue::Bool(lhs.as_f64()? <= rhs.as_f64()?)), - TypedBinaryOp::Gt => Some(ConstValue::Bool(lhs.as_f64()? > rhs.as_f64()?)), - TypedBinaryOp::GtEq => Some(ConstValue::Bool(lhs.as_f64()? >= rhs.as_f64()?)), - TypedBinaryOp::Add => fold_numeric( - lhs, - rhs, - |left, right| left + right, - |left, right| left + right, - ), - TypedBinaryOp::Sub => fold_numeric( - lhs, - rhs, - |left, right| left - right, - |left, right| left - right, - ), - TypedBinaryOp::Mul => fold_numeric( - lhs, - rhs, - |left, right| left * right, - |left, right| left * right, - ), - TypedBinaryOp::Div => Some(ConstValue::Real(lhs.as_f64()? / rhs.as_f64()?)), - TypedBinaryOp::Pow => Some(ConstValue::Real(lhs.as_f64()?.powf(rhs.as_f64()?))), + AnalyzedBinaryOp::Eq => Some(ConstValue::Bool(lhs == rhs)), + AnalyzedBinaryOp::NotEq => Some(ConstValue::Bool(lhs != rhs)), + AnalyzedBinaryOp::Lt => Some(ConstValue::Bool(lhs.as_f64()? < rhs.as_f64()?)), + AnalyzedBinaryOp::LtEq => Some(ConstValue::Bool(lhs.as_f64()? <= rhs.as_f64()?)), + AnalyzedBinaryOp::Gt => Some(ConstValue::Bool(lhs.as_f64()? > rhs.as_f64()?)), + AnalyzedBinaryOp::GtEq => Some(ConstValue::Bool(lhs.as_f64()? >= rhs.as_f64()?)), + AnalyzedBinaryOp::Add => { + fold_numeric(lhs, rhs, i64::checked_add, |left, right| left + right) + } + AnalyzedBinaryOp::Sub => { + fold_numeric(lhs, rhs, i64::checked_sub, |left, right| left - right) + } + AnalyzedBinaryOp::Mul => { + fold_numeric(lhs, rhs, i64::checked_mul, |left, right| left * right) + } + AnalyzedBinaryOp::Div => Some(ConstValue::Real(lhs.as_f64()? / rhs.as_f64()?)), + AnalyzedBinaryOp::Pow => Some(ConstValue::Real(lhs.as_f64()?.powf(rhs.as_f64()?))), } } fn fold_numeric( lhs: &ConstValue, rhs: &ConstValue, - int_op: impl FnOnce(i64, i64) -> i64, + int_op: impl FnOnce(i64, i64) -> Option, real_op: impl FnOnce(f64, f64) -> f64, ) -> Option { match (lhs, rhs) { - (ConstValue::Int(lhs), ConstValue::Int(rhs)) => Some(ConstValue::Int(int_op(*lhs, *rhs))), + (ConstValue::Int(lhs), ConstValue::Int(rhs)) => Some(match int_op(*lhs, *rhs) { + Some(value) => ConstValue::Int(value), + // Overflowing integer arithmetic degrades to `Real`, matching the + // f64 arithmetic the backends perform at runtime. + None => ConstValue::Real(real_op(*lhs as f64, *rhs as f64)), + }), _ => Some(ConstValue::Real(real_op(lhs.as_f64()?, rhs.as_f64()?))), } } -fn fold_call(intrinsic: MathIntrinsic, values: &[ConstValue]) -> Option { +fn fold_call(intrinsic: MathFunction, values: &[ConstValue]) -> Option { match intrinsic { - MathIntrinsic::Abs => match values.first()? { + MathFunction::Abs => match values.first()? { ConstValue::Int(value) => Some(ConstValue::Int(value.abs())), ConstValue::Real(value) => Some(ConstValue::Real(value.abs())), ConstValue::Bool(_) => None, }, - MathIntrinsic::Ceil => Some(ConstValue::Real(values.first()?.as_f64()?.ceil())), - MathIntrinsic::Exp => Some(ConstValue::Real(values.first()?.as_f64()?.exp())), - MathIntrinsic::Floor => Some(ConstValue::Real(values.first()?.as_f64()?.floor())), - MathIntrinsic::Ln | MathIntrinsic::Log => { + MathFunction::Ceil => Some(ConstValue::Real(values.first()?.as_f64()?.ceil())), + MathFunction::Exp => Some(ConstValue::Real(values.first()?.as_f64()?.exp())), + MathFunction::Floor => Some(ConstValue::Real(values.first()?.as_f64()?.floor())), + MathFunction::Ln | MathFunction::Log => { Some(ConstValue::Real(values.first()?.as_f64()?.ln())) } - MathIntrinsic::Log10 => Some(ConstValue::Real(values.first()?.as_f64()?.log10())), - MathIntrinsic::Log2 => Some(ConstValue::Real(values.first()?.as_f64()?.log2())), - MathIntrinsic::Max => Some(ConstValue::Real( + MathFunction::Log10 => Some(ConstValue::Real(values.first()?.as_f64()?.log10())), + MathFunction::Log2 => Some(ConstValue::Real(values.first()?.as_f64()?.log2())), + MathFunction::Max => Some(ConstValue::Real( values.first()?.as_f64()?.max(values.get(1)?.as_f64()?), )), - MathIntrinsic::Min => Some(ConstValue::Real( + MathFunction::Min => Some(ConstValue::Real( values.first()?.as_f64()?.min(values.get(1)?.as_f64()?), )), - MathIntrinsic::Pow => Some(ConstValue::Real( + MathFunction::Pow => Some(ConstValue::Real( values.first()?.as_f64()?.powf(values.get(1)?.as_f64()?), )), - MathIntrinsic::Round => Some(ConstValue::Real(values.first()?.as_f64()?.round())), - MathIntrinsic::Sin => Some(ConstValue::Real(values.first()?.as_f64()?.sin())), - MathIntrinsic::Cos => Some(ConstValue::Real(values.first()?.as_f64()?.cos())), - MathIntrinsic::Tan => Some(ConstValue::Real(values.first()?.as_f64()?.tan())), - MathIntrinsic::Sqrt => Some(ConstValue::Real(values.first()?.as_f64()?.sqrt())), + MathFunction::Round => Some(ConstValue::Real(values.first()?.as_f64()?.round())), + MathFunction::Sin => Some(ConstValue::Real(values.first()?.as_f64()?.sin())), + MathFunction::Cos => Some(ConstValue::Real(values.first()?.as_f64()?.cos())), + MathFunction::Tan => Some(ConstValue::Real(values.first()?.as_f64()?.tan())), + MathFunction::Sqrt => Some(ConstValue::Real(values.first()?.as_f64()?.sqrt())), } } @@ -2817,21 +2858,21 @@ mod tests { fn analyzes_structured_block_corpus() { let src = STRUCTURED_BLOCK_CORPUS; let module = parse_module(src).expect("structured-block fixture parses"); - let typed = analyze_module(&module).expect("structured-block fixture analyzes"); + let analyzed = analyze_module(&module).expect("structured-block fixture analyzes"); - assert_eq!(typed.models.len(), 4); - let transit = &typed.models[1]; + assert_eq!(analyzed.models.len(), 4); + let transit = &analyzed.models[1]; assert_eq!(transit.kind, ModelKind::Ode); assert_eq!(transit.states[0].size, Some(4)); assert!(transit.dynamics.is_some()); - let analytical = &typed.models[2]; + let analytical = &analyzed.models[2]; assert!(matches!( analytical.analytical.as_ref().map(|value| value.structure), Some(AnalyticalKernel::OneCompartmentWithAbsorption) )); - let sde = &typed.models[3]; + let sde = &analyzed.models[3]; assert_eq!(sde.particles, Some(1000)); assert!(sde.drift.is_some()); assert!(sde.diffusion.is_some()); @@ -2841,8 +2882,8 @@ mod tests { fn derives_values_across_if_branches() { let src = STRUCTURED_BLOCK_CORPUS; let model = parse_model(src.split("\n\n\n").next().unwrap()).expect("single model parses"); - let typed = analyze_model(&model).expect("single model analyzes"); - let ke_symbol = typed + let analyzed = analyze_model(&model).expect("single model analyzes"); + let ke_symbol = analyzed .symbols .iter() .find(|symbol| symbol.name == "ke") @@ -2871,9 +2912,9 @@ model analytical_ok { "#; let model = parse_model(src).expect("model parses"); - let typed = analyze_model(&model).expect("model analyzes"); + let analyzed = analyze_model(&model).expect("model analyzes"); assert!(matches!( - typed.analytical.as_ref().map(|value| value.structure), + analyzed.analytical.as_ref().map(|value| value.structure), Some(AnalyticalKernel::OneCompartmentWithAbsorption) )); } @@ -3422,7 +3463,7 @@ model broken { .contains("state array size must be an integer constant")); } - fn typed_model_signature(model: &TypedModel) -> String { + fn typed_model_signature(model: &AnalyzedModel) -> String { let mut lines = Vec::new(); lines.push(format!("kind:{:?}", model.kind)); lines.push(format!( @@ -3487,14 +3528,14 @@ model broken { lines.join("\n") } - fn join_names(model: &TypedModel, ids: &[SymbolId]) -> String { + fn join_names(model: &AnalyzedModel, ids: &[SymbolId]) -> String { ids.iter() .map(|id| symbol_name(model, *id)) .collect::>() .join(",") } - fn join_constants(model: &TypedModel) -> String { + fn join_constants(model: &AnalyzedModel) -> String { model .constants .iter() @@ -3509,7 +3550,7 @@ model broken { .join(",") } - fn join_covariates(model: &TypedModel) -> String { + fn join_covariates(model: &AnalyzedModel) -> String { model .covariates .iter() @@ -3524,7 +3565,7 @@ model broken { .join(",") } - fn join_states(model: &TypedModel) -> String { + fn join_states(model: &AnalyzedModel) -> String { model .states .iter() @@ -3533,7 +3574,7 @@ model broken { .join(",") } - fn join_routes(model: &TypedModel) -> String { + fn join_routes(model: &AnalyzedModel) -> String { model .routes .iter() @@ -3562,7 +3603,7 @@ model broken { .join(",") } - fn block_signature(model: &TypedModel, block: &TypedStatementBlock) -> String { + fn block_signature(model: &AnalyzedModel, block: &AnalyzedStatementBlock) -> String { block .statements .iter() @@ -3571,19 +3612,19 @@ model broken { .join(";") } - fn stmt_signature(model: &TypedModel, stmt: &TypedStmt) -> String { + fn stmt_signature(model: &AnalyzedModel, stmt: &AnalyzedStmt) -> String { match &stmt.kind { - TypedStmtKind::Let(value) => format!( + AnalyzedStmtKind::Let(value) => format!( "let({}:{})", symbol_name(model, value.symbol), expr_signature(model, &value.value) ), - TypedStmtKind::Assign(value) => format!( + AnalyzedStmtKind::Assign(value) => format!( "assign({}={})", assign_target_signature(model, &value.target), expr_signature(model, &value.value) ), - TypedStmtKind::If(value) => format!( + AnalyzedStmtKind::If(value) => format!( "if({}){{{}}}else{{{}}}", expr_signature(model, &value.condition), value @@ -3602,7 +3643,7 @@ model broken { .join(";")) .unwrap_or_default() ), - TypedStmtKind::For(value) => format!( + AnalyzedStmtKind::For(value) => format!( "for({}:{}..{}){{{}}}", symbol_name(model, value.binding), expr_signature(model, &value.range.start), @@ -3617,27 +3658,27 @@ model broken { } } - fn assign_target_signature(model: &TypedModel, target: &TypedAssignTarget) -> String { + fn assign_target_signature(model: &AnalyzedModel, target: &AnalyzedAssignTarget) -> String { match &target.kind { - TypedAssignTargetKind::Derived(symbol) => { + AnalyzedAssignTargetKind::Derived(symbol) => { format!("derived:{}", symbol_name(model, *symbol)) } - TypedAssignTargetKind::Output(symbol) => { + AnalyzedAssignTargetKind::Output(symbol) => { format!("output:{}", symbol_name(model, *symbol)) } - TypedAssignTargetKind::StateInit(place) => { + AnalyzedAssignTargetKind::StateInit(place) => { format!("init:{}", state_place_signature(model, place)) } - TypedAssignTargetKind::Derivative(place) => { + AnalyzedAssignTargetKind::Derivative(place) => { format!("ddt:{}", state_place_signature(model, place)) } - TypedAssignTargetKind::Noise(place) => { + AnalyzedAssignTargetKind::Noise(place) => { format!("noise:{}", state_place_signature(model, place)) } } } - fn state_place_signature(model: &TypedModel, place: &TypedStatePlace) -> String { + fn state_place_signature(model: &AnalyzedModel, place: &AnalyzedStatePlace) -> String { let name = symbol_name(model, place.state); match &place.index { Some(index) => format!("{}[{}]", name, expr_signature(model, index)), @@ -3645,31 +3686,31 @@ model broken { } } - fn expr_signature(model: &TypedModel, expr: &TypedExpr) -> String { + fn expr_signature(model: &AnalyzedModel, expr: &AnalyzedExpr) -> String { match &expr.kind { - TypedExprKind::Literal(value) => format!("lit:{value:?}:{:?}", expr.ty), - TypedExprKind::Symbol(symbol) => { + AnalyzedExprKind::Literal(value) => format!("lit:{value:?}:{:?}", expr.ty), + AnalyzedExprKind::Symbol(symbol) => { format!("sym:{}:{:?}", symbol_name(model, *symbol), expr.ty) } - TypedExprKind::StateValue(place) => format!( + AnalyzedExprKind::StateValue(place) => format!( "state:{}:{:?}", state_place_signature(model, place), expr.ty ), - TypedExprKind::Unary { op, expr: inner } => { + AnalyzedExprKind::Unary { op, expr: inner } => { format!("un:{op:?}:{}", expr_signature(model, inner)) } - TypedExprKind::Binary { op, lhs, rhs } => format!( + AnalyzedExprKind::Binary { op, lhs, rhs } => format!( "bin:{op:?}:{}:{}:{:?}", expr_signature(model, lhs), expr_signature(model, rhs), expr.ty ), - TypedExprKind::Call { callee, args } => format!( + AnalyzedExprKind::Call { callee, args } => format!( "call:{}({})", match callee { - TypedCall::Math(intrinsic) => format!("math:{intrinsic:?}"), - TypedCall::Rate(symbol) => format!("rate:{}", symbol_name(model, *symbol)), + AnalyzedCall::Math(intrinsic) => format!("math:{intrinsic:?}"), + AnalyzedCall::Rate(symbol) => format!("rate:{}", symbol_name(model, *symbol)), }, args.iter() .map(|arg| expr_signature(model, arg)) @@ -3679,7 +3720,7 @@ model broken { } } - fn symbol_name(model: &TypedModel, symbol: SymbolId) -> String { + fn symbol_name(model: &AnalyzedModel, symbol: SymbolId) -> String { model .symbols .iter() diff --git a/pharmsol-dsl/src/authoring.rs b/pharmsol-dsl/src/authoring.rs index 4233d7a5..91a16bf4 100644 --- a/pharmsol-dsl/src/authoring.rs +++ b/pharmsol-dsl/src/authoring.rs @@ -1,8 +1,8 @@ use std::collections::{BTreeMap, BTreeSet}; -use super::ast::*; use super::diagnostic::{Applicability, DiagnosticSuggestion, ParseError, Span, TextEdit}; use super::parser::{parse_expr_fragment, parse_place_fragment}; +use super::syntax::*; use crate::name_match::{ common_prefix_len, edit_distance, is_high_confidence_match, is_single_adjacent_transposition, }; @@ -310,8 +310,8 @@ impl<'a> AuthoringParser<'a> { let span = Span::new(line_offset + leading, line_offset + trailing); self.note_span(span); - if find_top_level_arrow(trimmed).is_some() { - return self.parse_route_line(trimmed, span.start, span); + if let Some(arrow) = find_top_level_arrow(trimmed) { + return self.parse_route_line(trimmed, arrow, span.start, span); } let eq_index = find_top_level_assignment(trimmed).ok_or_else(|| { @@ -421,9 +421,9 @@ impl<'a> AuthoringParser<'a> { return Ok(()); } - if lhs_trimmed == "kernel" { + if lhs_trimmed == "function" { return Err(ParseError::new( - "`kernel = ...` has been renamed to `structure = ...`", + "`function = ...` has been renamed to `structure = ...`", span, )); } @@ -435,7 +435,7 @@ impl<'a> AuthoringParser<'a> { } if let Some(name_segment) = lhs_trimmed.strip_prefix("const ") { - let name_abs = span.start + (lhs.find("const").unwrap() + "const ".len()); + let name_abs = span.start + (lhs.len() - lhs_trimmed.len()) + "const ".len(); let name = parse_ident_segment(name_segment, name_abs)?; let value = parse_expr_at(rhs, rhs_abs)?; self.constants.push(Binding { @@ -484,10 +484,10 @@ impl<'a> AuthoringParser<'a> { fn parse_route_line( &mut self, trimmed: &str, + arrow: usize, line_start: usize, span: Span, ) -> Result<(), ParseError> { - let arrow = find_top_level_arrow(trimmed).unwrap(); let lhs = &trimmed[..arrow]; let rhs = &trimmed[arrow + 2..]; let call = parse_call_head(lhs, line_start)? @@ -1168,15 +1168,32 @@ fn parse_expr_at(src: &str, abs_start: usize) -> Result { } fn parse_surface_rhs(src: &str, abs_start: usize) -> Result { + parse_surface_rhs_at(src, abs_start, 0) +} + +fn parse_surface_rhs_at( + src: &str, + abs_start: usize, + depth: usize, +) -> Result { let trimmed = src.trim_start(); let leading = src.len() - trimmed.len(); if starts_with_keyword(trimmed, "if") { - return parse_if_rhs(trimmed, abs_start + leading); + return parse_if_rhs(trimmed, abs_start + leading, depth); } Ok(SurfaceRhs::Expr(parse_expr_at(src, abs_start)?)) } -fn parse_if_rhs(src: &str, abs_start: usize) -> Result { +fn parse_if_rhs(src: &str, abs_start: usize, depth: usize) -> Result { + if depth >= crate::parser::MAX_NESTING_DEPTH { + return Err(ParseError::new( + format!( + "conditional expression is nested too deeply (maximum nesting depth is {})", + crate::parser::MAX_NESTING_DEPTH + ), + Span::new(abs_start, abs_start + src.len().min(2)), + )); + } let rest = &src[2..]; let rest_leading = rest.len() - rest.trim_start().len(); let rest = &rest[rest_leading..]; @@ -1206,9 +1223,12 @@ fn parse_if_rhs(src: &str, abs_start: usize) -> Result { })?; let condition = parse_expr_at(condition_src, rest_abs + 1)?; - let then_branch = parse_surface_rhs(&remaining[..else_index], remaining_abs)?; - let else_branch = - parse_surface_rhs(&remaining[else_index + 4..], remaining_abs + else_index + 4)?; + let then_branch = parse_surface_rhs_at(&remaining[..else_index], remaining_abs, depth + 1)?; + let else_branch = parse_surface_rhs_at( + &remaining[else_index + 4..], + remaining_abs + else_index + 4, + depth + 1, + )?; let span = Span::new(abs_start, remaining_abs + remaining.len()); Ok(SurfaceRhs::If { diff --git a/pharmsol-dsl/src/diagnostic.rs b/pharmsol-dsl/src/diagnostic.rs index f7a1adda..e153b45a 100644 --- a/pharmsol-dsl/src/diagnostic.rs +++ b/pharmsol-dsl/src/diagnostic.rs @@ -1,3 +1,5 @@ +//! Spans, diagnostic codes, and rendered error reports for the DSL pipeline. + use std::fmt; use std::sync::Arc; @@ -58,8 +60,8 @@ impl fmt::Display for DiagnosticCode { } pub const DSL_PARSE_GENERIC: DiagnosticCode = DiagnosticCode::new("DSL1000"); -pub const DSL_SEMANTIC_GENERIC: DiagnosticCode = DiagnosticCode::new("DSL2000"); -pub const DSL_LOWERING_GENERIC: DiagnosticCode = DiagnosticCode::new("DSL3000"); +pub const DSL_ANALYSIS_GENERIC: DiagnosticCode = DiagnosticCode::new("DSL2000"); +pub const DSL_COMPILE_GENERIC: DiagnosticCode = DiagnosticCode::new("DSL3000"); pub const DSL_BACKEND_GENERIC: DiagnosticCode = DiagnosticCode::new("DSL4000"); #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -70,8 +72,8 @@ pub enum DiagnosticSeverity { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DiagnosticPhase { Parse, - Semantic, - Lowering, + Analysis, + Compile, Backend, } @@ -237,8 +239,19 @@ impl Diagnostic { .to_string() .len(); rendered.push_str(&format!("{:>width$} |\n", "", width = gutter)); + let mut last_line = None; for label in &self.labels { - rendered.push_str(&render_label(src, label, &self.message, gutter)); + let label_line = line_info(src, label.span.start.min(src.len())).0; + // Repeat the source text only when the label moves to a new line. + let show_source = last_line != Some(label_line); + rendered.push_str(&render_label( + src, + label, + &self.message, + gutter, + show_source, + )); + last_line = Some(label_line); } } for note in &self.notes { @@ -440,8 +453,8 @@ impl DiagnosticPhase { fn as_str(self) -> &'static str { match self { Self::Parse => "parse", - Self::Semantic => "semantic", - Self::Lowering => "lowering", + Self::Analysis => "analysis", + Self::Compile => "compile", Self::Backend => "backend", } } @@ -500,6 +513,15 @@ impl ParseError { } } + /// Marker propagated after a fatal parse error was recorded; carries no + /// diagnostics of its own and is always merged into a non-empty error. + pub(crate) fn aborted() -> Self { + Self { + diagnostics: Vec::new(), + source: None, + } + } + pub fn with_note(mut self, note: impl Into) -> Self { self.diagnostics[0].notes.push(note.into()); self @@ -580,14 +602,22 @@ impl fmt::Display for ParseError { if let Some(source) = self.source() { return f.write_str(&self.render(source)); } - let span = self.diagnostic().primary_span(); + let diagnostic = self.diagnostic(); + let span = diagnostic.primary_span(); write!( f, - "{} at bytes {}..{}", - self.diagnostic().message, - span.start, - span.end - ) + "error[{}]: {} (at bytes {}..{})", + diagnostic.code, diagnostic.message, span.start, span.end + )?; + let remaining = self.diagnostics.len() - 1; + if remaining > 0 { + write!( + f, + " (+{remaining} more error{})", + if remaining == 1 { "" } else { "s" } + )?; + } + Ok(()) } } @@ -606,10 +636,10 @@ fn render_label( label: &DiagnosticLabel, fallback_message: &str, gutter: usize, + show_source: bool, ) -> String { let offset = label.span.start.min(src.len()); let (line, _, line_start, line_end) = line_info(src, offset); - let line_text = &src[line_start..line_end]; let marker_start = src[line_start..offset].chars().count(); let highlight_end = label.span.end.min(line_end).max(offset); let marker_len = src[offset..highlight_end].chars().count().max(1); @@ -624,12 +654,14 @@ fn render_label( }); let mut rendered = String::new(); - rendered.push_str(&format!( - "{:>width$} | {}\n", - line, - line_text, - width = gutter - )); + if show_source { + rendered.push_str(&format!( + "{:>width$} | {}\n", + line, + &src[line_start..line_end], + width = gutter + )); + } rendered.push_str(&format!( "{:>width$} | {}{}", "", diff --git a/pharmsol-dsl/src/execution.rs b/pharmsol-dsl/src/execution.rs index 2384432a..98c97050 100644 --- a/pharmsol-dsl/src/execution.rs +++ b/pharmsol-dsl/src/execution.rs @@ -1,19 +1,30 @@ +//! Ready-to-run model compiled from an analyzed model. +//! +//! An [`ExecutionModel`] carries everything a simulation backend needs: +//! resolved symbols, dense buffer layouts, and the model functions +//! (dynamics, outputs, init, and friends) as straight-line programs. + use std::collections::BTreeMap; use std::fmt; use std::sync::Arc; use crate::{ - AnalyticalKernel, ConstValue, CovariateInterpolation, Diagnostic, DiagnosticPhase, - DiagnosticReport, MathIntrinsic, ModelKind, RouteKind, RoutePropertyKind, Span, Symbol, - SymbolId, SymbolKind, SymbolType, TypedAssignTargetKind, TypedBinaryOp, TypedCall, TypedExpr, - TypedExprKind, TypedModel, TypedModule, TypedRangeExpr, TypedStatePlace, TypedStatementBlock, - TypedStmt, TypedStmtKind, TypedUnaryOp, ValueType, DSL_LOWERING_GENERIC, + AnalyticalKernel, AnalyzedAssignTargetKind, AnalyzedBinaryOp, AnalyzedCall, AnalyzedExpr, + AnalyzedExprKind, AnalyzedModel, AnalyzedModule, AnalyzedRangeExpr, AnalyzedStatePlace, + AnalyzedStatementBlock, AnalyzedStmt, AnalyzedStmtKind, AnalyzedUnaryOp, ConstValue, + CovariateInterpolation, Diagnostic, DiagnosticPhase, DiagnosticReport, MathFunction, ModelKind, + RouteKind, RoutePropertyKind, Span, Symbol, SymbolId, SymbolKind, SymbolType, ValueType, + DSL_COMPILE_GENERIC, }; -pub fn lower_typed_module(module: &TypedModule) -> Result { +/// Compiles every model in an analyzed module into its ready-to-run form. +/// +/// This is the final pipeline stage after [`parse_module`](crate::parse_module) +/// and [`analyze_module`](crate::analyze_module). +pub fn compile_analyzed_module(module: &AnalyzedModule) -> Result { let mut models = Vec::with_capacity(module.models.len()); for model in &module.models { - models.push(lower_typed_model(model)?); + models.push(compile_analyzed_model(model)?); } Ok(ExecutionModule { models, @@ -21,8 +32,12 @@ pub fn lower_typed_module(module: &TypedModule) -> Result Result { - ExecutionLowerer::new(model)?.lower() +/// Compiles an analyzed model into its ready-to-run [`ExecutionModel`]. +/// +/// This is the final pipeline stage after [`parse_model`](crate::parse_model) +/// and [`analyze_model`](crate::analyze_model). +pub fn compile_analyzed_model(model: &AnalyzedModel) -> Result { + ModelCompiler::new(model)?.compile() } #[derive(Debug, Clone, PartialEq)] @@ -36,14 +51,14 @@ pub struct ExecutionModel { pub name: String, pub kind: ModelKind, pub metadata: ExecutionMetadata, - pub abi: ExecutionAbi, - pub kernels: Vec, + pub layout: ExecutionLayout, + pub functions: Vec, pub span: Span, } impl ExecutionModel { - pub fn kernel(&self, role: KernelRole) -> Option<&ExecutionKernel> { - self.kernels.iter().find(|kernel| kernel.role == role) + pub fn function(&self, kind: ModelFunctionKind) -> Option<&ModelFunction> { + self.functions.iter().find(|function| function.kind == kind) } } @@ -116,29 +131,23 @@ pub struct RouteDestination { } #[derive(Debug, Clone, PartialEq, Eq)] -pub struct ExecutionAbi { - pub scalar: ScalarAbi, - pub calling_convention: CallingConvention, - pub parameter_buffer: DenseBufferLayout, - pub covariate_buffer: DenseBufferLayout, - pub state_buffer: DenseBufferLayout, - pub derived_buffer: DenseBufferLayout, - pub output_buffer: DenseBufferLayout, - pub route_buffer: DenseBufferLayout, +pub struct ExecutionLayout { + pub scalar: ScalarType, + pub parameter_buffer: BufferLayout, + pub covariate_buffer: BufferLayout, + pub state_buffer: BufferLayout, + pub derived_buffer: BufferLayout, + pub output_buffer: BufferLayout, + pub route_buffer: BufferLayout, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ScalarAbi { +pub enum ScalarType { F64, } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum CallingConvention { - DenseF64Buffers, -} - #[derive(Debug, Clone, PartialEq, Eq)] -pub struct DenseBufferLayout { +pub struct BufferLayout { pub kind: BufferKind, pub len: usize, pub slots: Vec, @@ -162,15 +171,15 @@ pub struct BufferSlot { } #[derive(Debug, Clone, PartialEq)] -pub struct ExecutionKernel { - pub role: KernelRole, - pub signature: KernelSignature, - pub implementation: KernelImplementation, +pub struct ModelFunction { + pub kind: ModelFunctionKind, + pub signature: FunctionSignature, + pub body: FunctionBody, pub span: Span, } #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub enum KernelRole { +pub enum ModelFunctionKind { Derive, Dynamics, Outputs, @@ -183,18 +192,18 @@ pub enum KernelRole { } #[derive(Debug, Clone, PartialEq, Eq)] -pub struct KernelSignature { - pub args: Vec, +pub struct FunctionSignature { + pub args: Vec, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct KernelArgument { - pub kind: KernelArgumentKind, - pub access: KernelAccess, +pub struct FunctionArgument { + pub kind: FunctionArgumentKind, + pub access: Access, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum KernelArgumentKind { +pub enum FunctionArgumentKind { Time, Parameters, Covariates, @@ -211,13 +220,13 @@ pub enum KernelArgumentKind { } #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum KernelAccess { +pub enum Access { Input, Output, } #[derive(Debug, Clone, PartialEq)] -pub enum KernelImplementation { +pub enum FunctionBody { Statements(ExecutionProgram), AnalyticalBuiltin(AnalyticalKernel), } @@ -330,11 +339,11 @@ pub enum ExecutionExprKind { Literal(ConstValue), Load(ExecutionLoad), Unary { - op: TypedUnaryOp, + op: AnalyzedUnaryOp, expr: Box, }, Binary { - op: TypedBinaryOp, + op: AnalyzedBinaryOp, lhs: Box, rhs: Box, }, @@ -356,21 +365,21 @@ pub enum ExecutionLoad { #[derive(Debug, Clone, PartialEq)] pub enum ExecutionCall { - Math(MathIntrinsic), + Math(MathFunction), } #[derive(Clone, PartialEq, Eq)] -pub struct LoweringError { +pub struct CompileError { diagnostic: Box, source: Option>, } -impl LoweringError { +impl CompileError { fn new(message: impl Into, span: Span) -> Self { Self { diagnostic: Box::new(Diagnostic::error( - DSL_LOWERING_GENERIC, - DiagnosticPhase::Lowering, + DSL_COMPILE_GENERIC, + DiagnosticPhase::Compile, message, span, )), @@ -413,13 +422,13 @@ impl LoweringError { } } -impl fmt::Debug for LoweringError { +impl fmt::Debug for CompileError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Display::fmt(self, f) } } -impl fmt::Display for LoweringError { +impl fmt::Display for CompileError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if let Some(source) = self.source() { return f.write_str(&self.render(source)); @@ -427,16 +436,16 @@ impl fmt::Display for LoweringError { let span = self.diagnostic.primary_span(); write!( f, - "{} at bytes {}..{}", - self.diagnostic.message, span.start, span.end + "error[{}]: {} (at bytes {}..{})", + self.diagnostic.code, self.diagnostic.message, span.start, span.end ) } } -impl std::error::Error for LoweringError {} +impl std::error::Error for CompileError {} -struct ExecutionLowerer<'a> { - model: &'a TypedModel, +struct ModelCompiler<'a> { + model: &'a AnalyzedModel, metadata: ExecutionMetadata, symbol_map: BTreeMap, parameter_slots: BTreeMap, @@ -453,8 +462,8 @@ struct StateLayout { len: usize, } -impl<'a> ExecutionLowerer<'a> { - fn new(model: &'a TypedModel) -> Result { +impl<'a> ModelCompiler<'a> { + fn new(model: &'a AnalyzedModel) -> Result { let symbol_map: BTreeMap = model .symbols .iter() @@ -473,7 +482,7 @@ impl<'a> ExecutionLowerer<'a> { span: constant.span, }) }) - .collect::, LoweringError>>()?; + .collect::, CompileError>>()?; let mut parameter_slots = BTreeMap::new(); let parameters = model @@ -490,7 +499,7 @@ impl<'a> ExecutionLowerer<'a> { span: symbol.span, }) }) - .collect::, LoweringError>>()?; + .collect::, CompileError>>()?; let mut covariate_slots = BTreeMap::new(); let covariates = model @@ -508,7 +517,7 @@ impl<'a> ExecutionLowerer<'a> { span: covariate.span, }) }) - .collect::, LoweringError>>()?; + .collect::, CompileError>>()?; let mut state_slots = BTreeMap::new(); let mut states = Vec::with_capacity(model.states.len()); @@ -530,7 +539,12 @@ impl<'a> ExecutionLowerer<'a> { len, span: state.span, }); - next_state_offset += len; + next_state_offset = next_state_offset.checked_add(len).ok_or_else(|| { + CompileError::new( + "combined state sizes exceed the supported state space", + state.span, + ) + })?; } let uses_authoring_route_kinds = @@ -547,7 +561,7 @@ impl<'a> ExecutionLowerer<'a> { RoutePropertyKind::Lag => "lag", RoutePropertyKind::Bioavailability => "bioavailability", }; - return Err(LoweringError::new( + return Err(CompileError::new( format!( "DSL authoring does not allow `{label}` on infusion route `{}`", symbol.name @@ -557,25 +571,25 @@ impl<'a> ExecutionLowerer<'a> { .with_note("lag and bioavailability are bolus-only route properties")); } } - let index = if uses_authoring_route_kinds { - match route.kind.expect("authoring routes must preserve kind") { - RouteKind::Bolus => { - let index = next_bolus_index; - next_bolus_index += 1; - index - } - RouteKind::Infusion => { - let index = next_infusion_index; - next_infusion_index += 1; - index - } + // Authoring source always records a kind per route; canonical + // `model {}` source never does. Mixed models fall back to + // declaration order. + let index = match (uses_authoring_route_kinds, route.kind) { + (true, Some(RouteKind::Bolus)) => { + let index = next_bolus_index; + next_bolus_index += 1; + index } - } else { - declaration_index + (true, Some(RouteKind::Infusion)) => { + let index = next_infusion_index; + next_infusion_index += 1; + index + } + _ => declaration_index, }; route_slots.insert(route.symbol, index); let destination = - lower_route_destination(&symbol_map, &state_slots, &route.destination)?; + compile_route_destination(&symbol_map, &state_slots, &route.destination)?; routes.push(ExecutionRoute { symbol: route.symbol, name: symbol.name.clone(), @@ -610,7 +624,7 @@ impl<'a> ExecutionLowerer<'a> { span: symbol.span, }) }) - .collect::, LoweringError>>()?; + .collect::, CompileError>>()?; let mut output_slots = BTreeMap::new(); let outputs = model @@ -627,7 +641,7 @@ impl<'a> ExecutionLowerer<'a> { span: symbol.span, }) }) - .collect::, LoweringError>>()?; + .collect::, CompileError>>()?; Ok(Self { model, @@ -655,61 +669,62 @@ impl<'a> ExecutionLowerer<'a> { }) } - fn lower(self) -> Result { - let abi = self.build_abi(); - let mut kernels = Vec::new(); + fn compile(self) -> Result { + let layout = self.build_layout(); + let mut functions = Vec::new(); if let Some(block) = &self.model.derive { - kernels.push(self.lower_statement_kernel(KernelRole::Derive, block)?); + functions.push(self.compile_statement_function(ModelFunctionKind::Derive, block)?); } if let Some(block) = &self.model.init { - kernels.push(self.lower_init_kernel(block)?); + functions.push(self.compile_init_function(block)?); } if let Some(block) = &self.model.dynamics { - kernels.push(self.lower_statement_kernel(KernelRole::Dynamics, block)?); + functions.push(self.compile_statement_function(ModelFunctionKind::Dynamics, block)?); } if let Some(block) = &self.model.drift { - kernels.push(self.lower_statement_kernel(KernelRole::Drift, block)?); + functions.push(self.compile_statement_function(ModelFunctionKind::Drift, block)?); } if let Some(block) = &self.model.diffusion { - kernels.push(self.lower_statement_kernel(KernelRole::Diffusion, block)?); + functions.push(self.compile_statement_function(ModelFunctionKind::Diffusion, block)?); } - if let Some(kernel) = - self.lower_route_property_kernel(RoutePropertyKind::Lag, KernelRole::RouteLag)? + if let Some(function) = self + .compile_route_property_function(RoutePropertyKind::Lag, ModelFunctionKind::RouteLag)? { - kernels.push(kernel); + functions.push(function); } - if let Some(kernel) = self.lower_route_property_kernel( + if let Some(function) = self.compile_route_property_function( RoutePropertyKind::Bioavailability, - KernelRole::RouteBioavailability, + ModelFunctionKind::RouteBioavailability, )? { - kernels.push(kernel); + functions.push(function); } if let Some(analytical) = &self.model.analytical { - kernels.push(ExecutionKernel { - role: KernelRole::Analytical, - signature: signature_for(KernelRole::Analytical), - implementation: KernelImplementation::AnalyticalBuiltin(analytical.structure), + functions.push(ModelFunction { + kind: ModelFunctionKind::Analytical, + signature: signature_for(ModelFunctionKind::Analytical), + body: FunctionBody::AnalyticalBuiltin(analytical.structure), span: analytical.span, }); } - kernels.push(self.lower_statement_kernel(KernelRole::Outputs, &self.model.outputs_block)?); + functions.push( + self.compile_statement_function(ModelFunctionKind::Outputs, &self.model.outputs_block)?, + ); Ok(ExecutionModel { name: self.model.name.clone(), kind: self.model.kind, metadata: self.metadata, - abi, - kernels, + layout, + functions, span: self.model.span, }) } - fn build_abi(&self) -> ExecutionAbi { - ExecutionAbi { - scalar: ScalarAbi::F64, - calling_convention: CallingConvention::DenseF64Buffers, - parameter_buffer: DenseBufferLayout { + fn build_layout(&self) -> ExecutionLayout { + ExecutionLayout { + scalar: ScalarType::F64, + parameter_buffer: BufferLayout { kind: BufferKind::Parameters, len: self.metadata.parameters.len(), slots: self @@ -723,7 +738,7 @@ impl<'a> ExecutionLowerer<'a> { }) .collect(), }, - covariate_buffer: DenseBufferLayout { + covariate_buffer: BufferLayout { kind: BufferKind::Covariates, len: self.metadata.covariates.len(), slots: self @@ -737,7 +752,7 @@ impl<'a> ExecutionLowerer<'a> { }) .collect(), }, - state_buffer: DenseBufferLayout { + state_buffer: BufferLayout { kind: BufferKind::States, len: self.metadata.states.iter().map(|state| state.len).sum(), slots: self @@ -751,7 +766,7 @@ impl<'a> ExecutionLowerer<'a> { }) .collect(), }, - derived_buffer: DenseBufferLayout { + derived_buffer: BufferLayout { kind: BufferKind::Derived, len: self.metadata.derived.len(), slots: self @@ -765,7 +780,7 @@ impl<'a> ExecutionLowerer<'a> { }) .collect(), }, - output_buffer: DenseBufferLayout { + output_buffer: BufferLayout { kind: BufferKind::Outputs, len: self.metadata.outputs.len(), slots: self @@ -779,7 +794,7 @@ impl<'a> ExecutionLowerer<'a> { }) .collect(), }, - route_buffer: DenseBufferLayout { + route_buffer: BufferLayout { kind: BufferKind::Routes, len: self .metadata @@ -802,22 +817,22 @@ impl<'a> ExecutionLowerer<'a> { } } - fn lower_statement_kernel( + fn compile_statement_function( &self, - role: KernelRole, - block: &TypedStatementBlock, - ) -> Result { - let mut locals = LocalLowering::default(); + kind: ModelFunctionKind, + block: &AnalyzedStatementBlock, + ) -> Result { + let mut locals = CompileLocals::default(); let statements = block .statements .iter() - .map(|stmt| self.lower_stmt(stmt, &mut locals)) - .collect::, LoweringError>>()?; + .map(|stmt| self.compile_stmt(stmt, &mut locals)) + .collect::, CompileError>>()?; - Ok(ExecutionKernel { - role, - signature: signature_for(role), - implementation: KernelImplementation::Statements(ExecutionProgram { + Ok(ModelFunction { + kind, + signature: signature_for(kind), + body: FunctionBody::Statements(ExecutionProgram { locals: locals.locals, body: ExecutionBlock { statements, @@ -828,11 +843,11 @@ impl<'a> ExecutionLowerer<'a> { }) } - fn lower_init_kernel( + fn compile_init_function( &self, - block: &TypedStatementBlock, - ) -> Result { - let mut locals = LocalLowering::default(); + block: &AnalyzedStatementBlock, + ) -> Result { + let mut locals = CompileLocals::default(); let mut statements = self .metadata .states @@ -866,14 +881,14 @@ impl<'a> ExecutionLowerer<'a> { block .statements .iter() - .map(|stmt| self.lower_stmt(stmt, &mut locals)) - .collect::, LoweringError>>()?, + .map(|stmt| self.compile_stmt(stmt, &mut locals)) + .collect::, CompileError>>()?, ); - Ok(ExecutionKernel { - role: KernelRole::Init, - signature: signature_for(KernelRole::Init), - implementation: KernelImplementation::Statements(ExecutionProgram { + Ok(ModelFunction { + kind: ModelFunctionKind::Init, + signature: signature_for(ModelFunctionKind::Init), + body: FunctionBody::Statements(ExecutionProgram { locals: locals.locals, body: ExecutionBlock { statements, @@ -884,11 +899,11 @@ impl<'a> ExecutionLowerer<'a> { }) } - fn lower_route_property_kernel( + fn compile_route_property_function( &self, property_kind: RoutePropertyKind, - role: KernelRole, - ) -> Result, LoweringError> { + kind: ModelFunctionKind, + ) -> Result, CompileError> { if !self.model.routes.iter().any(|route| { route .properties @@ -899,7 +914,7 @@ impl<'a> ExecutionLowerer<'a> { } let mut statements = Vec::with_capacity(self.model.routes.len()); - let mut locals = LocalLowering::default(); + let mut locals = CompileLocals::default(); let default_value = match property_kind { RoutePropertyKind::Lag => literal_real(0.0, self.model.span), RoutePropertyKind::Bioavailability => literal_real(1.0, self.model.span), @@ -935,7 +950,7 @@ impl<'a> ExecutionLowerer<'a> { } let route_name = self.symbol_name(route.symbol)?.to_string(); let route_index = *self.route_slots.get(&route.symbol).ok_or_else(|| { - LoweringError::new( + CompileError::new( format!("route `{}` has no execution slot", route_name), route.span, ) @@ -945,7 +960,7 @@ impl<'a> ExecutionLowerer<'a> { .iter() .find(|property| property.kind == property_kind) { - Some(property) => self.lower_expr(&property.value, &mut locals)?, + Some(property) => self.compile_expr(&property.value, &mut locals)?, None => continue, }; let target_kind = match property_kind { @@ -966,10 +981,10 @@ impl<'a> ExecutionLowerer<'a> { }); } - Ok(Some(ExecutionKernel { - role, - signature: signature_for(role), - implementation: KernelImplementation::Statements(ExecutionProgram { + Ok(Some(ModelFunction { + kind, + signature: signature_for(kind), + body: FunctionBody::Statements(ExecutionProgram { locals: locals.locals, body: ExecutionBlock { statements, @@ -980,29 +995,29 @@ impl<'a> ExecutionLowerer<'a> { })) } - fn lower_stmt( + fn compile_stmt( &self, - stmt: &TypedStmt, - locals: &mut LocalLowering, - ) -> Result { + stmt: &AnalyzedStmt, + locals: &mut CompileLocals, + ) -> Result { let kind = match &stmt.kind { - TypedStmtKind::Let(let_stmt) => { + AnalyzedStmtKind::Let(let_stmt) => { let local = locals.local_slot(let_stmt.symbol, self)?; ExecutionStmtKind::Let(ExecutionLetStmt { local, - value: self.lower_expr(&let_stmt.value, locals)?, + value: self.compile_expr(&let_stmt.value, locals)?, }) } - TypedStmtKind::Assign(assign) => ExecutionStmtKind::Assign(ExecutionAssignStmt { - target: self.lower_target(&assign.target.kind, assign.target.span, locals)?, - value: self.lower_expr(&assign.value, locals)?, + AnalyzedStmtKind::Assign(assign) => ExecutionStmtKind::Assign(ExecutionAssignStmt { + target: self.compile_target(&assign.target.kind, assign.target.span, locals)?, + value: self.compile_expr(&assign.value, locals)?, }), - TypedStmtKind::If(if_stmt) => ExecutionStmtKind::If(ExecutionIfStmt { - condition: self.lower_expr(&if_stmt.condition, locals)?, + AnalyzedStmtKind::If(if_stmt) => ExecutionStmtKind::If(ExecutionIfStmt { + condition: self.compile_expr(&if_stmt.condition, locals)?, then_branch: if_stmt .then_branch .iter() - .map(|stmt| self.lower_stmt(stmt, locals)) + .map(|stmt| self.compile_stmt(stmt, locals)) .collect::, _>>()?, else_branch: if_stmt .else_branch @@ -1010,20 +1025,20 @@ impl<'a> ExecutionLowerer<'a> { .map(|branch| { branch .iter() - .map(|stmt| self.lower_stmt(stmt, locals)) - .collect::, LoweringError>>() + .map(|stmt| self.compile_stmt(stmt, locals)) + .collect::, CompileError>>() }) .transpose()?, }), - TypedStmtKind::For(for_stmt) => { + AnalyzedStmtKind::For(for_stmt) => { let local = locals.local_slot(for_stmt.binding, self)?; ExecutionStmtKind::For(ExecutionForStmt { local, - range: self.lower_range(&for_stmt.range, locals)?, + range: self.compile_range(&for_stmt.range, locals)?, body: for_stmt .body .iter() - .map(|stmt| self.lower_stmt(stmt, locals)) + .map(|stmt| self.compile_stmt(stmt, locals)) .collect::, _>>()?, }) } @@ -1035,49 +1050,49 @@ impl<'a> ExecutionLowerer<'a> { }) } - fn lower_range( + fn compile_range( &self, - range: &TypedRangeExpr, - locals: &mut LocalLowering, - ) -> Result { + range: &AnalyzedRangeExpr, + locals: &mut CompileLocals, + ) -> Result { Ok(ExecutionRange { - start: self.lower_expr(&range.start, locals)?, - end: self.lower_expr(&range.end, locals)?, + start: self.compile_expr(&range.start, locals)?, + end: self.compile_expr(&range.end, locals)?, span: range.span, }) } - fn lower_target( + fn compile_target( &self, - target: &TypedAssignTargetKind, + target: &AnalyzedAssignTargetKind, span: Span, - locals: &mut LocalLowering, - ) -> Result { + locals: &mut CompileLocals, + ) -> Result { let kind = match target { - TypedAssignTargetKind::Derived(symbol) => { + AnalyzedAssignTargetKind::Derived(symbol) => { ExecutionTargetKind::Derived(self.slot_for_derived(*symbol, span)?) } - TypedAssignTargetKind::Output(symbol) => { + AnalyzedAssignTargetKind::Output(symbol) => { ExecutionTargetKind::Output(self.slot_for_output(*symbol, span)?) } - TypedAssignTargetKind::StateInit(place) => { - ExecutionTargetKind::StateInit(self.lower_state_ref(place, locals)?) + AnalyzedAssignTargetKind::StateInit(place) => { + ExecutionTargetKind::StateInit(self.compile_state_ref(place, locals)?) } - TypedAssignTargetKind::Derivative(place) => { - ExecutionTargetKind::StateDerivative(self.lower_state_ref(place, locals)?) + AnalyzedAssignTargetKind::Derivative(place) => { + ExecutionTargetKind::StateDerivative(self.compile_state_ref(place, locals)?) } - TypedAssignTargetKind::Noise(place) => { - ExecutionTargetKind::StateNoise(self.lower_state_ref(place, locals)?) + AnalyzedAssignTargetKind::Noise(place) => { + ExecutionTargetKind::StateNoise(self.compile_state_ref(place, locals)?) } }; Ok(ExecutionTarget { kind, span }) } - fn lower_expr( + fn compile_expr( &self, - expr: &TypedExpr, - locals: &mut LocalLowering, - ) -> Result { + expr: &AnalyzedExpr, + locals: &mut CompileLocals, + ) -> Result { if let Some(constant) = &expr.constant { return Ok(ExecutionExpr { kind: ExecutionExprKind::Literal(constant.clone()), @@ -1088,8 +1103,8 @@ impl<'a> ExecutionLowerer<'a> { } let kind = match &expr.kind { - TypedExprKind::Literal(constant) => ExecutionExprKind::Literal(constant.clone()), - TypedExprKind::Symbol(symbol) => { + AnalyzedExprKind::Literal(constant) => ExecutionExprKind::Literal(constant.clone()), + AnalyzedExprKind::Symbol(symbol) => { let symbol_info = lookup_symbol(&self.symbol_map, *symbol, expr.span)?; match symbol_info.kind { SymbolKind::Parameter => ExecutionExprKind::Load(ExecutionLoad::Parameter( @@ -1105,37 +1120,37 @@ impl<'a> ExecutionLowerer<'a> { ExecutionLoad::Local(locals.local_slot(*symbol, self)?), ), SymbolKind::Constant => { - return Err(LoweringError::new( + return Err(CompileError::new( format!( - "constant `{}` should have been folded before execution lowering", + "constant `{}` should have been folded before execution compilation", symbol_info.name ), expr.span, )); } SymbolKind::State => { - return Err(LoweringError::new( + return Err(CompileError::new( format!( - "state `{}` should lower through a state reference", + "state `{}` should compile through a state reference", symbol_info.name ), expr.span, )); } SymbolKind::Route => { - return Err(LoweringError::new( + return Err(CompileError::new( format!( "route `{}` is not a scalar execution input", symbol_info.name ), expr.span, ) - .with_note("routes must lower through `rate(route)` or route metadata")); + .with_note("routes must compile through `rate(route)` or route metadata")); } SymbolKind::Output => { - return Err(LoweringError::new( + return Err(CompileError::new( format!( - "output `{}` cannot be read inside execution kernels", + "output `{}` cannot be read inside execution functions", symbol_info.name ), expr.span, @@ -1143,30 +1158,30 @@ impl<'a> ExecutionLowerer<'a> { } } } - TypedExprKind::StateValue(place) => { - ExecutionExprKind::Load(ExecutionLoad::State(self.lower_state_ref(place, locals)?)) - } - TypedExprKind::Unary { op, expr } => ExecutionExprKind::Unary { + AnalyzedExprKind::StateValue(place) => ExecutionExprKind::Load(ExecutionLoad::State( + self.compile_state_ref(place, locals)?, + )), + AnalyzedExprKind::Unary { op, expr } => ExecutionExprKind::Unary { op: *op, - expr: Box::new(self.lower_expr(expr, locals)?), + expr: Box::new(self.compile_expr(expr, locals)?), }, - TypedExprKind::Binary { op, lhs, rhs } => ExecutionExprKind::Binary { + AnalyzedExprKind::Binary { op, lhs, rhs } => ExecutionExprKind::Binary { op: *op, - lhs: Box::new(self.lower_expr(lhs, locals)?), - rhs: Box::new(self.lower_expr(rhs, locals)?), + lhs: Box::new(self.compile_expr(lhs, locals)?), + rhs: Box::new(self.compile_expr(rhs, locals)?), }, - TypedExprKind::Call { callee, args } => match callee { - TypedCall::Math(intrinsic) => ExecutionExprKind::Call { + AnalyzedExprKind::Call { callee, args } => match callee { + AnalyzedCall::Math(intrinsic) => ExecutionExprKind::Call { callee: ExecutionCall::Math(*intrinsic), args: args .iter() - .map(|arg| self.lower_expr(arg, locals)) + .map(|arg| self.compile_expr(arg, locals)) .collect::, _>>()?, }, - TypedCall::Rate(route) => { + AnalyzedCall::Rate(route) => { let route_name = self.symbol_name(*route)?.to_string(); let route_index = *self.route_slots.get(route).ok_or_else(|| { - LoweringError::new( + CompileError::new( format!("route `{}` has no execution slot", route_name), expr.span, ) @@ -1187,14 +1202,14 @@ impl<'a> ExecutionLowerer<'a> { }) } - fn lower_state_ref( + fn compile_state_ref( &self, - place: &TypedStatePlace, - locals: &mut LocalLowering, - ) -> Result { + place: &AnalyzedStatePlace, + locals: &mut CompileLocals, + ) -> Result { let state_name = self.symbol_name(place.state)?.to_string(); let layout = self.state_slots.get(&place.state).copied().ok_or_else(|| { - LoweringError::new( + CompileError::new( format!("state `{}` has no execution layout", state_name), place.span, ) @@ -1202,7 +1217,7 @@ impl<'a> ExecutionLowerer<'a> { let index = place .index .as_ref() - .map(|index| self.lower_expr(index, locals)) + .map(|index| self.compile_expr(index, locals)) .transpose()? .map(Box::new); Ok(ExecutionStateRef { @@ -1214,9 +1229,9 @@ impl<'a> ExecutionLowerer<'a> { }) } - fn slot_for_parameter(&self, symbol: SymbolId, span: Span) -> Result { + fn slot_for_parameter(&self, symbol: SymbolId, span: Span) -> Result { self.parameter_slots.get(&symbol).copied().ok_or_else(|| { - LoweringError::new( + CompileError::new( format!( "parameter `{}` has no ABI slot", self.symbol_name(symbol).unwrap_or("") @@ -1226,9 +1241,9 @@ impl<'a> ExecutionLowerer<'a> { }) } - fn slot_for_covariate(&self, symbol: SymbolId, span: Span) -> Result { + fn slot_for_covariate(&self, symbol: SymbolId, span: Span) -> Result { self.covariate_slots.get(&symbol).copied().ok_or_else(|| { - LoweringError::new( + CompileError::new( format!( "covariate `{}` has no ABI slot", self.symbol_name(symbol).unwrap_or("") @@ -1238,9 +1253,9 @@ impl<'a> ExecutionLowerer<'a> { }) } - fn slot_for_derived(&self, symbol: SymbolId, span: Span) -> Result { + fn slot_for_derived(&self, symbol: SymbolId, span: Span) -> Result { self.derived_slots.get(&symbol).copied().ok_or_else(|| { - LoweringError::new( + CompileError::new( format!( "derived value `{}` has no ABI slot", self.symbol_name(symbol).unwrap_or("") @@ -1250,9 +1265,9 @@ impl<'a> ExecutionLowerer<'a> { }) } - fn slot_for_output(&self, symbol: SymbolId, span: Span) -> Result { + fn slot_for_output(&self, symbol: SymbolId, span: Span) -> Result { self.output_slots.get(&symbol).copied().ok_or_else(|| { - LoweringError::new( + CompileError::new( format!( "output `{}` has no ABI slot", self.symbol_name(symbol).unwrap_or("") @@ -1262,37 +1277,37 @@ impl<'a> ExecutionLowerer<'a> { }) } - fn symbol_name(&self, symbol: SymbolId) -> Result<&str, LoweringError> { + fn symbol_name(&self, symbol: SymbolId) -> Result<&str, CompileError> { Ok(&lookup_symbol(&self.symbol_map, symbol, self.model.span)?.name) } } #[derive(Default)] -struct LocalLowering { +struct CompileLocals { locals: Vec, slots: BTreeMap, } -impl LocalLowering { +impl CompileLocals { fn local_slot( &mut self, symbol: SymbolId, - lowerer: &ExecutionLowerer<'_>, - ) -> Result { + compiler: &ModelCompiler<'_>, + ) -> Result { if let Some(slot) = self.slots.get(&symbol).copied() { return Ok(slot); } - let symbol_info = lookup_symbol(&lowerer.symbol_map, symbol, lowerer.model.span)?; + let symbol_info = lookup_symbol(&compiler.symbol_map, symbol, compiler.model.span)?; let ty = match symbol_info.ty { SymbolType::Scalar(ty) => ty, SymbolType::Array { .. } => { - return Err(LoweringError::new( + return Err(CompileError::new( format!("local `{}` must be scalar", symbol_info.name), symbol_info.span, )); } SymbolType::Route => { - return Err(LoweringError::new( + return Err(CompileError::new( format!("local `{}` cannot be a route handle", symbol_info.name), symbol_info.span, )); @@ -1312,120 +1327,117 @@ impl LocalLowering { } } -fn signature_for(role: KernelRole) -> KernelSignature { - let args = match role { - KernelRole::Derive => vec![ - arg(KernelArgumentKind::Time, KernelAccess::Input), - arg(KernelArgumentKind::Parameters, KernelAccess::Input), - arg(KernelArgumentKind::Covariates, KernelAccess::Input), - arg(KernelArgumentKind::RouteInputs, KernelAccess::Input), - arg(KernelArgumentKind::States, KernelAccess::Input), - arg(KernelArgumentKind::Derived, KernelAccess::Output), +fn signature_for(kind: ModelFunctionKind) -> FunctionSignature { + let args = match kind { + ModelFunctionKind::Derive => vec![ + arg(FunctionArgumentKind::Time, Access::Input), + arg(FunctionArgumentKind::Parameters, Access::Input), + arg(FunctionArgumentKind::Covariates, Access::Input), + arg(FunctionArgumentKind::RouteInputs, Access::Input), + arg(FunctionArgumentKind::States, Access::Input), + arg(FunctionArgumentKind::Derived, Access::Output), ], - KernelRole::Dynamics => vec![ - arg(KernelArgumentKind::Time, KernelAccess::Input), - arg(KernelArgumentKind::States, KernelAccess::Input), - arg(KernelArgumentKind::Parameters, KernelAccess::Input), - arg(KernelArgumentKind::Covariates, KernelAccess::Input), - arg(KernelArgumentKind::RouteInputs, KernelAccess::Input), - arg(KernelArgumentKind::Derived, KernelAccess::Input), - arg(KernelArgumentKind::StateDerivatives, KernelAccess::Output), + ModelFunctionKind::Dynamics => vec![ + arg(FunctionArgumentKind::Time, Access::Input), + arg(FunctionArgumentKind::States, Access::Input), + arg(FunctionArgumentKind::Parameters, Access::Input), + arg(FunctionArgumentKind::Covariates, Access::Input), + arg(FunctionArgumentKind::RouteInputs, Access::Input), + arg(FunctionArgumentKind::Derived, Access::Input), + arg(FunctionArgumentKind::StateDerivatives, Access::Output), ], - KernelRole::Outputs => vec![ - arg(KernelArgumentKind::Time, KernelAccess::Input), - arg(KernelArgumentKind::States, KernelAccess::Input), - arg(KernelArgumentKind::Parameters, KernelAccess::Input), - arg(KernelArgumentKind::Covariates, KernelAccess::Input), - arg(KernelArgumentKind::RouteInputs, KernelAccess::Input), - arg(KernelArgumentKind::Derived, KernelAccess::Input), - arg(KernelArgumentKind::Outputs, KernelAccess::Output), + ModelFunctionKind::Outputs => vec![ + arg(FunctionArgumentKind::Time, Access::Input), + arg(FunctionArgumentKind::States, Access::Input), + arg(FunctionArgumentKind::Parameters, Access::Input), + arg(FunctionArgumentKind::Covariates, Access::Input), + arg(FunctionArgumentKind::RouteInputs, Access::Input), + arg(FunctionArgumentKind::Derived, Access::Input), + arg(FunctionArgumentKind::Outputs, Access::Output), ], - KernelRole::Init => vec![ - arg(KernelArgumentKind::Time, KernelAccess::Input), - arg(KernelArgumentKind::Parameters, KernelAccess::Input), - arg(KernelArgumentKind::Covariates, KernelAccess::Input), - arg(KernelArgumentKind::RouteInputs, KernelAccess::Input), - arg(KernelArgumentKind::Derived, KernelAccess::Input), - arg(KernelArgumentKind::InitialState, KernelAccess::Output), + ModelFunctionKind::Init => vec![ + arg(FunctionArgumentKind::Time, Access::Input), + arg(FunctionArgumentKind::Parameters, Access::Input), + arg(FunctionArgumentKind::Covariates, Access::Input), + arg(FunctionArgumentKind::RouteInputs, Access::Input), + arg(FunctionArgumentKind::Derived, Access::Input), + arg(FunctionArgumentKind::InitialState, Access::Output), ], - KernelRole::Drift => vec![ - arg(KernelArgumentKind::Time, KernelAccess::Input), - arg(KernelArgumentKind::States, KernelAccess::Input), - arg(KernelArgumentKind::Parameters, KernelAccess::Input), - arg(KernelArgumentKind::Covariates, KernelAccess::Input), - arg(KernelArgumentKind::RouteInputs, KernelAccess::Input), - arg(KernelArgumentKind::Derived, KernelAccess::Input), - arg(KernelArgumentKind::StateDerivatives, KernelAccess::Output), + ModelFunctionKind::Drift => vec![ + arg(FunctionArgumentKind::Time, Access::Input), + arg(FunctionArgumentKind::States, Access::Input), + arg(FunctionArgumentKind::Parameters, Access::Input), + arg(FunctionArgumentKind::Covariates, Access::Input), + arg(FunctionArgumentKind::RouteInputs, Access::Input), + arg(FunctionArgumentKind::Derived, Access::Input), + arg(FunctionArgumentKind::StateDerivatives, Access::Output), ], - KernelRole::Diffusion => vec![ - arg(KernelArgumentKind::Time, KernelAccess::Input), - arg(KernelArgumentKind::States, KernelAccess::Input), - arg(KernelArgumentKind::Parameters, KernelAccess::Input), - arg(KernelArgumentKind::Covariates, KernelAccess::Input), - arg(KernelArgumentKind::RouteInputs, KernelAccess::Input), - arg(KernelArgumentKind::Derived, KernelAccess::Input), - arg(KernelArgumentKind::StateNoise, KernelAccess::Output), + ModelFunctionKind::Diffusion => vec![ + arg(FunctionArgumentKind::Time, Access::Input), + arg(FunctionArgumentKind::States, Access::Input), + arg(FunctionArgumentKind::Parameters, Access::Input), + arg(FunctionArgumentKind::Covariates, Access::Input), + arg(FunctionArgumentKind::RouteInputs, Access::Input), + arg(FunctionArgumentKind::Derived, Access::Input), + arg(FunctionArgumentKind::StateNoise, Access::Output), ], - KernelRole::RouteLag => vec![ - arg(KernelArgumentKind::Time, KernelAccess::Input), - arg(KernelArgumentKind::Parameters, KernelAccess::Input), - arg(KernelArgumentKind::Covariates, KernelAccess::Input), - arg(KernelArgumentKind::RouteInputs, KernelAccess::Input), - arg(KernelArgumentKind::Derived, KernelAccess::Input), - arg(KernelArgumentKind::RouteLag, KernelAccess::Output), + ModelFunctionKind::RouteLag => vec![ + arg(FunctionArgumentKind::Time, Access::Input), + arg(FunctionArgumentKind::Parameters, Access::Input), + arg(FunctionArgumentKind::Covariates, Access::Input), + arg(FunctionArgumentKind::RouteInputs, Access::Input), + arg(FunctionArgumentKind::Derived, Access::Input), + arg(FunctionArgumentKind::RouteLag, Access::Output), ], - KernelRole::RouteBioavailability => vec![ - arg(KernelArgumentKind::Time, KernelAccess::Input), - arg(KernelArgumentKind::Parameters, KernelAccess::Input), - arg(KernelArgumentKind::Covariates, KernelAccess::Input), - arg(KernelArgumentKind::RouteInputs, KernelAccess::Input), - arg(KernelArgumentKind::Derived, KernelAccess::Input), - arg( - KernelArgumentKind::RouteBioavailability, - KernelAccess::Output, - ), + ModelFunctionKind::RouteBioavailability => vec![ + arg(FunctionArgumentKind::Time, Access::Input), + arg(FunctionArgumentKind::Parameters, Access::Input), + arg(FunctionArgumentKind::Covariates, Access::Input), + arg(FunctionArgumentKind::RouteInputs, Access::Input), + arg(FunctionArgumentKind::Derived, Access::Input), + arg(FunctionArgumentKind::RouteBioavailability, Access::Output), ], - KernelRole::Analytical => vec![ - arg(KernelArgumentKind::Time, KernelAccess::Input), - arg(KernelArgumentKind::States, KernelAccess::Input), - arg(KernelArgumentKind::Parameters, KernelAccess::Input), - arg(KernelArgumentKind::Covariates, KernelAccess::Input), - arg(KernelArgumentKind::RouteInputs, KernelAccess::Input), - arg(KernelArgumentKind::Derived, KernelAccess::Input), - arg(KernelArgumentKind::AnalyticalState, KernelAccess::Output), + ModelFunctionKind::Analytical => vec![ + arg(FunctionArgumentKind::Time, Access::Input), + arg(FunctionArgumentKind::States, Access::Input), + arg(FunctionArgumentKind::Parameters, Access::Input), + arg(FunctionArgumentKind::Covariates, Access::Input), + arg(FunctionArgumentKind::RouteInputs, Access::Input), + arg(FunctionArgumentKind::Derived, Access::Input), + arg(FunctionArgumentKind::AnalyticalState, Access::Output), ], }; - KernelSignature { args } + FunctionSignature { args } } -fn arg(kind: KernelArgumentKind, access: KernelAccess) -> KernelArgument { - KernelArgument { kind, access } +fn arg(kind: FunctionArgumentKind, access: Access) -> FunctionArgument { + FunctionArgument { kind, access } } fn lookup_symbol<'a>( symbols: &'a BTreeMap, symbol: SymbolId, span: Span, -) -> Result<&'a Symbol, LoweringError> { +) -> Result<&'a Symbol, CompileError> { symbols.get(&symbol).copied().ok_or_else(|| { - LoweringError::new( - format!("symbol id {symbol} is missing from the typed model symbol table"), + CompileError::new( + format!("symbol id {symbol} is missing from the analyzed model symbol table"), span, ) }) } -fn lower_route_destination( +fn compile_route_destination( symbols: &BTreeMap, state_slots: &BTreeMap, - destination: &TypedStatePlace, -) -> Result { + destination: &AnalyzedStatePlace, +) -> Result { let symbol = lookup_symbol(symbols, destination.state, destination.span)?; let layout = state_slots .get(&destination.state) .copied() .ok_or_else(|| { - LoweringError::new( + CompileError::new( format!("state `{}` has no execution layout", symbol.name), destination.span, ) @@ -1435,7 +1447,7 @@ fn lower_route_destination( Some(index) => constant_index(index, destination.span)?, }; if element >= layout.len { - return Err(LoweringError::new( + return Err(CompileError::new( format!( "route destination for `{}` indexes element {}, but state length is {}", symbol.name, element, layout.len @@ -1451,14 +1463,14 @@ fn lower_route_destination( }) } -fn constant_index(expr: &TypedExpr, span: Span) -> Result { +fn constant_index(expr: &AnalyzedExpr, span: Span) -> Result { let value = expr .constant .as_ref() .and_then(ConstValue::as_i64) - .ok_or_else(|| LoweringError::new("expected a compile-time integer index", span))?; + .ok_or_else(|| CompileError::new("expected a compile-time integer index", span))?; if value < 0 { - return Err(LoweringError::new( + return Err(CompileError::new( "expected a non-negative compile-time index", span, )); @@ -1491,26 +1503,26 @@ mod tests { use crate::{analyze_module, parse_module}; #[test] - fn lowers_structured_block_corpus_into_execution_models() { + fn compiles_structured_block_corpus_into_execution_models() { let execution = structured_block_execution(); assert_eq!(execution.models.len(), 4); let ode = find_model(&execution, "one_cmt_oral_iv"); - assert_eq!(ode.abi.parameter_buffer.len, 5); - assert_eq!(ode.abi.covariate_buffer.len, 1); - assert_eq!(ode.abi.state_buffer.len, 2); - assert_eq!(ode.abi.derived_buffer.len, 3); - assert_eq!(ode.abi.output_buffer.len, 1); - assert_eq!(ode.abi.route_buffer.len, 2); + assert_eq!(ode.layout.parameter_buffer.len, 5); + assert_eq!(ode.layout.covariate_buffer.len, 1); + assert_eq!(ode.layout.state_buffer.len, 2); + assert_eq!(ode.layout.derived_buffer.len, 3); + assert_eq!(ode.layout.output_buffer.len, 1); + assert_eq!(ode.layout.route_buffer.len, 2); assert_eq!(ode.metadata.routes[0].destination.state_offset, 0); assert_eq!( - kernel_roles(ode), + function_kinds(ode), vec![ - KernelRole::Derive, - KernelRole::Dynamics, - KernelRole::RouteLag, - KernelRole::RouteBioavailability, - KernelRole::Outputs, + ModelFunctionKind::Derive, + ModelFunctionKind::Dynamics, + ModelFunctionKind::RouteLag, + ModelFunctionKind::RouteBioavailability, + ModelFunctionKind::Outputs, ] ); } @@ -1536,21 +1548,21 @@ out(cp) = central / v ~ continuous() "#; let model = crate::parse_model(src).expect("authoring model parses"); - let typed = crate::analyze_model(&model).expect("authoring model analyzes"); - let lowered = crate::lower_typed_model(&typed).expect("authoring model lowers"); - - assert_eq!(lowered.abi.route_buffer.len, 1); - assert_eq!(lowered.metadata.routes.len(), 2); - assert_eq!(lowered.metadata.routes[0].kind, Some(RouteKind::Bolus)); - assert_eq!(lowered.metadata.routes[1].kind, Some(RouteKind::Infusion)); - assert_eq!(lowered.metadata.routes[0].declaration_index, 0); - assert_eq!(lowered.metadata.routes[1].declaration_index, 1); - assert_eq!(lowered.metadata.routes[0].index, 0); - assert_eq!(lowered.metadata.routes[1].index, 0); - assert!(lowered.metadata.routes[0].has_lag); - assert!(lowered.metadata.routes[0].has_bioavailability); - assert!(!lowered.metadata.routes[1].has_lag); - assert!(!lowered.metadata.routes[1].has_bioavailability); + let analyzed = crate::analyze_model(&model).expect("authoring model analyzes"); + let compiled = crate::compile_analyzed_model(&analyzed).expect("authoring model compiles"); + + assert_eq!(compiled.layout.route_buffer.len, 1); + assert_eq!(compiled.metadata.routes.len(), 2); + assert_eq!(compiled.metadata.routes[0].kind, Some(RouteKind::Bolus)); + assert_eq!(compiled.metadata.routes[1].kind, Some(RouteKind::Infusion)); + assert_eq!(compiled.metadata.routes[0].declaration_index, 0); + assert_eq!(compiled.metadata.routes[1].declaration_index, 1); + assert_eq!(compiled.metadata.routes[0].index, 0); + assert_eq!(compiled.metadata.routes[1].index, 0); + assert!(compiled.metadata.routes[0].has_lag); + assert!(compiled.metadata.routes[0].has_bioavailability); + assert!(!compiled.metadata.routes[1].has_lag); + assert!(!compiled.metadata.routes[1].has_bioavailability); } #[test] @@ -1573,11 +1585,11 @@ out(outeq_2) = depot / v "#; let model = crate::parse_model(src).expect("authoring model parses"); - let typed = crate::analyze_model(&model).expect("authoring model analyzes"); - let lowered = crate::lower_typed_model(&typed).expect("authoring model lowers"); + let analyzed = crate::analyze_model(&model).expect("authoring model analyzes"); + let compiled = crate::compile_analyzed_model(&analyzed).expect("authoring model compiles"); assert_eq!( - lowered + compiled .metadata .routes .iter() @@ -1586,7 +1598,7 @@ out(outeq_2) = depot / v vec!["input_10", "iv"] ); assert_eq!( - lowered + compiled .metadata .outputs .iter() @@ -1595,8 +1607,8 @@ out(outeq_2) = depot / v vec!["cp", "outeq_2"] ); assert_eq!( - lowered - .abi + compiled + .layout .route_buffer .slots .iter() @@ -1605,8 +1617,8 @@ out(outeq_2) = depot / v vec!["input_10", "iv"] ); assert_eq!( - lowered - .abi + compiled + .layout .output_buffer .slots .iter() @@ -1634,10 +1646,9 @@ out(cp) = central / v ~ continuous() "#; let model = crate::parse_model(src).expect("authoring model parses"); - let typed = crate::analyze_model(&model).expect("authoring model analyzes"); - let error = crate::lower_typed_model(&typed) - .err() - .expect("infusion lag should fail during lowering"); + let analyzed = crate::analyze_model(&model).expect("authoring model analyzes"); + let error = crate::compile_analyzed_model(&analyzed) + .expect_err("infusion lag should fail during compilation"); assert!(error .to_string() @@ -1662,10 +1673,9 @@ out(cp) = central / v ~ continuous() "#; let model = crate::parse_model(src).expect("authoring model parses"); - let typed = crate::analyze_model(&model).expect("authoring model analyzes"); - let error = crate::lower_typed_model(&typed) - .err() - .expect("infusion bioavailability should fail during lowering"); + let analyzed = crate::analyze_model(&model).expect("authoring model analyzes"); + let error = crate::compile_analyzed_model(&analyzed) + .expect_err("infusion bioavailability should fail during compilation"); assert!(error .to_string() @@ -1676,20 +1686,22 @@ out(cp) = central / v ~ continuous() fn flattens_array_states_and_preserves_loop_structure() { let execution = structured_block_execution(); let transit = find_model(&execution, "transit_absorption"); - assert_eq!(transit.abi.state_buffer.len, 5); + assert_eq!(transit.layout.state_buffer.len, 5); assert_eq!(transit.metadata.states[0].name, "transit"); assert_eq!(transit.metadata.states[0].offset, 0); assert_eq!(transit.metadata.states[0].len, 4); assert_eq!(transit.metadata.states[1].name, "central"); assert_eq!(transit.metadata.states[1].offset, 4); - assert!(transit.kernel(KernelRole::RouteLag).is_none()); - assert!(transit.kernel(KernelRole::RouteBioavailability).is_none()); + assert!(transit.function(ModelFunctionKind::RouteLag).is_none()); + assert!(transit + .function(ModelFunctionKind::RouteBioavailability) + .is_none()); let dynamics = transit - .kernel(KernelRole::Dynamics) - .expect("dynamics kernel"); - let KernelImplementation::Statements(program) = &dynamics.implementation else { - panic!("expected statement-based dynamics kernel"); + .function(ModelFunctionKind::Dynamics) + .expect("dynamics function"); + let FunctionBody::Statements(program) = &dynamics.body else { + panic!("expected statement-based dynamics function"); }; assert!(program .body @@ -1699,48 +1711,50 @@ out(cp) = central / v ~ continuous() } #[test] - fn analytical_models_lower_to_builtin_execution_kernels() { + fn analytical_models_compile_to_builtin_execution_functions() { let execution = structured_block_execution(); let analytical = find_model(&execution, "one_cmt_abs"); - let kernel = analytical - .kernel(KernelRole::Analytical) - .expect("analytical kernel"); + let function = analytical + .function(ModelFunctionKind::Analytical) + .expect("analytical function"); assert_eq!( - kernel.signature.args, + function.signature.args, vec![ - arg(KernelArgumentKind::Time, KernelAccess::Input), - arg(KernelArgumentKind::States, KernelAccess::Input), - arg(KernelArgumentKind::Parameters, KernelAccess::Input), - arg(KernelArgumentKind::Covariates, KernelAccess::Input), - arg(KernelArgumentKind::RouteInputs, KernelAccess::Input), - arg(KernelArgumentKind::Derived, KernelAccess::Input), - arg(KernelArgumentKind::AnalyticalState, KernelAccess::Output), + arg(FunctionArgumentKind::Time, Access::Input), + arg(FunctionArgumentKind::States, Access::Input), + arg(FunctionArgumentKind::Parameters, Access::Input), + arg(FunctionArgumentKind::Covariates, Access::Input), + arg(FunctionArgumentKind::RouteInputs, Access::Input), + arg(FunctionArgumentKind::Derived, Access::Input), + arg(FunctionArgumentKind::AnalyticalState, Access::Output), ] ); assert!(matches!( - kernel.implementation, - KernelImplementation::AnalyticalBuiltin(AnalyticalKernel::OneCompartmentWithAbsorption) + function.body, + FunctionBody::AnalyticalBuiltin(AnalyticalKernel::OneCompartmentWithAbsorption) )); } #[test] - fn sde_models_emit_runtime_kernels_and_zero_filled_init() { + fn sde_models_emit_runtime_functions_and_zero_filled_init() { let execution = structured_block_execution(); let sde = find_model(&execution, "vanco_sde"); assert_eq!(sde.metadata.particles, Some(1000)); assert_eq!( - kernel_roles(sde), + function_kinds(sde), vec![ - KernelRole::Init, - KernelRole::Drift, - KernelRole::Diffusion, - KernelRole::Outputs, + ModelFunctionKind::Init, + ModelFunctionKind::Drift, + ModelFunctionKind::Diffusion, + ModelFunctionKind::Outputs, ] ); - let init = sde.kernel(KernelRole::Init).expect("init kernel"); - let KernelImplementation::Statements(program) = &init.implementation else { - panic!("expected statement init kernel"); + let init = sde + .function(ModelFunctionKind::Init) + .expect("init function"); + let FunctionBody::Statements(program) = &init.body else { + panic!("expected statement init function"); }; assert!(program.body.statements.len() > sde.metadata.states.len()); assert!(matches!( @@ -1756,19 +1770,21 @@ out(cp) = central / v ~ continuous() } #[test] - fn route_property_kernels_fill_defaults_for_unconfigured_routes() { + fn route_property_functions_fill_defaults_for_unconfigured_routes() { let execution = structured_block_execution(); let ode = find_model(&execution, "one_cmt_oral_iv"); - let lag = ode.kernel(KernelRole::RouteLag).expect("lag kernel"); + let lag = ode + .function(ModelFunctionKind::RouteLag) + .expect("lag function"); let bio = ode - .kernel(KernelRole::RouteBioavailability) - .expect("bioavailability kernel"); + .function(ModelFunctionKind::RouteBioavailability) + .expect("bioavailability function"); - let KernelImplementation::Statements(lag_program) = &lag.implementation else { - panic!("expected statement lag kernel"); + let FunctionBody::Statements(lag_program) = &lag.body else { + panic!("expected statement lag function"); }; - let KernelImplementation::Statements(bio_program) = &bio.implementation else { - panic!("expected statement bioavailability kernel"); + let FunctionBody::Statements(bio_program) = &bio.body else { + panic!("expected statement bioavailability function"); }; assert_eq!(lag_program.body.statements.len(), 3); @@ -1798,8 +1814,8 @@ out(cp) = central / v ~ continuous() fn structured_block_execution() -> ExecutionModule { let src = STRUCTURED_BLOCK_CORPUS; let module = parse_module(src).expect("structured-block fixture parses"); - let typed = analyze_module(&module).expect("structured-block fixture analyzes"); - lower_typed_module(&typed).expect("execution lowering succeeds") + let analyzed = analyze_module(&module).expect("structured-block fixture analyzes"); + compile_analyzed_module(&analyzed).expect("execution compilation succeeds") } fn find_model<'a>(module: &'a ExecutionModule, name: &str) -> &'a ExecutionModel { @@ -1810,7 +1826,11 @@ out(cp) = central / v ~ continuous() .unwrap_or_else(|| panic!("missing model {name}")) } - fn kernel_roles(model: &ExecutionModel) -> Vec { - model.kernels.iter().map(|kernel| kernel.role).collect() + fn function_kinds(model: &ExecutionModel) -> Vec { + model + .functions + .iter() + .map(|function| function.kind) + .collect() } } diff --git a/pharmsol-dsl/src/lexer.rs b/pharmsol-dsl/src/lexer.rs index a1593651..38daa44a 100644 --- a/pharmsol-dsl/src/lexer.rs +++ b/pharmsol-dsl/src/lexer.rs @@ -402,6 +402,13 @@ impl<'a> Lexer<'a> { Span::new(start, self.pos), ) })?; + if !value.is_finite() { + return Err(ParseError::new( + format!("number literal `{raw}` is too large and overflows to infinity"), + Span::new(start, self.pos), + ) + .with_help("use a smaller magnitude, such as `1e308`")); + } Ok(TokenKind::Number(value)) } diff --git a/pharmsol-dsl/src/lib.rs b/pharmsol-dsl/src/lib.rs index 81bc7c44..2d462deb 100644 --- a/pharmsol-dsl/src/lib.rs +++ b/pharmsol-dsl/src/lib.rs @@ -1,28 +1,37 @@ -//! Backend-neutral frontend crate for the pharmsol DSL. +//! Compiles pharmsol DSL model source into a ready-to-run form. //! -//! Use this crate when you need the DSL frontend as an engineering API: parse -//! model source, inspect diagnostics, analyze names and types, and lower a -//! validated model into the execution representation that backends consume. +//! This crate reads model source text written in the pharmsol DSL and turns +//! it into an [`ExecutionModel`]: a fully checked, ready-to-run model that the +//! simulation backends in the main `pharmsol` crate execute directly. //! -//! Do not use this crate when you already know you want JIT compilation, -//! native AoT artifacts, WASM artifacts, or `Subject`-based prediction -//! helpers. Those runtime-facing workflows stay in the main `pharmsol` crate -//! under `pharmsol::dsl`. +//! Use this crate when you need to work with model source as data: +//! +//! - parse DSL text into a syntax tree +//! - inspect spans and diagnostics +//! - check names, types, and model structure +//! - compile a validated model into its execution form +//! +//! Do not use this crate for JIT compilation, native ahead-of-time export or +//! load, WASM runtime loading, or `Subject`-based prediction helpers. Those +//! workflows stay in `pharmsol::dsl` in the main `pharmsol` crate. //! //! Main entrypoints: //! -//! - [`parse_model`] and [`parse_module`] for turning DSL source text into the -//! syntax tree in [`ast`]. -//! - [`analyze_model`] and [`analyze_module`] for semantic validation and the -//! typed IR in [`ir`]. -//! - [`lower_typed_model`] and [`lower_typed_module`] for lowering typed models -//! into the execution representation in [`execution`]. +//! - [`compile_model`] and [`compile_module`] run the whole pipeline in one +//! call, failing with the unified [`DslError`]. +//! - [`parse_model`] and [`parse_module`] turn DSL source text into the +//! syntax tree in [`syntax`]. +//! - [`analyze_model`] and [`analyze_module`] check names, types, and model +//! structure, producing the analyzed model in [`analysis`]. +//! - [`compile_analyzed_model`] and [`compile_analyzed_module`] compile an +//! analyzed model into the ready-to-run form in [`execution`]. //! -//! The frontend pipeline is intentionally simple: +//! The pipeline is intentionally simple: //! //! 1. Parse source text into syntax. -//! 2. Analyze the syntax into a typed model. -//! 3. Lower the typed model into an [`ExecutionModel`] or [`ExecutionModule`]. +//! 2. Analyze the syntax into a checked model. +//! 3. Compile the checked model into an [`ExecutionModel`] or +//! [`ExecutionModule`]. //! //! This crate accepts both canonical `model { ... }` source and the authoring //! shorthand used by the `pharmsol` examples. The returned diagnostics carry @@ -30,16 +39,16 @@ //! //! Main modules: //! -//! - [`ast`] for syntax-level nodes. +//! - [`syntax`] for the syntax tree. +//! - [`analysis`] for the analyzed, fully checked model. //! - [`diagnostic`] for spans, diagnostic codes, and rendered reports. -//! - [`ir`] for the typed intermediate representation. -//! - [`execution`] for the lowered execution model shared by JIT, AoT, and +//! - [`execution`] for the ready-to-run model shared by the JIT, AoT, and //! WASM backends. //! -//! Smallest parse-analyze-lower example: +//! Smallest one-shot example: //! //! ```rust -//! use pharmsol_dsl::{analyze_model, lower_typed_model, parse_model}; +//! use pharmsol_dsl::compile_model; //! //! let source = r#" //! name = bimodal_ke @@ -55,28 +64,52 @@ //! out(cp) = central / v //! "#; //! -//! let syntax = parse_model(source).expect("model parses"); -//! let typed = analyze_model(&syntax).expect("model analyzes"); -//! let execution = lower_typed_model(&typed).expect("model lowers"); +//! let execution = compile_model(source).expect("model compiles"); //! //! assert_eq!(execution.name, "bimodal_ke"); //! assert_eq!(execution.metadata.routes.len(), 1); //! assert_eq!(execution.metadata.outputs.len(), 1); //! ``` //! +//! The same pipeline staged, for callers that need the intermediate +//! representations: +//! +//! ```rust +//! use pharmsol_dsl::{analyze_model, compile_analyzed_model, parse_model}; +//! +//! # let source = "name = m\nkind = ode\nstates = central\nddt(central) = 0\nout(cp) = central"; +//! let syntax = parse_model(source).expect("source parses"); +//! let analyzed = analyze_model(&syntax).expect("model is valid"); +//! let execution = compile_analyzed_model(&analyzed).expect("model compiles"); +//! # assert_eq!(execution.name, "m"); +//! ``` +//! +//! Errors from any stage carry source spans and render as annotated reports +//! when printed: +//! +//! ```rust +//! use pharmsol_dsl::compile_model; +//! +//! let error = compile_model("name = m\nstates = central\nout(cp) = missing_state").unwrap_err(); +//! let report = error.to_string(); +//! assert!(report.contains("error[DSL2000]")); +//! assert!(report.contains("unknown identifier `missing_state`")); +//! ``` +//! //! If you are building an authoring tool, custom compiler, or diagnostics UI, //! stay in this crate. If you want a complete source-to-runtime workflow, //! switch to `pharmsol::dsl` in the main crate. -pub mod ast; +pub mod analysis; +mod analyze; mod authoring; pub mod diagnostic; pub mod execution; -pub mod ir; mod lexer; mod name_match; mod parser; -mod semantic; +mod pipeline; +pub mod syntax; #[cfg(test)] mod test_fixtures; @@ -84,13 +117,18 @@ mod test_fixtures; pub const NUMERIC_ROUTE_PREFIX: &str = "input_"; /// Canonical prefix for numeric output labels such as `outeq_1`. pub const NUMERIC_OUTPUT_PREFIX: &str = "outeq_"; +/// Upper bound for compile-time-resolved sizes: state array sizes, route +/// destination indices, and particle counts. Keeps compiled models within a +/// comfortably allocatable state space. +pub const MAX_CONST_USIZE: usize = 1_048_576; pub(crate) const RATE_FUNCTION_NAME: &str = "rate"; -pub use ast::*; +pub use analysis::*; +pub use analyze::{analyze_model, analyze_module, AnalysisError}; pub use diagnostic::*; pub use execution::{ - lower_typed_model, lower_typed_module, ExecutionModel, ExecutionModule, LoweringError, + compile_analyzed_model, compile_analyzed_module, CompileError, ExecutionModel, ExecutionModule, }; -pub use ir::*; -pub use parser::{parse_model, parse_module}; -pub use semantic::{analyze_model, analyze_module, SemanticError}; +pub use parser::{parse_model, parse_module, MAX_NESTING_DEPTH}; +pub use pipeline::{compile_model, compile_module, DslError}; +pub use syntax::*; diff --git a/pharmsol-dsl/src/parser.rs b/pharmsol-dsl/src/parser.rs index 98c6b0a4..7f0de6e1 100644 --- a/pharmsol-dsl/src/parser.rs +++ b/pharmsol-dsl/src/parser.rs @@ -1,8 +1,11 @@ -use super::ast::*; use super::authoring; use super::diagnostic::{ParseError, Span}; use super::lexer::{lex, Token, TokenKind}; +use super::syntax::*; +/// Parses DSL source text into a syntax tree of one or more models. +/// +/// Accepts both canonical `model { ... }` source and the authoring shorthand. pub fn parse_module(src: &str) -> Result { let parsed = (|| { let leading = strip_leading_layout(src); @@ -23,6 +26,10 @@ pub fn parse_module(src: &str) -> Result { parsed.map_err(|error| error.with_source(src)) } +/// Parses DSL source text into a syntax tree of exactly one model. +/// +/// Like [`parse_module`], but fails if the source does not contain exactly +/// one model. pub fn parse_model(src: &str) -> Result { let module = parse_module(src)?; match module.models.len() { @@ -96,11 +103,19 @@ pub(crate) fn parse_place_fragment(src: &str) -> Result { parsed.map_err(|error| error.with_source(src)) } +/// Maximum nesting depth for expressions and statements. +/// +/// Guards against stack exhaustion from pathologically nested input such as +/// `((((...))))`; the limit is far beyond any realistic model. +pub const MAX_NESTING_DEPTH: usize = 256; + struct Parser { tokens: Vec, index: usize, src_len: usize, layout_boundaries: Vec, + depth: usize, + fatal: bool, } #[derive(Clone, Copy)] @@ -129,9 +144,39 @@ impl Parser { index: 0, src_len, layout_boundaries: Vec::new(), + depth: 0, + fatal: false, } } + /// Runs `parse` with one recursion slot; errors once nesting exceeds + /// [`MAX_NESTING_DEPTH`] instead of overflowing the call stack. The depth + /// error is fatal: recovery loops stop so the diagnostic surfaces once + /// instead of cascading per enclosing block. + fn with_depth( + &mut self, + construct: &str, + parse: impl FnOnce(&mut Self) -> Result, + ) -> Result { + if self.depth >= MAX_NESTING_DEPTH { + self.fatal = true; + let span = self + .peek_span() + .unwrap_or_else(|| Span::empty(self.src_len)); + return Err(ParseError::new( + format!( + "{construct} is nested too deeply (maximum nesting depth is {MAX_NESTING_DEPTH})" + ), + span, + ) + .with_help("split the model into simpler expressions or statements")); + } + self.depth += 1; + let result = parse(self); + self.depth -= 1; + result + } + fn parse_module(&mut self) -> Result { let start = self.peek_span().unwrap_or_else(|| Span::empty(0)); let mut models = Vec::new(); @@ -716,12 +761,12 @@ impl Parser { } fn parse_stmt(&mut self) -> Result { - match self.peek_kind() { - Some(TokenKind::If) => self.parse_if_stmt(), - Some(TokenKind::For) => self.parse_for_stmt(), - Some(TokenKind::Let) => self.parse_let_stmt(), - _ => self.parse_assign_stmt(), - } + self.with_depth("statement", |parser| match parser.peek_kind() { + Some(TokenKind::If) => parser.parse_if_stmt(), + Some(TokenKind::For) => parser.parse_for_stmt(), + Some(TokenKind::Let) => parser.parse_let_stmt(), + _ => parser.parse_assign_stmt(), + }) } fn parse_if_stmt(&mut self) -> Result { @@ -734,7 +779,7 @@ impl Parser { .is_some() { if self.at(|kind| matches!(kind, TokenKind::If)) { - let nested = self.parse_if_stmt()?; + let nested = self.with_depth("statement", |parser| parser.parse_if_stmt())?; end_span = nested.span; Some(vec![nested]) } else { @@ -911,11 +956,13 @@ impl Parser { })?; match token.kind { TokenKind::Ident(name) => Ok(Ident::new(name, token.span)), + // `usize::MAX as f64` rounds up to 2^64; keep the bound exclusive + // so the cast never saturates. TokenKind::Number(value) if value.is_finite() && value >= 0.0 && value.fract() == 0.0 - && value <= usize::MAX as f64 => + && value < usize::MAX as f64 => { Ok(Ident::new((value as usize).to_string(), token.span)) } @@ -943,6 +990,12 @@ impl Parser { } fn parse_expr(&mut self, min_precedence: u8) -> Result { + self.with_depth("expression", |parser| { + parser.parse_expr_inner(min_precedence) + }) + } + + fn parse_expr_inner(&mut self, min_precedence: u8) -> Result { let mut lhs = self.parse_prefix_expr()?; while let Some((op, precedence, right_assoc)) = self.peek_binary_op() { if precedence < min_precedence { @@ -976,6 +1029,10 @@ impl Parser { } fn parse_prefix_expr(&mut self) -> Result { + self.with_depth("expression", |parser| parser.parse_prefix_expr_inner()) + } + + fn parse_prefix_expr_inner(&mut self) -> Result { match self.peek_kind() { Some(TokenKind::Plus) => { let operator = self.bump().unwrap(); @@ -1221,6 +1278,9 @@ impl Parser { where F: FnOnce(&TokenKind) -> bool, { + if self.fatal { + return Err(ParseError::aborted()); + } let context = context.into(); let token = self.bump().ok_or_else(|| { ParseError::new( @@ -1589,7 +1649,7 @@ impl Parser { } fn is_eof(&self) -> bool { - self.index >= self.tokens.len() + self.fatal || self.index >= self.tokens.len() } fn peek(&self) -> Option<&Token> { @@ -1724,7 +1784,7 @@ out(cp) = central } #[test] - fn authoring_dx_and_ddt_lower_equivalently() { + fn authoring_dx_and_ddt_compile_equivalently() { let dx_src = r#" name = derivative_alias kind = ode diff --git a/pharmsol-dsl/src/pipeline.rs b/pharmsol-dsl/src/pipeline.rs new file mode 100644 index 00000000..a7c8b1b0 --- /dev/null +++ b/pharmsol-dsl/src/pipeline.rs @@ -0,0 +1,128 @@ +//! One-shot source-to-execution pipeline and its unified error type. +//! +//! [`compile_model`] and [`compile_module`] run the full pipeline +//! (parse, analyze, compile) in a single call. Failures are reported as +//! [`DslError`], which unifies the phase-specific error types returned by the +//! individual stages behind one `std::error::Error` implementation. + +use std::fmt; +use std::sync::Arc; + +use crate::analyze::{analyze_model, analyze_module, AnalysisError}; +use crate::diagnostic::{Diagnostic, DiagnosticPhase, DiagnosticReport, ParseError}; +use crate::execution::{ + compile_analyzed_model, compile_analyzed_module, CompileError, ExecutionModel, ExecutionModule, +}; +use crate::parser::{parse_model, parse_module}; + +/// Error produced by any stage of the DSL pipeline. +/// +/// Use [`DslError::phase`] to learn which stage failed and +/// [`DslError::diagnostics`] for the structured diagnostics. When source text +/// is attached — always the case for errors returned by [`compile_model`] and +/// [`compile_module`] — printing the error renders the annotated report: +/// +/// ```rust +/// use pharmsol_dsl::compile_model; +/// +/// let error = compile_model("model broken { kind ode").unwrap_err(); +/// assert!(error.to_string().contains("error[DSL1000]")); +/// assert_eq!(error.phase(), pharmsol_dsl::DiagnosticPhase::Parse); +/// ``` +#[derive(Clone, thiserror::Error)] +pub enum DslError { + /// Parsing the source text failed. + #[error(transparent)] + Parse(#[from] ParseError), + /// Analyzing the model failed. + #[error(transparent)] + Analysis(#[from] AnalysisError), + /// Compiling to the execution model failed. + #[error(transparent)] + Compile(#[from] CompileError), +} + +impl DslError { + /// The pipeline phase that produced this error. + pub fn phase(&self) -> DiagnosticPhase { + match self { + Self::Parse(_) => DiagnosticPhase::Parse, + Self::Analysis(_) => DiagnosticPhase::Analysis, + Self::Compile(_) => DiagnosticPhase::Compile, + } + } + + /// All diagnostics carried by this error, in source order where known. + pub fn diagnostics(&self) -> &[Diagnostic] { + match self { + Self::Parse(error) => error.diagnostics(), + Self::Analysis(error) => std::slice::from_ref(error.diagnostic()), + Self::Compile(error) => std::slice::from_ref(error.diagnostic()), + } + } + + /// Attaches source text so `Display` renders the annotated report. + pub fn with_source(self, source: impl Into>) -> Self { + let source = source.into(); + match self { + Self::Parse(error) => Self::Parse(error.with_source(source)), + Self::Analysis(error) => Self::Analysis(error.with_source(source)), + Self::Compile(error) => Self::Compile(error.with_source(source)), + } + } + + /// Source text attached to this error, if any. + pub fn source(&self) -> Option<&str> { + match self { + Self::Parse(error) => error.source(), + Self::Analysis(error) => error.source(), + Self::Compile(error) => error.source(), + } + } + + /// Renders the diagnostics against `src` without attaching it. + pub fn render(&self, src: &str) -> String { + match self { + Self::Parse(error) => error.render(src), + Self::Analysis(error) => error.render(src), + Self::Compile(error) => error.render(src), + } + } + + /// Structured, JSON-serializable report for editors and tooling. + pub fn diagnostic_report(&self, source_name: impl Into) -> DiagnosticReport { + let source_name = source_name.into(); + match self { + Self::Parse(error) => error.diagnostic_report(source_name), + Self::Analysis(error) => error.diagnostic_report(source_name), + Self::Compile(error) => error.diagnostic_report(source_name), + } + } +} + +impl fmt::Debug for DslError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(self, f) + } +} + +/// Parses, analyzes, and compiles the module in `src` in one call. +/// +/// This is the one-shot equivalent of [`parse_module`], [`analyze_module`], +/// and [`compile_analyzed_module`] in sequence. Errors carry the source text, so +/// printing them renders the annotated report. +pub fn compile_module(src: &str) -> Result { + let parsed = parse_module(src)?; + let analyzed = analyze_module(&parsed).map_err(|error| error.with_source(src))?; + compile_analyzed_module(&analyzed).map_err(|error| error.with_source(src).into()) +} + +/// Parses, analyzes, and compiles the single model in `src` in one call. +/// +/// Like [`compile_module`], but requires the source to contain exactly one +/// model, mirroring [`parse_model`]. +pub fn compile_model(src: &str) -> Result { + let parsed = parse_model(src)?; + let analyzed = analyze_model(&parsed).map_err(|error| error.with_source(src))?; + compile_analyzed_model(&analyzed).map_err(|error| error.with_source(src).into()) +} diff --git a/pharmsol-dsl/src/ast.rs b/pharmsol-dsl/src/syntax.rs similarity index 98% rename from pharmsol-dsl/src/ast.rs rename to pharmsol-dsl/src/syntax.rs index 6cff4483..56f87d74 100644 --- a/pharmsol-dsl/src/ast.rs +++ b/pharmsol-dsl/src/syntax.rs @@ -1,3 +1,9 @@ +//! Syntax tree produced by parsing pharmsol DSL source text. +//! +//! These types mirror what the author wrote, before any name or type +//! checking. Use [`parse_model`](crate::parse_model) or +//! [`parse_module`](crate::parse_module) to build them. + use std::fmt::{self, Write}; use serde::{Deserialize, Serialize}; diff --git a/pharmsol-dsl/tests/dsl_authoring_edge_cases.rs b/pharmsol-dsl/tests/dsl_authoring_edge_cases.rs index 335a7f86..0fe375cb 100644 --- a/pharmsol-dsl/tests/dsl_authoring_edge_cases.rs +++ b/pharmsol-dsl/tests/dsl_authoring_edge_cases.rs @@ -1,4 +1,4 @@ -use pharmsol_dsl::{analyze_model, lower_typed_model, parse_model, parse_module}; +use pharmsol_dsl::{analyze_model, compile_analyzed_model, parse_model, parse_module}; #[test] fn output_annotation_is_optional() { @@ -24,7 +24,7 @@ out(cp) = central } #[test] -fn dx_and_ddt_lower_equivalently() { +fn dx_and_ddt_compile_equivalently() { let dx_src = r#" name = derivative_alias kind = ode @@ -162,7 +162,7 @@ out(cp) = central ~ continous() } #[test] -fn mixed_named_and_prefixed_numeric_output_labels_lower_and_round_trip() { +fn mixed_named_and_prefixed_numeric_output_labels_compile_and_round_trip() { let src = r#" name = mixed_output_labels kind = ode @@ -181,11 +181,11 @@ out(outeq_1) = 3 * central / v .models .first() .expect("authoring DSL should produce one model"); - let typed = analyze_model(&model).expect("mixed output labels should analyze"); - let lowered = lower_typed_model(&typed).expect("mixed output labels should lower"); + let analyzed = analyze_model(model).expect("mixed output labels should analyze"); + let compiled = compile_analyzed_model(&analyzed).expect("mixed output labels should compile"); assert_eq!( - lowered + compiled .metadata .outputs .iter() @@ -194,7 +194,7 @@ out(outeq_1) = 3 * central / v vec!["cp", "outeq_0", "outeq_1"] ); assert_eq!( - lowered + compiled .metadata .outputs .iter() @@ -210,7 +210,7 @@ out(outeq_1) = 3 * central / v } #[test] -fn prefixed_numeric_route_and_output_labels_lower_and_round_trip() { +fn prefixed_numeric_route_and_output_labels_compile_and_round_trip() { let src = r#" name = prefixed_numeric_route_output_labels kind = ode @@ -227,12 +227,13 @@ out(outeq_1) = central / v .models .first() .expect("authoring DSL should produce one model"); - let typed = analyze_model(model).expect("prefixed numeric route/output labels should analyze"); - let lowered = - lower_typed_model(&typed).expect("prefixed numeric route/output labels should lower"); + let analyzed = + analyze_model(model).expect("prefixed numeric route/output labels should analyze"); + let compiled = compile_analyzed_model(&analyzed) + .expect("prefixed numeric route/output labels should compile"); assert_eq!( - lowered + compiled .metadata .routes .iter() @@ -241,7 +242,7 @@ out(outeq_1) = central / v vec!["input_1"] ); assert_eq!( - lowered + compiled .metadata .outputs .iter() diff --git a/pharmsol-dsl/tests/frontend_hardening.rs b/pharmsol-dsl/tests/frontend_hardening.rs new file mode 100644 index 00000000..a8f96519 --- /dev/null +++ b/pharmsol-dsl/tests/frontend_hardening.rs @@ -0,0 +1,469 @@ +//! Hardening and API tests for the DSL frontend: error quality, pathological +//! inputs, constant evaluation edges, and the unified pipeline surface. + +use pharmsol_dsl::{ + analyze_model, compile_analyzed_model, compile_model, compile_module, parse_model, + AnalysisError, ConstValue, Diagnostic, DiagnosticCode, DiagnosticPhase, DiagnosticReport, + DslError, ParseError, Span, DSL_ANALYSIS_GENERIC, DSL_PARSE_GENERIC, MAX_CONST_USIZE, + MAX_NESTING_DEPTH, +}; + +fn ode_skeleton(body: &str) -> String { + format!( + "model m {{ kind ode states {{ central }} dynamics {{ {body} }} outputs {{ cp = central }} }}" + ) +} + +fn ode_with_constants(constants: &str) -> String { + format!( + "model m {{ kind ode constants {{ {constants} }} states {{ central }} dynamics {{ ddt(central) = 0 }} outputs {{ cp = central }} }}" + ) +} + +// --------------------------------------------------------------------------- +// Number literals +// --------------------------------------------------------------------------- + +#[test] +fn rejects_number_literals_that_overflow_to_infinity() { + for src in [ + ode_skeleton("ddt(central) = 1e999"), + ode_skeleton("ddt(central) = -1e999"), + "name = m\nkind = ode\nstates = central\nddt(central) = 1e999\nout(cp) = central" + .to_string(), + ] { + let err = parse_model(&src).expect_err("overflowing literal must be rejected"); + let rendered = err.render(&src); + assert!(rendered.contains("overflows to infinity"), "{}", rendered); + } +} + +#[test] +fn accepts_large_but_finite_number_literals() { + let src = ode_skeleton("ddt(central) = 1e308"); + let model = parse_model(&src).expect("finite literal parses"); + let analyzed = analyze_model(&model).expect("finite literal analyzes"); + assert!(matches!( + analyzed.constants.as_slice(), + [] // no constants; the literal lives in the dynamics block + )); +} + +// --------------------------------------------------------------------------- +// Recursion depth +// --------------------------------------------------------------------------- + +fn nested_expr(open: &str, close: &str, depth: usize) -> String { + let mut expr = String::new(); + for _ in 0..depth { + expr.push_str(open); + } + expr.push('1'); + for _ in 0..depth { + expr.push_str(close); + } + expr +} + +#[test] +fn rejects_deeply_nested_expressions_without_crashing() { + let parens = ode_skeleton(&format!("ddt(central) = {}", nested_expr("(", ")", 10_000))); + let err = parse_model(&parens).expect_err("deep parens must fail cleanly"); + assert_eq!(err.diagnostics().len(), 1, "{}", err.render(&parens)); + assert!(err.diagnostics()[0].message.contains("nested too deeply")); + + let unary = ode_skeleton(&format!("ddt(central) = {}1", "-".repeat(10_000))); + let err = parse_model(&unary).expect_err("deep unary chain must fail cleanly"); + assert!(err.diagnostics()[0].message.contains("nested too deeply")); + + let caret = ode_skeleton(&format!("ddt(central) = 1{}", "^1".repeat(5_000))); + let err = parse_model(&caret).expect_err("deep caret chain must fail cleanly"); + assert!(err.diagnostics()[0].message.contains("nested too deeply")); +} + +#[test] +fn rejects_deeply_nested_statements_without_crashing() { + let mut body = String::new(); + for _ in 0..1_000 { + body.push_str("if true { "); + } + body.push_str("ddt(central) = 1"); + for _ in 0..1_000 { + body.push_str(" }"); + } + let src = ode_skeleton(&body); + let err = parse_model(&src).expect_err("deep statement nesting must fail cleanly"); + assert!( + err.diagnostics() + .iter() + .any(|diagnostic| diagnostic.message.contains("nested too deeply")), + "{}", + err.render(&src) + ); + + let mut body = String::from("if true { ddt(central) = 1 }"); + for _ in 0..1_000 { + body.push_str(" else if true { ddt(central) = 1 }"); + } + let src = ode_skeleton(&body); + let err = parse_model(&src).expect_err("deep else-if chain must fail cleanly"); + assert!( + err.diagnostics() + .iter() + .any(|diagnostic| diagnostic.message.contains("nested too deeply")), + "{}", + err.render(&src) + ); +} + +#[test] +fn rejects_deeply_nested_authoring_conditionals_without_crashing() { + let mut rhs = String::from("if (true) 1"); + for _ in 0..1_000 { + rhs.push_str(" else if (true) 1"); + } + rhs.push_str(" else 2"); + let src = format!("states = central\nddt(central) = 0\nout(cp) = {rhs}\n"); + let err = parse_model(&src).expect_err("deep authoring conditionals must fail cleanly"); + let rendered = err.render(&src); + assert!(rendered.contains("nested too deeply"), "{}", rendered); +} + +#[test] +fn moderate_nesting_still_parses() { + let parens = ode_skeleton(&format!("ddt(central) = {}", nested_expr("(", ")", 64))); + parse_model(&parens).expect("64 levels of parens parse"); + + let mut body = String::new(); + for _ in 0..100 { + body.push_str("if true { "); + } + body.push_str("ddt(central) = 1"); + for _ in 0..100 { + body.push_str(" }"); + } + parse_model(&ode_skeleton(&body)).expect("100 nested if statements parse"); +} + +// --------------------------------------------------------------------------- +// Constant evaluation +// --------------------------------------------------------------------------- + +fn analyzed_constant(src: &str, name: &str) -> ConstValue { + let model = parse_model(src).expect("model parses"); + let analyzed = analyze_model(&model).expect("model analyzes"); + analyzed + .constants + .iter() + .find(|constant| { + analyzed + .symbols + .iter() + .any(|symbol| symbol.id == constant.symbol && symbol.name == name) + }) + .unwrap_or_else(|| panic!("constant `{name}` exists")) + .value + .clone() +} + +#[test] +fn integer_constant_overflow_degrades_to_real_instead_of_panicking() { + for constants in ["x = 4611686018427387904 * 2", "x = 9223372036854775807 + 1"] { + assert_eq!( + analyzed_constant(&ode_with_constants(constants), "x"), + ConstValue::Real(9.223372036854776e18), + "{constants}" + ); + } + assert_eq!( + analyzed_constant(&ode_with_constants("x = -9223372036854775807 - 2"), "x"), + ConstValue::Real(-9.223372036854776e18) + ); +} + +#[test] +fn negating_i64_min_degrades_to_real_instead_of_panicking() { + let src = ode_with_constants("a = -9223372036854775807 - 1, b = -a"); + assert_eq!( + analyzed_constant(&src, "b"), + ConstValue::Real(9.223372036854776e18) + ); +} + +#[test] +fn constant_two_to_the_63_stays_real() { + let src = ode_with_constants("x = 9223372036854775808"); + assert_eq!( + analyzed_constant(&src, "x"), + ConstValue::Real(9.223372036854776e18) + ); +} + +#[test] +fn compile_time_calls_enforce_intrinsic_arity() { + for (call, message) in [ + ( + "pow(2, 3, 4)", + "function `pow` expects 2 argument(s), got 3", + ), + ("max(1)", "function `max` expects 2 argument(s), got 1"), + ] { + let src = ode_with_constants(&format!("x = {call}")); + let model = parse_model(&src).expect("model parses"); + let err = analyze_model(&model).expect_err("arity mismatch must fail"); + let rendered = err.render(&src); + assert!(rendered.contains(message), "{}", rendered); + } +} + +#[test] +fn state_array_size_rejects_non_representable_integer() { + let src = "model m { kind ode states { x[9223372036854775808], central } dynamics { ddt(central) = 0 } outputs { cp = central } }"; + let model = parse_model(src).expect("model parses"); + let err = analyze_model(&model).expect_err("2^63 state size must fail"); + let rendered = err.render(src); + assert!( + rendered.contains("state array size must be an integer constant"), + "{}", + rendered + ); +} + +#[test] +fn compile_time_sizes_are_capped() { + let src = format!( + "model m {{ kind ode states {{ x[{}], central }} dynamics {{ ddt(central) = 0 }} outputs {{ cp = central }} }}", + MAX_CONST_USIZE + 1 + ); + let model = parse_model(&src).expect("model parses"); + let err = analyze_model(&model).expect_err("oversized state array must fail"); + let rendered = err.render(&src); + assert!( + rendered.contains(&format!( + "state array size exceeds the maximum supported value of {MAX_CONST_USIZE}" + )), + "{}", + rendered + ); + + let src = format!( + "model m {{ kind sde states {{ central }} particles {} drift {{ ddt(central) = 0 }} diffusion {{ noise(central) = 0 }} outputs {{ cp = central }} }}", + MAX_CONST_USIZE + 1 + ); + let model = parse_model(&src).expect("model parses"); + let err = analyze_model(&model).expect_err("oversized particle count must fail"); + let rendered = err.render(&src); + assert!( + rendered.contains(&format!( + "particles exceeds the maximum supported value of {MAX_CONST_USIZE}" + )), + "{}", + rendered + ); + + let src = format!( + "model m {{ kind ode states {{ x[{MAX_CONST_USIZE}], central }} dynamics {{ ddt(x[0]) = 0, ddt(central) = 0 }} outputs {{ cp = central }} }}" + ); + let model = parse_model(&src).expect("model parses"); + analyze_model(&model).expect("state array at the cap still analyzes"); +} + +// --------------------------------------------------------------------------- +// Unified pipeline and DslError +// --------------------------------------------------------------------------- + +const VALID_MODEL: &str = r#" +name = bimodal_ke +kind = ode + +params = ke, v +states = central +outputs = cp + +infusion(iv) -> central + +dx(central) = -ke * central +out(cp) = central / v +"#; + +#[test] +fn compile_model_matches_the_staged_pipeline() { + let staged = compile_analyzed_model( + &analyze_model(&parse_model(VALID_MODEL).expect("parses")).expect("analyzes"), + ) + .expect("lowers"); + let one_shot = compile_model(VALID_MODEL).expect("compiles"); + + assert_eq!(one_shot.name, staged.name); + assert_eq!(one_shot.metadata.routes.len(), 1); + assert_eq!(one_shot.metadata.outputs.len(), 1); +} + +#[test] +fn compile_module_lowers_every_model() { + let src = "model a { kind ode states { central } dynamics { ddt(central) = 0 } outputs { cp = central } }\nmodel b { kind ode states { central } dynamics { ddt(central) = 0 } outputs { cp = central } }"; + let module = compile_module(src).expect("module compiles"); + assert_eq!(module.models.len(), 2); +} + +#[test] +fn dsl_error_reports_phase_and_renders_source() { + let err = compile_model("model broken { kind ode").unwrap_err(); + assert!(matches!(err, DslError::Parse(_))); + assert_eq!(err.phase(), DiagnosticPhase::Parse); + assert!(!err.diagnostics().is_empty()); + assert!(err.source().is_some()); + assert!(err.to_string().contains("error[DSL1000]")); + assert!(format!("{err:?}").contains("error[DSL1000]")); + + let src = ode_skeleton("ddt(central) = mystery"); + let err = compile_model(&src).unwrap_err(); + assert!(matches!(err, DslError::Analysis(_))); + assert_eq!(err.phase(), DiagnosticPhase::Analysis); + assert!(err.to_string().contains("error[DSL2000]")); + assert!(err.to_string().contains("unknown identifier `mystery`")); + + let src = "name = m\nkind = ode\nstates = central\ninfusion(iv) -> central\nlag(iv) = 0.5\nddt(central) = 0\nout(cp) = central\n"; + let err = compile_model(src).unwrap_err(); + assert!(matches!(err, DslError::Compile(_))); + assert_eq!(err.phase(), DiagnosticPhase::Compile); + let rendered = err.to_string(); + assert!( + rendered.contains("does not allow `lag` on infusion route `iv`"), + "{}", + rendered + ); +} + +#[test] +fn dsl_error_builds_structured_reports() { + let err = compile_model("model broken { kind ode").unwrap_err(); + let report: DiagnosticReport = err.diagnostic_report("broken.dsl"); + let json = report.to_json().expect("report serializes"); + assert!(json.contains("broken.dsl")); + assert!(json.contains("DSL1000")); + + let src = ode_skeleton("ddt(central) = mystery"); + let err = compile_model(&src).unwrap_err(); + assert!(err.render(&src).contains("error[DSL2000]")); + let report = err.diagnostic_report("model.dsl"); + assert_eq!(report.diagnostics.len(), 1); +} + +#[test] +fn dsl_error_with_source_renders_after_the_fact() { + let err = parse_model("model broken { kind ode").unwrap_err(); + let err = DslError::from(err); + let rendered = err.with_source("model broken { kind ode").to_string(); + assert!(rendered.contains("error[DSL1000]")); +} + +// --------------------------------------------------------------------------- +// Console formatting +// --------------------------------------------------------------------------- + +#[test] +fn display_without_source_is_compact_and_coded() { + let err = ParseError::new("expected `->`", Span::new(3, 5)); + assert_eq!( + err.to_string(), + "error[DSL1000]: expected `->` (at bytes 3..5)" + ); + + let first = Diagnostic::error( + DSL_PARSE_GENERIC, + DiagnosticPhase::Parse, + "first problem", + Span::new(0, 1), + ); + let second = Diagnostic::error( + DSL_PARSE_GENERIC, + DiagnosticPhase::Parse, + "second problem", + Span::new(2, 3), + ); + let err = ParseError::from_diagnostics(vec![first, second]); + assert_eq!( + err.to_string(), + "error[DSL1000]: first problem (at bytes 0..1) (+1 more error)" + ); + + let err = AnalysisError::new("unknown identifier `ke`", Span::new(10, 12)); + assert_eq!( + err.to_string(), + "error[DSL2000]: unknown identifier `ke` (at bytes 10..12)" + ); +} + +#[test] +fn render_does_not_repeat_source_for_labels_on_the_same_line() { + let src = "model m { kind ode constants { x = 1, x = 2 } states { central } dynamics { ddt(central) = x } outputs { cp = central } }"; + let model = parse_model(src).expect("model parses"); + let err = analyze_model(&model).expect_err("duplicate constant must fail"); + let rendered = err.render(src); + assert!(rendered.contains("duplicate constant `x`"), "{}", rendered); + assert_eq!( + rendered.matches("constants { x = 1, x = 2 }").count(), + 1, + "same-line labels share one source excerpt:\n{}", + rendered + ); +} + +#[test] +fn malformed_inputs_fail_with_errors_not_panics() { + let cases = [ + "", + "model", + "model {", + "model m {", + "model m { kind }", + "model m { kind ode", + "states = ", + "states = central\nddt(central) = ", + "states = central\nddt(central) = 1 +\n", + "bolus(iv) -> ", + "x = if (true) 1", + "x = f(", + "params = ke, ", + "kind = invalid", + "model m { kind ode states { x[] } }", + "model m { kind ode routes { oral -> } }", + "model m { kind ode outputs { 18446744073709551616 = central } }", + ]; + for src in cases { + assert!( + parse_model(src).is_err(), + "input should fail cleanly: {src:?}" + ); + } +} + +#[test] +fn route_destination_index_is_bounds_checked() { + let src = "model m { kind ode states { transit[4], central } routes { oral -> transit[9] } dynamics { ddt(transit[0]) = 0, ddt(central) = 0 } outputs { cp = central } }"; + let err = compile_model(src).unwrap_err(); + assert!(matches!(err, DslError::Compile(_))); + let rendered = err.render(src); + assert!( + rendered.contains("indexes element 9, but state length is 4"), + "{}", + rendered + ); +} + +#[test] +fn diagnostic_codes_are_stable_and_documented() { + assert_eq!(DSL_PARSE_GENERIC, DiagnosticCode::new("DSL1000")); + assert_eq!(DSL_ANALYSIS_GENERIC, DiagnosticCode::new("DSL2000")); + + let src = ode_skeleton(&format!("ddt(central) = {}", nested_expr("(", ")", 1_000))); + let err = parse_model(&src).expect_err("deep nesting must fail"); + assert!( + err.diagnostics()[0] + .message + .contains(&format!("maximum nesting depth is {MAX_NESTING_DEPTH}")), + "{}", + err.diagnostics()[0].message + ); +} diff --git a/pharmsol-macros/src/lib.rs b/pharmsol-macros/src/lib.rs index af7ff54a..f9f746e3 100644 --- a/pharmsol-macros/src/lib.rs +++ b/pharmsol-macros/src/lib.rs @@ -83,9 +83,9 @@ enum OdeRouteKind { } struct AnalyticalKernelSpec { - kernel: ResolverAnalyticalKernel, + function: ResolverAnalyticalKernel, runtime_path: TokenStream2, - metadata_kernel: TokenStream2, + metadata_function: TokenStream2, state_count: usize, } @@ -419,15 +419,20 @@ impl Parse for AnalyticalInput { validate_unique_symbolic_indices("output", &outputs, "analytical!")?; validate_routes(&routes, &states, "analytical!")?; - let kernel_spec = resolve_analytical_structure(&structure)?; - validate_analytical_structure_inputs(&structure, kernel_spec.kernel, ¶ms, &derived)?; - if states.len() != kernel_spec.state_count { + let function_spec = resolve_analytical_structure(&structure)?; + validate_analytical_structure_inputs( + &structure, + function_spec.function, + ¶ms, + &derived, + )?; + if states.len() != function_spec.state_count { return Err(syn::Error::new_spanned( &structure, format!( "analytical structure `{}` expects {} state value(s), but `states` declares {}", structure, - kernel_spec.state_count, + function_spec.state_count, states.len() ), )); @@ -454,7 +459,7 @@ impl Parse for AnalyticalInput { )?; validate_analytical_derive_contract( - kernel_spec.kernel, + function_spec.function, ¶ms, &derived, &covariates, @@ -1084,13 +1089,13 @@ fn analytical_error_span<'a>(names: &'a [Ident], target: &str) -> Option<&'a Ide fn validate_analytical_structure_inputs( structure: &Ident, - kernel: ResolverAnalyticalKernel, + function: ResolverAnalyticalKernel, params: &[Ident], derived: &[Ident], ) -> syn::Result { let primary_names = params.iter().map(Ident::to_string).collect::>(); let derived_names = derived.iter().map(Ident::to_string).collect::>(); - AnalyticalStructureInputPlan::for_kernel(kernel, &primary_names, &derived_names).map_err( + AnalyticalStructureInputPlan::for_kernel(function, &primary_names, &derived_names).map_err( |error| match error { pharmsol_dsl::AnalyticalStructureInputError::DuplicatePrimary { name } => { let span = analytical_error_span(params, &name).unwrap_or(structure); @@ -1300,7 +1305,7 @@ fn analyze_derive_expr( } fn validate_analytical_derive_contract( - kernel: ResolverAnalyticalKernel, + function: ResolverAnalyticalKernel, params: &[Ident], derived: &[Ident], covariates: &[Ident], @@ -1344,8 +1349,8 @@ fn validate_analytical_derive_contract( }; let required_derived = match validate_analytical_structure_inputs( - &Ident::new(kernel.name(), Span::call_site()), - kernel, + &Ident::new(function.name(), Span::call_site()), + function, params, derived, ) { @@ -1374,7 +1379,7 @@ fn validate_analytical_derive_contract( let message = if required_derived.contains(&name) { format!( "derived parameter `{name}` is not definitely assigned on every path before analytical structure `{}` uses it", - kernel.name() + function.name() ) } else { format!( @@ -2545,7 +2550,7 @@ fn expand_diffeq( fn resolve_analytical_structure(structure: &Ident) -> syn::Result { let structure_name = structure.to_string(); - let (kernel, runtime_path, metadata_kernel, state_count) = match structure_name.as_str() { + let (function, runtime_path, metadata_function, state_count) = match structure_name.as_str() { "one_compartment" => ( ResolverAnalyticalKernel::OneCompartment, quote! { ::pharmsol::equation::one_compartment }, @@ -2627,9 +2632,9 @@ fn resolve_analytical_structure(structure: &Ident) -> syn::Result TokenStream { /// | `states` | yes | State identifiers | /// | `outputs` | yes | Output identifiers | /// | `routes` | no | Route declarations | -/// | `structure` | yes | Built‑in kernel name, e.g. `one_compartment` or `one_compartment_with_absorption` | +/// | `structure` | yes | Built‑in function name, e.g. `one_compartment` or `one_compartment_with_absorption` | /// | `derive` | no | Closure `\|t\| { … }` computing derived parameters from primaries and covariates | /// | `lag` | no | Lag‑time closure | /// | `fa` | no | Bioavailability closure | @@ -3448,13 +3453,13 @@ pub fn analytical(input: TokenStream) -> TokenStream { let input = syn::parse_macro_input!(input as AnalyticalInput); let route_bindings = ode_route_input_bindings(&input.routes); - let kernel_spec = match resolve_analytical_structure(&input.structure) { + let function_spec = match resolve_analytical_structure(&input.structure) { Ok(spec) => spec, Err(error) => return error.to_compile_error().into(), }; let projection = match validate_analytical_structure_inputs( &input.structure, - kernel_spec.kernel, + function_spec.function, &input.params, &input.derived, ) { @@ -3517,7 +3522,7 @@ pub fn analytical(input: TokenStream) -> TokenStream { Ok(derive) => derive, Err(error) => return error.to_compile_error().into(), }; - let eq = expand_analytical_runtime(&kernel_spec.runtime_path, projection.kind()); + let eq = expand_analytical_runtime(&function_spec.runtime_path, projection.kind()); let out = match expand_analytical_out( &input.out, @@ -3590,7 +3595,7 @@ pub fn analytical(input: TokenStream) -> TokenStream { let states = &input.states; let outputs = &input.outputs; let routes = expand_analytical_route_metadata(&input.routes, &lag_routes, &fa_routes); - let metadata_kernel = kernel_spec.metadata_kernel; + let metadata_function = function_spec.metadata_function; let covariate_metadata = if covariates.is_empty() { quote! {} } else { @@ -3608,7 +3613,7 @@ pub fn analytical(input: TokenStream) -> TokenStream { .states([#(stringify!(#states)),*]) .outputs([#(stringify!(#outputs)),*]) #(.route(#routes))* - .analytical_kernel(#metadata_kernel); + .analytical_kernel(#metadata_function); ::pharmsol::equation::Analytical::new( #eq, diff --git a/src/dsl/aot.rs b/src/dsl/aot.rs index 0e76c4cc..7e5bf462 100644 --- a/src/dsl/aot.rs +++ b/src/dsl/aot.rs @@ -21,7 +21,9 @@ use super::compiled_backend_abi::{ OUTPUTS_SYMBOL, ROUTE_BIOAVAILABILITY_SYMBOL, ROUTE_LAG_SYMBOL, }; #[cfg(feature = "dsl-aot-load")] -use super::native::{CompiledNativeModel, DenseKernelFn, NativeExecutionArtifact, NativeModelInfo}; +use super::native::{ + CompiledModelFunction, CompiledNativeModel, NativeExecutionArtifact, NativeModelInfo, +}; #[cfg(feature = "dsl-aot")] use super::rust_backend::{emit_rust_backend_source, RustBackendFlavor}; #[cfg(feature = "dsl-aot")] @@ -34,8 +36,8 @@ use crate::build_support::{rustc_host_target, rustup_installed_targets}; #[cfg(feature = "dsl-aot-load")] use pharmsol_dsl::ModelKind; #[cfg(feature = "dsl-aot")] -use pharmsol_dsl::{analyze_module, lower_typed_model, parse_module, ExecutionModel}; -use pharmsol_dsl::{Diagnostic, DiagnosticReport, LoweringError, ParseError, SemanticError}; +use pharmsol_dsl::{analyze_module, compile_analyzed_model, parse_module, ExecutionModel}; +use pharmsol_dsl::{AnalysisError, CompileError, Diagnostic, DiagnosticReport, ParseError}; /// ABI version for native AoT artifacts produced by this crate. pub const AOT_API_VERSION: u32 = 2; @@ -116,9 +118,9 @@ pub enum AotError { #[error("failed to parse DSL source: {0}")] Parse(#[source] ParseError), #[error("failed to analyze DSL source: {0}")] - Semantic(#[source] SemanticError), - #[error("failed to lower DSL model: {0}")] - Lowering(#[source] LoweringError), + Analysis(#[source] AnalysisError), + #[error("failed to compile DSL model: {0}")] + Compile(#[source] CompileError), #[error("{0}")] ModelSelection(String), #[error("AoT artifact API version mismatch: expected {expected}, found {found}")] @@ -135,8 +137,8 @@ impl AotError { pub fn diagnostic(&self) -> Option<&Diagnostic> { match self { Self::Parse(error) => Some(error.diagnostic()), - Self::Semantic(error) => Some(error.diagnostic()), - Self::Lowering(error) => Some(error.diagnostic()), + Self::Analysis(error) => Some(error.diagnostic()), + Self::Compile(error) => Some(error.diagnostic()), _ => None, } } @@ -149,8 +151,8 @@ impl AotError { let source_name = source_name.into(); match self { Self::Parse(error) => Some(error.diagnostic_report(source_name)), - Self::Semantic(error) => Some(error.diagnostic_report(source_name)), - Self::Lowering(error) => Some(error.diagnostic_report(source_name)), + Self::Analysis(error) => Some(error.diagnostic_report(source_name)), + Self::Compile(error) => Some(error.diagnostic_report(source_name)), _ => None, } } @@ -160,8 +162,8 @@ impl fmt::Debug for AotError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Parse(error) => fmt::Display::fmt(error, f), - Self::Semantic(error) => fmt::Display::fmt(error, f), - Self::Lowering(error) => fmt::Display::fmt(error, f), + Self::Analysis(error) => fmt::Display::fmt(error, f), + Self::Compile(error) => fmt::Display::fmt(error, f), _ => fmt::Display::fmt(self, f), } } @@ -213,18 +215,18 @@ pub fn compile_module_source_to_aot( ) -> Result { let parsed = parse_module(source).map_err(|error| AotError::Parse(error.with_source(source)))?; - let typed = - analyze_module(&parsed).map_err(|error| AotError::Semantic(error.with_source(source)))?; + let analyzed = + analyze_module(&parsed).map_err(|error| AotError::Analysis(error.with_source(source)))?; let model = match model_name { - Some(name) => typed + Some(name) => analyzed .models .iter() .find(|model| model.name == name) .ok_or_else(|| { AotError::ModelSelection(format!("model `{name}` not found in module")) })?, - None if typed.models.len() == 1 => &typed.models[0], + None if analyzed.models.len() == 1 => &analyzed.models[0], None => { return Err(AotError::ModelSelection( "module contains multiple models; pass an explicit model name".to_string(), @@ -232,13 +234,13 @@ pub fn compile_module_source_to_aot( } }; - let execution = - lower_typed_model(model).map_err(|error| AotError::Lowering(error.with_source(source)))?; + let execution = compile_analyzed_model(model) + .map_err(|error| AotError::Compile(error.with_source(source)))?; export_execution_model_to_aot(&execution, options, event_callback) } #[cfg(feature = "dsl-aot")] -/// Export a lowered execution model as a native AoT artifact. +/// Export a compiled execution model as a native AoT artifact. /// /// Use this lower-level entrypoint when you already own the frontend pipeline /// and only need artifact generation. @@ -301,7 +303,7 @@ pub fn export_execution_model_to_aot( /// Read only the metadata from a native AoT artifact. /// /// This is useful when you need to inspect model identity, routes, outputs, or -/// buffer sizes without loading the executable kernels. +/// buffer sizes without loading the executable functions. pub fn read_aot_model_info(path: impl AsRef) -> Result { let library = unsafe { Library::new(path.as_ref()) } .map_err(|error| AotError::Load(error.to_string()))?; @@ -322,14 +324,14 @@ pub fn load_aot_model(path: impl AsRef) -> Result Result Result { - let symbol: Symbol = library +) -> Result { + let symbol: Symbol = library .get(name.as_bytes()) .map_err(|_| AotError::MissingSymbol(name))?; Ok(*symbol) } #[cfg(feature = "dsl-aot-load")] -unsafe fn load_optional_kernel(library: &Library, name: &'static str) -> Option { +unsafe fn load_optional_function( + library: &Library, + name: &'static str, +) -> Option { library - .get::(name.as_bytes()) + .get::(name.as_bytes()) .ok() .map(|symbol| *symbol) } @@ -466,7 +471,7 @@ mod tests { use crate::test_fixtures::STRUCTURED_BLOCK_CORPUS; use crate::{Parameters, SubjectBuilderExt}; use approx::assert_relative_eq; - use pharmsol_dsl::{DiagnosticPhase, DSL_SEMANTIC_GENERIC}; + use pharmsol_dsl::{DiagnosticPhase, DSL_ANALYSIS_GENERIC}; use std::sync::{Arc, Mutex}; use tempfile::tempdir; @@ -480,13 +485,13 @@ mod tests { fn load_corpus_model(name: &str) -> ExecutionModel { let source = STRUCTURED_BLOCK_CORPUS; let parsed = pharmsol_dsl::parse_module(source).expect("parse corpus module"); - let typed = pharmsol_dsl::analyze_module(&parsed).expect("analyze corpus module"); - let model = typed + let analyzed = pharmsol_dsl::analyze_module(&parsed).expect("analyze corpus module"); + let model = analyzed .models .iter() .find(|model| model.name == name) .expect("model in corpus module"); - pharmsol_dsl::lower_typed_model(model).expect("lower corpus model") + pharmsol_dsl::compile_analyzed_model(model).expect("lower corpus model") } fn resolve_cross_target_smoke_target() -> Result { @@ -783,7 +788,7 @@ mod tests { } #[test] - fn aot_compile_preserves_semantic_diagnostic_structure() { + fn aot_compile_preserves_analysis_diagnostic_structure() { let source = r#" model broken { kind ode @@ -806,8 +811,8 @@ model broken { .expect_err("invalid DSL should fail before AoT compilation"); let diagnostic = error.diagnostic().expect("AoT should expose diagnostic"); - assert_eq!(diagnostic.phase, DiagnosticPhase::Semantic); - assert_eq!(diagnostic.code, DSL_SEMANTIC_GENERIC); + assert_eq!(diagnostic.phase, DiagnosticPhase::Analysis); + assert_eq!(diagnostic.code, DSL_ANALYSIS_GENERIC); assert!(diagnostic.message.contains("unknown route `oral`")); let rendered = error .render_diagnostic(source) @@ -826,7 +831,7 @@ model broken { } #[test] - fn aot_compile_preserves_semantic_suggestions() { + fn aot_compile_preserves_analysis_suggestions() { let source = r#" model broken { kind ode diff --git a/src/dsl/compiled_backend_abi.rs b/src/dsl/compiled_backend_abi.rs index a3c28121..4431ef59 100644 --- a/src/dsl/compiled_backend_abi.rs +++ b/src/dsl/compiled_backend_abi.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; use super::model_info::NativeModelInfo; -use pharmsol_dsl::execution::{ExecutionModel, KernelRole}; +use pharmsol_dsl::execution::{ExecutionModel, ModelFunctionKind}; #[cfg(any( test, @@ -121,7 +121,7 @@ pub const JS_KERNEL_EXPORTS: [(&str, &str); 8] = [ ]; #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] -pub struct CompiledKernelAvailability { +pub struct CompiledFunctionAvailability { pub derive: bool, pub dynamics: bool, pub outputs: bool, @@ -132,36 +132,38 @@ pub struct CompiledKernelAvailability { pub route_bioavailability: bool, } -impl CompiledKernelAvailability { +impl CompiledFunctionAvailability { pub fn from_execution_model(model: &ExecutionModel) -> Self { let mut availability = Self::default(); - for kernel in &model.kernels { - match kernel.role { - KernelRole::Derive => availability.derive = true, - KernelRole::Dynamics => availability.dynamics = true, - KernelRole::Outputs => availability.outputs = true, - KernelRole::Init => availability.init = true, - KernelRole::Drift => availability.drift = true, - KernelRole::Diffusion => availability.diffusion = true, - KernelRole::RouteLag => availability.route_lag = true, - KernelRole::RouteBioavailability => availability.route_bioavailability = true, - KernelRole::Analytical => {} + for function in &model.functions { + match function.kind { + ModelFunctionKind::Derive => availability.derive = true, + ModelFunctionKind::Dynamics => availability.dynamics = true, + ModelFunctionKind::Outputs => availability.outputs = true, + ModelFunctionKind::Init => availability.init = true, + ModelFunctionKind::Drift => availability.drift = true, + ModelFunctionKind::Diffusion => availability.diffusion = true, + ModelFunctionKind::RouteLag => availability.route_lag = true, + ModelFunctionKind::RouteBioavailability => { + availability.route_bioavailability = true + } + ModelFunctionKind::Analytical => {} } } availability } - pub fn has(self, role: KernelRole) -> bool { + pub fn has(self, role: ModelFunctionKind) -> bool { match role { - KernelRole::Derive => self.derive, - KernelRole::Dynamics => self.dynamics, - KernelRole::Outputs => self.outputs, - KernelRole::Init => self.init, - KernelRole::Drift => self.drift, - KernelRole::Diffusion => self.diffusion, - KernelRole::RouteLag => self.route_lag, - KernelRole::RouteBioavailability => self.route_bioavailability, - KernelRole::Analytical => false, + ModelFunctionKind::Derive => self.derive, + ModelFunctionKind::Dynamics => self.dynamics, + ModelFunctionKind::Outputs => self.outputs, + ModelFunctionKind::Init => self.init, + ModelFunctionKind::Drift => self.drift, + ModelFunctionKind::Diffusion => self.diffusion, + ModelFunctionKind::RouteLag => self.route_lag, + ModelFunctionKind::RouteBioavailability => self.route_bioavailability, + ModelFunctionKind::Analytical => false, } } } @@ -170,7 +172,7 @@ impl CompiledKernelAvailability { pub struct CompiledModelInfoEnvelope { pub abi_version: u32, pub model: NativeModelInfo, - pub kernels: CompiledKernelAvailability, + pub functions: CompiledFunctionAvailability, } #[cfg(any(feature = "dsl-aot", feature = "dsl-wasm-compile"))] @@ -181,7 +183,7 @@ pub fn compiled_model_info_envelope( CompiledModelInfoEnvelope { abi_version, model: NativeModelInfo::from_execution_model(model), - kernels: CompiledKernelAvailability::from_execution_model(model), + functions: CompiledFunctionAvailability::from_execution_model(model), } } @@ -208,17 +210,17 @@ pub fn decode_compiled_model_info( } #[cfg(any(feature = "dsl-aot", feature = "dsl-wasm-compile"))] -pub fn compiled_kernel_symbol(role: KernelRole) -> Option<&'static str> { +pub fn compiled_function_symbol(role: ModelFunctionKind) -> Option<&'static str> { match role { - KernelRole::Derive => Some(DERIVE_SYMBOL), - KernelRole::Dynamics => Some(DYNAMICS_SYMBOL), - KernelRole::Outputs => Some(OUTPUTS_SYMBOL), - KernelRole::Init => Some(INIT_SYMBOL), - KernelRole::Drift => Some(DRIFT_SYMBOL), - KernelRole::Diffusion => Some(DIFFUSION_SYMBOL), - KernelRole::RouteLag => Some(ROUTE_LAG_SYMBOL), - KernelRole::RouteBioavailability => Some(ROUTE_BIOAVAILABILITY_SYMBOL), - KernelRole::Analytical => None, + ModelFunctionKind::Derive => Some(DERIVE_SYMBOL), + ModelFunctionKind::Dynamics => Some(DYNAMICS_SYMBOL), + ModelFunctionKind::Outputs => Some(OUTPUTS_SYMBOL), + ModelFunctionKind::Init => Some(INIT_SYMBOL), + ModelFunctionKind::Drift => Some(DRIFT_SYMBOL), + ModelFunctionKind::Diffusion => Some(DIFFUSION_SYMBOL), + ModelFunctionKind::RouteLag => Some(ROUTE_LAG_SYMBOL), + ModelFunctionKind::RouteBioavailability => Some(ROUTE_BIOAVAILABILITY_SYMBOL), + ModelFunctionKind::Analytical => None, } } @@ -241,7 +243,7 @@ pub struct OutputBufferPlan { #[cfg(test)] pub fn output_buffer_plan( info: &NativeModelInfo, - role: KernelRole, + role: ModelFunctionKind, aliases_states: bool, aliases_derived: bool, ) -> OutputBufferPlan { @@ -255,21 +257,22 @@ pub fn output_buffer_plan( OutputBufferPlan { binding, - len: kernel_output_len(info, role), + len: function_output_len(info, role), zero_before_call: matches!(binding, OutputBufferBinding::Scratch), } } #[cfg(test)] -fn kernel_output_len(info: &NativeModelInfo, role: KernelRole) -> usize { +fn function_output_len(info: &NativeModelInfo, role: ModelFunctionKind) -> usize { match role { - KernelRole::Derive => info.derived_len, - KernelRole::Dynamics | KernelRole::Init | KernelRole::Drift | KernelRole::Diffusion => { - info.state_len - } - KernelRole::Outputs => info.output_len, - KernelRole::RouteLag | KernelRole::RouteBioavailability => info.route_len, - KernelRole::Analytical => 0, + ModelFunctionKind::Derive => info.derived_len, + ModelFunctionKind::Dynamics + | ModelFunctionKind::Init + | ModelFunctionKind::Drift + | ModelFunctionKind::Diffusion => info.state_len, + ModelFunctionKind::Outputs => info.output_len, + ModelFunctionKind::RouteLag | ModelFunctionKind::RouteBioavailability => info.route_len, + ModelFunctionKind::Analytical => 0, } } @@ -313,7 +316,7 @@ mod tests { } #[test] - fn compiled_model_info_round_trips_kernel_availability_and_dimensions() { + fn compiled_model_info_round_trips_function_availability_and_dimensions() { let envelope = CompiledModelInfoEnvelope { abi_version: 7, model: NativeModelInfo { @@ -358,7 +361,7 @@ mod tests { analytical: None, particles: Some(32), }, - kernels: CompiledKernelAvailability { + functions: CompiledFunctionAvailability { derive: true, dynamics: true, outputs: true, @@ -394,17 +397,17 @@ mod tests { particles: None, }; - let scratch = output_buffer_plan(&info, KernelRole::Diffusion, false, false); + let scratch = output_buffer_plan(&info, ModelFunctionKind::Diffusion, false, false); assert_eq!(scratch.binding, OutputBufferBinding::Scratch); assert_eq!(scratch.len, 2); assert!(scratch.zero_before_call); - let states = output_buffer_plan(&info, KernelRole::Dynamics, true, false); + let states = output_buffer_plan(&info, ModelFunctionKind::Dynamics, true, false); assert_eq!(states.binding, OutputBufferBinding::States); assert_eq!(states.len, 2); assert!(!states.zero_before_call); - let derived = output_buffer_plan(&info, KernelRole::Derive, false, true); + let derived = output_buffer_plan(&info, ModelFunctionKind::Derive, false, true); assert_eq!(derived.binding, OutputBufferBinding::Derived); assert_eq!(derived.len, 3); assert!(!derived.zero_before_call); diff --git a/src/dsl/jit.rs b/src/dsl/jit.rs index d9691b57..3182a840 100644 --- a/src/dsl/jit.rs +++ b/src/dsl/jit.rs @@ -7,20 +7,20 @@ use cranelift::prelude::*; use cranelift_jit::{JITBuilder, JITModule}; use cranelift_module::{Linkage, Module}; -pub use super::native::DenseKernelFn; +pub use super::native::CompiledModelFunction; use super::native::{ CompiledNativeModel, NativeAnalyticalModel, NativeExecutionArtifact, NativeModelInfo, NativeOdeModel, NativeSdeModel, }; use pharmsol_dsl::execution::{ ExecutionBlock, ExecutionCall, ExecutionExpr, ExecutionExprKind, ExecutionForStmt, - ExecutionIfStmt, ExecutionKernel, ExecutionLoad, ExecutionModel, ExecutionProgram, - ExecutionStateRef, ExecutionStmt, ExecutionStmtKind, ExecutionTarget, ExecutionTargetKind, - KernelImplementation, KernelRole, + ExecutionIfStmt, ExecutionLoad, ExecutionModel, ExecutionProgram, ExecutionStateRef, + ExecutionStmt, ExecutionStmtKind, ExecutionTarget, ExecutionTargetKind, FunctionBody, + ModelFunction, ModelFunctionKind, }; use pharmsol_dsl::{ - ConstValue, Diagnostic, DiagnosticPhase, DiagnosticReport, MathIntrinsic, ModelKind, Span, - TypedBinaryOp, TypedUnaryOp, ValueType, DSL_BACKEND_GENERIC, + AnalyzedBinaryOp, AnalyzedUnaryOp, ConstValue, Diagnostic, DiagnosticPhase, DiagnosticReport, + MathFunction, ModelKind, Span, ValueType, DSL_BACKEND_GENERIC, }; mod externs { @@ -141,8 +141,8 @@ impl std::fmt::Display for JitCompileError { let span = self.diagnostic.primary_span(); write!( f, - "{} at bytes {}..{}", - self.diagnostic.message, span.start, span.end + "error[{}]: {} (at bytes {}..{})", + self.diagnostic.code, self.diagnostic.message, span.start, span.end ) } } @@ -190,7 +190,7 @@ struct ExternRefs { } #[derive(Clone, Copy)] -struct KernelArgs { +struct FunctionArgs { _time: Value, states: Value, params: Value, @@ -208,7 +208,7 @@ struct LocalBinding { struct EmitEnv<'a> { _ptr_ty: Type, - args: KernelArgs, + args: FunctionArgs, externs: ExternRefs, locals: &'a BTreeMap, } @@ -219,9 +219,9 @@ struct LoweredValue { ty: ValueType, } -/// Compile one lowered execution model into a reusable JIT kernel artifact. +/// Compile one compiled execution model into a reusable JIT function artifact. /// -/// This builds the raw Cranelift-compiled kernel bundle for all roles present in +/// This builds the raw Cranelift-compiled function bundle for all roles present in /// the model. Most callers should use [`compile_execution_model_to_jit`] instead. pub fn compile_execution_artifact( model: &ExecutionModel, @@ -261,78 +261,78 @@ pub fn compile_execution_artifact( let mut ctx = module.make_context(); let mut builder_context = FunctionBuilderContext::new(); - let derive = compile_role_kernel( + let derive = compile_role_function( &mut module, &mut ctx, &mut builder_context, ptr_ty, externs, model, - KernelRole::Derive, + ModelFunctionKind::Derive, )?; - let dynamics = compile_role_kernel( + let dynamics = compile_role_function( &mut module, &mut ctx, &mut builder_context, ptr_ty, externs, model, - KernelRole::Dynamics, + ModelFunctionKind::Dynamics, )?; - let outputs = compile_role_kernel( + let outputs = compile_role_function( &mut module, &mut ctx, &mut builder_context, ptr_ty, externs, model, - KernelRole::Outputs, + ModelFunctionKind::Outputs, )? - .ok_or_else(|| JitCompileError::new("missing outputs kernel", Some(model.span)))?; - let init = compile_role_kernel( + .ok_or_else(|| JitCompileError::new("missing outputs function", Some(model.span)))?; + let init = compile_role_function( &mut module, &mut ctx, &mut builder_context, ptr_ty, externs, model, - KernelRole::Init, + ModelFunctionKind::Init, )?; - let drift = compile_role_kernel( + let drift = compile_role_function( &mut module, &mut ctx, &mut builder_context, ptr_ty, externs, model, - KernelRole::Drift, + ModelFunctionKind::Drift, )?; - let diffusion = compile_role_kernel( + let diffusion = compile_role_function( &mut module, &mut ctx, &mut builder_context, ptr_ty, externs, model, - KernelRole::Diffusion, + ModelFunctionKind::Diffusion, )?; - let route_lag = compile_role_kernel( + let route_lag = compile_role_function( &mut module, &mut ctx, &mut builder_context, ptr_ty, externs, model, - KernelRole::RouteLag, + ModelFunctionKind::RouteLag, )?; - let route_bioavailability = compile_role_kernel( + let route_bioavailability = compile_role_function( &mut module, &mut ctx, &mut builder_context, ptr_ty, externs, model, - KernelRole::RouteBioavailability, + ModelFunctionKind::RouteBioavailability, )?; module @@ -389,52 +389,52 @@ fn declare_externs(module: &mut JITModule, span: Span) -> Result Result, JitCompileError> { - let Some(kernel) = model.kernel(role) else { + let Some(function) = model.function(role) else { return Ok(None); }; - let KernelImplementation::Statements(program) = &kernel.implementation else { + let FunctionBody::Statements(program) = &function.body else { return Ok(None); }; - let function_name = format!("{}_{}", model.name, kernel_role_name(role)); - let function_id = emit_statement_kernel( + let function_name = format!("{}_{}", model.name, function_kind_name(role)); + let function_id = emit_statement_function( module, ctx, builder_context, ptr_ty, externs, &function_name, - kernel, + function, program, )?; Ok(Some(function_id)) } #[allow(clippy::too_many_arguments)] -fn emit_statement_kernel( +fn emit_statement_function( module: &mut JITModule, ctx: &mut cranelift::codegen::Context, builder_context: &mut FunctionBuilderContext, ptr_ty: Type, externs: ExternIds, function_name: &str, - kernel: &ExecutionKernel, + function: &ModelFunction, program: &ExecutionProgram, ) -> Result { - ctx.func.signature = dense_kernel_signature(module); + ctx.func.signature = dense_function_signature(module); let function_id = module .declare_function(function_name, Linkage::Local, &ctx.func.signature) - .map_err(|error| JitCompileError::new(error.to_string(), Some(kernel.span)))?; + .map_err(|error| JitCompileError::new(error.to_string(), Some(function.span)))?; let mut builder = FunctionBuilder::new(&mut ctx.func, builder_context); let entry = builder.create_block(); @@ -443,7 +443,7 @@ fn emit_statement_kernel( builder.seal_block(entry); let params = builder.block_params(entry); - let args = KernelArgs { + let args = FunctionArgs { _time: params[0], states: params[1], params: params[2], @@ -495,12 +495,12 @@ fn emit_statement_kernel( module .define_function(function_id, ctx) - .map_err(|error| JitCompileError::new(error.to_string(), Some(kernel.span)))?; + .map_err(|error| JitCompileError::new(error.to_string(), Some(function.span)))?; module.clear_context(ctx); Ok(function_id) } -fn dense_kernel_signature(module: &mut JITModule) -> cranelift::codegen::ir::Signature { +fn dense_function_signature(module: &mut JITModule) -> cranelift::codegen::ir::Signature { let mut signature = module.make_signature(); let ptr_ty = module.target_config().pointer_type(); signature.params.push(AbiParam::new(types::F64)); @@ -513,21 +513,21 @@ fn dense_kernel_signature(module: &mut JITModule) -> cranelift::codegen::ir::Sig fn function_pointer( module: &mut JITModule, function_id: cranelift_module::FuncId, -) -> DenseKernelFn { +) -> CompiledModelFunction { unsafe { mem::transmute(module.get_finalized_function(function_id)) } } -fn kernel_role_name(role: KernelRole) -> &'static str { +fn function_kind_name(role: ModelFunctionKind) -> &'static str { match role { - KernelRole::Derive => "derive", - KernelRole::Dynamics => "dynamics", - KernelRole::Outputs => "outputs", - KernelRole::Init => "init", - KernelRole::Drift => "drift", - KernelRole::Diffusion => "diffusion", - KernelRole::RouteLag => "route_lag", - KernelRole::RouteBioavailability => "route_bioavailability", - KernelRole::Analytical => "analytical", + ModelFunctionKind::Derive => "derive", + ModelFunctionKind::Dynamics => "dynamics", + ModelFunctionKind::Outputs => "outputs", + ModelFunctionKind::Init => "init", + ModelFunctionKind::Drift => "drift", + ModelFunctionKind::Diffusion => "diffusion", + ModelFunctionKind::RouteLag => "route_lag", + ModelFunctionKind::RouteBioavailability => "route_bioavailability", + ModelFunctionKind::Analytical => "analytical", } } @@ -703,11 +703,11 @@ fn lower_expr( lower_binary(builder, env, *op, lhs, rhs, expr.ty, expr.span) } ExecutionExprKind::Call { callee, args } => { - let lowered = args + let compiled = args .iter() .map(|arg| lower_expr(builder, env, arg)) .collect::, _>>()?; - lower_call(builder, env, callee, &lowered, expr.ty, expr.span) + lower_call(builder, env, callee, &compiled, expr.ty, expr.span) } } } @@ -758,16 +758,16 @@ fn lower_load( fn lower_unary( builder: &mut FunctionBuilder<'_>, _env: &EmitEnv<'_>, - op: TypedUnaryOp, + op: AnalyzedUnaryOp, value: LoweredValue, target_ty: ValueType, span: Span, ) -> Result { match op { - TypedUnaryOp::Plus => cast_value(builder, value, target_ty, span), - TypedUnaryOp::Minus => { + AnalyzedUnaryOp::Plus => cast_value(builder, value, target_ty, span), + AnalyzedUnaryOp::Minus => { let value = cast_value(builder, value, target_ty, span)?; - let lowered = match target_ty { + let compiled = match target_ty { ValueType::Real => builder.ins().fneg(value.value), ValueType::Int => builder.ins().ineg(value.value), ValueType::Bool => { @@ -778,17 +778,17 @@ fn lower_unary( } }; Ok(LoweredValue { - value: lowered, + value: compiled, ty: target_ty, }) } - TypedUnaryOp::Not => { + AnalyzedUnaryOp::Not => { let condition = as_bool(builder, value, span)?; let condition_i64 = bool_to_i64(builder, condition); let is_zero = builder.ins().icmp_imm(IntCC::Equal, condition_i64, 0); - let lowered = bool_to_i64(builder, is_zero); + let compiled = bool_to_i64(builder, is_zero); Ok(LoweredValue { - value: lowered, + value: compiled, ty: ValueType::Bool, }) } @@ -798,14 +798,14 @@ fn lower_unary( fn lower_binary( builder: &mut FunctionBuilder<'_>, env: &EmitEnv<'_>, - op: TypedBinaryOp, + op: AnalyzedBinaryOp, lhs: LoweredValue, rhs: LoweredValue, target_ty: ValueType, span: Span, ) -> Result { match op { - TypedBinaryOp::Or => { + AnalyzedBinaryOp::Or => { let lhs = as_i64_bool(builder, lhs, span)?; let rhs = as_i64_bool(builder, rhs, span)?; Ok(LoweredValue { @@ -813,7 +813,7 @@ fn lower_binary( ty: ValueType::Bool, }) } - TypedBinaryOp::And => { + AnalyzedBinaryOp::And => { let lhs = as_i64_bool(builder, lhs, span)?; let rhs = as_i64_bool(builder, rhs, span)?; Ok(LoweredValue { @@ -821,30 +821,39 @@ fn lower_binary( ty: ValueType::Bool, }) } - TypedBinaryOp::Eq | TypedBinaryOp::NotEq => { + AnalyzedBinaryOp::Eq | AnalyzedBinaryOp::NotEq => { let value = lower_equality(builder, lhs, rhs, target_ty, op, span)?; Ok(LoweredValue { value, ty: ValueType::Bool, }) } - TypedBinaryOp::Lt | TypedBinaryOp::LtEq | TypedBinaryOp::Gt | TypedBinaryOp::GtEq => { + AnalyzedBinaryOp::Lt + | AnalyzedBinaryOp::LtEq + | AnalyzedBinaryOp::Gt + | AnalyzedBinaryOp::GtEq => { let value = lower_comparison(builder, lhs, rhs, span, op)?; Ok(LoweredValue { value, ty: ValueType::Bool, }) } - TypedBinaryOp::Add | TypedBinaryOp::Sub | TypedBinaryOp::Mul => { + AnalyzedBinaryOp::Add | AnalyzedBinaryOp::Sub | AnalyzedBinaryOp::Mul => { let lhs = cast_value(builder, lhs, target_ty, span)?; let rhs = cast_value(builder, rhs, target_ty, span)?; let value = match (op, target_ty) { - (TypedBinaryOp::Add, ValueType::Real) => builder.ins().fadd(lhs.value, rhs.value), - (TypedBinaryOp::Sub, ValueType::Real) => builder.ins().fsub(lhs.value, rhs.value), - (TypedBinaryOp::Mul, ValueType::Real) => builder.ins().fmul(lhs.value, rhs.value), - (TypedBinaryOp::Add, ValueType::Int) => builder.ins().iadd(lhs.value, rhs.value), - (TypedBinaryOp::Sub, ValueType::Int) => builder.ins().isub(lhs.value, rhs.value), - (TypedBinaryOp::Mul, ValueType::Int) => builder.ins().imul(lhs.value, rhs.value), + (AnalyzedBinaryOp::Add, ValueType::Real) => { + builder.ins().fadd(lhs.value, rhs.value) + } + (AnalyzedBinaryOp::Sub, ValueType::Real) => { + builder.ins().fsub(lhs.value, rhs.value) + } + (AnalyzedBinaryOp::Mul, ValueType::Real) => { + builder.ins().fmul(lhs.value, rhs.value) + } + (AnalyzedBinaryOp::Add, ValueType::Int) => builder.ins().iadd(lhs.value, rhs.value), + (AnalyzedBinaryOp::Sub, ValueType::Int) => builder.ins().isub(lhs.value, rhs.value), + (AnalyzedBinaryOp::Mul, ValueType::Int) => builder.ins().imul(lhs.value, rhs.value), _ => { return Err(JitCompileError::new( "invalid arithmetic operand types", @@ -857,7 +866,7 @@ fn lower_binary( ty: target_ty, }) } - TypedBinaryOp::Div => { + AnalyzedBinaryOp::Div => { let lhs = cast_value(builder, lhs, ValueType::Real, span)?; let rhs = cast_value(builder, rhs, ValueType::Real, span)?; Ok(LoweredValue { @@ -865,10 +874,10 @@ fn lower_binary( ty: ValueType::Real, }) } - TypedBinaryOp::Pow => lower_call( + AnalyzedBinaryOp::Pow => lower_call( builder, env, - &ExecutionCall::Math(MathIntrinsic::Pow), + &ExecutionCall::Math(MathFunction::Pow), &[lhs, rhs], target_ty, span, @@ -894,13 +903,13 @@ fn lower_call( fn lower_math_call( builder: &mut FunctionBuilder<'_>, env: &EmitEnv<'_>, - intrinsic: MathIntrinsic, + intrinsic: MathFunction, args: &[LoweredValue], target_ty: ValueType, span: Span, ) -> Result { match intrinsic { - MathIntrinsic::Max | MathIntrinsic::Min => { + MathFunction::Max | MathFunction::Min => { if args.len() != 2 { return Err(JitCompileError::new( format!("{:?} expects 2 arguments", intrinsic), @@ -912,7 +921,7 @@ fn lower_math_call( let value = match target_ty { ValueType::Real => { let compare = builder.ins().fcmp( - if intrinsic == MathIntrinsic::Max { + if intrinsic == MathFunction::Max { FloatCC::GreaterThan } else { FloatCC::LessThan @@ -924,7 +933,7 @@ fn lower_math_call( } ValueType::Int => { let compare = builder.ins().icmp( - if intrinsic == MathIntrinsic::Max { + if intrinsic == MathFunction::Max { IntCC::SignedGreaterThan } else { IntCC::SignedLessThan @@ -946,7 +955,7 @@ fn lower_math_call( ty: target_ty, }) } - MathIntrinsic::Abs if target_ty == ValueType::Int => { + MathFunction::Abs if target_ty == ValueType::Int => { let value = cast_value(builder, args[0], ValueType::Int, span)?; let is_negative = builder .ins() @@ -959,20 +968,20 @@ fn lower_math_call( } _ => { let function = match intrinsic { - MathIntrinsic::Abs => env.externs.abs, - MathIntrinsic::Ceil => env.externs.ceil, - MathIntrinsic::Exp => env.externs.exp, - MathIntrinsic::Floor => env.externs.floor, - MathIntrinsic::Ln | MathIntrinsic::Log => env.externs.ln, - MathIntrinsic::Log10 => env.externs.log10, - MathIntrinsic::Log2 => env.externs.log2, - MathIntrinsic::Pow => env.externs.pow, - MathIntrinsic::Round => env.externs.round, - MathIntrinsic::Sin => env.externs.sin, - MathIntrinsic::Cos => env.externs.cos, - MathIntrinsic::Tan => env.externs.tan, - MathIntrinsic::Sqrt => env.externs.sqrt, - MathIntrinsic::Max | MathIntrinsic::Min => unreachable!(), + MathFunction::Abs => env.externs.abs, + MathFunction::Ceil => env.externs.ceil, + MathFunction::Exp => env.externs.exp, + MathFunction::Floor => env.externs.floor, + MathFunction::Ln | MathFunction::Log => env.externs.ln, + MathFunction::Log10 => env.externs.log10, + MathFunction::Log2 => env.externs.log2, + MathFunction::Pow => env.externs.pow, + MathFunction::Round => env.externs.round, + MathFunction::Sin => env.externs.sin, + MathFunction::Cos => env.externs.cos, + MathFunction::Tan => env.externs.tan, + MathFunction::Sqrt => env.externs.sqrt, + MathFunction::Max | MathFunction::Min => unreachable!(), }; let mut call_args = Vec::with_capacity(args.len()); @@ -1000,12 +1009,12 @@ fn lower_equality( lhs: LoweredValue, rhs: LoweredValue, target_ty: ValueType, - op: TypedBinaryOp, + op: AnalyzedBinaryOp, span: Span, ) -> Result { let predicate = match op { - TypedBinaryOp::Eq => true, - TypedBinaryOp::NotEq => false, + AnalyzedBinaryOp::Eq => true, + AnalyzedBinaryOp::NotEq => false, _ => unreachable!(), }; @@ -1046,17 +1055,17 @@ fn lower_comparison( lhs: LoweredValue, rhs: LoweredValue, span: Span, - op: TypedBinaryOp, + op: AnalyzedBinaryOp, ) -> Result { let comparison = if lhs.ty == ValueType::Real || rhs.ty == ValueType::Real { let lhs = cast_value(builder, lhs, ValueType::Real, span)?; let rhs = cast_value(builder, rhs, ValueType::Real, span)?; builder.ins().fcmp( match op { - TypedBinaryOp::Lt => FloatCC::LessThan, - TypedBinaryOp::LtEq => FloatCC::LessThanOrEqual, - TypedBinaryOp::Gt => FloatCC::GreaterThan, - TypedBinaryOp::GtEq => FloatCC::GreaterThanOrEqual, + AnalyzedBinaryOp::Lt => FloatCC::LessThan, + AnalyzedBinaryOp::LtEq => FloatCC::LessThanOrEqual, + AnalyzedBinaryOp::Gt => FloatCC::GreaterThan, + AnalyzedBinaryOp::GtEq => FloatCC::GreaterThanOrEqual, _ => unreachable!(), }, lhs.value, @@ -1067,10 +1076,10 @@ fn lower_comparison( let rhs = cast_value(builder, rhs, ValueType::Int, span)?; builder.ins().icmp( match op { - TypedBinaryOp::Lt => IntCC::SignedLessThan, - TypedBinaryOp::LtEq => IntCC::SignedLessThanOrEqual, - TypedBinaryOp::Gt => IntCC::SignedGreaterThan, - TypedBinaryOp::GtEq => IntCC::SignedGreaterThanOrEqual, + AnalyzedBinaryOp::Lt => IntCC::SignedLessThan, + AnalyzedBinaryOp::LtEq => IntCC::SignedLessThanOrEqual, + AnalyzedBinaryOp::Gt => IntCC::SignedGreaterThan, + AnalyzedBinaryOp::GtEq => IntCC::SignedGreaterThanOrEqual, _ => unreachable!(), }, lhs.value, @@ -1090,7 +1099,7 @@ fn cast_value( return Ok(value); } - let lowered = match (value.ty, target_ty) { + let compiled = match (value.ty, target_ty) { (ValueType::Int, ValueType::Real) => builder.ins().fcvt_from_sint(types::F64, value.value), (ValueType::Bool, ValueType::Real) => { let condition = as_bool(builder, value, span)?; @@ -1113,7 +1122,7 @@ fn cast_value( }; Ok(LoweredValue { - value: lowered, + value: compiled, ty: target_ty, }) } @@ -1237,7 +1246,7 @@ fn state_address( /// /// ```rust,no_run /// use pharmsol::dsl::{ -/// analyze_model, compile_execution_model_to_jit, lower_typed_model, parse_model, +/// analyze_model, compile_execution_model_to_jit, compile_analyzed_model, parse_model, /// }; /// /// let parsed = parse_model( @@ -1255,8 +1264,8 @@ fn state_address( /// } /// "#, /// )?; -/// let typed = analyze_model(&parsed)?; -/// let execution = lower_typed_model(&typed)?; +/// let analyzed = analyze_model(&parsed)?; +/// let execution = compile_analyzed_model(&analyzed)?; /// let compiled = compile_execution_model_to_jit(&execution)?; /// # let _ = compiled; /// # Ok::<(), Box>(()) @@ -1339,18 +1348,18 @@ mod tests { use crate::{equation, Parameters, Subject, SubjectBuilderExt, ODE, SDE}; use approx::assert_relative_eq; use diffsol::Vector; - use pharmsol_dsl::execution::DenseBufferLayout; + use pharmsol_dsl::execution::BufferLayout; fn load_corpus_model(name: &str) -> ExecutionModel { let source = STRUCTURED_BLOCK_CORPUS; let parsed = pharmsol_dsl::parse_module(source).expect("parse corpus module"); - let typed = pharmsol_dsl::analyze_module(&parsed).expect("analyze corpus module"); - let model = typed + let analyzed = pharmsol_dsl::analyze_module(&parsed).expect("analyze corpus module"); + let model = analyzed .models .iter() .find(|model| model.name == name) .expect("model present in corpus module"); - pharmsol_dsl::lower_typed_model(model).expect("lower corpus model") + pharmsol_dsl::compile_analyzed_model(model).expect("lower corpus model") } #[test] @@ -1399,8 +1408,9 @@ dx(central) = ka * depot - ke * central out(cp) = central / v ~ continuous() "#; let parsed = pharmsol_dsl::parse_model(source).expect("authoring model parses"); - let typed = pharmsol_dsl::analyze_model(&parsed).expect("authoring model analyzes"); - let model = pharmsol_dsl::lower_typed_model(&typed).expect("authoring model lowers"); + let analyzed = pharmsol_dsl::analyze_model(&parsed).expect("authoring model analyzes"); + let model = + pharmsol_dsl::compile_analyzed_model(&analyzed).expect("authoring model lowers"); let jit = compile_ode_model_to_jit(&model) .expect("compile jit ode model") .with_solver(OdeSolver::ExplicitRk(ExplicitRkTableau::Tsit45)); @@ -1497,7 +1507,7 @@ out(cp) = central / v ~ continuous() } } - fn slot_index(layout: &DenseBufferLayout, name: &str) -> usize { + fn slot_index(layout: &BufferLayout, name: &str) -> usize { layout .slots .iter() @@ -1507,19 +1517,19 @@ out(cp) = central / v ~ continuous() } #[test] - fn compiles_dense_execution_kernels_for_ode_models() { + fn compiles_dense_execution_functions_for_ode_models() { let model = load_corpus_model("one_cmt_oral_iv"); let artifact = compile_execution_artifact(&model).expect("compile execution artifact"); - let mut derived = vec![0.0; model.abi.derived_buffer.len]; - let mut dx = vec![0.0; model.abi.state_buffer.len]; - let mut out = vec![0.0; model.abi.output_buffer.len]; + let mut derived = vec![0.0; model.layout.derived_buffer.len]; + let mut dx = vec![0.0; model.layout.state_buffer.len]; + let mut out = vec![0.0; model.layout.output_buffer.len]; let states = [100.0, 0.0]; let params = [1.0, 5.0, 50.0, 1.5, 0.8]; let covariates = [70.0]; let routes = [0.0, 0.0]; - let derive = artifact.derive.expect("derive kernel present"); + let derive = artifact.derive.expect("derive function present"); unsafe { derive( 0.0, @@ -1530,7 +1540,7 @@ out(cp) = central / v ~ continuous() derived.as_ptr(), derived.as_mut_ptr(), ); - artifact.dynamics.expect("dynamics kernel present")( + artifact.dynamics.expect("dynamics function present")( 0.0, states.as_ptr(), params.as_ptr(), @@ -1550,16 +1560,16 @@ out(cp) = central / v ~ continuous() ); } - let derived_layout = &model.abi.derived_buffer; + let derived_layout = &model.layout.derived_buffer; assert!((derived[slot_index(derived_layout, "cl_i")] - 5.0).abs() < 1e-12); assert!((derived[slot_index(derived_layout, "v_i")] - 50.0).abs() < 1e-12); assert!((derived[slot_index(derived_layout, "ke")] - 0.1).abs() < 1e-12); - let state_layout = &model.abi.state_buffer; + let state_layout = &model.layout.state_buffer; assert!((dx[slot_index(state_layout, "depot")] + 100.0).abs() < 1e-12); assert!((dx[slot_index(state_layout, "central")] - 100.0).abs() < 1e-12); - let output_layout = &model.abi.output_buffer; + let output_layout = &model.layout.output_buffer; assert_eq!(out[slot_index(output_layout, "cp")], 0.0); } @@ -1787,8 +1797,9 @@ model analytical_mixed { } "#; let parsed = pharmsol_dsl::parse_model(source).expect("analytical model parses"); - let typed = pharmsol_dsl::analyze_model(&parsed).expect("analytical model analyzes"); - let model = pharmsol_dsl::lower_typed_model(&typed).expect("analytical model lowers"); + let analyzed = pharmsol_dsl::analyze_model(&parsed).expect("analytical model analyzes"); + let model = + pharmsol_dsl::compile_analyzed_model(&analyzed).expect("analytical model lowers"); let jit = compile_analytical_model_to_jit(&model).expect("compile jit analytical model"); assert_eq!(jit.info().derived, vec!["ke".to_string()]); diff --git a/src/dsl/mod.rs b/src/dsl/mod.rs index 0f4cb0ab..0e08126b 100644 --- a/src/dsl/mod.rs +++ b/src/dsl/mod.rs @@ -2,21 +2,21 @@ //! //! Use this module when you want to work with pharmsol models as source text //! and stay inside the main crate for the full workflow: parse DSL source, -//! inspect diagnostics, lower to the execution model, compile to a runtime +//! inspect diagnostics, compile to the execution model and then to a runtime //! backend, load saved artifacts, and run predictions. //! -//! Use the `pharmsol-dsl` crate directly only when you need the backend-neutral -//! frontend as an engineering API. That crate owns parsing, diagnostics, -//! semantic analysis, and lowering. This module re-exports that stable -//! frontend surface and adds the backend-specific entrypoints that stay owned -//! by `pharmsol`. +//! Use the `pharmsol-dsl` crate directly only when you need the source-to-execution +//! compiler as an engineering API. That crate owns parsing, diagnostics, +//! analysis, and compilation to the execution model. This module re-exports +//! that stable compiler surface and adds the backend-specific entrypoints +//! that stay owned by `pharmsol`. //! //! Main entrypoints: //! //! - [`parse_model`], [`parse_module`], [`analyze_model`], and -//! [`analyze_module`] for frontend-only validation and inspection. -//! - [`lower_typed_model`] and [`lower_typed_module`] for lowering typed models -//! into the execution representation used by the runtime backends. +//! [`analyze_module`] for source-level validation and inspection. +//! - [`compile_analyzed_model`] and [`compile_analyzed_module`] for compiling +//! analyzed models into the ready-to-run form used by the runtime backends. //! - [`compile_module_source_to_runtime`] and [`compile_execution_model_to_runtime`] //! for the one-stop compile-and-run path. //! - [`load_runtime_artifact`], [`load_aot_model`], and @@ -25,8 +25,8 @@ //! //! Common workflow choices: //! -//! - Frontend only: parse, analyze, and lower when you need diagnostics, -//! authoring tools, or your own backend. +//! - Compiler only: parse, analyze, and compile to the execution model when +//! you need diagnostics, authoring tools, or your own backend. //! - In-process execution: compile straight to [`RuntimeCompilationTarget`] and //! keep everything inside the current process. //! - Native artifact shipping: export a native AoT artifact, then load it later @@ -36,7 +36,7 @@ //! //! Feature map: //! -//! - `dsl-core`: enables this facade and the frontend re-exports from +//! - `dsl-core`: enables this facade and the compiler re-exports from //! `pharmsol-dsl`. //! - `dsl-jit`: enables in-process JIT compilation through //! [`compile_module_source_to_runtime`] with @@ -86,7 +86,7 @@ //! # Ok::<(), pharmsol::dsl::RuntimeError>(()) //! ``` //! -//! For a lower-level frontend pipeline without backend selection, use +//! For just the source-to-execution compiler without backend selection, use //! `pharmsol-dsl`. For a complete runtime path inside the main crate, stay in //! [`pharmsol::dsl`](self). @@ -135,7 +135,7 @@ pub use aot::{ pub use aot::{load_aot_model, read_aot_model_info}; #[cfg(all(not(feature = "dsl-aot"), feature = "dsl-aot-load"))] pub use aot::{AotError, AOT_API_VERSION}; -pub use compiled_backend_abi::{CompiledKernelAvailability, CompiledModelInfoEnvelope}; +pub use compiled_backend_abi::{CompiledFunctionAvailability, CompiledModelInfoEnvelope}; #[cfg(feature = "dsl-jit")] pub use jit::{ compile_analytical_model_to_jit, compile_execution_artifact, compile_execution_model_to_jit, @@ -152,7 +152,7 @@ pub use model_info::{NativeCovariateInfo, NativeModelInfo, NativeOutputInfo, Nat ) ))] pub use native::{ - CompiledNativeModel, DenseKernelFn, NativeAnalyticalModel, NativeExecutionArtifact, + CompiledModelFunction, CompiledNativeModel, NativeAnalyticalModel, NativeExecutionArtifact, NativeOdeModel, NativeSdeModel, RuntimeBackend, }; pub use pharmsol_dsl::*; diff --git a/src/dsl/model_info.rs b/src/dsl/model_info.rs index a77f15ba..feb4178d 100644 --- a/src/dsl/model_info.rs +++ b/src/dsl/model_info.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; use pharmsol_dsl::execution::{ ExecutionExpr, ExecutionExprKind, ExecutionLoad, ExecutionModel, ExecutionStmt, - ExecutionStmtKind, KernelImplementation, KernelRole, + ExecutionStmtKind, FunctionBody, ModelFunctionKind, }; use pharmsol_dsl::{AnalyticalKernel, CovariateInterpolation, ModelKind, RouteKind}; @@ -12,7 +12,7 @@ use pharmsol_dsl::{AnalyticalKernel, CovariateInterpolation, ModelKind, RouteKin /// /// This is the shared inspection surface returned by the native AoT, WASM, and /// runtime loaders. It keeps public labels and buffer sizes available without -/// exposing backend-specific kernel details. +/// exposing backend-specific function details. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct NativeModelInfo { /// Public model name. @@ -40,7 +40,7 @@ pub struct NativeModelInfo { pub output_len: usize, /// Length of the dense route-input buffer used during execution. pub route_len: usize, - /// Analytical kernel metadata when the compiled model is analytical. + /// Analytical function metadata when the compiled model is analytical. pub analytical: Option, /// Particle count when the compiled model is stochastic. pub particles: Option, @@ -77,7 +77,7 @@ pub struct NativeRouteInfo { pub index: usize, /// Coarse route kind when declared in metadata. pub kind: Option, - /// Dense destination state offset used by compiled kernels. + /// Dense destination state offset used by compiled functions. pub destination_offset: usize, /// Public destination state name. pub destination_name: String, @@ -101,7 +101,7 @@ pub struct NativeOutputInfo { } impl NativeModelInfo { - /// Build public compiled-model metadata from a lowered execution model. + /// Build public compiled-model metadata from a compiled execution model. pub fn from_execution_model(model: &ExecutionModel) -> Self { let explicit_route_input_usage = explicit_route_input_usage(model); Self { @@ -166,10 +166,10 @@ impl NativeModelInfo { index: output.index, }) .collect(), - state_len: model.abi.state_buffer.len, - derived_len: model.abi.derived_buffer.len, - output_len: model.abi.output_buffer.len, - route_len: model.abi.route_buffer.len, + state_len: model.layout.state_buffer.len, + derived_len: model.layout.derived_buffer.len, + output_len: model.layout.output_buffer.len, + route_len: model.layout.route_buffer.len, analytical: model.metadata.analytical, particles: model.metadata.particles, } @@ -183,16 +183,16 @@ fn explicit_route_input_usage(model: &ExecutionModel) -> Vec { .iter() .map(|route| (route.symbol, route.declaration_index)) .collect::>(); - let Some(kernel) = (match model.kind { - ModelKind::Ode => model.kernel(KernelRole::Dynamics), - ModelKind::Sde => model.kernel(KernelRole::Drift), + let Some(function) = (match model.kind { + ModelKind::Ode => model.function(ModelFunctionKind::Dynamics), + ModelKind::Sde => model.function(ModelFunctionKind::Drift), ModelKind::Analytical => None, }) else { return vec![false; model.metadata.routes.len()]; }; let mut usage = vec![false; model.metadata.routes.len()]; - if let KernelImplementation::Statements(program) = &kernel.implementation { + if let FunctionBody::Statements(program) = &function.body { mark_route_inputs_in_statements(&program.body.statements, &declaration_slots, &mut usage); } usage @@ -261,13 +261,13 @@ fn mark_route_inputs_in_expr( #[cfg(test)] mod tests { use super::*; - use pharmsol_dsl::{analyze_model, lower_typed_model, parse_model}; + use pharmsol_dsl::{analyze_model, compile_analyzed_model, parse_model}; fn load_model_info(src: &str) -> NativeModelInfo { let model = parse_model(src).expect("model parses"); - let typed = analyze_model(&model).expect("model analyzes"); - let lowered = lower_typed_model(&typed).expect("model lowers"); - NativeModelInfo::from_execution_model(&lowered) + let analyzed = analyze_model(&model).expect("model analyzes"); + let compiled = compile_analyzed_model(&analyzed).expect("model lowers"); + NativeModelInfo::from_execution_model(&compiled) } #[test] diff --git a/src/dsl/native.rs b/src/dsl/native.rs index b0aba8fc..64fe8d60 100644 --- a/src/dsl/native.rs +++ b/src/dsl/native.rs @@ -14,10 +14,10 @@ use rayon::prelude::*; use cranelift_jit::JITModule; #[cfg(feature = "dsl-aot-load")] use libloading::Library; -use pharmsol_dsl::execution::KernelRole; +use pharmsol_dsl::execution::ModelFunctionKind; use pharmsol_dsl::{ AnalyticalKernel, AnalyticalStructureInputKind, AnalyticalStructureInputPlan, ModelKind, - RouteKind, NUMERIC_OUTPUT_PREFIX, NUMERIC_ROUTE_PREFIX, + RouteKind, }; pub use super::model_info::{ @@ -32,6 +32,7 @@ use crate::{ DEFAULT_BOUND_ERROR_MODEL_CACHE_SIZE, DEFAULT_CACHE_SIZE, }, equation::{ + metadata::ValidatedRoute, ode::{closure_helpers::PMProblem, ExplicitRkTableau, OdeSolver, SdirkTableau}, sde::simulate_sde_event_with, EqnKind, Equation, EquationPriv, EquationTypes, Predictions, @@ -42,7 +43,7 @@ use crate::{ Event, Observation, Occasion, Parameters, PharmsolError, Subject, ValidatedModelMetadata, }; -pub type DenseKernelFn = unsafe extern "C" fn( +pub type CompiledModelFunction = unsafe extern "C" fn( t: f64, states: *const f64, params: *const f64, @@ -65,11 +66,11 @@ pub enum RuntimeBackend { Wasm, } -pub(crate) trait KernelSession { +pub(crate) trait FunctionSession { #[allow(clippy::too_many_arguments)] unsafe fn invoke_raw( &mut self, - role: KernelRole, + role: ModelFunctionKind, time: f64, states: *const f64, params: *const f64, @@ -82,8 +83,8 @@ pub(crate) trait KernelSession { pub(crate) trait RuntimeArtifact: Send + Sync + std::fmt::Debug { fn backend(&self) -> RuntimeBackend; - fn has_kernel(&self, role: KernelRole) -> bool; - fn start_session(&self) -> Result, PharmsolError>; + fn has_function(&self, role: ModelFunctionKind) -> bool; + fn start_session(&self) -> Result, PharmsolError>; } #[allow(dead_code)] @@ -111,14 +112,14 @@ impl std::fmt::Debug for NativeArtifactOwner { pub struct NativeExecutionArtifact { pub model_name: String, - pub derive: Option, - pub dynamics: Option, - pub outputs: DenseKernelFn, - pub init: Option, - pub drift: Option, - pub diffusion: Option, - pub route_lag: Option, - pub route_bioavailability: Option, + pub derive: Option, + pub dynamics: Option, + pub outputs: CompiledModelFunction, + pub init: Option, + pub drift: Option, + pub diffusion: Option, + pub route_lag: Option, + pub route_bioavailability: Option, _owner: Option, } @@ -149,14 +150,14 @@ impl NativeExecutionArtifact { #[allow(clippy::too_many_arguments)] pub(crate) fn from_jit_module( model_name: String, - derive: Option, - dynamics: Option, - outputs: DenseKernelFn, - init: Option, - drift: Option, - diffusion: Option, - route_lag: Option, - route_bioavailability: Option, + derive: Option, + dynamics: Option, + outputs: CompiledModelFunction, + init: Option, + drift: Option, + diffusion: Option, + route_lag: Option, + route_bioavailability: Option, module: JITModule, ) -> Self { Self { @@ -177,14 +178,14 @@ impl NativeExecutionArtifact { #[allow(clippy::too_many_arguments)] pub(crate) fn from_library( model_name: String, - derive: Option, - dynamics: Option, - outputs: DenseKernelFn, - init: Option, - drift: Option, - diffusion: Option, - route_lag: Option, - route_bioavailability: Option, + derive: Option, + dynamics: Option, + outputs: CompiledModelFunction, + init: Option, + drift: Option, + diffusion: Option, + route_lag: Option, + route_bioavailability: Option, library: Library, ) -> Self { Self { @@ -202,14 +203,14 @@ impl NativeExecutionArtifact { } } -struct NativeKernelSession<'a> { +struct NativeFunctionSession<'a> { artifact: &'a NativeExecutionArtifact, } -impl KernelSession for NativeKernelSession<'_> { +impl FunctionSession for NativeFunctionSession<'_> { unsafe fn invoke_raw( &mut self, - role: KernelRole, + role: ModelFunctionKind, time: f64, states: *const f64, params: *const f64, @@ -218,25 +219,25 @@ impl KernelSession for NativeKernelSession<'_> { derived: *const f64, out: *mut f64, ) -> Result<(), PharmsolError> { - let kernel = match role { - KernelRole::Derive => self.artifact.derive, - KernelRole::Dynamics => self.artifact.dynamics, - KernelRole::Outputs => Some(self.artifact.outputs), - KernelRole::Init => self.artifact.init, - KernelRole::Drift => self.artifact.drift, - KernelRole::Diffusion => self.artifact.diffusion, - KernelRole::RouteLag => self.artifact.route_lag, - KernelRole::RouteBioavailability => self.artifact.route_bioavailability, - KernelRole::Analytical => None, + let function = match role { + ModelFunctionKind::Derive => self.artifact.derive, + ModelFunctionKind::Dynamics => self.artifact.dynamics, + ModelFunctionKind::Outputs => Some(self.artifact.outputs), + ModelFunctionKind::Init => self.artifact.init, + ModelFunctionKind::Drift => self.artifact.drift, + ModelFunctionKind::Diffusion => self.artifact.diffusion, + ModelFunctionKind::RouteLag => self.artifact.route_lag, + ModelFunctionKind::RouteBioavailability => self.artifact.route_bioavailability, + ModelFunctionKind::Analytical => None, } .ok_or_else(|| { PharmsolError::OtherError(format!( - "model `{}` does not provide a {:?} kernel", + "model `{}` does not provide a {:?} function", self.artifact.model_name, role )) })?; - kernel(time, states, params, covariates, routes, derived, out); + function(time, states, params, covariates, routes, derived, out); Ok(()) } } @@ -252,22 +253,22 @@ impl RuntimeArtifact for NativeExecutionArtifact { } } - fn has_kernel(&self, role: KernelRole) -> bool { + fn has_function(&self, role: ModelFunctionKind) -> bool { match role { - KernelRole::Derive => self.derive.is_some(), - KernelRole::Dynamics => self.dynamics.is_some(), - KernelRole::Outputs => true, - KernelRole::Init => self.init.is_some(), - KernelRole::Drift => self.drift.is_some(), - KernelRole::Diffusion => self.diffusion.is_some(), - KernelRole::RouteLag => self.route_lag.is_some(), - KernelRole::RouteBioavailability => self.route_bioavailability.is_some(), - KernelRole::Analytical => false, + ModelFunctionKind::Derive => self.derive.is_some(), + ModelFunctionKind::Dynamics => self.dynamics.is_some(), + ModelFunctionKind::Outputs => true, + ModelFunctionKind::Init => self.init.is_some(), + ModelFunctionKind::Drift => self.drift.is_some(), + ModelFunctionKind::Diffusion => self.diffusion.is_some(), + ModelFunctionKind::RouteLag => self.route_lag.is_some(), + ModelFunctionKind::RouteBioavailability => self.route_bioavailability.is_some(), + ModelFunctionKind::Analytical => false, } } - fn start_session(&self) -> Result, PharmsolError> { - Ok(Box::new(NativeKernelSession { artifact: self })) + fn start_session(&self) -> Result, PharmsolError> { + Ok(Box::new(NativeFunctionSession { artifact: self })) } } @@ -450,8 +451,8 @@ fn runtime_model_metadata(info: &NativeModelInfo) -> Result Option { - self.info - .routes - .iter() - .find(|route| route.name == name) - .map(|route| route.index) - } - - fn output_index(&self, name: &str) -> Option { - self.info - .outputs - .iter() - .find(|output| output.name == name) - .map(|output| output.index) - } - fn metadata_route_index_for_label(&self, label: &str) -> Option { - self.route_index(label).or_else(|| { - canonical_numeric_alias(label, NUMERIC_ROUTE_PREFIX) - .and_then(|alias| self.route_index(alias.as_str())) - }) + self.metadata() + .route_for_label(label) + .map(ValidatedRoute::input_index) } fn metadata_output_index_for_label(&self, label: &str) -> Option { - self.output_index(label).or_else(|| { - canonical_numeric_alias(label, NUMERIC_OUTPUT_PREFIX) - .and_then(|alias| self.output_index(alias.as_str())) - }) + self.metadata().output_for_label(label) } fn validate_support_point(&self, support_point: &[f64]) -> Result<(), PharmsolError> { @@ -796,7 +777,7 @@ impl SharedNativeModel { #[allow(clippy::too_many_arguments)] fn refresh_derived( &self, - session: &mut dyn KernelSession, + session: &mut dyn FunctionSession, time: f64, state: &[f64], support_point: &[f64], @@ -806,10 +787,10 @@ impl SharedNativeModel { cov_buf: &mut [f64], ) -> Result<(), PharmsolError> { self.fill_cov_buffer(covariates, time, cov_buf); - if self.artifact.has_kernel(KernelRole::Derive) { + if self.artifact.has_function(ModelFunctionKind::Derive) { unsafe { session.invoke_raw( - KernelRole::Derive, + ModelFunctionKind::Derive, time, state.as_ptr(), support_point.as_ptr(), @@ -828,7 +809,7 @@ impl SharedNativeModel { #[allow(clippy::too_many_arguments)] fn write_outputs( &self, - session: &mut dyn KernelSession, + session: &mut dyn FunctionSession, time: f64, state: &[f64], support_point: &[f64], @@ -839,7 +820,7 @@ impl SharedNativeModel { ) -> Result<(), PharmsolError> { unsafe { session.invoke_raw( - KernelRole::Outputs, + ModelFunctionKind::Outputs, time, state.as_ptr(), support_point.as_ptr(), @@ -854,7 +835,7 @@ impl SharedNativeModel { fn initial_state( &self, - session: &mut dyn KernelSession, + session: &mut dyn FunctionSession, support_point: &[f64], covariates: &Covariates, occasion_index: usize, @@ -874,10 +855,10 @@ impl SharedNativeModel { &mut derived, &mut cov_buf, )?; - if self.artifact.has_kernel(KernelRole::Init) { + if self.artifact.has_function(ModelFunctionKind::Init) { unsafe { session.invoke_raw( - KernelRole::Init, + ModelFunctionKind::Init, 0.0, state.as_ptr(), support_point.as_ptr(), @@ -894,13 +875,15 @@ impl SharedNativeModel { fn apply_route_properties( &self, - session: &mut dyn KernelSession, + session: &mut dyn FunctionSession, events: &mut [Event], covariates: &Covariates, support_point: &[f64], ) -> Result<(), PharmsolError> { - if !self.artifact.has_kernel(KernelRole::RouteLag) - && !self.artifact.has_kernel(KernelRole::RouteBioavailability) + if !self.artifact.has_function(ModelFunctionKind::RouteLag) + && !self + .artifact + .has_function(ModelFunctionKind::RouteBioavailability) { return Ok(()); } @@ -922,7 +905,7 @@ impl SharedNativeModel { })?; self.validate_input_for_kind(input, RouteKind::Bolus)?; - if self.artifact.has_kernel(KernelRole::RouteLag) { + if self.artifact.has_function(ModelFunctionKind::RouteLag) { lag_values.fill(0.0); self.refresh_derived( session, @@ -936,7 +919,7 @@ impl SharedNativeModel { )?; unsafe { session.invoke_raw( - KernelRole::RouteLag, + ModelFunctionKind::RouteLag, bolus.time(), zero_state.as_ptr(), support_point.as_ptr(), @@ -952,7 +935,10 @@ impl SharedNativeModel { } } - if self.artifact.has_kernel(KernelRole::RouteBioavailability) { + if self + .artifact + .has_function(ModelFunctionKind::RouteBioavailability) + { fa_values.fill(1.0); self.refresh_derived( session, @@ -966,7 +952,7 @@ impl SharedNativeModel { )?; unsafe { session.invoke_raw( - KernelRole::RouteBioavailability, + ModelFunctionKind::RouteBioavailability, bolus.time(), zero_state.as_ptr(), support_point.as_ptr(), @@ -1007,7 +993,7 @@ impl SharedNativeModel { fn observation_prediction( &self, - session: &mut dyn KernelSession, + session: &mut dyn FunctionSession, observation: &Observation, state: &[f64], support_point: &[f64], @@ -1170,16 +1156,16 @@ impl NativeOdeModel { let cov_buf = RefCell::new(vec![0.0; self.shared.info.covariates.len()]); let derived_buf = RefCell::new(vec![0.0; self.shared.info.derived_len]); let shared = Arc::clone(&self.shared); - if !shared.artifact.has_kernel(KernelRole::Dynamics) { + if !shared.artifact.has_function(ModelFunctionKind::Dynamics) { return Err(PharmsolError::OtherError(format!( - "model `{}` does not have a dynamics kernel", + "model `{}` does not have a dynamics function", shared.info.name ))); } - let kernel_error = RefCell::new(None::); + let function_error = RefCell::new(None::); let diffeq_session = &session; - let diffeq_error = &kernel_error; + let diffeq_error = &function_error; let diffeq = move |x: &V, p: &V, t: f64, @@ -1212,7 +1198,7 @@ impl NativeOdeModel { if let Err(error) = unsafe { session.invoke_raw( - KernelRole::Dynamics, + ModelFunctionKind::Dynamics, t, x.as_slice().as_ptr(), p.as_slice().as_ptr(), @@ -1269,7 +1255,7 @@ impl NativeOdeModel { infusions.as_slice(), &mut output, &session, - &kernel_error, + &function_error, )?; }}; } @@ -1285,7 +1271,7 @@ impl NativeOdeModel { } } - if let Some(error) = kernel_error.into_inner() { + if let Some(error) = function_error.into_inner() { return Err(error); } } @@ -1302,8 +1288,8 @@ impl NativeOdeModel { covariates: &Covariates, infusions: &[Infusion], output: &mut SubjectPredictions, - session: &RefCell>, - kernel_error: &RefCell>, + session: &RefCell>, + function_error: &RefCell>, ) -> Result<(), PharmsolError> where F: Fn(&V, &V, f64, &mut V, &V, &V, &Covariates) + 'a, @@ -1326,8 +1312,8 @@ impl NativeOdeModel { } Event::Infusion(_) => {} Event::Observation(observation) => { - if kernel_error.borrow().is_some() { - return Err(kernel_error.borrow_mut().take().unwrap()); + if function_error.borrow().is_some() { + return Err(function_error.borrow_mut().take().unwrap()); } let prediction = self.shared.observation_prediction( &mut **session.borrow_mut(), @@ -1349,8 +1335,8 @@ impl NativeOdeModel { match solver.set_stop_time(next_event.time()) { Ok(_) => loop { match solver.step() { - Ok(_) if kernel_error.borrow().is_some() => { - return Err(kernel_error.borrow_mut().take().unwrap()); + Ok(_) if function_error.borrow().is_some() => { + return Err(function_error.borrow_mut().take().unwrap()); } Ok(OdeSolverStopReason::InternalTimestep) => continue, Ok(OdeSolverStopReason::TstopReached) => break, @@ -1741,7 +1727,7 @@ impl NativeAnalyticalModel { #[allow(clippy::too_many_arguments)] fn solve_interval( &self, - session: &mut dyn KernelSession, + session: &mut dyn FunctionSession, state: &mut [f64], support_point: &[f64], covariates: &Covariates, @@ -1790,7 +1776,7 @@ impl NativeAnalyticalModel { let next_state = apply_analytical_kernel( self.shared.info.analytical.ok_or_else(|| { PharmsolError::OtherError(format!( - "model `{}` does not declare an analytical kernel", + "model `{}` does not declare an analytical function", self.shared.info.name )) })?, @@ -2174,15 +2160,15 @@ impl NativeSdeModel { let support = Arc::new(support_point.to_vec()); let infusion_events = Arc::new(infusions.to_vec()); let covariates = covariates.clone(); - if !shared.artifact.has_kernel(KernelRole::Drift) { + if !shared.artifact.has_function(ModelFunctionKind::Drift) { return Err(PharmsolError::OtherError(format!( - "model `{}` does not have a drift kernel", + "model `{}` does not have a drift function", shared.info.name ))); } - if !shared.artifact.has_kernel(KernelRole::Diffusion) { + if !shared.artifact.has_function(ModelFunctionKind::Diffusion) { return Err(PharmsolError::OtherError(format!( - "model `{}` does not have a diffusion kernel", + "model `{}` does not have a diffusion function", shared.info.name ))); } @@ -2204,11 +2190,11 @@ impl NativeSdeModel { let drift_state = particle.clone(); let artifact = Arc::clone(&shared.artifact); let session = RefCell::new(artifact.start_session()?); - let kernel_error = RefCell::new(None::); + let function_error = RefCell::new(None::); let drift_session = &session; let diffusion_session = &session; - let drift_error = &kernel_error; - let diffusion_error = &kernel_error; + let drift_error = &function_error; + let diffusion_error = &function_error; let next = simulate_sde_event_with( move |time, state, out| { if drift_error.borrow().is_some() { @@ -2235,7 +2221,7 @@ impl NativeSdeModel { } if let Err(error) = unsafe { session.invoke_raw( - KernelRole::Drift, + ModelFunctionKind::Drift, time, state.as_ptr(), support.as_ptr(), @@ -2277,7 +2263,7 @@ impl NativeSdeModel { } if let Err(error) = unsafe { session.invoke_raw( - KernelRole::Diffusion, + ModelFunctionKind::Diffusion, time, state.as_ptr(), support_for_diffusion.as_ptr(), @@ -2295,7 +2281,7 @@ impl NativeSdeModel { start_time, end_time, ); - if let Some(error) = kernel_error.into_inner() { + if let Some(error) = function_error.into_inner() { return Err(error); } *particle = next; @@ -2572,19 +2558,12 @@ fn sort_events(events: &mut [Event]) { }); } -fn canonical_numeric_alias(label: &str, prefix: &str) -> Option { - if label.is_empty() || !label.chars().all(|ch| ch.is_ascii_digit()) { - return None; - } - Some(format!("{prefix}{label}")) -} - fn build_analytical_parameter_projection( info: &NativeModelInfo, ) -> Result { - let kernel = info.analytical.ok_or_else(|| { + let function = info.analytical.ok_or_else(|| { PharmsolError::OtherError(format!( - "model `{}` does not declare an analytical kernel", + "model `{}` does not declare an analytical function", info.name )) })?; @@ -2598,7 +2577,7 @@ fn build_analytical_parameter_projection( } AnalyticalStructureInputPlan::for_kernel( - kernel, + function, info.parameters.iter().map(String::as_str), info.derived.iter().map(String::as_str), ) @@ -2649,7 +2628,7 @@ fn project_analytical_parameters( } fn apply_analytical_kernel( - kernel: AnalyticalKernel, + function: AnalyticalKernel, state: &[f64], params: &V, dt: f64, @@ -2658,7 +2637,7 @@ fn apply_analytical_kernel( ) -> V { let state = V::from_vec(state.to_vec(), NalgebraContext::new()); let route_inputs = V::from_vec(route_inputs.to_vec(), NalgebraContext::new()); - match kernel { + match function { AnalyticalKernel::OneCompartment => { crate::simulator::equation::analytical::one_compartment( &state, @@ -2773,11 +2752,10 @@ fn apply_analytical_kernel( #[cfg(test)] mod tests { use super::{ - build_analytical_parameter_projection, canonical_numeric_alias, - project_analytical_parameters, KernelSession, NativeAnalyticalModel, NativeCovariateInfo, - NativeModelInfo, NativeOdeModel, NativeOutputInfo, NativeRouteInfo, NativeSdeModel, - NativeStateInfo, RuntimeArtifact, RuntimeBackend, SharedNativeModel, NUMERIC_OUTPUT_PREFIX, - NUMERIC_ROUTE_PREFIX, + build_analytical_parameter_projection, project_analytical_parameters, FunctionSession, + NativeAnalyticalModel, NativeCovariateInfo, NativeModelInfo, NativeOdeModel, + NativeOutputInfo, NativeRouteInfo, NativeSdeModel, NativeStateInfo, RuntimeArtifact, + RuntimeBackend, SharedNativeModel, }; #[cfg(any( feature = "dsl-jit", @@ -2807,7 +2785,7 @@ mod tests { Parameters, Subject, }; use diffsol::VectorHost; - use pharmsol_dsl::execution::KernelRole; + use pharmsol_dsl::execution::ModelFunctionKind; use pharmsol_dsl::{ AnalyticalKernel, AnalyticalStructureInputKind, CovariateInterpolation, ModelKind, RouteKind, @@ -2830,11 +2808,11 @@ mod tests { panic!("dummy artifact backend should not be used in tests") } - fn has_kernel(&self, _role: KernelRole) -> bool { + fn has_function(&self, _role: ModelFunctionKind) -> bool { false } - fn start_session(&self) -> Result, PharmsolError> { + fn start_session(&self) -> Result, PharmsolError> { panic!("dummy artifact sessions should not be used in tests") } } @@ -2881,7 +2859,7 @@ mod tests { fn analytical_model_info( parameters: &[&str], derived: &[&str], - kernel: AnalyticalKernel, + function: AnalyticalKernel, ) -> NativeModelInfo { NativeModelInfo { name: "analytical_projection".to_string(), @@ -2889,7 +2867,7 @@ mod tests { parameters: parameters.iter().map(|name| (*name).to_string()).collect(), derived: derived.iter().map(|name| (*name).to_string()).collect(), covariates: Vec::new(), - states: (0..kernel.state_count()) + states: (0..function.state_count()) .map(|offset| NativeStateInfo { name: format!("state_{offset}"), offset, @@ -2897,11 +2875,11 @@ mod tests { .collect(), routes: Vec::new(), outputs: Vec::new(), - state_len: kernel.state_count(), + state_len: function.state_count(), derived_len: derived.len(), output_len: 0, route_len: 0, - analytical: Some(kernel), + analytical: Some(function), particles: None, } } @@ -3160,31 +3138,6 @@ mod tests { .build() } - #[test] - fn canonical_numeric_alias_maps_bare_numeric_labels_to_contextual_prefixes() { - assert_eq!( - canonical_numeric_alias("1", NUMERIC_ROUTE_PREFIX), - Some("input_1".to_string()) - ); - assert_eq!( - canonical_numeric_alias("10", NUMERIC_OUTPUT_PREFIX), - Some("outeq_10".to_string()) - ); - } - - #[test] - fn canonical_numeric_alias_ignores_symbolic_and_prefixed_labels() { - assert_eq!(canonical_numeric_alias("iv", NUMERIC_ROUTE_PREFIX), None); - assert_eq!( - canonical_numeric_alias("input_1", NUMERIC_ROUTE_PREFIX), - None - ); - assert_eq!( - canonical_numeric_alias("outeq_2", NUMERIC_OUTPUT_PREFIX), - None - ); - } - #[test] fn validate_input_for_kind_reports_structured_route_kind_error() { let model = bolus_only_shared_model(); diff --git a/src/dsl/runtime.rs b/src/dsl/runtime.rs index 976288ed..c09d794f 100644 --- a/src/dsl/runtime.rs +++ b/src/dsl/runtime.rs @@ -109,8 +109,8 @@ use crate::{ Parameters, PharmsolError, Subject, ValidatedModelMetadata, }; use pharmsol_dsl::{ - analyze_module, lower_typed_model, parse_module, Diagnostic, DiagnosticReport, ExecutionModel, - LoweringError, ModelKind, ParseError, SemanticError, + analyze_module, compile_analyzed_model, parse_module, AnalysisError, CompileError, Diagnostic, + DiagnosticReport, ExecutionModel, ModelKind, ParseError, }; pub type RuntimeModelInfo = NativeModelInfo; @@ -267,9 +267,9 @@ pub enum RuntimeError { #[error("failed to parse DSL source: {0}")] Parse(#[source] ParseError), #[error("failed to analyze DSL source: {0}")] - Semantic(#[source] SemanticError), + Semantic(#[source] AnalysisError), #[error("failed to lower DSL model: {0}")] - Lowering(#[source] LoweringError), + Lowering(#[source] CompileError), #[error("{0}")] ModelSelection(String), #[cfg(feature = "dsl-jit")] @@ -339,18 +339,18 @@ pub fn compile_module_source_to_runtime( ) -> Result { let parsed = parse_module(source).map_err(|error| RuntimeError::Parse(error.with_source(source)))?; - let typed = analyze_module(&parsed) + let analyzed = analyze_module(&parsed) .map_err(|error| RuntimeError::Semantic(error.with_source(source)))?; let model = match model_name { - Some(name) => typed + Some(name) => analyzed .models .iter() .find(|model| model.name == name) .ok_or_else(|| { RuntimeError::ModelSelection(format!("model `{name}` not found in module")) })?, - None if typed.models.len() == 1 => &typed.models[0], + None if analyzed.models.len() == 1 => &analyzed.models[0], None => { return Err(RuntimeError::ModelSelection( "module contains multiple models; pass an explicit model name".to_string(), @@ -358,7 +358,7 @@ pub fn compile_module_source_to_runtime( } }; - let execution = lower_typed_model(model) + let execution = compile_analyzed_model(model) .map_err(|error| RuntimeError::Lowering(error.with_source(source)))?; compile_execution_model_to_runtime(&execution, target, event_callback).map_err(|error| { #[cfg(feature = "dsl-jit")] @@ -369,7 +369,7 @@ pub fn compile_module_source_to_runtime( }) } -/// Compile a lowered execution model to a selected runtime backend. +/// Compile a compiled execution model to a selected runtime backend. /// /// Use this when you already own the frontend pipeline and only need the final /// backend step. @@ -445,7 +445,7 @@ pub fn compile_module_source_to_runtime_wasm( } #[cfg(feature = "dsl-wasm")] -/// Compile a lowered execution model straight to a host-side runtime model via +/// Compile a compiled execution model straight to a host-side runtime model via /// the WASM path. pub fn compile_execution_model_to_runtime_wasm( model: &ExecutionModel, @@ -596,13 +596,13 @@ out(cp) = central / v ~ continuous() fn corpus_model(name: &str) -> ExecutionModel { let parsed = pharmsol_dsl::parse_module(corpus_source()).expect("parse corpus module"); - let typed = pharmsol_dsl::analyze_module(&parsed).expect("analyze corpus module"); - let model = typed + let analyzed = pharmsol_dsl::analyze_module(&parsed).expect("analyze corpus module"); + let model = analyzed .models .iter() .find(|model| model.name == name) .expect("model present in corpus module"); - pharmsol_dsl::lower_typed_model(model).expect("lower corpus model") + pharmsol_dsl::compile_analyzed_model(model).expect("lower corpus model") } fn ode_subject() -> Subject { diff --git a/src/dsl/rust_backend.rs b/src/dsl/rust_backend.rs index 850e13b7..bc6ddc04 100644 --- a/src/dsl/rust_backend.rs +++ b/src/dsl/rust_backend.rs @@ -1,15 +1,15 @@ use std::fmt::Write; use super::compiled_backend_abi::{ - compiled_kernel_symbol, encode_compiled_model_info, API_VERSION_SYMBOL, + compiled_function_symbol, encode_compiled_model_info, API_VERSION_SYMBOL, MODEL_INFO_JSON_LEN_SYMBOL, MODEL_INFO_JSON_PTR_SYMBOL, }; use pharmsol_dsl::execution::{ ExecutionBlock, ExecutionCall, ExecutionExpr, ExecutionExprKind, ExecutionLoad, ExecutionModel, ExecutionProgram, ExecutionStateRef, ExecutionStmt, ExecutionStmtKind, ExecutionTargetKind, - KernelImplementation, + FunctionBody, }; -use pharmsol_dsl::{MathIntrinsic, TypedBinaryOp, TypedUnaryOp, ValueType}; +use pharmsol_dsl::{AnalyzedBinaryOp, AnalyzedUnaryOp, MathFunction, ValueType}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RustBackendFlavor { @@ -76,10 +76,10 @@ pub fn emit_rust_backend_source( .unwrap(); writeln!(source).unwrap(); - for kernel in &model.kernels { - if let Some(symbol) = compiled_kernel_symbol(kernel.role) { - if let KernelImplementation::Statements(program) = &kernel.implementation { - emit_statement_kernel(&mut source, program, symbol)?; + for function in &model.functions { + if let Some(symbol) = compiled_function_symbol(function.kind) { + if let FunctionBody::Statements(program) = &function.body { + emit_statement_function(&mut source, program, symbol)?; writeln!(source).unwrap(); } } @@ -87,7 +87,7 @@ pub fn emit_rust_backend_source( Ok(source) } -fn emit_statement_kernel( +fn emit_statement_function( source: &mut String, program: &ExecutionProgram, symbol: &'static str, @@ -231,8 +231,8 @@ fn emit_expr(expr: &ExecutionExpr) -> Result { ExecutionExprKind::Unary { op, expr: inner } => { let inner = emit_expr(inner)?; match op { - TypedUnaryOp::Plus => cast_expr(inner.rendered, inner.ty, expr.ty), - TypedUnaryOp::Minus => match expr.ty { + AnalyzedUnaryOp::Plus => cast_expr(inner.rendered, inner.ty, expr.ty), + AnalyzedUnaryOp::Minus => match expr.ty { ValueType::Real | ValueType::Int => { format!("-({})", cast_expr(inner.rendered, inner.ty, expr.ty)) } @@ -240,7 +240,7 @@ fn emit_expr(expr: &ExecutionExpr) -> Result { return Err("cannot emit unary minus for boolean expressions".to_string()) } }, - TypedUnaryOp::Not => { + AnalyzedUnaryOp::Not => { format!( "!({})", cast_expr(inner.rendered, inner.ty, ValueType::Bool) @@ -274,7 +274,7 @@ fn emit_load(load: &ExecutionLoad, ty: ValueType) -> Result { } fn emit_binary_expr( - op: TypedBinaryOp, + op: AnalyzedBinaryOp, lhs: &ExecutionExpr, rhs: &ExecutionExpr, result_ty: ValueType, @@ -282,17 +282,17 @@ fn emit_binary_expr( let lhs = emit_expr(lhs)?; let rhs = emit_expr(rhs)?; Ok(match op { - TypedBinaryOp::Or => format!( + AnalyzedBinaryOp::Or => format!( "({}) || ({})", cast_expr(lhs.rendered, lhs.ty, ValueType::Bool), cast_expr(rhs.rendered, rhs.ty, ValueType::Bool) ), - TypedBinaryOp::And => format!( + AnalyzedBinaryOp::And => format!( "({}) && ({})", cast_expr(lhs.rendered, lhs.ty, ValueType::Bool), cast_expr(rhs.rendered, rhs.ty, ValueType::Bool) ), - TypedBinaryOp::Eq | TypedBinaryOp::NotEq => { + AnalyzedBinaryOp::Eq | AnalyzedBinaryOp::NotEq => { let operand_ty = if lhs.ty == ValueType::Real || rhs.ty == ValueType::Real { ValueType::Real } else if lhs.ty == ValueType::Bool && rhs.ty == ValueType::Bool { @@ -300,24 +300,31 @@ fn emit_binary_expr( } else { ValueType::Int }; - let operator = if op == TypedBinaryOp::Eq { "==" } else { "!=" }; + let operator = if op == AnalyzedBinaryOp::Eq { + "==" + } else { + "!=" + }; format!( "({}) {operator} ({})", cast_expr(lhs.rendered, lhs.ty, operand_ty), cast_expr(rhs.rendered, rhs.ty, operand_ty) ) } - TypedBinaryOp::Lt | TypedBinaryOp::LtEq | TypedBinaryOp::Gt | TypedBinaryOp::GtEq => { + AnalyzedBinaryOp::Lt + | AnalyzedBinaryOp::LtEq + | AnalyzedBinaryOp::Gt + | AnalyzedBinaryOp::GtEq => { let operand_ty = if lhs.ty == ValueType::Real || rhs.ty == ValueType::Real { ValueType::Real } else { ValueType::Int }; let operator = match op { - TypedBinaryOp::Lt => "<", - TypedBinaryOp::LtEq => "<=", - TypedBinaryOp::Gt => ">", - TypedBinaryOp::GtEq => ">=", + AnalyzedBinaryOp::Lt => "<", + AnalyzedBinaryOp::LtEq => "<=", + AnalyzedBinaryOp::Gt => ">", + AnalyzedBinaryOp::GtEq => ">=", _ => unreachable!(), }; format!( @@ -326,11 +333,11 @@ fn emit_binary_expr( cast_expr(rhs.rendered, rhs.ty, operand_ty) ) } - TypedBinaryOp::Add | TypedBinaryOp::Sub | TypedBinaryOp::Mul => { + AnalyzedBinaryOp::Add | AnalyzedBinaryOp::Sub | AnalyzedBinaryOp::Mul => { let operator = match op { - TypedBinaryOp::Add => "+", - TypedBinaryOp::Sub => "-", - TypedBinaryOp::Mul => "*", + AnalyzedBinaryOp::Add => "+", + AnalyzedBinaryOp::Sub => "-", + AnalyzedBinaryOp::Mul => "*", _ => unreachable!(), }; format!( @@ -339,12 +346,12 @@ fn emit_binary_expr( cast_expr(rhs.rendered, rhs.ty, result_ty) ) } - TypedBinaryOp::Div => format!( + AnalyzedBinaryOp::Div => format!( "({}) / ({})", cast_expr(lhs.rendered, lhs.ty, ValueType::Real), cast_expr(rhs.rendered, rhs.ty, ValueType::Real) ), - TypedBinaryOp::Pow => { + AnalyzedBinaryOp::Pow => { let lhs = cast_expr(lhs.rendered, lhs.ty, ValueType::Real); let rhs = cast_expr(rhs.rendered, rhs.ty, ValueType::Real); cast_expr(format!("({lhs}).powf({rhs})"), ValueType::Real, result_ty) @@ -363,13 +370,13 @@ fn emit_call_expr( } fn emit_math_call( - intrinsic: MathIntrinsic, + intrinsic: MathFunction, args: &[ExecutionExpr], result_ty: ValueType, ) -> Result { let args = args.iter().map(emit_expr).collect::, _>>()?; Ok(match intrinsic { - MathIntrinsic::Max | MathIntrinsic::Min => { + MathFunction::Max | MathFunction::Min => { if args.len() != 2 { return Err(format!("{intrinsic:?} expects 2 arguments")); } @@ -377,7 +384,7 @@ fn emit_math_call( ValueType::Real => { let lhs = cast_expr(args[0].rendered.clone(), args[0].ty, ValueType::Real); let rhs = cast_expr(args[1].rendered.clone(), args[1].ty, ValueType::Real); - let method = if intrinsic == MathIntrinsic::Max { + let method = if intrinsic == MathFunction::Max { "max" } else { "min" @@ -387,7 +394,7 @@ fn emit_math_call( ValueType::Int => { let lhs = cast_expr(args[0].rendered.clone(), args[0].ty, ValueType::Int); let rhs = cast_expr(args[1].rendered.clone(), args[1].ty, ValueType::Int); - let function = if intrinsic == MathIntrinsic::Max { + let function = if intrinsic == MathFunction::Max { "std::cmp::max" } else { "std::cmp::min" @@ -399,20 +406,20 @@ fn emit_math_call( } } } - MathIntrinsic::Abs if result_ty == ValueType::Int => { + MathFunction::Abs if result_ty == ValueType::Int => { let value = cast_expr(args[0].rendered.clone(), args[0].ty, ValueType::Int); format!("({value}).abs()") } _ => { let function = match intrinsic { - MathIntrinsic::Abs => "abs", - MathIntrinsic::Ceil => "ceil", - MathIntrinsic::Exp => "exp", - MathIntrinsic::Floor => "floor", - MathIntrinsic::Ln | MathIntrinsic::Log => "ln", - MathIntrinsic::Log10 => "log10", - MathIntrinsic::Log2 => "log2", - MathIntrinsic::Pow => { + MathFunction::Abs => "abs", + MathFunction::Ceil => "ceil", + MathFunction::Exp => "exp", + MathFunction::Floor => "floor", + MathFunction::Ln | MathFunction::Log => "ln", + MathFunction::Log10 => "log10", + MathFunction::Log2 => "log2", + MathFunction::Pow => { if args.len() != 2 { return Err("pow expects 2 arguments".to_string()); } @@ -424,12 +431,12 @@ fn emit_math_call( result_ty, )); } - MathIntrinsic::Round => "round", - MathIntrinsic::Sin => "sin", - MathIntrinsic::Cos => "cos", - MathIntrinsic::Tan => "tan", - MathIntrinsic::Sqrt => "sqrt", - MathIntrinsic::Max | MathIntrinsic::Min => unreachable!(), + MathFunction::Round => "round", + MathFunction::Sin => "sin", + MathFunction::Cos => "cos", + MathFunction::Tan => "tan", + MathFunction::Sqrt => "sqrt", + MathFunction::Max | MathFunction::Min => unreachable!(), }; let value = cast_expr(args[0].rendered.clone(), args[0].ty, ValueType::Real); cast_expr( diff --git a/src/dsl/wasm.rs b/src/dsl/wasm.rs index a09b0247..79d631f3 100644 --- a/src/dsl/wasm.rs +++ b/src/dsl/wasm.rs @@ -7,21 +7,21 @@ use crate::dsl::model_info; use wasmtime::{Engine, Instance, Linker, Memory, Module, Store, TypedFunc}; use super::compiled_backend_abi::{ - decode_compiled_model_info, CompiledKernelAvailability, ALLOC_F64_BUFFER_SYMBOL, + decode_compiled_model_info, CompiledFunctionAvailability, ALLOC_F64_BUFFER_SYMBOL, API_VERSION_SYMBOL, DERIVE_SYMBOL, DIFFUSION_SYMBOL, DRIFT_SYMBOL, DYNAMICS_SYMBOL, FREE_F64_BUFFER_SYMBOL, INIT_SYMBOL, MODEL_INFO_JSON_LEN_SYMBOL, MODEL_INFO_JSON_PTR_SYMBOL, OUTPUTS_SYMBOL, ROUTE_BIOAVAILABILITY_SYMBOL, ROUTE_LAG_SYMBOL, }; -use super::native::{KernelSession, NativeModelInfo, RuntimeArtifact, RuntimeBackend}; +use super::native::{FunctionSession, NativeModelInfo, RuntimeArtifact, RuntimeBackend}; use super::wasm_compile::{WasmError, WASM_API_VERSION}; use super::wasm_direct_emitter::{ DIRECT_WASM_BINARY_MATH_IMPORTS, DIRECT_WASM_IMPORT_MODULE, DIRECT_WASM_UNARY_MATH_IMPORTS, }; use crate::PharmsolError; -use pharmsol_dsl::execution::KernelRole; +use pharmsol_dsl::execution::ModelFunctionKind; #[derive(Clone, Copy, Debug, Default)] -struct WasmKernelAvailability { +struct WasmFunctionAvailability { derive: bool, dynamics: bool, outputs: bool, @@ -32,23 +32,23 @@ struct WasmKernelAvailability { route_bioavailability: bool, } -impl WasmKernelAvailability { - fn has(self, role: KernelRole) -> bool { +impl WasmFunctionAvailability { + fn has(self, role: ModelFunctionKind) -> bool { match role { - KernelRole::Derive => self.derive, - KernelRole::Dynamics => self.dynamics, - KernelRole::Outputs => self.outputs, - KernelRole::Init => self.init, - KernelRole::Drift => self.drift, - KernelRole::Diffusion => self.diffusion, - KernelRole::RouteLag => self.route_lag, - KernelRole::RouteBioavailability => self.route_bioavailability, - KernelRole::Analytical => false, + ModelFunctionKind::Derive => self.derive, + ModelFunctionKind::Dynamics => self.dynamics, + ModelFunctionKind::Outputs => self.outputs, + ModelFunctionKind::Init => self.init, + ModelFunctionKind::Drift => self.drift, + ModelFunctionKind::Diffusion => self.diffusion, + ModelFunctionKind::RouteLag => self.route_lag, + ModelFunctionKind::RouteBioavailability => self.route_bioavailability, + ModelFunctionKind::Analytical => false, } } - fn compiled(self) -> CompiledKernelAvailability { - CompiledKernelAvailability { + fn compiled(self) -> CompiledFunctionAvailability { + CompiledFunctionAvailability { derive: self.derive, dynamics: self.dynamics, outputs: self.outputs, @@ -65,8 +65,8 @@ pub(crate) struct WasmExecutionArtifact { info: NativeModelInfo, engine: Engine, module: Module, - kernels: WasmKernelAvailability, - session_pool: Mutex>, + functions: WasmFunctionAvailability, + session_pool: Mutex>, } impl std::fmt::Debug for WasmExecutionArtifact { @@ -74,7 +74,7 @@ impl std::fmt::Debug for WasmExecutionArtifact { f.debug_struct("WasmExecutionArtifact") .field("model", &self.info.name) .field("kind", &self.info.kind) - .field("kernels", &self.kernels) + .field("functions", &self.functions) .finish() } } @@ -85,23 +85,23 @@ struct WasmBuffer { len: usize, } -type WasmKernelParams = (f64, i32, i32, i32, i32, i32, i32); -type WasmKernelFunc = TypedFunc; -type WasmSessionPool = Mutex>; +type WasmFunctionParams = (f64, i32, i32, i32, i32, i32, i32); +type WasmModelFunction = TypedFunc; +type WasmSessionPool = Mutex>; -struct WasmKernelSession { +struct WasmFunctionSession { info: NativeModelInfo, store: Store<()>, memory: Memory, free: TypedFunc<(i32, i32), ()>, - derive: Option, - dynamics: Option, - outputs: WasmKernelFunc, - init: Option, - drift: Option, - diffusion: Option, - route_lag: Option, - route_bioavailability: Option, + derive: Option, + dynamics: Option, + outputs: WasmModelFunction, + init: Option, + drift: Option, + diffusion: Option, + route_lag: Option, + route_bioavailability: Option, states: WasmBuffer, params: WasmBuffer, covariates: WasmBuffer, @@ -110,17 +110,17 @@ struct WasmKernelSession { out: WasmBuffer, } -struct PooledWasmKernelSession<'a> { - session: Option, +struct PooledWasmFunctionSession<'a> { + session: Option, pool: &'a WasmSessionPool, } -impl WasmKernelSession { +impl WasmFunctionSession { fn new( info: &NativeModelInfo, engine: &Engine, module: &Module, - kernels: WasmKernelAvailability, + functions: WasmFunctionAvailability, ) -> Result { let mut store = Store::new(engine, ()); let linker = configured_wasm_linker(engine)?; @@ -147,38 +147,38 @@ impl WasmKernelSession { .max(info.route_len), )?; - let derive = if kernels.derive { + let derive = if functions.derive { optional_typed_func(&instance, &mut store, DERIVE_SYMBOL)? } else { None }; - let dynamics = if kernels.dynamics { + let dynamics = if functions.dynamics { optional_typed_func(&instance, &mut store, DYNAMICS_SYMBOL)? } else { None }; let outputs = typed_func(&instance, &mut store, OUTPUTS_SYMBOL)?; - let init = if kernels.init { + let init = if functions.init { optional_typed_func(&instance, &mut store, INIT_SYMBOL)? } else { None }; - let drift = if kernels.drift { + let drift = if functions.drift { optional_typed_func(&instance, &mut store, DRIFT_SYMBOL)? } else { None }; - let diffusion = if kernels.diffusion { + let diffusion = if functions.diffusion { optional_typed_func(&instance, &mut store, DIFFUSION_SYMBOL)? } else { None }; - let route_lag = if kernels.route_lag { + let route_lag = if functions.route_lag { optional_typed_func(&instance, &mut store, ROUTE_LAG_SYMBOL)? } else { None }; - let route_bioavailability = if kernels.route_bioavailability { + let route_bioavailability = if functions.route_bioavailability { optional_typed_func(&instance, &mut store, ROUTE_BIOAVAILABILITY_SYMBOL)? } else { None @@ -206,28 +206,28 @@ impl WasmKernelSession { }) } - fn kernel(&self, role: KernelRole) -> Result { + fn function(&self, role: ModelFunctionKind) -> Result { match role { - KernelRole::Derive => self.derive.clone(), - KernelRole::Dynamics => self.dynamics.clone(), - KernelRole::Outputs => Some(self.outputs.clone()), - KernelRole::Init => self.init.clone(), - KernelRole::Drift => self.drift.clone(), - KernelRole::Diffusion => self.diffusion.clone(), - KernelRole::RouteLag => self.route_lag.clone(), - KernelRole::RouteBioavailability => self.route_bioavailability.clone(), - KernelRole::Analytical => None, + ModelFunctionKind::Derive => self.derive.clone(), + ModelFunctionKind::Dynamics => self.dynamics.clone(), + ModelFunctionKind::Outputs => Some(self.outputs.clone()), + ModelFunctionKind::Init => self.init.clone(), + ModelFunctionKind::Drift => self.drift.clone(), + ModelFunctionKind::Diffusion => self.diffusion.clone(), + ModelFunctionKind::RouteLag => self.route_lag.clone(), + ModelFunctionKind::RouteBioavailability => self.route_bioavailability.clone(), + ModelFunctionKind::Analytical => None, } .ok_or_else(|| { PharmsolError::OtherError(format!( - "model `{}` does not provide a {:?} kernel", + "model `{}` does not provide a {:?} function", self.info.name, role )) }) } } -impl Drop for WasmKernelSession { +impl Drop for WasmFunctionSession { fn drop(&mut self) { for buffer in [ &self.states, @@ -246,10 +246,10 @@ impl Drop for WasmKernelSession { } } -impl KernelSession for WasmKernelSession { +impl FunctionSession for WasmFunctionSession { unsafe fn invoke_raw( &mut self, - role: KernelRole, + role: ModelFunctionKind, time: f64, states: *const f64, params: *const f64, @@ -307,12 +307,12 @@ impl KernelSession for WasmKernelSession { } else { self.out.ptr }; - let out_len = kernel_output_len(&self.info, role); + let out_len = function_output_len(&self.info, role); if out_ptr == self.out.ptr { zero_f64s(&self.memory, &mut self.store, out_ptr, out_len).map_err(map_memory_error)?; } - self.kernel(role)? + self.function(role)? .call( &mut self.store, ( @@ -327,7 +327,7 @@ impl KernelSession for WasmKernelSession { ) .map_err(|error| { PharmsolError::OtherError(format!( - "WASM kernel {:?} trap for model `{}`: {error}", + "WASM function {:?} trap for model `{}`: {error}", role, self.info.name )) })?; @@ -343,10 +343,10 @@ impl KernelSession for WasmKernelSession { } } -impl KernelSession for PooledWasmKernelSession<'_> { +impl FunctionSession for PooledWasmFunctionSession<'_> { unsafe fn invoke_raw( &mut self, - role: KernelRole, + role: ModelFunctionKind, time: f64, states: *const f64, params: *const f64, @@ -362,7 +362,7 @@ impl KernelSession for PooledWasmKernelSession<'_> { } } -impl Drop for PooledWasmKernelSession<'_> { +impl Drop for PooledWasmFunctionSession<'_> { fn drop(&mut self) { if let Some(session) = self.session.take() { self.pool @@ -378,11 +378,11 @@ impl RuntimeArtifact for WasmExecutionArtifact { RuntimeBackend::Wasm } - fn has_kernel(&self, role: KernelRole) -> bool { - self.kernels.has(role) + fn has_function(&self, role: ModelFunctionKind) -> bool { + self.functions.has(role) } - fn start_session(&self) -> Result, PharmsolError> { + fn start_session(&self) -> Result, PharmsolError> { let session = self .session_pool .lock() @@ -391,19 +391,21 @@ impl RuntimeArtifact for WasmExecutionArtifact { let session = match session { Some(session) => session, - None => WasmKernelSession::new(&self.info, &self.engine, &self.module, self.kernels) - .map_err(|error| { - PharmsolError::OtherError(format!( - "failed to instantiate WASM runtime for model `{}`: {error}", - self.info.name - )) - })?, + None => { + WasmFunctionSession::new(&self.info, &self.engine, &self.module, self.functions) + .map_err(|error| { + PharmsolError::OtherError(format!( + "failed to instantiate WASM runtime for model `{}`: {error}", + self.info.name + )) + })? + } }; - Ok(Box::new(PooledWasmKernelSession { + Ok(Box::new(PooledWasmFunctionSession { session: Some(session), pool: &self.session_pool, - }) as Box) + }) as Box) } } @@ -484,8 +486,8 @@ fn load_wasm_artifact_from_module( let memory = instance .get_memory(&mut store, "memory") .ok_or(WasmError::MissingExport("memory"))?; - let (info, expected_kernels) = read_model_info_envelope(&instance, &mut store, &memory)?; - let kernels = WasmKernelAvailability { + let (info, expected_functions) = read_model_info_envelope(&instance, &mut store, &memory)?; + let functions = WasmFunctionAvailability { derive: instance.get_func(&mut store, DERIVE_SYMBOL).is_some(), dynamics: instance.get_func(&mut store, DYNAMICS_SYMBOL).is_some(), outputs: instance.get_func(&mut store, OUTPUTS_SYMBOL).is_some(), @@ -497,12 +499,12 @@ fn load_wasm_artifact_from_module( .get_func(&mut store, ROUTE_BIOAVAILABILITY_SYMBOL) .is_some(), }; - let found_kernels = kernels.compiled(); - if found_kernels != expected_kernels { - return Err(WasmError::KernelMetadataMismatch { + let found_functions = functions.compiled(); + if found_functions != expected_functions { + return Err(WasmError::FunctionMetadataMismatch { model: info.name.clone(), - expected: expected_kernels, - found: found_kernels, + expected: expected_functions, + found: found_functions, }); } @@ -512,7 +514,7 @@ fn load_wasm_artifact_from_module( info, engine, module, - kernels, + functions, session_pool: Mutex::new(Vec::new()), }, )) @@ -571,15 +573,16 @@ fn alloc_buffer( Ok(WasmBuffer { ptr, len }) } -fn kernel_output_len(info: &NativeModelInfo, role: KernelRole) -> usize { +fn function_output_len(info: &NativeModelInfo, role: ModelFunctionKind) -> usize { match role { - KernelRole::Derive => info.derived_len, - KernelRole::Dynamics | KernelRole::Init | KernelRole::Drift | KernelRole::Diffusion => { - info.state_len - } - KernelRole::Outputs => info.output_len, - KernelRole::RouteLag | KernelRole::RouteBioavailability => info.route_len, - KernelRole::Analytical => 0, + ModelFunctionKind::Derive => info.derived_len, + ModelFunctionKind::Dynamics + | ModelFunctionKind::Init + | ModelFunctionKind::Drift + | ModelFunctionKind::Diffusion => info.state_len, + ModelFunctionKind::Outputs => info.output_len, + ModelFunctionKind::RouteLag | ModelFunctionKind::RouteBioavailability => info.route_len, + ModelFunctionKind::Analytical => 0, } } @@ -632,7 +635,7 @@ fn read_model_info_envelope( instance: &Instance, store: &mut Store<()>, memory: &Memory, -) -> Result<(NativeModelInfo, CompiledKernelAvailability), WasmError> { +) -> Result<(NativeModelInfo, CompiledFunctionAvailability), WasmError> { let ptr = typed_func::<(), i32>(instance, store, MODEL_INFO_JSON_PTR_SYMBOL)? .call(&mut *store, ()) .map_err(|error| WasmError::Load(error.to_string()))?; @@ -655,7 +658,7 @@ fn read_model_info_envelope( found: envelope.abi_version, }); } - Ok((envelope.model, envelope.kernels)) + Ok((envelope.model, envelope.functions)) } fn write_f64s( @@ -767,13 +770,13 @@ fn byte_range( mod tests { use super::*; use crate::dsl::{ - compile_execution_artifact, CompiledKernelAvailability, CompiledModelInfoEnvelope, + compile_execution_artifact, CompiledFunctionAvailability, CompiledModelInfoEnvelope, NativeModelInfo, NativeOutputInfo, NativeRouteInfo, }; use crate::test_fixtures::STRUCTURED_BLOCK_CORPUS; use approx::assert_relative_eq; use pharmsol_dsl::{ - analyze_module, lower_typed_model, parse_module, ExecutionModel, ModelKind, RouteKind, + analyze_module, compile_analyzed_model, parse_module, ExecutionModel, ModelKind, RouteKind, }; use std::path::{Path, PathBuf}; use tempfile::tempdir; @@ -786,13 +789,13 @@ mod tests { fn load_corpus_model(name: &str) -> ExecutionModel { let source = STRUCTURED_BLOCK_CORPUS; let parsed = parse_module(source).expect("parse corpus module"); - let typed = analyze_module(&parsed).expect("analyze corpus module"); - let model = typed + let analyzed = analyze_module(&parsed).expect("analyze corpus module"); + let model = analyzed .models .iter() .find(|model| model.name == name) .expect("model in corpus module"); - lower_typed_model(model).expect("lower corpus model") + compile_analyzed_model(model).expect("lower corpus model") } fn loader_test_model_info(name: &str) -> NativeModelInfo { @@ -929,9 +932,9 @@ mod tests { let metadata = serde_json::to_vec(&CompiledModelInfoEnvelope { abi_version: WASM_API_VERSION, model: model_info, - kernels: CompiledKernelAvailability { + functions: CompiledFunctionAvailability { outputs: true, - ..CompiledKernelAvailability::default() + ..CompiledFunctionAvailability::default() }, }) .expect("metadata json"); @@ -958,9 +961,9 @@ mod tests { let metadata = serde_json::to_vec(&CompiledModelInfoEnvelope { abi_version: WASM_API_VERSION + 1, model: model_info, - kernels: CompiledKernelAvailability { + functions: CompiledFunctionAvailability { outputs: true, - ..CompiledKernelAvailability::default() + ..CompiledFunctionAvailability::default() }, }) .expect("metadata json"); @@ -979,14 +982,14 @@ mod tests { } #[test] - fn rejects_kernel_metadata_mismatch_from_compiled_envelope() { - let model_info = loader_test_model_info("kernel_metadata_mismatch"); + fn rejects_function_metadata_mismatch_from_compiled_envelope() { + let model_info = loader_test_model_info("function_metadata_mismatch"); let metadata = serde_json::to_vec(&CompiledModelInfoEnvelope { abi_version: WASM_API_VERSION, model: model_info, - kernels: CompiledKernelAvailability { + functions: CompiledFunctionAvailability { outputs: true, - ..CompiledKernelAvailability::default() + ..CompiledFunctionAvailability::default() }, }) .expect("metadata json"); @@ -1000,11 +1003,11 @@ mod tests { assert!(matches!( error, - WasmError::KernelMetadataMismatch { + WasmError::FunctionMetadataMismatch { ref model, expected, found, - } if model == "kernel_metadata_mismatch" + } if model == "function_metadata_mismatch" && expected.outputs && !found.outputs )); @@ -1048,7 +1051,7 @@ mod tests { } #[test] - fn wasm_ode_artifact_exports_browser_bundle_and_matches_jit_kernels() { + fn wasm_ode_artifact_exports_browser_bundle_and_matches_jit_functions() { let model = load_corpus_model("one_cmt_oral_iv"); let work_dir = tempdir().expect("tempdir"); let output_path = work_dir.path().join("one_cmt_oral_iv.wasm"); @@ -1067,7 +1070,7 @@ mod tests { assert!(loader.contains(ROUTE_LAG_SYMBOL)); assert!(loader.contains(ROUTE_BIOAVAILABILITY_SYMBOL)); - let jit = compile_execution_artifact(&model).expect("compile jit kernels"); + let jit = compile_execution_artifact(&model).expect("compile jit functions"); let (mut store, instance, memory) = instantiate_module(&output_path); let api_version = typed_func::<(), u32>(&instance, &mut store, API_VERSION_SYMBOL); @@ -1235,7 +1238,7 @@ mod tests { } #[test] - fn reuses_wasm_kernel_sessions_across_start_session_calls() { + fn reuses_wasm_function_sessions_across_start_session_calls() { let model = load_corpus_model("one_cmt_oral_iv"); let work_dir = tempdir().expect("tempdir"); let output_path = work_dir.path().join("one_cmt_oral_iv_reuse.wasm"); @@ -1256,9 +1259,9 @@ mod tests { } #[test] - fn wasm_runtime_preserves_state_aliasing_for_dynamics_kernel() { + fn wasm_runtime_preserves_state_aliasing_for_dynamics_function() { let model = load_corpus_model("one_cmt_oral_iv"); - let jit = compile_execution_artifact(&model).expect("compile jit kernels"); + let jit = compile_execution_artifact(&model).expect("compile jit functions"); let bytes = super::super::wasm_compile::compile_execution_model_to_wasm_bytes(&model) .expect("emit direct wasm bytes"); let (info, artifact) = load_wasm_artifact_bytes(&bytes).expect("load direct wasm"); @@ -1292,7 +1295,7 @@ mod tests { ); session .invoke_raw( - KernelRole::Dynamics, + ModelFunctionKind::Dynamics, 0.0, actual.as_ptr(), params.as_ptr(), @@ -1301,7 +1304,7 @@ mod tests { derived.as_ptr(), actual.as_mut_ptr(), ) - .expect("invoke aliased dynamics kernel"); + .expect("invoke aliased dynamics function"); } for (actual, expected) in actual.iter().zip(expected.iter()) { @@ -1312,7 +1315,7 @@ mod tests { #[test] fn wasm_runtime_zeroes_non_aliased_diffusion_outputs() { let model = load_corpus_model("vanco_sde"); - let jit = compile_execution_artifact(&model).expect("compile jit kernels"); + let jit = compile_execution_artifact(&model).expect("compile jit functions"); let bytes = super::super::wasm_compile::compile_execution_model_to_wasm_bytes(&model) .expect("emit direct wasm bytes"); let (info, artifact) = load_wasm_artifact_bytes(&bytes).expect("load direct wasm"); @@ -1338,7 +1341,7 @@ mod tests { ); session .invoke_raw( - KernelRole::Diffusion, + ModelFunctionKind::Diffusion, 0.0, states.as_ptr(), params.as_ptr(), @@ -1347,7 +1350,7 @@ mod tests { derived.as_ptr(), actual.as_mut_ptr(), ) - .expect("invoke diffusion kernel"); + .expect("invoke diffusion function"); } for (actual, expected) in actual.iter().zip(expected.iter()) { @@ -1357,9 +1360,9 @@ mod tests { } #[test] - fn wasm_runtime_matches_jit_route_property_kernels() { + fn wasm_runtime_matches_jit_route_property_functions() { let model = load_corpus_model("one_cmt_oral_iv"); - let jit = compile_execution_artifact(&model).expect("compile jit kernels"); + let jit = compile_execution_artifact(&model).expect("compile jit functions"); let bytes = super::super::wasm_compile::compile_execution_model_to_wasm_bytes(&model) .expect("emit direct wasm bytes"); let (info, artifact) = load_wasm_artifact_bytes(&bytes).expect("load direct wasm"); @@ -1397,7 +1400,7 @@ mod tests { ); session .invoke_raw( - KernelRole::RouteLag, + ModelFunctionKind::RouteLag, 0.0, states.as_ptr(), params.as_ptr(), @@ -1406,10 +1409,10 @@ mod tests { derived.as_ptr(), actual_lag.as_mut_ptr(), ) - .expect("invoke route lag kernel"); + .expect("invoke route lag function"); session .invoke_raw( - KernelRole::RouteBioavailability, + ModelFunctionKind::RouteBioavailability, 0.0, states.as_ptr(), params.as_ptr(), @@ -1418,7 +1421,7 @@ mod tests { derived.as_ptr(), actual_bioavailability.as_mut_ptr(), ) - .expect("invoke route bioavailability kernel"); + .expect("invoke route bioavailability function"); } for (actual, expected) in actual_lag.iter().zip(expected_lag.iter()) { diff --git a/src/dsl/wasm_compile.rs b/src/dsl/wasm_compile.rs index 0e1b400d..0861e16b 100644 --- a/src/dsl/wasm_compile.rs +++ b/src/dsl/wasm_compile.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use super::compiled_backend_abi::{ - compiled_model_info_envelope, CompiledKernelAvailability, CompiledModelInfoEnvelope, + compiled_model_info_envelope, CompiledFunctionAvailability, CompiledModelInfoEnvelope, ALLOC_F64_BUFFER_SYMBOL, API_VERSION_SYMBOL, FREE_F64_BUFFER_SYMBOL, JS_KERNEL_EXPORTS, MODEL_INFO_JSON_LEN_SYMBOL, MODEL_INFO_JSON_PTR_SYMBOL, }; @@ -15,8 +15,8 @@ use super::wasm_direct_emitter::{ DIRECT_WASM_BINARY_MATH_IMPORTS, DIRECT_WASM_IMPORT_MODULE, DIRECT_WASM_UNARY_MATH_IMPORTS, }; use pharmsol_dsl::{ - analyze_module, lower_typed_model, parse_module, Diagnostic, DiagnosticReport, ExecutionModel, - LoweringError, ParseError, SemanticError, + analyze_module, compile_analyzed_model, parse_module, AnalysisError, CompileError, Diagnostic, + DiagnosticReport, ExecutionModel, ParseError, }; /// ABI version for compiled WASM artifacts produced by this crate. @@ -34,7 +34,7 @@ static BROWSER_LOADER_SOURCE: OnceLock = OnceLock::new(); pub struct CompiledWasmModule { /// Raw compiled WASM bytes. pub wasm_bytes: Vec, - /// Serialized model metadata and kernel availability. + /// Serialized model metadata and function availability. pub metadata: CompiledModelInfoEnvelope, /// JavaScript loader source for browser-side instantiation. pub browser_loader_source: String, @@ -173,9 +173,9 @@ pub enum WasmError { #[error("failed to parse DSL source: {0}")] Parse(#[source] ParseError), #[error("failed to analyze DSL source: {0}")] - Semantic(#[source] SemanticError), + Semantic(#[source] AnalysisError), #[error("failed to lower DSL model: {0}")] - Lowering(#[source] LoweringError), + Lowering(#[source] CompileError), #[error("{0}")] ModelSelection(String), #[error("failed to emit WASM module source: {0}")] @@ -185,12 +185,12 @@ pub enum WasmError { #[error("WASM artifact API version mismatch: expected {expected}, found {found}")] ApiVersionMismatch { expected: u32, found: u32 }, #[error( - "WASM kernel metadata mismatch for model `{model}`: expected {expected:?}, found {found:?}" + "WASM function metadata mismatch for model `{model}`: expected {expected:?}, found {found:?}" )] - KernelMetadataMismatch { + FunctionMetadataMismatch { model: String, - expected: CompiledKernelAvailability, - found: CompiledKernelAvailability, + expected: CompiledFunctionAvailability, + found: CompiledFunctionAvailability, }, #[error("missing required WASM export `{0}`")] MissingExport(&'static str), @@ -243,12 +243,12 @@ impl fmt::Debug for WasmError { } } -/// Compile a lowered execution model to raw WASM bytes. +/// Compile a compiled execution model to raw WASM bytes. pub fn compile_execution_model_to_wasm_bytes(model: &ExecutionModel) -> Result, WasmError> { emit_execution_model_to_wasm_bytes(model, WASM_API_VERSION) } -/// Compile a lowered execution model to a portable WASM bundle. +/// Compile a compiled execution model to a portable WASM bundle. pub fn compile_execution_model_to_wasm_module( model: &ExecutionModel, ) -> Result { @@ -309,18 +309,18 @@ fn compile_module_source_to_wasm_module_uncached( ) -> Result { let parsed = parse_module(source).map_err(|error| WasmError::Parse(error.with_source(source)))?; - let typed = + let analyzed = analyze_module(&parsed).map_err(|error| WasmError::Semantic(error.with_source(source)))?; let model = match model_name { - Some(name) => typed + Some(name) => analyzed .models .iter() .find(|model| model.name == name) .ok_or_else(|| { WasmError::ModelSelection(format!("model `{name}` not found in module")) })?, - None if typed.models.len() == 1 => &typed.models[0], + None if analyzed.models.len() == 1 => &analyzed.models[0], None => { return Err(WasmError::ModelSelection( "module contains multiple models; pass an explicit model name".to_string(), @@ -328,8 +328,8 @@ fn compile_module_source_to_wasm_module_uncached( } }; - let execution = - lower_typed_model(model).map_err(|error| WasmError::Lowering(error.with_source(source)))?; + let execution = compile_analyzed_model(model) + .map_err(|error| WasmError::Lowering(error.with_source(source)))?; compile_execution_model_to_wasm_module(&execution) } @@ -344,7 +344,7 @@ pub fn browser_loader_source() -> String { } fn build_browser_loader_source() -> String { - let kernel_symbol_entries = JS_KERNEL_EXPORTS + let function_symbol_entries = JS_KERNEL_EXPORTS .iter() .map(|(name, symbol)| format!(" {name}: \"{symbol}\"")) .collect::>() @@ -362,7 +362,7 @@ const FREE_F64_BUFFER_SYMBOL = "{free_f64_buffer_symbol}"; {direct_wasm_import_object} const KERNEL_SYMBOLS = Object.freeze({{ -{kernel_symbol_entries} +{function_symbol_entries} }}); {runtime_wrapper_source} @@ -425,14 +425,14 @@ export function createPharmsolDslWasmModel(instance) {{ throw new Error(`Expected pharmsol DSL WASM metadata version ${{API_VERSION}}, got ${{infoEnvelope.abi_version}}`); }} - const kernels = Object.fromEntries( + const functions = Object.fromEntries( Object.entries(KERNEL_SYMBOLS) .filter(([, symbol]) => typeof exports[symbol] === "function") .map(([name, symbol]) => [name, exports[symbol].bind(exports)]) ); - for (const [name, available] of Object.entries(infoEnvelope.kernels)) {{ - if (Boolean(available) !== Object.prototype.hasOwnProperty.call(kernels, name)) {{ + for (const [name, available] of Object.entries(infoEnvelope.functions)) {{ + if (Boolean(available) !== Object.prototype.hasOwnProperty.call(functions, name)) {{ throw new Error(`Kernel metadata mismatch for ${{name}}`); }} }} @@ -441,7 +441,7 @@ export function createPharmsolDslWasmModel(instance) {{ info: infoEnvelope.model, instance, memory, - kernels, + functions, createF64Buffer(length) {{ return createBufferHandle(exports, memory, length); }}, @@ -457,7 +457,7 @@ export function createPharmsolDslWasmModel(instance) {{ alloc_f64_buffer_symbol = ALLOC_F64_BUFFER_SYMBOL, free_f64_buffer_symbol = FREE_F64_BUFFER_SYMBOL, direct_wasm_import_object = direct_wasm_import_object, - kernel_symbol_entries = kernel_symbol_entries, + function_symbol_entries = function_symbol_entries, runtime_wrapper_source = runtime_wrapper_source, ) } @@ -506,13 +506,13 @@ function createParameterIndexResolver(parameters) { function normalizeKernelName(name) { if (!KNOWN_KERNELS.includes(name)) { - throw new Error(`Unknown kernel \`${name}\``); + throw new Error(`Unknown function \`${name}\``); } return name; } function hasKernel(model, name) { - return KNOWN_KERNELS.includes(name) && typeof model.kernels[name] === "function"; + return KNOWN_KERNELS.includes(name) && typeof model.functions[name] === "function"; } function normalizeTime(time) { @@ -600,12 +600,12 @@ function kernelOutputTarget(handles, views, info, kernelName) { } /** - * High-level reusable browser session layered on top of the raw kernel exports. + * High-level reusable browser session layered on top of the raw function exports. * Use this for repeated evaluations. It keeps stable typed-array views alive and * exposes a zero-copy fast path via `invokeKernelView(...)` and `evaluateOutputsView(...)`. * * If you need custom output aliasing, hand-managed buffer lifetimes, or direct - * access to the raw kernel ABI, use `model.kernels` plus `model.createF64Buffer(...)` directly. + * access to the raw function ABI, use `model.functions` plus `model.createF64Buffer(...)` directly. */ export function createPharmsolDslWasmSession(model) { const info = model.info; @@ -668,7 +668,7 @@ export function createPharmsolDslWasmSession(model) { function ensureReady(kind, label) { if (!initialized[kind]) { throw new Error( - `Missing ${label} for model \`${info.name}\`; set it on the session or pass it in the kernel inputs.` + `Missing ${label} for model \`${info.name}\`; set it on the session or pass it in the function inputs.` ); } } @@ -702,9 +702,9 @@ export function createPharmsolDslWasmSession(model) { function invokeKernelInternal(kernelName, inputs = {}, options = {}) { assertOpen(); const normalizedKernelName = normalizeKernelName(kernelName); - const kernel = model.kernels[normalizedKernelName]; - if (typeof kernel !== "function") { - throw new Error(`Model \`${info.name}\` does not expose kernel \`${normalizedKernelName}\``); + const modelFunction = model.functions[normalizedKernelName]; + if (typeof modelFunction !== "function") { + throw new Error(`Model \`${info.name}\` does not expose function \`${normalizedKernelName}\``); } prepareBaseInputs(inputs); @@ -721,7 +721,7 @@ export function createPharmsolDslWasmSession(model) { if (target.zeroBeforeCall) { target.view.fill(0); } - kernel( + modelFunction( time, handles.states.ptr, handles.params.ptr, @@ -793,7 +793,7 @@ export function createPharmsolDslWasmSession(model) { * `model.evaluateOutputs(...)` for simple one-off output evaluation. * * The raw low-level surface remains available on the returned object via - * `model.kernels`, `model.memory`, `model.instance`, and `model.createF64Buffer(...)`. + * `model.functions`, `model.memory`, `model.instance`, and `model.createF64Buffer(...)`. */ function createHighLevelPharmsolDslWasmModelApi(lowLevelModel) { const routeIndex = createNamedIndexResolver(lowLevelModel.info.routes, "route"); @@ -899,7 +899,7 @@ fn direct_wasm_browser_import_object_source() -> String { mod tests { use super::*; use pharmsol_dsl::{ - DiagnosticPhase, DSL_LOWERING_GENERIC, DSL_PARSE_GENERIC, DSL_SEMANTIC_GENERIC, + DiagnosticPhase, DSL_ANALYSIS_GENERIC, DSL_COMPILE_GENERIC, DSL_PARSE_GENERIC, }; const SIMPLE_SOURCE: &str = r#" @@ -925,7 +925,7 @@ out(cp) = central / v ~ continuous() assert!(!compiled.wasm_bytes.is_empty()); assert_eq!(compiled.metadata.model.name, "example_ode"); assert_eq!(compiled.metadata.abi_version, WASM_API_VERSION); - assert!(compiled.metadata.kernels.outputs); + assert!(compiled.metadata.functions.outputs); } #[test] @@ -1002,7 +1002,7 @@ out(cp) = central / v ~ continuous() } #[test] - fn compile_module_source_to_wasm_module_preserves_semantic_diagnostic_structure() { + fn compile_module_source_to_wasm_module_preserves_analysis_diagnostic_structure() { let source = r#" name = broken kind = ode @@ -1017,11 +1017,11 @@ dx(central) = rate(orla) out(cp) = central ~ continuous() "#; let error = compile_module_source_to_wasm_module(source, None) - .expect_err("invalid semantic route reference should fail before wasm emission"); + .expect_err("invalid route reference should fail analysis before wasm emission"); let diagnostic = error.diagnostic().expect("wasm should expose diagnostic"); - assert_eq!(diagnostic.phase, DiagnosticPhase::Semantic); - assert_eq!(diagnostic.code, DSL_SEMANTIC_GENERIC); + assert_eq!(diagnostic.phase, DiagnosticPhase::Analysis); + assert_eq!(diagnostic.code, DSL_ANALYSIS_GENERIC); assert!(diagnostic.message.contains("unknown route `orla`")); assert!(diagnostic .suggestions @@ -1043,12 +1043,12 @@ out(cp) = central ~ continuous() .diagnostic_report("inline.dsl") .expect("diagnostic report"); assert_eq!(report.diagnostics[0].code, "DSL2000"); - assert_eq!(report.diagnostics[0].phase, "semantic"); + assert_eq!(report.diagnostics[0].phase, "analysis"); assert!(!report.diagnostics[0].suggestions.is_empty()); } #[test] - fn compile_module_source_to_wasm_module_preserves_lowering_diagnostic_structure() { + fn compile_module_source_to_wasm_module_preserves_compile_diagnostic_structure() { let source = r#" name = broken kind = ode @@ -1067,11 +1067,11 @@ dx(central) = transit[3] - central out(cp) = central ~ continuous() "#; let error = compile_module_source_to_wasm_module(source, None) - .expect_err("out-of-bounds route destination should fail during lowering"); + .expect_err("out-of-bounds route destination should fail during compilation"); let diagnostic = error.diagnostic().expect("wasm should expose diagnostic"); - assert_eq!(diagnostic.phase, DiagnosticPhase::Lowering); - assert_eq!(diagnostic.code, DSL_LOWERING_GENERIC); + assert_eq!(diagnostic.phase, DiagnosticPhase::Compile); + assert_eq!(diagnostic.code, DSL_COMPILE_GENERIC); assert!(diagnostic .message .contains("route destination for `transit` indexes element 4")); @@ -1090,7 +1090,7 @@ out(cp) = central ~ continuous() .diagnostic_report("inline.dsl") .expect("diagnostic report"); assert_eq!(report.diagnostics[0].code, "DSL3000"); - assert_eq!(report.diagnostics[0].phase, "lowering"); + assert_eq!(report.diagnostics[0].phase, "compile"); assert_eq!(report.source.name, "inline.dsl"); } diff --git a/src/dsl/wasm_direct_emitter.rs b/src/dsl/wasm_direct_emitter.rs index 857f2ac7..abcd3df3 100644 --- a/src/dsl/wasm_direct_emitter.rs +++ b/src/dsl/wasm_direct_emitter.rs @@ -7,18 +7,20 @@ use wasm_encoder::{ }; use super::compiled_backend_abi::{ - compiled_kernel_symbol, encode_compiled_model_info, ALLOC_F64_BUFFER_SYMBOL, + compiled_function_symbol, encode_compiled_model_info, ALLOC_F64_BUFFER_SYMBOL, API_VERSION_SYMBOL, FREE_F64_BUFFER_SYMBOL, MODEL_INFO_JSON_LEN_SYMBOL, MODEL_INFO_JSON_PTR_SYMBOL, }; use crate::dsl::WasmError; use pharmsol_dsl::execution::{ ExecutionAssignStmt, ExecutionCall, ExecutionExpr, ExecutionExprKind, ExecutionForStmt, - ExecutionIfStmt, ExecutionKernel, ExecutionLoad, ExecutionModel, ExecutionProgram, - ExecutionStateRef, ExecutionStmt, ExecutionStmtKind, ExecutionTargetKind, KernelImplementation, - KernelRole, + ExecutionIfStmt, ExecutionLoad, ExecutionModel, ExecutionProgram, ExecutionStateRef, + ExecutionStmt, ExecutionStmtKind, ExecutionTargetKind, FunctionBody, ModelFunction, + ModelFunctionKind, +}; +use pharmsol_dsl::{ + AnalyzedBinaryOp, AnalyzedUnaryOp, ConstValue, MathFunction, ModelKind, ValueType, }; -use pharmsol_dsl::{ConstValue, MathIntrinsic, ModelKind, TypedBinaryOp, TypedUnaryOp, ValueType}; const PAGE_SIZE: usize = 65_536; const ABI_PTR_ALIGNMENT: usize = std::mem::size_of::(); @@ -27,7 +29,7 @@ const MODEL_INFO_PTR: i32 = 0; const API_VERSION_TYPE: u32 = 0; const ALLOC_TYPE: u32 = 1; const FREE_TYPE: u32 = 2; -const KERNEL_TYPE: u32 = 3; +const MODEL_FUNCTION_TYPE: u32 = 3; const UNARY_REAL_IMPORT_TYPE: u32 = 4; const BINARY_REAL_IMPORT_TYPE: u32 = 5; @@ -46,65 +48,65 @@ pub(crate) const DIRECT_WASM_IMPORT_MODULE: &str = "pharmsol_dsl_host_math"; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) struct DirectUnaryMathImport { pub name: &'static str, - pub intrinsic: MathIntrinsic, + pub intrinsic: MathFunction, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) struct DirectBinaryMathImport { pub name: &'static str, - pub intrinsic: MathIntrinsic, + pub intrinsic: MathFunction, } pub(crate) const DIRECT_WASM_UNARY_MATH_IMPORTS: [DirectUnaryMathImport; 8] = [ DirectUnaryMathImport { name: "exp", - intrinsic: MathIntrinsic::Exp, + intrinsic: MathFunction::Exp, }, DirectUnaryMathImport { name: "ln", - intrinsic: MathIntrinsic::Ln, + intrinsic: MathFunction::Ln, }, DirectUnaryMathImport { name: "log10", - intrinsic: MathIntrinsic::Log10, + intrinsic: MathFunction::Log10, }, DirectUnaryMathImport { name: "log2", - intrinsic: MathIntrinsic::Log2, + intrinsic: MathFunction::Log2, }, DirectUnaryMathImport { name: "round", - intrinsic: MathIntrinsic::Round, + intrinsic: MathFunction::Round, }, DirectUnaryMathImport { name: "sin", - intrinsic: MathIntrinsic::Sin, + intrinsic: MathFunction::Sin, }, DirectUnaryMathImport { name: "cos", - intrinsic: MathIntrinsic::Cos, + intrinsic: MathFunction::Cos, }, DirectUnaryMathImport { name: "tan", - intrinsic: MathIntrinsic::Tan, + intrinsic: MathFunction::Tan, }, ]; pub(crate) const DIRECT_WASM_BINARY_MATH_IMPORTS: [DirectBinaryMathImport; 1] = [DirectBinaryMathImport { name: "pow", - intrinsic: MathIntrinsic::Pow, + intrinsic: MathFunction::Pow, }]; -const DIRECT_SUPPORTED_STATEMENT_KERNEL_ROLES: [KernelRole; 8] = [ - KernelRole::Derive, - KernelRole::Dynamics, - KernelRole::Outputs, - KernelRole::Init, - KernelRole::Drift, - KernelRole::Diffusion, - KernelRole::RouteLag, - KernelRole::RouteBioavailability, +const DIRECT_SUPPORTED_STATEMENT_KERNEL_ROLES: [ModelFunctionKind; 8] = [ + ModelFunctionKind::Derive, + ModelFunctionKind::Dynamics, + ModelFunctionKind::Outputs, + ModelFunctionKind::Init, + ModelFunctionKind::Drift, + ModelFunctionKind::Diffusion, + ModelFunctionKind::RouteLag, + ModelFunctionKind::RouteBioavailability, ]; #[derive(Debug, Clone, Copy)] @@ -113,7 +115,7 @@ struct WasmLocalBinding { ty: ValueType, } -struct KernelEmitState<'a> { +struct FunctionEmitState<'a> { model_name: &'a str, locals: BTreeMap, next_hidden_i64_local: u32, @@ -123,7 +125,7 @@ pub(crate) fn compile_execution_model_to_wasm_bytes( model: &ExecutionModel, api_version: u32, ) -> Result, WasmError> { - let kernels = collect_direct_statement_kernels(model)?; + let model_functions = collect_direct_statement_functions(model)?; let metadata = encode_compiled_model_info(model, api_version)?; let metadata_bytes = metadata.into_bytes(); let aligned_heap_start = align_to_f64_boundary(metadata_bytes.len())?; @@ -175,8 +177,8 @@ pub(crate) fn compile_execution_model_to_wasm_bytes( functions.function(API_VERSION_TYPE); functions.function(ALLOC_TYPE); functions.function(FREE_TYPE); - for _ in &kernels { - functions.function(KERNEL_TYPE); + for _ in &model_functions { + functions.function(MODEL_FUNCTION_TYPE); } module.section(&functions); @@ -234,17 +236,20 @@ pub(crate) fn compile_execution_model_to_wasm_bytes( ExportKind::Func, first_defined_function_index + 4, ); - for (kernel_index, kernel) in kernels.iter().enumerate() { - let symbol = compiled_kernel_symbol(kernel.role).ok_or_else(|| { + for (function_index, function) in model_functions.iter().enumerate() { + let symbol = compiled_function_symbol(function.kind).ok_or_else(|| { WasmError::DirectBackendUnsupported { model: model.name.clone(), - reason: format!("missing compiled kernel symbol for role {:?}", kernel.role), + reason: format!( + "missing compiled function symbol for role {:?}", + function.kind + ), } })?; exports.export( symbol, ExportKind::Func, - first_defined_function_index + 5 + kernel_index as u32, + first_defined_function_index + 5 + function_index as u32, ); } module.section(&exports); @@ -262,8 +267,8 @@ pub(crate) fn compile_execution_model_to_wasm_bytes( )); code.function(&alloc_function()); code.function(&free_function()); - for kernel in &kernels { - code.function(&emit_statement_kernel(model, kernel)?); + for function in &model_functions { + code.function(&emit_statement_function(model, function)?); } module.section(&code); @@ -274,9 +279,9 @@ pub(crate) fn compile_execution_model_to_wasm_bytes( Ok(module.finish()) } -fn collect_direct_statement_kernels( +fn collect_direct_statement_functions( model: &ExecutionModel, -) -> Result, WasmError> { +) -> Result, WasmError> { if !matches!( model.kind, ModelKind::Ode | ModelKind::Analytical | ModelKind::Sde @@ -291,38 +296,38 @@ fn collect_direct_statement_kernels( } let unsupported_roles = model - .kernels + .functions .iter() - .filter(|kernel| { - kernel.role != KernelRole::Analytical - && !DIRECT_SUPPORTED_STATEMENT_KERNEL_ROLES.contains(&kernel.role) + .filter(|function| { + function.kind != ModelFunctionKind::Analytical + && !DIRECT_SUPPORTED_STATEMENT_KERNEL_ROLES.contains(&function.kind) }) - .map(|kernel| format!("{:?}", kernel.role)) + .map(|function| format!("{:?}", function.kind)) .collect::>(); if !unsupported_roles.is_empty() { return Err(WasmError::DirectBackendUnsupported { model: model.name.clone(), reason: format!( - "direct emission supports only metadata-only analytical kernels plus statement kernels {:?}; found {}", + "direct emission supports only metadata-only analytical functions plus statement functions {:?}; found {}", DIRECT_SUPPORTED_STATEMENT_KERNEL_ROLES, unsupported_roles.join(", ") ), }); } - if model.kernel(KernelRole::Outputs).is_none() { + if model.function(ModelFunctionKind::Outputs).is_none() { return Err(WasmError::DirectBackendUnsupported { model: model.name.clone(), - reason: "direct emitter requires an outputs kernel".to_string(), + reason: "direct emitter requires an outputs function".to_string(), }); } - let mut kernels = Vec::new(); + let mut functions = Vec::new(); for role in DIRECT_SUPPORTED_STATEMENT_KERNEL_ROLES { - if let Some(kernel) = model.kernel(role) { - match kernel.implementation { - KernelImplementation::Statements(_) => kernels.push(kernel), - KernelImplementation::AnalyticalBuiltin(_) => { + if let Some(function) = model.function(role) { + match function.body { + FunctionBody::Statements(_) => functions.push(function), + FunctionBody::AnalyticalBuiltin(_) => { return Err(WasmError::DirectBackendUnsupported { model: model.name.clone(), reason: format!( @@ -335,11 +340,8 @@ fn collect_direct_statement_kernels( } } - if let Some(kernel) = model.kernel(KernelRole::Analytical) { - if !matches!( - kernel.implementation, - KernelImplementation::AnalyticalBuiltin(_) - ) { + if let Some(function) = model.function(ModelFunctionKind::Analytical) { + if !matches!(function.body, FunctionBody::AnalyticalBuiltin(_)) { return Err(WasmError::DirectBackendUnsupported { model: model.name.clone(), reason: "direct emitter expects analytical execution to remain metadata-driven" @@ -348,13 +350,13 @@ fn collect_direct_statement_kernels( } if model.metadata.analytical.is_none() { return Err(WasmError::Emit(format!( - "analytical model `{}` lowered an analytical kernel without analytical metadata", + "analytical model `{}` compiled an analytical function without analytical metadata", model.name ))); } } - Ok(kernels) + Ok(functions) } fn const_i32_function(value: i32) -> Function { @@ -422,18 +424,18 @@ fn free_function() -> Function { function } -fn emit_statement_kernel( +fn emit_statement_function( model: &ExecutionModel, - kernel: &ExecutionKernel, + function: &ModelFunction, ) -> Result { - let program = match &kernel.implementation { - KernelImplementation::Statements(program) => program, - KernelImplementation::AnalyticalBuiltin(_) => { + let program = match &function.body { + FunctionBody::Statements(program) => program, + FunctionBody::AnalyticalBuiltin(_) => { return Err(WasmError::DirectBackendUnsupported { model: model.name.clone(), reason: format!( "direct W04 emitter does not support analytical builtins for {:?}", - kernel.role + function.kind ), }) } @@ -460,7 +462,7 @@ fn emit_statement_kernel( } let mut function = Function::new(locals); - let mut state = KernelEmitState { + let mut state = FunctionEmitState { model_name: &model.name, locals: local_bindings, next_hidden_i64_local: next_local_index, @@ -471,7 +473,7 @@ fn emit_statement_kernel( } fn emit_program( - state: &mut KernelEmitState<'_>, + state: &mut FunctionEmitState<'_>, program: &ExecutionProgram, function: &mut Function, ) -> Result<(), WasmError> { @@ -482,7 +484,7 @@ fn emit_program( } fn emit_statement( - state: &mut KernelEmitState<'_>, + state: &mut FunctionEmitState<'_>, statement: &ExecutionStmt, function: &mut Function, ) -> Result<(), WasmError> { @@ -501,7 +503,7 @@ fn emit_statement( } fn emit_assignment( - state: &mut KernelEmitState<'_>, + state: &mut FunctionEmitState<'_>, assign: &ExecutionAssignStmt, function: &mut Function, ) -> Result<(), WasmError> { @@ -515,7 +517,7 @@ fn emit_assignment( } fn emit_if( - state: &mut KernelEmitState<'_>, + state: &mut FunctionEmitState<'_>, if_stmt: &ExecutionIfStmt, function: &mut Function, ) -> Result<(), WasmError> { @@ -541,7 +543,7 @@ fn emit_if( } fn emit_for( - state: &mut KernelEmitState<'_>, + state: &mut FunctionEmitState<'_>, for_stmt: &ExecutionForStmt, function: &mut Function, ) -> Result<(), WasmError> { @@ -594,7 +596,7 @@ fn emit_for( } fn emit_expr( - state: &mut KernelEmitState<'_>, + state: &mut FunctionEmitState<'_>, expr: &ExecutionExpr, function: &mut Function, ) -> Result<(), WasmError> { @@ -605,8 +607,8 @@ fn emit_expr( emit_expr(state, inner, function)?; emit_cast_stack(inner.ty, expr.ty, function, state.model_name)?; match op { - TypedUnaryOp::Plus => Ok(()), - TypedUnaryOp::Minus => match expr.ty { + AnalyzedUnaryOp::Plus => Ok(()), + AnalyzedUnaryOp::Minus => match expr.ty { ValueType::Real => { function.instruction(&Instruction::F64Neg); Ok(()) @@ -621,7 +623,7 @@ fn emit_expr( reason: "cannot apply unary minus to a boolean expression".to_string(), }), }, - TypedUnaryOp::Not => { + AnalyzedUnaryOp::Not => { emit_cast_stack(expr.ty, ValueType::Bool, function, state.model_name)?; function.instruction(&Instruction::I32Eqz); Ok(()) @@ -642,29 +644,29 @@ fn emit_expr( } fn emit_binary( - state: &mut KernelEmitState<'_>, - op: TypedBinaryOp, + state: &mut FunctionEmitState<'_>, + op: AnalyzedBinaryOp, lhs: &ExecutionExpr, rhs: &ExecutionExpr, result_ty: ValueType, function: &mut Function, ) -> Result<(), WasmError> { match op { - TypedBinaryOp::Or => { + AnalyzedBinaryOp::Or => { emit_expr(state, lhs, function)?; emit_cast_stack(lhs.ty, ValueType::Bool, function, state.model_name)?; emit_expr(state, rhs, function)?; emit_cast_stack(rhs.ty, ValueType::Bool, function, state.model_name)?; function.instruction(&Instruction::I32Or); } - TypedBinaryOp::And => { + AnalyzedBinaryOp::And => { emit_expr(state, lhs, function)?; emit_cast_stack(lhs.ty, ValueType::Bool, function, state.model_name)?; emit_expr(state, rhs, function)?; emit_cast_stack(rhs.ty, ValueType::Bool, function, state.model_name)?; function.instruction(&Instruction::I32And); } - TypedBinaryOp::Eq | TypedBinaryOp::NotEq => { + AnalyzedBinaryOp::Eq | AnalyzedBinaryOp::NotEq => { let operand_ty = if lhs.ty == ValueType::Real || rhs.ty == ValueType::Real { ValueType::Real } else if lhs.ty == ValueType::Bool && rhs.ty == ValueType::Bool { @@ -677,20 +679,29 @@ fn emit_binary( emit_expr(state, rhs, function)?; emit_cast_stack(rhs.ty, operand_ty, function, state.model_name)?; match (operand_ty, op) { - (ValueType::Real, TypedBinaryOp::Eq) => function.instruction(&Instruction::F64Eq), - (ValueType::Real, TypedBinaryOp::NotEq) => { + (ValueType::Real, AnalyzedBinaryOp::Eq) => { + function.instruction(&Instruction::F64Eq) + } + (ValueType::Real, AnalyzedBinaryOp::NotEq) => { function.instruction(&Instruction::F64Ne) } - (ValueType::Int, TypedBinaryOp::Eq) => function.instruction(&Instruction::I64Eq), - (ValueType::Int, TypedBinaryOp::NotEq) => function.instruction(&Instruction::I64Ne), - (ValueType::Bool, TypedBinaryOp::Eq) => function.instruction(&Instruction::I32Eq), - (ValueType::Bool, TypedBinaryOp::NotEq) => { + (ValueType::Int, AnalyzedBinaryOp::Eq) => function.instruction(&Instruction::I64Eq), + (ValueType::Int, AnalyzedBinaryOp::NotEq) => { + function.instruction(&Instruction::I64Ne) + } + (ValueType::Bool, AnalyzedBinaryOp::Eq) => { + function.instruction(&Instruction::I32Eq) + } + (ValueType::Bool, AnalyzedBinaryOp::NotEq) => { function.instruction(&Instruction::I32Ne) } _ => unreachable!(), }; } - TypedBinaryOp::Lt | TypedBinaryOp::LtEq | TypedBinaryOp::Gt | TypedBinaryOp::GtEq => { + AnalyzedBinaryOp::Lt + | AnalyzedBinaryOp::LtEq + | AnalyzedBinaryOp::Gt + | AnalyzedBinaryOp::GtEq => { let operand_ty = if lhs.ty == ValueType::Real || rhs.ty == ValueType::Real { ValueType::Real } else { @@ -701,29 +712,57 @@ fn emit_binary( emit_expr(state, rhs, function)?; emit_cast_stack(rhs.ty, operand_ty, function, state.model_name)?; match (operand_ty, op) { - (ValueType::Real, TypedBinaryOp::Lt) => function.instruction(&Instruction::F64Lt), - (ValueType::Real, TypedBinaryOp::LtEq) => function.instruction(&Instruction::F64Le), - (ValueType::Real, TypedBinaryOp::Gt) => function.instruction(&Instruction::F64Gt), - (ValueType::Real, TypedBinaryOp::GtEq) => function.instruction(&Instruction::F64Ge), - (ValueType::Int, TypedBinaryOp::Lt) => function.instruction(&Instruction::I64LtS), - (ValueType::Int, TypedBinaryOp::LtEq) => function.instruction(&Instruction::I64LeS), - (ValueType::Int, TypedBinaryOp::Gt) => function.instruction(&Instruction::I64GtS), - (ValueType::Int, TypedBinaryOp::GtEq) => function.instruction(&Instruction::I64GeS), + (ValueType::Real, AnalyzedBinaryOp::Lt) => { + function.instruction(&Instruction::F64Lt) + } + (ValueType::Real, AnalyzedBinaryOp::LtEq) => { + function.instruction(&Instruction::F64Le) + } + (ValueType::Real, AnalyzedBinaryOp::Gt) => { + function.instruction(&Instruction::F64Gt) + } + (ValueType::Real, AnalyzedBinaryOp::GtEq) => { + function.instruction(&Instruction::F64Ge) + } + (ValueType::Int, AnalyzedBinaryOp::Lt) => { + function.instruction(&Instruction::I64LtS) + } + (ValueType::Int, AnalyzedBinaryOp::LtEq) => { + function.instruction(&Instruction::I64LeS) + } + (ValueType::Int, AnalyzedBinaryOp::Gt) => { + function.instruction(&Instruction::I64GtS) + } + (ValueType::Int, AnalyzedBinaryOp::GtEq) => { + function.instruction(&Instruction::I64GeS) + } _ => unreachable!(), }; } - TypedBinaryOp::Add | TypedBinaryOp::Sub | TypedBinaryOp::Mul => { + AnalyzedBinaryOp::Add | AnalyzedBinaryOp::Sub | AnalyzedBinaryOp::Mul => { emit_expr(state, lhs, function)?; emit_cast_stack(lhs.ty, result_ty, function, state.model_name)?; emit_expr(state, rhs, function)?; emit_cast_stack(rhs.ty, result_ty, function, state.model_name)?; match (result_ty, op) { - (ValueType::Real, TypedBinaryOp::Add) => function.instruction(&Instruction::F64Add), - (ValueType::Real, TypedBinaryOp::Sub) => function.instruction(&Instruction::F64Sub), - (ValueType::Real, TypedBinaryOp::Mul) => function.instruction(&Instruction::F64Mul), - (ValueType::Int, TypedBinaryOp::Add) => function.instruction(&Instruction::I64Add), - (ValueType::Int, TypedBinaryOp::Sub) => function.instruction(&Instruction::I64Sub), - (ValueType::Int, TypedBinaryOp::Mul) => function.instruction(&Instruction::I64Mul), + (ValueType::Real, AnalyzedBinaryOp::Add) => { + function.instruction(&Instruction::F64Add) + } + (ValueType::Real, AnalyzedBinaryOp::Sub) => { + function.instruction(&Instruction::F64Sub) + } + (ValueType::Real, AnalyzedBinaryOp::Mul) => { + function.instruction(&Instruction::F64Mul) + } + (ValueType::Int, AnalyzedBinaryOp::Add) => { + function.instruction(&Instruction::I64Add) + } + (ValueType::Int, AnalyzedBinaryOp::Sub) => { + function.instruction(&Instruction::I64Sub) + } + (ValueType::Int, AnalyzedBinaryOp::Mul) => { + function.instruction(&Instruction::I64Mul) + } _ => { return Err(WasmError::DirectBackendUnsupported { model: state.model_name.to_string(), @@ -735,29 +774,29 @@ fn emit_binary( } }; } - TypedBinaryOp::Div => { + AnalyzedBinaryOp::Div => { emit_expr(state, lhs, function)?; emit_cast_stack(lhs.ty, ValueType::Real, function, state.model_name)?; emit_expr(state, rhs, function)?; emit_cast_stack(rhs.ty, ValueType::Real, function, state.model_name)?; function.instruction(&Instruction::F64Div); } - TypedBinaryOp::Pow => { - emit_math_call(state, MathIntrinsic::Pow, &[lhs, rhs], result_ty, function)? + AnalyzedBinaryOp::Pow => { + emit_math_call(state, MathFunction::Pow, &[lhs, rhs], result_ty, function)? } } Ok(()) } fn emit_math_call( - state: &mut KernelEmitState<'_>, - intrinsic: MathIntrinsic, + state: &mut FunctionEmitState<'_>, + intrinsic: MathFunction, args: &[&ExecutionExpr], result_ty: ValueType, function: &mut Function, ) -> Result<(), WasmError> { match intrinsic { - MathIntrinsic::Max | MathIntrinsic::Min => { + MathFunction::Max | MathFunction::Min => { if args.len() != 2 { return Err(WasmError::DirectBackendUnsupported { model: state.model_name.to_string(), @@ -770,7 +809,7 @@ fn emit_math_call( emit_cast_stack(args[0].ty, ValueType::Real, function, state.model_name)?; emit_expr(state, args[1], function)?; emit_cast_stack(args[1].ty, ValueType::Real, function, state.model_name)?; - if intrinsic == MathIntrinsic::Max { + if intrinsic == MathFunction::Max { function.instruction(&Instruction::F64Max); } else { function.instruction(&Instruction::F64Min); @@ -789,7 +828,7 @@ fn emit_math_call( function.instruction(&Instruction::LocalGet(rhs_local)); function.instruction(&Instruction::LocalGet(lhs_local)); function.instruction(&Instruction::LocalGet(rhs_local)); - if intrinsic == MathIntrinsic::Max { + if intrinsic == MathFunction::Max { function.instruction(&Instruction::I64GtS); } else { function.instruction(&Instruction::I64LtS); @@ -804,7 +843,7 @@ fn emit_math_call( } } } - MathIntrinsic::Abs if result_ty == ValueType::Int => { + MathFunction::Abs if result_ty == ValueType::Int => { if args.len() != 1 { return Err(WasmError::DirectBackendUnsupported { model: state.model_name.to_string(), @@ -825,11 +864,7 @@ fn emit_math_call( function.instruction(&Instruction::Select); } _ => { - let expected_arity = if intrinsic == MathIntrinsic::Pow { - 2 - } else { - 1 - }; + let expected_arity = if intrinsic == MathFunction::Pow { 2 } else { 1 }; if args.len() != expected_arity { return Err(WasmError::DirectBackendUnsupported { model: state.model_name.to_string(), @@ -841,38 +876,38 @@ fn emit_math_call( emit_cast_stack(arg.ty, ValueType::Real, function, state.model_name)?; } match intrinsic { - MathIntrinsic::Abs => function.instruction(&Instruction::F64Abs), - MathIntrinsic::Ceil => function.instruction(&Instruction::F64Ceil), - MathIntrinsic::Exp => function.instruction(&Instruction::Call( - unary_math_import_index(MathIntrinsic::Exp)?, + MathFunction::Abs => function.instruction(&Instruction::F64Abs), + MathFunction::Ceil => function.instruction(&Instruction::F64Ceil), + MathFunction::Exp => function.instruction(&Instruction::Call( + unary_math_import_index(MathFunction::Exp)?, )), - MathIntrinsic::Floor => function.instruction(&Instruction::F64Floor), - MathIntrinsic::Ln | MathIntrinsic::Log => function.instruction(&Instruction::Call( - unary_math_import_index(MathIntrinsic::Ln)?, + MathFunction::Floor => function.instruction(&Instruction::F64Floor), + MathFunction::Ln | MathFunction::Log => function.instruction(&Instruction::Call( + unary_math_import_index(MathFunction::Ln)?, )), - MathIntrinsic::Log10 => function.instruction(&Instruction::Call( - unary_math_import_index(MathIntrinsic::Log10)?, + MathFunction::Log10 => function.instruction(&Instruction::Call( + unary_math_import_index(MathFunction::Log10)?, )), - MathIntrinsic::Log2 => function.instruction(&Instruction::Call( - unary_math_import_index(MathIntrinsic::Log2)?, + MathFunction::Log2 => function.instruction(&Instruction::Call( + unary_math_import_index(MathFunction::Log2)?, )), - MathIntrinsic::Pow => function.instruction(&Instruction::Call( - binary_math_import_index(MathIntrinsic::Pow)?, + MathFunction::Pow => function.instruction(&Instruction::Call( + binary_math_import_index(MathFunction::Pow)?, )), - MathIntrinsic::Round => function.instruction(&Instruction::Call( - unary_math_import_index(MathIntrinsic::Round)?, + MathFunction::Round => function.instruction(&Instruction::Call( + unary_math_import_index(MathFunction::Round)?, )), - MathIntrinsic::Sin => function.instruction(&Instruction::Call( - unary_math_import_index(MathIntrinsic::Sin)?, + MathFunction::Sin => function.instruction(&Instruction::Call( + unary_math_import_index(MathFunction::Sin)?, )), - MathIntrinsic::Cos => function.instruction(&Instruction::Call( - unary_math_import_index(MathIntrinsic::Cos)?, + MathFunction::Cos => function.instruction(&Instruction::Call( + unary_math_import_index(MathFunction::Cos)?, )), - MathIntrinsic::Tan => function.instruction(&Instruction::Call( - unary_math_import_index(MathIntrinsic::Tan)?, + MathFunction::Tan => function.instruction(&Instruction::Call( + unary_math_import_index(MathFunction::Tan)?, )), - MathIntrinsic::Sqrt => function.instruction(&Instruction::F64Sqrt), - MathIntrinsic::Max | MathIntrinsic::Min => unreachable!(), + MathFunction::Sqrt => function.instruction(&Instruction::F64Sqrt), + MathFunction::Max | MathFunction::Min => unreachable!(), }; emit_cast_stack(ValueType::Real, result_ty, function, state.model_name)?; } @@ -890,7 +925,7 @@ fn emit_literal(value: &ConstValue, function: &mut Function) -> Result<(), WasmE } fn emit_load( - state: &mut KernelEmitState<'_>, + state: &mut FunctionEmitState<'_>, load: &ExecutionLoad, target_ty: ValueType, function: &mut Function, @@ -950,7 +985,7 @@ fn emit_dense_load( } fn emit_state_load( - state: &mut KernelEmitState<'_>, + state: &mut FunctionEmitState<'_>, base_ptr_local: u32, state_ref: &ExecutionStateRef, target_ty: ValueType, @@ -964,7 +999,7 @@ fn emit_state_load( } fn emit_target_byte_offset( - state: &mut KernelEmitState<'_>, + state: &mut FunctionEmitState<'_>, target: &ExecutionTargetKind, function: &mut Function, ) -> Result<(), WasmError> { @@ -985,7 +1020,7 @@ fn emit_target_byte_offset( } fn emit_state_ref_byte_offset( - state: &mut KernelEmitState<'_>, + state: &mut FunctionEmitState<'_>, state_ref: &ExecutionStateRef, function: &mut Function, ) -> Result<(), WasmError> { @@ -1103,12 +1138,12 @@ fn count_hidden_i64_locals_in_expr(expr: &ExecutionExpr) -> usize { .map(count_hidden_i64_locals_in_expr) .sum::(); let local_cost = match callee { - ExecutionCall::Math(MathIntrinsic::Max | MathIntrinsic::Min) + ExecutionCall::Math(MathFunction::Max | MathFunction::Min) if expr.ty == ValueType::Int => { 2 } - ExecutionCall::Math(MathIntrinsic::Abs) if expr.ty == ValueType::Int => 1, + ExecutionCall::Math(MathFunction::Abs) if expr.ty == ValueType::Int => 1, _ => 0, }; arg_cost + local_cost @@ -1120,7 +1155,7 @@ fn direct_wasm_import_count() -> usize { DIRECT_WASM_UNARY_MATH_IMPORTS.len() + DIRECT_WASM_BINARY_MATH_IMPORTS.len() } -fn unary_math_import_index(intrinsic: MathIntrinsic) -> Result { +fn unary_math_import_index(intrinsic: MathFunction) -> Result { DIRECT_WASM_UNARY_MATH_IMPORTS .iter() .position(|import| import.intrinsic == intrinsic) @@ -1132,7 +1167,7 @@ fn unary_math_import_index(intrinsic: MathIntrinsic) -> Result { }) } -fn binary_math_import_index(intrinsic: MathIntrinsic) -> Result { +fn binary_math_import_index(intrinsic: MathFunction) -> Result { DIRECT_WASM_BINARY_MATH_IMPORTS .iter() .position(|import| import.intrinsic == intrinsic) @@ -1166,7 +1201,7 @@ fn f64_memarg() -> MemArg { } } -impl KernelEmitState<'_> { +impl FunctionEmitState<'_> { fn local(&self, index: usize) -> Result { self.locals .get(&index) @@ -1185,10 +1220,9 @@ impl KernelEmitState<'_> { pub(crate) fn w03_minimal_outputs_execution_model() -> ExecutionModel { use super::Span; use pharmsol_dsl::execution::{ - BufferKind, BufferSlot, CallingConvention, DenseBufferLayout, ExecutionAbi, ExecutionBlock, + Access, BufferKind, BufferLayout, BufferSlot, ExecutionBlock, ExecutionLayout, ExecutionMetadata, ExecutionProgram, ExecutionSlot, ExecutionState, ExecutionStateRef, - ExecutionTarget, KernelAccess, KernelArgument, KernelArgumentKind, KernelSignature, - ScalarAbi, + ExecutionTarget, FunctionArgument, FunctionArgumentKind, FunctionSignature, ScalarType, }; let span = Span::empty(0); @@ -1234,10 +1268,9 @@ pub(crate) fn w03_minimal_outputs_execution_model() -> ExecutionModel { particles: None, analytical: None, }, - abi: ExecutionAbi { - scalar: ScalarAbi::F64, - calling_convention: CallingConvention::DenseF64Buffers, - parameter_buffer: DenseBufferLayout { + layout: ExecutionLayout { + scalar: ScalarType::F64, + parameter_buffer: BufferLayout { kind: BufferKind::Parameters, len: 2, slots: vec![ @@ -1253,12 +1286,12 @@ pub(crate) fn w03_minimal_outputs_execution_model() -> ExecutionModel { }, ], }, - covariate_buffer: DenseBufferLayout { + covariate_buffer: BufferLayout { kind: BufferKind::Covariates, len: 0, slots: vec![], }, - state_buffer: DenseBufferLayout { + state_buffer: BufferLayout { kind: BufferKind::States, len: 1, slots: vec![BufferSlot { @@ -1267,12 +1300,12 @@ pub(crate) fn w03_minimal_outputs_execution_model() -> ExecutionModel { len: 1, }], }, - derived_buffer: DenseBufferLayout { + derived_buffer: BufferLayout { kind: BufferKind::Derived, len: 0, slots: vec![], }, - output_buffer: DenseBufferLayout { + output_buffer: BufferLayout { kind: BufferKind::Outputs, len: 1, slots: vec![BufferSlot { @@ -1281,47 +1314,47 @@ pub(crate) fn w03_minimal_outputs_execution_model() -> ExecutionModel { len: 1, }], }, - route_buffer: DenseBufferLayout { + route_buffer: BufferLayout { kind: BufferKind::Routes, len: 0, slots: vec![], }, }, - kernels: vec![ExecutionKernel { - role: KernelRole::Outputs, - signature: KernelSignature { + functions: vec![ModelFunction { + kind: ModelFunctionKind::Outputs, + signature: FunctionSignature { args: vec![ - KernelArgument { - kind: KernelArgumentKind::Time, - access: KernelAccess::Input, + FunctionArgument { + kind: FunctionArgumentKind::Time, + access: Access::Input, }, - KernelArgument { - kind: KernelArgumentKind::States, - access: KernelAccess::Input, + FunctionArgument { + kind: FunctionArgumentKind::States, + access: Access::Input, }, - KernelArgument { - kind: KernelArgumentKind::Parameters, - access: KernelAccess::Input, + FunctionArgument { + kind: FunctionArgumentKind::Parameters, + access: Access::Input, }, - KernelArgument { - kind: KernelArgumentKind::Covariates, - access: KernelAccess::Input, + FunctionArgument { + kind: FunctionArgumentKind::Covariates, + access: Access::Input, }, - KernelArgument { - kind: KernelArgumentKind::RouteInputs, - access: KernelAccess::Input, + FunctionArgument { + kind: FunctionArgumentKind::RouteInputs, + access: Access::Input, }, - KernelArgument { - kind: KernelArgumentKind::Derived, - access: KernelAccess::Input, + FunctionArgument { + kind: FunctionArgumentKind::Derived, + access: Access::Input, }, - KernelArgument { - kind: KernelArgumentKind::Outputs, - access: KernelAccess::Output, + FunctionArgument { + kind: FunctionArgumentKind::Outputs, + access: Access::Output, }, ], }, - implementation: KernelImplementation::Statements(ExecutionProgram { + body: FunctionBody::Statements(ExecutionProgram { locals: vec![], body: ExecutionBlock { statements: vec![ExecutionStmt { @@ -1332,7 +1365,7 @@ pub(crate) fn w03_minimal_outputs_execution_model() -> ExecutionModel { }, value: ExecutionExpr { kind: ExecutionExprKind::Binary { - op: TypedBinaryOp::Add, + op: AnalyzedBinaryOp::Add, lhs: Box::new(ExecutionExpr { kind: ExecutionExprKind::Load(ExecutionLoad::State( ExecutionStateRef { @@ -1349,10 +1382,10 @@ pub(crate) fn w03_minimal_outputs_execution_model() -> ExecutionModel { }), rhs: Box::new(ExecutionExpr { kind: ExecutionExprKind::Binary { - op: TypedBinaryOp::Div, + op: AnalyzedBinaryOp::Div, lhs: Box::new(ExecutionExpr { kind: ExecutionExprKind::Binary { - op: TypedBinaryOp::Add, + op: AnalyzedBinaryOp::Add, lhs: Box::new(ExecutionExpr { kind: ExecutionExprKind::Load( ExecutionLoad::Parameter(0), @@ -1434,13 +1467,13 @@ mod tests { fn direct_emitter_compiles_real_ode_corpus_model() { let source = STRUCTURED_BLOCK_CORPUS; let parsed = pharmsol_dsl::parse_module(source).expect("parse corpus source"); - let typed = pharmsol_dsl::analyze_module(&parsed).expect("analyze corpus source"); - let model = typed + let analyzed = pharmsol_dsl::analyze_module(&parsed).expect("analyze corpus source"); + let model = analyzed .models .iter() .find(|model| model.name == "one_cmt_oral_iv") .expect("ode corpus model"); - let execution = pharmsol_dsl::lower_typed_model(model).expect("lower corpus model"); + let execution = pharmsol_dsl::compile_analyzed_model(model).expect("lower corpus model"); let bytes = compile_execution_model_to_wasm_bytes(&execution, 1) .expect("emit direct ode wasm bytes"); diff --git a/src/lib.rs b/src/lib.rs index 6e36c982..815d6993 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,7 +16,7 @@ //! - [`optimize`] for optimizer-oriented workflows //! //! The DSL runtime surface is feature-gated. When you enable `dsl-core`, the -//! `pharmsol::dsl` module adds parsing, analysis, lowering, compile, and runtime +//! `pharmsol::dsl` module adds parsing, analysis, compilation, and runtime //! entrypoints for models written as DSL source text. //! //! ## Quick Start @@ -78,7 +78,7 @@ //! //! DSL work is feature-gated: //! -//! - `dsl-core`: exposes the `pharmsol::dsl` facade and frontend types +//! - `dsl-core`: exposes the `pharmsol::dsl` facade and DSL compiler types //! - `dsl-jit`: adds in-process JIT compilation //! - `dsl-aot`: adds native ahead-of-time artifact compilation //! - `dsl-aot-load`: adds native artifact loading diff --git a/src/simulator/equation/metadata.rs b/src/simulator/equation/metadata.rs index 43103e03..5204bcb0 100644 --- a/src/simulator/equation/metadata.rs +++ b/src/simulator/equation/metadata.rs @@ -30,7 +30,10 @@ //! assert!(metadata.output("cp").is_some()); //! ``` -use pharmsol_dsl::{AnalyticalKernel, CovariateInterpolation, ModelKind}; +use pharmsol_dsl::{ + AnalyticalKernel, CovariateInterpolation, ModelKind, NUMERIC_OUTPUT_PREFIX, + NUMERIC_ROUTE_PREFIX, +}; use std::fmt; use thiserror::Error; @@ -67,7 +70,7 @@ pub enum ModelMetadataError { "metadata declares {declared} particle(s) but validation provided {fallback} fallback particle(s)" )] ParticleCountConflict { declared: usize, fallback: usize }, - #[error("{kind:?} metadata cannot declare an analytical kernel")] + #[error("{kind:?} metadata cannot declare an analytical function")] AnalyticalKernelNotAllowed { kind: ModelKind }, } @@ -103,7 +106,7 @@ impl fmt::Display for NameDomain { /// Route lookups expose two different indices: /// - [`ValidatedModelMetadata::route_declaration_index`] is the route position in /// declaration order. -/// - [`ValidatedModelMetadata::route_index`] is the dense execution input index +/// - [`ValidatedRoute::input_index`] is the dense execution input index /// for that route kind. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ValidatedModelMetadata { @@ -206,6 +209,41 @@ impl ValidatedModelMetadata { self.outputs.iter().position(|output| output.name() == name) } + /// Resolve a public route label from data to a declared route. + /// + /// Resolution order: + /// 1. exact route name, + /// 2. canonical numeric alias (`input_`) for a bare numeric label, + /// matching Pmetrics `INPUT` numbering. + /// + /// A bare numeric label never falls back to a declaration position, so + /// `"10"` does not resolve unless an `input_10` route is declared. + /// + /// Use [`ValidatedRoute::input_index`] on the result for the dense + /// execution input slot. + pub fn route_for_label(&self, label: &str) -> Option<&ValidatedRoute> { + self.route(label).or_else(|| { + if !is_bare_numeric_label(label) { + return None; + } + self.route(&format!("{NUMERIC_ROUTE_PREFIX}{label}")) + }) + } + + /// Resolve a public output label from data to its dense output index. + /// + /// Uses the same resolution order as [`Self::route_for_label`]: exact + /// output name, then the `outeq_` alias matching Pmetrics `OUTEQ` + /// numbering, with no positional fallback. + pub fn output_for_label(&self, label: &str) -> Option { + self.output_index(label).or_else(|| { + if !is_bare_numeric_label(label) { + return None; + } + self.output_index(&format!("{NUMERIC_OUTPUT_PREFIX}{label}")) + }) + } + pub fn parameter(&self, name: &str) -> Option<&Parameter> { self.parameter_index(name) .map(|index| &self.parameters[index]) @@ -392,7 +430,7 @@ impl ModelMetadata { self } - /// Set the analytical kernel identity for built-in analytical models. + /// Set the analytical function identity for built-in analytical models. pub fn analytical_kernel(mut self, analytical: AnalyticalKernel) -> Self { self.analytical = Some(analytical); self @@ -438,7 +476,7 @@ impl ModelMetadata { self.particles } - /// Get the declared analytical kernel identity. + /// Get the declared analytical function identity. pub fn analytical_kernel_decl(&self) -> Option { self.analytical } @@ -733,6 +771,11 @@ impl Route { } } +/// Returns `true` for labels consisting only of ASCII digits (`"0"`, `"12"`). +fn is_bare_numeric_label(label: &str) -> bool { + !label.is_empty() && label.chars().all(|ch| ch.is_ascii_digit()) +} + fn resolve_kind( declared_kind: Option, requested_kind: Option, @@ -1018,6 +1061,48 @@ mod tests { ); } + #[test] + fn numeric_labels_resolve_via_canonical_alias_only() { + let metadata = new("mixed_labels") + .kind(ModelKind::Ode) + .parameters(["ke", "v"]) + .states(["gut", "central"]) + .outputs(["cp", "outeq_0", "outeq_1"]) + .route(Route::infusion("iv").to_state("central")) + .route(Route::bolus("input_0").to_state("gut")) + .validate() + .expect("metadata should validate"); + + // Exact names resolve first. + assert_eq!(metadata.output_for_label("cp"), Some(0)); + assert_eq!( + metadata + .route_for_label("iv") + .map(|route| route.declaration_index()), + Some(0) + ); + + // Bare numeric labels resolve through the `outeq_`/`input_` + // aliases, including `0`. + assert_eq!(metadata.output_for_label("0"), Some(1)); + assert_eq!(metadata.output_for_label("1"), Some(2)); + assert_eq!( + metadata + .route_for_label("0") + .map(|route| route.declaration_index()), + Some(1) + ); + + // Without a declared alias, a bare numeric label never falls back to + // a declaration position. + assert_eq!(metadata.output_for_label("2"), None); + assert_eq!(metadata.output_for_label("3"), None); + assert!(metadata.route_for_label("1").is_none()); + assert!(metadata.route_for_label("2").is_none()); + assert_eq!(metadata.output_for_label("missing"), None); + assert!(metadata.route_for_label("missing").is_none()); + } + #[test] fn duplicate_names_fail_validation() { let error = new("dup_params") @@ -1231,7 +1316,7 @@ mod tests { #[test] fn analytical_kernel_is_limited_to_analytical_models() { - let error = new("ode_kernel") + let error = new("ode_function") .kind(ModelKind::Ode) .parameters(["ke"]) .states(["central"]) @@ -1239,7 +1324,7 @@ mod tests { .route(Route::infusion("iv").to_state("central")) .analytical_kernel(AnalyticalKernel::OneCompartment) .validate() - .expect_err("ODE metadata cannot declare an analytical kernel"); + .expect_err("ODE metadata cannot declare an analytical function"); assert_eq!( error, diff --git a/src/simulator/equation/mod.rs b/src/simulator/equation/mod.rs index 875f7c62..974eb89c 100644 --- a/src/simulator/equation/mod.rs +++ b/src/simulator/equation/mod.rs @@ -55,7 +55,6 @@ pub use analytical::*; pub use metadata::*; pub use ode::*; pub use pharmsol_dsl::{AnalyticalKernel, ModelKind}; -use pharmsol_dsl::{NUMERIC_OUTPUT_PREFIX, NUMERIC_ROUTE_PREFIX}; pub use sde::*; use crate::{ @@ -199,15 +198,9 @@ pub(crate) trait EquationPriv: EquationTypes { expected_kind: RouteKind, ) -> Result { if let Some(metadata) = self.metadata() { - let route = metadata - .route(label.as_str()) - .or_else(|| { - canonical_numeric_alias(label.as_str(), NUMERIC_ROUTE_PREFIX) - .and_then(|alias| metadata.route(alias.as_str())) - }) - .ok_or_else(|| { - PharmsolError::unknown_input_label(label.as_str(), &metadata.route_labels()) - })?; + let route = metadata.route_for_label(label.as_str()).ok_or_else(|| { + PharmsolError::unknown_input_label(label.as_str(), &metadata.route_labels()) + })?; if route.kind() != expected_kind { return Err(PharmsolError::UnsupportedInputRouteKind { @@ -229,15 +222,9 @@ pub(crate) trait EquationPriv: EquationTypes { fn resolve_output_label(&self, label: &OutputLabel) -> Result { if let Some(metadata) = self.metadata() { - return metadata - .output_index(label.as_str()) - .or_else(|| { - canonical_numeric_alias(label.as_str(), NUMERIC_OUTPUT_PREFIX) - .and_then(|alias| metadata.output_index(alias.as_str())) - }) - .ok_or_else(|| { - PharmsolError::unknown_output_label(label.as_str(), &metadata.output_labels()) - }); + return metadata.output_for_label(label.as_str()).ok_or_else(|| { + PharmsolError::unknown_output_label(label.as_str(), &metadata.output_labels()) + }); } label @@ -359,13 +346,6 @@ pub(crate) trait EquationPriv: EquationTypes { } } -fn canonical_numeric_alias(label: &str, prefix: &str) -> Option { - if label.is_empty() || !label.chars().all(|ch| ch.is_ascii_digit()) { - return None; - } - Some(format!("{prefix}{label}")) -} - /// Trait for handwritten model equations that can be simulated. /// /// [`Equation`] is the shared interface implemented by handwritten [`ODE`], diff --git a/src/simulator/equation/ode/mod.rs b/src/simulator/equation/ode/mod.rs index 499897eb..cff749f5 100644 --- a/src/simulator/equation/ode/mod.rs +++ b/src/simulator/equation/ode/mod.rs @@ -787,7 +787,7 @@ mod tests { .build() } - fn explicit_route_kernel( + fn explicit_route_function( _x: &V, _p: &V, _t: f64, @@ -799,7 +799,7 @@ mod tests { dx[0] = b[0] + rateiv[0]; } - fn injected_route_kernel( + fn injected_route_function( _x: &V, _p: &V, _t: f64, @@ -825,7 +825,7 @@ mod tests { y[0] = x[0]; } - fn counting_kernel( + fn counting_function( _x: &V, _p: &V, _t: f64, @@ -911,7 +911,7 @@ mod tests { #[test] fn handwritten_ode_defaults_to_explicit_route_vectors() { let ode = ODE::new( - explicit_route_kernel, + explicit_route_function, zero_lag, unit_fa, zero_init, @@ -955,7 +955,7 @@ mod tests { #[test] fn handwritten_ode_metadata_input_policy_is_descriptive_only() { let ode = ODE::new( - injected_route_kernel, + injected_route_function, zero_lag, unit_fa, zero_init, @@ -994,7 +994,7 @@ mod tests { #[test] fn handwritten_ode_metadata_resolves_raw_numeric_aliases_against_canonical_labels() { let ode = ODE::new( - explicit_route_kernel, + explicit_route_function, zero_lag, unit_fa, zero_init, @@ -1055,10 +1055,16 @@ mod tests { fn handwritten_ode_estimate_predictions_uses_prediction_cache() { PREDICTION_CACHE_DIFFEQ_CALLS.store(0, Ordering::SeqCst); - let ode = ODE::new(counting_kernel, zero_lag, unit_fa, zero_init, state_output) - .with_nstates(1) - .with_ndrugs(1) - .with_nout(1); + let ode = ODE::new( + counting_function, + zero_lag, + unit_fa, + zero_init, + state_output, + ) + .with_nstates(1) + .with_ndrugs(1) + .with_nout(1); let subject = Subject::builder("cached_predictions") .bolus(0.0, 100.0, 0) .observation(1.0, 0.0, 0) diff --git a/tests/authoring_parity_corpus.rs b/tests/authoring_parity_corpus.rs index 7b0fd634..8877f812 100644 --- a/tests/authoring_parity_corpus.rs +++ b/tests/authoring_parity_corpus.rs @@ -11,7 +11,7 @@ use pharmsol::prelude::*; #[cfg(feature = "dsl-jit")] use pharmsol::Predictions; use pharmsol_dsl::{ - analyze_model, lower_typed_model, parse_model, CovariateInterpolation, ExecutionModel, + analyze_model, compile_analyzed_model, parse_model, CovariateInterpolation, ExecutionModel, ModelKind, RouteKind as DslRouteKind, }; @@ -376,8 +376,8 @@ impl RouteKindParity { fn load_execution_model(src: &str) -> ExecutionModel { let parsed = parse_model(src).expect("DSL model should parse"); - let typed = analyze_model(&parsed).expect("DSL model should analyze"); - lower_typed_model(&typed).expect("DSL model should lower") + let analyzed = analyze_model(&parsed).expect("DSL model should analyze"); + compile_analyzed_model(&analyzed).expect("DSL model should compile") } #[cfg(feature = "dsl-jit")] @@ -487,7 +487,7 @@ fn dsl_metadata_view(src: &str) -> MetadataParityView { parameters, covariates, states, - route_input_count: model.abi.route_buffer.len, + route_input_count: model.layout.route_buffer.len, routes, outputs, analytical_kernel: model.metadata.analytical, @@ -1397,8 +1397,9 @@ fn route_input_policy_mismatches_are_detected_explicitly() { fn invalid_dsl_infusion_route_properties_fail_explicitly() { let model = parse_model(ODE_INVALID_INFUSION_LAG_DSL).expect("invalid DSL fixture should parse"); - let typed = analyze_model(&model).expect("invalid DSL fixture should analyze"); - let error = lower_typed_model(&typed).expect_err("infusion lag should fail during lowering"); + let analyzed = analyze_model(&model).expect("invalid DSL fixture should analyze"); + let error = + compile_analyzed_model(&analyzed).expect_err("infusion lag should fail during lowering"); assert!(error .to_string() diff --git a/tests/browser-e2e/run.mjs b/tests/browser-e2e/run.mjs index 4e6c7a88..a2e1d656 100644 --- a/tests/browser-e2e/run.mjs +++ b/tests/browser-e2e/run.mjs @@ -155,7 +155,7 @@ async function main() { JSON.stringify(invalid), ); assert.equal(invalid.diagnosticReport.source.name, "inline.dsl"); - assert.equal(invalid.diagnosticReport.diagnostics[0].phase, "semantic"); + assert.equal(invalid.diagnosticReport.diagnostics[0].phase, "analysis"); assert.equal(invalid.diagnosticReport.diagnostics[0].code, "DSL2000"); assert.equal(invalid.cacheEntries, 1, JSON.stringify(invalid)); assert.ok( diff --git a/tests/support/runtime_corpus.rs b/tests/support/runtime_corpus.rs index 55e61b9f..924432ce 100644 --- a/tests/support/runtime_corpus.rs +++ b/tests/support/runtime_corpus.rs @@ -485,13 +485,13 @@ pub fn compile_wasm_runtime_from_bytes( case: CorpusCase, ) -> Result> { let parsed = dsl::parse_module(case.source())?; - let typed = dsl::analyze_module(&parsed)?; - let model = typed + let analyzed = dsl::analyze_module(&parsed)?; + let model = analyzed .models .iter() .find(|model| model.name == case.model_name()) .ok_or_else(|| io::Error::other(format!("{}: missing model in source", case.label())))?; - let execution = dsl::lower_typed_model(model)?; + let execution = dsl::compile_analyzed_model(model)?; let bytes = dsl::compile_execution_model_to_wasm_bytes(&execution)?; Ok(adjust_runtime_model( case,