use crate::ast::{BinaryOp, Expr, Literal, TypeSpecifier};
use crate::model::{FhirType, Model, field_type, is_a, named_type, specializes, variant_type};
use fhir_types::schema::Kind;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum SemanticError {
#[error("{type_name} has no element {element}")]
UnknownElement {
type_name: String,
element: String,
},
#[error("the expression names the type {named}, the context is a {context}")]
ContextType {
named: String,
context: String,
},
#[error("{name} names no type of the model")]
UnknownType {
name: String,
},
#[error("{function}() takes a Boolean criterion, the expression gives a literal {found}")]
Criterion {
function: &'static str,
found: &'static str,
},
#[error("{function}() takes no {type_name}")]
Input {
function: &'static str,
type_name: String,
},
}
type Reach = Option<Vec<FhirType>>;
pub fn check(expr: &Expr, model: &dyn Model, context: FhirType) -> Result<(), SemanticError> {
check_any(expr, model, &[context])
}
pub fn check_any(
expr: &Expr,
model: &dyn Model,
contexts: &[FhirType],
) -> Result<(), SemanticError> {
let reach = Some(contexts.to_vec());
Checker { model }.infer(expr, &reach, &reach).map(|_| ())
}
struct Checker<'m> {
model: &'m dyn Model,
}
fn union(a: Reach, b: Reach) -> Reach {
let (mut a, b) = (a?, b?);
for ty in b {
if !a.contains(&ty) {
a.push(ty);
}
}
Some(a)
}
impl Checker<'_> {
fn infer(&self, expr: &Expr, focus: &Reach, this: &Reach) -> Result<Reach, SemanticError> {
match expr {
Expr::Literal(_) | Expr::Empty | Expr::External(_) | Expr::Index | Expr::Total => {
Ok(None)
}
Expr::This => Ok(this.clone()),
Expr::Member { input, name } => {
let from = match input {
Some(input) => self.infer(input, focus, this)?,
None => focus.clone(),
};
self.member(&from, name, input.is_none())
}
Expr::Call { input, name, args } => {
let from = match input {
Some(input) => self.infer(input, focus, this)?,
None => focus.clone(),
};
self.call(&from, name, args, focus, this)
}
Expr::Invocation { input, .. } => {
self.infer(input, focus, this)?;
Ok(None)
}
Expr::Indexer { input, index } => {
self.infer(index, focus, this)?;
self.infer(input, focus, this)
}
Expr::Negate(operand) | Expr::Plus(operand) => {
self.infer(operand, focus, this)?;
Ok(None)
}
Expr::Binary { op, left, right } => {
let a = self.infer(left, focus, this)?;
let b = match op {
BinaryOp::And | BinaryOp::Implies => self.infer(
right,
&self.narrow(left, true, focus),
&self.narrow(left, true, this),
)?,
BinaryOp::Or => self.infer(
right,
&self.narrow(left, false, focus),
&self.narrow(left, false, this),
)?,
_ => self.infer(right, focus, this)?,
};
Ok(if *op == BinaryOp::Union {
union(a, b)
} else {
None
})
}
Expr::Is {
operand,
type_specifier,
} => {
self.infer(operand, focus, this)?;
self.resolve(type_specifier)?;
Ok(None)
}
Expr::As {
operand,
type_specifier,
} => {
self.infer(operand, focus, this)?;
self.resolve(type_specifier)
}
Expr::SortKey { key, .. } => self.infer(key, focus, this),
}
}
fn resolve(&self, specifier: &TypeSpecifier) -> Result<Reach, SemanticError> {
let system = crate::value::SYSTEM_TYPES.contains(&specifier.name.as_str());
let fhir = || named_type(self.model, &specifier.name);
match specifier.namespace.as_deref() {
None | Some("FHIR") if fhir().is_some() => Ok(fhir().map(|ty| vec![ty])),
None | Some("System") if system => Ok(None),
_ => Err(SemanticError::UnknownType {
name: specifier.to_string(),
}),
}
}
fn narrow(&self, guard: &Expr, assume: bool, reach: &Reach) -> Reach {
match guard {
Expr::Is {
operand,
type_specifier,
} if matches!(operand.as_ref(), Expr::This) => {
self.narrow_to(type_specifier, assume, reach)
}
Expr::Call { input, name, args }
if name == "is" && input.as_deref().is_none_or(|i| matches!(i, Expr::This)) =>
{
match args.as_slice() {
[arg] => match arg.as_type_specifier() {
Some(specifier) => self.narrow_to(&specifier, assume, reach),
None => reach.clone(),
},
_ => reach.clone(),
}
}
Expr::Call {
input: Some(inner),
name,
args,
} if name == "not" && args.is_empty() => self.narrow(inner, !assume, reach),
Expr::Binary {
op: BinaryOp::And,
left,
right,
} => {
if assume {
self.narrow(right, true, &self.narrow(left, true, reach))
} else {
union(
self.narrow(left, false, reach),
self.narrow(right, false, reach),
)
}
}
Expr::Binary {
op: BinaryOp::Or,
left,
right,
} => {
if assume {
union(
self.narrow(left, true, reach),
self.narrow(right, true, reach),
)
} else {
self.narrow(right, false, &self.narrow(left, false, reach))
}
}
_ => reach.clone(),
}
}
fn narrow_to(&self, specifier: &TypeSpecifier, assume: bool, reach: &Reach) -> Reach {
let types = reach.as_ref()?;
let Ok(Some(named)) = self.resolve(specifier) else {
return reach.clone();
};
let Some(target) = named.first() else {
return reach.clone();
};
let mut kept: Vec<FhirType> = Vec::new();
for ty in types {
let found = if specializes(self.model, ty, target.name) {
assume.then_some(*ty)
} else if assume && specializes(self.model, target, ty.name) {
Some(*target)
} else if assume {
None
} else {
Some(*ty)
};
if let Some(found) = found
&& !kept.contains(&found)
{
kept.push(found);
}
}
Some(kept)
}
fn member(&self, from: &Reach, name: &str, at_root: bool) -> Result<Reach, SemanticError> {
let Some(types) = from else {
return Ok(None);
};
if types.is_empty() {
return Ok(Some(Vec::new()));
}
let mut reached: Vec<FhirType> = Vec::new();
let mut known = true;
let mut defined = false;
for ty in types {
if at_root && !ty.is_primitive() && is_a(self.model, ty, name) {
defined = true;
reached.push(*ty);
continue;
}
let schema = match ty.schema() {
Some(schema) => Some(schema),
None => self.model.schemas().type_named("Element"),
};
let Some(field) =
schema.and_then(|schema| schema.fields.iter().find(|field| field.name == name))
else {
continue;
};
defined = true;
match field.kind {
Kind::Choice(variants) => {
for (suffix, kind) in variants {
match variant_type(self.model, field, suffix, *kind) {
Some(found) if !reached.contains(&found) => reached.push(found),
Some(_) => {}
None => known = false,
}
}
}
kind => match field_type(self.model, field, kind) {
Some(found) if !reached.contains(&found) => reached.push(found),
Some(_) => {}
None => known = false,
},
}
}
if !defined {
let names = types
.iter()
.map(|ty| ty.name)
.collect::<Vec<_>>()
.join(" | ");
if at_root && named_type(self.model, name).is_some() {
return Err(SemanticError::ContextType {
named: String::from(name),
context: names,
});
}
return Err(SemanticError::UnknownElement {
type_name: names,
element: String::from(name),
});
}
Ok(known.then_some(reached))
}
fn call(
&self,
from: &Reach,
name: &str,
args: &[Expr],
focus: &Reach,
this: &Reach,
) -> Result<Reach, SemanticError> {
let lambdas: &[usize] = match name {
"where" | "exists" | "all" | "select" | "repeat" | "aggregate" => &[0],
"trace" => &[1],
"iif" => &[0, 1, 2],
_ => &[],
};
if name == "iif"
&& let Some(Expr::Literal(literal)) = args.first()
{
let found = match literal {
Literal::Boolean(_) => None,
Literal::String(_) => Some("String"),
Literal::Integer(_) => Some("Integer"),
Literal::Decimal(_) => Some("Decimal"),
Literal::Date(_) => Some("Date"),
Literal::DateTime(_) => Some("DateTime"),
Literal::Time(_) => Some("Time"),
Literal::Quantity(..) => Some("Quantity"),
};
if let Some(found) = found {
return Err(SemanticError::Criterion {
function: "iif",
found,
});
}
}
if name == "toString"
&& let Some(types) = from
&& !types.is_empty()
&& types
.iter()
.all(|ty| !ty.is_primitive() && !is_a(self.model, ty, "Quantity"))
{
return Err(SemanticError::Input {
function: "toString",
type_name: types
.iter()
.map(|ty| ty.name)
.collect::<Vec<_>>()
.join(" | "),
});
}
let mut outputs = Vec::new();
for (index, arg) in args.iter().enumerate() {
let typed_argument = matches!(name, "ofType" | "as" | "is") && index == 0;
if typed_argument {
if let Some(specifier) = arg.as_type_specifier() {
self.resolve(&specifier)?;
}
continue;
}
let reach = if name == "iif" && index > 0 {
let guarded = match args.first() {
Some(criterion) => self.narrow(criterion, index == 1, from),
None => from.clone(),
};
self.infer(arg, &guarded, &guarded)?
} else if lambdas.contains(&index) || name == "sort" {
self.infer(arg, from, from)?
} else {
self.infer(arg, focus, this)?
};
outputs.push(reach);
}
let first = outputs.first().cloned().flatten();
Ok(match name {
"where" => match args.first() {
Some(criterion) => self.narrow(criterion, true, from),
None => from.clone(),
},
"first" | "last" | "tail" | "skip" | "take" | "single" | "distinct" | "intersect"
| "exclude" | "trace" | "checkModifiers" | "slice" | "sort" => from.clone(),
"select" => first,
"union" | "combine" => union(from.clone(), first),
"extension" => named_type(self.model, "Extension").map(|ty| vec![ty]),
"ofType" | "as" => match args.first().and_then(Expr::as_type_specifier) {
Some(specifier) => self.resolve(&specifier)?,
None => None,
},
_ => None,
})
}
}