Files
core/LANGUAGE.md
T
2026-11-03 03:32:07 +00:00

26 KiB
Raw Blame History

Dever 语言规范

Dever 使用静态类型、命名输出和互斥分句。应用由 API、CMD、Job 声明入口,编译为原生程序。

本文代码块是语法片段,放入对应角色的 .dever 文件。完整项目见 示例。

项目与命名

config/setting.json
module/<组件>/<领域>/<角色>.dever
test/<组件>/<领域>/<主题>.dever
data/

配置来自 config/setting.json。run 读取项目配置,独立程序读取可执行文件旁的 config/setting.json。

名称 格式
组件、领域、主题、字段、局部值 ASCII lower_snake_case
函数、SQL 方法、标准库方法 ASCII lowerCamelCase
类型、choice 分支 UpperCamelCase

源码使用 UTF-8 和 .dever 后缀,不写分号。说明放领域 README 和 doc,Markdown 不参与编译。词法支持 # 行注释,项目源码不写说明注释;// 表示整数除法,不支持块注释。

角色 职责与可见性
app 公开领域能力;可调用本领域 Domain、Model、Port 和其他领域 App
domain 本领域业务规则;不能调用其他领域或执行外部 I/O
model 字段、查询和迁移,只由本领域 App 操作
api 绑定本领域 App,或声明自动 REST
port 外部操作的输入、输出和错误
adapter 实现 Port,可调用本文件函数和标准能力
job 绑定本领域 App 的持久任务

应用不声明 package、import、exposes、internal 或 main。App 函数按 组件.领域.函数 调用;同组件可省略组件,同领域使用 app.函数。跨领域只访问 App,公开参数和输出不能暴露私有类型。领域之间不得循环依赖。

角色可使用 app/publish.dever 等主题文件,主题名不进入调用名,目录允许一个文件。App/Domain/Port/Adapter/Job 的主文件与目录二选一;Model/API 主文件可与目录共存。只有 API 目录可递归,其路径参与 URL。目录不能命名为 common/shared/utils/helper/base/service。

类型与值

类型 语义
Bool true / false
Int 有符号 64 位整数,检查溢出
Decimal decimal128,最多 34 位有效数字,银行家舍入
Float IEEE 754 binary64
Text / Bytes UTF-8 文本 / 字节序列
Id / Uuid 不透明标识 / UUID
DateTime / Date / Time / Duration UTC 时间点、日期、日内时间、时长;毫秒精度
Json 不透明 JSON 值,保留数字文本
Secret 仅在规定的输入、密码和摘要操作中使用的敏感值
T? T 或 null
List / Map<K,V> 同类型列表 / 保留插入顺序的映射

整数字面量默认 Int,小数字面量默认 Decimal;明确 Float 上下文可接收小数。科学计数形式为 Float。Decimal 与 Float 不隐式混合。Text 支持反斜杠、引号、换行、回车、Tab 和 \u{HEX} 转义。

字段型 type:

type User {
  name: Text
  phone: Text?
}

rename(user: User, name: Text) (result: User) {
  result = user
  result.name = name
}

构造使用 User { name = "Lin" phone = null },必须一次给全字段。字段型、choice、List、Map 使用值语义,修改局部副本不改变原值。

选择型 type:

type State {
  Draft
  Published
  Rejected(reason: Text)
}

构造为 State.Published 或 State.Rejected("原因"),载荷按位置传入。

List 写为 [1, 2],Map 写为 {"name" = "Lin"}。空集合需由上下文确定类型。Map 键允许 Text、Int、Bool、Id 和无载荷 choice。Map 相等比较包含插入顺序;List 按位置比较。Float 的 NaN 与自身不相等。

函数与分句

greet(name: Text) (message: Text) {
  message = "Hello, " + name
}

函数格式为 名称(输入) (命名输出),空输入或输出仍写 ()。调用按位置传参;单输出直接得到值,多输出按名称访问。输出必须在正常结束前赋值。

= 可初始化或修改当前函数的参数、局部值和输出;变量类型保持不变,读取前须初始化。字段只能在完整值上读取或修改。

局部值首次声明可写 input: CreateInput = dever.json.decode(payload),为解码、空集合或 null 提供具体类型;类型省略时仍由表达式推断。后续赋值只写 input = ...,不重复声明类型,也不改变原类型。

同名、同参数数量的分句须相邻,输出类型和错误声明一致。分句互不重叠并覆盖全部输入,没有先后优先级:

label(state: State.Published) (text: Text) {
  text = "published"
}

label(state: State.Rejected(reason)) (text: Text) {
  text = reason
}

label(state: other) (text: Text) {
  text = "draft"
}

模式支持类型、choice 分支、null、常量、Int/Decimal 范围和 other。other 是该参数位置未被明确模式覆盖的补集;多个参数按全部组合检查。choice 载荷按位置绑定,不使用的载荷可写 _。

可空值用 value: Text 与 value: null 两个分句处理。范围可写 score: Int >= 60 and < 90。没有 if/else、switch/match、return、条件表达式、用户递归、lambda、运行时闭包或用户泛型。

运算与范围

运算 规则
+ - * 同类数值;Int 可提升为 Decimal;Text 支持 +
/ Int/Int 和 Decimal 运算返回 Decimal;Float/Float 返回 Float
// % 仅 Int,整数除法向零截断
== != 同类型且可比较的值
< <= > >= Int、Decimal、Float
and or not Bool;and/or 短路

优先级依次为调用/字段、一元运算、乘除取余、加减、比较、and、or。同级从左到右。

字段、choice 载荷、函数及 handler 参数都可声明 Int、Decimal 范围:

type Port {
  value: Int >= 1 and <= 65535
}

构造、赋值和返回都须满足范围。未知 Int 先经范围分句确认;不支持任意用户谓词。静态可知的数值故障在检查时拒绝,运行时故障报告源码位置并终止当前执行。

集合与 handler

使用具名函数处理集合,不提供 for/while、break/continue。

操作 行为
each(handler, values[, context]) 零输出时遍历;单输出时生成 List
filter(handler, values[, context]) Bool handler 筛选 List
find(handler, values[, context]) 返回 List 的首个匹配项或 null
sum(values) / sum(handler, values[, context]) 数值 List 求和
reduce(handler, values, initial) handler 输入为元素、当前状态,返回下一状态
reduceUntil(handler, values, initial) 另输出 stop: Bool,处理当前项后停止
parallelEach(handler, values, limit[, context]) 并行执行零输出 handler,返回前收尾全部工作
contains(values, target) 判断 List 中是否存在相等项,直接返回 Bool

List/Bytes 支持 each、reduce、reduceUntil;Bytes 元素是 0–255 的 Int。Stream/AsyncStream 的 each 只接受零输出 handler。filter/find/sum 只处理 List。除 parallelEach 外按顺序处理,空集合求和为零,reduce 返回初始状态。

context 最多一个,需要多项数据时用 record;reduce 的共享数据放 State。parallelEach 的非挂起并发数为 1–256,挂起任务容量为 1–65536,并受运行时容量限制。同步 Stream 不接受挂起 handler。

handler 是签名匹配的具名函数或 handler 参数,不是可存储、比较或返回的运行时值:

transformAll(
  transform: handler(value: Int) (result: Text),
  values: List<Int>
) (results: List<Text>) {
  results = each(transform, values)
}

append、put、remove 返回新集合;get、first、find 返回可空值。entries 返回有序 MapEntry 列表。length 接受 List、Map、Text、Bytes,Text 按 Unicode 码点计数。

失败、纯函数与敏感值

choice 的错误分支显式标记 error:

type DeliveryError {
  error Unavailable(message: Text)
}

普通调用返回成功值,失败自动向调用方传播。fail(DeliveryError.Unavailable("原因")) 主动失败;result(call) 捕获后交给覆盖所有结果的分句处理。

捕获按错误类型及变体身份匹配,不按消息或载荷形状猜测。结果 Choice 的 error 分支可包装原错误 Choice,实际构造的仍是原失败变体;原错误 Choice 含有成功分支不妨碍捕获。参数求值的失败继续向外传播。

错误必须传播、返回或明确恢复。读取消息、记录日志、覆盖变量不算处理;恢复函数写 recover("具体原因"),全部分句理由一致。失败义务跟随 record、choice 和集合,局部查找不能代表处理了其余错误。程序故障不属于可捕获业务失败。

pure 写在输出声明后,禁止 I/O、时钟、随机源、并发等效果;检查沿调用链和 handler 传播。pure 函数仍可能有数值故障。

private 字段只能由所属源码边界访问,含私有字段的值不能直接用于公开 wire 输出。Model 私有字段仅供同领域 App 操作,跨边界返回显式 View。

Secret 不可比较、打印、拼接、转为普通文本、作为 Model 列或输出。密码使用 dever.crypto;哈希和验证属于阻塞操作。测试源可用 secret(Text) 构造固定输入,生产源码没有此能力。

并发与资源

挂起由调用图推断,不写 async/await。普通调用等待完成,显式 run 才启动子任务。

操作 合同
run(call) 启动 Task
wait(task) 等待并消费 Task
stop(task) 请求取消并等待退出;仅接受零输出 Task
timeout(task, ms, fallback) 到期先清理任务,再调用输出签名相同的零输入 fallback
race(task, task, ...) 取首个完成结果,清理其余任务;输出签名须相同
group(limit) / run(group, call) 有界零输出任务组,满时等待;最终 wait/stop
parallel(pure_call) 在线程池执行非挂起纯计算
blocking(call) 在线程池执行明确的非挂起阻塞调用
channel(T, capacity) 有界通道;send/receive 挂起,close 唤醒等待者

Task/Group 不能复制、嵌套、传参或逃逸,必须消费一次。异常路径也会收尾子任务;取消不撤销已经发生的外部副作用。程序故障不会变成 timeout 的正常 fallback。

Group、Channel 容量为 1–65536。Channel 关闭后仍可读完缓存,之后 receive 返回 null。Channel 元素不能为可空类型;需要传递空值时,用含可空字段的消息 Choice 区分消息与关闭。

跨线程或挂起点存活的普通值须可传递。挂起调用链中的同步文件、睡眠、密码计算等操作用 blocking;网络等待和 dever.task.sleep 自动挂起。

File、Socket、Listener、Stream、AsyncStream 等资源的副本共享状态。普通数据的值语义不适用于这些句柄。

  • Stream 按需单次消费,别名共享读取位置;reduceUntil 停止后可继续读取同步 Stream。
  • AsyncStream 的消费结束、提前停止、故障或取消会关闭流;别名不能继续消费。
  • stream(channel/list/bytes) 创建 AsyncStream;dever.task.ticks(ms) 创建周期流。
  • 关闭流释放其持有的资源别名,不关闭调用者另外持有的 File、Socket 或 Channel。

API、身份与权限

API 文件只声明绑定:

get profile = app.profile
public post login = app.login
authenticated get current = app.current
long post generate = app.generate
cmd export = app.export
rest model

HTTP 支持 get/post/put/delete。URL 为组件、领域、API 主题路径和 action;CMD 为 组件.领域.命令。API 绑定本领域 App 的一个可序列化输出。

GET/DELETE 从 query 读取标量;POST/PUT 从 JSON 对象读取;CMD 接受单个 JSON 对象。未知、重复和缺少必填字段失败。成功包裹为 {"code":0,"message":"ok","data":...},失败的 data 为 null。dever.api.Error 映射 400/401/403/404/409/429,其他内部失败返回安全的 500。

声明 访问要求
默认 HTTP 可信身份与精确角色权限
public HTTP 匿名入口
authenticated HTTP 可信身份;对象所有权与业务权限由 App 检查

public/authenticated 不用于 CMD 或 REST。权限键为 component.domain.site.action,REST 的 action 为 read/create/replace/delete。HTTP 方法不进入权限键。

sites.<key>.path 匹配 API 父目录,每条接口须恰好匹配一个站点。例如 admin 匹配 api/admin/**,不匹配 api/admin.dever,也不改写 URL。

认证 Provider 绑定项目的 verify App。它接收 dever.auth.Claims,返回仅含 id: Text、user_id: UserModelId?、tenant_id: TenantModelId? 的身份 record。verify 校验账号、会话、租户和成员关系,只读 global Model,不返回权限列表。

身份读取使用 dever.auth 和 dever.site,ID 类型须匹配 verify。客户端参数不能建立身份。public、CMD、Job 和普通测试不能直接读取 HTTP 身份。

角色与权限由核心管理。授权读取允许受保护或 authenticated 入口;修改只允许普通受保护写入口。Owner 为受保护角色,不按特定用户 ID 判断。业务 capability 在可信租户和入口命名空间内注册,撤销同时清理 grants,重新注册不恢复授权。

HTTP 元数据用 dever.api 读取和设置。敏感头不能读作普通 Text;Cookie 登录/退出用 issueCookie/clearCookie。Cookie 写入及 Cookie 认证的写请求要求 Origin、Host 精确匹配站点 origin。响应元数据仅在处理与编码成功后发布。

Upload 仅用于 POST 的顶层 multipart 参数,不能复制、嵌套、输出或进入 CMD。显式关闭,或一次性传给本领域 Port,由 Adapter 使用 storage;失败和取消清理临时文件。

完整配置与调用边界见 应用指引和 签名索引。

Model 与查询

主文件的记录名对应领域,主题文件对应主题名:

type Article {
  title: Text(1, 200)
  body: Text
  published: Bool default false index
}

自动生成只读 id 和 created_at。每个 Model 的 id 是不同的名义类型,不能用 Int 或其他 Model 的 ID 替代。

字段支持存储标量、无载荷 choice、Model ID 及可空形式。Text/Bytes 可指定长度,Decimal(P,S) 指定精度;generated 用于非空 Uuid。default 为静态值,index/unique 可用于单字段或顶层复合索引。外键自动建立索引,不自动加载关联。结构化数据显式使用 Json。

Model 的 Text 字段写入时默认去除首尾 Unicode 空白,再检查长度;不处理内部空白。raw 保留原文,lower/upper 在去除首尾空白后转换大小写,三者互斥。可空字段的 null 保持不变,空串不转为 null。普通函数中的 Text、查询条件、读取结果和原生 SQL 不自动转换。

type Category {
  name: Text(1, 80)
  code: Text(1, 40) lower unique
  description: Text?
  content: Text raw
  active: Bool default true
}

create、update、条件更新、createMany、upsert 和 REST 使用同一写入规则。default、Seed 在编译期按该规则处理。富文本、代码、签名原文等需要逐字保留的字段使用 raw;不会依据字段名称猜测策略。去除空白不负责 HTML 安全过滤。

同领域 App 使用 model.create/createMany/get/first/list/cursor/count/exists/stream/update/delete/upsert。多 Model 使用 model.topic.operation。查询块只出现在调用位置,不是可保存的动态 Map:

page = model.list({
  where = published == true
  order = [created_at.desc, id.desc]
  page = 1
  size = 20
})

where 支持布尔组合、比较、null、in、between、contains、startsWith、endsWith;结构静态确定,值参数绑定。list 返回 items/page/size/total/pages;cursor 返回 items/next/has_more,不做 count。max_page_size 限制结果规模。

update(id, values) 返回记录,不存在时失败;条件 update/delete 返回影响行数,必须明确 where。delete(id) 返回影响行数。关系用 with 显式加载,返回 Related 的 Unloaded/Loaded 状态;仅 first/list/cursor 支持一层关系。

REST 字段

create/replace/search 表达式写在 Model 字段上:引用 input 时转换客户端值,否则由服务端提供并拒绝客户端同名输入。表达式只允许纯计算及指定的只读请求能力。search 最多一个,不能叫 page/size。

private owner user_id: ... = dever.auth.userId() 创建时绑定可信用户,全部读写 SQL 均包含 owner 条件。private 字段不出现在 REST 输出;没有 replace 来源时 PUT 保留其原值。

普通 CRUD 直接使用 rest model;关联编排、状态转换或不同输出合同由 App 实现。

事务

transaction publish(id: model.id) (article: model.Article) {
  article = model.update(id, { published = true })
}

普通写 HTTP、CMD、Job 建立整体事务,成功编码后提交。嵌套 transaction 复用同一连接的事务;失败或取消回滚后传播原错误。GET 不写库或入队。

long 入口不建立整体事务,写库和入队必须进入显式 transaction。外部 I/O、Worker、等待、并发和密码计算放在事务外,写入依赖的状态在事务内重新确认。long 已提交阶段不会因后续失败撤销。

事务不能跨物理数据库或混用 global/tenant 存储,不暴露 begin/commit/rollback。数据库失败保留 dever.database.Error。

数据库、迁移与租户

Model 优先使用组件同名连接,否则 database.default;显式 database report 绑定固定连接,缺失时报错。SQLite/PostgreSQL 配置在 setting.json;PostgreSQL 明确选择 TLS system/disabled。

当前 Model 是目标 schema。空库建最终表;安全变更自动应用,收紧约束先检查数据。字段重命名用 from old_name;破坏性变化用命名 migrate。已执行迁移的内容与顺序不可修改。

migrate 可含 before/after SQL,每块提供 sqlite、postgres 和参数列表,只允许单条参数化 INSERT/UPDATE/DELETE。全新数据库不执行历史数据变换。seed 按唯一键补缺失记录,不覆盖既有数据;迁移原子性限于所属 Model。

复杂查询在 Model 声明 sql name(inputs) (output),两种方言均为静态字符串;结果是具体 Model、私有 SQL record 或本领域 App View。只有可证明只读的 SQL 能用于 GET。

启用 tenant 后,global type 使用控制库,其余 Model/Job 使用租户库。只支持 database 隔离。租户库须先显式迁移;缺失、未就绪或 schema 过期时报错,不回退控制库。

运维入口为 dever tenant migrate、dever tenant owner、dever tenant component enable|disable。数据库权限和真实数据操作由部署侧管理。

Job

database default

job store(input: app.Request) () retry(3) timeout(30000) {
  app.store(input)
}

Job 输入为零个或一个本领域 App record,无输出,只绑定本领域 App。retry 为 1–100 次,包含首次执行;timeout 为 1–3600000 毫秒。定时声明为 schedule name = "0 * * * *",使用 UTC。

App 使用 dever.job.enqueue/enqueueAt,传入任务、可选 payload 和幂等 key。key 为 1–1024 字节;同租户、任务身份和 key 的活跃任务复用 Id。入队与业务写入可共享事务。

任务至少执行一次,外部副作用须业务幂等。受保护 HTTP 入队保存可信身份与精确入口权限键,执行时重验;public/authenticated 不能入队角色权限绑定的任务。CMD、schedule、测试使用 System 身份。身份失效、坏 payload 或不兼容 schema 进入 blocked。

runtime.mode 为 api/worker/all。任务配置包括 workers、poll_ms、lease_ms 和重试间隔;SQLite 的 lease 必须长于 Job timeout,worker 连接池需留出监管连接。停止时先停止接收和 claim,再按 shutdown_ms 排空。

测试使用临时数据库和用例独立时钟,可用 dever.test.advanceClock、drainJobs 验证队列。

Port、Adapter 与依赖

Port 声明输入、命名输出和明确失败集合:

send(message: Text) () fails app.DeliveryError

Adapter 用 port.send(...) (...) { ... } 实现。每个文件完整实现一个 Port;单实现自动绑定,多实现由 setting.json 的 adapter 配置选择。实现只能抛出 Port 声明的错误。Port 不接受 handler 或数值范围参数。

Adapter 可声明 setting,选中的实现从配置读取同类型字段。外部程序用 external command/exec/pip/npm/go;代码可信性、网络、文件和设备权限由运行环境负责,allow 声明不是 OS 沙箱。

方式 用途
external command 普通 ELF;argv/stdin 输入,code/stdout/stderr 输出,无 shell
external exec 遵循 Port Worker 协议的程序
external pip/npm/go 由相应生态实现 Port,使用锁定资源

外部调用和 fake 用法见 依赖、外部库示例。协议与运行资源见 SDK。

Dever Package 是 module 下的可复用组件,使用 dever package add/update/remove/list/doctor/export 管理。dever.lock 固定版本、来源和摘要,不能覆盖本地同名组件。已有锁通过 dever lib install 恢复资源;run/build 消费锁定资产。当前 CLI 不提供 Lib add/update 或 target add。

标准库与数据格式

标准库按包名直接调用,无需 import。功能目录见 标准库,参数和输出见 签名索引。

JSON 数据

API/CMD 根据参数和输出类型编解码。Decimal/Id/Uuid 使用字符串,ModelId 为整数,日期时间使用规范字符串,Duration 为整数毫秒。支持 List、可空、可公开传输的 record 和无载荷、无 error 分支的 Choice。此类 Choice 按声明的分支名编码为 JSON 字符串,也可作 query 或 multipart 标量;未知分支报错。带载荷或 error 分支的 Choice、Map、资源不用于普通 wire。缺少可空字段得到 null,未知、重复和缺少必填字段失败。

已有 Json 需要转换成具体类型时,使用 dever.json.decode(value);目标类型由局部声明、命名输出、字段或参数类型确定。直接从已有原文解码,不转为 Text 或 Document。解码失败传播 dever.json.JsonResult.Failed。Text 不在此处清洗。

需要处理解码失败时,直接写 decoded: DecodeResult = result(dever.json.decode(payload))。DecodeResult 必须有一个含单个目标类型字段的成功分支,以及包装 dever.json.JsonResult 的 error 分支,例如 Parsed(value: CreateInput) 和 error Invalid(cause: dever.json.JsonResult)。不需要另写解码函数;无类型上下文时不会猜测目标类型。

单个布尔条件失败使用 dever.api.require(condition, dever.api.Error.Invalid);false 传播指定错误,true 继续。该函数不代替业务状态分支和执行时的权限校验。

日期时间使用 RFC3339,输出 UTC,最多毫秒精度;Date 为 YYYY-MM-DD,Time 为 HH:MM:SS[.sss]。无效日期、非有限 Float、越界整数失败。

Json 提供 fromText/toText、object/array、propertyValue/setProperty 等操作;encode 按实参具体类型输出 Json。原始数字不经过 Float。Json 限制为 16 MiB、64 层、65536 个值。

需要遍历节点时使用 parse/stringify 和 Document:root 为根 ID,nodes 为连续节点列表,子 ID 小于父 ID。Number 保留原文,Object 保留键顺序。node/property/element 缺失返回 null;JSON null 是实际节点。Document 的解析深度上限为 256,stringify 检查手工节点引用。

文件、HTTP 与流

文件 create 只建新文件,read 可能短读。I/O 错误通过声明失败传播;流的终止错误最多产生一次,之后结束。TCP 支持同时读写,每个方向串行;部分写入后失败关闭连接,不重发整条消息。

HTTP 支持 HTTP/1、HTTP/2、TLS、连接池、流式响应、SSE 与 HTTP/1 WebSocket。普通应用使用 API 声明,底层服务和客户端放 Adapter。

  • Limits 控制头、正文、期限和连接数;HTTP/2 另限制流数与接收窗口。
  • serve/send 缓冲完整正文;serveLive/open/openStream 按块处理。
  • 池和会话归创建它们的任务作用域,响应须消费或关闭后释放名额。
  • TLS 校验证书和名称,不自动降级;HTTP/2 使用 ALPN 或明文 prior knowledge。
  • HTTP 协议头和正文定界由引擎管理;应用不写 Content-Length/Transfer-Encoding/Upgrade。
  • 关闭 Listener 停止接收并排空;stop 取消服务并等待清理。
  • 不支持 CONNECT、非空 trailer、服务器推送、WebSocket-over-HTTP/2、WebSocket 子协议或压缩扩展。

检查、测试与构建

dever new my_app
dever fmt my_app --check
dever check my_app
dever test my_app --filter hello.greeting
dever run my_app -- hello.greeting.greet '{"name":"Dever"}'
dever build my_app --output my_app/program

check 检查源码、配置、类型规则和生产入口可达性,不启动应用。重复实现和业务合理性仍需代码审查。

测试入口把文件主题转成 lowerCamelCase,例如 account_login.dever 对应 accountLogin() (),可调用本领域 App/Domain。外部 Port 使用用例独立 fake;Model 使用临时 SQLite。筛选不跳过源码检查,每例独立进程,默认 60 秒;任一失败返回非零。

dever api <root> --output <新文件> 导出接口快照。已有 dever.api 时,check/run/build 检查基线;变更需审查新快照,工具不自动覆盖。

构建输出须是新文件。生成的程序可脱离开发包运行,仍需配套配置和系统运行库。

安装环境与交叉编译见 安装说明和 编译器开发。