Initial Dever 0.0.1

This commit is contained in:
Codex committed 2026-11-03 03:32:07 +00:00
commit 81233d098c
1479 files changed
+163782

No files matched your search

+2609
View File
File diff suppressed because it is too large. Load diff
+127
View File
@@ -0,0 +1,127 @@
[workspace]
members = ["src/runtime"]
resolver = "3"
[workspace.package]
version = "0.0.1"
edition = "2024"
rust-version = "1.95"
publish = false
[workspace.lints.rust]
unsafe_code = "forbid"
[workspace.dependencies]
dever-runtime = { path = "src/runtime" }
pulldown-cmark = { version = "=0.13.4", default-features = false }
dec = "=0.4.11"
getrandom = "=0.2.17"
argon2 = { version = "=0.5.3", features = ["zeroize"] }
sha2 = "=0.10.9"
hmac = "=0.12.1"
base64 = "=0.22.1"
subtle = "=2.6.1"
zeroize = "=1.9.0"
uuid = { version = "=1.28.0", default-features = false }
chrono = { version = "=0.4.45", default-features = false, features = ["std"] }
indexmap = "=2.14.0"
ahash = "=0.8.12"
serde = { version = "=1.0.228", features = ["derive"] }
serde_json = { version = "=1.0.145", features = ["raw_value"] }
node-semver = "=2.2.0"
url = "=2.5.8"
zip = { version = "=2.4.2", default-features = false, features = ["deflate"] }
rustix = { version = "=1.1.2", features = ["net", "process", "fs"] }
tokio = { version = "=1.53.1", features = ["rt-multi-thread", "sync", "time", "macros"] }
tokio-util = { version = "=0.7.19", features = ["rt"] }
futures-util = { version = "=0.3.34", default-features = false, features = ["std", "sink"] }
tokio-tungstenite = { version = "=0.30.0", default-features = false, features = ["handshake"] }
hyper = { version = "=1.11.1", features = ["http1", "http2", "client", "server"] }
hyper-util = { version = "=0.1.20", features = ["tokio", "client-legacy", "http1", "http2"] }
http-body-util = "=0.1.5"
bytes = "=1.12.1"
rustls = { version = "=0.23.44", default-features = false, features = ["std", "ring", "tls12"] }
tokio-rustls = { version = "=0.26.5", default-features = false, features = ["ring", "tls12"] }
webpki-roots = "=1.0.8"
tower-service = "=0.3.3"
deadpool-sqlite = { version = "=0.14.0", features = ["bundled", "hooks", "functions"] }
deadpool-postgres = "=0.14.2"
tokio-postgres = { version = "=0.7.18", features = ["with-uuid-1"] }
tokio-postgres-rustls = { version = "=0.14.0", default-features = false, features = ["ring"] }
rustls-native-certs = "=0.8.4"
[package]
name = "dever"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
publish.workspace = true
[features]
postgres = ["dever-runtime/postgres"]
[[bin]]
name = "dever"
path = "src/cli/main.rs"
[[example]]
name = "native-release"
path = "src/cli/release/main.rs"
[dependencies]
dec.workspace = true
dever-runtime = { workspace = true, features = ["wire", "calendar", "uuid"] }
pulldown-cmark.workspace = true
serde.workspace = true
serde_json.workspace = true
sha2.workspace = true
base64.workspace = true
node-semver.workspace = true
url.workspace = true
zip.workspace = true
ureq = { version = "=3.4.2", default-features = false, features = ["rustls"] }
ring = "=0.17.14"
goblin = "=0.10.7"
tar = { version = "=0.4.44", default-features = false }
flate2 = "=1.1.5"
pep440_rs = "=0.7.3"
pep508_rs = "=0.9.2"
[target.'cfg(target_os = "linux")'.dependencies]
rustix.workspace = true
[dev-dependencies]
dever-runtime = { workspace = true, features = ["http"] }
hmac.workspace = true
[lints]
workspace = true
[[test]]
name = "syntax"
path = "../test/rust/syntax.rs"
[[test]]
name = "compiler"
path = "../test/rust/compiler.rs"
[[test]]
name = "native"
path = "../test/rust/native.rs"
[[bench]]
name = "performance"
path = "../test/rust/performance.rs"
harness = false
[profile.release]
codegen-units = 1
lto = "fat"
overflow-checks = true
strip = "symbols"
[profile.dev]
debug = 0
[profile.test]
debug = 0
+74
View File
@@ -0,0 +1,74 @@
# 编译器开发
编译路线为 Dever → 具体类型 Rust → Cargo/rustc → 原生程序。语法、检查、代码生成与运行库各自负责一层。
## 目录
| 路径 | 内容 |
| --- | --- |
| `src/syntax/` | 源码位置、词法、语法和诊断 |
| `src/compiler/project/` | 项目、配置与依赖加载 |
| `src/compiler/check/`、`model/` | 类型检查和已检查的程序 |
| `src/compiler/emit/`、`build/` | Rust 生成与原生构建 |
| `src/compiler/inspect/` | 符号、说明、调用关系和流程图 |
| `src/runtime/` | 运行库,复用 Rust 标准库及已有依赖 |
| `src/process/` | 子进程启动、等待与取消 |
| `src/cli/` | 命令行入口 |
| `../lib/`、`../sdk/` | Dever 标准库与外部 Worker SDK |
| `../test/rust/` | 检查器、原生执行、运行库和性能测试 |
只有本目录及 `src/runtime/` 两份 Cargo 清单。CLI 不单独拆包,生成程序只依赖运行库。
## 构建
需要 Rust 1.95.0 和 C 编译工具,版本见 `rust-toolchain.toml`。在本目录执行;平台开发使用已有 dev 容器。
```sh
cargo +1.95.0 fetch --locked
cargo +1.95.0 build --locked --offline --features postgres --bin dever
compiler="${CARGO_TARGET_DIR:-target}/debug/dever"
"$compiler" new ../target/hello-example
"$compiler" check ../target/hello-example
"$compiler" test ../target/hello-example
"$compiler" run ../target/hello-example -- hello.greeting.greet '{"name":"Dever"}'
```
`CARGO_TARGET_DIR` 可指定构建缓存。已有依赖缓存时省略 fetch;锁文件不随日常构建更新。项目和显式输出文件须使用新路径。
## 检查与测试
```sh
cargo +1.95.0 fmt --all --check
cargo +1.95.0 clippy --locked --offline --workspace --all-targets --all-features --no-deps -- -D warnings
cargo +1.95.0 test --locked --offline --test syntax --test compiler
cargo +1.95.0 test --locked --offline --test native inspection::
cargo +1.95.0 test --locked --offline --test native toolchain::
```
按改动选择现有测试:运行库测试位于 `../test/rust/runtime/`,应用原生测试位于 `../test/rust/native/`。数据库测试使用独立配置,不能连接业务数据库。测试通过数以真实执行结果为准,ignored 不计为通过。
语法和运行行为以 [语言规范](../LANGUAGE.md)及相关测试为准;实现约束见 [AGENTS](../AGENTS.md)。性能测试见 [测试方法](../test/performance/README.md)。
## 打包
从同一份源码构建发行编译器,再在仓库根组装:
```sh
cargo +1.95.0 build --locked --offline --release --features postgres --bin dever
compiler="$(realpath "${CARGO_TARGET_DIR:-target}/release/dever")"
cd ..
python3 build/online.py --compiler "$compiler" --work /tmp/dever-online --output target/releases/0.0.1/online
python3 build/development.py --compiler "$compiler" --work /tmp/dever-offline --output target/releases/0.0.1/dever-0.0.1-linux-x86_64-development.tar.xz
```
组装环境为 Debian 13,工作目录和输出须不存在。在线包与离线包共用 `build/development.py` 的输入准备;底层 `dever release` 只组装已提供的编译器、源码和清单。
脚本保留许可证、锁定依赖及链接所需文件,输出 SHA-256。归档不带构建缓存、用户配置或凭据;只裁剪复制进包的工具,不修改系统安装。
发布前在独立目录验证创建、检查、测试、构建和运行。模板、标准库说明嵌入编译器,修改后须重建编译器和安装包。打包不自动上传、安装或切换开发环境。
## 目标平台
`dever build --target` 支持 `linux-x86_64` 和 `linux-aarch64`。交叉构建须预备目标标准库、链接器和原生依赖;`run/test` 在本机执行。当前安装包只提供已验证的 Linux x86_64 环境。
发行构建使用 release、fat LTO、单代码生成单元、溢出检查和符号裁剪。业务程序仍保留 Dever 源码位置诊断。
+4
View File
@@ -0,0 +1,4 @@
[toolchain]
channel = "1.95.0"
profile = "minimal"
components = ["rustfmt", "clippy"]
+100
View File
@@ -0,0 +1,100 @@
//! 解析候选 CLI 的命令、源码路径和显式入口。
//! 这里只检查参数形状;源码是否合法由 compiler 判定。
use std::path::PathBuf;
mod tenant;
pub struct Args {
pub command: String,
pub source: PathBuf,
pub output: Option<PathBuf>,
pub entry: String,
pub target: Option<dever::compiler::build::BuildTarget>,
pub arguments: Vec<String>,
}
pub fn parse(mut arguments: impl Iterator<Item = String>) -> Result<Option<Args>, String> {
let command = arguments.next().unwrap_or_else(|| "help".into());
if matches!(command.as_str(), "help" | "--help" | "-h") {
println!(
r#"dever new <new-directory>
dever skill path [project-root]
dever skill install
dever check <source>
dever inspect <source> [--symbol package.function] [--depth 1] [--json]
dever inspect <project> --links [--check]
dever api <source> [--output <new-file>]
dever emit <source>
dever fmt <source> [--check]
dever test <project-root> [--filter component.domain.topic] [--timeout-ms 60000]
dever build <source> --output <new-file> [--entry package.function] [--target linux-x86_64|linux-aarch64]
dever run <source> [--entry package.function]
dever run <project-root> -- [--tenant <id>] component.domain.command '<json-object>'
dever tenant migrate <project-root> <tenant-id>
dever tenant owner <project-root> <tenant-id> <site> <user-id>
dever tenant component enable|disable <project-root> <tenant-id> <component>
dever package add|update|remove|list|doctor <project-root> [spec...]
dever package export <project-root> <package> <relative-directory> --output <new-directory>
dever lib install <project-root>
dever release <author-root> --output <new-directory>
Source accepts .dever, a package directory or a project with module/. Descriptions live in doc/.
Core entry defaults to main.main. Applications start HTTP APIs or invoke a CMD.
run reads the project's config/setting.json; build outputs use deployment configuration.
Online installations prepare the build environment in the background; run/test/build wait when needed.
Once prepared, locked dependencies build offline. release packages a prepared local compiler candidate."#
);
return Ok(None);
}
if command == "--version" {
println!("dever {}", env!("CARGO_PKG_VERSION"));
return Ok(None);
}
if command == "tenant" {
return tenant::parse(arguments).map(Some);
}
if !matches!(command.as_str(), "check" | "api" | "emit" | "build" | "run") {
return Err(format!("unknown command '{command}'"));
}
let source = arguments.next().ok_or("missing source path")?.into();
let mut output = None;
let mut entry = "main".to_owned();
let mut forwarded = Vec::new();
let mut target = None;
while let Some(argument) = arguments.next() {
match argument.as_str() {
"--" if command == "run" => {
forwarded.extend(arguments);
break;
}
"--output" if output.is_none() => {
output = Some(arguments.next().ok_or("missing output path")?.into())
}
"--entry" => entry = arguments.next().ok_or("missing entry name")?,
"--target" if command == "build" && target.is_none() => {
let value = arguments.next().ok_or("missing build target")?;
target = Some(dever::compiler::build::BuildTarget::parse(&value)?);
}
_ => return Err(format!("unknown or repeated argument '{argument}'")),
}
}
if command == "build" && output.is_none() {
return Err("build requires --output".into());
}
if !matches!(command.as_str(), "build" | "api") && output.is_some() {
return Err("--output is only valid for build or api".into());
}
Ok(Some(Args {
command,
source,
output,
entry,
target,
arguments: forwarded,
}))
}
#[cfg(test)]
#[path = "../../../../test/rust/cli/args.rs"]
mod tests;
+54
View File
@@ -0,0 +1,54 @@
//! 运维命令复用当前项目生成的原生入口,CLI 不直接打开部署数据库。
use super::Args;
pub(super) fn parse(mut arguments: impl Iterator<Item = String>) -> Result<Args, String> {
let operation = arguments.next().ok_or("missing tenant operation")?;
let action = if operation == "component" {
Some(arguments.next().ok_or("missing component operation")?)
} else {
None
};
let source = arguments.next().ok_or("missing project root")?.into();
let tenant = arguments
.next()
.and_then(|value| value.parse::<i64>().ok())
.filter(|value| *value > 0)
.ok_or("tenant requires a positive internal id")?;
let (command, input) = match operation.as_str() {
"migrate" => ("dever.tenant.migrate", "null".to_owned()),
"component" => {
let command = match action.as_deref() {
Some("enable") => "dever.tenant.enable",
Some("disable") => "dever.tenant.disable",
_ => return Err("expected component enable or disable".into()),
};
let component = arguments.next().ok_or("missing component name")?;
(command, serde_json::to_string(&component).unwrap())
}
"owner" => {
let site = arguments.next().ok_or("missing owner site")?;
let user = arguments
.next()
.and_then(|value| value.parse::<i64>().ok())
.filter(|value| *value > 0)
.ok_or("owner requires a positive internal user id")?;
(
"dever.auth.owner",
serde_json::json!({"site": site, "user_id": user}).to_string(),
)
}
_ => return Err("expected tenant migrate, owner or component".into()),
};
if arguments.next().is_some() {
return Err("unexpected tenant command argument".into());
}
Ok(Args {
command: "run".into(),
source,
output: None,
entry: "main".into(),
target: None,
arguments: vec!["--tenant".into(), tenant.to_string(), command.into(), input],
})
}
+46
View File
@@ -0,0 +1,46 @@
//! 依赖命令只解析参数;解析、缓存和锁的所有权留在 project::dependency。
use dever::compiler::project::dependency;
use std::path::Path;
/// 由主 CLI 在加载 Project 前分派,避免首次 add 被缺失锁阻止。
pub fn execute(command: &str, arguments: &[String]) -> Result<String, String> {
let action = arguments.first().ok_or("missing dependency action")?;
let root = arguments.get(1).ok_or("missing project root")?;
let root = Path::new(root);
let specs = &arguments[2..];
match (command, action.as_str()) {
("package", "export") => {
if specs.len() != 4 || specs[2] != "--output" {
return Err("dever package export <root> <package> <relative-directory> --output <new-directory>".into());
}
dependency::export(root, &specs[0], &specs[1], Path::new(&specs[3]))
}
("package", _) => dependency::execute(action, root, specs),
("lib", _) => {
let mut target = dever::compiler::toolchain::BuildTarget::host()?;
let mut values = Vec::new();
let mut arguments = specs.iter();
let mut selected = false;
while let Some(value) = arguments.next() {
if value == "--target" {
if selected
|| !matches!(action.as_str(), "add" | "update" | "install" | "doctor")
{
return Err(
"--target is accepted once for Lib add/update/install/doctor".into(),
);
}
target = dever::compiler::toolchain::BuildTarget::parse(
arguments.next().ok_or("missing --target value")?,
)?;
selected = true;
} else {
values.push(value.clone());
}
}
dependency::library::command::execute(action, root, &values, target)
}
_ => Err("expected package or lib command".into()),
}
}
+160
View File
@@ -0,0 +1,160 @@
//! 最小引导 CLI:串联检查、生成 Rust、构建和执行。
//! run 只管理自己创建的临时目录与直接子进程,不激活全局工具链。
mod args;
mod dependency;
mod project;
mod release;
mod tools;
use dever::compiler::{build, emit, project::Project};
use std::io::Write;
fn main() -> std::process::ExitCode {
match execute() {
Ok(code) => std::process::ExitCode::from(code),
Err(error) => {
eprintln!("{error}");
std::process::ExitCode::FAILURE
}
}
}
fn execute() -> Result<u8, String> {
let raw = std::env::args().skip(1).collect::<Vec<_>>();
if let Some(result) = tools::execute(&raw) {
return result;
}
if let Some(command) = raw
.first()
.filter(|name| matches!(name.as_str(), "package" | "lib"))
{
println!("{}", dependency::execute(command, &raw[1..])?);
return Ok(0);
}
let Some(arguments) = args::parse(raw.into_iter())? else {
return Ok(0);
};
let project = Project::load(&arguments.source)?;
if arguments.command == "api" {
let snapshot = project.api_snapshot()?;
if let Some(path) = arguments.output {
let mut file = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&path)
.map_err(|error| format!("{}: {error}", path.display()))?;
if let Err(error) = file
.write_all(snapshot.as_bytes())
.and_then(|()| file.sync_all())
{
let _ = std::fs::remove_file(&path);
return Err(format!("{}: {error}", path.display()));
}
} else {
std::io::stdout()
.lock()
.write_all(snapshot.as_bytes())
.map_err(|error| error.to_string())?;
}
return Ok(0);
}
let program = project.check()?;
if !program.application() && !arguments.arguments.is_empty() {
return Err("core fixture entries do not accept command arguments".into());
}
match arguments.command.as_str() {
"check" => println!("checked {} source files", project.sources.len()),
"emit" => {
// 核心库没有固定入口,emit 保留所有函数供检查;应用按 CMD 输出。
let functions = if !program.application() {
(0..program.functions.len()).collect()
} else {
program.entries().collect()
};
print!("{}", emit::generate(&program, &project.sources, &functions));
if program.application() {
print!("{}", emit::application::generate(&program));
}
}
"build" => {
let binary = build::build_for_target(
&program,
&project.sources,
&arguments.entry,
arguments.output.as_ref().unwrap(),
arguments.target.unwrap_or(build::BuildTarget::host()?),
)?;
println!("{}", binary.display());
}
"run" => {
let name = format!(
".dever-run-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_err(|error| error.to_string())?
.as_nanos()
);
// 应用二进制与持久 data 使用项目根;退出只删本次成功构建的临时文件。
let directory = if program.application() {
arguments
.source
.canonicalize()
.map_err(|error| error.to_string())?
} else {
let directory = std::env::temp_dir().join(&name);
std::fs::create_dir(&directory).map_err(|error| error.to_string())?;
directory
};
let destination = directory.join(if program.application() { &name } else { "app" });
let mut built = false;
let result = (|| {
let binary =
build::build(&program, &project.sources, &arguments.entry, &destination)?;
built = true;
let mut forwarded = Vec::new();
if program.has_adapters() || program.has_http() || program.has_database() {
let root = arguments
.source
.canonicalize()
.map_err(|error| error.to_string())?;
forwarded.extend([
"--config".into(),
root.join("config/setting.json")
.to_string_lossy()
.into_owned(),
]);
}
forwarded.extend(arguments.arguments.iter().cloned());
let process = dever::process::command::start(
binary.as_os_str(),
&forwarded,
&std::env::current_dir().map_err(|error| error.to_string())?,
false,
)
.map_err(|error| error.to_string())?;
Ok(process
.wait()
.map_err(|error| error.to_string())?
.code()
.unwrap_or(1) as u8)
})();
let cleanup = if !program.application() {
std::fs::remove_dir_all(&directory)
} else if built {
std::fs::remove_file(&destination)
} else {
Ok(())
};
// 主操作失败优先保留原错误;只有执行成功时才把清理失败向外报告。
return match (result, cleanup) {
(Err(error), _) => Err(error),
(Ok(0), Err(error)) => Err(error.to_string()),
(Ok(code), _) => Ok(code),
};
}
_ => unreachable!(),
}
Ok(0)
}
+18
View File
@@ -0,0 +1,18 @@
//! 项目创建入口不读取目标项目配置,也不安装或激活任何工具。
use std::path::Path;
pub fn execute(arguments: &[String]) -> Result<u8, String> {
let mut directory = None;
for argument in arguments {
if !argument.starts_with('-') && directory.is_none() {
directory = Some(argument);
} else {
return Err("usage: dever new <new-directory>".into());
}
}
let directory = directory.ok_or("usage: dever new <new-directory>")?;
dever::compiler::project::create(Path::new(directory))?;
println!("created {directory}");
Ok(0)
}
+15
View File
@@ -0,0 +1,15 @@
//! 私有作者入口:只组装已准备的本地候选包,不签名、不安装、不联网。
#[path = "mod.rs"]
mod release;
fn main() -> std::process::ExitCode {
let arguments = std::env::args().skip(1).collect::<Vec<_>>();
match release::execute(&arguments) {
Ok(code) => std::process::ExitCode::from(code),
Err(error) => {
eprintln!("{error}");
std::process::ExitCode::FAILURE
}
}
}
+19
View File
@@ -0,0 +1,19 @@
//! 两个作者入口共用同一本地打包实现,不附带签名、安装或发布行为。
pub fn execute(arguments: &[String]) -> Result<u8, String> {
let [author, flag, output] = arguments else {
return Err("local release requires <author-root> --output <new-directory>".into());
};
if flag != "--output" {
return Err("local release requires <author-root> --output <new-directory>".into());
}
let manifest = dever::compiler::toolchain::packaging::create(
std::path::Path::new(author),
std::path::Path::new(output),
)?;
println!(
"created unsigned local bootstrap {} for {} at {output}",
manifest.version, manifest.target
);
Ok(0)
}
+72
View File
@@ -0,0 +1,72 @@
//! 源码查询与领域索引共用同一份检查结果,不参与应用构建。
use dever::compiler::project::Project;
use std::io::Write;
use std::path::Path;
pub(super) fn execute(arguments: &[String]) -> Result<u8, String> {
let source = arguments
.first()
.ok_or("usage: dever inspect <source> [--symbol name] [--depth 1] [--json] | <project> --links [--check]")?;
let mut arguments = arguments[1..].iter();
let mut symbol = None;
let mut depth = None;
let mut json = false;
let mut links = false;
let mut check = false;
while let Some(argument) = arguments.next() {
match argument.as_str() {
"--symbol" if symbol.is_none() => {
symbol = Some(arguments.next().ok_or("missing symbol name")?)
}
"--depth" if depth.is_none() => {
depth = Some(
arguments
.next()
.ok_or("missing depth")?
.parse::<usize>()
.map_err(|_| "depth must be a nonnegative integer")?,
);
}
"--json" if !json => json = true,
"--links" if !links => links = true,
"--check" if !check => check = true,
_ => return Err(format!("unknown or repeated argument '{argument}'")),
}
}
if depth.is_some() && symbol.is_none() {
return Err("--depth requires --symbol".into());
}
if check && !links {
return Err("--check requires --links".into());
}
if links && (symbol.is_some() || depth.is_some() || json) {
return Err("--links cannot be combined with --symbol, --depth or --json".into());
}
let path = Path::new(source)
.canonicalize()
.map_err(|error| format!("{source}: {error}"))?;
let mut index = Project::load(&path)?.inspect()?;
if links {
if !index.documentation_errors.is_empty() {
return Err(index.documentation_errors.join("\n"));
}
return super::links::execute(&path, index.links(&path)?, check);
}
if let Some(symbol) = symbol {
index.select(symbol, depth.unwrap_or(1))?;
}
let mut output = std::io::stdout().lock();
if json {
serde_json::to_writer(&mut output, &index).map_err(|error| error.to_string())?;
writeln!(output).map_err(|error| error.to_string())?;
} else {
for error in &index.documentation_errors {
eprintln!("{error}");
}
output
.write_all(index.text().as_bytes())
.map_err(|error| error.to_string())?;
}
Ok(0)
}
+84
View File
@@ -0,0 +1,84 @@
//! 只管理带生成标记的领域 link.md;先核对全部目标,再写入实际变化。
use dever::compiler::inspect::LINK_MARKER;
use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
pub(super) fn execute(
root: &Path,
expected: BTreeMap<PathBuf, String>,
check: bool,
) -> Result<u8, String> {
let count = expected.len();
let mut existing = BTreeMap::new();
// 固定两层业务目录,不扫描数据、依赖目录或自行创建新的组织层级。
for component in directories(&root.join("module"))? {
for domain in directories(&component)? {
let path = domain.join("link.md");
match fs::read_to_string(&path) {
Ok(text) => {
existing.insert(path, text);
}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => return Err(format!("{}: {error}", path.display())),
}
}
}
let mut changed = Vec::new();
for (path, text) in expected {
let previous = existing.remove(&path);
if previous
.as_ref()
.is_some_and(|value| !value.starts_with(LINK_MARKER))
{
return Err(format!(
"{}: refusing to overwrite a handwritten link.md",
path.display()
));
}
if previous.as_ref() != Some(&text) {
changed.push((path, Some(text)));
}
}
for (path, text) in existing {
if text.starts_with(LINK_MARKER) {
changed.push((path, None));
}
}
for (path, text) in &changed {
let action = if check {
"stale"
} else if let Some(text) = text {
fs::write(path, text).map_err(|error| format!("{}: {error}", path.display()))?;
"updated"
} else {
fs::remove_file(path).map_err(|error| format!("{}: {error}", path.display()))?;
"removed"
};
println!("{action} {}", path.strip_prefix(root).unwrap().display());
}
println!(
"checked {count} domain indexes; {} {}",
changed.len(),
if check { "stale" } else { "changed" }
);
Ok(u8::from(check && !changed.is_empty()))
}
fn directories(path: &Path) -> Result<Vec<PathBuf>, String> {
let mut directories = Vec::new();
for entry in fs::read_dir(path).map_err(|error| format!("{}: {error}", path.display()))? {
let entry = entry.map_err(|error| format!("{}: {error}", path.display()))?;
if entry
.file_type()
.map_err(|error| format!("{}: {error}", entry.path().display()))?
.is_dir()
&& !entry.file_name().to_string_lossy().starts_with('.')
{
directories.push(entry.path());
}
}
directories.sort();
Ok(directories)
}
+39
View File
@@ -0,0 +1,39 @@
//! 正式源码工具在普通应用参数解析前分派,输出退出码交回唯一 CLI 入口。
use dever::compiler::format;
use std::path::Path;
mod inspect;
mod links;
mod skill;
mod testing;
pub fn execute(arguments: &[String]) -> Option<Result<u8, String>> {
match arguments.first().map(String::as_str) {
Some("new") => Some(super::project::execute(&arguments[1..])),
Some("release") => Some(super::release::execute(&arguments[1..])),
Some("skill") => Some(skill::execute(&arguments[1..])),
Some("inspect") => Some(inspect::execute(&arguments[1..])),
Some("fmt") => Some(format(arguments)),
Some("test") => Some(testing::execute(arguments)),
_ => None,
}
}
fn format(arguments: &[String]) -> Result<u8, String> {
let source = arguments.get(1).ok_or("fmt requires a source path")?;
let check = match &arguments[2..] {
[] => false,
[flag] if flag == "--check" => true,
_ => return Err("usage: dever fmt <source> [--check]".into()),
};
let report = format::project(Path::new(source), check)?;
for path in &report.changed {
println!(
"{} {}",
if check { "unformatted" } else { "formatted" },
path.display()
);
}
println!("checked {} source files", report.checked);
Ok(u8::from(check && !report.changed.is_empty()))
}
+35
View File
@@ -0,0 +1,35 @@
//! skill 只解析当前候选的规则并更新调用者项目,不操作全局安装。
use dever::compiler::toolchain::skill;
use std::path::Path;
pub(super) fn execute(arguments: &[String]) -> Result<u8, String> {
match arguments {
[command] if command == "path" => {
let directory = std::env::current_dir().map_err(|error| error.to_string())?;
let project = match std::fs::symlink_metadata(directory.join("module")) {
Ok(_) => Some(directory.as_path()),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => None,
Err(error) => return Err(error.to_string()),
};
println!("{}", skill::path(project)?.display());
}
[command, project] if command == "path" => {
println!("{}", skill::path(Some(Path::new(project)))?.display());
}
[command] if command == "install" => {
let directory = std::env::current_dir().map_err(|error| error.to_string())?;
let changed = skill::install(&directory)?;
println!(
"{}",
if changed {
"installed Dever guidance in AGENTS.md"
} else {
"Dever guidance is current"
}
);
}
_ => return Err("usage: dever skill path [project-root] | dever skill install".into()),
}
Ok(0)
}
+60
View File
@@ -0,0 +1,60 @@
//! Test 参数独立校验;过滤发生在全源码检查之后、编译之前。
use dever::compiler::testing::{self, Suite};
use std::path::Path;
use std::time::Duration;
pub(super) fn execute(arguments: &[String]) -> Result<u8, String> {
let root = arguments.get(1).ok_or("test requires a project root")?;
let mut filter = None;
let mut timeout = None;
let mut options = arguments[2..].iter();
while let Some(option) = options.next() {
match option.as_str() {
"--filter" if filter.is_none() => {
filter = Some(options.next().ok_or("missing test filter")?.as_str())
}
"--timeout-ms" if timeout.is_none() => {
let text = options.next().ok_or("missing test timeout")?;
if text.is_empty() || !text.bytes().all(|value| value.is_ascii_digit()) {
return Err("test timeout must be positive integer milliseconds".into());
}
let value: u64 = text.parse().map_err(|_| "invalid test timeout")?;
if value == 0 {
return Err("test timeout must be positive".into());
}
timeout = Some(Duration::from_millis(value));
}
_ => return Err(format!("unknown or repeated test argument '{option}'")),
}
}
let suite = Suite::load(Path::new(root))?;
let report = testing::run(&suite, filter, timeout.unwrap_or(Duration::from_secs(60)))?;
for case in &report.cases {
println!(
"{} {} ({:.3}s)",
if case.timed_out {
"TIMEOUT"
} else if case.success {
"PASS"
} else {
"FAIL"
},
case.name,
case.elapsed.as_secs_f64()
);
if !case.stdout.is_empty() {
println!("stdout [{}]:\n{}", case.name, case.stdout);
}
if !case.stderr.is_empty() {
eprintln!("stderr [{}]:\n{}", case.name, case.stderr);
}
}
let passed = report.cases.iter().filter(|case| case.success).count();
println!(
"{passed} passed; {} failed; {} not run",
report.cases.len() - passed,
report.not_run
);
Ok(u8::from(!report.success()))
}
+265
View File
@@ -0,0 +1,265 @@
//! 以生成内容标识构建缓存,并输出独立的原生程序。
//! 构建锁保护同一缓存;用户指定的输出必须新建,不能覆盖已有文件。
mod resources;
mod toolchain;
pub use resources::Resource;
pub use crate::compiler::toolchain::BuildTarget;
use crate::compiler::{
emit,
model::{Program, usage::Usage},
};
use crate::syntax::source::Source;
use std::collections::BTreeSet;
use std::hash::{Hash, Hasher};
use std::io::{Read, Seek};
use std::path::{Path, PathBuf};
pub fn build(
program: &Program,
sources: &[Source],
entry: &str,
output: &Path,
) -> Result<PathBuf, String> {
build_for_target(program, sources, entry, output, BuildTarget::host()?)
}
/// 显式目标进入缓存身份和 rustc 参数;跨平台 Worker 尚未准备时直接报错。
pub fn build_for_target(
program: &Program,
sources: &[Source],
entry: &str,
output: &Path,
target: BuildTarget,
) -> Result<PathBuf, String> {
let resources = crate::compiler::project::dependency::worker::prepare(program, target)?;
build_with_resources(program, sources, entry, output, target, &resources)
}
/// 已验证资源的发行消费与自有夹具共用同一生成、缓存和目标校验路径。
pub fn build_with_resources(
program: &Program,
sources: &[Source],
entry: &str,
output: &Path,
target: BuildTarget,
resources: &[Resource],
) -> Result<PathBuf, String> {
let (roots, main) = if program.application() {
if entry != "main" {
return Err("application entries come from API/CMD declarations, not --entry".into());
}
(
program.entries().collect::<BTreeSet<_>>(),
emit::application::generate(program),
)
} else {
let (package, name) = entry.rsplit_once('.').unwrap_or(("main", entry));
let index = program
.functions
.iter()
.position(|function| {
program.packages[function.signature.source] == package
&& function.signature.name == name
&& function.signature.inputs.is_empty()
&& function.signature.static_parameters == 0
})
.ok_or_else(|| format!("no zero-input function '{entry}'"))?;
if program.functions[index]
.signature
.outputs
.iter()
.any(|(_, kind)| kind.contains_secret(&program.definitions))
{
return Err("entry output contains Secret and cannot be rendered".into());
}
if program.functions[index].signature.effects.database {
return Err("Model execution requires an application entry".into());
}
if program.functions[index].signature.effects.request {
return Err("request context requires an application HTTP entry".into());
}
let call = emit::task::entry(
&format!("function{index}"),
Vec::new(),
program.functions[index].signature.effects,
program.has_workers(program.reachable([index])),
);
(
[index].into(),
format!(
"fn main() {{ match {call} {{ Ok(value) => println!(\"{{:?}}\", value), Err(error) => {{ eprintln!(\"{{error}}\"); std::process::exit(1); }} }} }}\n"
),
)
};
let functions = program.reachable(roots.iter().copied());
let mut source = emit::generate(program, sources, &roots);
source.push_str(&main);
if program.external_workers().next().is_some() && resources.is_empty() {
return Err("external Adapter requires prepared Worker resources".into());
}
resources::source(resources, &mut source)?;
let features = runtime_features(program, &functions);
compile_for_target(&source, &features, output, false, target, resources)
}
/// 源码测试与正式构建共享缓存、离线工具链和不可覆盖输出;测试只改变优化配置。
pub(crate) fn compile(
source: &str,
features: &[&str],
output: &Path,
testing: bool,
) -> Result<PathBuf, String> {
compile_for_target(source, features, output, testing, BuildTarget::host()?, &[])
}
fn compile_for_target(
source: &str,
features: &[&str],
output: &Path,
testing: bool,
target: BuildTarget,
resources: &[Resource],
) -> Result<PathBuf, String> {
let rust_root = crate::compiler::toolchain::rust_root()?;
toolchain::prepare(&rust_root)?;
toolchain::require_target(&rust_root, target)?;
let profile = if testing {
"opt-level = 1\nlto = false\ncodegen-units = 16\noverflow-checks = true\nstrip = \"symbols\""
} else {
"lto = \"fat\"\ncodegen-units = 1\noverflow-checks = true\nstrip = \"symbols\""
};
let mut hash = std::collections::hash_map::DefaultHasher::new();
source.hash(&mut hash);
// 相同 Rust 源码可选择不同数据库驱动;依赖特性也是生成项目的身份。
features.hash(&mut hash);
// Cargo 清单属于构建输入;优化或符号配置变化时不能复用旧清单目录。
profile.hash(&mut hash);
target.hash(&mut hash);
// 相对依赖使已使用过的开发包也能移动;路径布局参与旧缓存的区分。
let runtime_path = "../../../src/runtime";
runtime_path.hash(&mut hash);
let name = format!("dever-program-{:016x}", hash.finish());
let directory = rust_root.join("target/generated").join(&name);
std::fs::create_dir_all(&directory).map_err(|error| error.to_string())?;
let lock = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(directory.join("build.lock"))
.map_err(|error| error.to_string())?;
// 持有锁直到构建并复制产物完成,避免同一生成项目被并发改写。
lock.lock().map_err(|error| error.to_string())?;
resources::stage(&directory, resources)?;
let features = format!(", features = {features:?}");
let manifest = format!(
"[package]\nname = {name:?}\nversion = \"0.0.1\"\nedition = \"2024\"\n[workspace]\n[[bin]]\nname = {name:?}\npath = \"main.rs\"\n[dependencies]\ndever-runtime = {{ path = {:?}{features} }}\n[profile.release]\n{profile}\n",
runtime_path
);
write_source(&directory.join("main.rs"), source)?;
write_source(&directory.join("Cargo.toml"), &manifest)?;
let binary = toolchain::compile(
&rust_root,
&directory,
&rust_root.join("target/programs"),
&name,
target,
)?;
let mut binary = std::fs::File::open(&binary).map_err(|error| error.to_string())?;
let mut header = [0; 64];
binary
.read_exact(&mut header)
.map_err(|error| error.to_string())?;
target.validate_executable(&header)?;
binary.rewind().map_err(|error| error.to_string())?;
let permissions = binary
.metadata()
.map_err(|error| error.to_string())?
.permissions();
let mut destination = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(output)
.map_err(|error| format!("{}: {error}", output.display()))?;
let result = std::io::copy(&mut binary, &mut destination)
.and_then(|_| destination.set_permissions(permissions))
.and_then(|()| destination.sync_all());
if let Err(error) = result {
let _ = std::fs::remove_file(output);
return Err(error.to_string());
}
output.canonicalize().map_err(|error| error.to_string())
}
/// 按实际输出的函数和类型选择依赖;类型引用本身不会启动运行时。
pub fn runtime_features(program: &Program, functions: &BTreeSet<usize>) -> Vec<&'static str> {
let mut features = Vec::new();
if program.has_workers(functions.iter().copied()) {
features.push("worker");
}
if !program.jobs.is_empty() {
features.push("job");
}
if program.has_roles() {
features.push("authorization");
}
if program.has_auth() {
features.push("auth");
}
if program.has_http() {
features.push("api");
}
if !program.adapters.ports.is_empty() {
features.push("adapter");
}
let usage = Usage::new(program, functions);
if usage.upload {
features.push("upload");
}
if program.has_database() || usage.database {
features.push("database");
if cfg!(feature = "postgres") {
features.push("postgres");
}
}
if program.application() || usage.json {
features.push("wire");
}
if usage.crypto {
features.push("crypto");
} else if usage.secret {
features.push("secret");
}
if usage.uuid {
features.push("uuid");
}
if usage.calendar {
features.push("calendar");
}
if program.has_http() || usage.http {
features.push("http");
} else if usage.tls {
features.push("tls");
} else if usage.net {
features.push("net");
} else if usage.task {
features.push("task");
}
features
}
fn write_source(path: &Path, contents: &str) -> Result<(), String> {
match std::fs::read_to_string(path) {
Ok(previous) if previous == contents => Ok(()),
Ok(_) => Err(format!(
"generated source identity collision: {}",
path.display()
)),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
std::fs::write(path, contents).map_err(|error| error.to_string())
}
Err(error) => Err(error.to_string()),
}
}
+76
View File
@@ -0,0 +1,76 @@
//! 资源是独立二进制输入;源码仅引用固定名称,摘要和模式进入生成内容身份。
use sha2::{Digest, Sha256};
use std::{collections::BTreeSet, fmt::Write, io::Write as IoWrite, path::Path};
#[derive(Clone)]
pub struct Resource {
pub path: String,
pub bytes: Vec<u8>,
pub executable: bool,
}
pub(super) fn source(resources: &[Resource], output: &mut String) -> Result<(), String> {
if resources.is_empty() {
return Ok(());
}
if resources.len() > 4096 {
return Err("Worker resource count exceeds 4096".into());
}
let mut paths = BTreeSet::new();
let mut total = 0usize;
output.push_str("fn worker_assets() -> &'static dever_runtime::worker::Bundle {\nstatic FILES: &[dever_runtime::worker::Asset] = &[\n");
for (index, resource) in resources.iter().enumerate() {
if resource.path.is_empty()
|| resource.path.contains(['\\', ':', '\0'])
|| resource
.path
.split('/')
.any(|part| matches!(part, "" | "." | ".."))
|| !paths.insert(&resource.path)
{
return Err("invalid or duplicate Worker resource path".into());
}
total = total
.checked_add(resource.bytes.len())
.ok_or("Worker resources exceed size limit")?;
if total > 256 * 1024 * 1024 || resource.bytes.len() > 128 * 1024 * 1024 {
return Err("Worker resources exceed size limit".into());
}
let sha256 = format!("{:x}", Sha256::digest(&resource.bytes));
writeln!(output, "dever_runtime::worker::Asset {{ path: {:?}, bytes: include_bytes!(\"resource{index}\"), sha256: {sha256:?}, executable: {} }},", resource.path, resource.executable).unwrap();
}
output.push_str("];\nstatic BUNDLE: dever_runtime::worker::Bundle = dever_runtime::worker::Bundle { files: FILES }; &BUNDLE }\n");
Ok(())
}
pub(super) fn stage(directory: &Path, resources: &[Resource]) -> Result<(), String> {
for (index, resource) in resources.iter().enumerate() {
let path = directory.join(format!("resource{index}"));
let mut options = std::fs::OpenOptions::new();
options.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600);
}
match options.open(&path) {
Ok(mut file) => file
.write_all(&resource.bytes)
.and_then(|()| file.sync_all())
.map_err(|error| error.to_string())?,
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
let metadata =
std::fs::symlink_metadata(&path).map_err(|error| error.to_string())?;
if !metadata.is_file()
|| metadata.len() != resource.bytes.len() as u64
|| std::fs::read(&path).map_err(|error| error.to_string())? != resource.bytes
{
return Err("existing compiler resource input differs".into());
}
}
Err(error) => return Err(error.to_string()),
}
}
Ok(())
}
+106
View File
@@ -0,0 +1,106 @@
//! 通过独立 process 模块调用固定版本的官方 Cargo/rustc。
//! 保持离线 release 构建,失败直接返回实际退出状态与诊断。
use crate::compiler::toolchain::{BuildTarget, RUST_VERSION};
use crate::process::{child::output::require_success, command};
use std::ffi::OsString;
use std::path::{Path, PathBuf};
/// 在线安装与编译共用准备脚本和文件锁;就绪后的构建不会联网。
pub(super) fn prepare(rust_root: &Path) -> Result<(), String> {
let tools = rust_root.parent().unwrap().join("toolchain");
if !tools.join("online").is_file() || tools.join("ready").is_file() {
return Ok(());
}
let child = command::start("bash".as_ref(), &[tools.join("prepare")], rust_root, false)
.map_err(|error| format!("cannot prepare Dever build environment: {error}"))?;
let status = child.wait().map_err(|error| error.to_string())?;
if !status.success() {
return Err(format!(
"Dever environment preparation failed ({status}). Details: {}. Run the command again to resume.",
tools.join("prepare.log").display()
));
}
Ok(())
}
pub(super) fn compile(
rust_root: &Path,
project: &Path,
target: &Path,
name: &str,
platform: BuildTarget,
) -> Result<PathBuf, String> {
let (program, mut arguments) = executable(rust_root, "cargo");
arguments.extend([
OsString::from("build"),
OsString::from("--offline"),
OsString::from("--release"),
OsString::from("--target-dir"),
target.as_os_str().to_owned(),
]);
// 本机保留既有 Cargo 缓存;交叉目标使用 Cargo 自带的独立目标目录。
let cross = platform != BuildTarget::host()?;
if cross {
arguments.extend([
OsString::from("--target"),
OsString::from(platform.triple()),
]);
}
let child =
command::start(&program, &arguments, project, true).map_err(|error| error.to_string())?;
let output = child.output().map_err(|error| error.to_string())?;
if !output.status.success() {
let log = project.join("build.log");
std::fs::write(&log, &output.stderr).map_err(|error| error.to_string())?;
return Err(format!(
"Dever build failed ({}). Details: {}",
output.status,
log.display()
));
}
let directory = if cross {
target.join(platform.triple())
} else {
target.to_path_buf()
};
Ok(directory.join("release").join(name))
}
pub(super) fn require_target(rust_root: &Path, target: BuildTarget) -> Result<(), String> {
if target == BuildTarget::host()? {
return Ok(());
}
let (program, mut arguments) = executable(rust_root, "rustc");
arguments.extend([
"--print".into(),
"target-libdir".into(),
"--target".into(),
target.triple().into(),
]);
let directory = std::env::current_dir().map_err(|error| error.to_string())?;
let child = command::start(&program, &arguments, &directory, true)
.map_err(|error| error.to_string())?;
let output = child.output().map_err(|error| error.to_string())?;
let output = require_success(output)?;
let location = std::str::from_utf8(&output)
.map_err(|error| error.to_string())?
.trim();
if !Path::new(location).is_dir() {
return Err(format!(
"{} Rust standard library is not prepared for {RUST_VERSION}; cross builds require an offline target sysroot and linker",
target.name()
));
}
Ok(())
}
/// 开发包直接使用私有工具链;源码作者仍使用 rustup 管理的固定版本。
fn executable(rust_root: &Path, name: &str) -> (OsString, Vec<OsString>) {
let tools = rust_root.parent().unwrap().join("toolchain");
if tools.is_dir() {
(tools.join("bin").join(name).into_os_string(), Vec::new())
} else {
(name.into(), vec![format!("+{RUST_VERSION}").into()])
}
}
+53
View File
@@ -0,0 +1,53 @@
//! 将已验证声明交给快照;模板保留参数合同,范围使用声明而非函数体推导结果。
use super::handler::Expansion;
use crate::compiler::model::{Signature, api, package};
use crate::syntax::ast;
use std::collections::BTreeSet;
pub(super) fn exports(
expansion: &Expansion,
signatures: &[Signature],
declarations: &[ast::TypeDeclaration],
packages: &[String],
) -> api::Exports {
let functions = expansion
.exports
.iter()
.map(|export| {
let mut signature = signatures[export.index].clone();
signature
.output_ranges
.clone_from(&expansion.instances[export.index].signature.output_ranges);
let mut recoveries = BTreeSet::new();
let mut pending = vec![export.index];
let mut visited = BTreeSet::new();
while let Some(index) = pending.pop() {
if !visited.insert(index) {
continue;
}
if let Some(reason) = &signatures[index].recovery {
recoveries.insert(reason.clone());
}
pending.extend(expansion.instances[index].scope.targets.values().copied());
}
api::Function {
signature,
handlers: export.handlers.clone(),
recoveries,
}
})
.collect();
let definitions = declarations
.iter()
.enumerate()
.filter(|(_, declaration)| {
declaration.public && !package::standard(&packages[declaration.span.source])
})
.map(|(index, _)| index)
.collect();
api::Exports {
functions,
definitions,
}
}
+186
View File
@@ -0,0 +1,186 @@
//! HTTP 声明复用 App 签名和 wire 合同;查询输入仅允许标量。
use super::*;
pub(in crate::compiler::check) fn check(
entries: &[ast::HttpEntry],
signatures: &[Signature],
definitions: &[model::Definition],
names: &Types,
verifiers: &[model::auth::Verifier],
) -> Result<Vec<model::HttpEntry>, Diagnostic> {
let mut result = Vec::new();
let mut seen = HashSet::new();
for entry in entries {
let span = entry.span;
if !entry.public && verifiers.is_empty() {
return Err(Diagnostic::new(
"C014",
"authenticated HTTP requires a compiled App verify function",
span,
));
}
let command = ast::Command {
name: entry.name.clone(),
target: entry.target.clone(),
long: entry.long,
span,
};
let checked = commands(&[command], signatures, definitions, names, true)?;
let function = checked[0].function;
let signature = &signatures[function];
if signature
.inputs
.iter()
.any(|(_, kind)| *kind == Type::Upload)
{
if entry.method != "post"
|| signature
.inputs
.iter()
.filter(|(_, kind)| *kind == Type::Upload)
.count()
!= 1
{
return Err(Diagnostic::new(
"C013",
"Upload requires one top-level POST input",
span,
));
}
for (_, kind) in &signature.inputs {
let inner = if let Type::Optional(inner) = kind {
inner.as_ref()
} else {
kind
};
if !matches!(
inner,
Type::Upload
| Type::Bool
| Type::Int
| Type::Float
| Type::Decimal
| Type::Text
| Type::Uuid
| Type::Id
| Type::ModelId(_)
| Type::DateTime
| Type::Date
| Type::Time
| Type::Duration
) && !matches!(inner, Type::Named(index) if matches!(definitions[*index].shape, Shape::Choice(_)))
{
return Err(Diagnostic::new(
"C013",
"multipart inputs must be Upload and scalars",
span,
));
}
}
}
if (entry.public || entry.authenticated) && signature.effects.enqueue {
return Err(Diagnostic::new(
"C014",
"HTTP enqueue requires a protected permission-bound entry",
span,
));
}
if (entry.public || entry.authenticated || entry.method == "get")
&& signature.effects.authorization_write
{
return Err(Diagnostic::new(
"C014",
"authorization writes require a protected write API",
span,
));
}
if entry.public && signature.effects.identity {
return Err(Diagnostic::new(
"C014",
"public HTTP cannot read trusted identity",
span,
));
}
if signature.effects.request && signature.effects.blocking {
return Err(Diagnostic::new(
"C014",
"request context cannot move to a blocking worker",
span,
));
}
if entry.method == "get" && signature.effects.database_write {
return Err(Diagnostic::new("C014", "GET cannot write to a Model", span));
}
if matches!(entry.method.as_str(), "get" | "delete") {
for (_, kind) in &signature.inputs {
let inner = if let Type::Optional(inner) = kind {
inner
} else {
kind
};
if !matches!(
inner,
Type::Bool
| Type::Int
| Type::Float
| Type::Decimal
| Type::Text
| Type::Uuid
| Type::Id
| Type::ModelId(_)
| Type::DateTime
| Type::Date
| Type::Time
| Type::Duration
) && !matches!(inner, Type::Named(index) if matches!(definitions[*index].shape, Shape::Choice(_)))
{
return Err(Diagnostic::new(
"C005",
"HTTP query inputs must be scalar",
span,
));
}
}
}
let identity = &names.packages[span.source];
let (domain, _) = package::role(identity).unwrap();
let topic = identity
.strip_prefix(&format!("{domain}.api."))
.unwrap_or("")
.replace('.', "/");
let path = format!(
"/{}/{}{}",
domain.replace('.', "/"),
if topic.is_empty() {
String::new()
} else {
format!("{topic}/")
},
entry.name
);
let method = entry.method.to_ascii_uppercase();
if !seen.insert((method.clone(), path.clone())) {
return Err(Diagnostic::new(
"C002",
"duplicate HTTP method and path",
span,
));
}
result.push(model::HttpEntry {
access: if entry.public {
model::auth::Access::Public
} else if entry.authenticated {
model::auth::Access::Authenticated
} else {
model::auth::Access::Protected
},
method,
path,
topic,
function,
long: entry.long,
});
}
Ok(result)
}
+383
View File
@@ -0,0 +1,383 @@
//! 检查应用职责、CMD 绑定和可静态编码的边界类型。
//! 入口只绑定本领域 App,所有业务参数在调用前完成严格解码。
use super::{names, package::Types};
use crate::compiler::model::{self, Shape, Signature, Type, package};
use crate::syntax::{ast, diagnostic::Diagnostic, source::Span};
use std::collections::HashSet;
pub(super) mod http;
pub(super) mod port;
pub(super) mod rest;
pub(super) fn reachable(
entries: impl Iterator<Item = usize>,
expansion: &super::handler::Expansion,
packages: &[String],
) -> Result<(), Diagnostic> {
let mut pending = entries.collect::<Vec<_>>();
if pending.is_empty() {
return Ok(());
}
// 用展开后的静态调用边确认可达性;原型检查实例不能使死代码变成生产入口。
let mut visited = HashSet::new();
let mut declarations = HashSet::new();
while let Some(index) = pending.pop() {
if !visited.insert(index) {
continue;
}
let instance = &expansion.instances[index];
declarations.insert((instance.span.source, instance.span.start));
pending.extend(&expansion.calls[index]);
}
for instance in &expansion.instances {
if !instance.prototype
&& matches!(
package::role(&packages[instance.span.source]),
Some((_, "app" | "domain"))
)
&& !declarations.contains(&(instance.span.source, instance.span.start))
{
return Err(Diagnostic::new(
"C016",
"application function is unreachable from production entries",
instance.span,
));
}
}
Ok(())
}
pub(super) fn prepare(modules: &mut [ast::Module], packages: &[String]) -> Result<(), Diagnostic> {
for module in modules {
for declaration in &mut module.declarations {
let span = declaration.span();
let role = package::role(&packages[span.source]).map(|(_, role)| role);
match declaration {
ast::Declaration::External(_) => {
if role != Some("adapter") {
return Err(Diagnostic::new(
"C006",
"external requires an Adapter source",
span,
));
}
}
ast::Declaration::Persistence(_) => {
if role != Some("model") {
return Err(Diagnostic::new(
"C006",
"storage declarations require a Model source",
span,
));
}
}
ast::Declaration::Rest(_) => {
if role != Some("api") {
return Err(Diagnostic::new(
"C006",
"REST declarations require an API source",
span,
));
}
}
ast::Declaration::Job(value) => {
if role != Some("job") {
return Err(Diagnostic::new(
"C006",
"Job declarations require a Job source",
span,
));
}
super::job::prepare(value)?;
}
ast::Declaration::Schedule(value) => {
names::identifier(&value.target, false, span)?;
if role != Some("job") {
return Err(Diagnostic::new(
"C006",
"schedule declarations require a Job source",
span,
));
}
}
ast::Declaration::Database(name, _) => {
names::identifier(name, false, span)?;
if !matches!(role, Some("model" | "job")) {
return Err(Diagnostic::new(
"C006",
"database binding requires a Model source",
span,
));
}
}
ast::Declaration::Http(value) => {
names::identifier(&value.name, false, span)?;
if role != Some("api") {
return Err(Diagnostic::new(
"C006",
"HTTP declarations require an API source",
span,
));
}
}
ast::Declaration::Command(value) => {
names::identifier(&value.name, false, span)?;
if role != Some("api") {
return Err(Diagnostic::new(
"C006",
"CMD declarations require an API source",
span,
));
}
}
ast::Declaration::Type(value) => {
if value.global && role != Some("model") {
return Err(Diagnostic::new(
"C006",
"global types belong to Model",
span,
));
}
if value.setting && (role != Some("adapter") || value.public) {
return Err(Diagnostic::new(
"C006",
"setting belongs to an Adapter",
span,
));
}
declaration_role(role, value.public, span)?;
value.public |= role == Some("app")
|| (role == Some("model")
&& (matches!(value.body, ast::TypeBody::Choice(_))
|| package::model_record(&packages[span.source]).as_deref()
== Some(value.name.as_str())));
}
ast::Declaration::Function(value) => {
if role == Some("model") || (value.transaction && role != Some("app")) {
return Err(Diagnostic::new(
"C006",
"Model declares storage; transaction functions belong to App",
span,
));
}
declaration_role(role, value.public, span)?;
if role == Some("adapter")
&& (value.inputs.iter().any(|input| input.name == "setting")
|| value.outputs.iter().any(|field| field.name == "setting"))
{
return Err(Diagnostic::new(
"C005",
"Adapter parameters cannot shadow setting",
span,
));
}
if (role == Some("port")) != value.contract {
return Err(Diagnostic::new(
"C006",
"only Port declares bodyless operations with fails Choice",
span,
));
}
value.public |= role == Some("app");
}
}
}
}
Ok(())
}
fn declaration_role(role: Option<&str>, public: bool, span: Span) -> Result<(), Diagnostic> {
match role {
Some("app" | "domain" | "port" | "adapter" | "model" | "test") if public => Err(
Diagnostic::new("C006", "application declarations do not use public", span),
),
Some("api") => Err(Diagnostic::new(
"C006",
"API sources declare entries, not types or functions",
span,
)),
Some("app" | "domain" | "port" | "adapter" | "model" | "test") | None => Ok(()),
Some(_) => Err(Diagnostic::new(
"U001",
"this application role is not implemented yet",
span,
)),
}
}
pub(super) fn commands(
declarations: &[ast::Command],
signatures: &[Signature],
definitions: &[model::Definition],
names: &Types,
http: bool,
) -> Result<Vec<model::Command>, Diagnostic> {
let mut commands = Vec::new();
let mut seen = HashSet::new();
for declaration in declarations {
let span = declaration.span;
let (domain, _) = package::role(&names.packages[span.source]).unwrap();
let function_name = declaration
.target
.strip_prefix("app.")
.filter(|name| !name.contains('.'))
.ok_or_else(|| {
Diagnostic::new("C006", "CMD must bind app.function in its own domain", span)
})?;
let matching = signatures
.iter()
.enumerate()
.filter(|(_, signature)| {
package::namespace(&names.packages[signature.source]) == domain
&& signature.name == function_name
})
.collect::<Vec<_>>();
let [(function, signature)] = matching.as_slice() else {
return Err(Diagnostic::new(
"C005",
"CMD target must resolve to one App signature",
span,
));
};
if !http && signature.effects.request {
return Err(Diagnostic::new(
"C014",
"CMD cannot access HTTP request context",
span,
));
}
if declaration.long && signature.effects.unscoped_write {
return Err(Diagnostic::new(
"C014",
"long entry writes require an explicit transaction function",
span,
));
}
if !declaration.long
&& signature.effects.database_write
&& signature.effects.transaction_unsafe()
{
return Err(Diagnostic::new(
"C014",
"writing entries cannot mix an automatic transaction with external or concurrent work",
span,
));
}
if signature.static_parameters != 0 {
return Err(Diagnostic::new(
"C005",
"CMD cannot expose handler parameters",
span,
));
}
for (_, kind) in &signature.inputs {
if http && *kind == Type::Upload {
continue;
}
wire_input(kind, definitions, span, true)?;
}
for (_, kind) in &signature.outputs {
wire_type(kind, definitions, span)?;
}
let name = format!("{domain}.{}", declaration.name);
if !seen.insert(name.clone()) {
return Err(Diagnostic::new(
"C002",
"duplicate application command",
span,
));
}
commands.push(model::Command {
name,
function: *function,
long: declaration.long,
});
}
Ok(commands)
}
pub(super) fn wire_type(
kind: &Type,
definitions: &[model::Definition],
span: Span,
) -> Result<(), Diagnostic> {
wire_input(kind, definitions, span, false)
}
fn wire_input(
kind: &Type,
definitions: &[model::Definition],
span: Span,
secret: bool,
) -> Result<(), Diagnostic> {
wire_inner(kind, definitions, span, secret, &mut HashSet::new())
}
fn wire_inner(
kind: &Type,
definitions: &[model::Definition],
span: Span,
secret: bool,
seen: &mut HashSet<usize>,
) -> Result<(), Diagnostic> {
match kind {
Type::Secret if secret => Ok(()),
Type::Bool
| Type::Int
| Type::Float
| Type::Decimal
| Type::Json
| Type::Text
| Type::Uuid
| Type::Id
| Type::ModelId(_)
| Type::DateTime
| Type::Date
| Type::Time
| Type::Duration => Ok(()),
Type::List(inner) | Type::Optional(inner) => {
wire_inner(inner, definitions, span, secret, seen)
}
Type::Named(index) if !seen.insert(*index) => Ok(()),
Type::Named(index) => match &definitions[*index].shape {
Shape::Record(fields) => {
if !definitions[*index].private_fields.is_empty() {
return Err(Diagnostic::new(
"C006",
"private fields cannot cross a command wire boundary",
span,
));
}
if definitions[*index].ranges.iter().any(Option::is_some) {
return Err(Diagnostic::new(
"C011",
"constrained records cannot cross a command wire boundary",
span,
));
}
for (_, kind) in fields {
wire_inner(kind, definitions, span, secret, seen)?;
}
Ok(())
}
Shape::Choice(variants)
if variants
.iter()
.all(|variant| !variant.error && variant.fields.is_empty()) =>
{
Ok(())
}
Shape::Choice(_) => Err(Diagnostic::new(
"C005",
"only payload-free non-error Choice can cross a wire boundary",
span,
)),
},
_ => Err(Diagnostic::new(
"U001",
"this command wire type is not implemented yet",
span,
)),
}
}
@@ -0,0 +1,102 @@
//! 检查 Port 的值签名和公开错误集合,沿用普通类型解析与合同规则。
use super::super::super::{names, package::Types, signature};
use crate::compiler::model::{Definition, Failure, Shape, Signature, Type, package};
use crate::syntax::{ast, diagnostic::Diagnostic};
pub(super) fn plain(declaration: &ast::Function) -> Result<(), Diagnostic> {
if declaration.pure
|| declaration.recovery.is_some()
|| declaration.inputs.iter().any(
|input| !matches!(&input.pattern, ast::Pattern::Typed(_, bounds) if bounds.is_empty()),
)
|| declaration
.outputs
.iter()
.any(|field| !field.bounds.is_empty())
{
return Err(Diagnostic::new(
"C005",
"Port signatures require plain typed inputs and outputs without handlers, ranges, pure or recover",
declaration.span,
));
}
Ok(())
}
pub(super) fn signature(
declaration: &ast::Function,
types: &Types,
definitions: &[Definition],
) -> Result<Signature, Diagnostic> {
plain(declaration)?;
for input in &declaration.inputs {
if let ast::Pattern::Typed(reference, _) = &input.pattern {
// 值类型必须能独立解析,不能把 Choice 分支模式当作 Port 参数类型。
names::resolve(reference, types)?;
}
}
let signature = signature::check(std::slice::from_ref(declaration), types, definitions)?;
for (_, kind) in signature.inputs.iter().chain(&signature.outputs) {
names::validate(kind, definitions, declaration.span)?;
}
Ok(signature)
}
pub(super) fn failures(
declaration: &ast::Function,
types: &Types,
definitions: &[Definition],
) -> Result<Vec<Failure>, Diagnostic> {
let reference = declaration.fails.as_ref().ok_or_else(|| {
Diagnostic::new(
"C005",
"Port operations require fails with a public error Choice",
declaration.span,
)
})?;
let Type::Named(index) = names::resolve(reference, types)? else {
return Err(Diagnostic::new(
"C005",
"fails requires a public App or standard error Choice",
reference.span,
));
};
let definition = &definitions[index];
types.public_type(&Type::Named(index), reference.span)?;
if !matches!(
package::role(&types.packages[definition.source]),
Some((_, "app"))
) && !package::standard(&types.packages[definition.source])
{
return Err(Diagnostic::new(
"C006",
"fails requires a public App or standard error Choice",
reference.span,
));
}
let Shape::Choice(variants) = &definition.shape else {
return Err(Diagnostic::new(
"C005",
"fails requires an error Choice",
reference.span,
));
};
let failures = variants
.iter()
.enumerate()
.filter(|(_, variant)| variant.error)
.map(|(variant, _)| Failure {
definition: index,
variant,
})
.collect::<Vec<_>>();
if failures.is_empty() {
return Err(Diagnostic::new(
"C005",
"fails Choice must contain an explicit error variant",
reference.span,
));
}
Ok(failures)
}
@@ -0,0 +1,295 @@
//! 外部实现由所属 Port 生成唯一静态合同,输入输出继续使用具体 wire 类型。
use super::{Ports, Types, schema};
use crate::compiler::model::{Definition, Shape, Type, adapter::External, package};
use crate::syntax::{ast, diagnostic::Diagnostic, source::Span};
use serde_json::{Value, json};
use std::collections::{BTreeMap, BTreeSet};
impl Ports {
pub fn external(
&mut self,
declarations: &[ast::External],
groups: &[Vec<ast::Function>],
names: &Types,
definitions: &[Definition],
types: &[ast::TypeDeclaration],
) -> Result<(), Diagnostic> {
for declaration in declarations {
let span = declaration.span;
let source = span.source;
let error = |message: &str| Diagnostic::new("C006", message, span);
if self.external.contains_key(&source)
|| groups.iter().any(|group| group[0].span.source == source)
|| types
.iter()
.any(|kind| kind.span.source == source && !kind.setting)
{
return Err(error(
"external Adapter contains only one external and an optional setting declaration",
));
}
if !matches!(
declaration.ecosystem.as_str(),
"exec" | "pip" | "npm" | "go" | "command"
) {
return Err(error("unknown external ecosystem"));
}
if declaration.entry.contains(['\\', ':', '\0'])
|| declaration
.entry
.split('/')
.any(|part| matches!(part, "" | "." | ".."))
{
return Err(error("external entry requires a portable relative path"));
}
let mut capabilities = BTreeSet::new();
for capability in &declaration.capabilities {
if !matches!(capability.as_str(), "network" | "file" | "process" | "gpu")
|| !capabilities.insert(capability.clone())
{
return Err(error("unknown or repeated external capability"));
}
}
let mut libs = Vec::new();
for request in &declaration.libs {
let request = if declaration.ecosystem == "exec"
|| request.starts_with(&format!("{}:", declaration.ecosystem))
{
request.clone()
} else {
format!("{}:{request}", declaration.ecosystem)
};
if declaration.ecosystem == "command"
|| !request.split_once(':').is_some_and(|(ecosystem, spec)| {
matches!(ecosystem, "pip" | "npm" | "go")
&& spec.rsplit_once('@').is_some_and(|(name, version)| {
!name.is_empty()
&& !name.contains(':')
&& !version.is_empty()
&& !version.contains('@')
})
})
|| request.chars().any(char::is_whitespace)
|| libs.contains(&request)
{
return Err(error(
"external Lib requires a unique exact name@version in its own ecosystem",
));
}
libs.push(request);
}
let (domain, _) = package::role(&names.packages[source]).expect("checked Adapter role");
let candidates: BTreeSet<_> = self
.operations
.keys()
.filter_map(|(owner, _)| {
package::role(&names.packages[*owner])
.is_some_and(|(owner, _)| owner == domain)
.then_some(*owner)
})
.collect();
let port = (candidates.len() == 1)
.then(|| *candidates.first().unwrap())
.ok_or_else(|| {
error("external Adapter requires one unambiguous Port in its domain")
})?;
if declaration.ecosystem == "command"
&& (self.settings.contains_key(&source) || !libs.is_empty())
{
return Err(error("external command cannot declare setting or lib"));
}
let topic = names.packages[port]
.strip_prefix(&format!("{domain}.port"))
.unwrap();
let identity = format!("{domain}{topic}");
let adapter = names.packages[source]
.strip_prefix(&format!("{domain}.adapter."))
.unwrap_or("default");
let mut inputs = BTreeMap::new();
let mut outputs = BTreeMap::new();
let mut errors = BTreeMap::new();
let mut fingerprint = String::from("dever-component-v1;");
if declaration.ecosystem != "exec" {
fingerprint.push_str(&format!("ecosystem={};", declaration.ecosystem));
}
for lib in &libs {
fingerprint.push_str(&format!("lib={lib};"));
}
let setting = self
.settings
.get(&source)
.map(|index| {
schema::build(
&Type::Named(*index),
definitions,
&names.packages,
schema::Policy::SettingInput,
)
})
.transpose()
.map_err(|message| error(&message))?;
if let Some(setting) = &setting {
fingerprint.push_str(&format!("setting={};", setting.fingerprint));
} else {
fingerprint.push_str("setting=none;");
}
let mut operations = self
.operations
.iter()
.filter(|((owner, _), _)| *owner == port)
.map(|(key, contract)| (contract.span.start, key.clone()))
.collect::<Vec<_>>();
operations.sort_by_key(|(start, _)| *start);
let policy = if declaration.ecosystem == "command" {
if operations.len() != 1 {
return Err(error("command Adapter requires exactly one Port operation"));
}
command(
self.operations.get(&operations[0].1).unwrap(),
definitions,
&names.packages,
)
.map_err(error)?;
schema::Policy::Command
} else {
schema::Policy::Output
};
for (_, key) in operations {
let operation = &key.1;
let contract = self.operations.get_mut(&key).unwrap();
fingerprint.push_str(&format!("operation={operation};"));
inputs.insert(
operation.clone(),
record(
&contract.signature.inputs,
definitions,
&names.packages,
span,
"input",
policy,
&mut fingerprint,
)?,
);
outputs.insert(
operation.clone(),
record(
&contract.signature.outputs,
definitions,
&names.packages,
span,
"output",
policy,
&mut fingerprint,
)?,
);
for failure in &contract.failures {
let definition = &definitions[failure.definition];
let Shape::Choice(variants) = &definition.shape else {
unreachable!()
};
let variant = &variants[failure.variant];
let identity = format!(
"{}.{}.{}",
package::namespace(&names.packages[definition.source]),
definition.name,
variant.name
);
fingerprint.push_str(&format!("{identity};"));
errors.insert(
identity,
record(
&variant.fields,
definitions,
&names.packages,
span,
"error-field",
policy,
&mut fingerprint,
)?,
);
}
contract
.implementations
.push((source, format!("port.{operation}")));
}
let setting = setting.map(|schema| schema.value);
let directory = names.packages[source]
.rsplit_once('.')
.unwrap()
.0
.replace('.', "/");
let entry = format!("module/{directory}/{}", declaration.entry);
let mut manifest = json!({"port":identity,"adapter":adapter,"ecosystem":declaration.ecosystem,"entry":entry,"libs":libs,"capabilities":capabilities,"operations":inputs.keys().collect::<Vec<_>>(),"inputs":inputs,"outputs":outputs,"errors":errors,"setting":setting});
manifest["schema"] = json!(schema::checksum(&fingerprint));
self.adapters.insert(source, port);
self.external.insert(
source,
External {
span,
ecosystem: declaration.ecosystem.clone(),
entry,
capabilities: capabilities.into_iter().collect(),
libs: libs.into_iter().collect(),
contract: serde_json::to_string(&manifest).unwrap(),
},
);
}
Ok(())
}
}
fn record(
fields: &[(String, Type)],
definitions: &[Definition],
packages: &[String],
span: Span,
field_kind: &str,
policy: schema::Policy,
fingerprint: &mut String,
) -> Result<Value, Diagnostic> {
let fields = fields
.iter()
.map(|(name, value)| {
let schema = schema::build(value, definitions, packages, policy)
.map_err(|message| Diagnostic::new("C005", message, span))?;
fingerprint.push_str(&format!("{field_kind}={name}:{};", schema.fingerprint));
Ok((name.clone(), schema.value))
})
.collect::<Result<BTreeMap<_, _>, Diagnostic>>()?;
Ok(json!({"type":"record", "fields":fields}))
}
fn command(
operation: &super::Operation,
definitions: &[Definition],
packages: &[String],
) -> Result<(), &'static str> {
let signature = &operation.signature;
let inputs = &signature.inputs;
if !matches!(inputs.len(), 1 | 2)
|| inputs[0] != ("args".into(), Type::List(Box::new(Type::Text)))
|| inputs.len() == 2 && inputs[1] != ("stdin".into(), Type::Optional(Box::new(Type::Bytes)))
{
return Err("command Port inputs must be args: List<Text> and optional stdin: Bytes?");
}
let [(name, Type::Named(output))] = signature.outputs.as_slice() else {
return Err("command Port output must be output: dever.process.Output");
};
let output = &definitions[*output];
if name != "output"
|| packages[output.source] != "dever.process"
|| output.name != "Output"
|| !matches!(&output.shape, Shape::Record(fields) if fields == &vec![("code".into(), Type::Int), ("stdout".into(), Type::Bytes), ("stderr".into(), Type::Bytes)])
{
return Err("command Port must return the standard process.Output contract");
}
let [failure] = operation.failures.as_slice() else {
return Err("command Port requires dever.process.Error");
};
let error = &definitions[failure.definition];
if packages[error.source] != "dever.process" || error.name != "Error" {
return Err("command Port requires dever.process.Error");
}
Ok(())
}
@@ -0,0 +1,265 @@
//! 将无函数体 Port 合同绑定到同领域的原生 Adapter。
//! 单实现直接调用;多实现共用合同并在入口读取配置后选择。
mod contract;
mod external;
mod schema;
mod settings;
mod types;
use super::super::{names, package::Types};
use crate::compiler::model::{Definition, Failure, Signature, package};
use crate::syntax::{ast, diagnostic::Diagnostic, source::Span};
use std::collections::{BTreeMap, HashMap, HashSet};
pub(in crate::compiler::check) struct Operation {
pub signature: Signature,
pub failures: Vec<Failure>,
pub span: Span,
pub implementations: Vec<(usize, String)>,
}
#[derive(Default)]
pub(crate) struct Ports {
pub(in crate::compiler::check) operations: BTreeMap<(usize, String), Operation>,
adapters: HashMap<usize, usize>,
pub(in crate::compiler::check) settings: BTreeMap<usize, usize>,
allowed: HashMap<usize, HashSet<usize>>,
external: BTreeMap<usize, crate::compiler::model::adapter::External>,
}
pub(crate) fn function_name(
function: &ast::Function,
packages: &[String],
) -> Result<(), Diagnostic> {
if matches!(
package::role(&packages[function.span.source]),
Some((_, "adapter" | "test"))
) && function.name.starts_with("port.")
{
let (prefix, method) = function.name.rsplit_once('.').unwrap();
for part in prefix.split('.') {
names::identifier(part, false, function.span)?;
}
names::function(method, function.span)
} else {
names::function(&function.name, function.span)
}
}
impl Ports {
pub fn new(
declarations: &[ast::Function],
names: &Types,
definitions: &[Definition],
types: &[ast::TypeDeclaration],
) -> Result<Self, Diagnostic> {
let mut ports = Self {
settings: settings::check(types, definitions)?,
..Self::default()
};
for declaration in declarations {
let signature = contract::signature(declaration, names, definitions)?;
let failures = contract::failures(declaration, names, definitions)?;
let allowed = ports.allowed.entry(signature.source).or_default();
for (_, kind) in signature.inputs.iter().chain(&signature.outputs) {
types::collect(kind, definitions, allowed);
}
if let Some(failure) = failures.first() {
types::collect(
&crate::compiler::model::Type::Named(failure.definition),
definitions,
allowed,
);
}
let key = (signature.source, signature.name.clone());
if ports
.operations
.insert(
key,
Operation {
signature,
failures,
span: declaration.span,
implementations: Vec::new(),
},
)
.is_some()
{
return Err(Diagnostic::new(
"C002",
"duplicate Port operation",
declaration.span,
));
}
}
Ok(ports)
}
pub fn bind<'group>(
&mut self,
groups: impl Iterator<Item = (&'group [ast::Function], &'group Signature)>,
names: &Types,
) -> Result<(), Diagnostic> {
for (group, signature) in groups {
let first = &group[0];
if !signature.name.contains('.') {
continue;
}
let (source, name) =
names
.target(&signature.name, signature.source)
.ok_or_else(|| {
Diagnostic::new("C004", "unknown Port implementation target", first.span)
})?;
let operation = self
.operations
.get_mut(&(source, name.to_owned()))
.ok_or_else(|| Diagnostic::new("C004", "unknown Port operation", first.span))?;
if signature.static_parameters != 0 {
return Err(Diagnostic::new(
"C005",
"Port implementations cannot add handler parameters",
first.span,
));
}
if signature.inputs != operation.signature.inputs
|| signature.outputs != operation.signature.outputs
{
return Err(Diagnostic::new(
"C005",
"Adapter input and output names and types must match the Port contract",
first.span,
));
}
if names.test_source != Some(signature.source)
&& self
.adapters
.insert(signature.source, source)
.is_some_and(|previous| previous != source)
{
return Err(Diagnostic::new(
"C006",
"one Adapter file must implement exactly one Port",
first.span,
));
}
operation
.implementations
.push((signature.source, signature.name.clone()));
}
for ((source, _), operation) in &self.operations {
if operation.implementations.is_empty() {
return Err(Diagnostic::new(
"C004",
"Port operation has no Adapter implementation",
operation.span,
));
}
let expected = self
.operations
.iter()
.filter(|((port, _), _)| port == source)
.flat_map(|(_, operation)| {
operation.implementations.iter().map(|(source, _)| *source)
})
.collect::<HashSet<_>>();
let actual = operation
.implementations
.iter()
.map(|(source, _)| *source)
.collect::<HashSet<_>>();
if actual != expected {
return Err(Diagnostic::new(
"C006",
"one Adapter file must implement the complete Port",
operation.span,
));
}
}
for (source, identity) in names.packages.iter().enumerate() {
if matches!(package::role(identity), Some((_, "adapter")))
&& !self.adapters.contains_key(&source)
{
return Err(Diagnostic::new(
"C006",
"Adapter file must implement a Port",
Span {
source,
start: 0,
end: 0,
},
));
}
}
Ok(())
}
/// 调用者看到声明的错误全集;实现缩小失败集合不能缩小公开合同。
pub fn finish(&self, signature: &mut Signature, span: Span) -> Result<(), Diagnostic> {
let Some(operation) = self.operations.values().find(|operation| {
operation
.implementations
.iter()
.any(|(source, name)| *source == signature.source && name == &signature.name)
}) else {
return Ok(());
};
if signature
.failures
.iter()
.any(|failure| !operation.failures.contains(failure))
{
return Err(Diagnostic::new(
"C012",
"Adapter failure is outside the Port fails contract",
span,
));
}
signature.failures.clone_from(&operation.failures);
signature.effects.unmediated_io = false;
Ok(())
}
pub fn layout(&self, names: &Types) -> crate::compiler::model::adapter::Adapters {
use crate::compiler::model::adapter::{Adapters, Implementation, Port};
let mut ports = BTreeMap::<usize, Vec<usize>>::new();
for ((port, _), operation) in &self.operations {
for (source, _) in &operation.implementations {
let implementations = ports.entry(*port).or_default();
if !implementations.contains(source) {
implementations.push(*source);
}
}
}
Adapters {
root: None,
ports: ports
.into_iter()
.map(|(source, mut implementations)| {
implementations.sort_unstable();
let identity = &names.packages[source];
let (domain, _) = package::role(identity).unwrap();
let topic = identity.strip_prefix(&format!("{domain}.port")).unwrap();
Port {
source,
name: format!("{domain}{topic}"),
implementations: implementations
.into_iter()
.map(|source| Implementation {
source,
name: names.packages[source]
.strip_prefix(&format!("{domain}.adapter."))
.unwrap_or("default")
.to_owned(),
setting: self.settings.get(&source).copied(),
external: self.external.get(&source).cloned(),
})
.collect(),
}
})
.collect(),
functions: BTreeMap::new(),
}
}
}
@@ -0,0 +1,145 @@
//! 保留 SDK v1 的 wire 节点顺序和指纹,避免签名不变却使旧锁/Worker 无法握手。
use crate::compiler::model::{Definition, Shape, Type, package};
use serde_json::{Value, json};
use std::collections::HashMap;
pub(super) struct Schema {
pub value: Value,
pub fingerprint: String,
}
#[derive(Clone, Copy, Debug)]
pub(super) enum Policy {
Output,
SettingInput,
Command,
}
pub(super) fn build(
kind: &Type,
definitions: &[Definition],
packages: &[String],
policy: Policy,
) -> Result<Schema, String> {
let mut nodes = Vec::new();
let value = node(
kind,
definitions,
packages,
policy,
&mut nodes,
&mut HashMap::new(),
0,
)?
.1;
Ok(Schema {
value,
fingerprint: format!("wire-v1:{policy:?}:{}", nodes.join("")),
})
}
fn node(
kind: &Type,
definitions: &[Definition],
packages: &[String],
policy: Policy,
nodes: &mut Vec<String>,
known: &mut HashMap<Type, (usize, Value)>,
depth: usize,
) -> Result<(usize, Value), String> {
if depth > 64 {
return Err("Worker wire schema exceeds depth limit".into());
}
if let Some(value) = known.get(kind) {
return Ok(value.clone());
}
let (fingerprint, value) = match kind {
Type::Optional(inner) | Type::List(inner) => {
let (index, value) = node(
inner,
definitions,
packages,
policy,
nodes,
known,
depth + 1,
)?;
if matches!(kind, Type::Optional(_)) {
(
format!("Nullable({index});"),
json!({"type":"nullable","value":value}),
)
} else {
(
format!("List({index});"),
json!({"type":"list","value":value}),
)
}
}
Type::Named(index) => {
let definition = &definitions[*index];
let Shape::Record(fields) = &definition.shape else {
return Err("Worker wire Choice is not supported".into());
};
if !definition.private_fields.is_empty()
|| definition.ranges.iter().any(Option::is_some)
{
return Err("Worker wire fields must be public and unconstrained".into());
}
let mut indices = Vec::new();
let mut values = serde_json::Map::new();
for (name, kind) in fields {
let (index, value) =
node(kind, definitions, packages, policy, nodes, known, depth + 1)?;
indices.push((name.clone(), index));
values.insert(name.clone(), value);
}
let name = format!(
"{}.{}",
package::namespace(&packages[definition.source]),
definition.name
);
(
format!("Record({name:?},{indices:?});"),
json!({"type":"record","name":name,"fields":values}),
)
}
Type::ModelId(index) => {
let definition = &definitions[*index];
let name = format!(
"{}.{}.Id",
package::namespace(&packages[definition.source]),
definition.name
);
(
format!("ModelId({name:?});"),
json!({"type":"model_id","name":name}),
)
}
_ => {
let name = match kind {
Type::Bool => "bool", Type::Int => "int64", Type::Float => "float64",
Type::Decimal => "decimal", Type::Text => "text", Type::Id => "id",
Type::Uuid => "uuid", Type::Json => "json", Type::DateTime => "datetime",
Type::Date => "date", Type::Time => "time", Type::Duration => "duration",
Type::Secret if matches!(policy, Policy::SettingInput) => "secret",
Type::Bytes if matches!(policy, Policy::Command) => "bytes",
_ => return Err("Worker wire values require portable public scalar/record/List types; Secret belongs only in setting".into()),
};
(format!("{kind:?};"), json!({"type":name}))
}
};
let index = nodes.len();
nodes.push(fingerprint);
known.insert(kind.clone(), (index, value.clone()));
Ok((index, value))
}
pub(super) fn checksum(identity: &str) -> String {
let mut hash = 0xcbf29ce484222325u64;
for byte in identity.bytes() {
hash = (hash ^ u64::from(byte)).wrapping_mul(0x100000001b3);
}
format!("{hash:016x}")
}
@@ -0,0 +1,52 @@
//! setting 是 Adapter 私有的输入记录;沿用 wire 值形状,额外允许 Secret。
use crate::compiler::model::{Definition, Shape, Type};
use crate::syntax::{ast, diagnostic::Diagnostic, source::Span};
use std::collections::BTreeMap;
pub(super) fn check(
declarations: &[ast::TypeDeclaration],
definitions: &[Definition],
) -> Result<BTreeMap<usize, usize>, Diagnostic> {
let mut settings = BTreeMap::new();
for (index, declaration) in declarations
.iter()
.enumerate()
.filter(|(_, value)| value.setting)
{
validate(&Type::Named(index), definitions, declaration.span)?;
settings.insert(declaration.span.source, index);
}
Ok(settings)
}
fn validate(kind: &Type, definitions: &[Definition], span: Span) -> Result<(), Diagnostic> {
match kind {
Type::Secret => Ok(()),
Type::List(inner) | Type::Optional(inner) => validate(inner, definitions, span),
Type::Named(index) => {
let definition = &definitions[*index];
let Shape::Record(fields) = &definition.shape else {
return Err(Diagnostic::new(
"C005",
"setting requires wire records, not Choice",
span,
));
};
if !definition.private_fields.is_empty()
|| definition.ranges.iter().any(Option::is_some)
{
return Err(Diagnostic::new(
"C005",
"setting records cannot contain private or constrained fields",
span,
));
}
for (_, kind) in fields {
validate(kind, definitions, span)?;
}
Ok(())
}
_ => super::super::wire_type(kind, definitions, span),
}
}
@@ -0,0 +1,61 @@
//! Port/Adapter 只获得合同实际使用的 App 类型,以及所实现 Port 的私有 DTO。
//! 检查覆盖类型声明、函数签名和函数体引用,不能用完整路径绕过职责边界。
use super::{Ports, Types};
use crate::compiler::model::{Definition, Shape, Type, package};
use crate::syntax::diagnostic::Diagnostic;
use std::collections::HashSet;
pub(super) fn collect(kind: &Type, definitions: &[Definition], found: &mut HashSet<usize>) {
match kind {
Type::Named(index) if found.insert(*index) => match &definitions[*index].shape {
Shape::Record(fields) => {
for (_, kind) in fields {
collect(kind, definitions, found);
}
}
Shape::Choice(variants) => {
for variant in variants {
for (_, kind) in &variant.fields {
collect(kind, definitions, found);
}
}
}
},
Type::List(inner)
| Type::Optional(inner)
| Type::Stream(inner)
| Type::AsyncStream(inner)
| Type::Channel(inner) => collect(inner, definitions, found),
Type::Map(key, value) | Type::MapEntry(key, value) => {
collect(key, definitions, found);
collect(value, definitions, found);
}
_ => {}
}
}
impl Ports {
pub fn check_types(&self, types: &Types, definitions: &[Definition]) -> Result<(), Diagnostic> {
for &(source, index, span) in types.role_types.borrow().iter() {
let definition = &definitions[index];
let port = self.adapters.get(&source).copied().unwrap_or(source);
let allowed = match package::role(&types.packages[definition.source]) {
Some((_, "app")) => self
.allowed
.get(&port)
.is_some_and(|allowed| allowed.contains(&index)),
Some((_, "port")) => definition.source == port,
_ => true,
};
if !allowed {
return Err(Diagnostic::new(
"C006",
"Port/Adapter type is outside its Port contract",
span,
));
}
}
Ok(())
}
}
@@ -0,0 +1,202 @@
//! 字段表达式复用普通函数的类型、失败和效果检查;不会在运行时解释表达式。
use crate::{
compiler::model::*,
syntax::{ast, diagnostic::Diagnostic},
};
pub(in crate::compiler::check) fn prepare(
declarations: &[ast::TypeDeclaration],
groups: &mut Vec<Vec<ast::Function>>,
packages: &[String],
) -> Result<(), Diagnostic> {
for declaration in declarations {
if !matches!(
package::role(&packages[declaration.span.source]),
Some((_, "model"))
) {
continue;
}
let ast::TypeBody::Record(fields) = &declaration.body else {
continue;
};
for field in fields {
if field.rest.owner.is_some()
&& (field.rest.create.is_some()
|| field.rest.replace.is_some()
|| field.rest.search.is_some())
{
return Err(Diagnostic::new(
"C014",
"owner cannot combine with REST field bindings",
field.span,
));
}
for (action, value) in [
("create", &field.rest.create),
("replace", &field.rest.replace),
("search", &field.rest.search),
] {
let Some(value) = value else {
continue;
};
let input = uses_input(value);
if input && field.private {
return Err(Diagnostic::new(
"C014",
"private REST fields cannot read client input",
field.span,
));
}
if action == "search" && (!input || field.private || field.kind.nullable) {
return Err(Diagnostic::new(
"C014",
"REST search requires public non-null client input",
field.span,
));
}
let mut kind = field.kind.clone();
kind.storage.clear();
let mut output = field.clone();
output.kind = kind.clone();
output.name = "value".into();
output.private = false;
output.unique = false;
output.index = false;
output.rest = Default::default();
output.persistence = Default::default();
groups.push(vec![ast::Function {
contract: false,
name: name(action, &field.name),
transaction: false,
inputs: if input {
vec![ast::Parameter {
name: "input".into(),
pattern: ast::Pattern::Typed(kind, vec![]),
span: field.span,
}]
} else {
vec![]
},
outputs: vec![output],
pure: false,
recovery: None,
fails: None,
public: false,
body: vec![ast::Statement::Assign {
annotation: None,
target: ast::Expression {
kind: ast::ExpressionKind::Name("value".into()),
span: value.span,
depth: 1,
},
value: value.clone(),
}],
span: value.span,
}]);
}
}
}
Ok(())
}
// 源码词法不能构造这个名字,字段转换不会意外成为可调用的 Model 方法。
pub(super) fn name(action: &str, field: &str) -> String {
format!("#rest_{action}_{field}")
}
fn uses_input(value: &ast::Expression) -> bool {
use ast::ExpressionKind::*;
match &value.kind {
Name(name) => name == "input",
Call(target, values) => uses_input(target) || values.iter().any(uses_input),
Field(base, _) | Unary(_, base) | Channel(_, base) => uses_input(base),
Binary(_, left, right) => uses_input(left) || uses_input(right),
List(values) => values.iter().any(uses_input),
Map(values) => values
.iter()
.any(|(key, value)| uses_input(key) || uses_input(value)),
Record(_, fields) => fields.iter().any(|(_, value)| uses_input(value)),
Literal(_) => false,
}
}
/// 沿静态调用图检查所有叶子,只读请求能力不能被包装函数变成响应写入。
pub(super) fn validate(
root: usize,
functions: &[Option<Function>],
calls: &[Vec<usize>],
packages: &[String],
) -> Result<(), Diagnostic> {
let mut pending = vec![root];
let mut seen = std::collections::HashSet::new();
while let Some(index) = pending.pop() {
if !seen.insert(index) {
continue;
}
let function = functions[index].as_ref().unwrap();
let effects = function.signature.effects;
let span = function.clauses[0].span;
if (Effects {
request: false,
identity: false,
authentication: false,
..effects
}) != Effects::default()
{
return Err(Diagnostic::new(
"C014",
"REST field bindings permit only values and read-only request context",
span,
));
}
if matches!(
package::role(&packages[function.signature.source]),
Some((_, "domain"))
) && !function.signature.pure
{
return Err(Diagnostic::new(
"C014",
"REST field Domain functions must be pure",
span,
));
}
for clause in &function.clauses {
for statement in &clause.body {
let (Statement::Assign { value, .. }
| Statement::Action(value)
| Statement::Fail(value)) = statement;
readonly(value)?;
}
}
pending.extend(&calls[index]);
}
Ok(())
}
fn readonly(value: &Expression) -> Result<(), Diagnostic> {
if let Node::Auth(call, _) = &value.kind {
use auth::Operation::*;
if !matches!(
call.operation,
Id | Session
| UserId
| TenantId
| OwnsUser
| Site
| RequestId
| Method
| Path
| ClientAddress
| Header
| Cookie
) {
return Err(Diagnostic::new(
"C014",
"REST field binding cannot mutate response or expose secrets",
value.span,
));
}
}
value.children(readonly)
}
@@ -0,0 +1,248 @@
//! REST 从同领域唯一 Model 得到固定字段合同,私有字段永不成为输入或输出。
use super::*;
use crate::compiler::model::{Function, persistence::Model, rest};
mod bindings;
pub(in crate::compiler::check) use bindings::prepare;
pub(in crate::compiler::check) struct Context<'program> {
pub models: &'program [Model],
pub declarations: &'program [ast::TypeDeclaration],
pub definitions: &'program [model::Definition],
pub packages: &'program [String],
pub http: &'program [model::HttpEntry],
pub verifiers: &'program [model::auth::Verifier],
pub signatures: &'program [model::Signature],
pub functions: &'program [Option<Function>],
pub calls: &'program [Vec<usize>],
}
pub(in crate::compiler::check) fn check(
entries: &[ast::Rest],
context: Context<'_>,
) -> Result<Vec<rest::Entry>, Diagnostic> {
let Context {
models,
declarations,
definitions,
packages,
http,
verifiers,
signatures,
functions,
calls,
} = context;
for (index, signature) in signatures.iter().enumerate() {
if matches!(
package::role(&packages[signature.source]),
Some((_, "model"))
) {
bindings::validate(index, functions, calls, packages)?;
}
}
let mut result = Vec::new();
let mut seen = HashSet::new();
for entry in entries {
let span = entry.span;
if verifiers.is_empty() {
return Err(Diagnostic::new(
"C014",
"REST requires a compiled App verify function",
span,
));
}
let identity = &packages[span.source];
let (namespace, _) = package::role(identity).unwrap();
let candidates = models
.iter()
.enumerate()
.filter(|(_, model)| {
package::role(&packages[model.source]) == Some((namespace, "model"))
&& entry.model.as_ref().is_none_or(|selector| {
packages[model.source] == format!("{namespace}.{selector}")
})
})
.collect::<Vec<_>>();
if candidates.len() != 1 {
return Err(Diagnostic::new(
"C014",
"REST requires one same-domain Model",
span,
));
}
let (index, model) = candidates[0];
let definition = &definitions[model.definition];
let read = (0..model.fields.len())
.filter(|position| !definition.private_fields.contains(position))
.collect::<Vec<_>>();
for position in &read {
if matches!(model.fields[*position].1, Type::Bytes) {
return Err(Diagnostic::new(
"C005",
"REST field cannot be serialized by the API codec",
span,
));
}
if model.columns[*position].choice.is_none() {
wire_type(&model.fields[*position].1, definitions, span)?;
}
}
let ast::TypeBody::Record(fields) = &declarations[model.definition].body else {
unreachable!()
};
let mut owner = None;
let mut search = None;
let mut create = Vec::new();
let mut replace = Vec::new();
for (position, field) in fields.iter().enumerate().skip(2) {
if let Some(expression) = &field.rest.owner {
let ast::ExpressionKind::Call(target, args) = &expression.kind else {
return Err(Diagnostic::new(
"C014",
"owner requires user_id() or tenant_id()",
field.span,
));
};
let user = match target.path().as_deref() {
Some("dever.auth.userId") => true,
Some("dever.auth.tenantId") => false,
_ => {
return Err(Diagnostic::new(
"C014",
"owner requires user_id() or tenant_id()",
field.span,
));
}
};
if owner.is_some()
|| !args.is_empty()
|| !field.persistence.is_empty()
|| !matches!(model.fields[position].1, Type::Int | Type::ModelId(_))
{
return Err(Diagnostic::new(
"C014",
"REST requires one non-null integer owner",
field.span,
));
}
owner = Some(rest::Owner { position, user });
continue;
}
for (action, value, target) in [
("create", &field.rest.create, &mut create),
("replace", &field.rest.replace, &mut replace),
] {
if model.columns[position].generated {
if value.is_some() {
return Err(Diagnostic::new(
"C014",
"generated fields cannot have REST write bindings",
field.span,
));
}
continue;
}
let function = value.as_ref().map(|_| {
signatures
.iter()
.position(|signature| {
signature.source == model.source
&& signature.name == bindings::name(action, &field.name)
})
.expect("prepared REST binding")
});
if function.is_none() && field.private {
if action == "create"
&& !matches!(model.fields[position].1, Type::Optional(_))
&& model.columns[position].default.is_none()
{
return Err(Diagnostic::new(
"C014",
"REST field is private and required without a source",
field.span,
));
}
continue;
}
target.push(rest::Binding {
position,
input: function.is_none_or(|index| !signatures[index].inputs.is_empty()),
function,
});
}
if field.rest.search.is_some() {
if matches!(field.name.as_str(), "page" | "size") {
return Err(Diagnostic::new(
"C014",
"REST search field cannot use reserved pagination names",
field.span,
));
}
if search.is_some()
|| !(field.index || field.unique)
|| matches!(
model.fields[position].1,
Type::Json | Type::Bytes | Type::Optional(_)
)
{
return Err(Diagnostic::new(
"C014",
"REST permits one indexed scalar search field",
field.span,
));
}
let function = signatures
.iter()
.position(|signature| {
signature.source == model.source
&& signature.name == bindings::name("search", &field.name)
})
.expect("prepared REST binding");
search = Some(rest::Binding {
position,
input: true,
function: Some(function),
});
}
}
let topic = identity
.strip_prefix(&format!("{namespace}.api."))
.unwrap_or("")
.replace('.', "/");
let path = format!(
"/{}{}{}",
namespace.replace('.', "/"),
if topic.is_empty() { "" } else { "/" },
topic
);
if seen.iter().any(|previous: &String| {
previous == &path
|| previous.starts_with(&format!("{path}/"))
|| path.starts_with(&format!("{previous}/"))
}) || http
.iter()
.any(|entry| entry.path == path || entry.path.starts_with(&format!("{path}/")))
{
return Err(Diagnostic::new(
"C002",
"duplicate HTTP route with REST",
span,
));
}
seen.insert(path.clone());
let (component, domain) = namespace.split_once('.').unwrap();
result.push(rest::Entry {
model: index,
path,
topic,
component: component.into(),
domain: domain.into(),
read,
create,
replace,
search,
owner,
});
}
Ok(result)
}
@@ -0,0 +1,69 @@
//! 异步流复用已有元素类型;构造不拉取,消费时才推导挂起效果。
use super::{Context, names};
use crate::compiler::model::*;
use crate::syntax::{ast, diagnostic::Diagnostic};
impl Context<'_> {
pub(super) fn stream(
&mut self,
arguments: &[ast::Expression],
expected: Option<&Type>,
source: &ast::Expression,
) -> Result<Expression, Diagnostic> {
let [value] = arguments else {
return Err(self.error(source, "stream requires one List, Bytes or Channel"));
};
let context = match expected {
Some(Type::AsyncStream(inner))
if matches!(value.kind, ast::ExpressionKind::List(_)) =>
{
Some(Type::List(inner.clone()))
}
_ => None,
};
let value = self.expression(value, context.as_ref())?;
let item = match &value.value_type {
Type::List(inner) | Type::Channel(inner) => inner.clone(),
Type::Bytes => Box::new(Type::Int),
_ => return Err(self.error(source, "stream requires a List, Bytes or Channel")),
};
let value_type = Type::AsyncStream(item);
names::validate(&value_type, self.definitions, source.span)?;
self.coerce(
Expression {
value_type,
kind: Node::Stream(Box::new(value)),
span: source.span,
},
expected,
)
}
pub(super) fn ticks(
&mut self,
arguments: &[ast::Expression],
expected: Option<&Type>,
source: &ast::Expression,
) -> Result<Expression, Diagnostic> {
if self.definition_names.packages[self.source] != "dever.task" {
return Err(Diagnostic::new(
"C006",
"tick intrinsic belongs to the official dever.task package",
source.span,
));
}
let [millis] = arguments else {
return Err(self.error(source, "ticks requires one duration"));
};
let millis = self.expression(millis, Some(&Type::Int))?;
self.coerce(
Expression {
value_type: Type::AsyncStream(Box::new(Type::Int)),
kind: Node::Ticks(Box::new(millis)),
span: source.span,
},
expected,
)
}
}
@@ -0,0 +1,79 @@
//! 授权函数复用标准名义记录和错误,写效果沿普通调用图传播到 API 声明。
use super::super::Context;
use crate::{
compiler::model::*,
syntax::{ast, diagnostic::Diagnostic},
};
impl Context<'_> {
pub(in crate::compiler::check) fn authorization(
&mut self,
operation: authorization::Operation,
arguments: &[ast::Expression],
expected: Option<&Type>,
source: &ast::Expression,
) -> Result<Expression, Diagnostic> {
use authorization::Operation::*;
if self.definition_names.packages[self.source] != "dever.auth" {
return Err(Diagnostic::new(
"C006",
"authorization intrinsics belong to dever.auth",
source.span,
));
}
let inputs = match operation {
Permissions | CurrentPermissions | Roles => vec![],
Role | Disable | Delete => vec![Type::Text],
UserRoles | HasProtectedRoles => vec![Type::Int],
RegisterCapability | RemoveCapability => vec![Type::Uuid],
Save => vec![
Type::Text,
Type::Text,
Type::Bool,
Type::List(Box::new(Type::Text)),
],
Grant | Revoke => vec![Type::Int, Type::Text],
};
if inputs.len() != arguments.len() {
return Err(self.error(source, "authorization argument count differs"));
}
let values = arguments
.iter()
.zip(&inputs)
.map(|(value, kind)| self.expression(value, Some(kind)))
.collect::<Result<Vec<_>, _>>()?;
let definition = |name: &str| {
self.definition_names
.resolve(name, self.source, source.span)?
.ok_or_else(|| self.error(source, "authorization standard type is missing"))
};
let output = match operation {
HasProtectedRoles => Type::Bool,
RegisterCapability => Type::Text,
Save | Grant | Revoke | Disable | Delete | RemoveCapability => Type::Unit,
Permissions | CurrentPermissions | Roles | Role | UserRoles => {
let record =
definition(if matches!(operation, Permissions | CurrentPermissions) {
"dever.auth.Permission"
} else {
"dever.auth.Role"
})?;
Type::List(Box::new(Type::Named(record)))
}
};
let call = authorization::Call {
operation,
api_error: definition("dever.api.Error")?,
database_error: definition("dever.database.Error")?,
};
self.coerce(
Expression {
kind: Node::Authorization(call, values),
value_type: output,
span: source.span,
},
expected,
)
}
}
+171
View File
@@ -0,0 +1,171 @@
//! Claims 到身份的唯一桥接是普通 App 函数;配置不能指定未编译或越权的入口。
use super::{handler::Expansion, package::Types};
use crate::{
compiler::model::*,
syntax::{diagnostic::Diagnostic, source::Span},
};
use std::collections::HashSet;
mod authorization;
mod operations;
pub(super) fn verifiers(
signatures: &[Signature],
functions: &[Option<Function>],
definitions: &[Definition],
models: &[persistence::Model],
names: &Types,
expansion: &Expansion,
) -> Result<Vec<auth::Verifier>, Diagnostic> {
let Some(claims) = definitions.iter().position(|definition| {
definition.name == "Claims" && names.packages[definition.source] == "dever.auth"
}) else {
return Ok(Vec::new());
};
let mut entries = Vec::new();
for (function, signature) in signatures.iter().enumerate() {
if signature.inputs.len() != 1
|| signature.inputs[0].1 != Type::Named(claims)
|| !matches!(
package::role(&names.packages[signature.source]),
Some((_, "app"))
)
{
continue;
}
let span = expansion.instances[function].span;
if signature.transaction
|| signature.effects.database_write
|| signature.effects.transaction_unsafe()
|| signature.effects.output
|| signature.effects.request
|| signature.effects.handler
|| signature.static_parameters != 0
{
return Err(Diagnostic::new(
"C014",
"Provider verify permits only value work, clock reads and global Model reads",
span,
));
}
let [(_, Type::Named(identity))] = signature.outputs.as_slice() else {
return Err(Diagnostic::new(
"C005",
"Provider verify must return its own identity record",
span,
));
};
let definition = &definitions[*identity];
let Shape::Record(fields) = &definition.shape else {
return Err(Diagnostic::new(
"C005",
"Provider identity must be a record",
span,
));
};
if names.owner(definition.source) != names.owner(signature.source)
|| !definition.private_fields.is_empty()
|| definition.ranges.iter().any(Option::is_some)
|| fields.len() != 3
{
return Err(Diagnostic::new(
"C005",
"Provider identity must expose exactly id, user_id and tenant_id in its own App",
span,
));
}
let id = fields
.iter()
.position(|(name, kind)| name == "id" && *kind == Type::Text)
.ok_or_else(|| Diagnostic::new("C005", "Provider identity requires id: Text", span))?;
let user = model_id(fields, "user_id", span)?;
let tenant = model_id(fields, "tenant_id", span)?;
global_reads(function, functions, models, &expansion.calls, span)?;
entries.push(auth::Verifier {
function,
name: format!(
"{}.{}",
package::namespace(&names.packages[signature.source]),
signature.name
),
claims,
id,
user,
tenant,
});
}
Ok(entries)
}
fn model_id(
fields: &[(String, Type)],
name: &str,
span: Span,
) -> Result<(usize, usize), Diagnostic> {
fields
.iter()
.enumerate()
.find_map(|(position, (field, kind))| {
if field == name
&& let Type::Optional(inner) = kind
&& let Type::ModelId(model) = inner.as_ref()
{
Some((position, *model))
} else {
None
}
})
.ok_or_else(|| {
Diagnostic::new(
"C005",
"Provider user_id and tenant_id require nullable nominal Model IDs",
span,
)
})
}
fn global_reads(
root: usize,
functions: &[Option<Function>],
models: &[persistence::Model],
calls: &[Vec<usize>],
span: Span,
) -> Result<(), Diagnostic> {
fn value(
expression: &Expression,
models: &[persistence::Model],
span: Span,
) -> Result<(), Diagnostic> {
if let Node::Database(query, _) = &expression.kind
&& query.models().any(|model| !models[model].global)
{
return Err(Diagnostic::new(
"C014",
"Provider verify cannot read a tenant Model",
span,
));
}
expression.children(|child| value(child, models, span))
}
let mut pending = vec![root];
let mut seen = HashSet::new();
while let Some(index) = pending.pop() {
if !seen.insert(index) {
continue;
}
let function = functions[index].as_ref().expect("checked function order");
for clause in &function.clauses {
for statement in &clause.body {
let (Statement::Assign {
value: expression, ..
}
| Statement::Action(expression)
| Statement::Fail(expression)) = statement;
value(expression, models, span)?;
}
}
pending.extend(&calls[index]);
}
Ok(())
}
+119
View File
@@ -0,0 +1,119 @@
//! 身份操作保留名义 ID;外部调用者不能直接使用标准库的底层接收点。
use super::super::Context;
use crate::{
compiler::model::*,
syntax::{ast, diagnostic::Diagnostic},
};
impl Context<'_> {
pub(in crate::compiler::check) fn auth(
&mut self,
operation: auth::Operation,
arguments: &[ast::Expression],
expected: Option<&Type>,
source: &ast::Expression,
) -> Result<Expression, Diagnostic> {
use auth::Operation::*;
let package = &self.definition_names.packages[self.source];
let named_id = matches!(&source.kind, ast::ExpressionKind::Call(target, _) if matches!(target.path().as_deref(), Some("dever.auth.userId" | "dever.auth.tenantId")))
&& matches!(expected, Some(Type::Optional(inner)) if matches!(inner.as_ref(), Type::ModelId(_)));
if operation != OwnsUser && !named_id && package != operation.package() {
return Err(Diagnostic::new(
"C006",
"request intrinsics belong to their official package",
source.span,
));
}
let mut model = None;
let (inputs, output) = match operation {
Id | Session | Site => (vec![], Type::Text),
UserId | TenantId => {
let inner = match expected {
Some(Type::Optional(inner)) if matches!(inner.as_ref(), Type::ModelId(_)) => {
let Type::ModelId(index) = inner.as_ref() else {
unreachable!()
};
model = Some(*index);
*inner.clone()
}
_ => Type::Int,
};
(vec![], Type::Optional(Box::new(inner)))
}
OwnsUser => {
let [argument] = arguments else {
return Err(self.error(source, "owns_user requires one nominal Model ID"));
};
let value = self.expression(argument, None)?;
let Type::ModelId(index) = value.value_type else {
return Err(self.error(source, "owns_user requires one nominal Model ID"));
};
model = Some(index);
(vec![Type::ModelId(index)], Type::Bool)
}
Issue | IssueCookie => (
vec![Type::Text, Type::Text, Type::Optional(Box::new(Type::Text))],
if operation == Issue {
Type::Secret
} else {
Type::Unit
},
),
ClearCookie => (vec![], Type::Unit),
RequestId | Method | Path | ClientAddress => (vec![], Type::Text),
Header | Cookie => (vec![Type::Text], Type::Optional(Box::new(Type::Text))),
SecretCookie => (vec![Type::Text], Type::Optional(Box::new(Type::Secret))),
SetHeader => (vec![Type::Text, Type::Text], Type::Unit),
SetCookie | SetSecretCookie => {
let definition = self
.definition_names
.resolve("dever.api.CookieOptions", self.source, source.span)?
.ok_or_else(|| self.error(source, "CookieOptions is missing"))?;
(
vec![
Type::Text,
if operation == SetCookie {
Type::Text
} else {
Type::Secret
},
Type::Named(definition),
],
Type::Unit,
)
}
};
if inputs.len() != arguments.len() {
return Err(self.error(source, "request operation argument count differs"));
}
let values = arguments
.iter()
.zip(&inputs)
.map(|(value, kind)| self.expression(value, Some(kind)))
.collect::<Result<Vec<_>, _>>()?;
let error = self
.definitions
.iter()
.position(|definition| {
definition.name == "Error"
&& self.definition_names.packages[definition.source] == "dever.api"
})
.ok_or_else(|| self.error(source, "request operations require dever.api.Error"))?;
self.coerce(
Expression {
kind: Node::Auth(
auth::Call {
operation,
error,
model,
},
values,
),
value_type: output,
span: source.span,
},
expected,
)
}
}
+83
View File
@@ -0,0 +1,83 @@
//! 校验官方 bytes 底层操作的权限、参数及名义返回类型。
//! 转换失败必须进入库定义的业务错误,不能降为无类型字符串或数值 Fault。
use super::Context;
use crate::compiler::model::*;
use crate::syntax::{ast, diagnostic::Diagnostic};
impl Context<'_> {
pub(super) fn bytes(
&mut self,
operation: BytesOperation,
arguments: &[ast::Expression],
expected: Option<&Type>,
source: &ast::Expression,
) -> Result<Expression, Diagnostic> {
if self.definition_names.packages[self.source] != "dever.bytes" {
return Err(Diagnostic::new(
"C006",
"byte intrinsics belong to the official dever.bytes package",
source.span,
));
}
let inputs = match operation {
BytesOperation::FromText => vec![Type::Text],
BytesOperation::FromInts => vec![Type::List(Box::new(Type::Int))],
BytesOperation::ToText | BytesOperation::Length => vec![Type::Bytes],
BytesOperation::At => vec![Type::Bytes, Type::Int],
BytesOperation::Slice => vec![Type::Bytes, Type::Int, Type::Int],
BytesOperation::Concat => vec![Type::Bytes, Type::Bytes],
};
if arguments.len() != inputs.len() {
return Err(self.error(source, "byte operation argument count differs"));
}
let values = arguments
.iter()
.zip(inputs)
.map(|(argument, kind)| self.expression(argument, Some(&kind)))
.collect::<Result<Vec<_>, _>>()?;
let value_type = match operation {
BytesOperation::FromText | BytesOperation::Concat => Type::Bytes,
BytesOperation::Length => Type::Int,
BytesOperation::At => Type::Int.optional(),
BytesOperation::ToText => {
self.byte_result("DecodeResult", "Decoded", "text", Type::Text, source)?
}
BytesOperation::FromInts | BytesOperation::Slice => {
self.byte_result("BufferResult", "Ready", "bytes", Type::Bytes, source)?
}
};
self.coerce(
Expression {
kind: Node::Bytes(operation, values),
value_type,
span: source.span,
},
expected,
)
}
fn byte_result(
&self,
name: &str,
success: &str,
field: &str,
kind: Type,
source: &ast::Expression,
) -> Result<Type, Diagnostic> {
let index = self
.definition_names
.resolve(name, self.source, source.span)?
.ok_or_else(|| self.error(source, "missing byte result type"))?;
let valid = matches!(&self.definitions[index].shape, Shape::Choice(variants) if variants.len() == 2
&& variants[0].name == success && !variants[0].error && variants[0].fields == [(field.into(), kind)]
&& variants[1].name == "Failed" && variants[1].error && variants[1].fields == [("message".into(), Type::Text)]);
if !valid {
return Err(self.error(
source,
"byte result type does not match its intrinsic contract",
));
}
Ok(Type::Named(index))
}
}
+76
View File
@@ -0,0 +1,76 @@
//! 只把官方 time 原语映射到对应名义结果;时间值没有隐式 Int 转换。
use super::Context;
use crate::compiler::model::*;
use crate::syntax::{ast, diagnostic::Diagnostic};
impl Context<'_> {
pub(super) fn calendar(
&mut self,
operation: CalendarOperation,
arguments: &[ast::Expression],
expected: Option<&Type>,
source: &ast::Expression,
) -> Result<Expression, Diagnostic> {
if self.definition_names.packages[self.source] != "dever.time" {
return Err(Diagnostic::new(
"C006",
"calendar intrinsics belong to the official dever.time package",
source.span,
));
}
let inputs = match operation {
CalendarOperation::UnixMillis
| CalendarOperation::MonotonicNanos
| CalendarOperation::Now => vec![],
CalendarOperation::ParseDateTime
| CalendarOperation::ParseDate
| CalendarOperation::ParseTime => vec![Type::Text],
CalendarOperation::FormatDateTime => vec![Type::DateTime],
CalendarOperation::FormatDate => vec![Type::Date],
CalendarOperation::FormatTime => vec![Type::Time],
CalendarOperation::Duration => vec![Type::Int],
CalendarOperation::Add | CalendarOperation::Subtract => {
vec![Type::DateTime, Type::Duration]
}
CalendarOperation::Difference => vec![Type::DateTime, Type::DateTime],
};
if inputs.len() != arguments.len() {
return Err(self.error(source, "calendar argument count differs"));
}
let values = arguments
.iter()
.zip(inputs)
.map(|(argument, kind)| self.expression(argument, Some(&kind)))
.collect::<Result<Vec<_>, _>>()?;
let value_type = if matches!(operation, CalendarOperation::Duration) {
Type::Duration
} else {
let (name, kind) = match operation {
CalendarOperation::UnixMillis | CalendarOperation::MonotonicNanos => {
("ClockResult", Type::Int)
}
CalendarOperation::Now
| CalendarOperation::ParseDateTime
| CalendarOperation::Add
| CalendarOperation::Subtract => ("DateTimeResult", Type::DateTime),
CalendarOperation::ParseDate => ("DateResult", Type::Date),
CalendarOperation::ParseTime => ("TimeResult", Type::Time),
CalendarOperation::FormatDateTime
| CalendarOperation::FormatDate
| CalendarOperation::FormatTime => ("TextResult", Type::Text),
CalendarOperation::Difference => ("DurationResult", Type::Duration),
CalendarOperation::Duration => unreachable!(),
};
self.io_result(name, &[("Read", vec![("value", kind)])], source)?
};
self.coerce(
Expression {
kind: Node::Calendar(operation, values),
value_type,
span: source.span,
},
expected,
)
}
}
+341
View File
@@ -0,0 +1,341 @@
//! 解析普通调用、捕获操作、集合操作和受限标准库底层操作。
//! 静态目标优先使用 handler 展开结果;参数按已检查的签名求值。
use super::Context;
use crate::compiler::model::*;
use crate::syntax::{ast, diagnostic::Diagnostic};
impl Context<'_> {
pub(super) fn call(
&mut self,
target: &ast::Expression,
arguments: &[ast::Expression],
expected: Option<&Type>,
source: &ast::Expression,
) -> Result<Expression, Diagnostic> {
let name = target
.path()
.ok_or_else(|| self.error(target, "call target must be statically named"))?;
if name == "dever.json.decode" {
return self.decode_json(arguments, expected, source);
}
if matches!(
name.as_str(),
"assert" | "assertEq" | "secret" | "dever.test.advanceClock" | "dever.test.drainJobs"
) {
return self.test_operation(&name, arguments, expected, source);
}
if let Some(operation) = upload::Operation::named(&name) {
return self.upload(operation, arguments, expected, source);
}
if matches!(name.as_str(), "dever.job.enqueue" | "dever.job.enqueueAt") {
return self.enqueue(name.ends_with("At"), arguments, expected, source);
}
if let Some(value) = self.database(&name, arguments, expected, source)? {
return Ok(value);
}
if let Some(operation) = auth::Operation::named(&name) {
return self.auth(operation, arguments, expected, source);
}
if let Some(operation) = authorization::Operation::named(&name) {
return self.authorization(operation, arguments, expected, source);
}
// 身份 ID 只在这两个固定入口接受期望的名义类型,不开放普通 Int 转换。
if matches!(name.as_str(), "dever.auth.userId" | "dever.auth.tenantId")
&& matches!(expected, Some(Type::Optional(inner)) if matches!(inner.as_ref(), Type::ModelId(_)))
{
let operation = if name.ends_with("userId") {
auth::Operation::UserId
} else {
auth::Operation::TenantId
};
return self.auth(operation, arguments, expected, source);
}
if let Some(&index) = self.scope.targets.get(&source.span.start) {
let signature = &self.signatures[index];
if arguments.len() != signature.inputs.len() {
return Err(self.error(source, "function argument count differs"));
}
let mut values = Vec::new();
for (argument, (_, kind)) in arguments.iter().zip(&signature.inputs) {
if signature.name == "encode"
&& self.definition_names.packages[signature.source] == "dever.json"
{
// 具体 wire 编码由编译器生成,失败传播继续复用普通源码函数。
values.push(self.encode_json(argument, kind, source)?);
} else {
values.push(self.expression(argument, Some(kind))?);
}
}
return self.coerce(
Expression {
kind: Node::Call(index, values),
value_type: signature.output(),
span: source.span,
},
expected,
);
}
if name == "result" {
if arguments.len() != 1 {
return Err(self.error(source, "result requires one function call"));
}
return self.capture(&arguments[0], expected, source);
}
if let Some(operation) = Schedule::named(&name) {
return self.schedule(operation, arguments, expected, source);
}
if name == "stream" {
return self.stream(arguments, expected, source);
}
if name == "dever.system.taskTicks" {
return self.ticks(arguments, expected, source);
}
if name == "send" {
return self.send(arguments, expected, source);
}
if name == "receive" {
return self.receive(arguments, expected, source);
}
if name == "group" {
return self.group(arguments, expected, source);
}
if name == "timeout" {
return self.timeout(arguments, expected, source);
}
if name == "race" {
return self.race(arguments, expected, source);
}
if matches!(name.as_str(), "wait" | "stop") {
return self.join(name == "stop", arguments, expected, source);
}
if matches!(
name.as_str(),
"dever.system.timeSleep" | "dever.system.taskSleep"
) {
let timer = if name == "dever.system.taskSleep" {
Timer::Delay
} else {
Timer::Sleep
};
return self.timer(timer, arguments, expected, source);
}
if name == "close" {
let [argument] = arguments else {
return Err(self.error(source, "close requires one stream"));
};
let value = self.expression(argument, None)?;
if !matches!(
value.value_type,
Type::Stream(_) | Type::AsyncStream(_) | Type::Channel(_)
) {
return Err(self.error(
source,
"core close requires a Stream, AsyncStream or Channel",
));
}
return self.coerce(
Expression {
kind: Node::Close(Box::new(value)),
value_type: Type::Unit,
span: source.span,
},
expected,
);
}
if let Some((index, variant)) = super::names::variant(
&name,
self.source,
self.definition_names,
self.definitions,
source.span,
)? {
return self.variant(index, variant, arguments, expected, source);
}
if arguments.len() == 1 && matches!(name.as_str(), "length" | "sum") {
let numeric = match expected {
Some(Type::Optional(inner)) => Some(inner.as_ref()),
kind => kind,
};
let context = if name == "sum"
&& matches!(&arguments[0].kind, ast::ExpressionKind::List(values) if values.is_empty())
{
numeric
.filter(|kind| kind.numeric())
.map(|kind| Type::List(Box::new(kind.clone())))
} else {
None
};
let value = self.expression(&arguments[0], context.as_ref())?;
let value_type = match (name.as_str(), &value.value_type) {
("length", Type::Text | Type::Bytes | Type::List(_) | Type::Map(_, _)) => Type::Int,
("sum", Type::List(inner)) if inner.numeric() => *inner.clone(),
_ => return Err(self.error(source, "invalid collection operation")),
};
let kind = if name == "sum" {
Node::Sum(Box::new(value))
} else {
Node::Length(Box::new(value))
};
return self.coerce(
Expression {
kind,
value_type,
span: source.span,
},
expected,
);
}
if let Some(operation) = Operation::named(&name) {
return self.collection(operation, arguments, expected, source);
}
if let Some(operation) = Access::named(&name) {
return self.access(operation, arguments, expected, source);
}
if let Some(operation) = CryptoOperation::named(&name) {
return self.crypto(operation, arguments, expected, source);
}
if let Some(operation) = CalendarOperation::named(&name) {
return self.calendar(operation, arguments, expected, source);
}
if let Some(operation) = StandardOperation::named(&name) {
return self.standard(operation, arguments, expected, source);
}
if let Some(operation) = BytesOperation::named(&name) {
return self.bytes(operation, arguments, expected, source);
}
if let Some(operation) = NetOperation::named(&name) {
return self.net(operation, arguments, expected, source);
}
if let Some(operation) = TlsOperation::named(&name) {
return self.tls(operation, arguments, expected, source);
}
if matches!(
name.as_str(),
"dever.system.httpServeLive" | "dever.system.httpServeLiveTls"
) {
return self.http_live(name.ends_with("Tls"), arguments, expected, source);
}
let websocket = match name.as_str() {
"dever.system.wsAccept" => Some(HttpOperation::WsAccept),
"dever.system.wsConnect" => Some(HttpOperation::WsConnect),
"dever.system.wsOpen" => Some(HttpOperation::WsOpen),
"dever.system.wsSend" => Some(HttpOperation::WsSend),
"dever.system.wsReceive" => Some(HttpOperation::WsReceive),
"dever.system.wsMessages" => Some(HttpOperation::WsMessages),
"dever.system.wsClose" => Some(HttpOperation::WsClose),
_ => None,
};
if let Some(operation) = websocket {
return self.websocket(operation, arguments, expected, source);
}
let reply = match name.as_str() {
"dever.system.httpRespond" => Some(HttpOperation::Respond),
"dever.system.httpStart" => Some(HttpOperation::Start),
"dever.system.httpWrite" => Some(HttpOperation::Write),
"dever.system.httpFinish" => Some(HttpOperation::Finish),
"dever.system.sseStart" => Some(HttpOperation::SseStart),
"dever.system.sseSend" => Some(HttpOperation::SseSend),
_ => None,
};
if let Some(operation) = reply {
return self.http_reply(operation, arguments, expected, source);
}
let pool = match name.as_str() {
"dever.system.httpClient" => Some(HttpOperation::Client),
"dever.system.httpRequest" => Some(HttpOperation::Request),
"dever.system.httpOpen" => Some(HttpOperation::Open),
"dever.system.httpOpenStream" => Some(HttpOperation::OpenStream),
"dever.system.httpCloseClient" => Some(HttpOperation::Close),
_ => None,
};
if let Some(operation) = pool {
return self.http_pool(operation, arguments, expected, source);
}
if matches!(
name.as_str(),
"dever.system.httpSend" | "dever.system.httpServe" | "dever.system.httpServeTls"
) {
return self.http(
name != "dever.system.httpSend",
name == "dever.system.httpServeTls",
arguments,
expected,
source,
);
}
if let Some(operation) = IoOperation::named(&name) {
return self.io(operation, arguments, expected, source);
}
if let Some(operation) = JsonOperation::named(&name) {
return self.json(operation, arguments, expected, source);
}
if matches!(
name.as_str(),
"dever.system.idFromText" | "dever.system.idToText"
) {
if self.definition_names.packages[self.source] != "dever.id" {
return Err(Diagnostic::new(
"C006",
"Id operations belong to the official dever.id package",
source.span,
));
}
let [argument] = arguments else {
return Err(self.error(source, "Id conversion requires one argument"));
};
let from_text = name == "dever.system.idFromText";
let input = if from_text { Type::Text } else { Type::Id };
let value = Box::new(self.expression(argument, Some(&input))?);
return self.coerce(
Expression {
kind: if from_text {
Node::Id(value)
} else {
Node::IdText(value)
},
value_type: if from_text { Type::Id } else { Type::Text },
span: source.span,
},
expected,
);
}
Err(self.error(
source,
format!(
"unknown or unsupported function '{name}' with {} inputs",
arguments.len()
),
))
}
pub(super) fn variant(
&mut self,
index: usize,
variant: usize,
arguments: &[ast::Expression],
expected: Option<&Type>,
source: &ast::Expression,
) -> Result<Expression, Diagnostic> {
let Shape::Choice(variants) = &self.definitions[index].shape else {
unreachable!()
};
let fields = variants[variant].fields.clone();
if fields.len() != arguments.len() {
return Err(self.error(source, "choice payload argument count differs"));
}
let values = arguments
.iter()
.zip(&fields)
.map(|(argument, (_, kind))| self.expression(argument, Some(kind)))
.collect::<Result<Vec<_>, _>>()?;
self.coerce(
Expression {
kind: Node::Variant(index, variant, values),
value_type: Type::Named(index),
span: source.span,
},
expected,
)
}
}
+74
View File
@@ -0,0 +1,74 @@
//! 通道元素是已知的具体可传递类型;发送移交值,接收按既有规则压平可空层。
use super::{Context, names};
use crate::compiler::model::*;
use crate::syntax::{ast, diagnostic::Diagnostic};
impl Context<'_> {
pub(super) fn channel(
&mut self,
element: &ast::TypeRef,
capacity: &ast::Expression,
expected: Option<&Type>,
source: &ast::Expression,
) -> Result<Expression, Diagnostic> {
let value_type = Type::Channel(Box::new(names::resolve(element, self.definition_names)?));
names::validate(&value_type, self.definitions, element.span)?;
let capacity = self.expression(capacity, Some(&Type::Int))?;
self.coerce(
Expression {
value_type,
kind: Node::Channel(Box::new(capacity)),
span: source.span,
},
expected,
)
}
pub(super) fn send(
&mut self,
arguments: &[ast::Expression],
expected: Option<&Type>,
source: &ast::Expression,
) -> Result<Expression, Diagnostic> {
let [channel, value] = arguments else {
return Err(self.error(source, "send requires a Channel and value"));
};
let channel = self.expression(channel, None)?;
let Type::Channel(element) = &channel.value_type else {
return Err(self.error(source, "send requires a Channel"));
};
let value = self.expression(value, Some(element))?;
self.coerce(
Expression {
value_type: Type::Unit,
kind: Node::Send(Box::new(channel), Box::new(value)),
span: source.span,
},
expected,
)
}
pub(super) fn receive(
&mut self,
arguments: &[ast::Expression],
expected: Option<&Type>,
source: &ast::Expression,
) -> Result<Expression, Diagnostic> {
let [channel] = arguments else {
return Err(self.error(source, "receive requires one Channel"));
};
let channel = self.expression(channel, None)?;
let Type::Channel(element) = &channel.value_type else {
return Err(self.error(source, "receive requires a Channel"));
};
self.coerce(
Expression {
value_type: element.as_ref().clone().optional(),
kind: Node::Receive(Box::new(channel)),
span: source.span,
},
expected,
)
}
}
+229
View File
@@ -0,0 +1,229 @@
//! 解析分句条件,并证明互斥、穷尽和可达。
//! 分句源顺序不代表优先级;other 是同一输入轴显式条件的补集。
mod partition;
use super::names;
use crate::compiler::model::*;
use crate::syntax::{ast, diagnostic::Diagnostic};
pub(super) fn range(
clauses: &[Clause],
clause: &Clause,
position: usize,
kind: &Type,
definitions: &[Definition],
remaining: &mut usize,
) -> Result<Option<numeric::Range>, Diagnostic> {
let kind = match kind {
Type::Optional(inner) => inner,
kind => kind,
};
if !matches!(kind, Type::Int | Type::Decimal) {
return Ok(None);
}
match clause.patterns[position].inner() {
Pattern::Number(value) => return Ok(Some(numeric::Range::point(*value))),
Pattern::Range(bounds) => return Ok(numeric::Range::bounds(kind, bounds)),
Pattern::Any => return Ok(Some(numeric::Range::full(kind))),
Pattern::Other => {}
_ => return Ok(None),
}
let column = clauses
.iter()
.map(|clause| clause.patterns[position].inner())
.collect::<Vec<_>>();
let cells = partition::cells(kind, &column, definitions);
*remaining = remaining
.checked_sub(cells.len().saturating_mul(column.len()))
.ok_or_else(|| {
Diagnostic::new("C015", "range proof exceeds its work limit", clause.span)
})?;
let mut range = None;
for (index, cell) in cells.iter().enumerate() {
let accepted = !column
.iter()
.any(|pattern| partition::matches(pattern, cell));
if accepted && let partition::Cell::Number(minimum) = cell {
let maximum = match cells.get(index + 1) {
Some(partition::Cell::Number(next)) => {
next.predecessor().expect("ordered numeric cells")
}
_ => numeric::Number::maximum(kind),
};
let actual = numeric::Range {
minimum: *minimum,
maximum,
};
range = Some(range.map_or(actual, |previous: numeric::Range| previous.hull(actual)));
}
}
Ok(range)
}
pub(super) fn resolve(
group: &[ast::Function],
signature: &Signature,
names: &super::package::Types,
definitions: &[Definition],
) -> Result<Vec<Vec<Pattern>>, Diagnostic> {
group.iter().map(|function| {
function.inputs.iter().zip(&signature.inputs).map(|(input, (_, kind))| {
let present = match kind {
Type::Optional(inner) => inner.as_ref(),
kind => kind,
};
let mut pattern = match &input.pattern {
ast::Pattern::Handler(_, _) => unreachable!("handler parameters are not clause axes"),
ast::Pattern::Other => Pattern::Other,
ast::Pattern::Literal(ast::Literal::Null) => Pattern::Null,
ast::Pattern::Literal(ast::Literal::Number(value)) => Pattern::Number(
numeric::Number::parse(value, present)
.map_err(|error| Diagnostic::new("C008", error, input.span))?
),
ast::Pattern::Literal(value) => Pattern::Literal(value.clone()),
ast::Pattern::Typed(reference, bounds) => {
if let Some((index, variant)) = names::variant_type(reference, names, definitions)? {
let Shape::Choice(variants) = &definitions[index].shape else { unreachable!() };
if !variants[variant].fields.is_empty() || !bounds.is_empty() || reference.nullable {
return Err(Diagnostic::new("C005", "variant pattern requires exactly its payload bindings", input.span));
}
Pattern::Variant(variant)
} else if bounds.is_empty() {
Pattern::Any
} else {
if !matches!(present, Type::Int | Type::Decimal) || reference.nullable {
return Err(Diagnostic::new("C005", "range patterns require nonnullable Int or Decimal", input.span));
}
Pattern::Range(bounds.iter().map(|bound| {
Ok(numeric::Bound {
operator: bound.operator.clone(),
value: numeric::Number::parse(&bound.value, present)
.map_err(|error| Diagnostic::new("C008", error, input.span))?,
})
}).collect::<Result<_, Diagnostic>>()?)
}
}
ast::Pattern::Variant(path, bindings) => {
let (index, variant) = names::variant_type(path, names, definitions)?
.ok_or_else(|| Diagnostic::new("C004", "unknown choice variant", input.span))?;
let Shape::Choice(variants) = &definitions[index].shape else { unreachable!() };
if bindings.len() != variants[variant].fields.len() {
return Err(Diagnostic::new("C005", "variant payload binding count differs", input.span));
}
Pattern::Variant(variant)
}
};
let full_nullable = matches!(&input.pattern, ast::Pattern::Typed(reference, _) if reference.nullable);
if matches!(kind, Type::Optional(_)) && !full_nullable && !matches!(pattern, Pattern::Null | Pattern::Other) {
pattern = Pattern::Present(Box::new(pattern));
}
Ok(pattern)
}).collect()
}).collect()
}
pub(super) fn validate(
patterns: &[Vec<Pattern>],
signature: &Signature,
definitions: &[Definition],
clauses: &[ast::Function],
) -> Result<(), Diagnostic> {
let partitions = signature
.inputs
.iter()
.enumerate()
.map(|(position, (_, kind))| {
let column = patterns
.iter()
.map(|row| &row[position])
.collect::<Vec<_>>();
partition::cells(kind, &column, definitions)
})
.collect::<Vec<_>>();
let mut reached = vec![false; clauses.len()];
coverage(
0,
&(0..clauses.len()).collect::<Vec<_>>(),
&partitions,
patterns,
clauses,
&mut reached,
)?;
if let Some(index) = reached.iter().position(|value| !value) {
return Err(Diagnostic::new(
"C009",
"unreachable function clause",
clauses[index].span,
));
}
Ok(())
}
fn coverage(
position: usize,
candidates: &[usize],
partitions: &[Vec<partition::Cell>],
patterns: &[Vec<Pattern>],
clauses: &[ast::Function],
reached: &mut [bool],
) -> Result<(), Diagnostic> {
if candidates.is_empty() {
return Err(Diagnostic::new(
"C009",
"function clauses do not cover every input",
clauses[0].span,
));
}
if position == partitions.len() {
if candidates.len() != 1 {
return Err(Diagnostic::new(
"C009",
"function clauses overlap",
clauses[candidates[1]].span,
));
}
reached[candidates[0]] = true;
return Ok(());
}
for cell in &partitions[position] {
let selected = candidates
.iter()
.copied()
.filter(|index| {
let pattern = &patterns[*index][position];
if matches!(pattern, Pattern::Other) {
!patterns
.iter()
.any(|row| partition::matches(&row[position], cell))
} else {
partition::matches(pattern, cell)
}
})
.collect::<Vec<_>>();
coverage(
position + 1,
&selected,
partitions,
patterns,
clauses,
reached,
)?;
}
Ok(())
}
pub(super) fn accepts_variant(
pattern: &Pattern,
variant: usize,
column: impl Iterator<Item = Pattern>,
) -> bool {
let cell = partition::Cell::Variant(variant);
if matches!(pattern, Pattern::Other) {
!column
.into_iter()
.any(|pattern| partition::matches(&pattern, &cell))
} else {
partition::matches(pattern, &cell)
}
}
@@ -0,0 +1,118 @@
//! 把输入值域划成对当前分句等价的有限单元。
//! 数值边界使用真实的相邻可表示值,最大值之后不再创建单元。
use crate::compiler::model::*;
use crate::syntax::ast::Literal;
pub(super) enum Cell {
Number(numeric::Number),
Bool(bool),
Text(Option<String>),
Variant(usize),
Null,
Whole,
}
pub(super) fn cells(kind: &Type, patterns: &[&Pattern], definitions: &[Definition]) -> Vec<Cell> {
// 所有分句都接受整列时,Bool、Choice、可空类型也只有一个匹配等价类。
if patterns
.iter()
.all(|pattern| matches!(pattern, Pattern::Any))
{
return vec![Cell::Whole];
}
if let Type::Optional(inner) = kind {
let patterns = patterns
.iter()
.map(|pattern| pattern.inner())
.collect::<Vec<_>>();
let mut result = cells(inner, &patterns, definitions);
result.push(Cell::Null);
return result;
}
match kind {
Type::Bool => vec![Cell::Bool(false), Cell::Bool(true)],
Type::Int | Type::Decimal => {
// 边界与后继划分所有可能条件;Decimal 不经过二进制浮点近似。
let mut edges = vec![numeric::Number::minimum(kind)];
for pattern in patterns {
match pattern {
Pattern::Number(value) => {
edges.push(*value);
edges.extend(value.successor());
}
Pattern::Range(bounds) => {
for bound in bounds {
let edge = if matches!(bound.operator.as_str(), ">" | "<=") {
bound.value.successor()
} else {
Some(bound.value)
};
edges.extend(edge);
}
}
_ => {}
}
}
edges
.sort_by(|left, right| left.partial_cmp(right).expect("finite values of one type"));
edges.dedup();
edges.into_iter().map(Cell::Number).collect()
}
Type::Float => {
// whole 还代表 NaN、无穷和所有未列出的值;正负零比较相等。
let mut values = patterns
.iter()
.filter_map(|pattern| match pattern {
Pattern::Number(value) => Some(*value),
_ => None,
})
.collect::<Vec<_>>();
values.sort_by(|left, right| left.partial_cmp(right).expect("finite Float literals"));
values.dedup();
values
.into_iter()
.map(Cell::Number)
.chain([Cell::Whole])
.collect()
}
Type::Text => {
let mut values = patterns
.iter()
.filter_map(|pattern| match pattern {
Pattern::Literal(Literal::Text(value)) => Some(value.clone()),
_ => None,
})
.collect::<Vec<_>>();
values.sort();
values.dedup();
values
.into_iter()
.map(|value| Cell::Text(Some(value)))
.chain([Cell::Text(None)])
.collect()
}
Type::Named(index) => match &definitions[*index].shape {
Shape::Choice(variants) => (0..variants.len()).map(Cell::Variant).collect(),
Shape::Record(_) => vec![Cell::Whole],
},
_ => vec![Cell::Whole],
}
}
pub(super) fn matches(pattern: &Pattern, cell: &Cell) -> bool {
match (pattern, cell) {
(Pattern::Any, _) => true,
(Pattern::Null, Cell::Null) => true,
(Pattern::Present(_), Cell::Null) => false,
(Pattern::Present(inner), cell) => matches(inner, cell),
(Pattern::Range(bounds), Cell::Number(value)) => {
bounds.iter().all(|bound| bound.matches(*value))
}
(Pattern::Number(value), Cell::Number(actual)) => value == actual,
(Pattern::Literal(Literal::Bool(value)), Cell::Bool(actual)) => value == actual,
(Pattern::Literal(Literal::Text(value)), Cell::Text(Some(actual))) => value == actual,
(Pattern::Variant(value), Cell::Variant(actual)) => value == actual,
_ => false,
}
}
+144
View File
@@ -0,0 +1,144 @@
//! 校验集合元素、静态 handler、上下文和归约状态的类型合同。
//! reduceUntil 必须返回下一状态和名为 stop 的 Bool。
use super::Context;
use crate::compiler::model::*;
use crate::syntax::{ast, diagnostic::Diagnostic};
pub(super) fn handler_arity(operation: Operation, arguments: usize) -> Option<usize> {
if operation == Operation::ParallelEach {
match arguments {
3 => Some(1),
4 => Some(2),
_ => None,
}
} else if operation.reduction() {
(arguments == 3).then_some(2)
} else {
match arguments {
2 => Some(1),
3 => Some(2),
_ => None,
}
}
}
impl Context<'_> {
pub(super) fn collection(
&mut self,
operation: Operation,
arguments: &[ast::Expression],
expected: Option<&Type>,
source: &ast::Expression,
) -> Result<Expression, Diagnostic> {
let arity = handler_arity(operation, arguments.len())
.ok_or_else(|| self.error(source, "invalid collection argument count"))?;
let target = &arguments[0];
let name = target
.path()
.ok_or_else(|| self.error(target, "handler must be a named function"))?;
if self.variables.iter().any(|(variable, _)| variable == &name) {
return Err(self.error(target, "a runtime value cannot be a handler"));
}
let handler = self
.scope
.targets
.get(&target.span.start)
.copied()
.ok_or_else(|| self.error(target, "handler name or input count does not match"))?;
let signature = self.signatures[handler].clone();
if signature.inputs.len() != arity {
return Err(self.error(target, "handler input count does not match"));
}
let item = &signature.inputs[0].1;
let source_sequence = &arguments[1];
let context = (matches!(
source_sequence.kind,
ast::ExpressionKind::List(_) | ast::ExpressionKind::Call(_, _)
) && !self.scope.targets.contains_key(&source_sequence.span.start))
.then(|| {
let kind = Box::new(item.clone());
if matches!(&source_sequence.kind, ast::ExpressionKind::Call(target, _) if target.path().as_deref() == Some("stream")) { Type::AsyncStream(kind) } else { Type::List(kind) }
});
let sequence = self.expression(source_sequence, context.as_ref())?;
let valid = match &sequence.value_type {
Type::List(inner) => inner.as_ref() == item,
Type::Stream(inner) | Type::AsyncStream(inner) => {
inner.as_ref() == item
&& (operation.reduction()
|| (matches!(operation, Operation::Each | Operation::ParallelEach)
&& signature.outputs.is_empty()))
}
Type::Bytes => {
*item == Type::Int
&& matches!(
operation,
Operation::Each
| Operation::ParallelEach
| Operation::Reduce
| Operation::ReduceUntil
)
}
_ => false,
};
if !valid {
return Err(self.error(
source_sequence,
"sequence type or operation does not match its handler",
));
}
let parallel = operation == Operation::ParallelEach;
if parallel
&& (!super::task::transferable(&signature, self.definitions)
|| signature.effects.handler)
{
return Err(Diagnostic::new(
"C013",
"parallel handler inputs and failures must be transferable with known effects",
source.span,
));
}
let limit = if parallel {
Some(self.expression(&arguments[2], Some(&Type::Int))?)
} else {
None
};
let extra = arguments
.get(if parallel { 3 } else { 2 })
.map(|value| self.expression(value, Some(&signature.inputs[1].1)))
.transpose()?;
let value_type = match (operation, signature.outputs.as_slice()) {
(Operation::Each | Operation::ParallelEach, []) => Type::Unit,
(Operation::Each, [(_, value)]) => Type::List(Box::new(value.clone())),
(Operation::Filter, [(_, Type::Bool)]) => sequence.value_type.clone(),
(Operation::Find, [(_, Type::Bool)]) => Type::Optional(Box::new(item.clone())),
(Operation::Sum, [(_, value)]) if value.numeric() => value.clone(),
(Operation::Reduce, [(_, value)]) if *value == signature.inputs[1].1 => value.clone(),
(Operation::ReduceUntil, [(_, value), (stop, Type::Bool)])
if *value == signature.inputs[1].1 && stop == "stop" =>
{
value.clone()
}
_ => {
return Err(self.error(
target,
"handler outputs do not match the collection operation",
));
}
};
self.coerce(
Expression {
kind: Node::Collection(Box::new(Collection {
operation,
handler,
sequence,
limit,
extra,
})),
value_type,
span: source.span,
},
expected,
)
}
}
+65
View File
@@ -0,0 +1,65 @@
//! 折叠已经确定类型的常量算术,并提前报告必然发生的故障。
//! 复用与运行时一致的受检整数和 Decimal 运算,不进行浮点重排。
use crate::compiler::model::{Expression, Node, Type};
use crate::syntax::{ast::Literal, diagnostic::Diagnostic};
use dever_runtime::value::decimal::Decimal;
pub(super) fn fold(mut expression: Expression) -> Result<Expression, Diagnostic> {
let Node::Binary(operator, left, right) = &expression.kind else {
return Ok(expression);
};
let value = match expression.value_type {
Type::Int => {
let (Some(left), Some(right)) = (integer(left), integer(right)) else {
return Ok(expression);
};
match operator.as_str() {
"+" => left.checked_add(right),
"-" => left.checked_sub(right),
"*" => left.checked_mul(right),
"//" => left.checked_div(right),
"%" => left.checked_rem(right),
_ => unreachable!("checked Int operator"),
}
.map(|value| value.to_string())
}
Type::Decimal => {
let (Some(left), Some(right)) = (decimal(left), decimal(right)) else {
return Ok(expression);
};
let mut context = Decimal::context();
match operator.as_str() {
"+" => left.checked_add(right, &mut context),
"-" => left.checked_sub(right, &mut context),
"*" => left.checked_mul(right, &mut context),
"/" => left.checked_div(right, &mut context),
_ => unreachable!("checked Decimal operator"),
}
.map(|value| value.to_string())
}
_ => return Ok(expression),
}
.ok_or_else(|| Diagnostic::new("C008", "statically known arithmetic fault", expression.span))?;
expression.kind = Node::Literal(Literal::Number(value));
Ok(expression)
}
fn integer(expression: &Expression) -> Option<i64> {
match &expression.kind {
Node::Literal(Literal::Number(value)) if expression.value_type == Type::Int => {
value.parse().ok()
}
_ => None,
}
}
fn decimal(expression: &Expression) -> Option<Decimal> {
match &expression.kind {
Node::Literal(Literal::Number(value)) => {
Some(Decimal::parse(value).expect("checked Decimal literal"))
}
Node::Decimal(value) => integer(value).map(Decimal::from_int),
_ => None,
}
}
+66
View File
@@ -0,0 +1,66 @@
//! Secret 只进入既有批准的密码学接收点;参数和名义结果必须匹配官方库。
use super::Context;
use crate::compiler::model::*;
use crate::syntax::{ast, diagnostic::Diagnostic};
impl Context<'_> {
pub(super) fn crypto(
&mut self,
operation: CryptoOperation,
arguments: &[ast::Expression],
expected: Option<&Type>,
source: &ast::Expression,
) -> Result<Expression, Diagnostic> {
if self.definition_names.packages[self.source] != "dever.crypto" {
return Err(Diagnostic::new(
"C006",
"crypto intrinsics belong to the official dever.crypto package",
source.span,
));
}
let inputs = match operation {
CryptoOperation::Token => vec![Type::Int],
CryptoOperation::PasswordHash => vec![Type::Secret],
CryptoOperation::PasswordVerify => vec![Type::Secret, Type::Text],
CryptoOperation::Sha256 => vec![Type::Bytes],
CryptoOperation::HmacSha256 => vec![Type::Secret, Type::Bytes],
CryptoOperation::ConstantTimeEq => vec![Type::Bytes, Type::Bytes],
};
if inputs.len() != arguments.len() {
return Err(self.error(source, "crypto argument count differs"));
}
let values = arguments
.iter()
.zip(inputs)
.map(|(value, kind)| self.expression(value, Some(&kind)))
.collect::<Result<Vec<_>, _>>()?;
let value_type = match operation {
CryptoOperation::Token => self.io_result(
"TokenResult",
&[("Ready", vec![("value", Type::Secret)])],
source,
)?,
CryptoOperation::PasswordHash => self.io_result(
"HashResult",
&[("Ready", vec![("value", Type::Text)])],
source,
)?,
CryptoOperation::PasswordVerify => self.io_result(
"VerifyResult",
&[("Ready", vec![("value", Type::Bool)])],
source,
)?,
CryptoOperation::Sha256 | CryptoOperation::HmacSha256 => Type::Bytes,
CryptoOperation::ConstantTimeEq => Type::Bool,
};
self.coerce(
Expression {
value_type,
kind: Node::Crypto(operation, values),
span: source.span,
},
expected,
)
}
}
@@ -0,0 +1,142 @@
//! 查询函数只在 where 内解释;字段和 SQL 固定,输入按字段类型绑定。
use super::*;
impl super::super::Context<'_> {
pub(super) fn condition(
&mut self,
source: &ast::Expression,
fields: &[(String, Type)],
values: &mut Vec<Expression>,
bound_fields: &mut Vec<usize>,
) -> Result<(String, String), Diagnostic> {
match &source.kind {
ast::ExpressionKind::Unary(operator, value) if operator == "not" => {
let (sqlite, postgres) = self.condition(value, fields, values, bound_fields)?;
Ok((format!("(NOT {sqlite})"), format!("(NOT {postgres})")))
}
ast::ExpressionKind::Binary(operator, left, right)
if matches!(operator.as_str(), "and" | "or") =>
{
let (left_sqlite, left_postgres) =
self.condition(left, fields, values, bound_fields)?;
let (right_sqlite, right_postgres) =
self.condition(right, fields, values, bound_fields)?;
let operator = operator.to_ascii_uppercase();
Ok((
format!("({left_sqlite} {operator} {right_sqlite})"),
format!("({left_postgres} {operator} {right_postgres})"),
))
}
ast::ExpressionKind::Binary(operator, left, right)
if matches!(operator.as_str(), "==" | "!=" | "<" | "<=" | ">" | ">=") =>
{
let position = self.condition_field(left, fields)?;
let (name, kind) = &fields[position];
values.push(self.expression(right, Some(kind))?);
bound_fields.push(position);
let (sqlite, postgres) = match operator.as_str() {
"==" => ("IS", "IS NOT DISTINCT FROM"),
"!=" => ("IS NOT", "IS DISTINCT FROM"),
other => (other, other),
};
let parameter = values.len();
Ok((
format!("(\"{name}\" {sqlite} ?{parameter})"),
format!("(\"{name}\" {postgres} ${parameter})"),
))
}
ast::ExpressionKind::Call(target, arguments) => {
let ast::ExpressionKind::Name(operation) = &target.kind else {
return Err(self.error(target, "where requires a static query function"));
};
let arity = match operation.as_str() {
"in" | "contains" | "startsWith" | "endsWith" => 2,
"between" => 3,
_ => return Err(self.error(target, "unknown Model query function")),
};
if arguments.len() != arity {
return Err(self.error(source, "wrong number of query function arguments"));
}
let position = self.condition_field(&arguments[0], fields)?;
let (name, kind) = &fields[position];
let operands = if operation == "in" {
let ast::ExpressionKind::List(items) = &arguments[1].kind else {
return Err(self.error(&arguments[1], "in requires a direct value list"));
};
if !(1..=100).contains(&items.len()) {
return Err(
self.error(&arguments[1], "in requires between 1 and 100 values")
);
}
items.as_slice()
} else {
&arguments[1..]
};
if matches!(operation.as_str(), "contains" | "startsWith" | "endsWith")
&& !matches!(kind, Type::Text)
&& !matches!(kind, Type::Optional(inner) if **inner == Type::Text)
{
return Err(self.error(&arguments[0], "text query requires a Text field"));
}
let start = values.len() + 1;
for operand in operands {
values.push(self.expression(operand, Some(kind))?);
bound_fields.push(position);
}
let column = format!("\"{name}\"");
let (sqlite, postgres) = match operation.as_str() {
"in" => {
let sqlite = (start..=values.len())
.map(|index| format!("?{index}"))
.collect::<Vec<_>>()
.join(",");
let postgres = (start..=values.len())
.map(|index| format!("${index}"))
.collect::<Vec<_>>()
.join(",");
(
format!("{column} IN ({sqlite})"),
format!("{column} IN ({postgres})"),
)
}
"between" => (
format!("{column} BETWEEN ?{start} AND ?{}", start + 1),
format!("{column} BETWEEN ${start} AND ${}", start + 1),
),
"contains" => (
format!("instr(CAST({column} AS BLOB), CAST(?{start} AS BLOB)) > 0"),
format!("POSITION(${start} IN {column}) > 0"),
),
"startsWith" => (
format!("instr(CAST({column} AS BLOB), CAST(?{start} AS BLOB)) = 1"),
format!("LEFT({column}, LENGTH(${start})) = ${start}"),
),
"endsWith" => (
format!(
"substr(CAST({column} AS BLOB), length(CAST({column} AS BLOB)) - length(CAST(?{start} AS BLOB)) + 1) = CAST(?{start} AS BLOB)"
),
format!("RIGHT({column}, LENGTH(${start})) = ${start}"),
),
_ => unreachable!(),
};
Ok((format!("({sqlite})"), format!("({postgres})")))
}
_ => Err(self.error(source, "unsupported static Model condition")),
}
}
fn condition_field(
&self,
source: &ast::Expression,
fields: &[(String, Type)],
) -> Result<usize, Diagnostic> {
let ast::ExpressionKind::Name(name) = &source.kind else {
return Err(self.error(source, "condition must compare a Model field"));
};
fields
.iter()
.position(|(field, _)| field == name)
.ok_or_else(|| self.error(source, "unknown condition field"))
}
}
@@ -0,0 +1,144 @@
//! 默认值与 Seed 先转换为受检存储常量,不把非法值留到启动或 SQL 执行阶段。
use super::*;
use dever_runtime::{
time::calendar::{Date, DateTime, Time},
value::{decimal::Decimal, uuid::Uuid},
wire::json::Json,
};
use persistence::{Column, Value};
pub(super) fn check(
source: &ast::Expression,
kind: &Type,
column: &Column,
names: &Types,
) -> Result<Value, Diagnostic> {
let invalid = || {
Diagnostic::new(
"C014",
"invalid constant for persistent column",
source.span,
)
};
if let Type::Optional(inner) = kind {
if matches!(
source.kind,
ast::ExpressionKind::Literal(ast::Literal::Null)
) {
return Ok(Value::Null);
}
return check(source, inner, column, names);
}
if let (Type::Named(index), Some(variants), Some(path)) = (kind, &column.choice, source.path())
{
let Some((prefix, variant)) = path.rsplit_once('.') else {
return Err(invalid());
};
if names.resolve(prefix, source.span.source, source.span)? != Some(*index)
|| !variants.iter().any(|(name, _)| name == variant)
{
return Err(invalid());
}
return Ok(Value::Text(variant.into()));
}
let value = match &source.kind {
ast::ExpressionKind::Literal(value) => value.clone(),
ast::ExpressionKind::Unary(operator, value) if operator == "-" => match &value.kind {
ast::ExpressionKind::Literal(ast::Literal::Number(value)) => {
ast::Literal::Number(format!("-{value}"))
}
_ => return Err(invalid()),
},
_ => return Err(invalid()),
};
Ok(match (kind, value) {
(Type::Bool, ast::Literal::Bool(value)) => Value::Integer(i64::from(value)),
(Type::Int | Type::ModelId(_) | Type::Duration, ast::Literal::Number(value)) => {
Value::Integer(value.parse().map_err(|_| invalid())?)
}
(Type::Float, ast::Literal::Number(value)) => {
let value = value.parse::<f64>().map_err(|_| invalid())?;
if !value.is_finite() {
return Err(invalid());
}
Value::Real(value)
}
(Type::Decimal, ast::Literal::Number(value)) => {
let value = Decimal::parse(&value).map_err(|_| invalid())?;
let (precision, scale) = column.decimal.expect("checked Decimal column");
let value = dec::Decimal128::from_be_bytes(value.to_be_bytes());
let coefficient = value.coefficient();
let shift = value.exponent() + scale as i32;
let scaled = if coefficient == 0 {
0
} else if shift >= 0 {
coefficient
.checked_mul(10_i128.checked_pow(shift as u32).ok_or_else(invalid)?)
.ok_or_else(invalid)?
} else {
let divisor = 10_i128
.checked_pow(shift.unsigned_abs())
.ok_or_else(invalid)?;
if coefficient % divisor != 0 {
return Err(invalid());
}
coefficient / divisor
};
let limit = 10_i128.pow(precision);
if scaled <= -limit || scaled >= limit {
return Err(invalid());
}
Value::Text(format!(
"{:0width$}",
scaled + limit,
width = precision as usize + 1
))
}
(Type::Text, ast::Literal::Text(value)) => {
let value = match column.text {
persistence::TextMode::Raw => value,
persistence::TextMode::Trim => value.trim().to_owned(),
persistence::TextMode::Lower => value.trim().to_lowercase(),
persistence::TextMode::Upper => value.trim().to_uppercase(),
};
if let Some((min, max)) = column.limits
&& !(min as usize..=max as usize).contains(&value.chars().count())
{
return Err(invalid());
}
Value::Text(value)
}
(Type::Uuid, ast::Literal::Text(value)) => {
Value::Blob(Uuid::parse(&value).ok_or_else(invalid)?.as_bytes().to_vec())
}
(Type::DateTime, ast::Literal::Text(value)) => {
Value::Integer(DateTime::parse(&value).map_err(|_| invalid())?.millis())
}
(Type::Date, ast::Literal::Text(value)) => {
Value::Integer(Date::parse(&value).map_err(|_| invalid())?.millis())
}
(Type::Time, ast::Literal::Text(value)) => {
Value::Integer(Time::parse(&value).map_err(|_| invalid())?.millis())
}
(Type::DateTime, ast::Literal::Number(value)) => Value::Integer(
DateTime::from_millis(value.parse().map_err(|_| invalid())?)
.map_err(|_| invalid())?
.millis(),
),
(Type::Date, ast::Literal::Number(value)) => Value::Integer(
Date::from_millis(value.parse().map_err(|_| invalid())?)
.map_err(|_| invalid())?
.millis(),
),
(Type::Time, ast::Literal::Number(value)) => Value::Integer(
Time::from_millis(value.parse().map_err(|_| invalid())?)
.map_err(|_| invalid())?
.millis(),
),
(Type::Json, ast::Literal::Text(value)) => {
Value::Text(Json::parse(&value).map_err(|_| invalid())?.as_str().into())
}
_ => return Err(invalid()),
})
}
+185
View File
@@ -0,0 +1,185 @@
//! 查询字段、条件树和排序在编译时确定,右侧值沿普通类型检查进入参数绑定。
use super::*;
use crate::compiler::model::persistence::Operation;
pub(super) struct Filter {
pub condition: String,
pub postgres_condition: String,
pub order: String,
pub order_fields: Vec<(usize, bool)>,
pub values: Vec<Expression>,
pub fields: Vec<usize>,
pub relations: Vec<usize>,
}
impl super::super::Context<'_> {
pub(super) fn model_filter(
&mut self,
arguments: &[ast::Expression],
model: usize,
operation: Operation,
source: &ast::Expression,
) -> Result<Filter, Diagnostic> {
let fields = self.models[model].fields.clone();
let entries = match arguments {
[] => &[][..],
[block] => match &block.kind {
ast::ExpressionKind::Map(entries) => entries.as_slice(),
_ => return Err(self.error(block, "query requires a direct field block")),
},
_ => return Err(self.error(source, "query accepts at most one field block")),
};
let mut seen = std::collections::HashSet::new();
let mut filter = String::new();
let mut postgres_filter = String::new();
let mut order = String::from("\"id\" DESC");
let mut order_fields = vec![(0, false)];
let mut values = Vec::new();
let mut bound_fields = Vec::new();
let mut page = None;
let mut size = None;
let mut after = None;
let mut relations = Vec::new();
for (key, value) in entries {
let ast::ExpressionKind::Name(name) = &key.kind else {
return Err(self.error(key, "query option must be named"));
};
if !seen.insert(name) {
return Err(self.error(key, "duplicate query option"));
}
match name.as_str() {
"with"
if matches!(
operation,
Operation::First | Operation::List | Operation::Cursor
) =>
{
let ast::ExpressionKind::List(items) = &value.kind else {
return Err(self.error(value, "with requires a static relation list"));
};
for item in items {
let ast::ExpressionKind::Name(name) = &item.kind else {
return Err(
self.error(item, "with accepts one-level relation names only")
);
};
let relation = self.models[model]
.relations
.iter()
.position(|relation| relation.name == *name)
.ok_or_else(|| self.error(item, "unknown Model relation"))?;
if relations.contains(&relation) {
return Err(self.error(item, "duplicate Model relation"));
}
relations.push(relation);
}
}
"where" => {
let (sqlite, postgres) =
self.condition(value, &fields, &mut values, &mut bound_fields)?;
filter = format!(" WHERE {sqlite}");
postgres_filter = format!(" WHERE {postgres}");
}
"order"
if matches!(
operation,
Operation::First | Operation::List | Operation::Cursor | Operation::Stream
) =>
{
let ast::ExpressionKind::List(items) = &value.kind else {
return Err(self.error(value, "order requires a static field list"));
};
let mut columns = Vec::new();
let mut ordered = std::collections::HashSet::new();
order_fields.clear();
for item in items {
let ast::ExpressionKind::Field(base, direction) = &item.kind else {
return Err(self.error(item, "order requires field.asc or field.desc"));
};
let ast::ExpressionKind::Name(name) = &base.kind else {
return Err(self.error(item, "order requires a Model field"));
};
if !fields.iter().any(|(field, _)| field == name)
|| !matches!(direction.as_str(), "asc" | "desc")
|| !ordered.insert(name.as_str())
{
return Err(self.error(item, "unknown or duplicate ordering field"));
}
columns.push(format!("\"{name}\" {}", direction.to_ascii_uppercase()));
order_fields.push((
fields.iter().position(|(field, _)| field == name).unwrap(),
direction == "asc",
));
}
if !ordered.contains("id") {
columns.push("\"id\" DESC".into());
order_fields.push((0, false));
}
order = columns.join(",");
}
"page" if operation == Operation::List => page = Some(value),
"size" if matches!(operation, Operation::List | Operation::Cursor) => {
size = Some(value)
}
"after" if operation == Operation::Cursor => after = Some(value),
_ => {
return Err(Diagnostic::new(
"U001",
"unsupported Model query option",
key.span,
));
}
}
}
if operation == Operation::List {
for (value, default) in [(page, "1"), (size, "20")] {
values.push(match value {
Some(value) => self.expression(value, Some(&Type::Int))?,
None => Expression {
kind: Node::Literal(ast::Literal::Number(default.into())),
value_type: Type::Int,
span: source.span,
},
});
}
}
if operation == Operation::Cursor {
if order_fields
.iter()
.any(|(field, _)| matches!(fields[*field].1, Type::Optional(_)))
{
return Err(self.error(
source,
"cursor order does not support nullable Model fields",
));
}
let record = Type::Optional(Box::new(Type::Named(self.models[model].definition)));
values.push(match after {
Some(value) => self.expression(value, Some(&record))?,
None => Expression {
kind: Node::Literal(ast::Literal::Null),
value_type: record,
span: source.span,
},
});
values.push(match size {
Some(value) => self.expression(value, Some(&Type::Int))?,
None => Expression {
kind: Node::Literal(ast::Literal::Number("20".into())),
value_type: Type::Int,
span: source.span,
},
});
}
Ok(Filter {
condition: filter,
postgres_condition: postgres_filter,
order,
order_fields,
values,
fields: bound_fields,
relations,
})
}
}
+359
View File
@@ -0,0 +1,359 @@
//! Model 定义提供静态字段与名义 ID,App 的匿名块直接检查成绑定参数。
mod condition;
mod constant;
mod filter;
mod native;
mod options;
mod privacy;
pub(super) use privacy::Privacy;
mod query;
pub(super) mod relation;
mod schema;
mod write;
use super::package::Types;
use crate::{
compiler::model::{persistence::Model, *},
syntax::{ast, diagnostic::Diagnostic},
};
pub(super) fn prepare(
declarations: &mut Vec<ast::TypeDeclaration>,
names: &mut Types,
) -> Result<(), Diagnostic> {
let base = declarations.len();
let mut pages = Vec::new();
for (index, declaration) in declarations.iter_mut().enumerate() {
let identity = &names.packages[declaration.span.source];
let Some(expected) = package::model_record(identity) else {
continue;
};
if let ast::TypeBody::Choice(variants) = &declaration.body {
if variants.is_empty()
|| variants
.iter()
.any(|variant| variant.error || !variant.fields.is_empty())
{
return Err(Diagnostic::new(
"C014",
"Model choices require non-error variants without payloads",
declaration.span,
));
}
continue;
}
if declaration.name != expected {
continue;
}
let ast::TypeBody::Record(fields) = &mut declaration.body else {
return Err(Diagnostic::new(
"U001",
"Model choices are not implemented yet",
declaration.span,
));
};
if fields
.iter()
.any(|field| matches!(field.name.as_str(), "id" | "created_at"))
{
return Err(Diagnostic::new(
"C014",
"id and created_at are generated Model fields",
declaration.span,
));
}
if !names.insert(declaration.span.source, "id".into(), index, true) {
return Err(Diagnostic::new(
"C002",
"duplicate Model source",
declaration.span,
));
}
for (position, (name, kind)) in [("id", "id"), ("created_at", "DateTime")]
.into_iter()
.enumerate()
{
fields.insert(
position,
ast::Field {
name: name.into(),
kind: ast::TypeRef {
name: kind.into(),
arguments: vec![],
storage: vec![],
nullable: false,
span: declaration.span,
},
bounds: vec![],
private: false,
unique: false,
index: false,
rest: Default::default(),
persistence: Default::default(),
span: declaration.span,
},
);
}
names.insert(
declaration.span.source,
"Page".into(),
base + pages.len(),
true,
);
let kind = |name: &str| ast::TypeRef {
name: name.into(),
arguments: vec![],
storage: vec![],
nullable: false,
span: declaration.span,
};
let mut fields = [
("items", kind("List")),
("page", kind("Int")),
("size", kind("Int")),
("total", kind("Int")),
("pages", kind("Int")),
]
.into_iter()
.map(|(name, kind)| ast::Field {
name: name.into(),
kind,
bounds: vec![],
private: false,
unique: false,
index: false,
rest: Default::default(),
persistence: Default::default(),
span: declaration.span,
})
.collect::<Vec<_>>();
fields[0].kind.arguments.push(kind(&declaration.name));
pages.push(ast::TypeDeclaration {
name: "Page".into(),
global: false,
setting: false,
public: true,
body: ast::TypeBody::Record(fields),
span: declaration.span,
});
names.insert(
declaration.span.source,
"Cursor".into(),
base + pages.len(),
true,
);
let mut item = kind("List");
item.arguments.push(kind(&declaration.name));
let mut next = kind(&declaration.name);
next.nullable = true;
let fields = [("items", item), ("next", next), ("has_more", kind("Bool"))]
.into_iter()
.map(|(name, kind)| ast::Field {
name: name.into(),
kind,
bounds: vec![],
private: false,
unique: false,
index: false,
rest: Default::default(),
persistence: Default::default(),
span: declaration.span,
})
.collect();
pages.push(ast::TypeDeclaration {
name: "Cursor".into(),
global: false,
setting: false,
public: true,
body: ast::TypeBody::Record(fields),
span: declaration.span,
});
}
declarations.extend(pages);
relation::prepare(declarations, names);
Ok(())
}
pub(super) fn check(
definitions: &[Definition],
declarations: &[ast::TypeDeclaration],
names: &Types,
bindings: &[Option<String>],
persistence: &[ast::Persistence],
) -> Result<Vec<Model>, Diagnostic> {
let mut models = Vec::new();
for (index, definition) in definitions.iter().enumerate() {
let identity = &names.packages[definition.source];
let Some((_, "model")) = package::role(identity) else {
continue;
};
if names.resolve("id", definition.source, declarations[index].span)? != Some(index) {
continue;
}
let Shape::Record(fields) = &definition.shape else {
continue;
};
let ast::TypeBody::Record(source_fields) = &declarations[index].body else {
unreachable!()
};
let mut columns = Vec::new();
for (field, (_, kind)) in source_fields.iter().zip(fields) {
let decimal = if field.kind.name == "Decimal" {
match field.kind.storage.as_slice() {
[precision, scale] if (1..=34).contains(precision) && scale <= precision => {
Some((*precision, *scale))
}
_ => {
return Err(Diagnostic::new(
"C014",
"Decimal storage requires precision 1..34 and scale 0..precision",
field.kind.span,
));
}
}
} else {
None
};
let limits = match field.kind.storage.as_slice() {
_ if decimal.is_some() => None,
[] => None,
[max] if *max > 0 => Some((0, *max)),
[min, max] if min <= max && *max > 0 => Some((*min, *max)),
_ => {
return Err(Diagnostic::new(
"C014",
"invalid storage length bounds",
field.kind.span,
));
}
};
let base = if let Type::Optional(inner) = kind {
inner.as_ref()
} else {
kind
};
let choice = if let Type::Named(index) = base {
match &declarations[*index].body {
ast::TypeBody::Choice(variants)
if variants
.iter()
.all(|variant| !variant.error && variant.fields.is_empty()) =>
{
Some(
variants
.iter()
.map(|variant| {
(
variant.name.clone(),
variant
.label
.clone()
.unwrap_or_else(|| variant.name.clone()),
)
})
.collect(),
)
}
_ => None,
}
} else {
None
};
if field.persistence.generated
&& (*kind != Type::Uuid || field.persistence.default.is_some())
{
return Err(Diagnostic::new(
"C014",
"generated requires non-null Uuid without a default",
field.span,
));
}
if field.persistence.text.is_some() && *base != Type::Text {
return Err(Diagnostic::new(
"C014",
"raw, lower and upper require a Text Model field",
field.span,
));
}
let mut column = persistence::Column {
text: field.persistence.text.unwrap_or_default(),
limits,
decimal,
generated: field.persistence.generated,
default: None,
previous: field.persistence.previous.clone(),
choice,
unique: field.unique,
index: field.index,
};
if let Some(value) = &field.persistence.default {
column.default = Some(constant::check(value, kind, &column, names)?);
}
columns.push(column);
}
let supported = |kind: &Type| {
matches!(
kind,
Type::Bool
| Type::Int
| Type::Float
| Type::Text
| Type::Bytes
| Type::ModelId(_)
| Type::DateTime
| Type::Decimal
| Type::Uuid
| Type::Date
| Type::Time
| Type::Duration
| Type::Json
)
};
if fields.iter().zip(&columns).any(|((_, kind), column)| {
column.choice.is_none()
&& !supported(if let Type::Optional(inner) = kind {
inner
} else {
kind
})
}) || definition.ranges.iter().any(Option::is_some)
{
return Err(Diagnostic::new(
"U001",
"this persistent field type or constraint is not implemented yet",
crate::syntax::source::Span {
source: definition.source,
start: 0,
end: 0,
},
));
}
let span = crate::syntax::source::Span {
source: definition.source,
start: 0,
end: 0,
};
let page = names.resolve("Page", definition.source, span)?.unwrap();
let cursor = names.resolve("Cursor", definition.source, span)?.unwrap();
models.push(Model {
global: declarations[index].global,
definition: index,
page,
cursor,
source: definition.source,
table: identity.replace('.', "_"),
component: identity.split('.').next().unwrap().into(),
fields: fields.clone(),
columns,
indexes: vec![],
seeds: vec![],
migrations: vec![],
sql: vec![],
relations: vec![],
database: bindings[definition.source].clone(),
});
}
schema::check(&mut models, declarations, persistence, names)?;
native::check(&mut models, definitions, declarations, persistence, names)?;
Ok(models)
}
@@ -0,0 +1,311 @@
//! SQL 合同在编译期确定返回记录、标量编码和只读证明;执行沿用 Model 的连接。
mod sql;
use super::*;
use crate::compiler::{
check::names,
model::persistence::{Column, NativeSql, Operation, Query, Write},
};
pub(super) fn check(
models: &mut [Model],
definitions: &[Definition],
sources: &[ast::TypeDeclaration],
declarations: &[ast::Persistence],
names: &Types,
) -> Result<(), Diagnostic> {
let mut used = std::collections::HashSet::new();
let tables = models
.iter()
.map(|model| model.table.clone())
.collect::<Vec<_>>();
for model in models.iter_mut() {
for declaration in declarations
.iter()
.filter(|declaration| declaration.span.source == model.source)
{
let ast::PersistenceKind::Sql(value) = &declaration.kind else {
continue;
};
let span = declaration.span;
let invalid = |message| Diagnostic::new("C014", message, span);
names::function(&value.name, span)?;
if Operation::named(&value.name).is_some()
|| model.sql.iter().any(|sql| sql.name == value.name)
{
return Err(invalid("duplicate or reserved Model SQL operation"));
}
let [output] = value.outputs.as_slice() else {
return Err(invalid("SQL requires one record output"));
};
field_contract(output)?;
let output = names::resolve(&output.kind, names)?;
let row = match &output {
Type::Optional(inner) | Type::List(inner) => &**inner,
other => other,
};
let Type::Named(record) = row else {
return Err(invalid(
"SQL output requires a concrete record, optional record or List",
));
};
let result = &definitions[*record];
let Shape::Record(fields) = &result.shape else {
return Err(invalid("SQL output requires a record"));
};
let same_model = *record == model.definition;
let own_record = result.source == model.source;
let app_view = package::role(&names.packages[result.source])
== package::role(&names.packages[model.source]).map(|(domain, _)| (domain, "app"));
if !same_model && !own_record && !app_view {
return Err(invalid(
"SQL result must belong to its Model or the same domain App",
));
}
if !same_model
&& (result.ranges.iter().any(Option::is_some) || !result.private_fields.is_empty())
{
return Err(invalid(
"SQL result records require public unconstrained scalar fields",
));
}
used.insert(*record);
let mut inputs = Vec::new();
let mut input_columns = Vec::new();
let mut seen = std::collections::HashSet::new();
for input in &value.inputs {
field_contract(input)?;
names::identifier(&input.name, false, input.span)?;
if !seen.insert(&input.name) {
return Err(invalid("duplicate SQL parameter"));
}
let kind = names::resolve(&input.kind, names)?;
input_columns.push(column(&kind, &input.kind, sources)?);
inputs.push(kind);
}
let ast::TypeBody::Record(source_fields) = &sources[*record].body else {
unreachable!()
};
if !same_model {
for field in source_fields {
field_contract(field)?;
}
}
let columns = if same_model {
model.columns.clone()
} else {
fields
.iter()
.zip(source_fields)
.map(|((_, kind), source)| column(kind, &source.kind, sources))
.collect::<Result<Vec<_>, _>>()?
};
let (sqlite, mut dependencies) =
sql::check(&value.sqlite, false, inputs.len(), model, span, &tables)?;
let (postgres, pg_dependencies) =
sql::check(&value.postgres, true, inputs.len(), model, span, &tables)?;
dependencies.extend(pg_dependencies);
dependencies.sort_unstable();
dependencies.dedup();
let record = *record;
model.sql.push(NativeSql {
dependencies,
name: value.name.clone(),
inputs,
input_columns,
output,
record,
columns,
sqlite: value.sqlite.clone(),
postgres: value.postgres.clone(),
readonly: sqlite && postgres,
});
}
}
for (index, declaration) in sources.iter().enumerate() {
if matches!(
package::role(&names.packages[declaration.span.source]),
Some((_, "model"))
) && matches!(declaration.body, ast::TypeBody::Record(_))
&& !matches!(declaration.name.as_str(), "Page" | "Cursor")
&& !models.iter().any(|model| model.definition == index)
&& !used.contains(&index)
{
return Err(Diagnostic::new(
"C014",
"additional Model records must be SQL result records",
declaration.span,
));
}
}
Ok(())
}
fn field_contract(field: &ast::Field) -> Result<(), Diagnostic> {
if field.private
|| field.unique
|| field.index
|| !field.bounds.is_empty()
|| !field.rest.is_empty()
|| !field.persistence.is_empty()
{
Err(Diagnostic::new(
"C014",
"SQL parameters and outputs cannot have field constraints or storage modifiers",
field.span,
))
} else {
Ok(())
}
}
fn column(
kind: &Type,
source: &ast::TypeRef,
declarations: &[ast::TypeDeclaration],
) -> Result<Column, Diagnostic> {
let kind = if let Type::Optional(inner) = kind {
&**inner
} else {
kind
};
let choice = if let Type::Named(index) = kind {
match &declarations[*index].body {
ast::TypeBody::Choice(variants)
if variants
.iter()
.all(|value| !value.error && value.fields.is_empty()) =>
{
Some(
variants
.iter()
.map(|value| {
(
value.name.clone(),
value.label.clone().unwrap_or_else(|| value.name.clone()),
)
})
.collect(),
)
}
_ => None,
}
} else {
None
};
if choice.is_none()
&& !matches!(
kind,
Type::Bool
| Type::Int
| Type::Float
| Type::Decimal
| Type::Text
| Type::Bytes
| Type::Uuid
| Type::DateTime
| Type::Date
| Type::Time
| Type::Duration
| Type::Json
| Type::ModelId(_)
)
{
return Err(Diagnostic::new(
"C014",
"SQL requires supported storage scalars",
source.span,
));
}
let decimal = if *kind == Type::Decimal {
match source.storage.as_slice() {
[] => None,
[precision, scale] if (1..=34).contains(precision) && scale <= precision => {
Some((*precision, *scale))
}
_ => {
return Err(Diagnostic::new(
"C014",
"invalid SQL Decimal precision or scale",
source.span,
));
}
}
} else if !source.storage.is_empty() {
return Err(Diagnostic::new(
"C014",
"SQL scalar parameters and result fields cannot have storage bounds",
source.span,
));
} else {
None
};
Ok(Column {
text: persistence::TextMode::Raw,
limits: None,
decimal,
generated: false,
default: None,
previous: None,
choice,
unique: false,
index: false,
})
}
impl crate::compiler::check::Context<'_> {
pub(in crate::compiler::check::database) fn native_sql(
&mut self,
model: usize,
name: &str,
arguments: &[ast::Expression],
expected: Option<&Type>,
source: &ast::Expression,
) -> Result<Expression, Diagnostic> {
let index = self.models[model]
.sql
.iter()
.position(|sql| sql.name == name)
.ok_or_else(|| self.error(source, "unknown Model operation"))?;
let sql = &self.models[model].sql[index];
let inputs = sql.inputs.clone();
let output = sql.output.clone();
let readonly = sql.readonly;
let dependencies = sql.dependencies.clone();
if inputs.len() != arguments.len() {
return Err(self.error(source, "SQL argument count differs from its declaration"));
}
let values = arguments
.iter()
.zip(&inputs)
.map(|(argument, kind)| self.expression(argument, Some(kind)))
.collect::<Result<Vec<_>, _>>()?;
let error = self
.definition_names
.resolve("dever.database.Error", self.source, source.span)?
.ok_or_else(|| self.error(source, "database standard error is missing"))?;
self.coerce(
Expression {
kind: Node::Database(
Query {
dependencies,
model,
operation: Operation::Sql { index, readonly },
fields: vec![],
write: Write::None,
error,
filter: String::new(),
postgres_filter: String::new(),
order: String::new(),
order_fields: vec![],
relations: vec![],
},
values,
),
value_type: output,
span: source.span,
},
expected,
)
}
}
@@ -0,0 +1,343 @@
//! 仅证明有限的自有表投影只读;无法证明的 SQL 保留写效果,不凭 SELECT 前缀放行 GET。
use crate::{
compiler::model::persistence::Model,
syntax::{diagnostic::Diagnostic, source::Span},
};
use std::collections::BTreeSet;
struct Token {
text: String,
quoted: bool,
}
impl Token {
fn plain(text: impl Into<String>) -> Self {
Self {
text: text.into(),
quoted: false,
}
}
fn keyword(&self) -> &str {
if self.quoted { "" } else { &self.text }
}
}
pub(super) fn check(
sql: &str,
postgres: bool,
parameters: usize,
model: &Model,
span: Span,
tables: &[String],
) -> Result<(bool, Vec<usize>), Diagnostic> {
let invalid = || {
Diagnostic::new(
"C014",
"SQL requires one parameterized SELECT/WITH statement without internal tables",
span,
)
};
let bytes = sql.as_bytes();
let mut cursor = 0;
let mut tokens = Vec::new();
let mut bindings = BTreeSet::new();
let mut ended = false;
while cursor < bytes.len() {
let byte = bytes[cursor];
if byte.is_ascii_whitespace() {
cursor += 1;
continue;
}
if bytes[cursor..].starts_with(b"--") {
cursor += 2;
while cursor < bytes.len() && bytes[cursor] != b'\n' {
cursor += 1
}
continue;
}
if bytes[cursor..].starts_with(b"/*") {
cursor += 2;
let mut depth = 1;
while cursor < bytes.len() && depth > 0 {
if postgres && bytes[cursor..].starts_with(b"/*") {
depth += 1;
cursor += 2
} else if bytes[cursor..].starts_with(b"*/") {
depth -= 1;
cursor += 2
} else {
cursor += 1
}
}
if depth != 0 {
return Err(invalid());
}
continue;
}
if ended {
return Err(invalid());
}
if byte == b';' {
ended = true;
cursor += 1;
continue;
}
if matches!(byte, b'\'' | b'"' | b'`' | b'[') {
let close = if byte == b'[' { b']' } else { byte };
cursor += 1;
let start = cursor;
while cursor < bytes.len() {
if bytes[cursor] == close {
if byte != b'[' && bytes.get(cursor + 1) == Some(&close) {
cursor += 2;
continue;
}
break;
}
cursor += 1;
}
if cursor == bytes.len() {
return Err(invalid());
}
let word = &sql[start..cursor];
if byte != b'\'' && reserved(word) {
return Err(invalid());
}
tokens.push(Token {
text: if byte == b'\'' {
"<literal>".into()
} else {
word.to_ascii_lowercase()
},
quoted: byte != b'\'',
});
cursor += 1;
continue;
}
if matches!(byte, b'?' | b'$') {
if byte != if postgres { b'$' } else { b'?' } {
return Err(invalid());
}
cursor += 1;
let start = cursor;
while cursor < bytes.len() && bytes[cursor].is_ascii_digit() {
cursor += 1
}
let index = sql[start..cursor].parse::<usize>().map_err(|_| invalid())?;
if index == 0 || index > parameters {
return Err(invalid());
}
bindings.insert(index);
tokens.push(Token::plain("<parameter>"));
continue;
}
if byte.is_ascii_alphabetic() || byte == b'_' {
let start = cursor;
cursor += 1;
while cursor < bytes.len()
&& (bytes[cursor].is_ascii_alphanumeric() || bytes[cursor] == b'_')
{
cursor += 1
}
let word = sql[start..cursor].to_ascii_lowercase();
if reserved(&word) || (word == "u" && bytes.get(cursor) == Some(&b'&')) {
return Err(invalid());
}
tokens.push(Token::plain(word));
continue;
}
if byte == b'@'
|| (byte == b':'
&& bytes.get(cursor + 1).is_some_and(u8::is_ascii_alphabetic)
&& (cursor == 0 || bytes[cursor - 1] != b':'))
{
return Err(invalid());
}
tokens.push(Token::plain(char::from(byte).to_string()));
cursor += 1;
}
if !matches!(tokens.first().map(Token::keyword), Some("select" | "with"))
|| bindings.len() != parameters
{
return Err(invalid());
}
let ctes = tokens
.windows(3)
.filter(|words| words[1].keyword() == "as" && words[2].keyword() == "(")
.map(|words| words[0].text.as_str())
.collect::<BTreeSet<_>>();
table_references(&tokens, &ctes, tables, span)?;
let dependencies = tables
.iter()
.enumerate()
.filter_map(|(index, name)| {
tokens
.iter()
.any(|token| token.text == *name)
.then_some(index)
})
.collect();
Ok((readonly(&tokens, model), dependencies))
}
// 每层 SELECT 分别跟踪 FROM;逗号 JOIN、嵌套查询也必须提供完整静态依赖。
fn table_references(
tokens: &[Token],
ctes: &BTreeSet<&str>,
tables: &[String],
span: Span,
) -> Result<(), Diagnostic> {
let invalid = || {
Diagnostic::new(
"C014",
"SQL table must reference a declared Model or local CTE without schema qualification",
span,
)
};
let mut sources = vec![false];
for (position, token) in tokens.iter().enumerate() {
let source = sources.last_mut().unwrap();
let table = matches!(token.keyword(), "from" | "join" | "into" | "update")
|| (*source && token.keyword() == ",");
match token.keyword() {
"from" | "join" => *source = true,
"where" | "group" | "having" | "order" | "limit" | "offset" | "union" | "except"
| "intersect" | "returning" => *source = false,
"(" => sources.push(false),
")" => {
if sources.len() == 1 {
return Err(invalid());
}
sources.pop();
}
_ => (),
}
if table {
let name = tokens.get(position + 1).ok_or_else(invalid)?;
if name.keyword() == "(" {
continue;
}
if (!ctes.contains(name.text.as_str()) && !tables.contains(&name.text))
|| tokens
.get(position + 2)
.is_some_and(|token| matches!(token.keyword(), "." | "("))
{
return Err(invalid());
}
}
}
if sources.len() != 1 {
return Err(invalid());
}
Ok(())
}
fn reserved(name: &str) -> bool {
let name = name.to_ascii_lowercase();
name.starts_with("_dever_")
|| name.starts_with("sqlite_")
|| matches!(
name.as_str(),
"pg_catalog" | "pg_toast" | "information_schema"
)
}
fn readonly(tokens: &[Token], model: &Model) -> bool {
let mut cursor = 0;
let eat = |cursor: &mut usize, expected: &str| {
if tokens
.get(*cursor)
.is_some_and(|value| value.keyword() == expected)
{
*cursor += 1;
true
} else {
false
}
};
let field = |cursor: &mut usize| {
if tokens
.get(*cursor)
.is_some_and(|value| model.fields.iter().any(|(name, _)| *name == value.text))
{
*cursor += 1;
true
} else {
false
}
};
if !eat(&mut cursor, "select") {
return false;
}
loop {
// 字段别名、COUNT(*) 和参数化分页不增加 SQL 副作用;不放行任意函数。
let projection = field(&mut cursor)
|| (eat(&mut cursor, "count")
&& eat(&mut cursor, "(")
&& eat(&mut cursor, "*")
&& eat(&mut cursor, ")"));
if !projection {
return false;
}
if eat(&mut cursor, "as") {
let Some(alias) = tokens.get(cursor) else {
return false;
};
if !alias.quoted
&& !alias.text.bytes().enumerate().all(|(index, byte)| {
byte == b'_'
|| byte.is_ascii_alphabetic()
|| (index > 0 && byte.is_ascii_digit())
})
{
return false;
}
cursor += 1;
}
if !eat(&mut cursor, ",") {
break;
}
}
if !eat(&mut cursor, "from")
|| tokens
.get(cursor)
.is_none_or(|token| token.text != model.table)
{
return false;
}
cursor += 1;
if eat(&mut cursor, "where") {
loop {
if !field(&mut cursor) || !eat(&mut cursor, "=") || !eat(&mut cursor, "<parameter>") {
return false;
}
if !eat(&mut cursor, "and") {
break;
}
}
}
if eat(&mut cursor, "order") {
if !eat(&mut cursor, "by") {
return false;
}
loop {
if !field(&mut cursor) {
return false;
}
if !eat(&mut cursor, "asc") {
eat(&mut cursor, "desc");
}
if !eat(&mut cursor, ",") {
break;
}
}
}
if eat(&mut cursor, "limit") && !eat(&mut cursor, "<parameter>") {
return false;
}
if eat(&mut cursor, "offset") && !eat(&mut cursor, "<parameter>") {
return false;
}
cursor == tokens.len()
}
@@ -0,0 +1,69 @@
//! Model 选项在编译期展开为有序 Map,不查询数据库或动态检查变体。
use crate::{
compiler::{check::Context, model::*},
syntax::{ast, diagnostic::Diagnostic},
};
impl Context<'_> {
pub(in crate::compiler::check) fn model_options(
&self,
source: &ast::Expression,
) -> Result<Option<Expression>, Diagnostic> {
let Some(path) = source.path() else {
return Ok(None);
};
let Some(name) = path.strip_suffix(".options") else {
return Ok(None);
};
let Some(index) = self
.definition_names
.resolve(name, self.source, source.span)?
else {
return Ok(None);
};
let definition = &self.definitions[index];
if !matches!(
package::role(&self.definition_names.packages[definition.source]),
Some((_, "model"))
) {
return Ok(None);
}
let Shape::Choice(variants) = &definition.shape else {
return Ok(None);
};
if definition.related {
return Ok(None);
}
let text = |value: String| Expression {
kind: Node::Literal(ast::Literal::Text(value)),
value_type: Type::Text,
span: source.span,
};
Ok(Some(Expression {
kind: Node::Map(
variants
.iter()
.enumerate()
.map(|(position, variant)| {
(
Expression {
kind: Node::Variant(index, position, vec![]),
value_type: Type::Named(index),
span: source.span,
},
text(
variant
.label
.clone()
.unwrap_or_else(|| variant.name.clone()),
),
)
})
.collect(),
),
value_type: Type::Map(Box::new(Type::Named(index)), Box::new(Type::Text)),
span: source.span,
}))
}
}
+107
View File
@@ -0,0 +1,107 @@
//! 只把私有 Model 数据标为敏感;普通 record 的 private 字段仍是合法封装。
//! 反向类型边传播一次,Related 环不会导致递归或漏检,运行程序不增加检查。
use crate::{
compiler::model::{Definition, Shape, Signature, Type, persistence::Model},
syntax::{diagnostic::Diagnostic, source::Span},
};
pub(in crate::compiler::check) struct Privacy(Vec<bool>);
impl Privacy {
pub fn new(definitions: &[Definition], models: &[Model]) -> Self {
let mut parents = vec![Vec::new(); definitions.len()];
for (index, definition) in definitions.iter().enumerate() {
let mut field = |kind: &Type| visit(kind, &mut |child| parents[child].push(index));
match &definition.shape {
Shape::Record(fields) => {
for (_, kind) in fields {
field(kind);
}
}
Shape::Choice(variants) => {
for variant in variants {
for (_, kind) in &variant.fields {
field(kind);
}
}
}
}
}
let mut private = vec![false; definitions.len()];
let mut pending = Vec::new();
for model in models {
if !definitions[model.definition].private_fields.is_empty() {
private[model.definition] = true;
pending.push(model.definition);
}
}
while let Some(index) = pending.pop() {
for &parent in &parents[index] {
if !private[parent] {
private[parent] = true;
pending.push(parent);
}
}
}
Self(private)
}
pub fn signature(
&self,
signature: &Signature,
definitions: &[Definition],
span: Span,
) -> Result<(), Diagnostic> {
for (_, kind) in signature.inputs.iter().chain(&signature.outputs) {
self.check(kind, span)?;
}
if signature
.outputs
.iter()
.any(|(_, kind)| kind.contains_secret(definitions))
{
return Err(Diagnostic::new(
"C006",
"App outputs cannot contain Secret",
span,
));
}
Ok(())
}
pub fn check(&self, kind: &Type, span: Span) -> Result<(), Diagnostic> {
let mut private = false;
visit(kind, &mut |index| private |= self.0[index]);
if private {
Err(Diagnostic::new(
"C006",
"private Model snapshots cannot cross App contracts; expose a View",
span,
))
} else {
Ok(())
}
}
}
fn visit(kind: &Type, named: &mut impl FnMut(usize)) {
match kind {
Type::Named(index) => named(*index),
Type::List(inner)
| Type::Optional(inner)
| Type::Stream(inner)
| Type::AsyncStream(inner)
| Type::Channel(inner) => visit(inner, named),
Type::Map(key, value) | Type::MapEntry(key, value) => {
visit(key, named);
visit(value, named);
}
Type::Outputs(fields) => {
for (_, kind) in fields {
visit(kind, named);
}
}
_ => {}
}
}
+192
View File
@@ -0,0 +1,192 @@
use super::*;
use crate::compiler::model::persistence::{Operation, Query, Write};
impl super::super::Context<'_> {
pub(in crate::compiler::check) fn database(
&mut self,
name: &str,
arguments: &[ast::Expression],
expected: Option<&Type>,
source: &ast::Expression,
) -> Result<Option<Expression>, Diagnostic> {
let Some((prefix, operation)) = name.rsplit_once('.') else {
return Ok(None);
};
if prefix != "model" && !prefix.starts_with("model.") {
return Ok(None);
}
if !matches!(
package::role(&self.definition_names.packages[self.source]),
Some((_, "app"))
) {
return Err(Diagnostic::new(
"C006",
"Model operations belong to their owning App",
source.span,
));
}
let definition = self
.definition_names
.resolve(&format!("{prefix}.id"), self.source, source.span)?
.ok_or_else(|| self.error(source, "unknown Model"))?;
let model = self
.models
.iter()
.position(|model| model.definition == definition)
.unwrap();
let operation = match Operation::named(operation) {
Some(operation) => operation,
None => {
return self
.native_sql(model, operation, arguments, expected, source)
.map(Some);
}
};
let mut values = Vec::new();
let mut selected = Vec::new();
let mut filter = String::new();
let mut postgres_filter = String::new();
let mut order = String::from("\"id\" DESC");
let mut order_fields = vec![(0, false)];
let mut write = Write::None;
let mut relations = Vec::new();
let conditional = matches!(operation, Operation::Update | Operation::Delete)
&& arguments
.first()
.is_some_and(|value| matches!(value.kind, ast::ExpressionKind::Map(_)));
let output = if conditional {
let expected = if operation == Operation::Update { 2 } else { 1 };
if arguments.len() != expected {
return Err(self.error(
source,
"conditional write requires a query and update fields when updating",
));
}
let checked = self.model_filter(&arguments[..1], model, operation, source)?;
if checked.condition.is_empty() {
return Err(self.error(source, "conditional write requires where"));
}
filter = checked.condition;
postgres_filter = checked.postgres_condition;
values = checked.values;
selected = checked.fields;
write = Write::Conditional {
filter_count: values.len(),
};
if operation == Operation::Update {
let mut updated = Vec::new();
self.model_input(&arguments[1], model, &mut updated, &mut values)?;
if updated.is_empty() {
return Err(self.error(&arguments[1], "update requires at least one field"));
}
selected.extend(updated);
}
Type::Int
} else {
match operation {
Operation::Sql { .. } => unreachable!("SQL is checked separately"),
Operation::Create | Operation::Update => {
let block = if operation == Operation::Update {
let [id, block] = arguments else {
return Err(self.error(source, "update requires id and field block"));
};
values.push(self.expression(id, Some(&Type::ModelId(definition)))?);
block
} else {
let [block] = arguments else {
return Err(self.error(source, "create requires one field block"));
};
block
};
self.model_input(block, model, &mut selected, &mut values)?;
if operation == Operation::Create {
self.complete_model_input(block, model, &mut selected, &mut values)?;
} else if selected.is_empty() {
return Err(self.error(block, "update requires at least one field"));
}
Type::Named(definition)
}
Operation::CreateMany | Operation::Upsert => {
write = self.model_write(
arguments,
model,
operation,
source,
&mut selected,
&mut values,
)?;
if operation == Operation::CreateMany {
Type::Int
} else {
Type::Named(definition)
}
}
Operation::Get | Operation::Delete => {
let [id] = arguments else {
return Err(self.error(source, "Model operation requires one nominal id"));
};
values.push(self.expression(id, Some(&Type::ModelId(definition)))?);
if operation == Operation::Get {
Type::Named(definition)
} else {
Type::Int
}
}
Operation::Count
| Operation::Exists
| Operation::First
| Operation::List
| Operation::Stream
| Operation::Cursor => {
let checked = self.model_filter(arguments, model, operation, source)?;
filter = checked.condition;
postgres_filter = checked.postgres_condition;
order = checked.order;
order_fields = checked.order_fields;
selected = checked.fields;
values = checked.values;
relations = checked.relations;
match operation {
Operation::Count => Type::Int,
Operation::Exists => Type::Bool,
Operation::First => Type::Optional(Box::new(Type::Named(definition))),
Operation::Cursor => Type::Named(self.models[model].cursor),
Operation::Stream => Type::AsyncStream(Box::new(Type::Named(definition))),
_ => Type::Named(self.models[model].page),
}
}
}
};
let error = self
.definition_names
.resolve("dever.database.Error", self.source, source.span)?
.ok_or_else(|| self.error(source, "database standard error is missing"))?;
self.coerce(
Expression {
kind: Node::Database(
Query {
dependencies: relations
.iter()
.map(|index| self.models[model].relations[*index].model)
.collect(),
model,
operation,
fields: selected,
write,
error,
filter,
postgres_filter,
order,
order_fields,
relations,
},
values,
),
value_type: output,
span: source.span,
},
expected,
)
.map(Some)
}
}
@@ -0,0 +1,228 @@
//! Related 是编译器专用的两态具体 choice;只装箱载荷以允许自关联,不改变 Model 标量存储。
use super::*;
use crate::compiler::model::persistence::Relation;
pub(super) fn prepare(declarations: &mut Vec<ast::TypeDeclaration>, names: &mut Types) {
let models = declarations
.iter()
.enumerate()
.filter(|(_, declaration)| {
matches!(declaration.body, ast::TypeBody::Record(_))
&& package::model_record(&names.packages[declaration.span.source]).as_deref()
== Some(declaration.name.as_str())
})
.map(|(index, declaration)| (index, declaration.clone()))
.collect::<Vec<_>>();
for (model, declaration) in models {
for container in ["", "?", "List"] {
let mut value = ast::TypeRef {
name: declaration.name.clone(),
arguments: vec![],
storage: vec![],
nullable: container == "?",
span: declaration.span,
};
let mut kind = Type::Named(model);
if container == "?" {
kind = Type::Optional(Box::new(kind));
}
if container == "List" {
kind = Type::List(Box::new(kind));
value = ast::TypeRef {
name: "List".into(),
arguments: vec![value],
storage: vec![],
nullable: false,
span: declaration.span,
};
}
let index = declarations.len();
names.related.insert(kind, index);
let name = format!("Related{index}");
names.insert(
declaration.span.source,
format!("Related<{index}>"),
index,
true,
);
declarations.push(ast::TypeDeclaration {
name,
global: false,
setting: false,
public: true,
span: declaration.span,
body: ast::TypeBody::Choice(vec![
ast::Variant {
name: "Unloaded".into(),
label: None,
fields: vec![],
error: false,
span: declaration.span,
},
ast::Variant {
name: "Loaded".into(),
label: None,
fields: vec![ast::Field {
name: "value".into(),
kind: value,
bounds: vec![],
private: false,
unique: false,
index: false,
rest: Default::default(),
persistence: Default::default(),
span: declaration.span,
}],
error: false,
span: declaration.span,
},
]),
});
}
}
}
pub(in crate::compiler::check) fn check(
models: &mut [Model],
definitions: &mut [Definition],
declarations: &[ast::Persistence],
names: &Types,
) -> Result<(), Diagnostic> {
for index in 0..models.len() {
let mut relations = Vec::new();
let span = crate::syntax::source::Span {
source: models[index].source,
start: 0,
end: 0,
};
for (field, (name, kind)) in models[index].fields.iter().enumerate().skip(2) {
let (kind, optional) = match kind {
Type::Optional(inner) => (&**inner, true),
kind => (kind, false),
};
let Type::ModelId(target) = kind else {
continue;
};
let Some(name) = name.strip_suffix("_id") else {
continue;
};
let target = models
.iter()
.position(|model| model.definition == *target)
.ok_or_else(|| {
Diagnostic::new("C014", "relation target must be a persistent Model", span)
})?;
let payload = if optional {
Type::Optional(Box::new(Type::Named(models[target].definition)))
} else {
Type::Named(models[target].definition)
};
relations.push(Relation {
name: name.into(),
model: target,
field,
many: false,
optional,
definition: names.related[&payload],
});
}
for declaration in declarations
.iter()
.filter(|declaration| declaration.span.source == models[index].source)
{
let ast::PersistenceKind::Relation { name, target } = &declaration.kind else {
continue;
};
super::super::names::identifier(name, false, declaration.span)?;
let (package, field) = target.rsplit_once('.').ok_or_else(|| {
Diagnostic::new(
"C014",
"relation requires a Model foreign-key field",
declaration.span,
)
})?;
let definition = names
.resolve(
&format!("{package}.id"),
models[index].source,
declaration.span,
)?
.ok_or_else(|| {
Diagnostic::new("C014", "unknown relation Model", declaration.span)
})?;
let target = models
.iter()
.position(|model| model.definition == definition)
.ok_or_else(|| {
Diagnostic::new(
"C014",
"relation target must be a persistent Model",
declaration.span,
)
})?;
let field = models[target]
.fields
.iter()
.position(|(name, _)| name == field)
.ok_or_else(|| {
Diagnostic::new("C014", "unknown relation field", declaration.span)
})?;
let kind = &models[target].fields[field].1;
let kind = if let Type::Optional(inner) = kind {
&**inner
} else {
kind
};
if *kind != Type::ModelId(models[index].definition) || field < 2 {
return Err(Diagnostic::new(
"C014",
"reverse relation must reference this Model id",
declaration.span,
));
}
relations.push(Relation {
name: name.clone(),
model: target,
field,
many: true,
optional: false,
definition: names.related[&Type::List(Box::new(Type::Named(definition)))],
});
}
let mut seen = models[index]
.fields
.iter()
.map(|(name, _)| name.clone())
.collect::<std::collections::HashSet<_>>();
for relation in &relations {
if !seen.insert(relation.name.clone()) {
return Err(Diagnostic::new(
"C014",
"relation name collides with a Model field or relation",
span,
));
}
if let (Some(left), Some(right)) =
(&models[index].database, &models[relation.model].database)
&& left != right
{
return Err(Diagnostic::new(
"C014",
"relation cannot cross explicit database bindings",
span,
));
}
}
let definition = &mut definitions[models[index].definition];
let Shape::Record(fields) = &mut definition.shape else {
unreachable!()
};
for relation in &relations {
fields.push((relation.name.clone(), Type::Named(relation.definition)));
definition.ranges.push(None);
}
models[index].relations = relations;
}
Ok(())
}
@@ -0,0 +1,103 @@
//! 命名迁移按源码顺序保留操作;SQL 与参数共同进入不可变历史签名。
use super::*;
pub(super) fn check(
source: &ast::Migration,
span: crate::syntax::source::Span,
model: &Model,
) -> Result<persistence::Migration, Diagnostic> {
let invalid = |message| Diagnostic::new("C014", message, span);
crate::compiler::check::names::identifier(&source.name, false, span)?;
let mut result = persistence::Migration {
name: source.name.clone(),
drops: vec![],
before: vec![],
after: vec![],
};
for step in &source.steps {
match step {
ast::MigrationStep::Drop(name) => {
crate::compiler::check::names::identifier(name, false, span)?;
if matches!(name.as_str(), "id" | "created_at")
|| model.fields.iter().any(|(field, _)| field == name)
|| result.drops.contains(name)
|| model
.columns
.iter()
.any(|column| column.previous.as_ref() == Some(name))
{
return Err(invalid(
"migration drop conflicts with target fields or rename",
));
}
result.drops.push(name.clone());
}
ast::MigrationStep::Data {
before,
sqlite,
postgres,
parameters,
} => {
let parameters = parameters
.iter()
.map(|value| parameter(value, span))
.collect::<Result<Vec<_>, _>>()?;
super::sql::check(sqlite, false, parameters.len(), span)?;
super::sql::check(postgres, true, parameters.len(), span)?;
let statement = persistence::Statement {
sqlite: sqlite.clone(),
postgres: postgres.clone(),
parameters,
};
if *before {
result.before.push(statement);
} else {
result.after.push(statement);
}
}
}
}
if source.steps.is_empty() {
return Err(invalid("migration cannot be empty"));
}
Ok(result)
}
fn parameter(
source: &ast::Expression,
span: crate::syntax::source::Span,
) -> Result<persistence::Value, Diagnostic> {
let invalid = || {
Diagnostic::new(
"C014",
"migration parameters require Bool, Int, Text, null or exponent Float literals",
span,
)
};
let literal = match &source.kind {
ast::ExpressionKind::Literal(value) => value.clone(),
ast::ExpressionKind::Unary(operator, value) if operator == "-" => match &value.kind {
ast::ExpressionKind::Literal(ast::Literal::Number(number)) => {
ast::Literal::Number(format!("-{number}"))
}
_ => return Err(invalid()),
},
_ => return Err(invalid()),
};
Ok(match literal {
ast::Literal::Null => persistence::Value::Null,
ast::Literal::Bool(value) => persistence::Value::Integer(i64::from(value)),
ast::Literal::Text(value) => persistence::Value::Text(value),
ast::Literal::Number(value) if value.contains(['e', 'E']) => {
let value = value.parse::<f64>().map_err(|_| invalid())?;
if !value.is_finite() {
return Err(invalid());
}
persistence::Value::Real(value)
}
ast::Literal::Number(value) => {
persistence::Value::Integer(value.parse().map_err(|_| invalid())?)
}
})
}
@@ -0,0 +1,116 @@
//! Model 的 schema 计划完全静态;重命名、索引和唯一键在访问存储前确定。
mod migration;
mod seed;
mod sql;
use super::*;
pub(super) fn check(
models: &mut [Model],
sources: &[ast::TypeDeclaration],
declarations: &[ast::Persistence],
names: &Types,
) -> Result<(), Diagnostic> {
for model in models.iter_mut() {
let ast::TypeBody::Record(fields) = &sources[model.definition].body else {
unreachable!()
};
let span = sources[model.definition].span;
let invalid = |message| Diagnostic::new("C014", message, span);
let mut previous = std::collections::HashSet::new();
for column in &model.columns {
if let Some(name) = &column.previous {
crate::compiler::check::names::identifier(name, false, span)?;
if matches!(name.as_str(), "id" | "created_at")
|| model.fields.iter().any(|(field, _)| field == name)
|| !previous.insert(name)
{
return Err(invalid("ambiguous Model field rename"));
}
}
}
let source = declarations
.iter()
.filter(|declaration| declaration.span.source == model.source)
.collect::<Vec<_>>();
for declaration in &source {
if let ast::PersistenceKind::Index { unique, fields } = &declaration.kind {
let mut positions = Vec::new();
for name in fields {
let position = model
.fields
.iter()
.position(|(field, _)| field == name)
.ok_or_else(|| invalid("unknown index field"))?;
if positions.contains(&position) {
return Err(invalid("duplicate index field"));
}
positions.push(position);
}
if model
.indexes
.iter()
.any(|index| index.fields == positions && index.unique == *unique)
{
return Err(invalid("duplicate Model index"));
}
model.indexes.push(persistence::Index {
fields: positions,
unique: *unique,
});
}
}
// owner 与 search 的组合在 schema 中建立普通索引,保持请求筛选可用索引。
if let (Some(owner), Some(search)) = (
fields.iter().position(|field| field.rest.owner.is_some()),
fields.iter().position(|field| field.rest.search.is_some()),
) {
let positions = vec![owner, search];
if !model.indexes.iter().any(|index| index.fields == positions) {
model.indexes.push(persistence::Index {
fields: positions,
unique: false,
});
}
}
let mut seeded = false;
for declaration in source {
match &declaration.kind {
ast::PersistenceKind::Index { .. } | ast::PersistenceKind::Sql(_) => (),
ast::PersistenceKind::Relation { .. } => (),
ast::PersistenceKind::Seed(rows) => {
if seeded {
return Err(invalid("only one Seed block is allowed per Model"));
}
seeded = true;
model.seeds = seed::check(rows, model, names)?;
}
ast::PersistenceKind::Migration(value) => {
if model
.migrations
.iter()
.any(|migration| migration.name == value.name)
{
return Err(invalid("duplicate migration name"));
}
model
.migrations
.push(migration::check(value, declaration.span, model)?);
}
}
}
}
for declaration in declarations {
if !models
.iter()
.any(|model| model.source == declaration.span.source)
{
return Err(Diagnostic::new(
"C014",
"storage declaration requires a Model record",
declaration.span,
));
}
}
Ok(())
}
@@ -0,0 +1,74 @@
//! Seed 只接受静态字段值,并选择完整非空唯一键判断记录是否已存在。
use super::*;
pub(super) fn check(
rows: &[ast::Expression],
model: &Model,
names: &Types,
) -> Result<Vec<persistence::Seed>, Diagnostic> {
rows.iter()
.map(|row| {
let invalid = |message| Diagnostic::new("C014", message, row.span);
let ast::ExpressionKind::Map(entries) = &row.kind else {
return Err(invalid("Seed row requires a direct field block"));
};
let mut values = Vec::new();
for (key, value) in entries {
let ast::ExpressionKind::Name(name) = &key.kind else {
return Err(invalid("Seed fields must be statically named"));
};
let Some(position) = model.fields.iter().position(|(field, _)| field == name)
else {
return Err(invalid("unknown Seed field"));
};
if position < 2 || model.columns[position].generated {
return Err(invalid("Seed cannot assign generated fields"));
}
if values.iter().any(|(field, _)| *field == position) {
return Err(invalid("duplicate Seed field"));
}
values.push((
position,
super::super::constant::check(
value,
&model.fields[position].1,
&model.columns[position],
names,
)?,
));
}
for (position, (_, kind)) in model.fields.iter().enumerate().skip(2) {
if !values.iter().any(|(field, _)| *field == position)
&& !model.columns[position].generated
&& model.columns[position].default.is_none()
&& !matches!(kind, Type::Optional(_))
{
return Err(invalid("Seed omits a required field"));
}
}
let key = model
.columns
.iter()
.enumerate()
.filter(|(_, column)| column.unique)
.map(|(position, _)| vec![position])
.chain(
model
.indexes
.iter()
.filter(|index| index.unique)
.map(|index| index.fields.clone()),
)
.find(|fields| {
fields.iter().all(|field| {
values.iter().any(|(position, value)| {
position == field && !matches!(value, persistence::Value::Null)
})
})
})
.ok_or_else(|| invalid("Seed requires one complete non-null unique key"))?;
Ok(persistence::Seed { values, key })
})
.collect()
}
@@ -0,0 +1,196 @@
//! 迁移 SQL 只允许一条参数化 DML;引号和注释中的分隔符不改变语句边界。
use crate::syntax::{diagnostic::Diagnostic, source::Span};
use std::collections::BTreeSet;
pub(super) fn check(
sql: &str,
postgres: bool,
parameters: usize,
span: Span,
) -> Result<(), Diagnostic> {
let invalid = || {
Diagnostic::new(
"C014",
"migration requires one INSERT/UPDATE/DELETE with complete numbered bindings and no internal tables",
span,
)
};
let bytes = sql.as_bytes();
let mut cursor = 0;
let mut first = true;
let mut ended = false;
let mut target = false;
let mut bindings = BTreeSet::new();
while cursor < bytes.len() {
let byte = bytes[cursor];
if byte.is_ascii_whitespace() {
cursor += 1;
continue;
}
if bytes[cursor..].starts_with(b"--") {
cursor += 2;
while cursor < bytes.len() && bytes[cursor] != b'\n' {
cursor += 1;
}
continue;
}
if bytes[cursor..].starts_with(b"/*") {
cursor += 2;
let mut depth = 1;
while cursor < bytes.len() && depth > 0 {
if postgres && bytes[cursor..].starts_with(b"/*") {
depth += 1;
cursor += 2;
} else if bytes[cursor..].starts_with(b"*/") {
depth -= 1;
cursor += 2;
} else {
cursor += 1;
}
}
if depth != 0 {
return Err(invalid());
}
continue;
}
if ended {
return Err(invalid());
}
if byte == b';' {
if first {
return Err(invalid());
}
ended = true;
cursor += 1;
continue;
}
if matches!(byte, b'\'' | b'"' | b'`' | b'[') {
if first {
return Err(invalid());
}
let quote = if byte == b'[' { b']' } else { byte };
cursor += 1;
let start = cursor;
let mut closed = false;
while cursor < bytes.len() {
if bytes[cursor] == quote {
if byte != b'[' && bytes.get(cursor + 1) == Some(&quote) {
cursor += 2;
continue;
}
closed = true;
break;
}
cursor += 1;
}
if !closed || ((byte != b'\'' || target) && reserved(&sql[start..cursor])) {
return Err(invalid());
}
cursor += 1;
target = target
&& bytes[cursor..]
.iter()
.find(|byte| !byte.is_ascii_whitespace())
== Some(&b'.');
continue;
}
if byte == b'$' && postgres && !bytes.get(cursor + 1).is_some_and(u8::is_ascii_digit) {
if first {
return Err(invalid());
}
let start = cursor;
cursor += 1;
while cursor < bytes.len()
&& (bytes[cursor].is_ascii_alphanumeric() || bytes[cursor] == b'_')
{
cursor += 1;
}
if bytes.get(cursor) != Some(&b'$') {
return Err(invalid());
}
cursor += 1;
let delimiter = &sql[start..cursor];
let Some(end) = sql[cursor..].find(delimiter) else {
return Err(invalid());
};
cursor += end + delimiter.len();
continue;
}
if matches!(byte, b'?' | b'$') {
if first || byte != if postgres { b'$' } else { b'?' } {
return Err(invalid());
}
cursor += 1;
let start = cursor;
while cursor < bytes.len() && bytes[cursor].is_ascii_digit() {
cursor += 1;
}
let binding = sql[start..cursor].parse::<usize>().map_err(|_| invalid())?;
if binding == 0 || binding > parameters {
return Err(invalid());
}
bindings.insert(binding);
continue;
}
if byte.is_ascii_alphabetic() || byte == b'_' {
let start = cursor;
cursor += 1;
while cursor < bytes.len()
&& (bytes[cursor].is_ascii_alphanumeric() || bytes[cursor] == b'_')
{
cursor += 1;
}
let word = &sql[start..cursor];
if first {
if !["INSERT", "UPDATE", "DELETE"]
.iter()
.any(|name| word.eq_ignore_ascii_case(name))
{
return Err(invalid());
}
first = false;
}
if reserved(word)
|| (word.eq_ignore_ascii_case("u") && bytes.get(cursor) == Some(&b'&'))
{
return Err(invalid());
}
target = (target
&& bytes[cursor..]
.iter()
.find(|byte| !byte.is_ascii_whitespace())
== Some(&b'.'))
|| ["UPDATE", "INTO", "FROM", "JOIN"]
.iter()
.any(|name| word.eq_ignore_ascii_case(name));
continue;
}
if first
|| matches!(byte, b'@')
|| (byte == b':'
&& bytes.get(cursor + 1).is_some_and(u8::is_ascii_alphabetic)
&& (cursor == 0 || bytes[cursor - 1] != b':'))
{
return Err(invalid());
}
if byte != b'.' {
target = false;
}
cursor += 1;
}
if first || bindings.len() != parameters {
return Err(invalid());
}
Ok(())
}
fn reserved(name: &str) -> bool {
let name = name.to_ascii_lowercase();
name.starts_with("_dever_")
|| name.starts_with("sqlite_")
|| matches!(
name.as_str(),
"pg_catalog" | "pg_toast" | "information_schema"
)
}
+129
View File
@@ -0,0 +1,129 @@
//! 创建、批量创建和 upsert 共用字段检查,不引入可在运行时保存的写入对象。
use super::*;
use crate::compiler::model::persistence::{Operation, Write};
impl super::super::Context<'_> {
pub(super) fn model_input(
&mut self,
block: &ast::Expression,
model: usize,
selected: &mut Vec<usize>,
values: &mut Vec<Expression>,
) -> Result<(), Diagnostic> {
let ast::ExpressionKind::Map(entries) = &block.kind else {
return Err(self.error(block, "Model input must be a direct field block"));
};
for (key, value) in entries {
let ast::ExpressionKind::Name(name) = &key.kind else {
return Err(self.error(key, "Model field must be statically named"));
};
let position = self.models[model]
.fields
.iter()
.position(|(field, _)| field == name)
.filter(|position| *position >= 2)
.ok_or_else(|| self.error(key, "unknown or generated Model field"))?;
if self.models[model].columns[position].generated {
return Err(self.error(key, "generated Model fields are read-only"));
}
if selected.contains(&position) {
return Err(self.error(key, "duplicate Model field"));
}
let kind = self.models[model].fields[position].1.clone();
values.push(self.expression(value, Some(&kind))?);
selected.push(position);
}
Ok(())
}
pub(super) fn complete_model_input(
&self,
block: &ast::Expression,
model: usize,
selected: &mut Vec<usize>,
values: &mut Vec<Expression>,
) -> Result<(), Diagnostic> {
for (position, (_, kind)) in self.models[model].fields.iter().enumerate().skip(2) {
let column = &self.models[model].columns[position];
if selected.contains(&position) || column.generated || column.default.is_some() {
continue;
}
if !matches!(kind, Type::Optional(_)) {
return Err(self.error(block, "missing required Model field"));
}
selected.push(position);
values.push(Expression {
kind: Node::Literal(ast::Literal::Null),
value_type: kind.clone(),
span: block.span,
});
}
Ok(())
}
pub(super) fn model_write(
&mut self,
arguments: &[ast::Expression],
model: usize,
operation: Operation,
source: &ast::Expression,
selected: &mut Vec<usize>,
values: &mut Vec<Expression>,
) -> Result<Write, Diagnostic> {
if operation == Operation::CreateMany {
let [list] = arguments else {
return Err(self.error(
source,
"createMany requires one direct list of field blocks",
));
};
let ast::ExpressionKind::List(rows) = &list.kind else {
return Err(self.error(list, "createMany requires one direct list of field blocks"));
};
let mut ends = Vec::new();
for row in rows {
let mut fields = Vec::new();
self.model_input(row, model, &mut fields, values)?;
self.complete_model_input(row, model, &mut fields, values)?;
selected.extend(fields);
ends.push(values.len());
}
return Ok(Write::Many { ends });
}
let [key, create, update] = arguments else {
return Err(self.error(
source,
"upsert requires unique key, create and update field blocks",
));
};
self.model_input(key, model, selected, values)?;
let metadata = &self.models[model];
let unique = selected.len() == 1 && metadata.columns[selected[0]].unique
|| metadata.indexes.iter().any(|index| {
index.unique
&& index.fields.len() == selected.len()
&& index.fields.iter().all(|field| selected.contains(field))
});
if selected.is_empty() || !unique {
return Err(self.error(key, "upsert key must match one complete unique index"));
}
if values
.iter()
.any(|value| matches!(value.kind, Node::Literal(ast::Literal::Null)))
{
return Err(self.error(key, "upsert key cannot contain null"));
}
let key_count = selected.len();
self.model_input(create, model, selected, values)?;
self.complete_model_input(create, model, selected, values)?;
let insert_count = selected.len();
let mut updated = Vec::new();
self.model_input(update, model, &mut updated, values)?;
selected.extend(updated);
Ok(Write::Upsert {
key_count,
insert_count,
})
}
}
+351
View File
@@ -0,0 +1,351 @@
//! 沿具体调用依赖传播效果,包含 result 和集合内的静态 handler。
//! 未调用模板的原型保留未知 handler 效果;具体纯函数绑定仍可使用。
use crate::compiler::model::*;
use crate::syntax::diagnostic::Diagnostic;
pub(super) fn check(
function: &mut Function,
signatures: &[Signature],
packages: &[String],
) -> Result<(), Diagnostic> {
let mut effects = body(function, signatures, false);
if effects.suspend {
effects = body(function, signatures, true);
}
if function.signature.pure
&& (Effects {
configuration: false,
..effects
}) != Effects::default()
{
return Err(Diagnostic::new(
"C013",
"pure function invokes I/O or an unrestricted handler",
function.clauses[0].span,
));
}
if effects.suspend && (effects.blocking || effects.handler) {
return Err(Diagnostic::new(
"C013",
"suspending function contains inline blocking or unrestricted handler effects; use blocking for a known synchronous boundary",
function.clauses[0].span,
));
}
if function.signature.transaction && (!effects.database || effects.transaction_unsafe()) {
return Err(Diagnostic::new(
"C014",
"transaction requires database work without external or concurrent operations",
function.clauses[0].span,
));
}
if effects.unmediated_io
&& matches!(
package::role(&packages[function.signature.source]),
Some((_, "app" | "domain"))
)
{
return Err(Diagnostic::new(
"C013",
"App and Domain external I/O requires a Port boundary",
function.clauses[0].span,
));
}
if effects.request
&& matches!(
package::role(&packages[function.signature.source]),
Some((_, "domain" | "port" | "adapter"))
)
{
return Err(Diagnostic::new(
"C014",
"request context belongs to App",
function.clauses[0].span,
));
}
if function.signature.transaction {
effects.unscoped_write = false;
}
function.signature.effects = effects;
Ok(())
}
fn body(function: &Function, signatures: &[Signature], suspending: bool) -> Effects {
let mut effects = Effects::default();
for clause in &function.clauses {
for statement in &clause.body {
let value = match statement {
Statement::Assign { value, .. }
| Statement::Action(value)
| Statement::Fail(value) => value,
};
effects.include(expression(value, signatures, suspending));
}
}
effects
}
pub(super) fn expression(
value: &Expression,
signatures: &[Signature],
suspending: bool,
) -> Effects {
let mut effects = Effects::default();
match &value.kind {
Node::Literal(_) | Node::Variable(_) => {}
Node::Assert(values) => {
effects.output = true;
effects.include(arguments(values, signatures, suspending));
}
Node::TestClock(value) => {
effects.timer = true;
effects.include(expression(value, signatures, suspending));
}
Node::TestDrain { limit, handlers } => {
effects.include(expression(limit, signatures, suspending));
if !handlers.is_empty() {
effects.database = true;
effects.schedule = true;
effects.suspend = true;
effects.configuration = handlers
.iter()
.any(|index| signatures[*index].effects.configuration);
}
}
Node::Close(value) => {
if matches!(value.value_type, Type::Channel(_)) {
effects.channel = true;
effects.suspend = true;
} else if matches!(value.value_type, Type::AsyncStream(_)) {
effects.async_stream = true;
} else {
effects.stream = true;
}
effects.include(expression(value, signatures, suspending));
}
Node::Stream(source) | Node::Ticks(source) => {
effects.async_stream = true;
effects.timer = matches!(value.kind, Node::Ticks(_));
effects.include(expression(source, signatures, suspending));
}
Node::Channel(capacity) => {
effects.channel = true;
effects.include(expression(capacity, signatures, suspending));
}
Node::Send(channel, value) => {
effects.channel = true;
effects.suspend = true;
effects.include(expression(channel, signatures, suspending));
effects.include(expression(value, signatures, suspending));
}
Node::Receive(channel) => {
effects.channel = true;
effects.suspend = true;
effects.include(expression(channel, signatures, suspending));
}
Node::Call(index, values) => {
effects.include(signatures[*index].effects);
effects.include(arguments(values, signatures, suspending));
}
Node::Database(query, values) => {
effects.database = true;
effects.async_stream |= query.operation == persistence::Operation::Stream;
effects.database_write = query.operation.writes();
effects.unscoped_write = query.operation.writes();
effects.suspend = true;
effects.schedule = true;
effects.include(arguments(values, signatures, suspending));
}
Node::Auth(call, values) => {
effects.request = true;
effects.identity = call.operation.identity();
effects.authentication = call.operation.package() == "dever.auth";
effects.include(arguments(values, signatures, suspending));
}
Node::Authorization(call, values) => {
effects.request = true;
effects.identity = true;
effects.authentication = true;
effects.authorization = true;
effects.authorization_write = call.operation.writes();
effects.database = true;
effects.database_write = call.operation.writes();
effects.unscoped_write = call.operation.writes();
effects.suspend = true;
effects.schedule = true;
effects.include(arguments(values, signatures, suspending));
}
Node::Enqueue(_, values) => {
effects.enqueue = true;
effects.database = true;
effects.database_write = true;
effects.unscoped_write = true;
effects.suspend = true;
effects.schedule = true;
effects.include(arguments(values, signatures, suspending));
}
Node::Crypto(operation, values) => {
effects.include(operation.effects());
effects.include(arguments(values, signatures, suspending));
}
Node::Calendar(operation, values) => {
effects.clock = operation.clock();
effects.include(arguments(values, signatures, suspending));
}
Node::Net(operation, values) => {
effects.include(operation.effects());
effects.include(arguments(values, signatures, suspending));
}
Node::Http(operation, values) => {
effects.schedule = matches!(
operation,
HttpOperation::Client
| HttpOperation::WsAccept
| HttpOperation::WsConnect
| HttpOperation::WsOpen
);
if let HttpOperation::Serve(handler, _) | HttpOperation::ServeLive(handler, _) =
operation
{
effects.include(signatures[*handler].effects);
effects.handler = false;
effects.schedule = true;
}
effects.network = true;
effects.unmediated_io = true;
effects.suspend = !matches!(operation, HttpOperation::WsMessages);
effects.include(arguments(values, signatures, suspending));
}
Node::Io(operation, values) => {
effects.include(operation.effects());
effects.include(arguments(values, signatures, suspending));
}
Node::Upload(operation, values) => {
effects.include(operation.effects());
effects.include(arguments(values, signatures, suspending));
}
Node::Json(_, values) => effects.include(arguments(values, signatures, suspending)),
Node::Schedule(operation, target, values) => {
effects = signatures[*target].effects;
effects.concurrent |= *operation == Schedule::Run;
effects.blocking = false;
effects.handler = false;
effects.suspend = true;
effects.schedule = true;
effects.include(arguments(values, signatures, suspending));
}
Node::Join { task, .. } => {
effects.include(expression(task, signatures, suspending));
effects.suspend = true;
effects.schedule = true;
}
Node::Group(limit) => {
effects.include(expression(limit, signatures, suspending));
effects.suspend = true;
effects.schedule = true;
}
Node::GroupRun {
group,
target,
arguments: values,
} => {
effects = signatures[*target].effects;
effects.concurrent = true;
effects.blocking = false;
effects.handler = false;
effects.suspend = true;
effects.schedule = true;
effects.include(expression(group, signatures, suspending));
effects.include(arguments(values, signatures, suspending));
}
Node::Timeout {
task,
millis,
fallback,
} => {
effects.include(expression(task, signatures, suspending));
effects.include(expression(millis, signatures, suspending));
effects.include(signatures[*fallback].effects);
effects.suspend = true;
effects.schedule = true;
effects.timer = true;
}
Node::Race(tasks) => {
effects.include(arguments(tasks, signatures, suspending));
effects.suspend = true;
effects.schedule = true;
}
Node::Timer(timer, value) => {
effects.include(expression(value, signatures, suspending));
effects.timer = true;
effects.suspend |= *timer == Timer::Delay;
effects.blocking |= *timer == Timer::Sleep;
}
Node::List(values)
| Node::Variant(_, _, values)
| Node::Access(_, values)
| Node::Standard(_, values)
| Node::Tls(_, values)
| Node::Bytes(_, values) => effects.include(arguments(values, signatures, suspending)),
Node::Record(_, values) => {
for (_, value) in values {
effects.include(expression(value, signatures, suspending));
}
}
Node::Map(values) => {
for (key, value) in values {
effects.include(expression(key, signatures, suspending));
effects.include(expression(value, signatures, suspending));
}
}
Node::Field(value, _)
| Node::TestSecret(value)
| Node::Unary(_, value)
| Node::Some(value)
| Node::Decimal(value)
| Node::Id(value)
| Node::IdText(value)
| Node::Length(value)
| Node::Sum(value) => effects.include(expression(value, signatures, suspending)),
Node::Binary(_, left, right) => {
effects.include(expression(left, signatures, suspending));
effects.include(expression(right, signatures, suspending));
}
Node::Capture(capture) => {
effects.include(expression(&capture.call, signatures, suspending));
}
Node::Collection(collection) => {
let handler = signatures[collection.handler].effects;
effects.include(handler);
if collection.operation == Operation::ParallelEach {
effects.parallel = true;
effects.suspend |=
suspending || matches!(collection.sequence.value_type, Type::AsyncStream(_));
effects.schedule |= effects.suspend;
effects.blocking = !effects.suspend;
} else {
effects.async_stream |=
matches!(collection.sequence.value_type, Type::AsyncStream(_));
effects.suspend |= effects.async_stream;
effects.blocking |= matches!(collection.sequence.value_type, Type::Stream(_));
}
effects.stream |= matches!(collection.sequence.value_type, Type::Stream(_));
effects.include(expression(&collection.sequence, signatures, suspending));
if let Some(limit) = &collection.limit {
effects.include(expression(limit, signatures, suspending));
}
if let Some(extra) = &collection.extra {
effects.include(expression(extra, signatures, suspending));
}
}
}
effects
}
fn arguments(values: &[Expression], signatures: &[Signature], suspending: bool) -> Effects {
let mut effects = Effects::default();
for value in values {
effects.include(expression(value, signatures, suspending));
}
effects
}
+352
View File
@@ -0,0 +1,352 @@
//! 把语法表达式解析为具体类型表达式,保留原始位置和求值顺序。
//! 只允许合同明确规定的转换,例如 Int 到 Decimal 和非空值到可空值。
use super::Context;
use crate::compiler::model::*;
use crate::syntax::{
ast::{self, ExpressionKind as Syntax, Literal},
diagnostic::Diagnostic,
};
impl Context<'_> {
pub(super) fn expression(
&mut self,
source: &ast::Expression,
expected: Option<&Type>,
) -> Result<Expression, Diagnostic> {
if let Some(value) = self.model_options(source)? {
return self.coerce(value, expected);
}
if let Some(path) = source.path()
&& let Some((index, variant)) = super::names::variant(
&path,
self.source,
self.definition_names,
self.definitions,
source.span,
)?
{
return self.variant(index, variant, &[], expected, source);
}
let value = match &source.kind {
Syntax::Literal(literal) => self.literal(literal, expected, source)?,
Syntax::Name(name) => {
let index = self
.variables
.iter()
.position(|(candidate, _)| candidate == name)
.ok_or_else(|| self.error(source, format!("unknown value '{name}'")))?;
if !self.initialized[index] {
return Err(self.error(source, format!("'{name}' is not initialized")));
}
Expression {
kind: Node::Variable(index),
value_type: self.variables[index].1.clone(),
span: source.span,
}
}
Syntax::Field(base, name) => {
let base = self.expression(base, None)?;
let fields = base.value_type.fields(self.definitions).ok_or_else(|| {
self.error(
source,
"field access requires a record, MapEntry or named outputs",
)
})?;
let index = fields
.iter()
.position(|(field, _)| *field == name)
.ok_or_else(|| self.error(source, format!("unknown field '{name}'")))?;
let value_type = fields[index].1.clone();
if let Type::Named(record) = &base.value_type {
let definition = &self.definitions[*record];
if definition.private_fields.contains(&index) {
self.definition_names.private_field(
self.source,
definition.source,
source.span,
)?;
}
}
Expression {
kind: Node::Field(Box::new(base), index),
value_type,
span: source.span,
}
}
Syntax::Channel(element, capacity) => {
return self.channel(element, capacity, expected, source);
}
Syntax::Call(target, arguments) => {
return self.call(target, arguments, expected, source);
}
Syntax::Unary(operator, operand) => {
if operator == "-"
&& let Syntax::Literal(Literal::Number(number)) = &operand.kind
{
return self.expression(
&ast::Expression {
kind: Syntax::Literal(Literal::Number(format!("-{number}"))),
span: source.span,
depth: 1,
},
expected,
);
}
let operand = self.expression(operand, expected)?;
if (operator == "not" && operand.value_type != Type::Bool)
|| (operator == "-" && !operand.value_type.numeric())
{
return Err(self.error(source, "invalid unary operand"));
}
Expression {
value_type: operand.value_type.clone(),
kind: Node::Unary(operator.clone(), Box::new(operand)),
span: source.span,
}
}
Syntax::Binary(operator, left, right) => {
self.binary(operator, left, right, expected, source)?
}
Syntax::List(items) => {
let inner = match present_type(expected) {
Some(Type::List(inner)) => Some(inner.as_ref()),
_ => None,
};
let mut values = Vec::new();
let mut element = inner.cloned();
for item in items {
let value = self.expression(item, element.as_ref())?;
if element.is_none() {
element = Some(value.value_type.clone());
}
values.push(value);
}
let element = element
.ok_or_else(|| self.error(source, "empty List requires a type context"))?;
Expression {
kind: Node::List(values),
value_type: Type::List(Box::new(element)),
span: source.span,
}
}
Syntax::Record(name, fields) => {
let index = self
.definition_names
.resolve(name, self.source, source.span)?
.ok_or_else(|| self.error(source, format!("unknown record '{name}'")))?;
let Shape::Record(declared) = &self.definitions[index].shape else {
return Err(self.error(source, "record construction requires a record type"));
};
let definition = &self.definitions[index];
if matches!(
package::role(&self.definition_names.packages[definition.source]),
Some((_, "model"))
) {
return Err(Diagnostic::new(
"C014",
"Model snapshots are produced by ORM operations",
source.span,
));
}
if !definition.private_fields.is_empty() {
self.definition_names.private_field(
self.source,
definition.source,
source.span,
)?;
}
let declared = declared.clone();
if fields.len() != declared.len() {
return Err(self.error(
source,
"record construction must supply every field exactly once",
));
}
let mut values = Vec::new();
let mut supplied = std::collections::HashSet::new();
// 保存源码字段顺序;声明中的字段位置只用于最终存放结果。
for (name, expression) in fields {
let position = declared
.iter()
.position(|(field, _)| field == name)
.ok_or_else(|| self.error(source, format!("unknown field '{name}'")))?;
if !supplied.insert(position) {
return Err(self.error(source, format!("duplicate field '{name}'")));
}
values.push((
position,
self.expression(expression, Some(&declared[position].1))?,
));
}
Expression {
kind: Node::Record(index, values),
value_type: Type::Named(index),
span: source.span,
}
}
Syntax::Map(entries) => self.map(entries, expected, source)?,
};
self.coerce(super::constants::fold(value)?, expected)
}
fn literal(
&self,
literal: &Literal,
expected: Option<&Type>,
source: &ast::Expression,
) -> Result<Expression, Diagnostic> {
let value_type = match literal {
Literal::Bool(_) => Type::Bool,
Literal::Text(_) => Type::Text,
Literal::Null => match expected {
Some(Type::Optional(inner)) => Type::Optional(inner.clone()),
_ => return Err(self.error(source, "null requires a nullable type context")),
},
Literal::Number(value) => {
if value.contains(['e', 'E'])
|| (value.contains('.') && present_type(expected) == Some(&Type::Float))
{
if !value.parse::<f64>().is_ok_and(f64::is_finite) {
return Err(self.error(source, "Float literal is not representable"));
}
Type::Float
} else if value.contains('.') {
dever_runtime::value::decimal::Decimal::parse(value)
.map_err(|error| self.error(source, error))?;
Type::Decimal
} else {
value
.parse::<i64>()
.map_err(|_| self.error(source, "Int literal is not representable"))?;
Type::Int
}
}
};
Ok(Expression {
kind: Node::Literal(literal.clone()),
value_type,
span: source.span,
})
}
fn binary(
&mut self,
operator: &str,
left: &ast::Expression,
right: &ast::Expression,
expected: Option<&Type>,
source: &ast::Expression,
) -> Result<Expression, Diagnostic> {
let context = (present_type(expected) == Some(&Type::Float)).then_some(&Type::Float);
let (mut left, mut right) = if matches!(operator, "==" | "!=")
&& matches!(left.kind, Syntax::Literal(Literal::Null))
{
let right = self.expression(right, context)?;
(self.expression(left, Some(&right.value_type))?, right)
} else {
let left = self.expression(left, context)?;
let context = if matches!(operator, "==" | "!=")
&& matches!(left.value_type, Type::Optional(_))
{
Some(&left.value_type)
} else {
context
};
let right = self.expression(right, context)?;
(left, right)
};
if matches!(operator, "==" | "!=")
&& matches!(right.value_type, Type::Optional(_))
&& !matches!(left.value_type, Type::Optional(_))
{
left = self.coerce(left, Some(&right.value_type))?;
}
if matches!(
(&left.value_type, &right.value_type),
(Type::Int, Type::Decimal) | (Type::Decimal, Type::Int)
) || (operator == "/" && left.value_type == Type::Int && right.value_type == Type::Int)
{
left = self.coerce(left, Some(&Type::Decimal))?;
right = self.coerce(right, Some(&Type::Decimal))?;
}
if left.value_type != right.value_type {
return Err(self.error(source, "binary operand types differ"));
}
let kind = &left.value_type;
let value_type = match operator {
"and" | "or" if *kind == Type::Bool => Type::Bool,
"==" | "!=" if kind.comparable(self.definitions) => Type::Bool,
"<" | "<=" | ">" | ">=" if kind.numeric() => Type::Bool,
"+" if *kind == Type::Text => Type::Text,
"+" | "-" | "*" | "/" if kind.numeric() => kind.clone(),
"//" | "%" if *kind == Type::Int => Type::Int,
_ => {
return Err(self.error(
source,
format!("operator '{operator}' does not accept {kind:?}"),
));
}
};
Ok(Expression {
kind: Node::Binary(operator.into(), Box::new(left), Box::new(right)),
value_type,
span: source.span,
})
}
pub(super) fn coerce(
&self,
value: Expression,
expected: Option<&Type>,
) -> Result<Expression, Diagnostic> {
let Some(expected) = expected else {
return Ok(value);
};
if &value.value_type == expected {
return Ok(value);
}
let span = value.span;
if *expected == Type::Decimal && value.value_type == Type::Int {
// Int 常量在编译期提升;先按 Int 取值,避免把整数 -0 变成 Decimal 负零。
let kind = match &value.kind {
Node::Literal(Literal::Number(number)) => Node::Literal(Literal::Number(
number
.parse::<i64>()
.expect("checked Int literal")
.to_string(),
)),
_ => Node::Decimal(Box::new(value)),
};
return Ok(Expression {
kind,
value_type: Type::Decimal,
span,
});
}
if let Type::Optional(inner) = expected {
let value = self.coerce(value, Some(inner))?;
return Ok(Expression {
kind: Node::Some(Box::new(value)),
value_type: expected.clone(),
span,
});
}
Err(Diagnostic::new(
"C005",
format!("expected {expected:?}, got {:?}", value.value_type),
span,
))
}
pub(super) fn error(&self, source: &ast::Expression, message: impl Into<String>) -> Diagnostic {
Diagnostic::new("C005", message, source.span)
}
}
fn present_type(expected: Option<&Type>) -> Option<&Type> {
match expected {
Some(Type::Optional(inner)) => Some(inner),
expected => expected,
}
}
+167
View File
@@ -0,0 +1,167 @@
//! 为 result(call) 选择唯一且名义身份匹配的 choice。
//! 仅载荷形状相同不足以匹配业务错误;数值 Fault 不进入捕获集合。
use crate::compiler::{check::Context, model::*};
use crate::syntax::{ast, diagnostic::Diagnostic};
use std::collections::BTreeMap;
impl Context<'_> {
pub(crate) fn capture(
&mut self,
call: &ast::Expression,
expected: Option<&Type>,
source: &ast::Expression,
) -> Result<Expression, Diagnostic> {
let target = match expected {
Some(Type::Optional(inner)) => Some(inner.as_ref()),
expected => expected,
};
// 泛型解码直接使用结果 Choice 的成功字段作为类型上下文,不生成包装函数。
let decoded = if matches!(&call.kind, ast::ExpressionKind::Call(target, _)
if target.path().as_deref() == Some("dever.json.decode"))
{
let Some(Type::Named(index)) = target else {
return Err(self.error(
source,
"result(dever.json.decode(...)) requires an explicit result choice",
));
};
let Shape::Choice(variants) = &self.definitions[*index].shape else {
return Err(self.error(source, "JSON decode result requires a choice"));
};
let mut successes = variants.iter().filter(|variant| !variant.error);
let (Some(success), None) = (successes.next(), successes.next()) else {
return Err(self.error(
source,
"JSON decode result requires exactly one success variant",
));
};
let [(_, kind)] = success.fields.as_slice() else {
return Err(self.error(source, "JSON decode success variant requires one value"));
};
Some(kind.clone())
} else {
None
};
let value = self.expression(call, decoded.as_ref())?;
let decoded_failure;
let (origin, failures) = match &value.kind {
Node::Call(function, _) => {
let signature = &self.signatures[*function];
(signature.source, signature.failures.as_slice())
}
Node::Json(JsonOperation::Decode { error }, _) => {
decoded_failure = [Failure {
definition: *error,
variant: 1,
}];
(self.definitions[*error].source, decoded_failure.as_slice())
}
_ => {
return Err(self.error(
source,
"result requires a function call or typed JSON decode",
));
}
};
let mut candidates = Vec::new();
for (index, definition) in self.definitions.iter().enumerate() {
if definition.source != self.source && !self.definition_names.exported(index) {
continue;
}
if target.is_none()
&& definition.source != self.source
&& definition.source != origin
&& !failures.iter().any(|failure| failure.definition == index)
{
continue;
}
if target.is_some_and(|kind| *kind != Type::Named(index)) {
continue;
}
let Shape::Choice(variants) = &definition.shape else {
continue;
};
let successes = variants
.iter()
.enumerate()
.filter(|(_, variant)| !variant.error)
.collect::<Vec<_>>();
let [(success, variant)] = successes.as_slice() else {
continue;
};
let outputs_match = match &value.value_type {
Type::Unit => variant.fields.is_empty(),
Type::Outputs(outputs) => variant
.fields
.iter()
.map(|(_, kind)| kind)
.eq(outputs.iter().map(|(_, kind)| kind)),
kind => matches!(variant.fields.as_slice(), [(_, field)] if field == kind),
};
if !outputs_match {
continue;
}
if let Some(targets) = targets(index, variants, failures, self.definitions) {
candidates.push((index, *success, targets));
}
}
if candidates.len() != 1 {
return Err(Diagnostic::new(
"C005",
"result needs one unambiguous choice with matching success outputs and all declared errors",
source.span,
));
}
let (definition, success, targets) = candidates.pop().unwrap();
let capture = Capture {
call: value,
definition,
success,
targets,
};
self.coerce(
Expression {
kind: Node::Capture(Box::new(capture)),
value_type: Type::Named(definition),
span: source.span,
},
expected,
)
}
}
fn targets(
index: usize,
variants: &[Variant],
actual: &[Failure],
definitions: &[Definition],
) -> Option<Vec<(Failure, usize, bool)>> {
let mut mapping = BTreeMap::new();
for (position, variant) in variants.iter().enumerate().filter(|(_, value)| value.error) {
let direct = Failure {
definition: index,
variant: position,
};
if actual.contains(&direct) {
mapping.insert(direct, (position, false));
}
if let [(_, Type::Named(inner))] = variant.fields.as_slice()
&& let Shape::Choice(_) = &definitions[*inner].shape
{
for failure in actual.iter().filter(|failure| failure.definition == *inner) {
if mapping.insert(*failure, (position, true)).is_some() {
return None;
}
}
}
}
actual
.iter()
.map(|failure| {
mapping
.get(failure)
.map(|(variant, wrapped)| (*failure, *variant, *wrapped))
})
.collect()
}
@@ -0,0 +1,194 @@
//! 推导完整遍历、提前停止及归约中的失败处理义务。
//! 未知长度归约合并初始与迭代摘要直到稳定,保留空输入的可能性。
use super::{State, flow};
use crate::compiler::model::*;
use crate::syntax::diagnostic::Diagnostic;
use std::collections::{BTreeMap, BTreeSet};
impl State<'_> {
pub(super) fn collection(&mut self, collection: &Collection) -> Result<Flow, Diagnostic> {
if collection
.sequence
.value_type
.database_stream(self.definitions)
&& let Some(definition) = self.database_error
{
let Shape::Choice(variants) = &self.definitions[definition].shape else {
unreachable!()
};
self.failures
.extend((0..variants.len()).map(|variant| Failure {
definition,
variant,
}));
}
let sequence = self.expression(&collection.sequence)?;
if let Some(limit) = &collection.limit {
self.expression(limit)?;
}
let extra = collection
.extra
.as_ref()
.map(|value| self.expression(value))
.transpose()?;
let signature = &self.signatures[collection.handler];
self.failures.extend(&signature.failures);
let before = self.pending.clone();
// Stream 的失败元素尚未产生;只在实际消费合同中引入义务,关闭不等于丢弃已读错误。
let shape =
if let Type::Stream(item) | Type::AsyncStream(item) = &collection.sequence.value_type {
match &sequence.sequence {
// 转发流可能没有现存错误 token,但未来拉取仍会产生错误义务。
Some(Sequence::Unknown(value)) if value.tokens.is_empty() => {
Sequence::Unknown(Box::new(self.unknown(item, 0)?))
}
Some(sequence) => sequence.clone(),
None => Sequence::Unknown(Box::new(self.unknown(item, 0)?)),
}
} else {
sequence
.sequence
.clone()
.unwrap_or_else(|| Sequence::Unknown(Box::default()))
};
let mut produced = Vec::new();
let mut state = extra.clone().unwrap_or_default();
let mut fresh = BTreeMap::new();
match &shape {
Sequence::Known(items) => {
for item in items {
self.budget(0)?;
let value =
self.apply_handler(collection, item, &state, &mut BTreeMap::new())?;
if collection.operation.reduction() {
state = value;
produced.push(state.clone());
} else {
produced.push(value);
}
}
}
Sequence::Unknown(item) => {
if collection.operation.reduction() {
// 初始状态参与合并,因为未知集合可以完全不执行 handler。
loop {
self.budget(0)?;
let value = self.apply_handler(collection, item, &state, &mut fresh)?;
let previous = state.clone();
state.merge(&value);
if previous == state {
break;
}
}
produced.push(state.clone());
} else {
produced.push(self.apply_handler(collection, item, &state, &mut fresh)?);
}
}
}
let empty = matches!(&shape, Sequence::Known(items) if items.is_empty());
let nonempty = matches!(&shape, Sequence::Known(items) if !items.is_empty());
let partial = matches!(
collection.operation,
Operation::Find | Operation::ReduceUntil
);
self.pending = before;
// 提前结束只保证访问首项,不能用选中的输出消费剩余元素的义务。
if partial {
if let Sequence::Known(items) = &shape
&& let Some(first) = items.first()
{
self.pending.retain(|token| !first.tokens.contains(token));
}
} else {
self.pending
.retain(|token| !sequence.tokens.contains(token));
}
if let Some(extra) = &extra
&& (nonempty || collection.operation.reduction())
{
self.pending.retain(|token| !extra.tokens.contains(token));
}
let mut output = match collection.operation {
Operation::ParallelEach => Flow::default(),
Operation::Each if signature.outputs.is_empty() => Flow::default(),
Operation::Each => Flow::sequence(match shape {
Sequence::Known(_) => Sequence::Known(produced),
Sequence::Unknown(_) => Sequence::Unknown(Box::new(produced.pop().unwrap())),
}),
Operation::Filter if empty => Flow::sequence(Sequence::Known(Vec::new())),
Operation::Filter => Flow::sequence(Sequence::Unknown(Box::new(shape.element()))),
Operation::Find => {
let item = if empty {
return Ok(Flow::variants([(0, Flow::default())]));
} else if matches!(&shape, Sequence::Known(items) if items.len() == 1) {
shape.element()
} else {
self.unknown(&signature.inputs[0].1, 0)?
};
Flow::variants([(0, Flow::default()), (1, item)])
}
Operation::Sum => Flow::default(),
Operation::Reduce => state,
Operation::ReduceUntil => {
if let Some((first, rest)) = produced.split_first() {
let mut output = first.clone();
for value in rest {
output.merge(value);
}
output
} else {
state
}
}
};
let retained: BTreeSet<_> = self.pending.intersection(&output.tokens).copied().collect();
if !retained.is_empty() {
let mut mapping = BTreeMap::new();
for token in &output.tokens {
let target = if retained.contains(token) {
self.token()?
} else {
*token
};
mapping.insert(*token, BTreeSet::from([target]));
}
output = self.instantiate(&output, &mut mapping, 0)?;
}
self.pending.extend(&output.tokens);
self.closed_streams.extend(sequence.streams);
self.refresh_streams(&mut output);
Ok(output)
}
fn apply_handler(
&mut self,
collection: &Collection,
item: &Flow,
extra: &Flow,
fresh: &mut BTreeMap<usize, BTreeSet<usize>>,
) -> Result<Flow, Diagnostic> {
let signature = &self.signatures[collection.handler];
let mut mapping = fresh.clone();
for template in &signature.input_flows {
for token in &template.tokens {
mapping.remove(token);
}
}
for (template, actual) in signature.input_flows.iter().zip([item, extra]) {
flow::bind(template, actual, &mut mapping);
}
let output = if let Some(value) = &signature.returns {
self.instantiate(value, &mut mapping, 0)?
} else {
Flow::default()
};
*fresh = mapping;
if collection.operation == Operation::ReduceUntil {
Ok(output.fields.get(&0).cloned().unwrap_or_default())
} else {
Ok(output)
}
}
}
@@ -0,0 +1,371 @@
//! 按表达式求值规则传播失败摘要。
//! 可能被短路跳过的处理不能解除已有义务;捕获调用的参数失败仍向外传播。
use super::State;
use crate::compiler::model::*;
use crate::syntax::{ast::Literal, diagnostic::Diagnostic};
use std::collections::BTreeMap;
impl State<'_> {
pub(super) fn expression(&mut self, expression: &Expression) -> Result<Flow, Diagnostic> {
self.budget(0)?;
match &expression.kind {
Node::Enqueue(call, arguments) => {
for argument in arguments {
self.expression(argument)?;
}
let Shape::Choice(variants) = &self.definitions[call.error].shape else {
unreachable!()
};
self.failures
.extend((0..variants.len()).map(|variant| Failure {
definition: call.error,
variant,
}));
self.unknown(&expression.value_type, 0)
}
Node::Authorization(call, arguments) => {
for argument in arguments {
self.expression(argument)?;
}
for definition in [call.api_error, call.database_error] {
let Shape::Choice(variants) = &self.definitions[definition].shape else {
unreachable!()
};
self.failures
.extend((0..variants.len()).map(|variant| Failure {
definition,
variant,
}));
}
self.unknown(&expression.value_type, 0)
}
Node::Auth(call, arguments) => {
for argument in arguments {
self.expression(argument)?;
}
if call.operation != auth::Operation::Site {
let Shape::Choice(variants) = &self.definitions[call.error].shape else {
unreachable!()
};
self.failures.extend(variants.iter().enumerate().filter_map(
|(variant, value)| {
matches!(
value.name.as_str(),
"Invalid" | "Unauthorized" | "Forbidden"
)
.then_some(Failure {
definition: call.error,
variant,
})
},
));
}
self.unknown(&expression.value_type, 0)
}
Node::Json(JsonOperation::Decode { error }, arguments) => {
self.expression(&arguments[0])?;
self.failures.insert(Failure {
definition: *error,
variant: 1,
});
self.unknown(&expression.value_type, 0)
}
Node::Database(query, arguments) => {
for argument in arguments {
self.expression(argument)?;
}
let Shape::Choice(variants) = &self.definitions[query.error].shape else {
unreachable!()
};
self.failures
.extend((0..variants.len()).map(|variant| Failure {
definition: query.error,
variant,
}));
self.unknown(&expression.value_type, 0)
}
Node::Literal(Literal::Null) => Ok(Flow::variants([(0, Flow::default())])),
Node::Literal(_) => Ok(Flow::default()),
Node::Variable(index) => {
let mut value = self.values[*index].clone();
self.refresh_streams(&mut value);
Ok(value)
}
Node::Field(value, index) => Ok(self
.expression(value)?
.fields
.get(index)
.cloned()
.unwrap_or_default()),
Node::Some(value) => Ok(Flow::variants([(1, self.expression(value)?)])),
Node::List(values) => Ok(Flow::sequence(Sequence::Known(
values
.iter()
.map(|value| self.expression(value))
.collect::<Result<_, _>>()?,
))),
Node::Map(entries) => {
let mut values = Vec::new();
for (key, value) in entries {
values.push(Flow::fields([
(0, self.expression(key)?),
(1, self.expression(value)?),
]));
}
Ok(Flow::sequence(Sequence::Known(values)))
}
Node::Access(operation, arguments) => {
self.access(*operation, arguments, &expression.value_type)
}
Node::Net(operation, arguments) => {
for argument in arguments {
self.expression(argument)?;
}
match operation {
NetOperation::Connections => self.lazy_stream(),
NetOperation::Chunks => {
let Type::Named(index) = expression.value_type else {
unreachable!("checked stream result");
};
Ok(Flow::variants([
(0, Flow::fields([(0, self.lazy_stream()?)])),
(1, self.unknown_variant(index, 1, 0)?),
]))
}
_ => self.unknown(&expression.value_type, 0),
}
}
Node::Http(HttpOperation::Open | HttpOperation::OpenStream, arguments) => {
for argument in arguments {
let value = self.expression(argument)?;
// 上传源的失败由 HTTP 打开结果或响应流交付,不能留下重复义务。
self.pending.retain(|token| !value.tokens.contains(token));
}
let Type::Named(index) = expression.value_type else {
unreachable!("checked HTTP stream result");
};
// 下载尚未拉取;沿用 TCP 流的延迟失败证明,关闭不会伪造已读错误。
Ok(Flow::variants([
(
0,
Flow::fields([(0, Flow::fields([(2, self.lazy_stream()?)]))]),
),
(1, self.unknown_variant(index, 1, 0)?),
]))
}
Node::Crypto(_, arguments)
| Node::Assert(arguments)
| Node::Calendar(_, arguments)
| Node::Standard(_, arguments)
| Node::Bytes(_, arguments)
| Node::Http(_, arguments)
| Node::Tls(_, arguments)
| Node::Io(_, arguments)
| Node::Upload(_, arguments)
| Node::Json(_, arguments) => {
for argument in arguments {
self.expression(argument)?;
}
self.unknown(&expression.value_type, 0)
}
Node::Variant(index, variant, values) => {
let mut fields = Vec::new();
for (index, value) in values.iter().enumerate() {
fields.push((index, self.expression(value)?));
}
let value = Flow::fields(fields);
Ok(Flow::variants([(
*variant,
self.variant(*index, *variant, value)?,
)]))
}
Node::Record(_, values) => {
let mut fields = Vec::new();
for (index, value) in values {
fields.push((*index, self.expression(value)?));
}
Ok(Flow::fields(fields))
}
Node::Close(value) => {
let value = self.expression(value)?;
self.closed_streams.extend(value.streams);
Ok(Flow::default())
}
Node::Unary(_, value)
| Node::TestSecret(value)
| Node::TestClock(value)
| Node::TestDrain { limit: value, .. }
| Node::Decimal(value)
| Node::Id(value)
| Node::IdText(value)
| Node::Length(value)
| Node::Sum(value) => {
self.expression(value)?;
Ok(Flow::default())
}
Node::Binary(operator, left, right) => {
self.expression(left)?;
if matches!(operator.as_str(), "and" | "or") {
// 右侧仍需通过检查,但可能不执行,处理义务必须保留该路径。
if let Node::Literal(Literal::Bool(value)) = left.kind {
if value == (operator == "and") {
self.expression(right)?;
} else {
let pending = self.pending.clone();
let closed = self.closed_streams.clone();
self.expression(right)?;
self.pending = pending;
self.closed_streams = closed;
}
} else {
let pending = self.pending.clone();
self.expression(right)?;
self.pending.extend(pending);
}
} else {
self.expression(right)?;
}
Ok(Flow::default())
}
Node::Call(index, arguments) => {
let signature = &self.signatures[*index];
let mut mapping = self.arguments(arguments, signature)?;
self.failures.extend(&signature.failures);
if let Some(value) = &signature.returns {
self.instantiate(value, &mut mapping, 0)
} else {
Ok(Flow::default())
}
}
Node::Schedule(operation, index, arguments) => {
self.schedule(*operation, *index, arguments)
}
Node::Join { task, stop } => self.join(task, *stop),
Node::Timeout {
task,
millis,
fallback,
} => self.timeout(task, millis, *fallback),
Node::Race(tasks) => self.race(tasks),
Node::Group(limit) => self.group(limit),
Node::Stream(value) => {
let mut value = self.expression(value)?;
value.streams.insert(self.symbol()?);
Ok(value)
}
Node::Ticks(millis) => {
self.expression(millis)?;
Ok(Flow {
streams: [self.symbol()?].into(),
..Flow::default()
})
}
Node::Channel(capacity) => {
self.expression(capacity)?;
Ok(Flow::default())
}
Node::Send(channel, value) => {
self.expression(channel)?;
let value = self.expression(value)?;
self.pending.retain(|token| !value.tokens.contains(token));
Ok(Flow::default())
}
Node::Receive(channel) => {
self.expression(channel)?;
self.unknown(&expression.value_type, 0)
}
Node::GroupRun {
group,
target,
arguments,
} => self.group_run(group, *target, arguments),
Node::Timer(_, millis) => {
self.expression(millis)?;
self.unknown(&expression.value_type, 0)
}
Node::Capture(capture) => self.capture(capture),
Node::Collection(collection) => self.collection(collection),
}
}
fn capture(&mut self, capture: &Capture) -> Result<Flow, Diagnostic> {
let mut possibilities = BTreeMap::<usize, Flow>::new();
match &capture.call.kind {
Node::Call(function, arguments) => {
let signature = &self.signatures[*function];
let mut mapping = self.arguments(arguments, signature)?;
if let Some(value) = &signature.returns {
let value = self.instantiate(value, &mut mapping, 0)?;
let fields = if signature.outputs.len() == 1 {
Flow::fields([(0, value)])
} else {
value
};
possibilities.insert(capture.success, fields);
}
}
Node::Json(JsonOperation::Decode { .. }, arguments) => {
self.expression(&arguments[0])?;
let value = self.unknown(&capture.call.value_type, 0)?;
possibilities.insert(capture.success, Flow::fields([(0, value)]));
}
_ => unreachable!("checked capture call"),
}
for (failure, target, wrapped) in &capture.targets {
let source = self.unknown_variant(failure.definition, failure.variant, 0)?;
let value = if *wrapped {
self.variant(
capture.definition,
*target,
Flow::fields([(0, Flow::variants([(failure.variant, source)]))]),
)?
} else {
source
};
if let Some(previous) = possibilities.get_mut(target) {
previous.merge(&value);
} else {
possibilities.insert(*target, value);
}
}
Ok(Flow::variants(possibilities))
}
pub(super) fn fail(&mut self, value: &Expression) -> Result<(), Diagnostic> {
let flow = self.expression(value)?;
let Type::Named(index) = value.value_type else {
return Err(Diagnostic::new(
"C005",
"fail requires an error choice variant",
value.span,
));
};
let Shape::Choice(variants) = &self.definitions[index].shape else {
return Err(Diagnostic::new(
"C005",
"fail requires an error choice variant",
value.span,
));
};
let possible = if flow.variants.is_empty() {
(0..variants.len()).collect::<Vec<_>>()
} else {
flow.variants.keys().copied().collect()
};
for variant in possible {
if !variants[variant].error {
return Err(Diagnostic::new(
"C005",
"fail can receive a non-error variant",
value.span,
));
}
self.failures.insert(Failure {
definition: index,
variant,
});
}
Ok(())
}
}
+247
View File
@@ -0,0 +1,247 @@
//! 构造、绑定和实例化嵌套值的失败摘要。
//! 函数摘要中的义务编号在调用处重新映射,避免不同调用互相抵消错误。
use super::State;
use crate::compiler::model::*;
use crate::syntax::diagnostic::Diagnostic;
use std::collections::{BTreeMap, BTreeSet};
pub(super) fn tracked_types(definitions: &[Definition]) -> Vec<bool> {
fn tracked(kind: &Type, definitions: &[Definition], values: &mut [Option<bool>]) -> bool {
match kind {
Type::List(_) | Type::Map(_, _) | Type::AsyncStream(_) => true,
Type::Optional(inner) => tracked(inner, definitions, values),
Type::MapEntry(key, value) => {
tracked(key, definitions, values) || tracked(value, definitions, values)
}
Type::Named(index) => {
if let Some(value) = values[*index] {
return value;
}
let value = !definitions[*index].related
&& match &definitions[*index].shape {
Shape::Record(fields) => fields
.iter()
.any(|(_, kind)| tracked(kind, definitions, values)),
Shape::Choice(variants) => variants.iter().any(|variant| {
variant.error
|| variant
.fields
.iter()
.any(|(_, kind)| tracked(kind, definitions, values))
}),
};
values[*index] = Some(value);
value
}
_ => false,
}
}
// 类型检查已排除值类型环;只为含集合、流或错误的记录建立结构摘要。
let mut values = vec![None; definitions.len()];
for index in 0..definitions.len() {
tracked(&Type::Named(index), definitions, &mut values);
}
values.into_iter().map(Option::unwrap).collect()
}
impl State<'_> {
pub(super) fn unknown(&mut self, kind: &Type, depth: usize) -> Result<Flow, Diagnostic> {
self.budget(depth)?;
if let Type::List(inner) | Type::AsyncStream(inner) = kind {
let mut value =
Flow::sequence(Sequence::Unknown(Box::new(self.unknown(inner, depth + 1)?)));
if matches!(kind, Type::AsyncStream(_)) {
value.streams.insert(self.symbol()?);
}
return Ok(value);
}
if let Type::Map(key, value) = kind {
let entry = Type::MapEntry(key.clone(), value.clone());
return Ok(Flow::sequence(Sequence::Unknown(Box::new(
self.unknown(&entry, depth + 1)?,
))));
}
// 没有业务错误的记录仍可能含集合或流,字段摘要不能因此丢弃。
match kind {
Type::Named(index) if !self.tracked_types[*index] => Ok(Flow::default()),
Type::Named(index) => match &self.definitions[*index].shape {
Shape::Record(fields) => {
let mut values = Vec::new();
for (index, (_, kind)) in fields.iter().enumerate() {
values.push((index, self.unknown(kind, depth + 1)?));
}
Ok(Flow::fields(values))
}
Shape::Choice(variants) => {
let mut values = Vec::new();
for variant in 0..variants.len() {
values.push((variant, self.unknown_variant(*index, variant, depth + 1)?));
}
Ok(Flow::variants(values))
}
},
Type::Optional(inner) => Ok(Flow::variants([
(0, Flow::default()),
(1, self.unknown(inner, depth + 1)?),
])),
Type::MapEntry(key, value) => Ok(Flow::fields([
(0, self.unknown(key, depth + 1)?),
(1, self.unknown(value, depth + 1)?),
])),
_ => Ok(Flow::default()),
}
}
pub(super) fn unknown_variant(
&mut self,
index: usize,
variant: usize,
depth: usize,
) -> Result<Flow, Diagnostic> {
self.budget(depth)?;
let Shape::Choice(variants) = &self.definitions[index].shape else {
unreachable!()
};
let mut fields = Vec::new();
for (field, (_, kind)) in variants[variant].fields.iter().enumerate() {
fields.push((field, self.unknown(kind, depth + 1)?));
}
self.variant(index, variant, Flow::fields(fields))
}
pub(super) fn variant(
&mut self,
index: usize,
variant: usize,
mut fields: Flow,
) -> Result<Flow, Diagnostic> {
let Shape::Choice(variants) = &self.definitions[index].shape else {
unreachable!()
};
if variants[variant].error {
fields.tokens.insert(self.token()?);
fields.guaranteed = true;
}
Ok(fields)
}
pub(super) fn instantiate(
&mut self,
template: &Flow,
mapping: &mut BTreeMap<usize, BTreeSet<usize>>,
depth: usize,
) -> Result<Flow, Diagnostic> {
self.budget(depth)?;
// 子摘要按调用处重新映射,不先深拷贝再逐项覆盖。
let mut value = Flow {
guaranteed: template.guaranteed,
task: template.task.clone(),
..Flow::default()
};
for resource in &template.streams {
if !mapping.contains_key(resource) {
mapping.insert(*resource, BTreeSet::from([self.symbol()?]));
}
value.streams.extend(&mapping[resource]);
}
let mut tokens = BTreeSet::new();
for token in &template.tokens {
if !mapping.contains_key(token) {
mapping.insert(*token, BTreeSet::from([self.token()?]));
}
tokens.extend(&mapping[token]);
}
self.pending.extend(&tokens);
value.tokens = tokens;
for (index, field) in &template.fields {
value
.fields
.insert(*index, self.instantiate(field, mapping, depth + 1)?);
}
for (index, variant) in &template.variants {
value
.variants
.insert(*index, self.instantiate(variant, mapping, depth + 1)?);
}
value.sequence =
match &template.sequence {
Some(Sequence::Known(values)) => Some(Sequence::Known(
values
.iter()
.map(|value| self.instantiate(value, mapping, depth + 1))
.collect::<Result<_, _>>()?,
)),
Some(Sequence::Unknown(value)) => Some(Sequence::Unknown(Box::new(
self.instantiate(value, mapping, depth + 1)?,
))),
None => None,
};
self.refresh_streams(&mut value);
Ok(value)
}
}
pub(super) fn bind(template: &Flow, actual: &Flow, mapping: &mut BTreeMap<usize, BTreeSet<usize>>) {
for resource in &template.streams {
mapping
.entry(*resource)
.or_default()
.extend(&actual.streams);
}
for token in &template.tokens {
mapping.entry(*token).or_default();
}
let mut nested: BTreeSet<_> = template
.fields
.values()
.chain(template.variants.values())
.flat_map(|value| value.tokens.iter().copied())
.collect();
if let Some(sequence) = &template.sequence {
nested.extend(sequence.element().tokens);
}
for token in template.tokens.difference(&nested) {
mapping.entry(*token).or_default().extend(&actual.tokens);
}
for (index, field) in &template.fields {
bind(
field,
actual.fields.get(index).unwrap_or(&Flow::default()),
mapping,
);
}
for (index, variant) in &template.variants {
bind(
variant,
actual.variants.get(index).unwrap_or(&Flow::default()),
mapping,
);
}
if let Some(sequence) = &template.sequence {
let actual = actual.sequence.as_ref();
match (sequence, actual) {
(Sequence::Known(values), Some(Sequence::Known(actual)))
if values.len() == actual.len() =>
{
for (value, actual) in values.iter().zip(actual) {
bind(value, actual, mapping);
}
}
_ => bind(
&sequence.element(),
&actual.map(Sequence::element).unwrap_or_default(),
mapping,
),
}
}
}
pub(super) fn assign(target: &mut Flow, fields: &[usize], value: Flow) {
let Some((field, remaining)) = fields.split_first() else {
*target = value;
return;
};
assign(target.fields.entry(*field).or_default(), remaining, value);
*target = Flow::fields(std::mem::take(&mut target.fields));
}
+256
View File
@@ -0,0 +1,256 @@
//! 逐分句追踪业务失败的处理义务,并推导函数失败集合。
//! 只有返回、传播或显式恢复才能解除义务;分析结果不改变运行时错误身份。
mod capture;
mod collection;
mod expression;
mod flow;
mod storage;
mod stream;
mod task;
use crate::compiler::model::*;
use crate::syntax::{diagnostic::Diagnostic, source::Span};
use std::collections::{BTreeMap, BTreeSet};
pub(super) fn check(
function: &mut Function,
definitions: &[Definition],
signatures: &[Signature],
database_error: Option<usize>,
) -> Result<(), Diagnostic> {
let tracked_types = flow::tracked_types(definitions);
let mut next = 0;
let mut failures = BTreeSet::new();
let mut returns: Option<Flow> = None;
let mut inputs: Vec<Option<Flow>> = vec![None; function.signature.inputs.len()];
for clause in &function.clauses {
let mut state = State {
database_error,
definitions,
signatures,
tracked_types: &tracked_types,
values: vec![Flow::default(); clause.variables.len()],
pending: BTreeSet::new(),
closed_streams: BTreeSet::new(),
failures: BTreeSet::new(),
next,
work: 0,
span: clause.span,
};
for (position, (_, kind)) in function.signature.inputs.iter().enumerate() {
let pattern = &clause.patterns[position];
let value = match (&clause.variables[position].1, pattern) {
(_, Pattern::Null) => Flow::default(),
(Type::Named(index), _)
if matches!(definitions[*index].shape, Shape::Choice(_)) =>
{
let Shape::Choice(variants) = &definitions[*index].shape else {
unreachable!()
};
let mut possibilities = Vec::new();
for variant in 0..variants.len() {
if super::clauses::accepts_variant(
pattern,
variant,
function
.clauses
.iter()
.map(|clause| clause.patterns[position].clone()),
) {
possibilities
.push((variant, state.unknown_variant(*index, variant, 0)?));
}
}
Flow::variants(possibilities)
}
(kind, _) => state.unknown(kind, 0)?,
};
let input = if matches!(kind, Type::Optional(_))
&& matches!(pattern, Pattern::Present(_) | Pattern::Other)
{
Flow::variants([(1, value.clone())])
} else if matches!(pattern, Pattern::Null) {
Flow::variants([(0, Flow::default())])
} else {
value.clone()
};
if let Some(previous) = &mut inputs[position] {
previous.merge(&input);
} else {
inputs[position] = Some(input);
}
state.values[position] = value;
}
for binding in &clause.bindings {
let Pattern::Variant(variant) = clause.patterns[binding.input].inner() else {
unreachable!()
};
state.values[binding.variable] = state.values[binding.input]
.variants
.get(variant)
.and_then(|value| value.fields.get(&binding.field))
.cloned()
.unwrap_or_default();
}
for statement in &clause.body {
match statement {
Statement::Assign {
variable,
fields,
value,
} => {
let value = state.expression(value)?;
flow::assign(&mut state.values[*variable], fields, value);
}
Statement::Action(value) => {
state.expression(value)?;
}
Statement::Fail(value) => state.fail(value)?,
}
}
if !clause.terminates {
let mut output = match clause.outputs.as_slice() {
[] => Flow::default(),
[index] => state.values[*index].clone(),
outputs => Flow::fields(
outputs
.iter()
.enumerate()
.map(|(field, index)| (field, state.values[*index].clone())),
),
};
state.refresh_streams(&mut output);
if function.signature.recovery.is_none()
&& !output.guaranteed
&& !state.pending.is_subset(&output.tokens)
{
return Err(Diagnostic::new(
"C012",
"failure is not handled: return, propagate, or declare an explicit recovery policy",
clause.span,
));
}
if let Some(previous) = &mut returns {
previous.merge(&output);
} else {
returns = Some(output);
}
}
next = state.next;
failures.extend(state.failures);
}
function.signature.failures = failures.into_iter().collect();
function.signature.input_flows = inputs.into_iter().map(Option::unwrap).collect();
function.signature.returns = returns;
Ok(())
}
struct State<'program> {
database_error: Option<usize>,
definitions: &'program [Definition],
signatures: &'program [Signature],
tracked_types: &'program [bool],
values: Vec<Flow>,
/// 尚未由当前分句处理或转移出去的错误义务。
pending: BTreeSet<usize>,
closed_streams: BTreeSet<usize>,
failures: BTreeSet<Failure>,
next: usize,
work: usize,
span: Span,
}
pub(super) fn prototype(
signature: &mut Signature,
definitions: &[Definition],
span: Span,
) -> Result<(), Diagnostic> {
let tracked_types = flow::tracked_types(definitions);
let mut state = State {
database_error: None,
definitions,
signatures: &[],
tracked_types: &tracked_types,
values: Vec::new(),
pending: BTreeSet::new(),
closed_streams: BTreeSet::new(),
failures: BTreeSet::new(),
next: 0,
work: 0,
span,
};
signature.input_flows = signature
.inputs
.iter()
.map(|(_, kind)| state.unknown(kind, 0))
.collect::<Result<_, _>>()?;
signature.returns = Some(match signature.outputs.as_slice() {
[] => Flow::default(),
[(_, kind)] => state.unknown(kind, 0)?,
fields => Flow::fields(
fields
.iter()
.enumerate()
.map(|(index, (_, kind))| Ok((index, state.unknown(kind, 0)?)))
.collect::<Result<Vec<_>, Diagnostic>>()?,
),
});
Ok(())
}
impl State<'_> {
fn symbol(&mut self) -> Result<usize, Diagnostic> {
self.budget(0)?;
let symbol = self.next;
self.next += 1;
Ok(symbol)
}
fn token(&mut self) -> Result<usize, Diagnostic> {
let token = self.symbol()?;
self.pending.insert(token);
Ok(token)
}
fn budget(&mut self, depth: usize) -> Result<(), Diagnostic> {
// 达到分析边界就报告失败,不能把未完成的证明当作合同通过。
self.work += 1;
if self.work > 16_384 || depth > 128 {
return Err(Diagnostic::new(
"C015",
"failure analysis exceeds its structural limit",
self.span,
));
}
Ok(())
}
fn arguments(
&mut self,
arguments: &[Expression],
signature: &Signature,
) -> Result<BTreeMap<usize, BTreeSet<usize>>, Diagnostic> {
self.call_inputs(arguments, signature, true)
}
fn call_inputs(
&mut self,
arguments: &[Expression],
signature: &Signature,
consume: bool,
) -> Result<BTreeMap<usize, BTreeSet<usize>>, Diagnostic> {
let mut mapping = BTreeMap::new();
let values = arguments
.iter()
.map(|argument| self.expression(argument))
.collect::<Result<Vec<_>, _>>()?;
for (value, template) in values.iter().zip(&signature.input_flows) {
flow::bind(template, value, &mut mapping);
if consume {
self.pending.retain(|token| !value.tokens.contains(token));
}
}
Ok(mapping)
}
}
@@ -0,0 +1,82 @@
//! 追踪 List/Map 存取对失败义务的影响。
//! 读取、覆盖或删除部分元素不等于处理整个容器中的业务错误。
use super::State;
use crate::compiler::model::*;
use crate::syntax::diagnostic::Diagnostic;
impl State<'_> {
pub(super) fn access(
&mut self,
operation: Access,
arguments: &[Expression],
kind: &Type,
) -> Result<Flow, Diagnostic> {
let mut values = arguments
.iter()
.map(|argument| self.expression(argument))
.collect::<Result<Vec<_>, _>>()?;
let container = std::mem::take(&mut values[0]);
match operation {
Access::Contains => Ok(Flow::default()),
Access::Entries => Ok(container),
Access::Append => {
let value = std::mem::take(&mut values[1]);
let guaranteed = value.guaranteed || container.guaranteed;
let sequence = match container.sequence.expect("List flow") {
Sequence::Known(mut items) => {
items.push(value);
Sequence::Known(items)
}
Sequence::Unknown(mut item) => {
item.merge(&value);
Sequence::Unknown(item)
}
};
let mut output = Flow::sequence(sequence);
output.guaranteed = guaranteed;
Ok(output)
}
Access::First => {
if let Some(Sequence::Known(items)) = container.sequence {
if let Some(item) = items.into_iter().next() {
let Type::List(inner) = &arguments[0].value_type else {
unreachable!()
};
return Ok(if matches!(inner.as_ref(), Type::Optional(_)) {
item
} else {
Flow::variants([(1, item)])
});
}
Ok(Flow::variants([(0, Flow::default())]))
} else {
self.unknown(kind, 0)
}
}
Access::Get => self.unknown(kind, 0),
Access::Remove => {
if matches!(&container.sequence, Some(Sequence::Known(items)) if items.is_empty()) {
Ok(container)
} else {
self.unknown(kind, 0)
}
}
Access::Put => {
let entry = Flow::fields([
(0, std::mem::take(&mut values[1])),
(1, std::mem::take(&mut values[2])),
]);
if matches!(&container.sequence, Some(Sequence::Known(items)) if items.is_empty()) {
return Ok(Flow::sequence(Sequence::Known(vec![entry])));
}
let mut output = self.unknown(kind, 0)?;
let mut item = output.sequence.take().expect("Map flow").element();
item.merge(&entry);
output = Flow::sequence(Sequence::Unknown(Box::new(item)));
output.guaranteed = entry.guaranteed;
Ok(output)
}
}
}
}
+57
View File
@@ -0,0 +1,57 @@
//! 异步流别名只需在错误证明中跟踪:关闭的源不能再兑现处理原元素的承诺。
//! 这些编号和集合不会进入生成程序;普通 List 复制仍保留独立可消费的值。
use super::State;
use crate::compiler::model::{Flow, Sequence};
use crate::syntax::diagnostic::Diagnostic;
use std::collections::BTreeSet;
impl State<'_> {
pub(super) fn lazy_stream(&mut self) -> Result<Flow, Diagnostic> {
Ok(Flow {
streams: [self.symbol()?].into(),
..Flow::default()
})
}
pub(super) fn refresh_streams(&self, value: &mut Flow) -> bool {
if !value.streams.is_disjoint(&self.closed_streams) {
value.tokens.clear();
value.guaranteed = false;
value.sequence = Some(Sequence::Known(Vec::new()));
return true;
}
let mut nested = BTreeSet::new();
let mut changed = false;
for child in value.fields.values_mut().chain(value.variants.values_mut()) {
nested.extend(&child.tokens);
changed |= self.refresh_streams(child);
}
if let Some(sequence) = &mut value.sequence {
nested.extend(sequence.element().tokens);
match sequence {
Sequence::Known(items) => {
for item in items {
changed |= self.refresh_streams(item);
}
}
Sequence::Unknown(item) => changed |= self.refresh_streams(item),
}
}
if changed {
value.tokens.retain(|token| !nested.contains(token));
value.tokens.extend(
value
.fields
.values()
.chain(value.variants.values())
.flat_map(|child| child.tokens.iter().copied()),
);
if let Some(sequence) = &value.sequence {
value.tokens.extend(sequence.element().tokens);
}
value.guaranteed = false;
}
changed
}
}
+156
View File
@@ -0,0 +1,156 @@
//! 提交只保存处理承诺;正常 wait 才完成输入处理并引入输出错误义务。
//! stop 可能在工作开始前取消,不能把已存在的错误当成已经被子任务处理。
use super::State;
use crate::compiler::model::{failure::Deferred, *};
use crate::syntax::diagnostic::Diagnostic;
impl State<'_> {
pub(super) fn schedule(
&mut self,
operation: Schedule,
target: usize,
arguments: &[Expression],
) -> Result<Flow, Diagnostic> {
let signature = &self.signatures[target];
let mut mapping = self.call_inputs(arguments, signature, operation != Schedule::Run)?;
let before = self.pending.clone();
let inputs = mapping.values().flatten().copied().collect();
let output = if let Some(value) = &signature.returns {
self.instantiate(value, &mut mapping, 0)?
} else {
Flow::default()
};
if operation == Schedule::Run {
self.pending = before;
Ok(Flow {
task: Some(Box::new(Deferred {
inputs,
output,
failures: signature.failures.iter().copied().collect(),
})),
..Flow::default()
})
} else {
self.failures.extend(&signature.failures);
Ok(output)
}
}
pub(super) fn join(&mut self, task: &Expression, stop: bool) -> Result<Flow, Diagnostic> {
let value = self.expression(task)?;
let deferred = value.task.expect("checked affine handle");
self.failures.extend(&deferred.failures);
if !stop {
self.pending
.retain(|token| !deferred.inputs.contains(token));
}
self.pending.extend(&deferred.output.tokens);
Ok(deferred.output)
}
pub(super) fn timeout(
&mut self,
task: &Expression,
millis: &Expression,
fallback: usize,
) -> Result<Flow, Diagnostic> {
let flow = self.expression(task)?;
self.expression(millis)?;
let deferred = flow.task.expect("Task originates from run");
let Type::Task(target) = task.value_type else {
unreachable!("checked Task");
};
self.failures.extend(&self.signatures[target].failures);
let signature = &self.signatures[fallback];
self.failures.extend(&signature.failures);
let fallback = signature
.returns
.as_ref()
.map(|value| self.instantiate(value, &mut Default::default(), 0))
.transpose()?;
if fallback.is_none() {
self.pending
.retain(|token| !deferred.inputs.contains(token));
}
let mut output = self.signatures[target]
.returns
.as_ref()
.map(|_| deferred.output);
if let Some(fallback) = fallback {
if let Some(output) = &mut output {
output.merge(&fallback);
} else {
output = Some(fallback);
}
}
let output = output.unwrap_or_default();
self.pending.extend(&output.tokens);
Ok(output)
}
pub(super) fn race(&mut self, tasks: &[Expression]) -> Result<Flow, Diagnostic> {
let mut output: Option<Flow> = None;
let mut handled: Option<std::collections::BTreeSet<usize>> = None;
for task in tasks {
let value = self.expression(task)?;
let deferred = value.task.expect("Task originates from run");
let Type::Task(target) = task.value_type else {
unreachable!("checked Task");
};
self.failures.extend(&self.signatures[target].failures);
if self.signatures[target].returns.is_some() {
// 只有每个可能胜出的任务都处理的输入,才能在正常返回时解除义务。
if let Some(handled) = &mut handled {
handled.retain(|token| deferred.inputs.contains(token));
} else {
handled = Some(deferred.inputs);
}
if let Some(output) = &mut output {
output.merge(&deferred.output);
} else {
output = Some(deferred.output);
}
}
}
if let Some(handled) = handled {
self.pending.retain(|token| !handled.contains(token));
}
let output = output.unwrap_or_default();
self.pending.extend(&output.tokens);
Ok(output)
}
pub(super) fn group(&mut self, limit: &Expression) -> Result<Flow, Diagnostic> {
self.expression(limit)?;
Ok(Flow {
task: Some(Box::new(Deferred {
inputs: Default::default(),
output: Flow::default(),
failures: Default::default(),
})),
..Flow::default()
})
}
pub(super) fn group_run(
&mut self,
group: &Expression,
target: usize,
arguments: &[Expression],
) -> Result<Flow, Diagnostic> {
let Node::Variable(index) = group.kind else {
unreachable!("checked local Group");
};
let pending = self
.schedule(Schedule::Run, target, arguments)?
.task
.expect("checked run");
let group = self.values[index].task.as_mut().expect("checked Group");
group.inputs.extend(pending.inputs);
group.failures.extend(pending.failures);
// 提交时可能观察到已有子任务的失败;正常 wait 才兑现所有输入处理承诺。
self.failures.extend(&group.failures);
Ok(Flow::default())
}
}
+171
View File
@@ -0,0 +1,171 @@
//! 把选择器与只读配置访问加入已有特化和依赖图,不建立第二套调用分析。
use super::super::{application::port::Ports, failure};
use super::contract::Template;
use crate::compiler::model::{
adapter::{Adapters, Generated},
*,
};
use crate::syntax::{diagnostic::Diagnostic, source::Span};
use std::collections::HashMap;
type Targets = HashMap<(usize, String, usize), usize>;
type Settings = HashMap<(usize, String), usize>;
pub(super) fn templates(
templates: &mut Vec<Template>,
ports: &Ports,
layout: &Adapters,
definitions: &[Definition],
) -> (Targets, Settings) {
let mut targets = Targets::new();
let mut settings = Settings::new();
for operation in ports.operations.values() {
let port = layout
.ports
.iter()
.position(|port| port.source == operation.signature.source)
.unwrap();
let candidates = layout.ports[port]
.implementations
.iter()
.enumerate()
.map(|(index, implementation)| {
if implementation.external.is_some() {
let target = templates.len();
let mut signature = operation.signature.clone();
signature.failures.clone_from(&operation.failures);
signature.source = implementation.source;
signature.effects.file = true;
signature.effects.suspend = true;
signature.effects.schedule = true;
templates.push(Template {
generated: Some(Generated::External {
port,
implementation: index,
operation: operation.signature.name.clone(),
}),
span: operation.span,
arity: signature.inputs.len(),
signature,
group: Vec::new(),
handlers: Vec::new(),
});
return target;
}
let (_, name) = operation
.implementations
.iter()
.find(|(source, _)| *source == implementation.source)
.unwrap();
templates
.iter()
.position(|template| {
template.signature.source == implementation.source
&& template.signature.name == *name
})
.unwrap()
})
.collect::<Vec<_>>();
let index = if candidates.len() == 1 {
candidates[0]
} else {
let index = templates.len();
let mut signature = operation.signature.clone();
signature.failures.clone_from(&operation.failures);
templates.push(Template {
generated: Some(Generated::Dispatch {
port,
targets: candidates,
}),
span: operation.span,
arity: signature.inputs.len(),
signature,
group: Vec::new(),
handlers: Vec::new(),
});
index
};
targets.insert(
(
operation.signature.source,
operation.signature.name.clone(),
operation.signature.inputs.len(),
),
index,
);
}
for (port, item) in layout.ports.iter().enumerate() {
for (implementation, adapter) in item.implementations.iter().enumerate() {
if adapter.external.is_some() {
continue;
}
let Some(setting) = adapter.setting else {
continue;
};
let Shape::Record(fields) = &definitions[setting].shape else {
unreachable!()
};
for (field, (name, kind)) in fields.iter().enumerate() {
let index = templates.len();
settings.insert((adapter.source, name.clone()), index);
templates.push(Template {
generated: Some(Generated::Setting {
port,
implementation,
field,
}),
span: Span {
source: adapter.source,
start: 0,
end: 0,
},
arity: 0,
group: Vec::new(),
handlers: Vec::new(),
signature: Signature {
transaction: false,
name: format!("setting.{name}"),
source: adapter.source,
inputs: Vec::new(),
outputs: vec![(name.clone(), kind.clone())],
input_ranges: Vec::new(),
output_ranges: vec![None],
pure: true,
recovery: None,
static_parameters: 0,
failures: Vec::new(),
input_flows: Vec::new(),
returns: None,
effects: Effects::default(),
},
});
}
}
}
(targets, settings)
}
pub(in crate::compiler::check) fn check(
generated: &Generated,
signature: &mut Signature,
definitions: &[Definition],
signatures: &[Signature],
span: Span,
) -> Result<(), Diagnostic> {
failure::prototype(signature, definitions, span)?;
signature.effects.configuration = true;
if let Generated::Dispatch { targets, .. } = generated {
for target in targets {
signature.effects.include(signatures[*target].effects);
}
if signature.effects.suspend && (signature.effects.blocking || signature.effects.handler) {
return Err(Diagnostic::new(
"C013",
"Port candidates mix suspending and inline blocking effects; isolate blocking work inside its Adapter",
span,
));
}
}
Ok(())
}
+188
View File
@@ -0,0 +1,188 @@
//! 检查 handler 参数合同并从运行时签名中移除静态参数。
//! 参数类型、命名输出和公开类型边界都在特化前验证。
use super::super::{names, signature};
use crate::compiler::model::*;
use crate::syntax::{ast, diagnostic::Diagnostic, source::Span};
use std::collections::HashSet;
pub(super) struct Template {
pub generated: Option<crate::compiler::model::adapter::Generated>,
pub span: Span,
pub group: Vec<ast::Function>,
pub signature: Signature,
pub handlers: Vec<Parameter>,
pub arity: usize,
}
#[derive(Clone)]
pub(super) struct Parameter {
pub position: usize,
pub name: String,
pub inputs: Vec<(String, Type)>,
pub outputs: Vec<(String, Type)>,
pub input_ranges: Vec<Option<numeric::Range>>,
pub output_ranges: Vec<Option<numeric::Range>>,
pub span: Span,
}
pub(super) fn templates(
groups: Vec<Vec<ast::Function>>,
types: &super::super::package::Types,
definitions: &[Definition],
) -> Result<Vec<Template>, Diagnostic> {
let mut templates = Vec::new();
for mut group in groups {
let first = &group[0];
let arity = first.inputs.len();
let mut handlers = Vec::new();
for (position, input) in first.inputs.iter().enumerate() {
if let ast::Pattern::Handler(inputs, outputs) = &input.pattern {
let input_ranges = super::super::range::fields(inputs, types)?;
let output_ranges = super::super::range::fields(outputs, types)?;
let inputs = fields(inputs, types)?;
let outputs = fields(outputs, types)?;
for (_, kind) in inputs.iter().chain(&outputs) {
names::validate(kind, definitions, input.span)?;
if first.public {
types.public_type(kind, input.span)?;
}
}
distinct(
inputs.iter().chain(&outputs).map(|(name, _)| name),
input.span,
)?;
handlers.push(Parameter {
position,
name: input.name.clone(),
inputs,
outputs,
input_ranges,
output_ranges,
span: input.span,
});
}
}
for clause in &group {
distinct(
clause
.inputs
.iter()
.map(|input| &input.name)
.chain(clause.outputs.iter().map(|field| &field.name)),
clause.span,
)?;
for (position, input) in clause.inputs.iter().enumerate() {
let expected = handlers.iter().find(|handler| handler.position == position);
match (&input.pattern, expected) {
(ast::Pattern::Handler(inputs, outputs), Some(expected))
if input.name == expected.name
&& fields(inputs, types)? == expected.inputs
&& fields(outputs, types)? == expected.outputs
&& super::super::range::fields(inputs, types)?
== expected.input_ranges
&& super::super::range::fields(outputs, types)?
== expected.output_ranges => {}
(ast::Pattern::Handler(_, _), _) | (_, Some(_)) => {
return Err(Diagnostic::new(
"C013",
"handler clause signatures disagree",
input.span,
));
}
_ => {}
}
}
}
for clause in &mut group {
clause
.inputs
.retain(|input| !matches!(input.pattern, ast::Pattern::Handler(_, _)));
}
let mut signature = signature::check(&group, types, definitions)?;
signature.static_parameters = handlers.len();
templates.push(Template {
generated: None,
span: group[0].span,
group,
signature,
handlers,
arity,
});
}
Ok(templates)
}
fn fields(
fields: &[ast::Field],
types: &super::super::package::Types,
) -> Result<Vec<(String, Type)>, Diagnostic> {
fields
.iter()
.map(|field| {
names::identifier(&field.name, false, field.span)?;
if field.private
|| field.unique
|| field.index
|| !field.rest.is_empty()
|| !field.persistence.is_empty()
{
return Err(Diagnostic::new(
"C005",
"handler fields cannot carry visibility or storage metadata",
field.span,
));
}
Ok((field.name.clone(), names::resolve(&field.kind, types)?))
})
.collect()
}
fn distinct<'name>(
names: impl Iterator<Item = &'name String>,
span: Span,
) -> Result<(), Diagnostic> {
let mut seen = HashSet::new();
for name in names {
if !seen.insert(name) {
return Err(Diagnostic::new(
"C002",
"input and output names must be distinct",
span,
));
}
}
Ok(())
}
impl Parameter {
pub(super) fn accepts(&self, signature: &Signature) -> bool {
self.inputs
.iter()
.map(|(_, kind)| kind)
.eq(signature.inputs.iter().map(|(_, kind)| kind))
&& self.outputs == signature.outputs
}
pub(super) fn prototype(&self) -> Signature {
Signature {
transaction: false,
name: self.name.clone(),
inputs: self.inputs.clone(),
outputs: self.outputs.clone(),
input_ranges: self.input_ranges.clone(),
output_ranges: self.output_ranges.clone(),
source: self.span.source,
pure: false,
effects: Effects {
handler: true,
..Effects::default()
},
recovery: None,
static_parameters: 0,
failures: Vec::new(),
input_flows: Vec::new(),
returns: None,
}
}
}
+238
View File
@@ -0,0 +1,238 @@
//! 按静态函数绑定展开 handler 模板,并建立具体调用依赖。
//! 未使用模板仍通过原型检查,但这些验证实例不会输出为运行时代码。
pub(super) mod adapter;
mod contract;
mod resolve;
use super::names;
use crate::compiler::model::*;
use crate::syntax::{ast, diagnostic::Diagnostic, source::Span};
use contract::Template;
use std::collections::{HashMap, HashSet};
#[derive(Default)]
pub(super) struct Scope {
pub dependencies: Vec<usize>,
pub targets: HashMap<usize, usize>,
pub bindings: HashMap<String, usize>,
}
pub(super) struct Instance {
pub generated: Option<crate::compiler::model::adapter::Generated>,
pub group: Vec<ast::Function>,
pub scope: Scope,
pub signature: Signature,
pub prototype: bool,
pub span: Span,
}
pub(super) struct Expansion {
pub adapters: crate::compiler::model::adapter::Adapters,
pub instances: Vec<Instance>,
pub concrete: usize,
pub order: Vec<usize>,
pub calls: Vec<Vec<usize>>,
pub contracts: Vec<(usize, Signature, Span)>,
pub exports: Vec<Export>,
}
pub(super) struct Export {
pub index: usize,
pub handlers: Vec<(usize, Signature)>,
}
struct Expander<'types> {
templates: Vec<Template>,
instances: Vec<Instance>,
cache: HashMap<(usize, Vec<usize>), usize>,
cursor: usize,
types: &'types super::package::Types,
contracts: Vec<(usize, Signature, Span)>,
port_targets: HashMap<(usize, String, usize), usize>,
settings: HashMap<(usize, String), usize>,
settings_owners: HashSet<usize>,
}
pub(super) fn expand(
groups: Vec<Vec<ast::Function>>,
types: &super::package::Types,
definitions: &[Definition],
ports: &mut super::application::port::Ports,
) -> Result<Expansion, Diagnostic> {
let mut templates = contract::templates(groups, types, definitions)?;
ports.bind(
templates
.iter()
.map(|template| (template.group.as_slice(), &template.signature)),
types,
)?;
let mut adapters = ports.layout(types);
let (port_targets, settings) =
adapter::templates(&mut templates, ports, &adapters, definitions);
let mut expander = Expander {
templates,
instances: Vec::new(),
cache: HashMap::new(),
cursor: 0,
types,
contracts: Vec::new(),
port_targets,
settings,
settings_owners: ports.settings.keys().copied().collect(),
};
let mut exports = Vec::new();
for index in 0..expander.templates.len() {
if expander.templates[index].handlers.is_empty() {
let instance = expander.instance(index, Vec::new());
if let Some(export) = expander.export(index, instance) {
exports.push(export);
}
}
}
expander.resolve()?;
// 后续原型实例只用来验证未调用的模板,不能计入运行时代码。
let concrete = expander.instances.len();
let mut prototypes: Vec<usize> = Vec::new();
for index in 0..expander.templates.len() {
let template = &expander.templates[index];
if template.handlers.is_empty() {
continue;
}
let mut bindings = Vec::new();
for parameter in &template.handlers {
if let Some(index) = prototypes.iter().find(|index| {
let signature = &expander.instances[**index].signature;
parameter.accepts(signature)
&& parameter.input_ranges == signature.input_ranges
&& parameter.output_ranges == signature.output_ranges
}) {
bindings.push(*index);
continue;
}
let prototype = expander.instances.len();
bindings.push(prototype);
prototypes.push(prototype);
expander.instances.push(Instance {
generated: None,
group: Vec::new(),
scope: Scope::default(),
signature: parameter.prototype(),
prototype: true,
span: parameter.span,
});
}
let instance = expander.instance(index, bindings);
if let Some(export) = expander.export(index, instance) {
exports.push(export);
}
}
expander.resolve()?;
let edges = expander
.instances
.iter()
.map(|instance| {
let mut edges = instance.scope.targets.values().copied().collect::<Vec<_>>();
edges.extend(&instance.scope.dependencies);
edges.sort_unstable();
edges.dedup();
edges
})
.collect::<Vec<_>>();
let order = names::order(&edges).map_err(|index| {
Diagnostic::new(
"C007",
format!(
"recursive call involving '{}'",
expander.instances[index].signature.name
),
expander.instances[index].span,
)
})?;
adapters.functions = expander
.instances
.iter()
.enumerate()
.filter_map(|(index, instance)| {
instance
.generated
.clone()
.map(|generated| (index, generated))
})
.collect();
Ok(Expansion {
adapters,
instances: expander.instances,
concrete,
order,
calls: edges.into_iter().take(concrete).collect(),
contracts: expander.contracts,
exports,
})
}
impl Expander<'_> {
/// 每个公开声明只记录一次;模板使用原型效果,不混入某个调用方的具体 handler。
fn export(&self, template: usize, index: usize) -> Option<Export> {
let template = &self.templates[template];
(template
.group
.first()
.is_some_and(|function| function.public)
&& !crate::compiler::model::package::standard(
&self.types.packages[template.signature.source],
))
.then(|| Export {
index,
handlers: template
.handlers
.iter()
.map(|handler| (handler.position, handler.prototype()))
.collect(),
})
}
fn instance(&mut self, template: usize, handlers: Vec<usize>) -> usize {
// 相同模板和相同静态绑定只生成一次,不按调用次数复制函数体。
let key = (template, handlers);
if let Some(index) = self.cache.get(&key) {
return *index;
}
let definition = &self.templates[template];
let bindings = definition
.handlers
.iter()
.zip(&key.1)
.map(|(parameter, index)| (parameter.name.clone(), *index))
.collect();
let index = self.instances.len();
self.instances.push(Instance {
generated: None,
group: definition.group.clone(),
scope: Scope {
dependencies: Vec::new(),
targets: HashMap::new(),
bindings,
},
signature: definition.signature.clone(),
prototype: false,
span: definition.span,
});
self.cache.insert(key, index);
if let Some(mut generated) = self.templates[template].generated.clone() {
if let crate::compiler::model::adapter::Generated::Dispatch { targets, .. } =
&mut generated
{
for (position, target) in targets.iter_mut().enumerate() {
*target = self.instance(*target, Vec::new());
self.instances[index]
.scope
.targets
.insert(position, *target);
}
}
self.instances[index].generated = Some(generated);
}
index
}
}
+295
View File
@@ -0,0 +1,295 @@
//! 把具名函数及 handler 参数绑定到具体特化实例。
//! 只记录源码明确引用的包依赖,不把调用方传入的静态回调当作库的反向依赖。
use super::{Expander, Scope};
use crate::compiler::model::Operation;
use crate::syntax::{
ast::{self, ExpressionKind},
diagnostic::Diagnostic,
};
use std::collections::HashSet;
impl Expander<'_> {
pub(super) fn resolve(&mut self) -> Result<(), Diagnostic> {
while self.cursor < self.instances.len() {
let index = self.cursor;
self.cursor += 1;
let mut group = std::mem::take(&mut self.instances[index].group);
let mut scope = std::mem::take(&mut self.instances[index].scope);
for clause in &mut group {
let mut locals = clause
.inputs
.iter()
.map(|input| input.name.clone())
.chain(clause.outputs.iter().map(|output| output.name.clone()))
.collect::<HashSet<_>>();
for input in &clause.inputs {
if let ast::Pattern::Variant(_, bindings) = &input.pattern {
locals.extend(bindings.iter().filter(|name| name.as_str() != "_").cloned());
}
}
for statement in &mut clause.body {
match statement {
ast::Statement::Assign { target, value, .. } => {
if self.settings_owners.contains(&target.span.source)
&& target.path().is_some_and(|path| {
path == "setting" || path.starts_with("setting.")
})
{
return Err(Diagnostic::new(
"C005",
"Adapter setting is read-only",
target.span,
));
}
self.expression(value, &mut scope, &locals)?;
self.expression(target, &mut scope, &locals)?;
if let ExpressionKind::Name(name) = &target.kind {
if scope.bindings.contains_key(name) {
return Err(Diagnostic::new(
"C005",
"handler parameters cannot be assigned",
target.span,
));
}
locals.insert(name.clone());
}
}
ast::Statement::Action(value) => {
self.expression(value, &mut scope, &locals)?
}
}
}
}
self.instances[index].group = group;
self.instances[index].scope = scope;
}
Ok(())
}
fn expression(
&mut self,
value: &mut ast::Expression,
scope: &mut Scope,
locals: &HashSet<String>,
) -> Result<(), Diagnostic> {
if matches!(value.kind, ExpressionKind::Field(_, _))
&& let Some(path) = value.path()
&& let Some(field) = path.strip_prefix("setting.")
&& let Some(&template) = self.settings.get(&(value.span.source, field.to_owned()))
{
let index = self.instance(template, Vec::new());
scope.targets.insert(value.span.start, index);
value.kind = ExpressionKind::Call(
Box::new(ast::Expression {
kind: ExpressionKind::Name(path),
span: value.span,
depth: 1,
}),
Vec::new(),
);
return Ok(());
}
match &mut value.kind {
ExpressionKind::Call(target, arguments) => {
if target.path().as_deref() == Some("dever.test.drainJobs")
&& self.types.test_source == Some(value.span.source)
{
let templates = self
.templates
.iter()
.enumerate()
.filter_map(|(index, template)| {
matches!(
crate::compiler::model::package::role(
&self.types.packages[template.signature.source]
),
Some((_, "job"))
)
.then_some(index)
})
.collect::<Vec<_>>();
for template in templates {
scope.dependencies.push(self.instance(template, Vec::new()));
}
for argument in arguments {
self.expression(argument, scope, locals)?;
}
return Ok(());
}
if matches!(
target.path().as_deref(),
Some("dever.job.enqueue" | "dever.job.enqueueAt")
) {
for argument in arguments.iter_mut().skip(1) {
self.expression(argument, scope, locals)?;
}
return Ok(());
}
if let Some(name) = target.path() {
if let Some(index) = scope.bindings.get(&name) {
// 回调来自调用方绑定;此处没有库对调用方包的源码依赖。
if self.instances[*index].signature.inputs.len() != arguments.len() {
return Err(Diagnostic::new(
"C005",
"handler call argument count differs",
value.span,
));
}
scope.targets.insert(value.span.start, *index);
} else if let Some(template) =
self.template(&name, value.span, arguments.len())?
{
let parameters = self.templates[template].handlers.clone();
let mut handlers = Vec::new();
for parameter in &parameters {
let argument = &arguments[parameter.position];
let index =
self.named(argument, parameter.inputs.len(), scope, locals)?;
if !parameter.accepts(&self.instances[index].signature) {
return Err(Diagnostic::new(
"C005",
"handler input types or named outputs do not match",
argument.span,
));
}
handlers.push(index);
self.contracts
.push((index, parameter.prototype(), argument.span));
}
let index = self.instance(template, handlers);
scope.targets.insert(value.span.start, index);
for parameter in parameters.iter().rev() {
arguments.remove(parameter.position);
}
} else if (name == "dever.system.httpServe" && arguments.len() == 3)
|| (name == "dever.system.httpServeTls" && arguments.len() == 4)
{
let handler = &arguments[0];
let index = self.named(handler, 1, scope, locals)?;
scope.targets.insert(handler.span.start, index);
} else if (name == "dever.system.httpServeLive"
&& matches!(arguments.len(), 4 | 5))
|| (name == "dever.system.httpServeLiveTls"
&& matches!(arguments.len(), 5 | 6))
{
let base = if name.ends_with("Tls") { 5 } else { 4 };
let handler = &arguments[0];
let index = self.named(
handler,
2 + usize::from(arguments.len() > base),
scope,
locals,
)?;
scope.targets.insert(handler.span.start, index);
} else if name == "timeout" && arguments.len() == 3 {
let fallback = &arguments[2];
let index = self.named(fallback, 0, scope, locals)?;
scope.targets.insert(fallback.span.start, index);
} else if let Some(operation) = Operation::named(&name)
&& let Some(arity) =
super::super::collection::handler_arity(operation, arguments.len())
{
let target = &arguments[0];
let index = self.named(target, arity, scope, locals)?;
scope.targets.insert(target.span.start, index);
}
}
self.expression(target, scope, locals)?;
for argument in arguments {
self.expression(argument, scope, locals)?;
}
}
ExpressionKind::Channel(_, base)
| ExpressionKind::Field(base, _)
| ExpressionKind::Unary(_, base) => self.expression(base, scope, locals)?,
ExpressionKind::Binary(_, left, right) => {
self.expression(left, scope, locals)?;
self.expression(right, scope, locals)?;
}
ExpressionKind::List(values) => {
for value in values {
self.expression(value, scope, locals)?;
}
}
ExpressionKind::Map(values) => {
for (key, value) in values {
self.expression(key, scope, locals)?;
self.expression(value, scope, locals)?;
}
}
ExpressionKind::Record(_, fields) => {
for (_, value) in fields {
self.expression(value, scope, locals)?;
}
}
ExpressionKind::Literal(_) | ExpressionKind::Name(_) => {}
}
Ok(())
}
fn named(
&mut self,
value: &ast::Expression,
arity: usize,
scope: &Scope,
locals: &HashSet<String>,
) -> Result<usize, Diagnostic> {
let name = value.path().ok_or_else(|| {
Diagnostic::new(
"C005",
"handler must be a named function or static parameter",
value.span,
)
})?;
if let Some(&index) = scope.bindings.get(&name) {
if self.instances[index].signature.inputs.len() == arity {
return Ok(index);
}
} else if !locals.contains(&name)
&& let Some(template) = self.template(&name, value.span, arity)?
&& self.templates[template].handlers.is_empty()
{
return Ok(self.instance(template, Vec::new()));
}
Err(Diagnostic::new(
"C005",
"handler name or input count does not match; runtime values are not handlers",
value.span,
))
}
fn template(
&self,
name: &str,
span: crate::syntax::source::Span,
arity: usize,
) -> Result<Option<usize>, Diagnostic> {
let Some((source, name)) = self.types.target(name, span.source) else {
return Ok(None);
};
if let Some(&target) = self.port_targets.get(&(source, name.to_owned(), arity)) {
self.types.access(span.source, source, true, span)?;
return Ok(Some(target));
}
let Some(index) = self.templates.iter().position(|template| {
template.generated.is_none()
&& self.types.owner(template.signature.source) == source
&& template.signature.name == name
&& template.arity == arity
}) else {
return Ok(None);
};
self.types.access(span.source, source, true, span)?;
if !self.types.private_access(span.source, source) && !self.templates[index].group[0].public
{
return Err(Diagnostic::new(
"C006",
"function is private to its package",
span,
));
}
self.types.dependency(span.source, source, span);
Ok(Some(index))
}
}
+165
View File
@@ -0,0 +1,165 @@
//! Live handler 沿静态特化接线;共享 Context 是普通可转移值,不携带应用身份。
use super::*;
impl Context<'_> {
fn live_named(&self, name: &str, source: &ast::Expression) -> Result<Type, Diagnostic> {
self.definition_names
.resolve(name, self.source, source.span)?
.map(Type::Named)
.ok_or_else(|| self.error(source, "missing HTTP live type"))
}
pub(in crate::compiler::check) fn http_live(
&mut self,
tls: bool,
arguments: &[ast::Expression],
expected: Option<&Type>,
source: &ast::Expression,
) -> Result<Expression, Diagnostic> {
let base = 4 + usize::from(tls);
if !matches!(arguments.len().checked_sub(base), Some(0 | 1)) {
return Err(self.error(source, "HTTP live argument count differs"));
}
let context = arguments.len() > base;
if self.definition_names.packages[self.source] != "dever.http" && !context {
return Err(Diagnostic::new(
"C006",
"HTTP live intrinsics belong to dever.http",
source.span,
));
}
let handler = self
.scope
.targets
.get(&arguments[0].span.start)
.copied()
.ok_or_else(|| self.error(source, "HTTP live requires a static handler"))?;
let request = self.live_named("dever.http.Request", source)?;
let mut inputs = vec![
Type::Listener,
self.live_named("dever.http.Limits", source)?,
self.http_record(
"dever.http.LiveLimits",
&[
("chunk_bytes", Type::Int),
("idle_ms", Type::Int),
("heartbeat_ms", Type::Int),
],
source,
)?,
];
if tls {
inputs.push(Type::ServerTls);
}
let mut values = arguments[1..base]
.iter()
.zip(inputs)
.map(|(value, kind)| self.expression(value, Some(&kind)))
.collect::<Result<Vec<_>, _>>()?;
let mut handler_inputs = vec![request, Type::HttpReply];
if context {
let value = self.expression(&arguments[base], None)?;
if !value.value_type.transferable(self.definitions) {
return Err(self.error(source, "HTTP context must be transferable"));
}
handler_inputs.push(value.value_type.clone());
values.push(value);
}
let signature = &self.signatures[handler];
if signature
.inputs
.iter()
.map(|(_, kind)| kind)
.ne(handler_inputs.iter())
|| !signature.outputs.is_empty()
{
return Err(self.error(
source,
"HTTP live handler requires Request, HttpReply and matching Context with no output",
));
}
if signature.effects.request {
return Err(Diagnostic::new(
"C014",
"raw HTTP handlers do not carry application identity",
source.span,
));
}
if signature.effects.blocking {
return Err(Diagnostic::new(
"C013",
"HTTP live handler cannot block the protocol driver",
source.span,
));
}
self.coerce(
Expression {
kind: Node::Http(HttpOperation::ServeLive(handler, tls), values),
value_type: Type::Unit,
span: source.span,
},
expected,
)
}
pub(in crate::compiler::check) fn http_reply(
&mut self,
operation: HttpOperation,
arguments: &[ast::Expression],
expected: Option<&Type>,
source: &ast::Expression,
) -> Result<Expression, Diagnostic> {
let sse = matches!(operation, HttpOperation::SseStart | HttpOperation::SseSend);
if self.definition_names.packages[self.source]
!= if sse { "dever.sse" } else { "dever.http" }
{
return Err(Diagnostic::new(
"C006",
"HTTP reply intrinsics belong to their official package",
source.span,
));
}
let headers = Type::List(Box::new(self.live_named("dever.http.Header", source)?));
let mut inputs = vec![Type::HttpReply];
match operation {
HttpOperation::Respond => inputs.push(self.live_named("dever.http.Response", source)?),
HttpOperation::Start => inputs.extend([Type::Int, headers]),
HttpOperation::Write => inputs.push(Type::Bytes),
HttpOperation::Finish => (),
HttpOperation::SseStart => inputs.push(headers),
HttpOperation::SseSend => inputs.push(self.http_record(
"Event",
&[
("event", Type::Text),
("data", Type::Text),
("id", Type::Optional(Box::new(Type::Text))),
("retry_ms", Type::Optional(Box::new(Type::Int))),
],
source,
)?),
_ => unreachable!("reply operation"),
}
if inputs.len() != arguments.len() {
return Err(self.error(source, "HTTP reply argument count differs"));
}
let values = arguments
.iter()
.zip(inputs)
.map(|(value, kind)| self.expression(value, Some(&kind)))
.collect::<Result<Vec<_>, _>>()?;
let value_type = if matches!(operation, HttpOperation::Finish) {
self.io_result("dever.io.CloseResult", &[("Closed", vec![])], source)?
} else {
self.io_result("dever.io.WriteResult", &[("Done", vec![])], source)?
};
self.coerce(
Expression {
kind: Node::Http(operation, values),
value_type,
span: source.span,
},
expected,
)
}
}
+160
View File
@@ -0,0 +1,160 @@
//! HTTP 标准原语只接收固定名义记录;handler 继续使用已有静态特化。
use super::Context;
use crate::compiler::model::*;
use crate::syntax::{ast, diagnostic::Diagnostic};
mod live;
mod pool;
mod websocket;
impl Context<'_> {
pub(super) fn http(
&mut self,
serve: bool,
tls: bool,
arguments: &[ast::Expression],
expected: Option<&Type>,
source: &ast::Expression,
) -> Result<Expression, Diagnostic> {
if self.definition_names.packages[self.source] != "dever.http" {
return Err(Diagnostic::new(
"C006",
"HTTP intrinsics belong to the official dever.http package",
source.span,
));
}
if arguments.len() != if serve { 3 + usize::from(tls) } else { 4 } {
return Err(self.error(source, "HTTP argument count differs"));
}
let header = self.http_record(
"Header",
&[("name", Type::Text), ("value", Type::Bytes)],
source,
)?;
let headers = Type::List(Box::new(header));
let request = self.http_record(
"Request",
&[
("method", Type::Text),
("target", Type::Text),
("headers", headers.clone()),
("body", Type::Bytes),
],
source,
)?;
let response = self.http_record(
"Response",
&[
("status", Type::Int),
("headers", headers),
("body", Type::Bytes),
],
source,
)?;
let http2 = self.http_record(
"Http2Limits",
&[
("streams", Type::Int),
("stream_window_bytes", Type::Int),
("connection_window_bytes", Type::Int),
],
source,
)?;
let limits = self.http_record(
"Limits",
&[
("header_bytes", Type::Int),
("body_bytes", Type::Int),
("timeout_ms", Type::Int),
("connections", Type::Int),
("http2", Type::Optional(Box::new(http2))),
],
source,
)?;
let (operation, inputs, arguments, value_type) = if serve {
let handler = self
.scope
.targets
.get(&arguments[0].span.start)
.copied()
.ok_or_else(|| self.error(source, "HTTP route must be a static handler"))?;
let signature = &self.signatures[handler];
if signature.effects.request {
return Err(Diagnostic::new(
"C014",
"raw HTTP handlers do not carry application identity",
source.span,
));
}
if signature.inputs.len() != 1
|| signature.inputs[0].1 != request
|| signature.outputs != [("response".into(), response)]
{
return Err(self.error(
source,
"HTTP handler requires Request and named response: Response",
));
}
if signature.effects.blocking {
return Err(Diagnostic::new(
"C013",
"HTTP handler cannot block the protocol driver; isolate blocking calls",
source.span,
));
}
let mut inputs = vec![Type::Listener, limits];
if tls {
inputs.push(Type::ServerTls);
}
(
HttpOperation::Serve(handler, tls),
inputs,
&arguments[1..],
Type::Unit,
)
} else {
let result = self.io_result(
"ResponseResult",
&[("Read", vec![("response", response)])],
source,
)?;
(
HttpOperation::Send,
vec![Type::Text, Type::Int, request, limits],
arguments,
result,
)
};
let values = arguments
.iter()
.zip(inputs)
.map(|(value, kind)| self.expression(value, Some(&kind)))
.collect::<Result<Vec<_>, _>>()?;
self.coerce(
Expression {
kind: Node::Http(operation, values),
value_type,
span: source.span,
},
expected,
)
}
fn http_record(
&self,
name: &str,
fields: &[(&str, Type)],
source: &ast::Expression,
) -> Result<Type, Diagnostic> {
let index = self
.definition_names
.resolve(name, self.source, source.span)?
.ok_or_else(|| self.error(source, "missing HTTP record"))?;
let valid = matches!(&self.definitions[index].shape, Shape::Record(actual) if actual.iter().map(|(name, kind)| (name.as_str(), kind)).eq(fields.iter().map(|(name, kind)| (*name, kind))));
if !valid {
return Err(self.error(source, "HTTP record differs from its intrinsic contract"));
}
Ok(Type::Named(index))
}
}
+128
View File
@@ -0,0 +1,128 @@
//! 连接池原语沿用 HTTP 的名义记录和失败集合,不向业务暴露协议驱动。
use super::*;
impl Context<'_> {
pub(in crate::compiler::check) fn http_pool(
&mut self,
operation: HttpOperation,
arguments: &[ast::Expression],
expected: Option<&Type>,
source: &ast::Expression,
) -> Result<Expression, Diagnostic> {
if self.definition_names.packages[self.source] != "dever.http" {
return Err(Diagnostic::new(
"C006",
"HTTP intrinsics belong to dever.http",
source.span,
));
}
let named = |name| {
self.definition_names
.resolve(name, self.source, source.span)
.and_then(|index| {
index
.map(Type::Named)
.ok_or_else(|| self.error(source, "missing HTTP type"))
})
};
let (inputs, value_type) = match operation {
HttpOperation::Client => (
vec![
Type::Text,
Type::ClientTls,
named("Limits")?,
self.http_record(
"PoolLimits",
&[
("idle_ms", Type::Int),
("chunk_bytes", Type::Int),
("read_ms", Type::Int),
],
source,
)?,
],
self.io_result(
"ClientResult",
&[("Ready", vec![("client", Type::HttpClient)])],
source,
)?,
),
HttpOperation::Request => (
vec![Type::HttpClient, named("Request")?],
self.io_result(
"ResponseResult",
&[("Read", vec![("response", named("Response")?)])],
source,
)?,
),
HttpOperation::Open | HttpOperation::OpenStream => {
let event = self.io_result(
"dever.io.ReadEvent",
&[("Chunk", vec![("bytes", Type::Bytes)])],
source,
)?;
let body = Type::AsyncStream(Box::new(event));
let header = self.http_record(
"Header",
&[("name", Type::Text), ("value", Type::Bytes)],
source,
)?;
let headers = Type::List(Box::new(header));
let response = self.http_record(
"StreamResponse",
&[
("status", Type::Int),
("headers", headers.clone()),
("body", body.clone()),
],
source,
)?;
let (name, body) = if matches!(operation, HttpOperation::OpenStream) {
("StreamRequest", body)
} else {
("Request", Type::Bytes)
};
let request = self.http_record(
name,
&[
("method", Type::Text),
("target", Type::Text),
("headers", headers),
("body", body),
],
source,
)?;
(
vec![Type::HttpClient, request],
self.io_result(
"StreamResponseResult",
&[("Read", vec![("response", response)])],
source,
)?,
)
}
HttpOperation::Close => (
vec![Type::HttpClient],
self.io_result("dever.io.CloseResult", &[("Closed", vec![])], source)?,
),
_ => unreachable!(),
};
if arguments.len() != inputs.len() {
return Err(self.error(source, "HTTP pool argument count differs"));
}
let values = arguments
.iter()
.zip(inputs)
.map(|(value, kind)| self.expression(value, Some(&kind)))
.collect::<Result<Vec<_>, _>>()?;
self.coerce(
Expression {
kind: Node::Http(operation, values),
value_type,
span: source.span,
},
expected,
)
}
}
+104
View File
@@ -0,0 +1,104 @@
//! 标准包保留原有名义消息与失败合同;用户不能直接调用系统原语。
use super::*;
impl Context<'_> {
pub(in crate::compiler::check) fn websocket(
&mut self,
operation: HttpOperation,
arguments: &[ast::Expression],
expected: Option<&Type>,
source: &ast::Expression,
) -> Result<Expression, Diagnostic> {
if self.definition_names.packages[self.source] != "dever.websocket" {
return Err(Diagnostic::new(
"C006",
"WebSocket intrinsics belong to dever.websocket",
source.span,
));
}
let limits = self.http_record(
"Limits",
&[
("message_bytes", Type::Int),
("idle_ms", Type::Int),
("write_ms", Type::Int),
],
source,
)?;
let message = self.websocket_message(source)?;
let inputs = match operation {
HttpOperation::WsAccept => vec![Type::HttpReply, limits],
HttpOperation::WsConnect => vec![Type::Text, Type::Int, Type::Text, limits],
HttpOperation::WsOpen => vec![Type::Text, Type::ClientTls, limits],
HttpOperation::WsSend => vec![Type::WebSocket, message.clone()],
HttpOperation::WsReceive | HttpOperation::WsMessages => vec![Type::WebSocket],
HttpOperation::WsClose => vec![Type::WebSocket, Type::Int, Type::Text],
_ => unreachable!("WebSocket operation"),
};
if arguments.len() != inputs.len() {
return Err(self.error(source, "WebSocket argument count differs"));
}
let values = arguments
.iter()
.zip(inputs)
.map(|(argument, kind)| self.expression(argument, Some(&kind)))
.collect::<Result<Vec<_>, _>>()?;
let value_type = match operation {
HttpOperation::WsAccept | HttpOperation::WsConnect | HttpOperation::WsOpen => self
.io_result(
"ConnectResult",
&[("Connected", vec![("socket", Type::WebSocket)])],
source,
)?,
HttpOperation::WsSend => {
self.io_result("dever.io.WriteResult", &[("Done", vec![])], source)?
}
HttpOperation::WsReceive => self.io_result(
"SystemReceiveResult",
&[("Read", vec![("message", message)]), ("End", vec![])],
source,
)?,
HttpOperation::WsMessages => Type::AsyncStream(Box::new(self.io_result(
"MessageEvent",
&[("Read", vec![("message", message)])],
source,
)?)),
HttpOperation::WsClose => {
self.io_result("dever.io.CloseResult", &[("Closed", vec![])], source)?
}
_ => unreachable!("WebSocket operation"),
};
self.coerce(
Expression {
kind: Node::Http(operation, values),
value_type,
span: source.span,
},
expected,
)
}
fn websocket_message(&self, source: &ast::Expression) -> Result<Type, Diagnostic> {
let index = self
.definition_names
.resolve("Message", self.source, source.span)?
.ok_or_else(|| self.error(source, "missing WebSocket message type"))?;
let expected = [
("Text", Type::Text),
("Binary", Type::Bytes),
("Ping", Type::Bytes),
("Pong", Type::Bytes),
];
if !matches!(&self.definitions[index].shape, Shape::Choice(variants)
if variants.len() == expected.len() && variants.iter().zip(expected).all(|(variant, (name, kind))|
!variant.error && variant.name == name && variant.fields == [("value".into(), kind)]))
{
return Err(self.error(
source,
"WebSocket message type differs from its intrinsic contract",
));
}
Ok(Type::Named(index))
}
}
+111
View File
@@ -0,0 +1,111 @@
//! 只向嵌入的官方库开放文件原语,并核对其名义结果合同。
//! 用户通过 Dever 库取得成功值、传播失败或使用 result 显式处理。
use super::Context;
use crate::compiler::model::*;
use crate::syntax::{ast, diagnostic::Diagnostic};
impl Context<'_> {
pub(super) fn io(
&mut self,
operation: IoOperation,
arguments: &[ast::Expression],
expected: Option<&Type>,
source: &ast::Expression,
) -> Result<Expression, Diagnostic> {
let owner = if matches!(operation, IoOperation::Arguments) {
"dever.process"
} else {
"dever.io"
};
if self.definition_names.packages[self.source] != owner {
return Err(Diagnostic::new(
"C006",
format!("I/O intrinsic belongs to the official {owner} package"),
source.span,
));
}
let inputs = match operation {
IoOperation::Arguments => vec![],
IoOperation::Open | IoOperation::Create | IoOperation::Stdout => vec![Type::Text],
IoOperation::Read | IoOperation::Chunks => vec![Type::File, Type::Int],
IoOperation::Write => vec![Type::File, Type::Bytes],
IoOperation::Close => vec![Type::File],
};
if inputs.len() != arguments.len() {
return Err(self.error(source, "I/O operation argument count differs"));
}
let values = arguments
.iter()
.zip(inputs)
.map(|(argument, kind)| self.expression(argument, Some(&kind)))
.collect::<Result<Vec<_>, _>>()?;
let value_type = match operation {
IoOperation::Arguments => self.io_result(
"ArgumentsResult",
&[("Read", vec![("values", Type::List(Box::new(Type::Text)))])],
source,
)?,
IoOperation::Open | IoOperation::Create => self.io_result(
"OpenResult",
&[("Opened", vec![("file", Type::File)])],
source,
)?,
IoOperation::Read => self.io_result(
"SystemReadResult",
&[("Read", vec![("bytes", Type::Bytes)]), ("End", vec![])],
source,
)?,
IoOperation::Write => self.io_result("WriteResult", &[("Done", vec![])], source)?,
IoOperation::Close => self.io_result("CloseResult", &[("Closed", vec![])], source)?,
IoOperation::Stdout => Type::Unit,
IoOperation::Chunks => {
let item = self.io_result(
"ReadEvent",
&[("Chunk", vec![("bytes", Type::Bytes)])],
source,
)?;
self.io_result(
"ReadStreamResult",
&[("Streaming", vec![("stream", Type::Stream(Box::new(item)))])],
source,
)?
}
};
self.coerce(
Expression {
kind: Node::Io(operation, values),
value_type,
span: source.span,
},
expected,
)
}
pub(super) fn io_result(
&self,
name: &str,
success: &[(&str, Vec<(&str, Type)>)],
source: &ast::Expression,
) -> Result<Type, Diagnostic> {
let index = self
.definition_names
.resolve(name, self.source, source.span)?
.ok_or_else(|| self.error(source, "missing I/O result type"))?;
let mut expected = success.to_vec();
expected.push(("Failed", vec![("message", Type::Text)]));
let valid = matches!(&self.definitions[index].shape, Shape::Choice(variants)
if variants.len() == expected.len() && variants.iter().zip(&expected).enumerate().all(|(index, (variant, (name, fields)))| {
variant.name == *name && variant.error == (index == success.len())
&& variant.fields.iter().map(|(name, kind)| (name.as_str(), kind))
.eq(fields.iter().map(|(name, kind)| (*name, kind)))
}));
if !valid {
return Err(self.error(
source,
"I/O result type does not match its intrinsic contract",
));
}
Ok(Type::Named(index))
}
}
+63
View File
@@ -0,0 +1,63 @@
//! 入队只检查目标与 payload,不沿被排队 handler 传播同步效果或失败。
use super::super::Context;
use crate::{
compiler::model::*,
syntax::{ast, diagnostic::Diagnostic},
};
impl Context<'_> {
pub(in crate::compiler::check) fn enqueue(
&mut self,
at: bool,
arguments: &[ast::Expression],
expected: Option<&Type>,
source: &ast::Expression,
) -> Result<Expression, Diagnostic> {
let Some((domain, "app")) = package::role(&self.definition_names.packages[self.source])
else {
return Err(Diagnostic::new(
"C006",
"persistent enqueue belongs to App",
source.span,
));
};
let target = arguments
.first()
.and_then(ast::Expression::path)
.filter(|name| name.starts_with("job."))
.ok_or_else(|| self.error(source, "enqueue requires a statically named local Job"))?;
let identity = format!("{domain}.{target}");
let job = self
.jobs
.iter()
.position(|job| job.name == identity)
.ok_or_else(|| self.error(source, "unknown Job"))?;
let entry = &self.jobs[job];
let mut kinds = entry.payload.clone().into_iter().collect::<Vec<_>>();
kinds.push(Type::Text);
if at {
kinds.push(Type::DateTime);
}
if arguments.len() != kinds.len() + 1 {
return Err(self.error(source, "enqueue payload/key/time argument count differs"));
}
let values = arguments[1..]
.iter()
.zip(&kinds)
.map(|(value, kind)| self.expression(value, Some(kind)))
.collect::<Result<Vec<_>, _>>()?;
let error = self
.definition_names
.resolve("dever.database.Error", self.source, source.span)?
.ok_or_else(|| self.error(source, "database error is missing"))?;
self.coerce(
Expression {
kind: Node::Enqueue(job::Enqueue { job, at, error }, values),
value_type: Type::Id,
span: source.span,
},
expected,
)
}
}
+173
View File
@@ -0,0 +1,173 @@
//! Job 只适配同领域 App;入队目标单独登记,不把它加入同步调用依赖。
mod enqueue;
use super::{application, handler::Expansion, names, package::Types};
use crate::{
compiler::model::{self, *},
syntax::{ast, diagnostic::Diagnostic},
};
use std::collections::HashSet;
pub(super) fn prepare(job: &ast::Job) -> Result<(), Diagnostic> {
let function = &job.function;
let span = function.span;
names::function(&function.name, span)?;
if function.inputs.len() > 1
|| !function.outputs.is_empty()
|| !(1..=100).contains(&job.retries)
|| !(1..=3600000).contains(&job.timeout_ms)
{
return Err(Diagnostic::new(
"C014",
"Job requires zero/one payload, no output, retry 1..100 and timeout 1..3600000",
span,
));
}
for input in &function.inputs {
names::identifier(&input.name, false, input.span)?;
}
for statement in &function.body {
let ast::Statement::Action(value) = statement else {
return Err(Diagnostic::new(
"C014",
"Job body only adapts App calls",
span,
));
};
let ast::ExpressionKind::Call(target, _) = &value.kind else {
return Err(Diagnostic::new(
"C014",
"Job body only adapts App calls",
value.span,
));
};
if !target.path().is_some_and(|target| {
target
.strip_prefix("app.")
.is_some_and(|name| !name.contains('.'))
}) {
return Err(Diagnostic::new(
"C014",
"Job body only adapts App calls",
value.span,
));
}
}
Ok(())
}
pub(super) fn entries(
jobs: &[ast::Job],
signatures: &[Signature],
definitions: &[Definition],
names: &Types,
bindings: &[Option<String>],
) -> Result<Vec<model::job::Entry>, Diagnostic> {
let mut result = Vec::new();
let mut seen = HashSet::new();
for job in jobs {
let span = job.function.span;
let function = signatures
.iter()
.position(|signature| {
signature.source == span.source && signature.name == job.function.name
})
.unwrap();
let signature = &signatures[function];
let database = bindings[span.source]
.clone()
.ok_or_else(|| Diagnostic::new("C014", "Job source requires database binding", span))?;
let payload = signature.inputs.first().map(|(_, kind)| kind.clone());
if let Some(kind) = &payload {
let Type::Named(index) = kind else {
return Err(Diagnostic::new(
"C005",
"Job payload requires its own App record",
span,
));
};
let owner = package::role(&names.packages[definitions[*index].source]);
let domain = package::role(&names.packages[span.source]).unwrap().0;
if owner != Some((domain, "app")) {
return Err(Diagnostic::new(
"C006",
"Job payload belongs to its own App",
span,
));
}
application::wire_type(kind, definitions, span)?;
}
let name = format!("{}.{}", names.packages[span.source], signature.name);
if !seen.insert(name.clone()) {
return Err(Diagnostic::new("C002", "duplicate Job identity", span));
}
result.push(model::job::Entry {
name,
function,
database,
payload,
retries: job.retries,
timeout_ms: job.timeout_ms,
long: job.long,
});
}
Ok(result)
}
pub(super) fn schedules(
declarations: &[ast::Schedule],
jobs: &[model::job::Entry],
signatures: &[Signature],
names: &Types,
) -> Result<Vec<model::job::Schedule>, Diagnostic> {
let mut result = Vec::new();
let mut seen = HashSet::new();
for declaration in declarations {
let span = declaration.span;
let name = format!("{}.{}", names.packages[span.source], declaration.target);
let job = jobs
.iter()
.position(|job| job.name == name)
.ok_or_else(|| {
Diagnostic::new("C005", "schedule requires a Job in the same file", span)
})?;
if signatures[jobs[job].function].source != span.source
|| jobs[job].payload.is_some()
|| !seen.insert(job)
{
return Err(Diagnostic::new(
"C014",
"schedule requires one zero-input Job in the same file",
span,
));
}
dever_runtime::time::cron::Cron::parse(&declaration.cron)
.map_err(|message| Diagnostic::new("C014", message, span))?;
result.push(model::job::Schedule {
job,
cron: declaration.cron.clone(),
});
}
Ok(result)
}
pub(super) fn effects(
jobs: &[model::job::Entry],
signatures: &[Signature],
expansion: &Expansion,
) -> Result<(), Diagnostic> {
for job in jobs {
let effects = signatures[job.function].effects;
if effects.request
|| (job.long && effects.unscoped_write)
|| (!job.long && effects.database_write && effects.transaction_unsafe())
{
return Err(Diagnostic::new(
"C014",
"Job cannot access requests or mix transactions with external work",
expansion.instances[job.function].span,
));
}
}
Ok(())
}
+208
View File
@@ -0,0 +1,208 @@
//! 校验官方 JSON 包的边界操作和固定失败类型。
//! Json 是不透明值;业务代码不能直接接触动态节点表。
use super::Context;
use crate::compiler::model::*;
use crate::syntax::{ast, diagnostic::Diagnostic};
impl Context<'_> {
pub(super) fn decode_json(
&mut self,
arguments: &[ast::Expression],
expected: Option<&Type>,
source: &ast::Expression,
) -> Result<Expression, Diagnostic> {
let [input] = arguments else {
return Err(self.error(source, "dever.json.decode requires one Json argument"));
};
let kind = expected.ok_or_else(|| {
self.error(source, "dever.json.decode requires a concrete output type")
})?;
super::application::wire_type(kind, self.definitions, source.span)?;
let Type::Named(error) = self.result(
"dever.json.JsonResult",
"Parsed",
"value",
Type::Json,
source,
)?
else {
unreachable!()
};
let input = self.expression(input, Some(&Type::Json))?;
Ok(Expression {
kind: Node::Json(JsonOperation::Decode { error }, vec![input]),
value_type: kind.clone(),
span: source.span,
})
}
pub(super) fn json(
&mut self,
operation: JsonOperation,
arguments: &[ast::Expression],
expected: Option<&Type>,
source: &ast::Expression,
) -> Result<Expression, Diagnostic> {
if self.definition_names.packages[self.source] != "dever.json" {
return Err(Diagnostic::new(
"C006",
"JSON intrinsics belong to the official dever.json package",
source.span,
));
}
let inputs = match operation {
JsonOperation::FromText => vec![Type::Text],
JsonOperation::ToText | JsonOperation::AsText | JsonOperation::AsInteger => {
vec![Type::Json]
}
JsonOperation::SetProperty => vec![Type::Json, Type::Text, Type::Json],
JsonOperation::Property => vec![Type::Json, Type::Text],
JsonOperation::Encode | JsonOperation::Decode { .. } => {
unreachable!("concrete JSON calls are checked at the call site")
}
JsonOperation::Object => vec![Type::Map(Box::new(Type::Text), Box::new(Type::Json))],
JsonOperation::Parse => vec![Type::Text],
JsonOperation::Stringify => vec![self.json_type("Document", source)?],
JsonOperation::Node => vec![self.json_type("Document", source)?, Type::Int],
JsonOperation::DocumentProperty => {
vec![self.json_type("Document", source)?, Type::Int, Type::Text]
}
JsonOperation::Element => {
vec![self.json_type("Document", source)?, Type::Int, Type::Int]
}
};
if inputs.len() != arguments.len() {
return Err(self.error(source, "JSON operation argument count differs"));
}
let values = arguments
.iter()
.zip(inputs)
.map(|(value, kind)| self.expression(value, Some(&kind)))
.collect::<Result<Vec<_>, _>>()?;
let value_type = match operation {
JsonOperation::Decode { .. } => {
unreachable!("decode is checked at the concrete call site")
}
JsonOperation::FromText
| JsonOperation::SetProperty
| JsonOperation::Encode
| JsonOperation::Object => {
self.result("JsonResult", "Parsed", "value", Type::Json, source)?
}
JsonOperation::Parse => self.result_with_error(
"dever.json.value.ParseResult",
"Parsed",
"document",
self.json_type("Document", source)?,
vec![("position", Type::Int), ("message", Type::Text)],
source,
)?,
JsonOperation::Stringify => self.result_with_error(
"dever.json.value.WriteResult",
"Written",
"text",
Type::Text,
vec![("node", Type::Int), ("message", Type::Text)],
source,
)?,
JsonOperation::Node => self.json_type("Value", source)?.optional(),
JsonOperation::DocumentProperty | JsonOperation::Element => Type::Int.optional(),
JsonOperation::ToText => Type::Text,
JsonOperation::Property => self.result(
"PropertyResult",
"Found",
"value",
Type::Json.optional(),
source,
)?,
JsonOperation::AsText => {
self.result("TextResult", "Read", "value", Type::Text, source)?
}
JsonOperation::AsInteger => {
self.result("IntResult", "Read", "value", Type::Int, source)?
}
};
self.coerce(
Expression {
kind: Node::Json(operation, values),
value_type,
span: source.span,
},
expected,
)
}
fn json_type(&self, name: &str, source: &ast::Expression) -> Result<Type, Diagnostic> {
self.definition_names
.resolve(
&format!("dever.json.value.{name}"),
self.source,
source.span,
)?
.map(Type::Named)
.ok_or_else(|| self.error(source, "missing JSON document type"))
}
pub(super) fn encode_json(
&mut self,
argument: &ast::Expression,
result: &Type,
source: &ast::Expression,
) -> Result<Expression, Diagnostic> {
let value = self.expression(argument, None)?;
super::application::wire_type(&value.value_type, self.definitions, source.span)?;
Ok(Expression {
kind: Node::Json(JsonOperation::Encode, vec![value]),
value_type: result.clone(),
span: source.span,
})
}
fn result(
&self,
name: &str,
success: &str,
field: &str,
kind: Type,
source: &ast::Expression,
) -> Result<Type, Diagnostic> {
self.result_with_error(
name,
success,
field,
kind,
vec![("message", Type::Text)],
source,
)
}
fn result_with_error(
&self,
name: &str,
success: &str,
field: &str,
kind: Type,
errors: Vec<(&str, Type)>,
source: &ast::Expression,
) -> Result<Type, Diagnostic> {
let index = self
.definition_names
.resolve(name, self.source, source.span)?
.ok_or_else(|| self.error(source, "missing JSON result type"))?;
let error_fields = errors
.into_iter()
.map(|(name, kind)| (name.to_owned(), kind))
.collect::<Vec<_>>();
let valid = matches!(&self.definitions[index].shape, Shape::Choice(variants) if variants.len() == 2
&& variants[0].name == success && !variants[0].error && variants[0].fields == [(field.into(), kind)]
&& variants[1].name == "Failed" && variants[1].error && variants[1].fields == error_fields);
if !valid {
return Err(self.error(
source,
"JSON result type does not match its intrinsic contract",
));
}
Ok(Type::Named(index))
}
}
+462
View File
@@ -0,0 +1,462 @@
//! 按声明、签名、依赖和函数体完成语义检查。
//! 先确认失败义务,再做局部值传递优化,最后交付可生成 Rust 的程序。
mod api;
mod application;
mod async_stream;
mod auth;
mod bytes;
mod calendar;
mod call;
mod channel;
mod clauses;
mod collection;
mod constants;
mod crypto;
mod database;
mod effects;
mod expressions;
mod failure;
mod handler;
mod http;
mod io;
mod job;
mod json;
mod names;
mod net;
mod nominal;
mod package;
mod range;
mod signature;
mod standard;
mod storage;
mod task;
mod testing;
mod tls;
mod upload;
mod values;
use crate::compiler::model::*;
use crate::syntax::{ast, diagnostic::Diagnostic};
use std::collections::HashMap;
pub fn check(modules: Vec<ast::Module>, packages: &[String]) -> Result<Program, Diagnostic> {
check_inner(modules, packages, false, None)
}
pub(crate) fn check_tests(
modules: Vec<ast::Module>,
packages: &[String],
test_source: Option<usize>,
) -> Result<Program, Diagnostic> {
check_inner(modules, packages, true, test_source)
}
fn check_inner(
mut modules: Vec<ast::Module>,
packages: &[String],
testing: bool,
test_source: Option<usize>,
) -> Result<Program, Diagnostic> {
for (source, identity) in packages.iter().enumerate() {
if matches!(
crate::compiler::model::package::role(identity),
Some((_, "test"))
) && Some(source) != test_source
{
return Err(Diagnostic::new(
"C006",
"Test source identity is assigned only by the test runner",
crate::syntax::source::Span {
source,
start: 0,
end: 0,
},
));
}
}
application::prepare(&mut modules, packages)?;
let mut declarations = Vec::new();
let mut commands = Vec::new();
let mut jobs = Vec::new();
let mut schedules = Vec::new();
let mut http = Vec::new();
let mut rest = Vec::new();
let mut persistence = Vec::new();
let mut ports = Vec::new();
let mut external = Vec::new();
let mut database_bindings = vec![None; packages.len()];
let mut groups: Vec<Vec<ast::Function>> = Vec::new();
let mut group_indexes: HashMap<_, usize> = HashMap::new();
let mut definition_names = package::Types::new(packages);
definition_names.test_source = test_source;
for module in modules {
let mut previous = None;
for declaration in module.declarations {
match declaration {
ast::Declaration::External(value) => {
previous = None;
external.push(value);
}
ast::Declaration::Persistence(value) => {
previous = None;
persistence.push(value);
}
ast::Declaration::Rest(value) => {
previous = None;
rest.push(value);
}
ast::Declaration::Job(value) => {
previous = None;
groups.push(vec![value.function.clone()]);
jobs.push(value);
}
ast::Declaration::Schedule(value) => {
previous = None;
schedules.push(value);
}
ast::Declaration::Database(name, span) => {
previous = None;
if database_bindings[span.source].replace(name).is_some() {
return Err(Diagnostic::new("C002", "duplicate database binding", span));
}
}
ast::Declaration::Http(value) => {
previous = None;
http.push(value);
}
ast::Declaration::Command(value) => {
previous = None;
commands.push(value);
}
ast::Declaration::Type(value) => {
names::identifier(&value.name, true, value.span)?;
previous = None;
if !definition_names.insert(
value.span.source,
value.name.clone(),
declarations.len(),
value.public,
) {
return Err(Diagnostic::new("C002", "duplicate type", value.span));
}
declarations.push(value);
}
ast::Declaration::Function(function) => {
application::port::function_name(&function, packages)?;
if matches!(function.name.as_str(), "fail" | "result") {
return Err(Diagnostic::new(
"C002",
"reserved core operation",
function.span,
));
}
for input in &function.inputs {
names::identifier(&input.name, false, input.span)?;
}
for field in &function.outputs {
names::identifier(&field.name, false, field.span)?;
if field.private
|| field.unique
|| field.index
|| !field.rest.is_empty()
|| !field.persistence.is_empty()
{
return Err(Diagnostic::new(
"C005",
"outputs cannot be private",
field.span,
));
}
}
if function.contract {
previous = None;
ports.push(function);
continue;
}
let key = (
definition_names.owner(function.span.source),
function.name.clone(),
function.inputs.len(),
);
if let Some(&index) = group_indexes.get(&key) {
if previous != Some(index) {
return Err(Diagnostic::new(
"C013",
"function clauses must be adjacent",
function.span,
));
}
groups[index].push(function);
} else {
group_indexes.insert(key, groups.len());
previous = Some(groups.len());
groups.push(vec![function]);
}
}
}
}
}
application::rest::prepare(&declarations, &mut groups, packages)?;
database::prepare(&mut declarations, &mut definition_names)?;
let mut definitions = nominal::check(&declarations, &definition_names)?;
let mut models = database::check(
&definitions,
&declarations,
&definition_names,
&database_bindings,
&persistence,
)?;
database::relation::check(
&mut models,
&mut definitions,
&persistence,
&definition_names,
)?;
let privacy = database::Privacy::new(&definitions, &models);
let mut ports =
application::port::Ports::new(&ports, &definition_names, &definitions, &declarations)?;
ports.external(
&external,
&groups,
&definition_names,
&definitions,
&declarations,
)?;
let expansion = handler::expand(groups, &definition_names, &definitions, &mut ports)?;
// 先检查声明和显式调用;函数体中的类型构造还会增加依赖。
definition_names.check_cycles()?;
for instance in &expansion.instances {
if matches!(
crate::compiler::model::package::role(&packages[instance.signature.source]),
Some((_, "app"))
) {
privacy.signature(&instance.signature, &definitions, instance.span)?;
}
for (_, kind) in instance
.signature
.inputs
.iter()
.chain(&instance.signature.outputs)
{
names::validate(kind, &definitions, instance.span)?;
}
}
let mut signatures = expansion
.instances
.iter()
.map(|instance| instance.signature.clone())
.collect::<Vec<_>>();
let mut functions: Vec<Option<Function>> =
(0..expansion.instances.len()).map(|_| None).collect();
let jobs = job::entries(
&jobs,
&signatures[..expansion.concrete],
&definitions,
&definition_names,
&database_bindings,
)?;
let schedules = job::schedules(&schedules, &jobs, &signatures, &definition_names)?;
for &index in &expansion.order {
let instance = &expansion.instances[index];
if instance.prototype {
failure::prototype(&mut signatures[index], &definitions, instance.span)?;
continue;
}
if let Some(generated) = &instance.generated {
let mut signature = signatures[index].clone();
handler::adapter::check(
generated,
&mut signature,
&definitions,
&signatures,
instance.span,
)?;
signatures[index] = signature.clone();
functions[index] = Some(Function {
signature,
clauses: Vec::new(),
proven_arithmetic: Default::default(),
});
continue;
}
let group = &instance.group;
let signature = signatures[index].clone();
let patterns = clauses::resolve(group, &signature, &definition_names, &definitions)?;
clauses::validate(&patterns, &signature, &definitions, group)?;
let mut checked = Vec::new();
for (clause, patterns) in group.iter().zip(patterns) {
let mut context = Context {
jobs: &jobs,
models: &models,
function: index,
signatures: &signatures,
definitions: &definitions,
definition_names: &definition_names,
source: clause.span.source,
variables: signature.inputs.clone(),
initialized: vec![true; signature.inputs.len()],
scope: &instance.scope,
keys: HashMap::new(),
};
checked.push(context.body(clause, &signature, patterns)?);
}
let mut function = Function {
signature,
clauses: checked,
proven_arithmetic: Default::default(),
};
let database_error = definitions.iter().position(|definition| {
definition.name == "Error" && packages[definition.source] == "dever.database"
});
failure::check(&mut function, &definitions, &signatures, database_error)?;
if function.signature.transaction {
let definition = definition_names
.resolve("dever.database.Error", group[0].span.source, group[0].span)?
.ok_or_else(|| {
Diagnostic::new(
"C014",
"transaction requires database standard errors",
group[0].span,
)
})?;
let Shape::Choice(variants) = &definitions[definition].shape else {
unreachable!()
};
function
.signature
.failures
.extend((0..variants.len()).map(|variant| Failure {
definition,
variant,
}));
function.signature.failures.sort();
function.signature.failures.dedup();
}
range::check(&mut function, &definitions, &signatures)?;
effects::check(&mut function, &signatures, packages)?;
task::check(&mut function, &signatures, &definitions)?;
upload::check(&function, &signatures, &definitions, packages)?;
ports.finish(&mut function.signature, instance.span)?;
if group[0].public {
for failure in &function.signature.failures {
definition_names.public_type(&Type::Named(failure.definition), instance.span)?;
if matches!(
crate::compiler::model::package::role(&packages[instance.signature.source]),
Some((_, "app"))
) {
privacy.check(&Type::Named(failure.definition), instance.span)?;
}
}
}
// 复制消除不能让覆盖旧值或丢弃错误逃过合同检查。
crate::compiler::optimize::records(&mut function, &definitions);
signatures[index] = function.signature.clone();
functions[index] = Some(function);
}
// 此时包含函数体中解析到的全部类型引用。
for (index, expected, span) in &expansion.contracts {
range::handler(expected, &signatures[*index], *span)?;
}
definition_names.check_cycles()?;
ports.check_types(&definition_names, &definitions)?;
let verifiers = if http.is_empty() && rest.is_empty() {
Vec::new()
} else {
auth::verifiers(
&signatures[..expansion.concrete],
&functions,
&definitions,
&models,
&definition_names,
&expansion,
)?
};
let commands = application::commands(
&commands,
&signatures[..expansion.concrete],
&definitions,
&definition_names,
false,
)?;
job::effects(&jobs, &signatures, &expansion)?;
let http = application::http::check(
&http,
&signatures[..expansion.concrete],
&definitions,
&definition_names,
&verifiers,
)?;
if !testing {
application::reachable(
commands
.iter()
.map(|entry| entry.function)
.chain(http.iter().map(|entry| entry.function))
.chain(verifiers.iter().map(|entry| entry.function))
.chain(jobs.iter().map(|entry| entry.function))
.chain(
signatures
.iter()
.enumerate()
.filter(|(_, signature)| {
matches!(
crate::compiler::model::package::role(&packages[signature.source]),
Some((_, "model"))
)
})
.map(|(index, _)| index),
),
&expansion,
packages,
)?;
}
let rest = application::rest::check(
&rest,
application::rest::Context {
models: &models,
declarations: &declarations,
definitions: &definitions,
packages,
http: &http,
verifiers: &verifiers,
signatures: &signatures,
functions: &functions,
calls: &expansion.calls,
},
)?;
let exports = api::exports(&expansion, &signatures, &declarations, packages);
Ok(Program {
rest,
jobs,
schedules,
verifiers,
models,
adapters: expansion.adapters,
packages: packages.to_vec(),
definitions,
commands,
http,
exports,
calls: expansion.calls,
functions: functions
.into_iter()
.take(expansion.concrete)
.map(|value| value.expect("checked dependency order"))
.collect(),
})
}
struct Context<'program> {
jobs: &'program [crate::compiler::model::job::Entry],
models: &'program [persistence::Model],
function: usize,
signatures: &'program [Signature],
definitions: &'program [Definition],
definition_names: &'program package::Types,
source: usize,
variables: Vec<(String, Type)>,
initialized: Vec<bool>,
scope: &'program handler::Scope,
keys: HashMap<usize, storage::Key>,
}
+317
View File
@@ -0,0 +1,317 @@
//! 统一名称规则、类型引用、Map 键约束及依赖排序。
//! 跨包类型解析委托给 package,不能用局部名字绕过可见性。
use crate::compiler::model::*;
use crate::syntax::{ast, diagnostic::Diagnostic};
pub(super) fn function(name: &str, span: crate::syntax::source::Span) -> Result<(), Diagnostic> {
if name.starts_with(|value: char| value.is_ascii_lowercase())
&& name.bytes().all(|value| value.is_ascii_alphanumeric())
{
Ok(())
} else {
Err(Diagnostic::new(
"C010",
"function names require ASCII lowerCamelCase",
span,
))
}
}
pub(super) fn identifier(
name: &str,
type_name: bool,
span: crate::syntax::source::Span,
) -> Result<(), Diagnostic> {
let valid = if type_name {
name.starts_with(|value: char| value.is_ascii_uppercase())
&& name.bytes().all(|value| value.is_ascii_alphanumeric())
} else {
name.starts_with(|value: char| value.is_ascii_lowercase())
&& name.split('_').all(|word| {
!word.is_empty()
&& word
.bytes()
.all(|value| value.is_ascii_lowercase() || value.is_ascii_digit())
})
};
if !valid {
return Err(Diagnostic::new(
"C010",
if type_name {
"type names require ASCII UpperCamelCase"
} else {
"names require ASCII lower_snake_case"
},
span,
));
}
Ok(())
}
pub(super) fn resolve(
reference: &ast::TypeRef,
records: &super::package::Types,
) -> Result<Type, Diagnostic> {
if !reference.storage.is_empty()
&& !matches!(
package::role(&records.packages[reference.span.source]),
Some((_, "model"))
)
{
return Err(Diagnostic::new(
"C014",
"storage parameters belong to Model fields",
reference.span,
));
}
let kind = match (reference.name.as_str(), reference.arguments.as_slice()) {
("Bool", []) => Type::Bool,
("Int", []) => Type::Int,
("Float", []) => Type::Float,
("Decimal", []) => Type::Decimal,
("DateTime", []) => Type::DateTime,
("Date", []) => Type::Date,
("Time", []) => Type::Time,
("Duration", []) => Type::Duration,
("Text", []) => Type::Text,
("Id", []) => Type::Id,
("Uuid", []) => Type::Uuid,
("Bytes", []) => Type::Bytes,
("Json", []) => Type::Json,
("Secret", []) => Type::Secret,
("Upload", []) => Type::Upload,
("dever.system.File", []) => Type::File,
("dever.system.Socket", []) => Type::Socket,
("dever.system.Listener", []) => Type::Listener,
("dever.system.ClientTls", []) => Type::ClientTls,
("dever.system.ServerTls", []) => Type::ServerTls,
("dever.system.HttpClient", []) => Type::HttpClient,
("dever.system.HttpReply", []) => Type::HttpReply,
("dever.system.WebSocket", []) => Type::WebSocket,
("List", [item]) => Type::List(Box::new(resolve(item, records)?)),
("Stream", [item]) => Type::Stream(Box::new(resolve(item, records)?)),
("AsyncStream", [item]) => Type::AsyncStream(Box::new(resolve(item, records)?)),
("Channel", [item]) => Type::Channel(Box::new(resolve(item, records)?)),
("Related", [item]) => Type::Named(
*records
.related
.get(&resolve(item, records)?)
.ok_or_else(|| {
Diagnostic::new(
"C014",
"Related requires Model, nullable Model or List<Model>",
reference.span,
)
})?,
),
("Map", [key, value]) => Type::Map(
Box::new(resolve(key, records)?),
Box::new(resolve(value, records)?),
),
("MapEntry", [key, value]) => Type::MapEntry(
Box::new(resolve(key, records)?),
Box::new(resolve(value, records)?),
),
(name, []) => records
.resolve(name, reference.span.source, reference.span)?
.map(|index| {
if name.rsplit('.').next() == Some("id") {
Type::ModelId(index)
} else {
Type::Named(index)
}
})
.ok_or_else(|| {
Diagnostic::new(
"C005",
format!("unknown or unsupported type '{name}'"),
reference.span,
)
})?,
_ => {
return Err(Diagnostic::new(
"C005",
"invalid or unsupported type arguments",
reference.span,
));
}
};
if super::upload::nested(&kind) && (kind != Type::Upload || reference.nullable) {
return Err(Diagnostic::new(
"C013",
"Upload cannot be nested or optional",
reference.span,
));
}
Ok(if reference.nullable {
Type::Optional(Box::new(kind))
} else {
kind
})
}
pub(super) fn definition_cycles(
records: &[Definition],
declarations: &[ast::TypeDeclaration],
) -> Result<Vec<usize>, Diagnostic> {
fn dependencies(kind: &Type, output: &mut Vec<usize>) {
match kind {
Type::Named(index) => output.push(*index),
Type::List(inner)
| Type::Optional(inner)
| Type::Stream(inner)
| Type::Channel(inner)
| Type::AsyncStream(inner) => dependencies(inner, output),
Type::Map(key, value) | Type::MapEntry(key, value) => {
dependencies(key, output);
dependencies(value, output);
}
_ => {}
}
}
let mut edges = vec![Vec::new(); records.len()];
for (index, record) in records.iter().enumerate() {
match &record.shape {
Shape::Record(fields) => {
for (_, kind) in fields {
dependencies(kind, &mut edges[index]);
}
}
Shape::Choice(variants) => {
for variant in variants {
for (_, kind) in &variant.fields {
dependencies(kind, &mut edges[index]);
}
}
}
}
}
order(&edges).map_err(|index| {
Diagnostic::new(
"C007",
"recursive types are not allowed",
declarations[index].span,
)
})
}
pub(super) fn validate(
kind: &Type,
definitions: &[Definition],
span: crate::syntax::source::Span,
) -> Result<(), Diagnostic> {
match kind {
Type::Channel(inner) | Type::AsyncStream(inner) => {
if matches!(kind, Type::Channel(_)) && matches!(inner.as_ref(), Type::Optional(_)) {
return Err(Diagnostic::new(
"C013",
"Channel items cannot be optional; use a message choice to distinguish null from closure",
span,
));
}
validate(inner, definitions, span)?;
if !inner.transferable(definitions) {
return Err(Diagnostic::new(
"C013",
"Channel and AsyncStream items must be transferable",
span,
));
}
Ok(())
}
Type::Map(key, value) | Type::MapEntry(key, value) => {
if !key.map_key(definitions) {
return Err(Diagnostic::new("C005", "invalid Map key type", span));
}
validate(value, definitions, span)
}
Type::List(inner) | Type::Optional(inner) | Type::Stream(inner) => {
validate(inner, definitions, span)
}
_ => Ok(()),
}
}
pub(super) fn order(edges: &[Vec<usize>]) -> Result<Vec<usize>, usize> {
let mut pending = edges.iter().map(Vec::len).collect::<Vec<_>>();
let mut callers = vec![Vec::new(); edges.len()];
for (index, targets) in edges.iter().enumerate() {
for target in targets {
callers[*target].push(index);
}
}
let mut ready = pending
.iter()
.enumerate()
.filter_map(|(index, count)| (*count == 0).then_some(index))
.collect::<Vec<_>>();
let mut order = Vec::new();
while let Some(index) = ready.pop() {
order.push(index);
for caller in &callers[index] {
pending[*caller] -= 1;
if pending[*caller] == 0 {
ready.push(*caller);
}
}
}
match pending.iter().position(|count| *count != 0) {
Some(index) => Err(index),
None => Ok(order),
}
}
pub(super) fn variant(
path: &str,
source: usize,
names: &super::package::Types,
definitions: &[Definition],
span: crate::syntax::source::Span,
) -> Result<Option<(usize, usize)>, Diagnostic> {
let Some((name, variant)) = path.rsplit_once('.') else {
return Ok(None);
};
let Some(index) = names.resolve(name, source, span)? else {
return Ok(None);
};
let Shape::Choice(variants) = &definitions[index].shape else {
return Ok(None);
};
let variant = variants
.iter()
.position(|value| value.name == variant)
.ok_or_else(|| Diagnostic::new("C004", "unknown choice variant", span))?;
Ok(Some((index, variant)))
}
pub(super) fn variant_type(
reference: &ast::TypeRef,
names: &super::package::Types,
definitions: &[Definition],
) -> Result<Option<(usize, usize)>, Diagnostic> {
if matches!(
reference.name.as_str(),
"Related.Loaded" | "Related.Unloaded"
) {
let mut kind = reference.clone();
kind.name = "Related".into();
let Type::Named(index) = resolve(&kind, names)? else {
unreachable!()
};
return Ok(Some((
index,
usize::from(reference.name == "Related.Loaded"),
)));
}
if !reference.arguments.is_empty() {
return Ok(None);
}
variant(
&reference.name,
reference.span.source,
names,
definitions,
reference.span,
)
}
+101
View File
@@ -0,0 +1,101 @@
//! 核对网络参数和名义结果;用户只通过官方 dever.net 使用底层原语。
use super::Context;
use crate::compiler::model::*;
use crate::syntax::{ast, diagnostic::Diagnostic};
impl Context<'_> {
pub(super) fn net(
&mut self,
operation: NetOperation,
arguments: &[ast::Expression],
expected: Option<&Type>,
source: &ast::Expression,
) -> Result<Expression, Diagnostic> {
if self.definition_names.packages[self.source] != "dever.net" {
return Err(Diagnostic::new(
"C006",
"network intrinsics belong to the official dever.net package",
source.span,
));
}
let inputs = match operation {
NetOperation::Connect | NetOperation::Listen => vec![Type::Text, Type::Int],
NetOperation::ConnectTimeout => vec![Type::Text, Type::Int, Type::Int],
NetOperation::Accept
| NetOperation::Port
| NetOperation::CloseListener
| NetOperation::Connections => vec![Type::Listener],
NetOperation::Read | NetOperation::Timeout | NetOperation::Chunks => {
vec![Type::Socket, Type::Int]
}
NetOperation::Write => vec![Type::Socket, Type::Bytes],
NetOperation::Close => vec![Type::Socket],
};
if inputs.len() != arguments.len() {
return Err(self.error(source, "network argument count differs"));
}
let values = arguments
.iter()
.zip(inputs)
.map(|(argument, kind)| self.expression(argument, Some(&kind)))
.collect::<Result<Vec<_>, _>>()?;
let value_type = match operation {
NetOperation::Connect | NetOperation::ConnectTimeout | NetOperation::Accept => self
.io_result(
"ConnectResult",
&[("Connected", vec![("socket", Type::Socket)])],
source,
)?,
NetOperation::Listen => self.io_result(
"ListenResult",
&[("Listening", vec![("listener", Type::Listener)])],
source,
)?,
NetOperation::Port => self.io_result(
"PortResult",
&[("Bound", vec![("port", Type::Int)])],
source,
)?,
NetOperation::Read => self.io_result(
"SystemReadResult",
&[("Read", vec![("bytes", Type::Bytes)]), ("End", vec![])],
source,
)?,
NetOperation::Write | NetOperation::Timeout => {
self.io_result("dever.io.WriteResult", &[("Done", vec![])], source)?
}
NetOperation::Close | NetOperation::CloseListener => {
self.io_result("dever.io.CloseResult", &[("Closed", vec![])], source)?
}
NetOperation::Chunks => {
let item = self.io_result(
"dever.io.ReadEvent",
&[("Chunk", vec![("bytes", Type::Bytes)])],
source,
)?;
self.io_result(
"ReadStreamResult",
&[(
"Streaming",
vec![("stream", Type::AsyncStream(Box::new(item)))],
)],
source,
)?
}
NetOperation::Connections => Type::AsyncStream(Box::new(self.io_result(
"ConnectResult",
&[("Connected", vec![("socket", Type::Socket)])],
source,
)?)),
};
self.coerce(
Expression {
kind: Node::Net(operation, values),
value_type,
span: source.span,
},
expected,
)
}
}
+187
View File
@@ -0,0 +1,187 @@
//! 解析 record 与 choice 的字段、变体及名义类型关系。
//! 拒绝递归类型和公开字段的类型泄漏,保留 record 私有字段的位置。
use super::names;
use crate::compiler::model::*;
use crate::syntax::{ast, diagnostic::Diagnostic};
use std::collections::HashSet;
pub(super) fn check(
declarations: &[ast::TypeDeclaration],
names: &super::package::Types,
) -> Result<Vec<Definition>, Diagnostic> {
let mut definitions = Vec::new();
for declaration in declarations {
let mut private_fields = Vec::new();
let mut ranges = Vec::new();
let shape = match &declaration.body {
ast::TypeBody::Record(values) => {
ranges = super::range::fields(values, names)?;
private_fields.extend(
values
.iter()
.enumerate()
.filter_map(|(index, field)| field.private.then_some(index)),
);
Shape::Record(fields(values, names)?)
}
ast::TypeBody::Choice(values) => {
let mut seen = HashSet::new();
let mut variants = Vec::new();
for variant in values {
if variant.label.is_some()
&& !matches!(
package::role(&names.packages[declaration.span.source]),
Some((_, "model"))
)
{
return Err(Diagnostic::new(
"C014",
"display labels belong to Model choices",
variant.span,
));
}
if let Some(field) = variant.fields.iter().find(|field| field.private) {
return Err(Diagnostic::new(
"C005",
"choice payloads cannot be private",
field.span,
));
}
names::identifier(&variant.name, true, variant.span)?;
if !seen.insert(&variant.name) {
return Err(Diagnostic::new(
"C002",
"duplicate choice variant",
variant.span,
));
}
variants.push(Variant {
name: variant.name.clone(),
label: variant.label.clone(),
fields: fields(&variant.fields, names)?,
error: variant.error,
ranges: super::range::fields(&variant.fields, names)?,
});
}
Shape::Choice(variants)
}
};
definitions.push(Definition {
related: names
.related
.values()
.any(|index| *index == definitions.len()),
name: declaration.name.clone(),
shape,
source: declaration.span.source,
private_fields,
ranges,
comparable: false,
transferable: false,
contains_secret: false,
});
}
// 按依赖顺序计算一次;嵌套类型不能通过容器绕过资源不可比较的约束。
for index in names::definition_cycles(&definitions, declarations)? {
definitions[index].contains_secret = match &definitions[index].shape {
Shape::Record(fields) => fields
.iter()
.any(|(_, kind)| kind.contains_secret(&definitions)),
Shape::Choice(variants) => variants.iter().any(|variant| {
variant
.fields
.iter()
.any(|(_, kind)| kind.contains_secret(&definitions))
}),
};
definitions[index].transferable = match &definitions[index].shape {
Shape::Record(fields) => fields
.iter()
.all(|(_, kind)| kind.transferable(&definitions)),
Shape::Choice(variants) => variants.iter().all(|variant| {
variant
.fields
.iter()
.all(|(_, kind)| kind.transferable(&definitions))
}),
};
definitions[index].comparable = match &definitions[index].shape {
Shape::Record(fields) => fields.iter().all(|(_, kind)| kind.comparable(&definitions)),
Shape::Choice(variants) => variants.iter().all(|variant| {
variant
.fields
.iter()
.all(|(_, kind)| kind.comparable(&definitions))
}),
};
}
for (definition, declaration) in definitions.iter().zip(declarations) {
if let Shape::Choice(variants) = &definition.shape
&& variants.iter().any(|variant| {
variant.error
&& variant
.fields
.iter()
.any(|(_, kind)| kind.contains_secret(&definitions))
})
{
return Err(Diagnostic::new(
"C005",
"Secret cannot be stored in an error payload",
declaration.span,
));
}
let fields = match &definition.shape {
Shape::Record(fields) => fields.iter().collect::<Vec<_>>(),
Shape::Choice(variants) => variants
.iter()
.flat_map(|variant| &variant.fields)
.collect(),
};
for (index, (_, kind)) in fields.into_iter().enumerate() {
if super::upload::nested(kind) {
return Err(Diagnostic::new(
"C013",
"Upload cannot be stored in a record or choice",
declaration.span,
));
}
names::validate(kind, &definitions, declaration.span)?;
if declaration.public && !definition.private_fields.contains(&index) {
names.public_type(kind, declaration.span)?;
}
}
}
Ok(definitions)
}
fn fields(
fields: &[ast::Field],
definitions: &super::package::Types,
) -> Result<Vec<(String, Type)>, Diagnostic> {
let mut checked = Vec::new();
for field in fields {
if (field.unique || field.index || !field.rest.is_empty() || !field.persistence.is_empty())
&& !matches!(
package::role(&definitions.packages[field.span.source]),
Some((_, "model"))
)
{
return Err(Diagnostic::new(
"C014",
"storage modifiers belong to Model fields",
field.span,
));
}
names::identifier(&field.name, false, field.span)?;
if checked.iter().any(|(name, _)| name == &field.name) {
return Err(Diagnostic::new("C002", "duplicate field", field.span));
}
checked.push((
field.name.clone(),
names::resolve(&field.kind, definitions)?,
));
}
Ok(checked)
}
+112
View File
@@ -0,0 +1,112 @@
//! 解析应用内的 App/Domain 简写并检查职责调用边界。
//! 路径与访问权限分开判断,完整路径不能绕过同领域约束。
use super::Types;
use crate::compiler::model::package;
use crate::syntax::{diagnostic::Diagnostic, source::Span};
impl Types {
pub fn owner(&self, source: usize) -> usize {
self.owners[source]
}
pub fn target<'name>(&self, name: &'name str, source: usize) -> Option<(usize, &'name str)> {
let Some((prefix, name)) = name.rsplit_once('.') else {
return Some((self.owner(source), name));
};
let prefix = if let Some((domain, _)) = package::role(&self.packages[source]) {
if prefix == "app" {
domain.to_owned()
} else if matches!(
prefix.split('.').next(),
Some("domain" | "port" | "adapter" | "model")
) {
format!("{domain}.{prefix}")
} else if !prefix.contains('.') && !package::standard(prefix) {
let component = domain.split_once('.').unwrap().0;
format!("{component}.{prefix}")
} else {
prefix.to_owned()
}
} else {
prefix.to_owned()
};
self.packages
.iter()
.position(|identity| package::namespace(identity) == prefix)
.map(|index| (self.owner(index), name))
}
pub fn access(
&self,
source: usize,
target: usize,
function: bool,
span: Span,
) -> Result<(), Diagnostic> {
let caller = package::role(&self.packages[source]);
let callee = package::role(&self.packages[target]);
let allowed = match (caller, callee) {
(Some((_, "test")), Some((_, "app"))) => true,
(Some((first, "test")), Some((second, "domain"))) => first == second,
(Some((_, "test")), Some((_, "model"))) => !function,
(Some((first, "test")), Some((second, "port"))) => !function && first == second,
(Some((_, "test")), Some((_, "test"))) => source == target,
(Some((_, "model")), Some((_, "model"))) => !function,
(Some((first, "model")), Some((second, "app"))) => !function && first == second,
(Some((first, "app")), Some((second, "model"))) => !function || first == second,
(Some((_, "adapter")), None) => package::standard(&self.packages[target]),
(_, None) => true,
(Some((_, "app")), Some((_, "app"))) => true,
(Some((first, "api")), Some((second, "app"))) => first == second,
(Some((first, "job")), Some((second, "app"))) => first == second,
(Some((first, "domain")), Some((second, "app"))) => !function && first == second,
(Some((first, "app" | "domain")), Some((second, "domain"))) => first == second,
(Some((first, "model")), Some((second, "domain"))) => first == second,
(Some((first, "port" | "adapter")), Some((second, "app"))) => {
!function && first == second
}
(Some((first, "app")), Some((second, "port"))) => first == second,
(Some((first, "adapter")), Some((second, "port"))) => !function && first == second,
(Some((_, "port" | "adapter")), _) => self.owner(source) == self.owner(target),
_ => false,
};
if allowed {
Ok(())
} else {
Err(Diagnostic::new(
"C006",
"application role cannot access this declaration",
span,
))
}
}
pub fn private_access(&self, source: usize, target: usize) -> bool {
self.owner(source) == self.owner(target)
|| matches!((package::role(&self.packages[source]), package::role(&self.packages[target])),
(Some((first, "app")), Some((second, "model"))) if first == second)
|| matches!((package::role(&self.packages[source]), package::role(&self.packages[target])),
(Some((first, "app" | "domain" | "model" | "test")), Some((second, "domain"))) if first == second)
|| matches!((package::role(&self.packages[source]), package::role(&self.packages[target])),
(Some((first, "app" | "adapter" | "test")), Some((second, "port"))) if first == second)
}
pub fn private_field(
&self,
source: usize,
target: usize,
span: Span,
) -> Result<(), Diagnostic> {
// 构造、读取与修改共用声明所属包的访问边界,主题文件不能绕过它。
if self.private_access(source, target) {
Ok(())
} else {
Err(Diagnostic::new(
"C006",
"field is private to its package",
span,
))
}
}
}
+150
View File
@@ -0,0 +1,150 @@
//! 维护包内类型名、公开边界及显式包依赖。
//! 依赖记录仅属于编译期,不会给生成程序增加包查找。
mod access;
use crate::compiler::model::{Type, package};
use crate::syntax::{diagnostic::Diagnostic, source::Span};
use std::cell::RefCell;
use std::collections::{BTreeMap, HashMap, HashSet};
pub(crate) struct Types {
pub related: HashMap<Type, usize>,
pub packages: Vec<String>,
pub test_source: Option<usize>,
definitions: HashMap<(usize, String), usize>,
public: HashSet<usize>,
owners: Vec<usize>,
boundaries: Vec<usize>,
// 解析器共享只读名称表,同时记录成功解析的显式依赖;不影响类型映射。
dependencies: RefCell<Vec<BTreeMap<usize, Span>>>,
// Port/Adapter 的跨职责类型引用在合同绑定完成后统一核对。
pub role_types: RefCell<Vec<(usize, usize, Span)>>,
}
impl Types {
pub fn new(packages: &[String]) -> Self {
let mut namespaces = HashMap::new();
let mut domains = HashMap::new();
let mut owners = Vec::with_capacity(packages.len());
let mut boundaries = Vec::with_capacity(packages.len());
// 一次扫描确定首个源码编号,避免主题文件增多时反复遍历整个项目。
for (index, name) in packages.iter().enumerate() {
owners.push(*namespaces.entry(package::namespace(name)).or_insert(index));
boundaries.push(*domains.entry(package::boundary(name)).or_insert(index));
}
Self {
related: HashMap::new(),
packages: packages.to_vec(),
test_source: None,
definitions: HashMap::new(),
public: HashSet::new(),
owners,
boundaries,
dependencies: RefCell::new(vec![BTreeMap::new(); packages.len()]),
role_types: RefCell::new(Vec::new()),
}
}
pub fn insert(&mut self, source: usize, name: String, index: usize, public: bool) -> bool {
if public {
self.public.insert(index);
}
self.definitions
.insert((self.owner(source), name), index)
.is_none()
}
pub fn exported(&self, index: usize) -> bool {
self.public.contains(&index)
}
pub fn dependency(&self, source: usize, target: usize, span: Span) {
let source = self.boundaries[source];
let target = self.boundaries[target];
if source != target {
self.dependencies.borrow_mut()[source]
.entry(target)
.or_insert(span);
}
}
pub fn check_cycles(&self) -> Result<(), Diagnostic> {
let dependencies = self.dependencies.borrow();
let edges = dependencies
.iter()
.map(|targets| targets.keys().copied().collect())
.collect::<Vec<_>>();
if let Err(source) = super::names::order(&edges) {
return Err(Diagnostic::new(
"C007",
"cyclic package dependency",
*dependencies[source]
.values()
.next()
.expect("cycle has a dependency"),
));
}
Ok(())
}
pub fn public_type(&self, kind: &Type, span: Span) -> Result<(), Diagnostic> {
// 每个公开名义类型的字段另行校验,因此这里不重复展开整张类型图。
match kind {
Type::Named(index) if !self.exported(*index) => {
return Err(Diagnostic::new(
"C006",
"public boundary exposes a private type",
span,
));
}
Type::List(inner)
| Type::Optional(inner)
| Type::Stream(inner)
| Type::Channel(inner)
| Type::AsyncStream(inner) => self.public_type(inner, span)?,
Type::Map(key, value) | Type::MapEntry(key, value) => {
self.public_type(key, span)?;
self.public_type(value, span)?;
}
Type::Outputs(fields) => {
for (_, kind) in fields {
self.public_type(kind, span)?;
}
}
_ => {}
}
Ok(())
}
pub fn resolve(
&self,
name: &str,
source: usize,
span: Span,
) -> Result<Option<usize>, Diagnostic> {
let Some((target, name)) = self.target(name, source) else {
return Ok(None);
};
let Some(&index) = self.definitions.get(&(target, name.to_owned())) else {
return Ok(None);
};
self.access(source, target, false, span)?;
if matches!(
package::role(&self.packages[source]),
Some((_, "port" | "adapter"))
) && self.owner(source) != target
{
self.role_types.borrow_mut().push((source, index, span));
}
if !self.private_access(source, target) && !self.public.contains(&index) {
return Err(Diagnostic::new(
"C006",
"type is private to its package",
span,
));
}
self.dependency(source, target, span);
Ok(Some(index))
}
}
+370
View File
@@ -0,0 +1,370 @@
//! 由已检查表达式计算事实,同时验证每个构造和调用边界。
//! 不支持推导的数值操作只失去精度,不允许绕过合同。
use super::{Fact, Proof, number, require};
use crate::compiler::model::{
numeric::{Number, Range},
*,
};
use crate::syntax::{ast::Literal, diagnostic::Diagnostic, source::Span};
use std::{collections::BTreeMap, rc::Rc};
impl Proof<'_> {
pub(super) fn expression(&mut self, value: &Expression) -> Result<Rc<Fact>, Diagnostic> {
self.remaining = self.remaining.checked_sub(1).ok_or_else(|| {
Diagnostic::new("C015", "range proof exceeds its work limit", value.span)
})?;
let unknown = || Fact::unknown(&value.value_type);
Ok(match &value.kind {
Node::Literal(Literal::Number(text))
if matches!(value.value_type, Type::Int | Type::Decimal) =>
{
Fact::scalar(Some(Range::point(
Number::parse(text, &value.value_type).expect("checked literal"),
)))
}
Node::Literal(_) => unknown(),
Node::TestSecret(inner)
| Node::TestClock(inner)
| Node::TestDrain { limit: inner, .. } => {
self.expression(inner)?;
unknown()
}
Node::Variable(index) => self.variables[*index].clone(),
Node::Field(base, index) => {
self.expression(base)?
.field(*index, &base.value_type, self.definitions)
}
Node::Call(index, arguments) => {
self.arguments(*index, arguments)?;
self.outputs(*index)
}
Node::Record(index, values) => {
let mut fields = BTreeMap::new();
for (position, value) in values {
let fact = self.expression(value)?;
self.require_value(value, &fact, self.definitions[*index].ranges[*position])?;
fields.insert(*position, fact);
}
Rc::new(Fact {
fields,
..Fact::default()
})
}
Node::Variant(index, variant, values) => {
let Shape::Choice(variants) = &self.definitions[*index].shape else {
unreachable!()
};
for (value, &expected) in values.iter().zip(&variants[*variant].ranges) {
let fact = self.expression(value)?;
self.require_value(value, &fact, expected)?;
}
unknown()
}
Node::Capture(capture) => {
let fact = self.expression(&capture.call)?;
let Shape::Choice(variants) = &self.definitions[capture.definition].shape else {
unreachable!()
};
if let Node::Call(function, _) = capture.call.kind {
let signature = &self.signatures[function];
if signature.returns.is_some() {
for (((_, kind), &actual), &expected) in signature
.outputs
.iter()
.zip(&signature.output_ranges)
.zip(&variants[capture.success].ranges)
{
require(
actual.or_else(|| super::numeric_range(kind)),
expected,
value.span,
)?;
}
}
} else {
require(
fact.range
.or_else(|| super::numeric_range(&capture.call.value_type)),
variants[capture.success].ranges[0],
value.span,
)?;
}
unknown()
}
Node::Unary(operator, base) => {
let fact = self.expression(base)?;
if operator == "-" {
fact.range
.and_then(number::negate)
.map_or_else(unknown, |range| Fact::scalar(Some(range)))
} else {
unknown()
}
}
Node::Binary(operator, left, right) => {
let left = self.expression(left)?;
let right = self.expression(right)?;
if matches!(value.value_type, Type::Int | Type::Decimal) {
let range = left
.range
.zip(right.range)
.and_then(|(left, right)| number::binary(operator, left, right));
if value.value_type == Type::Int
&& matches!(operator.as_str(), "+" | "-" | "*")
&& range.is_some()
{
let span = value.span;
self.arithmetic.insert((span.source, span.start, span.end));
}
range.map_or_else(unknown, |range| Fact::scalar(Some(range)))
} else {
unknown()
}
}
Node::Decimal(base) => {
let fact = self.expression(base)?;
Fact::scalar(fact.range.map(number::decimal))
}
Node::Some(base) => self.expression(base)?,
Node::Length(base) => {
self.expression(base)?;
Fact::scalar(Some(Range {
minimum: Number::Int(0),
maximum: Number::Int(i64::MAX),
}))
}
Node::List(values) => {
let mut range = None;
let mut numeric = true;
for value in values {
let fact = self.expression(value)?;
numeric &= fact.range.is_some();
if let Some(actual) = fact.range {
range = Some(range.map_or(actual, |previous: Range| previous.hull(actual)));
}
}
Rc::new(Fact {
item: numeric.then(|| Fact::scalar(range)),
..Fact::default()
})
}
Node::Map(entries) => {
for (key, value) in entries {
self.expression(key)?;
self.expression(value)?;
}
unknown()
}
Node::Collection(collection) => self.collection(collection, value.span)?,
Node::Access(operation, values) => {
let facts = values
.iter()
.map(|value| self.expression(value))
.collect::<Result<Vec<_>, _>>()?;
match operation {
Access::First => facts[0].item.clone().unwrap_or_else(unknown),
_ => unknown(),
}
}
Node::Crypto(_, values)
| Node::Assert(values)
| Node::Calendar(_, values)
| Node::Standard(_, values)
| Node::Bytes(_, values)
| Node::Net(_, values)
| Node::Http(_, values)
| Node::Tls(_, values)
| Node::Database(_, values)
| Node::Auth(_, values)
| Node::Authorization(_, values)
| Node::Enqueue(_, values)
| Node::Io(_, values)
| Node::Upload(_, values)
| Node::Json(_, values) => {
for value in values {
self.expression(value)?;
}
unknown()
}
Node::Schedule(operation, target, values) => {
self.arguments(*target, values)?;
if *operation == Schedule::Run {
unknown()
} else {
self.outputs(*target)
}
}
Node::Join { task, .. } => {
self.expression(task)?;
if let Type::Task(target) = task.value_type {
self.outputs(target)
} else {
unknown()
}
}
Node::Timeout {
task,
millis,
fallback,
} => {
self.expression(task)?;
self.expression(millis)?;
let Type::Task(target) = task.value_type else {
unreachable!("checked Task");
};
self.alternatives([target, *fallback], value)?
}
Node::Race(tasks) => {
let mut targets = Vec::new();
for task in tasks {
self.expression(task)?;
let Type::Task(target) = task.value_type else {
unreachable!("checked Task");
};
targets.push(target);
}
self.alternatives(targets, value)?
}
Node::Stream(base) => self.expression(base)?,
Node::Channel(limit)
| Node::Receive(limit)
| Node::Group(limit)
| Node::Ticks(limit) => {
self.expression(limit)?;
unknown()
}
Node::Send(channel, sent) => {
self.expression(channel)?;
self.expression(sent)?;
unknown()
}
Node::GroupRun {
group,
target,
arguments,
} => {
self.expression(group)?;
self.arguments(*target, arguments)?;
unknown()
}
Node::Timer(_, base) => {
self.expression(base)?;
unknown()
}
Node::Id(base) | Node::IdText(base) | Node::Sum(base) | Node::Close(base) => {
self.expression(base)?;
unknown()
}
})
}
fn arguments(&mut self, function: usize, arguments: &[Expression]) -> Result<(), Diagnostic> {
for (argument, &expected) in arguments
.iter()
.zip(&self.signatures[function].input_ranges)
{
let fact = self.expression(argument)?;
self.require_value(argument, &fact, expected)?;
}
Ok(())
}
fn alternatives(
&mut self,
targets: impl IntoIterator<Item = usize>,
source: &Expression,
) -> Result<Rc<Fact>, Diagnostic> {
let mut fact: Option<Rc<Fact>> = None;
for target in targets {
if self.signatures[target].returns.is_none() {
continue;
}
self.remaining = self
.remaining
.checked_sub(self.signatures[target].outputs.len().max(1))
.ok_or_else(|| {
Diagnostic::new("C015", "range proof exceeds its work limit", source.span)
})?;
let value = self.outputs(target);
fact = Some(match fact {
Some(previous) => Fact::union(&previous, &value),
None => value,
});
}
Ok(fact.unwrap_or_else(|| Fact::unknown(&source.value_type)))
}
fn outputs(&self, function: usize) -> Rc<Fact> {
let ranges = &self.signatures[function].output_ranges;
let outputs = &self.signatures[function].outputs;
if let [range] = ranges.as_slice() {
return Fact::scalar(range.or_else(|| super::numeric_range(&outputs[0].1)));
}
Rc::new(Fact {
fields: ranges
.iter()
.enumerate()
.map(|(index, range)| {
(
index,
Fact::scalar(range.or_else(|| super::numeric_range(&outputs[index].1))),
)
})
.collect(),
..Fact::default()
})
}
fn collection(&mut self, collection: &Collection, span: Span) -> Result<Rc<Fact>, Diagnostic> {
let sequence = self.expression(&collection.sequence)?;
if let Some(limit) = &collection.limit {
self.expression(limit)?;
}
let extra = collection
.extra
.as_ref()
.map(|value| self.expression(value))
.transpose()?;
let signature = &self.signatures[collection.handler];
let item = if collection.sequence.value_type == Type::Bytes {
Fact::scalar(Some(Range {
minimum: Number::Int(0),
maximum: Number::Int(255),
}))
} else {
sequence
.item
.clone()
.unwrap_or_else(|| Fact::unknown(&signature.inputs[0].1))
};
self.require_value(&collection.sequence, &item, signature.input_ranges[0])?;
if let (Some(extra), Some(value)) = (&extra, &collection.extra) {
self.require_value(value, extra, signature.input_ranges[1])?;
if collection.operation.reduction() && signature.returns.is_some() {
require(signature.output_ranges[0], signature.input_ranges[1], span)?;
}
}
Ok(match collection.operation {
Operation::ParallelEach => Fact::unknown(&Type::Unit),
Operation::Each => Rc::new(Fact {
item: Some(self.outputs(collection.handler)),
..Fact::default()
}),
Operation::Filter => sequence,
Operation::Find => item,
Operation::Reduce | Operation::ReduceUntil => {
let initial = extra.expect("reduction state").range;
let range = if signature.returns.is_none() {
initial
} else {
initial
.zip(signature.output_ranges[0])
.map(|(initial, next)| initial.hull(next))
};
Fact::scalar(range)
}
Operation::Sum => Fact::unknown(&signature.outputs[0].1),
})
}
}
+78
View File
@@ -0,0 +1,78 @@
//! 值事实与源码变量分离:复制共享不可变快照,写入只复制被修改的路径。
//! 名义字段约束按访问读取,避免展开整个共享类型图。
use crate::compiler::model::{Definition, Type, numeric::Range};
use std::{collections::BTreeMap, rc::Rc};
#[derive(Clone, Default)]
pub(super) struct Fact {
pub range: Option<Range>,
pub fields: BTreeMap<usize, Rc<Fact>>,
pub item: Option<Rc<Fact>>,
}
impl Fact {
pub fn scalar(range: Option<Range>) -> Rc<Self> {
Rc::new(Self {
range,
..Self::default()
})
}
pub fn unknown(kind: &Type) -> Rc<Self> {
Self::scalar(matches!(kind, Type::Int | Type::Decimal).then(|| Range::full(kind)))
}
pub fn field(&self, index: usize, kind: &Type, definitions: &[Definition]) -> Rc<Self> {
if let Some(value) = self.fields.get(&index) {
return value.clone();
}
if let Type::Named(record) = kind
&& let Some(range) = definitions[*record].ranges[index]
{
return Self::scalar(Some(range));
}
Self::unknown(kind.fields(definitions).expect("checked field")[index].1)
}
pub fn union(left: &Self, right: &Self) -> Rc<Self> {
Rc::new(Self {
range: left
.range
.zip(right.range)
.map(|(left, right)| left.hull(right)),
fields: left
.fields
.iter()
.filter_map(|(index, value)| {
right
.fields
.get(index)
.map(|other| (*index, Self::union(value, other)))
})
.collect(),
item: left
.item
.as_ref()
.zip(right.item.as_ref())
.map(|(left, right)| Self::union(left, right)),
})
}
pub fn write(
target: &mut Rc<Self>,
path: &[usize],
kind: &Type,
value: Rc<Self>,
definitions: &[Definition],
) {
let Some((&index, rest)) = path.split_first() else {
*target = value;
return;
};
let mut field = target.field(index, kind, definitions);
let field_type = kind.fields(definitions).expect("checked field")[index].1;
Self::write(&mut field, rest, field_type, value, definitions);
Rc::make_mut(target).fields.insert(index, field);
}
}
+222
View File
@@ -0,0 +1,222 @@
//! 验证范围合同并推导成功返回值;证明只存在于编译期。
//! 按现有调用依赖顺序检查,生成器不接收动态范围检查节点。
mod expression;
mod facts;
mod number;
use super::names;
use crate::compiler::model::{
numeric::{Bound, Number, Range},
*,
};
use crate::syntax::{ast, diagnostic::Diagnostic, source::Span};
use facts::Fact;
use std::rc::Rc;
struct Proof<'program> {
definitions: &'program [Definition],
signatures: &'program [Signature],
variables: Vec<Rc<Fact>>,
remaining: usize,
arithmetic: std::collections::BTreeSet<(usize, usize, usize)>,
}
pub(super) fn check(
function: &mut Function,
definitions: &[Definition],
signatures: &[Signature],
) -> Result<(), Diagnostic> {
let mut proof = Proof {
definitions,
signatures,
variables: Vec::new(),
remaining: 16_384,
arithmetic: Default::default(),
};
let mut returns: Option<Vec<Option<Range>>> = None;
for clause in &function.clauses {
proof.variables = clause
.variables
.iter()
.map(|(_, kind)| Fact::unknown(kind))
.collect();
for (index, (_, kind)) in function.signature.inputs.iter().enumerate() {
if let Some(range) = super::clauses::range(
&function.clauses,
clause,
index,
kind,
definitions,
&mut proof.remaining,
)? {
proof.variables[index] = Fact::scalar(Some(range));
}
}
for binding in &clause.bindings {
let Type::Named(definition) = clause.variables[binding.input].1 else {
unreachable!()
};
let Shape::Choice(variants) = &definitions[definition].shape else {
unreachable!()
};
let Pattern::Variant(variant) = clause.patterns[binding.input].inner() else {
unreachable!()
};
if let Some(range) = variants[*variant].ranges[binding.field] {
proof.variables[binding.variable] = Fact::scalar(Some(range));
}
}
for statement in &clause.body {
match statement {
Statement::Assign {
variable,
fields,
value,
} => {
let fact = proof.expression(value)?;
let mut kind = &clause.variables[*variable].1;
let mut expected = clause
.outputs
.iter()
.position(|output| output == variable)
.and_then(|index| function.signature.output_ranges[index]);
for &index in fields {
expected = match kind {
Type::Named(record) => definitions[*record].ranges[index],
_ => None,
};
kind = kind.fields(definitions).expect("checked field write")[index].1;
}
proof.require_value(value, &fact, expected)?;
Fact::write(
&mut proof.variables[*variable],
fields,
&clause.variables[*variable].1,
fact,
definitions,
);
}
Statement::Action(value) | Statement::Fail(value) => {
proof.expression(value)?;
}
}
}
if !clause.terminates {
let actual = clause
.outputs
.iter()
.map(|index| proof.variables[*index].range)
.collect::<Vec<_>>();
for (&actual, &expected) in actual.iter().zip(&function.signature.output_ranges) {
require(actual, expected, clause.span)?;
}
if let Some(previous) = &mut returns {
for (previous, actual) in previous.iter_mut().zip(actual) {
*previous = previous.zip(actual).map(|(left, right)| left.hull(right));
}
} else {
returns = Some(actual);
}
}
}
if let Some(returns) = returns {
function.signature.output_ranges = returns;
}
function.proven_arithmetic = proof.arithmetic;
Ok(())
}
pub(super) fn handler(
expected: &Signature,
actual: &Signature,
span: Span,
) -> Result<(), Diagnostic> {
// 输入逆变:调用者承诺的值必须能交给实际 handler;输出协变反向检查。
for ((_, kind), (&provided, &required)) in expected
.inputs
.iter()
.zip(expected.input_ranges.iter().zip(&actual.input_ranges))
{
require(provided.or_else(|| numeric_range(kind)), required, span)?;
}
if actual.returns.is_none() {
return Ok(());
}
for ((_, kind), (&provided, &required)) in actual
.outputs
.iter()
.zip(actual.output_ranges.iter().zip(&expected.output_ranges))
{
require(provided.or_else(|| numeric_range(kind)), required, span)?;
}
Ok(())
}
fn numeric_range(kind: &Type) -> Option<Range> {
matches!(kind, Type::Int | Type::Decimal).then(|| Range::full(kind))
}
impl Proof<'_> {
fn require_value(
&self,
value: &Expression,
fact: &Fact,
expected: Option<Range>,
) -> Result<(), Diagnostic> {
// 无成功返回值的表达式先传播故障,不会抵达构造或赋值边界。
if let Err(error) = require(fact.range, expected, value.span)
&& !super::values::never_returns(value, self.signatures)
{
return Err(error);
}
Ok(())
}
}
pub(super) fn fields(
fields: &[ast::Field],
names: &super::package::Types,
) -> Result<Vec<Option<Range>>, Diagnostic> {
fields
.iter()
.map(|field| {
if field.bounds.is_empty() {
return Ok(None);
}
let kind = names::resolve(&field.kind, names)?;
if !matches!(kind, Type::Int | Type::Decimal) {
return Err(Diagnostic::new(
"C005",
"ranges require nonnullable Int or Decimal",
field.span,
));
}
let bounds = field
.bounds
.iter()
.map(|bound| {
Ok(Bound {
operator: bound.operator.clone(),
value: Number::parse(&bound.value, &kind)
.map_err(|error| Diagnostic::new("C008", error, field.span))?,
})
})
.collect::<Result<Vec<_>, Diagnostic>>()?;
Range::bounds(&kind, &bounds).map(Some).ok_or_else(|| {
Diagnostic::new("C011", "range contains no representable value", field.span)
})
})
.collect()
}
fn require(actual: Option<Range>, expected: Option<Range>, span: Span) -> Result<(), Diagnostic> {
if expected.is_some_and(|expected| actual.is_none_or(|actual| !expected.contains(actual))) {
return Err(Diagnostic::new(
"C011",
"value is not proven to satisfy its range",
span,
));
}
Ok(())
}
+83
View File
@@ -0,0 +1,83 @@
//! 仅对受检算术建立区间;不能证明的操作保留完整值域。
//! Decimal 端点使用运行时同一套舍入与溢出语义。
use crate::compiler::model::numeric::{Number, Range};
use dec::Decimal128;
use dever_runtime::value::decimal::Decimal;
pub(super) fn binary(operator: &str, left: Range, right: Range) -> Option<Range> {
if matches!(operator, "/" | "//") {
let zero = match right.minimum {
Number::Int(_) => Number::Int(0),
Number::Decimal(_) => Number::Decimal(Decimal128::ZERO),
Number::Float(_) => return None,
};
if right.minimum <= zero && right.maximum >= zero {
return None;
}
}
let mut result = None;
for left in [left.minimum, left.maximum] {
for right in [right.minimum, right.maximum] {
let value = operation(operator, left, right)?;
result = Some(result.map_or(Range::point(value), |range: Range| {
range.hull(Range::point(value))
}));
}
}
result
}
pub(super) fn negate(range: Range) -> Option<Range> {
let negate = |number| match number {
Number::Int(value) => value.checked_neg().map(Number::Int),
Number::Decimal(value) => Some(Number::Decimal(-value)),
Number::Float(_) => None,
};
Some(Range {
minimum: negate(range.maximum)?,
maximum: negate(range.minimum)?,
})
}
pub(super) fn decimal(range: Range) -> Range {
let convert = |value| {
let Number::Int(value) = value else {
unreachable!("checked Int conversion")
};
Number::Decimal(Decimal128::from_be_bytes(
Decimal::from_int(value).to_be_bytes(),
))
};
Range {
minimum: convert(range.minimum),
maximum: convert(range.maximum),
}
}
fn operation(operator: &str, left: Number, right: Number) -> Option<Number> {
match (left, right) {
(Number::Int(left), Number::Int(right)) => match operator {
"+" => left.checked_add(right),
"-" => left.checked_sub(right),
"*" => left.checked_mul(right),
"//" => left.checked_div(right),
_ => None,
}
.map(Number::Int),
(Number::Decimal(left), Number::Decimal(right)) => {
let left = Decimal::from_be_bytes(left.to_be_bytes());
let right = Decimal::from_be_bytes(right.to_be_bytes());
let mut context = Decimal::context();
match operator {
"+" => left.checked_add(right, &mut context),
"-" => left.checked_sub(right, &mut context),
"*" => left.checked_mul(right, &mut context),
"/" => left.checked_div(right, &mut context),
_ => None,
}
.map(|value| Number::Decimal(Decimal128::from_be_bytes(value.to_be_bytes())))
}
_ => None,
}
}
+169
View File
@@ -0,0 +1,169 @@
//! 合并同一函数各分句的输入类型与输出合同。
//! 各分句必须保持名称、可见性、pure 和 recover 一致。
use super::names;
use crate::compiler::model::*;
use crate::syntax::{ast, diagnostic::Diagnostic, source::Span};
use std::collections::HashSet;
pub(super) fn check(
group: &[ast::Function],
types: &super::package::Types,
definitions: &[Definition],
) -> Result<Signature, Diagnostic> {
let first = &group[0];
if first
.recovery
.as_ref()
.is_some_and(|reason| reason.trim().is_empty())
{
return Err(Diagnostic::new(
"C013",
"recovery requires a nonempty reason",
first.span,
));
}
let mut inputs = Vec::new();
// null/other 单独不能推导类型,须从同一参数位置的其他分句获得类型。
for position in 0..first.inputs.len() {
let mut inferred = None;
let mut nullable = false;
for clause in group {
let input = &clause.inputs[position];
nullable |= matches!(input.pattern, ast::Pattern::Literal(ast::Literal::Null));
if let Some(mut kind) = input_type(&input.pattern, input.span, types, definitions)? {
if let Type::Optional(inner) = kind {
nullable = true;
kind = *inner;
}
if inferred.as_ref().is_some_and(|previous| *previous != kind) {
return Err(Diagnostic::new(
"C005",
"clause input types disagree",
input.span,
));
}
inferred = Some(kind);
}
}
let kind = inferred.ok_or_else(|| {
Diagnostic::new(
"C005",
"cannot infer input type from null/other alone",
first.inputs[position].span,
)
})?;
inputs.push((
first.inputs[position].name.clone(),
if nullable {
Type::Optional(Box::new(kind))
} else {
kind
},
));
}
let outputs = first
.outputs
.iter()
.map(|field| Ok((field.name.clone(), names::resolve(&field.kind, types)?)))
.collect::<Result<Vec<_>, Diagnostic>>()?;
let mut seen = HashSet::new();
if outputs.iter().any(|(_, kind)| *kind == Type::Upload) {
return Err(Diagnostic::new(
"C013",
"Upload cannot be returned",
first.span,
));
}
let output_ranges = super::range::fields(&first.outputs, types)?;
for (name, _) in inputs.iter().chain(&outputs) {
if !seen.insert(name) {
return Err(Diagnostic::new(
"C002",
"input and output names must be distinct",
first.span,
));
}
}
for clause in &group[1..] {
let actual = clause
.outputs
.iter()
.map(|field| Ok((field.name.clone(), names::resolve(&field.kind, types)?)))
.collect::<Result<Vec<_>, Diagnostic>>()?;
if actual != outputs
|| super::range::fields(&clause.outputs, types)? != output_ranges
|| clause.pure != first.pure
|| clause.transaction != first.transaction
|| clause.recovery != first.recovery
|| clause.public != first.public
|| clause
.inputs
.iter()
.map(|input| &input.name)
.ne(inputs.iter().map(|(name, _)| name))
{
return Err(Diagnostic::new(
"C013",
"function clause contracts disagree",
clause.span,
));
}
}
let input_flows = vec![Flow::default(); inputs.len()];
if first.public {
for (_, kind) in inputs.iter().chain(&outputs) {
types.public_type(kind, first.span)?;
}
}
Ok(Signature {
name: first.name.clone(),
transaction: first.transaction,
input_ranges: vec![None; inputs.len()],
output_ranges,
inputs,
outputs,
source: first.span.source,
pure: first.pure,
effects: Effects::default(),
recovery: first.recovery.clone(),
static_parameters: 0,
failures: Vec::new(),
input_flows,
returns: None,
})
}
fn input_type(
pattern: &ast::Pattern,
span: Span,
types: &super::package::Types,
definitions: &[Definition],
) -> Result<Option<Type>, Diagnostic> {
Ok(match pattern {
ast::Pattern::Typed(reference, _) => {
if let Some((index, _)) = names::variant_type(reference, types, definitions)? {
Some(Type::Named(index))
} else {
Some(names::resolve(reference, types)?)
}
}
ast::Pattern::Variant(path, _) => {
let (index, _) = names::variant_type(path, types, definitions)?
.ok_or_else(|| Diagnostic::new("C004", "unknown choice variant", span))?;
Some(Type::Named(index))
}
ast::Pattern::Literal(ast::Literal::Bool(_)) => Some(Type::Bool),
ast::Pattern::Literal(ast::Literal::Text(_)) => Some(Type::Text),
ast::Pattern::Literal(ast::Literal::Number(number)) => {
let kind = numeric::Number::literal_type(number);
numeric::Number::parse(number, &kind)
.map_err(|error| Diagnostic::new("C008", error, span))?;
Some(kind)
}
ast::Pattern::Other | ast::Pattern::Literal(ast::Literal::Null) => None,
ast::Pattern::Handler(_, _) => {
unreachable!("handler inputs are resolved before value signatures")
}
})
}
+40
View File
@@ -0,0 +1,40 @@
//! 标准库原语按固定类型检查,不把名字或配置带入运行时解释。
use super::Context;
use crate::compiler::model::*;
use crate::syntax::{ast, diagnostic::Diagnostic};
impl Context<'_> {
pub(super) fn standard(
&mut self,
operation: StandardOperation,
arguments: &[ast::Expression],
expected: Option<&Type>,
source: &ast::Expression,
) -> Result<Expression, Diagnostic> {
let (package, inputs, value_type) = operation.signature();
if self.definition_names.packages[self.source] != package {
return Err(Diagnostic::new(
"C006",
"standard primitive belongs to its official package",
source.span,
));
}
if inputs.len() != arguments.len() {
return Err(self.error(source, "standard primitive argument count differs"));
}
let values = arguments
.iter()
.zip(inputs)
.map(|(argument, kind)| self.expression(argument, Some(&kind)))
.collect::<Result<Vec<_>, _>>()?;
self.coerce(
Expression {
value_type,
kind: Node::Standard(operation, values),
span: source.span,
},
expected,
)
}
}
+31
View File
@@ -0,0 +1,31 @@
//! 描述编译时可确认相同的 Map 键与别名。
//! 未知表达式只按来源区分,不猜测不同计算是否相等;运行时仍检查动态重复。
use super::super::Context;
use crate::compiler::model::{Expression, Node};
use crate::syntax::ast::Literal;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub(crate) enum Key {
Bool(bool),
Int(i64),
Text(String),
Variant(usize, usize),
Input(usize),
Expression(usize),
}
impl Context<'_> {
pub(in crate::compiler::check) fn key(&self, expression: &Expression) -> Key {
match &expression.kind {
Node::Literal(Literal::Bool(value)) => Key::Bool(*value),
Node::Literal(Literal::Text(value)) => Key::Text(value.clone()),
Node::Literal(Literal::Number(value)) => {
Key::Int(value.parse().expect("checked Int key"))
}
Node::Variant(index, variant, _) => Key::Variant(*index, *variant),
Node::Variable(index) => self.keys.get(index).cloned().unwrap_or(Key::Input(*index)),
_ => Key::Expression(expression.span.start),
}
}
}
+155
View File
@@ -0,0 +1,155 @@
//! 检查有序 Map 的构造、访问及 List 的基本存取。
//! 空容器从上下文推断类型,已知重复键在编译时拒绝。
mod key;
use super::Context;
use crate::compiler::model::*;
use crate::syntax::{ast, diagnostic::Diagnostic};
pub(super) use key::Key;
use std::collections::HashSet;
impl Context<'_> {
pub(super) fn map(
&mut self,
entries: &[(ast::Expression, ast::Expression)],
expected: Option<&Type>,
source: &ast::Expression,
) -> Result<Expression, Diagnostic> {
let (mut key_type, mut value_type) = match present(expected) {
Some(Type::Map(key, value)) => (Some(*key.clone()), Some(*value.clone())),
_ => (None, None),
};
let mut values = Vec::new();
let mut seen = HashSet::new();
for (key, value) in entries {
let key = self.expression(key, key_type.as_ref())?;
if !key.value_type.map_key(self.definitions) {
return Err(Diagnostic::new("C005", "invalid Map key type", key.span));
}
if !seen.insert(self.key(&key)) {
return Err(Diagnostic::new(
"C002",
"duplicate Map literal key",
key.span,
));
}
let value = self.expression(value, value_type.as_ref())?;
key_type.get_or_insert_with(|| key.value_type.clone());
value_type.get_or_insert_with(|| value.value_type.clone());
values.push((key, value));
}
let (Some(key), Some(value)) = (key_type, value_type) else {
return Err(self.error(source, "empty Map requires a type context"));
};
Ok(Expression {
kind: Node::Map(values),
value_type: Type::Map(Box::new(key), Box::new(value)),
span: source.span,
})
}
pub(super) fn access(
&mut self,
operation: Access,
arguments: &[ast::Expression],
expected: Option<&Type>,
source: &ast::Expression,
) -> Result<Expression, Diagnostic> {
let arity = match operation {
Access::First | Access::Entries => 1,
Access::Append | Access::Get | Access::Remove | Access::Contains => 2,
Access::Put => 3,
};
if arguments.len() != arity {
return Err(self.error(source, "collection operation argument count differs"));
}
let context = match (operation, present(expected)) {
(Access::Append, Some(kind @ Type::List(_)))
| (Access::Put | Access::Remove, Some(kind @ Type::Map(_, _))) => Some(kind.clone()),
(Access::First, Some(kind)) if matches!(&arguments[0].kind, ast::ExpressionKind::List(values) if values.is_empty()) => {
Some(Type::List(Box::new(kind.clone())))
}
(Access::Entries, Some(Type::List(inner))) => match inner.as_ref() {
Type::MapEntry(key, value) => Some(Type::Map(key.clone(), value.clone())),
_ => None,
},
_ => None,
};
let mut inferred = Vec::new();
let context = if context.is_none() {
match (&operation, &arguments[0].kind) {
(Access::Append | Access::Contains, ast::ExpressionKind::List(values))
if values.is_empty() =>
{
let value = self.expression(&arguments[1], None)?;
let kind = Type::List(Box::new(value.value_type.clone()));
inferred.push(value);
Some(kind)
}
(Access::Put, ast::ExpressionKind::Map(values)) if values.is_empty() => {
let key = self.expression(&arguments[1], None)?;
let value = self.expression(&arguments[2], None)?;
let kind = Type::Map(
Box::new(key.value_type.clone()),
Box::new(value.value_type.clone()),
);
inferred.extend([key, value]);
Some(kind)
}
_ => None,
}
} else {
context
};
let container = self.expression(&arguments[0], context.as_ref())?;
super::names::validate(&container.value_type, self.definitions, source.span)?;
let (value_type, kinds) = match (operation, &container.value_type) {
(Access::Contains, Type::List(inner)) if inner.comparable(self.definitions) => {
(Type::Bool, vec![*inner.clone()])
}
(Access::Append, Type::List(inner)) => {
(container.value_type.clone(), vec![*inner.clone()])
}
(Access::First, Type::List(inner)) => ((*inner.clone()).optional(), Vec::new()),
(Access::Get, Type::Map(key, value)) => {
((*value.clone()).optional(), vec![*key.clone()])
}
(Access::Put, Type::Map(key, value)) => (
container.value_type.clone(),
vec![*key.clone(), *value.clone()],
),
(Access::Remove, Type::Map(key, _)) => {
(container.value_type.clone(), vec![*key.clone()])
}
(Access::Entries, Type::Map(key, value)) => (
Type::List(Box::new(Type::MapEntry(key.clone(), value.clone()))),
Vec::new(),
),
_ => return Err(self.error(source, "invalid collection operation type")),
};
let mut values = vec![container];
if inferred.is_empty() {
for (argument, kind) in arguments[1..].iter().zip(kinds) {
values.push(self.expression(argument, Some(&kind))?);
}
} else {
values.extend(inferred);
}
self.coerce(
Expression {
kind: Node::Access(operation, values),
value_type,
span: source.span,
},
expected,
)
}
}
fn present(expected: Option<&Type>) -> Option<&Type> {
match expected {
Some(Type::Optional(inner)) => Some(inner),
expected => expected,
}
}
+140
View File
@@ -0,0 +1,140 @@
//! 挂起点检查仍需使用的值及按求值顺序暂存的参数,资源不能藏在 record 中跨线程。
use super::ownership::error;
use crate::compiler::model::*;
use crate::syntax::diagnostic::Diagnostic;
use std::collections::BTreeSet;
pub(super) fn check(
function: &Function,
signatures: &[Signature],
definitions: &[Definition],
) -> Result<(), Diagnostic> {
for clause in &function.clauses {
let mut live = clause.outputs.iter().copied().collect::<BTreeSet<_>>();
for statement in clause.body.iter().rev() {
let value = match statement {
Statement::Assign {
variable,
fields,
value,
} => {
if fields.is_empty() {
live.remove(variable);
} else {
live.insert(*variable);
}
value
}
Statement::Action(value) | Statement::Fail(value) => value,
};
expression(value, &mut live, clause, signatures, definitions)?;
}
}
Ok(())
}
fn expression(
value: &Expression,
live: &mut BTreeSet<usize>,
clause: &Clause,
signatures: &[Signature],
definitions: &[Definition],
) -> Result<(), Diagnostic> {
if let Node::Capture(capture) = &value.kind {
return expression(&capture.call, live, clause, signatures, definitions);
}
let suspends = match &value.kind {
Node::Call(index, _) => signatures[*index].effects.suspend,
Node::Net(operation, _) => operation.effects().suspend,
Node::Http(_, _) => true,
Node::Upload(operation, _) => operation.effects().suspend,
Node::Schedule(Schedule::Parallel | Schedule::Blocking, _, _)
| Node::Join { .. }
| Node::Timeout { .. }
| Node::Race(_)
| Node::GroupRun { .. }
| Node::Timer(Timer::Delay, _) => true,
Node::Send(_, _) | Node::Receive(_) => true,
Node::Close(value) => matches!(value.value_type, Type::Channel(_)),
Node::Collection(collection) => {
signatures[collection.handler].effects.suspend
|| collection.operation == Operation::ParallelEach
|| matches!(collection.sequence.value_type, Type::AsyncStream(_))
}
_ => false,
};
if suspends {
if let Node::Collection(collection) = &value.kind {
let signature = &signatures[collection.handler];
if collection.operation == Operation::ParallelEach
&& (matches!(collection.sequence.value_type, Type::Stream(_))
|| (!signature.effects.suspend
&& (signature.effects.parallel || signature.effects.schedule)))
{
return Err(error(
value.span,
"parallel workers in a suspending body cannot retain a synchronous Stream or start nested concurrency",
));
}
if signature
.inputs
.iter()
.chain(&signature.outputs)
.any(|(_, kind)| !kind.transferable(definitions))
{
return Err(error(
value.span,
"suspending collection retains non-transferable items, state or outputs",
));
}
}
for index in live.iter() {
let kind = &clause.variables[*index].1;
// Upload 可随原请求挂起;独立任务边界仍由 transferable 拒绝移交。
if !kind.affine() && *kind != Type::Upload && !kind.transferable(definitions) {
return Err(error(
value.span,
"non-transferable value remains live across suspension",
));
}
}
if let Node::Call(index, _) | Node::Schedule(_, index, _) = &value.kind
&& signatures[*index]
.inputs
.iter()
.any(|(_, kind)| *kind != Type::Upload && !kind.transferable(definitions))
{
return Err(error(
value.span,
"suspending call retains non-transferable arguments",
));
}
}
if let Node::Variable(index) = value.kind {
live.insert(index);
}
let mut children = Vec::new();
value
.children::<std::convert::Infallible>(|child| {
children.push(child);
Ok(())
})
.unwrap();
for (position, child) in children.iter().enumerate().rev() {
if super::super::effects::expression(child, signatures, true).suspend
&& children[..position].iter().any(|previous| {
!previous.value_type.affine()
&& previous.value_type != Type::Upload
&& !previous.value_type.transferable(definitions)
})
{
return Err(error(
child.span,
"argument evaluation retains a non-transferable temporary across suspension",
));
}
expression(child, live, clause, signatures, definitions)?;
}
Ok(())
}
+284
View File
@@ -0,0 +1,284 @@
//! 校验任务提交、受限 worker 边界及等待的具体结果类型。
mod liveness;
mod ownership;
use super::Context;
use crate::compiler::model::*;
use crate::syntax::{ast, diagnostic::Diagnostic};
pub(super) use ownership::check;
impl Context<'_> {
pub(super) fn schedule(
&mut self,
operation: Schedule,
arguments: &[ast::Expression],
expected: Option<&Type>,
source: &ast::Expression,
) -> Result<Expression, Diagnostic> {
if operation == Schedule::Run && arguments.len() == 2 {
return self.group_run(arguments, expected, source);
}
let [call] = arguments else {
return Err(self.error(source, "task boundary requires one function call"));
};
let value = self.expression(call, None)?;
let Node::Call(function, values) = value.kind else {
return Err(self.error(source, "task boundary requires an ordinary function call"));
};
let signature = &self.signatures[function];
if operation != Schedule::Run
&& (signature.effects.suspend
|| signature.effects.handler
|| signature.effects.schedule
|| (operation == Schedule::Parallel && !signature.pure)
|| (operation == Schedule::Blocking && !signature.effects.blocking))
{
return Err(Diagnostic::new(
"C013",
"worker boundary requires a pure or known blocking synchronous function",
source.span,
));
}
if !transferable(signature, self.definitions) {
return Err(Diagnostic::new(
"C013",
"task inputs, outputs and failure payloads must be transferable",
source.span,
));
}
self.coerce(
Expression {
value_type: if operation == Schedule::Run {
Type::Task(function)
} else {
signature.output()
},
kind: Node::Schedule(operation, function, values),
span: source.span,
},
expected,
)
}
pub(super) fn join(
&mut self,
stop: bool,
arguments: &[ast::Expression],
expected: Option<&Type>,
source: &ast::Expression,
) -> Result<Expression, Diagnostic> {
let [argument] = arguments else {
return Err(self.error(source, "wait/stop requires one Task"));
};
let task = self.expression(argument, None)?;
let output = match task.value_type {
Type::Task(function) => self.signatures[function].output(),
Type::Group(_) => Type::Unit,
_ => return Err(self.error(source, "wait/stop requires a Task or Group")),
};
if stop && output != Type::Unit {
return Err(self.error(source, "stop requires a zero-output Task"));
}
self.coerce(
Expression {
value_type: output,
kind: Node::Join {
task: Box::new(task),
stop,
},
span: source.span,
},
expected,
)
}
pub(super) fn group(
&mut self,
arguments: &[ast::Expression],
expected: Option<&Type>,
source: &ast::Expression,
) -> Result<Expression, Diagnostic> {
let [limit] = arguments else {
return Err(self.error(source, "group requires one limit"));
};
let limit = self.expression(limit, Some(&Type::Int))?;
self.coerce(
Expression {
value_type: Type::Group(self.function),
kind: Node::Group(Box::new(limit)),
span: source.span,
},
expected,
)
}
fn group_run(
&mut self,
arguments: &[ast::Expression],
expected: Option<&Type>,
source: &ast::Expression,
) -> Result<Expression, Diagnostic> {
let group = self.expression(&arguments[0], None)?;
if !matches!(group.value_type, Type::Group(_)) || !matches!(group.kind, Node::Variable(_)) {
return Err(self.error(source, "run(group, call) requires a local Group"));
}
let scheduled = self.schedule(Schedule::Run, &arguments[1..], None, source)?;
let Node::Schedule(Schedule::Run, target, values) = scheduled.kind else {
unreachable!("checked run");
};
if !self.signatures[target].outputs.is_empty() {
return Err(self.error(source, "Group tasks must have zero outputs"));
}
self.coerce(
Expression {
value_type: Type::Unit,
kind: Node::GroupRun {
group: Box::new(group),
target,
arguments: values,
},
span: source.span,
},
expected,
)
}
pub(super) fn timeout(
&mut self,
arguments: &[ast::Expression],
expected: Option<&Type>,
source: &ast::Expression,
) -> Result<Expression, Diagnostic> {
let [task, millis, fallback] = arguments else {
return Err(self.error(
source,
"timeout requires a Task, duration and static fallback",
));
};
let task = self.expression(task, None)?;
let Type::Task(target) = task.value_type else {
return Err(self.error(source, "timeout requires a Task"));
};
let millis = self.expression(millis, Some(&Type::Int))?;
let fallback = self
.scope
.targets
.get(&fallback.span.start)
.copied()
.ok_or_else(|| {
self.error(
source,
"timeout fallback must be a zero-input static function",
)
})?;
if !self.signatures[fallback].inputs.is_empty()
|| self.signatures[fallback].outputs != self.signatures[target].outputs
{
return Err(self.error(
source,
"timeout fallback must have the Task's named output signature",
));
}
self.coerce(
Expression {
value_type: self.signatures[target].output(),
kind: Node::Timeout {
task: Box::new(task),
millis: Box::new(millis),
fallback,
},
span: source.span,
},
expected,
)
}
pub(super) fn race(
&mut self,
arguments: &[ast::Expression],
expected: Option<&Type>,
source: &ast::Expression,
) -> Result<Expression, Diagnostic> {
if arguments.len() < 2 {
return Err(self.error(source, "race requires at least two Tasks"));
}
let mut tasks = Vec::new();
let mut signature = None;
for argument in arguments {
let task = self.expression(argument, None)?;
let Type::Task(target) = task.value_type else {
return Err(self.error(argument, "race arguments must be Tasks"));
};
let outputs = &self.signatures[target].outputs;
if signature.is_some_and(|previous| previous != outputs) {
return Err(self.error(argument, "racing Tasks must have identical named outputs"));
}
signature = Some(outputs);
tasks.push(task);
}
let Type::Task(first) = tasks[0].value_type else {
unreachable!("checked Task");
};
self.coerce(
Expression {
value_type: self.signatures[first].output(),
kind: Node::Race(tasks),
span: source.span,
},
expected,
)
}
pub(super) fn timer(
&mut self,
timer: Timer,
arguments: &[ast::Expression],
expected: Option<&Type>,
source: &ast::Expression,
) -> Result<Expression, Diagnostic> {
let package = if timer == Timer::Delay {
"dever.task"
} else {
"dever.time"
};
if self.definition_names.packages[self.source] != package {
return Err(Diagnostic::new(
"C006",
"timer intrinsics belong to their official package",
source.span,
));
}
let [millis] = arguments else {
return Err(self.error(source, "sleep requires one duration"));
};
let value = self.expression(millis, Some(&Type::Int))?;
let value_type = self.io_result("dever.time.SleepResult", &[("Done", vec![])], source)?;
self.coerce(
Expression {
value_type,
kind: Node::Timer(timer, Box::new(value)),
span: source.span,
},
expected,
)
}
}
pub(super) fn transferable(signature: &Signature, definitions: &[Definition]) -> bool {
// 入队继承入口提供的只读执行身份;请求、Cookie 和可变认证上下文仍不能离开请求。
!signature.effects.request
&& signature
.inputs
.iter()
.chain(&signature.outputs)
.all(|(_, kind)| kind.transferable(definitions))
&& signature.failures.iter().all(|failure| {
let Shape::Choice(variants) = &definitions[failure.definition].shape else {
unreachable!()
};
variants[failure.variant]
.fields
.iter()
.all(|(_, kind)| kind.transferable(definitions))
})
}
+141
View File
@@ -0,0 +1,141 @@
//! Task 不可复制或嵌套;正常返回前恰好消费一次,失败路径清理本函数剩余句柄。
use crate::compiler::model::*;
use crate::syntax::{ast::Literal, diagnostic::Diagnostic, source::Span};
use std::collections::BTreeSet;
pub fn check(
function: &mut Function,
signatures: &[Signature],
definitions: &[Definition],
) -> Result<(), Diagnostic> {
for clause in &function.clauses {
let mut state = Ownership {
tasks: BTreeSet::new(),
};
for statement in &clause.body {
match statement {
Statement::Assign {
variable,
fields,
value,
} if value.value_type.affine() => {
if !fields.is_empty()
|| !matches!(
value.kind,
Node::Schedule(Schedule::Run, _, _) | Node::Group(_)
)
{
return Err(error(
value.span,
"Task/Group cannot be copied or stored inside another value",
));
}
if state.tasks.contains(variable) {
return Err(error(
value.span,
"Task/Group must be consumed before reassignment",
));
}
state.expression(value, true)?;
state.tasks.insert(*variable);
}
Statement::Assign { value, .. }
| Statement::Action(value)
| Statement::Fail(value) => state.expression(value, false)?,
}
}
if !clause.terminates && !state.tasks.is_empty() {
return Err(error(
clause.span,
"Task/Group must be consumed by its joining operation before returning",
));
}
}
if function.signature.effects.suspend {
super::liveness::check(function, signatures, definitions)?;
}
Ok(())
}
struct Ownership {
tasks: BTreeSet<usize>,
}
impl Ownership {
fn expression(&mut self, value: &Expression, task_allowed: bool) -> Result<(), Diagnostic> {
if value.value_type.affine() && !task_allowed {
return Err(error(
value.span,
"Task/Group may only be used by its own task operations",
));
}
match &value.kind {
Node::Variable(index) if value.value_type.affine() => {
if !self.tasks.remove(index) {
return Err(error(value.span, "Task/Group has already been consumed"));
}
Ok(())
}
Node::Join { task, .. } => self.expression(task, true),
Node::Group(limit) => self.expression(limit, false),
Node::GroupRun {
group, arguments, ..
} => {
let Node::Variable(index) = group.kind else {
unreachable!("checked local Group");
};
if !self.tasks.contains(&index) {
return Err(error(group.span, "Group has already been consumed"));
}
for argument in arguments {
self.expression(argument, false)?;
}
if !self.tasks.contains(&index) {
return Err(error(
group.span,
"Group was consumed while evaluating its task arguments",
));
}
Ok(())
}
Node::Timeout { task, millis, .. } => {
self.expression(task, true)?;
self.expression(millis, false)
}
Node::Race(tasks) => {
for task in tasks {
self.expression(task, true)?;
}
Ok(())
}
Node::Schedule(Schedule::Run, _, values) => {
for value in values {
self.expression(value, false)?;
}
Ok(())
}
Node::Binary(operator, left, right) if matches!(operator.as_str(), "and" | "or") => {
self.expression(left, false)?;
let before = self.tasks.clone();
self.expression(right, false)?;
if let Node::Literal(Literal::Bool(value)) = left.kind {
if value != (operator == "and") {
self.tasks = before;
}
} else if self.tasks != before {
return Err(error(
right.span,
"short-circuit paths must consume the same Tasks",
));
}
Ok(())
}
_ => value.children(|child| self.expression(child, false)),
}
}
}
pub(super) fn error(span: Span, message: &str) -> Diagnostic {
Diagnostic::new("C013", message, span)
}
+66
View File
@@ -0,0 +1,66 @@
//! 断言、测试时钟和固定 Secret 输入只授予运行器加载的当前 Test 文件。
use super::Context;
use crate::compiler::model::{Expression, Node, Type};
use crate::syntax::{ast, diagnostic::Diagnostic};
impl Context<'_> {
pub(super) fn test_operation(
&mut self,
name: &str,
arguments: &[ast::Expression],
expected: Option<&Type>,
source: &ast::Expression,
) -> Result<Expression, Diagnostic> {
if self.definition_names.test_source != Some(self.source) {
return Err(Diagnostic::new(
"C006",
"assert, assertEq and secret require a compiler-owned Test source",
source.span,
));
}
let (kind, value_type) = match (name, arguments) {
("dever.test.advanceClock", [value]) => (
Node::TestClock(Box::new(self.expression(value, Some(&Type::Int))?)),
Type::Unit,
),
("dever.test.drainJobs", [value]) => (
Node::TestDrain {
limit: Box::new(self.expression(value, Some(&Type::Int))?),
handlers: self.jobs.iter().map(|entry| entry.function).collect(),
},
Type::Int,
),
("assert", [condition]) => (
Node::Assert(vec![self.expression(condition, Some(&Type::Bool))?]),
Type::Unit,
),
("assertEq", [actual, expected]) => {
let actual = self.expression(actual, None)?;
if !actual.value_type.comparable(self.definitions) {
return Err(self.error(source, "assertEq requires comparable values"));
}
let expected = self.expression(expected, Some(&actual.value_type))?;
if actual.value_type != expected.value_type {
return Err(
self.error(source, "assertEq values must have exactly the same type")
);
}
(Node::Assert(vec![actual, expected]), Type::Unit)
}
("secret", [value]) => (
Node::TestSecret(Box::new(self.expression(value, Some(&Type::Text))?)),
Type::Secret,
),
_ => return Err(self.error(source, "invalid test operation argument count")),
};
self.coerce(
Expression {
kind,
value_type,
span: source.span,
},
expected,
)
}
}
+59
View File
@@ -0,0 +1,59 @@
//! 检查 TLS 配置原语的包权限、参数和名义失败类型。
use super::Context;
use crate::{
compiler::model::*,
syntax::{ast, diagnostic::Diagnostic},
};
impl Context<'_> {
pub(super) fn tls(
&mut self,
operation: TlsOperation,
arguments: &[ast::Expression],
expected: Option<&Type>,
source: &ast::Expression,
) -> Result<Expression, Diagnostic> {
if self.definition_names.packages[self.source] != "dever.tls" {
return Err(Diagnostic::new(
"C006",
"TLS intrinsics belong to dever.tls",
source.span,
));
}
let (count, value_type) = match operation {
TlsOperation::System => (0, Type::ClientTls),
TlsOperation::Client => (
1,
self.io_result(
"ClientResult",
&[("Ready", vec![("config", Type::ClientTls)])],
source,
)?,
),
TlsOperation::Server => (
2,
self.io_result(
"ServerResult",
&[("Ready", vec![("config", Type::ServerTls)])],
source,
)?,
),
};
if arguments.len() != count {
return Err(self.error(source, "TLS argument count differs"));
}
let values = arguments
.iter()
.map(|value| self.expression(value, Some(&Type::Bytes)))
.collect::<Result<Vec<_>, _>>()?;
self.coerce(
Expression {
kind: Node::Tls(operation, values),
value_type,
span: source.span,
},
expected,
)
}
}
+80
View File
@@ -0,0 +1,80 @@
//! Upload 只在 HTTP App 与同领域 Port/Adapter 之间移动,不放宽事务和并发检查。
mod ownership;
use super::Context;
use crate::compiler::model::*;
use crate::syntax::{ast, diagnostic::Diagnostic};
pub(super) use ownership::check;
pub(super) fn nested(kind: &Type) -> bool {
match kind {
Type::Upload => true,
Type::Optional(inner)
| Type::List(inner)
| Type::Stream(inner)
| Type::AsyncStream(inner)
| Type::Channel(inner) => nested(inner),
Type::Map(key, value) | Type::MapEntry(key, value) => nested(key) || nested(value),
Type::Outputs(fields) => fields.iter().any(|(_, kind)| nested(kind)),
_ => false,
}
}
impl Context<'_> {
pub(super) fn upload(
&mut self,
operation: upload::Operation,
arguments: &[ast::Expression],
expected: Option<&Type>,
source: &ast::Expression,
) -> Result<Expression, Diagnostic> {
let role = package::role(&self.definition_names.packages[self.source]);
let allowed = if operation == upload::Operation::Put {
self.definition_names.packages[self.source] == "dever.storage"
} else {
matches!(role, Some((_, "app" | "adapter")))
};
if !allowed {
return Err(self.error(
source,
"Upload operations require App; storage.put requires its Adapter",
));
}
let [argument] = arguments else {
return Err(self.error(source, "Upload operation requires one Upload"));
};
let value = self.expression(argument, Some(&Type::Upload))?;
let value_type = match operation {
upload::Operation::Size => Type::Int,
upload::Operation::Filename | upload::Operation::ContentType => Type::Text,
upload::Operation::Close => Type::Unit,
upload::Operation::Put => {
let index = self
.definition_names
.resolve("dever.storage.PutResult", self.source, source.span)?
.ok_or_else(|| {
self.error(source, "storage.put requires the official PutResult")
})?;
let valid = matches!(&self.definitions[index].shape, Shape::Choice(variants)
if variants.len() == 2 && variants[0].name == "Stored" && !variants[0].error
&& variants[0].fields == [("key".into(), Type::Uuid)]
&& variants[1].name == "Failed" && variants[1].error
&& variants[1].fields == [("message".into(), Type::Text)]);
if !valid {
return Err(self.error(source, "invalid storage.PutResult contract"));
}
Type::Named(index)
}
};
self.coerce(
Expression {
kind: Node::Upload(operation, vec![value]),
value_type,
span: source.span,
},
expected,
)
}
}
Loaded 100 of 1479 files, more files were not shown because too many files have changed in this diff. Show more