1use std::collections::{BTreeMap, BTreeSet};
2use std::fmt;
3use std::sync::Arc;
4
5use crate::ast as syntax;
6use crate::diagnostic::{
7 Applicability, Diagnostic, DiagnosticPhase, DiagnosticReport, DiagnosticSuggestion, Span,
8 TextEdit, DSL_SEMANTIC_GENERIC,
9};
10use crate::ir::*;
11use crate::name_match::{
12 common_prefix_len, edit_distance, is_high_confidence_match, is_single_adjacent_transposition,
13};
14use crate::{ModelKind, NUMERIC_OUTPUT_PREFIX, NUMERIC_ROUTE_PREFIX, RATE_FUNCTION_NAME};
15
16const RESERVED_NAMES: &[&str] = &[
17 "abs",
18 "bioavailability",
19 "carry_forward",
20 "ceil",
21 "ddt",
22 "exp",
23 "floor",
24 "lag",
25 "linear",
26 "ln",
27 "locf",
28 "log",
29 "log10",
30 "log2",
31 "max",
32 "min",
33 "noise",
34 "pow",
35 RATE_FUNCTION_NAME,
36 "round",
37 "sin",
38 "cos",
39 "tan",
40 "sqrt",
41];
42
43#[derive(Default)]
44struct SemanticAssist {
45 context_labels: Vec<(Span, String)>,
46 secondary_labels: Vec<(Span, String)>,
47 helps: Vec<String>,
48 suggestions: Vec<DiagnosticSuggestion>,
49}
50
51impl SemanticAssist {
52 fn context_label(mut self, span: Span, message: impl Into<String>) -> Self {
53 self.context_labels.push((span, message.into()));
54 self
55 }
56
57 fn help(mut self, help: impl Into<String>) -> Self {
58 self.helps.push(help.into());
59 self
60 }
61
62 fn replacement_suggestion(
63 mut self,
64 span: Span,
65 replacement: impl Into<String>,
66 message: impl Into<String>,
67 applicability: Applicability,
68 ) -> Self {
69 self.suggestions.push(DiagnosticSuggestion {
70 message: message.into(),
71 edits: vec![TextEdit {
72 span,
73 replacement: replacement.into(),
74 }],
75 applicability,
76 });
77 self
78 }
79
80 fn apply(self, mut error: SemanticError) -> SemanticError {
81 for (span, message) in self.context_labels {
82 error = error.with_context_label(span, message);
83 }
84 for (span, message) in self.secondary_labels {
85 error = error.with_secondary_label(span, message);
86 }
87 for help in self.helps {
88 error = error.with_help(help);
89 }
90 for suggestion in self.suggestions {
91 error = error.with_suggestion(suggestion);
92 }
93 error
94 }
95}
96
97struct SimilarNameCandidate {
98 lookup_name: String,
99 assist: SemanticAssist,
100}
101
102impl SimilarNameCandidate {
103 fn new(lookup_name: impl Into<String>, assist: SemanticAssist) -> Self {
104 Self {
105 lookup_name: lookup_name.into(),
106 assist,
107 }
108 }
109}
110
111pub fn analyze_module(module: &syntax::Module) -> Result<TypedModule, SemanticError> {
112 let mut models = Vec::with_capacity(module.models.len());
113 for model in &module.models {
114 models.push(analyze_model(model)?);
115 }
116 Ok(TypedModule {
117 models,
118 span: module.span,
119 })
120}
121
122pub fn analyze_model(model: &syntax::Model) -> Result<TypedModel, SemanticError> {
123 Analyzer::new(model).analyze()
124}
125
126#[derive(Clone, PartialEq, Eq)]
127pub struct SemanticError {
128 diagnostic: Box<Diagnostic>,
129 source: Option<Arc<str>>,
130}
131
132impl SemanticError {
133 pub fn new(message: impl Into<String>, span: Span) -> Self {
134 Self {
135 diagnostic: Box::new(Diagnostic::error(
136 DSL_SEMANTIC_GENERIC,
137 DiagnosticPhase::Semantic,
138 message,
139 span,
140 )),
141 source: None,
142 }
143 }
144
145 pub fn with_note(mut self, note: impl Into<String>) -> Self {
146 self.diagnostic.notes.push(note.into());
147 self
148 }
149
150 pub fn with_help(mut self, help: impl Into<String>) -> Self {
151 self.diagnostic.helps.push(help.into());
152 self
153 }
154
155 pub fn with_secondary_label(mut self, span: Span, message: impl Into<String>) -> Self {
156 self.diagnostic = Box::new(self.diagnostic.with_secondary_label(span, message));
157 self
158 }
159
160 pub fn with_context_label(mut self, span: Span, message: impl Into<String>) -> Self {
161 self.diagnostic = Box::new(self.diagnostic.with_context_label(span, message));
162 self
163 }
164
165 pub fn with_suggestion(mut self, suggestion: DiagnosticSuggestion) -> Self {
166 self.diagnostic = Box::new(self.diagnostic.with_suggestion(suggestion));
167 self
168 }
169
170 pub fn diagnostic(&self) -> &Diagnostic {
171 self.diagnostic.as_ref()
172 }
173
174 pub fn into_diagnostic(self) -> Diagnostic {
175 *self.diagnostic
176 }
177
178 pub fn render(&self, src: &str) -> String {
179 self.diagnostic.render(src)
180 }
181
182 pub fn diagnostic_report(&self, source_name: impl Into<String>) -> DiagnosticReport {
183 DiagnosticReport::from_diagnostics(
184 source_name,
185 self.source(),
186 std::slice::from_ref(self.diagnostic.as_ref()),
187 )
188 }
189
190 pub fn with_source(mut self, source: impl Into<Arc<str>>) -> Self {
191 self.source = Some(source.into());
192 self
193 }
194
195 pub fn source(&self) -> Option<&str> {
196 self.source.as_deref()
197 }
198}
199
200impl fmt::Debug for SemanticError {
201 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202 fmt::Display::fmt(self, f)
203 }
204}
205
206impl fmt::Display for SemanticError {
207 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
208 if let Some(source) = self.source() {
209 return f.write_str(&self.render(source));
210 }
211 let span = self.diagnostic.primary_span();
212 write!(
213 f,
214 "{} at bytes {}..{}",
215 self.diagnostic.message, span.start, span.end
216 )
217 }
218}
219
220impl std::error::Error for SemanticError {}
221
222struct Analyzer<'a> {
223 model: &'a syntax::Model,
224 symbols: Vec<PendingSymbol>,
225 globals: Globals,
226}
227
228impl<'a> Analyzer<'a> {
229 fn new(model: &'a syntax::Model) -> Self {
230 Self {
231 model,
232 symbols: Vec::new(),
233 globals: Globals::default(),
234 }
235 }
236
237 fn analyze(mut self) -> Result<TypedModel, SemanticError> {
238 let sections = ModelSections::from_model(self.model)?;
239
240 let parameters = self.register_parameters(sections.parameters)?;
241 let constants = self.resolve_and_register_constants(sections.constants)?;
242 let covariates = self.register_covariates(sections.covariates)?;
243 let states = self.register_states(sections.states)?;
244 let routes = self.register_routes(sections.routes)?;
245
246 let derived = self.register_implicit_symbols(
247 sections.derive.map(|block| block.statements.as_slice()),
248 SymbolKind::Derived,
249 )?;
250 let outputs = self.register_implicit_symbols(
251 Some(
252 §ions
253 .outputs
254 .ok_or_else(|| {
255 SemanticError::new(
256 format!(
257 "model `{}` is missing an `outputs` block",
258 self.model.name.text
259 ),
260 self.model.span,
261 )
262 })?
263 .statements,
264 ),
265 SymbolKind::Output,
266 )?;
267
268 self.validate_kind_requirements(§ions, &states)?;
269
270 let derive_result = if let Some(block) = sections.derive {
271 Some(self.analyze_statement_block(block, BlockContext::Derive, BTreeSet::new())?)
272 } else {
273 None
274 };
275 let available_derived = derive_result
276 .as_ref()
277 .map(|result| result.available_derived.clone())
278 .unwrap_or_default();
279
280 let dynamics = if let Some(block) = sections.dynamics {
281 Some(self.analyze_statement_block(
282 block,
283 BlockContext::Dynamics,
284 available_derived.clone(),
285 )?)
286 } else {
287 None
288 };
289 let init = if let Some(block) = sections.init {
290 Some(self.analyze_statement_block(
291 block,
292 BlockContext::Init,
293 available_derived.clone(),
294 )?)
295 } else {
296 None
297 };
298 let drift = if let Some(block) = sections.drift {
299 Some(self.analyze_statement_block(
300 block,
301 BlockContext::Drift,
302 available_derived.clone(),
303 )?)
304 } else {
305 None
306 };
307 let diffusion = if let Some(block) = sections.diffusion {
308 Some(self.analyze_statement_block(
309 block,
310 BlockContext::Diffusion,
311 available_derived.clone(),
312 )?)
313 } else {
314 None
315 };
316 let outputs_block = self.analyze_statement_block(
317 sections.outputs.expect("outputs checked above"),
318 BlockContext::Outputs,
319 available_derived,
320 )?;
321
322 self.validate_kind_blocks(
323 self.model.kind,
324 ModelKindBlocks {
325 dynamics: dynamics.as_ref(),
326 drift: drift.as_ref(),
327 diffusion: diffusion.as_ref(),
328 analytical: sections.analytical,
329 particles: sections.particles,
330 },
331 &states,
332 )?;
333
334 self.validate_output_assignments(&outputs, &outputs_block)?;
335 if let Some(result) = &dynamics {
336 self.validate_state_coverage(result, &states, "dynamics")?;
337 }
338 if let Some(result) = &drift {
339 self.validate_state_coverage(result, &states, "drift")?;
340 }
341
342 let particles = if let Some(decl) = sections.particles {
343 Some(self.expect_const_usize(&decl.value, "particles", true)?)
344 } else {
345 None
346 };
347
348 let analytical = if let Some(block) = sections.analytical {
349 let structure =
350 AnalyticalKernel::from_name(&block.structure.text).ok_or_else(|| {
351 SemanticError::new(
352 format!("unknown analytical structure `{}`", block.structure.text),
353 block.structure.span,
354 )
355 })?;
356 let state_components = states
357 .iter()
358 .map(|state| state.size.unwrap_or(1))
359 .sum::<usize>();
360 if state_components != structure.state_count() {
361 return Err(SemanticError::new(
362 format!(
363 "analytical structure `{}` expects {} state value(s), but model declares {}",
364 block.structure.text,
365 structure.state_count(),
366 state_components
367 ),
368 block.structure.span,
369 ));
370 }
371 self.validate_analytical_structure_inputs(
372 structure,
373 block.structure.span,
374 ¶meters,
375 &derived,
376 derive_result.as_ref(),
377 )?;
378 Some(TypedAnalytical {
379 structure,
380 span: block.span,
381 })
382 } else {
383 None
384 };
385
386 let model_name = self.model.name.text.clone();
387 let model_kind = self.model.kind;
388 let model_span = self.model.span;
389 let symbols = self.finalize_symbols()?;
390 Ok(TypedModel {
391 name: model_name,
392 kind: model_kind,
393 symbols,
394 parameters,
395 constants,
396 covariates,
397 states,
398 routes,
399 derived,
400 outputs,
401 particles,
402 analytical,
403 derive: derive_result.map(|result| result.block),
404 dynamics: dynamics.map(|result| result.block),
405 outputs_block: outputs_block.block,
406 init: init.map(|result| result.block),
407 drift: drift.map(|result| result.block),
408 diffusion: diffusion.map(|result| result.block),
409 span: model_span,
410 })
411 }
412
413 fn register_parameters(
414 &mut self,
415 block: Option<&syntax::ParametersBlock>,
416 ) -> Result<Vec<SymbolId>, SemanticError> {
417 let mut parameters = Vec::new();
418 if let Some(block) = block {
419 for ident in &block.items {
420 let id = self.insert_global_symbol(
421 &ident.text,
422 SymbolKind::Parameter,
423 PendingSymbolType::Scalar(Some(ValueType::Real)),
424 ident.span,
425 )?;
426 self.globals.parameters.insert(ident.text.clone(), id);
427 parameters.push(id);
428 }
429 }
430 Ok(parameters)
431 }
432
433 fn resolve_and_register_constants(
434 &mut self,
435 block: Option<&syntax::ConstantsBlock>,
436 ) -> Result<Vec<TypedConstant>, SemanticError> {
437 let Some(block) = block else {
438 return Ok(Vec::new());
439 };
440
441 let mut bindings = BTreeMap::new();
442 for binding in &block.items {
443 if let Some(existing) = bindings.insert(binding.name.text.clone(), binding) {
444 return Err(SemanticAssist::default()
445 .context_label(
446 existing.name.span,
447 format!("constant `{}` first declared here", binding.name.text),
448 )
449 .help(format!(
450 "rename this constant to a unique name such as `{}_2`",
451 binding.name.text
452 ))
453 .replacement_suggestion(
454 binding.name.span,
455 format!("{}_2", binding.name.text),
456 format!("rename this constant to `{}_2`", binding.name.text),
457 Applicability::MaybeIncorrect,
458 )
459 .apply(SemanticError::new(
460 format!("duplicate constant `{}`", binding.name.text),
461 binding.name.span,
462 )));
463 }
464 }
465
466 let mut visiting = BTreeSet::new();
467 let mut typed = Vec::new();
468 for binding in &block.items {
469 let value = self.evaluate_const_expr(&binding.value, &bindings, &mut visiting)?;
470 let id = self.insert_global_symbol(
471 &binding.name.text,
472 SymbolKind::Constant,
473 PendingSymbolType::Scalar(Some(value.value_type())),
474 binding.name.span,
475 )?;
476 self.globals.constants.insert(binding.name.text.clone(), id);
477 self.globals
478 .constant_values
479 .insert(binding.name.text.clone(), value.clone());
480 typed.push(TypedConstant {
481 symbol: id,
482 value,
483 span: binding.span,
484 });
485 }
486 Ok(typed)
487 }
488
489 fn register_covariates(
490 &mut self,
491 block: Option<&syntax::CovariatesBlock>,
492 ) -> Result<Vec<TypedCovariate>, SemanticError> {
493 let mut covariates = Vec::new();
494 if let Some(block) = block {
495 for covariate in &block.items {
496 let interpolation = match covariate
497 .interpolation
498 .as_ref()
499 .map(|value| value.text.as_str())
500 {
501 None => None,
502 Some("linear") => Some(CovariateInterpolation::Linear),
503 Some("locf") | Some("carry_forward") => Some(CovariateInterpolation::Locf),
504 Some(other) => {
505 return Err(SemanticError::new(
506 format!("unknown covariate interpolation `{other}`"),
507 covariate.interpolation.as_ref().unwrap().span,
508 )
509 .with_note("supported interpolation names are `linear`, `locf`, and `carry_forward`"));
510 }
511 };
512 let id = self.insert_global_symbol(
513 &covariate.name.text,
514 SymbolKind::Covariate,
515 PendingSymbolType::Scalar(Some(ValueType::Real)),
516 covariate.name.span,
517 )?;
518 self.globals
519 .covariates
520 .insert(covariate.name.text.clone(), id);
521 covariates.push(TypedCovariate {
522 symbol: id,
523 interpolation,
524 span: covariate.span,
525 });
526 }
527 }
528 Ok(covariates)
529 }
530
531 fn register_states(
532 &mut self,
533 block: Option<&syntax::StatesBlock>,
534 ) -> Result<Vec<TypedState>, SemanticError> {
535 let Some(block) = block else {
536 return Err(SemanticError::new(
537 format!(
538 "model `{}` is missing a `states` block",
539 self.model.name.text
540 ),
541 self.model.span,
542 ));
543 };
544
545 let mut states = Vec::new();
546 for state in &block.items {
547 let size = match &state.size {
548 Some(expr) => Some(self.expect_const_usize(expr, "state array size", true)?),
549 None => None,
550 };
551 let pending_type = match size {
552 Some(size) => PendingSymbolType::Array {
553 element: ValueType::Real,
554 size,
555 },
556 None => PendingSymbolType::Scalar(Some(ValueType::Real)),
557 };
558 let id = self.insert_global_symbol(
559 &state.name.text,
560 SymbolKind::State,
561 pending_type,
562 state.name.span,
563 )?;
564 self.globals
565 .states
566 .insert(state.name.text.clone(), StateEntry { symbol: id, size });
567 states.push(TypedState {
568 symbol: id,
569 size,
570 span: state.span,
571 });
572 }
573 Ok(states)
574 }
575
576 fn register_routes(
577 &mut self,
578 block: Option<&syntax::RoutesBlock>,
579 ) -> Result<Vec<TypedRoute>, SemanticError> {
580 let mut routes = Vec::new();
581 if let Some(block) = block {
582 for route in &block.routes {
583 self.validate_route_label_name(&route.input)?;
584 let id = self.insert_global_symbol(
585 &route.input.text,
586 SymbolKind::Route,
587 PendingSymbolType::Route,
588 route.input.span,
589 )?;
590 self.globals.routes.insert(route.input.text.clone(), id);
591 let destination = self.analyze_state_place_const(&route.destination)?;
592 let mut seen_props = BTreeMap::new();
593 let mut properties = Vec::new();
594 for property in &route.properties {
595 let kind = match property.name.text.as_str() {
596 "lag" => RoutePropertyKind::Lag,
597 "bioavailability" => RoutePropertyKind::Bioavailability,
598 other => {
599 return Err(SemanticError::new(
600 format!("unknown route property `{other}`"),
601 property.name.span,
602 )
603 .with_note(
604 "supported route properties are `lag` and `bioavailability`",
605 ));
606 }
607 };
608 if let Some(existing_span) = seen_props.insert(kind, property.name.span) {
609 return Err(SemanticAssist::default()
610 .context_label(
611 existing_span,
612 format!(
613 "route property `{}` first declared here",
614 property.name.text
615 ),
616 )
617 .help(format!(
618 "each route can declare `{}` at most once",
619 property.name.text
620 ))
621 .apply(SemanticError::new(
622 format!("duplicate route property `{}`", property.name.text),
623 property.name.span,
624 )));
625 }
626 let env = BlockEnv::new(BTreeSet::new());
627 let value = self.analyze_expr(&property.value, &env)?;
628 self.expect_numeric(&value, "route property", property.value.span)?;
629 properties.push(TypedRouteProperty {
630 kind,
631 value,
632 span: property.span,
633 });
634 }
635 routes.push(TypedRoute {
636 symbol: id,
637 kind: route.kind,
638 destination,
639 properties,
640 span: route.span,
641 });
642 }
643 }
644 Ok(routes)
645 }
646
647 fn register_implicit_symbols(
648 &mut self,
649 statements: Option<&[syntax::Stmt]>,
650 kind: SymbolKind,
651 ) -> Result<Vec<SymbolId>, SemanticError> {
652 let mut collected_idents = Vec::new();
653 let Some(statements) = statements else {
654 return Ok(Vec::new());
655 };
656
657 let mut seen = BTreeSet::new();
658 collect_bare_assignment_names(statements, &mut seen, &mut collected_idents);
659 let mut symbols = Vec::new();
660 for ident in collected_idents {
661 if matches!(kind, SymbolKind::Output) {
662 self.validate_output_label_name(&ident)?;
663 }
664 if matches!(kind, SymbolKind::Derived) {
665 if let Some(parameter) = self.globals.parameters.get(&ident.text).copied() {
666 return Err(SemanticAssist::default()
667 .context_label(
668 self.symbol_span(parameter),
669 self.symbol_declared_here(parameter),
670 )
671 .help(
672 "names declared in `params` and derive-assigned names must be distinct",
673 )
674 .replacement_suggestion(
675 ident.span,
676 format!("{}_derived", ident.text),
677 format!("rename this derive target to `{}_derived`", ident.text),
678 Applicability::MaybeIncorrect,
679 )
680 .apply(SemanticError::new(
681 format!(
682 "derived name `{}` collides with primary parameter `{}`",
683 ident.text, ident.text
684 ),
685 ident.span,
686 )));
687 }
688 }
689 let id = self.insert_global_symbol(
690 &ident.text,
691 kind,
692 PendingSymbolType::Scalar(None),
693 ident.span,
694 )?;
695 match kind {
696 SymbolKind::Derived => {
697 self.globals.derived.insert(ident.text.clone(), id);
698 }
699 SymbolKind::Output => {
700 self.globals.outputs.insert(ident.text.clone(), id);
701 }
702 _ => unreachable!(),
703 }
704 symbols.push(id);
705 }
706 Ok(symbols)
707 }
708
709 fn analyze_statement_block(
710 &mut self,
711 block: &syntax::StatementBlock,
712 context: BlockContext,
713 available_derived: BTreeSet<SymbolId>,
714 ) -> Result<BlockAnalysis, SemanticError> {
715 let env = BlockEnv::new(available_derived);
716 let (statements, env, touched_states) =
717 self.analyze_stmt_list(&block.statements, context, env)?;
718 Ok(BlockAnalysis {
719 block: TypedStatementBlock {
720 context,
721 statements,
722 span: block.span,
723 },
724 available_derived: env.available_derived,
725 definite_targets: env.definite_targets,
726 touched_states,
727 })
728 }
729
730 fn analyze_stmt_list(
731 &mut self,
732 statements: &[syntax::Stmt],
733 context: BlockContext,
734 mut env: BlockEnv,
735 ) -> Result<(Vec<TypedStmt>, BlockEnv, BTreeSet<SymbolId>), SemanticError> {
736 let mut typed = Vec::with_capacity(statements.len());
737 let mut touched_states = BTreeSet::new();
738
739 for stmt in statements {
740 match &stmt.kind {
741 syntax::StmtKind::Let(let_stmt) => {
742 let value = self.analyze_expr(&let_stmt.value, &env)?;
743 let symbol = self.insert_local_symbol(
744 &mut env,
745 &let_stmt.name,
746 value.ty,
747 SymbolKind::Local,
748 )?;
749 typed.push(TypedStmt {
750 kind: TypedStmtKind::Let(TypedLetStmt { symbol, value }),
751 span: stmt.span,
752 });
753 }
754 syntax::StmtKind::Assign(assign) => {
755 let target = self.analyze_assign_target(&assign.target, context, &env)?;
756 let value = self.analyze_expr(&assign.value, &env)?;
757 self.expect_numeric(&value, "assignment value", assign.value.span)?;
758 match &target.kind {
759 TypedAssignTargetKind::Derived(symbol) => {
760 self.merge_symbol_type(*symbol, value.ty, assign.value.span)?;
761 env.available_derived.insert(*symbol);
762 env.definite_targets.insert(*symbol);
763 }
764 TypedAssignTargetKind::Output(symbol) => {
765 self.merge_symbol_type(*symbol, value.ty, assign.value.span)?;
766 env.definite_targets.insert(*symbol);
767 }
768 TypedAssignTargetKind::StateInit(place)
769 | TypedAssignTargetKind::Derivative(place)
770 | TypedAssignTargetKind::Noise(place) => {
771 touched_states.insert(place.state);
772 }
773 }
774 typed.push(TypedStmt {
775 kind: TypedStmtKind::Assign(TypedAssignStmt { target, value }),
776 span: stmt.span,
777 });
778 }
779 syntax::StmtKind::If(if_stmt) => {
780 let condition = self.analyze_expr(&if_stmt.condition, &env)?;
781 self.expect_bool(&condition, "if condition", if_stmt.condition.span)?;
782
783 let then_env = env.child_scope();
784 let (then_branch, then_env, then_states) =
785 self.analyze_stmt_list(&if_stmt.then_branch, context, then_env)?;
786 let mut branch_states = then_states;
787
788 let (else_branch, next_available, next_targets) = if let Some(else_branch) =
789 &if_stmt.else_branch
790 {
791 let else_env = env.child_scope();
792 let (else_typed, else_env, else_states) =
793 self.analyze_stmt_list(else_branch, context, else_env)?;
794 branch_states.extend(else_states);
795 let available = if context == BlockContext::Derive {
796 intersect_sets(&then_env.available_derived, &else_env.available_derived)
797 } else {
798 env.available_derived.clone()
799 };
800 let targets =
801 if matches!(context, BlockContext::Derive | BlockContext::Outputs) {
802 intersect_sets(
803 &then_env.definite_targets,
804 &else_env.definite_targets,
805 )
806 } else {
807 env.definite_targets.clone()
808 };
809 (Some(else_typed), available, targets)
810 } else {
811 let available = env.available_derived.clone();
812 let targets = env.definite_targets.clone();
813 (None, available, targets)
814 };
815
816 env.available_derived = next_available;
817 env.definite_targets = next_targets;
818 touched_states.extend(branch_states);
819 typed.push(TypedStmt {
820 kind: TypedStmtKind::If(TypedIfStmt {
821 condition,
822 then_branch,
823 else_branch,
824 }),
825 span: stmt.span,
826 });
827 }
828 syntax::StmtKind::For(for_stmt) => {
829 let start = self.analyze_expr(&for_stmt.range.start, &env)?;
830 let end = self.analyze_expr(&for_stmt.range.end, &env)?;
831 self.expect_int(&start, "for-loop range start", for_stmt.range.start.span)?;
832 self.expect_int(&end, "for-loop range end", for_stmt.range.end.span)?;
833
834 let mut loop_env = env.child_scope();
835 let binding = self.insert_local_symbol(
836 &mut loop_env,
837 &for_stmt.binding,
838 ValueType::Int,
839 SymbolKind::LoopBinding,
840 )?;
841 let (body, _loop_env, body_states) =
842 self.analyze_stmt_list(&for_stmt.body, context, loop_env)?;
843 touched_states.extend(body_states);
844 typed.push(TypedStmt {
845 kind: TypedStmtKind::For(TypedForStmt {
846 binding,
847 range: TypedRangeExpr {
848 start,
849 end,
850 span: for_stmt.range.span,
851 },
852 body,
853 }),
854 span: stmt.span,
855 });
856 }
857 }
858 }
859
860 Ok((typed, env, touched_states))
861 }
862
863 fn analyze_assign_target(
864 &mut self,
865 target: &syntax::AssignTarget,
866 context: BlockContext,
867 env: &BlockEnv,
868 ) -> Result<TypedAssignTarget, SemanticError> {
869 let kind = match context {
870 BlockContext::Derive => match &target.kind {
871 syntax::AssignTargetKind::Name(name) => {
872 let Some(symbol) = self.globals.derived.get(&name.text).copied() else {
873 return Err(SemanticError::new(
874 format!("`{}` is not a valid derive target", name.text),
875 name.span,
876 ));
877 };
878 TypedAssignTargetKind::Derived(symbol)
879 }
880 _ => {
881 return Err(SemanticError::new(
882 "derive assignments must target a bare identifier",
883 target.span,
884 ))
885 }
886 },
887 BlockContext::Outputs => match &target.kind {
888 syntax::AssignTargetKind::Name(name) => {
889 let Some(symbol) = self.globals.outputs.get(&name.text).copied() else {
890 return Err(SemanticError::new(
891 format!("`{}` is not a valid output target", name.text),
892 name.span,
893 ));
894 };
895 TypedAssignTargetKind::Output(symbol)
896 }
897 _ => {
898 return Err(SemanticError::new(
899 "outputs assignments must target a bare identifier",
900 target.span,
901 ))
902 }
903 },
904 BlockContext::Init => {
905 TypedAssignTargetKind::StateInit(self.analyze_runtime_state_place(target, env)?)
906 }
907 BlockContext::Dynamics | BlockContext::Drift => {
908 let place = self.expect_call_state_target(target, "ddt")?;
909 TypedAssignTargetKind::Derivative(
910 self.analyze_runtime_state_place_expr(&place, env)?,
911 )
912 }
913 BlockContext::Diffusion => {
914 let place = self.expect_call_state_target(target, "noise")?;
915 TypedAssignTargetKind::Noise(self.analyze_runtime_state_place_expr(&place, env)?)
916 }
917 };
918 Ok(TypedAssignTarget {
919 kind,
920 span: target.span,
921 })
922 }
923
924 fn expect_call_state_target(
925 &self,
926 target: &syntax::AssignTarget,
927 expected: &str,
928 ) -> Result<syntax::Place, SemanticError> {
929 match &target.kind {
930 syntax::AssignTargetKind::Call { callee, args }
931 if callee.text == expected && args.len() == 1 =>
932 {
933 self.place_from_expr(&args[0])
934 }
935 syntax::AssignTargetKind::Call { callee, .. } => Err(SemanticError::new(
936 format!(
937 "expected `{expected}(...)` assignment target, found `{}`",
938 callee.text
939 ),
940 target.span,
941 )),
942 _ => Err(SemanticError::new(
943 format!("expected `{expected}(...)` assignment target"),
944 target.span,
945 )),
946 }
947 }
948
949 fn place_from_expr(&self, expr: &syntax::Expr) -> Result<syntax::Place, SemanticError> {
950 match &expr.kind {
951 syntax::ExprKind::Name(name) => Ok(syntax::Place {
952 name: name.clone(),
953 index: None,
954 span: expr.span,
955 }),
956 syntax::ExprKind::Index { target, index } => match &target.kind {
957 syntax::ExprKind::Name(name) => Ok(syntax::Place {
958 name: name.clone(),
959 index: Some((**index).clone()),
960 span: expr.span,
961 }),
962 _ => Err(SemanticError::new(
963 "indexed assignment targets must index a state identifier",
964 expr.span,
965 )),
966 },
967 _ => Err(SemanticError::new(
968 "expected a state reference in assignment target",
969 expr.span,
970 )),
971 }
972 }
973
974 fn analyze_runtime_state_place(
975 &self,
976 target: &syntax::AssignTarget,
977 env: &BlockEnv,
978 ) -> Result<TypedStatePlace, SemanticError> {
979 let place = match &target.kind {
980 syntax::AssignTargetKind::Name(name) => syntax::Place {
981 name: name.clone(),
982 index: None,
983 span: target.span,
984 },
985 syntax::AssignTargetKind::Index { target, index } => syntax::Place {
986 name: target.clone(),
987 index: Some(index.clone()),
988 span: target.span,
989 },
990 syntax::AssignTargetKind::Call { .. } => {
991 return Err(SemanticError::new(
992 "unexpected call target in runtime state assignment",
993 target.span,
994 ))
995 }
996 };
997 self.analyze_runtime_state_place_expr(&place, env)
998 }
999
1000 fn analyze_runtime_state_place_expr(
1001 &self,
1002 place: &syntax::Place,
1003 env: &BlockEnv,
1004 ) -> Result<TypedStatePlace, SemanticError> {
1005 let state = self.globals.states.get(&place.name.text).ok_or_else(|| {
1006 let error = SemanticError::new(
1007 format!("unknown state `{}`", place.name.text),
1008 place.name.span,
1009 );
1010 match self.assist_for_unknown_state(&place.name) {
1011 Some(assist) => assist.apply(error),
1012 None => error,
1013 }
1014 })?;
1015 let index = match (&state.size, &place.index) {
1016 (Some(_), Some(index)) => {
1017 let index = self.analyze_expr(index, env)?;
1018 self.expect_int(&index, "state index", index.span)?;
1019 Some(Box::new(index))
1020 }
1021 (Some(_), None) => {
1022 return Err(SemanticError::new(
1023 format!("state array `{}` requires an index", place.name.text),
1024 place.span,
1025 ))
1026 }
1027 (None, Some(_)) => {
1028 return Err(SemanticError::new(
1029 format!(
1030 "state `{}` is scalar and cannot be indexed",
1031 place.name.text
1032 ),
1033 place.span,
1034 ))
1035 }
1036 (None, None) => None,
1037 };
1038 Ok(TypedStatePlace {
1039 state: state.symbol,
1040 index,
1041 span: place.span,
1042 })
1043 }
1044
1045 fn analyze_state_place_const(
1046 &self,
1047 place: &syntax::Place,
1048 ) -> Result<TypedStatePlace, SemanticError> {
1049 let state = self.globals.states.get(&place.name.text).ok_or_else(|| {
1050 let error = SemanticError::new(
1051 format!("unknown state `{}`", place.name.text),
1052 place.name.span,
1053 );
1054 match self.assist_for_unknown_state(&place.name) {
1055 Some(assist) => assist.apply(error),
1056 None => error,
1057 }
1058 })?;
1059 let index = match (&state.size, &place.index) {
1060 (Some(_), Some(index)) => {
1061 let value = self.expect_const_usize(index, "route destination index", false)?;
1062 Some(Box::new(TypedExpr {
1063 kind: TypedExprKind::Literal(ConstValue::Int(value as i64)),
1064 ty: ValueType::Int,
1065 constant: Some(ConstValue::Int(value as i64)),
1066 span: index.span,
1067 }))
1068 }
1069 (Some(_), None) => {
1070 return Err(SemanticError::new(
1071 format!("state array `{}` requires an index", place.name.text),
1072 place.span,
1073 ))
1074 }
1075 (None, Some(_)) => {
1076 return Err(SemanticError::new(
1077 format!(
1078 "state `{}` is scalar and cannot be indexed",
1079 place.name.text
1080 ),
1081 place.span,
1082 ))
1083 }
1084 (None, None) => None,
1085 };
1086 Ok(TypedStatePlace {
1087 state: state.symbol,
1088 index,
1089 span: place.span,
1090 })
1091 }
1092
1093 fn analyze_expr(
1094 &self,
1095 expr: &syntax::Expr,
1096 env: &BlockEnv,
1097 ) -> Result<TypedExpr, SemanticError> {
1098 match &expr.kind {
1099 syntax::ExprKind::Number(value) => {
1100 let constant = number_to_const(*value);
1101 Ok(TypedExpr {
1102 kind: TypedExprKind::Literal(constant.clone()),
1103 ty: constant.value_type(),
1104 constant: Some(constant),
1105 span: expr.span,
1106 })
1107 }
1108 syntax::ExprKind::Bool(value) => {
1109 let constant = ConstValue::Bool(*value);
1110 Ok(TypedExpr {
1111 kind: TypedExprKind::Literal(constant.clone()),
1112 ty: ValueType::Bool,
1113 constant: Some(constant),
1114 span: expr.span,
1115 })
1116 }
1117 syntax::ExprKind::Name(name) => self.analyze_name_expr(name, expr.span, env),
1118 syntax::ExprKind::Unary { op, expr: inner } => {
1119 let inner = self.analyze_expr(inner, env)?;
1120 let ty = match op {
1121 syntax::UnaryOp::Not => {
1122 self.expect_bool(&inner, "unary `!` operand", inner.span)?;
1123 ValueType::Bool
1124 }
1125 syntax::UnaryOp::Plus | syntax::UnaryOp::Minus => {
1126 self.expect_numeric(&inner, "unary numeric operand", inner.span)?;
1127 inner.ty
1128 }
1129 };
1130 let op = match op {
1131 syntax::UnaryOp::Plus => TypedUnaryOp::Plus,
1132 syntax::UnaryOp::Minus => TypedUnaryOp::Minus,
1133 syntax::UnaryOp::Not => TypedUnaryOp::Not,
1134 };
1135 let constant = inner
1136 .constant
1137 .as_ref()
1138 .and_then(|value| fold_unary(op, value));
1139 Ok(TypedExpr {
1140 kind: TypedExprKind::Unary {
1141 op,
1142 expr: Box::new(inner),
1143 },
1144 ty,
1145 constant,
1146 span: expr.span,
1147 })
1148 }
1149 syntax::ExprKind::Binary { op, lhs, rhs } => {
1150 let lhs = self.analyze_expr(lhs, env)?;
1151 let rhs = self.analyze_expr(rhs, env)?;
1152 let op = map_binary_op(*op);
1153 let ty = self.binary_result_type(op, &lhs, &rhs, expr.span)?;
1154 let constant = match (&lhs.constant, &rhs.constant) {
1155 (Some(lhs), Some(rhs)) => fold_binary(op, lhs, rhs),
1156 _ => None,
1157 };
1158 Ok(TypedExpr {
1159 kind: TypedExprKind::Binary {
1160 op,
1161 lhs: Box::new(lhs),
1162 rhs: Box::new(rhs),
1163 },
1164 ty,
1165 constant,
1166 span: expr.span,
1167 })
1168 }
1169 syntax::ExprKind::Call { callee, args } => {
1170 self.analyze_call(callee, args, expr.span, env)
1171 }
1172 syntax::ExprKind::Index { target, index } => {
1173 let index = self.analyze_expr(index, env)?;
1174 self.expect_int(&index, "index expression", index.span)?;
1175 match &target.kind {
1176 syntax::ExprKind::Name(name) => {
1177 let state = self.globals.states.get(&name.text).ok_or_else(|| {
1178 SemanticError::new(
1179 format!(
1180 "only state arrays can be indexed; `{}` is not a state",
1181 name.text
1182 ),
1183 name.span,
1184 )
1185 })?;
1186 if state.size.is_none() {
1187 return Err(SemanticError::new(
1188 format!("state `{}` is scalar and cannot be indexed", name.text),
1189 expr.span,
1190 ));
1191 }
1192 Ok(TypedExpr {
1193 kind: TypedExprKind::StateValue(TypedStatePlace {
1194 state: state.symbol,
1195 index: Some(Box::new(index)),
1196 span: expr.span,
1197 }),
1198 ty: ValueType::Real,
1199 constant: None,
1200 span: expr.span,
1201 })
1202 }
1203 _ => Err(SemanticError::new(
1204 "only state arrays can be indexed",
1205 expr.span,
1206 )),
1207 }
1208 }
1209 }
1210 }
1211
1212 fn analyze_name_expr(
1213 &self,
1214 name: &syntax::Ident,
1215 span: Span,
1216 env: &BlockEnv,
1217 ) -> Result<TypedExpr, SemanticError> {
1218 if let Some(symbol) = env.lookup_local(&name.text) {
1219 let ty = self.scalar_symbol_type(symbol).ok_or_else(|| {
1220 SemanticError::new(
1221 format!("local `{}` does not resolve to a scalar value", name.text),
1222 span,
1223 )
1224 })?;
1225 return Ok(TypedExpr {
1226 kind: TypedExprKind::Symbol(symbol),
1227 ty,
1228 constant: None,
1229 span,
1230 });
1231 }
1232
1233 if let Some(symbol) = self.globals.parameters.get(&name.text).copied() {
1234 return Ok(TypedExpr {
1235 kind: TypedExprKind::Symbol(symbol),
1236 ty: ValueType::Real,
1237 constant: None,
1238 span,
1239 });
1240 }
1241
1242 if let Some(symbol) = self.globals.constants.get(&name.text).copied() {
1243 let constant = self.globals.constant_values.get(&name.text).cloned();
1244 let ty = self
1245 .scalar_symbol_type(symbol)
1246 .expect("constant type must be known");
1247 return Ok(TypedExpr {
1248 kind: TypedExprKind::Symbol(symbol),
1249 ty,
1250 constant,
1251 span,
1252 });
1253 }
1254
1255 if let Some(symbol) = self.globals.covariates.get(&name.text).copied() {
1256 return Ok(TypedExpr {
1257 kind: TypedExprKind::Symbol(symbol),
1258 ty: ValueType::Real,
1259 constant: None,
1260 span,
1261 });
1262 }
1263
1264 if let Some(state) = self.globals.states.get(&name.text) {
1265 if state.size.is_some() {
1266 return Err(SemanticError::new(
1267 format!("state array `{}` requires an index", name.text),
1268 span,
1269 ));
1270 }
1271 let place = TypedStatePlace {
1272 state: state.symbol,
1273 index: None,
1274 span,
1275 };
1276 return Ok(TypedExpr {
1277 kind: TypedExprKind::StateValue(place),
1278 ty: ValueType::Real,
1279 constant: None,
1280 span,
1281 });
1282 }
1283
1284 if let Some(symbol) = self.globals.derived.get(&name.text).copied() {
1285 if !env.available_derived.contains(&symbol) {
1286 return Err(SemanticError::new(
1287 format!(
1288 "derived value `{}` is not definitely assigned at this point",
1289 name.text
1290 ),
1291 span,
1292 ));
1293 }
1294 let ty = self.scalar_symbol_type(symbol).ok_or_else(|| {
1295 SemanticError::new(
1296 format!(
1297 "derived value `{}` does not have a resolved type yet",
1298 name.text
1299 ),
1300 span,
1301 )
1302 })?;
1303 return Ok(TypedExpr {
1304 kind: TypedExprKind::Symbol(symbol),
1305 ty,
1306 constant: None,
1307 span,
1308 });
1309 }
1310
1311 if self.globals.routes.contains_key(&name.text) {
1312 let route = self.globals.routes[&name.text];
1313 return Err(self
1314 .assist_for_route_scalar(route, span)
1315 .apply(SemanticError::new(
1316 format!(
1317 "route `{}` cannot be used as a scalar value; use `rate({})`",
1318 name.text, name.text
1319 ),
1320 span,
1321 )));
1322 }
1323
1324 if self.globals.outputs.contains_key(&name.text) {
1325 let output = self.globals.outputs[&name.text];
1326 return Err(self
1327 .assist_for_output_scope(output)
1328 .apply(SemanticError::new(
1329 format!("output `{}` is not in expression scope", name.text),
1330 span,
1331 )));
1332 }
1333
1334 let error = SemanticError::new(format!("unknown identifier `{}`", name.text), span);
1335 Err(match self.assist_for_unknown_identifier(name, span, env) {
1336 Some(assist) => assist.apply(error),
1337 None => error,
1338 })
1339 }
1340
1341 fn analyze_call(
1342 &self,
1343 callee: &syntax::Ident,
1344 args: &[syntax::Expr],
1345 span: Span,
1346 env: &BlockEnv,
1347 ) -> Result<TypedExpr, SemanticError> {
1348 if callee.text == RATE_FUNCTION_NAME {
1349 if args.len() != 1 {
1350 return Err(SemanticError::new(
1351 format!(
1352 "`rate` expects exactly one route argument, got {}",
1353 args.len()
1354 ),
1355 callee.span,
1356 ));
1357 }
1358 if let syntax::ExprKind::Number(value) = &args[0].kind {
1359 if let Some(suffix) = numeric_label_literal_suffix(*value) {
1360 return Err(self.bare_numeric_route_error(args[0].span, &suffix));
1361 }
1362 }
1363 let syntax::ExprKind::Name(route_name) = &args[0].kind else {
1364 return Err(SemanticError::new(
1365 "`rate` expects a route identifier argument",
1366 args[0].span,
1367 ));
1368 };
1369 self.validate_route_label_name(route_name)?;
1370 let route = self
1371 .globals
1372 .routes
1373 .get(&route_name.text)
1374 .copied()
1375 .ok_or_else(|| {
1376 let error = SemanticError::new(
1377 format!("unknown route `{}` in `rate(...)`", route_name.text),
1378 route_name.span,
1379 );
1380 match self.assist_for_unknown_route(route_name) {
1381 Some(assist) => assist.apply(error),
1382 None => error,
1383 }
1384 })?;
1385 return Ok(TypedExpr {
1386 kind: TypedExprKind::Call {
1387 callee: TypedCall::Rate(route),
1388 args: Vec::new(),
1389 },
1390 ty: ValueType::Real,
1391 constant: None,
1392 span,
1393 });
1394 }
1395
1396 let intrinsic = MathIntrinsic::from_name(&callee.text).ok_or_else(|| {
1397 let error =
1398 SemanticError::new(format!("unknown function `{}`", callee.text), callee.span);
1399 match self.assist_for_unknown_function(callee) {
1400 Some(assist) => assist.apply(error),
1401 None => error,
1402 }
1403 })?;
1404 let expected_arity = intrinsic.arity();
1405 match expected_arity {
1406 IntrinsicArity::Exact(expected) if expected != args.len() => {
1407 return Err(SemanticError::new(
1408 format!(
1409 "function `{}` expects {} argument(s), got {}",
1410 callee.text,
1411 expected,
1412 args.len()
1413 ),
1414 callee.span,
1415 ))
1416 }
1417 _ => {}
1418 }
1419
1420 let mut typed_args = Vec::with_capacity(args.len());
1421 for arg in args {
1422 let typed = self.analyze_expr(arg, env)?;
1423 self.expect_numeric(&typed, &format!("`{}` argument", callee.text), arg.span)?;
1424 typed_args.push(typed);
1425 }
1426 let ty = call_result_type(intrinsic, &typed_args);
1427 let constant = typed_args
1428 .iter()
1429 .map(|arg| arg.constant.clone())
1430 .collect::<Option<Vec<_>>>()
1431 .and_then(|values| fold_call(intrinsic, &values));
1432 Ok(TypedExpr {
1433 kind: TypedExprKind::Call {
1434 callee: TypedCall::Math(intrinsic),
1435 args: typed_args,
1436 },
1437 ty,
1438 constant,
1439 span,
1440 })
1441 }
1442
1443 fn binary_result_type(
1444 &self,
1445 op: TypedBinaryOp,
1446 lhs: &TypedExpr,
1447 rhs: &TypedExpr,
1448 span: Span,
1449 ) -> Result<ValueType, SemanticError> {
1450 match op {
1451 TypedBinaryOp::Or | TypedBinaryOp::And => {
1452 self.expect_bool(lhs, "logical operand", lhs.span)?;
1453 self.expect_bool(rhs, "logical operand", rhs.span)?;
1454 Ok(ValueType::Bool)
1455 }
1456 TypedBinaryOp::Eq | TypedBinaryOp::NotEq => {
1457 if lhs.ty != rhs.ty {
1458 return Err(SemanticError::new(
1459 format!(
1460 "equality comparison requires matching operand types, found {:?} and {:?}",
1461 lhs.ty, rhs.ty
1462 ),
1463 span,
1464 ));
1465 }
1466 Ok(ValueType::Bool)
1467 }
1468 TypedBinaryOp::Lt | TypedBinaryOp::LtEq | TypedBinaryOp::Gt | TypedBinaryOp::GtEq => {
1469 self.expect_numeric(lhs, "comparison operand", lhs.span)?;
1470 self.expect_numeric(rhs, "comparison operand", rhs.span)?;
1471 Ok(ValueType::Bool)
1472 }
1473 TypedBinaryOp::Add | TypedBinaryOp::Sub | TypedBinaryOp::Mul => {
1474 self.expect_numeric(lhs, "arithmetic operand", lhs.span)?;
1475 self.expect_numeric(rhs, "arithmetic operand", rhs.span)?;
1476 Ok(promote_numeric(lhs.ty, rhs.ty))
1477 }
1478 TypedBinaryOp::Div | TypedBinaryOp::Pow => {
1479 self.expect_numeric(lhs, "arithmetic operand", lhs.span)?;
1480 self.expect_numeric(rhs, "arithmetic operand", rhs.span)?;
1481 Ok(ValueType::Real)
1482 }
1483 }
1484 }
1485
1486 fn expect_numeric(
1487 &self,
1488 expr: &TypedExpr,
1489 context: &str,
1490 span: Span,
1491 ) -> Result<(), SemanticError> {
1492 if expr.ty.is_numeric() {
1493 Ok(())
1494 } else {
1495 Err(SemanticError::new(
1496 format!("{context} must be numeric, found {:?}", expr.ty),
1497 span,
1498 ))
1499 }
1500 }
1501
1502 fn expect_bool(
1503 &self,
1504 expr: &TypedExpr,
1505 context: &str,
1506 span: Span,
1507 ) -> Result<(), SemanticError> {
1508 if expr.ty == ValueType::Bool {
1509 Ok(())
1510 } else {
1511 Err(SemanticError::new(
1512 format!("{context} must be boolean, found {:?}", expr.ty),
1513 span,
1514 ))
1515 }
1516 }
1517
1518 fn expect_int(&self, expr: &TypedExpr, context: &str, span: Span) -> Result<(), SemanticError> {
1519 if expr.ty == ValueType::Int {
1520 Ok(())
1521 } else {
1522 Err(SemanticError::new(
1523 format!("{context} must be integer-valued, found {:?}", expr.ty),
1524 span,
1525 ))
1526 }
1527 }
1528
1529 fn expect_const_usize(
1530 &self,
1531 expr: &syntax::Expr,
1532 context: &str,
1533 strictly_positive: bool,
1534 ) -> Result<usize, SemanticError> {
1535 let value = self.evaluate_const_expr(expr, &BTreeMap::new(), &mut BTreeSet::new())?;
1536 let Some(value) = value.as_i64() else {
1537 return Err(SemanticError::new(
1538 format!("{context} must be an integer constant"),
1539 expr.span,
1540 ));
1541 };
1542 if value < 0 || (strictly_positive && value == 0) {
1543 return Err(SemanticError::new(
1544 format!(
1545 "{context} must be {}",
1546 if strictly_positive {
1547 "positive"
1548 } else {
1549 "non-negative"
1550 }
1551 ),
1552 expr.span,
1553 ));
1554 }
1555 Ok(value as usize)
1556 }
1557
1558 fn evaluate_const_expr(
1559 &self,
1560 expr: &syntax::Expr,
1561 bindings: &BTreeMap<String, &syntax::Binding>,
1562 visiting: &mut BTreeSet<String>,
1563 ) -> Result<ConstValue, SemanticError> {
1564 match &expr.kind {
1565 syntax::ExprKind::Number(value) => Ok(number_to_const(*value)),
1566 syntax::ExprKind::Bool(value) => Ok(ConstValue::Bool(*value)),
1567 syntax::ExprKind::Name(name) => {
1568 if let Some(value) = self.globals.constant_values.get(&name.text) {
1569 return Ok(value.clone());
1570 }
1571 let binding = bindings.get(&name.text).ok_or_else(|| {
1572 SemanticError::new(
1573 format!(
1574 "unknown constant `{}` in compile-time expression",
1575 name.text
1576 ),
1577 name.span,
1578 )
1579 })?;
1580 if !visiting.insert(name.text.clone()) {
1581 return Err(SemanticError::new(
1582 format!("constant `{}` forms a dependency cycle", name.text),
1583 name.span,
1584 ));
1585 }
1586 let value = self.evaluate_const_expr(&binding.value, bindings, visiting)?;
1587 visiting.remove(&name.text);
1588 Ok(value)
1589 }
1590 syntax::ExprKind::Unary { op, expr } => {
1591 let value = self.evaluate_const_expr(expr, bindings, visiting)?;
1592 let op = match op {
1593 syntax::UnaryOp::Plus => TypedUnaryOp::Plus,
1594 syntax::UnaryOp::Minus => TypedUnaryOp::Minus,
1595 syntax::UnaryOp::Not => TypedUnaryOp::Not,
1596 };
1597 fold_unary(op, &value).ok_or_else(|| {
1598 SemanticError::new("invalid constant unary operation", expr.span)
1599 })
1600 }
1601 syntax::ExprKind::Binary { op, lhs, rhs } => {
1602 let lhs = self.evaluate_const_expr(lhs, bindings, visiting)?;
1603 let rhs = self.evaluate_const_expr(rhs, bindings, visiting)?;
1604 fold_binary(map_binary_op(*op), &lhs, &rhs).ok_or_else(|| {
1605 SemanticError::new("invalid constant binary operation", expr.span)
1606 })
1607 }
1608 syntax::ExprKind::Call { callee, args } => {
1609 if callee.text == RATE_FUNCTION_NAME {
1610 return Err(SemanticError::new(
1611 "`rate(...)` cannot appear in a compile-time expression",
1612 callee.span,
1613 ));
1614 }
1615 let intrinsic = MathIntrinsic::from_name(&callee.text).ok_or_else(|| {
1616 SemanticError::new(
1617 format!("unknown compile-time function `{}`", callee.text),
1618 callee.span,
1619 )
1620 })?;
1621 let mut values = Vec::with_capacity(args.len());
1622 for arg in args {
1623 values.push(self.evaluate_const_expr(arg, bindings, visiting)?);
1624 }
1625 fold_call(intrinsic, &values).ok_or_else(|| {
1626 SemanticError::new(
1627 format!("invalid compile-time call to `{}`", callee.text),
1628 expr.span,
1629 )
1630 })
1631 }
1632 syntax::ExprKind::Index { .. } => Err(SemanticError::new(
1633 "indexing is not allowed in compile-time expressions",
1634 expr.span,
1635 )),
1636 }
1637 }
1638
1639 fn insert_global_symbol(
1640 &mut self,
1641 name: &str,
1642 kind: SymbolKind,
1643 ty: PendingSymbolType,
1644 span: Span,
1645 ) -> Result<SymbolId, SemanticError> {
1646 if RESERVED_NAMES.contains(&name) {
1647 return Err(SemanticAssist::default()
1648 .help(format!(
1649 "rename `{name}` to a non-reserved identifier such as `{}_value`",
1650 name
1651 ))
1652 .replacement_suggestion(
1653 span,
1654 format!("{}_value", name),
1655 format!("rename `{name}` to `{}_value`", name),
1656 Applicability::MaybeIncorrect,
1657 )
1658 .apply(SemanticError::new(
1659 format!("`{name}` is reserved by the DSL and cannot be used as a symbol name"),
1660 span,
1661 )));
1662 }
1663 if let Some(existing) = self.globals.all_names.get(name).copied() {
1664 let existing_kind = self.symbols.get(existing).expect("valid symbol id").kind;
1665 if !allows_route_output_name_overlap(existing_kind, kind) {
1666 return Err(SemanticAssist::default()
1667 .context_label(
1668 self.symbol_span(existing),
1669 self.symbol_declared_here(existing),
1670 )
1671 .help(format!(
1672 "rename this declaration to a unique name such as `{}_2`",
1673 name
1674 ))
1675 .replacement_suggestion(
1676 span,
1677 format!("{}_2", name),
1678 format!("rename this declaration to `{}_2`", name),
1679 Applicability::MaybeIncorrect,
1680 )
1681 .apply(SemanticError::new(
1682 format!(
1683 "symbol name `{name}` collides with existing `{}`",
1684 self.symbol_name(existing)
1685 ),
1686 span,
1687 )));
1688 }
1689 }
1690 let id = self.symbols.len();
1691 self.symbols.push(PendingSymbol {
1692 id,
1693 name: name.to_string(),
1694 kind,
1695 ty,
1696 span,
1697 });
1698 self.globals.all_names.entry(name.to_string()).or_insert(id);
1699 Ok(id)
1700 }
1701
1702 fn validate_route_label_name(&self, label: &syntax::Ident) -> Result<(), SemanticError> {
1703 if let Some(suffix) = bare_numeric_label(&label.text) {
1704 return Err(self.bare_numeric_route_error(label.span, suffix));
1705 }
1706 if let Some(suffix) = canonical_numeric_suffix(&label.text, NUMERIC_OUTPUT_PREFIX) {
1707 return Err(self.wrong_prefix_route_error(label, suffix));
1708 }
1709 Ok(())
1710 }
1711
1712 fn validate_output_label_name(&self, label: &syntax::Ident) -> Result<(), SemanticError> {
1713 if let Some(suffix) = bare_numeric_label(&label.text) {
1714 return Err(self.bare_numeric_output_error(label.span, suffix));
1715 }
1716 if let Some(suffix) = canonical_numeric_suffix(&label.text, NUMERIC_ROUTE_PREFIX) {
1717 return Err(self.wrong_prefix_output_error(label, suffix));
1718 }
1719 Ok(())
1720 }
1721
1722 fn bare_numeric_route_error(&self, span: Span, suffix: &str) -> SemanticError {
1723 let replacement = format!("{NUMERIC_ROUTE_PREFIX}{suffix}");
1724 SemanticAssist::default()
1725 .help("numeric route labels must use the `input_<n>` form in authored DSL")
1726 .replacement_suggestion(
1727 span,
1728 replacement.clone(),
1729 format!("use `{replacement}`"),
1730 Applicability::Always,
1731 )
1732 .apply(SemanticError::new(
1733 format!(
1734 "bare numeric route labels are not allowed in the DSL; use `{replacement}` instead"
1735 ),
1736 span,
1737 ))
1738 }
1739
1740 fn bare_numeric_output_error(&self, span: Span, suffix: &str) -> SemanticError {
1741 let replacement = format!("{NUMERIC_OUTPUT_PREFIX}{suffix}");
1742 SemanticAssist::default()
1743 .help("numeric output labels must use the `outeq_<n>` form in authored DSL")
1744 .replacement_suggestion(
1745 span,
1746 replacement.clone(),
1747 format!("use `{replacement}`"),
1748 Applicability::Always,
1749 )
1750 .apply(SemanticError::new(
1751 format!(
1752 "bare numeric output labels are not allowed in the DSL; use `{replacement}` instead"
1753 ),
1754 span,
1755 ))
1756 }
1757
1758 fn wrong_prefix_route_error(&self, label: &syntax::Ident, suffix: &str) -> SemanticError {
1759 let replacement = format!("{NUMERIC_ROUTE_PREFIX}{suffix}");
1760 SemanticAssist::default()
1761 .help("numeric route labels use the `input_<n>` prefix")
1762 .replacement_suggestion(
1763 label.span,
1764 replacement.clone(),
1765 format!("use `{replacement}`"),
1766 Applicability::Always,
1767 )
1768 .apply(SemanticError::new(
1769 format!(
1770 "`{}` is an output label and cannot be used as a route; use `{replacement}` here",
1771 label.text
1772 ),
1773 label.span,
1774 ))
1775 }
1776
1777 fn wrong_prefix_output_error(&self, label: &syntax::Ident, suffix: &str) -> SemanticError {
1778 let replacement = format!("{NUMERIC_OUTPUT_PREFIX}{suffix}");
1779 SemanticAssist::default()
1780 .help("numeric output labels use the `outeq_<n>` prefix")
1781 .replacement_suggestion(
1782 label.span,
1783 replacement.clone(),
1784 format!("use `{replacement}`"),
1785 Applicability::Always,
1786 )
1787 .apply(SemanticError::new(
1788 format!(
1789 "`{}` is a route label and cannot be used as an output target; use `{replacement}` here",
1790 label.text
1791 ),
1792 label.span,
1793 ))
1794 }
1795
1796 fn insert_local_symbol(
1797 &mut self,
1798 env: &mut BlockEnv,
1799 ident: &syntax::Ident,
1800 ty: ValueType,
1801 kind: SymbolKind,
1802 ) -> Result<SymbolId, SemanticError> {
1803 if let Some(existing) = env
1804 .lookup_local(&ident.text)
1805 .or_else(|| self.globals.all_names.get(&ident.text).copied())
1806 {
1807 return Err(SemanticAssist::default()
1808 .context_label(
1809 self.symbol_span(existing),
1810 self.symbol_declared_here(existing),
1811 )
1812 .help(format!(
1813 "rename this local binding to a unique name such as `{}_local`",
1814 ident.text
1815 ))
1816 .replacement_suggestion(
1817 ident.span,
1818 format!("{}_local", ident.text),
1819 format!("rename this local binding to `{}_local`", ident.text),
1820 Applicability::MaybeIncorrect,
1821 )
1822 .apply(SemanticError::new(
1823 format!(
1824 "local symbol `{}` would shadow an existing symbol",
1825 ident.text
1826 ),
1827 ident.span,
1828 )));
1829 }
1830 let id = self.symbols.len();
1831 self.symbols.push(PendingSymbol {
1832 id,
1833 name: ident.text.clone(),
1834 kind,
1835 ty: PendingSymbolType::Scalar(Some(ty)),
1836 span: ident.span,
1837 });
1838 env.insert_local(ident.text.clone(), id);
1839 Ok(id)
1840 }
1841
1842 fn merge_symbol_type(
1843 &mut self,
1844 symbol: SymbolId,
1845 ty: ValueType,
1846 span: Span,
1847 ) -> Result<(), SemanticError> {
1848 let entry = self.symbols.get_mut(symbol).expect("valid symbol id");
1849 match &mut entry.ty {
1850 PendingSymbolType::Scalar(slot) => match slot {
1851 None => *slot = Some(ty),
1852 Some(existing) if *existing == ty => {}
1853 Some(existing) if existing.is_numeric() && ty.is_numeric() => {
1854 *slot = Some(promote_numeric(*existing, ty));
1855 }
1856 Some(existing) => {
1857 return Err(SemanticError::new(
1858 format!(
1859 "symbol `{}` is assigned incompatible types {:?} and {:?}",
1860 entry.name, existing, ty
1861 ),
1862 span,
1863 ));
1864 }
1865 },
1866 PendingSymbolType::Array { .. } | PendingSymbolType::Route => {
1867 return Err(SemanticError::new(
1868 format!(
1869 "symbol `{}` is not assignable as a scalar target",
1870 entry.name
1871 ),
1872 span,
1873 ));
1874 }
1875 }
1876 Ok(())
1877 }
1878
1879 fn scalar_symbol_type(&self, symbol: SymbolId) -> Option<ValueType> {
1880 match &self.symbols.get(symbol)?.ty {
1881 PendingSymbolType::Scalar(Some(ty)) => Some(*ty),
1882 PendingSymbolType::Scalar(None) => None,
1883 PendingSymbolType::Array { .. } | PendingSymbolType::Route => None,
1884 }
1885 }
1886
1887 fn symbol_name(&self, symbol: SymbolId) -> &str {
1888 &self.symbols[symbol].name
1889 }
1890
1891 fn symbol_span(&self, symbol: SymbolId) -> Span {
1892 self.symbols[symbol].span
1893 }
1894
1895 fn symbol_kind_label(&self, symbol: SymbolId) -> &'static str {
1896 match self.symbols[symbol].kind {
1897 SymbolKind::Parameter => "parameter",
1898 SymbolKind::Constant => "constant",
1899 SymbolKind::Covariate => "covariate",
1900 SymbolKind::State => "state",
1901 SymbolKind::Route => "route",
1902 SymbolKind::Derived => "derived value",
1903 SymbolKind::Output => "output",
1904 SymbolKind::Local => "local",
1905 SymbolKind::LoopBinding => "loop binding",
1906 }
1907 }
1908
1909 fn symbol_declared_here(&self, symbol: SymbolId) -> String {
1910 format!(
1911 "{} `{}` declared here",
1912 self.symbol_kind_label(symbol),
1913 self.symbol_name(symbol)
1914 )
1915 }
1916
1917 fn assist_for_symbol_replacement(&self, symbol: SymbolId, span: Span) -> SemanticAssist {
1918 let name = self.symbol_name(symbol).to_string();
1919 SemanticAssist::default()
1920 .context_label(self.symbol_span(symbol), self.symbol_declared_here(symbol))
1921 .replacement_suggestion(
1922 span,
1923 name.clone(),
1924 format!("did you mean `{name}`?"),
1925 Applicability::MaybeIncorrect,
1926 )
1927 }
1928
1929 fn assist_for_route_scalar(&self, route: SymbolId, span: Span) -> SemanticAssist {
1930 let name = self.symbol_name(route).to_string();
1931 SemanticAssist::default()
1932 .context_label(self.symbol_span(route), self.symbol_declared_here(route))
1933 .help(format!("route inputs are read through `rate({name})`"))
1934 .replacement_suggestion(
1935 span,
1936 format!("rate({name})"),
1937 format!("did you mean `rate({name})`?"),
1938 Applicability::MaybeIncorrect,
1939 )
1940 }
1941
1942 fn assist_for_output_scope(&self, output: SymbolId) -> SemanticAssist {
1943 SemanticAssist::default()
1944 .context_label(self.symbol_span(output), self.symbol_declared_here(output))
1945 .help(
1946 "outputs are assignment targets inside the `outputs` block and are not available as expression values",
1947 )
1948 }
1949
1950 fn assist_for_unknown_identifier(
1951 &self,
1952 name: &syntax::Ident,
1953 span: Span,
1954 env: &BlockEnv,
1955 ) -> Option<SemanticAssist> {
1956 let mut seen = BTreeSet::new();
1957 let mut candidates = Vec::new();
1958
1959 for scope in env.locals.iter().rev() {
1960 for (candidate_name, symbol) in scope {
1961 if seen.insert(candidate_name.clone()) {
1962 candidates.push(SimilarNameCandidate::new(
1963 candidate_name.clone(),
1964 self.assist_for_symbol_replacement(*symbol, span),
1965 ));
1966 }
1967 }
1968 }
1969
1970 for symbol in self
1971 .globals
1972 .parameters
1973 .values()
1974 .chain(self.globals.constants.values())
1975 .chain(self.globals.covariates.values())
1976 .chain(
1977 self.globals
1978 .states
1979 .values()
1980 .filter(|entry| entry.size.is_none())
1981 .map(|entry| &entry.symbol),
1982 )
1983 {
1984 let candidate_name = self.symbol_name(*symbol).to_string();
1985 if seen.insert(candidate_name.clone()) {
1986 candidates.push(SimilarNameCandidate::new(
1987 candidate_name,
1988 self.assist_for_symbol_replacement(*symbol, span),
1989 ));
1990 }
1991 }
1992
1993 for symbol in &env.available_derived {
1994 let candidate_name = self.symbol_name(*symbol).to_string();
1995 if seen.insert(candidate_name.clone()) {
1996 candidates.push(SimilarNameCandidate::new(
1997 candidate_name,
1998 self.assist_for_symbol_replacement(*symbol, span),
1999 ));
2000 }
2001 }
2002
2003 for symbol in self.globals.routes.values() {
2004 let candidate_name = self.symbol_name(*symbol).to_string();
2005 if seen.insert(candidate_name.clone()) {
2006 candidates.push(SimilarNameCandidate::new(
2007 candidate_name,
2008 self.assist_for_route_scalar(*symbol, span),
2009 ));
2010 }
2011 }
2012
2013 best_similar_name_assist(&name.text, candidates)
2014 }
2015
2016 fn assist_for_unknown_state(&self, state_name: &syntax::Ident) -> Option<SemanticAssist> {
2017 let candidates = self
2018 .globals
2019 .states
2020 .values()
2021 .map(|entry| {
2022 SimilarNameCandidate::new(
2023 self.symbol_name(entry.symbol).to_string(),
2024 self.assist_for_symbol_replacement(entry.symbol, state_name.span),
2025 )
2026 })
2027 .collect::<Vec<_>>();
2028 best_similar_name_assist(&state_name.text, candidates)
2029 }
2030
2031 fn assist_for_unknown_route(&self, route_name: &syntax::Ident) -> Option<SemanticAssist> {
2032 let candidates = self
2033 .globals
2034 .routes
2035 .values()
2036 .map(|symbol| {
2037 let name = self.symbol_name(*symbol).to_string();
2038 SimilarNameCandidate::new(
2039 name.clone(),
2040 SemanticAssist::default()
2041 .context_label(
2042 self.symbol_span(*symbol),
2043 self.symbol_declared_here(*symbol),
2044 )
2045 .replacement_suggestion(
2046 route_name.span,
2047 name.clone(),
2048 format!("did you mean `{name}`?"),
2049 Applicability::MaybeIncorrect,
2050 ),
2051 )
2052 })
2053 .collect::<Vec<_>>();
2054 best_similar_name_assist(&route_name.text, candidates)
2055 }
2056
2057 fn assist_for_unknown_function(&self, callee: &syntax::Ident) -> Option<SemanticAssist> {
2058 let mut candidates = MathIntrinsic::ALL
2059 .iter()
2060 .map(|intrinsic| {
2061 let name = intrinsic.name().to_string();
2062 SimilarNameCandidate::new(
2063 name.clone(),
2064 SemanticAssist::default().replacement_suggestion(
2065 callee.span,
2066 name.clone(),
2067 format!("did you mean `{name}`?"),
2068 Applicability::MaybeIncorrect,
2069 ),
2070 )
2071 })
2072 .collect::<Vec<_>>();
2073 candidates.push(SimilarNameCandidate::new(
2074 RATE_FUNCTION_NAME,
2075 SemanticAssist::default()
2076 .help("`rate` reads route inputs as `rate(route)`")
2077 .replacement_suggestion(
2078 callee.span,
2079 RATE_FUNCTION_NAME,
2080 "did you mean `rate`?",
2081 Applicability::MaybeIncorrect,
2082 ),
2083 ));
2084 best_similar_name_assist(&callee.text, candidates)
2085 }
2086
2087 fn finalize_symbols(self) -> Result<Vec<Symbol>, SemanticError> {
2088 self.symbols
2089 .into_iter()
2090 .map(|symbol| {
2091 let ty = match symbol.ty {
2092 PendingSymbolType::Scalar(Some(ty)) => SymbolType::Scalar(ty),
2093 PendingSymbolType::Scalar(None) => {
2094 return Err(SemanticError::new(
2095 format!(
2096 "symbol `{}` does not have a resolved scalar type",
2097 symbol.name
2098 ),
2099 symbol.span,
2100 ))
2101 }
2102 PendingSymbolType::Array { element, size } => {
2103 SymbolType::Array { element, size }
2104 }
2105 PendingSymbolType::Route => SymbolType::Route,
2106 };
2107 Ok(Symbol {
2108 id: symbol.id,
2109 name: symbol.name,
2110 kind: symbol.kind,
2111 ty,
2112 span: symbol.span,
2113 })
2114 })
2115 .collect()
2116 }
2117
2118 fn validate_kind_requirements(
2119 &self,
2120 sections: &ModelSections<'_>,
2121 states: &[TypedState],
2122 ) -> Result<(), SemanticError> {
2123 if states.is_empty() {
2124 return Err(SemanticError::new(
2125 format!(
2126 "model `{}` must declare at least one state",
2127 self.model.name.text
2128 ),
2129 self.model.span,
2130 ));
2131 }
2132 if sections.outputs.is_none() {
2133 return Err(SemanticError::new(
2134 format!(
2135 "model `{}` is missing an `outputs` block",
2136 self.model.name.text
2137 ),
2138 self.model.span,
2139 ));
2140 }
2141 Ok(())
2142 }
2143
2144 fn validate_kind_blocks(
2145 &self,
2146 kind: ModelKind,
2147 blocks: ModelKindBlocks<'_>,
2148 states: &[TypedState],
2149 ) -> Result<(), SemanticError> {
2150 match kind {
2151 ModelKind::Ode => {
2152 if blocks.dynamics.is_none() {
2153 return Err(SemanticError::new(
2154 "ODE models require a `dynamics` block",
2155 self.model.span,
2156 ));
2157 }
2158 if blocks.drift.is_some() || blocks.diffusion.is_some() {
2159 return Err(SemanticError::new(
2160 "ODE models cannot declare `drift` or `diffusion` blocks",
2161 self.model.span,
2162 ));
2163 }
2164 if blocks.analytical.is_some() {
2165 return Err(SemanticError::new(
2166 "ODE models cannot declare an `analytical` block",
2167 self.model.span,
2168 ));
2169 }
2170 if let Some(particles_decl) = blocks.particles {
2171 return Err(SemanticError::new(
2172 "ODE models cannot declare `particles`",
2173 particles_decl.span,
2174 ));
2175 }
2176 }
2177 ModelKind::Analytical => {
2178 if blocks.analytical.is_none() {
2179 return Err(SemanticError::new(
2180 "analytical models require an `analytical` block",
2181 self.model.span,
2182 ));
2183 }
2184 if blocks.dynamics.is_some() || blocks.drift.is_some() || blocks.diffusion.is_some()
2185 {
2186 return Err(SemanticError::new(
2187 "analytical models cannot declare `dynamics`, `drift`, or `diffusion` blocks",
2188 self.model.span,
2189 ));
2190 }
2191 if let Some(particles_decl) = blocks.particles {
2192 return Err(SemanticError::new(
2193 "analytical models cannot declare `particles`",
2194 particles_decl.span,
2195 ));
2196 }
2197 }
2198 ModelKind::Sde => {
2199 if blocks.drift.is_none() || blocks.diffusion.is_none() {
2200 return Err(SemanticError::new(
2201 "SDE models require both `drift` and `diffusion` blocks",
2202 self.model.span,
2203 ));
2204 }
2205 if blocks.dynamics.is_some() {
2206 return Err(SemanticError::new(
2207 "SDE models cannot declare a `dynamics` block",
2208 self.model.span,
2209 ));
2210 }
2211 if blocks.analytical.is_some() {
2212 return Err(SemanticError::new(
2213 "SDE models cannot declare an `analytical` block",
2214 self.model.span,
2215 ));
2216 }
2217 if blocks.particles.is_none() {
2218 return Err(SemanticError::new(
2219 "SDE models require `particles`",
2220 self.model.span,
2221 ));
2222 }
2223 }
2224 }
2225
2226 if states.is_empty() {
2227 return Err(SemanticError::new(
2228 "typed model validation requires at least one state",
2229 self.model.span,
2230 ));
2231 }
2232 Ok(())
2233 }
2234
2235 fn validate_output_assignments(
2236 &self,
2237 outputs: &[SymbolId],
2238 block: &BlockAnalysis,
2239 ) -> Result<(), SemanticError> {
2240 for output in outputs {
2241 if !block.definite_targets.contains(output) {
2242 return Err(SemanticError::new(
2243 format!(
2244 "output `{}` is not definitely assigned on all control-flow paths",
2245 self.symbol_name(*output)
2246 ),
2247 block.block.span,
2248 ));
2249 }
2250 }
2251 Ok(())
2252 }
2253
2254 fn validate_analytical_structure_inputs(
2255 &self,
2256 structure: AnalyticalKernel,
2257 structure_span: Span,
2258 parameters: &[SymbolId],
2259 derived: &[SymbolId],
2260 derive_result: Option<&BlockAnalysis>,
2261 ) -> Result<(), SemanticError> {
2262 let plan = AnalyticalStructureInputPlan::for_kernel(
2263 structure,
2264 parameters.iter().map(|symbol| self.symbol_name(*symbol)),
2265 derived.iter().map(|symbol| self.symbol_name(*symbol)),
2266 )
2267 .map_err(|error| SemanticError::new(error.to_string(), structure_span))?;
2268
2269 let Some(derive_result) = derive_result else {
2270 return Ok(());
2271 };
2272
2273 let mut required_derived_symbols = Vec::new();
2274 match plan.kind() {
2275 AnalyticalStructureInputKind::AllPrimary { .. } => {}
2276 AnalyticalStructureInputKind::AllDerived { indices, .. } => {
2277 for (required_name, index) in structure
2278 .required_parameter_names()
2279 .iter()
2280 .zip(indices.iter().copied())
2281 {
2282 required_derived_symbols.push((*required_name, derived[index]));
2283 }
2284 }
2285 AnalyticalStructureInputKind::Mixed { bindings } => {
2286 for (required_name, binding) in structure
2287 .required_parameter_names()
2288 .iter()
2289 .zip(bindings.iter())
2290 {
2291 if binding.source == AnalyticalStructureInputSource::Derived {
2292 required_derived_symbols.push((*required_name, derived[binding.index]));
2293 }
2294 }
2295 }
2296 }
2297
2298 for (required_name, symbol) in required_derived_symbols {
2299 if !derive_result.available_derived.contains(&symbol) {
2300 return Err(SemanticError::new(
2301 format!(
2302 "derived value `{required_name}` is not definitely assigned on all control-flow paths before analytical structure `{}` uses it",
2303 structure.name()
2304 ),
2305 derive_result.block.span,
2306 )
2307 .with_help(format!(
2308 "assign `{required_name}` on every control-flow path in `derive` before the analytical structure runs"
2309 )));
2310 }
2311 }
2312
2313 Ok(())
2314 }
2315
2316 fn validate_state_coverage(
2317 &self,
2318 block: &BlockAnalysis,
2319 states: &[TypedState],
2320 block_name: &str,
2321 ) -> Result<(), SemanticError> {
2322 for state in states {
2323 if !block.touched_states.contains(&state.symbol) {
2324 return Err(SemanticError::new(
2325 format!(
2326 "{block_name} block does not assign `{}`",
2327 self.symbol_name(state.symbol)
2328 ),
2329 block.block.span,
2330 ));
2331 }
2332 }
2333 Ok(())
2334 }
2335}
2336
2337fn allows_route_output_name_overlap(existing: SymbolKind, new: SymbolKind) -> bool {
2338 matches!(
2339 (existing, new),
2340 (SymbolKind::Route, SymbolKind::Output) | (SymbolKind::Output, SymbolKind::Route)
2341 )
2342}
2343
2344fn bare_numeric_label(src: &str) -> Option<&str> {
2345 (!src.is_empty() && src.chars().all(|ch| ch.is_ascii_digit())).then_some(src)
2346}
2347
2348fn canonical_numeric_suffix<'a>(src: &'a str, prefix: &str) -> Option<&'a str> {
2349 let suffix = src.strip_prefix(prefix)?;
2350 (!suffix.is_empty() && suffix.chars().all(|ch| ch.is_ascii_digit())).then_some(suffix)
2351}
2352
2353fn numeric_label_literal_suffix(value: f64) -> Option<String> {
2354 (value.is_finite() && value >= 0.0 && value.fract() == 0.0 && value <= usize::MAX as f64)
2355 .then(|| (value as usize).to_string())
2356}
2357
2358#[derive(Default)]
2359struct Globals {
2360 all_names: BTreeMap<String, SymbolId>,
2361 parameters: BTreeMap<String, SymbolId>,
2362 constants: BTreeMap<String, SymbolId>,
2363 constant_values: BTreeMap<String, ConstValue>,
2364 covariates: BTreeMap<String, SymbolId>,
2365 states: BTreeMap<String, StateEntry>,
2366 routes: BTreeMap<String, SymbolId>,
2367 derived: BTreeMap<String, SymbolId>,
2368 outputs: BTreeMap<String, SymbolId>,
2369}
2370
2371#[derive(Debug, Clone, Copy)]
2372struct StateEntry {
2373 symbol: SymbolId,
2374 size: Option<usize>,
2375}
2376
2377#[derive(Clone)]
2378struct BlockEnv {
2379 locals: Vec<BTreeMap<String, SymbolId>>,
2380 available_derived: BTreeSet<SymbolId>,
2381 definite_targets: BTreeSet<SymbolId>,
2382}
2383
2384impl BlockEnv {
2385 fn new(available_derived: BTreeSet<SymbolId>) -> Self {
2386 Self {
2387 locals: vec![BTreeMap::new()],
2388 available_derived,
2389 definite_targets: BTreeSet::new(),
2390 }
2391 }
2392
2393 fn child_scope(&self) -> Self {
2394 let mut next = self.clone();
2395 next.locals.push(BTreeMap::new());
2396 next
2397 }
2398
2399 fn insert_local(&mut self, name: String, symbol: SymbolId) {
2400 self.locals
2401 .last_mut()
2402 .expect("local scope")
2403 .insert(name, symbol);
2404 }
2405
2406 fn lookup_local(&self, name: &str) -> Option<SymbolId> {
2407 self.locals
2408 .iter()
2409 .rev()
2410 .find_map(|scope| scope.get(name).copied())
2411 }
2412}
2413
2414struct BlockAnalysis {
2415 block: TypedStatementBlock,
2416 available_derived: BTreeSet<SymbolId>,
2417 definite_targets: BTreeSet<SymbolId>,
2418 touched_states: BTreeSet<SymbolId>,
2419}
2420
2421#[derive(Clone)]
2422enum PendingSymbolType {
2423 Scalar(Option<ValueType>),
2424 Array { element: ValueType, size: usize },
2425 Route,
2426}
2427
2428struct PendingSymbol {
2429 id: SymbolId,
2430 name: String,
2431 kind: SymbolKind,
2432 ty: PendingSymbolType,
2433 span: Span,
2434}
2435
2436struct ModelKindBlocks<'a> {
2437 dynamics: Option<&'a BlockAnalysis>,
2438 drift: Option<&'a BlockAnalysis>,
2439 diffusion: Option<&'a BlockAnalysis>,
2440 analytical: Option<&'a syntax::AnalyticalBlock>,
2441 particles: Option<&'a syntax::ParticlesDecl>,
2442}
2443
2444#[derive(Default)]
2445struct ModelSections<'a> {
2446 parameters: Option<&'a syntax::ParametersBlock>,
2447 constants: Option<&'a syntax::ConstantsBlock>,
2448 covariates: Option<&'a syntax::CovariatesBlock>,
2449 states: Option<&'a syntax::StatesBlock>,
2450 routes: Option<&'a syntax::RoutesBlock>,
2451 derive: Option<&'a syntax::StatementBlock>,
2452 dynamics: Option<&'a syntax::StatementBlock>,
2453 outputs: Option<&'a syntax::StatementBlock>,
2454 analytical: Option<&'a syntax::AnalyticalBlock>,
2455 init: Option<&'a syntax::StatementBlock>,
2456 drift: Option<&'a syntax::StatementBlock>,
2457 diffusion: Option<&'a syntax::StatementBlock>,
2458 particles: Option<&'a syntax::ParticlesDecl>,
2459}
2460
2461impl<'a> ModelSections<'a> {
2462 fn from_model(model: &'a syntax::Model) -> Result<Self, SemanticError> {
2463 let mut sections = Self::default();
2464 for item in &model.items {
2465 match item {
2466 syntax::ModelItem::Parameters(block) => {
2467 set_once(&mut sections.parameters, block, "parameters")?
2468 }
2469 syntax::ModelItem::Constants(block) => {
2470 set_once(&mut sections.constants, block, "constants")?
2471 }
2472 syntax::ModelItem::Covariates(block) => {
2473 set_once(&mut sections.covariates, block, "covariates")?
2474 }
2475 syntax::ModelItem::States(block) => {
2476 set_once(&mut sections.states, block, "states")?
2477 }
2478 syntax::ModelItem::Routes(block) => {
2479 set_once(&mut sections.routes, block, "routes")?
2480 }
2481 syntax::ModelItem::Derive(block) => {
2482 set_once(&mut sections.derive, block, "derive")?
2483 }
2484 syntax::ModelItem::Dynamics(block) => {
2485 set_once(&mut sections.dynamics, block, "dynamics")?
2486 }
2487 syntax::ModelItem::Outputs(block) => {
2488 set_once(&mut sections.outputs, block, "outputs")?
2489 }
2490 syntax::ModelItem::Analytical(block) => {
2491 set_once(&mut sections.analytical, block, "analytical")?
2492 }
2493 syntax::ModelItem::Init(block) => set_once(&mut sections.init, block, "init")?,
2494 syntax::ModelItem::Drift(block) => set_once(&mut sections.drift, block, "drift")?,
2495 syntax::ModelItem::Diffusion(block) => {
2496 set_once(&mut sections.diffusion, block, "diffusion")?
2497 }
2498 syntax::ModelItem::Particles(block) => {
2499 set_once(&mut sections.particles, block, "particles")?
2500 }
2501 }
2502 }
2503 Ok(sections)
2504 }
2505}
2506
2507fn set_once<'a, T>(slot: &mut Option<&'a T>, value: &'a T, name: &str) -> Result<(), SemanticError>
2508where
2509 T: HasSpan,
2510{
2511 if let Some(existing) = *slot {
2512 return Err(SemanticAssist::default()
2513 .context_label(
2514 existing.span(),
2515 format!("`{name}` section first declared here"),
2516 )
2517 .help(format!("each model can declare `{name}` at most once"))
2518 .apply(SemanticError::new(
2519 format!("duplicate `{name}` section in model body"),
2520 value.span(),
2521 )));
2522 }
2523 *slot = Some(value);
2524 Ok(())
2525}
2526
2527fn best_similar_name_assist(
2528 needle: &str,
2529 candidates: Vec<SimilarNameCandidate>,
2530) -> Option<SemanticAssist> {
2531 let original_needle = needle;
2532 let needle = needle.to_ascii_lowercase();
2533 let mut best: Option<((usize, usize, usize), SemanticAssist)> = None;
2534 let mut tied = false;
2535
2536 for candidate in candidates {
2537 if candidate.lookup_name == original_needle {
2538 continue;
2539 }
2540 let lookup = candidate.lookup_name.to_ascii_lowercase();
2541 let distance = if is_single_adjacent_transposition(&needle, &lookup) {
2542 1
2543 } else {
2544 edit_distance(&needle, &lookup)
2545 };
2546 let prefix = common_prefix_len(&needle, &lookup);
2547 if !is_high_confidence_match(&needle, &lookup, distance, prefix) {
2548 continue;
2549 }
2550 let score = (
2551 distance,
2552 usize::MAX - prefix,
2553 needle.len().abs_diff(lookup.len()),
2554 );
2555 match &best {
2556 None => {
2557 best = Some((score, candidate.assist));
2558 tied = false;
2559 }
2560 Some((best_score, _)) if score < *best_score => {
2561 best = Some((score, candidate.assist));
2562 tied = false;
2563 }
2564 Some((best_score, _)) if score == *best_score => tied = true,
2565 _ => {}
2566 }
2567 }
2568
2569 if tied {
2570 None
2571 } else {
2572 best.map(|(_, assist)| assist)
2573 }
2574}
2575
2576trait HasSpan {
2577 fn span(&self) -> Span;
2578}
2579
2580impl HasSpan for syntax::ParametersBlock {
2581 fn span(&self) -> Span {
2582 self.span
2583 }
2584}
2585impl HasSpan for syntax::ConstantsBlock {
2586 fn span(&self) -> Span {
2587 self.span
2588 }
2589}
2590impl HasSpan for syntax::CovariatesBlock {
2591 fn span(&self) -> Span {
2592 self.span
2593 }
2594}
2595impl HasSpan for syntax::StatesBlock {
2596 fn span(&self) -> Span {
2597 self.span
2598 }
2599}
2600impl HasSpan for syntax::RoutesBlock {
2601 fn span(&self) -> Span {
2602 self.span
2603 }
2604}
2605impl HasSpan for syntax::StatementBlock {
2606 fn span(&self) -> Span {
2607 self.span
2608 }
2609}
2610impl HasSpan for syntax::AnalyticalBlock {
2611 fn span(&self) -> Span {
2612 self.span
2613 }
2614}
2615impl HasSpan for syntax::ParticlesDecl {
2616 fn span(&self) -> Span {
2617 self.span
2618 }
2619}
2620
2621fn collect_bare_assignment_names(
2622 statements: &[syntax::Stmt],
2623 seen: &mut BTreeSet<String>,
2624 output: &mut Vec<syntax::Ident>,
2625) {
2626 for statement in statements {
2627 match &statement.kind {
2628 syntax::StmtKind::Assign(assign) => {
2629 if let syntax::AssignTargetKind::Name(name) = &assign.target.kind {
2630 if seen.insert(name.text.clone()) {
2631 output.push(name.clone());
2632 }
2633 }
2634 }
2635 syntax::StmtKind::If(if_stmt) => {
2636 collect_bare_assignment_names(&if_stmt.then_branch, seen, output);
2637 if let Some(else_branch) = &if_stmt.else_branch {
2638 collect_bare_assignment_names(else_branch, seen, output);
2639 }
2640 }
2641 syntax::StmtKind::For(for_stmt) => {
2642 collect_bare_assignment_names(&for_stmt.body, seen, output);
2643 }
2644 syntax::StmtKind::Let(_) => {}
2645 }
2646 }
2647}
2648
2649fn number_to_const(value: f64) -> ConstValue {
2650 if value.is_finite()
2651 && value.fract() == 0.0
2652 && value >= i64::MIN as f64
2653 && value <= i64::MAX as f64
2654 {
2655 ConstValue::Int(value as i64)
2656 } else {
2657 ConstValue::Real(value)
2658 }
2659}
2660
2661fn promote_numeric(lhs: ValueType, rhs: ValueType) -> ValueType {
2662 if lhs == ValueType::Real || rhs == ValueType::Real {
2663 ValueType::Real
2664 } else {
2665 ValueType::Int
2666 }
2667}
2668
2669fn intersect_sets(set_a: &BTreeSet<SymbolId>, set_b: &BTreeSet<SymbolId>) -> BTreeSet<SymbolId> {
2670 set_a.intersection(set_b).copied().collect()
2671}
2672
2673fn map_binary_op(op: syntax::BinaryOp) -> TypedBinaryOp {
2674 match op {
2675 syntax::BinaryOp::Or => TypedBinaryOp::Or,
2676 syntax::BinaryOp::And => TypedBinaryOp::And,
2677 syntax::BinaryOp::Eq => TypedBinaryOp::Eq,
2678 syntax::BinaryOp::NotEq => TypedBinaryOp::NotEq,
2679 syntax::BinaryOp::Lt => TypedBinaryOp::Lt,
2680 syntax::BinaryOp::LtEq => TypedBinaryOp::LtEq,
2681 syntax::BinaryOp::Gt => TypedBinaryOp::Gt,
2682 syntax::BinaryOp::GtEq => TypedBinaryOp::GtEq,
2683 syntax::BinaryOp::Add => TypedBinaryOp::Add,
2684 syntax::BinaryOp::Sub => TypedBinaryOp::Sub,
2685 syntax::BinaryOp::Mul => TypedBinaryOp::Mul,
2686 syntax::BinaryOp::Div => TypedBinaryOp::Div,
2687 syntax::BinaryOp::Pow => TypedBinaryOp::Pow,
2688 }
2689}
2690
2691fn call_result_type(intrinsic: MathIntrinsic, args: &[TypedExpr]) -> ValueType {
2692 match intrinsic {
2693 MathIntrinsic::Abs => args.first().map_or(ValueType::Real, |arg| arg.ty),
2694 MathIntrinsic::Min | MathIntrinsic::Max => args
2695 .iter()
2696 .map(|arg| arg.ty)
2697 .reduce(promote_numeric)
2698 .unwrap_or(ValueType::Real),
2699 MathIntrinsic::Floor
2700 | MathIntrinsic::Ceil
2701 | MathIntrinsic::Exp
2702 | MathIntrinsic::Ln
2703 | MathIntrinsic::Log
2704 | MathIntrinsic::Log10
2705 | MathIntrinsic::Log2
2706 | MathIntrinsic::Pow
2707 | MathIntrinsic::Round
2708 | MathIntrinsic::Sin
2709 | MathIntrinsic::Cos
2710 | MathIntrinsic::Tan
2711 | MathIntrinsic::Sqrt => ValueType::Real,
2712 }
2713}
2714
2715fn fold_unary(op: TypedUnaryOp, value: &ConstValue) -> Option<ConstValue> {
2716 match (op, value) {
2717 (TypedUnaryOp::Plus, ConstValue::Int(value)) => Some(ConstValue::Int(*value)),
2718 (TypedUnaryOp::Plus, ConstValue::Real(value)) => Some(ConstValue::Real(*value)),
2719 (TypedUnaryOp::Minus, ConstValue::Int(value)) => Some(ConstValue::Int(-value)),
2720 (TypedUnaryOp::Minus, ConstValue::Real(value)) => Some(ConstValue::Real(-value)),
2721 (TypedUnaryOp::Not, ConstValue::Bool(value)) => Some(ConstValue::Bool(!value)),
2722 _ => None,
2723 }
2724}
2725
2726fn fold_binary(op: TypedBinaryOp, lhs: &ConstValue, rhs: &ConstValue) -> Option<ConstValue> {
2727 match op {
2728 TypedBinaryOp::Or => Some(ConstValue::Bool(
2729 matches!(lhs, ConstValue::Bool(true)) || matches!(rhs, ConstValue::Bool(true)),
2730 )),
2731 TypedBinaryOp::And => Some(ConstValue::Bool(
2732 matches!(lhs, ConstValue::Bool(true)) && matches!(rhs, ConstValue::Bool(true)),
2733 )),
2734 TypedBinaryOp::Eq => Some(ConstValue::Bool(lhs == rhs)),
2735 TypedBinaryOp::NotEq => Some(ConstValue::Bool(lhs != rhs)),
2736 TypedBinaryOp::Lt => Some(ConstValue::Bool(lhs.as_f64()? < rhs.as_f64()?)),
2737 TypedBinaryOp::LtEq => Some(ConstValue::Bool(lhs.as_f64()? <= rhs.as_f64()?)),
2738 TypedBinaryOp::Gt => Some(ConstValue::Bool(lhs.as_f64()? > rhs.as_f64()?)),
2739 TypedBinaryOp::GtEq => Some(ConstValue::Bool(lhs.as_f64()? >= rhs.as_f64()?)),
2740 TypedBinaryOp::Add => fold_numeric(
2741 lhs,
2742 rhs,
2743 |left, right| left + right,
2744 |left, right| left + right,
2745 ),
2746 TypedBinaryOp::Sub => fold_numeric(
2747 lhs,
2748 rhs,
2749 |left, right| left - right,
2750 |left, right| left - right,
2751 ),
2752 TypedBinaryOp::Mul => fold_numeric(
2753 lhs,
2754 rhs,
2755 |left, right| left * right,
2756 |left, right| left * right,
2757 ),
2758 TypedBinaryOp::Div => Some(ConstValue::Real(lhs.as_f64()? / rhs.as_f64()?)),
2759 TypedBinaryOp::Pow => Some(ConstValue::Real(lhs.as_f64()?.powf(rhs.as_f64()?))),
2760 }
2761}
2762
2763fn fold_numeric(
2764 lhs: &ConstValue,
2765 rhs: &ConstValue,
2766 int_op: impl FnOnce(i64, i64) -> i64,
2767 real_op: impl FnOnce(f64, f64) -> f64,
2768) -> Option<ConstValue> {
2769 match (lhs, rhs) {
2770 (ConstValue::Int(lhs), ConstValue::Int(rhs)) => Some(ConstValue::Int(int_op(*lhs, *rhs))),
2771 _ => Some(ConstValue::Real(real_op(lhs.as_f64()?, rhs.as_f64()?))),
2772 }
2773}
2774
2775fn fold_call(intrinsic: MathIntrinsic, values: &[ConstValue]) -> Option<ConstValue> {
2776 match intrinsic {
2777 MathIntrinsic::Abs => match values.first()? {
2778 ConstValue::Int(value) => Some(ConstValue::Int(value.abs())),
2779 ConstValue::Real(value) => Some(ConstValue::Real(value.abs())),
2780 ConstValue::Bool(_) => None,
2781 },
2782 MathIntrinsic::Ceil => Some(ConstValue::Real(values.first()?.as_f64()?.ceil())),
2783 MathIntrinsic::Exp => Some(ConstValue::Real(values.first()?.as_f64()?.exp())),
2784 MathIntrinsic::Floor => Some(ConstValue::Real(values.first()?.as_f64()?.floor())),
2785 MathIntrinsic::Ln | MathIntrinsic::Log => {
2786 Some(ConstValue::Real(values.first()?.as_f64()?.ln()))
2787 }
2788 MathIntrinsic::Log10 => Some(ConstValue::Real(values.first()?.as_f64()?.log10())),
2789 MathIntrinsic::Log2 => Some(ConstValue::Real(values.first()?.as_f64()?.log2())),
2790 MathIntrinsic::Max => Some(ConstValue::Real(
2791 values.first()?.as_f64()?.max(values.get(1)?.as_f64()?),
2792 )),
2793 MathIntrinsic::Min => Some(ConstValue::Real(
2794 values.first()?.as_f64()?.min(values.get(1)?.as_f64()?),
2795 )),
2796 MathIntrinsic::Pow => Some(ConstValue::Real(
2797 values.first()?.as_f64()?.powf(values.get(1)?.as_f64()?),
2798 )),
2799 MathIntrinsic::Round => Some(ConstValue::Real(values.first()?.as_f64()?.round())),
2800 MathIntrinsic::Sin => Some(ConstValue::Real(values.first()?.as_f64()?.sin())),
2801 MathIntrinsic::Cos => Some(ConstValue::Real(values.first()?.as_f64()?.cos())),
2802 MathIntrinsic::Tan => Some(ConstValue::Real(values.first()?.as_f64()?.tan())),
2803 MathIntrinsic::Sqrt => Some(ConstValue::Real(values.first()?.as_f64()?.sqrt())),
2804 }
2805}
2806
2807#[cfg(test)]
2808mod tests {
2809 use super::*;
2810 use crate::test_fixtures::{
2811 RECOMMENDED_STYLE_AUTHORING, RECOMMENDED_STYLE_CANONICAL, STRUCTURED_BLOCK_CORPUS,
2812 };
2813 use crate::RouteKind;
2814 use crate::{parse_model, parse_module};
2815
2816 #[test]
2817 fn analyzes_structured_block_corpus() {
2818 let src = STRUCTURED_BLOCK_CORPUS;
2819 let module = parse_module(src).expect("structured-block fixture parses");
2820 let typed = analyze_module(&module).expect("structured-block fixture analyzes");
2821
2822 assert_eq!(typed.models.len(), 4);
2823 let transit = &typed.models[1];
2824 assert_eq!(transit.kind, ModelKind::Ode);
2825 assert_eq!(transit.states[0].size, Some(4));
2826 assert!(transit.dynamics.is_some());
2827
2828 let analytical = &typed.models[2];
2829 assert!(matches!(
2830 analytical.analytical.as_ref().map(|value| value.structure),
2831 Some(AnalyticalKernel::OneCompartmentWithAbsorption)
2832 ));
2833
2834 let sde = &typed.models[3];
2835 assert_eq!(sde.particles, Some(1000));
2836 assert!(sde.drift.is_some());
2837 assert!(sde.diffusion.is_some());
2838 }
2839
2840 #[test]
2841 fn derives_values_across_if_branches() {
2842 let src = STRUCTURED_BLOCK_CORPUS;
2843 let model = parse_model(src.split("\n\n\n").next().unwrap()).expect("single model parses");
2844 let typed = analyze_model(&model).expect("single model analyzes");
2845 let ke_symbol = typed
2846 .symbols
2847 .iter()
2848 .find(|symbol| symbol.name == "ke")
2849 .expect("derived symbol exists");
2850 assert!(matches!(ke_symbol.ty, SymbolType::Scalar(ValueType::Real)));
2851 }
2852
2853 #[test]
2854 fn analytical_model_accepts_straight_line_required_derived_assignment() {
2855 let src = r#"
2856model analytical_ok {
2857 kind analytical
2858 parameters { ka, ke0, v }
2859 states { depot, central }
2860 routes { oral -> depot }
2861 derive {
2862 ke = ke0
2863 }
2864 analytical {
2865 structure = one_compartment_with_absorption
2866 }
2867 outputs {
2868 cp = central / v
2869 }
2870}
2871"#;
2872
2873 let model = parse_model(src).expect("model parses");
2874 let typed = analyze_model(&model).expect("model analyzes");
2875 assert!(matches!(
2876 typed.analytical.as_ref().map(|value| value.structure),
2877 Some(AnalyticalKernel::OneCompartmentWithAbsorption)
2878 ));
2879 }
2880
2881 #[test]
2882 fn analytical_model_accepts_required_derived_assignment_across_if_else() {
2883 let src = r#"
2884model analytical_ok {
2885 kind analytical
2886 parameters { ka, ke0, v }
2887 states { depot, central }
2888 routes { oral -> depot }
2889 derive {
2890 if true {
2891 ke = ke0
2892 } else {
2893 ke = ke0 * 2.0
2894 }
2895 }
2896 analytical {
2897 structure = one_compartment_with_absorption
2898 }
2899 outputs {
2900 cp = central / v
2901 }
2902}
2903"#;
2904
2905 let model = parse_model(src).expect("model parses");
2906 analyze_model(&model).expect("model analyzes");
2907 }
2908
2909 #[test]
2910 fn analytical_model_accepts_loop_updates_after_initial_derived_assignment() {
2911 let src = r#"
2912model analytical_ok {
2913 kind analytical
2914 parameters { ka, ke0, v }
2915 states { depot, central }
2916 routes { oral -> depot }
2917 derive {
2918 ke = ke0
2919 for step in 0..2 {
2920 ke = ke + 0.0
2921 }
2922 }
2923 analytical {
2924 structure = one_compartment_with_absorption
2925 }
2926 outputs {
2927 cp = central / v
2928 }
2929}
2930"#;
2931
2932 let model = parse_model(src).expect("model parses");
2933 analyze_model(&model).expect("model analyzes");
2934 }
2935
2936 #[test]
2937 fn analytical_model_rejects_missing_required_structure_name_across_params_and_derived() {
2938 let src = r#"
2939model analytical_broken {
2940 kind analytical
2941 parameters { ka, kel, v }
2942 states { depot, central }
2943 routes { oral -> depot }
2944 analytical {
2945 structure = one_compartment_with_absorption
2946 }
2947 outputs {
2948 cp = central / v
2949 }
2950}
2951"#;
2952
2953 let model = parse_model(src).expect("model parses");
2954 let err = analyze_model(&model).expect_err("missing required structure name must fail");
2955 assert!(err
2956 .render(src)
2957 .contains("analytical structure `one_compartment_with_absorption` requires `ke`"));
2958 assert!(err
2959 .render(src)
2960 .contains("did you mean `ke` instead of `kel`?"));
2961 }
2962
2963 #[test]
2964 fn analytical_model_rejects_overlap_between_params_and_derive_assigned_names() {
2965 let src = r#"
2966model analytical_broken {
2967 kind analytical
2968 parameters { ka, ke, v }
2969 states { depot, central }
2970 routes { oral -> depot }
2971 derive {
2972 ke = ke
2973 }
2974 analytical {
2975 structure = one_compartment_with_absorption
2976 }
2977 outputs {
2978 cp = central / v
2979 }
2980}
2981"#;
2982
2983 let model = parse_model(src).expect("model parses");
2984 let err = analyze_model(&model).expect_err("param/derived overlap must fail");
2985 assert!(err
2986 .render(src)
2987 .contains("derived name `ke` collides with primary parameter `ke`"));
2988 assert!(err
2989 .render(src)
2990 .contains("names declared in `params` and derive-assigned names must be distinct"));
2991 }
2992
2993 #[test]
2994 fn analytical_model_rejects_non_bare_derive_target() {
2995 let src = r#"
2996model analytical_broken {
2997 kind analytical
2998 parameters { ka, ke0, v }
2999 states { depot, central }
3000 routes { oral -> depot }
3001 derive {
3002 ddt(central) = ke0
3003 }
3004 analytical {
3005 structure = one_compartment_with_absorption
3006 }
3007 outputs {
3008 cp = central / v
3009 }
3010}
3011"#;
3012
3013 let model = parse_model(src).expect("model parses");
3014 let err = analyze_model(&model).expect_err("non-bare derive target must fail");
3015 assert!(err
3016 .render(src)
3017 .contains("derive assignments must target a bare identifier"));
3018 }
3019
3020 #[test]
3021 fn analytical_model_rejects_conditionally_assigned_required_derived_name() {
3022 let src = r#"
3023model analytical_broken {
3024 kind analytical
3025 parameters { ka, ke0, v }
3026 states { depot, central }
3027 routes { oral -> depot }
3028 derive {
3029 if true {
3030 ke = ke0
3031 }
3032 }
3033 analytical {
3034 structure = one_compartment_with_absorption
3035 }
3036 outputs {
3037 cp = central / v
3038 }
3039}
3040"#;
3041
3042 let model = parse_model(src).expect("model parses");
3043 let err = analyze_model(&model)
3044 .expect_err("conditionally assigned required derived name must fail");
3045 assert!(err.render(src).contains(
3046 "derived value `ke` is not definitely assigned on all control-flow paths before analytical structure `one_compartment_with_absorption` uses it"
3047 ));
3048 assert!(err
3049 .render(src)
3050 .contains("assign `ke` on every control-flow path in `derive` before the analytical structure runs"));
3051 }
3052
3053 #[test]
3054 fn analytical_model_rejects_loop_only_required_derived_assignment() {
3055 let src = r#"
3056model analytical_broken {
3057 kind analytical
3058 parameters { ka, ke0, v }
3059 states { depot, central }
3060 routes { oral -> depot }
3061 derive {
3062 for step in 0..2 {
3063 ke = ke0
3064 }
3065 }
3066 analytical {
3067 structure = one_compartment_with_absorption
3068 }
3069 outputs {
3070 cp = central / v
3071 }
3072}
3073"#;
3074
3075 let model = parse_model(src).expect("model parses");
3076 let err =
3077 analyze_model(&model).expect_err("loop-only required derived assignment must fail");
3078 assert!(err.render(src).contains(
3079 "derived value `ke` is not definitely assigned on all control-flow paths before analytical structure `one_compartment_with_absorption` uses it"
3080 ));
3081 }
3082
3083 #[test]
3084 fn analytical_model_authoring_surface_accepts_declared_derived_assignment() {
3085 let src = r#"
3086 name = analytical_authoring
3087 kind = analytical
3088 params = ka, ke0, v
3089 derived = ke
3090 states = depot, central
3091 outputs = cp
3092
3093 bolus(oral) -> depot
3094
3095 ke = ke0
3096 structure = one_compartment_with_absorption
3097 out(cp) = central / v ~ continuous()
3098 "#;
3099
3100 let model = parse_model(src).expect("authoring model parses");
3101 analyze_model(&model).expect("authoring model analyzes");
3102 }
3103
3104 #[test]
3105 fn analytical_model_authoring_surface_rejects_undeclared_derived_assignment() {
3106 let src = r#"
3107 name = analytical_authoring
3108 kind = analytical
3109 params = ka, ke0, v
3110 derived = kel
3111 states = depot, central
3112 outputs = cp
3113
3114 bolus(oral) -> depot
3115
3116 ke = ke0
3117 structure = one_compartment_with_absorption
3118 out(cp) = central / v ~ continuous()
3119 "#;
3120
3121 let err = parse_model(src).expect_err("undeclared derived assignment must fail");
3122 assert!(err
3123 .render(src)
3124 .contains("derived value `ke` is not declared in `derived = ...`"));
3125 }
3126
3127 #[test]
3128 fn analytical_model_authoring_surface_rejects_param_derived_overlap() {
3129 let src = r#"
3130 name = analytical_authoring
3131 kind = analytical
3132 params = ka, ke, v
3133 derived = ke
3134 states = depot, central
3135 outputs = cp
3136
3137 bolus(oral) -> depot
3138
3139 structure = one_compartment_with_absorption
3140 out(cp) = central / v ~ continuous()
3141 "#;
3142
3143 let err = parse_model(src).expect_err("param/derived overlap must fail");
3144 assert!(err
3145 .render(src)
3146 .contains("derived name `ke` collides with primary parameter `ke`"));
3147 assert!(err
3148 .render(src)
3149 .contains("names declared in `params` and `derived` must be distinct"));
3150 }
3151
3152 #[test]
3153 fn authoring_fixture_preserves_route_kind_while_remaining_equivalent() {
3154 let authoring_surface = RECOMMENDED_STYLE_AUTHORING;
3155 let canonical = RECOMMENDED_STYLE_CANONICAL;
3156
3157 let authoring_model = parse_model(authoring_surface).expect("authoring model parses");
3158 let canonical_model = parse_model(canonical).expect("canonical model parses");
3159
3160 let authoring_typed = analyze_model(&authoring_model).expect("authoring model analyzes");
3161 let canonical_typed = analyze_model(&canonical_model).expect("canonical model analyzes");
3162
3163 assert_eq!(
3164 typed_model_signature(&authoring_typed),
3165 typed_model_signature(&canonical_typed)
3166 );
3167 assert_eq!(authoring_typed.routes[0].kind, Some(RouteKind::Bolus));
3168 assert_eq!(canonical_typed.routes[0].kind, None);
3169 }
3170
3171 #[test]
3172 fn rejects_unknown_route_in_rate_call() {
3173 let src = r#"
3174model broken {
3175 kind ode
3176 states { central }
3177 dynamics {
3178 ddt(central) = rate(oral)
3179 }
3180 outputs {
3181 cp = central
3182 }
3183}
3184"#;
3185 let model = parse_model(src).expect("model parses");
3186 let err = analyze_model(&model).expect_err("unknown route must fail");
3187 assert!(err.render(src).contains("unknown route `oral`"));
3188 }
3189
3190 #[test]
3191 fn suggests_similar_state_name_for_unknown_identifier() {
3192 let src = r#"
3193model broken {
3194 kind ode
3195 states { central }
3196 dynamics {
3197 ddt(central) = 0
3198 }
3199 outputs {
3200 cp = cental
3201 }
3202}
3203"#;
3204 let model = parse_model(src).expect("model parses");
3205 let err = analyze_model(&model).expect_err("unknown identifier must fail");
3206
3207 assert!(err
3208 .diagnostic()
3209 .suggestions
3210 .iter()
3211 .any(|suggestion| suggestion.message.contains("did you mean `central`?")));
3212 assert!(err
3213 .render(src)
3214 .contains("suggestion: did you mean `central`?"));
3215 }
3216
3217 #[test]
3218 fn suggests_case_variant_for_unknown_identifier() {
3219 let src = r#"
3220model broken {
3221 kind ode
3222 parameters { Ke }
3223 states { central }
3224 dynamics {
3225 ddt(central) = -ke * central
3226 }
3227 outputs {
3228 cp = central
3229 }
3230}
3231"#;
3232 let model = parse_model(src).expect("model parses");
3233 let err = analyze_model(&model).expect_err("case-mismatched identifier must fail");
3234
3235 assert!(err
3236 .diagnostic()
3237 .suggestions
3238 .iter()
3239 .any(|suggestion| suggestion.message.contains("did you mean `Ke`?")));
3240 assert!(err.render(src).contains("suggestion: did you mean `Ke`?"));
3241 }
3242
3243 #[test]
3244 fn suggests_similar_intrinsic_for_unknown_function() {
3245 let src = r#"
3246model broken {
3247 kind ode
3248 states { central }
3249 dynamics {
3250 ddt(central) = 0
3251 }
3252 outputs {
3253 cp = sqt(central)
3254 }
3255}
3256"#;
3257 let model = parse_model(src).expect("model parses");
3258 let err = analyze_model(&model).expect_err("unknown function must fail");
3259
3260 assert!(err
3261 .diagnostic()
3262 .suggestions
3263 .iter()
3264 .any(|suggestion| suggestion.message.contains("did you mean `sqrt`?")));
3265 assert!(err.render(src).contains("suggestion: did you mean `sqrt`?"));
3266 }
3267
3268 #[test]
3269 fn route_scalar_usage_reports_help_and_context() {
3270 let src = r#"
3271model broken {
3272 kind ode
3273 states { central }
3274 routes { oral -> central }
3275 dynamics {
3276 ddt(central) = oral
3277 }
3278 outputs {
3279 cp = central
3280 }
3281}
3282"#;
3283 let model = parse_model(src).expect("model parses");
3284 let err = analyze_model(&model).expect_err("route scalar usage must fail");
3285
3286 assert!(err
3287 .diagnostic()
3288 .helps
3289 .iter()
3290 .any(|help| help.contains("route inputs are read through `rate(oral)`")));
3291 assert!(err.render(src).contains("route `oral` declared here"));
3292 assert!(err
3293 .render(src)
3294 .contains("suggestion: did you mean `rate(oral)`?"));
3295 }
3296
3297 #[test]
3298 fn output_scope_violation_reports_help_and_context() {
3299 let src = r#"
3300model broken {
3301 kind ode
3302 states { central }
3303 dynamics {
3304 ddt(central) = cp
3305 }
3306 outputs {
3307 cp = central
3308 }
3309}
3310"#;
3311 let model = parse_model(src).expect("model parses");
3312 let err = analyze_model(&model).expect_err("output scope violation must fail");
3313
3314 assert!(
3315 err.diagnostic()
3316 .helps
3317 .iter()
3318 .any(|help| help
3319 .contains("outputs are assignment targets inside the `outputs` block"))
3320 );
3321 assert!(err.render(src).contains("output `cp` declared here"));
3322 }
3323
3324 #[test]
3325 fn reserved_name_reports_rename_suggestion() {
3326 let src = r#"
3327model broken {
3328 kind ode
3329 parameters { log }
3330 states { central }
3331 dynamics {
3332 ddt(central) = 0
3333 }
3334 outputs {
3335 cp = central
3336 }
3337}
3338"#;
3339 let model = parse_model(src).expect("model parses");
3340 let err = analyze_model(&model).expect_err("reserved name must fail");
3341
3342 assert!(err.render(src).contains("rename `log` to `log_value`"));
3343 assert!(err
3344 .diagnostic()
3345 .suggestions
3346 .iter()
3347 .any(|suggestion| suggestion
3348 .edits
3349 .iter()
3350 .any(|edit| edit.replacement == "log_value")));
3351 }
3352
3353 #[test]
3354 fn duplicate_constant_points_to_first_declaration() {
3355 let src = r#"
3356model broken {
3357 kind ode
3358 constants {
3359 ka = 1
3360 ka = 2
3361 }
3362 states { central }
3363 dynamics {
3364 ddt(central) = 0
3365 }
3366 outputs {
3367 cp = central
3368 }
3369}
3370"#;
3371 let model = parse_model(src).expect("model parses");
3372 let err = analyze_model(&model).expect_err("duplicate constant must fail");
3373
3374 assert!(err
3375 .render(src)
3376 .contains("constant `ka` first declared here"));
3377 assert!(err.render(src).contains("rename this constant to `ka_2`"));
3378 }
3379
3380 #[test]
3381 fn rejects_missing_output_assignment_on_all_paths() {
3382 let src = r#"
3383model broken {
3384 kind ode
3385 states { central }
3386 dynamics {
3387 ddt(central) = 0
3388 }
3389 outputs {
3390 if true {
3391 cp = central
3392 }
3393 }
3394}
3395"#;
3396 let model = parse_model(src).expect("model parses");
3397 let err = analyze_model(&model).expect_err("partial output assignment must fail");
3398 assert!(err
3399 .render(src)
3400 .contains("output `cp` is not definitely assigned on all control-flow paths"));
3401 }
3402
3403 #[test]
3404 fn rejects_non_integer_array_size() {
3405 let src = r#"
3406model broken {
3407 kind ode
3408 constants { n = 1.5 }
3409 states { transit[n] }
3410 dynamics {
3411 ddt(transit[0]) = 0
3412 }
3413 outputs {
3414 cp = 0
3415 }
3416}
3417"#;
3418 let model = parse_model(src).expect("model parses");
3419 let err = analyze_model(&model).expect_err("non-integer array size must fail");
3420 assert!(err
3421 .render(src)
3422 .contains("state array size must be an integer constant"));
3423 }
3424
3425 fn typed_model_signature(model: &TypedModel) -> String {
3426 let mut lines = Vec::new();
3427 lines.push(format!("kind:{:?}", model.kind));
3428 lines.push(format!(
3429 "parameters:{}",
3430 join_names(model, &model.parameters)
3431 ));
3432 lines.push(format!("constants:{}", join_constants(model)));
3433 lines.push(format!("covariates:{}", join_covariates(model)));
3434 lines.push(format!("states:{}", join_states(model)));
3435 lines.push(format!("routes:{}", join_routes(model)));
3436 lines.push(format!("derived:{}", join_names(model, &model.derived)));
3437 lines.push(format!("outputs:{}", join_names(model, &model.outputs)));
3438 lines.push(format!("particles:{:?}", model.particles));
3439 lines.push(format!(
3440 "analytical:{:?}",
3441 model.analytical.as_ref().map(|value| value.structure)
3442 ));
3443 lines.push(format!(
3444 "derive:{}",
3445 model
3446 .derive
3447 .as_ref()
3448 .map(|block| block_signature(model, block))
3449 .unwrap_or_default()
3450 ));
3451 lines.push(format!(
3452 "dynamics:{}",
3453 model
3454 .dynamics
3455 .as_ref()
3456 .map(|block| block_signature(model, block))
3457 .unwrap_or_default()
3458 ));
3459 lines.push(format!(
3460 "init:{}",
3461 model
3462 .init
3463 .as_ref()
3464 .map(|block| block_signature(model, block))
3465 .unwrap_or_default()
3466 ));
3467 lines.push(format!(
3468 "drift:{}",
3469 model
3470 .drift
3471 .as_ref()
3472 .map(|block| block_signature(model, block))
3473 .unwrap_or_default()
3474 ));
3475 lines.push(format!(
3476 "diffusion:{}",
3477 model
3478 .diffusion
3479 .as_ref()
3480 .map(|block| block_signature(model, block))
3481 .unwrap_or_default()
3482 ));
3483 lines.push(format!(
3484 "outputs_block:{}",
3485 block_signature(model, &model.outputs_block)
3486 ));
3487 lines.join("\n")
3488 }
3489
3490 fn join_names(model: &TypedModel, ids: &[SymbolId]) -> String {
3491 ids.iter()
3492 .map(|id| symbol_name(model, *id))
3493 .collect::<Vec<_>>()
3494 .join(",")
3495 }
3496
3497 fn join_constants(model: &TypedModel) -> String {
3498 model
3499 .constants
3500 .iter()
3501 .map(|constant| {
3502 format!(
3503 "{}={:?}",
3504 symbol_name(model, constant.symbol),
3505 constant.value
3506 )
3507 })
3508 .collect::<Vec<_>>()
3509 .join(",")
3510 }
3511
3512 fn join_covariates(model: &TypedModel) -> String {
3513 model
3514 .covariates
3515 .iter()
3516 .map(|covariate| {
3517 format!(
3518 "{}@{:?}",
3519 symbol_name(model, covariate.symbol),
3520 covariate.interpolation
3521 )
3522 })
3523 .collect::<Vec<_>>()
3524 .join(",")
3525 }
3526
3527 fn join_states(model: &TypedModel) -> String {
3528 model
3529 .states
3530 .iter()
3531 .map(|state| format!("{}[{:#?}]", symbol_name(model, state.symbol), state.size))
3532 .collect::<Vec<_>>()
3533 .join(",")
3534 }
3535
3536 fn join_routes(model: &TypedModel) -> String {
3537 model
3538 .routes
3539 .iter()
3540 .map(|route| {
3541 let destination = state_place_signature(model, &route.destination);
3542 let properties = route
3543 .properties
3544 .iter()
3545 .map(|property| {
3546 format!(
3547 "{:?}={}",
3548 property.kind,
3549 expr_signature(model, &property.value)
3550 )
3551 })
3552 .collect::<Vec<_>>()
3553 .join("|");
3554 format!(
3555 "{}->{}{{{}}}",
3556 symbol_name(model, route.symbol),
3557 destination,
3558 properties
3559 )
3560 })
3561 .collect::<Vec<_>>()
3562 .join(",")
3563 }
3564
3565 fn block_signature(model: &TypedModel, block: &TypedStatementBlock) -> String {
3566 block
3567 .statements
3568 .iter()
3569 .map(|stmt| stmt_signature(model, stmt))
3570 .collect::<Vec<_>>()
3571 .join(";")
3572 }
3573
3574 fn stmt_signature(model: &TypedModel, stmt: &TypedStmt) -> String {
3575 match &stmt.kind {
3576 TypedStmtKind::Let(value) => format!(
3577 "let({}:{})",
3578 symbol_name(model, value.symbol),
3579 expr_signature(model, &value.value)
3580 ),
3581 TypedStmtKind::Assign(value) => format!(
3582 "assign({}={})",
3583 assign_target_signature(model, &value.target),
3584 expr_signature(model, &value.value)
3585 ),
3586 TypedStmtKind::If(value) => format!(
3587 "if({}){{{}}}else{{{}}}",
3588 expr_signature(model, &value.condition),
3589 value
3590 .then_branch
3591 .iter()
3592 .map(|stmt| stmt_signature(model, stmt))
3593 .collect::<Vec<_>>()
3594 .join(";"),
3595 value
3596 .else_branch
3597 .as_ref()
3598 .map(|branch| branch
3599 .iter()
3600 .map(|stmt| stmt_signature(model, stmt))
3601 .collect::<Vec<_>>()
3602 .join(";"))
3603 .unwrap_or_default()
3604 ),
3605 TypedStmtKind::For(value) => format!(
3606 "for({}:{}..{}){{{}}}",
3607 symbol_name(model, value.binding),
3608 expr_signature(model, &value.range.start),
3609 expr_signature(model, &value.range.end),
3610 value
3611 .body
3612 .iter()
3613 .map(|stmt| stmt_signature(model, stmt))
3614 .collect::<Vec<_>>()
3615 .join(";")
3616 ),
3617 }
3618 }
3619
3620 fn assign_target_signature(model: &TypedModel, target: &TypedAssignTarget) -> String {
3621 match &target.kind {
3622 TypedAssignTargetKind::Derived(symbol) => {
3623 format!("derived:{}", symbol_name(model, *symbol))
3624 }
3625 TypedAssignTargetKind::Output(symbol) => {
3626 format!("output:{}", symbol_name(model, *symbol))
3627 }
3628 TypedAssignTargetKind::StateInit(place) => {
3629 format!("init:{}", state_place_signature(model, place))
3630 }
3631 TypedAssignTargetKind::Derivative(place) => {
3632 format!("ddt:{}", state_place_signature(model, place))
3633 }
3634 TypedAssignTargetKind::Noise(place) => {
3635 format!("noise:{}", state_place_signature(model, place))
3636 }
3637 }
3638 }
3639
3640 fn state_place_signature(model: &TypedModel, place: &TypedStatePlace) -> String {
3641 let name = symbol_name(model, place.state);
3642 match &place.index {
3643 Some(index) => format!("{}[{}]", name, expr_signature(model, index)),
3644 None => name,
3645 }
3646 }
3647
3648 fn expr_signature(model: &TypedModel, expr: &TypedExpr) -> String {
3649 match &expr.kind {
3650 TypedExprKind::Literal(value) => format!("lit:{value:?}:{:?}", expr.ty),
3651 TypedExprKind::Symbol(symbol) => {
3652 format!("sym:{}:{:?}", symbol_name(model, *symbol), expr.ty)
3653 }
3654 TypedExprKind::StateValue(place) => format!(
3655 "state:{}:{:?}",
3656 state_place_signature(model, place),
3657 expr.ty
3658 ),
3659 TypedExprKind::Unary { op, expr: inner } => {
3660 format!("un:{op:?}:{}", expr_signature(model, inner))
3661 }
3662 TypedExprKind::Binary { op, lhs, rhs } => format!(
3663 "bin:{op:?}:{}:{}:{:?}",
3664 expr_signature(model, lhs),
3665 expr_signature(model, rhs),
3666 expr.ty
3667 ),
3668 TypedExprKind::Call { callee, args } => format!(
3669 "call:{}({})",
3670 match callee {
3671 TypedCall::Math(intrinsic) => format!("math:{intrinsic:?}"),
3672 TypedCall::Rate(symbol) => format!("rate:{}", symbol_name(model, *symbol)),
3673 },
3674 args.iter()
3675 .map(|arg| expr_signature(model, arg))
3676 .collect::<Vec<_>>()
3677 .join(",")
3678 ),
3679 }
3680 }
3681
3682 fn symbol_name(model: &TypedModel, symbol: SymbolId) -> String {
3683 model
3684 .symbols
3685 .iter()
3686 .find(|entry| entry.id == symbol)
3687 .map(|entry| entry.name.clone())
3688 .unwrap_or_else(|| format!("#{symbol}"))
3689 }
3690}