Initial Dever 0.0.1
This commit is contained in:
commit
81233d098c
1479 files changed
+163782
No files matched your search
Generated
+2609
File diff suppressed because it is too large.
Load diff
+127
@@ -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
|
||||
@@ -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 源码位置诊断。
|
||||
@@ -0,0 +1,4 @@
|
||||
[toolchain]
|
||||
channel = "1.95.0"
|
||||
profile = "minimal"
|
||||
components = ["rustfmt", "clippy"]
|
||||
@@ -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;
|
||||
@@ -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],
|
||||
})
|
||||
}
|
||||
@@ -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()),
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()))
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()))
|
||||
}
|
||||
@@ -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()),
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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()])
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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()),
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -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("e) {
|
||||
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"
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 ¶meters {
|
||||
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))
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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>,
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in new issue
Block a user