Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions benches/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,9 +265,9 @@ pub fn assay_error_models() -> AssayErrorModels {
.expect("plasma assay error model")
}

/// Residual error model. Indexes outputs by dense `usize` — assumes a single output at index 0.
/// Residual error model for the `plasma` output.
pub fn residual_error_models() -> ResidualErrorModels {
ResidualErrorModels::new().add(0, ResidualErrorModel::constant(0.2))
ResidualErrorModels::new().add("plasma", ResidualErrorModel::constant(0.2))
}

// ───────────────────────────── Handwritten factories ─────────────────
Expand Down
125 changes: 1 addition & 124 deletions pharmsol-dsl/src/analyze.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,7 @@ use crate::name_match::{
common_prefix_len, edit_distance, is_high_confidence_match, is_single_adjacent_transposition,
};
use crate::syntax;
use crate::{
ModelKind, RouteKind, MAX_CONST_USIZE, NUMERIC_OUTPUT_PREFIX, NUMERIC_ROUTE_PREFIX,
RATE_FUNCTION_NAME,
};
use crate::{ModelKind, RouteKind, MAX_CONST_USIZE, RATE_FUNCTION_NAME};

const RESERVED_NAMES: &[&str] = &[
"abs",
Expand Down Expand Up @@ -628,7 +625,6 @@ impl<'a> Analyzer<'a> {
let mut route_kinds = BTreeMap::<String, BTreeSet<Option<RouteKind>>>::new();
if let Some(block) = block {
for route in &block.routes {
self.validate_route_label_name(&route.input)?;
let (id, prefer_for_rate) = match route_kinds.get_mut(&route.input.text) {
Some(seen_kinds) => {
let duplicate = route.kind.is_none()
Expand Down Expand Up @@ -885,9 +881,6 @@ impl<'a> Analyzer<'a> {
collect_bare_assignment_names(statements, &mut seen, &mut collected_idents);
let mut symbols = Vec::new();
for ident in collected_idents {
if matches!(kind, SymbolKind::Output) {
self.validate_output_label_name(&ident)?;
}
if matches!(kind, SymbolKind::Derived) {
if let Some(parameter) = self.globals.parameters.get(&ident.text).copied() {
return Err(AnalysisAssist::default()
Expand Down Expand Up @@ -1634,18 +1627,12 @@ impl<'a> Analyzer<'a> {
callee.span,
));
}
if let syntax::ExprKind::Number(value) = &args[0].kind {
if let Some(suffix) = numeric_label_literal_suffix(*value) {
return Err(self.bare_numeric_route_error(args[0].span, &suffix));
}
}
let syntax::ExprKind::Name(route_name) = &args[0].kind else {
return Err(AnalysisError::new(
"`rate` expects a route identifier argument",
args[0].span,
));
};
self.validate_route_label_name(route_name)?;
let route = self
.globals
.routes
Expand Down Expand Up @@ -2067,100 +2054,6 @@ impl<'a> Analyzer<'a> {
Ok(id)
}

fn validate_route_label_name(&self, label: &syntax::Ident) -> Result<(), AnalysisError> {
if let Some(suffix) = bare_numeric_label(&label.text) {
return Err(self.bare_numeric_route_error(label.span, suffix));
}
if let Some(suffix) = canonical_numeric_suffix(&label.text, NUMERIC_OUTPUT_PREFIX) {
return Err(self.wrong_prefix_route_error(label, suffix));
}
Ok(())
}

fn validate_output_label_name(&self, label: &syntax::Ident) -> Result<(), AnalysisError> {
if let Some(suffix) = bare_numeric_label(&label.text) {
return Err(self.bare_numeric_output_error(label.span, suffix));
}
if let Some(suffix) = canonical_numeric_suffix(&label.text, NUMERIC_ROUTE_PREFIX) {
return Err(self.wrong_prefix_output_error(label, suffix));
}
Ok(())
}

fn bare_numeric_route_error(&self, span: Span, suffix: &str) -> AnalysisError {
let replacement = format!("{NUMERIC_ROUTE_PREFIX}{suffix}");
AnalysisAssist::default()
.help("numeric route labels must use the `input_<n>` form in authored DSL")
.replacement_suggestion(
span,
replacement.clone(),
format!("use `{replacement}`"),
Applicability::Always,
)
.apply(AnalysisError::new(
format!(
"bare numeric route labels are not allowed in the DSL; use `{replacement}` instead"
),
span,
))
}

fn bare_numeric_output_error(&self, span: Span, suffix: &str) -> AnalysisError {
let replacement = format!("{NUMERIC_OUTPUT_PREFIX}{suffix}");
AnalysisAssist::default()
.help("numeric output labels must use the `outeq_<n>` form in authored DSL")
.replacement_suggestion(
span,
replacement.clone(),
format!("use `{replacement}`"),
Applicability::Always,
)
.apply(AnalysisError::new(
format!(
"bare numeric output labels are not allowed in the DSL; use `{replacement}` instead"
),
span,
))
}

fn wrong_prefix_route_error(&self, label: &syntax::Ident, suffix: &str) -> AnalysisError {
let replacement = format!("{NUMERIC_ROUTE_PREFIX}{suffix}");
AnalysisAssist::default()
.help("numeric route labels use the `input_<n>` prefix")
.replacement_suggestion(
label.span,
replacement.clone(),
format!("use `{replacement}`"),
Applicability::Always,
)
.apply(AnalysisError::new(
format!(
"`{}` is an output label and cannot be used as a route; use `{replacement}` here",
label.text
),
label.span,
))
}

fn wrong_prefix_output_error(&self, label: &syntax::Ident, suffix: &str) -> AnalysisError {
let replacement = format!("{NUMERIC_OUTPUT_PREFIX}{suffix}");
AnalysisAssist::default()
.help("numeric output labels use the `outeq_<n>` prefix")
.replacement_suggestion(
label.span,
replacement.clone(),
format!("use `{replacement}`"),
Applicability::Always,
)
.apply(AnalysisError::new(
format!(
"`{}` is a route label and cannot be used as an output target; use `{replacement}` here",
label.text
),
label.span,
))
}

fn insert_local_symbol(
&mut self,
env: &mut BlockEnv,
Expand Down Expand Up @@ -2726,22 +2619,6 @@ fn allows_route_output_name_overlap(existing: SymbolKind, new: SymbolKind) -> bo
)
}

fn bare_numeric_label(src: &str) -> Option<&str> {
(!src.is_empty() && src.chars().all(|ch| ch.is_ascii_digit())).then_some(src)
}

fn canonical_numeric_suffix<'a>(src: &'a str, prefix: &str) -> Option<&'a str> {
let suffix = src.strip_prefix(prefix)?;
(!suffix.is_empty() && suffix.chars().all(|ch| ch.is_ascii_digit())).then_some(suffix)
}

fn numeric_label_literal_suffix(value: f64) -> Option<String> {
// `usize::MAX as f64` rounds up to 2^64; keep the bound exclusive so the
// cast below never saturates.
(value.is_finite() && value >= 0.0 && value.fract() == 0.0 && value < usize::MAX as f64)
.then(|| (value as usize).to_string())
}

#[derive(Default)]
struct Globals {
all_names: BTreeMap<String, SymbolId>,
Expand Down
96 changes: 1 addition & 95 deletions pharmsol-dsl/src/authoring.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use super::syntax::*;
use crate::name_match::{
common_prefix_len, edit_distance, is_high_confidence_match, is_single_adjacent_transposition,
};
use crate::{NUMERIC_OUTPUT_PREFIX, NUMERIC_ROUTE_PREFIX, RATE_FUNCTION_NAME};
use crate::RATE_FUNCTION_NAME;

const DEFAULT_MODEL_NAME: &str = "main";

Expand Down Expand Up @@ -1638,49 +1638,6 @@ fn parse_label_segment(src: &str, abs_start: usize, kind: LabelKind) -> Result<I
));
}

if let Some(suffix) = bare_numeric_label(trimmed) {
let replacement = kind.canonical_label(suffix);
return Err(ParseError::new(
format!(
"bare numeric {} labels are not allowed in the DSL; use `{replacement}` instead",
kind.noun()
),
span,
)
.with_help(format!(
"numeric {} labels must use the `{}` prefix in authored DSL",
kind.noun(),
kind.prefix_pattern()
))
.with_suggestion(DiagnosticSuggestion {
message: format!("use `{replacement}`"),
edits: vec![TextEdit { span, replacement }],
applicability: Applicability::Always,
}));
}

if let Some(suffix) = canonical_numeric_suffix(trimmed, kind.wrong_prefix()) {
let replacement = kind.canonical_label(suffix);
return Err(ParseError::new(
format!(
"`{trimmed}` is {} label and cannot be used as {}; use `{replacement}` here",
kind.wrong_kind_phrase(),
kind.noun_phrase()
),
span,
)
.with_help(format!(
"numeric {} labels use the `{}` prefix",
kind.noun(),
kind.prefix_pattern()
))
.with_suggestion(DiagnosticSuggestion {
message: format!("use `{replacement}`"),
edits: vec![TextEdit { span, replacement }],
applicability: Applicability::Always,
}));
}

Ok(Ident::new(trimmed, span))
}

Expand All @@ -1697,57 +1654,6 @@ impl LabelKind {
Self::Output => "output label",
}
}

fn noun(self) -> &'static str {
match self {
Self::Route => "route",
Self::Output => "output",
}
}

fn noun_phrase(self) -> &'static str {
match self {
Self::Route => "a route",
Self::Output => "an output",
}
}

fn wrong_kind_phrase(self) -> &'static str {
match self {
Self::Route => "an output",
Self::Output => "a route",
}
}

fn canonical_label(self, suffix: &str) -> String {
match self {
Self::Route => format!("{NUMERIC_ROUTE_PREFIX}{suffix}"),
Self::Output => format!("{NUMERIC_OUTPUT_PREFIX}{suffix}"),
}
}

fn wrong_prefix(self) -> &'static str {
match self {
Self::Route => NUMERIC_OUTPUT_PREFIX,
Self::Output => NUMERIC_ROUTE_PREFIX,
}
}

fn prefix_pattern(self) -> &'static str {
match self {
Self::Route => "input_<n>",
Self::Output => "outeq_<n>",
}
}
}

fn bare_numeric_label(src: &str) -> Option<&str> {
(!src.is_empty() && src.chars().all(|ch| ch.is_ascii_digit())).then_some(src)
}

fn canonical_numeric_suffix<'a>(src: &'a str, prefix: &str) -> Option<&'a str> {
let suffix = src.strip_prefix(prefix)?;
(!suffix.is_empty() && suffix.chars().all(|ch| ch.is_ascii_digit())).then_some(suffix)
}

fn parse_place_at(src: &str, abs_start: usize) -> Result<Place, ParseError> {
Expand Down
18 changes: 9 additions & 9 deletions pharmsol-dsl/src/execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1573,20 +1573,20 @@ out(cp) = central / v ~ continuous()
}

#[test]
fn canonical_numeric_channel_names_flow_into_execution_metadata_and_abi() {
let src = r#"name = canonical_numeric_channels
fn numeric_channel_names_flow_into_execution_metadata_and_abi() {
let src = r#"name = numeric_channels
kind = ode

params = ke, v
states = depot, central
outputs = cp, outeq_2
outputs = cp, 2


dx(depot) = bolus(input_10) - ke * depot
dx(depot) = bolus(10) - ke * depot
dx(central) = infusion(iv) - ke * central

out(cp) = central / v
out(outeq_2) = depot / v
out(2) = depot / v
"#;

let model = crate::parse_model(src).expect("authoring model parses");
Expand All @@ -1600,7 +1600,7 @@ out(outeq_2) = depot / v
.iter()
.map(|route| route.name.as_str())
.collect::<Vec<_>>(),
vec!["input_10", "iv"]
vec!["10", "iv"]
);
assert_eq!(
compiled
Expand All @@ -1609,7 +1609,7 @@ out(outeq_2) = depot / v
.iter()
.map(|output| output.name.as_str())
.collect::<Vec<_>>(),
vec!["cp", "outeq_2"]
vec!["cp", "2"]
);
assert_eq!(
compiled
Expand All @@ -1619,7 +1619,7 @@ out(outeq_2) = depot / v
.iter()
.map(|slot| slot.name.as_str())
.collect::<Vec<_>>(),
vec!["input_10", "iv"]
vec!["10", "iv"]
);
assert_eq!(
compiled
Expand All @@ -1629,7 +1629,7 @@ out(outeq_2) = depot / v
.iter()
.map(|slot| slot.name.as_str())
.collect::<Vec<_>>(),
vec!["cp", "outeq_2"]
vec!["cp", "2"]
);
}

Expand Down
4 changes: 0 additions & 4 deletions pharmsol-dsl/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,6 @@ pub mod syntax;
#[cfg(test)]
mod test_fixtures;

/// Canonical prefix for numeric route labels such as `input_1`.
pub const NUMERIC_ROUTE_PREFIX: &str = "input_";
/// Canonical prefix for numeric output labels such as `outeq_1`.
pub const NUMERIC_OUTPUT_PREFIX: &str = "outeq_";
/// Upper bound for compile-time-resolved sizes: state array sizes, route
/// destination indices, and particle counts. Keeps compiled models within a
/// comfortably allocatable state space.
Expand Down
Loading
Loading