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

+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);
}
}
}
}
}