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

+532
View File
@@ -0,0 +1,532 @@
use std::fmt::Write;
use crate::hir::{ApiRoute, Program};
use crate::specialize::{self, Specialization};
use crate::wire::{Node, Policy, Schema};
use super::rust_string;
pub(super) fn emit_routes(emitter: &super::Emitter<'_>, output: &mut String) {
let program = emitter.program;
emit_authorization(program, output);
let mut codecs = std::collections::BTreeMap::new();
let mut route_codecs = Vec::new();
for (route_index, route) in program.api_routes.iter().enumerate() {
let schema = Schema::from_field(
&program.functions[route.function].outputs[0],
&program.types,
Policy::Output,
)
.expect("checked API wire schema");
let next = codecs.len();
let index = *codecs
.entry(schema.fingerprint().to_owned())
.or_insert_with(|| {
output.push_str(&super::wire::emit(&schema, next));
next
});
route_codecs.push(index);
for (input_index, parameter) in program.functions[route.function]
.parameters
.iter()
.enumerate()
{
if parameter.value_type() == Some(&crate::types::Type::Upload) {
continue;
}
let schema = Schema::build(
parameter.value_type().expect("checked API input"),
&program.types,
Policy::ApiInput,
)
.expect("checked API input schema");
output.push_str(&super::wire::emit_named(
&schema,
&format!("api_input_{route_index}_{input_index}"),
));
}
}
if !program.api_rest.is_empty() {
super::rest::emit_helpers(program, output);
super::rest::emit_database_error(output);
}
emit_failure_mapper(program, output);
output.push_str("async fn api_route(mut request: dever_runtime::http::Request) -> Result<dever_runtime::http::Response, String> {\n");
output.push_str("let path = request.target.split('?').next().unwrap_or(\"\").to_owned();\nmatch (request.method.as_str(), path.as_str()) {\n");
for (route_index, (route, codec)) in program.api_routes.iter().zip(route_codecs).enumerate() {
writeln!(
output,
"({}, {}) => {{",
rust_string(route.method),
rust_string(&route.path)
)
.expect("string formatting");
emit_auth_begin(
program,
route.auth,
&route.directory,
route.anonymous,
output,
);
emit_components(&route.components, output);
emit_permission(program, route.permission, output);
emit_handler(program, route, route_index, codec, output);
emit_auth_end(route.auth, output);
output.push_str("},\n");
}
for rest in &program.api_rest {
for method in ["GET", "POST"] {
writeln!(
output,
"({}, {}) => {{",
rust_string(method),
rust_string(&rest.path)
)
.expect("string formatting");
emit_auth_begin(program, rest.auth, &rest.directory, false, output);
emit_components(&rest.components, output);
emit_permission(program, rest_permission(rest, method), output);
emit_read_body(output);
super::rest::emit_handler(emitter, rest, method, false, output);
emit_auth_end(rest.auth, output);
output.push_str("},\n");
}
}
output.push_str("(_, path) => {\n");
let mut paths = std::collections::BTreeMap::<&str, Vec<&str>>::new();
for route in &program.api_routes {
paths.entry(&route.path).or_default().push(route.method);
}
for rest in &program.api_rest {
paths.entry(&rest.path).or_default().extend(["GET", "POST"]);
}
for rest in &program.api_rest {
writeln!(
output,
"if path.starts_with({}) {{ return match request.method.as_str() {{",
rust_string(&format!("{}/", rest.path))
)
.expect("string formatting");
for method in ["GET", "PUT", "DELETE"] {
writeln!(output, "{} => {{", rust_string(method)).expect("string formatting");
emit_auth_begin(program, rest.auth, &rest.directory, false, output);
emit_components(&rest.components, output);
emit_permission(program, rest_permission(rest, method), output);
emit_read_body(output);
super::rest::emit_handler(emitter, rest, method, true, output);
emit_auth_end(rest.auth, output);
output.push_str("},\n");
}
output.push_str("_ => Ok(dever_runtime::api::method_not_allowed(&[\"GET\", \"PUT\", \"DELETE\"])),\n}; }\n");
}
output.push_str("match path {\n");
for (path, methods) in paths {
writeln!(
output,
"{} => Ok(dever_runtime::api::method_not_allowed(&{:?})),",
rust_string(path),
methods
)
.expect("string formatting");
}
output.push_str("_ => Ok(dever_runtime::api::not_found()),\n}\n}\n}\n}\n");
}
fn emit_authorization(program: &Program, output: &mut String) {
if program.permissions.is_empty() {
return;
}
output.push_str(
"static AUTHORIZATION_PERMISSIONS: &[dever_runtime::auth::store::Permission] = &[\n",
);
for permission in &program.permissions {
writeln!(output,
"dever_runtime::auth::store::Permission {{ key: {}, component: {}, domain: {}, site: {}, action: {}, method: {} }},",
rust_string(&permission.key),
rust_string(&permission.component),
rust_string(&permission.domain),
rust_string(&permission.site),
rust_string(&permission.action),
rust_string(permission.method),
).expect("string formatting");
}
output.push_str(r#"];
fn authorization_control_database() -> Result<dever_runtime::database::Database, dever_runtime::orm::Error> {
let settings = dever_runtime::config::settings();
let connection = settings.tenant().map(|tenant| tenant.database()).unwrap_or("default");
dever_runtime::database::database_for(Some(connection), "")
}
async fn authorization_initialize() -> Result<(), AppError> {
let database = authorization_control_database().map_err(|error| AppError::database("authorization initialization", error))?;
dever_runtime::auth::store::sync_catalog(database.clone(), AUTHORIZATION_PERMISSIONS).await.map_err(|error| AppError::database("permission catalog", error))?;
dever_runtime::auth::store::initialize_roles(database).await.map_err(|error| AppError::database("authorization roles", error))
}
async fn tenant_owner(tenant_id: i64, site: &str, user_id: i64) -> Result<(), AppError> {
let settings = dever_runtime::config::settings();
if !settings.sites().contains_key(site) { return Err(AppError::fault("tenant owner", format!("unknown site '{site}'"))); }
let tenant = settings.tenant().ok_or_else(|| AppError::fault("tenant owner", "tenant storage is not configured"))?;
let connection = tenant.database().to_owned();
dever_runtime::tenant::scope(tenant_id, async move {
let database = dever_runtime::tenant::database(&connection).await.map_err(|error| AppError::database("tenant owner", error))?;
dever_runtime::auth::store::initialize_roles(database.clone()).await.map_err(|error| AppError::database("tenant owner", error))?;
dever_runtime::auth::store::provision_owner(database, user_id, site).await.map_err(|error| AppError::database("tenant owner", error))?;
Ok(())
}).await
}
async fn api_require_components(components: &[&str]) -> Result<(), dever_runtime::auth::Error> {
if components.is_empty() { return Ok(()); }
let tenant_id = dever_runtime::auth::tenant_id()?.ok_or(dever_runtime::auth::Error::Forbidden)?;
match dever_runtime::tenant::require_components(tenant_id, components, TENANT_COMPONENTS).await {
Ok(()) => Ok(()),
Err(error) if error.kind() == dever_runtime::orm::ErrorKind::InvalidData => Err(dever_runtime::auth::Error::Forbidden),
Err(error) => { dever_runtime::log::error(error.to_string(), Vec::new()); Err(dever_runtime::auth::Error::Internal) }
}
}
async fn api_authorize(permission: &'static str, site: &'static str) -> Result<(), dever_runtime::auth::Error> {
dever_runtime::auth::authorize_permission(permission, site).await
}
"#);
}
fn emit_permission(program: &Program, permission: Option<usize>, output: &mut String) {
let Some(permission) = permission.map(|id| &program.permissions[id]) else {
return;
};
writeln!(output,
"if let Err(error) = api_authorize({}, {}).await {{ return Ok(dever_runtime::api::auth_error(error)); }};",
rust_string(&permission.key),
rust_string(&permission.site),
).expect("string formatting");
}
fn emit_components(components: &[String], output: &mut String) {
if components.is_empty() {
return;
}
writeln!(output,
"if let Err(error) = api_require_components(&[{}]).await {{ return Ok(dever_runtime::api::auth_error(error)); }};",
components.iter().map(|component| rust_string(component)).collect::<Vec<_>>().join(", "),
).expect("string formatting");
}
fn rest_permission(route: &crate::hir::ApiRest, method: &str) -> Option<usize> {
match method {
"GET" => route.permissions.read,
"POST" => route.permissions.create,
"PUT" => route.permissions.replace,
"DELETE" => route.permissions.delete,
_ => unreachable!("checked REST method"),
}
}
fn emit_failure_mapper(program: &Program, output: &mut String) {
output.push_str("fn api_failure_response(error: AppError) -> dever_runtime::http::Response {\nmatch &error.kind {\n");
if let Some((error_id, definition)) = program
.types
.iter()
.enumerate()
.find(|(_, definition)| definition.name == "dever.api.Error")
{
let crate::types::Shape::Choice(variants) = &definition.shape else {
unreachable!("official dever.api.Error is a choice")
};
let failures = program
.failures
.iter()
.flat_map(|failures| failures.iter())
.copied()
.collect::<std::collections::BTreeSet<_>>();
for (variant, status) in [
("Invalid", 400),
("Unauthorized", 401),
("Forbidden", 403),
("NotFound", 404),
("Conflict", 409),
("TooManyRequests", 429),
] {
let index = variants
.iter()
.position(|candidate| candidate.name == variant)
.expect("official dever.api.Error variant");
if failures.contains(&crate::hir::Failure {
ty: error_id,
variant: index,
}) {
writeln!(output, "AppErrorKind::E{error_id}V{index} => dever_runtime::api::standard_error({status}),")
.expect("string formatting");
}
}
}
output.push_str("_ => { dever_runtime::log::error(\"API handler failed\", Vec::new()); dever_runtime::api::internal_error() }\n}\n}\n");
}
fn emit_auth_begin(
program: &Program,
auth: Option<usize>,
directory: &[String],
anonymous: bool,
output: &mut String,
) {
let Some(auth) = auth else { return };
let hook = &program.auth[auth];
let directory = directory
.iter()
.map(|part| rust_string(part))
.collect::<Vec<_>>()
.join(", ");
writeln!(output,
"let auth_prepared = match dever_runtime::auth::prepare(&[{directory}], &request, {anonymous}) {{ Ok(prepared) => prepared, Err(error) => return Ok(dever_runtime::api::auth_error(error)), }};"
).expect("string formatting");
writeln!(output,
"let auth_identity = match auth_prepared.claims() {{\nSome(claims) => {{\nlet auth_claims = T{} {{ f0: claims.subject.clone(), f1: claims.session.clone(), f2: claims.tenant.clone(), f3: claims.site.clone() }};",
hook.claims,
).expect("string formatting");
let target = Specialization {
function: hook.function,
handlers: Vec::new(),
};
let mut arguments = vec!["auth_claims".to_owned()];
if !specialize::database_effects(program, &target).is_empty() {
arguments.push("None".to_owned());
}
let call = format!(
"f{}({}){}",
hook.function,
arguments.join(", "),
if specialize::suspends(program, &target) {
".await"
} else {
""
},
);
writeln!(output,
"let identity: T{} = match {call} {{ Ok(identity) => identity, Err(error) => return Ok(api_failure_response(error)), }};\nSome(dever_runtime::auth::Identity {{ id: identity.f0, user_id: identity.f1.map(|value| value.0), tenant_id: identity.f2.map(|value| value.0) }})\n}},\nNone => None,\n}};\nlet auth_response = dever_runtime::auth::scope(auth_prepared, auth_identity, async move {{",
hook.identity,
).expect("string formatting");
}
fn emit_auth_end(auth: Option<usize>, output: &mut String) {
if auth.is_some() {
output.push_str("}).await;\nmatch auth_response { Ok(response) => response, Err(error) => Ok(dever_runtime::api::auth_error(error)), }\n");
}
}
fn emit_handler(
program: &Program,
route: &ApiRoute,
route_index: usize,
codec: usize,
output: &mut String,
) {
let function = &program.functions[route.function];
if route.multipart {
output.push_str("let mut inputs = match dever_runtime::api::upload::read_multipart(&request).await { Ok(inputs) => inputs, Err(error) => return Ok(dever_runtime::api::invalid_input(error)), };\n");
} else {
emit_read_body(output);
output.push_str("let mut inputs = match dever_runtime::api::Inputs::from_request(&request) {\nOk(inputs) => inputs, Err(error) => return Ok(dever_runtime::api::invalid_input(error)),\n};\n");
}
for (index, (name, parameter)) in route.inputs.iter().zip(&function.parameters).enumerate() {
if parameter.value_type() == Some(&crate::types::Type::Upload) {
writeln!(output, "let p{index} = match inputs.take({}) {{ Ok(upload) => upload, Err(error) => return Ok(dever_runtime::api::invalid_input(error)), }};", rust_string(name)).expect("string formatting");
continue;
}
let schema = Schema::build(
parameter.value_type().expect("checked API value input"),
&program.types,
Policy::ApiInput,
)
.expect("checked API input schema");
let kind = schema.query_kind();
let read = if matches!(schema.nodes[schema.root], Node::Nullable(_)) {
"optional_json"
} else {
"json"
};
writeln!(output,
"let raw_{index} = match inputs.{read}({}, dever_runtime::api::QueryValue::{kind}) {{ Ok(value) => value, Err(error) => return Ok(dever_runtime::api::invalid_input(error)), }};\nlet p{index} = match api_input_{route_index}_{index}_decode(&raw_{index}) {{ Ok(value) => value, Err(error) => return Ok(dever_runtime::api::invalid_input(dever_runtime::api::InputError(error))), }};",
rust_string(name),
).expect("string formatting");
}
output.push_str("if let Err(error) = inputs.finish() { return Ok(dever_runtime::api::invalid_input(error)); }\n");
let target = Specialization {
function: route.function,
handlers: Vec::new(),
};
let password_crypto = specialize::uses_intrinsic(
program,
&target,
crate::intrinsic::Intrinsic::CryptoPasswordVerify,
) || specialize::uses_intrinsic(
program,
&target,
crate::intrinsic::Intrinsic::CryptoPasswordHash,
);
let transaction_database = (!password_crypto)
.then(|| super::transaction_database(program, route.function))
.flatten();
if let Some(database) = &transaction_database {
writeln!(output, "let transaction_database = match {database} {{ Ok(database) => database, Err(error) => {{ dever_runtime::log::error(error.to_string(), Vec::new()); return Ok(dever_runtime::api::internal_error()); }} }};\nlet transaction = match transaction_database.begin().await {{ Ok(value) => value, Err(error) => {{ dever_runtime::log::error(error.to_string(), Vec::new()); return Ok(dever_runtime::api::internal_error()); }} }};")
.expect("string formatting");
}
let mut arguments = (0..route.inputs.len())
.map(|index| format!("p{index}"))
.collect::<Vec<_>>();
if !specialize::database_effects(program, &target).is_empty() {
arguments.push(
if transaction_database.is_some() {
"Some(&transaction)"
} else {
"None"
}
.to_owned(),
);
}
let call = format!(
"f{}({}){}",
route.function,
arguments.join(", "),
if specialize::suspends(program, &target) {
".await"
} else {
""
}
);
let failure_mapper = "api_failure_response";
if transaction_database.is_some() {
writeln!(output, "let response = match {call} {{ Ok(value) => value, Err(error) => {{ if transaction.rollback().await.is_err() {{ dever_runtime::log::error(\"API rollback failed\", Vec::new()); }} return Ok({failure_mapper}(error)); }} }};")
.expect("string formatting");
} else {
writeln!(output, "let response = match {call} {{ Ok(value) => value, Err(error) => return Ok({failure_mapper}(error)), }};")
.expect("string formatting");
}
let conversion = format!("wire_{codec}_encode(&response)");
writeln!(output,
"let value = match {conversion} {{ Ok(value) => value, Err(_error) => {{ {} dever_runtime::log::error(\"API response serialization failed\", Vec::new()); return Ok(dever_runtime::api::internal_error()); }} }};",
if transaction_database.is_some() { "let _ = transaction.rollback().await;" } else { "" },
).expect("string formatting");
if transaction_database.is_some() {
output.push_str("if let Err(error) = transaction.commit().await { dever_runtime::log::error(error.to_string(), Vec::new()); return Ok(dever_runtime::api::internal_error()); }\n");
}
output.push_str("if let Err(error) = dever_runtime::api::commit_response_metadata() { dever_runtime::log::error(error, Vec::new()); return Ok(dever_runtime::api::internal_error()); }\nOk(dever_runtime::api::success(value))");
}
fn emit_read_body(output: &mut String) {
output.push_str("if let Err(error) = dever_runtime::api::upload::read_body(&mut request).await { return Ok(dever_runtime::api::invalid_input(error)); }\n");
}
pub(super) fn emit_commands(program: &Program, output: &mut String) {
if program.api_commands.is_empty() {
return;
}
for (command_index, command) in program.api_commands.iter().enumerate() {
let function = &program.functions[command.function];
let schema = Schema::from_field(&function.outputs[0], &program.types, Policy::Output)
.expect("checked CMD output schema");
output.push_str(&super::wire::emit_named(
&schema,
&format!("cmd_output_{command_index}"),
));
for (input_index, parameter) in function.parameters.iter().enumerate() {
let schema = Schema::build(
parameter.value_type().expect("checked CMD input"),
&program.types,
Policy::ApiInput,
)
.expect("checked CMD input schema");
output.push_str(&super::wire::emit_named(
&schema,
&format!("cmd_input_{command_index}_{input_index}"),
));
}
}
output.push_str("async fn cmd_dispatch(name: &str, body: &str) -> Result<dever_runtime::wire::Encoded, AppError> {\nmatch name {\n");
for (command_index, command) in program.api_commands.iter().enumerate() {
let function = &program.functions[command.function];
writeln!(output, "{} => {{", rust_string(&command.name)).expect("string formatting");
if !command.components.is_empty() {
writeln!(output,
"let tenant_id = dever_runtime::tenant::current_id().map_err(|error| AppError::database(\"command component\", error))?;\ndever_runtime::tenant::require_components(tenant_id, &[{}], TENANT_COMPONENTS).await.map_err(|error| AppError::database(\"command component\", error))?;",
command.components.iter().map(|component| rust_string(component)).collect::<Vec<_>>().join(", "),
).expect("string formatting");
}
output.push_str("let mut inputs = dever_runtime::api::Inputs::from_json_object(body).map_err(|error| AppError::fault(\"command\", error))?;\n");
for (input_index, name) in command.inputs.iter().enumerate() {
let parameter = &function.parameters[input_index];
let schema = Schema::build(
parameter.value_type().expect("checked CMD input"),
&program.types,
Policy::ApiInput,
)
.expect("checked CMD input schema");
let read = if matches!(schema.nodes[schema.root], Node::Nullable(_)) {
"optional_raw_json"
} else {
"raw_json"
};
let absent = if read == "optional_raw_json" {
".unwrap_or_else(|| \"null\".into())"
} else {
""
};
writeln!(output,
"let raw_{input_index} = inputs.{read}({}).map_err(|error| AppError::fault(\"command\", error))?{absent};\nlet p{input_index} = cmd_input_{command_index}_{input_index}_decode(&raw_{input_index}).map_err(|error| AppError::fault(\"command\", error))?;",
rust_string(name),
).expect("string formatting");
}
output.push_str("inputs.finish().map_err(|error| AppError::fault(\"command\", error))?;\n");
let target = Specialization {
function: command.function,
handlers: Vec::new(),
};
let transaction_database = super::transaction_database(program, command.function);
if let Some(database) = &transaction_database {
writeln!(output, "let transaction_database = ({database}).map_err(|error| AppError::database(\"command\", error))?;\nlet transaction = transaction_database.begin().await.map_err(|error| AppError::database(\"command\", error))?;")
.expect("string formatting");
}
let mut arguments = (0..command.inputs.len())
.map(|index| format!("p{index}"))
.collect::<Vec<_>>();
if !specialize::database_effects(program, &target).is_empty() {
arguments.push(
if transaction_database.is_some() {
"Some(&transaction)"
} else {
"None"
}
.into(),
);
}
writeln!(
output,
"let result = f{}({}){};",
command.function,
arguments.join(", "),
if specialize::suspends(program, &target) {
".await"
} else {
""
},
)
.expect("string formatting");
if transaction_database.is_some() {
output.push_str("let value = match result { Ok(value) => value, Err(error) => { return Err(match transaction.rollback().await { Ok(()) => error, Err(rollback) => error.cause(rollback) }); } };\n");
} else {
output.push_str("let value = result?;\n");
}
if transaction_database.is_some() {
writeln!(output, "let encoded = match cmd_output_{command_index}_encode(&value) {{ Ok(encoded) => encoded, Err(error) => {{ let rollback = transaction.rollback().await; return Err(match rollback {{ Ok(()) => AppError::fault(\"command\", error), Err(rollback) => AppError::fault(\"command\", error).cause(rollback) }}); }} }};")
.expect("string formatting");
} else {
writeln!(output, "let encoded = cmd_output_{command_index}_encode(&value).map_err(|error| AppError::fault(\"command\", error))?;")
.expect("string formatting");
}
if transaction_database.is_some() {
output.push_str("transaction.commit().await.map_err(|error| AppError::database(\"command\", error))?;\n");
}
output.push_str("Ok(encoded)\n");
output.push_str("},\n");
}
output.push_str(
"_ => Err(AppError::fault(\"command\", format!(\"unknown CMD '{name}'\"))),\n}\n}\n",
);
}
+607
View File
@@ -0,0 +1,607 @@
use std::collections::BTreeSet;
use std::ffi::OsStr;
use std::fs::{self, File, OpenOptions};
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::SystemTime;
use crate::hir::Program;
use crate::source::SourceMap;
mod cache;
mod cleanup;
use cleanup::{BuildDirectory, build_directory, build_directory_at, clean_stale_build_directories};
pub use cleanup::{CleanupSummary, clean_project_artifacts};
/// 单个生成源码统一优化,避免分区使标准集合的泛型内联取决于程序大小。
pub const OPTIMIZATION_ARGS: [&str; 8] = [
"-C",
"opt-level=3",
"-C",
"codegen-units=1",
"-C",
"lto=fat",
"-C",
"strip=symbols",
];
const TEST_COMPILATION_ARGS: [&str; 6] = [
"-C",
"opt-level=1",
"-C",
"codegen-units=16",
"-C",
"strip=symbols",
];
/// Owns the temporary build directory for as long as the executable is needed.
pub struct NativeProgram {
directory: BuildDirectory,
executable: PathBuf,
cache_hit: bool,
}
impl NativeProgram {
/// The authenticated compile client verifies the service response before
/// transferring these bytes into a caller-owned executable lifetime.
pub fn from_verified_bytes(bytes: &[u8]) -> Result<Self, String> {
let artifact = Self::new()?;
let mut options = OpenOptions::new();
options.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o700);
}
let mut output = options
.open(&artifact.executable)
.map_err(|error| format!("cannot create compiled executable: {error}"))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
output
.set_permissions(fs::Permissions::from_mode(0o700))
.map_err(|error| format!("cannot set compiled executable permissions: {error}"))?;
}
output
.write_all(bytes)
.and_then(|()| output.sync_all())
.map_err(|error| format!("cannot write compiled executable: {error}"))?;
Ok(artifact)
}
fn new() -> Result<Self, String> {
let directory = build_directory()
.map_err(|error| format!("cannot create native build directory: {error}"))?;
Ok(Self {
executable: directory.join(format!("program{}", std::env::consts::EXE_SUFFIX)),
directory: BuildDirectory(directory),
cache_hit: false,
})
}
pub fn executable(&self) -> &Path {
&self.executable
}
pub fn cache_hit(&self) -> bool {
self.cache_hit
}
/// Creates a new output file; existing files (including sources) are never replaced.
pub fn save(&self, path: &Path) -> io::Result<()> {
let mut input = File::open(&self.executable)?;
let mut output = OpenOptions::new().write(true).create_new(true).open(path)?;
let result = (|| {
io::copy(&mut input, &mut output)?;
output.set_permissions(input.metadata()?.permissions())?;
output.sync_all()
})();
if result.is_err() {
drop(output);
let _ = fs::remove_file(path);
}
result
}
}
/// Compile already identified inputs using the shared private artifact owner.
/// The callback receives the owned build directory and a new executable path.
/// It must link only the exact inputs represented by `identity`.
pub fn compile_artifact(
cache_directory: &Path,
identity: &str,
build: impl FnOnce(&Path, &Path) -> Result<(), String>,
) -> Result<NativeProgram, String> {
let _ = clean_stale_build_directories(SystemTime::now());
let cache = cache::Cache::for_artifact(cache_directory, identity);
compile_cached(&cache, cache_directory, |artifact| {
build(&artifact.directory.0, &artifact.executable)
})
}
fn compile_cached(
cache: &cache::Cache,
cache_directory: &Path,
build: impl FnOnce(&NativeProgram) -> Result<(), String>,
) -> Result<NativeProgram, String> {
let mut artifact = NativeProgram::new()?;
if cache
.restore(&artifact.executable)
.map_err(|error| format!("cannot restore native cache: {error}"))?
{
artifact.cache_hit = true;
return Ok(artifact);
}
build(&artifact)?;
publish_artifact(cache, cache_directory, &artifact)?;
Ok(artifact)
}
fn publish_artifact(
cache: &cache::Cache,
cache_directory: &Path,
artifact: &NativeProgram,
) -> Result<(), String> {
fs::create_dir_all(cache_directory)
.map_err(|error| format!("cannot create native cache: {error}"))?;
let staging = BuildDirectory(
build_directory_at(cache_directory)
.map_err(|error| format!("cannot stage native cache: {error}"))?,
);
cache
.publish(&artifact.executable, &staging.0)
.map_err(|error| format!("cannot publish native cache: {error}"))
}
pub fn compile(
program: &Program,
sources: &SourceMap,
entry: &str,
rustc: &OsStr,
) -> Result<NativeProgram, String> {
let workspace = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
compile_with_cache(
program,
sources,
entry,
rustc,
&workspace.join("target/native-artifacts"),
)
}
pub fn compile_application(
program: &Program,
sources: &SourceMap,
entry: &str,
rustc: &OsStr,
profile: dever_runtime::config::RuntimeProfile,
) -> Result<NativeProgram, String> {
let workspace = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
let entry = program.entry(entry)?;
compile_entry(
program,
sources,
entry,
rustc,
&workspace.join("target/native-artifacts"),
Some(profile),
&OPTIMIZATION_ARGS,
)
}
pub fn compile_project(
program: &Program,
sources: &SourceMap,
rustc: &OsStr,
profile: dever_runtime::config::RuntimeProfile,
) -> Result<NativeProgram, String> {
compile_project_with_resources(program, sources, rustc, profile, &[])
}
pub fn compile_project_with_resources(
program: &Program,
sources: &SourceMap,
rustc: &OsStr,
profile: dever_runtime::config::RuntimeProfile,
resources: &[crate::native::EmbeddedResource],
) -> Result<NativeProgram, String> {
let workspace = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
let generated = super::emit_project_with_resources(program, sources, profile, resources)?;
let api = !program.api_routes.is_empty() || !program.api_rest.is_empty();
compile_generated(
GeneratedProgram {
source: generated,
resources,
},
rustc,
&workspace.join("target/native-artifacts"),
Some(profile),
api,
&OPTIMIZATION_ARGS,
)
}
pub fn compile_test_suite(
program: &Program,
sources: &SourceMap,
rustc: &OsStr,
) -> Result<NativeProgram, String> {
let workspace = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
let profile = program
.tests()
.iter()
.any(crate::hir::TestCase::uses_database)
.then_some(dever_runtime::config::RuntimeProfile {
sqlite: true,
postgres: false,
});
let generated = super::emit_test_suite(program, sources, profile)?;
compile_generated(
GeneratedProgram {
source: generated,
resources: &[],
},
rustc,
&workspace.join("target/native-artifacts"),
profile,
false,
&TEST_COMPILATION_ARGS,
)
}
/// The explicit cache directory lets embedding callers and tests own their artifacts.
pub fn compile_with_cache(
program: &Program,
sources: &SourceMap,
entry: &str,
rustc: &OsStr,
cache_directory: &Path,
) -> Result<NativeProgram, String> {
let entry = program.entry(entry)?;
compile_entry(
program,
sources,
entry,
rustc,
cache_directory,
None,
&OPTIMIZATION_ARGS,
)
}
fn compile_entry(
program: &Program,
sources: &SourceMap,
entry: usize,
rustc: &OsStr,
cache_directory: &Path,
profile: Option<dever_runtime::config::RuntimeProfile>,
compilation_args: &[&str],
) -> Result<NativeProgram, String> {
let generated = super::emit_entry(program, sources, entry, profile)?;
let api = super::api_enabled(program, entry)?;
compile_generated(
GeneratedProgram {
source: generated,
resources: &[],
},
rustc,
cache_directory,
profile,
api,
compilation_args,
)
}
struct GeneratedProgram<'a> {
source: String,
resources: &'a [crate::native::EmbeddedResource],
}
fn compile_generated(
generated: GeneratedProgram<'_>,
rustc: &OsStr,
cache_directory: &Path,
profile: Option<dever_runtime::config::RuntimeProfile>,
api: bool,
compilation_args: &[&str],
) -> Result<NativeProgram, String> {
// Stale cleanup is best effort and must never replace the current compile result.
let _ = clean_stale_build_directories(SystemTime::now());
let version = compiler_version(rustc)?;
// Emitted code has already pruned unreachable functions and includes direct
// system intrinsics as well as official source wrappers.
let crypto = generated.source.contains("dever_runtime::crypto::");
let wire = generated.source.contains("dever_runtime::wire::")
|| generated.source.contains("initialize_ports");
let external = generated.source.contains("dever_runtime::external::");
let runtime = runtime_library(rustc, profile, api, crypto, wire, external)?;
let cache = cache::Cache::new(
cache_directory,
&generated.source,
rustc,
&version,
&runtime,
compilation_args,
)
.map_err(|error| format!("cannot identify native build inputs: {error}"))?;
compile_cached(&cache, cache_directory, |artifact| {
compile_executable(artifact, &generated, &runtime, rustc, compilation_args)
})
}
fn compile_executable(
artifact: &NativeProgram,
generated: &GeneratedProgram<'_>,
runtime: &cache::RuntimeInputs,
rustc: &OsStr,
compilation_args: &[&str],
) -> Result<(), String> {
let runtime_directory = artifact.directory.0.join("runtime");
runtime
.write(&runtime_directory)
.map_err(|error| format!("cannot stage native runtime: {error}"))?;
let source = artifact.directory.0.join("program.rs");
// Raw resources are separate compiler inputs. Expanding an interpreter
// into decimal Rust literals multiplies source size and allocator use.
for (index, resource) in super::resource_inputs(generated.resources)
.unique
.iter()
.enumerate()
{
let path = artifact
.directory
.0
.join(super::resource_source_filename(index));
let mut options = fs::OpenOptions::new();
options.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600);
}
let mut file = options
.open(path)
.map_err(|error| format!("cannot stage external resource: {error}"))?;
file.write_all(&resource.bytes)
.map_err(|error| format!("cannot stage external resource: {error}"))?;
}
fs::write(&source, &generated.source)
.map_err(|error| format!("cannot write generated program: {error}"))?;
let result = Command::new(rustc)
.arg("--edition=2024")
.arg("--crate-name=dever_program")
.args(compilation_args)
.arg("--extern")
.arg(format!(
"dever_runtime={}",
runtime_directory.join("libdever_runtime.rlib").display()
))
.arg("-L")
.arg(format!(
"dependency={}",
runtime_directory.join("deps").display()
))
.arg(&source)
.arg("-o")
.arg(&artifact.executable)
.output()
.map_err(|error| {
format!(
"cannot invoke rustc '{}': {error}; set RUSTC or add rustc to PATH",
rustc.to_string_lossy()
)
})?;
if !result.status.success() {
return Err(format!(
"native compiler failed ({}):\n{}",
result.status,
String::from_utf8_lossy(&result.stderr)
));
}
Ok(())
}
fn compiler_version(rustc: &OsStr) -> Result<Vec<u8>, String> {
let result = Command::new(rustc)
.arg("--version")
.arg("--verbose")
.output()
.map_err(|error| {
format!(
"cannot invoke rustc '{}': {error}; set RUSTC or add rustc to PATH",
rustc.to_string_lossy()
)
})?;
if !result.status.success() {
return Err(format!(
"cannot query rustc version ({}): {}",
result.status,
String::from_utf8_lossy(&result.stderr)
));
}
Ok(result.stdout)
}
fn runtime_library(
rustc: &OsStr,
profile: Option<dever_runtime::config::RuntimeProfile>,
api: bool,
crypto: bool,
wire: bool,
external: bool,
) -> Result<cache::RuntimeInputs, String> {
let workspace = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
let runtime_root = workspace.join("target/native-runtime");
let target = runtime_root.join("cargo");
let cargo = std::env::var_os("CARGO").unwrap_or_else(|| {
Path::new(rustc)
.parent()
.filter(|parent| !parent.as_os_str().is_empty())
.map(|parent| {
parent
.join(format!("cargo{}", std::env::consts::EXE_SUFFIX))
.into_os_string()
})
.unwrap_or_else(|| "cargo".into())
});
let mut command = Command::new(cargo);
command.args([
"build",
"--offline",
"--locked",
"--release",
"--message-format=json-render-diagnostics",
"-p",
"dever-runtime",
]);
let mut features = Vec::new();
if let Some(profile) = profile {
features.push(match (profile.sqlite, profile.postgres) {
(true, true) => "sqlite,postgres",
(true, false) => "sqlite",
(false, true) => "postgres",
(false, false) => "database",
});
}
if api {
features.push("api");
}
if crypto {
features.push("crypto");
}
if wire {
features.push("wire");
}
if external {
features.push("external");
}
if !features.is_empty() {
command.args(["--features", &features.join(",")]);
}
let result = command
.arg("--manifest-path")
.arg(workspace.join("Cargo.toml"))
.arg("--target-dir")
.arg(&target)
.env("RUSTC", rustc)
.output()
.map_err(|error| format!("cannot prepare native runtime: {error}"))?;
if !result.status.success() {
return Err(format!(
"native runtime compilation failed ({}):\n{}",
result.status,
String::from_utf8_lossy(&result.stderr)
));
}
let artifacts = cargo_runtime_artifacts(
&result.stdout,
&target,
&workspace.join("crates/dever-runtime/Cargo.toml"),
)?;
cache::RuntimeInputs::publish(
&runtime_root.join("inputs"),
&artifacts.runtime,
&artifacts.dependencies,
)
.map_err(|error| format!("cannot publish native runtime inputs: {error}"))
}
struct CargoRuntimeArtifacts {
runtime: PathBuf,
dependencies: Vec<PathBuf>,
}
fn cargo_runtime_artifacts(
bytes: &[u8],
target: &Path,
runtime_manifest: &Path,
) -> Result<CargoRuntimeArtifacts, String> {
let dependency_root = fs::canonicalize(target.join("release/deps"))
.map_err(|error| format!("cannot locate Cargo runtime dependencies: {error}"))?;
let runtime_manifest = fs::canonicalize(runtime_manifest)
.map_err(|error| format!("cannot locate dever-runtime manifest: {error}"))?;
let mut runtime_candidates = BTreeSet::new();
let mut dependencies = BTreeSet::new();
for line in bytes
.split(|byte| *byte == b'\n')
.filter(|line| !line.is_empty())
{
let message: serde_json::Value = serde_json::from_slice(line)
.map_err(|error| format!("invalid Cargo artifact message: {error}"))?;
if message.get("reason").and_then(serde_json::Value::as_str) != Some("compiler-artifact") {
continue;
}
let target_name = message
.pointer("/target/name")
.and_then(serde_json::Value::as_str);
let is_runtime = target_name == Some("dever_runtime")
&& message
.get("manifest_path")
.and_then(serde_json::Value::as_str)
.and_then(|path| fs::canonicalize(path).ok())
.is_some_and(|path| path == runtime_manifest);
let Some(filenames) = message
.get("filenames")
.and_then(serde_json::Value::as_array)
else {
continue;
};
for filename in filenames.iter().filter_map(serde_json::Value::as_str) {
let path = fs::canonicalize(filename).map_err(|error| {
format!("cannot inspect Cargo runtime artifact '{filename}': {error}")
})?;
let extension = path.extension().and_then(OsStr::to_str);
if is_runtime
&& extension == Some("rmeta")
&& path.parent() == Some(dependency_root.as_path())
{
let hashed_runtime = path.with_extension("rlib");
if hashed_runtime.is_file() {
runtime_candidates.insert(fs::canonicalize(&hashed_runtime).map_err(
|error| {
format!(
"cannot inspect Cargo runtime artifact '{}': {error}",
hashed_runtime.display(),
)
},
)?);
}
}
if !matches!(extension, Some("rlib" | "so" | "dylib" | "dll")) {
continue;
}
if is_runtime && extension == Some("rlib") {
runtime_candidates.insert(path.clone());
}
if path.parent() == Some(dependency_root.as_path()) {
dependencies.insert(path);
}
}
}
let hashed_runtime = runtime_candidates
.iter()
.filter(|path| path.parent() == Some(dependency_root.as_path()))
.cloned()
.collect::<Vec<_>>();
let runtime = match hashed_runtime.as_slice() {
[runtime] => runtime.clone(),
[] => match runtime_candidates
.into_iter()
.collect::<Vec<_>>()
.as_slice()
{
[runtime] => runtime.clone(),
[] => return Err("Cargo did not report the dever-runtime library artifact".into()),
_ => return Err("Cargo reported multiple dever-runtime library artifacts".into()),
},
_ => return Err("Cargo reported multiple hashed dever-runtime library artifacts".into()),
};
dependencies.remove(&runtime);
Ok(CargoRuntimeArtifacts {
runtime,
dependencies: dependencies.into_iter().collect(),
})
}
+478
View File
@@ -0,0 +1,478 @@
use std::ffi::OsStr;
use std::fs::{self, File};
use std::hash::{Hash, Hasher};
use std::io::{self, Read};
use std::path::{Path, PathBuf};
use std::sync::{
OnceLock,
atomic::{AtomicU64, Ordering},
};
use sha2::{Digest, Sha256};
static NEXT_RUNTIME_IDENTITY: AtomicU64 = AtomicU64::new(0);
/// Cargo may rebuild its shared target after returning. Compile against the exact
/// bytes used for the cache identity rather than mutable files in that target.
pub(super) struct RuntimeInputs {
files: RuntimeFiles,
identity: String,
}
enum RuntimeFiles {
Snapshot(Vec<(PathBuf, Vec<u8>)>),
Unchanged { root: PathBuf, paths: Vec<PathBuf> },
}
impl RuntimeInputs {
pub(super) fn publish(
root: &Path,
runtime: &Path,
dependencies: &[PathBuf],
) -> io::Result<Self> {
fs::create_dir_all(root)?;
let staging = unique_directory(root, ".dever-runtime-input")?;
let result = (|| {
fs::create_dir(staging.join("deps"))?;
link_or_copy(runtime, &staging.join("libdever_runtime.rlib"))?;
for dependency in dependencies {
let name = dependency.file_name().ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
"runtime dependency has no filename",
)
})?;
link_or_copy(dependency, &staging.join("deps").join(name))?;
}
let staged = Self::read(&staging)?;
let destination = root.join(&staged.identity);
match fs::rename(&staging, &destination) {
Ok(()) => Ok(Self::read(&destination)?),
Err(_error) if is_real_directory(&destination) => {
let existing = Self::read(&destination)?;
if existing.identity != staged.identity {
return Err(io::Error::other("native runtime input identity collision"));
}
Ok(existing)
}
Err(error) => Err(error),
}
})();
if staging.exists() {
let _ = fs::remove_dir_all(&staging);
}
result
}
pub(super) fn read(root: &Path) -> io::Result<Self> {
validate_runtime_root(root)?;
let mut paths = vec![PathBuf::from("libdever_runtime.rlib")];
for entry in fs::read_dir(root.join("deps"))? {
let path = entry?.path();
paths.push(Path::new("deps").join(path.file_name().expect("dependency filename")));
}
paths.sort();
let metadata = paths
.iter()
.map(|path| {
let metadata = fs::metadata(root.join(path))?;
Ok((path.clone(), metadata.len(), metadata.modified()?))
})
.collect::<io::Result<Vec<_>>>()?;
let metadata_identity = fingerprint(&metadata);
if let Some(identity) = read_runtime_identity(root, &metadata_identity) {
cleanup_runtime_identities(root, &metadata_identity);
return Ok(Self {
files: RuntimeFiles::Unchanged {
root: root.to_path_buf(),
paths,
},
identity,
});
}
let files = read_files(root, &paths)?;
let identity = fingerprint(&files);
write_runtime_identity(root, &metadata_identity, &identity);
Ok(Self {
files: RuntimeFiles::Snapshot(files),
identity,
})
}
pub(super) fn write(&self, root: &Path) -> io::Result<()> {
fs::create_dir(root)?;
fs::create_dir(root.join("deps"))?;
match &self.files {
RuntimeFiles::Snapshot(files) => write_files(root, files),
RuntimeFiles::Unchanged {
root: source_root,
paths,
} => {
let files = read_files(source_root, paths)?;
if fingerprint(&files) != self.identity {
return Err(io::Error::other(
"native runtime changed after its cache identity was read",
));
}
write_files(root, &files)
}
}
}
}
fn validate_runtime_root(root: &Path) -> io::Result<()> {
let metadata = fs::symlink_metadata(root)?;
if metadata.file_type().is_symlink() || !metadata.is_dir() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"native runtime input '{}' is not a real directory",
root.display()
),
));
}
for entry in fs::read_dir(root)? {
let entry = entry?;
let name = entry.file_name();
let name = name.to_string_lossy();
let metadata = fs::symlink_metadata(entry.path())?;
let valid = match name.as_ref() {
"libdever_runtime.rlib" => metadata.is_file() && !metadata.file_type().is_symlink(),
"deps" => metadata.is_dir() && !metadata.file_type().is_symlink(),
name if name.starts_with(".dever-runtime-identity-") => {
metadata.is_file() && !metadata.file_type().is_symlink()
}
_ => false,
};
if !valid {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"unexpected native runtime input '{}'",
entry.path().display()
),
));
}
}
let runtime = root.join("libdever_runtime.rlib");
let metadata = fs::symlink_metadata(&runtime)?;
if metadata.file_type().is_symlink() || !metadata.is_file() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"native runtime input '{}' is not a regular file",
runtime.display()
),
));
}
for entry in fs::read_dir(root.join("deps"))? {
let entry = entry?;
let path = entry.path();
let metadata = fs::symlink_metadata(&path)?;
let supported = path.extension().is_some_and(|extension| {
matches!(extension.to_str(), Some("rlib" | "so" | "dylib" | "dll"))
});
if metadata.file_type().is_symlink() || !metadata.is_file() || !supported {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("unexpected native runtime dependency '{}'", path.display()),
));
}
}
Ok(())
}
fn is_real_directory(path: &Path) -> bool {
fs::symlink_metadata(path)
.is_ok_and(|metadata| metadata.is_dir() && !metadata.file_type().is_symlink())
}
fn unique_directory(root: &Path, prefix: &str) -> io::Result<PathBuf> {
loop {
let path = root.join(format!(
"{prefix}-{}-{}",
std::process::id(),
NEXT_RUNTIME_IDENTITY.fetch_add(1, Ordering::Relaxed),
));
match fs::create_dir(&path) {
Ok(()) => return Ok(path),
Err(error) if error.kind() == io::ErrorKind::AlreadyExists => continue,
Err(error) => return Err(error),
}
}
}
fn link_or_copy(source: &Path, destination: &Path) -> io::Result<()> {
let metadata = fs::symlink_metadata(source)?;
if metadata.file_type().is_symlink() || !metadata.is_file() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"native runtime input '{}' is not a regular file",
source.display()
),
));
}
match fs::hard_link(source, destination) {
Ok(()) => Ok(()),
Err(_) => {
fs::copy(source, destination)?;
Ok(())
}
}
}
fn read_files(root: &Path, paths: &[PathBuf]) -> io::Result<Vec<(PathBuf, Vec<u8>)>> {
paths
.iter()
.map(|path| Ok((path.clone(), fs::read(root.join(path))?)))
.collect()
}
fn write_files(root: &Path, files: &[(PathBuf, Vec<u8>)]) -> io::Result<()> {
for (path, bytes) in files {
fs::write(root.join(path), bytes)?;
}
Ok(())
}
fn read_runtime_identity(root: &Path, metadata_identity: &str) -> Option<String> {
let text =
fs::read_to_string(root.join(format!(".dever-runtime-identity-{metadata_identity}")))
.ok()?;
let mut lines = text.lines();
if lines.next()? != "dever-runtime-identity-v1" || lines.next()? != metadata_identity {
return None;
}
let identity = lines.next()?;
if lines.next().is_some()
|| identity.len() != 64
|| !identity.bytes().all(|byte| byte.is_ascii_hexdigit())
{
return None;
}
Some(identity.to_owned())
}
fn write_runtime_identity(root: &Path, metadata_identity: &str, identity: &str) {
let path = root.join(format!(".dever-runtime-identity-{metadata_identity}"));
let staging = root.join(format!(
".dever-runtime-identity-{}-{}",
std::process::id(),
NEXT_RUNTIME_IDENTITY.fetch_add(1, Ordering::Relaxed),
));
if fs::write(
&staging,
format!("dever-runtime-identity-v1\n{metadata_identity}\n{identity}\n"),
)
.is_ok()
{
let _ = fs::rename(&staging, &path);
}
let _ = fs::remove_file(staging);
cleanup_runtime_identities(root, metadata_identity);
}
fn cleanup_runtime_identities(root: &Path, metadata_identity: &str) {
let current = format!(".dever-runtime-identity-{metadata_identity}");
if let Ok(entries) = fs::read_dir(root) {
for entry in entries.filter_map(Result::ok) {
let name = entry.file_name();
let name = name.to_string_lossy();
let stored_identity = name.strip_prefix(".dever-runtime-identity-");
if name != current
&& stored_identity.is_some_and(|identity| {
matches!(identity.len(), 32 | 64)
&& identity.bytes().all(|byte| byte.is_ascii_hexdigit())
})
{
let _ = fs::remove_file(entry.path());
}
}
}
}
pub(super) struct Cache {
directory: PathBuf,
identity: String,
}
impl Cache {
pub(super) fn for_artifact(root: &Path, identity: &str) -> Self {
Self {
directory: root.join(fingerprint(&identity)),
identity: identity.to_owned(),
}
}
pub(super) fn new(
root: &Path,
generated: &str,
rustc: &OsStr,
version: &[u8],
runtime: &RuntimeInputs,
options: &[&str],
) -> io::Result<Self> {
static COMPILER: OnceLock<String> = OnceLock::new();
let compiler = match COMPILER.get() {
Some(identity) => identity.clone(),
None => {
// Generated source already captures every backend input. Partition
// compiler builds by their file identity without rehashing a large
// debug compiler binary on every separate CLI invocation.
let path = std::env::current_exe()?;
let metadata = fs::metadata(&path)?;
let identity = fingerprint(&(
env!("CARGO_PKG_VERSION"),
path,
metadata.len(),
metadata.modified()?,
));
COMPILER.get_or_init(|| identity).clone()
}
};
let tool = executable_path(rustc)?;
let environment: Vec<_> = [
"PATH",
"RUSTUP_TOOLCHAIN",
"RUSTC_BOOTSTRAP",
"RUSTFLAGS",
"CARGO_ENCODED_RUSTFLAGS",
"CARGO_BUILD_TARGET",
"RUSTC_WRAPPER",
"RUSTC_WORKSPACE_WRAPPER",
"CC",
"CFLAGS",
"LDFLAGS",
"LIBRARY_PATH",
"LD_LIBRARY_PATH",
"SDKROOT",
"MACOSX_DEPLOYMENT_TARGET",
"SOURCE_DATE_EPOCH",
]
.into_iter()
.map(|name| (name, std::env::var_os(name)))
.collect();
let identity = format!(
"dever-native-cache-v1\ncompiler={compiler}\ngenerated={}\ntool={}\nversion={}\nruntime={}\noptions={}\nplatform={}-{}\nenvironment={}\n",
fingerprint(&generated),
fingerprint(&(rustc, tool.as_os_str(), file_fingerprint(&tool)?)),
fingerprint(&version),
runtime.identity,
fingerprint(&options),
std::env::consts::OS,
std::env::consts::ARCH,
fingerprint(&environment),
);
Ok(Self {
directory: root.join(fingerprint(&identity)),
identity,
})
}
pub(super) fn restore(&self, executable: &Path) -> io::Result<bool> {
let stored = match fs::read_to_string(self.directory.join("identity")) {
Ok(identity) => identity,
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(false),
Err(error) => return Err(error),
};
if stored != self.identity {
return Err(io::Error::other("native cache identity mismatch"));
}
let program = self.directory.join("program");
let expected = fs::read_to_string(self.directory.join("executable-fingerprint"))?;
if file_fingerprint(&program)? != expected {
return Err(io::Error::other(
"native cache executable is incomplete or modified",
));
}
fs::copy(program, executable)?;
Ok(true)
}
pub(super) fn publish(&self, executable: &Path, staging: &Path) -> io::Result<()> {
fs::copy(executable, staging.join("program"))?;
fs::write(staging.join("identity"), &self.identity)?;
fs::write(
staging.join("executable-fingerprint"),
file_fingerprint(executable)?,
)?;
match fs::rename(staging, &self.directory) {
Ok(()) => Ok(()),
// Another complete build won the same key. Incomplete staging directories
// never have a cache key and are never considered by restore.
Err(error) => match fs::read_to_string(self.directory.join("identity")) {
Ok(identity) if identity == self.identity => Ok(()),
Ok(_) => Err(io::Error::other("native cache identity collision")),
Err(_) => Err(error),
},
}
}
}
fn executable_path(command: &OsStr) -> io::Result<PathBuf> {
let path = Path::new(command);
if path.components().count() > 1 {
return fs::canonicalize(path);
}
for directory in std::env::split_paths(&std::env::var_os("PATH").unwrap_or_default()) {
let path = directory.join(command);
if path.is_file() {
return fs::canonicalize(path);
}
#[cfg(windows)]
{
let path = path.with_extension("exe");
if path.is_file() {
return fs::canonicalize(path);
}
}
}
Err(io::Error::new(
io::ErrorKind::NotFound,
"compiler executable not found",
))
}
fn fingerprint(value: &impl Hash) -> String {
let mut hasher = CryptographicHasher(Sha256::new());
value.hash(&mut hasher);
hex(&hasher.0.finalize())
}
fn file_fingerprint(path: &Path) -> io::Result<String> {
let mut file = File::open(path)?;
let mut hasher = Sha256::new();
let mut bytes = [0_u8; 65536];
loop {
let length = file.read(&mut bytes)?;
if length == 0 {
break;
}
hasher.update(&bytes[..length]);
}
Ok(hex(&hasher.finalize()))
}
struct CryptographicHasher(Sha256);
impl Hasher for CryptographicHasher {
fn finish(&self) -> u64 {
let digest = self.0.clone().finalize();
u64::from_le_bytes(digest[..8].try_into().expect("SHA-256 prefix"))
}
fn write(&mut self, bytes: &[u8]) {
self.0.update(bytes);
}
}
fn hex(bytes: &[u8]) -> String {
const DIGITS: &[u8; 16] = b"0123456789abcdef";
let mut output = String::with_capacity(bytes.len() * 2);
for byte in bytes {
output.push(DIGITS[usize::from(byte >> 4)] as char);
output.push(DIGITS[usize::from(byte & 0x0f)] as char);
}
output
}
@@ -0,0 +1,343 @@
use std::fs::{self, OpenOptions};
use std::io;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
static NEXT_BUILD: AtomicU64 = AtomicU64::new(0);
const BUILD_DIRECTORY_PREFIX: &str = "dever-build-";
const BUILD_MARKER: &str = ".dever-owner";
const BUILD_MARKER_VERSION: &str = "dever-build-owner-v1";
const STALE_BUILD_AGE: Duration = Duration::from_secs(24 * 60 * 60);
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct CleanupSummary {
pub directories: u64,
pub files: u64,
pub bytes: u64,
}
pub(super) struct BuildDirectory(pub(super) PathBuf);
impl Drop for BuildDirectory {
fn drop(&mut self) {
// Cleanup cannot replace a compilation/runtime result with a second error.
let _ = fs::remove_dir_all(&self.0);
}
}
pub(super) fn build_directory() -> io::Result<PathBuf> {
let path = build_directory_at(&std::env::temp_dir())?;
if let Err(error) = write_build_marker(&path, SystemTime::now()) {
let _ = fs::remove_dir_all(&path);
return Err(error);
}
Ok(path)
}
pub(super) fn build_directory_at(parent: &Path) -> io::Result<PathBuf> {
loop {
let id = NEXT_BUILD.fetch_add(1, Ordering::Relaxed);
let path = parent.join(format!(
"{BUILD_DIRECTORY_PREFIX}{}-{id}",
std::process::id()
));
let mut builder = fs::DirBuilder::new();
#[cfg(unix)]
{
use std::os::unix::fs::DirBuilderExt;
builder.mode(0o700);
}
match builder.create(&path) {
Ok(()) => return Ok(path),
Err(error) if error.kind() == io::ErrorKind::AlreadyExists => continue,
Err(error) => return Err(error),
}
}
}
pub fn clean_project_artifacts(project_root: &Path) -> io::Result<CleanupSummary> {
let now = SystemTime::now();
let mut summary = clean_stale_build_directories(now)?;
let owner = current_owner_id(&std::env::temp_dir())?;
let entries = match fs::read_dir(project_root) {
Ok(entries) => entries,
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(summary),
Err(error) => return Err(error),
};
for entry in entries {
let Ok(entry) = entry else {
continue;
};
let name = entry.file_name();
let Some(pid) = name.to_str().and_then(legacy_run_pid) else {
continue;
};
if pid == u64::from(std::process::id()) {
continue;
}
let Ok(metadata) = fs::symlink_metadata(entry.path()) else {
continue;
};
if metadata.file_type().is_symlink()
|| !metadata.is_file()
|| !owner_matches(&metadata, owner)
{
continue;
}
let Ok(modified) = metadata.modified() else {
continue;
};
if !is_stale(modified, now) {
continue;
}
if process_is_active(pid) {
continue;
}
let bytes = metadata.len();
fs::remove_file(entry.path())?;
summary.files += 1;
summary.bytes += bytes;
}
Ok(summary)
}
pub(super) fn clean_stale_build_directories(now: SystemTime) -> io::Result<CleanupSummary> {
let root = std::env::temp_dir();
let owner = current_owner_id(&root)?;
let mut summary = CleanupSummary::default();
for entry in fs::read_dir(&root)? {
let Ok(entry) = entry else {
continue;
};
let name = entry.file_name();
let Some(pid) = name.to_str().and_then(build_directory_pid) else {
continue;
};
if pid == u64::from(std::process::id()) {
continue;
}
let path = entry.path();
let Ok(metadata) = fs::symlink_metadata(&path) else {
continue;
};
if metadata.file_type().is_symlink()
|| !metadata.is_dir()
|| !owner_matches(&metadata, owner)
{
continue;
}
let Ok(Some(marker)) = read_build_marker(&path) else {
continue;
};
if marker.pid != pid
|| marker.owner != owner
|| !is_stale(marker.created, now)
|| process_is_active(pid)
{
continue;
}
let Ok(Some((files, bytes))) = directory_usage(&path) else {
continue;
};
fs::remove_dir_all(&path)?;
summary.directories += 1;
summary.files += files;
summary.bytes += bytes;
}
Ok(summary)
}
struct BuildMarker {
created: SystemTime,
pid: u64,
owner: u64,
}
fn write_build_marker(path: &Path, created: SystemTime) -> io::Result<()> {
let seconds = created
.duration_since(UNIX_EPOCH)
.map_err(|_| io::Error::other("system clock is before the Unix epoch"))?
.as_secs();
let owner = metadata_owner(&fs::metadata(path)?);
fs::write(
path.join(BUILD_MARKER),
format!(
"{BUILD_MARKER_VERSION}\ncreated={seconds}\npid={}\nowner={owner}\ncompiler={}\n",
std::process::id(),
env!("CARGO_PKG_VERSION"),
),
)
}
fn read_build_marker(path: &Path) -> io::Result<Option<BuildMarker>> {
let marker_path = path.join(BUILD_MARKER);
let metadata = match fs::symlink_metadata(&marker_path) {
Ok(metadata) => metadata,
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None),
Err(error) => return Err(error),
};
if metadata.file_type().is_symlink() || !metadata.is_file() {
return Ok(None);
}
let text = fs::read_to_string(marker_path)?;
let mut lines = text.lines();
if lines.next() != Some(BUILD_MARKER_VERSION) {
return Ok(None);
}
let Some(created) = parse_marker_number(lines.next(), "created") else {
return Ok(None);
};
let Some(pid) = parse_marker_number(lines.next(), "pid") else {
return Ok(None);
};
let Some(owner) = parse_marker_number(lines.next(), "owner") else {
return Ok(None);
};
let compiler = lines.next().and_then(|line| line.strip_prefix("compiler="));
if compiler != Some(env!("CARGO_PKG_VERSION")) || lines.next().is_some() {
return Ok(None);
}
Ok(Some(BuildMarker {
created: UNIX_EPOCH + Duration::from_secs(created),
pid,
owner,
}))
}
fn parse_marker_number(line: Option<&str>, key: &str) -> Option<u64> {
line.and_then(|line| line.strip_prefix(key))
.and_then(|line| line.strip_prefix('='))
.and_then(|value| value.parse().ok())
}
fn build_directory_pid(name: &str) -> Option<u64> {
let suffix = name.strip_prefix(BUILD_DIRECTORY_PREFIX)?;
let (pid, id) = suffix.split_once('-')?;
if id.is_empty() || !id.bytes().all(|byte| byte.is_ascii_digit()) {
return None;
}
pid.parse().ok()
}
fn legacy_run_pid(name: &str) -> Option<u64> {
let pid = name.strip_prefix(".dever-run-")?;
if pid.is_empty() || !pid.bytes().all(|byte| byte.is_ascii_digit()) {
return None;
}
pid.parse().ok()
}
fn is_stale(created: SystemTime, now: SystemTime) -> bool {
now.duration_since(created)
.is_ok_and(|age| age >= STALE_BUILD_AGE)
}
#[cfg(target_os = "linux")]
fn process_is_active(pid: u64) -> bool {
match fs::symlink_metadata(Path::new("/proc").join(pid.to_string())) {
Ok(metadata) => metadata.is_dir() && !metadata.file_type().is_symlink(),
Err(error) if error.kind() == io::ErrorKind::NotFound => false,
Err(_) => true,
}
}
#[cfg(all(unix, not(target_os = "linux")))]
fn process_is_active(pid: u64) -> bool {
std::process::Command::new("/bin/kill")
.args(["-0", &pid.to_string()])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map_or(true, |status| status.success())
}
#[cfg(windows)]
fn process_is_active(pid: u64) -> bool {
let filter = format!("PID eq {pid}");
let Ok(output) = std::process::Command::new("tasklist.exe")
.args(["/FI", &filter, "/FO", "CSV", "/NH"])
.output()
else {
return true;
};
if !output.status.success() {
return true;
}
let expected = format!("\"{pid}\"");
String::from_utf8_lossy(&output.stdout)
.lines()
.any(|line| line.split(',').nth(1) == Some(expected.as_str()))
}
#[cfg(not(any(unix, windows)))]
fn process_is_active(_pid: u64) -> bool {
true
}
fn directory_usage(path: &Path) -> io::Result<Option<(u64, u64)>> {
let mut files = 0_u64;
let mut bytes = 0_u64;
for entry in fs::read_dir(path)? {
let entry = entry?;
let metadata = fs::symlink_metadata(entry.path())?;
if metadata.file_type().is_symlink() {
return Ok(None);
}
if metadata.is_dir() {
let Some((child_files, child_bytes)) = directory_usage(&entry.path())? else {
return Ok(None);
};
files += child_files;
bytes += child_bytes;
} else if metadata.is_file() {
files += 1;
bytes += metadata.len();
} else {
return Ok(None);
}
}
Ok(Some((files, bytes)))
}
#[cfg(unix)]
fn metadata_owner(metadata: &fs::Metadata) -> u64 {
use std::os::unix::fs::MetadataExt;
u64::from(metadata.uid())
}
#[cfg(not(unix))]
fn metadata_owner(_metadata: &fs::Metadata) -> u64 {
0
}
fn current_owner_id(parent: &Path) -> io::Result<u64> {
loop {
let path = parent.join(format!(
".dever-owner-probe-{}-{}",
std::process::id(),
NEXT_BUILD.fetch_add(1, Ordering::Relaxed),
));
let mut options = OpenOptions::new();
options.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600);
}
match options.open(&path) {
Ok(file) => {
let result = file.metadata().map(|metadata| metadata_owner(&metadata));
drop(file);
let _ = fs::remove_file(path);
return result;
}
Err(error) if error.kind() == io::ErrorKind::AlreadyExists => continue,
Err(error) => return Err(error),
}
}
}
fn owner_matches(metadata: &fs::Metadata, owner: u64) -> bool {
metadata_owner(metadata) == owner
}
+330
View File
@@ -0,0 +1,330 @@
use crate::hir::{CollectionOp as Op, Expression, HandlerTarget, SequenceKind};
use crate::specialize::{self, Specialization};
use crate::types::Type;
use super::{DatabaseContext, Emitter, optional_result, rust_type};
impl Emitter<'_> {
pub(super) fn collection(
&self,
operation: Op,
handler: Option<HandlerTarget>,
sequence: Option<SequenceKind>,
arguments: &[Expression],
result: &Expression,
) -> String {
let traverses = matches!(
operation,
Op::Each | Op::Reduce | Op::ReduceUntil | Op::Filter | Op::Find | Op::Sum
);
let split = if traverses {
self.split_arguments(&arguments[0])
} else {
None
};
let pipeline = if traverses {
self.fused_iterator(&arguments[0])
} else {
None
};
let mut bindings = if let Some((bindings, _)) = &pipeline {
bindings.clone()
} else if let Some([text, separator]) = split {
format!(
"let a0_text = {}; let a0_separator = {};",
self.expression(text),
self.expression(separator)
)
} else {
self.bind_arguments(arguments)
};
if pipeline.is_some() || split.is_some() {
for (index, argument) in arguments.iter().enumerate().skip(1) {
bindings.push_str(&format!(
"let a{index}: {} = {};",
rust_type(&argument.ty),
self.expression(argument)
));
}
}
if operation == Op::ParallelEach {
let target = Specialization {
function: specialize::resolve_handler(
handler.expect("checked parallel handler"),
&self.bindings,
),
handlers: Vec::new(),
};
let async_handler = specialize::suspends(self.program, &target);
let sequence = sequence.expect("checked parallel sequence");
let (dispatch, values, element) = match sequence {
SequenceKind::AsyncStream => ("parallel_each_stream_typed", "a0", "element"),
SequenceKind::List if async_handler => (
"parallel_each_stream_typed",
"dever_runtime::async_stream::AsyncStream::from_values(a0.into_values())",
"element",
),
SequenceKind::Bytes if async_handler => (
"parallel_each_stream_typed",
"dever_runtime::async_stream::AsyncStream::from_values(a0.into_values().map(i64::from))",
"element",
),
SequenceKind::List if self.async_context => {
("parallel_each_typed", "a0.into_values()", "element")
}
SequenceKind::Bytes if self.async_context => (
"parallel_each_typed",
"a0.into_values()",
"i64::from(element)",
),
SequenceKind::List => ("each_slice_typed", "a0.values()", "element.clone()"),
SequenceKind::Bytes => ("each_slice_typed", "a0.values()", "i64::from(*element)"),
SequenceKind::Stream => {
("each_typed", "std::iter::from_fn(|| a0.pull())", "element")
}
SequenceKind::RowStream => unreachable!("checked sequential database stream"),
};
let context = if arguments.len() == 3 {
", a2.clone()"
} else {
""
};
let call = if async_handler {
let capture = if arguments.len() == 3 {
"let context = a2.clone();"
} else {
""
};
let argument = if arguments.len() == 3 {
", context"
} else {
""
};
let invoke = self.invoke(
&target,
format!("{element}{argument}"),
DatabaseContext::Detached,
);
format!(
"task::{dispatch}({values}, a1, move |element| {{ {capture} async move {{ {invoke}.await }} }}).await"
)
} else if sequence == SequenceKind::AsyncStream {
let capture = if arguments.len() == 3 {
"let context = a2.clone();"
} else {
""
};
let argument = if arguments.len() == 3 {
", context"
} else {
""
};
let invoke = self.invoke(
&target,
format!("{element}{argument}"),
DatabaseContext::Detached,
);
format!(
"task::{dispatch}({values}, a1, move |element| {{ {capture} async move {{ {invoke} }} }}).await"
)
} else if self.async_context {
let invoke = self.invoke(
&target,
format!("{element}{context}"),
DatabaseContext::Detached,
);
format!("task::{dispatch}({values}, a1, move |element| {invoke}).await")
} else {
let invoke = self.invoke(
&target,
format!("{element}{context}"),
DatabaseContext::Detached,
);
format!("dever_runtime::concurrent::{dispatch}({values}, a1, |element| {invoke})")
};
return format!(
"{{ {bindings}\n{} }}",
self.call_failure(&call, result.span)
);
}
let body = match operation {
Op::Append => "a0.append(a1)".into(),
Op::First => optional_result("a0.into_first()", list_element(&arguments[0].ty)),
Op::Get => {
let Type::Map(_, value) = &arguments[0].ty else {
unreachable!()
};
optional_result("a0.into_get(&a1)", value)
}
Op::Put => "a0.put(a1, a2)".into(),
Op::Remove => "a0.remove(&a1)".into(),
Op::Entries => "a0.into_entries()".into(),
Op::Length => match &arguments[0].ty {
Type::List(_) => "a0.values().len() as i64".into(),
Type::Map(_, _) => "a0.pairs().count() as i64".into(),
Type::Text => "a0.chars().count() as i64".into(),
Type::Bytes => "a0.length()".into(),
_ => unreachable!(),
},
Op::Close => "a0.close(); ()".into(),
_ => {
let sequence = sequence.expect("checked sequence operation");
let consume =
sequence == SequenceKind::List && arguments[0].ty.movable(&self.program.types);
let list_values = if let Some((_, iterator)) = &pipeline {
iterator.as_str()
} else if split.is_some() {
"dever_runtime::text::pieces(&a0_text, &a0_separator)"
} else if consume {
"a0.into_values()"
} else {
"a0.values()"
};
let element = match sequence {
SequenceKind::List
if consume && !matches!(operation, Op::Filter | Op::Find) =>
{
"element"
}
SequenceKind::List => "element.clone()",
SequenceKind::Bytes => "i64::from(*element)",
SequenceKind::Stream | SequenceKind::AsyncStream | SequenceKind::RowStream => {
"element"
}
};
let call = match handler {
Some(handler) => {
let target = Specialization {
function: specialize::resolve_handler(handler, &self.bindings),
handlers: Vec::new(),
};
let state = if matches!(operation, Op::Reduce | Op::ReduceUntil) {
", state"
} else if arguments.len() == 2 {
", a1.clone()"
} else {
""
};
let wait = if specialize::suspends(self.program, &target) {
".await"
} else {
""
};
let invoke = self.invoke(
&target,
format!("{element}{state}"),
DatabaseContext::Inherit,
);
self.call_failure(&format!("{invoke}{wait}"), result.span)
}
None => element.into(),
};
let loop_over = |body: &str| match sequence {
SequenceKind::List if consume => {
format!("for element in {list_values} {{ {body} }}")
}
SequenceKind::List | SequenceKind::Bytes => {
format!("for element in a0.values() {{ {body} }}")
}
SequenceKind::Stream => {
format!("while let Some(element) = a0.pull() {{ {body} }}")
}
SequenceKind::AsyncStream => {
format!(
"let _stream_owner = a0.close_on_drop(); while let Some(element) = a0.pull().await {{ {body} }}"
)
}
SequenceKind::RowStream => {
let pull = self.database_failure("a0.pull().await", result.span);
format!(
"let _stream_owner = a0.close_on_drop(); while let Some(element) = {pull} {{ {body} }}"
)
}
};
match operation {
Op::Each if result.ty == Type::Unit => loop_over(&format!("{call};")),
Op::Each => {
let capacity = match sequence {
SequenceKind::List if split.is_some() || pipeline.is_some() => "0",
SequenceKind::List => "a0.values().len()",
SequenceKind::Bytes => "a0.values().len()",
SequenceKind::Stream
| SequenceKind::AsyncStream
| SequenceKind::RowStream => unreachable!("checked stream each output"),
};
format!(
"let mut values = Vec::with_capacity({capacity}); {} List::new(values)",
loop_over(&format!("values.push({call});"))
)
}
Op::Filter => {
let selected = if consume {
"element"
} else {
"element.clone()"
};
format!(
"let mut values = Vec::new(); for element in {list_values} {{ if {call} {{ values.push({selected}); }} }} List::new(values)"
)
}
Op::Find => {
let selected = if consume {
"element"
} else {
"element.clone()"
};
let found = if matches!(list_element(&arguments[0].ty), Type::Nullable(_)) {
selected.to_owned()
} else {
format!("Some({selected})")
};
format!(
"let mut found = None; for element in {list_values} {{ if {call} {{ found = {found}; break; }} }} found"
)
}
Op::Sum => {
let zero = match result.ty {
Type::Int => "0i64",
Type::Decimal => "DecimalValue::ZERO",
Type::Float => "0.0f64",
_ => unreachable!(),
};
let add = match result.ty {
Type::Int => {
self.failure("number::int_add(total, projected)", result.span)
}
Type::Decimal => {
self.failure("total.checked_add(projected)", result.span)
}
Type::Float => "total + projected".into(),
_ => unreachable!(),
};
format!(
"let mut total: {} = {zero}; for element in {list_values} {{ let projected = {call}; total = {add}; }} total",
rust_type(&result.ty)
)
}
Op::Reduce => format!(
"let mut state = a1; {} state",
loop_over(&format!("state = {call};"))
),
Op::ReduceUntil => format!(
"let mut state = a1; {} state",
loop_over(&format!(
"let step = {call}; state = step.0; if step.1 {{ break; }}"
))
),
_ => unreachable!(),
}
}
};
format!("{{ {bindings}\n{body} }}")
}
}
fn list_element(ty: &Type) -> &Type {
let Type::List(element) = ty else {
unreachable!()
};
element
}
+494
View File
@@ -0,0 +1,494 @@
use crate::hir::{CallArgument, CallTarget, Expression, ExpressionKind as Expr};
use crate::specialize::{self, Specialization};
use crate::syntax::{BinaryOperator as Op, UnaryOperator};
use crate::types::Type;
use super::{DatabaseContext, Emitter, field_suffix, optional_result, rust_type, typed_constant};
impl Emitter<'_> {
pub(super) fn expression(&self, value: &Expression) -> String {
match &value.kind {
Expr::Constant(constant) => typed_constant(constant, &value.ty),
Expr::Local(slot) if self.can_move(value) => format!("_v{slot}"),
Expr::Local(slot) => format!("_v{slot}.clone()"),
Expr::Field { .. } => self.field_expression(value),
Expr::ChoiceOptions { choice } => {
let crate::types::Shape::Choice(variants) = &self.program.types[*choice].shape
else {
unreachable!("checked Model choice options")
};
let entries = variants
.iter()
.enumerate()
.map(|(variant, definition)| {
format!(
"(T{choice}::V{variant}, String::from({}))",
super::rust_string(
definition.label.as_deref().unwrap_or(&definition.name)
)
)
})
.collect::<Vec<_>>()
.join(", ");
format!("Map::<T{choice}, String>::from_unique(vec![{entries}])")
}
Expr::ModelOperation { model, operation } => {
self.model_operation(*model, operation, value)
}
Expr::JobEnqueue {
target,
arguments,
scheduled,
} => self.enqueue_job(*target, arguments, *scheduled, value.span),
Expr::Call { target, arguments } => {
let instance = self.call_instance(*target, arguments);
let call = self.call(&instance, arguments, DatabaseContext::Inherit);
let call = if specialize::suspends(self.program, &instance) {
format!("{call}.await")
} else {
call
};
self.call_failure(&call, value.span)
}
Expr::Fail(error) => self.fail_expression(error, value),
Expr::CaptureResult {
target,
arguments,
choice,
success,
} => self.capture_result(*target, arguments, *choice, *success, value),
Expr::RunCall {
group,
target,
arguments,
} => {
let instance = self.call_instance(*target, arguments);
let call = self.call(&instance, arguments, DatabaseContext::Detached);
let call = if specialize::suspends(self.program, &instance) {
call
} else {
format!("task::blocking_typed(move || {call})")
};
let operation = match group {
Some(group) => format!("{}.run({call}).await", self.group_local(group)),
None => format!("task::run_typed({call}).await"),
};
if group.is_some() {
self.call_failure(&operation, value.span)
} else {
self.failure(&operation, value.span)
}
}
Expr::AwaitTask(task) => self.call_failure(
&format!("task::wait({}).await", self.expression(task)),
value.span,
),
Expr::StopTask(task) => self.call_failure(
&format!("task::stop({}).await", self.expression(task)),
value.span,
),
Expr::Group(limit) => self.failure(
&format!(
"task::Group::<AppError>::new_typed({})",
self.expression(limit)
),
value.span,
),
Expr::AwaitGroup(group) => self.call_failure(
&format!("task::Group::wait({}).await", self.expression(group)),
value.span,
),
Expr::StopGroup(group) => self.call_failure(
&format!("task::Group::stop({}).await", self.expression(group)),
value.span,
),
Expr::ParallelCall { target, arguments } => {
self.thread_call("parallel", *target, arguments, value)
}
Expr::BlockingCall { target, arguments } => {
self.thread_call("blocking", *target, arguments, value)
}
Expr::Channel { element, capacity } => self.failure(
&format!(
"channel::Channel::<{}>::new({})",
rust_type(element),
self.expression(capacity)
),
value.span,
),
Expr::ChannelSend {
channel,
value: item,
} => self.failure(
&format!(
"({}).send({}).await",
self.expression(channel),
self.expression(item)
),
value.span,
),
Expr::ChannelReceive(channel) => {
let Type::Channel(element) = &channel.ty else {
unreachable!("checked channel receive")
};
let received = self.failure(
&format!("({}).receive().await", self.expression(channel)),
value.span,
);
optional_result(&received, element)
}
Expr::ChannelClose(channel) => self.failure(
&format!("({}).close().await", self.expression(channel)),
value.span,
),
Expr::Intrinsic {
operation,
handler,
arguments,
} => self.intrinsic(*operation, *handler, arguments, value),
Expr::Collection {
operation,
handler,
sequence,
arguments,
} => self.collection(*operation, *handler, *sequence, arguments, value),
Expr::Record { fields } => {
let Type::Named(id) = value.ty else {
unreachable!()
};
// Field expressions execute in source order, independent of declaration order.
let bindings = fields
.iter()
.map(|(index, value)| format!("let field{index} = {};", self.expression(value)))
.collect::<Vec<_>>()
.join("\n");
let fields = fields
.iter()
.map(|(index, _)| format!("f{index}: field{index}"))
.collect::<Vec<_>>()
.join(", ");
format!("{{ {bindings} T{id} {{ {fields} }} }}")
}
Expr::Variant { variant, arguments } => {
let Type::Named(id) = value.ty else {
unreachable!()
};
if arguments.is_empty() {
format!("T{id}::V{variant}")
} else {
format!("T{id}::V{variant}({})", self.arguments(arguments))
}
}
Expr::List(elements) => format!(
"{}::new(vec![{}])",
rust_type(&value.ty).replacen("List<", "List::<", 1),
self.arguments(elements)
),
Expr::Map(entries) => {
let entries = entries
.iter()
.map(|(key, value)| {
format!("({}, {})", self.expression(key), self.expression(value))
})
.collect::<Vec<_>>()
.join(", ");
self.failure(
&format!(
"{}::new(vec![{entries}])",
rust_type(&value.ty).replacen("Map<", "Map::<", 1)
),
value.span,
)
}
Expr::Promote(value) => format!("DecimalValue::from_int({})", self.expression(value)),
Expr::Some(value) => format!("Some({})", self.expression(value)),
Expr::Unary {
operator,
value: operand,
} => {
let operand_text = self.expression(operand);
match (operator, &operand.ty) {
(UnaryOperator::Not, _) => format!("!({operand_text})"),
(UnaryOperator::Negate, Type::Float) => format!("-({operand_text})"),
(UnaryOperator::Negate, Type::Int) => {
self.failure(&format!("number::int_neg({operand_text})"), value.span)
}
(UnaryOperator::Negate, Type::Decimal) => {
self.failure(&format!("({operand_text}).checked_neg()"), value.span)
}
_ => unreachable!(),
}
}
Expr::Binary {
left,
operator,
right,
} => self.binary(left, *operator, right, value),
}
}
fn can_move(&self, value: &Expression) -> bool {
self.moves.contains(&(value as *const Expression)) && value.ty.movable(&self.program.types)
}
fn fail_expression(&self, error: &Expression, result: &Expression) -> String {
let Type::Named(ty) = error.ty else {
unreachable!("checked failure choice")
};
let Expr::Variant { variant, arguments } = &error.kind else {
unreachable!("checked direct failure variant")
};
let payload = if arguments.is_empty() {
String::new()
} else {
format!("({})", self.arguments(arguments))
};
format!(
"return Err(AppError::business(AppErrorKind::E{ty}V{variant}{payload}, {}))",
self.location(result.span)
)
}
fn capture_result(
&self,
target: CallTarget,
arguments: &[CallArgument],
choice: usize,
success: usize,
result: &Expression,
) -> String {
let instance = self.call_instance(target, arguments);
let mut call = self.call(&instance, arguments, DatabaseContext::Inherit);
if specialize::suspends(self.program, &instance) {
call.push_str(".await");
}
let success_fields = match &self.program.types[choice].shape {
crate::types::Shape::Choice(variants) => variants[success].fields.len(),
_ => unreachable!("checked result capture choice"),
};
let success_arm = match success_fields {
0 => format!("Ok(()) => T{choice}::V{success}"),
1 => format!("Ok(value) => T{choice}::V{success}(value)"),
count => {
let values = (0..count)
.map(|index| format!("value{index}"))
.collect::<Vec<_>>();
format!(
"Ok(({})) => T{choice}::V{success}({})",
values.join(", "),
values.join(", ")
)
}
};
let failures = specialize::failures(self.program, &instance);
let error_arms = crate::capture::targets(&self.program.types, choice, &failures)
.expect("validated result capture errors")
.into_iter()
.map(|(failure, target)| {
let crate::types::Shape::Choice(variants) = &self.program.types[failure.ty].shape
else {
unreachable!("checked error choice")
};
let definition = &variants[failure.variant];
let fields = (0..definition.fields.len())
.map(|index| format!("field{index}"))
.collect::<Vec<_>>();
let payload = if fields.is_empty() {
String::new()
} else {
format!("({})", fields.join(", "))
};
let error = format!("T{}::V{}{payload}", failure.ty, failure.variant);
let value = if target.wrapped {
format!("T{choice}::V{}({error})", target.variant)
} else {
error
};
format!(
"Err(AppError {{ kind: AppErrorKind::E{}V{}{payload}, .. }}) => {value}",
failure.ty, failure.variant
)
})
.collect::<Vec<_>>()
.join(", ");
format!(
"match {call} {{ {success_arm}, {error_arms}, Err(error) => return Err(error.at({})) }}",
self.location(result.span)
)
}
fn call_instance(&self, target: CallTarget, arguments: &[CallArgument]) -> Specialization {
match target {
CallTarget::Function(function) => {
specialize::call_specialization(function, arguments, &self.bindings)
}
CallTarget::Handler(target) => Specialization {
function: specialize::resolve_handler(target, &self.bindings),
handlers: Vec::new(),
},
}
}
fn call(
&self,
function: &Specialization,
arguments: &[CallArgument],
database: DatabaseContext,
) -> String {
self.invoke(function, self.call_arguments(arguments), database)
}
fn group_local(&self, group: &Expression) -> String {
let Expr::Local(slot) = group.kind else {
unreachable!("checked Group operand")
};
format!("_v{slot}")
}
fn thread_call(
&self,
operation: &str,
target: CallTarget,
arguments: &[CallArgument],
result: &Expression,
) -> String {
let mut value_index = 0;
let mut bindings = Vec::new();
let mut call_arguments = Vec::new();
for argument in arguments {
match argument {
CallArgument::Value(value) => {
bindings.push(format!(
"let a{value_index}: {} = {};",
rust_type(&value.ty),
self.expression(value)
));
call_arguments.push(format!("a{value_index}"));
value_index += 1;
}
CallArgument::Handler(_) => {}
}
}
let function = self.call_instance(target, arguments);
let call = self.invoke(
&function,
call_arguments.join(", "),
DatabaseContext::Detached,
);
let operation = self.call_failure(
&format!("task::{operation}_typed(move || {call}).await"),
result.span,
);
format!("{{ {} {operation} }}", bindings.join(" "))
}
fn field_expression(&self, expression: &Expression) -> String {
let mut base = expression;
let mut fields = Vec::new();
while let Expr::Field { value, index } = &base.kind {
fields.push(field_suffix(&value.ty, *index));
base = value;
}
let suffix = fields.into_iter().rev().collect::<String>();
if let Expr::Local(slot) = base.kind {
let projected = format!("_v{slot}{suffix}");
if self.can_move(expression) || self.can_move(base) {
projected
} else {
format!("{projected}.clone()")
}
} else {
format!("({}){suffix}", self.expression(base))
}
}
pub(super) fn arguments(&self, arguments: &[Expression]) -> String {
arguments
.iter()
.map(|value| self.expression(value))
.collect::<Vec<_>>()
.join(", ")
}
fn call_arguments(&self, arguments: &[CallArgument]) -> String {
arguments
.iter()
.filter_map(|argument| match argument {
CallArgument::Value(value) => Some(self.expression(value)),
CallArgument::Handler(_) => None,
})
.collect::<Vec<_>>()
.join(", ")
}
pub(super) fn bind_arguments(&self, arguments: &[Expression]) -> String {
arguments
.iter()
.enumerate()
.map(|(index, value)| {
format!(
"let a{index}: {} = {};",
rust_type(&value.ty),
self.expression(value)
)
})
.collect::<Vec<_>>()
.join("\n")
}
fn binary(
&self,
left: &Expression,
operator: Op,
right: &Expression,
result: &Expression,
) -> String {
let left_text = self.expression(left);
let right_text = self.expression(right);
let symbol = match operator {
Op::Add => "+",
Op::Subtract => "-",
Op::Multiply => "*",
Op::Divide => "/",
Op::IntegerDivide => "/",
Op::Remainder => "%",
Op::Equal => "==",
Op::NotEqual => "!=",
Op::Less => "<",
Op::LessEqual => "<=",
Op::Greater => ">",
Op::GreaterEqual => ">=",
Op::And => "&&",
Op::Or => "||",
};
if matches!(operator, Op::And | Op::Or) {
return format!("(({left_text}) {symbol} ({right_text}))");
}
let operation = if matches!(
operator,
Op::Equal | Op::NotEqual | Op::Less | Op::LessEqual | Op::Greater | Op::GreaterEqual
) {
format!("a0 {symbol} a1")
} else if left.ty == Type::Text {
"a0 + &a1".into()
} else if left.ty == Type::Float {
format!("a0 {symbol} a1")
} else {
let method = match operator {
Op::Add => "add",
Op::Subtract => "sub",
Op::Multiply => "mul",
Op::Divide | Op::IntegerDivide => "div",
Op::Remainder => "rem",
_ => unreachable!(),
};
let operation = if left.ty == Type::Int {
format!("number::int_{method}(a0, a1)")
} else {
format!("a0.checked_{method}(a1)")
};
self.failure(&operation, result.span)
};
format!("{{ let a0 = {left_text}; let a1 = {right_text}; {operation} }}")
}
}
+314
View File
@@ -0,0 +1,314 @@
use crate::hir::{Expression, HandlerTarget};
use crate::specialize::{self, Specialization};
use crate::types::{Parameter, Shape, Type};
use super::{DatabaseContext, Emitter};
#[derive(Clone, Copy)]
enum Direction {
ToRuntime,
ToLanguage,
}
impl Emitter<'_> {
pub(super) fn live_network(
&self,
operation: crate::intrinsic::Intrinsic,
arguments: &[Expression],
result: &Expression,
) -> String {
use crate::intrinsic::Intrinsic::*;
let input = |index: usize| {
self.network_value(
&format!("a{index}"),
&arguments[index].ty,
Direction::ToRuntime,
)
};
let call = match operation {
TlsClient => "dever_runtime::tls::client(&a0)".into(),
TlsServer => "dever_runtime::tls::server(&a0, &a1)".into(),
HttpClientNew => format!(
"dever_runtime::http::client(&a0, a1, {}, {}).await",
input(2),
input(3)
),
HttpCloseClient => "dever_runtime::http::close_client(&a0).await".into(),
HttpRequest | HttpOpen | HttpOpenStream => {
let method = match operation {
HttpRequest => "request",
HttpOpen => "open",
_ => "open_stream",
};
return self.http_result(
&format!("dever_runtime::http::{method}(&a0, {}).await", input(1)),
result,
);
}
HttpRespond => format!("dever_runtime::http::respond(&a0, {}).await", input(1)),
HttpStart => format!("dever_runtime::http::start(&a0, a1, {}).await", input(2)),
HttpWrite => "dever_runtime::http::write(&a0, a1).await".into(),
HttpFinish => "dever_runtime::http::finish(&a0).await".into(),
SseStart => format!("dever_runtime::sse::start(&a0, {}).await", input(1)),
SseSend => format!("dever_runtime::sse::send(&a0, {}).await", input(1)),
WsAccept => format!("dever_runtime::websocket::accept(&a0, {}).await", input(1)),
WsConnect => format!(
"dever_runtime::websocket::connect(&a0, a1, &a2, {}).await",
input(3)
),
WsOpen => format!(
"dever_runtime::websocket::open(&a0, a1, {}).await",
input(2)
),
WsSend => format!("dever_runtime::websocket::send(&a0, {}).await", input(1)),
WsClose => "dever_runtime::websocket::close(&a0, a1, &a2).await".into(),
WsReceive | WsMessages => {
let event = match &result.ty {
Type::AsyncStream(event) => event.as_ref(),
event => event,
};
let Type::Named(id) = event else {
unreachable!()
};
let Shape::Choice(variants) = &self.program.types[*id].shape else {
unreachable!()
};
let read = variants
.iter()
.find(|variant| variant.name == "Read")
.unwrap();
let message =
self.network_value("message", &read.fields[0].ty, Direction::ToLanguage);
if matches!(operation, WsMessages) {
let event = self.resource_result(
&format!("event.map(|message| {message})"),
event,
"Read",
false,
);
return format!("dever_runtime::websocket::messages(a0).map(|event| {event})");
}
return self.resource_result(&format!(
"dever_runtime::websocket::receive(&a0).await.map(|message| message.map(|message| {message}))"
), event, "Read", true);
}
_ => unreachable!("live network intrinsic"),
};
let success = match operation {
TlsClient | TlsServer | HttpClientNew => "Ready",
WsAccept | WsConnect | WsOpen => "Connected",
HttpFinish | WsClose | HttpCloseClient => "Closed",
_ => "Done",
};
self.resource_result(&call, &result.ty, success, false)
}
pub(super) fn http_serve(
&self,
handler: HandlerTarget,
arguments: &[Expression],
result: &Expression,
live: bool,
tls: bool,
) -> String {
let target = Specialization {
function: specialize::resolve_handler(handler, &self.bindings),
handlers: Vec::new(),
};
let wait = if specialize::suspends(self.program, &target) {
".await"
} else {
""
};
let function = &self.program.functions[target.function];
let Parameter::Value(request) = &function.parameters[0] else {
unreachable!("checked HTTP request")
};
let request = self.network_value("request", request, Direction::ToLanguage);
let tls_index = if live { 3 } else { 2 };
let context_index = tls_index + usize::from(tls);
let tls_argument = if tls {
format!(", a{tls_index}")
} else {
String::new()
};
let suffix = if tls { "_tls" } else { "" };
let (capture, context) = if arguments.len() > context_index {
(
format!("let context = a{context_index}.clone();"),
", context",
)
} else {
(String::new(), "")
};
if live {
let limits = self.network_value("a1", &arguments[1].ty, Direction::ToRuntime);
let live_limits = self.network_value("a2", &arguments[2].ty, Direction::ToRuntime);
let invoke = self.invoke(
&target,
format!("{request}, reply{context}"),
DatabaseContext::Detached,
);
return self.failure(&format!(
"dever_runtime::http::serve_live{suffix}(move |request, reply| {{ {capture} async move {{ {invoke}{wait}.map_err(|error| error.to_string()) }} }}, a0, {limits}, {live_limits}{tls_argument}).await",
), result.span);
}
let response =
self.network_value("response", &function.outputs[0].ty, Direction::ToRuntime);
let limits = self.network_value("a1", &arguments[1].ty, Direction::ToRuntime);
let invoke = self.invoke(
&target,
format!("{request}{context}"),
DatabaseContext::Detached,
);
self.failure(&format!(
"dever_runtime::http::serve{suffix}(move |request| {{ {capture} async move {{ let response = {invoke}{wait}.map_err(|error| error.to_string())?; Ok({response}) }} }}, a0, {limits}{tls_argument}).await",
), result.span)
}
pub(super) fn http_send(&self, arguments: &[Expression], result: &Expression) -> String {
let request = self.network_value("a2", &arguments[2].ty, Direction::ToRuntime);
let limits = self.network_value("a3", &arguments[3].ty, Direction::ToRuntime);
self.http_result(
&format!("dever_runtime::http::send(&a0, a1, {request}, {limits}).await"),
result,
)
}
fn http_result(&self, call: &str, result: &Expression) -> String {
let Type::Named(id) = &result.ty else {
unreachable!("checked HTTP result")
};
let Shape::Choice(variants) = &self.program.types[*id].shape else {
unreachable!()
};
let read = variants
.iter()
.find(|variant| variant.name == "Read")
.expect("checked Read variant");
let response = self.network_value("response", &read.fields[0].ty, Direction::ToLanguage);
self.resource_result(
&format!("{call}.map(|response| {response})"),
&result.ty,
"Read",
false,
)
}
// 校验器固定报文记录的字段契约;桥接按字段名生成,源码重排字段不会错位。
fn network_value(&self, value: &str, ty: &Type, direction: Direction) -> String {
match ty {
Type::Named(id) => {
let definition = &self.program.types[*id];
let Shape::Record(fields) = &definition.shape else {
let Shape::Choice(variants) = &definition.shape else {
unreachable!()
};
let arms = variants
.iter()
.enumerate()
.map(|(index, variant)| {
let runtime = format!(
"dever_runtime::{}::{}",
definition
.name
.strip_prefix("dever.")
.unwrap()
.replace('.', "::"),
variant.name
);
let language = format!("T{id}::V{index}");
let payload =
self.network_value("payload", &variant.fields[0].ty, direction);
let (source, target) = match direction {
Direction::ToRuntime => (language, runtime),
Direction::ToLanguage => (runtime, language),
};
format!("{source}(payload) => {target}({payload})")
})
.collect::<Vec<_>>()
.join(", ");
return format!("match {value} {{ {arms} }}");
};
let name = match direction {
Direction::ToRuntime => format!(
"dever_runtime::{}",
definition
.name
.strip_prefix("dever.")
.expect("official record")
.replace('.', "::"),
),
Direction::ToLanguage => format!("T{id}"),
};
let fields = fields
.iter()
.enumerate()
.map(|(index, field)| {
let (source, target) = match direction {
Direction::ToRuntime => {
(format!("record.f{index}"), field.name.clone())
}
Direction::ToLanguage => {
(format!("record.{}", field.name), format!("f{index}"))
}
};
format!(
"{target}: {}",
self.network_value(&source, &field.ty, direction)
)
})
.collect::<Vec<_>>()
.join(", ");
format!("{{ let record = {value}; {name} {{ {fields} }} }}")
}
Type::List(element) => {
let element = self.network_value("header", element, direction);
match direction {
Direction::ToRuntime => {
format!("{value}.into_values().map(|header| {element}).collect()")
}
Direction::ToLanguage => {
format!("List::new({value}.into_iter().map(|header| {element}).collect())")
}
}
}
Type::Nullable(element) => {
let element = self.network_value("element", element, direction);
format!("{value}.map(|element| {element})")
}
Type::AsyncStream(event) => {
let conversion = match direction {
Direction::ToLanguage => self.resource_result("event", event, "Chunk", false),
Direction::ToRuntime => {
let Type::Named(id) = event.as_ref() else {
unreachable!()
};
let Shape::Choice(variants) = &self.program.types[*id].shape else {
unreachable!()
};
let arms = variants
.iter()
.enumerate()
.map(|(index, variant)| {
let constructor =
if variant.name == "Chunk" { "Ok" } else { "Err" };
format!("T{id}::V{index}(payload) => {constructor}(payload)")
})
.collect::<Vec<_>>()
.join(", ");
format!("match event {{ {arms} }}")
}
};
format!("{value}.map(|event| {conversion})")
}
Type::Text
| Type::Bytes
| Type::Int
| Type::ClientTls
| Type::ServerTls
| Type::HttpClient => value.into(),
_ => unreachable!("checked HTTP field type"),
}
}
}
+409
View File
@@ -0,0 +1,409 @@
use crate::hir::{Expression, HandlerTarget};
use crate::intrinsic::Intrinsic;
use crate::types::{Shape, Type};
use super::{DatabaseContext, Emitter};
impl Emitter<'_> {
pub(super) fn intrinsic(
&self,
intrinsic: Intrinsic,
handler: Option<HandlerTarget>,
arguments: &[Expression],
result: &Expression,
) -> String {
use Intrinsic::*;
if matches!(
intrinsic,
UploadFilename | UploadContentType | UploadSize | UploadClose | UploadStore
) {
let crate::hir::ExpressionKind::Local(slot) = arguments[0].kind else {
unreachable!("checked upload local")
};
if matches!(intrinsic, UploadStore) {
return self.resource_result(
&format!("dever_runtime::api::upload::store(_v{slot}).await"),
&result.ty,
"Stored",
false,
);
}
let method = match intrinsic {
UploadFilename => "filename",
UploadContentType => "content_type",
UploadSize => "size",
_ => "close",
};
return format!("_v{slot}.{method}()");
}
let operation = match intrinsic {
UploadFilename | UploadContentType | UploadSize | UploadClose | UploadStore => unreachable!("upload handled above"),
TestSecret => "dever_runtime::secret::Secret::from_input(a0.into_bytes())".into(),
CryptoToken => self.resource_result("dever_runtime::crypto::token(a0)", &result.ty, "Ready", false),
CryptoPasswordHash => self.resource_result("dever_runtime::crypto::password_hash(&a0)", &result.ty, "Ready", false),
CryptoPasswordVerify => self.resource_result("dever_runtime::crypto::password_verify(&a0, &a1)", &result.ty, "Ready", false),
CryptoSha256 => "dever_runtime::crypto::sha256(&a0)".into(),
CryptoHmacSha256 => "dever_runtime::crypto::hmac_sha256(&a0, &a1)".into(),
CryptoConstantTimeEq => "dever_runtime::crypto::constant_time_eq(&a0, &a1)".into(),
TestAssert => format!(
"if !a0 {{ return Err(AppError::fault({}, \"assertion failed\")); }}",
self.location(result.span)
),
TestAssertEq => format!(
"if a0 != a1 {{ let mut actual = String::new(); a0.render_to(&mut actual); let mut expected = String::new(); a1.render_to(&mut expected); return Err(AppError::fault({}, format!(\"assert_eq failed: actual = {{actual}}, expected = {{expected}}\"))); }}",
self.location(result.span)
),
TlsSystem => "dever_runtime::tls::system()".into(),
TaskTicks => self.failure("task::ticks(a0)", result.span),
TaskTimeout => {
let target = crate::specialize::Specialization {
function: crate::specialize::resolve_handler(handler.expect("timeout handler"), &self.bindings),
handlers: vec![],
};
let wait = if crate::specialize::suspends(self.program, &target) { ".await" } else { "" };
let invoke = self.invoke(&target, String::new(), DatabaseContext::Inherit);
format!("match {} {{ Some(value) => value, None => {} }}",
self.call_failure("task::wait_timeout(a0, a1).await", result.span),
self.call_failure(&format!("{invoke}{wait}"), result.span))
}
TaskRace => {
let tasks = (0..arguments.len()).map(|index| format!("a{index}")).collect::<Vec<_>>().join(", ");
self.call_failure(&format!("task::race(vec![{tasks}]).await"), result.span)
}
StreamOf => match &arguments[0].ty {
Type::Channel(_) => "a0.stream()".into(),
Type::List(_) => "dever_runtime::async_stream::AsyncStream::from_values(a0.into_values())".into(),
Type::Bytes => "dever_runtime::async_stream::AsyncStream::from_values(a0.into_values().map(i64::from))".into(),
_ => unreachable!(),
},
TextTrim => "a0.trim().to_owned()".into(),
TextLower => "a0.to_lowercase()".into(),
TextUpper => "a0.to_uppercase()".into(),
TextCodepoint => "dever_runtime::text::codepoint(&a0)".into(),
TextFromCodepoint => "dever_runtime::text::from_codepoint(a0)".into(),
TextAt => "dever_runtime::text::at(&a0, a1)".into(),
TextSlice => "dever_runtime::text::slice(&a0, a1, a2)".into(),
TextIndexOf => "dever_runtime::text::index_of(&a0, &a1)".into(),
TextContains => "a0.contains(&a1)".into(),
TextStartsWith => "a0.starts_with(&a1)".into(),
TextEndsWith => "a0.ends_with(&a1)".into(),
TextSplit => "dever_runtime::text::split(&a0, &a1)".into(),
TextReplace => "a0.replace(&a1, &a2)".into(),
IntParse => "number::parse_int(&a0)".into(),
IntToText | DecimalToText | FloatToText => "a0.to_string()".into(),
DecimalParse => "number::parse_decimal(&a0)".into(),
DecimalFromInt => "DecimalValue::from_int(a0)".into(),
FloatParse => "number::parse_float(&a0)".into(),
ProcessArguments => self.resource_result(
"dever_runtime::process::arguments()",
&result.ty,
"Read",
false,
),
TimeUnixMillis => self.resource_result(
"dever_runtime::time::unix_millis()",
&result.ty,
"Read",
false,
),
TimeNow => self.resource_result("dever_runtime::time::now()", &result.ty, "Read", false),
TimeParseDateTime => self.resource_result("dever_runtime::time::parse_datetime(&a0)", &result.ty, "Read", false),
TimeFormatDateTime => self.resource_result("dever_runtime::time::format_datetime(a0)", &result.ty, "Read", false),
TimeParseDate => self.resource_result("dever_runtime::time::parse_date(&a0)", &result.ty, "Read", false),
TimeFormatDate => self.resource_result("dever_runtime::time::format_date(a0)", &result.ty, "Read", false),
TimeParseTime => self.resource_result("dever_runtime::time::parse_time(&a0)", &result.ty, "Read", false),
TimeFormatTime => self.resource_result("dever_runtime::time::format_time(a0)", &result.ty, "Read", false),
TimeDuration => "dever_runtime::time::duration(a0)".into(),
TimeAdd => self.resource_result("dever_runtime::time::add(a0, a1)", &result.ty, "Read", false),
TimeSubtract => self.resource_result("dever_runtime::time::subtract(a0, a1)", &result.ty, "Read", false),
TimeDifference => self.resource_result("dever_runtime::time::difference(a0, a1)", &result.ty, "Read", false),
TimeMonotonicNanos => self.resource_result(
"dever_runtime::time::monotonic_nanos()",
&result.ty,
"Read",
false,
),
TimeSleep => {
self.resource_result("dever_runtime::time::sleep(a0)", &result.ty, "Done", false)
}
TaskSleep => self.resource_result(
"task::sleep(a0).await",
&result.ty,
"Done",
false,
),
DecimalRound => self.failure("a0.round(a1)", result.span),
FloatFromInt => "a0 as f64".into(),
FloatSqrt => "a0.sqrt()".into(),
FloatSin => "a0.sin()".into(),
FloatCos => "a0.cos()".into(),
FloatLog => "a0.ln()".into(),
FloatPow => "a0.powf(a1)".into(),
FloatIsNan => "a0.is_nan()".into(),
FloatIsFinite => "a0.is_finite()".into(),
FloatIsInfinite => "a0.is_infinite()".into(),
StdoutWrite => self.failure("resource::stdout_write(&a0)", result.span),
LogDebug | LogInfo | LogWarn | LogError => {
let level = match intrinsic {
LogDebug => "debug",
LogInfo => "info",
LogWarn => "warn",
LogError => "error",
_ => unreachable!(),
};
format!(
"dever_runtime::log::{level}(a0, a1.pairs().map(|(name, value)| dever_runtime::log::Field {{ name: name.clone(), value: value.clone() }}).collect())"
)
}
IdFromText => "dever_runtime::Id(a0)".into(),
IdToText => "a0.0".into(),
UuidParse => "dever_runtime::orm::Uuid::parse(&a0).ok()".into(),
UuidToText => "a0.to_string()".into(),
BytesFromText => "Bytes::from_string(a0)".into(),
BytesLength => "a0.length()".into(),
BytesAt => "a0.at(a1)".into(),
BytesConcat => "a0.concat(&a1)".into(),
BytesToText => self.resource_result("a0.into_text()", &result.ty, "Decoded", false),
BytesFromInts => {
self.resource_result("Bytes::from_ints(a0.values())", &result.ty, "Ready", false)
}
BytesSlice => self.resource_result("a0.slice(a1, a2)", &result.ty, "Ready", false),
FileOpen => {
self.resource_result("resource::file_open(&a0)", &result.ty, "Opened", false)
}
FileCreate => {
self.resource_result("resource::file_create(&a0)", &result.ty, "Opened", false)
}
FileRead => {
self.resource_result("resource::read(&a0, a1)", &result.ty, "Read", true)
}
FileWrite => {
self.resource_result("resource::write(&a0, &a1)", &result.ty, "Done", false)
}
FileClose | TcpClose | TcpCloseListener => {
self.resource_result("a0.close()", &result.ty, "Closed", false)
}
FileChunks => self.read_stream_result("resource::read_chunks(a0, a1)", &result.ty),
TcpChunks => self.read_stream_result("dever_runtime::net::chunks(a0, a1)", &result.ty),
TcpRead => self.resource_result(
"dever_runtime::net::read(&a0, a1).await", &result.ty, "Read", true,
),
TcpWrite => self.resource_result(
"dever_runtime::net::write(&a0, &a1).await", &result.ty, "Done", false,
),
TcpConnect => self.resource_result(
"dever_runtime::net::connect(&a0, a1).await",
&result.ty,
"Connected",
false,
),
TcpConnectTimeout => self.resource_result(
"dever_runtime::net::connect_timeout(&a0, a1, a2).await",
&result.ty,
"Connected",
false,
),
TcpListen => self.resource_result(
"dever_runtime::net::listen(&a0, a1).await",
&result.ty,
"Listening",
false,
),
TcpAccept => {
self.resource_result("dever_runtime::net::accept(&a0).await", &result.ty, "Connected", false)
}
TcpPort => self.resource_result("dever_runtime::net::port(&a0)", &result.ty, "Bound", false),
TcpTimeout => {
self.resource_result("dever_runtime::net::timeout(&a0, a1)", &result.ty, "Done", false)
}
TcpConnections => self.connection_stream(&result.ty),
ApiServe => self.failure("dever_runtime::api::serve(api_route).await", result.span),
ApiRequestId => self.failure("dever_runtime::api::request_id()", result.span),
ApiMethod => self.failure("dever_runtime::api::method()", result.span),
ApiPath => self.failure("dever_runtime::api::path()", result.span),
ApiClientAddress => self.failure("dever_runtime::api::client_address()", result.span),
ApiHeader => self.failure("dever_runtime::api::header(&a0)", result.span),
ApiCookie => self.failure("dever_runtime::api::cookie(&a0)", result.span),
ApiSecretCookie => self.failure("dever_runtime::api::secret_cookie(&a0)", result.span),
ApiResponseHeader => self.failure("dever_runtime::api::response_header(&a0, &a1)", result.span),
ApiResponseCookie => self.failure(&format!(
"dever_runtime::api::response_cookie(&a0, &a1, {})",
self.api_cookie_options(),
), result.span),
ApiResponseSecretCookie => self.failure(&format!(
"dever_runtime::api::response_secret_cookie(&a0, a1, {})",
self.api_cookie_options(),
), result.span),
AuthIssue => self.failure("dever_runtime::auth::issue(&a0, &a1, a2.as_deref())", result.span),
AuthIssueCookie => self.failure("dever_runtime::auth::issue_cookie(&a0, &a1, a2.as_deref())", result.span),
AuthClearCookie => self.failure("dever_runtime::auth::clear_cookie()", result.span),
AuthOwnsUser => self.failure("dever_runtime::auth::owns_user(a0.0)", result.span),
AuthId => self.failure("dever_runtime::auth::id()", result.span),
AuthSession => self.failure("dever_runtime::auth::session()", result.span),
AuthUserId | AuthTenantId => {
let method = if intrinsic == AuthUserId { "user_id" } else { "tenant_id" };
let call = format!("dever_runtime::auth::{method}()");
let call = match &result.ty {
Type::Nullable(inner) => match inner.as_ref() {
Type::Named(id) => format!("{call}.map(|value| value.map(T{id}))"),
_ => call,
},
_ => unreachable!("authentication ID getter returns a nullable value"),
};
self.failure(&call, result.span)
}
AuthPermissions => {
let Type::List(permission) = &result.ty else { unreachable!("checked permission list") };
let Type::Named(permission) = permission.as_ref() else { unreachable!("checked Permission type") };
self.failure(&format!(
"dever_runtime::auth::permissions().await.map(|values| List::new(values.into_iter().map(|value| T{permission} {{ f0: value.key, f1: value.component, f2: value.domain, f3: value.site, f4: value.action, f5: value.method }}).collect()))"
), result.span)
}
AuthSaveRole => self.failure(
"dever_runtime::auth::save_role(&a0, &a1, a2, a3.into_values().collect()).await",
result.span,
),
AuthGrantRole => self.failure(
"dever_runtime::auth::grant_role(a0, &a1).await",
result.span,
),
AuthRevokeRole => self.failure(
"dever_runtime::auth::revoke_role(a0, &a1).await",
result.span,
),
AuthDisableRole => self.failure(
"dever_runtime::auth::disable_role(&a0).await",
result.span,
),
SiteKey => self.failure("dever_runtime::auth::site_key()", result.span),
JobServe => self.failure("job_serve().await", result.span),
TestJobDrain => self.failure("job_drain(a0).await", result.span),
TestClockAdvance => self.failure("JOB_CLOCK.get().expect(\"test clock initialized\").advance(a0)", result.span),
HttpServe | HttpServeTls => self.http_serve(handler.expect("checked HTTP handler"), arguments, result, false, matches!(intrinsic, HttpServeTls)),
HttpServeLive | HttpServeLiveTls => self.http_serve(handler.expect("checked HTTP handler"), arguments, result, true, matches!(intrinsic, HttpServeLiveTls)),
HttpSend => self.http_send(arguments, result),
operation @ (HttpRespond | HttpStart | HttpWrite | HttpFinish | SseStart | SseSend
| WsAccept | WsConnect | WsSend | WsReceive | WsMessages | WsClose | WsOpen
| TlsClient | TlsServer | HttpClientNew | HttpCloseClient | HttpRequest | HttpOpen | HttpOpenStream) => self.live_network(operation, arguments, result),
};
format!("{{ {}\n{operation} }}", self.bind_arguments(arguments))
}
fn api_cookie_options(&self) -> String {
let (id, definition) = self
.program
.types
.iter()
.enumerate()
.find(|(_, definition)| definition.name == "dever.api.SameSite")
.expect("official SameSite type");
let Shape::Choice(variants) = &definition.shape else {
unreachable!("official SameSite choice")
};
let arms = variants
.iter()
.enumerate()
.map(|(index, variant)| {
let name = match variant.name.as_str() {
"Strict" => "Strict",
"Lax" => "Lax",
"None" => "None",
_ => unreachable!("official SameSite variant"),
};
format!("T{id}::V{index} => dever_runtime::api::SameSite::{name}")
})
.collect::<Vec<_>>()
.join(", ");
format!(
"dever_runtime::api::CookieOptions {{ path: a2.f0, domain: a2.f1, same_site: match a2.f2 {{ {arms} }}, secure: a2.f3, http_only: a2.f4, max_age: a2.f5 }}"
)
}
fn read_stream_result(&self, call: &str, output: &Type) -> String {
let Type::Named(result_id) = output else {
unreachable!("checked read stream result")
};
let Shape::Choice(result_variants) = &self.program.types[*result_id].shape else {
unreachable!()
};
let streaming = variant(result_variants, "Streaming");
let failed = variant(result_variants, "Failed");
let (Type::Stream(event) | Type::AsyncStream(event)) =
&result_variants[streaming].fields[0].ty
else {
unreachable!()
};
let Type::Named(event_id) = event.as_ref() else {
unreachable!()
};
let Shape::Choice(event_variants) = &self.program.types[*event_id].shape else {
unreachable!()
};
let chunk = variant(event_variants, "Chunk");
let event_failed = variant(event_variants, "Failed");
format!(
"match {call} {{ Ok(stream) => T{result_id}::V{streaming}(stream.map(|event| match event {{ Ok(bytes) => T{event_id}::V{chunk}(bytes), Err(message) => T{event_id}::V{event_failed}(message) }})), Err(message) => T{result_id}::V{failed}(message) }}"
)
}
fn connection_stream(&self, output: &Type) -> String {
let Type::AsyncStream(connection) = output else {
unreachable!("checked connection Stream")
};
let Type::Named(id) = connection.as_ref() else {
unreachable!()
};
let Shape::Choice(variants) = &self.program.types[*id].shape else {
unreachable!()
};
let connected = variant(variants, "Connected");
let failed = variant(variants, "Failed");
format!(
"dever_runtime::net::connections(a0).map(|event| match event {{ Ok(socket) => T{id}::V{connected}(socket), Err(message) => T{id}::V{failed}(message) }})"
)
}
pub(super) fn resource_result(
&self,
call: &str,
output: &Type,
success_name: &str,
eof: bool,
) -> String {
let Type::Named(id) = output else {
unreachable!("official result choice")
};
let Shape::Choice(variants) = &self.program.types[*id].shape else {
unreachable!()
};
let variant = |name| {
variants
.iter()
.position(|variant| variant.name == name)
.expect("checked system result contract")
};
let success = variant(success_name);
let failed = variant("Failed");
if eof {
let end = variant("End");
format!(
"match {call} {{ Ok(Some(bytes)) => T{id}::V{success}(bytes), Ok(None) => T{id}::V{end}, Err(message) => T{id}::V{failed}(message.into()) }}"
)
} else if variants[success].fields.is_empty() {
format!(
"match {call} {{ Ok(()) => T{id}::V{success}, Err(message) => T{id}::V{failed}(message.into()) }}"
)
} else {
format!(
"match {call} {{ Ok(value) => T{id}::V{success}(value), Err(message) => T{id}::V{failed}(message.into()) }}"
)
}
}
}
fn variant(variants: &[crate::types::Variant], name: &str) -> usize {
variants
.iter()
.position(|variant| variant.name == name)
.expect("checked system result contract")
}
+457
View File
@@ -0,0 +1,457 @@
use super::{Emitter, rust_string};
use crate::hir::Expression;
use crate::model::{DatabaseOwner, ModelScope};
use crate::source::Span;
use std::fmt::Write;
pub(super) fn targets(
program: &crate::hir::Program,
instances: &std::collections::BTreeSet<crate::specialize::Specialization>,
) -> std::collections::BTreeSet<usize> {
let mut targets = std::collections::BTreeSet::new();
for instance in instances {
if matches!(
program.functions[instance.function].kind,
crate::syntax::FunctionKind::Job { .. }
) {
targets.insert(instance.function);
}
crate::check::visit(&program.functions[instance.function], |expression| {
if let crate::hir::ExpressionKind::JobEnqueue { target, .. } = expression.kind {
targets.insert(target);
}
});
}
targets
}
pub(crate) fn scope(program: &crate::hir::Program, target: usize) -> ModelScope {
let instance = crate::specialize::Specialization {
function: target,
handlers: Vec::new(),
};
let component_is_tenant = program.packages[program.functions[target].owner]
.domain
.as_ref()
.is_some_and(|(component, _)| program.tenant_components.contains(component));
if component_is_tenant || crate::specialize::database_effects(program, &instance).iter().any(|owner| {
matches!(owner, DatabaseOwner::Model(model) if program.models[*model].scope == ModelScope::Tenant)
}) {
ModelScope::Tenant
} else {
ModelScope::Global
}
}
fn scope_name(scope: ModelScope) -> &'static str {
match scope {
ModelScope::Global => "Global",
ModelScope::Tenant => "Tenant",
}
}
pub(super) fn definitions(
emitter: &Emitter<'_>,
output: &mut String,
instances: &std::collections::BTreeSet<crate::specialize::Specialization>,
) {
let targets = targets(emitter.program, instances);
if targets.is_empty() && !uses_test_controls(emitter.program, instances) {
return;
}
output.push_str("static JOB_CLOCK: std::sync::OnceLock<dever_runtime::time::Clock> = std::sync::OnceLock::new();\n");
if targets.is_empty() {
output.push_str("async fn job_drain(limit: i64) -> Result<i64, String> { if !(1..=10000).contains(&limit) { return Err(\"test Job drain limit must be between 1 and 10000\".into()); } Ok(0) }\n");
return;
}
for target in &targets {
let target = *target;
let job = emitter
.program
.jobs
.iter()
.find(|job| job.function == target)
.expect("checked Job target");
if let Some(schema) = &job.payload {
output.push_str(&super::wire::emit_named(schema, &format!("job_{target}")));
}
writeln!(output, "static JOB_STORAGE_{target}: std::sync::OnceLock<dever_runtime::database::StorageBinding> = std::sync::OnceLock::new();\nasync fn job_database_{target}() -> Result<dever_runtime::database::Database, dever_runtime::orm::Error> {{ JOB_STORAGE_{target}.get().expect(\"Job storage initialized before entry\").database().await }}\nstatic JOB_SPEC_{target}: dever_runtime::job::Spec = dever_runtime::job::Spec {{ target: {}, schema: {}, attempts: {}, timeout_ms: {} }};",
rust_string(&emitter.program.functions[target].name), rust_string(job.payload.as_ref().map(|schema| schema.fingerprint()).unwrap_or("wire-v1:Job:Unit")), job.attempts, job.timeout_ms).expect("string formatting");
}
output.push_str("fn job_bindings() -> Vec<dever_runtime::job::worker::Binding> { vec![\n");
for target in &targets {
let job = emitter
.program
.jobs
.iter()
.find(|job| job.function == *target)
.expect("checked Job");
let schedule = job
.schedule
.as_ref()
.map(|cron| {
format!(
"Some(dever_runtime::cron::Cron::parse({}).expect(\"checked cron\"))",
rust_string(cron)
)
})
.unwrap_or_else(|| "None".into());
let scope = scope_name(scope(emitter.program, *target));
writeln!(output, "dever_runtime::job::worker::Binding::scoped(JOB_STORAGE_{target}.get().expect(\"Job storage initialized before entry\"), dever_runtime::database::ModelScope::{scope}, &JOB_SPEC_{target}, {schedule}),").expect("string formatting");
}
output.push_str("] }\n");
emit_identity_failure(emitter.program, output);
output.push_str("async fn job_dispatch_verified(claim: dever_runtime::job::store::Claim) -> dever_runtime::job::store::Outcome {\nuse dever_runtime::job::store::Outcome;\nmatch claim.target.as_str() {\n");
for target in &targets {
let job = emitter
.program
.jobs
.iter()
.find(|job| job.function == *target)
.expect("checked Job");
writeln!(output, "{} => {{ if claim.schema != JOB_SPEC_{target}.schema {{ return Outcome::SchemaMismatch; }}", rust_string(&emitter.program.functions[*target].name)).expect("string formatting");
let mut arguments = Vec::new();
if job.payload.is_some() {
writeln!(output, "let payload = match job_{target}_decode(&claim.payload) {{ Ok(payload) => payload, Err(_) => return Outcome::InvalidPayload }};").expect("string formatting");
arguments.push("payload");
} else {
output.push_str("if !dever_runtime::wire::parse(&claim.payload).is_ok_and(|value| value.is_null()) { return Outcome::InvalidPayload; }\n");
}
let instance = crate::specialize::Specialization {
function: *target,
handlers: Vec::new(),
};
let transaction_database = super::transaction_database(emitter.program, *target);
if let Some(database) = &transaction_database {
writeln!(output, "let transaction_database = match {database} {{ Ok(database) => database, Err(_) => return Outcome::Retry }};\nlet transaction = match transaction_database.begin().await {{ Ok(value) => value, Err(_) => return Outcome::Retry }};")
.expect("string formatting");
}
if !crate::specialize::database_effects(emitter.program, &instance).is_empty() {
arguments.push(if transaction_database.is_some() {
"Some(&transaction)"
} else {
"None"
});
}
let call = format!("{}({})", emitter.names[&instance], arguments.join(", "));
let call = if crate::specialize::suspends(emitter.program, &instance) {
format!("{call}.await")
} else {
format!("task::blocking_typed(move || {call}).await")
};
if transaction_database.is_some() {
writeln!(output, "match {call} {{ Ok(()) => match transaction.commit().await {{ Ok(()) => Outcome::Success, Err(_) => Outcome::Retry }}, Err(_) => {{ let _ = transaction.rollback().await; Outcome::Retry }} }} }},").expect("string formatting");
} else {
writeln!(
output,
"match {call} {{ Ok(()) => Outcome::Success, Err(_) => Outcome::Retry }} }},"
)
.expect("string formatting");
}
}
output.push_str("_ => Outcome::UnknownTarget,\n}\n}\n");
emit_component_check(emitter.program, output);
emit_dispatch(emitter.program, output);
output.push_str("async fn job_drain(limit: i64) -> Result<i64, String> { dever_runtime::job::worker::drain(job_bindings(), JOB_CLOCK.get().expect(\"clock initialized\").clone(), limit, job_dispatch).await }\nasync fn job_serve() -> Result<(), String> { if !dever_runtime::lifecycle::worker_enabled() { return Ok(()); } dever_runtime::job::worker::serve(job_bindings(), JOB_CLOCK.get().expect(\"clock initialized\").clone(), dever_runtime::config::settings().jobs()?, job_dispatch).await }\n");
}
fn emit_identity_failure(program: &crate::hir::Program, output: &mut String) {
let failures = program
.failures
.iter()
.flat_map(|failures| failures.iter())
.copied()
.collect::<std::collections::BTreeSet<_>>();
let unauthorized = program
.types
.iter()
.enumerate()
.find(|(_, definition)| definition.name == "dever.api.Error")
.and_then(|(id, definition)| match &definition.shape {
crate::types::Shape::Choice(variants) => Some((id, variants)),
_ => None,
});
output.push_str("fn job_identity_failure(error: AppError) -> dever_runtime::job::store::Outcome {\nuse dever_runtime::job::store::Outcome;\nmatch &error.kind {\n");
if let Some((id, variants)) = unauthorized {
for name in ["Unauthorized", "Forbidden"] {
if let Some(index) = variants.iter().position(|variant| variant.name == name)
&& failures.contains(&crate::hir::Failure {
ty: id,
variant: index,
})
{
writeln!(
output,
"AppErrorKind::E{id}V{index} => Outcome::IdentityRejected,"
)
.expect("string formatting");
}
}
}
output.push_str("_ => { dever_runtime::log::error(\"Job identity verification failed\", Vec::new()); Outcome::Retry }\n}\n}\n");
}
fn emit_dispatch(program: &crate::hir::Program, output: &mut String) {
output.push_str("async fn job_dispatch(claim: dever_runtime::job::store::Claim) -> dever_runtime::job::store::Outcome {\nuse dever_runtime::job::store::Outcome;\nmatch claim.execution.clone() {\ndever_runtime::job::ExecutionIdentity::System { .. } => { if let Err(outcome) = job_require_components(&claim).await { return outcome; } job_dispatch_verified(claim).await },\ndever_runtime::job::ExecutionIdentity::User { tenant_id, provider, site, subject, session, tenant_claim, permission_key } => {\n");
if program.auth.is_empty() || program.permissions.is_empty() {
output.push_str("let _ = (tenant_id, provider, site, subject, session, tenant_claim, permission_key);\nOutcome::InvalidIdentity\n");
} else {
output.push_str("if dever_runtime::auth::validate_job_source(&provider, &site).is_err() { return Outcome::InvalidIdentity; }\nmatch provider.as_str() {\n");
for hook in &program.auth {
let instance = crate::specialize::Specialization {
function: hook.function,
handlers: Vec::new(),
};
writeln!(output, "{} => {{", rust_string(&hook.provider)).expect("string formatting");
writeln!(output,
"let auth_claims = T{} {{ f0: subject.clone(), f1: session.clone(), f2: tenant_claim.clone(), f3: site.clone() }};",
hook.claims,
).expect("string formatting");
let mut arguments = vec!["auth_claims".to_owned()];
if !crate::specialize::database_effects(program, &instance).is_empty() {
arguments.push("None".into());
}
let call = format!(
"f{}({}){}",
hook.function,
arguments.join(", "),
if crate::specialize::suspends(program, &instance) {
".await"
} else {
""
}
);
writeln!(output,
"let identity: T{} = match {call} {{ Ok(identity) => identity, Err(error) => return job_identity_failure(error), }};\nlet verified_identity = dever_runtime::auth::Identity {{ id: identity.f0, user_id: identity.f1.map(|value| value.0), tenant_id: identity.f2.map(|value| value.0) }};\nlet verified_claims = dever_runtime::auth::Claims {{ subject, session, tenant: tenant_claim, site: site.clone() }};\nif dever_runtime::auth::validate_job_identity(&verified_claims, &verified_identity).is_err() || verified_identity.tenant_id != tenant_id {{ return Outcome::IdentityRejected; }}\nif !AUTHORIZATION_PERMISSIONS.iter().any(|permission| permission.key == permission_key && permission.site == site) {{ return Outcome::IdentityRejected; }}\nif let Err(outcome) = job_require_components(&claim).await {{ return outcome; }}\nlet Some(user_id) = verified_identity.user_id else {{ return Outcome::IdentityRejected; }};\nlet authorization_database = if tenant_id.is_some() {{ let Some(tenant) = dever_runtime::config::settings().tenant() else {{ return Outcome::InvalidIdentity; }}; match dever_runtime::tenant::database(tenant.database()).await {{ Ok(database) => database, Err(_) => return Outcome::Retry }} }} else {{ match authorization_control_database() {{ Ok(database) => database, Err(_) => return Outcome::Retry }} }};\nmatch dever_runtime::auth::store::authorize(authorization_database, user_id, &site, &permission_key).await {{ Ok(true) => job_dispatch_verified(claim).await, Ok(false) => Outcome::IdentityRejected, Err(_) => Outcome::Retry }}\n}},",
hook.identity,
).expect("string formatting");
}
output.push_str("_ => Outcome::InvalidIdentity,\n}\n");
}
output.push_str("}\n}\n}\n");
}
fn emit_component_check(program: &crate::hir::Program, output: &mut String) {
output.push_str("async fn job_require_components(claim: &dever_runtime::job::store::Claim) -> Result<(), dever_runtime::job::store::Outcome> {\nmatch claim.target.as_str() {\n");
for job in &program.jobs {
if job.components.is_empty() {
continue;
}
writeln!(output,
"{} => dever_runtime::job::require_components(&claim.execution, &[{}], TENANT_COMPONENTS).await?,",
rust_string(&program.functions[job.function].name),
job.components.iter().map(|component| rust_string(component)).collect::<Vec<_>>().join(", "),
).expect("string formatting");
}
output.push_str("_ => {},\n}\nOk(())\n}\n");
}
pub(super) fn initialize(
emitter: &Emitter<'_>,
output: &mut String,
instances: &std::collections::BTreeSet<crate::specialize::Specialization>,
span: Span,
) {
if targets(emitter.program, instances).is_empty() {
return;
}
if !emitter.sources.get(span.source).is_test() {
initialize_clock(emitter, output, instances, span);
}
let mut connections = std::collections::BTreeSet::new();
for target in targets(emitter.program, instances) {
let (explicit, root) = emitter
.program
.database_selector(DatabaseOwner::Job(target));
let explicit = explicit
.map(|name| format!("Some({})", rust_string(name)))
.unwrap_or_else(|| "None".into());
let job_scope = scope(emitter.program, target);
let scope = scope_name(job_scope);
writeln!(output, "JOB_STORAGE_{target}.set(dever_runtime::database::StorageBinding::new({explicit}, {}, dever_runtime::database::ModelScope::{scope})).map_err(|_| AppError::fault({}, \"Job storage initialized twice\"))?;\nlet job_database_{target} = if dever_runtime::config::settings().tenant().is_some() && matches!(dever_runtime::database::ModelScope::{scope}, dever_runtime::database::ModelScope::Tenant) {{ None }} else {{ Some({}) }};", rust_string(root), emitter.location(span), emitter.database_failure(&format!("job_database_{target}().await"), span)).expect("string formatting");
if connections.insert((explicit, root, job_scope == ModelScope::Tenant)) {
let initialize = emitter.database_failure(&format!("dever_runtime::job::store::initialize(job_database_{target}.as_ref().expect(\"selected Job database\").clone()).await"), span);
writeln!(
output,
"if job_database_{target}.is_some() {{ {initialize}; }}"
)
.expect("string formatting");
}
let job = emitter
.program
.jobs
.iter()
.find(|job| job.function == target)
.expect("checked Job");
if let Some(cron) = &job.schedule {
let validate = emitter.database_failure(&format!("dever_runtime::job::store::validate_schedule(job_database_{target}.as_ref().expect(\"selected Job database\").clone(), &JOB_SPEC_{target}, &dever_runtime::cron::Cron::parse({}).expect(\"checked cron\")).await", rust_string(cron)), span);
writeln!(
output,
"if job_database_{target}.is_some() {{ {validate}; }}"
)
.expect("string formatting");
}
}
}
pub(super) fn tenant_migration(
emitter: &Emitter<'_>,
output: &mut String,
instances: &std::collections::BTreeSet<crate::specialize::Specialization>,
database_enabled: bool,
) {
if !database_enabled {
return;
}
let targets = targets(emitter.program, instances);
if !targets
.iter()
.any(|target| scope(emitter.program, *target) == ModelScope::Tenant)
{
// ORM tenant migration calls the Job migration hook unconditionally.
// Keep that generated contract total for applications without tenant
// Jobs instead of leaving an unresolved function reference.
output.push_str("async fn job_migrate_tenant(_tenant_id: i64, _schema_fingerprint: &str, _databases: &mut std::collections::BTreeMap<String, dever_runtime::database::Database>) -> Result<(), AppError> { Ok(()) }\n");
return;
}
output.push_str("async fn job_migrate_tenant(tenant_id: i64, schema_fingerprint: &str, databases: &mut std::collections::BTreeMap<String, dever_runtime::database::Database>) -> Result<(), AppError> {\nlet settings = dever_runtime::config::settings();\n");
let span = targets
.iter()
.next()
.map(|target| emitter.program.functions[*target].span);
let mut connections = std::collections::BTreeMap::new();
for target in targets
.into_iter()
.filter(|target| scope(emitter.program, *target) == ModelScope::Tenant)
{
let (explicit, root) = emitter
.program
.database_selector(DatabaseOwner::Job(target));
let explicit_code = explicit
.map(|name| format!("Some({})", rust_string(name)))
.unwrap_or_else(|| "None".into());
let identity = (explicit.map(str::to_owned), root.to_owned());
let connection_target = *connections.entry(identity).or_insert(target);
if connection_target == target {
writeln!(output, "let job_connection_{target} = settings.resolve_database({explicit_code}, {}).map_err(|error| AppError::fault(\"tenant Job migration\", error))?.0.to_owned();\nif !databases.contains_key(&job_connection_{target}) {{ let database = dever_runtime::tenant::begin_migration(&job_connection_{target}, tenant_id, schema_fingerprint).await.map_err(|error| AppError::database(\"tenant Job migration\", error))?; databases.insert(job_connection_{target}.clone(), database); }}\n{};", rust_string(root), emitter.database_failure(&format!("dever_runtime::job::store::initialize(databases.get(&job_connection_{target}).expect(\"tenant Job database initialized\").clone()).await"), span.expect("tenant Job has source span"))).expect("string formatting");
}
let job = emitter
.program
.jobs
.iter()
.find(|job| job.function == target)
.expect("checked Job");
if let Some(cron) = &job.schedule {
let validate = emitter.database_failure(&format!("dever_runtime::job::store::validate_schedule(databases.get(&job_connection_{connection_target}).expect(\"tenant Job database initialized\").clone(), &JOB_SPEC_{target}, &dever_runtime::cron::Cron::parse({}).expect(\"checked cron\")).await", rust_string(cron)), span.expect("tenant Job has source span"));
writeln!(output, "{validate};").expect("string formatting");
}
}
output.push_str("Ok(())\n}\n");
}
fn uses_test_controls(
program: &crate::hir::Program,
instances: &std::collections::BTreeSet<crate::specialize::Specialization>,
) -> bool {
instances.iter().any(|instance| {
let mut found = false;
crate::check::visit(&program.functions[instance.function], |expression| {
if let crate::hir::ExpressionKind::Intrinsic {
operation:
crate::intrinsic::Intrinsic::TestJobDrain
| crate::intrinsic::Intrinsic::TestClockAdvance,
..
} = expression.kind
{
found = true;
}
});
found
})
}
pub(super) fn initialize_clock(
emitter: &Emitter<'_>,
output: &mut String,
instances: &std::collections::BTreeSet<crate::specialize::Specialization>,
span: Span,
) {
if targets(emitter.program, instances).is_empty()
&& !uses_test_controls(emitter.program, instances)
{
return;
}
let clock = if emitter.sources.get(span.source).is_test() {
"for_test"
} else {
"default"
};
writeln!(output, "JOB_CLOCK.set(dever_runtime::time::Clock::{clock}()).map_err(|_| AppError::fault({}, \"Job clock initialized twice\"))?;", emitter.location(span)).expect("string formatting");
}
pub(super) fn preflight(
emitter: &Emitter<'_>,
output: &mut String,
instances: &std::collections::BTreeSet<crate::specialize::Specialization>,
span: Span,
) {
for target in targets(emitter.program, instances) {
writeln!(
output,
"{};",
emitter.database_failure(&format!("JOB_SPEC_{target}.validate()"), span)
)
.expect("string formatting");
let (explicit, root) = emitter
.program
.database_selector(DatabaseOwner::Job(target));
writeln!(output, "if dever_runtime::lifecycle::worker_enabled() {{ dever_runtime::config::settings().validate_job_connection({}, {}, JOB_SPEC_{target}.timeout_ms)?; }}", explicit.map(|name| format!("Some({})", rust_string(name))).unwrap_or_else(|| "None".into()), rust_string(root)).expect("string formatting");
}
}
impl Emitter<'_> {
pub(super) fn database_owner_at(&self, owner: DatabaseOwner) -> String {
match owner {
DatabaseOwner::Model(id) => self.database_at(&self.program.models[id]),
DatabaseOwner::Job(function) => format!("_job_database_{function}.clone()"),
}
}
pub(super) fn enqueue_job(
&self,
target: usize,
arguments: &[Expression],
scheduled: bool,
span: Span,
) -> String {
let job = self
.program
.jobs
.iter()
.find(|job| job.function == target)
.expect("checked Job");
let mut output = String::from("{");
for (index, argument) in arguments.iter().enumerate() {
write!(output, "let j{index} = {};", self.expression(argument))
.expect("string formatting");
}
let payload = if job.payload.is_some() {
self.failure(&format!("job_{target}_encode(&j0)"), span)
} else {
"dever_runtime::wire::Encoded::null()".into()
};
let key = usize::from(job.payload.is_some());
let run_at = if scheduled {
format!("j{}", key + 1)
} else {
self.database_failure("JOB_CLOCK.get().expect(\"clock initialized\").now()", span)
};
let database = self.database_failure(&format!("job_database_{target}().await"), span);
let call = format!(
"dever_runtime::job::enqueue({database}, _database, &JOB_SPEC_{target}, {payload}, &j{key}, {run_at}).await"
);
output.push_str(&self.database_failure(&call, span));
output.push('}');
output
}
}
+140
View File
@@ -0,0 +1,140 @@
use std::collections::HashSet;
use crate::hir::{CallArgument, Clause, Expression, ExpressionKind, Statement};
/// HIR 在发射期间不可变;节点地址只标识同一子句中的具体读取,不进入生成程序。
pub(crate) fn last_uses(clause: &Clause) -> HashSet<*const Expression> {
let mut live: HashSet<usize> = clause.outputs.iter().copied().collect();
let mut moves = HashSet::new();
for statement in clause.body.iter().rev() {
let mut overwritten = None;
let value = match statement {
Statement::Assign {
slot,
fields,
value,
} => {
if fields.is_empty() {
live.remove(slot);
} else {
// 字段赋值必须保留宿主的其余字段,不能在右侧读值时移动宿主。
live.insert(*slot);
overwritten = Some((*slot, fields.clone()));
}
value
}
Statement::Call(value) => value,
};
visit(value, &mut live, &mut moves, &mut overwritten);
}
moves
}
fn visit(
expression: &Expression,
live: &mut HashSet<usize>,
moves: &mut HashSet<*const Expression>,
overwritten: &mut Option<(usize, Vec<usize>)>,
) {
use ExpressionKind::*;
if let Option::Some((slot, path)) = super::updates::place(expression) {
if let Option::Some((target, fields)) = overwritten
&& *target == slot
&& super::updates::overlaps(fields, &path)
{
// 右侧最后一次重叠读取必须恰好是被覆盖字段,不能提前移动整个宿主。
if *fields == path {
moves.insert(expression as *const Expression);
}
*overwritten = None;
}
let mut base = expression;
while let Field { value, .. } = &base.kind {
base = value;
}
if live.insert(slot) {
moves.insert(base as *const Expression);
}
return;
}
match &expression.kind {
Local(slot) => {
if live.insert(*slot) {
moves.insert(expression as *const Expression);
}
}
Constant(_) | ChoiceOptions { .. } => {}
ModelOperation { operation, .. } => {
for value in operation.expressions().into_iter().rev() {
visit(value, live, moves, overwritten);
}
}
JobEnqueue { arguments, .. } => {
for value in arguments.iter().rev() {
visit(value, live, moves, overwritten);
}
}
Field { value, .. }
| Fail(value)
| AwaitTask(value)
| StopTask(value)
| Group(value)
| AwaitGroup(value)
| StopGroup(value)
| ChannelReceive(value)
| ChannelClose(value)
| Promote(value)
| Some(value)
| Unary { value, .. } => visit(value, live, moves, overwritten),
Call { arguments, .. }
| CaptureResult { arguments, .. }
| ParallelCall { arguments, .. }
| BlockingCall { arguments, .. } => {
for argument in arguments.iter().rev() {
if let CallArgument::Value(value) = argument {
visit(value, live, moves, overwritten);
}
}
}
RunCall {
group, arguments, ..
} => {
for argument in arguments.iter().rev() {
if let CallArgument::Value(value) = argument {
visit(value, live, moves, overwritten);
}
}
if let Option::Some(group) = group {
visit(group, live, moves, overwritten);
}
}
Channel { capacity, .. } => visit(capacity, live, moves, overwritten),
ChannelSend { channel, value } => {
visit(value, live, moves, overwritten);
visit(channel, live, moves, overwritten);
}
Intrinsic { arguments, .. }
| Collection { arguments, .. }
| Variant { arguments, .. }
| List(arguments) => {
for argument in arguments.iter().rev() {
visit(argument, live, moves, overwritten);
}
}
Record { fields } => {
for (_, value) in fields.iter().rev() {
visit(value, live, moves, overwritten);
}
}
Map(entries) => {
for (key, value) in entries.iter().rev() {
visit(value, live, moves, overwritten);
visit(key, live, moves, overwritten);
}
}
Binary { left, right, .. } => {
visit(right, live, moves, overwritten);
visit(left, live, moves, overwritten);
}
}
}
File diff suppressed because it is too large. Load diff
+368
View File
@@ -0,0 +1,368 @@
use super::*;
pub(super) fn definitions(
emitter: &Emitter<'_>,
output: &mut String,
instances: &BTreeSet<Specialization>,
) {
for instance in instances {
let id = instance.function;
let function = &emitter.program.functions[id];
if function.port.is_some() {
writeln!(
output,
"static PORT_{id}: std::sync::OnceLock<usize> = std::sync::OnceLock::new();"
)
.expect("string formatting");
}
}
for adapter in &emitter.program.adapters {
let Some(ty) = adapter.setting else {
continue;
};
if !adapter
.operations
.values()
.any(|id| instances.iter().any(|instance| instance.function == *id))
{
continue;
}
let schema = crate::wire::Schema::build(
&Type::Named(ty),
&emitter.program.types,
crate::wire::Policy::SettingInput,
)
.expect("checked setting schema");
// Type IDs occupy their own range after API route schema indexes.
let codec = emitter.program.api_routes.len() + ty;
output.push_str(&wire::emit(&schema, codec));
writeln!(
output,
"static ADAPTER_SETTING_{}: std::sync::OnceLock<T{ty}> = std::sync::OnceLock::new();",
adapter.owner
)
.expect("string formatting");
}
let external = instances
.iter()
.filter_map(|instance| {
emitter.program.functions[instance.function]
.external
.map(|_| instance.function)
})
.collect::<BTreeSet<_>>();
for implementation in external {
external_codecs(emitter, output, implementation);
}
}
pub(super) fn function(
emitter: &Emitter<'_>,
output: &mut String,
instance: &Specialization,
) -> bool {
let function = &emitter.program.functions[instance.function];
if function.setting.is_some() {
writeln!(output, "Ok(ADAPTER_SETTING_{}.get().expect(\"Adapter setting initialized before entry\").clone())\n}}", function.owner).expect("string formatting");
return true;
}
if let Some(external) = function.external {
let adapter = &emitter.program.adapters[external.adapter];
let contract = emitter.program.functions[external.contract]
.port
.as_ref()
.expect("external Port contract");
let arguments = (0..function.parameters.len())
.map(|index| format!("&_p{index}"))
.collect::<Vec<_>>()
.join(", ");
let location = emitter.location(function.span);
writeln!(output, "let payload = external_{0}_inputs({arguments}).map_err(|error| AppError::fault({location}, error))?;", instance.function).expect("string formatting");
writeln!(output, "match dever_runtime::component::call({}, {}, &payload).await.map_err(|error| AppError::fault({location}, error))? {{", rust_string(&external_key(adapter)), rust_string(&contract.operation)).expect("string formatting");
writeln!(output, "dever_runtime::component::Reply::Result(payload) => external_{0}_outputs(&payload).map_err(|error| AppError::fault({location}, error)),", instance.function).expect("string formatting");
writeln!(output, "dever_runtime::component::Reply::Error {{ identity, payload }} => Err(external_{0}_error(&identity, &payload, {location}).map_err(|error| AppError::fault({location}, error))?),\n}}\n}}", instance.function).expect("string formatting");
return true;
}
let Some(port) = &function.port else {
return false;
};
if port.implementations.is_empty() {
output.push_str("unreachable!(\"checked Port has an implementation\")\n}\n");
return true;
}
let arguments = (0..function.parameters.len())
.map(|index| format!("_p{index}"))
.collect::<Vec<_>>()
.join(", ");
let invoke = |id: usize| {
let target = Specialization {
function: id,
handlers: Vec::new(),
};
format!(
"{}({arguments}){}",
emitter.names[&target],
if specialize::suspends(emitter.program, &target) {
".await"
} else {
""
}
)
};
if let [implementation] = port.implementations.as_slice() {
writeln!(output, "{}\n}}", invoke(*implementation)).expect("string formatting");
} else {
writeln!(
output,
"match *PORT_{}.get().expect(\"Port initialized before entry\") {{",
instance.function
)
.expect("string formatting");
for implementation in &port.implementations {
writeln!(output, "{implementation} => {},", invoke(*implementation))
.expect("string formatting");
}
output.push_str("_ => unreachable!(\"closed Port binding\"),\n}\n}\n");
}
true
}
pub(super) fn initialize(
emitter: &Emitter<'_>,
output: &mut String,
instances: &BTreeSet<Specialization>,
span: Span,
) -> Result<(), String> {
let program = emitter.program;
let ports: BTreeSet<_> = instances
.iter()
.filter_map(|instance| {
program.functions[instance.function]
.port
.as_ref()
.map(|_| program.functions[instance.function].owner)
})
.collect();
if ports.is_empty() {
return Ok(());
}
let identities: BTreeSet<_> = program
.functions
.iter()
.filter_map(|function| {
function
.port
.as_ref()
.map(|port| rust_string(&port.identity))
})
.collect();
let location = emitter.location(span);
let asynchronous = has_external(program, instances);
let closure = if asynchronous {
"|| async {"
} else {
"|| -> Result<(), String> {"
};
let settings = if asynchronous {
"dever_runtime::component::adapter_settings()"
} else {
"dever_runtime::config::Settings::load_adapter_settings()"
};
writeln!(output, "let initialize_ports = {closure}\nlet settings = {settings}?;\nsettings.validate_adapter_names(&[{}])?;", identities.into_iter().collect::<Vec<_>>().join(", ")).expect("string formatting");
for owner in ports {
let adapters: Vec<_> = program
.adapters
.iter()
.filter(|adapter| !adapter.fake && adapter.port_owner == owner)
.collect();
if adapters.is_empty() {
return Err("reachable Port has no production Adapter".into());
}
let identity = &adapters[0].identity;
let names = adapters
.iter()
.map(|adapter| rust_string(&adapter.name))
.collect::<Vec<_>>()
.join(", ");
writeln!(output, "let (selected, setting) = settings.select_adapter({}, &[{names}])?;\nmatch selected {{", rust_string(identity)).expect("string formatting");
for (index, adapter) in adapters.iter().enumerate() {
writeln!(output, "{index} => {{").expect("string formatting");
if let Some(ty) = adapter.setting {
let codec = program.api_routes.len() + ty;
writeln!(output, "let value = wire_{codec}_decode(setting.as_deref().ok_or(\"selected Adapter requires setting\")?)?;\nADAPTER_SETTING_{}.set(value).map_err(|_| String::from(\"Adapter setting initialized twice\"))?;", adapter.owner).expect("string formatting");
} else {
output.push_str("if setting.is_some() { return Err(String::from(\"selected Adapter does not declare setting\")); }\n");
}
if let crate::hir::AdapterImplementation::External(external) = &adapter.implementation {
let capabilities = external
.capabilities
.iter()
.map(|capability| format!("{}.into()", rust_string(capability)))
.collect::<Vec<_>>()
.join(", ");
let operations = adapter
.operations
.keys()
.map(|port| {
format!(
"{}.into()",
rust_string(
&program.functions[*port]
.port
.as_ref()
.expect("Port operation")
.operation,
)
)
})
.collect::<Vec<_>>()
.join(", ");
let setting = if adapter.setting.is_some() {
"setting"
} else {
"None"
};
writeln!(output, "dever_runtime::component::start(dever_runtime::component::Definition {{ key: {}.into(), ecosystem: {}.into(), entry: {}.into(), port: {}.into(), adapter: {}.into(), schema: {}.into(), capabilities: vec![{capabilities}], operations: vec![{operations}], setting: {setting}, timeout_ms: 30_000 }}).await?;", rust_string(&external_key(adapter)), rust_string(external.ecosystem.keyword()), rust_string(&external.entry), rust_string(&adapter.identity), rust_string(&adapter.name), rust_string(&external.schema)).expect("string formatting");
}
for (port, implementation) in &adapter.operations {
if instances.iter().any(|instance| instance.function == *port) {
writeln!(output, "PORT_{port}.set({implementation}).map_err(|_| String::from(\"Port initialized twice\"))?;").expect("string formatting");
}
}
output.push_str("},\n");
}
output.push_str("_ => unreachable!(\"validated Adapter selection\"),\n}\n");
}
writeln!(output, "Ok::<(), String>(())\n}};\ninitialize_ports(){}.map_err(|error| AppError::fault({location}, error))?;", if asynchronous { ".await" } else { "" }).expect("string formatting");
Ok(())
}
pub(super) fn has_external(program: &Program, instances: &BTreeSet<Specialization>) -> bool {
instances
.iter()
.any(|instance| program.functions[instance.function].external.is_some())
}
fn external_key(adapter: &crate::hir::Adapter) -> String {
format!("{}:{}", adapter.identity, adapter.name)
}
fn external_codecs(emitter: &Emitter<'_>, output: &mut String, implementation: usize) {
let function = &emitter.program.functions[implementation];
let external = function.external.expect("external implementation");
let crate::hir::AdapterImplementation::External(adapter) =
&emitter.program.adapters[external.adapter].implementation
else {
unreachable!("external implementation Adapter")
};
let policy = crate::wire::Policy::external(adapter.ecosystem);
let contract = emitter.program.functions[external.contract]
.port
.as_ref()
.expect("Port contract");
let mut input_schemas = Vec::new();
for (index, parameter) in function.parameters.iter().enumerate() {
let field = crate::types::Field {
name: contract.input_names[index].clone(),
ty: parameter
.value_type()
.expect("Port value parameter")
.clone(),
private: false,
bounds: Vec::new(),
};
let schema = crate::wire::Schema::from_field(&field, &emitter.program.types, policy)
.expect("checked external input schema");
let prefix = format!("external_{implementation}_input_{index}");
output.push_str(&wire::emit_named(&schema, &prefix));
input_schemas.push((field, schema, prefix));
}
let input_parameters = input_schemas
.iter()
.enumerate()
.map(|(index, (field, _, _))| format!("p{index}: &{}", rust_type(&field.ty)))
.collect::<Vec<_>>()
.join(", ");
writeln!(output, "fn external_{implementation}_inputs({input_parameters}) -> Result<dever_runtime::wire::Encoded, String> {{\nlet mut writer = dever_runtime::wire::Encoder::default();\nwriter.begin_object()?;").expect("string formatting");
for (index, (field, schema, prefix)) in input_schemas.iter().enumerate() {
writeln!(
output,
"writer.key({})?; {prefix}_e{}(p{index}, &mut writer)?;",
rust_string(&field.name),
schema.root
)
.expect("string formatting");
}
output.push_str("writer.end()?; writer.finish()\n}\n");
let mut output_schemas = Vec::new();
for (index, field) in function.outputs.iter().enumerate() {
let schema = crate::wire::Schema::from_field(field, &emitter.program.types, policy)
.expect("checked external output schema");
let prefix = format!("external_{implementation}_output_{index}");
output.push_str(&wire::emit_named(&schema, &prefix));
output_schemas.push((field, schema, prefix));
}
let output_names = function
.outputs
.iter()
.map(|field| rust_string(&field.name))
.collect::<Vec<_>>()
.join(", ");
writeln!(output, "fn external_{implementation}_outputs(text: &str) -> Result<{}, String> {{\nlet node = dever_runtime::wire::parse(text)?;\nlet fields = node.fields(&[{output_names}])?;\nif fields.len() != {} {{ return Err(\"external Adapter result has missing fields\".into()); }}", rust_type(&output_type(&function.outputs)), function.outputs.len()).expect("string formatting");
for (index, (field, schema, prefix)) in output_schemas.iter().enumerate() {
writeln!(output, "let value_{index} = {prefix}_d{}(fields.get({}).ok_or(\"external Adapter result has missing fields\")?)?;", schema.root, rust_string(&field.name)).expect("string formatting");
}
let value = match function.outputs.len() {
0 => "()".into(),
1 => "value_0".into(),
count => format!(
"({})",
(0..count)
.map(|index| format!("value_{index}"))
.collect::<Vec<_>>()
.join(", ")
),
};
writeln!(output, "Ok({value})\n}}").expect("string formatting");
writeln!(output, "fn external_{implementation}_error(identity: &str, text: &str, location: &'static str) -> Result<AppError, String> {{\nmatch identity {{").expect("string formatting");
for failure in &contract.failures {
let crate::types::Shape::Choice(variants) = &emitter.program.types[failure.ty].shape else {
unreachable!()
};
let variant = &variants[failure.variant];
let identity = format!(
"{}.{}",
emitter.program.types[failure.ty].name, variant.name
);
writeln!(output, "{} => {{\nlet node = dever_runtime::wire::parse(text)?;\nlet fields = node.fields(&[{}])?;\nif fields.len() != {} {{ return Err(\"external Adapter error has missing fields\".into()); }}", rust_string(&identity), variant.fields.iter().map(|field| rust_string(&field.name)).collect::<Vec<_>>().join(", "), variant.fields.len()).expect("string formatting");
for (index, field) in variant.fields.iter().enumerate() {
let schema = crate::wire::Schema::from_field(field, &emitter.program.types, policy)
.expect("checked external error schema");
let prefix = format!("external_{implementation}_error_{}_{}", failure.ty, index);
output.push_str(&wire::emit_named(&schema, &prefix));
writeln!(output, "let field_{index} = {prefix}_d{}(fields.get({}).ok_or(\"external Adapter error has missing fields\")?)?;", schema.root, rust_string(&field.name)).expect("string formatting");
}
let payload = if variant.fields.is_empty() {
String::new()
} else {
format!(
"({})",
(0..variant.fields.len())
.map(|index| format!("field_{index}"))
.collect::<Vec<_>>()
.join(", ")
)
};
writeln!(
output,
"Ok(AppError::business(AppErrorKind::E{}V{}{payload}, location))\n}},",
failure.ty, failure.variant
)
.expect("string formatting");
}
output.push_str(
"_ => Err(\"external Adapter returned an undeclared business error\".into()),\n}\n}\n",
);
}
+214
View File
@@ -0,0 +1,214 @@
use std::fmt::Write;
use crate::types::{DefinitionKind, Shape, Type};
use super::{Emitter, rust_string, rust_type};
impl Emitter<'_> {
pub(super) fn types(&self, output: &mut String) {
for (id, definition) in self.program.types.iter().enumerate() {
if definition.kind == DefinitionKind::ModelId {
writeln!(
output,
"#[derive(Clone, Debug, Eq, Hash, PartialEq)]\nstruct T{id}(i64);\nimpl Render for T{id} {{ fn render_to(&self, output: &mut String) {{ self.0.render_to(output); }} }}"
)
.expect("string formatting");
continue;
}
let equality = if Type::Named(id).comparable(&self.program.types) {
", PartialEq"
} else {
""
};
let key = matches!(&definition.shape, Shape::Choice(variants) if variants.iter().all(|variant| variant.fields.is_empty()));
writeln!(
output,
"#[derive(Clone, Debug{equality}{})]",
if key { ", Eq, Hash" } else { "" }
)
.expect("string formatting");
match &definition.shape {
Shape::Record(fields) => {
writeln!(output, "struct T{id} {{").expect("string formatting");
for (index, field) in fields.iter().enumerate() {
writeln!(output, "f{index}: {},", rust_type(&field.ty))
.expect("string formatting");
}
output.push_str("}\n");
if !Type::Named(id).observable(&self.program.types) {
continue;
}
writeln!(
output,
"impl Render for T{id} {{ fn render_to(&self, output: &mut String) {{"
)
.expect("string formatting");
writeln!(
output,
"output.push_str({}); output.push_str(\" {{ \");",
rust_string(&definition.name)
)
.expect("string formatting");
for (index, field) in fields.iter().enumerate() {
if index != 0 {
output.push_str("output.push_str(\", \");\n");
}
writeln!(
output,
"output.push_str({}); output.push_str(\" = \"); self.f{index}.render_to(output);",
rust_string(&field.name)
)
.expect("string formatting");
}
output.push_str("output.push_str(\" }\");\n} }\n");
}
Shape::Choice(variants) => {
writeln!(output, "enum T{id} {{").expect("string formatting");
for (index, variant) in variants.iter().enumerate() {
let payload = if variant.fields.is_empty() {
String::new()
} else {
format!(
"({})",
variant
.fields
.iter()
.map(|field| rust_type(&field.ty))
.collect::<Vec<_>>()
.join(", ")
)
};
writeln!(output, "V{index}{payload},").expect("string formatting");
}
output.push_str("}\n");
if !Type::Named(id).observable(&self.program.types) {
continue;
}
writeln!(
output,
"impl Render for T{id} {{ fn render_to(&self, output: &mut String) {{ match self {{"
)
.expect("string formatting");
for (index, variant) in variants.iter().enumerate() {
let name = rust_string(&format!("{}.{}", definition.name, variant.name));
if variant.fields.is_empty() {
writeln!(output, "Self::V{index} => output.push_str({name}),")
.expect("string formatting");
} else {
let bindings = (0..variant.fields.len())
.map(|index| format!("field{index}"))
.collect::<Vec<_>>();
write!(
output,
"Self::V{index}({}) => {{ output.push_str({name}); output.push('(');",
bindings.join(", ")
)
.expect("string formatting");
for (field, binding) in bindings.iter().enumerate() {
if field != 0 {
output.push_str("output.push_str(\", \");");
}
write!(output, "{binding}.render_to(output);")
.expect("string formatting");
}
output.push_str("output.push(')'); },\n");
}
}
output.push_str("} } }\n");
}
}
}
}
pub(super) fn errors(&self, output: &mut String) {
let failures = self
.program
.failures
.iter()
.flat_map(|failures| failures.iter())
.copied()
.chain(crate::capture::database_errors(&self.program.types))
.collect::<std::collections::BTreeSet<_>>();
output.push_str("#[derive(Debug)]\nenum AppErrorKind {\n");
for failure in &failures {
let Shape::Choice(variants) = &self.program.types[failure.ty].shape else {
unreachable!("failure type is a choice")
};
let variant = &variants[failure.variant];
if variant.fields.is_empty() {
writeln!(output, "E{}V{},", failure.ty, failure.variant)
.expect("string formatting");
} else {
writeln!(
output,
"E{}V{}({}),",
failure.ty,
failure.variant,
variant
.fields
.iter()
.map(|field| rust_type(&field.ty))
.collect::<Vec<_>>()
.join(", ")
)
.expect("string formatting");
}
}
output.push_str("Fault(String),\n}\n#[derive(Debug)]\nstruct AppError { kind: AppErrorKind, locations: Vec<&'static str>, causes: Vec<String> }\n");
output.push_str("impl AppError {\nfn business(kind: AppErrorKind, location: &'static str) -> Self { Self { kind, locations: vec![location], causes: Vec::new() } }\nfn fault(location: &'static str, error: impl std::fmt::Display) -> Self { Self { kind: AppErrorKind::Fault(error.to_string()), locations: vec![location], causes: Vec::new() } }\nfn at(mut self, location: &'static str) -> Self { self.locations.push(location); self }\nfn cause(mut self, error: impl std::fmt::Display) -> Self { self.causes.push(error.to_string()); self }\n}\n");
output.push_str("impl From<String> for AppError { fn from(message: String) -> Self { Self { kind: AppErrorKind::Fault(message), locations: Vec::new(), causes: Vec::new() } } }\n");
output.push_str("impl AppError { fn database(location: &'static str, error: dever_runtime::orm::Error) -> Self { let kind = match error.kind() {\n");
for failure in crate::capture::database_errors(&self.program.types) {
let Shape::Choice(variants) = &self.program.types[failure.ty].shape else {
unreachable!("database error choice")
};
writeln!(
output,
"dever_runtime::orm::ErrorKind::{} => AppErrorKind::E{}V{}(error.to_string()),",
variants[failure.variant].name, failure.ty, failure.variant
)
.expect("string formatting");
}
output.push_str("}; Self::business(kind, location) } }\n");
output.push_str("impl std::fmt::Display for AppError { fn fmt(&self, output: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { for location in self.locations.iter().rev() { write!(output, \"{location}: \")?; } match &self.kind {\n");
for failure in &failures {
let Shape::Choice(variants) = &self.program.types[failure.ty].shape else {
unreachable!("failure type is a choice")
};
let variant = &variants[failure.variant];
let name = rust_string(&format!(
"{}.{}",
self.program.types[failure.ty].name, variant.name
));
if variant.fields.is_empty() {
writeln!(
output,
"AppErrorKind::E{}V{} => output.write_str({name}),",
failure.ty, failure.variant
)
.expect("string formatting");
} else {
let bindings = (0..variant.fields.len())
.map(|index| format!("field{index}"))
.collect::<Vec<_>>();
write!(
output,
"AppErrorKind::E{}V{}({}) => {{ let mut rendered = String::from({name}); rendered.push('(');",
failure.ty,
failure.variant,
bindings.join(", ")
)
.expect("string formatting");
for (index, binding) in bindings.iter().enumerate() {
if index != 0 {
output.push_str("rendered.push_str(\", \");");
}
write!(output, "{binding}.render_to(&mut rendered);")
.expect("string formatting");
}
output.push_str("rendered.push(')'); output.write_str(&rendered) },\n");
}
}
output.push_str("AppErrorKind::Fault(message) => output.write_str(message),\n}?; for cause in &self.causes { write!(output, \"; caused by: {cause}\")?; } Ok(()) } }\nimpl std::error::Error for AppError {}\n");
}
}
+491
View File
@@ -0,0 +1,491 @@
use std::fmt::Write;
use crate::hir::{ApiRest, Program};
use crate::model::{
ModelFieldType, ModelSchema, RestContract, RestOwnerSource, RestValueSource, RestWriteField,
};
use crate::types::{Shape, Type};
use crate::wire::{Policy, Schema};
use super::orm::{database_sql, parameter_sql, quoted, select_columns};
use super::rust_string;
pub(super) fn emit_helpers(program: &Program, output: &mut String) {
let mut emitted = std::collections::BTreeSet::new();
for route in &program.api_rest {
let model = &program.models[route.model];
if !emitted.insert(model.record) {
continue;
}
let contract = &route.contract;
let Shape::Record(fields) = &program.types[model.record].shape else {
unreachable!()
};
for &index in &contract.read {
let schema = Schema::build(&fields[index].ty, &program.types, Policy::Output)
.expect("checked REST output field");
output.push_str(&super::wire::emit_named(
&schema,
&format!("rest_{}_f{index}_out", model.record),
));
}
let inputs = contract
.create
.iter()
.chain(&contract.replace)
.filter_map(|write| match write.source {
RestValueSource::Input
| RestValueSource::Binding {
uses_input: true, ..
} => Some(write.field),
RestValueSource::Binding {
uses_input: false, ..
} => None,
})
.chain(contract.search.iter().map(|search| search.field))
.collect::<std::collections::BTreeSet<_>>();
for index in inputs {
let schema = Schema::build(&fields[index].ty, &program.types, Policy::ApiInput)
.expect("checked REST input field");
output.push_str(&super::wire::emit_named(
&schema,
&format!("rest_{}_f{index}_in", model.record),
));
}
writeln!(
output,
"fn rest_{}_encode(value: &T{}) -> Result<dever_runtime::wire::Encoded, String> {{",
model.record, model.record
)
.expect("format");
output.push_str(
"let mut writer = dever_runtime::wire::Encoder::default(); writer.begin_object()?;\n",
);
for &index in &contract.read {
writeln!(output, "writer.key({})?; writer.json(rest_{}_f{index}_out_encode(&value.f{index})?.as_str())?;", rust_string(&model.fields[index].name), model.record).expect("format");
}
output.push_str("writer.end()?; writer.finish()\n}\n");
}
}
/// Emit a REST match arm body. The caller owns routing and supplies the checked method/detail pair.
pub(super) fn emit_handler(
emitter: &super::Emitter<'_>,
route: &ApiRest,
method: &str,
detail: bool,
output: &mut String,
) {
let program = emitter.program;
let model = &program.models[route.model];
let contract = &route.contract;
let table = quoted(&model.table);
let id_condition = format!("{} = ?1", quoted("id"));
if detail {
writeln!(output, "let rest_id = match path.strip_prefix({}).and_then(|id| id.parse::<i64>().ok()).filter(|id| *id > 0) {{ Some(id) => id, None => return Ok(dever_runtime::api::not_found()) }};", rust_string(&format!("{}/", route.path))).expect("format");
}
match (method, detail) {
("GET", false) => {
output.push_str("let mut inputs = match dever_runtime::api::Inputs::from_request(&request) { Ok(inputs) => inputs, Err(error) => return Ok(dever_runtime::api::invalid_input(error)) };\n");
output.push_str("let page = match inputs.optional_int(\"page\") { Ok(value) => value.unwrap_or(1), Err(error) => return Ok(dever_runtime::api::invalid_input(error)) };\n");
output.push_str("let size = match inputs.optional_int(\"size\") { Ok(value) => value.unwrap_or(20), Err(error) => return Ok(dever_runtime::api::invalid_input(error)) };\n");
parse_search(emitter, model, contract, output);
finish_inputs(output);
owner_value(output, contract);
database(output, model);
output.push_str("let (page, size, offset) = match dever_runtime::orm::pagination(page, size, database.max_page_size()) { Ok(value) => value, Err(error) => return Ok(dever_runtime::api::invalid_input(dever_runtime::api::InputError(error.to_string()))) };\n");
let (owner_filter, count_parameters, page_parameters, limit_slot) = match contract.owner
{
Some((index, _)) => (
format!(" WHERE {} = ?1", quoted(&model.fields[index].name)),
"vec![dever_runtime::orm::Value::Int(rest_owner_id)]",
"vec![dever_runtime::orm::Value::Int(rest_owner_id), dever_runtime::orm::Value::Int(size), dever_runtime::orm::Value::Int(offset)]",
2,
),
None => (
String::new(),
"vec![]",
"vec![dever_runtime::orm::Value::Int(size), dever_runtime::orm::Value::Int(offset)]",
1,
),
};
let count = format!("SELECT COUNT(*) FROM {table}{owner_filter}");
let list = format!(
"SELECT {} FROM {table}{owner_filter} ORDER BY {} DESC LIMIT ?{limit_slot} OFFSET ?{}",
select_columns(model),
quoted("id"),
limit_slot + 1
);
if let Some(search) = &contract.search {
let column = quoted(&model.fields[search.field].name);
let search_slot = usize::from(contract.owner.is_some()) + 1;
let search_parameter = parameter_sql(search_slot, &model.fields[search.field].ty);
let search_filter = if contract.owner.is_some() {
format!("{owner_filter} AND {column} = {search_parameter}")
} else {
format!(" WHERE {column} = {search_parameter}")
};
let search_count = format!("SELECT COUNT(*) FROM {table}{search_filter}");
let search_limit = search_slot + 1;
let search_list = format!(
"SELECT {} FROM {table}{search_filter} ORDER BY {} DESC LIMIT ?{search_limit} OFFSET ?{}",
select_columns(model),
quoted("id"),
search_limit + 1
);
let search_count_parameters = if contract.owner.is_some() {
"vec![dever_runtime::orm::Value::Int(rest_owner_id), rest_search_parameter.clone()]"
} else {
"vec![rest_search_parameter.clone()]"
};
let search_page_parameters = if contract.owner.is_some() {
"vec![dever_runtime::orm::Value::Int(rest_owner_id), rest_search_parameter, dever_runtime::orm::Value::Int(size), dever_runtime::orm::Value::Int(offset)]"
} else {
"vec![rest_search_parameter, dever_runtime::orm::Value::Int(size), dever_runtime::orm::Value::Int(offset)]"
};
writeln!(output, "let (count_sql, list_sql, count_parameters, page_parameters) = if let Some(rest_search_parameter) = rest_search_parameter {{ ({}, {}, {search_count_parameters}, {search_page_parameters}) }} else {{ ({}, {}, {count_parameters}, {page_parameters}) }};", database_sql(&search_count), database_sql(&search_list), database_sql(&count), database_sql(&list)).expect("format");
output.push_str("let count_rows = match database.query(count_sql, count_parameters).await { Ok(rows) => rows, Err(error) => return Ok(rest_database_error(error)) };\n");
} else {
writeln!(output, "let count_rows = match database.query({}, {count_parameters}).await {{ Ok(rows) => rows, Err(error) => return Ok(rest_database_error(error)) }};", database_sql(&count)).expect("format");
}
output.push_str("let total = match count_rows.into_iter().next().ok_or_else(|| dever_runtime::orm::Error::invalid_data(\"REST count returned no row\")).and_then(dever_runtime::orm::scalar).and_then(dever_runtime::orm::int) { Ok(value) => value, Err(error) => return Ok(rest_database_error(error)) };\n");
if contract.search.is_some() {
output.push_str("let rows = match database.query(list_sql, page_parameters).await { Ok(rows) => rows, Err(error) => return Ok(rest_database_error(error)) };\n");
} else {
writeln!(output, "let rows = match database.query({}, {page_parameters}).await {{ Ok(rows) => rows, Err(error) => return Ok(rest_database_error(error)) }};", database_sql(&list)).expect("format");
}
writeln!(output, "let mut writer = dever_runtime::wire::Encoder::default(); let value: Result<_, String> = (|| {{ writer.begin_object()?; writer.key(\"items\")?; writer.begin_array()?; for row in rows {{ let model = decode_model_{}(row).map_err(|error| error.to_string())?; writer.json(rest_{}_encode(&model)?.as_str())?; }} writer.end()?; writer.key(\"page\")?; writer.int(page)?; writer.key(\"size\")?; writer.int(size)?; writer.key(\"total\")?; writer.int(total)?; writer.end()?; writer.finish() }})();", model.record, model.record).expect("format");
success_value(output);
}
("GET", true) => {
output.push_str("let inputs = match dever_runtime::api::Inputs::from_request(&request) { Ok(inputs) => inputs, Err(error) => return Ok(dever_runtime::api::invalid_input(error)) };\n");
finish_inputs(output);
owner_value(output, contract);
database(output, model);
let (owner_filter, parameters) = owner_filter(model, contract, 2);
let select = format!(
"SELECT {} FROM {table} WHERE {id_condition}{owner_filter} LIMIT 1",
select_columns(model)
);
writeln!(output, "let rows = match database.query({}, vec![dever_runtime::orm::Value::Int(rest_id){parameters}]).await {{ Ok(rows) => rows, Err(error) => return Ok(rest_database_error(error)) }};", database_sql(&select)).expect("format");
output.push_str("let Some(row) = rows.into_iter().next() else { return Ok(dever_runtime::api::not_found()) };\n");
encode_row(output, model, false);
}
("POST", false) => {
parse_fields(emitter, model, &contract.create, output);
owner_value(output, contract);
database(output, model);
let generated = model
.fields
.iter()
.enumerate()
.filter(|(_, field)| field.generated && matches!(field.ty, ModelFieldType::Uuid))
.map(|(index, _)| index)
.collect::<Vec<_>>();
let mut write_fields = contract
.create
.iter()
.map(|write| write.field)
.collect::<Vec<_>>();
if let Some((owner, _)) = contract.owner {
write_fields.push(owner);
}
write_fields.extend(generated.iter().copied());
let columns = write_fields
.iter()
.map(|&index| quoted(&model.fields[index].name))
.collect::<Vec<_>>();
let sql = if columns.is_empty() {
format!(
"INSERT INTO {table} DEFAULT VALUES RETURNING {}",
select_columns(model)
)
} else {
let values = write_fields
.iter()
.enumerate()
.map(|(slot, &index)| parameter_sql(slot + 1, &model.fields[index].ty))
.collect::<Vec<_>>();
format!(
"INSERT INTO {table} ({}) VALUES ({}) RETURNING {}",
columns.join(", "),
values.join(", "),
select_columns(model)
)
};
let mut parameters = values(program, model, &contract.create, output);
if contract.owner.is_some() {
parameters.push_str("dever_runtime::orm::Value::Int(rest_owner_id), ");
}
for index in generated {
writeln!(output, "let generated_{index} = match dever_runtime::orm::Uuid::new_v7() {{ Ok(value) => value, Err(error) => return Ok(rest_database_error(error)) }};").expect("format");
write!(
parameters,
"dever_runtime::orm::Value::Uuid(generated_{index}), "
)
.expect("format");
}
begin_transaction(output);
writeln!(output, "let rows = match executor.query({}, vec![{parameters}]).await {{ Ok(rows) => rows, Err(error) => {{ drop(executor); rest_rollback(transaction).await; return Ok(rest_database_error(error)); }} }};", database_sql(&sql)).expect("format");
output.push_str("let Some(row) = rows.into_iter().next() else { drop(executor); rest_rollback(transaction).await; return Ok(dever_runtime::api::internal_error()) };\n");
encode_row(output, model, true);
}
("PUT", true) => {
parse_fields(emitter, model, &contract.replace, output);
owner_value(output, contract);
database(output, model);
let assignments = super::orm::update_assignments(
model,
contract.replace.iter().map(|write| write.field),
1,
);
let id_slot = contract.replace.len() + 1;
let (owner_filter, owner_parameters) = owner_filter(model, contract, id_slot + 1);
let sql = format!(
"UPDATE {table} SET {assignments} WHERE {} = ?{id_slot}{owner_filter} RETURNING {}",
quoted("id"),
select_columns(model)
);
let parameters = values(program, model, &contract.replace, output);
begin_transaction(output);
writeln!(output, "let rows = match executor.query({}, vec![{parameters}dever_runtime::orm::Value::Int(rest_id){owner_parameters}]).await {{ Ok(rows) => rows, Err(error) => {{ drop(executor); rest_rollback(transaction).await; return Ok(rest_database_error(error)); }} }};", database_sql(&sql)).expect("format");
output.push_str("let Some(row) = rows.into_iter().next() else { drop(executor); rest_rollback(transaction).await; return Ok(dever_runtime::api::not_found()) };\n");
encode_row(output, model, true);
}
("DELETE", true) => {
output.push_str("let inputs = match dever_runtime::api::Inputs::from_request(&request) { Ok(inputs) => inputs, Err(error) => return Ok(dever_runtime::api::invalid_input(error)) };\n");
finish_inputs(output);
owner_value(output, contract);
database(output, model);
let (owner_filter, owner_parameters) = owner_filter(model, contract, 2);
let sql = format!("DELETE FROM {table} WHERE {id_condition}{owner_filter}");
begin_transaction(output);
writeln!(output, "let affected = match executor.execute({}, vec![dever_runtime::orm::Value::Int(rest_id){owner_parameters}]).await {{ Ok(affected) => affected, Err(error) => {{ drop(executor); rest_rollback(transaction).await; return Ok(rest_database_error(error)); }} }};", database_sql(&sql)).expect("format");
output.push_str("drop(executor); if affected == 0 { rest_rollback(transaction).await; return Ok(dever_runtime::api::not_found()); } if let Err(error) = transaction.commit().await { return Ok(rest_database_error(error)); } Ok(dever_runtime::api::success(dever_runtime::wire::Encoded::null()))\n");
}
_ => unreachable!("checked REST route"),
}
}
pub(super) fn emit_database_error(output: &mut String) {
output.push_str("fn rest_database_error(error: dever_runtime::orm::Error) -> dever_runtime::http::Response { dever_runtime::log::error(\"REST database operation failed\", Vec::new()); if error.kind() == dever_runtime::orm::ErrorKind::NotFound { dever_runtime::api::not_found() } else { dever_runtime::api::internal_error() } }\n");
output.push_str("async fn rest_rollback(transaction: dever_runtime::database::Transaction) { if transaction.rollback().await.is_err() { dever_runtime::log::error(\"REST rollback failed\", Vec::new()); } }\n");
}
fn parse_fields(
emitter: &super::Emitter<'_>,
model: &ModelSchema,
writes: &[RestWriteField],
output: &mut String,
) {
let program = emitter.program;
output.push_str("let mut inputs = match dever_runtime::api::Inputs::from_request(&request) { Ok(inputs) => inputs, Err(error) => return Ok(dever_runtime::api::invalid_input(error)) };\n");
let Shape::Record(fields) = &program.types[model.record].shape else {
unreachable!()
};
for write in writes {
let index = write.field;
let reads_input = matches!(
write.source,
RestValueSource::Input
| RestValueSource::Binding {
uses_input: true,
..
}
);
if !reads_input {
continue;
}
let name = rust_string(&model.fields[index].name);
let prefix = format!("rest_{}_f{index}_in", model.record);
let nullable = matches!(fields[index].ty, Type::Nullable(_));
let read = if nullable {
"optional_raw_json"
} else {
"raw_json"
};
let absent = if nullable {
".unwrap_or_else(|| \"null\".into())"
} else {
""
};
writeln!(output, "let raw_{index} = match inputs.{read}({name}) {{ Ok(value) => value{absent}, Err(error) => return Ok(dever_runtime::api::invalid_input(error)) }};").expect("format");
writeln!(output, "let value_{index} = match {prefix}_decode(&raw_{index}) {{ Ok(value) => value, Err(error) => return Ok(dever_runtime::api::invalid_input(dever_runtime::api::InputError(error))) }};").expect("format");
if let ModelFieldType::Text { minimum, maximum } = model.fields[index].ty {
let value = if nullable {
format!("value_{index}.as_deref()")
} else {
format!("Some(value_{index}.as_str())")
};
let maximum =
maximum.map_or_else(|| "None".to_owned(), |value| format!("Some({value})"));
writeln!(output, "if let Err(error) = dever_runtime::api::text_bounds({name}, {value}, {minimum}, {maximum}) {{ return Ok(dever_runtime::api::invalid_input(error)); }}").expect("format");
}
}
finish_inputs(output);
for write in writes {
let RestValueSource::Binding {
expression,
uses_input,
} = &write.source
else {
continue;
};
let index = write.field;
if *uses_input {
writeln!(output, "let _v0 = value_{index};").expect("format");
}
writeln!(output, "let value_{index} = match (|| -> Result<_, AppError> {{ Ok({}) }})() {{ Ok(value) => value, Err(error) => return Ok(api_failure_response(error)) }};", emitter.expression(expression)).expect("format");
if let ModelFieldType::Text { minimum, maximum } = model.fields[index].ty {
let nullable = matches!(fields[index].ty, Type::Nullable(_));
let value = if nullable {
format!("value_{index}.as_deref()")
} else {
format!("Some(value_{index}.as_str())")
};
let maximum =
maximum.map_or_else(|| "None".to_owned(), |value| format!("Some({value})"));
let failure = if *uses_input {
"return Ok(dever_runtime::api::invalid_input(error))"
} else {
"dever_runtime::log::error(\"REST server field binding violated Model bounds\", Vec::new()); return Ok(dever_runtime::api::internal_error())"
};
writeln!(output, "if let Err(error) = dever_runtime::api::text_bounds({}, {value}, {minimum}, {maximum}) {{ {failure}; }}", rust_string(&model.fields[index].name)).expect("format");
}
}
}
fn parse_search(
emitter: &super::Emitter<'_>,
model: &ModelSchema,
contract: &RestContract,
output: &mut String,
) {
let Some(search) = &contract.search else {
return;
};
let program = emitter.program;
let Shape::Record(fields) = &program.types[model.record].shape else {
unreachable!()
};
let index = search.field;
let name = rust_string(&model.fields[index].name);
let prefix = format!("rest_{}_f{index}_in", model.record);
writeln!(output, "let raw_search = match inputs.optional_raw_json({name}) {{ Ok(value) => value, Err(error) => return Ok(dever_runtime::api::invalid_input(error)) }};").expect("format");
writeln!(output, "let rest_search_value = match raw_search {{ Some(raw) => match {prefix}_decode(&raw) {{ Ok(value) => Some(value), Err(error) => return Ok(dever_runtime::api::invalid_input(dever_runtime::api::InputError(error))) }}, None => None }};").expect("format");
if let ModelFieldType::Text { minimum, maximum } = model.fields[index].ty {
let maximum = maximum.map_or_else(|| "None".to_owned(), |value| format!("Some({value})"));
writeln!(output, "if let Err(error) = dever_runtime::api::text_bounds({name}, rest_search_value.as_deref(), {minimum}, {maximum}) {{ return Ok(dever_runtime::api::invalid_input(error)); }}").expect("format");
}
writeln!(output, "let rest_search_value = match rest_search_value {{ Some(value) => {{ let _v0 = value; match (|| -> Result<_, AppError> {{ Ok({}) }})() {{ Ok(value) => Some(value), Err(error) => return Ok(api_failure_response(error)) }} }}, None => None }};", emitter.expression(&search.expression)).expect("format");
if let ModelFieldType::Text { minimum, maximum } = model.fields[index].ty {
let maximum = maximum.map_or_else(|| "None".to_owned(), |value| format!("Some({value})"));
writeln!(output, "if let Err(error) = dever_runtime::api::text_bounds({name}, rest_search_value.as_deref(), {minimum}, {maximum}) {{ return Ok(dever_runtime::api::invalid_input(error)); }}").expect("format");
}
let value = value_expression(&fields[index].ty, &model.fields[index].ty, "value");
writeln!(output, "let rest_search_parameter = match rest_search_value {{ Some(value) => match {value} {{ Ok(value) => Some(value), Err(error) => return Ok(dever_runtime::api::invalid_input(dever_runtime::api::InputError(error.to_string()))) }}, None => None }};").expect("format");
}
fn values(
program: &Program,
model: &ModelSchema,
writes: &[RestWriteField],
output: &mut String,
) -> String {
let Shape::Record(fields) = &program.types[model.record].shape else {
unreachable!()
};
writes.iter().map(|write| {
let index = write.field;
let value = value_expression(&fields[index].ty, &model.fields[index].ty, &format!("value_{index}"));
writeln!(output, "let parameter_{index} = match {value} {{ Ok(value) => value, Err(error) => return Ok(dever_runtime::api::invalid_input(dever_runtime::api::InputError(error.to_string()))) }};").expect("format");
format!("parameter_{index}, ")
}).collect()
}
fn value_expression(ty: &Type, storage: &ModelFieldType, name: &str) -> String {
let value = "dever_runtime::orm::Value";
match ty {
Type::Nullable(inner) => format!(
"match {name} {{ Some(value) => {}, None => Ok::<_, dever_runtime::orm::Error>({value}::Null) }}",
value_expression(inner, storage, "value")
),
Type::Bool => format!("Ok::<_, dever_runtime::orm::Error>({value}::Bool({name}))"),
Type::Int | Type::DateTime | Type::Date | Type::Time | Type::Duration => {
format!("Ok::<_, dever_runtime::orm::Error>({value}::Int({name}))")
}
Type::Float => format!("Ok::<_, dever_runtime::orm::Error>({value}::Float({name}))"),
Type::Text | Type::Json => {
format!("Ok::<_, dever_runtime::orm::Error>({value}::Text({name}))")
}
Type::Uuid => format!("Ok::<_, dever_runtime::orm::Error>({value}::Uuid({name}))"),
Type::Named(_) => format!("Ok::<_, dever_runtime::orm::Error>({value}::Int({name}.0))"),
Type::Decimal => {
let ModelFieldType::Decimal { precision, scale } = storage else {
unreachable!()
};
format!("dever_runtime::orm::decimal_value({name}, {precision}, {scale})")
}
_ => unreachable!("checked REST wire field"),
}
}
fn database(output: &mut String, model: &ModelSchema) {
writeln!(output, "let database = match model_database_{}().await {{ Ok(database) => database, Err(error) => return Ok(rest_database_error(error)) }};", model.record).expect("format");
}
fn begin_transaction(output: &mut String) {
output.push_str("let transaction = match database.begin().await { Ok(transaction) => transaction, Err(error) => return Ok(rest_database_error(error)) };\n");
output.push_str("let executor = match dever_runtime::database::Executor::new(database, Some(&transaction)) { Ok(executor) => executor, Err(error) => return Ok(rest_database_error(error)) };\n");
}
fn finish_inputs(output: &mut String) {
output.push_str("if let Err(error) = inputs.finish() { return Ok(dever_runtime::api::invalid_input(error)); }\n");
}
fn owner_value(output: &mut String, contract: &RestContract) {
let Some((_, source)) = contract.owner else {
return;
};
let read = match source {
RestOwnerSource::User => "dever_runtime::auth::user_id()",
RestOwnerSource::Tenant => "dever_runtime::auth::tenant_id()",
};
writeln!(
output,
"let rest_owner_id = match {read} {{ Ok(Some(value)) => value, Ok(None) => return Ok(dever_runtime::api::standard_error(403)), Err(error) => return Ok(dever_runtime::api::auth_error(error)) }};"
)
.expect("format");
}
fn owner_filter(
model: &ModelSchema,
contract: &RestContract,
parameter: usize,
) -> (String, &'static str) {
match contract.owner {
Some((index, _)) => (
format!(" AND {} = ?{parameter}", quoted(&model.fields[index].name)),
", dever_runtime::orm::Value::Int(rest_owner_id)",
),
None => (String::new(), ""),
}
}
fn encode_row(output: &mut String, model: &ModelSchema, transactional: bool) {
let rollback = if transactional {
"drop(executor); rest_rollback(transaction).await; "
} else {
""
};
writeln!(output, "let model = match decode_model_{}(row) {{ Ok(model) => model, Err(error) => {{ {rollback}return Ok(rest_database_error(error)); }} }};", model.record).expect("format");
writeln!(output, "let value = match rest_{}_encode(&model) {{ Ok(value) => value, Err(_error) => {{ {rollback}return Ok(dever_runtime::api::internal_error()); }} }};", model.record).expect("format");
if transactional {
output.push_str("drop(executor); if let Err(error) = transaction.commit().await { return Ok(rest_database_error(error)); }\n");
}
output.push_str("Ok(dever_runtime::api::success(value))\n");
}
fn success_value(output: &mut String) {
output.push_str("match value { Ok(value) => Ok(dever_runtime::api::success(value)), Err(_error) => Ok(dever_runtime::api::internal_error()) }\n");
}
+273
View File
@@ -0,0 +1,273 @@
use std::collections::BTreeSet;
use crate::hir::{
CallArgument, CallTarget, Clause, CollectionOp, Expression, ExpressionKind as Expr, Projection,
SequenceKind, Statement,
};
use crate::intrinsic::Intrinsic;
use crate::specialize::{self, Specialization};
use crate::syntax::{BinaryOperator, UnaryOperator};
use crate::types::{Parameter, Type};
use super::Emitter;
impl Emitter<'_> {
pub(super) fn inline_sequences(&self, clause: &mut Clause) {
let mut index = 0;
while index + 1 < clause.body.len() {
let Statement::Assign {
slot,
fields,
value,
} = &clause.body[index]
else {
index += 1;
continue;
};
if !fields.is_empty() || clause.outputs.contains(slot) || !self.fusible(value) {
index += 1;
continue;
}
let next = match &clause.body[index + 1] {
Statement::Assign { value, .. } | Statement::Call(value) => value,
};
let Expr::Collection { arguments, .. } = &next.kind else {
index += 1;
continue;
};
if !matches!(arguments[0].kind, Expr::Local(id) if id == *slot)
|| !super::liveness::last_uses(clause)
.contains(&(&arguments[0] as *const Expression))
{
index += 1;
continue;
}
let replacement = value.clone();
let next = match &mut clause.body[index + 1] {
Statement::Assign { value, .. } | Statement::Call(value) => value,
};
let Expr::Collection { arguments, .. } = &mut next.kind else {
unreachable!()
};
arguments[0] = replacement;
clause.body.remove(index);
}
}
fn fusible(&self, expression: &Expression) -> bool {
let Expr::Collection {
operation,
handler: Some(handler),
arguments,
sequence,
..
} = &expression.kind
else {
return false;
};
matches!(operation, CollectionOp::Each | CollectionOp::Filter)
&& matches!(sequence, Some(SequenceKind::List | SequenceKind::Bytes))
&& arguments.len() == 1
&& matches!(expression.ty, Type::List(_))
&& expression.ty.movable(&self.program.types)
&& arguments[0].ty.movable(&self.program.types)
&& self.total_handler(specialize::resolve_handler(*handler, &self.bindings))
}
pub(super) fn fused_iterator(&self, expression: &Expression) -> Option<(String, String)> {
if !self.fusible(expression) {
return None;
}
let Expr::Collection {
operation,
handler: Some(handler),
arguments,
sequence,
..
} = &expression.kind
else {
unreachable!()
};
let (bindings, iterator) = self.fused_iterator(&arguments[0]).unwrap_or_else(|| {
if let Some([text, separator]) = self.split_arguments(&arguments[0]) {
return (
format!(
"let pipeline_text = {}; let pipeline_separator = {};",
self.expression(text),
self.expression(separator)
),
"dever_runtime::text::pieces(&pipeline_text, &pipeline_separator)".into(),
);
}
(
format!("let pipeline_source = {};", self.expression(&arguments[0])),
match sequence.expect("checked sequence") {
SequenceKind::List => "pipeline_source.into_values()",
SequenceKind::Bytes => {
"pipeline_source.values().iter().copied().map(i64::from)"
}
SequenceKind::Stream | SequenceKind::AsyncStream | SequenceKind::RowStream => {
unreachable!()
}
}
.into(),
)
});
let target = Specialization {
function: specialize::resolve_handler(*handler, &self.bindings),
handlers: Vec::new(),
};
let name = &self.names[&target];
let stage = if *operation == CollectionOp::Each {
format!("map(|element| {name}(element).expect(\"checked total handler\"))")
} else {
format!("filter(|element| {name}(element.clone()).expect(\"checked total handler\"))")
};
Some((bindings, format!("{iterator}.{stage}")))
}
// pure 只证明无外部效果;融合还要求所有可达操作都不会产生程序故障。
fn total_handler(&self, root: usize) -> bool {
let mut pending = vec![root];
let mut visited = BTreeSet::new();
while let Some(id) = pending.pop() {
if !visited.insert(id) {
continue;
}
let function = &self.program.functions[id];
if function.suspends || !self.program.suspension_handlers[id].is_empty() || !self.program.effects[id].is_empty() || !self.program.failures[id].is_empty() || !self.program.failure_handlers[id].is_empty() || function.parameters.iter().any(|parameter| !matches!(parameter, Parameter::Value(ty) if ty.movable(&self.program.types))) {
return false;
}
let mut total = true;
crate::check::visit(function, |expression| {
total &= match &expression.kind {
Expr::Constant(_)
| Expr::Local(_)
| Expr::Field { .. }
| Expr::Record { .. }
| Expr::Variant { .. }
| Expr::List(_)
| Expr::Some(_)
| Expr::Promote(_) => true,
Expr::Unary { operator, .. } => {
*operator == UnaryOperator::Not || expression.ty == Type::Float
}
Expr::Binary { operator, .. } => {
matches!(
operator,
BinaryOperator::Equal
| BinaryOperator::NotEqual
| BinaryOperator::Less
| BinaryOperator::LessEqual
| BinaryOperator::Greater
| BinaryOperator::GreaterEqual
| BinaryOperator::And
| BinaryOperator::Or
) || matches!(expression.ty, Type::Float | Type::Text)
}
Expr::Collection { operation, .. } => matches!(
operation,
CollectionOp::Length
| CollectionOp::First
| CollectionOp::Get
| CollectionOp::Entries
),
Expr::Call {
target: CallTarget::Function(callee),
arguments,
} if arguments
.iter()
.all(|argument| matches!(argument, CallArgument::Value(_))) =>
{
pending.push(*callee);
true
}
Expr::Intrinsic { operation, .. } => matches!(
operation,
Intrinsic::TextTrim
| Intrinsic::TextLower
| Intrinsic::TextUpper
| Intrinsic::TextCodepoint
| Intrinsic::TextFromCodepoint
| Intrinsic::TextAt
| Intrinsic::TextSlice
| Intrinsic::TextIndexOf
| Intrinsic::TextContains
| Intrinsic::TextStartsWith
| Intrinsic::TextEndsWith
| Intrinsic::TextSplit
| Intrinsic::TextReplace
| Intrinsic::IntToText
| Intrinsic::FloatToText
| Intrinsic::FloatFromInt
),
_ => false,
};
});
if !total {
return false;
}
}
true
}
/// 普通单表达式转发函数和直接原语共用识别,不依赖标准包或样例名称。
pub(super) fn split_arguments<'a>(
&self,
expression: &'a Expression,
) -> Option<[&'a Expression; 2]> {
match &expression.kind {
Expr::Intrinsic {
operation: Intrinsic::TextSplit,
arguments,
..
} => Some([&arguments[0], &arguments[1]]),
Expr::Call {
target: CallTarget::Function(id),
arguments,
} => {
let [clause] = self.program.functions[*id].clauses.as_slice() else {
return None;
};
let [
Statement::Assign {
slot,
fields,
value,
},
] = clause.body.as_slice()
else {
return None;
};
if !fields.is_empty() || clause.outputs != [*slot] {
return None;
}
let Expr::Intrinsic {
operation: Intrinsic::TextSplit,
arguments: forwarded,
..
} = &value.kind
else {
return None;
};
let input = |expression: &Expression| {
let Expr::Local(slot) = expression.kind else {
return None;
};
let binding = clause.bindings.iter().find(|binding| {
binding.slot == slot && matches!(binding.projection, Projection::Whole)
})?;
let CallArgument::Value(value) = &arguments[binding.input] else {
return None;
};
Some((binding.input, value))
};
let (left, text) = input(&forwarded[0])?;
let (right, separator) = input(&forwarded[1])?;
// 不能删除额外参数的副作用,或重排调用方参数求值。
(arguments.len() == 2 && left == 0 && right == 1).then_some([text, separator])
}
_ => None,
}
}
}
+250
View File
@@ -0,0 +1,250 @@
use std::collections::{BTreeMap, BTreeSet};
use crate::hir::{CallArgument, Clause, Expression, ExpressionKind as Expr, Statement};
use crate::types::{Definition, Shape, Type};
/// 更新副本只读取旧值中尚未修改的字段时,旧局部可直接转交给副本。
/// 在已完成契约检查的 HIR 副本上执行,不改变诊断使用的源码事实。
pub(super) fn coalesce(clause: &mut Clause, types: &[Definition]) {
for index in 0..clause.body.len() {
let Statement::Assign {
slot: target,
fields,
value,
} = &clause.body[index]
else {
continue;
};
let Expr::Local(source) = value.kind else {
continue;
};
let (target, source) = (*target, source);
if target == source || !fields.is_empty() || clause.outputs.contains(&source) {
continue;
}
let Type::Named(id) = value.ty else { continue };
if !matches!(types[id].shape, Shape::Record(_)) || !value.ty.movable(types) {
continue;
}
let mut changed = Vec::<Vec<usize>>::new();
let mut safe = true;
for statement in &mut clause.body[index + 1..] {
visit_reads(statement_value(statement), &mut |read| {
if let Some((slot, path)) = place(read)
&& slot == source
&& (path.is_empty() || changed.iter().any(|write| overlaps(write, &path)))
{
safe = false;
}
});
if let Statement::Assign { slot, fields, .. } = statement {
if *slot == source {
safe = false;
}
if *slot == target {
changed.push(fields.clone());
}
}
if !safe {
break;
}
}
if safe {
for statement in &mut clause.body[index + 1..] {
visit_reads(statement_value(statement), &mut |read| {
rename_place(read, source, target);
});
}
}
}
}
/// 大记录传给下一步后若只剩标量读取,先保留这些标量,避免为它们复制整个记录。
pub(super) fn snapshot_scalars(clause: &mut Clause, types: &[Definition]) {
let mut index = 0;
while index < clause.body.len() {
let mut sources = BTreeSet::new();
visit_reads(statement_value(&mut clause.body[index]), &mut |read| {
if let Expr::Local(slot) = read.kind
&& matches!(read.ty, Type::Named(_))
&& read.ty.movable(types)
{
sources.insert(slot);
}
});
for source in sources {
if clause.outputs.contains(&source)
|| matches!(&clause.body[index], Statement::Assign { slot, .. } if *slot == source)
{
continue;
}
let mut snapshots = BTreeMap::new();
let mut safe = true;
for statement in &mut clause.body[index + 1..] {
if matches!(statement, Statement::Assign { slot, .. } if *slot == source) {
safe = false;
break;
}
visit_reads(statement_value(statement), &mut |read| {
if let Some((slot, path)) = place(read)
&& slot == source
{
if !path.is_empty()
&& matches!(
read.ty,
Type::Bool | Type::Int | Type::Float | Type::Decimal
)
{
snapshots.entry(path).or_insert_with(|| read.clone());
} else {
safe = false;
}
}
});
if !safe {
break;
}
}
if !safe || snapshots.is_empty() {
continue;
}
let mut slots = BTreeMap::new();
let mut assignments = Vec::new();
for (path, value) in snapshots {
let slot = clause.locals.len();
clause.locals.push(value.ty.clone());
slots.insert(path, slot);
assignments.push(Statement::Assign {
slot,
fields: Vec::new(),
value,
});
}
for statement in &mut clause.body[index + 1..] {
visit_reads(statement_value(statement), &mut |read| {
if let Some((slot, path)) = place(read)
&& slot == source
{
read.kind = Expr::Local(slots[&path]);
}
});
}
let added = assignments.len();
clause.body.splice(index..index, assignments);
index += added;
}
index += 1;
}
}
fn rename_place(expression: &mut Expression, source: usize, target: usize) {
match &mut expression.kind {
Expr::Local(slot) if *slot == source => *slot = target,
Expr::Field { value, .. } => rename_place(value, source, target),
_ => {}
}
}
pub(super) fn overlaps(left: &[usize], right: &[usize]) -> bool {
left.starts_with(right) || right.starts_with(left)
}
pub(crate) fn place(expression: &Expression) -> Option<(usize, Vec<usize>)> {
let mut base = expression;
let mut path = Vec::new();
while let Expr::Field { value, index } = &base.kind {
path.push(*index);
base = value;
}
let Expr::Local(slot) = base.kind else {
return None;
};
path.reverse();
Some((slot, path))
}
fn statement_value(statement: &mut Statement) -> &mut Expression {
match statement {
Statement::Assign { value, .. } | Statement::Call(value) => value,
}
}
fn visit_reads(expression: &mut Expression, apply: &mut impl FnMut(&mut Expression)) {
if place(expression).is_some() {
apply(expression);
return;
}
match &mut expression.kind {
Expr::Local(_) | Expr::Constant(_) | Expr::ChoiceOptions { .. } => {}
Expr::JobEnqueue { arguments, .. } => {
for value in arguments {
visit_reads(value, apply);
}
}
Expr::ModelOperation { operation, .. } => {
operation.visit_mut(&mut |value| visit_reads(value, apply));
}
Expr::Field { value, .. }
| Expr::Fail(value)
| Expr::AwaitTask(value)
| Expr::StopTask(value)
| Expr::Group(value)
| Expr::AwaitGroup(value)
| Expr::StopGroup(value)
| Expr::ChannelReceive(value)
| Expr::ChannelClose(value)
| Expr::Promote(value)
| Expr::Some(value)
| Expr::Unary { value, .. } => visit_reads(value, apply),
Expr::Call { arguments, .. }
| Expr::CaptureResult { arguments, .. }
| Expr::ParallelCall { arguments, .. }
| Expr::BlockingCall { arguments, .. } => {
for argument in arguments {
if let CallArgument::Value(value) = argument {
visit_reads(value, apply);
}
}
}
Expr::RunCall {
group, arguments, ..
} => {
if let Some(group) = group {
visit_reads(group, apply);
}
for argument in arguments {
if let CallArgument::Value(value) = argument {
visit_reads(value, apply);
}
}
}
Expr::Channel { capacity, .. } => visit_reads(capacity, apply),
Expr::ChannelSend { channel, value } => {
visit_reads(channel, apply);
visit_reads(value, apply);
}
Expr::Intrinsic { arguments, .. }
| Expr::Collection { arguments, .. }
| Expr::Variant { arguments, .. }
| Expr::List(arguments) => {
for argument in arguments {
visit_reads(argument, apply);
}
}
Expr::Record { fields } => {
for (_, value) in fields {
visit_reads(value, apply);
}
}
Expr::Map(entries) => {
for (key, value) in entries {
visit_reads(key, apply);
visit_reads(value, apply);
}
}
Expr::Binary { left, right, .. } => {
visit_reads(left, apply);
visit_reads(right, apply);
}
}
}
+135
View File
@@ -0,0 +1,135 @@
use std::fmt::Write;
use super::rust_string;
use crate::wire::{Node, Schema};
/// Emit concrete helpers into the checked program's generated Rust module.
/// The index is local to that module; callers cache schemas by fingerprint.
pub fn emit(schema: &Schema, index: usize) -> String {
emit_named(schema, &format!("wire_{index}"))
}
pub(super) fn emit_named(schema: &Schema, prefix: &str) -> String {
let mut output = String::new();
let root = schema.root;
let ty = rust_type(schema, root);
writeln!(output, "fn {prefix}_decode(text: &str) -> Result<{ty}, String> {{ let node = dever_runtime::wire::parse(text)?; {prefix}_d{root}(&node) }}").expect("string formatting");
if !schema.policy.permits_secret() {
writeln!(output, "fn {prefix}_encode(value: &{ty}) -> Result<dever_runtime::wire::Encoded, String> {{ let mut writer = dever_runtime::wire::Encoder::default(); {prefix}_e{root}(value, &mut writer)?; writer.finish() }}").expect("string formatting");
}
for (id, node) in schema.nodes.iter().enumerate() {
let ty = rust_type(schema, id);
writeln!(output, "fn {prefix}_d{id}(node: &dever_runtime::wire::Node<'_>) -> Result<{ty}, String> {{ {} }}", decode(schema, node, prefix)).expect("string formatting");
if !schema.policy.permits_secret() {
writeln!(output, "fn {prefix}_e{id}(value: &{ty}, writer: &mut dever_runtime::wire::Encoder) -> Result<(), String> {{ {} }}", encode(node, prefix)).expect("string formatting");
}
}
output
}
fn rust_type(schema: &Schema, id: usize) -> String {
match &schema.nodes[id] {
Node::Bytes => "dever_runtime::bytes::Bytes".into(),
Node::Bool => "bool".into(),
Node::Int | Node::DateTime | Node::Date | Node::Time | Node::Duration => "i64".into(),
Node::Float => "f64".into(),
Node::Decimal => "dever_runtime::number::DecimalValue".into(),
Node::Text | Node::Json => "String".into(),
Node::Id => "dever_runtime::Id".into(),
Node::Uuid => "dever_runtime::orm::Uuid".into(),
Node::Secret => "dever_runtime::secret::Secret".into(),
Node::Nullable(inner) => format!("Option<{}>", rust_type(schema, *inner)),
Node::List(inner) => format!(
"dever_runtime::collections::List<{}>",
rust_type(schema, *inner)
),
Node::Record { id, .. } | Node::ModelId { id, .. } => format!("T{id}"),
}
}
fn decode(schema: &Schema, node: &Node, prefix: &str) -> String {
match node {
Node::Bytes => "node.bytes()".into(),
Node::Bool => "node.boolean()".into(),
Node::Int | Node::Duration => "node.int()".into(),
Node::ModelId { id, .. } => format!("Ok(T{id}(node.int()?))"),
Node::Float => "node.float()".into(),
Node::Text => "Ok(node.text()?.to_owned())".into(),
Node::Json => "Ok(node.raw().to_owned())".into(),
Node::Secret => {
"Ok(dever_runtime::secret::Secret::from_input(node.text()?.as_bytes().to_vec()))".into()
}
Node::Id => "Ok(dever_runtime::Id(node.text()?.to_owned()))".into(),
Node::Uuid => {
"dever_runtime::orm::Uuid::parse(node.text()?).map_err(|error| error.to_string())"
.into()
}
Node::Decimal => {
"dever_runtime::number::DecimalValue::parse(node.text()?).map_err(str::to_owned)".into()
}
Node::DateTime => "dever_runtime::time::parse_datetime(node.text()?)".into(),
Node::Date => "dever_runtime::time::parse_date(node.text()?)".into(),
Node::Time => "dever_runtime::time::parse_time(node.text()?)".into(),
Node::Nullable(inner) => format!(
"if node.is_null() {{ Ok(None) }} else {{ Ok(Some({prefix}_d{inner}(node)?)) }}"
),
Node::List(inner) => format!(
"Ok(dever_runtime::collections::List::new(node.list()?.iter().map({prefix}_d{inner}).collect::<Result<Vec<_>, String>>()?))"
),
Node::Record { id, fields, .. } => {
let names = fields
.iter()
.map(|(name, _)| rust_string(name))
.collect::<Vec<_>>()
.join(", ");
let mut body = format!("let fields = node.fields(&[{names}])?; Ok(T{id} {{");
for (index, (name, child)) in fields.iter().enumerate() {
let name = rust_string(name);
if matches!(schema.nodes[*child], Node::Nullable(_)) {
write!(body, "f{index}: match fields.get({name}) {{ Some(value) => {prefix}_d{child}(value)?, None => None }},").expect("string formatting");
} else {
write!(body, "f{index}: {prefix}_d{child}(fields.get({name}).ok_or(\"missing required wire field\")?)?,").expect("string formatting");
}
}
body.push_str("})");
body
}
}
}
fn encode(node: &Node, prefix: &str) -> String {
match node {
Node::Bytes => "writer.bytes(value)".into(),
Node::Bool => "writer.boolean(*value)".into(),
Node::Int | Node::Duration => "writer.int(*value)".into(),
Node::ModelId { .. } => "writer.int(value.0)".into(),
Node::Float => "writer.float(*value)".into(),
Node::Text => "writer.text(value)".into(),
Node::Json => "writer.json(value)".into(),
Node::Id => "writer.text(&value.0)".into(),
Node::Uuid | Node::Decimal => "writer.text(&value.to_string())".into(),
Node::DateTime => "writer.text(&dever_runtime::time::format_datetime(*value)?)".into(),
Node::Date => "writer.text(&dever_runtime::time::format_date(*value)?)".into(),
Node::Time => "writer.text(&dever_runtime::time::format_time(*value)?)".into(),
Node::Nullable(inner) => format!(
"match value {{ Some(value) => {prefix}_e{inner}(value, writer), None => writer.null() }}"
),
Node::List(inner) => format!(
"writer.begin_array()?; for value in value.values() {{ {prefix}_e{inner}(value, writer)?; }} writer.end()"
),
Node::Record { fields, .. } => {
let mut body = "writer.begin_object()?;".to_owned();
for (index, (name, child)) in fields.iter().enumerate() {
write!(
body,
"writer.key({})?; {prefix}_e{child}(&value.f{index}, writer)?;",
rust_string(name)
)
.expect("string formatting");
}
body.push_str("writer.end()");
body
}
Node::Secret => unreachable!("input-only schema never emits an encoder"),
}
}