Initial Dever 0.0.1

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

No files matched your search

+127
View File
@@ -0,0 +1,127 @@
use super::{ADAPTER, APP, PORT, packages};
#[test]
fn roles_cannot_call_around_the_port_boundary() {
for call in [
"app.read(value)",
"domain.read(value)",
"port.read(value)",
"foreign.library.read(value)",
"shop.account.adapter.other.helper(value)",
] {
let adapter = format!("port.read(value: Int) (answer: Int) {{ answer = {call} }}");
let error = packages(&[
("shop.account.app", APP),
(
"shop.account.domain",
"read(value: Int) (answer: Int) { answer = value }",
),
("shop.account.port", PORT),
("shop.account.adapter.local", &adapter),
(
"foreign.library",
"public read(value: Int) (answer: Int) { answer = value }",
),
])
.unwrap_err();
assert!(
matches!(error.code, "C004" | "C005" | "C006"),
"{call}: {error:?}"
);
}
for caller in ["shop.account.domain", "shop.other.app", "foreign"] {
let error = packages(&[
("shop.account.app", APP),
("shop.account.port", PORT),
("shop.account.adapter", ADAPTER),
(
caller,
"entry(value: Int) (answer: Int) { answer = shop.account.port.read(value) }",
),
])
.unwrap_err();
assert_eq!(error.code, "C006", "{caller}: {error:?}");
}
for call in [
"adapter.helper(value)",
"adapter.port.read(value)",
"shop.account.adapter.port.read(value)",
] {
assert!(
packages(&[
("shop.account.app", &APP.replace("port.read(value)", call)),
("shop.account.port", PORT),
("shop.account.adapter", ADAPTER)
])
.is_err(),
"{call}"
);
}
}
#[test]
fn only_app_types_in_the_complete_contract_graph_are_visible() {
let app = "type Problem { error Missing(message: Text) }\ntype Person { name: Text }\ntype Unrelated { number: Int }\nread(value: Person) (answer: Text) { answer = port.read(port.Row { person = value }) }";
let port = "type Row { person: app.Person }\nread(value: Row) (answer: Text) fails app.Problem";
let adapter = "port.read(value: port.Row) (answer: Text) { answer = label(value.person) }\nlabel(value: app.Person) (answer: Text) { answer = value.name }";
assert!(
packages(&[
("shop.account.adapter", adapter),
("shop.account.port", port),
("shop.account.app", app)
])
.is_ok()
);
for bad in [
format!("{adapter}\nunused(value: app.Unrelated) () {{}}"),
adapter.replace(
"answer = label(value.person)",
"hidden = app.Unrelated { number = 1 }\nanswer = label(value.person)",
),
format!("{adapter}\ntype Hidden {{ value: List<app.Unrelated> }}"),
] {
assert_eq!(
packages(&[
("shop.account.adapter", &bad),
("shop.account.port", port),
("shop.account.app", app)
])
.unwrap_err()
.code,
"C006"
);
}
let bad = format!("{port}\ntype Unused {{ value: app.Unrelated }}");
assert_eq!(
packages(&[
("shop.account.app", app),
("shop.account.port", &bad),
("shop.account.adapter", adapter)
])
.unwrap_err()
.code,
"C006"
);
}
#[test]
fn adapter_cannot_read_another_ports_private_dto() {
let error = packages(&[
("shop.account.app", APP),
("shop.account.port", PORT),
(
"shop.account.port.other",
"type Hidden { value: Int }\nwrite(value: Int) () fails app.Problem",
),
(
"shop.account.adapter.first",
&format!("{ADAPTER}\nunused(value: port.other.Hidden) () {{}}"),
),
(
"shop.account.adapter.second",
"port.other.write(value: Int) () {}",
),
])
.unwrap_err();
assert_eq!(error.code, "C006");
}
+125
View File
@@ -0,0 +1,125 @@
use super::signature;
use dever::compiler::{check, model::Program};
use dever::syntax::{diagnostic::Diagnostic, source::Source};
fn packages(files: &[(&str, &str)]) -> Result<Program, Diagnostic> {
let mut modules = Vec::new();
let mut identities = Vec::new();
for (index, (identity, text)) in files.iter().enumerate() {
let path = if *identity == "dever.io" {
"lib/dever/io.dever"
} else {
"case.dever"
};
modules.push(dever::syntax::parse(&Source::new(
index,
path,
(*text).into(),
))?);
identities.push((*identity).into());
}
check::check(modules, &identities)
}
const IO: &str = include_str!("../../../../lib/dever/io.dever");
const PORT: &str = "open(path: Text) (file: dever.system.File) fails dever.io.OpenResult";
const ADAPTER: &str = "port.open(path: Text) (file: dever.system.File) { file = open(path) }\nopen(path: Text) (file: dever.system.File) { file = dever.io.open(path) }";
#[test]
fn boundary_retains_real_effects_without_permitting_unmediated_io() {
let app = "open(path: Text) (file: dever.system.File) { file = port.open(path) }";
let program = packages(&[
("shop.files.app", app),
("shop.files.port", PORT),
("shop.files.adapter", ADAPTER),
("dever.io", IO),
])
.unwrap();
let effects = signature(&program, "open").effects;
assert!(effects.file && effects.blocking && !effects.unmediated_io);
for body in [
"file = dever.io.open(path)",
"old = port.open(path)\nfile = dever.io.open(path)",
"files = each(dever.io.open, [path])\nfile = port.open(path)",
"old = result(dever.io.open(path))\nfile = port.open(path)",
] {
let app = format!("open(path: Text) (file: dever.system.File) {{ {body} }}");
assert!(
packages(&[
("shop.files.app", &app),
("shop.files.port", PORT),
("shop.files.adapter", ADAPTER),
("dever.io", IO)
])
.is_err(),
"{body}"
);
}
let pure = app.replace(" {", " pure {");
assert_eq!(
packages(&[
("shop.files.app", &pure),
("shop.files.port", PORT),
("shop.files.adapter", ADAPTER),
("dever.io", IO)
])
.unwrap_err()
.code,
"C013"
);
let scheduled = app.replace("port.open(path)", "blocking(port.open(path))");
assert!(
packages(&[
("shop.files.app", &scheduled),
("shop.files.port", PORT),
("shop.files.adapter", ADAPTER),
("dever.io", IO)
])
.is_ok()
);
let asynchronous = app.replace(
"file = port.open(path)",
"other = blocking(port.open(path))\nfile = port.open(path)",
);
assert_eq!(
packages(&[
("shop.files.app", &asynchronous),
("shop.files.port", PORT),
("shop.files.adapter", ADAPTER),
("dever.io", IO)
])
.unwrap_err()
.code,
"C013"
);
}
#[test]
fn selection_cannot_hide_blocking_candidate_effects() {
let app = "open(path: Text) (file: dever.system.File) { file = port.open(path) }";
let worker =
"port.open(path: Text) (file: dever.system.File) { file = blocking(dever.io.open(path)) }";
assert_eq!(
packages(&[
("shop.files.app", app),
("shop.files.port", PORT),
("shop.files.adapter.inline", ADAPTER),
("shop.files.adapter.worker", worker),
("dever.io", IO),
])
.unwrap_err()
.code,
"C013"
);
let program = packages(&[
("shop.files.app", app),
("shop.files.port", PORT),
("shop.files.adapter.first", worker),
("shop.files.adapter.second", worker),
("dever.io", IO),
])
.unwrap();
let effects = signature(&program, "open").effects;
assert!(effects.file && effects.suspend && effects.configuration);
assert!(!effects.blocking && !effects.unmediated_io);
}
+216
View File
@@ -0,0 +1,216 @@
use super::package::packages;
use dever::compiler::model::{Node, Program, Signature, Statement};
mod boundary;
mod effects;
mod settings;
const APP: &str = "type Problem { error Missing(message: Text)\n error Denied(code: Int) }\nread(value: Int) (answer: Int) { answer = port.read(value) }";
const PORT: &str = "read(value: Int) (answer: Int) fails app.Problem";
const ADAPTER: &str = "port.read(value: Int) (answer: Int) { answer = helper(value) }\nhelper(value: Int) (answer: Int) { answer = value + 1 }";
fn signature<'program>(program: &'program Program, name: &str) -> &'program Signature {
&program
.functions
.iter()
.find(|function| function.signature.name == name)
.unwrap()
.signature
}
#[test]
fn port_alias_binds_directly_and_preserves_the_declared_error_set() {
let program = packages(&[
("shop.account.app", APP),
("shop.account.port", PORT),
("shop.account.adapter", ADAPTER),
("shop.account.api", "cmd read = app.read"),
])
.unwrap();
assert_eq!(program.functions.len(), 3);
let app = &program.functions[program.commands[0].function];
let Statement::Assign { value, .. } = &app.clauses[0].body[0] else {
panic!()
};
let Node::Call(index, _) = value.kind else {
panic!()
};
assert_eq!(program.functions[index].signature.name, "port.read");
assert_eq!(app.signature.failures.len(), 2);
assert_eq!(
app.signature.failures,
program.functions[index].signature.failures
);
assert!(!app.signature.effects.unmediated_io);
assert!(program.api_snapshot().contains("Problem.Denied"));
}
#[test]
fn topics_dtos_static_handlers_and_adapter_clauses_share_existing_type_checks() {
let program = packages(&[
(
"shop.account.app",
"type Problem { error Missing(message: Text) }\nread(value: Int) (answer: Int) { row = port.mail.Row { value = value }\nanswer = apply(port.mail.read, row) }\napply(action: handler(row: port.mail.Row) (answer: Int), row: port.mail.Row) (answer: Int) { answer = action(row) }",
),
(
"shop.account.port.mail",
"type Row { value: Int }\nread(row: Row) (answer: Int) fails app.Problem",
),
(
"shop.account.adapter.local",
"port.mail.read(row: port.mail.Row) (answer: Int) { answer = row.value }",
),
]);
// App 的公开 handler 参数不能泄漏 Port DTO;在领域内经普通 App 函数也不能绕过。
assert_eq!(program.unwrap_err().code, "C006");
let program = packages(&[
("shop.account.app", APP),
("shop.account.port", PORT),
("shop.account.adapter", "port.read(value: Int < 0) (answer: Int) { fail(app.Problem.Missing(\"negative\")) }\nport.read(value: other) (answer: Int) { answer = value }"),
]).unwrap();
assert_eq!(signature(&program, "read").failures.len(), 2);
assert!(packages(&[
("shop.account.app", "type Problem { error Missing(message: Text) }\nread(value: Int) (answer: Int) { row = port.mail.Row { value = value }\nanswer = port.mail.read(row) }"),
("shop.account.port.mail", "type Row { value: Int }\nread(row: Row) (answer: Int) fails app.Problem"),
("shop.account.adapter.local", "port.mail.read(row: port.mail.Row) (answer: Int) { answer = row.value }"),
]).is_ok());
assert!(
packages(&[
(
"shop.account.app",
&APP.replace("port.read(value)", "sum(each(port.read, [value]))")
),
("shop.account.port", PORT),
("shop.account.adapter", ADAPTER),
])
.is_ok()
);
}
#[test]
fn ports_reject_missing_mismatched_and_partial_implementations() {
for adapter in [
"port.read(wrong: Int) (answer: Int) { answer = wrong }",
"port.read(value: Text) (answer: Int) { answer = 1 }",
"port.read(value: Int) (wrong: Int) { wrong = value }",
"port.read(value: Int, action: handler() ()) (answer: Int) { answer = value }",
"port.unknown(value: Int) (answer: Int) { answer = value }",
"helper(value: Int) (answer: Int) { answer = value }",
] {
assert!(
packages(&[
("shop.account.app", APP),
("shop.account.port", PORT),
("shop.account.adapter", adapter)
])
.is_err(),
"{adapter}"
);
}
assert!(packages(&[("shop.account.app", APP), ("shop.account.port", PORT)]).is_err());
assert!(
packages(&[
("shop.account.app", APP),
(
"shop.account.port",
&format!("{PORT}\nwrite(value: Int) () fails app.Problem")
),
("shop.account.adapter.first", ADAPTER),
(
"shop.account.adapter.second",
"port.write(value: Int) () {}"
),
])
.unwrap_err()
.message
.contains("complete Port")
);
assert!(
packages(&[
("shop.account.app", APP),
("shop.account.port", PORT),
("shop.account.adapter.first", ADAPTER),
("shop.account.adapter.second", ADAPTER),
])
.is_ok()
);
}
#[test]
fn port_declarations_require_plain_types_and_public_explicit_errors() {
for port in [
"read(value: Int) (answer: Int) { answer = value }",
"read(value: Int < 0) (answer: Int) fails app.Problem",
"read(value: other) (answer: Int) fails app.Problem",
"read(value: handler() ()) (answer: Int) fails app.Problem",
"read(value: Int) (answer: Int > 0) fails app.Problem",
"read(value: Int) (answer: Int) pure fails app.Problem",
"read(value: Int) (answer: Int) recover(\"ignore\") fails app.Problem",
"read(value: Int) (answer: Int) fails Int",
"read(value: Int) (answer: Int) fails app.Problem?",
"read(value: Int) (answer: Int) fails app.Record",
"read(value: Int) (answer: Int) fails app.State",
"type Local { error Failed }\nread(value: Int) (answer: Int) fails Local",
"public read(value: Int) (answer: Int) fails app.Problem",
] {
let app = format!("{APP}\ntype Record {{ value: Int }}\ntype State {{ Ready }}");
assert!(
packages(&[
("shop.account.app", &app),
("shop.account.port", port),
("shop.account.adapter", ADAPTER)
])
.is_err(),
"{port}"
);
}
assert!(packages(&[("main", "read(value: Int) (answer: Int) fails Problem")]).is_err());
assert_eq!(
packages(&[
("shop.account.app", APP),
("shop.account.port", &format!("{PORT}\n{PORT}")),
("shop.account.adapter", ADAPTER)
])
.unwrap_err()
.code,
"C002"
);
}
#[test]
fn implementations_cannot_propagate_undeclared_errors() {
let adapter = "type Private { error Other }\nport.read(value: Int) (answer: Int) { fail(Private.Other()) }";
assert_eq!(
packages(&[
("shop.account.app", APP),
("shop.account.port", PORT),
("shop.account.adapter", adapter)
])
.unwrap_err()
.code,
"C012"
);
let app = "type Problem { error Missing(message: Text)\nerror Denied(code: Int) }\ntype ReadResult { Ready(value: Int)\nerror Failed(problem: Problem) }\nread(value: Int) (answer: ReadResult) { answer = result(port.read(value)) }";
assert!(
packages(&[
("shop.account.app", app),
("shop.account.port", PORT),
("shop.account.adapter", ADAPTER)
])
.is_ok()
);
let app = app.replace(
"answer = result(port.read(value))",
"lost = result(port.read(value))\nanswer = ReadResult.Ready(value)",
);
assert_eq!(
packages(&[
("shop.account.app", &app),
("shop.account.port", PORT),
("shop.account.adapter", ADAPTER)
])
.unwrap_err()
.code,
"C012"
);
}
+194
View File
@@ -0,0 +1,194 @@
use super::{APP, PORT, packages, signature};
#[test]
fn external_worker_secrets_belong_only_in_settings() {
let adapter = "setting { token: Secret }\nexternal exec \"worker\" {}";
for kind in [
"Text",
"Secret",
"Secret?",
"List<Secret>",
"app.Credentials",
] {
let app = format!(
"type Problem {{ error Failed(message: Text) }}\ntype Credentials {{ tokens: List<Secret?> }}\nread(value: {kind}) (answer: Int) {{ answer = port.read(value) }}"
);
let port = format!("read(value: {kind}) (answer: Int) fails app.Problem");
let result = packages(&[
("shop.account.app", &app),
("shop.account.port", &port),
("shop.account.adapter", adapter),
]);
if kind == "Text" {
assert!(result.is_ok(), "{result:?}");
} else {
let error = result.unwrap_err();
assert!(
error.message.contains("Secret belongs only in setting"),
"{kind}: {error:?}"
);
}
}
}
#[test]
fn settings_are_private_readonly_typed_values() {
let adapter = "setting { offset: Int\ntoken: Secret }\nport.read(value: Int) (answer: Int) { answer = helper(value) }\nhelper(value: Int) (answer: Int) pure { answer = value + setting.offset }";
let program = packages(&[
("shop.account.app", APP),
("shop.account.port", PORT),
("shop.account.adapter", adapter),
])
.unwrap();
assert!(signature(&program, "read").effects.configuration);
assert!(signature(&program, "helper").pure);
assert_eq!(signature(&program, "read").failures.len(), 2);
for body in [
"setting.offset = value\nanswer = value",
"setting = value\nanswer = value",
"answer = setting.missing",
"answer = setting.offset()",
"answer = setting.token",
] {
let bad = format!(
"setting {{ offset: Int\ntoken: Secret }}\nport.read(value: Int) (answer: Int) {{ {body} }}"
);
assert!(
packages(&[
("shop.account.app", APP),
("shop.account.port", PORT),
("shop.account.adapter", &bad)
])
.is_err(),
"{body}"
);
}
let empty =
"setting {}\nport.read(value: Int) (answer: Int) { setting = value\nanswer = value }";
assert!(
packages(&[
("shop.account.app", APP),
("shop.account.port", PORT),
("shop.account.adapter", empty)
])
.is_err()
);
}
#[test]
fn settings_reject_wrong_roles_invalid_wire_fields_and_shadowing() {
for owner in [
"main",
"shop.account.app",
"shop.account.port",
"shop.account.domain",
] {
assert!(
packages(&[(owner, "setting { offset: Int }")]).is_err(),
"{owner}"
);
}
for setting in [
"setting { Ready }",
"setting { private offset: Int }",
"setting { offset: Int > 0 }",
"setting { file: dever.system.File }",
"setting { values: Map<Text, Secret> }",
"setting { offset: Int }\nsetting { token: Secret }",
"setting { missing: Unknown }",
] {
let adapter =
format!("{setting}\nport.read(value: Int) (answer: Int) {{ answer = value }}");
assert!(
packages(&[
("shop.account.app", APP),
("shop.account.port", PORT),
("shop.account.adapter", &adapter)
])
.is_err(),
"{setting}"
);
}
for helper in [
"helper(setting: Int) () {}",
"helper() (setting: Int) { setting = 1 }",
] {
let adapter = format!(
"setting {{ offset: Int }}\nport.read(value: Int) (answer: Int) {{ answer = value }}\n{helper}"
);
assert!(
packages(&[
("shop.account.app", APP),
("shop.account.port", PORT),
("shop.account.adapter", &adapter)
])
.is_err()
);
}
}
#[test]
fn unused_adapter_candidates_still_enforce_failures_and_complete_contracts() {
let valid = "port.read(value: Int) (answer: Int) { answer = value }";
let invalid = "type Private { error Failed }\nport.read(value: Int) (answer: Int) { fail(Private.Failed()) }";
assert_eq!(
packages(&[
("shop.account.app", APP),
("shop.account.port", PORT),
("shop.account.adapter.local", valid),
("shop.account.adapter.remote", invalid)
])
.unwrap_err()
.code,
"C012"
);
let extra_port = format!("{PORT}\nwrite(value: Int) () fails app.Problem");
let complete = format!("{valid}\nport.write(value: Int) () {{}}");
assert!(
packages(&[
("shop.account.app", APP),
("shop.account.port", &extra_port),
("shop.account.adapter.local", &complete),
("shop.account.adapter.remote", valid)
])
.is_err()
);
}
#[test]
fn selection_keeps_all_candidate_effects_and_failure_identity() {
let quiet = "setting { offset: Int }\nport.read(value: Int) (answer: Int) { answer = value + setting.offset }";
let scheduled = "port.read(value: Int) (answer: Int) { answer = wait(run(helper(value))) }\nhelper(value: Int) (answer: Int) { answer = value }";
let program = packages(&[
("shop.account.app", APP),
("shop.account.port", PORT),
("shop.account.adapter.local", quiet),
("shop.account.adapter.remote", scheduled),
])
.unwrap();
let effects = signature(&program, "read").effects;
assert!(effects.configuration && effects.suspend && effects.schedule);
let pure = APP.replace("(answer: Int) {", "(answer: Int) pure {");
assert_eq!(
packages(&[
("shop.account.app", &pure),
("shop.account.port", PORT),
("shop.account.adapter.local", quiet),
("shop.account.adapter.remote", scheduled),
])
.unwrap_err()
.code,
"C013"
);
let error = "port.read(value: Int) (answer: Int) { fail(app.Problem.Missing(\"missing\")) }";
let program = packages(&[
("shop.account.app", APP),
("shop.account.port", PORT),
("shop.account.adapter.local", quiet),
("shop.account.adapter.remote", error),
])
.unwrap();
assert_eq!(signature(&program, "read").failures.len(), 2);
assert!(signature(&program, "read").effects.configuration);
assert!(!program.api_snapshot().contains("setting.offset"));
}