moondmn

    Experimental exact-decimal FEEL and DMN interpreter

    dmn
    feel
    decision
    Download zip
    Version
    0.2.1
    License
    Apache-2.0
    Last updated
    12 hours ago
    Downloads
    56

    #MoonDMN

    MoonDMN 是用 MoonBit 从零写的 OMG DMN 1.5 决策引擎和 FEEL 表达式求值器。 不依赖 JVM,能编译到 native、wasm-gc 和 js,并用官方 DMN TCK 逐例验证。

    English: a from-scratch DMN 1.5 decision engine and FEEL evaluator in MoonBit. It runs without a JVM, compiles to native, wasm-gc and JavaScript, and is checked case by case against the official DMN TCK (Level 2: 116/116, Level 3: 3265/3291).

    #版本对应

    面对应说明
    当前源码0.2.1,待发布包含 Playground 多文件 import 与宿主函数桥;已发布 0.2.0 的源码对应 tag v0.2.0
    mooncakessundaysebasidian-byte/moondmn@0.2.0包内容固定,不随 main 变化
    Pages最近一次推送到 main 的构建pages.yml 每次 push 到 main 都会重新构建,可能领先于 0.2.0

    定位发布提交:git rev-list -n 1 v0.2.0。下面三条命令分别复现三个面:

    # 源码测试 git checkout v0.2.0 moon test --target all # 安装包调用:在仓库外新建模块,把“安装与接入”中的 import 和示例原样放入 cmd/main moon new consumer && cd consumer moon add sundaysebasidian-byte/moondmn@0.2.0 moon run cmd/main # 线上引擎:打开 Pages;对应的构建见 Actions 中 Pages 工作流的运行记录 gh run list --workflow pages.yml --limit 1

    #适用场景

    • 业务规则:审批、定价、风控、资格判定这类需要审计、需要版本管理的决策逻辑。
    • 跨平台嵌入:同一份 DMN 模型可以跑在服务端(native)、浏览器和插件(wasm-gc)、Node 和前端(js)。
    • 规范学习:精确十进制、三值逻辑、时区和区间语义都有可执行的示例和测试。
    • 回归验证:官方 TCK 是端到端回归,语义一改,通过数就会变。

    #Playground

    Playground 是纯静态页面,没有框架也没有 CDN,引擎在浏览器里直接运行。

    • 左:DMN XML 编辑器。可以粘贴、点上传按钮,或把多个文件拖进来;用 + 添加文件,编辑文件名,通过 Entry 选择入口模型。解析错误和 lint 告警列在下方,点一条会定位到对应行。
    • 中:按 InputData 和 ItemDefinition 自动生成的输入表单。支持数字、文本、日期、时间、日期时间、时长、布尔、枚举(allowedValues)、嵌套结构和集合。多文件导入模型使用 JSON 输入,可按别名嵌套传值。
    • 右:结果、决策依赖图(DRG)和决策表。图里点一个决策就求值它;决策表高亮命中的规则行并标出命中策略,点规则行会跳到 XML 对应位置。
    • 顶部的 FEEL 标签页求单行表达式并显示结果类型。右上角的时间输入框是 now()、today() 用的时钟。

    内置贷款审批、保险定价(UNIQUE、COLLECT、PRIORITY、ANY、RULE ORDER、FIRST)、日期与时长计算三个示例,都是自己写的,不含 TCK 文件。

    本地构建和运行:

    scripts/build-web.sh node web/smoke.mjs python3 -m http.server -d web/dist 8000

    然后打开 http://localhost:8000/。scripts/build-web.sh 把 playground 包编译为 js,输出到 web/dist; web/smoke.mjs 直接调用导出函数,对三个示例的结果、命中规则和 lint 做断言。导出函数的参数和返回值见 docs/api.md。 推送到 main 后由 .github/workflows/pages.yml 部署到 GitHub Pages。

    #环境要求

    • MoonBit 工具链 0.1.20261028 或更新版本(moon version --all 查看)。
    • 首次构建会从 mooncakes 拉取 Milky2018/xml 和 moonbitlang/x。
    • 运行 Playground 的冒烟测试需要 Node.js(CI 使用 22)。

    export PATH="$HOME/.moon/bin:$PATH" moon version moon build

    #安装与接入

    在已有 MoonBit 模块中安装:

    moon add sundaysebasidian-byte/moondmn

    从源码构建并运行 CLI:

    git clone https://github.com/sundaysebasidian-byte/moondmn.git cd moondmn moon build moon run cmd/main -- feel 'sum([1, 2, 3]) * 2'

    输出:

    12

    也可以把本仓库放进同一个 workspace 使用。无论哪种方式,都在使用它的包(例如 moon new 生成的 cmd/main/moon.pkg)里添加:

    import { "sundaysebasidian-byte/moondmn", "sundaysebasidian-byte/moondmn/feel/types", }

    之后就能调用 @moondmn.feel_eval、@moondmn.parse_model 等;feel/types 提供错误类型 @types.Failure。完整示例见 examples/api-demo:

    fn main {
    let result = @moondmn.feel_eval("sum([1, 2, 3]) * 2") catch {
    @types.Failure(_, _, _, message) => "error: " + message
    }
    println(result) // 12
    }

    moon run examples/api-demo

    #命令行

    命令输出 JSON 或纯文本。失败时输出 {"error": "...", "message": "..."}(带行列号),退出码为 1。

    #feel:求值表达式

    moon run cmd/main -- feel 'Applicant Age >= 18' --context examples/input.json

    true

    moon run cmd/main -- feel 'list replace([1, 2, 3], 2, 4)'

    [1,4,3]

    now() 和 today() 需要显式给时钟:--now 2024-02-29T23:30:00-05:00。

    #eval:执行模型里的决策

    moon run cmd/main -- eval examples/eligibility.dmn \ --decision Eligible --input examples/input.json

    true

    #lint:静态检查

    检查未定义的引用,以及决策表规则重叠和缺口。

    moon run cmd/main -- lint examples/eligibility.dmn

    []

    #tck:运行官方用例

    ./scripts/fetch-tck.sh moon run cmd/main --target native -- tck .tck/TestCases/compliance-level-2 \ --json evidence/tck-level-2.json moon run cmd/main --target native -- tck .tck/TestCases/compliance-level-3 \ --json evidence/tck-level-3.json

    不加 --json 会把完整报告打印到标准输出:

    moon run cmd/main --target native -- tck .tck/TestCases/compliance-level-3/0068-feel-equality

    #覆盖矩阵

    状态为已支持或明确不支持;TCK 例外见“已知失败”表。

    能力状态说明
    FEEL 精确十进制、三值逻辑与 null 相等已支持34 位、半偶舍入、scale [-6111,6176];sqrt、log、exp 等超越函数经有限精度浮点桥接
    日期、时间、时长、时区已支持,时区数据有限时区内置 IANA 2026e 的 39 个名称、1970–2100 年;范围外返回 E_ZONE_RANGE,未知名称返回 E_ZONE_UNSUPPORTED,DST gap 报错、overlap 取较早时刻
    列表、上下文、区间、过滤器、路径、迭代器已支持多词名称按作用域内已知键识别;比较式区间保留运算符,(=3) 与 [3..3] 不相等
    用户函数、BKM、决策服务、DRG、类型系统已支持类型化参数含单元素列表包装与 date→UTC 午夜转换
    决策表已支持U、A、P、F、R、O、C、C+、C<、C>、C# 全部命中策略
    内置函数已支持(约 90 个)覆盖 §10.3.4 的各分组,TCK 内置函数套件除下表所列外全部通过;matches/replace/split 使用自研 XPath 正则(标志 s m i x q)
    lint已支持未定义引用、重复规则、缺口;输入域为有限枚举(最多 4096 个组合)时可证明完整覆盖,开放域只给提示,不判错
    now() / today()已支持,需注入时钟API 的 clock 参数、CLI 的 --now;引擎不读系统时钟
    多文件 import 与命名空间引用已支持别名、嵌套导入、namespace 隔离、导入类型与 invocable;Playground 支持内存多文件导入;7 个 TCK 用例全部通过。见 API 与示例
    外部函数(Java)已支持宿主函数桥;JVM 调用不支持影响:只有使用 external {java:...} 的模型;18 个 TCK 用例,均调用 JDK 类库。wasm-gc / js 没有 JVM,引擎不模拟 JDK

    #一致性结果

    固定提交 1e349176e5ae689d924a2a2c7a8df25c220d8b07 的官方 TCK,本地实测:

    级别用例数通过通过率
    Level 2116116100.0%
    Level 33291326599.21%

    目标是 Level 2 ≥ 95%、Level 3 ≥ 80%。

    #已知失败(Level 3 剩余 26 个)

    原因用例数性质
    Java 外部函数(0076-feel-external-java)18宿主桥已支持;不调用 JVM、不模拟 JDK,未注册时 E_EXTERNAL_UNBOUND
    驱动不提供时钟(1148、1149)4库已支持注入时钟,TCK 驱动未注入
    小数秒相等(0068)2实现保留小数秒;TCK 期望忽略毫秒,与 DMN 1.5 §10.3.2.3 的文本存在差异
    无日期的命名时区 time 相减(0100)2无日期无法唯一确定 DST 偏移,显式拒绝

    范围内行为不正确的已知缺陷:0 个。这些数字不代表完整 DMN 兼容。 结论、逐套件矩阵、逐例失败原因和复现步骤见 docs/conformance.md,逐例原始报告在 evidence/。

    #和同类实现的区别

    • 和 JVM 引擎(Camunda、Drools、KIE)比:MoonDMN 不依赖 JVM,可以编译到 wasm-gc 和 js;错误以结构化 JSON 返回,方便嵌入多语言服务。
    • 和脚本语言实现比:四则运算和比较用精确十进制而不是浮点,金额计算不会有舍入偏差(sqrt、log、exp 仍经过浮点);三值逻辑、null 相等和区间语义按 DMN 规范实现。
    • 和只覆盖语法的项目比:仓库带 TCK 驱动和逐例报告,结论可以复现、可以审计。

    #开发与测试

    moon fmt --check moon check --deny-warn moon test # native moon test --target wasm-gc moon test --target wasm moon test --target js moon info # 重新生成 .mbti,提交后不应有差异

    每个后端 302 个单元测试,另有 33 项 CLI 进程级检查(python3 scripts/test-cli.py)和 36 项 Playground 冒烟测试。

    CI 在 Ubuntu、macOS、Windows 上执行上述检查,另有单独的 TCK 任务和 Playground 任务, 见 .github/workflows/ci.yml。

    #平台说明

    CLI 的文件 import 需要 native/js 文件加载器。wasm-gc 无文件系统,通过 parse_model 的 resolver 回调传入文件;Playground 的 JS 构建使用内存虚拟文件表,支持多文件 import,不访问宿主文件系统。路径规则与 API 见 接入文档。

    #限制与路线图

    • Java 外部函数仅调用显式注册的宿主回调;不提供 JVM。
    • TCK Level 3 还有 26 个失败,逐例列在 docs/conformance.md。
    • 时区只有 39 个名称,只覆盖 1970–2100 年。
    • now()、today() 没有隐含的系统时钟,必须由调用方注入。
    • 部分隐式转换和函数元数据(0103-feel-is-function)还有差距。
    • 决策表里的 FEEL 每次求值时重新编译,整表预编译还没做;性能数字见 docs/performance.md。
    • Playground 的命中规则只覆盖决策自己的决策表,BKM 内的决策表不显示命中。

    #许可证

    Apache License 2.0,见 LICENSE;上游项目和测试数据来源见 NOTICE。

    compile

    Compile FEEL once; include input names containing spaces in names.

    evaluate_compiled

    Reuse a compiled expression with fresh JSON input and optional clock.

    evaluate_decision

    Evaluate a named decision with exact JSON numbers and an optional clock.

    feel_eval

    Evaluate FEEL against a JSON object. The optional clock supplies now/today.

    parse_model

    Parse DMN 1.3/1.4/1.5 XML; failures carry code, line and column.

    register_external

    Register a callback in a host scope passed to feel_eval/evaluate_decision.

    Source Files