# Dever 语言开发指南 本文描述当前已经实现的 Dever 语言。它面向使用 Dever 编写程序的人,不涉及编译器内部实现。 编译、检查、静态特化和运行时边界的设计见 [Dever 语言实现原理](IMPLEMENTATION.md)。 ## 1. 快速开始 先按 [安装指引](skills/references/install.md) 安装机器版 Dever。AI 开发使用独立 [dever-language skill](skills/SKILL.md)。已有命令时直接生成项目;目标目录必须不存在,父目录必须已存在: ```bash dever new my_app # Markdown 写法:dever new my_app --markdown ``` 命令接收项目根。Dever 源码固定放在 `module/`,部署配置固定放在 `config/setting.json`,运行数据放在 `data/`。模板生成: ```text my_app/ config/ setting.json AGENTS.md README.md module/ hello/ greeting/ app.dever api.dever test/hello/greeting/greet.dever ``` `my_app/module/hello/greeting/app.dever`: ```dever greet(name: Text) (message: Text) { message = "Hello, " + name } ``` `my_app/module/hello/greeting/api.dever`: ```dever cmd greet = app.greet ``` `my_app/config/setting.json`: ```json { "log": { "level": "info" } } ``` 模板不需要数据库或 HTTP 配置。添加 HTTP 时再完整配置站点与身份,不能只添加 get 而遗漏 sites/auth。配置不会编译进二进制。`run` 使用项目目录中的配置;独立二进制启动时使用可执行文件同级的 `config/setting.json`,相对数据路径也以该目录为基准。 语言在开发和安装后的命令名均为 `dever`,Go 框架使用 `dever-go`。开发仓库生成 `target/debug/dever`;机器版启动器根据 `config/setting.json` 中可选的精确 `dever.version` 选择已安装版本,未锁定项目使用机器活动版本,再把项目命令和退出码原样分派。启动器的内部构建名为 `dever-launcher`,安装后仍叫 `dever`,版本核心为 `dever-core`。Linux 机器安装由多个项目共享。源码构建不会替换机器上已有的全局命令。 以下命令在已经提供 `dever` 的开发环境中执行: ```bash dever fmt my_app --check dever check my_app dever test my_app dever run my_app -- hello.greeting.greet '{"name":"Dever"}' dever build my_app --output my_app/app ``` Linux 开发环境运行构建产物: ```bash ./my_app/app hello.greeting.greet '{"name":"Dever"}' ``` - `new`:从随版本验证的模板原子创建新项目,不覆盖已有目录;`--markdown` 生成等价 Markdown 源码。 - 机器版 `update`:从官方仓库的 Releases 下载并校验,更新核心与配套 skill,不改业务项目文件。`skill path [project-root]` 返回对应版本的指引。在项目根目录运行 `skill install`,自动向 `AGENTS.md` 写入或更新 Dever 规则块,保留其他内容,无需指定技能目录。`new` 自动生成同一份规则。项目可用 `dever.version` 锁定版本;普通项目命令不会触发更新。 - `check`:加载生产源码,完成语法、名称、类型、分句及应用入口可达性和冗余硬检查;不执行应用或准备运行资源。 - `fmt --check`:只检查格式,不修改文件。 - `fmt`:统一格式化项目 `module/` 和存在的 `test/`;需要修复格式时单独执行 `dever fmt my_app`。 - `test`:检查生产源码与应用测试,再按名称串行运行每个隔离用例;没有 `test/` 时报告 0 个测试并成功。 - `run`:编译器从 API、CMD、Job 声明生成入口;无参数时按 `setting.json` 的 runtime mode 启动服务,`-- .. ''` 只执行指定命令。 - `build`:默认生成本机独立可执行程序;Linux x86_64 可加 `--target linux-aarch64`,使用预先准备的 ARM runtime 和依赖生成 ARM 程序。输出路径必须尚不存在,已有文件不会被覆盖。发布时把二进制放在项目根,使它能从同级 `config/setting.json` 加载部署配置。Windows 和 macOS 发行流程尚未交付。 `check`、`fmt`、`run` 和 `build` 默认只使用本地源码及随编译器提供的 package,不会隐式联网下载依赖。 ## 2. 源文件与领域职责 Dever 源文件使用 `.dever` 或 `.dever.md` 后缀和 UTF-8 编码。两种格式共用语言规则;Markdown 写法见下一小节。标识符使用 ASCII;注释和 Text 可以包含任意 UTF-8 字符。 ```dever # 单行注释 type Greeting { message: Text } greet(name: Text) (response: Greeting) { response = Greeting { message = "你好," + name } } ``` 规则如下: - `#` 开始单行注释,没有块注释。 - 不写分号,声明和语句由换行或结构边界分隔。 - 应用 `run/build` 不使用手写 `main.dever`;入口由 API、CMD 与 Job 声明生成。 - 业务源码固定为 `//.dever`,或在确实需要按主题拆分时使用 `///.dever`。 - role 只能是 `app`、`domain`、`model`、`port`、`adapter`、`api`、`job`。应用源码不写 `package`、`exposes`、`public` 或 `internal`。 - `app` 中的全部函数自动成为领域能力,调用名固定为 `..`;同组件可省略 component,同领域内部可写 `app.`。`app/.dever` 的 topic 不进入调用名。 - App 能力的输入、输出和向外传播的错误都属于公开契约,不能引用 Domain、Adapter 等私有类型。业务错误类型应声明在 App,Domain 可通过 `app.ErrorType.Variant` 使用它。 - `domain`、`model`、`port`、`adapter`、`api` 和 `job` 都是领域私有实现。跨领域、跨组件只能调用目标 App;API 和 Job 入口不是普通源码调用目标。 - App 可调用本领域 Domain、Port 及其他领域 App;API 只能调用同领域 App。Domain 只能调用同领域 Domain。Adapter 只能调用同文件 helper 与标准能力,不能反调 App、Domain、Model、Port 或其他 Adapter。Model 操作仍只能由所属领域 App 使用静态 DSL 执行。 - App、Domain、Port、Adapter、Job 的单文件与同名目录二选一,目录至少包含两个内聚主题文件。Model、API 可以同时使用角色主文件和主题目录,不受最少文件数限制。非 API 的主题目录保持一层,只有 `api/` 可递归。 - 一个文件可放一组内聚类型和函数,不按函数机械拆文件。禁止用 `common`、`shared`、`utils`、`helper`、`base`、`service` 作为业务源码桶。 - `port` 只声明类型和无函数体的操作合同,必须写 `fails app.ErrorChoice`;错误 choice 至少包含一个显式 error 分支,也可引用公开标准错误 choice。输入必须是无约束的明确值类型,输出同样不加范围约束。当前 Port 不接受 handler 参数。App/Domain 的外部文件、网络 I/O 必须经过 Port;日志、时间、随机数等已有能力保持原约束。 - 没有 `import`、`use`、通配导入或别名。 - 不同领域之间不能循环依赖;同领域职责文件共享一个依赖边界,函数递归仍被拒绝。 Port 示例:App 声明 `type DeliveryError { error Unavailable(message: Text) }`;同领域 `port.dever` 声明 `send(message: Text) () fails app.DeliveryError`;`adapter.dever` 实现 `port.send(message: Text) () { ... }`。Port topic 使用 `port.mail.send`。qualified 实现不能被普通函数调用。Port/Adapter 只能引用其合同实际使用的 App 类型,不能调用 App。Port 自有 DTO 保持私有,不能泄漏到 App 公开签名。 一个 Adapter 文件完整实现一个 Port;单实现自动绑定,多实现由 `config/setting.json` 的 `adapter..use` 选择,根 adapter 名为 `default`,topic 名为实现名。编译产物包含全部候选,运行时选择不改变 failure 全集、effect 或 suspension 合同。每个实现的实际 failure 必须是声明全集的子集。 Adapter 可声明 `setting { endpoint: Text token: Secret }`,通过只读 `setting.endpoint` 访问。配置形如 `{"adapter":{"notification.mail":{"use":"smtp","setting":{"endpoint":"...","token":"..."}}}}`。仅选中实现要求 setting;未知 Port/实现/字段、重复键、缺失或错误类型在入口前失败。无 setting 的单实现允许省略配置文件。Secret 不可观察,配置错误不回显值;不使用环境变量。 外部程序同样只能实现 Port。需要调用现成 Linux 二进制时用 `external command`;需要类型化多操作 Worker 协议时用 `external exec` 或生态 Adapter。 普通二进制不必实现 Dever 协议,也不必另写 Python/Go 包装。例如将程序放在 `module/media/tool/bin/tool`: ```dever # module/media/tool/port.dever execute(args: List, stdin: Bytes?) (output: dever.process.Output) fails dever.process.Error ``` ```dever # module/media/tool/adapter.dever external command "bin/tool" {} ``` ```dever # module/media/tool/app.dever version() (text: Text) { output = port.execute(["--version"], null) text = dever.bytes.to_text(output.stdout) } ``` ```dever # module/media/tool/api.dever cmd version = app.version ``` 执行 `dever run . -- media.tool.version '{}'`,或先 `dever build . --output program` 再执行 `./program media.tool.version '{}'`。 command Port 只有一个操作,操作名由业务决定;输入固定为 `args: List`,可选第二项 `stdin: Bytes?`,输出固定为 `output: dever.process.Output`,错误为 `dever.process.Error`。只需 argv 时可省略整项 stdin 参数。Output 有 `code: Int`、`stdout: Bytes`、`stderr: Bytes`;二进制输出不做隐式文本转换,正常非零退出码也是结果。每次调用启动一次进程,不重试,不使用 shell;空格、引号、`$()` 等按参数原样传递。未提供 stdin 时立即发送 EOF。启动失败、超时和输出超限产生源位置 runtime fault。 调用限额只从 `config/setting.json` 读取:`{"adapter":{"media.tool":{"command":{"timeout_ms":30000,"output_limit":1048576}}}}`。示例值就是默认值;timeout_ms 最大 3600000,output_limit 为 stdout/stderr 合计字节数、最大 8388608,均必须为正数。argv 含终止符总计最多 65536 字节且不含 NUL,stdin 最多 8388608 字节。超时、取消和退出会回收本次受管进程组;不保证清理主动脱离进程组的守护程序。需要文件、网络或子进程时仍在声明中写 `allow file/network/process`,实际权限继承所在容器;command 不接受 `setting` 或 `lib` 声明。 `run/build` 离线嵌入 ELF 和相邻 `bin/lib/` 下显式随附的普通动态库文件;静态程序可直接使用,动态程序使用签名发行包的 GNU loader 和 OS 库。不扫描宿主库,不接受符号链接或 lib 子目录;缺库、错架构、SONAME/版本不匹配、越界搜索路径在构建期失败。Linux x86_64 与 ARM64 必须提供各自架构的二进制和库,Dever 不转换第三方二进制。单独使用 command 不需要 `dever lib` 或 `dever.lock`;资源可随 Dever Package 分发,Package 自身仍须锁定。测试继续用 Port fake,不执行真实命令。 在 Adapter 文件中写一个 `external exec` 声明,编译器会用对应 Port 的全部操作、输入、输出、业务错误和可选 `setting` 生成唯一协议合同,不需要再逐个重复函数: ```dever setting { endpoint: Text token: Secret } external exec "worker/mail" { allow network } ``` entry 是所属领域目录内的可移植相对路径;上例构建时收集 `module/notification/mail/worker/mail`,将全部候选 exec Worker 的字节、SHA-256 和可执行位嵌入程序。入口原子提取到私有 `data/cache/lib/`,每次启动 Worker 重新验证精确资源清单、摘要和 Unix 所有权/权限;不扫描其他 bundle,不依赖部署目录保留源码 Worker。路径不接受绝对形式、`.`、`..`、反斜杠或盘符,也不通过 shell、`PATH`、环境变量或外部命令字符串查找。capability 名称为 `network`、`file`、`process`、`gpu`;重复值和未知值在检查期拒绝。Worker 握手必须精确回显已编译的 Port、Adapter、schema、操作和 capability 集合。Linux Worker 直接执行,不创建内层 OS 沙箱。`allow` 是编译期效果及协议声明,不限制操作系统访问;实际文件、网络、GPU 权限由所在容器的用户、挂载、网络和设备策略决定。旧 Adapter `files` / `gpu` 授权字段已删除,真实路径通过业务 setting 传入。GPU 继承父容器设备和必要驱动环境,不重挂载设备,不修改主机安全策略。仅执行可信代码;配置与受管 C ABI 用法见 `sdk/README.md`。 `external` 只在 Adapter role 中有意义,且一个文件只能包含该声明和至多一个 `setting`。它完整实现且只能实现同领域一个 Port identity;不能同时写 Dever Adapter 函数或 helper。应用测试仍必须提供 case-local Dever Port fake,不启动生产 Worker,也不读取生产 Adapter setting。reference 后端明确不执行外部程序,正式执行只走 native 后端。 Worker 操作不设隐式执行期限,不会因执行超过 30 秒而自动失败;等待结果直到调用方主动取消、Worker 异常或应用停止。启动握手、协议写入和退出清理仍有独立短限时。`long` 只控制入口事务边界,不改变这些生命周期规则;业务确需期限时使用现有 Task 超时能力,不增加另一套 Worker 超时配置。`external command` 的显式命令限额不属于 Worker 协议,本次不改变其合同。 生态 Adapter 复用同一 Port 合同,声明中的 Lib 不带重复的生态前缀: ```dever external pip "worker/mail.py" { lib "httpx@0.28.1" allow network } ``` `pip/npm/go` 与 `exec` 使用相同的生命周期和 typed wire schema;各 Worker 的依赖与 runtime 独立锁在 `dever.lock`。Lib 解析是显式 `dever lib` 操作;`run/build` 只读取已锁定的资产。Python/JavaScript 打包器生成 SDK、合同和 runner,自动绑定入口中与 Port 操作同名的函数,不要求业务再写 handlers 注册表。运行树只从锁定 pack、依赖和当前 Adapter 源码生成;Python 固定使用 `-I -S -B`,Node 支持 ESM 和 CommonJS。启动解释器、argv 和工作目录必须匹配编译嵌入的资源清单,既不寻找系统语言环境,也不从部署配置接受任意命令。 Go Worker 使用锁定的目标专用 build pack 内的 compiler、linker 和标准库离线编译。入口源码声明 `package main`,按操作名导出 `func(context.Context, any, any) (any, error)`:`send` 对应 `Send`,`text.render` 对应 `TextRender`;生成入口静态绑定函数并由 Go 编译器检查签名,不要求业务手写 `main` 或注册表。构建按目标 build tags 选文件,支持 `go:embed`,导入只能来自标准库、当前 Adapter 源码、SDK 或锁定的 Go module 闭包。构建产物只携带 Worker 可执行文件与 checked 合同,启动不寻找系统 Go。参与解析的 `go.mod` 和最终 module ZIP 均验证官方 sumdb 签名、包含证明与 Go `h1`;项目现有锁保存 checkpoint,一致性和反回滚以该锁为锚,主动删除锁会重新建立锚。 `dever-lock-v6` 保存每个 Worker 的精确生态依赖,以及原始归档的准确来源和文件名。Python extras 使用规范化、排序后的 `pip:name[a,b]@version`,不同 Worker 不共享隐式 extras;实际选中 wheel 的 METADATA 决定依赖,原生 wheel 还必须符合签名解释器的 tags、扩展 ABI 和封闭动态库搜索规则。npm 保留每个安装实例的嵌套路径、peer 上下文、optional 省略原因和 bundled 归属,不能把不同依赖上下文压成一份全局版本表。 `dever lib install ` 按已有锁恢复 Package、Lib 和必要运行环境,不重新选择版本,不改项目配置和锁文件。缺少构建产物时使用收据中的完整固定输入重放,并核对输出摘要;第三方构建不能重现相同字节时明确失败。运行环境和构建工具通过机器共享签名扩展按需准备,`run/build` 仍不联网。ARM 应用资源先用 `dever target add linux-aarch64` 准备。 显式 `dever lib add/update` 可用签名 build pack 构建 PEP 517 sdist、执行 npm registry 包的 `preinstall/install/postinstall` 和原生 addon 编译。build pack 必须提供目标运行时、头文件、编译器、sysroot、固定 shell 和工具闭包;缺少的工具或库明确报错,不寻找宿主环境。Python 的静态与动态 build requirements 使用独立精确锁,console scripts 安装到隔离构建 prefix;metadata 与 wheel hook 分进程执行,最终 wheel 必须匹配父进程保存的 metadata。npm publishing hooks 不作为 registry 安装钩子执行,可选依赖构建失败会裁剪其不可用闭包,必需依赖失败则整个准备失败。 源码构建收据绑定源码摘要、运行时、工具包、固定 frontend、构建依赖与输出摘要;npm 还绑定完整安装图。`run/build` 离线消费已锁定输出,不再次执行上述安装钩子或源码构建。Node addon 在仅有目标 Node 和运行库的独立进程中逐个验证;所有 `.node` 文件必须有成功或明确拒绝记录,运行树只安装成功项,其他平台/ABI 的变体保留在原锁定归档中。本次源码生成的 addon 验证失败会使准备失败,不能改用纯 JavaScript 回退并声称原生构建成功。Linux 构建使用明确的签名工具与输入、独立临时工作区;构建钩子直接继承容器权限,不再宣称文件/网络隔离。构建期限为五分钟,stdout/stderr 各限 16 MiB;工作区每 250 ms 检查 512 MiB/65,536 项预算并在超限时回收子进程,这不是内核磁盘 quota,检查间隔内可暂时超限。最终安装树仍限 256 MiB/65,536 项。普通 Worker 由同一进程监管执行;其他平台不能借宿主工具绕过缺失的发行支持。 Dever Package 的请求放在 `config/setting.json.package`:`registry` 是显式 HTTPS registry origin,`use` 是 `name@version-range` 数组。`dever package add/update` 解析版本和传递依赖;Package 与 Lib 共用 `dever.lock` 和机器摘要缓存。Package 的源码归属于 `module//`,Worker 资源归属于同组件路径或 `worker//`;其他组件路径不能混入归档,也不能与项目本地组件重名。`check/test/run/build` 自动加载锁定源码,无须增加 import 或注册方法;缺失的锁定 Worker 必须失败,不能使用项目同名文件替代。`remove` 离线裁剪已验证锁,不重新选择剩余依赖版本。 `test///.dever` 可在本文件写 `port.send(...) (...) { ... }` fake;每个可达 Port 必须完整提供 fake,不读取生产 Adapter 配置或回退生产实现。同一套件只编译一次,每个 case 在独立进程绑定自己的 fake。编译检查与代码生成也按 case 独立推断 effect、failure 和挂起行为,不合并不同测试的 fake;生产合同独立检查。 持久任务使用同领域 `job.dever`,复杂时按主题拆成平铺 `job/*.dever`。Job 只适配同领域 App,不直接调用 Model、Domain、Port、Adapter 或并发原语: ```dever database default job publish(input: app.Publish) () retry(5) timeout(30000) { app.apply(input) } job cleanup() () retry(3) timeout(1000) { app.clean() } schedule cleanup = "0 * * * *" ``` Job 必须声明数据库、`retry` 和 `timeout`。输入为零个或一个同领域 App record,输出为零;payload 使用共享 wire codec,允许保留 Model 身份的 ModelId,不允许 Secret、private 字段、资源或 Choice。`retry(1..100)` 包含首次执行,`timeout(1..3600000)` 单位为毫秒。入口不能作为普通函数或 handler 传递。 App 使用 `dever.job.enqueue(job.publish, input, key)`;无 payload 时省略 input。`enqueue_at` 在 key 后增加 DateTime。返回 Id;key 为 1..1024 字节的业务幂等键。同物理租户、执行租户、Job identity 与 key 的 pending/running 任务返回同一 Id,终结后允许重新使用 key。transaction App 内业务写入与入队共享物理事务;跨连接事务被拒绝。入队不会同步调用 handler 或继承其 effect/failure。 入队必须处于编译器建立的可信执行作用域。受保护 HTTP 入口在自动权限校验成功后保存 User 身份的 Provider、站点、subject、session、租户声明和该入口唯一的 permission key;不保存 JWT、Cookie、角色或权限集合。Worker 每次执行都重新调用配置绑定的 verify,确认该 key 仍在当前编译产物的权限目录,并查询当前角色;session、租户、组件或权限已失效时任务进入 blocked,业务 handler 不执行。public HTTP 调用链不能入队;CMD、schedule 和测试入口使用明确的 System 身份。Job payload 内的用户或租户字段始终只是业务数据,不能成为可信执行身份。 `schedule` 仅绑定本文件零输入 Job。cron 固定为 UTC 五字段(分、时、日、月、星期),支持数字、`*`、列表、范围、步长,星期为 0..6;日和星期均受限时按 OR 匹配。首次启用从当前分钟建立 cursor,不回放历史;后续每轮最多扫描并推进 256 个分钟槽,仅匹配 cron 的槽生成任务。cursor、occurrence 和推进同事务,多 scheduler 与已完成任务都不会重建同一窗口。修改已持久化的 cron fingerprint 会失败,需要明确迁移。 应用入口由编译器根据 Job 与 API 声明生成;两者同时存在时,共用运行时并由唯一配置文件 `config/setting.json` 选择启动模式: ```json { "runtime": { "mode": "all", "shutdown_ms": 30000 }, "job": { "workers": 2, "poll_ms": 250, "lease_ms": 60000, "retry_base_ms": 1000, "retry_max_ms": 300000 } } ``` 这里还需配置实际 `database`,API 模式需配置 `http`。`mode` 为 `api`、`worker` 或 `all`;省略 runtime 时编译产物按实际 API/Job 能力推导 api、worker 或 all,关闭期限默认 30000 ms。显式 mode 仍必须与源码能力和所需配置一致。worker 连接池至少比 worker 数量多一个连接,留给 claim/heartbeat。SQLite 的写事务会阻塞续租,因此还要求 `lease_ms` 大于每个 Job 的 `timeout`;PostgreSQL 支持较短租约并续租。未知字段、无效范围、所选 mode 缺少源码能力或设置均在监听/claim 前失败,不支持环境变量或命令行覆盖。 任务是至少一次执行;外部副作用仍需业务幂等。每次 claim 增加 attempt 并生成独立 token,续租和结果提交要求 token 与有效 lease 匹配。崩溃、取消后的任务保留 lease,到期才能重领,达到次数上限进入 dead。失败按有界指数退避;未知 target、schema 不兼容、坏 payload、无可信执行身份、身份/能力复核失败,或不满足当前租约约束的历史任务策略进入 blocked,错误摘要不保存业务明文。私有队列 schema 升级时,无法证明执行身份或能力的历史活跃 User 任务也会阻断,不会降级为 System。SIGTERM/Ctrl-C 停止接收新请求和 claim,排空到共同期限后取消在途工作,汇合后关闭数据库并 flush 日志。 应用测试可用 `dever.test.advance_clock(ms)` 推进 case 独立时钟,`dever.test.drain_jobs(limit)` 执行到期任务并返回处理数(limit 为 1..10000)。测试复用生产队列状态机、case 的 SQLite 与 Port fake,不启动服务或读取部署配置;没有可达 Job 时 drain 返回 0,也不因此要求数据库。 `job.lease_ms` 范围为 100..7200000 毫秒,Job timeout 上限仍为 3600000 毫秒。测试使用最大租约,保证所有合法 timeout 都能在隔离 SQLite 中运行。 路径示例: ```text my_app/module/user/account/api.dever my_app/module/user/account/app.dever my_app/module/user/account/domain.dever my_app/module/user/account/model.dever my_app/module/news/article/app/publish.dever my_app/module/news/article/app/query.dever ``` 最后两项表示文章 App 已有至少两个内聚主题;对外仍调用 `news.article.publish(...)` 和 `news.article.count()`。只有一个主题文件时应改回 `news/article/app.dever`。 命名规则: | 对象 | 格式 | 示例 | | --- | --- | --- | | component、domain、role、topic、function、字段和局部值 | `lower_snake_case` | `user.account`、`can_sign_in` | | type、选择项 | `UpperCamelCase` | `User`、`Created` | | 缩写 | 按普通单词处理 | `Id`、`HttpClient`、`user_id` | ### 2.1 Markdown 程序源码 使用 `.dever.md` 可以把程序和面向人的用途说明放在一起。Dever 代码仍写在顶层 `dever` 或 `typescript dever` 围栏中;未安装 Dever 编辑器插件时,`typescript dever` 可让编辑器尝试使用 TypeScript 高亮,第二个标记用于确认其中仍是 Dever 源码。普通 `.md` 文档不参与编译。 Markdown 结构也是编译契约:一个一级标题说明包,一个二级标题说明一个 type 或一个逻辑函数。标题使用自然语言,技术名称、公开清单、输入输出和类型结构使用固定列表,并由编译器与代码核对。 例如 `user/account/api.dever.md`: ````markdown # 问候接口 向调用方返回一条问候语。 - 包:`user.account.api` - 公开类型:无 - 公开方法:无 - 使用:无 ## 获取问候 绑定本领域的问候能力。 - 声明:`get greeting` ```typescript dever get greeting = app.greet ``` ```` 规则如下: - 文件必须有且只有一个一级标题。一级标题下先写非空包说明,再依次写 `包`、`公开类型`、`公开方法`、`使用`;一级标题下不能有 Dever 代码块。 - 每个 type、逻辑函数或 API 声明必须分别使用一个二级标题、非空用途说明和一个 Dever 代码块。一个逻辑函数包括同一名称、同一参数数量的全部条件分句;不同声明不能共用说明,分句也不能拆到多个说明中。 - type 说明使用 `类型` 以及 `字段` 或 `分支`。函数说明使用 `函数`、`输入`、`输出`;API 说明使用 `声明`。空清单显式写 `无`;非空输入、输出、字段和分支均须带自然语言说明。 - 源码身份必须与路径一致。App 的公开名称和调用方式由 role 自动得到;Model 的记录、choice 和 ID 类型由 Model role 自动提供;API 声明不列入公开方法。字段、分支、函数输入和输出的名称、类型、数量及顺序必须与代码一致;同一函数各分句的输入参数名必须相同。函数模式按解析后的统一类型记录,例如 `true`/`false` 分句写 `Bool`。 - 路径规则与普通源码一致;`user/account/app.dever.md` 的源码身份是 `user.account.app`,其中 `register` 的调用方式是 `user.account.register(...)`。 - 两种源码格式可以在一个项目中混用;Model/API 主文件可与 topic 目录并存,其他 role 二选一。 - 程序围栏使用至少三个反引号或波浪号,允许 0–3 个空格缩进,信息字符串须是字面的 `dever` 或 `typescript dever`;关闭围栏使用相同字符且长度不短于开始围栏,尾部只允许空格,不允许 Tab。程序围栏必须闭合,不能依靠文件结束隐式关闭。 - 三级及更深标题、表格、普通补充说明、其它语言及无语言标记的代码块不执行。引用、列表、HTML 或展示代码块内的围栏不作为程序。补充内容放在所属 Dever 代码块之后。 - 函数输入输出、条件分句、类型、失败义务、`pure` 和依赖边界都按现有 Dever 规则检查,说明文字不能代替正式契约。 - `check/api/run/build` 命令不变,并强制检查 Markdown 契约;错误显示 `.dever.md` 原文中的行列并关联对应代码。`fmt` 检查结构并仅更新程序块内部,保留说明、围栏和展示代码块。 固定列表的完整格式见 [Dever Markdown 源码规范](MARKDOWN-SYNTAX.md),完整接口示例见 [Markdown CMS](examples/cms/md/module/news/article/api/admin/manage.dever.md)。安装 [VS Code 语法插件](editor/vscode/README.md) 后,两种围栏都使用与 `.dever` 相同的 Dever 高亮规则,`.dever.md` 保持 Markdown 语言模式。插件不提供补全、跳转或实时编译诊断;契约与语言合法性仍由编译器检查。 ### 2.2 应用测试 应用测试位于项目根的 `test///.dever` 或 `.dever.md`。文件名 `` 选择同名的普通 `topic() ()` 作为唯一入口;入口不能有参数或输出,也不能声明为 transaction。文件中的其他函数和类型只供本文件使用,不会自动执行或向生产源码、其他测试公开。路径只允许 component、domain、topic 三层,不建立 `common`、`shared` 或全局 fixture 目录。 测试是所属领域的受控友元:可调用同领域 App 和 Domain,跨领域只能调用目标 App。测试可以读取 App 返回的 Model 值,但不能直接执行 Model CRUD,也不能调用 Port、Adapter、API 或其他测试。生产源码同样不能反向依赖测试。测试不能启动 `dever.api.serve()`。 首版断言只有: ```dever register() () { account = user.account.register("New@Example.com", "New", "hash") assert(account.email == "new@example.com") assert_eq(account.display_name, "New") } ``` `assert` 只接受 Bool;`assert_eq` 要求两侧类型完全相同且可比较,不做隐式数值转换。失败报告测试身份、原始源码位置和实际/期望值。业务失败继续用 `result(call)` 与 choice 分句验证,不增加异常断言语法。 `dever test ` 在执行任何用例前检查全部生产和测试源码,并把所有测试入口编译成一个可缓存的原生测试程序;每个测试仍在独立原生进程与临时项目中运行。不使用 Model 的测试不初始化数据库;使用 Model 的测试走正式迁移、Seed、事务和关闭流程,但数据库配置由运行器在临时目录生成且只使用 SQLite。CLI 仍读取 `config/setting.json` 与锁中的依赖元数据,测试执行不会连接其中的部署数据库或使用项目 `data/`,也不从环境变量接收数据库覆盖;成功和失败都会清理临时文件。 定向运行使用 `--filter `,例如 `--filter user.account` 或 `--filter user.account.register`。它按完整身份或点分段前缀选择用例,不使用子串或通配符;无匹配时明确失败。筛选不跳过其他源码的检查,不改变原生 suite 的索引;未选择用例计入 `not run`。报告仍显示原有的 `component/domain/topic` 路径身份。 每例默认最多运行 60 秒,`--timeout-ms <正整数毫秒>` 可覆盖;限时不包含测试程序的编译。超时用例失败,运行器终止并回收本例拥有的进程组和后代,然后继续后续选中用例。任一用例失败或超时,命令返回非零。测试仍按顺序执行,不提供共享 setup、并行或重试。 ## 3. 类型与值 ### 3.1 基础类型 | 类型 | 含义 | | --- | --- | | `Bool` | `true` 或 `false` | | `Int` | 有符号 64 位整数,溢出不会环绕 | | `Decimal` | decimal128,最多 34 位有效数字,银行家舍入 | | `Float` | IEEE 754 binary64 近似数 | | `Text` | UTF-8 文本,不做隐式 Unicode 规范化 | | `Id` | 不透明稳定标识 | | `Bytes` | 二进制字节序列 | | `Secret` | 不透明敏感值;仅受控密码学及输入边界使用 | literal 规则: ```dever enabled = true count = 42 price = 19.90 ratio = 1.25e6 name = "中文和 Unicode 都可以" missing = null ``` - 整数 literal 默认是 `Int`。 - 带小数点的 literal 默认是 `Decimal`;明确的 Float 上下文可将其作为 `Float`。 - 科学计数 literal 是 `Float`。 - Text 支持 `\\`、`\"`、`\n`、`\r`、`\t` 和 `\u{HEX}`。 - `Decimal` 与 `Float` 不隐式混合;Int 转 Float 使用 `float.from_int`。 ### 3.2 字段型 type(record) 字段型 type 是固定结构的数据: ```dever type User { id: Id name: Text phone: Text? } create_user(id: Id, name: Text) (user: User) { user = User { id = id name = name phone = null } } rename(user: User, name: Text) (result: User) { result = user result.name = name } ``` 构造时必须一次写全全部字段。未知、重复或遗漏字段都是编译错误。字段只能在完整值上读取或修改,使用 `value.field = expression` 语法;不能靠逐字段赋值创建一个半初始化值。 ### 3.3 选择型 type(choice) 选择型 type 表示一个值当前只能处于其中一种状态: ```dever type CreateResult { Created(user: User) InvalidName(reason: Text) EmailAlreadyUsed(email: Text) } result = CreateResult.InvalidName("name is required") ``` 选择项必须通过所属 type 限定。选择项可以不携带数据,也可以声明一个或多个有名称、有类型的携带值。携带值的名称属于 type 契约;构造和匹配仍按声明位置书写,不是命名参数调用。 ### 3.4 可空、List 与 Map `Value?` 表示 `Value` 或 `null`。没有 `Value??`、隐式默认值、`?.` 或强制解包。 ```dever names = ["Ada", "Lin"] labels = { "env" = "production" "region" = "china" } ``` - List 的元素必须具有相同静态类型。 - Map 的类型是 `Map`,保留插入顺序。 - Map key 可以是 Text、Int、Bool、Id 或不携带数据的选择型 type。 - Decimal、Float、可空值、字段型值、List 和 Map 不能作为 Map key。 - 空 `[]` 和 `{}` 必须能从参数或输出等上下文推断类型。 - `get`、`first` 和 `find` 返回可空值;可空化是幂等的。 - `entries(map)` 返回保持插入顺序的 `List>`;每个 `MapEntry` 有 `key` 和 `value` 两个字段。修改局部 MapEntry 仍遵守值语义,不会回写原 Map。 ### 3.5 Stream 与资源 `Stream` 是按需拉取、顺序、一次消费的流。文件、Socket、Listener 和 Stream 是资源值;复制它们会建立指向同一资源状态的别名。Stream 的所有别名共享一个读取位置,任何别名消费或关闭后,其他别名都会看到相同状态。 `AsyncStream` 是异步拉取的资源流,等待下一项时挂起当前协程。它的别名共享读取位置和关闭状态;元素可跨线程传递时,流也可跨线程传递。它不与同步 Stream 隐式转换,不预取或自动收集为 List。 普通字段型值、选择型值、List 和 Map 使用值语义:修改一个局部名称不会改变调用方或其他名称持有的值。 ## 4. function 与命名输出 Dever 的所有行为都由 function 定义: ```dever line_total(price: Decimal, quantity: Int) (total: Decimal) { total = price * quantity } ``` - 第一组括号是输入,第二组括号是命名输出。 - 没有输入或输出时仍写 `()`。 - 不使用 `function` 或 `return` 关键字。 - 调用按位置传参,不支持命名参数调用。 - 零输出调用是一条动作语句。 - 单输出调用直接得到值。 - 多输出调用得到只能按输出名称访问的结果。 - 用户 function 不能直接或间接递归。 - function 签名由 package、名称和输入数量确定;同名但输入数量不同的 function 是不同签名。 多输出示例: ```dever name_parts(value: Text) (original: Text, normalized: Text) { original = text.trim(value) normalized = text.lower(original) } display(value: Text) (result: Text) { parts = name_parts(value) result = parts.original + ":" + parts.normalized } ``` 普通应用没有手写 `main()`;命名输出保留在 App 函数中,API/CMD 绑定一个可序列化输出并封装为 JSON。 ## 5. 分句是唯一条件机制 Dever 没有 `if`、`else`、`switch`、`match` 或条件表达式。所有业务分支都使用同名 function 分句: ```dever type OrderStatus { Draft Paid Rejected(reason: Text) } status_text(status: OrderStatus.Paid) (result: Text) { result = "paid" } status_text(status: OrderStatus.Rejected(reason)) (result: Text) { result = reason } status_text(status: other) (result: Text) { result = "pending" } ``` 可空值也通过分句处理: ```dever phone_text(phone: Text) (result: Text) { result = phone } phone_text(phone: null) (result: Text) { result = "not provided" } ``` 数值范围可以直接写在输入模式中: ```dever grade(score: Int < 60) (result: Text) { result = "fail" } grade(score: Int >= 60 and < 90) (result: Text) { result = "pass" } grade(score: Int >= 90) (result: Text) { result = "excellent" } ``` 同一签名的全部分句必须紧邻书写。中间插入 type 声明、其他 function 或另一输入数量的同名 function 后,再次书写原签名会直接报错,不会形成第二组规则。 分句输入支持:完整类型、选择项、`null`、Bool/Text/数值常量、Int/Decimal 范围和 `other`。选择项必须为每个携带值提供一个位置绑定;不需要使用的绑定可以写 `_`。编译器把一组分句作为规则检查: - 输入和输出契约一致; - 分句互不重叠; - 所有可能值都被覆盖; - 没有不可达分句。 分句没有源码顺序优先级。多输入分句按所有输入组合检查覆盖和重叠;`other` 是该输入位置在整组明确模式之外的补集,只能用于分句输入。 ## 6. 线性 body 与运算 function body 只包含顺序赋值和 function 调用: ```dever final_price(price: Decimal, rate: Decimal) (result: Decimal) { discounted = price * (1 - rate) result = decimal.round(discounted, 2) } ``` `=` 用于初始化,也用于修改当前 function 内的 input、局部值或 output。编译器保证读取前已初始化、字段宿主完整、局部类型稳定,并要求退出前给所有输出赋值。 运算规则: | 运算 | 适用范围 | | --- | --- | | `+ - *` | 同类 Int、Decimal、Float;Int 可无损提升为 Decimal | | `/` | Int/Int 与 Decimal 运算返回 Decimal;Float/Float 返回 Float | | `//`、`%` | 仅 Int,整数除法向零截断 | | `+` | 也用于两个 Text 的连接 | | `== !=` | 相同的可比较类型 | | `< <= > >=` | Int、Decimal、Float | | `and or not` | Bool,`and` 和 `or` 短路 | 相等比较也适用于成员本身可比较的字段型、选择型、可空值、List、Map 和 MapEntry。List 按位置比较;Map 按插入顺序比较,所以键值相同但插入顺序不同的两个 Map 不相等。Float 遵守 IEEE 754 规则,`NaN == NaN` 为 `false`,包括它嵌套在集合或字段中时。 优先级从高到低为:调用和字段访问、一元 `-`/`not`、`* / // %`、`+ -`、比较、`and`、`or`。同级二元运算从左到右,括号可以改变顺序。 Int 溢出、Decimal 除零等静态可知故障在检查阶段报错;运行时才知道的数值故障会终止当前执行并报告 `.dever` 文件、行和列。 ## 7. 集合与受控重复 Dever 没有 `for`、`while` 或用户递归。重复处理使用具名 handler 和标准组合器: ```dever views = each(to_view, users) active = filter(is_active, users) user = find(has_id, users, expected_id) total = sum(prices) total = sum(line_total, lines, tax_rate) state = reduce(update_state, values, initial_state) state = reduce_until(update_until_done, stream, initial_state) parallel_each(send_one, jobs, 4, context) ``` | 操作 | 可处理序列 | handler / 结果 | | --- | --- | --- | | `each` | List、Bytes;Stream、AsyncStream 仅限零输出 handler | 零输出时执行动作;单输出时生成 List | | `filter` | 仅 List | handler 输出 Bool,返回保留原顺序的 List | | `find` | 仅 List | handler 输出 Bool,返回第一个匹配项或 `null` | | `sum` | 仅数值 List | 直接求和,或用单输出数值 handler 投影后求和 | | `reduce` | List、Bytes、Stream、AsyncStream | handler 为 `(Item, State) -> State`,返回最终状态 | | `reduce_until` | List、Bytes、Stream、AsyncStream | handler 输出下一状态和 `stop: Bool`,处理当前元素后可停止 | | `parallel_each` | List、Bytes、Stream | 非挂起零输出 handler;第三个参数是请求并发数,范围为 1–256 | | `parallel_each` | List、Bytes、AsyncStream | 挂起零输出 handler;第三个参数是任务组容量,范围为 1–65536,同时受全局任务容量约束 | Bytes 的元素类型是 Int,值域为 0–255。List、Bytes 可使用非挂起或挂起 handler;同步 Stream 只使用非挂起 handler。AsyncStream 的 `each`、`reduce`、`reduce_until` 可使用两类 handler,具体类别由编译器推断。非挂起 handler 不能在挂起执行路径中隐藏阻塞或未限定的 handler effect。`each`、`filter`、`find` 和投影式 `sum` 最多可以带一个末尾 context,handler 的输入顺序是元素、context。`parallel_each` 的可选第四个参数也是 context。需要多项 context 时,先组合成一个字段型值。`reduce` 和 `reduce_until` 不接收 context;共享数据应放入 State。 空 List 的 `sum` 返回相应数值类型的零;空序列的 `reduce` 和 `reduce_until` 原样返回初始状态。`reduce_until` 在处理当前元素并得到 `stop = true` 后停止;如果输入是 Stream,下一个元素保持未消费,可以通过任一 Stream 别名继续读取。 AsyncStream 消费会使当前函数被推断为挂起。组合器本身顺序等待拉取和 handler 完成,不需要额外包装。消费返回、提前停止、故障或取消都会关闭这个异步流,其他别名不能继续消费。`parallel_each` 先等待空位再拉取,并在等待下一项时观察子任务故障;返回前清理已启动的工作。`close(stream)` 本身不挂起,会唤醒等待中的拉取。 `reduce` 示例: ```dever add(value: Int, total: Int) (next: Int) { next = total + value } total(values: List) (result: Int) { result = reduce(add, values, 0) } ``` `reduce_until` 的 handler 必须输出下一状态和名为 `stop` 的 Bool: ```dever take_until_limit(value: Int, total: Int) (next: Int, stop: Bool) { next = total + value stop = next >= 100 } ``` 除 `parallel_each` 外,List 和 Bytes 按位置顺序处理,Stream 按拉取顺序处理,挂起 handler 逐项等待完成。完全非挂起的 `parallel_each` 创建不超过请求并发数的作用域线程。处于挂起调用链时,命名的非挂起 handler 复用 runtime 的 blocking worker,实际并发受该 worker 上限约束,且不能再次触发并发;挂起 handler 则复用有界 Group,先等待任务容量再取下一项。`parallel_each` 的 Stream 仍由单一生产端顺序拉取,不会先把整个流收集到内存。handler 的业务错误保留原类型并由调用方继续传播,不转换成普通字符串。handler 副作用的完成顺序不保证,调用返回前会等待所有已启动工作结束。handler 输入和 context 不能包含 Stream。 ## 8. 静态 handler function 可以接收受限的静态 handler: ```dever transform_all( transform: handler(value: Int) (result: Text), values: List ) (results: List) { results = each(transform, values) } ``` handler 只能是签名匹配的具名 function,或者当前 function 收到的 handler 参数。它不是普通运行时值,不能存入局部值、字段、List、Map、Stream,不能比较,也不能作为输出返回。 ## 9. 挂起推断与结构化并发 所有 function 使用同一种声明语法。普通调用保持顺序完成;如果调用链包含网络、非阻塞定时器、AsyncStream 或其他挂起操作,编译器会沿静态调用图推断挂起 effect,并在生成的 Rust 内部插入 `.await`。Dever 源码不声明 `async`,也不写 `await`: ```dever sleep_ok(result: dever.time.SleepResult.Done) () recover("this operation may continue after timer failure") {} sleep_ok(result: dever.time.SleepResult.Failed(message)) () recover("this operation may continue after timer failure") {} delayed_value(value: Int) (result: Int) { sleep_ok(dever.task.sleep(10)) result = value } main() (result: Int) { task = run(delayed_value(42)) result = wait(task) } ``` 直接调用始终等待结果;只有 `run(call(...))` 显式并发启动。`run` 同时支持挂起函数和普通函数,普通函数会进入受限 blocking worker,不会堵塞调度线程。它返回局部仿射 `Task`:Task 不能复制、嵌套进其它值、跨线程传递或作为输出返回,并且必须恰好由一次 `wait(task)`、`stop(task)`、`timeout` 或 `race` 消费。`stop` 仅接受零输出 Task,语义是请求取消并等待任务停止;取消不会回滚已经发生的外部副作用。Task 离开异常路径时也会请求取消,不会成为脱离父作用域的后台任务。 动态数量的零输出任务使用 `Group`: ```dever workers = group(8) run(workers, write_one(message)) wait(workers) ``` `group(limit)` 的 limit 范围是 1–65536。达到 limit 后,新的 `run(group, call(...))` 会等待已有任务完成,不会无限排队。Group 也是局部仿射值,最终必须由 `wait(group)` 或 `stop(group)` 消费;第一个子任务失败时,其余子任务会被取消并等待结束。 CPU 计算和阻塞调用必须显式分类:`parallel(pure_call(...))` 只接受 pure 非挂起 function;`blocking(blocking_call(...))` 只接受 effect 分析已识别为阻塞系统边界且不含未知 handler effect 的非挂起 function。文件、同步睡眠和标准输出属于阻塞边界;TCP 调用会被推断为挂起。两种边界的参数都在提交前按源码顺序求值,工作由有界线程池承担。挂起函数不能原地执行阻塞调用、隐藏并发的集合 handler 或 effect 未知的非挂起 handler 参数;必须使用 `blocking` 或调整调用结构。 有界 Channel 用于任务通信: ```dever messages = channel(Text, 64) send(messages, "ready") message = receive(messages) close(messages) ``` Channel 容量范围是 1–65536。`send`、`receive` 会使当前调用链被推断为挂起;队列满时发送方等待,队列空时接收方等待。`receive` 返回 `T?`:收到值时返回该值,Channel 关闭且缓存值耗尽后返回 `null`。Dever 不产生嵌套可空类型,因此 `Channel` 的接收结果仍是 `T?`,发送的 `null` 与关闭后的空结果都表现为 `null`。`close(channel)` 幂等,并唤醒等待中的发送方和接收方;关闭前已经缓存的值仍可读完。`Channel` 和 `AsyncStream` 的 T 必须可跨线程传递。 跨挂起点仍存活或进入 `run`、Channel、`parallel`、`blocking` 的普通值必须可跨线程传递。Task 和 Group 是受编译器跟踪的局部仿射值,不能嵌套、传参或逃逸。完全非挂起的入口不初始化 Tokio;推断为挂起的入口创建唯一的有界 Tokio 多线程运行时。默认调度线程数按 CPU 数量确定且最多 64,CPU/阻塞 worker 默认最多 8,同时存活的 `run`/Group 子任务默认最多 4096,硬上限为 65536。 `dever.task.sleep(milliseconds)` 是非阻塞定时等待,要求非负毫秒数,成功无输出,失败默认传播;显式捕获使用 `dever.time.SleepResult`。已有 `dever.time.sleep` 保持同步阻塞语义;挂起调用链需要同步睡眠时必须使用 `blocking(...)` 隔离。运行时由生成入口统一启动,业务源码不手工创建或嵌套 runtime。 `timeout(task, millis, fallback)` 等待任务;millis 必须为正。到期先停止并排空任务,再执行零输入的静态 fallback。fallback 必须与任务的命名输出签名完全相同,支持零个、一个或多个输出。程序故障继续传播,不触发 fallback。`race(first_task, second_task, ...)` 至少接收两个输出签名相同的 Task,等待第一个完成,再停止并排空其余任务;它也不吞掉程序故障。两者会使调用链挂起,并消费传入的全部 Task。 ```dever unavailable() (value: Text) { value = "请求超时" } # lookup 的输出声明为 (value: Text)。 pending = run(lookup()) value = timeout(pending, 1000, unavailable) first_task = run(lookup_primary()) second_task = run(lookup_secondary()) value = race(first_task, second_task) ``` `stream(channel)`、`stream(list)`、`stream(bytes)` 创建按需拉取的 AsyncStream;元素必须可传递,Bytes 产生 Int。消费流时由编译器推断挂起。Channel 流先读完关闭前的缓存再结束,发送的 null 仍是一个实际元素。关闭流释放其 Channel 别名,不关闭调用方另持有的 Channel。 `dever.task.ticks(millis)` 创建 `AsyncStream`,周期必须为正;第一个值在一个周期后产生,序号从 1 开始。消费过慢时跳过错过的周期,不积压 tick 或预先创建任务。用 `each`/`reduce_until` 消费,关闭流释放定时器。 ## 10. 失败处理 可恢复失败使用 choice 中显式标记的 `error` 变体。编译器不根据 `Failed` 等名称猜测错误: ```dever type MessageResult { Ready(text: Text) error Failed(message: Text) } read_message(bytes: Bytes) (message: Text) { message = dever.bytes.to_text(bytes) } required_port(value: Int < 1) (port: Int) { fail(MessageResult.Failed("port must be positive")) } required_port(value: Int >= 1) (port: Int) { port = value } ``` 普通调用默认传播失败,因此 `message = dever.bytes.to_text(bytes)` 不需要 `?`、`try` 或 `await`。调用链的错误集合由编译器推导;未处理的失败到达 HTTP、Task 或 CLI 根边界时,由运行时统一回滚事务、记录一次并终止对应执行边界。 需要主动检查成功和全部错误变体时,使用 `result(call)` 捕获,然后交给穷尽分句: ```dever decode(result: dever.bytes.DecodeResult.Decoded(text)) (message: Text) recover("invalid input is shown to the user") { message = text } decode(result: dever.bytes.DecodeResult.Failed(reason)) (message: Text) recover("invalid input is shown to the user") { message = reason } main() (message: Text) { message = decode(result(dever.bytes.to_text(dever.bytes.from_text("hello")))) } ``` `fail(error_variant)` 主动产生一个领域失败并立即结束当前调用;只能传入带 `error` 标记的变体。`result(call)` 只关闭该次调用的默认传播,捕获结果仍必须被穷尽处理、继续返回或显式恢复。Dever 不提供 exception、`throw` 或 `catch`。 错误必须传播、传给同样受检查的处理函数,或转换成明确错误。读取消息、打印日志、覆盖变量或交给空函数都不算处理。主动降级需要声明 `recover("具体原因")`,同一函数所有分句必须声明相同理由;例如可选本地配置缺失时采用固定默认值。理由必须非空,编译器不判断这项业务策略是否合理。详见第 18 节。 编程错误或不可恢复运行时故障会终止当前执行,并带源码位置;普通 function 不能捕获这类故障。 ## 11. 常用核心操作 无需 package 前缀的核心名称: ```text Bool Int Decimal Float Text Id Bytes Secret List Map MapEntry Stream AsyncStream Channel true false null other each parallel_each reduce reduce_until filter find sum append first get put remove entries length close run wait stop group parallel blocking channel send receive timeout race stream ``` List 与 Map 操作: ```dever extended = append(values, value) first_value = first(values) label = get(labels, "env") labels = put(labels, "env", "development") labels = remove(labels, "env") pairs = entries(labels) size = length(values) ``` `append`、`put` 和 `remove` 返回新值,不修改原集合。`first(List)` 和 `get(Map, key)` 返回可空值。`entries(Map)` 返回按插入顺序排列的 MapEntry List。`length` 接受 List、Map、Text 或 Bytes;Text 按 Unicode codepoint 计数。核心 `close` 接受 Stream、AsyncStream 或 Channel,且没有输出。 ## 12. 标准 package 标准 package 本身也使用 Dever 编写。下表链接到当前公开源码;其中的 `public` 是编译器随附标准库的内部边界,不是应用源码语法范本: | package | 已提供的主要接口 | | --- | --- | | [`text`](library/text.dever) | `trim`、`lower`、`upper`、`at`、`slice`、`index_of`、`contains`、`starts_with`、`ends_with`、`split`、`replace`、`join`、`is_empty`、Unicode codepoint 转换 | | [`int`](library/int.dever) | `parse`、`to_text` | | [`decimal`](library/decimal.dever) | `parse`、`to_text`、`from_int`、`round` | | [`float`](library/float.dever) | `parse`、`to_text`、`from_int` | | [`math`](library/math.dever) | `sqrt`、`sin`、`cos`、`log`、`pow`、Float 分类 | | [`dever.id`](library/dever/id.dever) | `from_text`、`to_text` | | [`dever.bytes`](library/dever/bytes.dever) | Text/Bytes 转换、`from_ints`、`length`、`at`、`slice`、`concat` | | [`dever.process`](library/dever/process.dever) | `arguments` | | [`dever.crypto`](library/dever/crypto.dever) | `token`、`password_hash`、`password_verify`、`sha256`、`hmac_sha256`、`constant_time_eq` | | [`dever.time`](library/dever/time.dever) | `now`、`parse_datetime`/`format_datetime`、`parse_date`/`format_date`、`parse_time`/`format_time`、`duration`、`add`、`subtract`、`difference`、底层时钟与 `sleep` | | [`dever.task`](library/dever/task.dever) | 非阻塞 `sleep` 和周期 `ticks` | | [`dever.io`](library/dever/io.dever) | 标准输出、文件创建/打开/读写/关闭、分块 Stream | | [`dever.net`](library/dever/net.dever) | TCP 连接、监听、读写、超时、连接与数据 Stream | | [`dever.json`](library/dever/json.dever) | JSON `parse`、`stringify`、节点/属性/数组元素访问 | | [`dever.http`](library/dever/http.dever) | Hyper HTTP/1、HTTP/2 多路复用、流式客户端和静态 handler | 数值或索引解析失败通常返回可空值;文件、网络、Bytes、JSON、HTTP 和 ORM 的可恢复错误默认沿调用链传播。普通调用返回成功值,`result(call)` 才产生显式选择型结果;具体以 function 输出类型为准。 ### UTC 时间和类型化 wire `DateTime` 内部是 Unix epoch 有符号整数毫秒;`Date` 是 UTC 午夜对应的 epoch 毫秒,`Time` 是午夜起 `[0, 86400000)` 毫秒,`Duration` 是有符号毫秒。数据库表示、排序和 Model fingerprint 不随外部格式变化。 `dever.time.now()` 返回 UTC `DateTime`。`parse_datetime(Text)` 接受 RFC3339 及显式 offset,`format_datetime(DateTime)` 输出 UTC `YYYY-MM-DDTHH:MM:SS[.sss]Z`;零毫秒省略小数,非零固定三位。输入最多三位小数,拒绝闰秒、子毫秒精度和 UTC 转换后超出四位年份的日期。`parse_date`/`format_date` 使用 `YYYY-MM-DD`;`parse_time`/`format_time` 使用 `HH:MM:SS[.sss]`。日历无效或格式化值不在可表示范围内会失败。 `duration(millis: Int)` 构造 Duration;`add(DateTime, Duration)`、`subtract(DateTime, Duration)` 和 `difference(later: DateTime, earlier: DateTime)` 使用 checked i64 算术,最后一个返回 Duration。算术溢出显式失败;合法 i64 结果仍可能超出四位日历格式化范围。确定性转换与算术是 `pure`,读取当前时间有 time effect。普通调用传播失败,显式 `result(call)` 使用 `DateTimeResult`、`DateResult`、`TimeResult`、`DurationResult` 或 `TextResult`,成功分支均为 `Read(value)`,错误分支为 `Failed(message)`。 共享 wire schema 从已检查类型生成具体编解码器。Bool/Int/Float 使用 JSON 标量;Decimal/Id/Uuid 使用字符串,不经过 Float;ModelId 使用 i64 整数且 schema 保留 Model 的 nominal identity;DateTime/Date/Time 使用上述字符串格式,Duration 使用整数毫秒。支持 List、nullable 和没有 private 字段或字段范围约束的 record。缺失 nullable 字段得到 null,缺失必填字段、未知字段、重复键、错误标量和非有限 Float 均失败;Int 只接受 i64 范围内的整数字面量。Choice、Map、Related 和资源不属于当前 wire 合同。 边界 JSON 最多 16 MiB、64 层容器、65536 个值(含根和容器)。Json 保留验证后的原始 JSON 和数字拼写,不经过动态浮点转换。Secret 仅允许 API/setting 输入策略生成,普通输出和 Job payload 禁止;不同策略具有不同 schema identity。业务函数接收具体类型,边界节点不会传入业务代码。API response、Adapter setting 和 Job payload 复用此 codec;API 输入的后续扩展也必须复用这个 owner。 ### JSON JSON 解析使用扁平、带类型的 `Document`,不产生动态对象。`dever.json.parse` 直接返回 `Document`;`result(dever.json.parse(...))` 使用 `ParseResult` 捕获失败。这些公开 type 位于 `dever.json.value`,契约是: ```dever type Value { Null Boolean(value: Bool) Number(raw: Text) String(text: Text) Array(children: List) Object(members: Map) } type Document { root: Int nodes: Map } type ParseResult { Parsed(document: Document) error Failed(position: Int, message: Text) } type WriteResult { Written(text: Text) error Failed(node: Int, message: Text) } ``` 调用签名: ```text dever.json.parse(Text) -> dever.json.value.Document dever.json.stringify(dever.json.value.Document) -> Text dever.json.node(dever.json.value.Document, node_id: Int) -> dever.json.value.Value? dever.json.property(dever.json.value.Document, object_id: Int, name: Text) -> Int? dever.json.element(dever.json.value.Document, array_id: Int, index: Int) -> Int? ``` `Document` 用整数 ID 保存一棵 JSON 树:`root` 是根节点 ID;`nodes` 的 ID 从 0 连续递增;子节点 ID 小于父节点 ID。Object 将属性名映射到子节点 ID,Array 保存有序子节点 ID。`property` 和 `element` 返回节点 ID,再通过 `node` 取得 Value;缺失、类型不符或越界时返回 `null`。JSON 的 `null` 则是实际存在的 `Value.Null`,两者不同。 数字保留原始 JSON 文本,避免先丢失精度。解析失败位置是从 0 开始的 Unicode codepoint 偏移,包括输入末尾位置。重复键、非法转义、未配对代理项、多余输入和超过 256 层的嵌套会传播 `ParseResult.Failed`。手工构造 Document 时,`stringify` 会检查根节点、连续 ID、子节点引用和输出规模,失败时传播 `WriteResult.Failed`;两者都可用 `result` 显式捕获。当前函数和失败类型见 [JSON 标准包](library/dever/json.dever)及 [JSON 值类型](library/dever/json/value.dever)。 ### HTTP `dever.http` 使用 Hyper HTTP/1 和 HTTP/2 引擎,业务 handler 由 Dever 编译为静态 Rust async 调用。HTTP/1.1 支持持久连接、流水线、Content-Length 和 chunked;HTTP/2 在同一连接上并发处理多个请求流。 ```text dever.http.default_limits() -> Limits dever.http.default_http2_limits() -> Http2Limits dever.http.header(name: Text, value: Text) -> Header dever.http.header_value(List
, name: Text) -> Bytes? dever.http.serve(route, dever.system.Listener, Limits) -> () dever.http.send(address: Text, port: Int, Request, Limits) -> Response ``` `Request` 包含 `method: Text`、`target: Text`、`headers: List
`、`body: Bytes`;`Response` 包含 `status: Int`、`headers`、`body`。`Header` 包含 `name: Text` 和 `value: Bytes`,因此重复头和非 UTF-8 头值可以保留。接收到的名称为小写,列表保留同名头的值顺序,不承诺不同名称之间的原始排列。`header` 构造文本头;`header_value` 忽略名称大小写并返回第一项,找不到返回 null。 | Limits 字段 | 默认值 | 约束 | | --- | --- | --- | | header_bytes | 8192 | 至少 8192;协议解析缓冲区和头部预算 | | body_bytes | 1048576 | 非负;单条请求/响应正文的上限 | | timeout_ms | 5000 | 正数;分阶段服务端期限,客户端单次请求总期限 | | connections | 32 | 1–65536;服务端并发连接数或每个客户端池的连接上限,另受任务资源约束 | | http2 | null | null 使用 HTTP/1;Http2Limits 使用 HTTP/2 | 开启 HTTP/2 沿用原来的服务和客户端函数: ```dever http_limits() (limits: dever.http.Limits) { limits = dever.http.default_limits() limits.connections = 2 limits.http2 = dever.http.default_http2_limits() } ``` | Http2Limits 字段 | 默认值 | 约束 | | --- | --- | --- | | streams | 16 | 每连接并发流上限;connections × streams 不超过 65536,另受运行时任务容量约束 | | stream_window_bytes | 65535 | 单流接收窗口,1–2147483647 | | connection_window_bytes | 262144 | 连接总接收窗口,65535–2147483647 | HTTPS 的双方配置必须协商 ALPN `h2`;明文双方使用 HTTP/2 prior knowledge,不发送 h2c Upgrade,不自动降级协议。窗口固定,不自动扩大;帧和引擎发送缓冲设置为 16 KiB。窗口限制协议接收流量,不是整个进程内存上限:已收集的请求正文、每流待发块、TLS/系统缓冲和应用持有的值还需要内存。小机器应一起设置 connections、streams、body_bytes 和 chunk_bytes。 HTTP/2 的 `Request.target` 仍为 `/path?query` 或 OPTIONS `*`。引擎负责伪头;入站 authority 在没有 Host 时规范化为 Host,已有 Host 必须与 authority 一致。应用不能传入 Connection、Keep-Alive、Proxy-Connection 等逐连接头字段;请求 TE 只允许 trailers,响应不允许 TE。单流中止不关闭相邻流;连接级故障影响该连接的全部流。 `serve` 和 `send` 的正文在限额内完整缓冲;流式服务使用下节的 `serve_live`。单帧正文共享字节存储,多帧逐步合并,不为每个微小帧积累元数据队列。HTTP 引擎负责报文定界;应用不得传入 Content-Length、Transfer-Encoding 或 Upgrade。响应状态为 200–599,204/205/304 的正文必须为空;HEAD 由引擎抑制响应正文。HTTP/1 接收头数量沿用 Hyper 默认上限 100,HTTP/2 使用解压后的头列表字节预算;发送头预算为引擎生成的头预留 80 字节。 route 的签名为 `handler(request: dever.http.Request) (response: dever.http.Response)`;是否挂起由 route 的调用图推断: ```dever route(request: dever.http.Request) (response: dever.http.Response) { response = dever.http.Response { status = 200 headers = [dever.http.header("Content-Type", "text/plain; charset=utf-8")] body = dever.bytes.from_text(request.target) } } start(listener: dever.system.Listener) () { dever.http.serve(route, listener, dever.http.default_limits()) } ``` 在 Adapter 中先用 `dever.net.listen(...)` 创建 Listener,再调用 serve;编译器会自动等待网络结果。serve 持续接收连接,可作为 `server = run(dever.http.serve(...))` 启动,再用 `stop(server)` 取消并等待子任务清理。调用者持有的 Listener 别名仍由调用者关闭。普通应用的 HTTP 入口用 [模块 API](#模块-api) 声明,无需自行编写服务启动函数。 - 服务端先预留任务容量,再接受连接;没有无界应用队列或每连接独占线程。每条 HTTP/1 连接按顺序执行 handler。 - 握手超时或底层 I/O 错误关闭该连接;正文超时返回 408,handler 超时返回 504,正文超限返回 413。HTTP/1 随后关闭连接,HTTP/2 结束对应流。写入停滞超过期限关闭连接。 - 期限和取消在异步挂起处生效;已开始的 blocking 操作仍遵守既有“等待完成”的契约,需要操作自身的超时。 - 对端协议错误影响对应流或连接。无效服务配置、accept 故障、handler 程序故障及无效响应沿 serve 的任务作用域传播,并保留 Dever 故障位置。 - send 为单次请求,驱动 future 随请求一起完成或取消;需要复用连接时使用下文的 client/request。成功返回 Response,网络/协议/输入失败默认传播;通过 `result` 捕获时为 `ResponseResult.Read(response)` 或 `Failed(message)`,不自动重试。 - send 的 address 是数值 IPv4/IPv6,port 为 0–65535;请求必须有一个 Host 头。target 使用 `/path?query`,或 OPTIONS 的 `*`,不接受代理 absolute-form 或 CONNECT。 - 普通 `serve/send` 不接受升级;WebSocket 使用下节的 HTTP/1 专用入口。HTTPS 使用下文的 TLS 服务和客户端;当前不支持 CONNECT、非空 trailer、服务器推送或 WebSocket-over-HTTP/2。 ### 流式 HTTP 与 SSE `serve_live` 的 handler 是 `handler(request: Request, reply: dever.system.HttpReply) ()`。它可以发送普通响应、流式正文和 SSE;HTTP/1 还可升级为 WebSocket。handler 内部是否挂起由编译器推断。HTTP/1 每连接最多一个活跃 handler;HTTP/2 每请求流独立拥有 handler 和清理范围,并受 streams 限额约束。 ```text dever.http.default_live_limits() -> LiveLimits dever.http.serve_live(route, Listener, Limits, LiveLimits) -> () dever.http.respond(HttpReply, Response) -> () dever.http.start(HttpReply, status: Int, List
) -> () dever.http.write(HttpReply, Bytes) -> () dever.http.finish(HttpReply) -> () dever.sse.start(HttpReply, List
) -> () dever.sse.send(HttpReply, Event) -> () ``` `LiveLimits` 默认 `chunk_bytes = 65536`、`idle_ms = 60000`、`heartbeat_ms = 15000`;块大小必须为正,`0 < heartbeat_ms < idle_ms`。`Limits.body_bytes` 仍约束请求正文和 `respond` 的完整正文,流式输出按块限制,不累计整条长连接的字节数。响应通道仅容纳 1 块;还需计入当前写入、协议引擎、操作系统及应用自身的缓冲,不代表每条连接总共只用一块内存。 响应只能开始一次。`respond` 提交完整响应;`start` 提交流式响应头,随后 `write` 按调用顺序发送,`finish` 结束正文。204/205/304 使用 `respond`。重复开始、超大块、关闭后写入、向 SSE 通道直接写普通正文均传播 `Failed`,可用 `result` 捕获。写入等待受 `Limits.timeout_ms` 限制;对端停止消费时不会无限排队。 handler 返回会结束正文;正文被消费完或丢弃、HEAD 抑制正文、连接断开或 `stop(server)` 会取消相应 handler 并等待其子任务清理。已经交给通道的块不因取消而自动重发。`HttpReply` 是线程安全资源别名,不能在所属会话结束后继续使用。handler 没有提交响应就返回属于程序故障;提交响应头之后的程序故障会截断正文并沿服务任务传播。 首个响应头沿用普通请求期限;提交流式头之后不再使用整段 handler 的 5 秒期限。普通流在 `idle_ms` 内没有输出会关闭;SSE 在空闲 `heartbeat_ms` 后输出注释心跳。心跳不占用额外任务;客户端不读取时仍受 HTTP 停滞写入期限约束。 `dever.sse.Event` 字段为 `event: Text`、`data: Text`、`id: Text?`、`retry_ms: Int?`。空 event 使用浏览器默认事件名;data 按 CR/LF/CRLF 分行且保留尾部换行;空 id 会重置浏览器的事件编号。event 禁止换行,id 禁止换行和 NUL,retry_ms 非负。编码后的完整事件必须放入一块,心跳要求块上限至少 3 字节。`sse.start` 持有 Content-Type,并在应用未指定时添加 Cache-Control: no-cache。应用从请求 `Last-Event-ID` 头读取重连位置,负责历史保存和重放;库不缓存历史事件。 ### WebSocket `dever.websocket` 复用 tokio-tungstenite 的 RFC 6455 握手、掩码、分片重组、UTF-8 校验和关闭协议,不使用自写帧解析器。 ```text dever.websocket.default_limits() -> Limits dever.websocket.accept(HttpReply, Limits) -> dever.system.WebSocket dever.websocket.connect(address: Text, port: Int, target: Text, Limits) -> dever.system.WebSocket dever.websocket.open(url: Text, ClientTls, Limits) -> dever.system.WebSocket dever.websocket.send(WebSocket, Message) -> () dever.websocket.receive(WebSocket) -> ReceiveState dever.websocket.messages(WebSocket) -> AsyncStream dever.websocket.close(WebSocket, code: Int, reason: Text) -> () ``` 连接成功直接得到 WebSocket;用 `result` 捕获时使用 `ConnectResult`,分支为 `Connected(socket)` 和 `Failed(message)`。Message 包含 `Text(value: Text)`、`Binary(value: Bytes)`、`Ping(value: Bytes)` 和 `Pong(value: Bytes)`。receive 直接返回 `ReceiveState.Read(message)` 或正常关闭的 `End`,错误默认传播,捕获时使用 `ReceiveResult`。messages 在正常关闭时结束,故障产生最后一个 `MessageEvent.Failed`。持续接收可使用 `each` 或 `reduce_until`,无需循环或递归语法。 WebSocket Limits 默认 `message_bytes = 1048576`、`idle_ms = 60000`、`write_ms = 5000`,均为正数。message_bytes 限制完整数据消息及分片累计;控制帧最多 125 字节。每连接读取缓冲区为 4 KiB,协议写缓冲有上限。方向锁允许同时读写;idle_ms 是一次 receive(含等待方向锁)的期限,write_ms 约束一次写入、客户端握手或关闭握手。超时读取会关闭资源;已开始写帧时取消会关闭连接,尚在等待写锁的取消不影响正在写入的另一个任务。 客户端 connect 的 address 是数字 IP,target 为 `/path?query`。open 接受完整的 `ws://` 或 `wss://` URL,支持 DNS,WSS 使用传入的 ClientTls 校验证书和目标名称;不接受 URL 用户信息或 fragment。握手期限包含域名解析、TCP 和 TLS,不自动重试。服务端 accept 从当前 HTTP 请求升级,失败时应用仍可返回普通错误响应。升级后连接仍占服务端名额,并由原 handler 作用域拥有;handler 结束或父任务停止都会关闭它,即使其他地方还持有别名。 应用需持续调用 receive/messages 才能读取协议帧;收到 Ping 时引擎先发送 Pong,再交给应用。库不建立额外的定时 Ping 任务,可通过 send(Ping(...)) 实现应用保活。close 校验合法状态码和最多 123 UTF-8 字节的原因,等待对端关闭握手;关闭会唤醒等待中的读写。当前不提供关闭帧元数据、子协议协商或压缩扩展。 这些底层操作用于 Adapter,类型和函数定义见 [HTTP](library/dever/http.dever)、[SSE](library/dever/sse.dever)和 [WebSocket](library/dever/websocket.dever)标准包。 ### HTTP 连接池与流式客户端 ```text dever.http.default_pool_limits() -> PoolLimits dever.http.client(origin: Text, ClientTls, Limits, PoolLimits) -> dever.system.HttpClient dever.http.request(HttpClient, Request) -> Response dever.http.open(HttpClient, Request) -> StreamResponse dever.http.open_stream(HttpClient, StreamRequest) -> StreamResponse dever.http.close_client(HttpClient) -> () ``` client 返回固定 origin 的共享连接池;需要捕获时使用 `ClientResult`,成功项为 `Ready(client)`。origin 如 `https://example.com:443`,不能含用户信息、业务路径、query 或 fragment;支持 DNS。请求 target 仍用 `/path?query`,没有 Host 时由 origin 补上。每个目标建立自己的池,不保留不断增长的全局主机表;不自动重放已发送请求或跟随重定向。HTTP/2 连接停止接收新流时,协议引擎确认尚未发送的请求会在原期限内重新排队。 | PoolLimits 字段 | 默认值 | 约束 | | --- | --- | --- | | idle_ms | 10000 | 正数;归还的连接闲置多久后关闭,即使没有下一个请求也会回收 | | chunk_bytes | 65536 | 正数;流式上传块上限及每次下载返回块上限 | | read_ms | 60000 | 正数;一次上传取块或下载拉取的等待期限 | `Limits.connections` 限制物理连接数,空闲和正在排空的连接也计入上限。HTTP/1 的请求占用上限等于 connections;HTTP/2 的请求上限为 connections × streams,每连接另限 streams。每池一个作用域拥有驱动和有界队列;HTTP/2 的协议子任务也归连接作用域。池属于创建它的任务作用域,作用域结束后,即使外部还持有别名,也不能继续使用。直接调用 `create_client(...)` 会在当前任务内完成;不要通过一个立即结束的 `run` 子任务创建长期共享的池。 request 完整收集响应,正文受 `body_bytes` 限制,排队、建连、发送、接收合计受 `timeout_ms` 限制。open 发送普通 Request 并返回响应头与流;open_stream 的 StreamRequest 把 body 改为 `AsyncStream`,其余字段与 Request 相同。成功均为 `Read(response: StreamResponse)`,其中 body 也是该异步流;`Chunk(bytes)` 代表数据,终止错误最多产生一次 `Failed(message)`,正常 EOF 直接结束。 流式客户端只按需读取,不收集整段上传或下载正文;总长度不限于 `body_bytes`,单块及等待时间受 PoolLimits 限制。等待请求槽位、DNS/TCP/TLS 和响应头受 `timeout_ms` 限制,响应流随后按 `read_ms` 读取,适用于 SSE 等长连接。HTTP 出站长度和分块编码仍由引擎拥有,转发入站响应时须移除 Content-Length、Transfer-Encoding 等受保留的头。 HTTP/1 响应体完全读完才归还连接,提前 `close(response.body)`、故障或取消会关闭该连接。HTTP/2 的连接可同时服务多个请求;完成或关闭正文释放该流,不中止邻流。仅持有未消费的正文会继续占用请求名额,活跃连接不会被空闲回收。close_client 关闭所有池别名、唤醒等待者并等待驱动清理;下载块共享字节存储,应用保留的块仍计入应用自身内存。 ### TLS、共享上下文与优雅停机 ```text dever.tls.system() -> dever.system.ClientTls dever.tls.client(ca_pem: Bytes) -> dever.system.ClientTls dever.tls.server(cert_pem: Bytes, key_pem: Bytes) -> dever.system.ServerTls dever.http.serve_tls(route, Listener, Limits, ServerTls) -> () dever.http.serve_live_tls(route, Listener, Limits, LiveLimits, ServerTls) -> () ``` system 使用随二进制提供的公共 CA;client 仅信任指定 PEM CA,返回 Ready(config) 或 Failed(message)。server 读取应用提供的证书链和私钥 PEM,返回同样形状的结果。TLS 复用 rustls/tokio-rustls,校验证书链、有效期和目标名称;客户端和服务端配置可跨任务共享,并使用有界会话缓存。TLS 服务沿用 HTTP 的握手期限和连接上限;serve_live_tls 提供 HTTPS 流式响应和 SSE,HTTP/1 模式还支持 WSS 升级。HTTP 单独派生 ALPN 配置,不改变其他 TLS 使用者。 需要共享应用状态时,声明具体 Context,并传入系统 serve 原语的可选末尾参数。编译器要求 handler 的最后输入与 Context 类型一致且可跨任务传递;Context 按普通值语义复制,Channel、HttpClient 等资源字段共享同一资源。 ```dever type Context { prefix: Text requests: Channel } serve(listener: dever.system.Listener, context: Context) () { dever.system.http_serve(route, listener, dever.http.default_limits(), context) } route(request: dever.http.Request, context: Context) (response: dever.http.Response) { send(context.requests, 1) response = dever.http.Response { status = 200 headers = [] body = dever.bytes.from_text(context.prefix + request.target) } } ``` http_serve_live、http_serve_tls 和 http_serve_live_tls 也接受这个末尾参数;live handler 的输入顺序为 request、reply、context,TLS 配置位于 context 之前。这里直接绑定静态 handler,不引入闭包或动态路由解释器。 优雅停机先调用 `dever.net.close_listener(listener)` 并处理结果,再等待 server Task。HTTP 停止接收新连接,HTTP/2 同时发送 GOAWAY,已开始的响应继续完成;WebSocket/SSE 会话仍由现有 handler 拥有。客户端收到 GOAWAY 后不再向该连接派发新请求,也不自动重放已发送请求。使用 `timeout(server, grace_ms, expired)` 给排空设置上限,expired 是零输入、零输出的静态 handler;到期先停止剩余任务并等待清理。需要立即停止时使用 `stop(server)`。自动 API 服务的启动与停机由核心负责,配置见 [模块 API](#模块-api)。 ### 模块 API 普通应用由编译器从 API、CMD 和 Job 声明生成入口。没有命令参数时,`run` 和编译产物按 `config/setting.json` 的 runtime mode 启动可用服务;CMD 必须显式传名称和一个 JSON 对象,不能借 CMD 参数改变部署模式。`run/build` 拒绝手写 `module/main.dever`。 `module/user/account/app.dever`: ```dever type Greeting { message: Text } greet(name: Text) (greeting: Greeting) { greeting = Greeting { message = "你好," + name } } ``` `module/user/account/api.dever`: ```dever get greeting = app.greet public post greeting = app.greet cmd greeting = app.greet ``` 前两项对应 `GET`、`POST /user/account/greeting`;CMD 名称为 `user.account.greeting`,输入是单个 JSON 对象,输出同样使用 `{"code":0,"message":"ok","data":...}`。HTTP 声明默认要求认证,只有明确的登录、健康检查等单条 HTTP 声明可加 `public`;`public cmd` 和 `public rest` 非法。public 入口忽略浏览器自动携带的旧 Cookie,使过期会话不会阻止重新登录;客户端显式提交的无效 Authorization 仍返回 401,不能降级成匿名请求。`get/post/put/delete = app.` 只绑定本领域无歧义的 App 函数,继承其输入输出签名,输出名不必叫 `response`。GET 的完整调用链不能写数据库或入队 Job。普通写 HTTP/CMD/Job 由入口建立整体事务;涉及外部 I/O、Worker、等待、并发或密码计算的写入口必须显式加 `long`,并将写库与入队放进 `transaction` 函数。`long` 不建立入口级事务,已提交阶段不因后续失败撤销;写入时须在事务内重查账户、租户或成员状态。编译器只验证声明,不根据调用图自动切换事务模式。API role 不写函数体或旧 `get_` 前缀。GET/DELETE 从 query 读取标量输入,POST/PUT 从 `Content-Type: application/json` 的对象正文读取;命令也读取严格 JSON 对象。缺少必填值、额外或重复字段均拒绝。输入输出使用同一静态 wire codec,当前不支持 Choice、Bytes 等类型。 简单领域使用 `api.dever`;复杂领域可同时使用 `api.dever` 与 `api//.dever`,topic 依次进入 URL,例如 `module/user/account/api/admin/session.dever` 中的 `get status = app.status` 对应 `/user/account/admin/session/status`。`sites..path` 匹配这里的 API 父目录:`admin` 匹配 `api/admin/**`,不匹配同级文件 `api/admin.dever`,也不改写 URL。每条 HTTP API 必须且只能归属一个站点;目录遗漏、path 前缀重叠和同一路径同方法重复都会在运行前报错。`rest` 可为同领域唯一 Model 生成 GET 列表/详情、POST、PUT、DELETE;多 Model 时写 `rest model` 或 `rest model.` 消歧。列表只接受有界 `page`、`size` 和至多一个显式 `search` 条件,不开放任意查询。自动 REST 只序列化公开字段;`private owner user_id: ... = dever.auth.user_id()` 在创建时写入可信身份,并把同一 owner 条件放入全部读写 SQL。客户端不能提交 owner 或无 `input` 的服务端字段。需要不同资源策略或跨 Model 编排时仍使用 App,不把业务规则塞进 REST 声明。 HTTP 绑定的 App 调用链可用 `dever.api.request_id()`、`method()`、`path()`、`client_address()`、`header(name)`、`cookie(name)`、`secret_cookie(name)` 读取请求元数据,并用 `set_header(name, value)`、`set_cookie(name, value, options)`、`set_secret_cookie(name, secret, options)` 设置响应元数据;无需传递 Context 参数。`client_address()` 只返回直接 TCP peer 的地址,不信任转发 Header。`secure_cookie_options()` 提供 `Path=/`、`SameSite=Lax`、`Secure`、`HttpOnly` 的起点,业务可构造 `CookieOptions` 调整 Domain 和 Max-Age。Secret Cookie 使用 `v1.` 加无填充 base64url 存储原始 Secret 字节,读写不会把 Secret 转成业务 Text;非此格式的值读作缺席。敏感请求头不能经 `header` 读为 Text,Secret Cookie 必须使用 Secure 与 HttpOnly;响应 Header 不允许覆盖 Set-Cookie、Content-Type、Content-Length 等运行时字段或注入换行。Header/Cookie 先暂存在请求作用域,只有 handler 成功并且响应序列化成功后才发布;失败会丢弃待发布元数据;普通入口回滚未提交事务,`long` 入口不会撤销先前已经提交的 App 事务。Cookie 写请求和使用 Cookie 认证的非安全方法必须携带 Origin,并要求 Origin 与 Host 都精确匹配 `sites..origin` 配置的外部 HTTP/HTTPS scheme、规范化主机和有效端口;运行时不从内部明文监听协议猜外部 HTTPS,也不信任转发 Header。CMD/Job 调用链不能使用 HTTP 请求能力,脱离请求的并发任务也不能调用这些能力。 POST App 可以直接接收一个顶层 `Upload` 及普通标量,此时请求必须是 `multipart/form-data`。Runtime 按 `http.upload` 的总大小、单文件、字段、part、header 和 filename 上限流式写入 `data/tmp`;filename 仅作展示值,不能成为路径。`Upload` 是 affine 请求资源,不能嵌套、复制、作为输出或进入 GET/CMD/helper;App 可读取 metadata 后显式 `close_upload`,或一次性传给同领域 Port,再由 Adapter 调用 `dever.storage.put`。未消费资源及解析失败、handler 失败和取消路径会清理临时文件。 监听、站点和认证 Provider 都只在 `config/setting.json` 配置: ```json { "http": { "host": "127.0.0.1", "port": 8181 }, "auth": { "providers": { "session": { "verify": "user.account.verify", "jwtSecret": "replace-with-a-deployment-secret-of-at-least-32-bytes", "ttlSeconds": 3600, "cookie": "cms_session" } } }, "sites": { "admin": { "path": "admin", "auth": "session", "hosts": ["admin.example.com"], "origin": "https://admin.example.com" }, "front": { "path": "front", "auth": "session", "hosts": ["www.example.com"], "origin": "https://www.example.com" } }, "log": { "level": "info" } } ``` Provider 使用固定 HS256 token,校验算法、签名、有效期、Provider、站点、subject 和 session;密钥只放部署配置,不进入源码。Bearer 和配置指定的 Secure Cookie 二选一,不能同时提交;使用 Cookie 认证的非安全方法以及任何 Cookie 写入都必须通过上述精确同源检查。需要浏览器 Cookie 的站点必须配置 `origin`;它是部署入口的外部 origin,不是内部监听地址。Provider 的 `verify` 必须引用一个公开普通 App 函数:输入为 `dever.auth.Claims`,输出为该 App 自己定义的公开身份 record,字段严格且只能是 `id: Text`、`user_id: ?`、`tenant_id: ?`。verify 只负责验证账号、session、成员和租户关系;它只能读 global Model,不能返回权限列表、读取原始 HTTP 请求、再次读取身份、写数据库或入队 Job。 身份建立后,App 可读取 `dever.auth.id()`、`session()`、`user_id()`、`tenant_id()` 与 `dever.site.key()`;`dever.auth.owns_user(model_id)` 用于显式资源所有权比较。所有非 `public` HTTP API 都在读取业务输入和调用 App 前自动校验精确权限 `component.domain.site.action`;REST 固定生成 `read/create/replace/delete`,HTTP method 只是目录元数据,同一权限键不能重复声明。源码没有 `auth.require`,也不能自报权限。登录使用 `dever.auth.issue_cookie(subject, session, tenant)`,登出使用 `dever.auth.clear_cookie()`;两者自动采用当前 Provider 在 `setting.json` 中配置的 Cookie 名和 TTL。 `user_id()`、`tenant_id()` 默认返回 `Int?`;当赋值或函数参数明确期待某个 `model.id?` 时,返回对应的可空名义 ID,不需要再次查询会话表。编译器逐条检查受保护 API 的调用链和自动 REST 字段绑定:该 ID 必须与当前站点 `verify` 身份 record 的对应字段类型一致,`owns_user` 的参数也必须匹配其 `user_id`。这不是通用的 Int 到 ModelId 转换,非空 ID 仍须通过 `null` 分支处理取得。只有源码、没有部署配置的检查延后 Provider 匹配;配置检查和构建必须完成匹配。public、CMD、Job 和普通应用测试不会因此获得身份读取能力。 核心在控制库维护全局 `_dever_permission` 目录,服务启动时同步当前编译产物并把已删除权限标为 inactive。角色、角色权限和用户角色存放在当前身份对应的授权库:租户用户使用租户库,平台用户使用控制库。App 可通过 `dever.auth.permissions()`、`save_role(...)`、`grant_role(...)`、`revoke_role(...)`、`disable_role(...)` 实现很薄的后台管理适配;当前站点和租户由隐藏上下文确定。这些管理能力只能由受保护的写 API 到达,`public` 和 GET 调用链会被静态拒绝。新租户迁移完成后使用 `dever tenant owner ` 建立保留的全权限 Owner,普通 save/grant/revoke/disable 均不能修改该保留角色。底层 `issue` 与通用 Cookie API 保留给非浏览器协议组合。身份、租户和权限都不接受客户端参数或 Header 自报,Domain 保持纯业务规则。`dever.api.Error` 的 `Invalid`、`Unauthorized`、`Forbidden`、`NotFound`、`Conflict`、`TooManyRequests` 使用固定安全消息映射到 400/401/403/404/409/429;其它失败只记录脱敏日志并返回通用 500。 只有 CMD、没有 HTTP 声明的应用无需 `http`、`sites` 或 `auth` 配置。正常响应使用 HTTP 200,正文为 `{"code":0,"message":"ok","data":...}`;错误时 `code` 与 HTTP 状态一致且 `data` 为 `null`。多租户只有 database 隔离,不存在 mode 开关:`tenant.database` 选择控制连接,`global type` 使用平台库,其他持久化 Model 和 Job 使用可信身份或显式 `dever tenant migrate` 选择的租户物理库。缺少租户、租户未迁移、schema fingerprint 过期都会失败,绝不回退平台库。租户组件从 tenant Model 和 tenant Job 自动推导;混合组件整体按租户组件处理,它拥有的全部 Job 使用租户队列。public API 不能触达租户组件,API/CMD/Job 执行前统一检查组件状态。运维通过 `dever tenant component disable|enable ` 控制租户功能,平台组件或未知组件不能切换。 ## 13. Model、ORM 与数据库 ### 13.1 Model 文件与字段 Model 同时支持 `.dever` 和 `.dever.md`,复合扩展名不影响类型名或表结构。Markdown 中的 Seed、索引、关系、迁移和 SQL 声明按 [Markdown 规范](MARKDOWN-SYNTAX.md)分别说明;CMS 的两套实现位于 `examples/cms/dever` 与 `examples/cms/md`。 简单领域在 `//model.dever` 中定义一个与 domain 同名的 Model;复杂领域可使用 `model/.dever`,记录名与 topic 对应。文件继续使用普通 record `type`,不增加 `model` 或 `public` 关键字;主记录、choice 和生成的 ID 类型由 Model role 自动提供。以下 `user/account/model.dever` 对应 `user.account.model.Account`: ```dever type AccountStatus { Active = "启用" Disabled = "禁用" } type Account { uuid: Uuid generated unique email: Text(254) unique display_name: Text(1, 64) status: AccountStatus default AccountStatus.Active index organization_id: organization.team.model.id? } index(status, created_at) ``` 每个 Model 自动增加只读 `id` 和 `created_at: DateTime`。`id` 的类型是该 Model 专属的 `.id`,例如 `user.account.model.id`;不同 Model 的 ID 不能混用。`created_at` 和 generated 字段不出现在创建输入中。 Model 字段支持 `Bool`、`Int`、`Float`、`Decimal(P, S)`、`Text`、`Bytes`、`Uuid`、`DateTime`、`Date`、`Time`、`Duration`、`Json`、无载荷 choice、Model ID,以及它们的可空形式。`Text(max)`/`Bytes(max)` 指定最大长度,`Text(min, max)`/`Bytes(min, max)` 同时指定最小和最大长度。结构化值不会自动序列化为列,需要时显式使用 `Json`。 默认值和 Seed 共用编译期存储校验:Int 必须是 i64 范围内的整数,Float 必须有限,Decimal 精度和小数位不能溢出,Text 长度按 Unicode 标量计数。非法常量不会等到生成 Rust 或访问数据库后才报错。 - `generated` 当前用于由 ORM 生成 UUIDv7 的非空 `Uuid`。 - `default` 接受与字段类型匹配的静态常量或无载荷 choice 变体。 - `index` 和 `unique` 定义单字段索引;顶层 `index(a, b)`、`unique(a, b)` 定义复合索引。 - `target.model.name.id` 字段自动成为外键并自动建立普通索引,但不会自动加载目标记录。 - choice 的右侧 Text 是显示名称;数据库保存左侧稳定变体名,`Choice.options` 按声明顺序提供选项 Map。 自动 REST 的字段来源直接写在 Model 字段同行,不改变 App 中的普通 ORM:`create = expr` 只处理 POST,`replace = expr` 只处理 PUT,`search = expr` 处理可选 GET 列表筛选。表达式引用 `input` 时读取客户端同名值并做纯转换;不引用 `input` 时由服务端上下文提供,客户端同名字段会被拒绝。表达式只能调用同领域 `pure Domain`、无 effect 标准函数或精确白名单的只读 request/auth/site 能力,不能访问 Model、Job、时间、响应修改或并发。`search` 最多一个,必须是公开、非空、可索引 wire 标量;owner 与 search 同时存在时自动生成复合索引。 ### 13.2 CRUD、条件和分页 编译器为 Model 生成唯一一套短函数,不需要手写 Repository 或第二套 CRUD: ```dever created = model.create({ email = "reader@example.com" display_name = "Reader" }) page = model.list({ where = status == model.AccountStatus.Active and (display_name == "Reader" or display_name == "Admin") order = [created_at.desc, id.desc] page = 1 size = 20 }) changed = model.update(created.id, { status = model.AccountStatus.Disabled }) ``` 可用操作为 `create`、`create_many`、`get`、`first`、`list`、`cursor`、`count`、`exists`、`stream`、`update`、`delete` 和 `upsert`。这些操作只能在所属领域 App 内以 `model.`(多 Model 时为 `model..`)调用;main、API 和其他领域必须调用该领域 App。创建、更新和查询块只能直接出现在对应调用位置,由目标 Model 提供字段命名空间;它们不是运行时 Map 或可保存的查询对象。 `where` 支持 `and`、`or`、`not`、括号、比较、null,以及 `in`、`between`、`contains`、`starts_with`、`ends_with`。查询结构、字段和排序全部在编译期确定,运行值始终通过数据库参数绑定。 `list` 默认第一页、默认 20 条,返回包含 `items/page/size/total/pages` 的 Model `Page`;`cursor` 返回 `items/next/has_more` 且不执行 count;`stream` 按行拉取并保持有界内存。配置中的 `max_page_size` 同时限制分页、关系子列表和原生 SQL 多行结果,不提供无界 `all()`。 ### 13.3 关联 外键字段会合成同名去掉 `_id` 的 to-one 关系。反向 to-many 在目标 Model 文件中显式声明: ```dever relation articles = news.article.model.author_id ``` 查询只有写入 `with` 才加载关系: ```dever accounts = model.list({ order = [id.asc] with = [organization, articles] }) ``` 关系字段使用 `Related` 的 `Unloaded`/`Loaded(value)` 两态,业务代码通过分句穷尽处理。to-one 使用受控 JOIN;to-many 按当前父结果批量查询并归组,不逐条查询。`with` 只接受一层静态关系名,并只用于 `first`、`list` 和 `cursor`;`stream` 不加载关系。多对多关系使用显式中间 Model。 ### 13.4 事务和数据库绑定 事务是专用 function 声明,不向业务代码暴露 Database、Transaction、begin、commit 或 rollback: ```dever transaction register(email: Text, display_name: Text) (account: model.Account) { account = model.create({ email = email display_name = display_name }) } ``` 编译器沿完整调用图推导数据库 effect。普通写 HTTP、CMD 和 Job 在入口建立整体事务,响应编码成功后才提交;同一入口内的显式 `transaction` 复用外层事务。`long` 入口不建立整体事务,业务写库和持久入队必须跨入显式 `transaction`;自动 REST 写操作保持原来的事务边界。最外层 transaction 成功时提交;error、fault 或取消时先回滚当前事务再传播原失败。同一连接的嵌套 transaction 复用外层事务。无数据库操作、跨连接,以及事务内的并发、流式游标、HTTP、文件传输、外部 Worker 和密码计算都会在构建前被拒绝。`blocking(call())` 仅在目标不包含这些操作并满足原有非挂起、无数据库约束时可用。事务外可以等待或编排多个独立事务;写入前需在事务内重新核验状态。`long` 中先前已经提交的事务不会因为后续阶段或响应序列化失败而回滚;重试的幂等性、失败状态和外部产物清理由业务定义,数据库回滚不撤销外部调用。 `long` 只修饰 HTTP、CMD 或 Job 入口,不修饰 App 函数、`transaction` 或 `rest`;与 `public` 同用时写 `public long`。声明形式如下(App 与请求类型按各自领域定义): ```dever post create = app.create long post generate = app.generate long cmd generate = app.generate public long post login = app.login ``` Job 保留原来的重试和超时声明: ```dever long job generate(input: app.Request) () retry(3) timeout(30000) { app.run(input) } ``` `long` 表示入口不自动持有整体事务,不代表允许长事务、自动异步、自动延长超时或按运行秒数分类。普通自动事务中出现外部操作,或 `long` 调用链存在事务外写库,均在编译期报 C014;仅添加外部调用不能静默改变原子性。GET 即使声明 `long` 也不能写库或入队。 数据库错误默认传播,保留 `dever.database.Error` 的类别和消息:Pool、PoolExhausted、Connection、Timeout、Cancelled、Database、Constraint、NotFound、InvalidData、Migration。要主动捕获一个返回 Account 的 App 调用,可以声明下面的结果类型;`result(load_account(...))` 的成功载荷和完整错误集合必须与调用一致。 ```dever type ReadResult { Found(value: user.account.model.Account) error Failed(error: dever.database.Error) } ``` `Failed` 包装原始错误 choice,事务回滚和任务等待不会把它改成普通 fault。捕获后仍须按语言规则传播、返回或通过明确的 recover 策略处理;错误不会因读取或记录日志而被视为已处理。启动阶段的配置与迁移失败发生在业务入口之前,仍会阻止进程启动。 Model 默认按 component 名选择连接,例如 `user.account.model` 优先使用 `database.user`;该键不存在时使用 `database.default`。Model 文件需要固定连接时,在类型声明前写: ```dever database report ``` 显式连接不存在会直接失败,不回退 default。普通 CRUD 不接收数据库句柄,也不能按运行时字符串切换连接。 配置顶层存在 `tenant` 时,普通 Model 默认为租户 Model;平台账号、租户目录等共享数据在声明前写 `global type Account { ... }`。`tenant` 只接受 `database`、`max_pools` 和 `idle_timeout_ms`;`database` 指向保存租户就绪状态、组件开关和全局权限目录的控制连接。SQLite 连接为租户 Model 配置 `tenant_directory`,PostgreSQL 配置最多 43 个 lower_snake_case 字节的 `tenant_database_prefix`。业务请求不会自动建库,必须先执行 `dever tenant migrate `。PostgreSQL migration 会通过该连接的配置 URL 幂等执行 `CREATE DATABASE _`,部署角色必须具有建库权限;随后才连接租户库并迁移,失败不会写 ready marker。一个事务不能混用 global 与 tenant Model。 ### 13.5 Schema、迁移和 Seed 当前 Model 是目标 schema。启动时 ORM 比较数据库 catalog、内部 history 和二进制中的规范化 schema:空库直接建立最终表;安全且意图唯一的变化自动应用;收紧长度、精度或约束前先检查已有数据。失败不会截断、舍入或删除数据。 简单字段重命名使用 `from`: ```dever type User { display_name: Text(64) from nickname } ``` 已有数据库把 `nickname` 改名并保留数据;全新数据库只创建 `display_name`。复杂或破坏性变化使用稳定命名的 `migrate`,例如: ```dever migrate remove_legacy_code { drop legacy_code } ``` 命名迁移成功后只执行一次,已执行内容被修改时拒绝启动。SQLite 需要重建表的变化和 PostgreSQL DDL 都由同一逻辑 schema 计划产生。 需要在结构变化前清理旧数据,或在结构变化后回填数据时,在命名迁移中使用 `before`、`after`: ```dever migrate normalize_display_name { before { sqlite = "UPDATE user SET display_name = ?1 WHERE display_name = ?2" postgres = "UPDATE user SET display_name = $1 WHERE display_name = $2" parameters = ["Editor", "Legacy editor"] } after { sqlite = "UPDATE user SET display_name = ?1 WHERE display_name = ?2" postgres = "UPDATE user SET display_name = $1 WHERE display_name = $2" parameters = ["Administrator", "Admin"] } } ``` 每块必须声明两个方言和参数列表,只接受一条 `INSERT`、`UPDATE` 或 `DELETE`,不接受多语句、DDL、事务控制和保留的内部表名。值通过参数绑定;参数支持 Bool、Int、Text、null 和指数形式 Float,无类型上下文的小数不隐式转换成 Float。参数序号必须连续且覆盖参数列表。 已有表先验证 catalog 和历史,再按源码顺序执行未应用迁移的全部 `before`、自动结构变更、全部 `after`,最后执行 Seed 并记录历史。任一步失败都会回滚该 Model 的数据、结构和历史。全新数据库直接建立最终结构,不执行历史数据变换。已应用迁移的内容和顺序不能改变;变换只能引用当时已存在的表,不保证跨 Model 原子升级。 初始化数据直接写在 Model 文件的无版本 `seed` 块中: ```dever seed { { email = "admin@example.com" display_name = "Administrator" status = UserStatus.Active } } ``` 每条 Seed 必须完整提供一个唯一键。ORM 只插入缺失记录,不覆盖或删除线上数据;修改既有初始化数据使用命名迁移。schema、迁移和 Seed 在同一连接的事务与启动锁内执行。 ### 13.6 Typed native SQL 内置查询无法清楚表达的复杂读取可以在 Model 文件中声明静态、参数化的原生 SQL: ```dever type UserSummary { status: UserStatus total: Int } sql summary(status: UserStatus) (summary: UserSummary?) { sqlite = "SELECT status, COUNT(*) FROM user WHERE status = ?1 GROUP BY status" postgres = "SELECT status, COUNT(*) FROM user WHERE status = $1 GROUP BY status" } ``` 两种方言都必须是 Text 字面量。参数按声明顺序绑定,禁止字符串插值、动态列名和动态行 Map。结果可以是具体 Model、同文件中仅供 SQL 使用的私有 record,或同领域 App 定义的 View,并支持可空值和 List。App View 用 `app.ArticleView` 引用,字段必须公开且为支持的存储标量;不能包含 Secret、嵌套 Model,也不能引用其他领域的 View。SQL 结果普通 record 不接受字段范围约束,数据库解码不能证明这些约束。调用形式与内置操作一致,例如 `model.summary(status)`,并复用当前事务、连接池、statement cache 和具体行 decoder。 GET 允许两种方言均被证明只读的简单自有表投影:`SELECT 字段 FROM 自有表`,可带字段与编号参数的等值 `WHERE`(以 `AND` 连接)、自有字段 `ORDER BY` 和编号参数 `LIMIT`。函数、CTE、子查询、其他表或未识别的 SQL 仍按可能写入处理,不能绑定 GET;不会仅凭 `SELECT` 前缀放行。 ### 13.7 `setting.json` 数据库配置直接放在 `database` 对象中: ```json { "database": { "default": { "type": "sqlite", "path": "data/db/app.db", "max_connections": 1, "max_page_size": 100 }, "report": { "type": "postgres", "url": "postgres://user:password@db.example.com/report", "tls": "system", "min_connections": 0, "max_connections": 4, "max_page_size": 100, "wait_timeout_ms": 5000, "io_timeout_ms": 30000 } } } ``` `database.default` 必须存在。PostgreSQL 的 `tls` 必须明确为 `system` 或 `disabled`,证书校验失败不会自动降级明文;`wait_timeout_ms` 限制连接池等待,`io_timeout_ms` 独立限制 SQL、事务和迁移 I/O。两者省略时分别为 5000 和 30000 毫秒,可配置范围都是 1–300000。启动时创建 `data/db`、`data/upload`、`data/log`、`data/cache` 和 `data/tmp`;配置缺失或无效时直接失败,不生成默认配置。 ## 14. 文件、TCP 与 Stream 文件和 TCP 操作成功时返回资源或数据状态,错误默认传播。`dever.io.open/create` 返回 File;`read` 返回 `ReadState.Read(bytes)` 或 `End`;write 和资源 close 成功无输出。用 `result(call)` 捕获时,分别使用 OpenResult、ReadResult、WriteResult、CloseResult。读取可能短读;文件 `create` 只创建新文件,不覆盖已有文件。 ```dever type CountResult { Counted(total: Int) error Failed(message: Text) } sum_byte(value: Int, total: Int) (next: Int) { next = total + value } consume(event: dever.io.ReadEvent.Chunk(bytes), total: CountResult) (next: CountResult) { next = add_chunk(total, bytes) } consume(event: dever.io.ReadEvent.Failed(message), total: CountResult) (next: CountResult) { next = CountResult.Failed(message) } add_chunk(total: CountResult.Counted(value), bytes: Bytes) (next: CountResult) { next = CountResult.Counted(reduce(sum_byte, bytes, value)) } add_chunk(total: CountResult.Failed(message), bytes: Bytes) (next: CountResult) { next = CountResult.Failed(message) } streaming(result: dever.io.ReadStreamResult.Streaming(stream)) (total: CountResult) { total = reduce(consume, stream, CountResult.Counted(0)) close(stream) } streaming(result: dever.io.ReadStreamResult.Failed(message)) (total: CountResult) { total = CountResult.Failed(message) } opened(result: dever.io.OpenResult.Opened(file)) (total: CountResult) { total = streaming(dever.io.chunks(file, 4096)) } opened(result: dever.io.OpenResult.Failed(message)) (total: CountResult) { total = CountResult.Failed(message) } ``` `dever.io.chunks(file, limit)` 返回同步 Stream;`dever.net.chunks(socket, limit)` 返回 AsyncStream。显式捕获创建失败时分别使用 `dever.io.ReadStreamResult`、`dever.net.ReadStreamResult`。两种流都产生 `dever.io.ReadEvent.Chunk`;创建时 limit 非正数或源已关闭会直接失败。`dever.net.connections(listener)` 直接返回 `AsyncStream`。 关闭 File、Socket 或 Listener 的任一别名后,其他别名上的操作会返回失败。`close(stream)` 是幂等的零输出核心操作:它关闭该 Stream 的全部别名并释放 Stream 持有的源别名,但不会关闭调用方另外持有的 File 或 Socket。Stream 正常结束后所有别名都只会继续得到结束;终止性读取错误最多产生一个 `ReadEvent.Failed`,然后结束。资源最后一个别名离开作用域时会被释放;需要确定关闭结果时仍应调用对应 package 的 close 并处理它的选择型结果。 TCP 的 `connect`、`connect_timeout`、`listen`、`accept`、`read`、`write` 都会被推断为挂起;直接调用时顺序等待,需要并发时使用 `run(...)`。`port`、`timeout`、`close`、`close_listener`、`chunks`、`connections` 不挂起。所有网络等待复用程序的 Tokio runtime;Socket 支持同时读写,每个方向按操作串行,关闭会唤醒等待方。 `connect` 支持主机名,DNS 使用共享有界 blocking 通道,没有连接超时承诺。`connect_timeout` 要求数值 IPv4/IPv6 和正毫秒数。`timeout(socket, milliseconds)` 设置随后开始的每次读写操作的期限,包括等待同方向锁及全部写入;读超时不消费后续数据。一次写入已经写出部分字节后,超时、故障或取消会关闭 Socket,绝不自动重发整条消息。尚未写出字节的取消不关闭 Socket。文件操作仍是同步边界,在挂起调用链中使用 `blocking` 隔离。 关闭 AsyncStream 会结束所有流别名并释放生产者。它不会关闭调用方另外持有的 Socket 或 Listener;需要同时结束这些别名时显式调用 `dever.net.close` 或 `close_listener`。停止持有唯一资源别名的消费任务后,该资源随任务清理释放。 ## 15. 完整可运行示例 完整业务示例是 [CMS](examples/README.md),同时维护普通源码和 Markdown 两套实现。它展示账号、站点、权限、租户隔离、文章编辑与发布、文件上传和定时任务。每套都是独立项目,源码在 `module/`,测试在 `test/`。 在仓库根目录检查两套实现: ```sh dever check examples/cms/dever dever check examples/cms/md dever fmt examples/cms/dever --check dever fmt examples/cms/md --check dever test examples/cms/dever --filter news.article.publish dever test examples/cms/md --filter news.article.publish ``` 这些测试使用临时 SQLite,不连接部署数据库。实际运行 CMS 会使用部署配置和数据,先按示例说明完成密钥、账号、租户数据库和 Owner 配置。 只学习某项能力时,用随 skill 提供的独立小项目: | 场景 | 项目 | | --- | --- | | HTTP、自动 REST、身份核验 | [http-rest](skills/assets/examples/http-rest/) | | 持久 Job、事务入队、虚拟时钟测试 | [jobs](skills/assets/examples/jobs/) | | Python、JavaScript、Go 接入与 Port fake | [external-lib](skills/assets/examples/external-lib/) | 各项目的命令和验证范围见 [示例指引](skills/references/examples.md)。新建最小项目使用 `dever new`,不从历史代码猜语法。 `examples/old/` 只保存历史语言回归材料,不是当前应用模板,不能直接按应用执行 `check/run/build`。其中的 `main.dever`、`package` 和旧可见性写法不用于新项目。 ## 16. 语言边界与平台支持 ### 不提供的语法 Dever 用分句、类型化失败和集合操作表达控制流程。以下写法不属于当前语言,检查时会明确报错: - `if`/`else`、`switch`、`match` 和条件表达式;条件永久使用 function 分句。 - `for`、`while`、`break`、`continue` 和用户递归;重复使用集合/Stream 组合器。 - class、继承、interface、method、annotation、macro、unsafe 和进程内裸指针 FFI;受管 C ABI 通过隔离的 `external exec` Worker 提供,不新增业务源码中的指针类型。 - lambda、闭包、一般化一等 function、用户泛型 type/function。 - exception、`throw` 和 `catch`;可恢复错误使用推导式默认传播、`result(call)` 和 `fail(...)`。 ### 当前能力边界 Linux x86_64 已提供签名安装包、机器共享编译服务和配套 skill,也能生成 Linux ARM64 应用。API/CMD、Model、权限、数据库租户隔离、Job、应用测试和外部 Lib 都使用同一套语言合同;运行、构建不隐式下载依赖。 当前限制如下: - 通用动态 Json 值、Set 和 BigInt;持久化 JSON 使用 Model 的 `Json` 字段,普通 JSON 处理使用 `dever.json.Document`。 - CONNECT、非空 trailer、服务器推送、WebSocket-over-HTTP/2,以及 WebSocket 子协议、压缩扩展和关闭帧元数据。 - table/field 多租户隔离模式,以及按租户业务配置动态选择已编译 Adapter;当前只支持 database 隔离和进程级 Adapter 选择。 - 跨 GPU 厂商的设备验收、macOS/Windows 受管外部程序和完整宿主发行;当前也不提供 ARM64 宿主编译器安装包。GPU 支持不包含驱动安装、设备调度、模型分发或自动放宽容器权限。 - 编译器自举。 未支持的能力明确失败,不使用默认值伪装支持。具体安装与执行条件见 [安装指引](skills/references/install.md)。 ## 17. 开发建议 1. 先把领域状态建模为字段型和选择型 type。 2. 函数按完整业务职责命名,分支写成穷尽、互斥的分句。需要表达独立规则或复用时再抽取,不机械拆成碎片函数。 3. 需要重复时选择 `each`、`filter`、`find`、`sum`、`reduce` 或 `reduce_until`;只有副作用可并行且顺序不重要时使用 `parallel_each`。 4. 用可空值表达简单缺失;需要区分多种失败原因时定义选择型结果。 5. 在系统边界保留 Bytes、资源和显式失败,不隐式吞掉 I/O、JSON 或 HTTP 错误。 6. 每次修改后先执行 `fmt --check` 和 `check`,再运行或构建入口。 ## 18. 编译器约束与公开接口 普通字段型 type 可以直接构造,无需为每个数据类型增加工厂。需要数值不变量时,把范围写在字段上: ```dever type Port { value: Int >= 1 and <= 65535 } default_port() (port: Port) pure { port = Port { value = 8080 } } ``` `Port { value = 0 }` 编译失败。未知 `Int` 也不能直接放入该字段,先用已有范围分句验证;验证成功分句中可以直接构造。范围同样适用于 choice 载荷、命名输出和 handler 签名中的输入/输出。仅支持 Int、Decimal 的数值范围,不支持任意用户谓词。赋值、字段修改和返回必须继续满足范围。编译器保留复制前的事实,修改原值不会改变副本的事实;遇到不能证明的运算不会猜测安全或自动插入默认值。 真正需要隐藏表示时,使用 `private` 字段: ```dever type Session { private key: Text label: Text } ``` 当 Session 声明在 App 中时,其他领域可以使用它,但不能读取、修改 key,也不能构造含私有字段的 Session;普通 record 的 private 仍属于声明 package。`private` 不适用于 choice 载荷。不要给普通数据全部加 private,否则会人为增加构造和读取包装函数。它是源码访问边界,不是敏感数据加密机制。 Model 可以声明 `private password_hash: Text(255)`。只有同 component/domain 的 App 能读写私有字段,生成 CRUD 正常持久化;增加 private 本身不改变数据库列或迁移 fingerprint。含 private 字段的完整 Model、Page、Cursor、Related 及其嵌套容器不能穿过 App 参数/输出、API JSON、错误载荷或入口渲染。App 返回显式 View;循环 Model 关联不会绕过检查。普通 record 的 private 语义保持不变。 `Secret` 是独立内置标量,可以传参、赋值和存入 record/容器,但不可比较、拼接、渲染、普通 JSON 输出、作为 Map key、Model 列或错误载荷。App 可接收 Secret,不能输出它。检查器拒绝嵌套泄漏;原生运行时 Debug 固定显示脱敏占位,持有的字节在释放时清零。不提供 Secret-to-Text/Bytes 通用转换。 `dever.crypto.token(bytes)` 从操作系统生成 16–1024 字节的 Secret;`password_hash(Secret)` 返回 Argon2id v19 PHC Text,固定内存 19456 KiB、2 轮、1 lane、32 字节结果;`password_verify(Secret, Text)` 对错误密码返回 false,对损坏/超限 PHC 返回类型化失败。验证先检查资源上限,再执行哈希。token/hash/verify 失败分别使用 TokenResult/HashResult/VerifyResult,成功分支 Ready,失败分支 Failed(message: Text),错误不携带输入。哈希和验证是阻塞操作,挂起调用链使用已有 `blocking(...)` 边界。 `dever.crypto.sha256(Bytes)` 与 `hmac_sha256(Secret, Bytes)` 返回 Bytes;`constant_time_eq(Bytes, Bytes)` 对相同长度内容进行常量时间比较,长度不是秘密。仅 compiler-owned Test source 提供 `secret(Text)` 固定输入构造,生产源码没有该入口。所有部署配置仍只来自 `config/setting.json`。 `pure` 写在函数输出声明后,禁止文件、网络、时钟、进程参数读取、安全随机源、标准输出、Stream 操作和并发等效果。检查沿函数调用和具名 handler 传递;调用具体纯 handler 可以通过,声明为 pure 的泛化函数不能调用未限定效果的 handler。纯函数仍可能产生算术 fault,`pure` 不承诺不会失败。 领域之间不声明依赖清单。源码通过 App 名称引用跨领域类型、函数和 handler;编译器根据这些真实引用自动建立依赖图并检查循环。新增或删除引用只修改实际使用位置,不需要同步维护另一份声明。 主动恢复的例子: ```dever configured_or_default(result: domain.PortResult.Ready(port)) (value: domain.Port) pure recover("optional local configuration uses port 8080") { value = port } configured_or_default(result: domain.PortResult.Failed(message)) (value: domain.Port) pure recover("optional local configuration uses port 8080") { value = domain.Port { value = 8080 } } ``` 例中的 `domain.Port` 是包含 `value: Int >= 1 and <= 65535` 的 record;`domain.PortResult` 有 `Ready(port: Port)` 和 `error Failed(message: Text)` 两个分支。`recover` 允许这个函数结束时消费失败义务,应放在实际决定恢复策略的函数上,不能为通过编译给全部函数加同一个声明。 失败义务会跟随 record、choice 载荷和集合。未知集合的局部查找不能证明剩余元素已处理;可能覆盖旧值的 Map 更新也不能静默丢掉旧失败。可用完整遍历处理元素、继续返回原集合,或在真实恢复边界声明策略。空遍历不能消费没有传给 handler 的错误上下文,短路跳过的调用也不能算处理。分析是保守的,未实现任意程序的语义等价证明。 应用接口快照自动收录 App 能力、App 合同类型以及 Model 合同;topic 文件名和其他角色的私有实现不会进入快照。通过 API 基线约束后续变化: ```bash dever api my_app --output my_app/dever.api dever check my_app ``` 首次生成需要新路径;已存在的文件不会覆盖。`check`、`run`、`build` 自动检查项目根已有的 `dever.api`,变化时打印增删项并失败。快照记录公开名称、类型、可见字段、构造边界、约束、效果和恢复策略。正常修改私有实现不扩大公开接口。更新基线需要单独生成新文件并审查差异,不会自动“修复”基线。基线必须由项目审查或权限保护;如果允许 AI 同时任意修改源码和基线,它就不是授权边界。未提供基线时不会限制接口变化。 `check`、`run`、`build` 共用应用硬检查 C016:拒绝从生产入口不可达的 App/Domain 函数,以及符合现有判定条件的私有空转发和 Model CRUD 包装。对于生产入口可达的 App/Domain,还拒绝同一源码文件内契约和实现完全相同、没有副作用(包括内部调用)的私有函数,以及参数未变且前一次结果仍可复用的重复纯调用。有副作用、契约不同、参数或前一次结果已被修改的情况不按这两条重复规则拒绝。检查先于 Worker 和运行库准备,`check` 保持静态、离线。`test` 按自己的测试入口检查,不套用生产应用硬检查,测试调用不能使生产死代码通过 `check`。 编译器还会建议清理有证据的重复范围判断(W002)、相同私有实现(W003)、可复用的纯调用(W004)和没有增加契约的私有转发(W005)。未使用赋值沿用 W001,不叠加第二套规则。指定应用入口时,W007 提醒没有外部使用需求的公开函数;普通库检查不推测未知调用方。这些警告本身不改变执行、不作为硬错误;上述应用检查会另外拒绝符合 C016 判定条件的重复和转发,普通库和测试保留警告。CLI 已用相同主位置和关联源码证据报 C016 时,不再重复显示对应 W003/W004/W005;其他警告保留。检查不按行数或函数个数强迫抽象。官方源码执行同样的契约检查,CLI 默认只展示当前用户源码的建议;官方硬错误不会隐藏。 契约分析有明确的工作上限(当前每轮相应证明最多 16,384 个工作单元,名义类型嵌套最多 128 层)。超大嵌套失败结构或事实证明超过上限时报 C015,不会跳过检查后放行。共享结构使用缓存与写时复制,普通重复引用不会直接展开成指数数量的副本。 这些约束能拒绝确定的违规,并提示部分冗余,不能证明一个功能有业务价值,也不能完全杜绝过度设计。