Files
core/crates/dever-core/src/native.rs
T

1376 lines
54 KiB
Rust

use std::collections::{BTreeMap, BTreeSet, HashSet};
use std::fmt::Write;
use crate::hir::{self, Atom, Constant, Program, Projection, Statement};
use crate::source::{SourceMap, Span};
use crate::specialize::{self, Specialization};
use crate::types::{Parameter, Shape, Type, output_type};
mod api;
mod build;
mod collections;
mod expressions;
mod http;
mod intrinsics;
pub(crate) mod jobs;
pub(crate) mod liveness;
pub(crate) mod orm;
mod ports;
mod records;
mod rest;
mod sequences;
pub(crate) mod updates;
pub mod wire;
pub use build::{
CleanupSummary, NativeProgram, OPTIMIZATION_ARGS, clean_project_artifacts, compile,
compile_application, compile_artifact, compile_project, compile_project_with_resources,
compile_test_suite, compile_with_cache,
};
struct Emitter<'a> {
program: &'a Program,
sources: &'a SourceMap,
names: &'a BTreeMap<Specialization, String>,
bindings: BTreeMap<usize, usize>,
moves: HashSet<*const hir::Expression>,
async_context: bool,
}
type EmittedDefinitions = (
String,
BTreeSet<Specialization>,
BTreeMap<Specialization, String>,
);
/// Verified external Worker bytes supplied by the offline packager. The core
/// compiler only serializes them into the generated native program; it does
/// not resolve registries or inspect a machine cache.
#[derive(Clone, Debug)]
pub struct EmbeddedResource {
pub path: String,
pub bytes: Vec<u8>,
pub sha256: String,
pub executable: bool,
}
#[derive(Clone, Copy)]
enum DatabaseContext {
Inherit,
Detached,
}
/// Generated Rust is a disposable backend artifact, not a public source format.
pub fn emit(program: &Program, sources: &SourceMap, entry: &str) -> Result<String, String> {
emit_with_profile(program, sources, entry, None)
}
fn emit_with_profile(
program: &Program,
sources: &SourceMap,
entry: &str,
profile: Option<dever_runtime::config::RuntimeProfile>,
) -> Result<String, String> {
let entry = program.entry(entry)?;
emit_entry(program, sources, entry, profile)
}
pub fn emit_project(
program: &Program,
sources: &SourceMap,
profile: dever_runtime::config::RuntimeProfile,
) -> Result<String, String> {
emit_project_with_resources(program, sources, profile, &[])
}
pub fn emit_project_with_resources(
program: &Program,
sources: &SourceMap,
profile: dever_runtime::config::RuntimeProfile,
resources: &[EmbeddedResource],
) -> Result<String, String> {
if !program.permissions.is_empty() && !profile.sqlite && !profile.postgres {
return Err("protected API permissions require a configured database".into());
}
let serve_api = !program.api_routes.is_empty() || !program.api_rest.is_empty();
let serve_jobs = !program.jobs.is_empty();
if !serve_api && !serve_jobs && program.api_commands.is_empty() {
return Err("application has no API, Job, or CMD entry".into());
}
let span = program
.api_rest
.first()
.map(|entry| entry.span)
.or_else(|| {
program
.api_routes
.first()
.map(|entry| program.functions[entry.function].span)
})
.or_else(|| {
program
.api_commands
.first()
.map(|entry| program.functions[entry.function].span)
})
.or_else(|| {
program
.jobs
.first()
.map(|entry| program.functions[entry.function].span)
})
.expect("application entry exists");
let roots = program
.api_routes
.iter()
.map(|route| route.function)
.chain(program.api_commands.iter().map(|command| command.function))
.chain(program.jobs.iter().map(|job| job.function))
.chain(program.auth.iter().map(|hook| hook.function))
.chain(program.rest_binding_roots())
.map(|function| Specialization {
function,
handlers: Vec::new(),
})
.collect();
let (mut output, instances, names) =
emit_definitions(program, sources, roots, Some(profile), serve_api)?;
let emitter = Emitter {
program,
sources,
names: &names,
bindings: BTreeMap::new(),
moves: HashSet::new(),
async_context: false,
};
api::emit_commands(program, &mut output);
emit_command_tenant_scopes(program, &mut output);
output.push_str("async fn entry() -> Result<(), AppError> {\nlet args = std::env::args().skip(1).collect::<Vec<_>>();\n");
let cleanup_database = profile.sqlite || profile.postgres;
let cleanup_components = ports::has_external(program, &instances);
if cleanup_database || cleanup_components {
output.push_str("let application: Result<(), AppError> = async {\n");
}
output.push_str("let service_start = args.is_empty();\n");
if !resources.is_empty() {
output.push_str("dever_runtime::external::prepare(&dever_runtime::config::executable_directory()?, DEVER_EXTERNAL_BUNDLE, DEVER_EXTERNAL_RESOURCES).map_err(|error| AppError::fault(\"external resources\", error))?;\n");
}
emit_database_setup(
&emitter,
&mut output,
&instances,
profile,
span,
Some((serve_api, serve_jobs)),
);
if !program.permissions.is_empty() {
output.push_str("if service_start { authorization_initialize().await?; }\n");
}
output.push_str("if args.first().is_some_and(|argument| argument == \"--dever-tenant-migrate\") {\nif args.len() != 2 { return Err(AppError::fault(\"tenant migration\", \"expected --dever-tenant-migrate and one positive tenant id\")); }\nlet tenant_id = args[1].parse::<i64>().map_err(|_| AppError::fault(\"tenant migration\", \"tenant id must be a positive integer\"))?;\nif tenant_id <= 0 { return Err(AppError::fault(\"tenant migration\", \"tenant id must be positive\")); }\n");
if profile.sqlite || profile.postgres {
output.push_str("tenant_migrate(tenant_id).await?;\nreturn Ok(());\n");
} else {
output.push_str("return Err(AppError::fault(\"tenant migration\", \"application has no database runtime\"));\n");
}
output.push_str("}\n");
if !program.permissions.is_empty() {
output.push_str("if args.first().is_some_and(|argument| argument == \"--dever-tenant-owner\") {\nif args.len() != 4 { return Err(AppError::fault(\"tenant owner\", \"expected --dever-tenant-owner, one positive tenant id, one site, and one positive user id\")); }\nlet tenant_id = args[1].parse::<i64>().map_err(|_| AppError::fault(\"tenant owner\", \"tenant id must be a positive integer\"))?;\nlet user_id = args[3].parse::<i64>().map_err(|_| AppError::fault(\"tenant owner\", \"user id must be a positive integer\"))?;\nif tenant_id <= 0 || user_id <= 0 { return Err(AppError::fault(\"tenant owner\", \"tenant id and user id must be positive\")); }\ntenant_owner(tenant_id, &args[2], user_id).await?;\nreturn Ok(());\n}\n");
}
output.push_str("if args.first().is_some_and(|argument| argument == \"--dever-tenant-component\") {\nif args.len() != 4 { return Err(AppError::fault(\"tenant component\", \"expected --dever-tenant-component, one positive tenant id, enable or disable, and one component\")); }\nlet tenant_id = args[1].parse::<i64>().map_err(|_| AppError::fault(\"tenant component\", \"tenant id must be a positive integer\"))?;\nif tenant_id <= 0 { return Err(AppError::fault(\"tenant component\", \"tenant id must be positive\")); }\n");
if program.tenant_components.is_empty() {
output.push_str("return Err(AppError::fault(\"tenant component\", \"application has no tenant components\"));\n");
} else {
output.push_str("match args[2].as_str() {\n\"enable\" => dever_runtime::tenant::enable_component(tenant_id, &args[3], TENANT_COMPONENTS).await.map_err(|error| AppError::database(\"tenant component\", error))?,\n\"disable\" => dever_runtime::tenant::disable_component(tenant_id, &args[3], TENANT_COMPONENTS).await.map_err(|error| AppError::database(\"tenant component\", error))?,\n_ => return Err(AppError::fault(\"tenant component\", \"operation must be enable or disable\")),\n}\nreturn Ok(());\n");
}
output.push_str("}\n");
ports::initialize(&emitter, &mut output, &instances, span)?;
output.push_str("if service_start {\n");
if serve_api || serve_jobs {
let service = match (serve_api, serve_jobs) {
(true, true) => {
"dever_runtime::lifecycle::serve_both(dever_runtime::api::serve(api_route), job_serve()).await"
}
(true, false) => "dever_runtime::api::serve(api_route).await",
(false, true) => "job_serve().await",
(false, false) => unreachable!(),
};
writeln!(output, "dever_runtime::lifecycle::run(async move {{ {service}.map_err(AppError::from) }}).await?;")
.expect("string formatting");
} else {
output.push_str("return Err(AppError::fault(\"command\", \"application has no service entry; select a CMD and JSON input\"));\n");
}
output.push_str("} else {\n");
if !program.api_commands.is_empty() {
output.push_str("let data = if args.first().is_some_and(|argument| argument == \"--dever-tenant-cmd\") {\n");
output.push_str("if args.len() != 4 { return Err(AppError::fault(\"command\", \"expected --dever-tenant-cmd, one positive tenant id, CMD name, and one JSON object\")); }\n");
output.push_str("let tenant_id = args[1].parse::<i64>().map_err(|_| AppError::fault(\"command\", \"tenant id must be a positive integer\"))?;\nif tenant_id <= 0 { return Err(AppError::fault(\"command\", \"tenant id must be a positive integer\")); }\n");
output.push_str("match cmd_requires_tenant(&args[2]) {\nSome(true) => {},\nSome(false) => return Err(AppError::fault(\"command\", \"selected CMD does not use tenant storage; remove --tenant\")),\nNone => return Err(AppError::fault(\"command\", format!(\"unknown CMD '{}'\", args[2]))),\n}\n");
output.push_str("if dever_runtime::config::settings().tenant().is_none() { return Err(AppError::fault(\"command\", \"tenant storage is not configured\")); }\n");
if !cleanup_database {
output.push_str("return Err(AppError::fault(\"command\", \"application has no tenant database runtime\"))\n");
} else if program.jobs.is_empty() {
output.push_str("dever_runtime::tenant::scope(tenant_id, cmd_dispatch(&args[2], &args[3])).await?\n");
} else {
output.push_str("dever_runtime::job::scope_system(Some(tenant_id), dever_runtime::tenant::scope(tenant_id, cmd_dispatch(&args[2], &args[3]))).await?\n");
}
output.push_str("} else {\n");
output.push_str("if args.len() != 2 { return Err(AppError::fault(\"command\", \"expected CMD name and one JSON object\")); }\n");
output.push_str("match cmd_requires_tenant(&args[0]) {\nSome(true) => return Err(AppError::fault(\"command\", \"selected CMD requires an explicit tenant; use dever run --tenant\")),\nSome(false) => {},\nNone => return Err(AppError::fault(\"command\", format!(\"unknown CMD '{}'\", args[0]))),\n}\n");
if program.jobs.is_empty() {
output.push_str("cmd_dispatch(&args[0], &args[1]).await?\n};\n");
} else {
output.push_str("dever_runtime::job::scope_system(None, cmd_dispatch(&args[0], &args[1])).await?\n};\n");
}
output.push_str("dever_runtime::println(&format!(\"{{\\\"code\\\":0,\\\"message\\\":\\\"ok\\\",\\\"data\\\":{}}}\", data.as_str())).map_err(|error| AppError::fault(\"command\", error))?;\n");
} else {
output.push_str(
"return Err(AppError::fault(\"command\", \"application has no CMD entry\"));\n",
);
}
output.push_str("}\nOk(())\n");
if cleanup_database || cleanup_components {
emit_cleanup(
&emitter,
&mut output,
span,
cleanup_database,
cleanup_components,
);
}
output.push_str("}\n");
if !resources.is_empty() {
output.push_str(&embedded_resource_definitions(resources)?);
}
output.push_str("fn main() -> std::process::ExitCode {\nmatch task::run_entry_typed(entry())");
emit_main_result(&mut output);
Ok(output)
}
fn embedded_resource_definitions(resources: &[EmbeddedResource]) -> Result<String, String> {
validate_embedded_resources(resources)?;
let digest = embedded_bundle_digest(resources);
let inputs = resource_inputs(resources);
let definitions = inputs
.unique
.iter()
.enumerate()
.map(|(index, _)| {
format!(
"static DEVER_EXTERNAL_BYTES_{index}: &[u8] = include_bytes!({});\n",
rust_string(&resource_source_filename(index))
)
})
.collect::<String>();
let entries = resources.iter().zip(&inputs.indices).map(|(resource, index)| {
format!(
"dever_runtime::external::Resource {{ path: {}, bytes: DEVER_EXTERNAL_BYTES_{index}, sha256: {}, executable: {} }}",
rust_string(&resource.path), rust_string(&resource.sha256), resource.executable,
)
}).collect::<Vec<_>>().join(", ");
Ok(format!(
"\n{definitions}static DEVER_EXTERNAL_RESOURCES: &[dever_runtime::external::Resource<'static>] = &[{entries}];\nconst DEVER_EXTERNAL_BUNDLE: &str = {};\n",
rust_string(&digest)
))
}
pub(crate) fn validate_embedded_resources(resources: &[EmbeddedResource]) -> Result<(), String> {
use sha2::{Digest, Sha256};
for resource in resources {
let actual = Sha256::digest(&resource.bytes)
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>();
if actual != resource.sha256 {
return Err(format!(
"external resource '{}' has an invalid digest",
resource.path
));
}
}
Ok(())
}
pub(crate) struct ResourceInputs<'a> {
pub unique: Vec<&'a EmbeddedResource>,
pub indices: Vec<usize>,
}
pub(crate) fn resource_inputs(resources: &[EmbeddedResource]) -> ResourceInputs<'_> {
let mut by_digest = BTreeMap::new();
let mut unique = Vec::new();
let mut indices = Vec::with_capacity(resources.len());
for resource in resources {
let index = *by_digest
.entry(resource.sha256.as_str())
.or_insert_with(|| {
let index = unique.len();
unique.push(resource);
index
});
indices.push(index);
}
ResourceInputs { unique, indices }
}
fn resource_source_filename(index: usize) -> String {
format!("external-resource-{index}.bin")
}
pub(crate) fn embedded_bundle_digest(resources: &[EmbeddedResource]) -> String {
use sha2::{Digest, Sha256};
let mut digest = Sha256::new();
for resource in resources {
digest.update(resource.path.as_bytes());
digest.update([0]);
digest.update(resource.sha256.as_bytes());
digest.update([0]);
digest.update((resource.bytes.len() as u64).to_le_bytes());
digest.update([u8::from(resource.executable)]);
}
digest
.finalize()
.iter()
.map(|byte| format!("{byte:02x}"))
.collect()
}
fn emit_entry(
program: &Program,
sources: &SourceMap,
entry: usize,
profile: Option<dever_runtime::config::RuntimeProfile>,
) -> Result<String, String> {
let root = Specialization {
function: entry,
handlers: Vec::new(),
};
let serve_api = api_enabled(program, entry)?;
let serve_jobs = job_enabled(program, entry)?;
if serve_jobs && program.jobs.is_empty() {
return Err("dever.job.serve() requires at least one Job declaration".into());
}
let roots = std::iter::once(root.clone())
.chain(
program
.api_routes
.iter()
.filter(|_| serve_api)
.map(|route| Specialization {
function: route.function,
handlers: Vec::new(),
}),
)
.chain(
program
.jobs
.iter()
.filter(|_| serve_jobs)
.map(|job| Specialization {
function: job.function,
handlers: Vec::new(),
}),
)
.collect::<Vec<_>>();
let (mut output, instances, names) =
emit_definitions(program, sources, roots, profile, serve_api)?;
let emitter = Emitter {
program,
sources,
names: &names,
bindings: BTreeMap::new(),
moves: HashSet::new(),
async_context: false,
};
let root_suspends = specialize::suspends(program, &root);
let schema_suspends = profile
.is_some_and(|profile| profile.postgres || (profile.sqlite && !program.models.is_empty()));
let async_entry = root_suspends || schema_suspends;
output.push_str(if async_entry {
"async fn entry() -> Result<(), AppError> {\n"
} else {
"fn entry() -> Result<(), AppError> {\n"
});
let cleanup_database =
async_entry && profile.is_some_and(|profile| profile.sqlite || profile.postgres);
let cleanup_components = ports::has_external(program, &instances);
if cleanup_database || cleanup_components {
output.push_str("let application: Result<(), AppError> = async {\n");
}
ports::initialize(
&emitter,
&mut output,
&instances,
program.functions[entry].span,
)?;
if let Some(profile) = profile {
emit_database_setup(
&emitter,
&mut output,
&instances,
profile,
program.functions[entry].span,
None,
);
}
let invocation = entry_invocation(program, &names, &root);
let invocation = if serve_api || serve_jobs {
format!("dever_runtime::lifecycle::run(async move {{ {invocation} }}).await")
} else {
invocation
};
writeln!(output, "let result = {invocation}?;").expect("string formatting");
for (index, field) in program.functions[entry].outputs.iter().enumerate() {
let value = if program.functions[entry].outputs.len() == 1 {
"result".into()
} else {
format!("result.{index}")
};
writeln!(
output,
"let mut rendered{index} = String::from({}); rendered{index}.push_str(\" = \"); {value}.render_to(&mut rendered{index}); rendered{index}.push('\\n'); resource::stdout_write(&rendered{index})?;",
rust_string(&field.name),
)
.expect("string formatting");
}
output.push_str("Ok(())\n");
if cleanup_database || cleanup_components {
emit_cleanup(
&emitter,
&mut output,
program.functions[entry].span,
cleanup_database,
cleanup_components,
);
}
output.push_str("}\nfn main() -> std::process::ExitCode {\nmatch ");
output.push_str(if async_entry {
"task::run_entry_typed(entry())"
} else {
"entry()"
});
emit_main_result(&mut output);
Ok(output)
}
fn emit_test_suite(
program: &Program,
sources: &SourceMap,
profile: Option<dever_runtime::config::RuntimeProfile>,
) -> Result<String, String> {
if program.tests.is_empty() {
return Err("cannot compile an empty test suite".into());
}
let mut output = String::from("#![forbid(unsafe_code)]\n");
for index in 0..program.tests.len() {
let case = crate::check::test_program(program, index).map_err(|errors| {
errors
.iter()
.map(|error| error.render(sources))
.collect::<Vec<_>>()
.join("\n")
})?;
writeln!(output, "mod case_{index} {{").expect("string formatting");
output.push_str(&emit_test_case(&case, sources, index, profile)?);
output.push_str("}\n");
}
writeln!(output, "fn main() -> std::process::ExitCode {{\nmatch std::env::args().nth(1).and_then(|value| value.parse::<usize>().ok()) {{").expect("string formatting");
for index in 0..program.tests.len() {
writeln!(output, "Some({index}) => case_{index}::run(),").expect("string formatting");
}
output.push_str("_ => { eprintln!(\"test program requires a valid case index\"); std::process::ExitCode::FAILURE },\n}\n}\n");
Ok(output)
}
fn emit_test_case(
program: &Program,
sources: &SourceMap,
index: usize,
profile: Option<dever_runtime::config::RuntimeProfile>,
) -> Result<String, String> {
let test = &program.tests[index];
let root = Specialization {
function: test.function,
handlers: Vec::new(),
};
let (mut output, instances, names) =
emit_definitions(program, sources, vec![root.clone()], profile, false)?;
let emitter = Emitter {
program,
sources,
names: &names,
bindings: BTreeMap::new(),
moves: HashSet::new(),
async_context: false,
};
let async_entry = specialize::suspends(program, &root)
|| profile.is_some_and(|profile| profile.sqlite && test.uses_database());
output.push_str(if async_entry {
"async fn entry() -> Result<(), AppError> {\n"
} else {
"fn entry() -> Result<(), AppError> {\n"
});
let cleanup_database = async_entry && test.uses_database();
if cleanup_database {
output.push_str("let application: Result<(), AppError> = async {\n");
}
jobs::initialize_clock(&emitter, &mut output, &instances, test.span);
if let Some(profile) = profile.filter(|_| test.uses_database()) {
emit_database_setup(&emitter, &mut output, &instances, profile, test.span, None);
}
let invocation = entry_invocation(program, &names, &root);
if jobs::targets(program, &instances).is_empty() {
writeln!(output, "let _ = {invocation}?;\nOk(())").expect("string formatting");
} else {
writeln!(output, "dever_runtime::job::scope_system(None, async {{ let _ = {invocation}?; Ok(()) }}).await")
.expect("string formatting");
}
if cleanup_database {
emit_cleanup(&emitter, &mut output, test.span, true, false);
}
output.push_str("}\n");
writeln!(
output,
"pub fn run() -> std::process::ExitCode {{\nmatch {}",
if async_entry {
"task::run_entry_typed(entry())"
} else {
"entry()"
},
)
.expect("string formatting");
emit_main_result(&mut output);
Ok(output)
}
fn emit_cleanup(
emitter: &Emitter<'_>,
output: &mut String,
span: Span,
database: bool,
component: bool,
) {
output.push_str("}.await;\nlet mut cleanup: Option<AppError> = None;\n");
if component {
writeln!(output, "if let Err(error) = dever_runtime::component::shutdown().await {{ cleanup = Some(AppError::fault({}, error)); }}", emitter.location(span)).expect("string formatting");
}
if database {
writeln!(output, "if let Err(error) = dever_runtime::database::shutdown().await {{ cleanup = Some(match cleanup {{ Some(previous) => previous.cause(error), None => AppError::database({}, error) }}); }}", emitter.location(span)).expect("string formatting");
}
output.push_str("match (application, cleanup) {\n(Ok(()), None) => Ok(()),\n(Err(error), None) => Err(error),\n(Ok(()), Some(cleanup)) => Err(cleanup),\n(Err(error), Some(cleanup)) => Err(error.cause(cleanup)),\n}\n");
}
fn emit_definitions(
program: &Program,
sources: &SourceMap,
roots: Vec<Specialization>,
profile: Option<dever_runtime::config::RuntimeProfile>,
serve_api: bool,
) -> Result<EmittedDefinitions, String> {
let instances = specialize::execution_reachable(&program.functions, roots)
.map_err(|_| "recursive specialized handler calls are not allowed".to_owned())?;
let names: BTreeMap<_, _> = instances
.iter()
.cloned()
.map(|instance| {
let name = if instance.handlers.is_empty() {
format!("f{}", instance.function)
} else {
format!(
"f{}h{}",
instance.function,
instance
.handlers
.iter()
.map(usize::to_string)
.collect::<Vec<_>>()
.join("h")
)
};
(instance, name)
})
.collect();
let emitter = Emitter {
program,
sources,
names: &names,
bindings: BTreeMap::new(),
moves: HashSet::new(),
async_context: false,
};
let mut output = String::from(
"#![forbid(unsafe_code)]\n#![allow(dead_code, unused_mut, unused_variables, unused_parens)]\nuse dever_runtime::{number::{self, DecimalValue}, collections::{List, Map, MapEntry}, bytes::Bytes, channel, resource, render::Render, task};\n",
);
writeln!(
output,
"static TENANT_COMPONENTS: &[&str] = &[{}];",
program
.tenant_components
.iter()
.map(|component| rust_string(component))
.collect::<Vec<_>>()
.join(", ")
)
.expect("string formatting");
emitter.types(&mut output);
if profile.is_some() {
emitter.model_decoders(&mut output);
}
emitter.errors(&mut output);
ports::definitions(&emitter, &mut output, &instances);
jobs::definitions(&emitter, &mut output, &instances);
jobs::tenant_migration(
&emitter,
&mut output,
&instances,
profile.is_some_and(|profile| profile.sqlite || profile.postgres),
);
for instance in &instances {
let emitter = Emitter {
program,
sources,
names: &names,
bindings: specialize::handler_bindings(&program.functions[instance.function], instance),
moves: HashSet::new(),
async_context: specialize::suspends(program, instance),
};
emitter.function(&mut output, instance);
}
if serve_api {
api::emit_routes(&emitter, &mut output);
}
Ok((output, instances, names))
}
fn emit_database_setup(
emitter: &Emitter<'_>,
output: &mut String,
instances: &BTreeSet<Specialization>,
profile: dever_runtime::config::RuntimeProfile,
entry_span: Span,
application_services: Option<(bool, bool)>,
) {
let program = emitter.program;
let mut bindings = program
.models
.iter()
.map(|model| match &model.connection {
crate::model::ConnectionSelector::Explicit(name) => (
Some(name.as_str()),
model.package.split('.').next().unwrap(),
model.scope == crate::model::ModelScope::Tenant,
),
crate::model::ConnectionSelector::PackageRoot(root) => (
None,
root.as_str(),
model.scope == crate::model::ModelScope::Tenant,
),
})
.collect::<Vec<_>>();
bindings.extend(jobs::targets(program, instances).into_iter().map(|target| {
let (explicit, root) = program.database_selector(crate::model::DatabaseOwner::Job(target));
(
explicit,
root,
jobs::scope(program, target) == crate::model::ModelScope::Tenant,
)
}));
bindings.sort_unstable();
bindings.dedup();
let bindings = bindings
.iter()
.map(|(explicit, root, tenant)| {
format!(
"({}, {}, {tenant})",
explicit
.map(|name| format!("Some({})", rust_string(name)))
.unwrap_or_else(|| "None".into()),
rust_string(root)
)
})
.collect::<Vec<_>>()
.join(", ");
let mut transaction_roots = instances
.iter()
.filter(|instance| {
program.functions[instance.function].kind == crate::syntax::FunctionKind::Transaction
})
.cloned()
.collect::<BTreeSet<_>>();
if application_services.is_some() {
for function in program
.api_routes
.iter()
.map(|route| route.function)
.chain(program.api_commands.iter().map(|command| command.function))
.chain(program.jobs.iter().map(|job| job.function))
{
if function_writes_database(program, function) {
transaction_roots.insert(Specialization {
function,
handlers: Vec::new(),
});
}
}
}
let transactions = transaction_roots
.iter()
.map(|instance| {
let bindings = specialize::database_effects(program, instance)
.iter()
.map(|owner| {
let (explicit, root) = program.database_selector(*owner);
let tenant = match owner {
crate::model::DatabaseOwner::Model(index) => {
program.models[*index].scope == crate::model::ModelScope::Tenant
}
crate::model::DatabaseOwner::Job(target) => {
jobs::scope(program, *target) == crate::model::ModelScope::Tenant
}
};
(explicit, root, tenant)
})
.map(|(explicit, root, tenant)| {
format!(
"({}, {}, {tenant})",
explicit
.map(|name| format!("Some({})", rust_string(name)))
.unwrap_or_else(|| "None".into()),
rust_string(root)
)
})
.collect::<Vec<_>>()
.join(", ");
format!("&[{bindings}]")
})
.collect::<Vec<_>>()
.join(", ");
writeln!(
output,
"dever_runtime::config::bootstrap_scoped(dever_runtime::config::RuntimeProfile {{ sqlite: {}, postgres: {} }}, &[{bindings}], &[{transactions}])?;",
profile.sqlite,
profile.postgres,
)
.expect("string formatting");
let serve_api = application_services.map_or_else(
|| {
instances.iter().any(|instance| {
function_starts_service(&program.functions[instance.function], false)
})
},
|services| services.0,
);
let serve_jobs = application_services.map_or_else(
|| {
instances.iter().any(|instance| {
function_starts_service(&program.functions[instance.function], true)
})
},
|services| services.1,
);
if serve_api || serve_jobs {
if application_services.is_some() {
output.push_str("if service_start {\n");
}
writeln!(
output,
"dever_runtime::lifecycle::configure({serve_api}, {serve_jobs})?;"
)
.expect("string formatting");
if application_services.is_some() {
output.push_str("}\n");
}
}
jobs::preflight(emitter, output, instances, entry_span);
if profile.postgres {
let prepare =
emitter.database_failure("dever_runtime::database::prepare().await", entry_span);
writeln!(output, "{prepare};").expect("string formatting");
}
if profile.sqlite || profile.postgres {
emitter.initialize_models(output);
jobs::initialize(emitter, output, instances, entry_span);
}
}
fn entry_invocation(
program: &Program,
names: &BTreeMap<Specialization, String>,
root: &Specialization,
) -> String {
let root_database = if specialize::database_effects(program, root).is_empty() {
""
} else {
"None"
};
if specialize::suspends(program, root) {
format!("{}({root_database}).await", names[root])
} else {
format!("{}({root_database})", names[root])
}
}
fn emit_main_result(output: &mut String) {
output.push_str(" {\nOk(()) => { dever_runtime::log::flush(); std::process::ExitCode::SUCCESS },\nErr(error) => { dever_runtime::log::error(error.to_string(), vec![]); dever_runtime::log::flush(); std::process::ExitCode::FAILURE }\n}\n}\n");
}
fn api_enabled(program: &Program, entry: usize) -> Result<bool, String> {
service_enabled(program, entry, false)
}
fn service_enabled(program: &Program, entry: usize, worker: bool) -> Result<bool, String> {
let instances = specialize::reachable(
&program.functions,
[Specialization {
function: entry,
handlers: Vec::new(),
}],
)
.map_err(|_| "recursive specialized handler calls are not allowed".to_owned())?;
Ok(instances
.iter()
.any(|instance| function_starts_service(&program.functions[instance.function], worker)))
}
fn job_enabled(program: &Program, entry: usize) -> Result<bool, String> {
service_enabled(program, entry, true)
}
fn function_starts_service(function: &crate::hir::Function, worker: bool) -> bool {
let mut starts = false;
crate::check::visit(function, |expression| {
if let crate::hir::ExpressionKind::Intrinsic { operation, .. } = expression.kind {
starts |= if worker {
matches!(operation, crate::intrinsic::Intrinsic::JobServe)
} else {
matches!(operation, crate::intrinsic::Intrinsic::ApiServe)
};
}
});
starts
}
fn function_writes_database(program: &Program, function: usize) -> bool {
let root = Specialization {
function,
handlers: Vec::new(),
};
specialize::writes_database(program, &root)
}
fn emit_command_tenant_scopes(program: &Program, output: &mut String) {
if program.api_commands.is_empty() {
return;
}
output.push_str("fn cmd_requires_tenant(name: &str) -> Option<bool> {\nmatch name {\n");
for command in &program.api_commands {
let tenant = !command.components.is_empty();
writeln!(output, "{} => Some({tenant}),", rust_string(&command.name))
.expect("string formatting");
}
output.push_str("_ => None,\n}\n}\n");
}
fn transaction_database(program: &Program, function: usize) -> Option<String> {
if !function_writes_database(program, function) {
return None;
}
let root = Specialization {
function,
handlers: Vec::new(),
};
let owner = specialize::database_effects(program, &root)
.into_iter()
.next()?;
Some(match owner {
crate::model::DatabaseOwner::Model(index) => {
format!("model_database_{}().await", program.models[index].record,)
}
crate::model::DatabaseOwner::Job(index) => format!("job_database_{index}().await",),
})
}
impl Emitter<'_> {
fn function(&self, output: &mut String, instance: &Specialization) {
let id = instance.function;
let function = &self.program.functions[id];
let suspends = specialize::suspends(self.program, instance);
let mut parameters = function
.parameters
.iter()
.enumerate()
.filter_map(|(index, parameter)| {
parameter
.value_type()
.map(|ty| format!("_p{index}: {}", rust_type(ty)))
})
.collect::<Vec<_>>()
.join(", ");
let database_effects = specialize::database_effects(self.program, instance);
if !database_effects.is_empty() {
if !parameters.is_empty() {
parameters.push_str(", ");
}
parameters.push_str("_database: Option<&dever_runtime::database::Transaction>");
}
writeln!(
output,
"{}fn {}({parameters}) -> Result<{}, AppError> {{",
if suspends { "async " } else { "" },
self.names[instance],
rust_type(&output_type(&function.outputs))
)
.expect("string formatting");
if ports::function(self, output, instance) {
return;
}
for owner in &database_effects {
match owner {
crate::model::DatabaseOwner::Model(index) => {
let model = &self.program.models[*index];
let database = self.database_failure(
&format!("model_database_{}().await", model.record),
function.span,
);
writeln!(output, "let _model_database_{} = {database};", model.record)
.expect("string formatting");
}
crate::model::DatabaseOwner::Job(target) => {
let database = self
.database_failure(&format!("job_database_{target}().await"), function.span);
writeln!(output, "let _job_database_{target} = {database};")
.expect("string formatting");
}
}
}
let transaction = function.kind == crate::syntax::FunctionKind::Transaction;
if transaction {
let owner = *database_effects
.iter()
.next()
.expect("validated transaction database effect");
let database = self.database_owner_at(owner);
let begin = self.database_failure("transaction_database.begin().await", function.span);
writeln!(
output,
"let transaction_owner = if _database.is_none() {{ let transaction_database = {database}; Some({begin}) }} else {{ None }};\nlet _database = _database.or(transaction_owner.as_ref());\nlet transaction_result: Result<{}, AppError> = async {{",
rust_type(&output_type(&function.outputs))
)
.expect("string formatting");
}
for clause in &function.clauses {
let mut optimized = clause.clone();
self.inline_sequences(&mut optimized);
updates::coalesce(&mut optimized, &self.program.types);
updates::snapshot_scalars(&mut optimized, &self.program.types);
let clause = &optimized;
let clause_emitter = Emitter {
program: self.program,
sources: self.sources,
names: self.names,
bindings: self.bindings.clone(),
moves: liveness::last_uses(clause),
async_context: suspends,
};
let mut domains = clause.patterns.iter();
let guard = function
.parameters
.iter()
.enumerate()
.filter_map(|(index, parameter)| {
let Parameter::Value(ty) = parameter else {
return None;
};
let domain = domains.next().expect("checked value parameter domain");
Some(format!(
"({})",
domain
.iter()
.map(|atom| self.atom(atom, ty, &format!("_p{index}")))
.collect::<Vec<_>>()
.join(" || ")
))
})
.collect::<Vec<_>>()
.join(" && ");
debug_assert!(domains.next().is_none());
writeln!(
output,
"if {} {{",
if guard.is_empty() { "true" } else { &guard }
)
.expect("string formatting");
for (slot, ty) in clause.locals.iter().enumerate() {
writeln!(output, "let mut _v{slot}: {};", rust_type(ty))
.expect("string formatting");
}
for binding in &clause.bindings {
self.binding(
output,
binding,
function.parameters[binding.input]
.value_type()
.expect("value binding"),
clause
.bindings
.iter()
.filter(|other| other.input == binding.input)
.count()
== 1,
);
}
for statement in &clause.body {
match statement {
Statement::Assign {
slot,
fields,
value,
} => {
let mut target = format!("_v{slot}");
let mut ty = clause.locals[*slot].clone();
for index in fields {
target.push_str(&field_suffix(&ty, *index));
ty = self.field_type(&ty, *index);
}
writeln!(output, "{target} = {};", clause_emitter.expression(value))
.expect("string formatting");
}
Statement::Call(value) => {
writeln!(output, "{};", clause_emitter.expression(value))
.expect("string formatting");
}
}
}
if clause.terminates {
writeln!(output, "}}\n").expect("string formatting");
continue;
}
let result = match clause.outputs.as_slice() {
[] => "()".into(),
[slot] => format!("_v{slot}"),
slots => format!(
"({})",
slots
.iter()
.map(|slot| format!("_v{slot}"))
.collect::<Vec<_>>()
.join(", ")
),
};
writeln!(output, "return Ok({result});\n}}").expect("string formatting");
}
output.push_str("unreachable!(\"checked exhaustive clauses\")\n");
if transaction {
writeln!(
output,
"}}.await;\nif let Some(transaction) = transaction_owner {{\nreturn match transaction_result {{\nOk(value) => {{ transaction.commit().await.map_err(|error| AppError::database({}, error))?; Ok(value) }},\nErr(error) => match transaction.rollback().await {{ Ok(()) => Err(error), Err(rollback) => Err(error.cause(rollback)) }}\n}};\n}}\ntransaction_result",
self.location(function.span),
)
.expect("string formatting");
}
output.push_str("}\n");
}
fn invoke(
&self,
function: &Specialization,
mut arguments: String,
database: DatabaseContext,
) -> String {
if !specialize::database_effects(self.program, function).is_empty() {
if !arguments.is_empty() {
arguments.push_str(", ");
}
arguments.push_str(match database {
DatabaseContext::Inherit => "_database",
DatabaseContext::Detached => "None",
});
}
format!("{}({arguments})", self.names[function])
}
fn binding(&self, output: &mut String, binding: &hir::Binding, ty: &Type, unique: bool) {
let input = format!("_p{}", binding.input);
let value = match binding.projection {
Projection::Whole if unique && ty.movable(&self.program.types) => input.clone(),
Projection::Whole => format!("{input}.clone()"),
Projection::NonNull if unique && ty.movable(&self.program.types) => {
format!("{input}.expect(\"checked non-null clause\")")
}
Projection::NonNull => {
format!("{input}.as_ref().expect(\"checked non-null clause\").clone()")
}
Projection::Payload { variant, field } => {
if let Type::Related(_) = ty.base() {
debug_assert_eq!(variant, 1);
debug_assert_eq!(field, 0);
let input = if matches!(ty, Type::Nullable(_)) {
format!("{input}.as_ref().expect(\"checked non-null variant\")")
} else {
format!("&{input}")
};
writeln!(
output,
"_v{} = match {input} {{ dever_runtime::orm::Related::Loaded(payload) => payload.as_ref().clone(), _ => unreachable!(\"checked Related variant\") }};",
binding.slot,
)
.expect("string formatting");
return;
}
let Type::Named(id) = ty.base() else {
unreachable!()
};
let Shape::Choice(variants) = &self.program.types[*id].shape else {
unreachable!()
};
let fields = (0..variants[variant].fields.len())
.map(|index| if index == field { "payload" } else { "_" })
.collect::<Vec<_>>()
.join(", ");
let input = if matches!(ty, Type::Nullable(_)) {
format!("{input}.as_ref().expect(\"checked non-null variant\")")
} else {
format!("&{input}")
};
format!(
"match {input} {{ T{id}::V{variant}({fields}) => payload.clone(), _ => unreachable!(\"checked variant\") }}"
)
}
};
writeln!(output, "_v{} = {value};", binding.slot).expect("string formatting");
}
fn atom(&self, atom: &Atom, ty: &Type, value: &str) -> String {
if matches!(atom, Atom::Null) {
return format!("{value}.is_none()");
}
if let Type::Nullable(base) = ty {
return format!(
"{value}.as_ref().is_some_and(|_x| {})",
self.atom(atom, base, "(*_x)")
);
}
match atom {
Atom::Any => "true".into(),
Atom::Bool(boolean) => format!("{value} == {boolean}"),
Atom::Variant(variant) => {
if matches!(ty, Type::Related(_)) {
return format!(
"matches!({value}, dever_runtime::orm::Related::{})",
if *variant == 0 {
"Unloaded"
} else {
"Loaded(..)"
}
);
}
let Type::Named(id) = ty else { unreachable!() };
let Shape::Choice(variants) = &self.program.types[*id].shape else {
unreachable!()
};
format!(
"matches!({value}, T{id}::V{variant}{})",
if variants[*variant].fields.is_empty() {
""
} else {
"(..)"
}
)
}
Atom::Int(lo, hi) => range_guard(
value,
(*lo != i64::MIN).then(|| format!("{lo}i64")),
(*hi != i64::MAX).then(|| format!("{hi}i64")),
),
Atom::Decimal(lo, hi) => range_guard(
value,
(*lo != dever_runtime::number::DecimalValue::minimum())
.then(|| constant(&Constant::Decimal(*lo))),
(*hi != dever_runtime::number::DecimalValue::maximum())
.then(|| constant(&Constant::Decimal(*hi))),
),
Atom::Literal(literal) => format!("{value} == {}", typed_constant(literal, ty)),
Atom::Remainder(literals) => {
if literals.is_empty() {
"true".into()
} else {
literals
.iter()
.map(|literal| format!("{value} != {}", typed_constant(literal, ty)))
.collect::<Vec<_>>()
.join(" && ")
}
}
Atom::Null => unreachable!(),
}
}
fn field_type(&self, ty: &Type, index: usize) -> Type {
match ty {
Type::Named(id) => match &self.program.types[*id].shape {
Shape::Record(fields) => fields[index].ty.clone(),
_ => unreachable!(),
},
Type::Outputs(fields) => fields[index].ty.clone(),
Type::MapEntry(key, value) => {
if index == 0 {
*key.clone()
} else {
*value.clone()
}
}
_ => unreachable!("checked field access"),
}
}
fn failure(&self, expression: &str, span: Span) -> String {
format!(
"({expression}).map_err(|error| AppError::fault({}, error))?",
self.location(span)
)
}
fn database_failure(&self, expression: &str, span: Span) -> String {
format!(
"({expression}).map_err(|error| AppError::database({}, error))?",
self.location(span)
)
}
fn call_failure(&self, expression: &str, span: Span) -> String {
format!(
"({expression}).map_err(|error| error.at({}))?",
self.location(span)
)
}
fn location(&self, span: Span) -> String {
let source = self.sources.get(span.source);
let (line, column) = source.position(span.start);
let location = format!("{}:{line}:{column}", source.path().display());
rust_string(&location)
}
}
// 类型已保证整个 Int/有限 Decimal 域,只有分句实际缩小的边界才需运行时比较。
fn range_guard(value: &str, low: Option<String>, high: Option<String>) -> String {
let checks: Vec<_> = low
.into_iter()
.map(|low| format!("{value} >= {low}"))
.chain(high.into_iter().map(|high| format!("{value} <= {high}")))
.collect();
if checks.is_empty() {
"true".into()
} else {
checks.join(" && ")
}
}
fn optional_result(expression: &str, value: &Type) -> String {
if matches!(value, Type::Nullable(_)) {
format!("({expression}).flatten()")
} else {
expression.into()
}
}
fn rust_type(ty: &Type) -> String {
match ty {
Type::Unit => "()".into(),
Type::Bool => "bool".into(),
Type::Int => "i64".into(),
Type::Decimal => "DecimalValue".into(),
Type::Float => "f64".into(),
Type::Text => "String".into(),
Type::Id => "dever_runtime::Id".into(),
Type::Bytes => "Bytes".into(),
Type::Secret => "dever_runtime::secret::Secret".into(),
Type::Upload => "dever_runtime::api::upload::Upload".into(),
Type::Uuid => "dever_runtime::orm::Uuid".into(),
Type::Json => "String".into(),
Type::DateTime | Type::Date | Type::Time | Type::Duration => "i64".into(),
Type::File => "resource::FileHandle".into(),
Type::Socket => "dever_runtime::net::Socket".into(),
Type::Listener => "dever_runtime::net::Listener".into(),
Type::HttpReply => "dever_runtime::http::HttpReply".into(),
Type::WebSocket => "dever_runtime::websocket::WebSocket".into(),
Type::ClientTls => "dever_runtime::tls::ClientTls".into(),
Type::ServerTls => "dever_runtime::tls::ServerTls".into(),
Type::HttpClient => "dever_runtime::http::HttpClient".into(),
Type::Stream(element) => format!("resource::Stream<{}>", rust_type(element)),
Type::AsyncStream(element) => format!(
"dever_runtime::async_stream::AsyncStream<{}>",
rust_type(element)
),
Type::RowStream(element) => {
format!("dever_runtime::orm::RowStream<{}>", rust_type(element))
}
Type::Related(element) => format!("dever_runtime::orm::Related<{}>", rust_type(element)),
Type::Channel(element) => format!("channel::Channel<{}>", rust_type(element)),
Type::Task(outputs) => {
format!("task::Task<{}, AppError>", rust_type(&output_type(outputs)))
}
Type::Group => "task::Group<AppError>".into(),
Type::Named(id) => format!("T{id}"),
Type::Nullable(base) => format!("Option<{}>", rust_type(base)),
Type::List(base) => format!("List<{}>", rust_type(base)),
Type::Map(key, value) => format!("Map<{}, {}>", rust_type(key), rust_type(value)),
Type::MapEntry(key, value) => format!("MapEntry<{}, {}>", rust_type(key), rust_type(value)),
Type::Outputs(fields) => format!(
"({})",
fields
.iter()
.map(|field| rust_type(&field.ty))
.collect::<Vec<_>>()
.join(", ")
),
}
}
fn field_suffix(ty: &Type, index: usize) -> String {
match ty {
Type::Outputs(_) => format!(".{index}"),
Type::MapEntry(_, _) => {
if index == 0 {
".key".into()
} else {
".value".into()
}
}
_ => format!(".f{index}"),
}
}
fn constant(value: &Constant) -> String {
match value {
Constant::Null => "None".into(),
Constant::Bool(value) => value.to_string(),
Constant::Int(value) => format!("{value}i64"),
Constant::Decimal(value) => format!(
"DecimalValue::from_bytes({:?}).expect(\"checked Decimal literal\")",
value.to_bytes()
),
Constant::Float(value) => format!("f64::from_bits({}u64)", value.to_bits()),
Constant::Text(value) => format!("String::from({})", rust_string(value)),
}
}
fn typed_constant(value: &Constant, ty: &Type) -> String {
if let (Constant::Text(value), Type::Uuid) = (value, ty) {
let bytes = dever_runtime::orm::Uuid::parse(value)
.expect("checked Uuid literal")
.into_bytes();
format!("dever_runtime::orm::Uuid::from_bytes({bytes:?})")
} else {
constant(value)
}
}
fn rust_string(value: &str) -> String {
format!(
"\"{}\"",
value
.chars()
.flat_map(char::escape_default)
.collect::<String>()
)
}