feat: publish Dever core with project scaffolding and versioned AI skills

This commit is contained in:
Antigravity committed 2026-10-04 13:37:30 +00:00
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
+554
View File
@@ -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);
}
}
+648
View File
@@ -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,
}
}
+63
View File
@@ -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
}
+628
View File
@@ -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(&region[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,
}
}
+403
View File
@@ -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,
)
}
+123
View File
@@ -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())
}
+469
View File
@@ -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,
))
}
}
+148
View File
@@ -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"),
}
}
+253
View File
@@ -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 { .. } => {}
}
}
}
+890
View File
@@ -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(&parameters)
.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))
}
+640
View File
@@ -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))
}
+256
View File
@@ -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
}
+131
View File
@@ -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
+188
View File
@@ -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,
))
}
+184
View File
@@ -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,
}
}
+944
View File
@@ -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 &parameter.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
+173
View File
@@ -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;
}
}
}
}
+694
View File
@@ -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);
}
}
}
}
}
+147
View File
@@ -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,
})
})())
}
}
+313
View File
@@ -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
}