use std::collections::BTreeSet; use crate::diagnostic::Diagnostic; use crate::source::{SourceLayout, SourceRole, Span}; use crate::syntax::{self, TypeRef}; use crate::types::{Definition, DefinitionKind, Field, Shape, Type, Variant}; use super::{Checked, Context, cycles, path_name}; impl Context<'_> { pub(super) fn owns_private_fields(&self, id: usize) -> bool { let definition = &self.types[id]; if definition.kind != crate::types::DefinitionKind::Model { return definition.owner == self.owner; } let caller = &self.symbols.packages[self.owner].layout; let owner = &self.symbols.packages[definition.owner].layout; caller.role() == Some(crate::source::SourceRole::App) && caller.domain() == owner.domain() } pub(super) fn is_test(&self) -> bool { matches!( &self.symbols.packages[self.owner].layout, SourceLayout::Test { .. } ) } fn fragment_main(&self) -> bool { !self.symbols.strict_layout && matches!( &self.symbols.packages[self.owner].layout, SourceLayout::Main ) } pub(super) fn candidates(&self, name: &str) -> Vec { let scope = &self.symbols.packages[self.owner]; if !name.contains('.') { return vec![match scope.layout.role() { Some(SourceRole::App) => { format!("{}.{name}", scope.layout.app_prefix().unwrap()) } _ => format!("{}.{name}", scope.name), }]; } let mut candidates = Vec::new(); if let SourceLayout::Role { component, domain, .. } | SourceLayout::Test { component, domain, .. } = &scope.layout { if let Some(rest) = name.strip_prefix("app.") { candidates.push(format!("{component}.{domain}.{rest}")); } else if matches!( name.split('.').next(), Some("domain" | "model" | "port" | "adapter" | "api" | "job") ) { candidates.push(format!("{component}.{domain}.{name}")); } candidates.push(name.to_owned()); candidates.push(format!("{component}.{name}")); } else { candidates.push(name.to_owned()); } candidates.dedup(); candidates } pub(super) fn has_function(&self, name: &str, arity: usize) -> bool { self.candidates(name).iter().any(|candidate| { self.symbols .functions .contains_key(&(candidate.clone(), arity)) }) } pub(super) fn named_type(&self, name: &str, span: Span) -> Checked { let (name, id) = self .candidates(name) .into_iter() .find_map(|candidate| { self.symbols .types .get(&candidate) .copied() .map(|id| (candidate, id)) }) .ok_or_else(|| Diagnostic::error("C004", format!("unknown type '{name}'"), span))?; if !self.can_access_type(id) { let target = &self.symbols.packages[self.types[id].owner]; let legacy = matches!( &self.symbols.packages[self.owner].layout, SourceLayout::Loose ) || matches!(&target.layout, SourceLayout::Loose) || self.fragment_main(); return Err(Diagnostic::error( "C006", if legacy { format!("type '{name}' is private") } else { format!("type '{name}' is outside the current domain boundary") }, span, )); } Ok(id) } pub(super) fn variant(&self, name: &str, span: Span) -> Checked> { let Some((type_name, variant_name)) = name.rsplit_once('.') else { return Ok(None); }; let Some(id) = self .candidates(type_name) .into_iter() .find_map(|name| self.symbols.types.get(&name).copied()) else { return Ok(None); }; self.named_type(type_name, span)?; let Shape::Choice(variants) = &self.types[id].shape else { return Ok(None); }; let variant = variants .iter() .position(|variant| variant.name == variant_name) .ok_or_else(|| Diagnostic::error("C004", format!("unknown variant '{name}'"), span))?; Ok(Some((id, variant))) } pub(super) fn related_variant( &self, name: &str, arguments: &[TypeRef], span: Span, ) -> Checked)>> { let variant = match name { "Related.Unloaded" => 0, "Related.Loaded" => 1, _ => return Ok(None), }; if arguments.len() != 1 { return Err(Diagnostic::error( "C005", "Related variant patterns require one Model type argument", span, )); } let payload = self.resolve_type(&arguments[0])?; if !related_payload(&payload, self.types) { return Err(Diagnostic::error( "C005", "Related payload must be a Model, nullable Model or List", arguments[0].span, )); } let fields = if variant == 0 { Vec::new() } else { vec![Field { name: "value".into(), ty: payload.clone(), private: false, bounds: Vec::new(), }] }; Ok(Some((Type::Related(Box::new(payload)), variant, fields))) } pub(super) fn function(&self, name: &str, arity: usize, span: Span) -> Checked { let (qualified, id) = self .candidates(name) .into_iter() .find_map(|candidate| { self.symbols .functions .get(&(candidate.clone(), arity)) .copied() .map(|id| (candidate, id)) }) .ok_or_else(|| { Diagnostic::error( "C004", format!("unknown function '{name}' with {arity} argument(s)"), span, ) })?; if !self.can_call(id) { let target = &self.symbols.packages[self.functions[id].owner]; let legacy = matches!( &self.symbols.packages[self.owner].layout, SourceLayout::Loose ) || matches!(&target.layout, SourceLayout::Loose) || self.fragment_main(); return Err(Diagnostic::error( "C006", if legacy { format!("function '{qualified}' is private") } else { format!("function '{qualified}' is not callable from this source role") }, span, )); } Ok(id) } fn can_call(&self, function: usize) -> bool { if matches!( self.functions[function].kind, crate::syntax::FunctionKind::Job { .. } ) { return false; } if self.functions[function].implementation { return false; } let target = &self.symbols.packages[self.functions[function].owner]; let caller = &self.symbols.packages[self.owner]; if caller.layout.role() == Some(SourceRole::Job) && target.bundled { return false; } if matches!(&target.layout, SourceLayout::Test { .. }) { return matches!(&caller.layout, SourceLayout::Test { .. }) && self.functions[function].owner == self.owner; } if target.bundled { return self.functions[function].owner == self.owner || self.functions[function].public; } if self.fragment_main() { return self.functions[function].owner == self.owner || self.functions[function].public; } if target.layout.role() == Some(SourceRole::Api) { return false; } if matches!(&caller.layout, SourceLayout::Loose) || matches!(&target.layout, SourceLayout::Loose) { return self.functions[function].owner == self.owner || self.functions[function].public; } if matches!(&caller.layout, SourceLayout::Test { .. }) { return match target.layout.role() { Some(SourceRole::App) => true, Some(SourceRole::Domain) => caller.layout.domain() == target.layout.domain(), Some( SourceRole::Model | SourceRole::Port | SourceRole::Adapter | SourceRole::Api | SourceRole::Job, ) | None => false, }; } match caller.layout.role() { None => target.layout.role() == Some(SourceRole::App), Some(SourceRole::Api | SourceRole::Job) => { target.layout.role() == Some(SourceRole::App) && caller.layout.domain() == target.layout.domain() } Some(SourceRole::App) => { target.layout.role() == Some(SourceRole::App) || (matches!( target.layout.role(), Some(SourceRole::Domain | SourceRole::Port) ) && caller.layout.domain() == target.layout.domain()) } Some(SourceRole::Domain) => { target.layout.role() == Some(SourceRole::Domain) && caller.layout.domain() == target.layout.domain() } Some(SourceRole::Adapter) => self.functions[function].owner == self.owner, Some(SourceRole::Model | SourceRole::Port) => false, } } fn can_access_type(&self, definition: usize) -> bool { let target = &self.symbols.packages[self.types[definition].owner]; let caller = &self.symbols.packages[self.owner]; if matches!(&target.layout, SourceLayout::Test { .. }) { return matches!(&caller.layout, SourceLayout::Test { .. }) && self.types[definition].owner == self.owner; } if target.bundled { return self.types[definition].owner == self.owner || self.types[definition].public; } if self.fragment_main() { return self.types[definition].owner == self.owner || self.types[definition].public; } if matches!(&caller.layout, SourceLayout::Loose) || matches!(&target.layout, SourceLayout::Loose) { return self.types[definition].owner == self.owner || self.types[definition].public; } if target.layout.role() == Some(SourceRole::Model) { return self.types[definition].public && !matches!( caller.layout.role(), Some(SourceRole::Domain | SourceRole::Port | SourceRole::Adapter) ); } if self.types[definition].owner == self.owner { return true; } if matches!(&caller.layout, SourceLayout::Test { .. }) { return match target.layout.role() { Some(SourceRole::App) => self.types[definition].public, Some(SourceRole::Domain | SourceRole::Port) => { caller.layout.domain() == target.layout.domain() } Some(SourceRole::Model) => self.types[definition].public, Some(SourceRole::Adapter | SourceRole::Api | SourceRole::Job) | None => false, }; } if matches!( caller.layout.role(), Some(SourceRole::Port | SourceRole::Adapter) ) { if target.layout.role() == Some(SourceRole::App) && self.types[definition].public && caller.layout.domain() == target.layout.domain() { let name = &self.types[definition].name; return if caller.layout.role() == Some(SourceRole::Port) { caller.contract_types.contains(name) } else { self.symbols.packages.iter().any(|port| { caller.implemented_ports.contains(&port.name) && port.contract_types.contains(name) }) }; } return target.layout.role() == Some(SourceRole::Port) && caller.implemented_ports.contains(&target.name); } match target.layout.role() { Some(SourceRole::App) => self.types[definition].public, Some(SourceRole::Domain | SourceRole::Port) => { caller.layout.domain() == target.layout.domain() && (caller.layout.role() == Some(SourceRole::App) || caller.layout.role() == target.layout.role()) } Some(SourceRole::Adapter | SourceRole::Api | SourceRole::Model | SourceRole::Job) | None => false, } } pub(super) fn resolve_type(&self, reference: &TypeRef) -> Checked { let arguments = reference .arguments .iter() .map(|ty| self.resolve_type(ty)) .collect::>>()?; if arguments.iter().any(|ty| ty == &Type::Upload) { return Err(Diagnostic::error( "C005", "Upload cannot be stored in a container", reference.span, )); } let name = path_name(&reference.name); let ty = match (name.as_str(), arguments.as_slice()) { ("Bool", []) => Type::Bool, ("Int", []) => Type::Int, ("Decimal", []) => Type::Decimal, ("Float", []) => Type::Float, ("Text", []) => Type::Text, ("Id", []) => Type::Id, ("Bytes", []) => Type::Bytes, ("Secret", []) => Type::Secret, ("Upload", []) => Type::Upload, ("Uuid", []) => Type::Uuid, ("DateTime", []) => Type::DateTime, ("Date", []) => Type::Date, ("Time", []) => Type::Time, ("Duration", []) => Type::Duration, ("Json", []) => Type::Json, ("dever.system.File", []) => Type::File, ("dever.system.Socket", []) => Type::Socket, ("dever.system.Listener", []) => Type::Listener, ("dever.system.HttpReply", []) => Type::HttpReply, ("dever.system.WebSocket", []) => Type::WebSocket, ("dever.system.ClientTls", []) => Type::ClientTls, ("dever.system.ServerTls", []) => Type::ServerTls, ("dever.system.HttpClient", []) => Type::HttpClient, ("List", [element]) => Type::List(Box::new(element.clone())), ("Map", [key, value]) => Type::Map(Box::new(key.clone()), Box::new(value.clone())), ("MapEntry", [key, value]) => { Type::MapEntry(Box::new(key.clone()), Box::new(value.clone())) } ("Stream", [element]) => Type::Stream(Box::new(element.clone())), ("AsyncStream", [element]) => Type::AsyncStream(Box::new(element.clone())), ("Channel", [element]) => Type::Channel(Box::new(element.clone())), ("Related", [element]) if related_payload(element, self.types) => { Type::Related(Box::new(element.clone())) } ("Related", [_]) => { return Err(Diagnostic::error( "C005", "Related payload must be a Model, nullable Model or List", reference.span, )); } ("List" | "Map" | "MapEntry" | "Stream" | "AsyncStream" | "Channel" | "Related", _) => { return Err(Diagnostic::error( "C005", "incorrect number of type arguments", reference.span, )); } (_, []) => Type::Named(self.named_type(&name, reference.span)?), _ => { return Err(Diagnostic::error( "C005", "user-defined generic types are not supported", reference.span, )); } }; if reference.nullable && ty == Type::Upload { return Err(Diagnostic::error( "C005", "Upload cannot be nullable", reference.span, )); } Ok(if reference.nullable { ty.nullable() } else { ty }) } pub(super) fn fields(&self, fields: &[syntax::Field]) -> Checked> { let mut names = BTreeSet::new(); fields .iter() .map(|field| { if !names.insert(&field.name.text) { return Err(Diagnostic::error( "C002", "duplicate field or output name", field.span, )); } let ty = self.resolve_type(&field.ty)?; Ok(Field { name: field.name.text.clone(), bounds: super::clauses::field_bounds(field, &ty, self)?, ty, private: field.private, }) }) .collect() } pub(super) fn shape(&self, shape: &syntax::TypeShape) -> Checked { match shape { syntax::TypeShape::Record(fields) => Ok(Shape::Record(self.fields(fields)?)), syntax::TypeShape::Choice(variants) => { let mut names = BTreeSet::new(); Ok(Shape::Choice( variants .iter() .map(|variant| { if !names.insert(&variant.name.text) { return Err(Diagnostic::error( "C002", "duplicate choice variant", variant.span, )); } Ok(Variant { name: variant.name.text.clone(), fields: self.fields(&variant.payload)?, error: variant.error, label: variant.label.clone(), }) }) .collect::>()?, )) } } } pub(super) fn map_key(&self, ty: &Type) -> bool { map_key(ty, self.types) } pub(super) fn value_fields(&self, ty: &Type) -> Option> { match ty { Type::Named(id) => match &self.types[*id].shape { Shape::Record(fields) => Some(fields.clone()), _ => None, }, Type::MapEntry(key, value) => Some(vec![ Field { name: "key".into(), ty: *key.clone(), private: false, bounds: Vec::new(), }, Field { name: "value".into(), ty: *value.clone(), private: false, bounds: Vec::new(), }, ]), Type::Outputs(fields) => Some(fields.clone()), _ => None, } } pub(super) fn model_choice(&self, name: &str, span: Span) -> Checked> { let Some(id) = self .candidates(name) .into_iter() .find_map(|name| self.symbols.types.get(&name).copied()) else { return Ok(None); }; if self.types[id].kind != DefinitionKind::ModelChoice { return Ok(None); } self.named_type(name, span).map(Some) } } fn map_key(ty: &Type, definitions: &[Definition]) -> bool { match ty { Type::Bool | Type::Int | Type::Text | Type::Id => true, Type::Named(id) => { definitions[*id].kind == DefinitionKind::ModelId || matches!(&definitions[*id].shape, Shape::Choice(variants) if variants.iter().all(|variant| variant.fields.is_empty())) } _ => false, } } pub(super) fn referenced_types(ty: &Type, visit: &mut impl FnMut(usize)) { match ty { Type::Named(id) => visit(*id), Type::List(element) | Type::Nullable(element) | Type::Stream(element) | Type::AsyncStream(element) | Type::RowStream(element) | Type::Channel(element) => referenced_types(element, visit), Type::Map(key, value) | Type::MapEntry(key, value) => { referenced_types(key, visit); referenced_types(value, visit); } Type::Outputs(fields) | Type::Task(fields) => { for field in fields { referenced_types(&field.ty, visit); } } _ => {} } } pub(super) fn shape_fields(shape: &Shape) -> Vec<&Field> { match shape { Shape::Record(fields) => fields.iter().collect(), Shape::Choice(variants) => variants .iter() .flat_map(|variant| &variant.fields) .collect(), } } pub(super) fn validate_maps(ty: &Type, types: &[Definition], span: Span) -> Checked<()> { match ty { Type::Map(key, value) | Type::MapEntry(key, value) => { if !map_key(key, types) { return Err(Diagnostic::error("C005", "invalid Map key type", span)); } validate_maps(value, types, span) } Type::List(element) | Type::Nullable(element) | Type::Stream(element) | Type::AsyncStream(element) | Type::RowStream(element) | Type::Related(element) | Type::Channel(element) => validate_maps(element, types, span), _ => Ok(()), } } pub(super) fn validate_type_constraints( ty: &Type, types: &[Definition], span: Span, ) -> Checked<()> { validate_maps(ty, types, span)?; validate_transfer_containers(ty, types, span) } fn validate_transfer_containers(ty: &Type, types: &[Definition], span: Span) -> Checked<()> { match ty { Type::AsyncStream(element) | Type::RowStream(element) | Type::Channel(element) => { if !element.transferable(types) { let container = if matches!(ty, Type::AsyncStream(_) | Type::RowStream(_)) { "AsyncStream" } else { "Channel" }; return Err(Diagnostic::error( "C005", format!("{container} item type must be transferable between threads"), span, )); } validate_transfer_containers(element, types, span) } Type::List(element) | Type::Nullable(element) | Type::Related(element) | Type::Stream(element) => validate_transfer_containers(element, types, span), Type::Map(key, value) | Type::MapEntry(key, value) => { validate_transfer_containers(key, types, span)?; validate_transfer_containers(value, types, span) } _ => Ok(()), } } fn related_payload(ty: &Type, definitions: &[Definition]) -> bool { match ty { Type::Named(id) => definitions[*id].kind == DefinitionKind::Model, Type::Nullable(inner) => { matches!( inner.as_ref(), Type::Named(id) if definitions[*id].kind == DefinitionKind::Model ) } Type::List(inner) => { matches!( inner.as_ref(), Type::Named(id) if definitions[*id].kind == DefinitionKind::Model ) } _ => false, } } pub(super) fn validate_types(types: &[Definition], errors: &mut Vec) { let mut graph = vec![Vec::new(); types.len()]; for (id, definition) in types.iter().enumerate() { for field in shape_fields(&definition.shape) { let mut upload = false; crate::types::visit_type(&field.ty, &mut |ty| upload |= matches!(ty, Type::Upload)); if upload { errors.push(Diagnostic::error( "C005", "Upload cannot be stored in a record or choice", definition.span, )); } referenced_types(&field.ty, &mut |next| { graph[id].push((next, definition.span)) }); if let Err(error) = validate_maps(&field.ty, types, definition.span) { errors.push(error); } } } if let Some(span) = cycles::cycle(&graph) { errors.push(Diagnostic::error( "C007", "recursive type definitions are not allowed", span, )); } else if errors.is_empty() { crate::types::cache_properties(types, &graph); for definition in types { for field in shape_fields(&definition.shape) { if let Err(error) = validate_transfer_containers(&field.ty, types, definition.span) { errors.push(error); } } } } }