feat: publish Dever core with project scaffolding and versioned AI skills
This commit is contained in:
1 parent
70fa73cdc2
commit
ac458ffa7f
960 files changed
+221319
-1163
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,554 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use crate::diagnostic::Diagnostic;
|
||||
use crate::hir::{
|
||||
ApiRest, ApiRoute, AuthHook, Expression, ExpressionKind, Function, Permission, Program,
|
||||
RestPermissions,
|
||||
};
|
||||
use crate::source::{SourceRole, Span};
|
||||
use crate::syntax::{FunctionKind, Package};
|
||||
use crate::types::{Definition, DefinitionKind, Parameter, Shape, Type};
|
||||
|
||||
pub(super) struct Registration {
|
||||
pub hooks: Vec<AuthHook>,
|
||||
pub permissions: Vec<Permission>,
|
||||
}
|
||||
|
||||
struct SiteBinding {
|
||||
key: String,
|
||||
provider: String,
|
||||
}
|
||||
|
||||
pub(super) fn register(
|
||||
settings: Option<&dever_runtime::config::CompilationBindings>,
|
||||
packages: &[Package],
|
||||
functions: &[Function],
|
||||
types: &[Definition],
|
||||
routes: &mut [ApiRoute],
|
||||
rest: &mut [ApiRest],
|
||||
errors: &mut Vec<Diagnostic>,
|
||||
) -> Registration {
|
||||
let Some(settings) = settings else {
|
||||
return Registration {
|
||||
hooks: Vec::new(),
|
||||
permissions: Vec::new(),
|
||||
};
|
||||
};
|
||||
if routes.is_empty() && rest.is_empty() {
|
||||
return Registration {
|
||||
hooks: Vec::new(),
|
||||
permissions: Vec::new(),
|
||||
};
|
||||
}
|
||||
|
||||
let claims = types
|
||||
.iter()
|
||||
.position(|definition| definition.name == "dever.auth.Claims")
|
||||
.expect("official dever.auth.Claims type");
|
||||
let mut route_providers = Vec::with_capacity(routes.len());
|
||||
let mut rest_providers = Vec::with_capacity(rest.len());
|
||||
let mut providers = BTreeSet::new();
|
||||
|
||||
for route in routes.iter() {
|
||||
let binding = site_binding(settings, &route.directory, route.span, errors);
|
||||
if let Some(binding) = &binding {
|
||||
providers.insert(binding.provider.clone());
|
||||
}
|
||||
route_providers.push(binding);
|
||||
}
|
||||
for declaration in rest.iter() {
|
||||
let binding = site_binding(settings, &declaration.directory, declaration.span, errors);
|
||||
if let Some(binding) = &binding {
|
||||
providers.insert(binding.provider.clone());
|
||||
}
|
||||
rest_providers.push(binding);
|
||||
}
|
||||
|
||||
let mut hooks = Vec::new();
|
||||
let mut hook_by_provider = BTreeMap::new();
|
||||
for provider_key in providers {
|
||||
let Some(verify) = settings.providers.get(&provider_key) else {
|
||||
errors.push(Diagnostic::error(
|
||||
"C005",
|
||||
format!("site authentication provider '{provider_key}' is not configured"),
|
||||
first_provider_span(
|
||||
&provider_key,
|
||||
&route_providers,
|
||||
routes,
|
||||
&rest_providers,
|
||||
rest,
|
||||
),
|
||||
));
|
||||
continue;
|
||||
};
|
||||
let span = first_provider_span(
|
||||
&provider_key,
|
||||
&route_providers,
|
||||
routes,
|
||||
&rest_providers,
|
||||
rest,
|
||||
);
|
||||
let Some((function, identity)) =
|
||||
verify_contract(verify, claims, packages, functions, types, span, errors)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let hook = hooks.len();
|
||||
hooks.push(AuthHook {
|
||||
provider: provider_key.clone(),
|
||||
function,
|
||||
claims,
|
||||
identity,
|
||||
});
|
||||
hook_by_provider.insert(provider_key, hook);
|
||||
}
|
||||
|
||||
let mut permissions = Vec::new();
|
||||
let mut occupied = BTreeMap::new();
|
||||
for (route, binding) in routes.iter_mut().zip(&route_providers) {
|
||||
route.auth = binding
|
||||
.as_ref()
|
||||
.and_then(|binding| hook_by_provider.get(&binding.provider).copied());
|
||||
if !route.anonymous {
|
||||
route.permission = binding.as_ref().and_then(|binding| {
|
||||
register_permission(
|
||||
&mut permissions,
|
||||
&mut occupied,
|
||||
Permission {
|
||||
key: permission_key(
|
||||
&route.component,
|
||||
&route.domain,
|
||||
&binding.key,
|
||||
&route.action,
|
||||
),
|
||||
component: route.component.clone(),
|
||||
domain: route.domain.clone(),
|
||||
site: binding.key.clone(),
|
||||
action: route.action.clone(),
|
||||
method: route.method,
|
||||
},
|
||||
route.span,
|
||||
errors,
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
for (declaration, binding) in rest.iter_mut().zip(&rest_providers) {
|
||||
declaration.auth = binding
|
||||
.as_ref()
|
||||
.and_then(|binding| hook_by_provider.get(&binding.provider).copied());
|
||||
let Some(binding) = binding else { continue };
|
||||
let mut permission = |action, method| {
|
||||
register_permission(
|
||||
&mut permissions,
|
||||
&mut occupied,
|
||||
Permission {
|
||||
key: permission_key(
|
||||
&declaration.component,
|
||||
&declaration.domain,
|
||||
&binding.key,
|
||||
action,
|
||||
),
|
||||
component: declaration.component.clone(),
|
||||
domain: declaration.domain.clone(),
|
||||
site: binding.key.clone(),
|
||||
action: action.to_owned(),
|
||||
method,
|
||||
},
|
||||
declaration.span,
|
||||
errors,
|
||||
)
|
||||
};
|
||||
declaration.permissions = RestPermissions {
|
||||
read: permission("read", "GET"),
|
||||
create: permission("create", "POST"),
|
||||
replace: permission("replace", "PUT"),
|
||||
delete: permission("delete", "DELETE"),
|
||||
};
|
||||
}
|
||||
Registration { hooks, permissions }
|
||||
}
|
||||
|
||||
fn site_binding(
|
||||
settings: &dever_runtime::config::CompilationBindings,
|
||||
directory: &[String],
|
||||
span: Span,
|
||||
errors: &mut Vec<Diagnostic>,
|
||||
) -> Option<SiteBinding> {
|
||||
let directory = directory.iter().map(String::as_str).collect::<Vec<_>>();
|
||||
match settings.site_for_directory(&directory) {
|
||||
Ok(Some((key, site))) => Some(SiteBinding {
|
||||
key: key.to_owned(),
|
||||
provider: site.auth.clone(),
|
||||
}),
|
||||
Ok(None) => {
|
||||
errors.push(Diagnostic::error(
|
||||
"C005",
|
||||
"HTTP API source directory is not assigned to a configured site",
|
||||
span,
|
||||
));
|
||||
None
|
||||
}
|
||||
Err(message) => {
|
||||
errors.push(Diagnostic::error("C005", message, span));
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn permission_key(component: &str, domain: &str, site: &str, action: &str) -> String {
|
||||
format!("{component}.{domain}.{site}.{action}")
|
||||
}
|
||||
|
||||
fn register_permission(
|
||||
permissions: &mut Vec<Permission>,
|
||||
occupied: &mut BTreeMap<String, Span>,
|
||||
permission: Permission,
|
||||
span: Span,
|
||||
errors: &mut Vec<Diagnostic>,
|
||||
) -> Option<usize> {
|
||||
if let Some(previous) = occupied.insert(permission.key.clone(), span) {
|
||||
let mut error = Diagnostic::error(
|
||||
"C002",
|
||||
format!("duplicate API permission '{}'", permission.key),
|
||||
span,
|
||||
);
|
||||
error.related.push(crate::diagnostic::Label {
|
||||
span: previous,
|
||||
message: "first declaration of this permission".into(),
|
||||
});
|
||||
errors.push(error);
|
||||
return None;
|
||||
}
|
||||
let id = permissions.len();
|
||||
permissions.push(permission);
|
||||
Some(id)
|
||||
}
|
||||
|
||||
fn first_provider_span(
|
||||
provider: &str,
|
||||
route_providers: &[Option<SiteBinding>],
|
||||
routes: &[ApiRoute],
|
||||
rest_providers: &[Option<SiteBinding>],
|
||||
rest: &[ApiRest],
|
||||
) -> Span {
|
||||
route_providers
|
||||
.iter()
|
||||
.zip(routes)
|
||||
.find_map(|(candidate, route)| {
|
||||
(candidate.as_ref().map(|binding| binding.provider.as_str()) == Some(provider))
|
||||
.then_some(route.span)
|
||||
})
|
||||
.or_else(|| {
|
||||
rest_providers
|
||||
.iter()
|
||||
.zip(rest)
|
||||
.find_map(|(candidate, declaration)| {
|
||||
(candidate.as_ref().map(|binding| binding.provider.as_str()) == Some(provider))
|
||||
.then_some(declaration.span)
|
||||
})
|
||||
})
|
||||
.expect("provider originated from an API declaration")
|
||||
}
|
||||
|
||||
fn verify_contract(
|
||||
name: &str,
|
||||
claims: usize,
|
||||
packages: &[Package],
|
||||
functions: &[Function],
|
||||
types: &[Definition],
|
||||
span: Span,
|
||||
errors: &mut Vec<Diagnostic>,
|
||||
) -> Option<(usize, usize)> {
|
||||
let candidates = functions
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, function)| function.name == name)
|
||||
.collect::<Vec<_>>();
|
||||
let [(function_id, function)] = candidates.as_slice() else {
|
||||
errors.push(Diagnostic::error(
|
||||
"C005",
|
||||
if candidates.is_empty() {
|
||||
format!("authentication verify App '{name}' does not exist")
|
||||
} else {
|
||||
format!("authentication verify App '{name}' is overloaded")
|
||||
},
|
||||
span,
|
||||
));
|
||||
return None;
|
||||
};
|
||||
if packages[function.owner].layout.role() != Some(SourceRole::App)
|
||||
|| !function.public
|
||||
|| function.kind != FunctionKind::Ordinary
|
||||
|| function.parameters.as_slice() != [Parameter::Value(Type::Named(claims))]
|
||||
|| function.outputs.len() != 1
|
||||
{
|
||||
errors.push(Diagnostic::error(
|
||||
"C005",
|
||||
"authentication verify must be one ordinary App function with one dever.auth.Claims input and one identity record output",
|
||||
function.span,
|
||||
));
|
||||
return None;
|
||||
}
|
||||
let Type::Named(identity) = function.outputs[0].ty else {
|
||||
errors.push(Diagnostic::error(
|
||||
"C005",
|
||||
"authentication verify output must be an App-owned identity record",
|
||||
function.span,
|
||||
));
|
||||
return None;
|
||||
};
|
||||
if types[identity].owner != function.owner
|
||||
|| !types[identity].public
|
||||
|| types[identity].kind != DefinitionKind::Regular
|
||||
|| !identity_fields(&types[identity], types)
|
||||
{
|
||||
errors.push(Diagnostic::error(
|
||||
"C005",
|
||||
"authentication identity must be an App-owned public record with id: Text, user_id: ModelId?, and tenant_id: ModelId?",
|
||||
types[identity].span,
|
||||
));
|
||||
return None;
|
||||
}
|
||||
Some((*function_id, identity))
|
||||
}
|
||||
|
||||
fn identity_fields(identity: &Definition, types: &[Definition]) -> bool {
|
||||
let Shape::Record(fields) = &identity.shape else {
|
||||
return false;
|
||||
};
|
||||
let [id, user_id, tenant_id] = fields.as_slice() else {
|
||||
return false;
|
||||
};
|
||||
fields
|
||||
.iter()
|
||||
.all(|field| !field.private && field.bounds.is_empty())
|
||||
&& id.name == "id"
|
||||
&& id.ty == Type::Text
|
||||
&& user_id.name == "user_id"
|
||||
&& optional_model_id(&user_id.ty, types)
|
||||
&& tenant_id.name == "tenant_id"
|
||||
&& optional_model_id(&tenant_id.ty, types)
|
||||
}
|
||||
|
||||
fn optional_model_id(ty: &Type, types: &[Definition]) -> bool {
|
||||
matches!(ty, Type::Nullable(inner) if matches!(inner.as_ref(), Type::Named(id) if types[*id].kind == DefinitionKind::ModelId))
|
||||
}
|
||||
|
||||
pub(super) fn validate(program: &Program, configured: bool, errors: &mut Vec<Diagnostic>) {
|
||||
validate_identity_types(program, configured, errors);
|
||||
for hook in &program.auth {
|
||||
let root = crate::specialize::Specialization {
|
||||
function: hook.function,
|
||||
handlers: Vec::new(),
|
||||
};
|
||||
let Ok(reachable) = crate::specialize::reachable(&program.functions, [root]) else {
|
||||
continue;
|
||||
};
|
||||
let mut forbidden = None;
|
||||
let mut tenant_model = None;
|
||||
for instance in reachable {
|
||||
crate::check::visit(&program.functions[instance.function], |expression| {
|
||||
let writes = match &expression.kind {
|
||||
ExpressionKind::ModelOperation { operation, .. } => operation.writes_database(),
|
||||
ExpressionKind::JobEnqueue { .. } => true,
|
||||
_ => false,
|
||||
};
|
||||
if writes && forbidden.is_none() {
|
||||
forbidden = Some((
|
||||
expression.span,
|
||||
"authentication verify cannot write to a database or enqueue a Job",
|
||||
));
|
||||
}
|
||||
if let ExpressionKind::ModelOperation { model, .. } = &expression.kind
|
||||
&& program.models[*model].scope == crate::model::ModelScope::Tenant
|
||||
&& tenant_model.is_none()
|
||||
{
|
||||
tenant_model = Some((expression.span, program.models[*model].package.clone()));
|
||||
}
|
||||
});
|
||||
}
|
||||
if let Some((span, message)) = forbidden {
|
||||
errors.push(Diagnostic::error("C013", message, span));
|
||||
}
|
||||
if let Some((span, model)) = tenant_model {
|
||||
errors.push(Diagnostic::error(
|
||||
"C014",
|
||||
format!(
|
||||
"authentication verify may access only global Models; '{model}' is tenant-scoped"
|
||||
),
|
||||
span,
|
||||
));
|
||||
}
|
||||
if program.effects[hook.function].contains("request") {
|
||||
errors.push(Diagnostic::error(
|
||||
"C006",
|
||||
"authentication verify receives checked Claims and cannot read HTTP request capabilities",
|
||||
program.functions[hook.function].span,
|
||||
));
|
||||
}
|
||||
if program.effects[hook.function].contains("identity") {
|
||||
errors.push(Diagnostic::error(
|
||||
"C006",
|
||||
"authentication verify cannot read or require an identity before it establishes one",
|
||||
program.functions[hook.function].span,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
for (function_id, function) in program.functions.iter().enumerate() {
|
||||
let package = &program.packages[function.owner];
|
||||
if package.bundled || !program.effects[function_id].contains("identity") {
|
||||
continue;
|
||||
}
|
||||
if package.role != Some(SourceRole::App) {
|
||||
errors.push(Diagnostic::error(
|
||||
"C006",
|
||||
"authentication and site capabilities are available only from App functions",
|
||||
function.span,
|
||||
));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_identity_types(program: &Program, configured: bool, errors: &mut Vec<Diagnostic>) {
|
||||
for route in &program.api_routes {
|
||||
let context = IdentityContext {
|
||||
program,
|
||||
hook: route.auth.map(|index| &program.auth[index]),
|
||||
protected: !route.anonymous,
|
||||
configured,
|
||||
binding_span: route.span,
|
||||
};
|
||||
let root = crate::specialize::Specialization {
|
||||
function: route.function,
|
||||
handlers: Vec::new(),
|
||||
};
|
||||
let Ok(reachable) = crate::specialize::reachable(&program.functions, [root]) else {
|
||||
continue;
|
||||
};
|
||||
for instance in reachable {
|
||||
super::visit(&program.functions[instance.function], |expression| {
|
||||
context.validate_expression(expression, errors);
|
||||
});
|
||||
}
|
||||
}
|
||||
for route in &program.api_rest {
|
||||
let context = IdentityContext {
|
||||
program,
|
||||
hook: route.auth.map(|index| &program.auth[index]),
|
||||
protected: true,
|
||||
configured,
|
||||
binding_span: route.span,
|
||||
};
|
||||
if let Some((field, source)) = route.contract.owner {
|
||||
let Shape::Record(fields) = &program.types[program.models[route.model].record].shape
|
||||
else {
|
||||
unreachable!("checked Model record")
|
||||
};
|
||||
let (name, identity_field) = match source {
|
||||
crate::model::RestOwnerSource::User => ("user_id", 1),
|
||||
crate::model::RestOwnerSource::Tenant => ("tenant_id", 2),
|
||||
};
|
||||
context.validate_type(
|
||||
name,
|
||||
identity_field,
|
||||
fields[field].ty.clone().nullable(),
|
||||
program.types[program.models[route.model].record].span,
|
||||
errors,
|
||||
);
|
||||
}
|
||||
for expression in route
|
||||
.contract
|
||||
.create
|
||||
.iter()
|
||||
.chain(&route.contract.replace)
|
||||
.filter_map(|write| match &write.source {
|
||||
crate::model::RestValueSource::Binding { expression, .. } => Some(expression),
|
||||
crate::model::RestValueSource::Input => None,
|
||||
})
|
||||
.chain(
|
||||
route
|
||||
.contract
|
||||
.search
|
||||
.iter()
|
||||
.map(|search| &search.expression),
|
||||
)
|
||||
{
|
||||
super::visit_expression(expression, |value| {
|
||||
context.validate_expression(value, errors);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct IdentityContext<'a> {
|
||||
program: &'a Program,
|
||||
hook: Option<&'a AuthHook>,
|
||||
protected: bool,
|
||||
configured: bool,
|
||||
binding_span: Span,
|
||||
}
|
||||
|
||||
impl IdentityContext<'_> {
|
||||
fn validate_expression(&self, expression: &Expression, errors: &mut Vec<Diagnostic>) {
|
||||
use crate::intrinsic::Intrinsic;
|
||||
|
||||
let ExpressionKind::Intrinsic {
|
||||
operation,
|
||||
arguments,
|
||||
..
|
||||
} = &expression.kind
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let (name, field, actual) = match operation {
|
||||
Intrinsic::AuthUserId => ("user_id", 1, expression.ty.clone()),
|
||||
Intrinsic::AuthTenantId => ("tenant_id", 2, expression.ty.clone()),
|
||||
Intrinsic::AuthOwnsUser => ("owns_user", 1, arguments[0].ty.clone().nullable()),
|
||||
_ => return,
|
||||
};
|
||||
self.validate_type(name, field, actual, expression.span, errors);
|
||||
}
|
||||
|
||||
fn validate_type(
|
||||
&self,
|
||||
name: &str,
|
||||
field: usize,
|
||||
actual: Type,
|
||||
span: Span,
|
||||
errors: &mut Vec<Diagnostic>,
|
||||
) {
|
||||
if !optional_model_id(&actual, &self.program.types) || (!self.configured && self.protected)
|
||||
{
|
||||
return;
|
||||
}
|
||||
let expected = self.hook.and_then(|hook| {
|
||||
let Shape::Record(fields) = &self.program.types[hook.identity].shape else {
|
||||
return None;
|
||||
};
|
||||
Some(&fields[field].ty)
|
||||
});
|
||||
if self.protected && expected == Some(&actual) {
|
||||
return;
|
||||
}
|
||||
let message = match (self.protected, self.hook, expected) {
|
||||
(true, Some(hook), Some(expected)) => format!(
|
||||
"dever.auth.{name} nominal ID must match provider '{}' identity field {} rather than {}",
|
||||
hook.provider,
|
||||
expected.label(&self.program.types),
|
||||
actual.label(&self.program.types),
|
||||
),
|
||||
_ => format!(
|
||||
"typed dever.auth.{name} requires a protected API with a configured identity provider"
|
||||
),
|
||||
};
|
||||
let mut error = Diagnostic::error("C006", message, span);
|
||||
error.related.push(crate::diagnostic::Label {
|
||||
span: self.binding_span,
|
||||
message: "authentication context belongs to this API binding".into(),
|
||||
});
|
||||
errors.push(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,648 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use crate::diagnostic::Diagnostic;
|
||||
use crate::hir::{self, Atom, Binding, Constant, Domain, ExpressionKind, Function, Projection};
|
||||
use crate::source::Span;
|
||||
use crate::syntax::{FunctionClause, InputKind, Pattern, StatementKind};
|
||||
use crate::types::{HandlerSignature, Parameter, Shape, Type};
|
||||
|
||||
use super::{Checked, Context, path_name};
|
||||
|
||||
pub(super) struct Local {
|
||||
pub slot: usize,
|
||||
pub ty: Type,
|
||||
pub initialized: bool,
|
||||
pub last_write: Option<Span>,
|
||||
pub read: bool,
|
||||
pub known: Option<ExpressionKind>,
|
||||
pub affine_consumed: bool,
|
||||
}
|
||||
|
||||
pub(super) struct Body<'a, 'b> {
|
||||
pub context: &'a Context<'b>,
|
||||
pub locals: BTreeMap<String, Local>,
|
||||
pub handlers: BTreeMap<String, HandlerLocal>,
|
||||
pub slots: Vec<Type>,
|
||||
pub warnings: &'a mut Vec<Diagnostic>,
|
||||
}
|
||||
|
||||
pub(super) struct HandlerLocal {
|
||||
pub parameter: usize,
|
||||
pub signature: HandlerSignature,
|
||||
}
|
||||
|
||||
pub(super) fn expression(
|
||||
source: &crate::syntax::Expression,
|
||||
input: &Type,
|
||||
expected: &Type,
|
||||
context: &Context<'_>,
|
||||
warnings: &mut Vec<Diagnostic>,
|
||||
) -> Checked<hir::Expression> {
|
||||
let mut checker = Body {
|
||||
context,
|
||||
locals: BTreeMap::new(),
|
||||
handlers: BTreeMap::new(),
|
||||
slots: Vec::new(),
|
||||
warnings,
|
||||
};
|
||||
checker.declare("input", input.clone(), true, source.span)?;
|
||||
checker.expression(source, Some(expected))
|
||||
}
|
||||
|
||||
pub(super) fn check(
|
||||
source: &FunctionClause,
|
||||
patterns: Vec<Domain>,
|
||||
function: &Function,
|
||||
context: &Context<'_>,
|
||||
warnings: &mut Vec<Diagnostic>,
|
||||
) -> Checked<hir::Clause> {
|
||||
let mut checker = Body {
|
||||
context,
|
||||
locals: BTreeMap::new(),
|
||||
handlers: BTreeMap::new(),
|
||||
slots: Vec::new(),
|
||||
warnings,
|
||||
};
|
||||
let mut bindings = Vec::new();
|
||||
let mut domains = patterns.iter();
|
||||
for (input_index, (input, parameter)) in
|
||||
source.inputs.iter().zip(&function.parameters).enumerate()
|
||||
{
|
||||
let Parameter::Value(parameter) = parameter else {
|
||||
let Parameter::Handler(signature) = parameter else {
|
||||
unreachable!()
|
||||
};
|
||||
if checker.locals.contains_key(&input.name.text)
|
||||
|| checker.handlers.contains_key(&input.name.text)
|
||||
{
|
||||
return Err(Diagnostic::error(
|
||||
"C002",
|
||||
format!("duplicate local binding '{}'", input.name.text),
|
||||
input.name.span,
|
||||
));
|
||||
}
|
||||
checker.handlers.insert(
|
||||
input.name.text.clone(),
|
||||
HandlerLocal {
|
||||
parameter: input_index,
|
||||
signature: signature.clone(),
|
||||
},
|
||||
);
|
||||
continue;
|
||||
};
|
||||
let InputKind::Value(input_pattern) = &input.kind else {
|
||||
unreachable!("checked parameter kind")
|
||||
};
|
||||
let domain = domains.next().expect("checked value parameter domain");
|
||||
let non_null = matches!(parameter, Type::Nullable(_))
|
||||
&& !domain.iter().any(|atom| matches!(atom, Atom::Null));
|
||||
let ty = if non_null {
|
||||
parameter.base().clone()
|
||||
} else {
|
||||
parameter.clone()
|
||||
};
|
||||
let slot = checker.declare(&input.name.text, ty, true, input.name.span)?;
|
||||
checker
|
||||
.locals
|
||||
.get_mut(&input.name.text)
|
||||
.expect("declared input")
|
||||
.known = match domain.as_slice() {
|
||||
[Atom::Null] => Some(ExpressionKind::Constant(Constant::Null)),
|
||||
[Atom::Bool(value)] => Some(ExpressionKind::Constant(Constant::Bool(*value))),
|
||||
[Atom::Int(lo, hi)] if lo == hi => Some(ExpressionKind::Constant(Constant::Int(*lo))),
|
||||
[Atom::Decimal(lo, hi)] if lo == hi => {
|
||||
Some(ExpressionKind::Constant(Constant::Decimal(*lo)))
|
||||
}
|
||||
[Atom::Literal(Constant::Text(value))] => {
|
||||
Some(ExpressionKind::Constant(Constant::Text(value.clone())))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
bindings.push(Binding {
|
||||
slot,
|
||||
input: input_index,
|
||||
projection: if non_null {
|
||||
Projection::NonNull
|
||||
} else {
|
||||
Projection::Whole
|
||||
},
|
||||
});
|
||||
if let Pattern::Variant {
|
||||
name,
|
||||
arguments,
|
||||
bindings: names,
|
||||
span,
|
||||
} = input_pattern
|
||||
{
|
||||
let qualified = path_name(name);
|
||||
let (variant, fields) = if let Some((_, variant, fields)) =
|
||||
context.related_variant(&qualified, arguments, *span)?
|
||||
{
|
||||
(variant, fields)
|
||||
} else {
|
||||
let (id, variant) = context
|
||||
.variant(&qualified, *span)?
|
||||
.expect("checked choice pattern");
|
||||
let Shape::Choice(variants) = &context.types[id].shape else {
|
||||
unreachable!()
|
||||
};
|
||||
(variant, variants[variant].fields.clone())
|
||||
};
|
||||
for (field, (name, payload)) in names.iter().zip(&fields).enumerate() {
|
||||
if name.text == "_" {
|
||||
continue;
|
||||
}
|
||||
let slot = checker.declare(&name.text, payload.ty.clone(), true, name.span)?;
|
||||
bindings.push(Binding {
|
||||
slot,
|
||||
input: input_index,
|
||||
projection: Projection::Payload { variant, field },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
debug_assert!(domains.next().is_none());
|
||||
let mut outputs = Vec::new();
|
||||
for (field, source) in function.outputs.iter().zip(&source.outputs) {
|
||||
outputs.push(checker.declare(&field.name, field.ty.clone(), false, source.span)?);
|
||||
}
|
||||
let mut body = Vec::new();
|
||||
let mut terminates = false;
|
||||
for (index, statement) in source.body.iter().enumerate() {
|
||||
match &statement.kind {
|
||||
StatementKind::Assign { target, value } => {
|
||||
let name = &target[0].text;
|
||||
if name == "setting"
|
||||
&& checker.context.symbols.packages[checker.context.owner]
|
||||
.layout
|
||||
.role()
|
||||
== Some(crate::source::SourceRole::Adapter)
|
||||
{
|
||||
return Err(Diagnostic::error(
|
||||
"C006",
|
||||
"Adapter setting is read-only and cannot be shadowed",
|
||||
target[0].span,
|
||||
));
|
||||
}
|
||||
if target.len() == 1 {
|
||||
let expected = checker.locals.get(name).map(|local| local.ty.clone());
|
||||
let value = checker.expression(value, expected.as_ref())?;
|
||||
if value.ty == Type::Unit {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"a zero-output action cannot be assigned",
|
||||
value.span,
|
||||
));
|
||||
}
|
||||
let slot = match checker.locals.get_mut(name) {
|
||||
Some(local) => {
|
||||
if is_affine(&local.ty) && !local.affine_consumed {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
format!(
|
||||
"{} must be consumed before it can be overwritten",
|
||||
affine_label(&local.ty)
|
||||
),
|
||||
statement.span,
|
||||
));
|
||||
}
|
||||
warn_overwrite(local, checker.warnings);
|
||||
local.initialized = true;
|
||||
local.affine_consumed = false;
|
||||
local.read = false;
|
||||
local.last_write = Some(statement.span);
|
||||
local.known = known_value(&value.kind);
|
||||
local.slot
|
||||
}
|
||||
None => {
|
||||
let slot =
|
||||
checker.declare(name, value.ty.clone(), true, target[0].span)?;
|
||||
let local = checker.locals.get_mut(name).expect("declared local");
|
||||
local.last_write = Some(statement.span);
|
||||
local.known = known_value(&value.kind);
|
||||
slot
|
||||
}
|
||||
};
|
||||
body.push(hir::Statement::Assign {
|
||||
slot,
|
||||
fields: Vec::new(),
|
||||
value,
|
||||
});
|
||||
} else {
|
||||
let local = checker.read_local(name, target[0].span)?;
|
||||
let slot = local.slot;
|
||||
let mut ty = local.ty.clone();
|
||||
let mut fields = Vec::new();
|
||||
for name in &target[1..] {
|
||||
if matches!(ty, Type::Outputs(_)) {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"named call outputs are read-only projections",
|
||||
name.span,
|
||||
));
|
||||
}
|
||||
let (index, field_type) = checker.field_type(&ty, name)?;
|
||||
fields.push(index);
|
||||
ty = field_type;
|
||||
}
|
||||
let value = checker.expression(value, Some(&ty))?;
|
||||
let local = checker.locals.get_mut(name).expect("checked local");
|
||||
local.last_write = Some(statement.span);
|
||||
local.read = false;
|
||||
local.known = None;
|
||||
body.push(hir::Statement::Assign {
|
||||
slot,
|
||||
fields,
|
||||
value,
|
||||
});
|
||||
}
|
||||
}
|
||||
StatementKind::Call(expression) => {
|
||||
let value = checker.expression(expression, None)?;
|
||||
if value.ty != Type::Unit {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"only zero-output calls may be action statements",
|
||||
expression.span,
|
||||
));
|
||||
}
|
||||
if matches!(value.kind, ExpressionKind::Fail(_)) {
|
||||
if index + 1 != source.body.len() {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"fail must be the final statement in a function clause",
|
||||
expression.span,
|
||||
));
|
||||
}
|
||||
terminates = true;
|
||||
}
|
||||
body.push(hir::Statement::Call(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
if !terminates {
|
||||
for field in &function.outputs {
|
||||
checker.read_local(&field.name, source.span)?;
|
||||
}
|
||||
}
|
||||
if !terminates
|
||||
&& let Some((_, local)) = checker
|
||||
.locals
|
||||
.iter()
|
||||
.find(|(_, local)| is_affine(&local.ty) && !local.affine_consumed)
|
||||
{
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
format!(
|
||||
"{} must be consumed before the function exits",
|
||||
affine_label(&local.ty)
|
||||
),
|
||||
local.last_write.unwrap_or(source.span),
|
||||
));
|
||||
}
|
||||
for local in checker.locals.values() {
|
||||
if !local.read
|
||||
&& let Some(span) = local.last_write
|
||||
{
|
||||
checker.warnings.push(Diagnostic::error(
|
||||
"W001",
|
||||
"value is written but never read",
|
||||
span,
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(hir::Clause {
|
||||
patterns,
|
||||
bindings,
|
||||
locals: checker.slots,
|
||||
outputs,
|
||||
body,
|
||||
terminates,
|
||||
})
|
||||
}
|
||||
|
||||
fn warn_overwrite(local: &Local, warnings: &mut Vec<Diagnostic>) {
|
||||
if !local.read
|
||||
&& let Some(span) = local.last_write
|
||||
{
|
||||
warnings.push(Diagnostic::error(
|
||||
"W001",
|
||||
"value is overwritten before it is read",
|
||||
span,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
impl Body<'_, '_> {
|
||||
fn declare(&mut self, name: &str, ty: Type, initialized: bool, span: Span) -> Checked<usize> {
|
||||
if self.locals.contains_key(name) || self.handlers.contains_key(name) {
|
||||
return Err(Diagnostic::error(
|
||||
"C002",
|
||||
format!("duplicate local binding '{name}'"),
|
||||
span,
|
||||
));
|
||||
}
|
||||
let slot = self.slots.len();
|
||||
self.slots.push(ty.clone());
|
||||
self.locals.insert(
|
||||
name.into(),
|
||||
Local {
|
||||
slot,
|
||||
ty,
|
||||
initialized,
|
||||
last_write: None,
|
||||
read: false,
|
||||
known: None,
|
||||
affine_consumed: false,
|
||||
},
|
||||
);
|
||||
Ok(slot)
|
||||
}
|
||||
|
||||
pub(super) fn read_local(&mut self, name: &str, span: Span) -> Checked<&Local> {
|
||||
let local = self
|
||||
.locals
|
||||
.get_mut(name)
|
||||
.filter(|local| local.initialized)
|
||||
.ok_or_else(|| {
|
||||
Diagnostic::error(
|
||||
"C004",
|
||||
format!("unknown or uninitialized local '{name}'"),
|
||||
span,
|
||||
)
|
||||
})?;
|
||||
if is_affine(&local.ty) {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
format!(
|
||||
"{} is affine and may only be used by its task operation",
|
||||
affine_label(&local.ty)
|
||||
),
|
||||
span,
|
||||
));
|
||||
}
|
||||
local.read = true;
|
||||
Ok(local)
|
||||
}
|
||||
|
||||
pub(super) fn affine_local(
|
||||
&mut self,
|
||||
name: &str,
|
||||
span: Span,
|
||||
consume: bool,
|
||||
) -> Checked<(usize, Type)> {
|
||||
let local = self
|
||||
.locals
|
||||
.get_mut(name)
|
||||
.ok_or_else(|| Diagnostic::error("C004", format!("unknown local '{name}'"), span))?;
|
||||
if !is_affine(&local.ty) {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"this operation requires a Task or Group local",
|
||||
span,
|
||||
));
|
||||
}
|
||||
if local.affine_consumed {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
format!("{} has already been consumed", affine_label(&local.ty)),
|
||||
span,
|
||||
));
|
||||
}
|
||||
local.read = true;
|
||||
if consume {
|
||||
local.affine_consumed = true;
|
||||
}
|
||||
Ok((local.slot, local.ty.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
fn is_affine(ty: &Type) -> bool {
|
||||
matches!(ty, Type::Task(_) | Type::Group | Type::Upload)
|
||||
}
|
||||
|
||||
fn affine_label(ty: &Type) -> &'static str {
|
||||
match ty {
|
||||
Type::Task(_) => "Task",
|
||||
Type::Group => "Group",
|
||||
Type::Upload => "Upload",
|
||||
_ => unreachable!("affine label requires Task or Group"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn check_suspensions(program: &crate::hir::Program, errors: &mut Vec<Diagnostic>) {
|
||||
for function in &program.functions {
|
||||
for clause in &function.clauses {
|
||||
if let Err(error) = check_suspension_safety(clause, program) {
|
||||
errors.push(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_suspension_safety(clause: &hir::Clause, program: &crate::hir::Program) -> Checked<()> {
|
||||
let body = &clause.body;
|
||||
let locals = &clause.locals;
|
||||
let outputs = &clause.outputs;
|
||||
let mut live: BTreeSet<_> = outputs.iter().copied().collect();
|
||||
for statement in body.iter().rev() {
|
||||
match statement {
|
||||
hir::Statement::Assign {
|
||||
slot,
|
||||
fields,
|
||||
value,
|
||||
} => {
|
||||
if fields.is_empty() {
|
||||
live.remove(slot);
|
||||
} else {
|
||||
live.insert(*slot);
|
||||
}
|
||||
expression_liveness(value, locals, program, &mut live)?;
|
||||
}
|
||||
hir::Statement::Call(value) => {
|
||||
expression_liveness(value, locals, program, &mut live)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn expression_liveness(
|
||||
expression: &hir::Expression,
|
||||
locals: &[Type],
|
||||
program: &crate::hir::Program,
|
||||
live: &mut BTreeSet<usize>,
|
||||
) -> Checked<()> {
|
||||
if suspension_point(&expression.kind, program)
|
||||
&& let Some(slot) = live.iter().copied().find(|slot| {
|
||||
let ty = &locals[*slot];
|
||||
!matches!(ty, Type::Task(_) | Type::Group | Type::Upload)
|
||||
&& !ty.transferable(&program.types)
|
||||
})
|
||||
{
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
format!(
|
||||
"{} local cannot remain live across a suspension point",
|
||||
locals[slot].label(&program.types)
|
||||
),
|
||||
expression.span,
|
||||
));
|
||||
}
|
||||
|
||||
match &expression.kind {
|
||||
ExpressionKind::Local(slot) => {
|
||||
live.insert(*slot);
|
||||
}
|
||||
ExpressionKind::Field { value, .. }
|
||||
| ExpressionKind::Fail(value)
|
||||
| ExpressionKind::Unary { value, .. }
|
||||
| ExpressionKind::Promote(value)
|
||||
| ExpressionKind::Some(value)
|
||||
| ExpressionKind::AwaitTask(value)
|
||||
| ExpressionKind::StopTask(value)
|
||||
| ExpressionKind::Group(value)
|
||||
| ExpressionKind::AwaitGroup(value)
|
||||
| ExpressionKind::StopGroup(value)
|
||||
| ExpressionKind::ChannelReceive(value)
|
||||
| ExpressionKind::ChannelClose(value) => {
|
||||
expression_liveness(value, locals, program, live)?;
|
||||
}
|
||||
ExpressionKind::Call { arguments, .. }
|
||||
| ExpressionKind::CaptureResult { arguments, .. }
|
||||
| ExpressionKind::ParallelCall { arguments, .. }
|
||||
| ExpressionKind::BlockingCall { arguments, .. } => {
|
||||
call_arguments_liveness(arguments, locals, program, live)?;
|
||||
}
|
||||
ExpressionKind::ModelOperation { operation, .. } => {
|
||||
for value in operation.expressions().into_iter().rev() {
|
||||
expression_liveness(value, locals, program, live)?;
|
||||
}
|
||||
}
|
||||
ExpressionKind::RunCall {
|
||||
group, arguments, ..
|
||||
} => {
|
||||
call_arguments_liveness(arguments, locals, program, live)?;
|
||||
if let Some(group) = group {
|
||||
expression_liveness(group, locals, program, live)?;
|
||||
}
|
||||
}
|
||||
ExpressionKind::Channel { capacity, .. } => {
|
||||
expression_liveness(capacity, locals, program, live)?;
|
||||
}
|
||||
ExpressionKind::ChannelSend { channel, value } => {
|
||||
expression_liveness(value, locals, program, live)?;
|
||||
expression_liveness(channel, locals, program, live)?;
|
||||
}
|
||||
ExpressionKind::Intrinsic { arguments, .. }
|
||||
| ExpressionKind::JobEnqueue { arguments, .. }
|
||||
| ExpressionKind::Collection { arguments, .. }
|
||||
| ExpressionKind::Variant { arguments, .. }
|
||||
| ExpressionKind::List(arguments) => {
|
||||
for argument in arguments.iter().rev() {
|
||||
expression_liveness(argument, locals, program, live)?;
|
||||
}
|
||||
}
|
||||
ExpressionKind::Record { fields } => {
|
||||
for (_, value) in fields.iter().rev() {
|
||||
expression_liveness(value, locals, program, live)?;
|
||||
}
|
||||
}
|
||||
ExpressionKind::Map(entries) => {
|
||||
for (key, value) in entries.iter().rev() {
|
||||
expression_liveness(value, locals, program, live)?;
|
||||
expression_liveness(key, locals, program, live)?;
|
||||
}
|
||||
}
|
||||
ExpressionKind::Binary { left, right, .. } => {
|
||||
expression_liveness(right, locals, program, live)?;
|
||||
expression_liveness(left, locals, program, live)?;
|
||||
}
|
||||
ExpressionKind::Constant(_) | ExpressionKind::ChoiceOptions { .. } => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn call_arguments_liveness(
|
||||
arguments: &[hir::CallArgument],
|
||||
locals: &[Type],
|
||||
program: &crate::hir::Program,
|
||||
live: &mut BTreeSet<usize>,
|
||||
) -> Checked<()> {
|
||||
for argument in arguments.iter().rev() {
|
||||
if let hir::CallArgument::Value(value) = argument {
|
||||
expression_liveness(value, locals, program, live)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn suspension_point(kind: &ExpressionKind, program: &crate::hir::Program) -> bool {
|
||||
if let ExpressionKind::Intrinsic { operation, .. } = kind {
|
||||
return operation.is_async();
|
||||
}
|
||||
if let ExpressionKind::Call { target, .. } | ExpressionKind::CaptureResult { target, .. } = kind
|
||||
{
|
||||
return call_target_may_suspend(*target, program);
|
||||
}
|
||||
if matches!(
|
||||
kind,
|
||||
ExpressionKind::ModelOperation { .. } | ExpressionKind::JobEnqueue { .. }
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if let ExpressionKind::Collection {
|
||||
operation,
|
||||
handler,
|
||||
sequence,
|
||||
..
|
||||
} = kind
|
||||
{
|
||||
return *operation == hir::CollectionOp::ParallelEach
|
||||
|| (matches!(
|
||||
sequence,
|
||||
Some(hir::SequenceKind::AsyncStream | hir::SequenceKind::RowStream)
|
||||
) && *operation != hir::CollectionOp::Close)
|
||||
|| handler.is_some_and(|target| handler_may_suspend(target, program));
|
||||
}
|
||||
matches!(
|
||||
kind,
|
||||
ExpressionKind::RunCall { .. }
|
||||
| ExpressionKind::AwaitTask(_)
|
||||
| ExpressionKind::StopTask(_)
|
||||
| ExpressionKind::AwaitGroup(_)
|
||||
| ExpressionKind::StopGroup(_)
|
||||
| ExpressionKind::ParallelCall { .. }
|
||||
| ExpressionKind::BlockingCall { .. }
|
||||
| ExpressionKind::ChannelSend { .. }
|
||||
| ExpressionKind::ChannelReceive(_)
|
||||
| ExpressionKind::ChannelClose(_)
|
||||
)
|
||||
}
|
||||
|
||||
fn call_target_may_suspend(target: hir::CallTarget, program: &crate::hir::Program) -> bool {
|
||||
match target {
|
||||
hir::CallTarget::Function(id) => function_may_suspend(id, program),
|
||||
hir::CallTarget::Handler(target) => handler_may_suspend(target, program),
|
||||
}
|
||||
}
|
||||
|
||||
fn handler_may_suspend(target: hir::HandlerTarget, program: &crate::hir::Program) -> bool {
|
||||
match target {
|
||||
hir::HandlerTarget::Function(id) => function_may_suspend(id, program),
|
||||
hir::HandlerTarget::Parameter(_) => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn function_may_suspend(id: usize, program: &crate::hir::Program) -> bool {
|
||||
program.functions[id].suspends || !program.suspension_handlers[id].is_empty()
|
||||
}
|
||||
|
||||
fn known_value(value: &ExpressionKind) -> Option<ExpressionKind> {
|
||||
match value {
|
||||
ExpressionKind::Constant(_) => Some(value.clone()),
|
||||
ExpressionKind::Variant { arguments, .. } if arguments.is_empty() => Some(value.clone()),
|
||||
ExpressionKind::Some(inner) if known_value(&inner.kind).is_some() => Some(value.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
use std::cell::RefCell;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::diagnostic::Diagnostic;
|
||||
use crate::hir::Program;
|
||||
use crate::source::{SourceMap, SourceOrigin};
|
||||
|
||||
type CheckedProgram = Result<Program, Vec<Diagnostic>>;
|
||||
|
||||
struct CachedProgram {
|
||||
sources: Vec<(PathBuf, PathBuf, SourceOrigin, String)>,
|
||||
strict_layout: bool,
|
||||
result: CheckedProgram,
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
// Keep one compilation only: editors and embedding callers can reuse an unchanged
|
||||
// program without retaining every project ever checked by a long-lived process.
|
||||
static PREVIOUS: RefCell<Option<CachedProgram>> = const { RefCell::new(None) };
|
||||
}
|
||||
|
||||
pub(super) fn check(
|
||||
sources: &SourceMap,
|
||||
compute: impl FnOnce(&SourceMap) -> CheckedProgram,
|
||||
) -> CheckedProgram {
|
||||
let cached = PREVIOUS.with_borrow(|previous| {
|
||||
let previous = previous.as_ref()?;
|
||||
let matches = previous.strict_layout == sources.strict_layout()
|
||||
&& previous.sources.len() == sources.files().len()
|
||||
&& previous.sources.iter().zip(sources.files()).all(
|
||||
|((path, logical_path, origin, text), source)| {
|
||||
path == source.path()
|
||||
&& logical_path == source.logical_path()
|
||||
&& *origin == source.origin()
|
||||
&& text == source.text()
|
||||
},
|
||||
);
|
||||
matches.then(|| previous.result.clone())
|
||||
});
|
||||
if let Some(result) = cached {
|
||||
return result;
|
||||
}
|
||||
let result = compute(sources);
|
||||
PREVIOUS.with_borrow_mut(|previous| {
|
||||
*previous = Some(CachedProgram {
|
||||
strict_layout: sources.strict_layout(),
|
||||
sources: sources
|
||||
.files()
|
||||
.iter()
|
||||
.map(|source| {
|
||||
(
|
||||
source.path().to_owned(),
|
||||
source.logical_path().to_owned(),
|
||||
source.origin(),
|
||||
source.text().to_owned(),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
result: result.clone(),
|
||||
});
|
||||
});
|
||||
result
|
||||
}
|
||||
@@ -0,0 +1,628 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use dever_runtime::number::DecimalValue;
|
||||
|
||||
use crate::diagnostic::{Diagnostic, Label};
|
||||
use crate::hir::{Atom, Constant, Domain, ExpressionKind};
|
||||
use crate::syntax::{BinaryOperator as Op, FunctionClause, InputKind, Literal, Pattern};
|
||||
use crate::types::{Field, HandlerSignature, Parameter, Shape, Type};
|
||||
|
||||
use super::{Checked, Context, constants, path_name, symbols};
|
||||
|
||||
#[derive(Clone)]
|
||||
enum PatternSet {
|
||||
Any(bool),
|
||||
Null,
|
||||
Bool(bool),
|
||||
Variant(usize),
|
||||
Literal(Constant),
|
||||
Int(Option<(i64, i64)>),
|
||||
Decimal(Option<(DecimalValue, DecimalValue)>),
|
||||
Other,
|
||||
}
|
||||
|
||||
pub(super) fn signature(
|
||||
group: &[&FunctionClause],
|
||||
context: &Context<'_>,
|
||||
) -> Checked<(Vec<Parameter>, Vec<Field>)> {
|
||||
if group
|
||||
.iter()
|
||||
.any(|clause| clause.pure != group[0].pure || clause.recovery != group[0].recovery)
|
||||
{
|
||||
return Err(Diagnostic::error(
|
||||
"C013",
|
||||
"all clauses must have identical pure and recovery contracts",
|
||||
group[0].span,
|
||||
));
|
||||
}
|
||||
let mut parameters = Vec::new();
|
||||
for axis in 0..group[0].inputs.len() {
|
||||
if let InputKind::Handler(source) = &group[0].inputs[axis].kind {
|
||||
let signature = handler_signature(source, context)?;
|
||||
for clause in &group[1..] {
|
||||
let InputKind::Handler(candidate) = &clause.inputs[axis].kind else {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"all clauses must agree whether an input is a handler",
|
||||
clause.inputs[axis].span,
|
||||
));
|
||||
};
|
||||
if handler_signature(candidate, context)? != signature {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"all clauses must use the same handler signature",
|
||||
clause.inputs[axis].span,
|
||||
));
|
||||
}
|
||||
}
|
||||
parameters.push(Parameter::Handler(signature));
|
||||
continue;
|
||||
}
|
||||
let mut inferred = None;
|
||||
let mut nullable = false;
|
||||
for clause in group {
|
||||
let input = &clause.inputs[axis];
|
||||
let InputKind::Value(pattern) = &input.kind else {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"all clauses must agree whether an input is a handler",
|
||||
input.span,
|
||||
));
|
||||
};
|
||||
let ty = match pattern {
|
||||
Pattern::Typed { ty, .. } => {
|
||||
match context.variant(&path_name(&ty.name), ty.span)? {
|
||||
Some((id, _)) => Some(Type::Named(id)),
|
||||
None => Some(context.resolve_type(ty)?),
|
||||
}
|
||||
}
|
||||
Pattern::Variant {
|
||||
name,
|
||||
arguments,
|
||||
span,
|
||||
..
|
||||
} => {
|
||||
let name = path_name(name);
|
||||
if let Some((ty, _, _)) = context.related_variant(&name, arguments, *span)? {
|
||||
Some(ty)
|
||||
} else {
|
||||
if !arguments.is_empty() {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"choice variants do not accept type arguments",
|
||||
*span,
|
||||
));
|
||||
}
|
||||
Some(Type::Named(
|
||||
context
|
||||
.variant(&name, *span)?
|
||||
.ok_or_else(|| {
|
||||
Diagnostic::error(
|
||||
"C005",
|
||||
"expected a choice variant pattern",
|
||||
*span,
|
||||
)
|
||||
})?
|
||||
.0,
|
||||
))
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
if let Some(ty) = ty {
|
||||
merge(&mut inferred, &mut nullable, ty, input.span)?;
|
||||
}
|
||||
}
|
||||
let explicitly_typed = inferred.is_some();
|
||||
let mut literal_types = Vec::new();
|
||||
for clause in group {
|
||||
let input = &clause.inputs[axis];
|
||||
let InputKind::Value(pattern) = &input.kind else {
|
||||
unreachable!("input kind checked above")
|
||||
};
|
||||
if let Pattern::Literal { value, span } = pattern {
|
||||
if matches!(value, Literal::Null) {
|
||||
nullable = true;
|
||||
continue;
|
||||
}
|
||||
let value = constants::literal(value, inferred.as_ref(), *span)?;
|
||||
literal_types.push((value.ty, input.span));
|
||||
}
|
||||
}
|
||||
if !explicitly_typed && literal_types.iter().any(|(ty, _)| *ty == Type::Decimal) {
|
||||
inferred = Some(Type::Decimal);
|
||||
}
|
||||
for (ty, span) in literal_types {
|
||||
let ty = if ty == Type::Int && inferred == Some(Type::Decimal) {
|
||||
Type::Decimal
|
||||
} else {
|
||||
ty
|
||||
};
|
||||
merge(&mut inferred, &mut nullable, ty, span)?;
|
||||
}
|
||||
let ty = inferred.ok_or_else(|| {
|
||||
Diagnostic::error(
|
||||
"C005",
|
||||
"cannot infer this parameter's base type from null/other alone",
|
||||
group[0].inputs[axis].span,
|
||||
)
|
||||
})?;
|
||||
let ty = if nullable { ty.nullable() } else { ty };
|
||||
symbols::validate_type_constraints(&ty, context.types, group[0].inputs[axis].span)?;
|
||||
parameters.push(Parameter::Value(ty));
|
||||
}
|
||||
let outputs = context.fields(&group[0].outputs)?;
|
||||
for field in &outputs {
|
||||
symbols::validate_type_constraints(&field.ty, context.types, group[0].span)?;
|
||||
}
|
||||
for clause in &group[1..] {
|
||||
if context.fields(&clause.outputs)? != outputs {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"all clauses must have identical ordered output names and types",
|
||||
clause.span,
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok((parameters, outputs))
|
||||
}
|
||||
|
||||
fn handler_signature(
|
||||
source: &crate::syntax::HandlerSignature,
|
||||
context: &Context<'_>,
|
||||
) -> Checked<HandlerSignature> {
|
||||
let inputs = context.fields(&source.inputs)?;
|
||||
for (field, source) in inputs.iter().zip(&source.inputs) {
|
||||
symbols::validate_type_constraints(&field.ty, context.types, source.span)?;
|
||||
}
|
||||
let outputs = context.fields(&source.outputs)?;
|
||||
for (field, source) in outputs.iter().zip(&source.outputs) {
|
||||
symbols::validate_type_constraints(&field.ty, context.types, source.span)?;
|
||||
}
|
||||
Ok(HandlerSignature {
|
||||
bounds: inputs.iter().map(|field| field.bounds.clone()).collect(),
|
||||
parameters: inputs.into_iter().map(|field| field.ty).collect(),
|
||||
outputs,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn field_bounds(
|
||||
field: &crate::syntax::Field,
|
||||
ty: &Type,
|
||||
context: &Context<'_>,
|
||||
) -> Checked<Domain> {
|
||||
if field.bounds.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let pattern = Pattern::Typed {
|
||||
ty: field.ty.clone(),
|
||||
bounds: field.bounds.clone(),
|
||||
};
|
||||
let atom = match normalize(&pattern, ty, context, field.span)? {
|
||||
PatternSet::Int(Some((lo, hi))) => Atom::Int(lo, hi),
|
||||
PatternSet::Decimal(Some((lo, hi))) => Atom::Decimal(lo, hi),
|
||||
_ => {
|
||||
return Err(Diagnostic::error(
|
||||
"C011",
|
||||
"field constraint has an empty domain",
|
||||
field.span,
|
||||
));
|
||||
}
|
||||
};
|
||||
Ok(vec![atom])
|
||||
}
|
||||
|
||||
fn merge(
|
||||
inferred: &mut Option<Type>,
|
||||
nullable: &mut bool,
|
||||
ty: Type,
|
||||
span: crate::source::Span,
|
||||
) -> Checked<()> {
|
||||
*nullable |= matches!(ty, Type::Nullable(_));
|
||||
let base = ty.base().clone();
|
||||
match inferred {
|
||||
Some(previous) if *previous != base => Err(Diagnostic::error(
|
||||
"C005",
|
||||
"clauses disagree on this parameter's base type",
|
||||
span,
|
||||
)),
|
||||
_ => {
|
||||
*inferred = Some(base);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn check(
|
||||
group: &[&FunctionClause],
|
||||
parameters: &[Parameter],
|
||||
context: &Context<'_>,
|
||||
) -> Checked<Vec<Vec<Domain>>> {
|
||||
let value_axes = parameters
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(axis, parameter)| parameter.value_type().map(|ty| (axis, ty)))
|
||||
.collect::<Vec<_>>();
|
||||
let patterns: Vec<Vec<_>> = group
|
||||
.iter()
|
||||
.map(|clause| {
|
||||
value_axes
|
||||
.iter()
|
||||
.map(|(axis, ty)| {
|
||||
let input = &clause.inputs[*axis];
|
||||
let InputKind::Value(pattern) = &input.kind else {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"all clauses must agree whether an input is a handler",
|
||||
input.span,
|
||||
));
|
||||
};
|
||||
normalize(pattern, ty, context, input.span)
|
||||
})
|
||||
.collect::<Checked<_>>()
|
||||
})
|
||||
.collect::<Checked<_>>()?;
|
||||
let atoms: Vec<_> = value_axes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(axis, (_, ty))| partition(ty, patterns.iter().map(|row| &row[axis]), context))
|
||||
.collect();
|
||||
let mut products: Vec<Vec<BTreeSet<usize>>> = Vec::new();
|
||||
for (row, clause) in patterns.iter().zip(group) {
|
||||
let mut product = Vec::new();
|
||||
for (axis, pattern) in row.iter().enumerate() {
|
||||
let selected: BTreeSet<_> = atoms[axis]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(id, atom)| {
|
||||
let included = if matches!(pattern, PatternSet::Other) {
|
||||
!patterns.iter().any(|row| {
|
||||
!matches!(row[axis], PatternSet::Other) && contains(&row[axis], atom)
|
||||
})
|
||||
} else {
|
||||
contains(pattern, atom)
|
||||
};
|
||||
included.then_some(id)
|
||||
})
|
||||
.collect();
|
||||
if selected.is_empty() {
|
||||
return Err(Diagnostic::error(
|
||||
"C009",
|
||||
"unreachable clause: this pattern selects no values",
|
||||
clause.inputs[value_axes[axis].0].span,
|
||||
));
|
||||
}
|
||||
product.push(selected);
|
||||
}
|
||||
for (previous, other) in products.iter().enumerate() {
|
||||
if product
|
||||
.iter()
|
||||
.zip(other)
|
||||
.all(|(left, right)| !left.is_disjoint(right))
|
||||
{
|
||||
let mut error = Diagnostic::error("C009", "function clauses overlap", clause.span);
|
||||
error.related.push(Label {
|
||||
span: group[previous].span,
|
||||
message: "overlapping clause".into(),
|
||||
});
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
products.push(product);
|
||||
}
|
||||
// Subtract disjoint rectangles without enumerating the input Cartesian product.
|
||||
let mut remaining = vec![
|
||||
atoms
|
||||
.iter()
|
||||
.map(|axis| (0..axis.len()).collect())
|
||||
.collect::<Vec<BTreeSet<_>>>(),
|
||||
];
|
||||
for covered in &products {
|
||||
remaining = remaining
|
||||
.into_iter()
|
||||
.flat_map(|region| subtract(region, covered))
|
||||
.collect();
|
||||
}
|
||||
if !remaining.is_empty() {
|
||||
return Err(Diagnostic::error(
|
||||
"C009",
|
||||
"function clauses do not cover every input combination",
|
||||
group[0].span,
|
||||
));
|
||||
}
|
||||
Ok(products
|
||||
.into_iter()
|
||||
.map(|row| {
|
||||
row.into_iter()
|
||||
.enumerate()
|
||||
.map(|(axis, selected)| {
|
||||
selected
|
||||
.into_iter()
|
||||
.map(|id| atoms[axis][id].clone())
|
||||
.collect()
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn subtract(
|
||||
region: Vec<BTreeSet<usize>>,
|
||||
covered: &[BTreeSet<usize>],
|
||||
) -> Vec<Vec<BTreeSet<usize>>> {
|
||||
let intersections: Vec<_> = region
|
||||
.iter()
|
||||
.zip(covered)
|
||||
.map(|(left, right)| left.intersection(right).copied().collect::<BTreeSet<_>>())
|
||||
.collect();
|
||||
if intersections.iter().any(BTreeSet::is_empty) {
|
||||
return vec![region];
|
||||
}
|
||||
let mut remainder = Vec::new();
|
||||
for axis in 0..region.len() {
|
||||
let difference: BTreeSet<_> = region[axis].difference(&covered[axis]).copied().collect();
|
||||
if difference.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let mut piece = intersections[..axis].to_vec();
|
||||
piece.push(difference);
|
||||
piece.extend_from_slice(®ion[axis + 1..]);
|
||||
remainder.push(piece);
|
||||
}
|
||||
remainder
|
||||
}
|
||||
|
||||
fn normalize(
|
||||
pattern: &Pattern,
|
||||
parameter: &Type,
|
||||
context: &Context<'_>,
|
||||
span: crate::source::Span,
|
||||
) -> Checked<PatternSet> {
|
||||
match pattern {
|
||||
Pattern::Other(_) => Ok(PatternSet::Other),
|
||||
Pattern::Variant {
|
||||
name,
|
||||
arguments,
|
||||
bindings,
|
||||
..
|
||||
} => {
|
||||
let name = path_name(name);
|
||||
let (ty, variant, fields) =
|
||||
if let Some(found) = context.related_variant(&name, arguments, span)? {
|
||||
found
|
||||
} else {
|
||||
if !arguments.is_empty() {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"choice variants do not accept type arguments",
|
||||
span,
|
||||
));
|
||||
}
|
||||
let (id, variant) = context.variant(&name, span)?.ok_or_else(|| {
|
||||
Diagnostic::error("C005", "expected choice variant", span)
|
||||
})?;
|
||||
let Shape::Choice(variants) = &context.types[id].shape else {
|
||||
unreachable!()
|
||||
};
|
||||
(Type::Named(id), variant, variants[variant].fields.clone())
|
||||
};
|
||||
if ty != *parameter.base() {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"variant pattern has the wrong parameter type",
|
||||
span,
|
||||
));
|
||||
}
|
||||
if fields.len() != bindings.len() {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"choice pattern payload arity mismatch",
|
||||
span,
|
||||
));
|
||||
}
|
||||
Ok(PatternSet::Variant(variant))
|
||||
}
|
||||
Pattern::Literal {
|
||||
value: Literal::Null,
|
||||
..
|
||||
} => Ok(PatternSet::Null),
|
||||
Pattern::Literal { value, .. } => {
|
||||
let mut value = constants::literal(value, Some(parameter), span)?;
|
||||
if value.ty == Type::Int && parameter.base() == &Type::Decimal {
|
||||
value = constants::promote(value);
|
||||
}
|
||||
let ExpressionKind::Constant(value) = value.kind else {
|
||||
unreachable!()
|
||||
};
|
||||
Ok(match value {
|
||||
Constant::Bool(value) => PatternSet::Bool(value),
|
||||
Constant::Int(value) => PatternSet::Int(Some((value, value))),
|
||||
Constant::Decimal(value) => PatternSet::Decimal(Some((value, value))),
|
||||
_ => PatternSet::Literal(value),
|
||||
})
|
||||
}
|
||||
Pattern::Typed { ty, bounds } => {
|
||||
if let Some((id, variant)) = context.variant(&path_name(&ty.name), span)? {
|
||||
let Shape::Choice(variants) = &context.types[id].shape else {
|
||||
unreachable!()
|
||||
};
|
||||
if ty.nullable || !bounds.is_empty() || !variants[variant].fields.is_empty() {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"payload variants require an explicit binding list",
|
||||
span,
|
||||
));
|
||||
}
|
||||
return Ok(PatternSet::Variant(variant));
|
||||
}
|
||||
if bounds.is_empty() {
|
||||
return Ok(PatternSet::Any(ty.nullable));
|
||||
}
|
||||
if ty.nullable || !matches!(parameter.base(), Type::Int | Type::Decimal) {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"range bounds require non-nullable Int or Decimal patterns",
|
||||
span,
|
||||
));
|
||||
}
|
||||
let mut lower = false;
|
||||
let mut upper = false;
|
||||
let mut integers = Some((i64::MIN, i64::MAX));
|
||||
let mut decimals = Some((DecimalValue::minimum(), DecimalValue::maximum()));
|
||||
for bound in bounds {
|
||||
let seen = match bound.comparison {
|
||||
Op::Greater | Op::GreaterEqual => &mut lower,
|
||||
Op::Less | Op::LessEqual => &mut upper,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
if *seen {
|
||||
return Err(Diagnostic::error(
|
||||
"C009",
|
||||
"a range may have at most one lower and one upper bound",
|
||||
bound.span,
|
||||
));
|
||||
}
|
||||
*seen = true;
|
||||
let mut value = constants::literal(
|
||||
&Literal::Number(bound.number.clone()),
|
||||
Some(parameter),
|
||||
bound.span,
|
||||
)?;
|
||||
if value.ty == Type::Int && parameter.base() == &Type::Decimal {
|
||||
value = constants::promote(value);
|
||||
}
|
||||
match value.kind {
|
||||
ExpressionKind::Constant(Constant::Int(value))
|
||||
if parameter.base() == &Type::Int =>
|
||||
{
|
||||
integers = integers.and_then(|(lo, hi)| {
|
||||
Some(match bound.comparison {
|
||||
Op::Greater => (value.checked_add(1)?, hi),
|
||||
Op::GreaterEqual => (value, hi),
|
||||
Op::Less => (lo, value.checked_sub(1)?),
|
||||
Op::LessEqual => (lo, value),
|
||||
_ => unreachable!(),
|
||||
})
|
||||
});
|
||||
}
|
||||
ExpressionKind::Constant(Constant::Decimal(value))
|
||||
if parameter.base() == &Type::Decimal =>
|
||||
{
|
||||
decimals = decimals.and_then(|(lo, hi)| {
|
||||
Some(match bound.comparison {
|
||||
Op::Greater => (value.next_up()?, hi),
|
||||
Op::GreaterEqual => (value, hi),
|
||||
Op::Less => (lo, value.next_down()?),
|
||||
Op::LessEqual => (lo, value),
|
||||
_ => unreachable!(),
|
||||
})
|
||||
});
|
||||
}
|
||||
_ => {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"range bound has the wrong numeric type",
|
||||
bound.span,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(if parameter.base() == &Type::Int {
|
||||
PatternSet::Int(integers.filter(|(lo, hi)| lo <= hi))
|
||||
} else {
|
||||
PatternSet::Decimal(decimals.filter(|(lo, hi)| lo <= hi))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn partition<'a>(
|
||||
ty: &Type,
|
||||
patterns: impl Iterator<Item = &'a PatternSet>,
|
||||
context: &Context<'_>,
|
||||
) -> Domain {
|
||||
let patterns: Vec<_> = patterns.collect();
|
||||
let mut atoms = match ty.base() {
|
||||
Type::Bool => vec![Atom::Bool(false), Atom::Bool(true)],
|
||||
Type::Named(id) => match &context.types[*id].shape {
|
||||
Shape::Choice(variants) => (0..variants.len()).map(Atom::Variant).collect(),
|
||||
_ => vec![Atom::Any],
|
||||
},
|
||||
Type::Related(_) => vec![Atom::Variant(0), Atom::Variant(1)],
|
||||
Type::Int => {
|
||||
let mut cuts = BTreeSet::from([i64::MIN]);
|
||||
for pattern in &patterns {
|
||||
if let PatternSet::Int(Some((lo, hi))) = pattern {
|
||||
cuts.insert(*lo);
|
||||
if let Some(next) = hi.checked_add(1) {
|
||||
cuts.insert(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
let cuts: Vec<_> = cuts.into_iter().collect();
|
||||
cuts.iter()
|
||||
.enumerate()
|
||||
.map(|(index, lo)| {
|
||||
Atom::Int(*lo, cuts.get(index + 1).map_or(i64::MAX, |next| next - 1))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
Type::Decimal => {
|
||||
let mut cuts = BTreeSet::from([DecimalValue::minimum()]);
|
||||
for pattern in &patterns {
|
||||
if let PatternSet::Decimal(Some((lo, hi))) = pattern {
|
||||
cuts.insert(*lo);
|
||||
if let Some(next) = hi.next_up() {
|
||||
cuts.insert(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
let cuts: Vec<_> = cuts.into_iter().collect();
|
||||
cuts.iter()
|
||||
.enumerate()
|
||||
.map(|(index, lo)| {
|
||||
Atom::Decimal(
|
||||
*lo,
|
||||
cuts.get(index + 1)
|
||||
.and_then(|next| next.next_down())
|
||||
.unwrap_or_else(DecimalValue::maximum),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
Type::Text | Type::Float => {
|
||||
let mut constants = Vec::new();
|
||||
for pattern in &patterns {
|
||||
if let PatternSet::Literal(value) = pattern
|
||||
&& !constants.contains(value)
|
||||
{
|
||||
constants.push(value.clone());
|
||||
}
|
||||
}
|
||||
let mut atoms: Vec<_> = constants.iter().cloned().map(Atom::Literal).collect();
|
||||
atoms.push(Atom::Remainder(constants));
|
||||
atoms
|
||||
}
|
||||
_ => vec![Atom::Any],
|
||||
};
|
||||
if matches!(ty, Type::Nullable(_)) {
|
||||
atoms.push(Atom::Null);
|
||||
}
|
||||
atoms
|
||||
}
|
||||
|
||||
fn contains(pattern: &PatternSet, atom: &Atom) -> bool {
|
||||
match (pattern, atom) {
|
||||
(PatternSet::Any(nullable), Atom::Null) => *nullable,
|
||||
(PatternSet::Any(_), _) => true,
|
||||
(PatternSet::Null, Atom::Null) => true,
|
||||
(PatternSet::Bool(left), Atom::Bool(right)) => left == right,
|
||||
(PatternSet::Variant(left), Atom::Variant(right)) => left == right,
|
||||
(PatternSet::Literal(left), Atom::Literal(right)) => left == right,
|
||||
(PatternSet::Int(Some((lo, hi))), Atom::Int(start, _)) => lo <= start && start <= hi,
|
||||
(PatternSet::Decimal(Some((lo, hi))), Atom::Decimal(start, _)) => {
|
||||
lo <= start && start <= hi
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,403 @@
|
||||
use crate::diagnostic::Diagnostic;
|
||||
use crate::hir::{CollectionOp as Op, Expression as Value, ExpressionKind, SequenceKind};
|
||||
use crate::source::Span;
|
||||
use crate::syntax::{Expression, ExpressionKind as Ast};
|
||||
use crate::types::{Type, output_type};
|
||||
|
||||
use super::{Checked, body::Body, expressions::ungroup};
|
||||
|
||||
pub(super) fn operation(name: &str) -> Option<Op> {
|
||||
Some(match name {
|
||||
"each" => Op::Each,
|
||||
"parallel_each" => Op::ParallelEach,
|
||||
"reduce" => Op::Reduce,
|
||||
"reduce_until" => Op::ReduceUntil,
|
||||
"filter" => Op::Filter,
|
||||
"find" => Op::Find,
|
||||
"sum" => Op::Sum,
|
||||
"append" => Op::Append,
|
||||
"first" => Op::First,
|
||||
"get" => Op::Get,
|
||||
"put" => Op::Put,
|
||||
"remove" => Op::Remove,
|
||||
"entries" => Op::Entries,
|
||||
"length" => Op::Length,
|
||||
"close" => Op::Close,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
struct Sequence {
|
||||
value: Value,
|
||||
kind: SequenceKind,
|
||||
element: Type,
|
||||
}
|
||||
|
||||
impl Body<'_, '_> {
|
||||
pub(super) fn collection(
|
||||
&mut self,
|
||||
operation: Op,
|
||||
sources: &[Expression],
|
||||
expected: Option<&Type>,
|
||||
span: Span,
|
||||
) -> Checked<Value> {
|
||||
if matches!(
|
||||
operation,
|
||||
Op::Each | Op::ParallelEach | Op::Filter | Op::Find | Op::Reduce | Op::ReduceUntil
|
||||
) || (operation == Op::Sum && sources.len() != 1)
|
||||
{
|
||||
return self.handler_sequence(operation, sources, span);
|
||||
}
|
||||
let arity = match operation {
|
||||
Op::Put => 3,
|
||||
Op::Get | Op::Remove | Op::Append => 2,
|
||||
_ => 1,
|
||||
};
|
||||
if sources.len() != arity {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
format!("collection operation expects {arity} argument(s)"),
|
||||
span,
|
||||
));
|
||||
}
|
||||
let mut arguments = Vec::new();
|
||||
let mut sequence = None;
|
||||
let ty = match operation {
|
||||
Op::Append => {
|
||||
let list = if let Some(Type::List(_)) = expected {
|
||||
self.expression(&sources[0], expected)?
|
||||
} else if matches!(&ungroup(&sources[0]).kind, Ast::List(elements) if elements.is_empty())
|
||||
{
|
||||
let value = self.expression(&sources[1], None)?;
|
||||
let hint = Type::List(Box::new(value.ty.clone()));
|
||||
self.expression(&sources[0], Some(&hint))?
|
||||
} else {
|
||||
self.expression(&sources[0], None)?
|
||||
};
|
||||
let Type::List(element) = &list.ty else {
|
||||
return Err(expected_collection(span));
|
||||
};
|
||||
let value = self.expression(&sources[1], Some(element))?;
|
||||
let ty = list.ty.clone();
|
||||
arguments.extend([list, value]);
|
||||
ty
|
||||
}
|
||||
Op::Sum | Op::First => {
|
||||
let hint = expected.map(|ty| Type::List(Box::new(ty.clone())));
|
||||
let list = self.expression(
|
||||
&sources[0],
|
||||
hint.as_ref()
|
||||
.filter(|_| super::expressions::needs_context(&sources[0])),
|
||||
)?;
|
||||
let Type::List(element) = &list.ty else {
|
||||
return Err(expected_collection(span));
|
||||
};
|
||||
let ty = if operation == Op::First {
|
||||
element.as_ref().clone().nullable()
|
||||
} else {
|
||||
if !element.numeric() {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"sum requires Int, Decimal or Float elements",
|
||||
span,
|
||||
));
|
||||
}
|
||||
element.as_ref().clone()
|
||||
};
|
||||
arguments.push(list);
|
||||
if operation == Op::Sum {
|
||||
sequence = Some(SequenceKind::List);
|
||||
}
|
||||
ty
|
||||
}
|
||||
Op::Get | Op::Put | Op::Remove => {
|
||||
let hint = if matches!(operation, Op::Put | Op::Remove) {
|
||||
expected.filter(|ty| matches!(ty, Type::Map(_, _)))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let map = if hint.is_none()
|
||||
&& operation == Op::Put
|
||||
&& matches!(&ungroup(&sources[0]).kind, Ast::Map(entries) if entries.is_empty())
|
||||
{
|
||||
let key = self.expression(&sources[1], None)?;
|
||||
let value = self.expression(&sources[2], None)?;
|
||||
let ty = Type::Map(Box::new(key.ty), Box::new(value.ty));
|
||||
self.expression(&sources[0], Some(&ty))?
|
||||
} else {
|
||||
self.expression(&sources[0], hint)?
|
||||
};
|
||||
let Type::Map(key, value) = &map.ty else {
|
||||
return Err(expected_collection(span));
|
||||
};
|
||||
let key = self.expression(&sources[1], Some(key))?;
|
||||
let ty = if operation == Op::Get {
|
||||
value.as_ref().clone().nullable()
|
||||
} else {
|
||||
map.ty.clone()
|
||||
};
|
||||
let replacement = if operation == Op::Put {
|
||||
Some(self.expression(&sources[2], Some(value))?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
arguments.extend([map, key]);
|
||||
arguments.extend(replacement);
|
||||
ty
|
||||
}
|
||||
Op::Entries => {
|
||||
let map = self.expression(&sources[0], None)?;
|
||||
let Type::Map(key, value) = &map.ty else {
|
||||
return Err(expected_collection(span));
|
||||
};
|
||||
let ty = Type::List(Box::new(Type::MapEntry(key.clone(), value.clone())));
|
||||
arguments.push(map);
|
||||
ty
|
||||
}
|
||||
Op::Length => {
|
||||
let value = self.expression(&sources[0], None)?;
|
||||
if !matches!(
|
||||
value.ty,
|
||||
Type::List(_) | Type::Map(_, _) | Type::Text | Type::Bytes
|
||||
) {
|
||||
return Err(expected_collection(span));
|
||||
}
|
||||
arguments.push(value);
|
||||
Type::Int
|
||||
}
|
||||
Op::Close => {
|
||||
let value = self.expression(&sources[0], None)?;
|
||||
if !matches!(
|
||||
value.ty,
|
||||
Type::Stream(_) | Type::AsyncStream(_) | Type::RowStream(_)
|
||||
) {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"close requires a Stream or AsyncStream value",
|
||||
sources[0].span,
|
||||
));
|
||||
}
|
||||
sequence = Some(match value.ty {
|
||||
Type::AsyncStream(_) => SequenceKind::AsyncStream,
|
||||
Type::RowStream(_) => SequenceKind::RowStream,
|
||||
_ => SequenceKind::Stream,
|
||||
});
|
||||
arguments.push(value);
|
||||
Type::Unit
|
||||
}
|
||||
_ => unreachable!("handler operations dispatched above"),
|
||||
};
|
||||
Ok(Value {
|
||||
kind: ExpressionKind::Collection {
|
||||
operation,
|
||||
handler: None,
|
||||
sequence,
|
||||
arguments,
|
||||
},
|
||||
ty,
|
||||
span,
|
||||
})
|
||||
}
|
||||
|
||||
fn handler_sequence(
|
||||
&mut self,
|
||||
operation: Op,
|
||||
sources: &[Expression],
|
||||
span: Span,
|
||||
) -> Checked<Value> {
|
||||
let (handler_arity, valid_arity) = match operation {
|
||||
Op::ParallelEach => (
|
||||
sources.len().saturating_sub(2),
|
||||
(3..=4).contains(&sources.len()),
|
||||
),
|
||||
Op::Reduce | Op::ReduceUntil => (2, sources.len() == 3),
|
||||
_ => (
|
||||
sources.len().saturating_sub(1),
|
||||
(2..=3).contains(&sources.len()),
|
||||
),
|
||||
};
|
||||
if !valid_arity || !(1..=2).contains(&handler_arity) {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
if matches!(operation, Op::Reduce | Op::ReduceUntil) {
|
||||
"expected handler, sequence and initial state"
|
||||
} else if operation == Op::ParallelEach {
|
||||
"expected handler, sequence, worker count and optional context"
|
||||
} else {
|
||||
"expected handler, sequence and optional context"
|
||||
},
|
||||
span,
|
||||
));
|
||||
}
|
||||
let (handler, signature) = self.resolve_handler(&sources[0], handler_arity)?;
|
||||
let sequence_value = self.sequence(&sources[1], Some(&signature.parameters[0]))?;
|
||||
if operation == Op::ParallelEach && sequence_value.kind == SequenceKind::RowStream {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"database row streams use sequential each to retain one bounded connection",
|
||||
sources[1].span,
|
||||
));
|
||||
}
|
||||
if sequence_value.element != signature.parameters[0] {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"handler element type does not match the sequence",
|
||||
sources[0].span,
|
||||
));
|
||||
}
|
||||
if matches!(operation, Op::Filter | Op::Find | Op::Sum)
|
||||
&& sequence_value.kind != SequenceKind::List
|
||||
{
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"this operation only accepts List sequences",
|
||||
sources[1].span,
|
||||
));
|
||||
}
|
||||
let output = output_type(&signature.outputs);
|
||||
let mut arguments = vec![sequence_value.value];
|
||||
let ty = match operation {
|
||||
Op::ParallelEach => {
|
||||
if output != Type::Unit {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"parallel_each requires a zero-output handler",
|
||||
sources[0].span,
|
||||
));
|
||||
}
|
||||
if !signature
|
||||
.parameters
|
||||
.iter()
|
||||
.all(|ty| ty.transferable(self.context.types))
|
||||
{
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"parallel_each handler inputs cannot contain Stream values",
|
||||
sources[0].span,
|
||||
));
|
||||
}
|
||||
arguments.push(self.expression(&sources[2], Some(&Type::Int))?);
|
||||
if signature.parameters.len() == 2 {
|
||||
arguments.push(self.expression(&sources[3], Some(&signature.parameters[1]))?);
|
||||
}
|
||||
Type::Unit
|
||||
}
|
||||
Op::Each => {
|
||||
if signature.parameters.len() == 2 {
|
||||
arguments.push(self.expression(&sources[2], Some(&signature.parameters[1]))?);
|
||||
}
|
||||
if matches!(
|
||||
sequence_value.kind,
|
||||
SequenceKind::Stream | SequenceKind::AsyncStream | SequenceKind::RowStream
|
||||
) && output != Type::Unit
|
||||
{
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"each on a stream requires a zero-output handler",
|
||||
sources[0].span,
|
||||
));
|
||||
}
|
||||
if output == Type::Unit {
|
||||
Type::Unit
|
||||
} else if output.value() {
|
||||
Type::List(Box::new(output))
|
||||
} else {
|
||||
return Err(invalid_handler(sources[0].span));
|
||||
}
|
||||
}
|
||||
Op::Filter if output == Type::Bool => {
|
||||
if signature.parameters.len() == 2 {
|
||||
arguments.push(self.expression(&sources[2], Some(&signature.parameters[1]))?);
|
||||
}
|
||||
Type::List(Box::new(sequence_value.element.clone()))
|
||||
}
|
||||
Op::Find if output == Type::Bool => {
|
||||
if signature.parameters.len() == 2 {
|
||||
arguments.push(self.expression(&sources[2], Some(&signature.parameters[1]))?);
|
||||
}
|
||||
sequence_value.element.clone().nullable()
|
||||
}
|
||||
Op::Sum if output.numeric() => {
|
||||
if signature.parameters.len() == 2 {
|
||||
arguments.push(self.expression(&sources[2], Some(&signature.parameters[1]))?);
|
||||
}
|
||||
output
|
||||
}
|
||||
Op::Reduce => {
|
||||
let state = self.expression(&sources[2], Some(&signature.parameters[1]))?;
|
||||
if signature.outputs.len() != 1 || output != state.ty {
|
||||
return Err(invalid_handler(sources[0].span));
|
||||
}
|
||||
arguments.push(state);
|
||||
output
|
||||
}
|
||||
Op::ReduceUntil => {
|
||||
let state = self.expression(&sources[2], Some(&signature.parameters[1]))?;
|
||||
if signature.outputs.len() != 2
|
||||
|| signature.outputs[0].ty != state.ty
|
||||
|| signature.outputs[1].name != "stop"
|
||||
|| signature.outputs[1].ty != Type::Bool
|
||||
{
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"reduce_until handler must output state followed by 'stop: Bool'",
|
||||
sources[0].span,
|
||||
));
|
||||
}
|
||||
arguments.push(state);
|
||||
signature.parameters[1].clone()
|
||||
}
|
||||
_ => return Err(invalid_handler(sources[0].span)),
|
||||
};
|
||||
Ok(Value {
|
||||
kind: ExpressionKind::Collection {
|
||||
operation,
|
||||
handler: Some(handler),
|
||||
sequence: Some(sequence_value.kind),
|
||||
arguments,
|
||||
},
|
||||
ty,
|
||||
span,
|
||||
})
|
||||
}
|
||||
|
||||
fn sequence(&mut self, source: &Expression, expected: Option<&Type>) -> Checked<Sequence> {
|
||||
let hint = expected
|
||||
.map(|element| Type::List(Box::new(element.clone())))
|
||||
.filter(|_| super::expressions::needs_context(source));
|
||||
let value = self.expression(source, hint.as_ref())?;
|
||||
let (kind, element) = match &value.ty {
|
||||
Type::List(element) => (SequenceKind::List, element.as_ref().clone()),
|
||||
Type::Bytes => (SequenceKind::Bytes, Type::Int),
|
||||
Type::Stream(element) => (SequenceKind::Stream, element.as_ref().clone()),
|
||||
Type::AsyncStream(element) => (SequenceKind::AsyncStream, element.as_ref().clone()),
|
||||
Type::RowStream(element) => (SequenceKind::RowStream, element.as_ref().clone()),
|
||||
_ => return Err(expected_sequence(source.span)),
|
||||
};
|
||||
Ok(Sequence {
|
||||
value,
|
||||
kind,
|
||||
element,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn expected_collection(span: Span) -> Diagnostic {
|
||||
Diagnostic::error("C005", "incorrect collection operand type", span)
|
||||
}
|
||||
|
||||
fn expected_sequence(span: Span) -> Diagnostic {
|
||||
Diagnostic::error(
|
||||
"C005",
|
||||
"expected List, Bytes, Stream or AsyncStream sequence",
|
||||
span,
|
||||
)
|
||||
}
|
||||
|
||||
fn invalid_handler(span: Span) -> Diagnostic {
|
||||
Diagnostic::error(
|
||||
"C005",
|
||||
"handler has invalid outputs for this collection operation",
|
||||
span,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use crate::diagnostic::Diagnostic;
|
||||
use crate::hir::Program;
|
||||
use crate::model::{DatabaseOwner, ModelScope};
|
||||
use crate::specialize::{self, Specialization};
|
||||
|
||||
pub(super) fn register(program: &mut Program, errors: &mut Vec<Diagnostic>) {
|
||||
let mut tenant_components = program
|
||||
.models
|
||||
.iter()
|
||||
.filter(|model| model.scope == ModelScope::Tenant)
|
||||
.filter_map(|model| model.package.split('.').next().map(str::to_owned))
|
||||
.collect::<BTreeSet<_>>();
|
||||
for job in &program.jobs {
|
||||
if job_uses_tenant_storage(program, job.function)
|
||||
&& let Some(component) = function_component(program, job.function)
|
||||
{
|
||||
tenant_components.insert(component.to_owned());
|
||||
}
|
||||
}
|
||||
program.tenant_components = tenant_components.iter().cloned().collect();
|
||||
|
||||
let route_components = program
|
||||
.api_routes
|
||||
.iter()
|
||||
.map(|route| reachable_components(program, route.function, &tenant_components))
|
||||
.collect::<Vec<_>>();
|
||||
for (route, components) in program.api_routes.iter_mut().zip(route_components) {
|
||||
if route.anonymous && !components.is_empty() {
|
||||
errors.push(Diagnostic::error(
|
||||
"C014",
|
||||
format!(
|
||||
"public API cannot reach tenant components [{}]; tenant access requires an authenticated API",
|
||||
components.join(", ")
|
||||
),
|
||||
route.span,
|
||||
));
|
||||
}
|
||||
route.components = components;
|
||||
}
|
||||
|
||||
let command_components = program
|
||||
.api_commands
|
||||
.iter()
|
||||
.map(|command| reachable_components(program, command.function, &tenant_components))
|
||||
.collect::<Vec<_>>();
|
||||
for (command, components) in program.api_commands.iter_mut().zip(command_components) {
|
||||
command.components = components;
|
||||
}
|
||||
|
||||
for rest in &mut program.api_rest {
|
||||
let component = program.models[rest.model]
|
||||
.package
|
||||
.split('.')
|
||||
.next()
|
||||
.filter(|component| tenant_components.contains(*component))
|
||||
.map(str::to_owned);
|
||||
rest.components = component.into_iter().collect();
|
||||
}
|
||||
|
||||
let job_components = program
|
||||
.jobs
|
||||
.iter()
|
||||
.map(|job| reachable_components(program, job.function, &tenant_components))
|
||||
.collect::<Vec<_>>();
|
||||
for (job, components) in program.jobs.iter_mut().zip(job_components) {
|
||||
job.components = components;
|
||||
}
|
||||
}
|
||||
|
||||
fn reachable_components(
|
||||
program: &Program,
|
||||
function: usize,
|
||||
tenant_components: &BTreeSet<String>,
|
||||
) -> Vec<String> {
|
||||
let root = Specialization {
|
||||
function,
|
||||
handlers: Vec::new(),
|
||||
};
|
||||
let Ok(reachable) = specialize::execution_reachable(&program.functions, [root]) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let mut components = BTreeSet::new();
|
||||
for instance in reachable {
|
||||
if let Some(component) = function_component(program, instance.function)
|
||||
&& tenant_components.contains(component)
|
||||
{
|
||||
components.insert(component.to_owned());
|
||||
}
|
||||
for owner in &program.database_effects[instance.function] {
|
||||
match owner {
|
||||
DatabaseOwner::Model(model)
|
||||
if program.models[*model].scope == ModelScope::Tenant =>
|
||||
{
|
||||
if let Some(component) = program.models[*model].package.split('.').next() {
|
||||
components.insert(component.to_owned());
|
||||
}
|
||||
}
|
||||
DatabaseOwner::Job(job) if job_uses_tenant_storage(program, *job) => {
|
||||
if let Some(component) = function_component(program, *job) {
|
||||
components.insert(component.to_owned());
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
components.into_iter().collect()
|
||||
}
|
||||
|
||||
fn job_uses_tenant_storage(program: &Program, function: usize) -> bool {
|
||||
program.database_effects[function].iter().any(|owner| {
|
||||
matches!(owner, DatabaseOwner::Model(model) if program.models[*model].scope == ModelScope::Tenant)
|
||||
})
|
||||
}
|
||||
|
||||
fn function_component(program: &Program, function: usize) -> Option<&str> {
|
||||
program.packages[program.functions[function].owner]
|
||||
.domain
|
||||
.as_ref()
|
||||
.map(|(component, _)| component.as_str())
|
||||
}
|
||||
@@ -0,0 +1,469 @@
|
||||
use crate::diagnostic::Diagnostic;
|
||||
use crate::hir::{CallArgument, CollectionOp, Expression, ExpressionKind, SequenceKind};
|
||||
use crate::source::Span;
|
||||
use crate::syntax::{ContextualExpression, Expression as SourceExpression, ExpressionKind as Ast};
|
||||
use crate::types::{Field, Type, output_type};
|
||||
|
||||
use super::expressions::ungroup;
|
||||
use super::{Checked, body::Body};
|
||||
|
||||
impl Body<'_, '_> {
|
||||
pub(super) fn contextual(
|
||||
&mut self,
|
||||
operation: &ContextualExpression,
|
||||
span: Span,
|
||||
) -> Checked<Expression> {
|
||||
match operation {
|
||||
ContextualExpression::Channel { element, capacity } => {
|
||||
let element = self.context.resolve_type(element)?;
|
||||
super::symbols::validate_type_constraints(&element, self.context.types, span)?;
|
||||
if !element.transferable(self.context.types) {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"a Channel item type must be transferable between threads",
|
||||
span,
|
||||
));
|
||||
}
|
||||
let capacity = self.capacity(
|
||||
capacity,
|
||||
dever_runtime::channel::MAX_CHANNEL_CAPACITY,
|
||||
"channel capacity",
|
||||
)?;
|
||||
Ok(Expression {
|
||||
kind: ExpressionKind::Channel {
|
||||
element: element.clone(),
|
||||
capacity: Box::new(capacity),
|
||||
},
|
||||
ty: Type::Channel(Box::new(element)),
|
||||
span,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn named_concurrency(
|
||||
&mut self,
|
||||
name: &str,
|
||||
arguments: &[SourceExpression],
|
||||
span: Span,
|
||||
) -> Option<Checked<Expression>> {
|
||||
let result = match name {
|
||||
"run" => self.run(arguments, span),
|
||||
"wait" => self.wait(arguments, span),
|
||||
"stop" => self.stop(arguments, span),
|
||||
"group" => self.group(arguments, span),
|
||||
"parallel" => self.parallel(arguments, span),
|
||||
"blocking" => self.blocking(arguments, span),
|
||||
"send" => self.send(arguments, span),
|
||||
"receive" => self.receive(arguments, span),
|
||||
"close" => self.close(arguments, span),
|
||||
"timeout" => self.timeout(arguments, span),
|
||||
"race" => self.race(arguments, span),
|
||||
"stream" => self.stream(arguments, span),
|
||||
_ => return None,
|
||||
};
|
||||
Some(result)
|
||||
}
|
||||
|
||||
fn timeout(&mut self, arguments: &[SourceExpression], span: Span) -> Checked<Expression> {
|
||||
exact_arity("timeout", arguments, 3, span)?;
|
||||
let (task, Type::Task(outputs)) = self.affine_operand(&arguments[0], true)? else {
|
||||
return Err(Diagnostic::error("C005", "timeout requires a Task", span));
|
||||
};
|
||||
let duration = self.expression(&arguments[1], Some(&Type::Int))?;
|
||||
let handler = self.handler_reference(
|
||||
&arguments[2],
|
||||
&crate::types::HandlerSignature {
|
||||
parameters: vec![],
|
||||
bounds: vec![],
|
||||
outputs: outputs.clone(),
|
||||
},
|
||||
)?;
|
||||
Ok(Expression {
|
||||
kind: ExpressionKind::Intrinsic {
|
||||
operation: crate::intrinsic::Intrinsic::TaskTimeout,
|
||||
handler: Some(handler),
|
||||
arguments: vec![task, duration],
|
||||
},
|
||||
ty: output_type(&outputs),
|
||||
span,
|
||||
})
|
||||
}
|
||||
|
||||
fn race(&mut self, sources: &[SourceExpression], span: Span) -> Checked<Expression> {
|
||||
if sources.len() < 2 {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"race requires at least two Tasks",
|
||||
span,
|
||||
));
|
||||
}
|
||||
let mut arguments = Vec::new();
|
||||
let mut signature = None;
|
||||
for source in sources {
|
||||
let (task, Type::Task(outputs)) = self.affine_operand(source, true)? else {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"race requires Tasks",
|
||||
source.span,
|
||||
));
|
||||
};
|
||||
if signature
|
||||
.as_ref()
|
||||
.is_some_and(|expected| expected != &outputs)
|
||||
{
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"race Tasks must have identical output signatures",
|
||||
source.span,
|
||||
));
|
||||
}
|
||||
signature = Some(outputs);
|
||||
arguments.push(task);
|
||||
}
|
||||
Ok(Expression {
|
||||
kind: ExpressionKind::Intrinsic {
|
||||
operation: crate::intrinsic::Intrinsic::TaskRace,
|
||||
handler: None,
|
||||
arguments,
|
||||
},
|
||||
ty: output_type(&signature.expect("at least two tasks")),
|
||||
span,
|
||||
})
|
||||
}
|
||||
|
||||
fn stream(&mut self, sources: &[SourceExpression], span: Span) -> Checked<Expression> {
|
||||
exact_arity("stream", sources, 1, span)?;
|
||||
let value = self.expression(&sources[0], None)?;
|
||||
let element = match &value.ty {
|
||||
Type::Channel(element) | Type::List(element) => element.as_ref().clone(),
|
||||
Type::Bytes => Type::Int,
|
||||
_ => {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"stream requires Channel, List or Bytes",
|
||||
span,
|
||||
));
|
||||
}
|
||||
};
|
||||
if !element.transferable(self.context.types) {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"async stream elements must be transferable",
|
||||
span,
|
||||
));
|
||||
}
|
||||
Ok(Expression {
|
||||
kind: ExpressionKind::Intrinsic {
|
||||
operation: crate::intrinsic::Intrinsic::StreamOf,
|
||||
handler: None,
|
||||
arguments: vec![value],
|
||||
},
|
||||
ty: Type::AsyncStream(Box::new(element)),
|
||||
span,
|
||||
})
|
||||
}
|
||||
|
||||
fn wait(&mut self, arguments: &[SourceExpression], span: Span) -> Checked<Expression> {
|
||||
exact_arity("wait", arguments, 1, span)?;
|
||||
let (task, ty) = self.affine_operand(&arguments[0], true)?;
|
||||
match ty {
|
||||
Type::Task(outputs) => Ok(Expression {
|
||||
kind: ExpressionKind::AwaitTask(Box::new(task)),
|
||||
ty: output_type(&outputs),
|
||||
span,
|
||||
}),
|
||||
Type::Group => Ok(Expression {
|
||||
kind: ExpressionKind::AwaitGroup(Box::new(task)),
|
||||
ty: Type::Unit,
|
||||
span,
|
||||
}),
|
||||
_ => unreachable!("affine operands are Task or Group"),
|
||||
}
|
||||
}
|
||||
|
||||
fn run(&mut self, arguments: &[SourceExpression], span: Span) -> Checked<Expression> {
|
||||
match arguments {
|
||||
[call] => {
|
||||
let call = self.checked_static_call(call)?;
|
||||
self.require_transferable_call(&call.arguments, &call.outputs, span)?;
|
||||
let outputs = call.outputs.clone();
|
||||
Ok(Expression {
|
||||
kind: ExpressionKind::RunCall {
|
||||
group: None,
|
||||
target: call.target,
|
||||
arguments: call.arguments,
|
||||
},
|
||||
ty: Type::Task(outputs),
|
||||
span,
|
||||
})
|
||||
}
|
||||
[group, call] => {
|
||||
let (group, ty) = self.affine_operand(group, false)?;
|
||||
if ty != Type::Group {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"the first argument to run(group, call) must be a Group local",
|
||||
arguments[0].span,
|
||||
));
|
||||
}
|
||||
let call = self.checked_static_call(call)?;
|
||||
if !call.outputs.is_empty() {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"a Group accepts only zero-output calls",
|
||||
arguments[1].span,
|
||||
));
|
||||
}
|
||||
self.require_transferable_call(&call.arguments, &call.outputs, span)?;
|
||||
Ok(Expression {
|
||||
kind: ExpressionKind::RunCall {
|
||||
group: Some(Box::new(group)),
|
||||
target: call.target,
|
||||
arguments: call.arguments,
|
||||
},
|
||||
ty: Type::Unit,
|
||||
span,
|
||||
})
|
||||
}
|
||||
_ => Err(Diagnostic::error(
|
||||
"C005",
|
||||
"'run' expects a call, optionally preceded by a Group",
|
||||
span,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn stop(&mut self, arguments: &[SourceExpression], span: Span) -> Checked<Expression> {
|
||||
exact_arity("stop", arguments, 1, span)?;
|
||||
let (value, ty) = self.affine_operand(&arguments[0], true)?;
|
||||
let kind = match ty {
|
||||
Type::Task(outputs) if outputs.is_empty() => ExpressionKind::StopTask(Box::new(value)),
|
||||
Type::Task(_) => {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"stop accepts only a zero-output Task; wait for result-bearing tasks",
|
||||
arguments[0].span,
|
||||
));
|
||||
}
|
||||
Type::Group => ExpressionKind::StopGroup(Box::new(value)),
|
||||
_ => unreachable!("affine operands are Task or Group"),
|
||||
};
|
||||
Ok(Expression {
|
||||
kind,
|
||||
ty: Type::Unit,
|
||||
span,
|
||||
})
|
||||
}
|
||||
|
||||
fn group(&mut self, arguments: &[SourceExpression], span: Span) -> Checked<Expression> {
|
||||
exact_arity("group", arguments, 1, span)?;
|
||||
let limit = self.capacity(
|
||||
&arguments[0],
|
||||
dever_runtime::task::MAX_GROUP_TASKS,
|
||||
"group limit",
|
||||
)?;
|
||||
Ok(Expression {
|
||||
kind: ExpressionKind::Group(Box::new(limit)),
|
||||
ty: Type::Group,
|
||||
span,
|
||||
})
|
||||
}
|
||||
|
||||
fn parallel(&mut self, arguments: &[SourceExpression], span: Span) -> Checked<Expression> {
|
||||
exact_arity("parallel", arguments, 1, span)?;
|
||||
let call = self.checked_static_call(&arguments[0])?;
|
||||
if !call.pure {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"parallel requires a pure function call",
|
||||
arguments[0].span,
|
||||
));
|
||||
}
|
||||
self.require_transferable_call(&call.arguments, &call.outputs, span)?;
|
||||
Ok(Expression {
|
||||
kind: ExpressionKind::ParallelCall {
|
||||
target: call.target,
|
||||
arguments: call.arguments,
|
||||
},
|
||||
ty: output_type(&call.outputs),
|
||||
span,
|
||||
})
|
||||
}
|
||||
|
||||
fn blocking(&mut self, arguments: &[SourceExpression], span: Span) -> Checked<Expression> {
|
||||
exact_arity("blocking", arguments, 1, span)?;
|
||||
let call = self.checked_static_call(&arguments[0])?;
|
||||
self.require_transferable_call(&call.arguments, &call.outputs, span)?;
|
||||
Ok(Expression {
|
||||
kind: ExpressionKind::BlockingCall {
|
||||
target: call.target,
|
||||
arguments: call.arguments,
|
||||
},
|
||||
ty: output_type(&call.outputs),
|
||||
span,
|
||||
})
|
||||
}
|
||||
|
||||
fn send(&mut self, arguments: &[SourceExpression], span: Span) -> Checked<Expression> {
|
||||
exact_arity("send", arguments, 2, span)?;
|
||||
let channel = self.expression(&arguments[0], None)?;
|
||||
let Type::Channel(element) = &channel.ty else {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"send requires a Channel as its first argument",
|
||||
arguments[0].span,
|
||||
));
|
||||
};
|
||||
let value = self.expression(&arguments[1], Some(element))?;
|
||||
Ok(Expression {
|
||||
kind: ExpressionKind::ChannelSend {
|
||||
channel: Box::new(channel),
|
||||
value: Box::new(value),
|
||||
},
|
||||
ty: Type::Unit,
|
||||
span,
|
||||
})
|
||||
}
|
||||
|
||||
fn receive(&mut self, arguments: &[SourceExpression], span: Span) -> Checked<Expression> {
|
||||
exact_arity("receive", arguments, 1, span)?;
|
||||
let channel = self.expression(&arguments[0], None)?;
|
||||
let Type::Channel(element) = &channel.ty else {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"receive requires a Channel value",
|
||||
arguments[0].span,
|
||||
));
|
||||
};
|
||||
let ty = element.as_ref().clone().nullable();
|
||||
Ok(Expression {
|
||||
kind: ExpressionKind::ChannelReceive(Box::new(channel)),
|
||||
ty,
|
||||
span,
|
||||
})
|
||||
}
|
||||
|
||||
fn close(&mut self, arguments: &[SourceExpression], span: Span) -> Checked<Expression> {
|
||||
exact_arity("close", arguments, 1, span)?;
|
||||
let value = self.expression(&arguments[0], None)?;
|
||||
let kind = match &value.ty {
|
||||
Type::Channel(_) => ExpressionKind::ChannelClose(Box::new(value)),
|
||||
Type::Stream(_) | Type::AsyncStream(_) | Type::RowStream(_) => {
|
||||
ExpressionKind::Collection {
|
||||
operation: CollectionOp::Close,
|
||||
handler: None,
|
||||
sequence: Some(match value.ty {
|
||||
Type::AsyncStream(_) => SequenceKind::AsyncStream,
|
||||
Type::RowStream(_) => SequenceKind::RowStream,
|
||||
_ => SequenceKind::Stream,
|
||||
}),
|
||||
arguments: vec![value],
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"close requires a Stream, AsyncStream or Channel value",
|
||||
arguments[0].span,
|
||||
));
|
||||
}
|
||||
};
|
||||
Ok(Expression {
|
||||
kind,
|
||||
ty: Type::Unit,
|
||||
span,
|
||||
})
|
||||
}
|
||||
|
||||
fn affine_operand(
|
||||
&mut self,
|
||||
source: &SourceExpression,
|
||||
consume: bool,
|
||||
) -> Checked<(Expression, Type)> {
|
||||
let Ast::Name(path) = &ungroup(source).kind else {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"Task and Group operations require a direct local name",
|
||||
source.span,
|
||||
));
|
||||
};
|
||||
if path.len() != 1 {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"Task and Group cannot be stored or projected",
|
||||
source.span,
|
||||
));
|
||||
}
|
||||
let (slot, ty) = self.affine_local(&path[0].text, source.span, consume)?;
|
||||
Ok((
|
||||
Expression {
|
||||
kind: ExpressionKind::Local(slot),
|
||||
ty: ty.clone(),
|
||||
span: source.span,
|
||||
},
|
||||
ty,
|
||||
))
|
||||
}
|
||||
|
||||
fn capacity(
|
||||
&mut self,
|
||||
source: &SourceExpression,
|
||||
maximum: usize,
|
||||
label: &str,
|
||||
) -> Checked<Expression> {
|
||||
let value = self.expression(source, Some(&Type::Int))?;
|
||||
if let ExpressionKind::Constant(crate::hir::Constant::Int(constant)) = &value.kind
|
||||
&& (*constant <= 0
|
||||
|| usize::try_from(*constant).map_or(true, |constant| constant > maximum))
|
||||
{
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
format!("{label} must be between 1 and {maximum}"),
|
||||
source.span,
|
||||
));
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
fn require_transferable_call(
|
||||
&self,
|
||||
arguments: &[CallArgument],
|
||||
outputs: &[Field],
|
||||
span: Span,
|
||||
) -> Checked<()> {
|
||||
let arguments_transfer = arguments.iter().all(|argument| match argument {
|
||||
CallArgument::Value(value) => value.ty.transferable(self.context.types),
|
||||
CallArgument::Handler(_) => true,
|
||||
});
|
||||
let outputs_transfer = outputs
|
||||
.iter()
|
||||
.all(|field| field.ty.transferable(self.context.types));
|
||||
if arguments_transfer && outputs_transfer {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Diagnostic::error(
|
||||
"C005",
|
||||
"task inputs and outputs must be transferable between threads",
|
||||
span,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn exact_arity(
|
||||
name: &str,
|
||||
arguments: &[SourceExpression],
|
||||
expected: usize,
|
||||
span: Span,
|
||||
) -> Checked<()> {
|
||||
if arguments.len() == expected {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Diagnostic::error(
|
||||
"C005",
|
||||
format!("'{name}' expects {expected} argument(s)"),
|
||||
span,
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
use dever_runtime::number::{self, DecimalValue};
|
||||
|
||||
use crate::diagnostic::Diagnostic;
|
||||
use crate::hir::{Constant, Expression, ExpressionKind};
|
||||
use crate::source::Span;
|
||||
use crate::syntax::{BinaryOperator as Op, Literal, UnaryOperator};
|
||||
use crate::types::Type;
|
||||
|
||||
use super::Checked;
|
||||
|
||||
pub(super) fn literal(value: &Literal, expected: Option<&Type>, span: Span) -> Checked<Expression> {
|
||||
let (constant, ty) = match value {
|
||||
Literal::Bool(value) => (Constant::Bool(*value), Type::Bool),
|
||||
Literal::Text(value) if expected.map(Type::base) == Some(&Type::Uuid) => {
|
||||
dever_runtime::orm::Uuid::parse(value)
|
||||
.map_err(|error| Diagnostic::error("C005", error.to_string(), span))?;
|
||||
(Constant::Text(value.clone()), Type::Uuid)
|
||||
}
|
||||
Literal::Text(value) => (Constant::Text(value.clone()), Type::Text),
|
||||
Literal::Null => {
|
||||
let Some(ty @ Type::Nullable(_)) = expected else {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"null needs a nullable type context",
|
||||
span,
|
||||
));
|
||||
};
|
||||
(Constant::Null, ty.clone())
|
||||
}
|
||||
Literal::Number(text) => {
|
||||
if text.contains(['e', 'E'])
|
||||
|| (text.contains('.') && expected.map(Type::base) == Some(&Type::Float))
|
||||
{
|
||||
let value: f64 = text
|
||||
.parse()
|
||||
.map_err(|_| Diagnostic::error("C005", "invalid Float literal", span))?;
|
||||
if !value.is_finite() {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"Float literal must be finite",
|
||||
span,
|
||||
));
|
||||
}
|
||||
(Constant::Float(value), Type::Float)
|
||||
} else if text.contains('.') {
|
||||
(
|
||||
Constant::Decimal(
|
||||
DecimalValue::parse(text)
|
||||
.map_err(|message| Diagnostic::error("C005", message, span))?,
|
||||
),
|
||||
Type::Decimal,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
Constant::Int(text.parse().map_err(|_| {
|
||||
Diagnostic::error("C005", "Int literal is outside the 64-bit range", span)
|
||||
})?),
|
||||
Type::Int,
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(Expression {
|
||||
kind: ExpressionKind::Constant(constant),
|
||||
ty,
|
||||
span,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn promote(value: Expression) -> Expression {
|
||||
let span = value.span;
|
||||
let kind = match value.kind {
|
||||
ExpressionKind::Constant(Constant::Int(integer)) => {
|
||||
ExpressionKind::Constant(Constant::Decimal(DecimalValue::from_int(integer)))
|
||||
}
|
||||
_ => ExpressionKind::Promote(Box::new(value)),
|
||||
};
|
||||
Expression {
|
||||
kind,
|
||||
ty: Type::Decimal,
|
||||
span,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn unary(operator: UnaryOperator, value: &Constant, span: Span) -> Checked<Constant> {
|
||||
let result = match (operator, value) {
|
||||
(UnaryOperator::Not, Constant::Bool(value)) => Ok(Constant::Bool(!value)),
|
||||
(UnaryOperator::Negate, Constant::Int(value)) => number::int_neg(*value).map(Constant::Int),
|
||||
(UnaryOperator::Negate, Constant::Decimal(value)) => {
|
||||
value.checked_neg().map(Constant::Decimal)
|
||||
}
|
||||
(UnaryOperator::Negate, Constant::Float(value)) => Ok(Constant::Float(-value)),
|
||||
_ => unreachable!("checked unary operand"),
|
||||
};
|
||||
result.map_err(|message| Diagnostic::error("C008", message, span))
|
||||
}
|
||||
|
||||
pub(super) fn binary(
|
||||
left: &Constant,
|
||||
operator: Op,
|
||||
right: &Constant,
|
||||
span: Span,
|
||||
) -> Checked<Constant> {
|
||||
use Constant as C;
|
||||
let result = match (left, operator, right) {
|
||||
(left, Op::Equal, right) => Ok(C::Bool(left == right)),
|
||||
(left, Op::NotEqual, right) => Ok(C::Bool(left != right)),
|
||||
(C::Bool(left), Op::And, C::Bool(right)) => Ok(C::Bool(*left && *right)),
|
||||
(C::Bool(left), Op::Or, C::Bool(right)) => Ok(C::Bool(*left || *right)),
|
||||
(C::Text(left), Op::Add, C::Text(right)) => Ok(C::Text(format!("{left}{right}"))),
|
||||
(C::Int(left), op, C::Int(right)) => match op {
|
||||
Op::Add => number::int_add(*left, *right).map(C::Int),
|
||||
Op::Subtract => number::int_sub(*left, *right).map(C::Int),
|
||||
Op::Multiply => number::int_mul(*left, *right).map(C::Int),
|
||||
Op::IntegerDivide => number::int_div(*left, *right).map(C::Int),
|
||||
Op::Remainder => number::int_rem(*left, *right).map(C::Int),
|
||||
_ => Ok(C::Bool(compare(left.partial_cmp(right), op))),
|
||||
},
|
||||
(C::Decimal(left), op, C::Decimal(right)) => match op {
|
||||
Op::Add => left.checked_add(*right).map(C::Decimal),
|
||||
Op::Subtract => left.checked_sub(*right).map(C::Decimal),
|
||||
Op::Multiply => left.checked_mul(*right).map(C::Decimal),
|
||||
Op::Divide => left.checked_div(*right).map(C::Decimal),
|
||||
_ => Ok(C::Bool(compare(left.partial_cmp(right), op))),
|
||||
},
|
||||
(C::Float(left), op, C::Float(right)) => Ok(match op {
|
||||
Op::Add => C::Float(left + right),
|
||||
Op::Subtract => C::Float(left - right),
|
||||
Op::Multiply => C::Float(left * right),
|
||||
Op::Divide => C::Float(left / right),
|
||||
_ => C::Bool(compare(left.partial_cmp(right), op)),
|
||||
}),
|
||||
(C::Text(left), op, C::Text(right)) => Ok(C::Bool(compare(left.partial_cmp(right), op))),
|
||||
_ => unreachable!("checked binary operands"),
|
||||
};
|
||||
result.map_err(|message| Diagnostic::error("C008", message, span))
|
||||
}
|
||||
|
||||
fn compare(order: Option<std::cmp::Ordering>, operator: Op) -> bool {
|
||||
use std::cmp::Ordering::{Equal, Greater, Less};
|
||||
match operator {
|
||||
Op::Less => order == Some(Less),
|
||||
Op::LessEqual => matches!(order, Some(Less | Equal)),
|
||||
Op::Greater => order == Some(Greater),
|
||||
Op::GreaterEqual => matches!(order, Some(Greater | Equal)),
|
||||
_ => unreachable!("comparison operator"),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
use crate::diagnostic::Diagnostic;
|
||||
use crate::hir::{
|
||||
CallArgument, CallTarget, Expression, ExpressionKind, Function, HandlerTarget, Statement,
|
||||
};
|
||||
use crate::source::Span;
|
||||
use crate::types::{Definition, Parameter};
|
||||
|
||||
use super::{
|
||||
Symbols,
|
||||
symbols::{referenced_types, shape_fields},
|
||||
};
|
||||
|
||||
pub(super) fn check(
|
||||
functions: &[Function],
|
||||
types: &[Definition],
|
||||
symbols: &Symbols,
|
||||
errors: &mut Vec<Diagnostic>,
|
||||
) {
|
||||
let mut calls = vec![Vec::new(); functions.len()];
|
||||
let mut dependencies = vec![Vec::new(); symbols.packages.len()];
|
||||
for definition in types {
|
||||
for field in shape_fields(&definition.shape) {
|
||||
referenced_types(&field.ty, &mut |id| {
|
||||
dependency(
|
||||
&mut dependencies,
|
||||
definition.owner,
|
||||
types[id].owner,
|
||||
definition.span,
|
||||
symbols,
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
for (caller, function) in functions.iter().enumerate() {
|
||||
for ty in function
|
||||
.parameters
|
||||
.iter()
|
||||
.flat_map(|parameter| match parameter {
|
||||
Parameter::Value(ty) => vec![ty],
|
||||
Parameter::Handler(signature) => signature
|
||||
.parameters
|
||||
.iter()
|
||||
.chain(signature.outputs.iter().map(|field| &field.ty))
|
||||
.collect(),
|
||||
})
|
||||
.chain(function.outputs.iter().map(|field| &field.ty))
|
||||
{
|
||||
referenced_types(ty, &mut |id| {
|
||||
dependency(
|
||||
&mut dependencies,
|
||||
function.owner,
|
||||
types[id].owner,
|
||||
function.span,
|
||||
symbols,
|
||||
)
|
||||
});
|
||||
}
|
||||
visit(function, |expression| {
|
||||
referenced_types(&expression.ty, &mut |id| {
|
||||
dependency(
|
||||
&mut dependencies,
|
||||
function.owner,
|
||||
types[id].owner,
|
||||
expression.span,
|
||||
symbols,
|
||||
)
|
||||
});
|
||||
let callee = match expression.kind.static_call().map(|(target, _)| target) {
|
||||
Some(CallTarget::Function(function))
|
||||
| Some(CallTarget::Handler(HandlerTarget::Function(function))) => Some(function),
|
||||
_ => match expression.kind.callback() {
|
||||
Some(HandlerTarget::Function(function)) => Some(function),
|
||||
_ => None,
|
||||
},
|
||||
};
|
||||
if let Some(callee) = callee {
|
||||
calls[caller].push((callee, expression.span));
|
||||
dependency(
|
||||
&mut dependencies,
|
||||
function.owner,
|
||||
functions[callee].owner,
|
||||
expression.span,
|
||||
symbols,
|
||||
);
|
||||
}
|
||||
if let Some((_, arguments)) = expression.kind.static_call() {
|
||||
for argument in arguments {
|
||||
if let CallArgument::Handler(HandlerTarget::Function(target)) = argument {
|
||||
dependency(
|
||||
&mut dependencies,
|
||||
function.owner,
|
||||
functions[*target].owner,
|
||||
expression.span,
|
||||
symbols,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
for (graph, message) in [
|
||||
(&calls, "recursive function calls are not allowed"),
|
||||
(&dependencies, "cyclic package dependencies are not allowed"),
|
||||
] {
|
||||
if let Some(span) = cycle(graph) {
|
||||
errors.push(Diagnostic::error("C007", message, span));
|
||||
}
|
||||
}
|
||||
let roots = functions
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, function)| {
|
||||
function
|
||||
.parameters
|
||||
.iter()
|
||||
.all(|parameter| matches!(parameter, Parameter::Value(_)))
|
||||
})
|
||||
.map(|(function, _)| crate::specialize::Specialization {
|
||||
function,
|
||||
handlers: Vec::new(),
|
||||
});
|
||||
if let Err(span) = crate::specialize::reachable(functions, roots) {
|
||||
errors.push(Diagnostic::error(
|
||||
"C007",
|
||||
"recursive specialized handler calls are not allowed",
|
||||
span,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Explicit DFS frames keep long acyclic call chains off the compiler's call stack.
|
||||
pub(super) fn cycle(graph: &[Vec<(usize, Span)>]) -> Option<Span> {
|
||||
let mut state = vec![0_u8; graph.len()];
|
||||
for root in 0..graph.len() {
|
||||
if state[root] != 0 {
|
||||
continue;
|
||||
}
|
||||
state[root] = 1;
|
||||
let mut pending = vec![(root, 0)];
|
||||
while let Some((node, edge)) = pending.last_mut() {
|
||||
if *edge == graph[*node].len() {
|
||||
state[*node] = 2;
|
||||
pending.pop();
|
||||
continue;
|
||||
}
|
||||
let (next, span) = graph[*node][*edge];
|
||||
*edge += 1;
|
||||
match state[next] {
|
||||
1 => return Some(span),
|
||||
0 => {
|
||||
state[next] = 1;
|
||||
pending.push((next, 0));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn dependency(
|
||||
graph: &mut [Vec<(usize, Span)>],
|
||||
owner: usize,
|
||||
target: usize,
|
||||
span: Span,
|
||||
symbols: &Symbols,
|
||||
) {
|
||||
let same_domain = symbols.packages[owner].layout.domain().is_some()
|
||||
&& symbols.packages[owner].layout.domain() == symbols.packages[target].layout.domain();
|
||||
if owner != target && !same_domain {
|
||||
graph[owner].push((target, span));
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn visit(function: &Function, mut apply: impl FnMut(&Expression)) {
|
||||
let mut pending = Vec::new();
|
||||
for clause in &function.clauses {
|
||||
for statement in &clause.body {
|
||||
pending.push(match statement {
|
||||
Statement::Assign { value, .. } | Statement::Call(value) => value,
|
||||
});
|
||||
}
|
||||
}
|
||||
visit_pending(pending, &mut apply);
|
||||
}
|
||||
|
||||
pub(crate) fn visit_expression(expression: &Expression, mut apply: impl FnMut(&Expression)) {
|
||||
visit_pending(vec![expression], &mut apply);
|
||||
}
|
||||
|
||||
fn visit_pending<'a>(mut pending: Vec<&'a Expression>, apply: &mut impl FnMut(&'a Expression)) {
|
||||
while let Some(expression) = pending.pop() {
|
||||
apply(expression);
|
||||
match &expression.kind {
|
||||
ExpressionKind::Field { value, .. }
|
||||
| ExpressionKind::Fail(value)
|
||||
| ExpressionKind::Unary { value, .. }
|
||||
| ExpressionKind::Promote(value)
|
||||
| ExpressionKind::Some(value)
|
||||
| ExpressionKind::AwaitTask(value)
|
||||
| ExpressionKind::StopTask(value)
|
||||
| ExpressionKind::Group(value)
|
||||
| ExpressionKind::AwaitGroup(value)
|
||||
| ExpressionKind::StopGroup(value)
|
||||
| ExpressionKind::ChannelReceive(value)
|
||||
| ExpressionKind::ChannelClose(value) => pending.push(value),
|
||||
ExpressionKind::Call { arguments, .. }
|
||||
| ExpressionKind::CaptureResult { arguments, .. }
|
||||
| ExpressionKind::ParallelCall { arguments, .. }
|
||||
| ExpressionKind::BlockingCall { arguments, .. } => {
|
||||
pending.extend(arguments.iter().filter_map(|argument| match argument {
|
||||
CallArgument::Value(value) => Some(value),
|
||||
CallArgument::Handler(_) => None,
|
||||
}));
|
||||
}
|
||||
ExpressionKind::RunCall {
|
||||
group, arguments, ..
|
||||
} => {
|
||||
pending.extend(group.iter().map(Box::as_ref));
|
||||
pending.extend(arguments.iter().filter_map(|argument| match argument {
|
||||
CallArgument::Value(value) => Some(value),
|
||||
CallArgument::Handler(_) => None,
|
||||
}));
|
||||
}
|
||||
ExpressionKind::ModelOperation { operation, .. } => {
|
||||
pending.extend(operation.expressions());
|
||||
}
|
||||
ExpressionKind::Channel { capacity, .. } => pending.push(capacity),
|
||||
ExpressionKind::ChannelSend { channel, value } => {
|
||||
pending.extend([channel.as_ref(), value.as_ref()]);
|
||||
}
|
||||
ExpressionKind::Intrinsic { arguments, .. }
|
||||
| ExpressionKind::JobEnqueue { arguments, .. }
|
||||
| ExpressionKind::Collection { arguments, .. }
|
||||
| ExpressionKind::Variant { arguments, .. }
|
||||
| ExpressionKind::List(arguments) => pending.extend(arguments),
|
||||
ExpressionKind::Record { fields } => {
|
||||
pending.extend(fields.iter().map(|(_, value)| value));
|
||||
}
|
||||
ExpressionKind::Map(entries) => {
|
||||
for (key, value) in entries {
|
||||
pending.extend([key, value]);
|
||||
}
|
||||
}
|
||||
ExpressionKind::Binary { left, right, .. } => {
|
||||
pending.extend([left.as_ref(), right.as_ref()]);
|
||||
}
|
||||
ExpressionKind::Constant(_)
|
||||
| ExpressionKind::Local(_)
|
||||
| ExpressionKind::ChoiceOptions { .. } => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,890 @@
|
||||
use crate::diagnostic::Diagnostic;
|
||||
use crate::hir::{
|
||||
CallArgument, CallTarget, Expression as Value, ExpressionKind as Expr, HandlerTarget,
|
||||
};
|
||||
use crate::intrinsic::Intrinsic;
|
||||
use crate::source::Span;
|
||||
use crate::syntax::{self, Expression, ExpressionKind as Ast, Literal};
|
||||
use crate::types::{Field, HandlerSignature, Parameter, Shape, Type, output_type};
|
||||
|
||||
use super::{Checked, body::Body, constants, path_name};
|
||||
|
||||
pub(super) struct CheckedCall {
|
||||
pub target: CallTarget,
|
||||
pub arguments: Vec<CallArgument>,
|
||||
pub outputs: Vec<Field>,
|
||||
pub pure: bool,
|
||||
}
|
||||
|
||||
impl Body<'_, '_> {
|
||||
pub(super) fn expression(
|
||||
&mut self,
|
||||
source: &Expression,
|
||||
expected: Option<&Type>,
|
||||
) -> Checked<Value> {
|
||||
let mut value = match &source.kind {
|
||||
Ast::Literal(literal) => constants::literal(literal, expected, source.span)?,
|
||||
Ast::Group(inner) => self.expression(inner, expected)?,
|
||||
Ast::Name(path) => self.name(path, source.span)?,
|
||||
Ast::Field { value, name } => {
|
||||
if name.text == "options" {
|
||||
if let Ast::Name(path) = &value.kind {
|
||||
if let Some(choice) =
|
||||
self.context.model_choice(&path_name(path), value.span)?
|
||||
{
|
||||
Value {
|
||||
kind: Expr::ChoiceOptions { choice },
|
||||
ty: Type::Map(Box::new(Type::Named(choice)), Box::new(Type::Text)),
|
||||
span: source.span,
|
||||
}
|
||||
} else {
|
||||
let value = self.expression(value, None)?;
|
||||
self.field(value, name)?
|
||||
}
|
||||
} else {
|
||||
let value = self.expression(value, None)?;
|
||||
self.field(value, name)?
|
||||
}
|
||||
} else {
|
||||
let value = self.expression(value, None)?;
|
||||
self.field(value, name)?
|
||||
}
|
||||
}
|
||||
Ast::Unary { operator, value } => {
|
||||
self.unary(*operator, value, expected, source.span)?
|
||||
}
|
||||
Ast::Binary {
|
||||
left,
|
||||
operator,
|
||||
right,
|
||||
} => self.binary(left, *operator, right, expected, source.span)?,
|
||||
Ast::Record { name, fields } => self.record(name, fields, source.span)?,
|
||||
Ast::List(elements) => self.list(elements, expected, source.span)?,
|
||||
Ast::Map(entries) => self.map(entries, expected, source.span)?,
|
||||
Ast::Call {
|
||||
function,
|
||||
arguments,
|
||||
} => self.call(function, arguments, expected, source.span)?,
|
||||
Ast::Fail(error) => self.fail(error, source.span)?,
|
||||
Ast::CaptureResult(call) => self.capture_result(call, expected, source.span)?,
|
||||
Ast::Contextual(operation) => self.contextual(operation, source.span)?,
|
||||
};
|
||||
value.span = source.span;
|
||||
if nested_affine(&value.ty) {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"Task and Group values cannot be stored in another value",
|
||||
source.span,
|
||||
));
|
||||
}
|
||||
match expected {
|
||||
Some(expected) => self.coerce(value, expected),
|
||||
None => Ok(value),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn coerce(&self, value: Value, expected: &Type) -> Checked<Value> {
|
||||
if &value.ty == expected {
|
||||
return Ok(value);
|
||||
}
|
||||
if let Type::Nullable(base) = expected
|
||||
&& !matches!(value.ty, Type::Nullable(_))
|
||||
{
|
||||
let inner = self.coerce(value, base)?;
|
||||
return Ok(Value {
|
||||
span: inner.span,
|
||||
ty: expected.clone(),
|
||||
kind: Expr::Some(Box::new(inner)),
|
||||
});
|
||||
}
|
||||
if value.ty == Type::Int && expected == &Type::Decimal {
|
||||
return Ok(constants::promote(value));
|
||||
}
|
||||
Err(Diagnostic::error(
|
||||
"C005",
|
||||
format!(
|
||||
"expected {}, found {}",
|
||||
expected.label(self.context.types),
|
||||
value.ty.label(self.context.types)
|
||||
),
|
||||
value.span,
|
||||
))
|
||||
}
|
||||
|
||||
fn name(&mut self, path: &syntax::Path, span: Span) -> Checked<Value> {
|
||||
if path[0].text == "setting"
|
||||
&& self.context.symbols.packages[self.context.owner]
|
||||
.layout
|
||||
.role()
|
||||
== Some(crate::source::SourceRole::Adapter)
|
||||
{
|
||||
let (id, function) = self
|
||||
.context
|
||||
.functions
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, function)| {
|
||||
function.owner == self.context.owner && function.setting.is_some()
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
Diagnostic::error(
|
||||
"C006",
|
||||
"setting is available only in its declaring Adapter",
|
||||
span,
|
||||
)
|
||||
})?;
|
||||
let mut value = Value {
|
||||
kind: Expr::Call {
|
||||
target: CallTarget::Function(id),
|
||||
arguments: Vec::new(),
|
||||
},
|
||||
ty: Type::Named(function.setting.unwrap()),
|
||||
span,
|
||||
};
|
||||
for name in &path[1..] {
|
||||
value = self.field(value, name)?;
|
||||
}
|
||||
return Ok(value);
|
||||
}
|
||||
if path.len() > 1 && path.last().is_some_and(|name| name.text == "options") {
|
||||
let choice_name = path_name(&path[..path.len() - 1]);
|
||||
if let Some(choice) = self.context.model_choice(&choice_name, span)? {
|
||||
return Ok(self.choice_options(choice, span));
|
||||
}
|
||||
}
|
||||
if path.len() == 1 && self.handlers.contains_key(&path[0].text) {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"handler parameters are not runtime values",
|
||||
span,
|
||||
));
|
||||
}
|
||||
if path.len() > 1
|
||||
&& let Some((id, variant)) = self.context.variant(&path_name(path), span)?
|
||||
{
|
||||
return self.construct_variant(id, variant, &[], span);
|
||||
}
|
||||
if self.locals.contains_key(&path[0].text) {
|
||||
let local = self.read_local(&path[0].text, span)?;
|
||||
let mut value = Value {
|
||||
kind: local.known.clone().unwrap_or(Expr::Local(local.slot)),
|
||||
ty: local.ty.clone(),
|
||||
span,
|
||||
};
|
||||
for name in &path[1..] {
|
||||
value = self.field(value, name)?;
|
||||
}
|
||||
return Ok(value);
|
||||
}
|
||||
if let Some((id, variant)) = self.context.variant(&path_name(path), span)? {
|
||||
return self.construct_variant(id, variant, &[], span);
|
||||
}
|
||||
Err(Diagnostic::error(
|
||||
"C004",
|
||||
format!("unknown or uninitialized local '{}'", path_name(path)),
|
||||
span,
|
||||
))
|
||||
}
|
||||
|
||||
fn choice_options(&self, choice: usize, span: Span) -> Value {
|
||||
Value {
|
||||
kind: Expr::ChoiceOptions { choice },
|
||||
ty: Type::Map(Box::new(Type::Named(choice)), Box::new(Type::Text)),
|
||||
span,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn field_type(&self, ty: &Type, name: &syntax::Name) -> Checked<(usize, Type)> {
|
||||
if let Type::Named(id) = ty
|
||||
&& !self.context.owns_private_fields(*id)
|
||||
&& let Shape::Record(fields) = &self.context.types[*id].shape
|
||||
&& fields
|
||||
.iter()
|
||||
.any(|field| field.name == name.text && field.private)
|
||||
{
|
||||
return Err(Diagnostic::error(
|
||||
"C006",
|
||||
"private field is accessible only to its owner (Model: owning domain App)",
|
||||
name.span,
|
||||
));
|
||||
}
|
||||
let fields = self.context.value_fields(ty).ok_or_else(|| {
|
||||
Diagnostic::error(
|
||||
"C005",
|
||||
"field access requires an initialized record, MapEntry or named outputs",
|
||||
name.span,
|
||||
)
|
||||
})?;
|
||||
fields
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.find(|(_, field)| field.name == name.text)
|
||||
.map(|(index, field)| (index, field.ty))
|
||||
.ok_or_else(|| {
|
||||
Diagnostic::error("C004", format!("unknown field '{}'", name.text), name.span)
|
||||
})
|
||||
}
|
||||
|
||||
fn field(&self, value: Value, name: &syntax::Name) -> Checked<Value> {
|
||||
let (index, ty) = self.field_type(&value.ty, name)?;
|
||||
Ok(Value {
|
||||
kind: Expr::Field {
|
||||
value: Box::new(value),
|
||||
index,
|
||||
},
|
||||
ty,
|
||||
span: name.span,
|
||||
})
|
||||
}
|
||||
|
||||
fn call(
|
||||
&mut self,
|
||||
function: &Expression,
|
||||
arguments: &[Expression],
|
||||
expected: Option<&Type>,
|
||||
span: Span,
|
||||
) -> Checked<Value> {
|
||||
let Ast::Name(path) = &ungroup(function).kind else {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"only named functions are callable",
|
||||
function.span,
|
||||
));
|
||||
};
|
||||
let name = path_name(path);
|
||||
if let Some(value) = self.upload_call(&name, arguments, span) {
|
||||
return value;
|
||||
}
|
||||
if matches!(name.as_str(), "dever.job.enqueue" | "dever.job.enqueue_at") {
|
||||
return self.enqueue_job(arguments, name.ends_with("_at"), span);
|
||||
}
|
||||
if self.context.is_test()
|
||||
&& path.len() == 1
|
||||
&& let Some(assertion) = self.test_assertion(&name, arguments, span)?
|
||||
{
|
||||
return Ok(assertion);
|
||||
}
|
||||
if path.len() == 1
|
||||
&& let Some(handler) = self.handlers.get(&path[0].text)
|
||||
{
|
||||
let parameter = handler.parameter;
|
||||
let signature = handler.signature.clone();
|
||||
let checked = self.handler_call(parameter, &signature, arguments, span)?;
|
||||
return Ok(Value {
|
||||
kind: Expr::Call {
|
||||
target: checked.target,
|
||||
arguments: checked.arguments,
|
||||
},
|
||||
ty: output_type(&checked.outputs),
|
||||
span,
|
||||
});
|
||||
}
|
||||
if let Some((id, variant)) = self.context.variant(&name, function.span)? {
|
||||
return self.construct_variant(id, variant, arguments, span);
|
||||
}
|
||||
if let Some(value) = self.model_call(&name, arguments, span)? {
|
||||
return Ok(value);
|
||||
}
|
||||
if path.len() == 1 && self.locals.contains_key(&path[0].text) {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"local values are not callable",
|
||||
function.span,
|
||||
));
|
||||
}
|
||||
let local_function = self.context.has_function(&name, arguments.len());
|
||||
if !local_function {
|
||||
if let Some(result) = self.named_concurrency(&name, arguments, span) {
|
||||
if self.context.symbols.packages[self.context.owner]
|
||||
.layout
|
||||
.role()
|
||||
== Some(crate::source::SourceRole::Job)
|
||||
{
|
||||
return Err(Diagnostic::error(
|
||||
"C006",
|
||||
"Job entries call only their same-domain App",
|
||||
span,
|
||||
));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
if let Some(operation) = super::collections::operation(&name) {
|
||||
if self.context.symbols.packages[self.context.owner]
|
||||
.layout
|
||||
.role()
|
||||
== Some(crate::source::SourceRole::Job)
|
||||
{
|
||||
return Err(Diagnostic::error(
|
||||
"C006",
|
||||
"Job entries call only their same-domain App",
|
||||
span,
|
||||
));
|
||||
}
|
||||
return self.collection(operation, arguments, expected, span);
|
||||
}
|
||||
}
|
||||
if let Some(operation) = Intrinsic::from_name(&name) {
|
||||
if matches!(operation, Intrinsic::AuthOwnsUser) {
|
||||
let [argument] = arguments else {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"dever.auth.owns_user expects one Model id",
|
||||
span,
|
||||
));
|
||||
};
|
||||
let argument = self.expression(argument, None)?;
|
||||
let model_id = matches!(argument.ty, Type::Named(id)
|
||||
if self.context.types[id].kind == crate::types::DefinitionKind::ModelId);
|
||||
if !model_id {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"dever.auth.owns_user expects one Model id",
|
||||
argument.span,
|
||||
));
|
||||
}
|
||||
return Ok(Value {
|
||||
kind: Expr::Intrinsic {
|
||||
operation,
|
||||
handler: None,
|
||||
arguments: vec![argument],
|
||||
},
|
||||
ty: Type::Bool,
|
||||
span,
|
||||
});
|
||||
}
|
||||
if self.context.symbols.packages[self.context.owner]
|
||||
.layout
|
||||
.role()
|
||||
== Some(crate::source::SourceRole::Job)
|
||||
{
|
||||
return Err(Diagnostic::error(
|
||||
"C006",
|
||||
"Job entries call only their same-domain App",
|
||||
span,
|
||||
));
|
||||
}
|
||||
let mut signature = super::intrinsics::signature(operation, self.context, span)?;
|
||||
let shared_context = if matches!(
|
||||
operation,
|
||||
Intrinsic::HttpServe
|
||||
| Intrinsic::HttpServeLive
|
||||
| Intrinsic::HttpServeTls
|
||||
| Intrinsic::HttpServeLiveTls
|
||||
) && arguments.len() == signature.parameters.len() + 2
|
||||
{
|
||||
let context = self.expression(arguments.last().unwrap(), None)?;
|
||||
if !context.ty.transferable(self.context.types) {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"HTTP handler context must be transferable",
|
||||
context.span,
|
||||
));
|
||||
}
|
||||
let handler = signature.handler.as_mut().expect("HTTP handler");
|
||||
handler.parameters.push(context.ty.clone());
|
||||
handler.bounds.push(Default::default());
|
||||
Some(context)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let count = signature.parameters.len() + usize::from(signature.handler.is_some());
|
||||
if count + usize::from(shared_context.is_some()) != arguments.len() {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
format!("'{name}' expects {count} argument(s)"),
|
||||
span,
|
||||
));
|
||||
}
|
||||
let (handler, arguments) = match &signature.handler {
|
||||
Some(expected) => (
|
||||
Some(self.handler_reference(&arguments[0], expected)?),
|
||||
&arguments[1..],
|
||||
),
|
||||
None => (None, arguments),
|
||||
};
|
||||
let mut arguments: Vec<_> = arguments
|
||||
.iter()
|
||||
.zip(signature.parameters)
|
||||
.map(|(source, ty)| self.expression(source, Some(&ty)))
|
||||
.collect::<Checked<_>>()?;
|
||||
arguments.extend(shared_context);
|
||||
return Ok(Value {
|
||||
kind: Expr::Intrinsic {
|
||||
operation,
|
||||
handler,
|
||||
arguments,
|
||||
},
|
||||
ty: signature.output,
|
||||
span,
|
||||
});
|
||||
}
|
||||
let id = self
|
||||
.context
|
||||
.function(&name, arguments.len(), function.span)?;
|
||||
// The standard getter retains absence; only an explicit nominal context
|
||||
// selects the trusted ID representation. Route/provider validation follows.
|
||||
if let Some(Type::Nullable(inner)) = expected
|
||||
&& let Type::Named(model_id) = inner.as_ref()
|
||||
&& self.context.types[*model_id].kind == crate::types::DefinitionKind::ModelId
|
||||
&& let Some(operation) = match name.as_str() {
|
||||
"dever.auth.user_id" => Some(Intrinsic::AuthUserId),
|
||||
"dever.auth.tenant_id" => Some(Intrinsic::AuthTenantId),
|
||||
_ => None,
|
||||
}
|
||||
{
|
||||
return Ok(Value {
|
||||
kind: Expr::Intrinsic {
|
||||
operation,
|
||||
handler: None,
|
||||
arguments: Vec::new(),
|
||||
},
|
||||
ty: expected.expect("matched explicit nullable type").clone(),
|
||||
span,
|
||||
});
|
||||
}
|
||||
let checked = self.function_call(id, arguments)?;
|
||||
Ok(Value {
|
||||
kind: Expr::Call {
|
||||
target: checked.target,
|
||||
arguments: checked.arguments,
|
||||
},
|
||||
ty: output_type(&checked.outputs),
|
||||
span,
|
||||
})
|
||||
}
|
||||
|
||||
fn test_assertion(
|
||||
&mut self,
|
||||
name: &str,
|
||||
arguments: &[Expression],
|
||||
span: Span,
|
||||
) -> Checked<Option<Value>> {
|
||||
let mut output = Type::Unit;
|
||||
let (operation, checked) = match name {
|
||||
"secret" => {
|
||||
let [input] = arguments else {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"secret expects one Text argument",
|
||||
span,
|
||||
));
|
||||
};
|
||||
output = Type::Secret;
|
||||
(
|
||||
Intrinsic::TestSecret,
|
||||
vec![self.expression(input, Some(&Type::Text))?],
|
||||
)
|
||||
}
|
||||
"assert" => {
|
||||
let [condition] = arguments else {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"'assert' expects one Bool argument",
|
||||
span,
|
||||
));
|
||||
};
|
||||
(
|
||||
Intrinsic::TestAssert,
|
||||
vec![self.expression(condition, Some(&Type::Bool))?],
|
||||
)
|
||||
}
|
||||
"assert_eq" => {
|
||||
let [actual, expected] = arguments else {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"'assert_eq' expects two arguments",
|
||||
span,
|
||||
));
|
||||
};
|
||||
let actual = self.expression(actual, None)?;
|
||||
let expected =
|
||||
self.expression(expected, needs_context(expected).then_some(&actual.ty))?;
|
||||
if actual.ty != expected.ty {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
format!(
|
||||
"assert_eq requires the same type on both sides; found {} and {}",
|
||||
actual.ty.label(self.context.types),
|
||||
expected.ty.label(self.context.types)
|
||||
),
|
||||
span,
|
||||
));
|
||||
}
|
||||
if !actual.ty.comparable(self.context.types)
|
||||
|| !actual.ty.observable(self.context.types)
|
||||
{
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
format!(
|
||||
"assert_eq does not support {}",
|
||||
actual.ty.label(self.context.types)
|
||||
),
|
||||
span,
|
||||
));
|
||||
}
|
||||
(Intrinsic::TestAssertEq, vec![actual, expected])
|
||||
}
|
||||
_ => return Ok(None),
|
||||
};
|
||||
Ok(Some(Value {
|
||||
kind: Expr::Intrinsic {
|
||||
operation,
|
||||
handler: None,
|
||||
arguments: checked,
|
||||
},
|
||||
ty: output,
|
||||
span,
|
||||
}))
|
||||
}
|
||||
|
||||
fn fail(&mut self, source: &Expression, span: Span) -> Checked<Value> {
|
||||
let source = ungroup(source);
|
||||
let (path, arguments) = match &source.kind {
|
||||
Ast::Name(path) => (path, &[][..]),
|
||||
Ast::Call {
|
||||
function,
|
||||
arguments,
|
||||
} => {
|
||||
let Ast::Name(path) = &ungroup(function).kind else {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"fail requires a direct error choice variant",
|
||||
source.span,
|
||||
));
|
||||
};
|
||||
(path, arguments.as_slice())
|
||||
}
|
||||
_ => {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"fail requires a direct error choice variant",
|
||||
source.span,
|
||||
));
|
||||
}
|
||||
};
|
||||
let Some((id, variant)) = self.context.variant(&path_name(path), source.span)? else {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"fail requires a qualified error choice variant",
|
||||
source.span,
|
||||
));
|
||||
};
|
||||
let error = self.construct_failure_variant(id, variant, arguments, source.span)?;
|
||||
Ok(Value {
|
||||
kind: Expr::Fail(Box::new(error)),
|
||||
ty: Type::Unit,
|
||||
span,
|
||||
})
|
||||
}
|
||||
|
||||
fn capture_result(
|
||||
&mut self,
|
||||
source: &Expression,
|
||||
expected: Option<&Type>,
|
||||
span: Span,
|
||||
) -> Checked<Value> {
|
||||
let Some(Type::Named(choice)) = expected.map(Type::base) else {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"result(call) requires an expected choice type",
|
||||
span,
|
||||
));
|
||||
};
|
||||
let Shape::Choice(variants) = &self.context.types[*choice].shape else {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"result(call) requires an expected choice type",
|
||||
span,
|
||||
));
|
||||
};
|
||||
let success_variants = variants
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, variant)| !variant.error)
|
||||
.collect::<Vec<_>>();
|
||||
let [(success, success_variant)] = success_variants.as_slice() else {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"a result capture choice must declare exactly one success variant",
|
||||
self.context.types[*choice].span,
|
||||
));
|
||||
};
|
||||
let checked = self.checked_static_call(source)?;
|
||||
let outputs_match = checked.outputs.len() == success_variant.fields.len()
|
||||
&& checked
|
||||
.outputs
|
||||
.iter()
|
||||
.zip(&success_variant.fields)
|
||||
.all(|(output, field)| output.ty == field.ty);
|
||||
if !outputs_match {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"result capture success payload must match the called function outputs",
|
||||
span,
|
||||
));
|
||||
}
|
||||
Ok(Value {
|
||||
kind: Expr::CaptureResult {
|
||||
target: checked.target,
|
||||
arguments: checked.arguments,
|
||||
choice: *choice,
|
||||
success: *success,
|
||||
},
|
||||
ty: Type::Named(*choice),
|
||||
span,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn checked_static_call(&mut self, source: &Expression) -> Checked<CheckedCall> {
|
||||
let Ast::Call {
|
||||
function,
|
||||
arguments,
|
||||
} = &ungroup(source).kind
|
||||
else {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"this operation requires a direct named function call",
|
||||
source.span,
|
||||
));
|
||||
};
|
||||
let Ast::Name(path) = &ungroup(function).kind else {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"only named functions are callable",
|
||||
function.span,
|
||||
));
|
||||
};
|
||||
if path.len() == 1 {
|
||||
if let Some(handler) = self.handlers.get(&path[0].text) {
|
||||
return self.handler_call(
|
||||
handler.parameter,
|
||||
&handler.signature.clone(),
|
||||
arguments,
|
||||
source.span,
|
||||
);
|
||||
}
|
||||
if path.len() == 1 && self.locals.contains_key(&path[0].text) {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"local values are not callable",
|
||||
function.span,
|
||||
));
|
||||
}
|
||||
}
|
||||
let id = self
|
||||
.context
|
||||
.function(&path_name(path), arguments.len(), function.span)?;
|
||||
self.function_call(id, arguments)
|
||||
}
|
||||
|
||||
fn handler_call(
|
||||
&mut self,
|
||||
parameter: usize,
|
||||
signature: &HandlerSignature,
|
||||
arguments: &[Expression],
|
||||
span: Span,
|
||||
) -> Checked<CheckedCall> {
|
||||
if signature.parameters.len() != arguments.len() {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
format!("handler expects {} argument(s)", signature.parameters.len()),
|
||||
span,
|
||||
));
|
||||
}
|
||||
let arguments = arguments
|
||||
.iter()
|
||||
.zip(&signature.parameters)
|
||||
.map(|(argument, ty)| self.expression(argument, Some(ty)).map(CallArgument::Value))
|
||||
.collect::<Checked<_>>()?;
|
||||
Ok(CheckedCall {
|
||||
target: CallTarget::Handler(HandlerTarget::Parameter(parameter)),
|
||||
arguments,
|
||||
outputs: signature.outputs.clone(),
|
||||
pure: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn function_call(&mut self, id: usize, arguments: &[Expression]) -> Checked<CheckedCall> {
|
||||
let function = &self.context.functions[id];
|
||||
let parameters = function.parameters.clone();
|
||||
let storage_sink = function.name == "dever.storage.put";
|
||||
if storage_sink
|
||||
&& self.context.symbols.packages[self.context.owner]
|
||||
.layout
|
||||
.role()
|
||||
!= Some(crate::source::SourceRole::Adapter)
|
||||
{
|
||||
return Err(Diagnostic::error(
|
||||
"C006",
|
||||
"storage.put must be called by a storage Adapter through its Port",
|
||||
function.span,
|
||||
));
|
||||
}
|
||||
let upload_transfer = function.port.is_some() || storage_sink;
|
||||
let checked_arguments = arguments
|
||||
.iter()
|
||||
.zip(¶meters)
|
||||
.map(|(argument, parameter)| match parameter {
|
||||
Parameter::Value(ty) => {
|
||||
if ty == &Type::Upload {
|
||||
if !upload_transfer { return Err(Diagnostic::error("C006", "Upload ownership may transfer only through a Port to its storage Adapter", argument.span)); }
|
||||
let Ast::Name(path) = &argument.kind else { return Err(Diagnostic::error("C005", "Upload transfer requires a single local", argument.span)); };
|
||||
if path.len() != 1 { return Err(Diagnostic::error("C005", "Upload transfer requires a single local", argument.span)); }
|
||||
let (slot, actual) = self.affine_local(&path[0].text, argument.span, true)?;
|
||||
if actual != Type::Upload { return Err(Diagnostic::error("C005", "expected Upload local", argument.span)); }
|
||||
return Ok(CallArgument::Value(Value { kind: Expr::Local(slot), ty: actual, span: argument.span }));
|
||||
}
|
||||
self.expression(argument, Some(ty)).map(CallArgument::Value)
|
||||
}
|
||||
Parameter::Handler(expected) => self
|
||||
.handler_reference(argument, expected)
|
||||
.map(CallArgument::Handler),
|
||||
})
|
||||
.collect::<Checked<_>>()?;
|
||||
Ok(CheckedCall {
|
||||
target: CallTarget::Function(id),
|
||||
arguments: checked_arguments,
|
||||
outputs: function.outputs.clone(),
|
||||
pure: function.pure,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn handler_reference(
|
||||
&self,
|
||||
source: &Expression,
|
||||
expected: &HandlerSignature,
|
||||
) -> Checked<HandlerTarget> {
|
||||
let (target, actual) = self.resolve_handler(source, expected.parameters.len())?;
|
||||
let inputs_match = actual.parameters == expected.parameters
|
||||
&& expected
|
||||
.bounds
|
||||
.iter()
|
||||
.zip(&actual.bounds)
|
||||
.all(|(required, accepted)| crate::contracts::bounds_subset(required, accepted));
|
||||
let outputs_match = actual.outputs.len() == expected.outputs.len()
|
||||
&& actual
|
||||
.outputs
|
||||
.iter()
|
||||
.zip(&expected.outputs)
|
||||
.all(|(actual, expected)| {
|
||||
actual.name == expected.name
|
||||
&& actual.ty == expected.ty
|
||||
&& (matches!(target, HandlerTarget::Function(_))
|
||||
|| crate::contracts::bounds_subset(&actual.bounds, &expected.bounds))
|
||||
});
|
||||
// Concrete output guarantees are inferred from checked bodies by the
|
||||
// contract pass. Forwarded handlers have only their declared guarantees.
|
||||
if !inputs_match || !outputs_match {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"handler signature does not match the required signature",
|
||||
source.span,
|
||||
));
|
||||
}
|
||||
Ok(target)
|
||||
}
|
||||
|
||||
pub(super) fn resolve_handler(
|
||||
&self,
|
||||
source: &Expression,
|
||||
arity: usize,
|
||||
) -> Checked<(HandlerTarget, HandlerSignature)> {
|
||||
let Ast::Name(path) = &ungroup(source).kind else {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"handler must be a named function or handler parameter",
|
||||
source.span,
|
||||
));
|
||||
};
|
||||
if path.len() == 1 {
|
||||
if let Some(handler) = self.handlers.get(&path[0].text) {
|
||||
if handler.signature.parameters.len() != arity {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
format!(
|
||||
"handler expects {} argument(s)",
|
||||
handler.signature.parameters.len()
|
||||
),
|
||||
source.span,
|
||||
));
|
||||
}
|
||||
return Ok((
|
||||
HandlerTarget::Parameter(handler.parameter),
|
||||
handler.signature.clone(),
|
||||
));
|
||||
}
|
||||
if path.len() == 1 && self.locals.contains_key(&path[0].text) {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"local values cannot be handlers",
|
||||
source.span,
|
||||
));
|
||||
}
|
||||
}
|
||||
let id = self
|
||||
.context
|
||||
.function(&path_name(path), arity, source.span)?;
|
||||
let function = &self.context.functions[id];
|
||||
let parameters = function
|
||||
.parameters
|
||||
.iter()
|
||||
.map(|parameter| match parameter {
|
||||
Parameter::Value(ty) => Ok(ty.clone()),
|
||||
Parameter::Handler(_) => Err(Diagnostic::error(
|
||||
"C005",
|
||||
"a handler target cannot itself require handler inputs",
|
||||
source.span,
|
||||
)),
|
||||
})
|
||||
.collect::<Checked<Vec<_>>>()?;
|
||||
let signature = HandlerSignature {
|
||||
bounds: vec![Vec::new(); parameters.len()],
|
||||
parameters,
|
||||
outputs: function.outputs.clone(),
|
||||
};
|
||||
Ok((HandlerTarget::Function(id), signature))
|
||||
}
|
||||
}
|
||||
|
||||
fn nested_affine(ty: &Type) -> bool {
|
||||
match ty {
|
||||
Type::Task(_) | Type::Group | Type::Upload => false,
|
||||
Type::Nullable(inner)
|
||||
| Type::List(inner)
|
||||
| Type::Stream(inner)
|
||||
| Type::AsyncStream(inner)
|
||||
| Type::RowStream(inner)
|
||||
| Type::Channel(inner) => contains_affine(inner),
|
||||
Type::Map(key, value) | Type::MapEntry(key, value) => {
|
||||
contains_affine(key) || contains_affine(value)
|
||||
}
|
||||
Type::Outputs(fields) => fields.iter().any(|field| contains_affine(&field.ty)),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn contains_affine(ty: &Type) -> bool {
|
||||
matches!(ty, Type::Task(_) | Type::Group | Type::Upload) || nested_affine(ty)
|
||||
}
|
||||
|
||||
pub(super) fn needs_context(expression: &Expression) -> bool {
|
||||
match &expression.kind {
|
||||
Ast::Literal(Literal::Null) => true,
|
||||
Ast::List(elements) => elements.iter().all(needs_context),
|
||||
Ast::Map(entries) => entries.is_empty(),
|
||||
Ast::Group(inner) => needs_context(inner),
|
||||
Ast::CaptureResult(_) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn ungroup(mut expression: &Expression) -> &Expression {
|
||||
while let Ast::Group(inner) = &expression.kind {
|
||||
expression = inner;
|
||||
}
|
||||
expression
|
||||
}
|
||||
|
||||
pub(super) fn is_null(expression: &Expression) -> bool {
|
||||
matches!(ungroup(expression).kind, Ast::Literal(Literal::Null))
|
||||
}
|
||||
@@ -0,0 +1,640 @@
|
||||
use crate::diagnostic::Diagnostic;
|
||||
use crate::intrinsic::Intrinsic;
|
||||
use crate::source::Span;
|
||||
use crate::types::{Field, HandlerSignature, Shape, Type};
|
||||
|
||||
use super::{Checked, Context};
|
||||
|
||||
pub(super) struct Signature {
|
||||
pub handler: Option<HandlerSignature>,
|
||||
pub parameters: Vec<Type>,
|
||||
pub output: Type,
|
||||
}
|
||||
|
||||
pub(super) fn signature(
|
||||
operation: Intrinsic,
|
||||
context: &Context<'_>,
|
||||
span: Span,
|
||||
) -> Checked<Signature> {
|
||||
use Intrinsic::*;
|
||||
use Type::*;
|
||||
let mut handler = None;
|
||||
let result = |name: &str, success: &str, payload: &[Type], eof: bool| -> Checked<Type> {
|
||||
let id = context.named_type(name, span)?;
|
||||
let Shape::Choice(variants) = &context.types[id].shape else {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"system result must be a choice type",
|
||||
span,
|
||||
));
|
||||
};
|
||||
let mut expected = vec![(success, payload.to_vec()), ("Failed", vec![Text])];
|
||||
if eof {
|
||||
expected.push(("End", vec![]));
|
||||
}
|
||||
if variants.len() != expected.len()
|
||||
|| !expected.iter().all(|(name, payload)| {
|
||||
variants.iter().any(|variant| {
|
||||
variant.name == *name
|
||||
&& variant
|
||||
.fields
|
||||
.iter()
|
||||
.map(|field| &field.ty)
|
||||
.eq(payload.iter())
|
||||
})
|
||||
})
|
||||
{
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"official result type does not match its system primitive contract",
|
||||
span,
|
||||
));
|
||||
}
|
||||
Ok(Named(id))
|
||||
};
|
||||
let (parameters, output) = match operation {
|
||||
TestAssert | TestAssertEq | TestSecret | AuthOwnsUser => {
|
||||
unreachable!("polymorphic primitives are checked contextually")
|
||||
}
|
||||
CryptoToken => (
|
||||
vec![Int],
|
||||
result("dever.crypto.TokenResult", "Ready", &[Secret], false)?,
|
||||
),
|
||||
CryptoPasswordHash => (
|
||||
vec![Secret],
|
||||
result("dever.crypto.HashResult", "Ready", &[Text], false)?,
|
||||
),
|
||||
CryptoPasswordVerify => (
|
||||
vec![Secret, Text],
|
||||
result("dever.crypto.VerifyResult", "Ready", &[Bool], false)?,
|
||||
),
|
||||
CryptoSha256 => (vec![Bytes], Bytes),
|
||||
CryptoHmacSha256 => (vec![Secret, Bytes], Bytes),
|
||||
CryptoConstantTimeEq => (vec![Bytes, Bytes], Bool),
|
||||
ApiServe => (vec![], Unit),
|
||||
UploadFilename | UploadContentType => (vec![Upload], Text),
|
||||
UploadSize => (vec![Upload], Int),
|
||||
UploadClose => (vec![Upload], Unit),
|
||||
UploadStore => (
|
||||
vec![Upload],
|
||||
result("dever.storage.PutResult", "Stored", &[Uuid], false)?,
|
||||
),
|
||||
ApiRequestId | ApiMethod | ApiPath => (vec![], Text),
|
||||
ApiClientAddress => (vec![], Text.nullable()),
|
||||
ApiHeader | ApiCookie => (vec![Text], Text.nullable()),
|
||||
ApiSecretCookie => (vec![Text], Secret.nullable()),
|
||||
ApiResponseHeader => (vec![Text, Text], Unit),
|
||||
ApiResponseCookie | ApiResponseSecretCookie => {
|
||||
let options = context.named_type("dever.api.CookieOptions", span)?;
|
||||
let value = if matches!(operation, ApiResponseCookie) {
|
||||
Text
|
||||
} else {
|
||||
Secret
|
||||
};
|
||||
(vec![Text, value, Named(options)], Unit)
|
||||
}
|
||||
AuthIssue => (vec![Text, Text, Text.nullable()], Secret),
|
||||
AuthIssueCookie => (vec![Text, Text, Text.nullable()], Unit),
|
||||
AuthClearCookie => (vec![], Unit),
|
||||
AuthId | AuthSession | SiteKey => (vec![], Text),
|
||||
AuthUserId | AuthTenantId => (vec![], Int.nullable()),
|
||||
AuthPermissions => {
|
||||
let permission = context.named_type("dever.auth.Permission", span)?;
|
||||
(vec![], List(Box::new(Named(permission))))
|
||||
}
|
||||
AuthSaveRole => (vec![Text, Text, Bool, List(Box::new(Text))], Unit),
|
||||
AuthGrantRole | AuthRevokeRole => (vec![Int, Text], Unit),
|
||||
AuthDisableRole => (vec![Text], Unit),
|
||||
JobServe => {
|
||||
if !context.symbols.packages[context.owner].bundled
|
||||
&& !matches!(
|
||||
context.symbols.packages[context.owner].layout,
|
||||
crate::source::SourceLayout::Main
|
||||
)
|
||||
{
|
||||
return Err(Diagnostic::error(
|
||||
"C006",
|
||||
"only main may start the Job worker",
|
||||
span,
|
||||
));
|
||||
}
|
||||
(vec![], Unit)
|
||||
}
|
||||
TestJobDrain | TestClockAdvance => {
|
||||
if !context.is_test() {
|
||||
return Err(Diagnostic::error(
|
||||
"C006",
|
||||
"Job test controls are available only in test sources",
|
||||
span,
|
||||
));
|
||||
}
|
||||
(
|
||||
vec![Int],
|
||||
if matches!(operation, TestJobDrain) {
|
||||
Int
|
||||
} else {
|
||||
Unit
|
||||
},
|
||||
)
|
||||
}
|
||||
TaskTimeout | TaskRace | StreamOf => unreachable!("polymorphic core operation"),
|
||||
TaskTicks => (vec![Int], AsyncStream(Box::new(Int))),
|
||||
TextTrim | TextLower | TextUpper => (vec![Text], Text),
|
||||
TextCodepoint => (vec![Text], Int.nullable()),
|
||||
TextFromCodepoint => (vec![Int], Text.nullable()),
|
||||
TextAt => (vec![Text, Int], Text.nullable()),
|
||||
TextSlice => (vec![Text, Int, Int], Text.nullable()),
|
||||
TextIndexOf => (vec![Text, Text], Int.nullable()),
|
||||
TextContains | TextStartsWith | TextEndsWith => (vec![Text, Text], Bool),
|
||||
TextSplit => (vec![Text, Text], List(Box::new(Text))),
|
||||
TextReplace => (vec![Text, Text, Text], Text),
|
||||
IntParse => (vec![Text], Int.nullable()),
|
||||
IntToText => (vec![Int], Text),
|
||||
DecimalParse => (vec![Text], Decimal.nullable()),
|
||||
DecimalToText => (vec![Decimal], Text),
|
||||
DecimalFromInt => (vec![Int], Decimal),
|
||||
FloatParse => (vec![Text], Float.nullable()),
|
||||
FloatToText => (vec![Float], Text),
|
||||
ProcessArguments => (
|
||||
vec![],
|
||||
result(
|
||||
"dever.process.ArgumentsResult",
|
||||
"Read",
|
||||
&[List(Box::new(Text))],
|
||||
false,
|
||||
)?,
|
||||
),
|
||||
TimeUnixMillis | TimeMonotonicNanos => (
|
||||
vec![],
|
||||
result("dever.time.ClockResult", "Read", &[Int], false)?,
|
||||
),
|
||||
TimeNow => (
|
||||
vec![],
|
||||
result("dever.time.DateTimeResult", "Read", &[DateTime], false)?,
|
||||
),
|
||||
TimeParseDateTime => (
|
||||
vec![Text],
|
||||
result("dever.time.DateTimeResult", "Read", &[DateTime], false)?,
|
||||
),
|
||||
TimeParseDate => (
|
||||
vec![Text],
|
||||
result("dever.time.DateResult", "Read", &[Date], false)?,
|
||||
),
|
||||
TimeParseTime => (
|
||||
vec![Text],
|
||||
result("dever.time.TimeResult", "Read", &[Time], false)?,
|
||||
),
|
||||
TimeFormatDateTime => (
|
||||
vec![DateTime],
|
||||
result("dever.time.TextResult", "Read", &[Text], false)?,
|
||||
),
|
||||
TimeFormatDate => (
|
||||
vec![Date],
|
||||
result("dever.time.TextResult", "Read", &[Text], false)?,
|
||||
),
|
||||
TimeFormatTime => (
|
||||
vec![Time],
|
||||
result("dever.time.TextResult", "Read", &[Text], false)?,
|
||||
),
|
||||
TimeDuration => (vec![Int], Duration),
|
||||
TimeAdd | TimeSubtract => (
|
||||
vec![DateTime, Duration],
|
||||
result("dever.time.DateTimeResult", "Read", &[DateTime], false)?,
|
||||
),
|
||||
TimeDifference => (
|
||||
vec![DateTime, DateTime],
|
||||
result("dever.time.DurationResult", "Read", &[Duration], false)?,
|
||||
),
|
||||
TimeSleep | TaskSleep => (
|
||||
vec![Int],
|
||||
result("dever.time.SleepResult", "Done", &[], false)?,
|
||||
),
|
||||
DecimalRound => (vec![Decimal, Int], Decimal),
|
||||
FloatFromInt => (vec![Int], Float),
|
||||
FloatSqrt | FloatSin | FloatCos | FloatLog => (vec![Float], Float),
|
||||
FloatPow => (vec![Float, Float], Float),
|
||||
FloatIsNan | FloatIsFinite | FloatIsInfinite => (vec![Float], Bool),
|
||||
StdoutWrite => (vec![Text], Unit),
|
||||
LogDebug | LogInfo | LogWarn | LogError => {
|
||||
(vec![Text, Map(Box::new(Text), Box::new(Text))], Unit)
|
||||
}
|
||||
IdFromText => (vec![Text], Id),
|
||||
IdToText => (vec![Id], Text),
|
||||
UuidParse => (vec![Text], Uuid.nullable()),
|
||||
UuidToText => (vec![Uuid], Text),
|
||||
BytesFromText => (vec![Text], Bytes),
|
||||
BytesToText => (
|
||||
vec![Bytes],
|
||||
result("dever.bytes.DecodeResult", "Decoded", &[Text], false)?,
|
||||
),
|
||||
BytesFromInts => (
|
||||
vec![List(Box::new(Int))],
|
||||
result("dever.bytes.BufferResult", "Ready", &[Bytes], false)?,
|
||||
),
|
||||
BytesLength => (vec![Bytes], Int),
|
||||
BytesAt => (vec![Bytes, Int], Int.nullable()),
|
||||
BytesSlice => (
|
||||
vec![Bytes, Int, Int],
|
||||
result("dever.bytes.BufferResult", "Ready", &[Bytes], false)?,
|
||||
),
|
||||
BytesConcat => (vec![Bytes, Bytes], Bytes),
|
||||
FileOpen | FileCreate => (
|
||||
vec![Text],
|
||||
result("dever.io.OpenResult", "Opened", &[File], false)?,
|
||||
),
|
||||
FileRead => (
|
||||
vec![File, Int],
|
||||
result("dever.io.SystemReadResult", "Read", &[Bytes], true)?,
|
||||
),
|
||||
FileWrite => (
|
||||
vec![File, Bytes],
|
||||
result("dever.io.WriteResult", "Done", &[], false)?,
|
||||
),
|
||||
FileClose => (
|
||||
vec![File],
|
||||
result("dever.io.CloseResult", "Closed", &[], false)?,
|
||||
),
|
||||
FileChunks | TcpChunks => {
|
||||
let event = result("dever.io.ReadEvent", "Chunk", &[Bytes], false)?;
|
||||
let (source, stream, result_name) = if matches!(operation, FileChunks) {
|
||||
(File, Stream(Box::new(event)), "dever.io.ReadStreamResult")
|
||||
} else {
|
||||
(
|
||||
Socket,
|
||||
AsyncStream(Box::new(event)),
|
||||
"dever.net.ReadStreamResult",
|
||||
)
|
||||
};
|
||||
(
|
||||
vec![source, Int],
|
||||
result(result_name, "Streaming", &[stream], false)?,
|
||||
)
|
||||
}
|
||||
TcpConnect => (
|
||||
vec![Text, Int],
|
||||
result("dever.net.ConnectResult", "Connected", &[Socket], false)?,
|
||||
),
|
||||
TcpConnectTimeout => (
|
||||
vec![Text, Int, Int],
|
||||
result("dever.net.ConnectResult", "Connected", &[Socket], false)?,
|
||||
),
|
||||
TcpListen => (
|
||||
vec![Text, Int],
|
||||
result("dever.net.ListenResult", "Listening", &[Listener], false)?,
|
||||
),
|
||||
TcpAccept => (
|
||||
vec![Listener],
|
||||
result("dever.net.ConnectResult", "Connected", &[Socket], false)?,
|
||||
),
|
||||
TcpPort => (
|
||||
vec![Listener],
|
||||
result("dever.net.PortResult", "Bound", &[Int], false)?,
|
||||
),
|
||||
TcpRead => (
|
||||
vec![Socket, Int],
|
||||
result("dever.net.SystemReadResult", "Read", &[Bytes], true)?,
|
||||
),
|
||||
TcpWrite => (
|
||||
vec![Socket, Bytes],
|
||||
result("dever.io.WriteResult", "Done", &[], false)?,
|
||||
),
|
||||
TcpTimeout => (
|
||||
vec![Socket, Int],
|
||||
result("dever.io.WriteResult", "Done", &[], false)?,
|
||||
),
|
||||
TcpClose => (
|
||||
vec![Socket],
|
||||
result("dever.io.CloseResult", "Closed", &[], false)?,
|
||||
),
|
||||
TcpCloseListener => (
|
||||
vec![Listener],
|
||||
result("dever.io.CloseResult", "Closed", &[], false)?,
|
||||
),
|
||||
TcpConnections => {
|
||||
let connection = result("dever.net.ConnectResult", "Connected", &[Socket], false)?;
|
||||
(vec![Listener], AsyncStream(Box::new(connection)))
|
||||
}
|
||||
TlsSystem => (vec![], ClientTls),
|
||||
TlsClient => (
|
||||
vec![Bytes],
|
||||
result("dever.tls.ClientResult", "Ready", &[ClientTls], false)?,
|
||||
),
|
||||
TlsServer => (
|
||||
vec![Bytes, Bytes],
|
||||
result("dever.tls.ServerResult", "Ready", &[ServerTls], false)?,
|
||||
),
|
||||
HttpCloseClient => (
|
||||
vec![HttpClient],
|
||||
result("dever.io.CloseResult", "Closed", &[], false)?,
|
||||
),
|
||||
HttpServe | HttpServeTls | HttpSend | HttpServeLive | HttpServeLiveTls | HttpRespond
|
||||
| HttpStart | SseStart | HttpClientNew | HttpRequest | HttpOpen | HttpOpenStream => {
|
||||
let header = record(
|
||||
context,
|
||||
"dever.http.Header",
|
||||
&[("name", Text), ("value", Bytes)],
|
||||
span,
|
||||
)?;
|
||||
let headers = List(Box::new(header));
|
||||
let request = record(
|
||||
context,
|
||||
"dever.http.Request",
|
||||
&[
|
||||
("method", Text),
|
||||
("target", Text),
|
||||
("headers", headers.clone()),
|
||||
("body", Bytes),
|
||||
],
|
||||
span,
|
||||
)?;
|
||||
let response = record(
|
||||
context,
|
||||
"dever.http.Response",
|
||||
&[
|
||||
("status", Int),
|
||||
("headers", headers.clone()),
|
||||
("body", Bytes),
|
||||
],
|
||||
span,
|
||||
)?;
|
||||
let http2 = record(
|
||||
context,
|
||||
"dever.http.Http2Limits",
|
||||
&[
|
||||
("streams", Int),
|
||||
("stream_window_bytes", Int),
|
||||
("connection_window_bytes", Int),
|
||||
],
|
||||
span,
|
||||
)?;
|
||||
let limits = record(
|
||||
context,
|
||||
"dever.http.Limits",
|
||||
&[
|
||||
("header_bytes", Int),
|
||||
("body_bytes", Int),
|
||||
("timeout_ms", Int),
|
||||
("connections", Int),
|
||||
("http2", http2.nullable()),
|
||||
],
|
||||
span,
|
||||
)?;
|
||||
match operation {
|
||||
HttpServeLive | HttpServeLiveTls => {
|
||||
let live = record(
|
||||
context,
|
||||
"dever.http.LiveLimits",
|
||||
&[
|
||||
("chunk_bytes", Int),
|
||||
("idle_ms", Int),
|
||||
("heartbeat_ms", Int),
|
||||
],
|
||||
span,
|
||||
)?;
|
||||
handler = Some(HandlerSignature {
|
||||
parameters: vec![request, HttpReply],
|
||||
bounds: vec![Default::default(); 2],
|
||||
outputs: vec![],
|
||||
});
|
||||
let mut parameters = vec![Listener, limits, live];
|
||||
if matches!(operation, HttpServeLiveTls) {
|
||||
parameters.push(ServerTls);
|
||||
}
|
||||
(parameters, Unit)
|
||||
}
|
||||
HttpServe | HttpServeTls => {
|
||||
handler = Some(HandlerSignature {
|
||||
parameters: vec![request],
|
||||
bounds: vec![Default::default()],
|
||||
outputs: vec![Field {
|
||||
name: "response".into(),
|
||||
ty: response,
|
||||
private: false,
|
||||
bounds: Default::default(),
|
||||
}],
|
||||
});
|
||||
let mut parameters = vec![Listener, limits];
|
||||
if matches!(operation, HttpServeTls) {
|
||||
parameters.push(ServerTls);
|
||||
}
|
||||
(parameters, Unit)
|
||||
}
|
||||
HttpRespond | HttpStart | SseStart => {
|
||||
let parameters = match operation {
|
||||
HttpRespond => vec![HttpReply, response],
|
||||
HttpStart => vec![HttpReply, Int, headers],
|
||||
SseStart => vec![HttpReply, headers],
|
||||
_ => unreachable!(),
|
||||
};
|
||||
(
|
||||
parameters,
|
||||
result("dever.io.WriteResult", "Done", &[], false)?,
|
||||
)
|
||||
}
|
||||
HttpClientNew => {
|
||||
let pool = record(
|
||||
context,
|
||||
"dever.http.PoolLimits",
|
||||
&[("idle_ms", Int), ("chunk_bytes", Int), ("read_ms", Int)],
|
||||
span,
|
||||
)?;
|
||||
(
|
||||
vec![Text, ClientTls, limits, pool],
|
||||
result("dever.http.ClientResult", "Ready", &[HttpClient], false)?,
|
||||
)
|
||||
}
|
||||
HttpSend => (
|
||||
vec![Text, Int, request, limits],
|
||||
result("dever.http.ResponseResult", "Read", &[response], false)?,
|
||||
),
|
||||
HttpRequest => (
|
||||
vec![HttpClient, request],
|
||||
result("dever.http.ResponseResult", "Read", &[response], false)?,
|
||||
),
|
||||
HttpOpen | HttpOpenStream => {
|
||||
let event = result("dever.io.ReadEvent", "Chunk", &[Bytes], false)?;
|
||||
let body = AsyncStream(Box::new(event));
|
||||
let response = record(
|
||||
context,
|
||||
"dever.http.StreamResponse",
|
||||
&[
|
||||
("status", Int),
|
||||
("headers", headers.clone()),
|
||||
("body", body.clone()),
|
||||
],
|
||||
span,
|
||||
)?;
|
||||
let request = if matches!(operation, HttpOpenStream) {
|
||||
record(
|
||||
context,
|
||||
"dever.http.StreamRequest",
|
||||
&[
|
||||
("method", Text),
|
||||
("target", Text),
|
||||
("headers", headers),
|
||||
("body", body),
|
||||
],
|
||||
span,
|
||||
)?
|
||||
} else {
|
||||
request
|
||||
};
|
||||
(
|
||||
vec![HttpClient, request],
|
||||
result(
|
||||
"dever.http.StreamResponseResult",
|
||||
"Read",
|
||||
&[response],
|
||||
false,
|
||||
)?,
|
||||
)
|
||||
}
|
||||
_ => unreachable!("HTTP record operation"),
|
||||
}
|
||||
}
|
||||
HttpWrite => (
|
||||
vec![HttpReply, Bytes],
|
||||
result("dever.io.WriteResult", "Done", &[], false)?,
|
||||
),
|
||||
HttpFinish => (
|
||||
vec![HttpReply],
|
||||
result("dever.io.CloseResult", "Closed", &[], false)?,
|
||||
),
|
||||
SseSend => {
|
||||
let event = record(
|
||||
context,
|
||||
"dever.sse.Event",
|
||||
&[
|
||||
("event", Text),
|
||||
("data", Text),
|
||||
("id", Text.nullable()),
|
||||
("retry_ms", Int.nullable()),
|
||||
],
|
||||
span,
|
||||
)?;
|
||||
(
|
||||
vec![HttpReply, event],
|
||||
result("dever.io.WriteResult", "Done", &[], false)?,
|
||||
)
|
||||
}
|
||||
WsAccept | WsConnect | WsOpen => {
|
||||
let limits = record(
|
||||
context,
|
||||
"dever.websocket.Limits",
|
||||
&[("message_bytes", Int), ("idle_ms", Int), ("write_ms", Int)],
|
||||
span,
|
||||
)?;
|
||||
let parameters = match operation {
|
||||
WsAccept => vec![HttpReply, limits],
|
||||
WsOpen => vec![Text, ClientTls, limits],
|
||||
_ => vec![Text, Int, Text, limits],
|
||||
};
|
||||
(
|
||||
parameters,
|
||||
result(
|
||||
"dever.websocket.ConnectResult",
|
||||
"Connected",
|
||||
&[WebSocket],
|
||||
false,
|
||||
)?,
|
||||
)
|
||||
}
|
||||
WsSend | WsReceive | WsMessages => {
|
||||
let message = websocket_message(context, span)?;
|
||||
match operation {
|
||||
WsSend => (
|
||||
vec![WebSocket, message],
|
||||
result("dever.io.WriteResult", "Done", &[], false)?,
|
||||
),
|
||||
WsReceive => (
|
||||
vec![WebSocket],
|
||||
result(
|
||||
"dever.websocket.SystemReceiveResult",
|
||||
"Read",
|
||||
&[message],
|
||||
true,
|
||||
)?,
|
||||
),
|
||||
WsMessages => {
|
||||
let event = result("dever.websocket.MessageEvent", "Read", &[message], false)?;
|
||||
(vec![WebSocket], AsyncStream(Box::new(event)))
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
WsClose => (
|
||||
vec![WebSocket, Int, Text],
|
||||
result("dever.io.CloseResult", "Closed", &[], false)?,
|
||||
),
|
||||
};
|
||||
Ok(Signature {
|
||||
handler,
|
||||
parameters,
|
||||
output,
|
||||
})
|
||||
}
|
||||
|
||||
fn record(
|
||||
context: &Context<'_>,
|
||||
name: &str,
|
||||
expected: &[(&str, Type)],
|
||||
span: Span,
|
||||
) -> Checked<Type> {
|
||||
let id = context.named_type(name, span)?;
|
||||
let valid = match &context.types[id].shape {
|
||||
Shape::Record(fields) => {
|
||||
fields.len() == expected.len()
|
||||
&& expected.iter().all(|(name, ty)| {
|
||||
fields.iter().any(|field| {
|
||||
field.name == *name
|
||||
&& field.ty == *ty
|
||||
&& !field.private
|
||||
&& field.bounds.is_empty()
|
||||
})
|
||||
})
|
||||
}
|
||||
Shape::Choice(_) => false,
|
||||
};
|
||||
if !valid {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
format!("'{name}' does not match its system record contract"),
|
||||
span,
|
||||
));
|
||||
}
|
||||
Ok(Type::Named(id))
|
||||
}
|
||||
|
||||
fn websocket_message(context: &Context<'_>, span: Span) -> Checked<Type> {
|
||||
let id = context.named_type("dever.websocket.Message", span)?;
|
||||
let expected = [
|
||||
("Text", Type::Text),
|
||||
("Binary", Type::Bytes),
|
||||
("Ping", Type::Bytes),
|
||||
("Pong", Type::Bytes),
|
||||
];
|
||||
let valid = match &context.types[id].shape {
|
||||
Shape::Choice(variants) => {
|
||||
variants.len() == expected.len()
|
||||
&& expected.iter().all(|(name, ty)| {
|
||||
variants.iter().any(|variant| {
|
||||
variant.name == *name
|
||||
&& !variant.error
|
||||
&& variant.fields.len() == 1
|
||||
&& variant.fields[0].ty == *ty
|
||||
&& variant.fields[0].bounds.is_empty()
|
||||
&& !variant.fields[0].private
|
||||
})
|
||||
})
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
if !valid {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"WebSocket Message does not match its system contract",
|
||||
span,
|
||||
));
|
||||
}
|
||||
Ok(Type::Named(id))
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use crate::diagnostic::Diagnostic;
|
||||
use crate::hir::{Function, Job};
|
||||
use crate::model::ConnectionSelector;
|
||||
use crate::source::SourceRole;
|
||||
use crate::syntax::{Declaration, FunctionKind, InputKind, Package, Pattern};
|
||||
use crate::types::{Definition, DefinitionKind, Parameter, Shape, Type};
|
||||
use crate::wire::{Policy, Schema};
|
||||
|
||||
impl super::body::Body<'_, '_> {
|
||||
pub(super) fn enqueue_job(
|
||||
&mut self,
|
||||
arguments: &[crate::syntax::Expression],
|
||||
scheduled: bool,
|
||||
span: crate::source::Span,
|
||||
) -> super::Checked<crate::hir::Expression> {
|
||||
let caller = &self.context.symbols.packages[self.context.owner].layout;
|
||||
if caller.role() != Some(SourceRole::App) {
|
||||
return Err(Diagnostic::error(
|
||||
"C006",
|
||||
"only App may enqueue a Job",
|
||||
span,
|
||||
));
|
||||
}
|
||||
let Some(crate::syntax::Expression {
|
||||
kind: crate::syntax::ExpressionKind::Name(path),
|
||||
..
|
||||
}) = arguments.first()
|
||||
else {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"enqueue requires a static Job target",
|
||||
span,
|
||||
));
|
||||
};
|
||||
let name = super::path_name(path);
|
||||
let candidates = self.context.candidates(&name);
|
||||
let targets = self
|
||||
.context
|
||||
.functions
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, function)| {
|
||||
matches!(function.kind, FunctionKind::Job { .. })
|
||||
&& candidates.contains(&function.name)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let [(target, entry)] = targets.as_slice() else {
|
||||
return Err(Diagnostic::error(
|
||||
"C004",
|
||||
"enqueue target must name one declared Job",
|
||||
span,
|
||||
));
|
||||
};
|
||||
if self.context.symbols.packages[entry.owner].layout.domain() != caller.domain() {
|
||||
return Err(Diagnostic::error(
|
||||
"C006",
|
||||
"App may enqueue only a Job in its own domain",
|
||||
span,
|
||||
));
|
||||
}
|
||||
let mut expected = entry
|
||||
.parameters
|
||||
.iter()
|
||||
.map(|parameter| match parameter {
|
||||
Parameter::Value(ty) => Ok(ty.clone()),
|
||||
_ => Err(Diagnostic::error(
|
||||
"C005",
|
||||
"Job payload must be a record",
|
||||
span,
|
||||
)),
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
expected.push(Type::Text);
|
||||
if scheduled {
|
||||
expected.push(Type::DateTime);
|
||||
}
|
||||
if arguments.len() != expected.len() + 1 {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"enqueue requires target, optional payload, key and optional run_at",
|
||||
span,
|
||||
));
|
||||
}
|
||||
let arguments = arguments[1..]
|
||||
.iter()
|
||||
.zip(&expected)
|
||||
.map(|(argument, ty)| self.expression(argument, Some(ty)))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
Ok(crate::hir::Expression {
|
||||
kind: crate::hir::ExpressionKind::JobEnqueue {
|
||||
target: *target,
|
||||
arguments,
|
||||
scheduled,
|
||||
},
|
||||
ty: Type::Id,
|
||||
span,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn register(
|
||||
packages: &[Package],
|
||||
functions: &[Function],
|
||||
types: &[Definition],
|
||||
errors: &mut Vec<Diagnostic>,
|
||||
) -> Vec<Job> {
|
||||
let mut jobs = Vec::new();
|
||||
for (owner, package) in packages.iter().enumerate() {
|
||||
let is_job = package.layout.role() == Some(SourceRole::Job);
|
||||
let mut connection = None;
|
||||
let mut scheduled = BTreeSet::new();
|
||||
for declaration in &package.declarations {
|
||||
match declaration {
|
||||
Declaration::Database(binding) if is_job => {
|
||||
if connection
|
||||
.replace(ConnectionSelector::Explicit(binding.name.text.clone()))
|
||||
.is_some()
|
||||
{
|
||||
errors.push(Diagnostic::error(
|
||||
"C002",
|
||||
"duplicate Job database binding",
|
||||
binding.span,
|
||||
));
|
||||
}
|
||||
}
|
||||
Declaration::Function(clause) => {
|
||||
let declared = matches!(clause.kind, FunctionKind::Job { .. });
|
||||
if is_job != declared {
|
||||
errors.push(Diagnostic::error("C005", "job declarations belong only in job source; job source contains only Job entries", clause.span));
|
||||
}
|
||||
}
|
||||
Declaration::Schedule(_) if is_job => {}
|
||||
_ if is_job => errors.push(Diagnostic::error(
|
||||
"C005",
|
||||
"Job files declare database, jobs and schedules; payload records belong in App",
|
||||
crate::markdown::declaration_span(declaration),
|
||||
)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if !is_job {
|
||||
continue;
|
||||
}
|
||||
let Some(connection) = connection else {
|
||||
errors.push(Diagnostic::error(
|
||||
"C014",
|
||||
"Job source requires an explicit database binding",
|
||||
package.span,
|
||||
));
|
||||
continue;
|
||||
};
|
||||
for (function, entry) in functions
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, function)| function.owner == owner)
|
||||
{
|
||||
let FunctionKind::Job {
|
||||
attempts,
|
||||
timeout_ms,
|
||||
} = entry.kind
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let clauses = package
|
||||
.declarations
|
||||
.iter()
|
||||
.filter_map(|declaration| match declaration {
|
||||
Declaration::Function(clause)
|
||||
if entry.name.ends_with(&format!(".{}", clause.name.text)) =>
|
||||
{
|
||||
Some(clause)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if clauses.len() != 1 || entry.parameters.len() > 1 || !entry.outputs.is_empty()
|
||||
|| clauses.iter().any(|clause| clause.pure || clause.recovery.is_some() || clause.fails.is_some()
|
||||
|| clause.inputs.iter().any(|input| !matches!(&input.kind, InputKind::Value(Pattern::Typed { bounds, .. }) if bounds.is_empty())))
|
||||
{
|
||||
errors.push(Diagnostic::error("C005", "Job requires one plain zero-output declaration with zero inputs or one App record payload", entry.span));
|
||||
continue;
|
||||
}
|
||||
let payload = match entry.parameters.as_slice() {
|
||||
[] => None,
|
||||
[Parameter::Value(Type::Named(id))]
|
||||
if types[*id].kind == DefinitionKind::Regular
|
||||
&& matches!(types[*id].shape, Shape::Record(_))
|
||||
&& packages[types[*id].owner].layout.role() == Some(SourceRole::App)
|
||||
&& packages[types[*id].owner].layout.domain()
|
||||
== package.layout.domain() =>
|
||||
{
|
||||
match Schema::build(&Type::Named(*id), types, Policy::Job) {
|
||||
Ok(schema) => Some(schema),
|
||||
Err(reason) => {
|
||||
errors.push(Diagnostic::error("C005", reason, entry.span));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
errors.push(Diagnostic::error(
|
||||
"C005",
|
||||
"Job payload must be a wire-safe record owned by the same-domain App",
|
||||
entry.span,
|
||||
));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
jobs.push(Job {
|
||||
function,
|
||||
connection: connection.clone(),
|
||||
payload,
|
||||
attempts,
|
||||
timeout_ms,
|
||||
schedule: None,
|
||||
components: Vec::new(),
|
||||
});
|
||||
}
|
||||
for declaration in &package.declarations {
|
||||
let Declaration::Schedule(schedule) = declaration else {
|
||||
continue;
|
||||
};
|
||||
let identity = format!(
|
||||
"{}.{}",
|
||||
super::path_name(&package.name),
|
||||
schedule.target.text
|
||||
);
|
||||
let Some(job) = jobs
|
||||
.iter_mut()
|
||||
.find(|job| functions[job.function].name == identity)
|
||||
else {
|
||||
errors.push(Diagnostic::error(
|
||||
"C004",
|
||||
"schedule target must name a Job declared in this source",
|
||||
schedule.span,
|
||||
));
|
||||
continue;
|
||||
};
|
||||
if job.payload.is_some() || !scheduled.insert(job.function) {
|
||||
errors.push(Diagnostic::error(
|
||||
"C005",
|
||||
"schedule requires a unique zero-input Job target",
|
||||
schedule.span,
|
||||
));
|
||||
continue;
|
||||
}
|
||||
match dever_runtime::cron::Cron::parse(&schedule.cron) {
|
||||
Ok(_) => job.schedule = Some(schedule.cron.clone()),
|
||||
Err(reason) => errors.push(Diagnostic::error("C005", reason, schedule.span)),
|
||||
}
|
||||
}
|
||||
}
|
||||
jobs
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::diagnostic::{Diagnostic, Label};
|
||||
use crate::source::{SourceLayout, SourceMap, SourceRole, Span};
|
||||
|
||||
#[derive(Default)]
|
||||
struct RoleFiles {
|
||||
root: Option<Span>,
|
||||
topics: Vec<Span>,
|
||||
}
|
||||
|
||||
pub(super) fn validate(sources: &SourceMap) -> Vec<Diagnostic> {
|
||||
let mut errors = Vec::new();
|
||||
let mut roles: BTreeMap<(String, String, SourceRole), RoleFiles> = BTreeMap::new();
|
||||
|
||||
for source in sources.files() {
|
||||
let layout = match source.layout() {
|
||||
Ok(layout) => layout,
|
||||
Err(error) => {
|
||||
errors.push(error);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
match layout {
|
||||
SourceLayout::Main => {}
|
||||
SourceLayout::Test {
|
||||
component,
|
||||
domain,
|
||||
topic,
|
||||
} => {
|
||||
check_bucket_names(
|
||||
[&component, &domain, &topic],
|
||||
source.span(0, 0),
|
||||
&mut errors,
|
||||
);
|
||||
}
|
||||
SourceLayout::Loose if sources.strict_layout() => errors.push(Diagnostic::error(
|
||||
"C003",
|
||||
"application source must be main.dever or <component>/<domain>/<app|domain|model|port|adapter|api>.dever, with an optional role topic directory",
|
||||
source.span(0, 0),
|
||||
)),
|
||||
SourceLayout::Loose => {}
|
||||
SourceLayout::Role {
|
||||
component,
|
||||
domain,
|
||||
role,
|
||||
topics,
|
||||
} => {
|
||||
check_bucket_names(
|
||||
std::iter::once(&component)
|
||||
.chain(std::iter::once(&domain))
|
||||
.chain(topics.iter()),
|
||||
source.span(0, 0),
|
||||
&mut errors,
|
||||
);
|
||||
if role != SourceRole::Api && topics.len() > 1 {
|
||||
errors.push(Diagnostic::error(
|
||||
"C003",
|
||||
format!(
|
||||
"{} topic directories are flat; nested directories are supported only below api/",
|
||||
role.name()
|
||||
),
|
||||
source.span(0, 0),
|
||||
));
|
||||
}
|
||||
let files = roles.entry((component, domain, role)).or_default();
|
||||
if topics.is_empty() {
|
||||
files.root = Some(source.span(0, 0));
|
||||
} else {
|
||||
files.topics.push(source.span(0, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for ((_, _, role), files) in roles {
|
||||
// Model topics name persisted entities; API topics name route paths.
|
||||
// Folding either into the root file would change its public identity.
|
||||
if matches!(role, SourceRole::Model | SourceRole::Api) {
|
||||
continue;
|
||||
}
|
||||
if let (Some(root), Some(topic)) = (files.root, files.topics.first().copied()) {
|
||||
let mut error = Diagnostic::error(
|
||||
"C003",
|
||||
format!(
|
||||
"{}.dever and its {}/ directory are mutually exclusive",
|
||||
role.name(),
|
||||
role.name()
|
||||
),
|
||||
topic,
|
||||
);
|
||||
error.related.push(Label {
|
||||
span: root,
|
||||
message: "role file declared here".into(),
|
||||
});
|
||||
errors.push(error);
|
||||
} else if files.root.is_none() && files.topics.len() == 1 {
|
||||
errors.push(Diagnostic::error(
|
||||
"C003",
|
||||
format!(
|
||||
"a {}/ directory needs at least two cohesive topic files; merge a single topic into {}.dever",
|
||||
role.name(),
|
||||
role.name()
|
||||
),
|
||||
files.topics[0],
|
||||
));
|
||||
}
|
||||
}
|
||||
errors
|
||||
}
|
||||
|
||||
fn check_bucket_names<'a>(
|
||||
names: impl IntoIterator<Item = &'a String>,
|
||||
span: Span,
|
||||
errors: &mut Vec<Diagnostic>,
|
||||
) {
|
||||
for name in names {
|
||||
if matches!(
|
||||
name.as_str(),
|
||||
"common" | "shared" | "utils" | "helper" | "helpers" | "base" | "service" | "services"
|
||||
) {
|
||||
errors.push(Diagnostic::error(
|
||||
"C003",
|
||||
format!(
|
||||
"'{name}' is a generic source bucket; name the business domain or cohesive topic instead"
|
||||
),
|
||||
span,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,188 @@
|
||||
use crate::diagnostic::Diagnostic;
|
||||
use crate::syntax::{
|
||||
self, Declaration, InputKind, Name, Package, Pattern, StatementKind, TypeShape,
|
||||
};
|
||||
|
||||
use super::Checked;
|
||||
|
||||
pub(super) fn check(package: &Package) -> Checked<()> {
|
||||
for name in &package.name {
|
||||
snake(name)?;
|
||||
}
|
||||
for declaration in &package.declarations {
|
||||
match declaration {
|
||||
Declaration::Api(binding) => {
|
||||
snake(&binding.action)?;
|
||||
for segment in &binding.target {
|
||||
snake(segment)?;
|
||||
}
|
||||
}
|
||||
Declaration::Rest(rest) => {
|
||||
for segment in rest.model.iter().flatten() {
|
||||
snake(segment)?;
|
||||
}
|
||||
}
|
||||
Declaration::External(external) => {
|
||||
for capability in &external.capabilities {
|
||||
snake(capability)?;
|
||||
}
|
||||
}
|
||||
Declaration::Type(ty) => {
|
||||
camel(&ty.name)?;
|
||||
if matches!(
|
||||
ty.name.text.as_str(),
|
||||
"Bool"
|
||||
| "Int"
|
||||
| "Decimal"
|
||||
| "Float"
|
||||
| "Text"
|
||||
| "Id"
|
||||
| "Bytes"
|
||||
| "Secret"
|
||||
| "Uuid"
|
||||
| "DateTime"
|
||||
| "Date"
|
||||
| "Time"
|
||||
| "Duration"
|
||||
| "Json"
|
||||
| "List"
|
||||
| "Map"
|
||||
| "MapEntry"
|
||||
| "Stream"
|
||||
| "AsyncStream"
|
||||
| "Channel"
|
||||
| "Task"
|
||||
| "Group"
|
||||
) {
|
||||
return Err(Diagnostic::error(
|
||||
"C002",
|
||||
"type name is reserved by the language",
|
||||
ty.name.span,
|
||||
));
|
||||
}
|
||||
match &ty.shape {
|
||||
TypeShape::Record(fields) => check_fields(fields)?,
|
||||
TypeShape::Choice(variants) => {
|
||||
for variant in variants {
|
||||
camel(&variant.name)?;
|
||||
check_fields(&variant.payload)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Declaration::Function(function) => {
|
||||
for segment in function.name.text.split('.') {
|
||||
snake(&Name {
|
||||
text: segment.into(),
|
||||
span: function.name.span,
|
||||
})?;
|
||||
}
|
||||
for input in &function.inputs {
|
||||
snake(&input.name)?;
|
||||
match &input.kind {
|
||||
InputKind::Value(Pattern::Variant { bindings, .. }) => {
|
||||
for binding in bindings {
|
||||
if binding.text != "_" {
|
||||
snake(binding)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
InputKind::Handler(signature) => {
|
||||
check_fields(&signature.inputs)?;
|
||||
check_fields(&signature.outputs)?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
check_fields(&function.outputs)?;
|
||||
for statement in &function.body {
|
||||
if let StatementKind::Assign { target, .. } = &statement.kind {
|
||||
for name in target {
|
||||
snake(name)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Declaration::Database(binding) => snake(&binding.name)?,
|
||||
Declaration::Schedule(schedule) => snake(&schedule.target)?,
|
||||
Declaration::ModelIndex(index) => {
|
||||
for field in &index.fields {
|
||||
snake(field)?;
|
||||
}
|
||||
}
|
||||
Declaration::Relation(relation) => {
|
||||
snake(&relation.name)?;
|
||||
for segment in &relation.field {
|
||||
snake(segment)?;
|
||||
}
|
||||
}
|
||||
Declaration::Seed(seed) => {
|
||||
for row in &seed.rows {
|
||||
for field in row {
|
||||
snake(&field.name)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Declaration::Migration(migration) => {
|
||||
snake(&migration.name)?;
|
||||
for operation in &migration.operations {
|
||||
match operation {
|
||||
syntax::MigrationOperation::Drop(field) => snake(field)?,
|
||||
syntax::MigrationOperation::Sql(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Declaration::ModelSql(sql) => {
|
||||
snake(&sql.name)?;
|
||||
check_fields(&sql.inputs)?;
|
||||
check_fields(&sql.outputs)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_fields(fields: &[syntax::Field]) -> Checked<()> {
|
||||
for field in fields {
|
||||
snake(&field.name)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn snake(name: &Name) -> Checked<()> {
|
||||
if name
|
||||
.text
|
||||
.starts_with(|character: char| character.is_ascii_lowercase())
|
||||
&& name.text.split('_').all(|word| {
|
||||
!word.is_empty()
|
||||
&& word
|
||||
.chars()
|
||||
.all(|character| character.is_ascii_lowercase() || character.is_ascii_digit())
|
||||
})
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
Err(Diagnostic::error(
|
||||
"C010",
|
||||
"name must use ASCII lower_snake_case",
|
||||
name.span,
|
||||
))
|
||||
}
|
||||
|
||||
fn camel(name: &Name) -> Checked<()> {
|
||||
if name
|
||||
.text
|
||||
.starts_with(|character: char| character.is_ascii_uppercase())
|
||||
&& name
|
||||
.text
|
||||
.chars()
|
||||
.all(|character| character.is_ascii_alphanumeric())
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
Err(Diagnostic::error(
|
||||
"C010",
|
||||
"type and variant names must use ASCII UpperCamelCase",
|
||||
name.span,
|
||||
))
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
use super::{
|
||||
Checked,
|
||||
body::Body,
|
||||
constants,
|
||||
expressions::{is_null, ungroup},
|
||||
};
|
||||
use crate::diagnostic::Diagnostic;
|
||||
use crate::hir::{Constant, Expression as Value, ExpressionKind as Expr};
|
||||
use crate::source::Span;
|
||||
use crate::syntax::{
|
||||
BinaryOperator as Op, Expression, ExpressionKind as Ast, Literal, UnaryOperator,
|
||||
};
|
||||
use crate::types::Type;
|
||||
|
||||
impl Body<'_, '_> {
|
||||
pub(super) fn unary(
|
||||
&mut self,
|
||||
operator: UnaryOperator,
|
||||
source: &Expression,
|
||||
expected: Option<&Type>,
|
||||
span: Span,
|
||||
) -> Checked<Value> {
|
||||
if operator == UnaryOperator::Negate
|
||||
&& let Ast::Literal(Literal::Number(text)) = &ungroup(source).kind
|
||||
{
|
||||
return constants::literal(&Literal::Number(format!("-{text}")), expected, span);
|
||||
}
|
||||
let hint = if operator == UnaryOperator::Not {
|
||||
Some(&Type::Bool)
|
||||
} else {
|
||||
expected.map(Type::base).filter(|ty| ty.numeric())
|
||||
};
|
||||
let value = self.expression(source, hint)?;
|
||||
if (operator == UnaryOperator::Not && value.ty != Type::Bool)
|
||||
|| (operator == UnaryOperator::Negate && !value.ty.numeric())
|
||||
{
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"invalid unary operand type",
|
||||
span,
|
||||
));
|
||||
}
|
||||
let ty = value.ty.clone();
|
||||
let kind = if let Expr::Constant(constant) = &value.kind {
|
||||
Expr::Constant(constants::unary(operator, constant, span)?)
|
||||
} else {
|
||||
Expr::Unary {
|
||||
operator,
|
||||
value: Box::new(value),
|
||||
}
|
||||
};
|
||||
Ok(Value { kind, ty, span })
|
||||
}
|
||||
|
||||
pub(super) fn binary(
|
||||
&mut self,
|
||||
left: &Expression,
|
||||
operator: Op,
|
||||
right: &Expression,
|
||||
expected: Option<&Type>,
|
||||
span: Span,
|
||||
) -> Checked<Value> {
|
||||
let boolean = matches!(operator, Op::And | Op::Or);
|
||||
let equality = matches!(operator, Op::Equal | Op::NotEqual);
|
||||
let comparison = matches!(
|
||||
operator,
|
||||
Op::Less | Op::LessEqual | Op::Greater | Op::GreaterEqual
|
||||
);
|
||||
let hint = if boolean {
|
||||
Some(&Type::Bool)
|
||||
} else if !equality && !comparison && expected.map(Type::base) == Some(&Type::Float) {
|
||||
Some(&Type::Float)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let left_null = is_null(left);
|
||||
let (mut left, mut right) = if equality && left_null {
|
||||
let right = self.expression(right, None)?;
|
||||
(self.expression(left, Some(&right.ty))?, right)
|
||||
} else if hint.is_none() && contextual_decimal_literal(left) {
|
||||
let right = self.expression(right, None)?;
|
||||
let hint = (right.ty == Type::Float).then_some(&Type::Float);
|
||||
(self.expression(left, hint)?, right)
|
||||
} else {
|
||||
let left = self.expression(left, hint)?;
|
||||
let right_hint = if equality && is_null(right) {
|
||||
Some(&left.ty)
|
||||
} else if left.ty == Type::Float && contextual_decimal_literal(right) {
|
||||
Some(&Type::Float)
|
||||
} else {
|
||||
hint
|
||||
};
|
||||
let right = self.expression(right, right_hint)?;
|
||||
(left, right)
|
||||
};
|
||||
let ty = if equality {
|
||||
if left.ty != right.ty || !left.ty.comparable(self.context.types) {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"equality requires the same comparable type; it does not promote numbers",
|
||||
span,
|
||||
));
|
||||
}
|
||||
Type::Bool
|
||||
} else if boolean {
|
||||
Type::Bool
|
||||
} else if operator == Op::Add && left.ty == Type::Text && right.ty == Type::Text {
|
||||
Type::Text
|
||||
} else {
|
||||
if !left.ty.numeric() || !right.ty.numeric() {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"arithmetic and ordering require numeric operands",
|
||||
span,
|
||||
));
|
||||
}
|
||||
if matches!(operator, Op::IntegerDivide | Op::Remainder) {
|
||||
if left.ty != Type::Int || right.ty != Type::Int {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"// and % require Int operands",
|
||||
span,
|
||||
));
|
||||
}
|
||||
} else {
|
||||
let decimal = left.ty == Type::Decimal
|
||||
|| right.ty == Type::Decimal
|
||||
|| (operator == Op::Divide && left.ty == Type::Int && right.ty == Type::Int);
|
||||
if decimal {
|
||||
if left.ty == Type::Int {
|
||||
left = constants::promote(left);
|
||||
}
|
||||
if right.ty == Type::Int {
|
||||
right = constants::promote(right);
|
||||
}
|
||||
}
|
||||
if left.ty != right.ty {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"Float cannot implicitly mix with Int or Decimal",
|
||||
span,
|
||||
));
|
||||
}
|
||||
}
|
||||
if comparison {
|
||||
Type::Bool
|
||||
} else {
|
||||
left.ty.clone()
|
||||
}
|
||||
};
|
||||
if matches!(operator, Op::Divide | Op::IntegerDivide | Op::Remainder)
|
||||
&& matches!(&right.kind, Expr::Constant(Constant::Int(0)))
|
||||
{
|
||||
return Err(Diagnostic::error("C008", "Int division by zero", span));
|
||||
}
|
||||
if operator == Op::Divide
|
||||
&& matches!(&right.kind, Expr::Constant(Constant::Decimal(value)) if *value == dever_runtime::number::DecimalValue::ZERO)
|
||||
{
|
||||
return Err(Diagnostic::error("C008", "Decimal division by zero", span));
|
||||
}
|
||||
let kind = match (&left.kind, &right.kind) {
|
||||
(Expr::Constant(left), Expr::Constant(right)) => {
|
||||
Expr::Constant(constants::binary(left, operator, right, span)?)
|
||||
}
|
||||
_ => Expr::Binary {
|
||||
left: Box::new(left),
|
||||
operator,
|
||||
right: Box::new(right),
|
||||
},
|
||||
};
|
||||
Ok(Value { kind, ty, span })
|
||||
}
|
||||
}
|
||||
|
||||
fn contextual_decimal_literal(expression: &Expression) -> bool {
|
||||
match &ungroup(expression).kind {
|
||||
Ast::Literal(Literal::Number(text)) => text.contains('.') && !text.contains(['e', 'E']),
|
||||
Ast::Unary {
|
||||
operator: UnaryOperator::Negate,
|
||||
value,
|
||||
} => contextual_decimal_literal(value),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,944 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use crate::diagnostic::Diagnostic;
|
||||
use crate::hir::{
|
||||
Expression as Value, ExpressionKind as Expr, ModelCondition, ModelOperation, ModelOrder,
|
||||
QueryPlan, TextCondition,
|
||||
};
|
||||
use crate::model::{ModelFieldType, ModelSchema, ModelSqlCardinality, ModelSqlParameter};
|
||||
use crate::source::{SourceLayout, SourceRole, Span};
|
||||
use crate::syntax::{BinaryOperator, Expression, ExpressionKind as Ast, MapEntry};
|
||||
use crate::types::{Shape, Type};
|
||||
|
||||
use super::{Checked, body::Body, path_name};
|
||||
|
||||
const MAX_CREATE_MANY_ROWS: usize = 256;
|
||||
const MAX_QUERY_VALUES: usize = 100;
|
||||
const MAX_SQLITE_PARAMETERS: usize = 999;
|
||||
|
||||
#[derive(Clone, Copy, Eq, PartialEq)]
|
||||
enum QueryKind {
|
||||
Plain,
|
||||
Included,
|
||||
Cursor,
|
||||
Stream,
|
||||
}
|
||||
|
||||
impl Body<'_, '_> {
|
||||
pub(super) fn model_call(
|
||||
&mut self,
|
||||
name: &str,
|
||||
arguments: &[Expression],
|
||||
span: Span,
|
||||
) -> Checked<Option<Value>> {
|
||||
let Some((model_name, operation_name)) = name.rsplit_once('.') else {
|
||||
return Ok(None);
|
||||
};
|
||||
let built_in = matches!(
|
||||
operation_name,
|
||||
"create"
|
||||
| "create_many"
|
||||
| "get"
|
||||
| "first"
|
||||
| "list"
|
||||
| "cursor"
|
||||
| "count"
|
||||
| "exists"
|
||||
| "stream"
|
||||
| "update"
|
||||
| "delete"
|
||||
| "upsert"
|
||||
);
|
||||
let Some((model_index, model)) = self.resolve_model(model_name, span)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
let model = model.clone();
|
||||
let sql_operation = model
|
||||
.sql
|
||||
.iter()
|
||||
.position(|operation| operation.name == operation_name);
|
||||
if !built_in && sql_operation.is_none() {
|
||||
return Ok(None);
|
||||
}
|
||||
let operation = match operation_name {
|
||||
"create" => {
|
||||
arity(name, arguments, 1, span)?;
|
||||
ModelOperation::Create {
|
||||
values: self.write_values(&model, &arguments[0], true)?,
|
||||
}
|
||||
}
|
||||
"create_many" => {
|
||||
arity(name, arguments, 1, span)?;
|
||||
ModelOperation::CreateMany {
|
||||
rows: self.create_many_values(&model, &arguments[0])?,
|
||||
}
|
||||
}
|
||||
"get" => {
|
||||
arity(name, arguments, 1, span)?;
|
||||
let id = self.expression(&arguments[0], Some(&self.id_type(&model)))?;
|
||||
ModelOperation::Get { id: Box::new(id) }
|
||||
}
|
||||
"first" | "list" | "cursor" | "count" | "exists" | "stream" => {
|
||||
if arguments.len() > 1 {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
format!("'{name}' expects zero or one query block"),
|
||||
span,
|
||||
));
|
||||
}
|
||||
let query = match arguments.first() {
|
||||
Some(query) => self.query(
|
||||
&model,
|
||||
query,
|
||||
match operation_name {
|
||||
"first" | "list" => QueryKind::Included,
|
||||
"cursor" => QueryKind::Cursor,
|
||||
"stream" => QueryKind::Stream,
|
||||
_ => QueryKind::Plain,
|
||||
},
|
||||
)?,
|
||||
None => default_query(span),
|
||||
};
|
||||
match operation_name {
|
||||
"first" => ModelOperation::First { query },
|
||||
"list" => ModelOperation::List { query },
|
||||
"cursor" => ModelOperation::Cursor { query },
|
||||
"count" => ModelOperation::Count { query },
|
||||
"exists" => ModelOperation::Exists { query },
|
||||
"stream" => ModelOperation::Stream { query },
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
"update" => {
|
||||
arity(name, arguments, 2, span)?;
|
||||
let query = self.target_query(&model, &arguments[0])?;
|
||||
let values = self.write_values(&model, &arguments[1], false)?;
|
||||
if values.is_empty() {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"update requires at least one changed field",
|
||||
arguments[1].span,
|
||||
));
|
||||
}
|
||||
ModelOperation::Update { query, values }
|
||||
}
|
||||
"delete" => {
|
||||
arity(name, arguments, 1, span)?;
|
||||
ModelOperation::Delete {
|
||||
query: self.target_query(&model, &arguments[0])?,
|
||||
}
|
||||
}
|
||||
"upsert" => {
|
||||
arity(name, arguments, 3, span)?;
|
||||
let key = self.write_values(&model, &arguments[0], false)?;
|
||||
self.validate_upsert_key(&model, &key, arguments[0].span)?;
|
||||
let create = self.write_values(&model, &arguments[1], false)?;
|
||||
validate_distinct_fields(&key, &create, arguments[1].span)?;
|
||||
self.validate_required_create(
|
||||
&model,
|
||||
key.iter().chain(&create),
|
||||
arguments[1].span,
|
||||
)?;
|
||||
let update = self.write_values(&model, &arguments[2], false)?;
|
||||
if update.is_empty() {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"upsert requires at least one changed field",
|
||||
arguments[2].span,
|
||||
));
|
||||
}
|
||||
ModelOperation::Upsert {
|
||||
key,
|
||||
create,
|
||||
update,
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let operation = sql_operation.expect("resolved Model SQL operation");
|
||||
let declaration = &model.sql[operation];
|
||||
arity(name, arguments, declaration.parameters.len(), span)?;
|
||||
let arguments = arguments
|
||||
.iter()
|
||||
.zip(&declaration.parameters)
|
||||
.map(|(argument, parameter)| {
|
||||
let expected = self.sql_parameter_type(parameter);
|
||||
self.expression(argument, Some(&expected))
|
||||
})
|
||||
.collect::<Checked<Vec<_>>>()?;
|
||||
ModelOperation::Sql {
|
||||
operation,
|
||||
arguments,
|
||||
read_only: super::sql_read::proven(&model, declaration),
|
||||
}
|
||||
}
|
||||
};
|
||||
let ty = match &operation {
|
||||
ModelOperation::Create { .. }
|
||||
| ModelOperation::Get { .. }
|
||||
| ModelOperation::Upsert { .. } => Type::Named(model.record),
|
||||
ModelOperation::First { .. } => Type::Named(model.record).nullable(),
|
||||
ModelOperation::List { .. } => Type::Named(model.page),
|
||||
ModelOperation::Cursor { .. } => Type::Named(model.cursor),
|
||||
ModelOperation::Stream { .. } => Type::RowStream(Box::new(Type::Named(model.record))),
|
||||
ModelOperation::CreateMany { .. }
|
||||
| ModelOperation::Count { .. }
|
||||
| ModelOperation::Update { .. }
|
||||
| ModelOperation::Delete { .. } => Type::Int,
|
||||
ModelOperation::Exists { .. } => Type::Bool,
|
||||
ModelOperation::Sql { operation, .. } => {
|
||||
let result = model.sql[*operation].result;
|
||||
let row = Type::Named(result.record);
|
||||
match result.cardinality {
|
||||
ModelSqlCardinality::One => row,
|
||||
ModelSqlCardinality::Optional => row.nullable(),
|
||||
ModelSqlCardinality::Many => Type::List(Box::new(row)),
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(Some(Value {
|
||||
kind: Expr::ModelOperation {
|
||||
model: model_index,
|
||||
operation: Box::new(operation),
|
||||
},
|
||||
ty,
|
||||
span,
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_model(&self, name: &str, span: Span) -> Checked<Option<(usize, &ModelSchema)>> {
|
||||
let caller = &self.context.symbols.packages[self.context.owner];
|
||||
let candidates = match &caller.layout {
|
||||
SourceLayout::Role {
|
||||
component, domain, ..
|
||||
}
|
||||
| SourceLayout::Test {
|
||||
component, domain, ..
|
||||
} => {
|
||||
let local = if name == "model" {
|
||||
Some(format!("{component}.{domain}.model"))
|
||||
} else {
|
||||
name.strip_prefix("model.")
|
||||
.map(|rest| format!("{component}.{domain}.model.{rest}"))
|
||||
};
|
||||
local
|
||||
.into_iter()
|
||||
.chain(std::iter::once(name.to_owned()))
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
SourceLayout::Main | SourceLayout::Loose => vec![name.to_owned()],
|
||||
};
|
||||
let matches = self
|
||||
.context
|
||||
.models
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, model)| {
|
||||
candidates.contains(&model.package)
|
||||
|| matches!(&caller.layout, SourceLayout::Loose) && model.table == name
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
match matches.as_slice() {
|
||||
[] => Ok(None),
|
||||
[(index, model)] => {
|
||||
let target = &self.context.symbols.packages[self.context.types[model.record].owner];
|
||||
if !matches!(&caller.layout, SourceLayout::Loose)
|
||||
&& (caller.layout.role() != Some(SourceRole::App)
|
||||
|| caller.layout.domain() != target.layout.domain())
|
||||
{
|
||||
return Err(Diagnostic::error(
|
||||
"C006",
|
||||
"Model operations are private to their owning domain App",
|
||||
span,
|
||||
));
|
||||
}
|
||||
Ok(Some((*index, *model)))
|
||||
}
|
||||
_ => Err(Diagnostic::error(
|
||||
"C004",
|
||||
format!("Model name '{name}' is ambiguous; use its full package name"),
|
||||
span,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn write_values(
|
||||
&mut self,
|
||||
model: &ModelSchema,
|
||||
source: &Expression,
|
||||
creating: bool,
|
||||
) -> Checked<Vec<(usize, Value)>> {
|
||||
let entries = map_entries(source, if creating { "create" } else { "update" })?;
|
||||
let fields = self.model_fields(model).to_vec();
|
||||
let mut seen = BTreeSet::new();
|
||||
let mut values = Vec::new();
|
||||
for entry in entries {
|
||||
let name = map_key(entry)?;
|
||||
let Some((index, field)) = fields
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, field)| field.name == name)
|
||||
else {
|
||||
return Err(Diagnostic::error(
|
||||
"C004",
|
||||
format!("unknown Model field '{name}'"),
|
||||
entry.key.span,
|
||||
));
|
||||
};
|
||||
let field_type = field.ty.clone();
|
||||
if model.fields[index].generated {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
format!("generated Model field '{name}' cannot be written"),
|
||||
entry.key.span,
|
||||
));
|
||||
}
|
||||
if !seen.insert(index) {
|
||||
return Err(Diagnostic::error(
|
||||
"C002",
|
||||
format!("duplicate Model field '{name}'"),
|
||||
entry.span,
|
||||
));
|
||||
}
|
||||
values.push((index, self.expression(&entry.value, Some(&field_type))?));
|
||||
}
|
||||
if creating {
|
||||
self.validate_required_create(model, values.iter(), source.span)?;
|
||||
}
|
||||
Ok(values)
|
||||
}
|
||||
|
||||
fn create_many_values(
|
||||
&mut self,
|
||||
model: &ModelSchema,
|
||||
source: &Expression,
|
||||
) -> Checked<Vec<Vec<(usize, Value)>>> {
|
||||
let Ast::List(items) = &source.kind else {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"create_many requires a static List of anonymous create blocks",
|
||||
source.span,
|
||||
));
|
||||
};
|
||||
if items.is_empty() || items.len() > MAX_CREATE_MANY_ROWS {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
format!("create_many requires between 1 and {MAX_CREATE_MANY_ROWS} rows"),
|
||||
source.span,
|
||||
));
|
||||
}
|
||||
let mut rows = Vec::with_capacity(items.len());
|
||||
for item in items {
|
||||
rows.push(self.write_values(model, item, true)?);
|
||||
}
|
||||
let fields = rows[0].iter().map(|(field, _)| *field).collect::<Vec<_>>();
|
||||
if rows
|
||||
.iter()
|
||||
.skip(1)
|
||||
.any(|row| row.iter().map(|(field, _)| *field).collect::<Vec<_>>() != fields)
|
||||
{
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"create_many rows must write the same fields in the same order",
|
||||
source.span,
|
||||
));
|
||||
}
|
||||
let generated = model
|
||||
.fields
|
||||
.iter()
|
||||
.filter(|field| {
|
||||
field.generated && matches!(field.ty, crate::model::ModelFieldType::Uuid)
|
||||
})
|
||||
.count();
|
||||
if items.len().saturating_mul(fields.len() + generated) > MAX_SQLITE_PARAMETERS {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
format!("create_many exceeds the {MAX_SQLITE_PARAMETERS}-parameter batch limit"),
|
||||
source.span,
|
||||
));
|
||||
}
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
fn validate_required_create<'a>(
|
||||
&self,
|
||||
model: &ModelSchema,
|
||||
values: impl Iterator<Item = &'a (usize, Value)>,
|
||||
span: Span,
|
||||
) -> Checked<()> {
|
||||
let present = values.map(|(field, _)| *field).collect::<BTreeSet<_>>();
|
||||
for (index, field) in model.fields.iter().enumerate() {
|
||||
if !field.generated
|
||||
&& !field.nullable
|
||||
&& field.default.is_none()
|
||||
&& !present.contains(&index)
|
||||
{
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
format!("create is missing required Model field '{}'", field.name),
|
||||
span,
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_upsert_key(
|
||||
&self,
|
||||
model: &ModelSchema,
|
||||
key: &[(usize, Value)],
|
||||
span: Span,
|
||||
) -> Checked<()> {
|
||||
if key.is_empty() {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"upsert key cannot be empty",
|
||||
span,
|
||||
));
|
||||
}
|
||||
let names = key
|
||||
.iter()
|
||||
.map(|(field, _)| model.fields[*field].name.as_str())
|
||||
.collect::<BTreeSet<_>>();
|
||||
if !model.indexes.iter().any(|index| {
|
||||
index.unique
|
||||
&& index.fields.len() == names.len()
|
||||
&& index
|
||||
.fields
|
||||
.iter()
|
||||
.all(|field| names.contains(field.as_str()))
|
||||
}) {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"upsert key must exactly match a unique Model index",
|
||||
span,
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn target_query(&mut self, model: &ModelSchema, source: &Expression) -> Checked<QueryPlan> {
|
||||
if matches!(source.kind, Ast::Map(_)) {
|
||||
self.query(model, source, QueryKind::Plain)
|
||||
} else {
|
||||
let id = self.expression(source, Some(&self.id_type(model)))?;
|
||||
Ok(QueryPlan {
|
||||
condition: Some(ModelCondition::Compare {
|
||||
field: 0,
|
||||
operator: BinaryOperator::Equal,
|
||||
value: id,
|
||||
}),
|
||||
..default_query(source.span)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn query(
|
||||
&mut self,
|
||||
model: &ModelSchema,
|
||||
source: &Expression,
|
||||
kind: QueryKind,
|
||||
) -> Checked<QueryPlan> {
|
||||
let entries = map_entries(source, "query")?;
|
||||
let mut query = default_query(source.span);
|
||||
let mut seen = BTreeSet::new();
|
||||
for entry in entries {
|
||||
let name = map_key(entry)?;
|
||||
if !seen.insert(name.clone()) {
|
||||
return Err(Diagnostic::error(
|
||||
"C002",
|
||||
format!("duplicate query option '{name}'"),
|
||||
entry.span,
|
||||
));
|
||||
}
|
||||
match name.as_str() {
|
||||
"where" => query.condition = Some(self.condition(model, &entry.value)?),
|
||||
"order" => query.order = self.order(model, &entry.value)?,
|
||||
"with" if matches!(kind, QueryKind::Included | QueryKind::Cursor) => {
|
||||
query.relations = self.relations(model, &entry.value)?
|
||||
}
|
||||
"after" if kind == QueryKind::Cursor => {
|
||||
query.after = Some(
|
||||
self.expression(&entry.value, Some(&Type::Named(model.record).nullable()))?,
|
||||
)
|
||||
}
|
||||
"page" => query.page = self.expression(&entry.value, Some(&Type::Int))?,
|
||||
"size" => query.size = self.expression(&entry.value, Some(&Type::Int))?,
|
||||
_ => {
|
||||
return Err(Diagnostic::error(
|
||||
"C004",
|
||||
format!("unknown query option '{name}'"),
|
||||
entry.key.span,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
if !query.order.iter().any(|order| order.field == 0) {
|
||||
query.order.push(ModelOrder {
|
||||
field: 0,
|
||||
descending: true,
|
||||
});
|
||||
}
|
||||
if kind == QueryKind::Cursor
|
||||
&& query
|
||||
.order
|
||||
.iter()
|
||||
.any(|order| model.fields[order.field].nullable)
|
||||
{
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"cursor ordering does not support nullable Model fields",
|
||||
source.span,
|
||||
));
|
||||
}
|
||||
if kind == QueryKind::Stream && seen.contains("page") {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"stream traverses the full query and does not accept page",
|
||||
source.span,
|
||||
));
|
||||
}
|
||||
Ok(query)
|
||||
}
|
||||
|
||||
fn relations(&self, model: &ModelSchema, source: &Expression) -> Checked<Vec<usize>> {
|
||||
let Ast::List(items) = &source.kind else {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"with requires a static List of relation names",
|
||||
source.span,
|
||||
));
|
||||
};
|
||||
let mut seen = BTreeSet::new();
|
||||
let mut relations = Vec::new();
|
||||
for item in items {
|
||||
let Ast::Name(path) = &item.kind else {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"with entries must be bare relation names",
|
||||
item.span,
|
||||
));
|
||||
};
|
||||
if path.len() != 1 {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"nested relation loading is not supported",
|
||||
item.span,
|
||||
));
|
||||
}
|
||||
let Some(index) = model
|
||||
.relations
|
||||
.iter()
|
||||
.position(|relation| relation.name == path[0].text)
|
||||
else {
|
||||
return Err(Diagnostic::error(
|
||||
"C004",
|
||||
format!("unknown Model relation '{}'", path[0].text),
|
||||
item.span,
|
||||
));
|
||||
};
|
||||
if !seen.insert(index) {
|
||||
return Err(Diagnostic::error(
|
||||
"C002",
|
||||
format!("with cannot repeat relation '{}'", path[0].text),
|
||||
item.span,
|
||||
));
|
||||
}
|
||||
if model.relations[index].logical {
|
||||
return Err(Diagnostic::error(
|
||||
"C014",
|
||||
format!(
|
||||
"logical cross-scope relation '{}' cannot be loaded with 'with'",
|
||||
path[0].text
|
||||
),
|
||||
item.span,
|
||||
));
|
||||
}
|
||||
relations.push(index);
|
||||
}
|
||||
Ok(relations)
|
||||
}
|
||||
|
||||
fn condition(&mut self, model: &ModelSchema, source: &Expression) -> Checked<ModelCondition> {
|
||||
if let Ast::Group(value) = &source.kind {
|
||||
return self.condition(model, value);
|
||||
}
|
||||
if let Ast::Unary {
|
||||
operator: crate::syntax::UnaryOperator::Not,
|
||||
value,
|
||||
} = &source.kind
|
||||
{
|
||||
return Ok(ModelCondition::Not(Box::new(self.condition(model, value)?)));
|
||||
}
|
||||
if let Ast::Call {
|
||||
function,
|
||||
arguments,
|
||||
} = &source.kind
|
||||
{
|
||||
let Ast::Name(path) = &function.kind else {
|
||||
return Err(invalid_condition(source.span));
|
||||
};
|
||||
if path.len() == 1 {
|
||||
return self.condition_call(model, &path[0].text, arguments, source.span);
|
||||
}
|
||||
return Err(invalid_condition(source.span));
|
||||
}
|
||||
let Ast::Binary {
|
||||
left,
|
||||
operator,
|
||||
right,
|
||||
} = &source.kind
|
||||
else {
|
||||
return Err(invalid_condition(source.span));
|
||||
};
|
||||
if matches!(operator, BinaryOperator::And | BinaryOperator::Or) {
|
||||
let left = Box::new(self.condition(model, left)?);
|
||||
let right = Box::new(self.condition(model, right)?);
|
||||
return Ok(if *operator == BinaryOperator::And {
|
||||
ModelCondition::And(left, right)
|
||||
} else {
|
||||
ModelCondition::Or(left, right)
|
||||
});
|
||||
}
|
||||
if !matches!(
|
||||
operator,
|
||||
BinaryOperator::Equal
|
||||
| BinaryOperator::NotEqual
|
||||
| BinaryOperator::Less
|
||||
| BinaryOperator::LessEqual
|
||||
| BinaryOperator::Greater
|
||||
| BinaryOperator::GreaterEqual
|
||||
) {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"unsupported Model comparison",
|
||||
source.span,
|
||||
));
|
||||
}
|
||||
let field = field_name(left).ok_or_else(|| {
|
||||
Diagnostic::error(
|
||||
"C005",
|
||||
"the left side of a Model comparison must be a field",
|
||||
left.span,
|
||||
)
|
||||
})?;
|
||||
let fields = self.model_fields(model).to_vec();
|
||||
let Some((index, definition)) = fields
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, definition)| definition.name == field)
|
||||
else {
|
||||
return Err(Diagnostic::error(
|
||||
"C004",
|
||||
format!("unknown Model field '{field}'"),
|
||||
left.span,
|
||||
));
|
||||
};
|
||||
if matches!(right.kind, Ast::Literal(crate::syntax::Literal::Null))
|
||||
&& !matches!(operator, BinaryOperator::Equal | BinaryOperator::NotEqual)
|
||||
{
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"null only supports equality comparisons",
|
||||
right.span,
|
||||
));
|
||||
}
|
||||
let expected = definition.ty.clone();
|
||||
let value = self.expression(right, Some(&expected))?;
|
||||
Ok(ModelCondition::Compare {
|
||||
field: index,
|
||||
operator: *operator,
|
||||
value,
|
||||
})
|
||||
}
|
||||
|
||||
fn condition_call(
|
||||
&mut self,
|
||||
model: &ModelSchema,
|
||||
name: &str,
|
||||
arguments: &[Expression],
|
||||
span: Span,
|
||||
) -> Checked<ModelCondition> {
|
||||
let expected = match name {
|
||||
"in" | "contains" | "starts_with" | "ends_with" => 2,
|
||||
"between" => 3,
|
||||
_ => return Err(invalid_condition(span)),
|
||||
};
|
||||
if arguments.len() != expected {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
format!("where '{name}' expects {expected} arguments"),
|
||||
span,
|
||||
));
|
||||
}
|
||||
let field_name = field_name(&arguments[0]).ok_or_else(|| {
|
||||
Diagnostic::error(
|
||||
"C005",
|
||||
format!("the first '{name}' argument must be a Model field"),
|
||||
arguments[0].span,
|
||||
)
|
||||
})?;
|
||||
let fields = self.model_fields(model).to_vec();
|
||||
let Some((field, definition)) = fields
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, definition)| definition.name == field_name)
|
||||
else {
|
||||
return Err(Diagnostic::error(
|
||||
"C004",
|
||||
format!("unknown Model field '{field_name}'"),
|
||||
arguments[0].span,
|
||||
));
|
||||
};
|
||||
let field_type = definition.ty.clone();
|
||||
match name {
|
||||
"in" => {
|
||||
let Ast::List(items) = &arguments[1].kind else {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"where 'in' requires a static List as its second argument",
|
||||
arguments[1].span,
|
||||
));
|
||||
};
|
||||
if items.is_empty() || items.len() > MAX_QUERY_VALUES {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
format!("where 'in' requires between 1 and {MAX_QUERY_VALUES} values"),
|
||||
arguments[1].span,
|
||||
));
|
||||
}
|
||||
let values = items
|
||||
.iter()
|
||||
.map(|item| self.expression(item, Some(&field_type)))
|
||||
.collect::<Checked<Vec<_>>>()?;
|
||||
Ok(ModelCondition::In { field, values })
|
||||
}
|
||||
"between" => {
|
||||
let lower = self.expression(&arguments[1], Some(&field_type))?;
|
||||
let upper = self.expression(&arguments[2], Some(&field_type))?;
|
||||
if !field_type.base().numeric()
|
||||
&& !matches!(
|
||||
field_type.base(),
|
||||
Type::DateTime | Type::Date | Type::Time | Type::Duration
|
||||
)
|
||||
{
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"where 'between' requires an ordered numeric or time field",
|
||||
arguments[0].span,
|
||||
));
|
||||
}
|
||||
Ok(ModelCondition::Between {
|
||||
field,
|
||||
lower,
|
||||
upper,
|
||||
})
|
||||
}
|
||||
_ => {
|
||||
if field_type.base() != &Type::Text {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
format!("where '{name}' requires a Text field"),
|
||||
arguments[0].span,
|
||||
));
|
||||
}
|
||||
let value = self.expression(&arguments[1], Some(&Type::Text))?;
|
||||
let operation = match name {
|
||||
"contains" => TextCondition::Contains,
|
||||
"starts_with" => TextCondition::StartsWith,
|
||||
"ends_with" => TextCondition::EndsWith,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
Ok(ModelCondition::Text {
|
||||
field,
|
||||
operation,
|
||||
value,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn order(&self, model: &ModelSchema, source: &Expression) -> Checked<Vec<ModelOrder>> {
|
||||
let items: &[Expression] = match &source.kind {
|
||||
Ast::List(items) => items,
|
||||
_ => std::slice::from_ref(source),
|
||||
};
|
||||
let fields = self.model_fields(model);
|
||||
let mut seen = BTreeSet::new();
|
||||
let mut result = Vec::new();
|
||||
for item in items {
|
||||
let Ast::Name(path) = &item.kind else {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"order entries use 'field.asc' or 'field.desc'",
|
||||
item.span,
|
||||
));
|
||||
};
|
||||
if path.len() != 2 || !matches!(path[1].text.as_str(), "asc" | "desc") {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"order entries use 'field.asc' or 'field.desc'",
|
||||
item.span,
|
||||
));
|
||||
}
|
||||
let Some(index) = fields.iter().position(|field| field.name == path[0].text) else {
|
||||
return Err(Diagnostic::error(
|
||||
"C004",
|
||||
format!("unknown Model field '{}'", path[0].text),
|
||||
path[0].span,
|
||||
));
|
||||
};
|
||||
if !seen.insert(index) {
|
||||
return Err(Diagnostic::error(
|
||||
"C002",
|
||||
"order cannot repeat a Model field",
|
||||
item.span,
|
||||
));
|
||||
}
|
||||
result.push(ModelOrder {
|
||||
field: index,
|
||||
descending: path[1].text == "desc",
|
||||
});
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn model_fields(&self, model: &ModelSchema) -> &[crate::types::Field] {
|
||||
let Shape::Record(fields) = &self.context.types[model.record].shape else {
|
||||
unreachable!("Model record type")
|
||||
};
|
||||
fields
|
||||
}
|
||||
|
||||
fn id_type(&self, model: &ModelSchema) -> Type {
|
||||
self.model_fields(model)[0].ty.clone()
|
||||
}
|
||||
|
||||
fn sql_parameter_type(&self, parameter: &ModelSqlParameter) -> Type {
|
||||
let ty = match ¶meter.ty {
|
||||
ModelFieldType::Bool => Type::Bool,
|
||||
ModelFieldType::Int => Type::Int,
|
||||
ModelFieldType::Float => Type::Float,
|
||||
ModelFieldType::Decimal { .. } => Type::Decimal,
|
||||
ModelFieldType::Text { .. } => Type::Text,
|
||||
ModelFieldType::Bytes { .. } => Type::Bytes,
|
||||
ModelFieldType::Uuid => Type::Uuid,
|
||||
ModelFieldType::DateTime => Type::DateTime,
|
||||
ModelFieldType::Date => Type::Date,
|
||||
ModelFieldType::Time => Type::Time,
|
||||
ModelFieldType::Duration => Type::Duration,
|
||||
ModelFieldType::Json => Type::Json,
|
||||
ModelFieldType::Choice(name) => Type::Named(
|
||||
self.context
|
||||
.types
|
||||
.iter()
|
||||
.position(|definition| definition.name == *name)
|
||||
.expect("validated SQL choice type"),
|
||||
),
|
||||
ModelFieldType::ModelId(model) => {
|
||||
let id = format!("{model}.id");
|
||||
Type::Named(
|
||||
self.context
|
||||
.types
|
||||
.iter()
|
||||
.position(|definition| definition.name == id)
|
||||
.expect("validated SQL Model ID type"),
|
||||
)
|
||||
}
|
||||
};
|
||||
if parameter.nullable {
|
||||
ty.nullable()
|
||||
} else {
|
||||
ty
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn arity(name: &str, arguments: &[Expression], expected: usize, span: Span) -> Checked<()> {
|
||||
if arguments.len() == expected {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Diagnostic::error(
|
||||
"C005",
|
||||
format!("'{name}' expects {expected} argument(s)"),
|
||||
span,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn map_entries<'a>(source: &'a Expression, operation: &str) -> Checked<&'a [MapEntry]> {
|
||||
match &source.kind {
|
||||
Ast::Map(entries) => Ok(entries),
|
||||
_ => Err(Diagnostic::error(
|
||||
"C005",
|
||||
format!("{operation} requires an anonymous block"),
|
||||
source.span,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_key(entry: &MapEntry) -> Checked<String> {
|
||||
match &entry.key.kind {
|
||||
Ast::Name(path) if path.len() == 1 => Ok(path_name(path)),
|
||||
_ => Err(Diagnostic::error(
|
||||
"C005",
|
||||
"anonymous block keys must be bare names",
|
||||
entry.key.span,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_distinct_fields(
|
||||
left: &[(usize, Value)],
|
||||
right: &[(usize, Value)],
|
||||
span: Span,
|
||||
) -> Checked<()> {
|
||||
let left = left
|
||||
.iter()
|
||||
.map(|(field, _)| *field)
|
||||
.collect::<BTreeSet<_>>();
|
||||
if right.iter().any(|(field, _)| left.contains(field)) {
|
||||
return Err(Diagnostic::error(
|
||||
"C002",
|
||||
"upsert key and create blocks cannot repeat a Model field",
|
||||
span,
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn invalid_condition(span: Span) -> Diagnostic {
|
||||
Diagnostic::error(
|
||||
"C005",
|
||||
"where requires Model comparisons, not/and/or, in, between or Text matching",
|
||||
span,
|
||||
)
|
||||
}
|
||||
|
||||
fn field_name(source: &Expression) -> Option<&str> {
|
||||
match &source.kind {
|
||||
Ast::Name(path) if path.len() == 1 => Some(&path[0].text),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn default_query(span: Span) -> QueryPlan {
|
||||
QueryPlan {
|
||||
condition: None,
|
||||
order: vec![ModelOrder {
|
||||
field: 0,
|
||||
descending: true,
|
||||
}],
|
||||
relations: Vec::new(),
|
||||
after: None,
|
||||
page: int(1, span),
|
||||
size: int(20, span),
|
||||
}
|
||||
}
|
||||
|
||||
fn int(value: i64, span: Span) -> Value {
|
||||
Value {
|
||||
kind: Expr::Constant(crate::hir::Constant::Int(value)),
|
||||
ty: Type::Int,
|
||||
span,
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,173 @@
|
||||
//! A deliberately small proof of read-only SQL, not a general SQL parser.
|
||||
//! Anything outside this complete single-table projection keeps its write effect.
|
||||
|
||||
use crate::model::{ModelSchema, ModelSql};
|
||||
|
||||
pub(super) fn proven(model: &ModelSchema, operation: &ModelSql) -> bool {
|
||||
dialect(model, &operation.sqlite, b'?') && dialect(model, &operation.postgres, b'$')
|
||||
}
|
||||
|
||||
fn dialect(model: &ModelSchema, sql: &str, marker: u8) -> bool {
|
||||
let Some(tokens) = tokens(sql, marker) else {
|
||||
return false;
|
||||
};
|
||||
let mut query = Projection {
|
||||
tokens: &tokens,
|
||||
position: 0,
|
||||
model,
|
||||
};
|
||||
if !query.keyword("SELECT") || !query.fields(false) || !query.keyword("FROM") {
|
||||
return false;
|
||||
}
|
||||
if !query.identifier(&model.table) {
|
||||
return false;
|
||||
}
|
||||
if query.keyword("WHERE") {
|
||||
loop {
|
||||
if !query.field() || !query.take(Token::Equal) || !query.take(Token::Bind) {
|
||||
return false;
|
||||
}
|
||||
if !query.keyword("AND") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if query.keyword("ORDER") && (!query.keyword("BY") || !query.fields(true)) {
|
||||
return false;
|
||||
}
|
||||
if query.keyword("LIMIT") && !query.take(Token::Bind) {
|
||||
return false;
|
||||
}
|
||||
query.take(Token::Terminator);
|
||||
query.position == tokens.len()
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Eq, PartialEq)]
|
||||
enum Token<'a> {
|
||||
Word(&'a str),
|
||||
Quoted(&'a str),
|
||||
Bind,
|
||||
Comma,
|
||||
Equal,
|
||||
Terminator,
|
||||
}
|
||||
|
||||
impl Token<'_> {
|
||||
fn identifier(self, expected: &str) -> bool {
|
||||
match self {
|
||||
Self::Word(name) => name.eq_ignore_ascii_case(expected),
|
||||
Self::Quoted(name) => name == expected,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn tokens(sql: &str, marker: u8) -> Option<Vec<Token<'_>>> {
|
||||
let bytes = sql.as_bytes();
|
||||
let mut tokens = Vec::new();
|
||||
let mut position = 0;
|
||||
while position < bytes.len() {
|
||||
let start = position;
|
||||
let byte = bytes[position];
|
||||
position += 1;
|
||||
let token = match byte {
|
||||
b if b.is_ascii_whitespace() => continue,
|
||||
b',' => Token::Comma,
|
||||
b'=' => Token::Equal,
|
||||
b';' => Token::Terminator,
|
||||
b'"' => {
|
||||
let end = bytes[position..].iter().position(|byte| *byte == b'"')? + position;
|
||||
let name = &sql[position..end];
|
||||
position = end + 1;
|
||||
Token::Quoted(name)
|
||||
}
|
||||
b if b == marker => {
|
||||
let digits = position;
|
||||
while bytes.get(position).is_some_and(u8::is_ascii_digit) {
|
||||
position += 1;
|
||||
}
|
||||
if position == digits {
|
||||
return None;
|
||||
}
|
||||
// Existing Model SQL validation owns index bounds and complete binding.
|
||||
Token::Bind
|
||||
}
|
||||
b if b.is_ascii_alphabetic() || b == b'_' => {
|
||||
while bytes
|
||||
.get(position)
|
||||
.is_some_and(|b| b.is_ascii_alphanumeric() || *b == b'_')
|
||||
{
|
||||
position += 1;
|
||||
}
|
||||
Token::Word(&sql[start..position])
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
tokens.push(token);
|
||||
}
|
||||
Some(tokens)
|
||||
}
|
||||
|
||||
struct Projection<'a> {
|
||||
tokens: &'a [Token<'a>],
|
||||
position: usize,
|
||||
model: &'a ModelSchema,
|
||||
}
|
||||
|
||||
impl Projection<'_> {
|
||||
fn take(&mut self, token: Token<'_>) -> bool {
|
||||
if self.tokens.get(self.position) != Some(&token) {
|
||||
return false;
|
||||
}
|
||||
self.position += 1;
|
||||
true
|
||||
}
|
||||
|
||||
fn keyword(&mut self, keyword: &str) -> bool {
|
||||
if !matches!(self.tokens.get(self.position), Some(Token::Word(word)) if word.eq_ignore_ascii_case(keyword))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
self.position += 1;
|
||||
true
|
||||
}
|
||||
|
||||
fn identifier(&mut self, expected: &str) -> bool {
|
||||
if !self
|
||||
.tokens
|
||||
.get(self.position)
|
||||
.is_some_and(|token| token.identifier(expected))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
self.position += 1;
|
||||
true
|
||||
}
|
||||
|
||||
fn field(&mut self) -> bool {
|
||||
if !self.tokens.get(self.position).is_some_and(|token| {
|
||||
self.model
|
||||
.fields
|
||||
.iter()
|
||||
.any(|field| token.identifier(&field.name))
|
||||
}) {
|
||||
return false;
|
||||
}
|
||||
self.position += 1;
|
||||
true
|
||||
}
|
||||
|
||||
fn fields(&mut self, ordered: bool) -> bool {
|
||||
loop {
|
||||
if !self.field() {
|
||||
return false;
|
||||
}
|
||||
if ordered && !self.keyword("ASC") {
|
||||
self.keyword("DESC");
|
||||
}
|
||||
if !self.take(Token::Comma) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,694 @@
|
||||
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<String> {
|
||||
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<usize> {
|
||||
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<Option<(usize, usize)>> {
|
||||
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<Option<(Type, usize, Vec<Field>)>> {
|
||||
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<Model>",
|
||||
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<usize> {
|
||||
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<Type> {
|
||||
let arguments = reference
|
||||
.arguments
|
||||
.iter()
|
||||
.map(|ty| self.resolve_type(ty))
|
||||
.collect::<Checked<Vec<_>>>()?;
|
||||
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<Model>",
|
||||
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<Vec<Field>> {
|
||||
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<Shape> {
|
||||
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::<Checked<_>>()?,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn map_key(&self, ty: &Type) -> bool {
|
||||
map_key(ty, self.types)
|
||||
}
|
||||
|
||||
pub(super) fn value_fields(&self, ty: &Type) -> Option<Vec<Field>> {
|
||||
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<Option<usize>> {
|
||||
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::Related(element)
|
||||
| Type::Channel(element) => {
|
||||
if !matches!(ty, Type::Related(_)) {
|
||||
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<Diagnostic>) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
use super::{Checked, body::Body};
|
||||
use crate::diagnostic::Diagnostic;
|
||||
use crate::hir::{Expression, ExpressionKind};
|
||||
use crate::intrinsic::Intrinsic;
|
||||
use crate::source::Span;
|
||||
use crate::syntax::{Expression as SourceExpression, ExpressionKind as Ast};
|
||||
use crate::types::Type;
|
||||
|
||||
pub(super) fn validate_signatures(
|
||||
functions: &[crate::hir::Function],
|
||||
routes: &[crate::hir::ApiRoute],
|
||||
errors: &mut Vec<Diagnostic>,
|
||||
) {
|
||||
for (id, function) in functions.iter().enumerate() {
|
||||
if crate::source::SourceMap::is_standard(function.span.source) {
|
||||
continue;
|
||||
}
|
||||
let allowed = routes
|
||||
.iter()
|
||||
.any(|route| route.function == id && route.method == "POST")
|
||||
|| function.port.is_some()
|
||||
|| function.implementation;
|
||||
for parameter in &function.parameters {
|
||||
let Some(ty) = parameter.value_type() else {
|
||||
if let crate::types::Parameter::Handler(handler) = parameter
|
||||
&& handler
|
||||
.parameters
|
||||
.iter()
|
||||
.chain(handler.outputs.iter().map(|field| &field.ty))
|
||||
.any(|ty| ty == &Type::Upload)
|
||||
{
|
||||
errors.push(Diagnostic::error(
|
||||
"C005",
|
||||
"Upload cannot cross a handler boundary",
|
||||
function.span,
|
||||
));
|
||||
}
|
||||
continue;
|
||||
};
|
||||
let mut upload = false;
|
||||
crate::types::visit_type(ty, &mut |ty| upload |= matches!(ty, Type::Upload));
|
||||
if upload && (!allowed || ty != &Type::Upload) {
|
||||
errors.push(Diagnostic::error(
|
||||
"C005",
|
||||
"Upload requires a direct POST App, Port or Adapter input",
|
||||
function.span,
|
||||
));
|
||||
}
|
||||
}
|
||||
for field in &function.outputs {
|
||||
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 returned",
|
||||
function.span,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Body<'_, '_> {
|
||||
pub(super) fn upload_call(
|
||||
&mut self,
|
||||
name: &str,
|
||||
arguments: &[SourceExpression],
|
||||
span: Span,
|
||||
) -> Option<Checked<Expression>> {
|
||||
let (operation, ty, consume) = match name {
|
||||
"dever.api.upload_filename" | "dever.system.upload_filename" => {
|
||||
(Intrinsic::UploadFilename, Type::Text, false)
|
||||
}
|
||||
"dever.api.upload_content_type" | "dever.system.upload_content_type" => {
|
||||
(Intrinsic::UploadContentType, Type::Text, false)
|
||||
}
|
||||
"dever.api.upload_size" | "dever.system.upload_size" => {
|
||||
(Intrinsic::UploadSize, Type::Int, false)
|
||||
}
|
||||
"dever.api.close_upload" | "dever.system.upload_close" => {
|
||||
(Intrinsic::UploadClose, Type::Unit, true)
|
||||
}
|
||||
"dever.system.upload_store" => {
|
||||
let result = self.context.named_type("dever.storage.PutResult", span);
|
||||
match result {
|
||||
Ok(result) => (Intrinsic::UploadStore, Type::Named(result), true),
|
||||
Err(error) => return Some(Err(error)),
|
||||
}
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
Some((|| {
|
||||
if name.starts_with("dever.system.")
|
||||
&& !self.context.symbols.packages[self.context.owner].bundled
|
||||
{
|
||||
return Err(Diagnostic::error(
|
||||
"C006",
|
||||
"system upload primitives are reserved for the official library",
|
||||
span,
|
||||
));
|
||||
}
|
||||
let [argument] = arguments else {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"upload operation requires one Upload local",
|
||||
span,
|
||||
));
|
||||
};
|
||||
let Ast::Name(path) = &argument.kind else {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"upload operation requires an Upload local",
|
||||
span,
|
||||
));
|
||||
};
|
||||
if path.len() != 1 {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"upload operation requires an Upload local",
|
||||
span,
|
||||
));
|
||||
}
|
||||
let (slot, input) = self.affine_local(&path[0].text, argument.span, consume)?;
|
||||
if input != Type::Upload {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"upload operation requires Upload",
|
||||
span,
|
||||
));
|
||||
}
|
||||
Ok(Expression {
|
||||
kind: ExpressionKind::Intrinsic {
|
||||
operation,
|
||||
handler: None,
|
||||
arguments: vec![Expression {
|
||||
kind: ExpressionKind::Local(slot),
|
||||
ty: input,
|
||||
span: argument.span,
|
||||
}],
|
||||
},
|
||||
ty,
|
||||
span,
|
||||
})
|
||||
})())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use super::{
|
||||
Checked,
|
||||
body::Body,
|
||||
expressions::{is_null, needs_context},
|
||||
path_name,
|
||||
};
|
||||
use crate::diagnostic::Diagnostic;
|
||||
use crate::hir::{Expression as Value, ExpressionKind as Expr};
|
||||
use crate::source::Span;
|
||||
use crate::syntax::{self, Expression};
|
||||
use crate::types::{DefinitionKind, Shape, Type};
|
||||
|
||||
impl Body<'_, '_> {
|
||||
pub(super) fn record(
|
||||
&mut self,
|
||||
name: &syntax::Path,
|
||||
fields: &[syntax::RecordField],
|
||||
span: Span,
|
||||
) -> Checked<Value> {
|
||||
let id = self.context.named_type(&path_name(name), span)?;
|
||||
if matches!(
|
||||
self.context.types[id].kind,
|
||||
DefinitionKind::ModelId | DefinitionKind::ModelPage | DefinitionKind::ModelCursor
|
||||
) {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"generated Model values are created only by database operations",
|
||||
span,
|
||||
));
|
||||
}
|
||||
let Shape::Record(expected) = &self.context.types[id].shape else {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"choice values use a type-qualified variant constructor",
|
||||
span,
|
||||
));
|
||||
};
|
||||
let expected = expected.clone();
|
||||
if !self.context.owns_private_fields(id) && expected.iter().any(|field| field.private) {
|
||||
return Err(Diagnostic::error(
|
||||
"C006",
|
||||
"record with private fields can only be constructed by its owner (Model: owning domain App)",
|
||||
span,
|
||||
));
|
||||
}
|
||||
let mut seen = BTreeSet::new();
|
||||
let mut values = Vec::new();
|
||||
for field in fields {
|
||||
let Some((index, expected)) = expected
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, expected)| expected.name == field.name.text)
|
||||
else {
|
||||
return Err(Diagnostic::error(
|
||||
"C004",
|
||||
"unknown constructor field",
|
||||
field.name.span,
|
||||
));
|
||||
};
|
||||
if !seen.insert(index) {
|
||||
return Err(Diagnostic::error(
|
||||
"C002",
|
||||
"duplicate constructor field",
|
||||
field.span,
|
||||
));
|
||||
}
|
||||
values.push((index, self.expression(&field.value, Some(&expected.ty))?));
|
||||
}
|
||||
if seen.len() != expected.len() {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"record construction must initialize every field, including nullable fields",
|
||||
span,
|
||||
));
|
||||
}
|
||||
Ok(Value {
|
||||
kind: Expr::Record { fields: values },
|
||||
ty: Type::Named(id),
|
||||
span,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn construct_variant(
|
||||
&mut self,
|
||||
id: usize,
|
||||
variant: usize,
|
||||
arguments: &[Expression],
|
||||
span: Span,
|
||||
) -> Checked<Value> {
|
||||
self.construct_variant_with_kind(id, variant, arguments, span, false)
|
||||
}
|
||||
|
||||
pub(super) fn construct_failure_variant(
|
||||
&mut self,
|
||||
id: usize,
|
||||
variant: usize,
|
||||
arguments: &[Expression],
|
||||
span: Span,
|
||||
) -> Checked<Value> {
|
||||
self.construct_variant_with_kind(id, variant, arguments, span, true)
|
||||
}
|
||||
|
||||
fn construct_variant_with_kind(
|
||||
&mut self,
|
||||
id: usize,
|
||||
variant: usize,
|
||||
arguments: &[Expression],
|
||||
span: Span,
|
||||
failure: bool,
|
||||
) -> Checked<Value> {
|
||||
let Shape::Choice(variants) = &self.context.types[id].shape else {
|
||||
unreachable!()
|
||||
};
|
||||
if failure && !variants[variant].error {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"fail requires an error choice variant",
|
||||
span,
|
||||
));
|
||||
}
|
||||
if !failure && variants[variant].error {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"error choice variants can only be used with fail",
|
||||
span,
|
||||
));
|
||||
}
|
||||
let fields = variants[variant].fields.clone();
|
||||
if fields.len() != arguments.len() {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"choice constructor payload arity mismatch",
|
||||
span,
|
||||
));
|
||||
}
|
||||
let arguments = arguments
|
||||
.iter()
|
||||
.zip(fields)
|
||||
.map(|(argument, field)| self.expression(argument, Some(&field.ty)))
|
||||
.collect::<Checked<_>>()?;
|
||||
Ok(Value {
|
||||
kind: Expr::Variant { variant, arguments },
|
||||
ty: Type::Named(id),
|
||||
span,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn list(
|
||||
&mut self,
|
||||
elements: &[Expression],
|
||||
expected: Option<&Type>,
|
||||
span: Span,
|
||||
) -> Checked<Value> {
|
||||
let hint = match expected.map(Type::base) {
|
||||
Some(Type::List(element)) => Some(element.as_ref()),
|
||||
_ => None,
|
||||
};
|
||||
let (values, element) = self.homogeneous(elements, hint, span)?;
|
||||
Ok(Value {
|
||||
kind: Expr::List(values),
|
||||
ty: Type::List(Box::new(element)),
|
||||
span,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn map(
|
||||
&mut self,
|
||||
entries: &[syntax::MapEntry],
|
||||
expected: Option<&Type>,
|
||||
span: Span,
|
||||
) -> Checked<Value> {
|
||||
let (key_hint, value_hint) = match expected.map(Type::base) {
|
||||
Some(Type::Map(key, value)) => (Some(key.as_ref()), Some(value.as_ref())),
|
||||
_ => (None, None),
|
||||
};
|
||||
let keys: Vec<_> = entries.iter().map(|entry| entry.key.clone()).collect();
|
||||
let values: Vec<_> = entries.iter().map(|entry| entry.value.clone()).collect();
|
||||
let (keys, key) = self.homogeneous(&keys, key_hint, span)?;
|
||||
let (values, value) = self.homogeneous(&values, value_hint, span)?;
|
||||
if !self.context.map_key(&key) {
|
||||
return Err(Diagnostic::error("C005", "invalid Map key type", span));
|
||||
}
|
||||
let mut seen: Vec<&Value> = Vec::new();
|
||||
for key in &keys {
|
||||
if seen.iter().any(|previous| same_key(previous, key)) {
|
||||
return Err(Diagnostic::error(
|
||||
"C002",
|
||||
"duplicate Map literal key",
|
||||
key.span,
|
||||
));
|
||||
}
|
||||
seen.push(key);
|
||||
}
|
||||
Ok(Value {
|
||||
kind: Expr::Map(keys.into_iter().zip(values).collect()),
|
||||
ty: Type::Map(Box::new(key), Box::new(value)),
|
||||
span,
|
||||
})
|
||||
}
|
||||
|
||||
fn homogeneous(
|
||||
&mut self,
|
||||
sources: &[Expression],
|
||||
hint: Option<&Type>,
|
||||
span: Span,
|
||||
) -> Checked<(Vec<Value>, Type)> {
|
||||
if let Some(hint) = hint {
|
||||
return Ok((
|
||||
sources
|
||||
.iter()
|
||||
.map(|source| self.expression(source, Some(hint)))
|
||||
.collect::<Checked<_>>()?,
|
||||
hint.clone(),
|
||||
));
|
||||
}
|
||||
let seed = sources
|
||||
.iter()
|
||||
.position(|source| !needs_context(source))
|
||||
.ok_or_else(|| {
|
||||
Diagnostic::error(
|
||||
"C005",
|
||||
"empty or null-only collection requires a type context",
|
||||
span,
|
||||
)
|
||||
})?;
|
||||
let first = self.expression(&sources[seed], None)?;
|
||||
let mut ty = first.ty.clone();
|
||||
if !ty.value() {
|
||||
return Err(Diagnostic::error(
|
||||
"C005",
|
||||
"collections cannot contain actions or named multi-output results",
|
||||
span,
|
||||
));
|
||||
}
|
||||
let mut values: Vec<Option<Value>> = vec![None; sources.len()];
|
||||
values[seed] = Some(first);
|
||||
for (index, source) in sources.iter().enumerate() {
|
||||
if index == seed || needs_context(source) {
|
||||
continue;
|
||||
}
|
||||
let value = self.expression(source, None)?;
|
||||
ty = common_type(&ty, &value.ty).ok_or_else(|| {
|
||||
Diagnostic::error(
|
||||
"C005",
|
||||
"collection elements must have one stable type",
|
||||
source.span,
|
||||
)
|
||||
})?;
|
||||
values[index] = Some(value);
|
||||
}
|
||||
if sources.iter().any(is_null) {
|
||||
ty = ty.nullable();
|
||||
}
|
||||
let values = sources
|
||||
.iter()
|
||||
.zip(values)
|
||||
.map(|(source, value)| match value {
|
||||
Some(value) => self.coerce(value, &ty),
|
||||
None => self.expression(source, Some(&ty)),
|
||||
})
|
||||
.collect::<Checked<_>>()?;
|
||||
Ok((values, ty))
|
||||
}
|
||||
}
|
||||
|
||||
fn same_key(left: &Value, right: &Value) -> bool {
|
||||
if left.ty != right.ty {
|
||||
return false;
|
||||
}
|
||||
match (&left.kind, &right.kind) {
|
||||
(Expr::Constant(left), Expr::Constant(right)) => left == right,
|
||||
(
|
||||
Expr::Variant {
|
||||
variant: left,
|
||||
arguments: left_args,
|
||||
},
|
||||
Expr::Variant {
|
||||
variant: right,
|
||||
arguments: right_args,
|
||||
},
|
||||
) => left == right && left_args.is_empty() && right_args.is_empty(),
|
||||
(Expr::Local(left), Expr::Local(right)) => left == right,
|
||||
(
|
||||
Expr::Field {
|
||||
value: left,
|
||||
index: left_index,
|
||||
},
|
||||
Expr::Field {
|
||||
value: right,
|
||||
index: right_index,
|
||||
},
|
||||
) => left_index == right_index && same_key(left, right),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn common_type(left: &Type, right: &Type) -> Option<Type> {
|
||||
if left == right {
|
||||
return Some(left.clone());
|
||||
}
|
||||
if matches!(
|
||||
(left, right),
|
||||
(Type::Int, Type::Decimal) | (Type::Decimal, Type::Int)
|
||||
) {
|
||||
return Some(Type::Decimal);
|
||||
}
|
||||
if left.base() == right.base() {
|
||||
return Some(left.base().clone().nullable());
|
||||
}
|
||||
None
|
||||
}
|
||||
Reference in new issue
Block a user