Skip to content

Add math lib 添加数学库 - #122

Merged
PigeonNian merged 12 commits into
Anvil-Dev:dev/1.21.1from
QiuShui1012:math/1.21.1
Sep 16, 2026
Merged

PigeonNian merged 12 commits into
Anvil-Dev:dev/1.21.1from
QiuShui1012:math/1.21.1

Conversation

@QiuShui1012

Copy link
Copy Markdown
Contributor

No description provided.

@Gugle2308

Copy link
Copy Markdown

⚠️ Dangerous command requires approval:

cd /opt/data/workspace/AnvilLib; git fetch origin math/1.21.1 2>&1 | tail -1; echo "=== icon.png ==="; git show FETCH_HEAD:module.math/src/main/resources/icon.png | head -c 40 | od -c | head -3; echo ...

Reason: script execution via -e/-c flag

Reply /approve to execute, /approve session to approve this pattern for the session, /approve always to approve permanently, or /deny to cancel.

@Gugle2308

Copy link
Copy Markdown

代码审查摘要 — PR #122 Add math lib 添加数学库

操作: opened · 范围: 25 个文件(21 Java 新增,23 新文件 / 2358 行 diff)/ 目标分支 dev/1.21.1 · 无删除、无迁移(1.21.1 常规新功能 PR,跳过 26.1 迁移检查)

脚手架部分(build.gradle / gradle.properties / mods.toml / settings.gradle / module.mainjarJar / package-info null 注解)与既有 module.fontmodule.wheelmodule.multiblock 逐行一致 ✅。但表达式回写(encode)路径存在多处「写出的文本无法读回或读回值不同」的问题,而 IExpression.STREAM_CODEC = ByteBufCodecs.fromCodecWithRegistries(IExpression.CODEC) 意味着同一个回写路径同时用于存档与网络同步,数据会静默失真。

取证方式:把 FlatExpressionParser + FlatExpressionWriter 逐行等价转写成 Python 后跑 parse → write → parse 回环:20000 棵随机表达式树中 3478 次回读失败、134 次值改变;仅保留非负/非指数字面量后仍有 964 次值改变(排除已知成因后暴露出的更深层缺陷)。


🔴 关键

1. FlatExpressionWriter.visitBinary — 常量×常量被写成并置乘法,数字连排被当成一个数(值静默改变 / 解析失败)

isLiteralNumber(left) 为真时一律并置,没有排除「右侧也是数字字面量」:

flat 原文本 原值(x=2) 回写文本 回读值
2*3 6 23 23
2*0.5 1 20.5 20.5
0.5*4 2 0.54 0.54
sqrt(2*8) 4 sqrt(28) 5.2915
2*3+1 7 23+1 24
max(2*3,1) 6 max(23,1) 23
2.5*2.5 6.25 2.52.5 回读报错

右侧以数字结尾/开头即触发(splitparseNumber 会把 23 整段吃成一个数)。修法:并置仅在右侧不是数字字面量(且不以数字或负号开头)时使用,否则退回 *

2. 负数字面量(ConstantFunction < 0)在写出时缺少括号保护

parenthesize() 对常量走 Operator.of(function) == null → return false,于是负号被直接拼进表达式:

回写文本 结果
pow(-2, 2) -2^2 4 → -4 ❌(一元负号优先级低于 ^
multiply(2, -1) 2-1 -2 → 1
multiply(-1, x) -1x 值相同但树不同(文档声称同树)

对象形式({"function":…,"arguments":[2,-1]})与程序化构造(ConstantFunction.of(-1))都能造出这种树。修法:对文本以 - 开头的操作数统一加括号((-2)^22*(-1))。

3. number()Double.toString|v| ≥ 1e7|v| < 1e-3 时写出指数形式,而解析器不支持指数记法

parseNumber 只吃 digits[.digits],于是:

flat 原文本 回写文本 结果
0.0001+x 1.0E-4+x 回读报错 expected '(' after function 'E'
10000000+x 1.0E7+x 回读报错 ❌
x*10000000 / sqrt(0.0001) x*1.0E7 / sqrt(1.0E-4) 回读报错 ❌

(顶层纯常量在 Codec.either(DOUBLE, flat) 的第一支就被写成数字,不受影响;嵌套子项全部走 writer。)修法:整数/小数优先用十进制定点输出(如 BigDecimal/自写格式化),并顺手让 parseNumber 支持指数记法,两侧对称。

4. .github/modules.json 未新增 math 条目 —— CI 不会构建/发布该模块

CI(ci.yml / pull_request.yml / release.yml)的构建矩阵完全由 .github/modules.json + generate-matrix.js 驱动,build_and_test.ymlNOT_DEV = secrets.maven_url != ''(正式 CI 为 true)时 module.main 走的是已发布坐标

jarJar(api("dev.anvilcraft.lib:anvillib-math-neoforge-1.21.1:latest.release"))

缺条目 → anvillib-math-neoforge-1.21.1 永不被 publish → main 构建/下游解析直接失败,PR CI 也不会编译这个新模块(math 只在被 main 依赖时才顺带编译)。建议补:

{ "module": "math", "needs": ["util"] }

⚠️ 警告

5. LibFunctionTypesFUNCTION_TYPE 注册命名空间与 javadoc/仓库惯例不一致
LibBuiltInFunctions.TYPE_DFMAIN_ID(→ anvillib:builtin),而 LibFunctionTypes.DFAnvilLibMath.MOD_ID(→ anvillib_math:input|named|constant|custom)。但:

  • ConstantFunction javadoc 写 {"function":{"type":"anvillib:constant",…}}CustomFunction javadoc 写 "type":"anvillib:custom"示例 JSON 加载失败
  • 仓库惯例是共享注册表用 MAIN_ID(见 LibItemSubPredicatesAnvilLibRecipe.MAIN_ID)。

建议统一为 MAIN_ID(或同步修正文档与已有约定,二选一,不能两头不一致)。

6. 解析期内建函数判定用 getPath() → 命名空间函数被内建同名遮蔽
LibBuiltInFunctions.byName(withDefaultNamespace(lower).getPath())mymod:max(x,y,z) 的 path 也是 max,会静默解析成 anvillib:max;若下游注册的 mymod:add 是三参,还会被内建 arity=2 直接报错,该函数永远不可调用。建议仅在名字不带 : 时才走内建分支。

7. FlatExpressionParser.CACHEHolderGetter 实例为 key,从不清除 → 每次数据包重载泄漏一份
Map<HolderGetter<IFunction>, Map<String, IExpression>> 为静态强引用;每次 /reload 或重载世界都换新的注册表 lookup 实例(新 key),旧注册表 + 全部已解析表达式树被永久持有,且解析缓存本身没有上限。建议弱引用 key(WeakHashMap/WeakReference)或在重载时清空。

8. 数据包自定义函数调用不做参数个数校验CustomFunction.apply 只在 index < arguments.size() 时绑定,缺参会被 $(name) 静默当成 0(文档承诺「统一按声明顺序绑定」)。内建函数有 callChecked,建议注册表函数同样在解析期校验 arity。

9. 函数引用可自引用/互引用成环CustomFunction.body 里可以直接写自己的注册名(或 A→B→A),evaluate 无深度上限 → 触发 StackOverflowError 崩服务端。建议加求值深度上限或载入期环检测。


💡 建议

  • IFunction.CODEC.encode 每次对函数注册表全量 listElements().filter(input::equals) 反查引用(CustomFunctionequals 还要深比较整棵表达式树)→ 高频同步/NBT 存档是 O(n·tree)。可缓存 identity → ResourceKey 映射。
  • NumberArguments.with() 每次 new LinkedHashMap<>(named) 复制整个映射,自定义函数多参数时逐参复制为 O(n²);可用持久化不可变 map 或求值期栈式绑定。
  • IFunction.CODEC 第一个参数传入一个匿名 Codec,但 Codec.of(encoder, decoder) 只取它的 encode()——里面覆写的 decode 永不执行,属 dead code,易误导后续维护者。
  • IExpression.CODEC 的反向 xmap 强转 (FunctionExpression) expression:任何第三方 IExpression 实现会 CCE,建议在接口 javadoc 明确「单一节点类型」契约。
  • evaluateInt 对 NaN/±Inf 做 (int) Math.round(...) 会饱和到 0 / Integer.MIN_VALUE / Integer.MAX_VALUE,建议文档化或显式 clamp。
  • MIN/MAXparameters 固定为 List.of("values") 而 arity 可变(≥1),parameters() 与实际绑定语义不一致,建议注释说明或改变结构以避免误用。

🟢 看起来不错

  • 「枚举常量自身实现 IFunction,自带参数名 + arity + 注册名」的设计很干净,callChecked 把参数个数错误提前到解析期,报错信息还带位置;
  • 除零得 NaN/Inf、负数开方、下标越界、未绑定名一律不抛异常,number()/isLiteralNumber 对非有限值返回空并自动退回对象形式 —— 这几处边界处理得当;
  • ConstantFunction 内联形式与数字互认、Codec.lazyInitialized + 自定义 defer 处理静态初始化顺序,考虑周到;
  • mixins.jsonrefmap 名称与同级模块(font/wheel/…)保持一致 ✅。

🧪 测试建议

被测目标 推荐场景 优先级
FlatExpressionParserFlatExpressionWriter 属性测试:随机表达式树 parse→write→parse 值与结构一致(本次已证实可稳定复现多类失败) 🔴
number() / parseNumber() 边界 1e71e-31e-41e8、负数、0Math.rint 边界 🔴
visitBinary 常量×常量、常量×常量(负)、常量×符号、pow 负底数/负指数 🔴
IFunction.CODEC / IExpression.CODEC JSON→对象→JSON 与 NBT→对象→NBT 往返(含数据包函数引用与内联定义) 🔴
FlatExpressionParser.CACHE 连续多次 reload 后旧注册表是否仍被强引用(弱引用/清理断言) 🟡
CustomFunction.apply 实参数 ≠ 声明参数(多/少/0 个)、自引用与 A↔B 环 🟡
LibBuiltInFunctions.callChecked 每个内建函数的 arity 上下界与错误消息 🟢

📋 标题

gh 当前未认证(token invalid),未自动修改。建议补充到:

Add math lib: flat expression parser & function registry 新增数学库:表达式文本解析与函数注册表

结论:REQUEST_CHANGES — 脚手架质量高、设计清晰,但表达式回写与解析器之间存在多处不对称:常量并置(2*323)、负数字面量、Double.toString 指数形式都会让「写出去的数据读不回来或读回来变了值」,而这正好是存档/网络同步走的编码路径;另外新模块未接入 CI 矩阵。建议优先修 #1#4,并把 #1#3 固化为属性测试。

由 Hermes Agent 审查

@Gugle2308

Copy link
Copy Markdown

🌿 Roseau API Breaking Change Report

Module Status Breaking Changes
codec ✅ Compatible 0
collision ✅ Compatible 0
cube ✅ Compatible 0
config ✅ Compatible 0
integration ✅ Compatible 0
moveable-entity-block ✅ Compatible 0
network ✅ Compatible 0
rendering ⚪ Skipped
space-select ✅ Compatible 0
font ✅ Compatible 0
util ✅ Compatible 0
explosion ✅ Compatible 0
rpc ✅ Compatible 0
multiblock ✅ Compatible 0
recipe ✅ Compatible 0
registrum ✅ Compatible 0
sync ✅ Compatible 0
wheel ✅ Compatible 0
main ✅ Compatible 0

Full CSVs: see the Artifacts section of this workflow run.

@QiuShui1012
QiuShui1012 marked this pull request as draft September 14, 2026 16:13
@Gugle2308

Copy link
Copy Markdown
## 代码审查摘要 — PR #122(converted_to_draft:仅报严重问题,不阻塞性建议从略)

**操作:** converted_to_draft
**范围:** 25 个文件 / 2358 行 diff(23 新增文件,全部 Java + 构建脚本 + 资源;0 删除)
**范围:** 新增 Gradle 子模块 `module.math`(anvillib-math):表达式树 / flat 文本 ↔ 表达式 codec / 函数与函数类型注册表

---

### 🔴 关键

**1. `.github/modules.json` 未更新 → 新模块不构建、不发布,且会打断 main 的发布流程**

本 PR 只改了 `settings.gradle``include 'module.math'` + 项目名映射)和 `module.main/build.gradle``jarJar(api("dev.anvilcraft.lib:anvillib-math-neoforge-1.21.1:latest.release"))`),但仓库的构建/发布矩阵完全由 `.github/modules.json``.github/workflows/generate-matrix.js` 生成(`ci.yml` / `pull_request.yml` / `release.yml` 都只跑 `level_0..2` 的 matrix):

- `math` 不在 modules.json 里 → CI/Release 都不会构建 `module.math`,本模块的编译错误在 CI 里查不出来,也不会发布到 Maven Central;
- `release.yml``NOT_DEV=true` 分支,`module.main` 会去解析 `anvillib-math-neoforge-1.21.1:latest.release` —— 该坐标永远不会存在 → main 的发布流程直接失败。
- 依赖关系上 `module.math/build.gradle``jarJar(implementation project(":anvillib-util-neoforge-1.21.1"))``IFunction` 也 import 了 `dev.anvilcraft.lib.v2.util.ISerializer`。

建议补:`{ "module": "math", "needs": ["util"] }`(同时 `roseau_check``module_names` 也来自该文件,会一并漏掉 math)。

**2. `FlatExpressionWriter` 回写文本与解析器语法不自洽:常量×常量并置乘法静默改值(已实测)**

`visitBinary``isLiteralNumber(left)` 时把 `MULTIPLY` 写成并置,但右操作数**本身也是数字字面量**时会被左数字吞并:

| 表达式树 | 回写文本 | 重新解析结果 | 值变化 |
|---|---|---|---|
| `multiply(2, 3)`(即文本 `2*3`| `23` | `23` | **6 → 23** |
| `multiply(2.5, 100)` | `2.5100` | `2.51` | **250 → 2.51** |
| `multiply(2, 0.5)` | `20.5` | `20.5` | **1 → 20.5** |
| `multiply(2.5, 2.5)` | `2.52.5` | 解析失败 | 表达式无法加载 |
| `multiply(2, -1)` | `2-1` | `subtract(2, 1)` | **-2 → 1** |
| `multiply(-2, 3)` | `-23` | `-(23)` | **-6 → -23** |
| `multiply(-2, -3)` | `-2-3` | `-2-3` | **6 → -5** |

注意 `multiply(2,3)` 正是解析 `"2*3"` 得到的树——**最基础的输入就无法往返**。触发面不止数据包存档:`IExpression.STREAM_CODEC = ByteBufCodecs.fromCodecWithRegistries(IExpression.CODEC)` 走的也是这条 `encode` 路径(`FunctionExpression.FLAT_OR_OBJECT_CODEC``Codec.xor` `/ xmap(..., Either::left)` 永远先写 flat 文本),所以网络同步会把被改写的文本发到客户端 → 客户端解析出不同数值,或解析失败断连。

**3. 负数字面量永不补括号(`parenthesize` 对常量短路)**

`Operator.of()` 只识别 5 个内建运算符,`ConstantFunction` 命中 `default -> null``parenthesize()``child == null` 处直接返回 `false`,于是负数字面量在任何位置都不会加括号:

- `pow(-2, 2)``-2^2`,重新解析为 `subtract(0, pow(2,2))`**4 → -4**
- 与问题 2 叠加即产生 `2-1` / `-23` 这类并置乘法错误。

建议:`parenthesize()` 对常量按「负值在幂底数 / 并置乘法任一侧必须括号」处理,或对负数字面量统一加括号。

**4. `number()` 使用 `Double.toString` → 指数形式写出后读不回来**

`number()` 只在 `value == Math.rint(value) && |value| < 1e7` 时走 `Long.toString`,其余落回 `Double.toString`,会输出 `1.0E7` / `1.0E-4` 这类指数形式;而 `parseNumber()` 只扫描 `digits[.digits]``E` 被当成标识符 → 报 `expected '(' after function 'E7'`- `x*20000000``x*2.0E7` → 回读失败
- `x*0.0001``x*1.0E-4` → 回读失败

(顶层常量因为有 `Codec.either(DOUBLE, flat)` 第一支兜住不受影响,只有嵌套子项会踩到。)这是**写得出、读不回**:存档重载 / 数据包 reload / 网络包解码直接失败。

### ⚠️ 警告

**5. 数据包函数 id 路径含 `/` 无法往返**`functionName()``stripDefaultNamespace(key.location())` / `id.toString()` 原样输出,但 `isIdentifierPart()` 不含 `/``data/<ns>/anvillib/function/utils/triple.json`(子目录是数据包常规写法)→ 写出 `mymod:utils/triple(x)` / `utils/triple(x)` → 回读报 `expected '(' after function 'mymod:utils'`。建议 writer 对含非法字符的名字返回 `Optional.empty()`(自然回退到对象形式),或让标识符扫描器接受 `/`**6. 内建函数遮蔽其他命名空间的同名函数**`parseIdentifier``withDefaultNamespace(lower).getPath()` 查内建,与命名空间无关 → 实测 `othermod:max(x,1)` 被解析成内建 `MAX`;而 writer 又原样写 `othermod:max(x,1)`,回读变成内建 → 树与值都变。建议只在「无命名空间 或 namespace == MAIN_ID」时查内建。

**7. 注册命名空间混用,且 javadoc 示例与实际注册名不一致**:同一 `FUNCTION_TYPE` 注册表里 `LibBuiltInFunctions.TYPE_DF``MAIN_ID``anvillib:builtin`)、`LibFunctionTypes.DF``MOD_ID``anvillib_math:input/named/constant/custom`);而 `CustomFunction` / `ConstantFunction` 的示例写的是 `"type": "anvillib:custom"``"anvillib:constant"` → 按文档照抄的 JSON 会因为找不到类型而加载失败。仓库里两种先例都有(`LibItemSubPredicates` 用 MAIN_ID、`AnvilLibSyncEntries` 用 MOD_ID),但同一个 PR 内至少要对齐,并保证文档示例可直接加载。

**8. 静态解析缓存无上限且永不清理**`CACHE: Map<HolderGetter<IFunction>, Map<String, IExpression>>` 每次数据包 reload / 换存档都会换一个新的 registry 实例作为 key,旧实例与其中缓存的全部表达式树被强引用永久保留;`source` 侧也无上限(网络传入任意字符串都会入缓存)。建议弱引用 key 或在 reload 事件里清空。

### 💡 建议(不阻塞)

- **数据包函数缺 arity 校验**:只有内建函数走 `callChecked`,注册表函数(`CustomFunction`)没有参数个数信息,`apply()``min(parameters.size(), arguments.size())` 循环 → 多传的实参被静默丢弃、少传的 `$(name)` 取 0。建议在解析期按 `parameters.size()` 校验。
- **`CustomFunction` 无环/深度检测**:body 引用自身或互相引用时求值会 `StackOverflowError` 崩游戏,建议加深度上限或载入期环检测。
- **javadoc 与行为不符**`FlatExpressionWriter` 声称「任何回写结果重新解析都会得到同一棵表达式树」,实测不成立——负数字面量回读后被还原成 `subtract(0, v)` 形式(值相同、树不同,占 fuzz 样本的 34%),副带影响是顶层 `-2` 不再走 `Codec.either(DOUBLE, …)` 的数字分支而写成文本。
- `NamedFunction.name` 无字符校验,程序化构造含 `)` 的名字会写出不可回读文本(`$(a)b)`)。
- `InputFunction` record 构造器不校验非负(codec 用 `ExtraCodecs.NON_NEGATIVE_INT`),`InputFunction.of(-5)` 会静默取 0;`FlatExpressionParser.functionGetter``IFunction.getter` 是重复实现,可合并。
- `anvillib_math.mixins.json` 声明了 `required: true` 与包 `dev.anvilcraft.lib.v2.math.mixin`,但模块内不存在该包/任何 mixin(与 module.util 等空配置一致,可保留,确认下是否必要)。

### 🟢 看起来不错

- 表达式树统一为 `FunctionExpression` 一种节点(常量 / 输入 / 具名 / 内建 / 自定义都是调用),`Holder.direct` 内联 + `Holder.Reference` 引用两种形态清晰。
- 三种内联形式(数字 / flat 文本 / `function`+`arguments` 对象)的 `either``xor` 优先级正确,flat 写不出来时会正常回退到对象形式,不会丢数据。
- 内建函数在解析期做 arity 校验(`callChecked`),错误信息带位置;除零、负数开方、下标越界、名字未绑定都返回 `NaN`/`0` 而不抛异常,与 javadoc 一致。
- `Codec.lazyInitialized` + `IExpression.defer` 处理静态初始化顺序;`settings.gradle``gradle.properties``mods.toml``icon.png``jarJar` 列表(含 alphabetical 排序位置)都补齐了——只差 `modules.json`### 🧪 验证方式

无 Java 环境,按 skill 的等价转写 + 回环模糊测试法,把 `FlatExpressionParser` / `FlatExpressionWriter`(含 `parenthesize` / `number` / `isLiteralNumber` / `Operator.of`)逐行转写为 Python 后跑 `parse → write → parse`- 定向用例:`2*3``2.5*100``2*0.5``2.5*2.5``2*0.5``pow(-2,2)``multiply(2,-1)``x*20000000``x*0.0001` 全部复现(见上表);
- 随机树 fuzz(N=4000,常量/输入/四则/幂/单参函数,取 3 组输入点求值比对):**329 例值改变(8.2%)+ 19 例回读失败(0.5%)**,另有 1356 例仅结构不同(负号一元化);
- 「值改变 + 回读失败」两类在修复 2/3/4 后应回到 0——建议补一组以 `FlatExpressionParser.parse → FlatExpressionWriter.write → parse` 的往返等价性单测(含 `const×const`、负数字面量、`|v|≥1e7`/`|v|<1e-3` 三个边界族)。

**结论: COMMENT(draft 阶段不阻塞)** — 模块骨架、注册表与 codec 分层设计清晰,但 **flat 文本回写路径存在可复现的值改变与不可回读缺陷**`2*3``23``pow(-2,2)``-4``x*0.0001` 直接解码失败),且这条路径被 `IExpression.STREAM_CODEC` 用于网络同步,属于会静默污染存档/客户端数据的那一类;再加上 `modules.json` 缺项会让模块既不构建也不发布,建议在转正 (ready_for_review) 前先处理 🔴 1–4。

> 建议标题(本次 gh 未认证,无法自动修改):`Add math module: expression parsing/evaluation library 新增数学模块:表达式解析与求值库`

---
*由 Hermes Agent 审查*

@Gugle2308

Copy link
Copy Markdown

💾 Self-improvement review: Skill 'text-codec-roundtrip-pr-review' created. · Patched references/pr-class-routing.md in skill 'github-webhook-pr-review' (1 replacement).

@Gugle2308

Copy link
Copy Markdown

⚠️ Dangerous command requires approval:

cd /tmp; cat > ex.sh <<'EOF'
#!/bin/bash
# usage: ex.sh <diff> <filename-substring>
awk -v pat="$2" '/^diff --git/{p=(index($0,pat)>0)} p' "$1"
EOF
chmod +x ex.sh; ./ex.sh pr122.diff FlatExpressionPar...

Reason: shell execution via heredoc

Reply /approve to execute, /approve session to approve this pattern for the session, /approve always to approve permanently, or /deny to cancel.

@Gugle2308

Copy link
Copy Markdown

⚠️ Dangerous command requires approval:

cd /tmp; curl -s "https://api.github.com/repos/Anvil-Dev/AnvilLib/pulls/122" -o pr122.json; python3 -c "
import json
d=json.load(open('/tmp/pr122.json'))
print('state',d['state'],'merged',d.get('merge...

Reason: script execution via -e/-c flag

Reply /approve to execute, /approve session to approve this pattern for the session, /approve always to approve permanently, or /deny to cancel.

@Gugle2308

Copy link
Copy Markdown

已完成审查(diff 与 base..head 的 git diff 逐行一致:4644 行 / 37 文件 / +4390−0 / md5 校验通过,无 ghost 文件、无 EOF 缺失、无 TODO 或调试残留)。下面是审查意见。

代码审查摘要 — PR #122 Add math lib 添加数学库

操作: synchronize | 范围: 37 个文件(28 Java,33 新增,0 删除)/ 4644 行 diff / +4390 −0
分支: math/1.21.1dev/1.21.1(head 6d142ca6,base e8d84629

🧩 新模块接线检查(新模块 PR 最高频遗漏项)— 完整 ✅

位置 状态 证据
.github/modules.json { "module": "math", "needs": ["util"] },与三处枚举点交叉验证过:base 分支里只有 modules.json / settings.gradle / module.main 枚举模块
settings.gradle include 'module.math' + project(':module.math').name = 'anvillib-math-neoforge-1.21.1'
module.main/build.gradle NOT_DEV / dev 两支都补了 jarJar(api(...)),位置在字母序内
generate-matrix.js 契约 module_id=anvillib-mathmod_id=anvillib_mathgradle.properties、jar 名一致
gradle 依赖 ↔ needs module.math 只用到 v2.util.ISerializer(module.util),needs:["util"]jarJar(implementation project(":anvillib-util-neoforge-1.21.1")) 一致

🔴 关键

  • FlatExpressionWriter.functionName()(第 248–254 行)+ stripDefaultNamespace 与解析器「anvillib 命名空间优先内建」规则冲突 → 静默改值。
    写侧把 anvillib: 前缀一律省略;读侧 FlatExpressionParser.parseIdentifier 在 MAIN_ID 分支 LibBuiltInFunctions.byName(path)(第 442 行)再查注册表。因此数据包里注册的 anvillib:<内建名>(如 anvillib:sqrtanvillib:max永远无法按名引用,而且一个持有该函数引用的树回写后会被换成内建函数——这是本 PR 唯一会改变求值结果的往返缺口(对象形式 JSON → 编码 → 解码后语义变了),与类注释「任何回写结果重新解析都会得到同一棵表达式树」的承诺相悖。
    我用逐行等价转写的 parser/writer 复现:节点 anvillib:sqrt(4) → 文本 sqrt(4) → 重新解析得到 builtin sqrt;而 mymod:max(非 anvillib 命名空间)走「先查注册表」路径,正常往返 ✅。
    建议(一行级):functionName() 里若 key.location() 在 MAIN_ID 且 LibBuiltInFunctions.byName(path) != nullcall.function().value() 不是该内建,就返回 null(退回对象形式),避免静默替换。可达性低(需要有人在 anvillib 命名空间注册撞名函数),但代价是静默错值,建议顺手补上。

⚠️ 警告

  1. FlatExpressionWriter.negation(String) 第 168 行的 startsWith("-") 早退,让「解析器读得进来的文本写不回去」。 实测(模型复现):-(-0.5)0-(-1)x*(-(-2))sqrt(-(-0.25)) 都解析成 subtract(const 0, const -n),回写时直接返回空 → codec 退回对象形式。该守卫的理由是「--x 不是合法写法」,但 negation(String) 本身就无条件加括号,永远不可能写出 --x;去掉守卫后 -(-0.5) 重新解析恰好得到 subtract(0, const -0.5)(我用模型验证过子树结构逐节点相同)。影响:文本→树→文本不闭合(值不丢,只是表示形式退化;若下游依赖 flat 文本做规范写回/网络同步就会突然变成对象形式)。FlatExpressionTest.negationOfNegativeConstantFallsBackToObject 目前把这个回退当成预期行为固定下来了,改的话需要同步更新该用例。
  2. IFunction.bind()(第 164–167、175–181 行)在「变参不在末位」时绑定错位 / 抛越界异常。 固定形参位的「列表不该出现在这里」检查按形参序号values.get(fixed),而 values 是按实参顺序排的(变参可能吞掉多个实参位),两者在变参非末位时错位:
    • 形参 ["a","x...","b"] + 调用 f(1, $(xs...), 7) → 误报「列表只能传给变参」;
    • 形参 ["x...","b"] + 调用 f(1, $(xs...)) → 绑定循环走到 values.get(2) IndexOutOfBoundsException(不是可读的校验错误)。
      Parameters 明确宣称支持变参在任意位置(BuiltInFunctionTest.variadicMayAppearAnywhere 也断言了),但现有测试只覆盖了「变参在末位」和「变参首位的 forEach(它自己重写了 apply)」。另外 Parameters 第 15 行注释「变参之后的形参永远取不到值」与 bind()variadicCount = total - fixedCount(后面固定形参确实会拿到值)自相矛盾,建议统一口径:要么校验循环改为按「第 i 个形参实际消费的实参下标」检查,要么在 Parameters 构造期就限制变参必须在末位,并同步文档/实现/测试。
  3. FlatExpressionParser.CACHE(第 57–58、116 行)的弱键达不到注释声称的回收效果,且无容量上限。 缓存值是表达式树,引用注册表函数的树里持有 Holder.Reference,而 Holder.Reference 带一个 ownerHolderOwner)字段——实测 MC 类文件里确认(net/minecraft/core/Holder$Reference.classowner : Lnet/minecraft/core/HolderOwner;),注册表就是自己的 owner,而缓存键正是 RegistryOps.getter() 直接拿到的注册表实例 → 值强引用键,WeakHashMap 的弱键永远不可回收,每次数据包 reload 都会留下「旧注册表 + 该注册表下解析过的全部表达式」,与类注释「键用弱引用,换实例后旧缓存能被回收」不符;同时缓存没有条目上限,而 parseValue 是公开 API,下游用运行时拼接的文本反复调用会无界增长。建议:加容量上限(LRU),或在 reload 事件里清空,或把键换成不构成反向引用的标识。

💡 建议

  • 字面量可以解析成无穷1e99999(指数 ≤ 8 位)经 Double.parseDouble 得到 Infinity 常量且不报错,而回写侧对非有限值一律拒绝 → 用户写的合法文本反而写不回去。建议解析期就拒绝非有限字面量。
  • Operator.UNARY(第 383 行)是死代码Operator.of() 从不返回它,needsParentheses 也从不收到它,只活在注释里 → 删除或真正接进取负的括号判定。
  • Parameters.bind(List<Double>)(第 161 行)全仓库无调用方(实际用的是 IFunction.bind),且它把变参当末位处理(后面的固定形参会 arguments.get(size) 越界),属于会误导下游的公开 API → 删除或改为委托。
  • NamedFunctionIExpression.Reference.Named 是同一语义的两套节点$(name) 文本只解析成后者,只有对象形式能表达前者),测试里也得靠 canonical() 把它们折叠成同一个字符串才过得去 → 若没有刻意保留两套的正当理由,建议合并,减少「同一棵树两种表示」的认知负担。
  • 命名空间约定值得写进 javadoc:mod id 是 anvillib_math,但函数/类型都注册在 anvillibAnvilLibMath.MAIN_ID,与 "type": "anvillib:custom" 文档一致 ✅)。其它模块若也往 anvillib:function_type/anvillib:function 注册,需保证名字不冲突,建议在 LibRegistries 注释里点明这条约定。

🟢 看起来不错

  • 上一轮评审报过的三类回写 bug 都已修复并有回归用例:常量并置(isLiteralNumber 排除带符号常量 + juxtaPositionable 只放行字母/_/(/$2*3 不再写成 23)、负底数((-2)^2-2^2 语义分开)、指数记法(basicNumber 十进制优先 + signedNumberLiteralAhead 认回 -5.0E-4)、负零符号位。
  • 我用逐行等价转写的 parser + writer 模型跑了 60000 棵随机树(含极值/负零/指数常量、变参、取负、隐式乘法组合):解析失败 0、结构改变 0、求值改变 0、回写不稳定 0;唯一「写不出来」的 13 例全部是上面 ⚠️-1 的「取负负常量」族。定向用例也确认 lambda(含多参/变参/嵌套/作为 forEach 末位实参)、mymod:bar 这类数据包函数引用、anvillib:foo(不撞名)都能原样往返。
  • 求值侧边界处理到位:除零 → NaN/∞、负数开方 → NaN、下标越界与未绑定名字取 0,都不抛异常,符合 IExpression.evaluate 的 javadoc;CustomFunctionMAX_CALL_DEPTH = 64 + ThreadLocal 深度计数 + finally 还原,自引用与互引用被拦成异常而非 StackOverflowError(有测试)。
  • 测试质量高:20000 轮随机树往返 + 一批「真实踩过的坑」定向断言,MathTestBootstrap 用最小注册表把表达式求值与游戏解耦,addModdingDependenciesTo(sourceSets.test) 让单测在裸 JVM 上跑;math.fuzz.rounds 可调。

📋 声称验证表

PR 描述为空,按模块功能对照 diff:

声称 状态 对应文件
flat 表达式解析器(x*22x$(cost)^、lambda) FlatExpressionParser(555)
表达式回写成 flat 文本(规范形式) FlatExpressionWriter(411)
表达式树 + 三支编解码(数字 / flat 文本 / 对象) IExpression, FunctionExpression
函数类型注册表 + 数据包函数注册表 LibRegistries, LibFunctionTypes
内建函数(四则/幂/单参/变参/forEach) LibBuiltInFunctions
lambda、变参、自定义函数、递归保护 LambdaFunction, Parameter(s), CustomFunction
新模块 CI/发布/打包接线 .github/modules.json, settings.gradle, module.main/build.gradle
单元测试 4 个测试类 + MathTestBootstrap + MathFlatAssertions

🧪 测试建议

被测目标 推荐场景 优先级
IFunction.bind 变参不在末位 × 实参含 $(x...):断言绑定结果与异常类型(当前会越界) 🔴
FlatExpressionWriter.negation 对「解析器产出的树」断言可写性-(-0.5)0-(-1) 的 parse→write→parse 闭合 🟡
FlatExpressionParser.CACHE 连续两次 reload 后断言旧注册表可被 GC(弱引用探针) 🟡
注册表撞名 anvillib:<内建名> 数据包函数的往返语义(应退回对象形式或保真) 🟡
字面量边界 1e99999 等超大指数字面量的解析结果(当前静默得到 Infinity 🟢

结论: COMMENT — 逻辑与测试都相当扎实,上一轮的回写 bug 已修复且被随机往返测试锁住;上面 4 条属于边界/健壮性问题,其中 🔴 会静默改值、⚠️-2 会抛越界异常,建议合并前修掉或至少开 issue 跟进。

建议标题:feat(math): add math expression library (parser / evaluation / function registry) 新增数学表达式库——当前标题 Add math lib 添加数学库 准确但偏笼统,可补充「表达式解析/求值」这一核心内容。本次因 gh auth status 显示 token 已失效(Active account: Gugle2308,token invalid)未自动修改,按约定不导出凭据,留由 maintainer 决定。

由 Hermes Agent 审查

@Gugle2308

Copy link
Copy Markdown

💾 Self-improvement review: Patched SKILL.md in skill 'pr-diff-scope-reconciliation' (1 replacement). · Patched references/anvillib-math-pr122-findings.md in skill 'text-codec-roundtrip-pr-review' (1 replacement). · Skill 'text-codec-roundtrip-pr-review' updated (full rewrite). · Patched SKILL.md in skill 'dependency-bump-pr-review' (1 replacement).

@Gugle2308

Copy link
Copy Markdown

🌿 Roseau API Breaking Change Report

Module Status Breaking Changes
codec ✅ Compatible 0
collision ✅ Compatible 0
cube ✅ Compatible 0
config ✅ Compatible 0
integration ✅ Compatible 0
moveable-entity-block ✅ Compatible 0
network ✅ Compatible 0
rendering ⚪ Skipped
space-select ✅ Compatible 0
font ✅ Compatible 0
util ✅ Compatible 0
explosion ✅ Compatible 0
rpc ✅ Compatible 0
multiblock ✅ Compatible 0
recipe ✅ Compatible 0
registrum ✅ Compatible 0
sync ✅ Compatible 0
wheel ✅ Compatible 0
main ✅ Compatible 0

Full CSVs: see the Artifacts section of this workflow run.

@Gugle2308

Copy link
Copy Markdown

⚠️ Dangerous command requires approval:

curl -s "https://api.github.com/repos/Anvil-Dev/AnvilLib/pulls/122" | python3 -c "import sys,json;d=json.load(sys.stdin);print('title:',repr(d.get('title')));print('state:',d.get('state'));print('base...

Reason: Security scan — [HIGH] Pipe to interpreter: curl | python3: Command pipes output from 'curl' directly to interpreter 'python3'. Downloaded content will be executed without inspection.
Safer: tirith run https://api.github.com/repos/Anvil-Dev/AnvilLib/pulls/122 — or: vet https://api.github.com/repos/Anvil-Dev/AnvilLib/pulls/122 (https://getvet.sh); script execution via -e/-c flag

Reply /approve to execute, /approve session to approve this pattern for the session, /approve always to approve permanently, or /deny to cancel.

@Gugle2308

Copy link
Copy Markdown

审查完成。以下是审查意见(60k 轮独立 fuzz + 逐行等价转写验证):


代码审查摘要 — PR #122

操作: synchronize
范围: 37 个文件(28 Java / 6 测试 / 3 构建·CI,4487 增 / 0 删,全部新增文件) / 4741 行 diff
主题: 新增 module.math(anvillib-math)——flat 文本 ↔ 表达式树的双向编解码、内建函数注册表、数据包函数与 lambda

✅ 验证手段(先给证据,再给结论)

  1. 逐行等价转写:把 FlatExpressionParser + FlatExpressionWriter(含 signedNumberLiteralAhead / parseNumber 指数分支 / basicNumber / negation / needsParentheses / juxtaPositionable)转写成 Python 后跑了 60,000 轮随机树 fuzz(常量含 -0.0/1e-5/9.3e18/1e300、输入值、$(name)$(x...)):parsed=60000, unstable=0零回读失败、零值改变、零结构差异。作者上一版已知的坑(2*3→23pow(-2,2)→-2^20.0001→1.0E-4 读不回、负零丢符号位)在本版确认已修复且被测试覆盖。
  2. 定向复核2*3→"2*3"2*x→"2x"pow(-2,2)→"(-2)^2"-2^2→-(2^2)sqrt(0.0001)→"sqrt(0.0001)"constant(-0.0)→"-0.0"subtract(0,-0.5)→"-(-0.5)" —— 与 javadoc 和单测约定一致。
  3. CI 接入复核.github/modules.json 已补 { "module": "math", "needs": ["util"] }(新模块最高频的遗漏,本 PR 做对了),拓扑层级仍为 3 层,ci.yml 声明的 level_0..2 无需改动;settings.gradlemodule.main 双路 jarJargradle.propertiesmod_id=anvillib_math@Mod 一致)、mixins.json(与 util/font/sync 逐字节同构)均齐全。

🔴 关键

  • FlatExpressionWriter.java:61operand)+ :295juxtaPositionable)— lambda 作二元操作数时不补括号,回写文本读不回来 / 结构被改写。
    needsParentheses 只比优先级与结合性,而 lambda 不是 LibBuiltInFunctionsOperator.of 返回 null)→ 判为"不需要括号";juxtaPositionable 只看首字符,lambda 文本以字母开头也放行。而 FLAT_OR_OBJECT_CODEC.encode 只要 write 成功就优先写 flat 文本,于是坏文本被真正写进存档/网络包

    解析得到的树 回写文本 重新解析结果
    parse("2(x -> $(x))") 2x -> $(x) ❌ 报错 unexpected character '>' at position 4
    parse("1+(x -> $(x))") 1+x -> $(x) ❌ 报错 unexpected character '>' at position 5
    parse("(x -> $(x))*2") x -> $(x)*2 ⚠️ 变成 lambda(x, multiply($(x),2))(结构被静默改写)
    parse("(x -> $(x))^2") x -> $(x)^2 ⚠️ 变成 lambda(x, pow($(x),2))

    触发面很窄(需要 lambda 处于操作数位置,正常写法里 lambda 只作函数实参;实参位置的 lambda 我逐一验证过是好的),但 javadoc 明确承诺"任何回写结果重新解析都会得到同一棵表达式树",这里破了这个不变量。修复约 1 行:在 operand() 里对 call.function().value() instanceof LambdaFunction 强制补括号(或让 juxtaPositionable 拒绝含 " -> " 的文本)。

⚠️ 警告

  • LambdaFunction.java:76 — 缺递归深度保护,与 CustomFunction 不对称。
    CustomFunction.applyCustomFunction.java:44/85)用 MAX_CALL_DEPTH=64 + ThreadLocal 拦下了自引用/互相引用,lambda 的 apply 直接把函数体拿去求值。lambda 同样是 FUNCTION_KEY 的合法条⽬(数据包 "type":"anvillib:lambda",或下游 DeferredRegister 注册),两个 lambda 互相调用(A→B→A,不经过任何 CustomFunction)就会以 StackOverflowError 崩游戏,而不是像自定义函数那样抛可读的 IllegalStateException。建议在 LambdaFunction.apply 复用同一套深度计数。
  • FlatExpressionParser.java:146 clearCache() 全仓库无调用点。
    javadoc 自己写明"值强引用键,弱键实际回收不掉"——一旦缓存里的树持有注册表函数的 Holder.ReferenceWeakHashMap 键就永远回收不了,于是每次数据包重载都会永久保留一份旧函数注册表 + 最多 512 棵已解析表达式树。既然提供了 clearCache(),建议在模块内挂一个重载钩子(如 AddReloadListenerEvent / OnDatapackSyncEvent)自动调用,或至少在模块 README 里把这条要求写给下游(否则这就是一条只存在于文档里的缓解措施)。

💡 建议

  • IExpression.java:77 — 未检查的强制转换。 FLAT_OR_OBJECT_CODEC.encode 在 flat 写出失败时直接 (FunctionExpression) inputIExpression 是公开且未 sealed 的接口(模块本身就在鼓励下游扩展),第三方实现走到这里会抛 ClassCastException 而不是返回 DataResult.error。改成 instanceof 判断 + 错误返回更符合本文件其余部分的风格。
  • FlatExpressionParser.java:203 — lambda 参数校验的异常被 catch (IllegalArgumentException) 吞掉。 splitParameters 里那句"参数名重复或出现两个变参时直接报错,不留到调用时才发现"实际达不到:Parameters.parse 抛的 IAE 会被这个 catch 捕获并回退到 parseAdditive,用户看到的是 (a, a) -> $(a)expected '(' after function 'a' 这种误导性报错。建议换一个专用异常类型区分"不是 lambda"与"lambda 本身不合法"。
  • 解析期没有嵌套深度上限。 递归下降遇到上万层括号或 2^2^2^… 会抛 StackOverflowErrorError),而 parseResultcatch (RuntimeException),拦不住,会从 codec 里逃逸到数据包加载流程。既然自定义函数都做了深度保护,建议这里也加一个嵌套层数上限(顺带让错误仍然走 DataResult.error)。
  • LibBuiltInFunctions.java:154/156FOREACH 覆写 apply(List, Arguments) 时绕过了 parameters.checkArity 解析期与 FOREACH.call(...) 都会校验,所以影响仅限直接构造:arguments 为空时 arguments.get(-1)IndexOutOfBoundsException,只给 lambda 不给列表时静默返回 0。开头补一次 this.parameters().checkArity(arguments.size()) 即可与其他内建函数一致。
  • 死代码(小): FlatExpressionParser.java:172 stripDefaultNamespace javadoc 说是"用于测试与调试",但主代码与测试都没有调用;Arguments.java:79 bound(String) 同样无调用点。
  • 测试盲区(与上面 🔴 对应): MathFlatAssertions.randomTree 从不把 lambda 放进操作数位置,LambdaTest 只在实参位置写 lambda,FlatExpressionTest 完全没有 lambda,所以 20k 轮 fuzz 覆盖不到这个分支。建议 fuzz 的叶子集合里加入 lambda,并单独断言"某类节点不可回写时必须退回对象形式";顺带补一组 data pack 注册函数用例(跨命名空间 mymod:max 与内建同名时的优先级,目前只有 anvillib:sqrt 撞名被覆盖)。
  • modules.json 微瑕: 新条目插在 multiblockrecipe 之间且对齐列宽与相邻行不一致;同一层级内本来是字母序,挪到 multiblock 之前更整齐。

🟢 看起来不错

  • 新模块的 CI/构建接入完整:modules.json + settings.gradle + module.main 双路(NOT_DEV / project)jarJar + 模块 gradle.properties@Mod 值一致,依赖声明风格与其他模块逐字一致。
  • 双序列化路径设计干净:IExpression.CODEC(数字 / flat 文本 / 对象三支)与 FLAT_OR_OBJECT_CODEC 的"能写文本就写文本、写不出退回对象"的取舍有注释说明,并且解释了为什么不能用 Codec#xor
  • 命名空间约定自洽:注册表与类型都挂 anvillib,与"flat 短名补 anvillib"的规则一致;anvillib:<内建名> 撞名时 writableName 返回 null 退回对象形式,而不是静默把求值结果换掉——这个决定很对。
  • 求值侧的语义边界处理得细:变参至少吃一个实参、$(x...) 落在固定形参位报错、列表撑出的实参过量给可读错误而不是下标越界、forEach 末位必须是 lambda 且 lambda 不能以变参开头、Spread 单独求值直接报错;CustomFunction 的深度保护用 ThreadLocal + try/finally,异常路径也复原。
  • 单测质量高:往返断言同时校验值、结构、回写稳定性,还带 -Pmath.fuzz.rounds 可调轮数的随机树 fuzz 与 codec 往返;MathTestBootstrap 手搭注册表+RegistryOps 让表达式逻辑能在裸 JVM 上测,无需启动游戏。

结论: REQUEST_CHANGES — 整体质量很高,主要问题只有 operand() 里 lambda 操作数缺括号这一处(1 行修复,属静默写出不可回读文本的真实缺陷);LambdaFunction 深度保护与 clearCache() 挂接建议一并补上,其余为可选改进。修完即可合并。

(标题 Add math lib 添加数学库 已符合 <英文标题> <中文标题> 格式,未做修改;PR 描述为空,建议补一段模块用途与 anvillib:builtin/custom/lambda 的最小示例。)


由 Hermes Agent 审查

@Gugle2308

Copy link
Copy Markdown

💾 Self-improvement review: Patched references/anvillib-math-pr122-findings.md in skill 'text-codec-roundtrip-pr-review' (1 replacement). · Patched SKILL.md in skill 'text-codec-roundtrip-pr-review' (1 replacement).

@Gugle2308

Copy link
Copy Markdown

🌿 Roseau API Breaking Change Report

Module Status Breaking Changes
codec ✅ Compatible 0
collision ✅ Compatible 0
cube ✅ Compatible 0
config ✅ Compatible 0
integration ✅ Compatible 0
moveable-entity-block ✅ Compatible 0
network ✅ Compatible 0
rendering ⚪ Skipped
space-select ✅ Compatible 0
font ✅ Compatible 0
util ✅ Compatible 0
explosion ✅ Compatible 0
rpc ✅ Compatible 0
multiblock ✅ Compatible 0
recipe ✅ Compatible 0
registrum ✅ Compatible 0
sync ✅ Compatible 0
wheel ✅ Compatible 0
main ✅ Compatible 0

Full CSVs: see the Artifacts section of this workflow run.

@Gugle2308

Copy link
Copy Markdown

审查完成。diff 与本地 git diff e8d84629..178000b4 逐行一致(md5 相同,4892 行 / 38 文件 / +4632 −0),并做了等价转写回环 fuzz、定向用例与两处专项探针。以下是审查意见。


代码审查摘要 — PR #122 Add math lib 添加数学库

操作: synchronize(head 178000b4 ← 上轮 eb96a2c4
范围: 38 个文件(28 主代码 Java / 6 测试 / 4 构建·CI),4892 行 diff,+4632 −0(全部新增文件,无删除、无迁移;1.21.1 常规新模块 PR,跳过 26.1 迁移检查)
主题: 新增 module.math(anvillib-math)——flat 文本 ↔ 表达式树双向编解码、内建/数据包函数注册表、lambda 与变参

✅ 本轮验证手段(先给证据,再给结论)

  1. diff 完整性对账patch-diff 4892 行 / 38 文件,与本地 git diff <base e8d84629>..178000b4 逐行一致(md5 54e9bcedc984b4e1a531157da20ce047),无 ghost 文件、无 No newline 缺失、新增行无 TODO/FIXME/调试残留。
  2. 逐行等价转写 + 回环 fuzz(parser 与 writer 转写成 Python,write → parse 比对结构与值):
    • 基线 60000 轮:parsed=60000 unstable=0parse-fail / structure / value / unstable 四类全 0
    • lambda 感知 20000 轮(叶子集合补入单参 / 双参 / 变参 lambda,且落在操作数位置):五类全 0(含 not-writable)。上一轮同一探法读数是 parse-fail 1636 / structure 1160 —— 本轮的补括号修复属实测通过。
  3. 定向复核:作者新增用例的 4 个源 2(x -> $(x))1+(x -> $(x))(x -> $(x))*2(x -> $(x))^2 写出文本与期望逐字一致、重解析结构相同、二次回写稳定;另补 8 组操作数形状(2^(x -> $(x))(x->$(x))-(y->$(y))(x->$(x))/(y->$(y))2*((a, b) -> $(a)+$(b))min($(xs...),x -> $(x))-(x -> $(x))^2 等)全部稳定。
  4. 嵌套深度探针:作者测试的两种深输入在 depth=513 处报 expression nests too deeply(与 tooDeeplyNestedIsRejected 一致);符号链输入的读数见 ⚠️1。
  5. 名称解析探针writableName() 的返回值与 parseIdentifier() 的实际解析结果逐名对照,见 🔴1 / 🔴2 的最小复现。

📋 上轮(第三轮)结论存活核对

上轮发现 本轮状态 证据
🔴 lambda 作二元操作数不补括号(2(x -> $(x))2x -> $(x) ✅ 已修 + 有测试 needsParentheses() 第 285 行对 LambdaFunction 返回 true;并置过滤第 196 行排除 lambda。本轮 lambda fuzz 五类全 0,LambdaTest.lambdaOperandsStayWritable 覆盖 4 个形状
⚠️ clearCache() 全仓无调用点 ✅ 已修 新增 LibCacheReloadHandlerServerStartedEvent + OnDatapackSyncEvent);对 NeoForge 源码确认 OnDatapackSyncEvent 的触发点确实含 /reload,首轮加载另有 ServerStartedEvent 兜住
⚠️ LambdaFunction.apply 缺递归深度守卫 仍存(见 ⚠️2) LambdaFunction.java 全文无 MAX_CALL_DEPTHCustomFunction 仍是唯一带守卫的类型
💡 FLAT_OR_OBJECT_CODEC.encode 未检查强转 ✅ 已修 改成 instanceof FunctionExpression + DataResult.error(...)
💡 FOREACH.apply 直构绕过 arity 校验 ✅ 已修 + 有测试 FOREACH.apply 首行 this.parameters().checkArity(arguments.size())forEachChecksArityOnDirectConstruction
💡 解析期无嵌套深度上限 🟡 部分修复 新增 MAX_NESTING_DEPTH=512 + guardDepth,但 parseUnary 路径绕过(见 ⚠️1)
💡 测试盲区:randomTree 叶子无 lambda 🟡 部分缓解 作者补了手写用例,但 MathFlatAssertions.randomLeaf 仍只有常量/输入/命名值三支,randomTree 仍显式排除 FOREACH(见 💡1)
💡 死代码 Operator.UNARY / stripDefaultNamespace / Arguments.bound(String) ✅ 全部已删 本轮 + 上轮提交一并清理;Parameters.bind(List<Double>) 也删了
💡 modules.json 条目顺序与列宽 ✅ 已修 math 已移到 multiblock 之前,needs 列与其他条目对齐
NamedFunctionReference.Named 双节点 ⓘ 仍存(未复报,仅记录) flat 文本只产后者,测试仍需 canonical() 折叠

🔴 关键

1. FlatExpressionWriter.writableName() 只挡了「撞内建名」,没挡「撞输入变量名」→ 回写后静默改值

writableName()FlatExpressionWriter.java:264-270)判断能否省略 anvillib 命名空间的唯一条件是 LibBuiltInFunctions.byName(path) != null。但解析器 parseIdentifier()FlatExpressionParser.java:470-507)对裸标识符的解析顺序是:

variable(path)                      // x / y / z / x0 / x1 / x2 / x<digits>  ← 第一步,且不要求后面跟 '('
LibBuiltInFunctions.byName(path)
FUNCTION_KEY 注册表

所以数据包在 anvillib 下注册一个名叫 x0(或 x/y/z/x12)的函数后,引用它的树会被回写成裸名,重解析却变成输入值 × 操作数

注册函数      : anvillib:x0     (data/anvillib/anvillib/function/x0.json, type anvillib:custom)
树            : call(anvillib:x0, [const 5])
encode → 文本 : "x0(5)"
decode → 树   : multiply(<input 0>, 5)        ← 结构+值双变,且不再指回那个函数

等价转写实测(writableName() 按当前实现镜像后逐名跑 write → parse):

注册名 写出文本 重解析
anvillib:sqrt 对象形式(已挡) — ✅
anvillib:x / y / z x(5) / y(5) / z(5) <input 0/1/2> * 5
anvillib:x0 / x1 / x2 / x12 x0(5)x12(5) <input 0/1/2/12> * 5
anvillib:collision-free collision-free(5) 解析失败(见 🔴2)

x0/x1/x2 是相当常见的命名,这条触发面比上轮那条内建名撞名更宽。影响链与上轮一致:FLAT_OR_OBJECT_CODEC.encodeIExpression.java:72-85只要 write 成功就直接返回 flat 文本,不再写对象形式,所以坏文本会真的落进存档/网络包,decode 侧静默换值。
修法:在 writableName() 里把 parser 的变量规则也算上——path 形如 x/y/z/x<数字> 时返回 null(建议把 FlatExpressionParser.variable() 提成包内可见直接复用,避免两边规则各写一份再漂移)。

2. 函数名含标识符字符集之外的字符时「写得出、读不回」;本轮新增测试把这个缺口固化成了期望值

visitCallFlatExpressionWriter.java:230-241)把 functionName() 的返回值原样拼进文本,而标识符扫描器 isIdentifierPartFlatExpressionParser.java:603-605)只吃 [A-Za-z_][A-Za-z0-9_:.]*——-/ 都不在内(两者都是合法 ResourceLocation 路径字符)。

本轮新增的 CustomFunctionTest.builtInNameCollisionFallsBackToObject 第 258 行正是:

MathTestBootstrap.registerFunction("collision-free", ...);      // data/anvillib/anvillib/function/collision-free.json,合法
assertEquals("collision-free(3)", MathTestBootstrap.writeFlat(...));

该文本回读必然失败——等价转写用当前解析器复现:

parse("collision-free(3)")
  → expected '(' after function 'collision' at position 9 of expression "collision-free(3)"

parseIdentifier 读到 collision 就停在 -match('(') 失败。)第一轮提过的同类项「路径含 /」(data/<ns>/anvillib/function/utils/triple.json 这种常规写法)也仍未修。

由于 FLAT_OR_OBJECT_CODEC.encode 写得出就不回退,这条同样是写进数据后读不回来:作者测试只断言了文本、没有回读,于是 CI 全绿而缺口存活。
修法writableName() 里加字符集校验——path(非 anvillib 命名空间则含 namespace: 的整体)必须全部落在 isIdentifierStart/isIdentifierPart 内,否则返回 null 退回对象形式;并把该测试的期望值改成 assertNull(writeFlat(...)),需要断言「省命名空间」时换一个合法名字(如当前同一测试里的 collision-freecollisionfree)。
建议补一条通用回归:对 writeFlat 的结果再跑一次 parseValue 并断言结构相同——本轮新加的几个用例(含这一条)只要接上 MathFlatAssertions.assertRoundTrip 就能立刻暴露。

⚠️ 警告

1. 本轮新增的嵌套深度上限在 parseUnary 的符号链路径上不可达

guardDepth() 只包在 parseLambda()(198-216)与 parsePower()(332-340)上,注释写的是「递归下降的入口都要先过这里」。但 parseUnary()(319-330)自身是直接递归、且不经过这两个入口:

if (this.match('-')) return this.call("subtract", ConstantFunction.of(0).call(), this.parseUnary());  // ← 每个 '-' 一帧
if (this.match('+')) return this.parseUnary();

等价转写按同样位置插桩后的读数:

输入 结果 守卫计数最大值 parseUnary 递归层数
"("*5000 + "1" + ")"*5000 nests too deeply(depth=513)✅ 513 256
"1" + "^1"*5000 nests too deeply 513 512
"-"*60000 + "1" parsed(守卫全程未触发) 1 60000
"+"*60000 + "1" parsed 2 60001

递归帧数与输入字符数 1:1,计数器在这条路径上永远到不了 512 ⇒ StackOverflowErrorErrorparseResultcatch (RuntimeException) 拦不住)仍会从 codec 穿进数据包加载流程——正是这次加守卫想堵的那条链,只是换了入口。按注释里「2000 层括号就足以打穿默认栈」的同一口径,几千个 -/+ 量级的输入即等效。
修法:让 parseUnary 的符号递归也走 guardDepth,或改成先循环收集符号、解析完再逐层套 subtract(0, ·)——两处都是几行。

2. LambdaFunction.apply 仍无递归守卫(上轮 ⚠️2 存活)

CustomFunction.applyMAX_CALL_DEPTH=64 + ThreadLocal 计数(CustomFunction.java:44-98),LambdaFunction 没有。lambda 同样是 FUNCTION_KEY 的合法条目(数据包 JSON "type": "anvillib:lambda",或下游 DeferredRegister),并且按名调用它会走 LambdaFunction.apply → body.evaluate(...)——于是注册两个 body 互调的 lambda(anvillib:a 的 body 为 b(1)anvillib:b 的 body 为 a(1))就能在不经过带守卫类型的情况下把 Java 栈打穿;同样的环如果写成两个 CustomFunction,会在 64 层抛出可读的 IllegalStateException
修法:把上限搬到共用位置(IFunctionapply(Call) 默认实现,或 LambdaFunction.apply 里复用 CustomFunction 的那套计数),让「能注册进注册表、能互相引用」的节点类型受同一约束。

💡 建议

  1. fuzz 叶子集合仍未含 lambdaMathFlatAssertions.randomLeaf 只有常量 / InputFunction / NamedFunctionrandomTree 还显式排除 FOREACH)。本轮修复只被 4 个手写用例覆盖;把 lambda(单参/变参)与 FOREACH 补进叶子集合,作者自带的随机往返测试就能覆盖这类「新语法节点 × 操作数位置」的组合——上一轮漏掉的正是这个维度。
  2. 三处注释/javadoc 已与实现不符
    • FlatExpressionWriter.java:283-284 写「因此一律退回对象形式」,实现是返回 trueoperand() 补括号(行为是对的,注释会误导后续维护者);
    • MathFlatAssertions.java:58 仍写「个别形状(取负一个负常量)没有合法的 flat 写法,这时编码退回对象形式」——该形状上轮已改成可写;
    • IFunction.bind@throws 只列了「实参个数不符 / 列表用在固定形参位」,本轮新增的「$(x...) 铺开的实参多于该位所需」也抛 IllegalStateException,建议一起写上。
  3. IFunction.java:183-190 的未检查强转((IExpression.Reference) arguments.get(argument)).name()。当前安全(Many 只可能来自 Spread),但这份安全依赖的是别处的不变量;改成 instanceof/把名字随 Value 一起携带更稳。
  4. clearCache() 挂在 OnDatapackSyncEvent 上会在每个玩家进服时清空缓存(该事件在进服与 /reload 都会触发)。功能上无害,但每次进服都会丢掉整表缓存;按 event.getPlayer() == null 过滤成「仅全员同步/重载」更贴合意图。
  5. (记录)NamedFunctionIExpression.Reference.Named 仍是同一语义的两套节点,flat 文本只能产出后者——两轮未动,仅供后续决定是否合并。

🟢 看起来不错

  • 上轮的 🔴/⚠️/💡 基本全清,且每一项修复都配了定向回归用例lambdaOperandsStayWritablenegationOfNegativeConstantStaysWritablenonFiniteLiteralRejectedtooDeeplyNestedIsRejectedforEachChecksArityOnDirectConstructionbuiltInNameCollisionFallsBackToObjectvariadicNotLastBindsByConsumptionOrder),回归读数也从「1636 parse-fail」变成全 0。
  • needsParentheses 的修法同时兼顾了两侧:补括号 + 并置过滤,且注释解释了「-> 绑得比所有运算符都松」,2(x -> $(x)) 这类最危险的并置吞并路径被单独拦住。
  • 缓存治理这次落到实处:CACHE_CAPACITY=512 的 LRU + 主动 clearCache() + 事件挂钩,javadoc 也把「值强引用键 ⇒ 弱键回收不掉」的原因写清楚了。
  • bind 的形参绑定改成「按形参逐个 take」并补上「铺开过多」的可读报错,Parameters 注释与实现的自相矛盾也一并改对。
  • 死代码清理干净(Operator.UNARYstripDefaultNamespaceArguments.boundParameters.bind),modules.json 顺序/对齐、IExpression.encodeDataResult.errorFOREACH 直构 arity 校验都到位。
  • 新模块接线完整(settings.gradle / gradle.properties / mods.toml / mixins.json / icon.png / module.main 的 jarJar / .github/modules.jsonneeds:["util"]),本轮 diff 仍能全量 apply、CI 无 ghost。

🧪 测试建议

被测目标 推荐场景 优先级
FlatExpressionWriter.writableName() 表驱动:anvillib:{sqrt,x,y,z,x0,x12,collision-free,utils/triple},断言不可写的返回 null(现在只断言了文本) 🔴
writeFlat → parseValue 回读不变量 把新增用例接上 MathFlatAssertions.assertRoundTrip,断言写出文本能读回同一棵树 🔴
FlatExpressionParser.parseUnary() 符号链 "-".repeat(N)+"1""+".repeat(N)+"1" 应报 nests too deeply 而非 StackOverflowError ⚠️
LambdaFunction.apply 两个 body 互调的注册 lambda → 应抛可读错误;与 CustomFunction 的 64 层行为对齐 ⚠️
IFunction.bind 变参/列表 已覆盖 [a,x...,b][x...,b];建议再补「$(xs...) 恰好用尽」「两个 spread 拼满变参位」 💡
MathFlatAssertions.randomLeaf 叶子集合加入 lambda 与 FOREACH,让自带随机往返覆盖操作数位置 💡

结论: REQUEST_CHANGES — 本轮修复方向、质量与测试配套都很好(lambda 操作数、缓存生命周期、深度上限、CCE、arity、死代码清理全部落实,回环 fuzz 由「1636 例读不回」变为全 0)。剩下的两条 🔴 都属于「encode 写得出来、decode 读不回/读错」的静默数据改变,恰好是 FlatExpressionWriter 类注释承诺的那个不变量,且修法都集中在 writableName() 一处;⚠️1 是本轮新守卫的可绕过入口。这四处补完(含把固化缺口的测试期望值改掉)即可合并。

标题gh auth status 报 token invalid(Active account 存在),按 webhook 规矩未执行 gh pr edit;建议标题 Add math module: flat expression parsing & evaluation 新增数学模块:表达式解析与求值库(本次未自动修改)。


由 Hermes Agent 审查

@Gugle2308

Copy link
Copy Markdown

💾 Self-improvement review: Patched SKILL.md in skill 'text-codec-roundtrip-pr-review' (1 replacement). · Patched references/anvillib-math-pr122-findings.md in skill 'text-codec-roundtrip-pr-review' (1 replacement).

@Gugle2308

Copy link
Copy Markdown

🌿 Roseau API Breaking Change Report

Module Status Breaking Changes
codec ✅ Compatible 0
collision ✅ Compatible 0
cube ✅ Compatible 0
config ✅ Compatible 0
integration ✅ Compatible 0
moveable-entity-block ✅ Compatible 0
network ✅ Compatible 0
rendering ⚪ Skipped
space-select ✅ Compatible 0
font ✅ Compatible 0
util ✅ Compatible 0
explosion ✅ Compatible 0
rpc ✅ Compatible 0
multiblock ✅ Compatible 0
recipe ✅ Compatible 0
registrum ✅ Compatible 0
sync ✅ Compatible 0
wheel ✅ Compatible 0
main ✅ Compatible 0

Full CSVs: see the Artifacts section of this workflow run.

@Gugle2308

Copy link
Copy Markdown

⚠️ Dangerous command requires approval:

cd /tmp && rm -rf anvillib && git clone --depth 1 -b dev/1.21.1 https://github.com/Anvil-Dev/AnvilLib.git anvillib 2>&1 | tail -3 && cd anvillib && echo "--- Codec.of usage ---" && grep -rn "Codec\.of...

Reason: recursive delete

Reply /approve to execute, /approve session to approve this pattern for the session, /approve always to approve permanently, or /deny to cancel.

@Gugle2308

Copy link
Copy Markdown

⚠️ Dangerous command requires approval:

cd /tmp && curl -sL "https://api.github.com/repos/Anvil-Dev/AnvilLib/pulls/122" -o pr122.json && grep -o '"title": "[^"]*"' pr122.json | head -2; grep -o '"state": "[^"]*"' pr122.json | head -2; grep ...

Reason: script execution via -e/-c flag

Reply /approve to execute, /approve session to approve this pattern for the session, /approve always to approve permanently, or /deny to cancel.

@Gugle2308

Copy link
Copy Markdown

代码审查总结

结论:已审查 💬(草稿 PR;无关键问题,1 个警告 + 6 个建议,均不阻塞;草稿状态下即可讨论)

PR: #122 — Add math lib 添加数学库
作者: @QiuShui1012
修改文件: 38 个(+4830 −0 / 5 commits,全部为 module.math 新增 + 构建接入)

审查范围与取证

  • curl patch-diff.githubusercontent.com 拉取全量 diff(5090 行 / 38 文件 / 39 hunk,无 ghost 文件、无 EOF 缺换行、无删除文件)。
  • 交叉核对基分支仓库文件:settings.gradle.github/modules.json.github/workflows/generate-matrix.jspull_request.ymlci.ymlmodule.main/build.gradlemodule.util/codec/multiblockgradle.properties + build.gradle + anvillib_*.mixins.json
  • CI:head d5ff59e 的 29 个 check-run 全部 success,含 build-l2 (math, anvillib-math, anvillib_math) / build(即 :anvillib-math-neoforge-1.21.1:build,含 check → test,两处 2×20000 轮往返 fuzz 属该任务);作业日志匿名不可读(403),故测试执行细节以 job 结论为准,未本地复跑。

⚠️ 警告

  • FlatExpressionParser.java:56-63,130(CACHE / parseValue) — 解析缓存的生命周期只覆盖了服务端。注释里对「弱键回收不掉(值 → Holder.Reference.owner → 注册表)」的分析是对的,LibCacheReloadHandler.java:23,31 的两个事件也确实是清除时机;但 ServerStartedEventOnDatapackSyncEvent 都只在服务端触发。在连远程服务器的客户端上,IExpression.STREAM_CODECByteBufCodecs.fromCodecWithRegistries)解码 flat 文本时会走同一条 parseValue 路径,而客户端每次(重)连都会拿到一份新的数据包注册表实例 → CACHE 每次多一个条目,且该条目永久持有旧注册表 + 最多 512 棵表达式树。玩家反复换服/重连时这是单调增长(积分包体量下可达 MB 级),且代码里没有任何一处会在客户端清理它。建议:客户端断开路径也清一次(ClientPlayerNetworkEvent.LoggingOut 之类),或者既然弱键本来就回收不掉,索性把 CACHE 换成普通 HashMap 并把「必须显式 clearCache」写进契约——否则 WeakHashMap 会让读者误以为生命周期是自动的。

💡 建议

  1. FlatExpressionParser.java:493,498parseIdentifier 的注册表函数两个分支) — 注册表函数在解析期不校验实参个数,而内建函数走 callChecked 校验。数据包函数 mymod:triple(1)(形参 2 个)能正常解析并落进存档,直到求值时才由 IFunction.bindIllegalArgumentException(那时已在 BE tick / 数据包加载深处)。解析期就能拿到 functions.get(key).value().parameters(),一次 checkArity(arguments.size()) 即可与内建路径统一。注意例外:自行覆写 apply(List, Arguments) 而不使用 parameters() 的类型(如内建的 FOREACH)不能盲加校验,FOREACH 自身参数区间是 [2, ∞),落在范围内不受影响。
  2. FlatExpressionWriter.java:269-276writableName — 对非 anvillib 命名空间过度保守:shadowed 是按 id.getPath() 查的,所以注册在 mymod:max / mymod:sqrt 的数据包函数一律被判为撞名而返回 null;但解析器对非 anvillib 命名空间是「先查注册表、查到就用」(:494-499),mymod:max(...) 本来就能正确读回。建议把撞名检查限定在 id.getNamespace().equals(MAIN_ID) 时。当前后果只是「安全但没必要地退化成对象形式」,不会写坏数据。
  3. IFunction.java:57-79CODECCodec.of(Encoder, Decoder) 的语义是 encode 取第一个、decode 取第二个,因此匿名 codec 里覆写的 decodeDIRECT_CODEC.parse)是死代码;实际解码走 HOLDER_CODEC.map(Holder::value)RegistryFileCodec,同时吃 id 与内联对象)。行为符合注释意图,但那半截 decode 会让人误以为解码走 DIRECT_CODEC、且不依赖 RegistryOps(实际依赖)。建议删掉不可达的 decode,或加一行注释点明解码来源。
  4. LibBuiltInFunctions.type()LibFunctionTypes 的 5 个 Type — 取注册表引用的方式不一致:内建走 LibRegistries.FUNCTION_TYPE.getHolderOrThrow(TYPE.getKey()).value()(注释说是为绕开 DeferredHolderBuiltInRegistries 反查),其余 5 个走 LibFunctionTypes.INPUT.get()(即被绕开的那条路)。两者都只在运行时被 DIRECT_CODEC / STREAM_CODEC 的 dispatch 触发,所以要么存在会踩到 BuiltInRegistries 反查的时机(那 5 个也要改),要么不存在(内建的绕行可省)。想请作者确认是否有意为之。
  5. FlatExpressionParser.java:72MAX_NESTING_DEPTH = 512 — 实际语义比注释暗示的更小:一层括号/一层实参嵌套要花 3 次计数(parseLambda + parseUnary + parsePower),所以可用嵌套约 170 层(第 171 层超限);只有一元符号链是 1 个/次(512 个)。测试只钉了两端(5000/20000 层被拒、"-"*100 可过),中间阈值没有测试固定。建议补一条边界用例并把实际阈值写进 javadoc,否则以后谁挪动守卫位置都会静默改变可用深度,而这类表达式很可能是程序生成而非手写。
  6. .gitignore 新增 logs/ — 与本 PR 其余改动(新模块)无关,建议拆到独立提交或至少在描述里提一句。

附注:anvillib_math.mixins.json 里的 compatibilityLevel: "JAVA_8"refmap: "anvillib.refmap.json" 是仓库现有 6 个模块一致的模板默认值(util/codec/font/integration/multiblock 全同),并非本 PR 引入的偏差,故不作为问题提出;若日后要统一改,建议单独一个 PR。

✅ 表现良好

  • 文本↔表达式的往返不变量被当成一等公民处理:负字面量、指数记法、-0.0 符号位、常量并置(2*3 → 23)、并置右侧可接性、anvillib:<内建名> 撞名拒绝写出——正是这类 codec 最容易静默写坏存档的几类坑,都覆盖了;isWritableFunctionName 做到读写侧共用一份判断,避免了规则漂移。
  • 断言把「只要写出成功,写出的文本必须读回同一棵树」立为不变量(assertWrittenTextReadsBack),并配 2×20000 轮随机树 fuzz,随机数覆盖 10^-300~10^300、整数边界、-0.0、lambda 落在运算符操作数位置等维度。
  • 深度守卫(嵌套 512 / 调用深度 64)把 StackOverflowError 挡在 codec 之外——这点尤其正确,Error 不是 parseResultcatch (RuntimeException) 能接住的。
  • 新模块接入齐全且自洽:modules.json{module: math, needs: [util]}(与代码里 dev.anvilcraft.lib.v2.util.ISerializer 的真实依赖一致)、settings.gradle include + 重命名、module.main 开发/发布两条路径都 jarJarmodule.*/src/** 已覆盖 module.math 无需改 workflow 的 pathsgenerate-matrix.js"math" 推出的 anvillib-math / anvillib_mathsettings.gradlegradle.properties@Mod/@EventBusSubscriber 的 modid 完全对得上。
  • LibRegistries 的 javadoc 明确写出「模块 id 是 anvillib_math,两个注册表都挂在 anvillib 下;该命名空间是所有模块共用的」——这种跨模块约定能写下来很难得。

🧪 测试建议

被测目标 推荐测试场景 优先级
parseIdentifier 注册表分支 / IFunction.bind 数据包函数实参个数不符时,断言在 parseValue 期就抛错(对应建议 1) 🟡
FlatExpressionWriter.writableName 注册 mymod:maxmymod:sqrt 后断言 writeFlat 写出 mymod:max(...) 且能读回(对应建议 2) 🟡
FlatExpressionParser.guardDepth 嵌套边界:"f("*180+")"*180 应报 nests too deeply、170 层应通过,钉住 512 常量的实际语义 🟢
CACHE / clearCache 用两个不同 RegistryAccess 解析同一段文本,断言 CACHE 不随注册表实例累积、clearCache 后旧注册表可被 GC(对应建议 3) 🟢

标题: 现标题「Add math lib 添加数学库」已是「英文 + 中文」格式,无需修改。(另:本机 gh auth status 为未认证状态,即使需要改标题也无法执行 gh pr edit——如需调整请人工处理。)


由 Hermes Agent 审查

@Gugle2308

Copy link
Copy Markdown

💾 Self-improvement review: Patched SKILL.md in skill 'hermes-approvals' (1 replacement).

@Gugle2308

Copy link
Copy Markdown

🌿 Roseau API Breaking Change Report

Module Status Breaking Changes
codec ✅ Compatible 0
collision ✅ Compatible 0
cube ✅ Compatible 0
config ✅ Compatible 0
integration ✅ Compatible 0
moveable-entity-block ✅ Compatible 0
network ✅ Compatible 0
rendering ⚪ Skipped
space-select ✅ Compatible 0
font ✅ Compatible 0
util ✅ Compatible 0
explosion ✅ Compatible 0
rpc ✅ Compatible 0
multiblock ✅ Compatible 0
recipe ✅ Compatible 0
registrum ✅ Compatible 0
sync ✅ Compatible 0
wheel ✅ Compatible 0
main ✅ Compatible 0

Full CSVs: see the Artifacts section of this workflow run.

@Gugle2308

Copy link
Copy Markdown

⚠️ Dangerous command requires approval:

curl -sL "https://api.github.com/repos/Anvil-Dev/AnvilLib/pulls/122" | python3 -c "
import json,sys
d=json.load(sys.stdin)
for k in ['title','state','merged','additions','deletions','changed_files','c...

Reason: Security scan — [HIGH] Pipe to interpreter: curl | python3: Command pipes output from 'curl' directly to interpreter 'python3'. Downloaded content will be executed without inspection.
Safer: tirith run https://api.github.com/repos/Anvil-Dev/AnvilLib/pulls/122 — or: vet https://api.github.com/repos/Anvil-Dev/AnvilLib/pulls/122 (https://getvet.sh); script execution via -e/-c flag

Reply /approve to execute, /approve session to approve this pattern for the session, /approve always to approve permanently, or /deny to cancel.

@Gugle2308

Copy link
Copy Markdown

💾 Self-improvement review: Patched SKILL.md in skill 'text-codec-roundtrip-pr-review' (1 replacement). · Patched references/anvillib-math-pr122-findings.md in skill 'text-codec-roundtrip-pr-review' (1 replacement).

@Gugle2308

Copy link
Copy Markdown

🌿 Roseau API Breaking Change Report

Module Status Breaking Changes
codec ✅ Compatible 0
collision ✅ Compatible 0
cube ✅ Compatible 0
config ✅ Compatible 0
integration ✅ Compatible 0
moveable-entity-block ✅ Compatible 0
network ✅ Compatible 0
rendering ⚪ Skipped
space-select ✅ Compatible 0
font ✅ Compatible 0
util ✅ Compatible 0
explosion ✅ Compatible 0
rpc ✅ Compatible 0
multiblock ✅ Compatible 0
recipe ✅ Compatible 0
registrum ✅ Compatible 0
sync ✅ Compatible 0
wheel ✅ Compatible 0
main ✅ Compatible 0

Full CSVs: see the Artifacts section of this workflow run.

@Gugle2308

Copy link
Copy Markdown

⚠️ Dangerous command requires approval:

cd /tmp; rm -rf pr122x; mkdir -p pr122x; extract(){ f="$1"; START=$(grep -n "^diff --git" pr122.diff | grep -F "$f" | head -1 | cut -d: -f1); NEXT=$(awk -v s="$START" 'NR>1 && $1+0 > s+0 {print $1; ex...

Reason: recursive delete

Reply /approve to execute, /approve session to approve this pattern for the session, /approve always to approve permanently, or /deny to cancel.

@Gugle2308

Copy link
Copy Markdown

代码审查摘要 — PR #122

操作: synchronize当前为 draft,8 个提交)
范围: 39 个文件 / 5781 行 diff(35 新增文件、0 删除;30 Java、6 测试类;md5 0bb85e63…changed_files=39 与 diff 文件数一致,无截断、无 ghost 文件、无 EOF 缺失)
分支: math/1.21.1dev/1.21.1

新模块 module.mathanvillib_math):flat 表达式语法(2x2$(cost)+1x -> $(x)*2)的 parser + writer、函数注册表(函数类型注册表 + 函数数据包注册表)、内建函数枚举与约 80 个单元测试(含三轮随机往返 fuzz,共 60000 轮)。

🟢 新模块接入检查表(本 PR 无一遗漏)

接入点 状态 证据
settings.gradle include + 项目改名 include 'module.math' / anvillib-math-neoforge-1.21.1
.github/modules.json 构建矩阵 { "module": "math", "needs": ["util"] },拓扑层在 util 之后
module.main 聚合(jarJar 两支) NOT_DEVproject(...) 分支各加一行
gradle.propertiesmod_id=anvillib_math generate-matrix.jsanvillib_${module} 推导一致,CI 能定位到
neoforge.mods.toml + mixins.json 命名 ${mod_id}.mixins.jsonanvillib_math.mixins.json
测试接入 CI build test{useJUnitPlatform} + addModdingDependenciesTo(sourceSets.test).gitignorelogs/(Bootstrap 会写日志)
公开 API 复用 IFunction.Type 正确复用 dev.anvilcraft.lib.v2.util.ISerializer,代码未私设一套

🔴 关键

  • FlatExpressionParser.parseNamed vs IExpression.Reference 无对象形式 —— 解析接受的 $(...) 载荷可能永远编码不出去
    • 解析侧(parseNamed)只按 ) 截断并 trim,不校验字符集,因此 $(cost-1)$(a b)$(成本)$(x.)$()$(...) 都能成功解析成 Reference.Named/Spread
    • 写侧 FlatExpressionWriter.bareisWritableReferenceName 正确挡住它们并返回 Optional.empty(),注释写的是「必须退回对象形式」——Reference 根本没有对象形式IExpression.FLAT_OR_OBJECT_CODEC.encode 对非 FunctionExpression 直接 DataResult.error("Cannot encode … only FunctionExpression has an object form")
    • 结果:含这类 Reference 的表达式树 flat 写不出、对象也写不出 → 编码硬失败(不是退回、也不是静默改值)。可达路径:
      1. 数据包函数:Parameter 只拒绝空名与 ... 结尾,"parameters":["cost-1"], "body":"$(cost-1)*2" 完全合法 → 加载成功、求值正常;
      2. LibRegistries.registerDataRegistries 给函数注册表传了 networkCodec(注册表要同步给客户端),同步时逐条 encode → 玩家登录/注册表同步抛异常
      3. 任何 BE 存了这种表达式时的 NBT 保存同理。
    • 为什么测试没兜住:unwritableReferenceNamesFallBackToObject 用的是 NamedFunction.of("a b")对象形式,能退回),而解析器产出的是 Reference,两者是"同一个概念、两种节点",只有后者没有对象形式。
    • 建议任选其一:(a) parseNamedisWritableReferenceName 校验,让解析期就报错(与写侧对称,符合本 PR「写不出来的绝不落进存档」的原则);(b) 给 Reference 补对象形式(等价于 named 函数定义)——IExpression.ref 是公开 API,下游也能造出这种节点;(c) 同时在 Parameter/Parameters 限制形参名可写字符集,从源头堵住参数名驱动的 $(...)

⚠️ 警告

  • LibBuiltInFunctions.FOREACH 漏用 Arguments.isListvalues.addAll(inputs.list(name)) 对「名字拼错/未绑定成列表」静默当空列表,forEach($(typo...), x -> x) 得 0;而 IFunction.bind 在同样情况下抛 $(name...) is not bound to a listisList 的 javadoc 明确说它存在就是为了区分「名字写错了」,FOREACH 是唯一一个消费名字却没调它的地方 → 拼错的名字应该报错而不是算成 0。

💡 建议

  • 写出自校验的代价FlatExpressionWriter.write 每次编码都做「写→parse→再写→同义比较」,而 IExpression.STREAM_CODECByteBufCodecs.fromCodecWithRegistries(CODEC))也走这条路,所以每次网络同步/存档都要额外一次完整解析 + 一次 bare + 一次递归 sameMeaning,并把生成的文本塞进 512 条 LRU 解析缓存(挤掉真正被复用的条目)。若表达式会随 BE 数据频繁同步,建议留一条「可信来源直写」或缓存写出结果的快路径。
  • 网络编解码路径零测试:测试全部走 JsonOps + RegistryOpsIExpression.STREAM_CODEC / FunctionExpression.STREAM_CODEC / IFunction.HOLDER_STREAM_CODEC 在测试里没有任何调用(唯一命中是注释),IExpression.LIST_CODECIFunction.CODEC 在仓库内也无调用点。既然是 BE 数据同步/存档的真实路径,建议补一个 RegistryFriendlyByteBuf 往返用例。
  • IFunction.CODEC 编码器是 O(n) 扫描:每次 encode 都 lookup.listElements().filter(ref -> input.equals(ref.value())),函数一多、同步一频繁就会累积;建议改为按值建索引,或在 javadoc 里写明代价。
  • FlatExpressionParser.error() 把整段原文拼进消息… of expression "…"):本轮测试里 5000 层括号那个用例会把上万字符带进异常与日志,建议只截取报错位置附近的片段。
  • lambda 形参错误的报错口径splitParameters/Parameters.parse 抛的 IllegalArgumentException(重名、非法名)被 parseLambdacatch 吞掉并回退成普通表达式解析,于是 (a, a) -> $(a) 报的是 expected '(' after function 'a',与真实原因(重名)毫无关系。建议对「已经吃到 ->、只是参数列表非法」的情况给出真实原因。
  • evaluate 的异常契约不完整:javadoc 只说「除零、负数开方、下标越界、名字未绑定不抛异常」,但实际还会抛:$(name...) 未绑成列表(IllegalArgumentException)、列表落在固定形参位或铺开实参超出该位(IllegalStateException)、深度超限(IllegalStateException)。下游在 BE tick 里直接 evaluate 时这些就是崩溃点,建议明确写出或在 API 上提供「安全求值」入口。另外 evaluateInt(int) Math.round(...),超过 int 范围会静默回绕(如 3e9 → -1294967296),值得在 javadoc 里点一句。
  • README 未更新:仓库 README 有模块表格与「模块介绍」分节(Registrum/Util/Wheel 等)以及依赖坐标示例,新增 anvillib-math 未进表格/章节。

🟢 看起来不错

  • 本类 PR 的三个经典回环坑全部被显式处理:常量并置(2*3 → 23)不并置、负字面量走带符号字面量 + startsWith("-") 补括号((-2)^2-2^2 的语义差被钉死)、Double.toString 指数阈值用 BigDecimal.toPlainString + 逐位去零复验规避开;-0.0 保符号位、非有限值解析期即拒。
  • 并置乘法不是「凭首字符猜」juxtapositionReadsBack 实测读回同一棵树,2*e1(x) 这种被指数记法吃掉的情况真的被挡住(e1/e2/e1abc/e12x 有专门用例)。
  • 写出即自证write() 要求「写出的文本再解析、再写出完全相同」且 sameMeaning 成立,否则退回对象形式 —— 实测这层网真的兜住了我试出来的 InputFunction(-1)x-1 这类假稳定情况(sameFunction 按枚举名/注册键比较后会判否)。
  • 递归深度守卫parseLambda/parseUnary/parsePower 三处守卫 + 169 层边界钉死,把 StackOverflowErrorErrorcatch(RuntimeException) 拦不住)挡在 codec 之外;IFunction.MAX_CALL_DEPTH 覆盖了自引用与「只穿过 lambda」的互引用。
  • 缓存存活期诚实:javadoc 明说 WeakHashMap 弱键因「值强引用键」回收不掉、存活期是手动的,并挂好了 ServerStartedEvent / OnDatapackSyncEvent(getPlayer()==null) / 客户端 LoggingOut 三个调用点,还给了容量上限。
  • 解析期 arity 校验(含 $(x...) 按区间参与)与内建/数据包函数统一口径,避免了「能落进存档、求值时才炸」。
  • 测试质量:结构 + 值双断言、归一化比较、随机树 fuzz + lambda 操作数 + 并置三类专项 fuzz,且 fuzz 轮数可由 -Pmath.fuzz.rounds 调整。

📋 结论

COMMENT(PR 仍为 draft,建议不要转 ready for review)

  • 先处理 🔴:Reference 没有对象形式导致的可解析但不可编码的 $(...) 载荷(建议走 (a) 解析期校验,最小改动且与现有设计一致)。
  • ⚠️FOREACHisList 是一行改动,建议一并修。
  • 💡 各项对合并不阻塞,draft 阶段可后续处理;STREAM_CODEC 测试建议在本 PR 内补上,因为它是最容易在运行时才暴露的路径。
  • 标题本身已含中英双语、含义准确,建议(因 gh auth status 显示 token 已失效,本次未自动修改)改为更具体的:Add math module: flat expressions, functions and datapack registry 添加数学模块

🧪 测试建议

被测目标 推荐场景 优先级
parseNamed / IExpression.CODEC 编码 $(cost-1)$(a b)$(成本)$(...) 解析后再 encode,断言不抛异常(当前返回 DataResult.error 🔴
IExpression.STREAM_CODEC RegistryFriendlyByteBuf 往返:常量 / 文本 / 对象三支 + 含 Reference 的树 🔴
LibBuiltInFunctions.FOREACH $(typo...) 未绑定成列表时期望报错而非返回 0 🟡
IFunction.HOLDER_STREAM_CODEC Holder.direct 内联定义与注册表引用的编解码对称性 🟡
FlatExpressionParser.error 超长原文只截取报错片段(日志膨胀回归) 🟢

由 Hermes Agent 审查

@Gugle2308

Copy link
Copy Markdown

💾 Self-improvement review: Patched SKILL.md in skill 'text-codec-roundtrip-pr-review' (1 replacement). · Patched references/anvillib-math-pr122-findings.md in skill 'text-codec-roundtrip-pr-review' (1 replacement).

@Gugle2308

Copy link
Copy Markdown

代码审查摘要 — PR #122 Add math lib 添加数学库

操作: synchronize(draft,未合并)
head: b2a777581(2026-09-15T17:16Z,9 commits;本轮为上两次提交 6cc5558/b2a7775 之后的新修订)
范围: 39 个文件(35 新增 / 0 删除 / 4 修改)· 5574 additions · 0 deletions
其中 11 个 main Java + 7 个 test Java + module.math/build.gradle 288 行 + 资源/CI 接线

说明:本 PR 描述(body)为空,因此下方用「标题声称 → 实际内容」核对;纯库模块,已按库/API 层审查口径(跳过方块/渲染/资源生成类检查)。
审查期间 CI:build-l0 8/8 全绿、processor-build 成功、build-l1 进行中;math 所在的 build-l2/roseau-l2 尚未开始(这是本 PR 的合并闸门,评审时无法看到结果)。


🔴 关键

未发现需要阻止合并的问题。回写路径有运行时保险丝(见下方 🟢),未发现「写得出来但读回去变意思」的静默数据损坏。


⚠️ 警告

1. IFunction.bind — 空列表 $(x...) 落在「变参之后的固定形参位」时被误拒(最新提交引入的语义与实现不一致)

b2a7775 把变参下限改成了 0,并把「"x...": [] 是合法空调用」写进了 javadoc 与测试。但 bind 的实参游标只在「本形参实际吃到值」时才推进:

int variadicCount = total - parameters.fixedCount();
int argument = 0;
for (Parameter parameter : parameters.parameters()) {
    int take = parameter.variadic() ? variadicCount : 1;
    while (remaining > 0) { ...; argument++; }   // take == 0 时整段跳过,argument 不动
}

于是变参吃掉 0 个值后,它对应的那个 Many 仍留在 values 里,被下一个固定形参取到并撞上「列表只能传给变参位」的检查:

Parameters p = Parameters.parse(List.of("x...", "b"));
Arguments in = Arguments.of(List.of(), List.of("a"), List.of(new Arguments.Value.Many(List.of())));
IFunction.bind(List.of(IExpression.ref("a..."), ConstantFunction.of(1).call()), in, p);
// 期望:x -> Many([]),b -> 1
// 实际:IllegalStateException "Spread[name=a] is a list and can only be passed to a variadic parameter"

f(1)(省略空铺开)等价且能正常工作,因此这是「同一语义两种写法行为不一致」,且报错信息指向的是它本来就该在的变参位,会误导使用者。触发条件:函数形参里变参不在末位(Parameters 明确允许,且 BuiltInFunctionTest.variadicMayAppearAnywhere 钉住了这一点)+ 该变参位上传入的是绑定为空列表的 $(name...)。内建 forEach 因为覆写了 apply(List, Arguments) 不走 bind,所以本仓库内不触发,但数据包自定义函数(如 ["x...","b"])会。

建议:让 bind 显式记录每个实参被哪个形参消费(或先把 Many 归位到变参位)后再对固定形参位报错;并补一条 ["x...","b"] + 空 $(a...) 的测试。

2. 变参下限改成 0 后,三处 javadoc 仍是「至少一参」

  • Parameter 类注释:变参至少要有一个实参可吃(第 15 行)
  • LibBuiltInFunctions.MIN最小值,变参,至少一参。
  • LibBuiltInFunctions.MAX最大值,变参,至少一参。

而同一提交里 Parameter#minimumCount() 已改为「变参可以是零个」,BuiltInFunctionTest 也断言 MIN.minimumArity() == 0min()/max() 返回 0.0。三处注释与实现/测试互相矛盾,建议一并更新(Parameter#minimumCount 的方法注释本身是对的,可作为范本)。


💡 建议

3. FlatExpressionWriter#juxtaPositionable 注释与实现不符 — 注释写「并置只用于 2x2(x+1)2$(a) 这类写法,右侧只能是标识符、括号或 $(name)」,但实现已经去掉 '$' 分支(isLetter || '_' || '('),并且测试断言 2*$(x) 走显式乘号。属注释漂移,建议改成「右侧只能是标识符或括号;$(...) 一律显式写 *」。

4. 网络(stream codec)路径完全没有测试覆盖 — 全部测试只走 RegistryOps<JsonElement>MathTestBootstrap.ops()),没有任何 RegistryFriendlyByteBuf 往返用例;而 IExpression.STREAM_CODECFunctionExpression.STREAM_CODECIFunction.HOLDER_STREAM_CODECByteBufCodecs.holderRegistry)是下游同步的公开入口,LibClientCacheHandler 的注释也明确说客户端走 IExpression.STREAM_CODEC 解码 flat 文本。这里尤其要覆盖内建函数——它们到处是 Holder.direct(builtin)LibRegistries 注释里也写明「数据包注册表不能由代码注册,用它就用 Holder.direct」),而 JSON 侧靠 RegistryFileCodec 的内联分支能读回,网络侧是另一条实现。建议补一条 stream 往返测试(含 Holder.direct 内建函数 + 数据包函数引用),否则这条路径只能靠下游实测。

5. 回写每次 encode 都要「再解析一次 + 可能多次并置预检」 — 这是换取「写得出来就一定读得回去」的代价,设计上认同;但建议在 FlatExpressionWriter#write 的 javadoc 里点明这是 O(2×树规模) 的编解码开销,并说明它对高频路径(存档/同步每 tick 编码)的影响,避免下游把它当零成本。

6. 范围外的小改动.gitignore 新增 logs/ 与本次功能无关,可保留也可拆出(无影响,仅提示范围纯净度)。


🟢 看起来不错

  • 构建 / 发布接入完整(本类 PR 最高频的遗漏点):.github/modules.json 新增 { "module": "math", "needs": ["util"] }settings.gradleinclude + project(':module.math').name = 'anvillib-math-neoforge-1.21.1'module.main/build.gradle 两条分支(NOT_DEVlatest.release / dev 走 project(...))都已成对补齐。CI 是 .github/modules.json 驱动的动态矩阵(generate-matrix.js 拓扑分层,ci.yml/pull_request.ymlmodule.*/src/** 通配触发),math 落在 level 2,build-l2/roseau-l2 作业存在,模块命名(anvillib-math / anvillib_math / module.math)与 CI 推导规则完全一致。
  • 回写保险丝是本 PR 最稳的一环FlatExpressionWriter#write 在返回前实测「再解析 → 再写 → 文本完全相同 + sameMeaning」,任一不成立就返回空、由 IExpression.FLAT_OR_OBJECT_CODEC 退回对象形式。早期版本的 2*3 → 23pow(-2,2) → -2^2、负零丢符号位、1.0E-7 指数记法、2*e1(x) → 2e1(x)(被 parseNumber 当指数吞掉)等坑,现在既有实现(并置实测预检、text.startsWith("-") 补括号、BigDecimal#toPlainStringisWritableFunctionName/isWritableReferenceName 双向共用同一判断)也有测试逐条钉住,并用 3 组 20000 轮随机往返(普通树 / lambda 操作数 / 并置操作数)+ 固定种子兜底。
  • 健壮性设计:解析期用与求值期同一份 Parameters#checkArity 校验实参(内建与数据包函数报错口径统一)、MAX_NESTING_DEPTH = 512 且在一元符号链上也过守卫(StackOverflowErrorErrorcatch (RuntimeException) 拦不住,这点注释写得很清楚)、自定义函数/lambda 共用 MAX_CALL_DEPTH = 64 防环、解析缓存按注册表分组且有上限并在 /reloadOnDatapackSyncEvent#getPlayer() == null,与 NeoForge 21.1 语义一致)与客户端断开时清理。
  • 跨文件核对:LibRegistries@EventBusSubscriber(modid=...) + NewRegistryEvent / DataPackRegistryEvent.NewRegistry 写法与 module.recipe、AnvilCraft ModRegistryKeys 完全一致;CustomFunction javadoc 里的 data/<ns>/anvillib/function/*.jsonanvillib:custom 类型名和注册表键相符;build.gradle/gradle.properties(3 个字段)与 module.registrum 约定一致,toolchain 21、jarJar(implementation project(...)) 模式一致;测试已挂进 buildchecktest,CI 会真正跑到(这也是本模块首个 JUnit 测试集,module.util 现有的是 JavaExec 手写测试)。
  • TODO/FIXME、无调试输出、无硬编码凭据、无 EOF 换行缺失、无 CRLF。

📋 标题声称核对

声称 状态 对应实现
Add math lib / 添加数学库 新模块 module.math:表达式树(IExpression/FunctionExpression/Arguments)、flat 文本双向编解码(FlatExpressionParser 719 行 / FlatExpressionWriter 613 行)、内建函数与数据包函数注册表、lambda、7 个测试类
(附带)新增模块接入构建/发布 modules.jsonsettings.gradlemodule.main/build.gradle、CI 动态矩阵无需改动

标题已符合 <英文> <中文> 格式且描述准确,未修改


🧪 测试建议

被测目标 推荐测试场景 优先级
IFunction.bind Parameters.parse(["x...","b"]) + $(a...) 绑定为空列表落在变参位(当前误报,见 ⚠️1);Many 长度超过该位剩余需求的报错文案 🔴
IExpression.STREAM_CODEC / FunctionExpression.STREAM_CODEC / IFunction.HOLDER_STREAM_CODEC RegistryFriendlyByteBuf 往返:内建函数(Holder.direct)、数据包函数引用、含 lambda/custom 的树 🔴
FlatExpressionWriter#write 自校验失败时确实退回对象形式:编码结果不是字符串原语(如错误 arity 的程序化构造树) 🟡
FlatExpressionParser 缓存 512 上限淘汰行为;clearCache() 后旧注册表不被引用 🟡
Parameters#checkArity(List) $(x...) 落在无变参函数的固定位(sqrt($(x...)))应报「按非铺开个数」——已有测试,建议再加「两个铺开实参」边界 🟢
Arguments#isList 未绑定名字 vs 绑定空列表的区分(已有测试,保持) 🟢

结论: COMMENT — 设计与实现质量很高(回写自校验 + 解析期校验 + 深度守卫 + 缓存生命周期都很到位),构建/CI 接入无遗漏;无阻塞问题。建议合并前处理 ⚠️1(bind 空铺开误拒,附一条测试)与 ⚠️2(三处 javadoc 与新的变参语义对齐),并补一条网络 stream codec 往返测试(💡4)。另请把 PR 从 draft 转正后确认 build-l2 (math, ...)roseau-l2 两个作业通过再合并。


由 Hermes Agent 审查

@Gugle2308

Copy link
Copy Markdown

💾 Self-improvement review: Patched SKILL.md in skill 'text-codec-roundtrip-pr-review' (1 replacement). · Patched SKILL.md in skill 'ci-workflow-pr-review' (1 replacement). · Patched references/head-sha-freshness-and-prior-review-verification.md in skill 'github-webhook-pr-review' (1 replacement).

@Gugle2308

Copy link
Copy Markdown

🌿 Roseau API Breaking Change Report

Module Status Breaking Changes
codec ✅ Compatible 0
collision ✅ Compatible 0
cube ✅ Compatible 0
config ✅ Compatible 0
integration ✅ Compatible 0
moveable-entity-block ✅ Compatible 0
network ✅ Compatible 0
rendering ⚪ Skipped
space-select ✅ Compatible 0
font ✅ Compatible 0
util ✅ Compatible 0
explosion ✅ Compatible 0
rpc ✅ Compatible 0
multiblock ✅ Compatible 0
recipe ✅ Compatible 0
registrum ✅ Compatible 0
sync ✅ Compatible 0
wheel ✅ Compatible 0
main ✅ Compatible 0

Full CSVs: see the Artifacts section of this workflow run.

@Gugle2308

Copy link
Copy Markdown

🌿 Roseau API Breaking Change Report

Module Status Breaking Changes
codec ✅ Compatible 0
collision ✅ Compatible 0
cube ✅ Compatible 0
config ✅ Compatible 0
integration ✅ Compatible 0
moveable-entity-block ✅ Compatible 0
network ✅ Compatible 0
rendering ⚪ Skipped
space-select ✅ Compatible 0
font ✅ Compatible 0
util ✅ Compatible 0
explosion ✅ Compatible 0
rpc ✅ Compatible 0
multiblock ✅ Compatible 0
recipe ✅ Compatible 0
registrum ✅ Compatible 0
sync ✅ Compatible 0
wheel ✅ Compatible 0
main ✅ Compatible 0

Full CSVs: see the Artifacts section of this workflow run.

@Gugle2308

Copy link
Copy Markdown

⚠️ Dangerous command requires approval:

cd /tmp
rm -rf pr122src && mkdir -p pr122src
awk 'BEGIN{FS=":"} /^[0-9]+:diff --git/{print}' headers.txt > /dev/null
# build mapping via awk from headers.txt (format "N:diff --git a/x b/y")
awk -F: '{...

Reason: recursive delete

Reply /approve to execute, /approve session to approve this pattern for the session, /approve always to approve permanently, or /deny to cancel.

@Gugle2308

Copy link
Copy Markdown

⚠️ Dangerous command requires approval:

cd /tmp
curl -s "https://api.github.com/repos/Anvil-Dev/AnvilLib/commits/d401a887c900acb33e5c97097f003c378c78deb5" -o /tmp/c_d401.json
python3 -c "1" 2>/dev/null || true
echo "---- jq/tools ----"; whi...

Reason: script execution via -e/-c flag

Reply /approve to execute, /approve session to approve this pattern for the session, /approve always to approve permanently, or /deny to cancel.

@Gugle2308

Copy link
Copy Markdown

All prior findings verified as fixed; diff integrity confirmed (41 files / +6091 / −0, matching the API exactly). Writing the review.

代码审查摘要 — PR #122 Add math lib 添加数学库

操作: synchronize(draft,未合并)
head: d401a887(2026-09-16T06:27Z,10 commits)— 上两轮评论(#33 覆盖 6cc5558#35 覆盖 b2a7775)之后的新修订
范围: 41 个文件(37 新增 / 0 删除 / 4 修改)· +6091 / −0,与 API 逐项一致(changed_files=41additions=6091deletions=0
CI: build-l2 (math, anvillib-math, anvillib_math) / build = completed · success(上一轮评审时该作业尚未开始,现在闸门已通过)


✅ 上一轮(b2a7775)6 项发现——已逐项核实全部修好

上轮发现 状态 证据(当前 head)
⚠️ IFunction.bind 空列表 $(x...) 落在「变参之后的固定形参位」被误拒 ✅ 已修 IFunction.java:262-265 新增「变参自己空着时推游标」;234-238argument >= values.size() 前置检查
⚠️ 三处「变参至少一参」javadoc 与 minimumCount()==0 矛盾 ✅ 已修 Parameter 类注释改为「变参可以是零个实参」;MIN/MAX 改为「取不到值时返回 0」
💡 juxtaPositionable 注释漂移(仍写 2$(a) ✅ 已修 注释改为「右侧只能是标识符或括号;$(name) 一律显式写 *
💡 网络 stream codec 路径零覆盖 ✅ 已修 新增 StreamCodecTest(9 用例:Holder.direct 内建、数据包函数引用、lambda/custom 整树、字面量/对象两支、字节稳定性、holderRegistry 直柄)
💡 write 未点明自校验的编解码开销 ✅ 已修 write javadoc 新增「代价:O(2×) 树规模…高频路径应缓存编码结果」
💡 .gitignorelogs/ 属范围外 ⬜ 仍在 可选,不阻塞

bind 的新逻辑我按位置组合逐位核对过,未发现新漏洞:["x...","b"]+空 $(a...)x=[],b=1["a","x...","c"]+空 $(e...)a/x=[]/c 各就位;["b","x..."]+[$(a...),…] 也正确。variadicCount 不可能为负(checkArity(total) 先跑,minimumArity()==fixedCount)。


⚠️ 仍待处理

1. LibBuiltInFunctions.FOREACH 漏用 Arguments.isList(前轮已报、本轮未改)

LibBuiltInFunctions.java:177-178 仍自行解析铺开实参:

if (argument instanceof IExpression.Reference.Spread(String name)) {
    values.addAll(inputs.list(name));   // 名字拼错/未绑定 → 静默空列表

Arguments.list() 对「没绑定的名字」和「绑定成空列表的变参」都返回空(正是 Arguments.java:83-91isList 存在的理由,IFunction.java:210-213 也守着这条)。于是 forEach($(typo...), x -> $(x)) 静默返回 0,而同一个拼写错误在 min($(typo...)) 会抛出可读的 $(typo...) is not bound to a list。测试里 unboundSpreadNameIsReportedClearly 只覆盖了 min(走 bind)这条路径,forEach 没有对应用例。建议在 177 行前加同样的 inputs.isList(name) 守卫并补一条 forEach 用例。

2. 〔新〕variable() 会抛异常,但写侧调用方把它当布尔/可空判断用

FlatExpressionParser.variable()620-627)对 x + 溢出 int 的数字名抛 IllegalArgumentException("Input index is too large"),而它的两个写侧调用方并不期待异常:

  • isWritableFunctionName693)声明返回 boolean:数据包注册一个 anvillib:x3000000000 这类函数后,writableNamefunctionNamevisitCallbareFlatExpressionWriter.write 第 62 行(在 try 之外,try 从 65 行才开始),异常会直接穿透 FlatExpressionWriter.writeFlatExpressionParser.codec().encodeCodec.encode。而 FLAT_OR_OBJECT_CODEC.encode 是靠 encodeStart 返回失败 DataResult 才退回对象形式的——异常一来,「写不出来就退回对象形式」这条契约就断了,栈会一直抛到调用方(存档 saveAdditional / 网络同步路径)。触发面很窄(需要数据包函数命名为 anvillib:x<数字> 且数字 > 2³¹−1),但修起来很便宜。
  • resolveReferenceFlatExpressionWriter.java:131):同一名字的 NamedFunction 会被 sameMeaning 抛异常 → 被 writecatch 吞掉 → 永远退回对象形式(Reference.Named 因为 record 相等会提前返回,不受影响)。

对照:解析路径没这个问题——parseResultcatch (RuntimeException) 把同样的异常转成了 DataResult.error。建议二选一(推荐都做):(a) 让 variable() 对溢出下标返回 null,把「Input index is too large」留在 parseIdentifier 的解析路径上;(b) 把 bare(...) 也放进 write()try,让写侧任何 RuntimeException 都降级为对象形式而不是穿透 codec。


💡 建议

3. README 未更新,且现在已经与实现不符(前轮已报、未改)README.md 是模块仓库的对外入口,三处都缺 math:① 14-27 行的模块表格(Config…Wheel 共 12 项);② 「模块介绍」分节(无 Math 一节);③ 319-322 行「anvillib-neoforge-1.21.1 为聚合发行模块,默认打包并重导出以下子模块」的清单——本 PR 已在 module.main/build.gradlejarJar(api(…anvillib-math-neoforge-1.21.1…)),这份清单因此由「不完整」变成了「不准确」,建议一并补上 math 条目与依赖坐标示例。

4. Math.round 两处边界语义evaluateIntIExpression.java:103-105)与内建 ROUNDLibBuiltInFunctions.java:107-111):Math.round(NaN) == 0round(sqrt(-1)) → 0 而非 NaN)、超出 long 范围时饱和(round(1e300) → 9.223372036854776E18)、(int) 强转还会回绕(evaluateInt(3e9)-1294967296)。模块里其它函数(divide/sqrt)都刻意保留 NaN/Inf 语义,这两处值得在 javadoc 里点一句或显式 clamp。

5. Parameter/Parameters 形参名字符集(前轮 #33 的 (c) 建议,未改)——Parameter(canonical ctor)只拒绝空名与含 ... 的名字,CustomFunction.named(List.of("a b"), …)LambdaFunction.named(List.of("x)"), …) 都构造得出来。当前不会写坏数据(isWritableReferenceName / bare / 回写自校验三重兜底都会退回对象形式),只是给下游留了一类必然写不出文本的静默形状;从源头限制成标识符字符集可省掉这条路径。


🟢 看起来不错

  • 回写自校验仍然是本 PR 最稳的一环write() 返回前实测「再解析 → 再写 → 文本完全相同 + sameMeaning」,任一不成立即返回空并退回对象形式;juxtapositionReadsBack 实测并置可读回(挡 2*e1(x) → 2e1(x))、text.startsWith("-") 补括号、BigDecimal#toPlainString + trimTrailingZeros 处理 1.0E-7/0.0001 边界、负零保符号位、isWritableFunctionName/isWritableReferenceName 读写共用一份判断。我特意检查了 JUXTAPOSING/VERIFYING 两个 ThreadLocal 短路是否有「跳过预检 ⇒ 写出坏文本」的缝隙:由于接受前必须 bare(parseValue(text)).equals(text),更宽松的那次(短路)只要与保守那次(已预检)文本不同就会被拒,取不到坏结果。
  • StreamCodecTest 是真验证(比较形状 + 求值结果,不是只断言非空),补上了上轮点名的网络路径空白;ParseCacheTest 把「同一 registry 命中、clearCache 失效、超 512 淘汰、asLookup 稳定性」都钉住了。
  • 构建/CI 接入完整无遗漏:.github/modules.json 新增 { "module": "math", "needs": ["util"] }(落在 level 2,build-l2/roseau-l2 存在)、settings.gradleinclude + 重命名为 anvillib-math-neoforge-1.21.1module.main/build.gradle 两条分支(NOT_DEVlatest.release / dev 走 project(...))都成对补齐;generate-matrix.js 推导出的 anvillib-math / anvillib_math / module.math 与包名、archivesNamemods.toml 完全对得上。@EventBusSubscriber(modid=…) 不写 bus= 的写法与 module.recipemodule.multiblockmodule.util(ClientTickRecorder, 游戏总线事件) 一致,不是遗漏。测试已挂进 buildchecktest,CI 会真跑到。
  • 缓存生命周期:按注册表分组 + 上限 512 + /reloadOnDatapackSyncEvent#getPlayer()==null,我对着 NeoForge 源码确认了该语义)与客户端断连清理,javadoc 还诚实写明「弱键因值强引用键而回收不掉,存活期是手动的」。

📋 声称验证表(PR body 为空,按标题核对)

声称 状态 对应文件
flat 表达式解析(x*2/2x/$(name)/^/一元/隐式乘法/×÷·/lambda) FlatExpressionParser
反向回写(规范化文本 + 自校验 + 对象形式回退) FlatExpressionWriterIExpression.FLAT_OR_OBJECT_CODEC
函数类型 / 数据包函数双注册表,支持下游扩展 LibRegistriesfunction_typefunction)、LibFunctionTypesLibBuiltInFunctions
内建函数集(四则/乘方/abs/floor/ceil/round/sqrt/min/max/foreach) LibBuiltInFunctions(14 个)
值/表达式三种内联形式(数字 / flat 文本 / 对象) IExpression.CODEC
测试与构建接入 7 个测试类(~2000 行)+ modules.json/settings.gradle/module.main/build.gradle

结论: COMMENT(无阻塞项) — 上一轮全部发现已修好并各自补了测试,CI 的 math 构建已转绿,回写不变量与求值/解析校验都很扎实,我没有发现会静默写坏数据的路径。上面 ⚠️2 是本轮唯一的新发现(触发面窄但会让写侧异常穿透 codec,建议顺手加个兜底),⚠️1 与 💡3 是前一轮报过、本次未动的项。draft 状态下建议先处理 ⚠️1/⚠️2 与 README(💡3),再转 ready for review。

建议标题: Add anvillib-math module: flat math expression parser/writer 添加 anvillib-math 数学表达式模块(当前 Add math lib 添加数学库 格式已合规,只是没体现模块名与「解析/回写」这个核心能力;本次 gh auth status 显示 /opt/data/home/.config/gh/hosts.yml 的 token 已失效,为免触发凭据类操作未自动修改)。


由 Hermes Agent 审查

@Gugle2308

Copy link
Copy Markdown

💾 Self-improvement review: Patched references/anvillib-math-pr122-findings.md in skill 'text-codec-roundtrip-pr-review' (1 replacement). · Patched SKILL.md in skill 'text-codec-roundtrip-pr-review' (1 replacement). · Patched references/duplicate-review-detection-and-stop.md in skill 'github-webhook-pr-review' (1 replacement).

@Gugle2308

Copy link
Copy Markdown

🌿 Roseau API Breaking Change Report

Module Status Breaking Changes
codec ✅ Compatible 0
collision ✅ Compatible 0
cube ✅ Compatible 0
config ✅ Compatible 0
integration ✅ Compatible 0
moveable-entity-block ✅ Compatible 0
network ✅ Compatible 0
rendering ⚪ Skipped
space-select ✅ Compatible 0
font ✅ Compatible 0
util ✅ Compatible 0
explosion ✅ Compatible 0
rpc ✅ Compatible 0
multiblock ✅ Compatible 0
recipe ✅ Compatible 0
registrum ✅ Compatible 0
sync ✅ Compatible 0
wheel ✅ Compatible 0
main ✅ Compatible 0

Full CSVs: see the Artifacts section of this workflow run.

@Gugle2308

Copy link
Copy Markdown

⚠️ Dangerous command requires approval:

cd /tmp; curl -s "https://api.github.com/repos/Anvil-Dev/AnvilLib/pulls/122" -o pr122.json; python3 -c "
import json
d=json.load(open('/tmp/pr122.json'))
print('title:',d.get('title'))
print('state:',...

Reason: script execution via -e/-c flag

Reply /approve to execute, /approve session to approve this pattern for the session, /approve always to approve permanently, or /deny to cancel.

@Gugle2308

Copy link
Copy Markdown

代码审查摘要 — PR #122

操作: synchronize(标题:Add math lib 添加数学库,已是 <英文> <中文> 格式,无需改标题,未执行 gh pr edit
范围: 51 个文件 / 32 Java(23 主源码 + 9 测试)+ 19 非 Java(含 10 个 .log.gz),+6403 / −57 行
审查手段: 本机无 JDK,未实际编译或跑 gradle 测试;结论来自逐行阅读 + 与 dev/1.21.1 基线及其它模块(registrum/util/codec/sync/font/cube)交叉核对(本地 clone 用具名 ref 校验,未导出任何 token)。未发表任何 gh 评论。

📋 声称验证表(README Math 章节 vs 代码)

声称 状态 对应实现
flat 语法 2x/x^2/$(name)/x -> …,乘号 * × · FlatExpressionParser (parseMultiplicative×/·÷)
回写前自校验「再解析、再写一遍文本相同」 FlatExpressionWriter.write + sameMeaning
双注册表 function_type / 数据包 function LibRegistries + LibFunctionTypes(5 类型)+ builtin Type
内建函数列表(四则/pow/abs/floor/ceil/round/sqrt/min/max/foreach) LibBuiltInFunctions 枚举逐一对应
变参 "x..."、下限 0、可不在末位 Parameter/Parameters + IFunction.bind 的 variadicCount 配位
三种内联编码(数字/flat/对象)自动选择 IExpression.CODEC + FLAT_OR_OBJECT_CODEC
新模块 CI/构建接入 .github/modules.json {"module":"math","needs":["util"]}、settings.gradle include+rename、module.main jarJar(api math) 全齐(新模块 PR 最高频的遗漏点这里没踩)

🔴 关键(合并前处理)

  1. logs/debug-*.log.gz ×10 被提交(根目录 debug-1/3/4/5/6 + module.math/logs/debug-1..5)。基线 dev/1.21.1 既无 logs/ 也无任何被跟踪的 .log/.gz.gitignore 只写了 *.log 不覆盖 *.log.gz,所以是 git add . 顺手带进来的。内容已确认为本机运行日志(C:\Users\29569\AppData\Local\Temp、Java 21、netty 启动信息等)——体积很小但会把作者本机路径永久写进库历史。建议:git rm -r logs module.math/logs,并把 *.log.gz(或 logs/)补进 .gitignore

⚠️ 警告

  1. 链式运算符不受 MAX_NESTING_DEPTH 约束,树深无上限(FlatExpressionParser)。guardDepth 只挂在 parseLambda/parseUnary/parsePower 上,而 parseAdditive/parseMultiplicative循环1+1+1+…(N 项)解析期间 depth 每轮涨落回基线,永远不报错,但产出的树是 N 深的左脊。随后 IExpression.evaluate()(FunctionExpression→apply→bind→argument.evaluate)与 FlatExpressionWriter.bare() 都沿左脊递归 → StackOverflowError;它是 Errorwrite()catch (RuntimeException) 拦不住,parseResult 同理拦不住——正好是 javadoc 里写明要避免的「SOE 穿出 codec」。测试钉住了嵌套边界(5000 层括号/乘方、20000 个一元符号、169/512)却没有任何链长用例,所以这条静默存在。触发需要极长文本(数万项、几十 KB),属健壮性缺口而非日常 bug,但既然模块以「不让 SOE 穿出 codec」为目标,建议把链长/节点总数也纳入预算(或在 parseWhole 层限制源文本规模)。

  2. 对象形式(JSON)不校验实参个数。flat 文本走 checkedArityLibBuiltInFunctions.call/callChecked 也校验,但 FunctionExpression.MAP_CODEC 的解码路径完全不校验:手写数据包 JSON {"function":{"type":"anvillib:builtin","builtin":"add"},"arguments":[1]}(或 custom 函数体里同样的写法)能成功载入并落进存档,直到求值时 IFunction.bind 才抛 Expected 2 arguments but got 1——CustomFunctionTest#datapackArityIsCheckedWhileParsing 的注释里写明了这正是要避免的失败模式,只是对象分支漏了。建议在 FunctionExpression 紧凑构造器或 codec 上补 parameters.checkArity(arguments);注意 lambda 节点自身不带实参FunctionExpression.of(LambdaFunction…) 是零实参),必须按 instanceof LambdaFunction 跳过,$(x...) 则继续用已有的 checkArity(List) 区间逻辑。

💡 建议

  1. IExpression.STREAM_CODECByteBufCodecs.fromCodecWithRegistries(CODEC) 派生,意味着每次网络编码都要重跑「写 flat + 自校验(再解析一次)」,并置预检还会额外写/解析。javadoc 已承认 O(2×) 代价并把「缓存」推给下游,但这条正是默认路径;高频同步的 BE 会明显感到压力,可考虑给 STREAM_CODEC 一个按树缓存的编码实现(生命周期已有的 clearCache 语义可复用)。
  2. README.en.md 未同步:模块表与 ### Math Module 章节都没有(中文 README 已加)。它本来就缺 Collision/Font/Sync/Rpc/Explosion/Space Select(既有漂移),本次是继续扩大;另外 dev/26.1dev/1.21.8 都还没有 math 模块,后续需要跟进。
  3. .github/modules.json 新增行的 "needs" 列对齐与上下文的空行分组和相邻行不一致(纯排版)。
  4. IFunction.CODEC(「优先写注册引用、否则内联」)在模块内没有任何使用点,且其编码器会对 lookup.listElements() 做线性扫描。若是给下游准备的公开 API,建议补一个使用示例或测试;否则可考虑收敛可见性。

🟢 看起来不错

  • 回写正确性的工程手段很扎实write 的三步自校验(写出→解析→再写→sameMeaning)、并置前的 juxtapositionReadsBack 实测预检(防 2*e1(x) 被指数记法读成 multiply(20, x))、负常量/负零/指数记法/text.startsWith("-") 补括号,这几类都是同类 PR 最容易静默坏数据的地方,这里都堵住了,而且每条都配了测试(含 20000 轮 fuzz、lambda 当操作数、e 前缀注册名)。
  • 失败降级契约完整:写不出 flat 就退对象形式,且 writeFailureFallsBackToTheObjectBranch / writeSideFailuresNeverEscapeTheCodec 真的把「不抛异常 + 退对象」钉住了;IExpression.CODEC 对非 FunctionExpression 实现给 DataResult.error 而不是 ClassCastException。
  • 生命周期与守卫想得很全:求值期环守卫(MAX_CALL_DEPTH=64,CustomFunction 与 LambdaFunction 共用,注释解释了「环可以只穿过 lambda」)、解析期深度守卫及其「512 计数 = 169 层实嵌套」的显式说明、解析缓存容量 512 LRU + clearCache 的三个调用点(ServerStarted / DatapackSync(player==null) / ClientLoggingOut),连「弱键其实回收不掉」的成因都写在 javadoc 里。
  • 构建无漂移module.math/build.gradlemodule.registrum 逐行比对只多三处(addModdingDependenciesTo(sourceSets.test)test{ useJUnitPlatform + math.fuzz.rounds }、junit 依赖),且根 build.gradletest{} 只作用于根项目,所以这三处是必要而非冗余;空 mixins.json(JAVA_8 + required)与 util/codec/sync 约定一致,不是遗漏;mods.toml 依赖块与其它模块同形。
  • Javadoc 把边界行为都写明了(evaluateInt 的 NaN→0 / long 夹取 / int 回绕、min/max 空变参返回 0、Arguments.value(name) 对列表取最大值、$(name...) 只能喂变参位)。

🧪 测试建议

被测目标 推荐场景 优先级
FlatExpressionParser.parseValue 链式文本 "1+".repeat(20000)+"1" → 断言按预算报错,或断言 write/evaluate 不 SOE 🔴
IExpression.CODEC 对象形式 手写 {"function":{"builtin":"add"},"arguments":[1]} decode → 应返回失败 DataResult 🔴
IExpression.STREAM_CODEC 高频路径 同一棵树连续 encode 两次,确认无累积成本(配合 4 的缓存改动) 🟢

结论: REQUEST_CHANGES — 唯一阻塞项是那 10 个 logs/*.log.gz 二进制文件(一旦合并会永久留在 git 历史);代码本身完成度和测试质量都很高,第 2、3 条建议同期处理,其余为非阻塞改进。


由 Hermes Agent 审查(未编译/未运行测试:本环境无 JDK,结论基于源码逐行核对与跨分支交叉验证)

@Gugle2308

Copy link
Copy Markdown

💾 Self-improvement review: Patched SKILL.md in skill 'text-codec-roundtrip-pr-review' (1 replacement).

@Gugle2308

Copy link
Copy Markdown

代码审查摘要 — PR #122

操作: synchronize(PR 标题为 Add math lib 添加数学库,已含英文+中文,无需改标题)
范围: 42 个文件(37 新增 / 5 修改;0 删除),32 个 Java(23 main + 9 test),6711 行 diff;module.math 全新模块 + CI/README 接入
验证方式: 静态审查 diff + 与 dev/1.21.1 基线(浅克隆 /tmp/anvillib_base)交叉核对;未运行 Gradle 测试(本地无该项目的 MC 依赖环境),下面标注的问题均可由代码路径直接判定


🔴 关键

1. 解析器接受的 $(name) 载荷,有一部分永远无法再序列化(断掉「写不出来就退回对象形式」这条契约)

这是本 PR 自己反复强调的不变量被打破的唯一位置,且是真实可达的:

  • FlatExpressionParser.parseNamed(...)expression/FlatExpressionParser.java 第 424-437 行)只做到「读到 )、trim、非空」就返回 IExpression.ref(name)没有isWritableReferenceName 校验载荷;
  • FlatExpressionWriter.bare(...)Reference 分支要求 isWritableReferenceName(reference.name(), spread)(第 228-240 行),不满足就返回 Optional.empty()
  • IExpression.FLAT_OR_OBJECT_CODEC.encode(...)IExpression.java)在 flat 写失败后只对 FunctionExpression 走对象分支Reference 不是 FunctionExpression,直接
    DataResult.error("Cannot encode … as flat text or as an object; only FunctionExpression has an object form")

后果:下列文本都能被解析、能求值,但永远编码不出去IExpression.CODEC / STREAM_CODEC 同一路径,存档与网络同步都受影响),而且内层一个坏引用会让整棵树的对象形式也失败:

输入 解析结果 写出 对象形式
$(a b)$(价格)$(max-value)$(a/b) Reference.Named ❌ 含非标识符字符 ❌ 不存在
$(x.) Reference.Named("x.") ❌ 末位是 .
$(x....)$(a b...) Reference.Spread("x.") / Spread("a b")

对比:现有测试 CustomFunctionTest#unwritableReferenceNamesFallBackToObject 覆盖的是
FunctionExpression.of(NamedFunction.of("a b")) —— 那是 NamedFunction(有 anvillib:named 对象形式,能降级 ✅),没有覆盖解析器亲手产出的 Reference 路径,所以这个洞在测试里是隐形的。IExpression.ref("价格") 这类程序化构造同样中招。

建议(二选一或都做,改动都很小):

  1. parseNamed 里就地校验并带位置报错:isWritableReferenceName(base, spread)(base 去掉 ... 后缀)→ 让「解析器的语言 ⊆ 可回写语言」;
  2. 或在 FLAT_OR_OBJECT_CODEC.encode 里把 Reference.Named / Reference.Spread 也降级成对象形式(anvillib:named 类型已存在,NamedFunction 已是等价表示),这样「能解析的一定能序列化」。

⚠️ 警告

2. .github/workflows/roseau_comment.yml 的模块清单漏了 math

第 56-60 行是硬编码列表(generate-matrix.js 输出的 module_names 被忽略):

codec collision config explosion font integration
moveable-entity-block multiblock network recipe registrum
rpc space-select sync util wheel main

pull_request.ymlroseau-l0/l1/l2 是按 modules.json 矩阵跑的(本 PR 已加 { "module": "math", "needs": ["util"] } ✓),所以 roseau-math 报告会生成并上传,但评论表格里不会出现 math 行 —— 报告被静默丢弃。本 PR 因为模块全新、BC 为空可能看不出来,等后续 PR 改 math 的公开 API 时就会缺报告。顺带:cube 也不在这个列表里(存量遗漏)。请补 math(和 cube)。


💡 建议

  • README.en.md 未同步:本 PR 更新了 README.md 的模块表 / 聚合重导出清单 / 依赖片段 / 新增 Math 章节,但 README.en.md 一个字节没动,表里仍缺 Math(以及存量的 Collision、Explosion、Font、Rpc、Space Select、Sync)。要么一起补,要么在 PR 里说明英文 README 另行处理。
  • 未绑定的名字静默取 0Arguments.value(String) 对「名字没绑定」返回 0,而 $(x...) 未绑定成列表时是点名报错IFunction.bind / FOREACH 都专门守了这条,注释也写了「拼错一个字母就会静默变成 0」)。$(cost) 拼成 $(cot) 仍是静默 0,与上述设计取向不一致;建议至少在 javadoc 里写明这个默认(value(int) 写了「越界时取 0」,value(String) 没写)。
  • 超长指数字面量的报错信息会指向「函数」1e100000000 指数位超过 8 位时会回退成 1 + 标识符 e100000000,最终报 expected '(' after function 'e100000000'(值没有被静默读错 ✅,只是定位误导)。parseNumber 里那个位置可以单独报「number out of range/exponent too long」。
  • .gitignore*.log*.log* 会连带命中 something.logic 这类文件名,*.log.* 更精确(此改动顺带补齐了文件末尾换行 ✅)。
  • lambda 作实参只在 forEach 成立:其它函数(含数据包自定义函数)的固定/变参形参收到 lambda 实参时会被 IFunction.bind 当普通实参求值——变参形参下会静默得到 0,固定形参下抛「期望 N 个实参」的 IAE。建议在 IFunction#apply javadoc 注明「lambda 只被明确遍历实参的函数(如 forEach)接受」。
  • 编码成本write() 的自校验(parse + 再写 + sameMeaning)已在 javadoc 坦白约 2×;另外当整棵树写不出时,FLAT_OR_OBJECT_CODEC 走对象分支,其每个实参又会各自再试一次带自校验的 flat 写出,最坏是 O(n·深度)。高频路径(每 tick 编码)建议调用方缓存结果(javadoc 已提示),可以的话在 API 层再点一句。

🟢 看起来不错(含已核对项)

  • 回写自校验设计到位write()VERIFYING ThreadLocal 防递归、实测「再解析 + 再写文本相同 + sameMeaning」三重判据,并用 JUXTAPOSING 防「并置预检」自递归;sameMeaningNamedFunction / Reference.Named / x 三种「按名取值」做了归一,不会把稳定文本误判成不稳定。
  • 回环风险逐个被钉住:常量不并置(2*3 不写成 23)、负底数补括号((-2)^2-2^2 的语义差)、Double.toString 指数形式(1.0E-41.0E7 边界可读回)、负零符号位(写 -0.0)、2*e1(x) 被指数记法吃掉(juxtapositionReadsBack 实测)——正是这类 PR 最容易翻车的地方,全部有测试。
  • 递归/深度双重防护:解析期 MAX_NESTING_DEPTH=512(并在 javadoc 里诚实写明「计数不是层数、实际 169 层」并用测试钉住 169/170 与 "-"*20000);求值期 MAX_CALL_DEPTH=64 + ThreadLocal 守卫,自引用/互相引用(含绕过 CustomFunction 的 lambda 环)都有测试。
  • 缓存生命周期接对了事件ServerStartedEvent 覆盖首轮数据包加载、OnDatapackSyncEvent(仅 getPlayer()==null,正确区分「玩家进服」与 /reload)覆盖重载、客户端 ClientPlayerNetworkEvent.LoggingOut + Dist.CLIENT 限定;容量上限 + accessOrder 淘汰,并且诚实记录了「弱键因值反持键而回收不掉、必须手动清」这个反直觉点。
  • 变参语义完整Parameters / Parameter 的标识符校验、最多一个变参、变参可不在末位、$(x...) 摊开占用若干实参位、空列表合法(含「变参位空着时推游标」那个细节);$(x...) 未绑定成列表时点名报错而不是静默空列表。
  • 新模块接入核查(逐项)settings.gradle include + 重命名 ✅、.github/modules.json{ "module": "math", "needs": ["util"] } ✅(依赖链 util → codec 与代码里 dev.anvilcraft.lib.v2.util.ISerializer 的 import 一致,该接口在基线上确实存在)、module.main/build.gradle 的 NOT_DEV / dev 两条 jarJar 都加了 ✅、README 聚合清单与依赖片段用到的坐标 anvillib-math-neoforge-1.21.1settings.gradle 重命名一致 ✅、build.gradlemodule.util/build.gradle 模板逐行 diff 后只差 util 依赖与新增的 JUnit/test 配置 ✅、anvillib_math.mixins.json(空 mixins 列表 + anvillib.refmap.json)与仓库既有模块完全一致 ✅(不是遗留物)、package-infojavax.annotation 空值注解沿用仓库既有约定(该仓库未使用 jspecify)✅。
  • CustomFunction javadoc 的数据包路径经核对正确data/<namespace>/anvillib/function/triple.json 符合「data/<pack ns>/<注册表键 ns>/<注册表键 path>/」约定(AbstractRegistrum javadoc 同口径;AnvilCraft 实际产物 data/anvilcraft/anvillib/definitions/ 印证)。
  • 测试覆盖与规模扎实:JSON 与网络两条路径分别实测(含 Holder.direct 内建函数、holderRegistry 归一成注册表引用)、ParseCacheTest 钉缓存语义、三组随机树往返各 20000 轮(含专门补「lambda 作运算符操作数」「并置乘法」两个维度的随机),math.fuzz.rounds 可调且 CI 的 :anvillib-math-neoforge-1.21.1:build 会跑到 test ✅。

📋 声称验证表

PR 描述为空(body=null),无可核对的 checklist;改为核对 README(本 PR 自己写的)中的声称:

声称 状态 证据
flat 语法 x*22xx^2$(name)$(name...)x -> $(x)*2,乘号 */×/· parseMultiplicativematch('*'|'×'|'·')parseNamedparseLambda
反向回写并「自校验再解析、再写文本完全相同」 FlatExpressionWriter.write()
写不出来自动退回对象形式,不写出读不回文本 ⚠️ FunctionExpression 成立;Reference(解析器产出)无对象形式 → 见 🔴
双注册表(function_type / function)对下游开放 LibRegistries + LibFunctionTypes / DataPackRegistryEvent
内建函数:四则 + pow/abs/floor/ceil/round/sqrt/min/max/foreach LibBuiltInFunctions 13 个枚举,无缺无多
自定义函数变参下限 0、可不在末位 Parameter.minimumCount()IFunction.bind() 的配位逻辑与「空变参推游标」分支
三种内联形式(数字 / flat 文本 / 对象)自动选择 IExpression.CODEC = either(DOUBLE, FLAT_OR_OBJECT),常量优先写数字
新模块被 CI 构建(modules.jsonsettings.gradlemodule.main 三处均已补;但 Roseau 评论表漏 math → 见 ⚠️

结论: REQUEST_CHANGES — 代码质量与测试深度都远超同类模块 PR,绝大部分「parser/writer 回环」风险已被自校验与 fuzz 覆盖;但 🔴 那条是契约级漏洞(解析接受却永远无法序列化,Reference 无对象形式兜底),修复量只有几行(parseNamed 校验载荷,或让 Reference 也能降级),建议修掉后再合并。⚠️ 的 Roseau 清单是一行补充。修完这两点我这边可以转为 APPROVE。

@Gugle2308

Copy link
Copy Markdown

💾 Self-improvement review: Patched SKILL.md in skill 'text-codec-roundtrip-pr-review' (1 replacement).

@QiuShui1012
QiuShui1012 marked this pull request as ready for review September 16, 2026 11:06
@Gugle2308

Copy link
Copy Markdown

Review complete. Here's my assessment:


代码审查摘要 — PR #122 Add math lib 添加数学库

操作: ready_for_review(按约定仅报实质问题,风格项已略)
范围: 42 个文件(32 Java / 37 新增 / 0 删除,共 +6406 / −60 行)

  • 主体:module.math 新模块 — 3388 行源码(parser 737 + writer 630 + 函数体系 + 注册表)
  • 测试:6 个测试类 + 2 个测试工具类,82 个 @Test / 2509 行
  • gh auth status 显示 token 已失效(The token in /opt/data/home/.config/gh/hosts.json is invalid),因此未自动改标题,建议标题见文末

📋 声称验证表

检查项 状态 证据
新模块注册进 CI 矩阵(高频遗漏项) .github/modules.json{ "module": "math", "needs": ["util"] }generate-matrix.js 拓扑分层后 math 落 level_2
settings.gradle include + 项目重命名 include 'module.math' + project(':module.math').name = 'anvillib-math-neoforge-1.21.1'(与 module.mainproject(":anvillib-math-neoforge-1.21.1") 对齐)
聚合模块 jarJar 接入 module.main/build.gradle NOT_DEV 走 latest.release、dev 走 project(...),两侧都补齐
构建属性完整(无缺 expand 属性) module.math/gradle.properties 只放 mod_id/name/description,mod_version/mod_group_id/mod_license/range 均继承根 gradle.properties
mod 元数据与源码一致 mods.toml modId = ${mod_id} = anvillib_math@Mod(MOD_ID)@EventBusSubscriber(modid = MOD_ID) 一致;mixins.json 空配置与 module.util 既有约定一致(JAVA_8 / anvillib.refmap.json)
注册命名空间一致性 类型注册在 MAIN_IDanvillib),与 CustomFunction javadoc 的 "type": "anvillib:custom"、README 的 anvillib:function 路径一致
文档同步 README 模块表/重导出清单/依赖示例都补了 math(顺带补齐了 collision/explosion/font/rpc/space-select/sync)

🔴 关键

未发现阻塞合并的问题。回写路径(parser + writer)的经典坑位全部命中并已被机制性防住:常量并置(2*323)、负字面量(pow(-2,2)-2^2)、Double.toString 指数形式(1.0E-4)、负零丢符号位、2*e1(x)2e1(x) 被指数记法吃掉 —— 而且 FlatExpressionWriter.write() 在返回前做「再解析 + 再写 + sameMeaning」自校验,任何一处不成立就退回对象形式,这条设计把「写得出来但读不回去」的风险从「静默损坏存档」降级为「退化到对象形式」。

⚠️ 警告

  1. FlatExpressionParser.parseIdentifier / IFunction.bind — 解析期放行、求值期抛出的形状可以从数据包 JSON 构造出来
    IExpression.evaluate 的 javadoc 承诺「除零、负数开方、下标越界、名字未绑定等情况不会抛出异常」,但以下形状能被 IExpression.CODEC 正常解码,直到求值才抛:

    • 顶层 $(x...)Reference.Spread.evaluateIllegalStateException);
    • $(x...) 落在固定形参位Parameters#checkArity(List) 在有变参形参时一律放行(文档写的是「真实长度由 IFunction#bind 再判一次」),于是形如 f($(x...), 1)f 声明 (a, x...))解码成功、IFunction.bindIllegalStateException
    • forEach 末位不是 lambda(同样只在 apply 里判);
    • 自引用 / 两个注册 lambda 互相引用(guardedMAX_CALL_DEPTH ISE——这条是本意的反崩栈护栏)。

    下游 BE 通常直接在 tick 里调 evaluate(),这些异常没有捕获点就会打到 tick 循环。建议:把「会抛的那部分」写进 evaluate/evaluateInt 的 javadoc(现在读起来像「求值不抛」),并在 README 的接入示例里点明数据包作者写错时的表现。深度守卫那一条无需改动。

  2. 函数名大小写两侧规则不对称 — 读入方向存在静默遮蔽

    • parseIdentifiertoLowerCase(Locale.ROOT) 再查注册表(withDefaultNamespace(lower)function(lower, name)),而 FlatExpressionWriter.writableName 用注册路径原样写出。
    • 读入方向:mymod:Max 会被折成 mymod:max 去查表,查不到再退回内建 byName("max") —— 于是数据包里注册的 mymod:Max 在 flat 文本里被内建 max 静默顶替(求值结果可能完全不同),且没有任何回写自校验会经过这条读路径。
    • 写回方向:mymod:MyFunc / anvillib:MyFunc 这类含大写的路径永远写不出 flat 文本(writableName 判定通过、解析侧读不回 → 自校验兜底退回对象形式,不损坏数据,但 flat 文本这条快路径永久失效)。

    建议二选一:writableName/isWritableFunctionName 直接拒绝非全小写名字(规则显式化、报错可读),或解析侧不再折叠大小写。当前状态是「读放宽、写收紧」,缺少文档说明。

💡 建议(非阻塞)

  • IFunction.CODEC / IFunction.STREAM_CODEC / IExpression.LIST_CODEC 全 PR 无调用点、无测试:公开 API 留给下游可以理解,但 IFunction.CODEC 的自定义编码器带 lookup.listElements() 全表线性扫描 + input.equals(ref.value()) 的等值归一语义,一旦下游在存档/同步路径上用它就是 O(#函数)/次,而目前无人验证。至少补一个「注册条目等值时归一为引用」的用例(StreamCodecTest 已经覆盖了 HOLDER_STREAM_CODEC,这份可以对称补上)。另外 FunctionExpression.MAP_CODEC 里内联的 IExpression.CODEC.listOf() 与新增的 IExpression.LIST_CODEC 重复,建议统一引用后者。
  • IExpression.FLAT_OR_OBJECT_CODEC.decode 丢掉了 flat 侧的报错:flat 解析失败后直接改报对象形式的失败信息,数据包作者拼错文本时看到的是 Expected map-like object...,而不是带位置信息的 Invalid expression: ... at position 10 of expression "..."。建议把 flat 的错误合并进最终 DataResult.error,调试成本差别很大。
  • parseNumber 指数长度上限(≤8 位)超限时的回退this.position = mantissaEnd2e123456789 会落到标识符分支,报 expected '(' after function 'e123456789' —— 与真实原因(指数过大)无关。5 位指数的用例已被 nonFiniteLiteralRejected 钉住,超长指数建议同样报 number out of range
  • CI 每次 :anvillib-math-neoforge-1.21.1:build 都会跑 4 × 20000 轮 fuzz(math.fuzz.rounds 默认 20000),若构建时间被拉长可考虑调低默认值;另外 ci.ymldeploy-l2main 同层并行,而 main 在 NOT_DEV 下解析 anvillib-math-...:latest.release,本模块首次发布后那一轮 CI 存在 main 早于 deploy 取件的时序风险,值得留意一次。

🟢 看起来不错

  • 回写自校验(写→解析→再写 + sameMeaning 归一 NamedFunction/Reference.Named/InputFunction)是这份实现的核心价值,把整类回环 bug 从「静默改值」变成「退回对象形式」;
  • 双递归护栏(MAX_NESTING_DEPTH=512 计数上限 + IFunction.MAX_CALL_DEPTH=64),并且把「512 计数 ≈ 169 层实嵌套」这个反直觉边界用测试钉住了 —— 这一步做得比大多数库都细;
  • 解析缓存:容量上限 + accessOrder LRU + 弱键指回注册表的问题在 javadoc 里点明,并挂好了 ServerStartedEvent / OnDatapackSyncEventgetPlayer()==null)/ ClientPlayerNetworkEvent.LoggingOut 三处清理钩子,weak-hashmap 那类泄漏被正面处理了;
  • Arguments.withAll + IFunction.bind 的变参配位(变参任意位置、空变参时游标仍要推进)逻辑有专门的回归用例;
  • 写不出文本时一律 Optional.empty() 并靠 DataResult.error 走对象形式,writeSideFailuresNeverEscapeTheCodec 这组用例把「异常不得穿透 IExpression.CODEC」这条契约钉死了 —— 这是库代码最容易漏的一条。

🧪 测试建议

被测目标 建议场景 优先级
MathFlatAssertions.randomTree 随机树叶子/算子目前不含 Reference.SpreadCustomFunction、注册函数(仅 e1 系列单独覆盖);建议加「变参实参位 + Spread」的随机维度 🟡
IFunction.CODEC / IFunction.STREAM_CODEC 注册条目等值 → 归一为引用的编码用例;Holder.direct → 注册引用的网络往返 🟡
FlatExpressionParser.parseIdentifier 大写路径(mymod:Max 注册后能否被 flat 文本正确解析)——把上面 ⚠️2 的行为固定成明确期望 🟢

结论: COMMENT — 无阻塞项。工程质量很高(自校验回写 + 双递归护栏 + 8 个测试文件 + CI 接入齐全),上面 ⚠️1/⚠️2 建议在合并前给出明确结论(文档化或修正规则),💡 三项可后续跟进。

建议 PR 标题

Add math module: serializable math expression system 新增 Math 模块:可序列化的数学表达式系统

gh 当前未认证,gh pr edit 无法执行 —— 未自动修改标题;本次审查流程用 curl 公开 diff,未使用任何 token。)


由 Hermes Agent 审查

@Gugle2308

Copy link
Copy Markdown

💾 Self-improvement review: Patched SKILL.md in skill 'text-codec-roundtrip-pr-review' (1 replacement). · Patched references/anvillib-math-pr122-findings.md in skill 'text-codec-roundtrip-pr-review' (1 replacement).

@PigeonNian
PigeonNian merged commit 7285ae1 into Anvil-Dev:dev/1.21.1 Sep 16, 2026
39 checks passed
@Gugle2308

Copy link
Copy Markdown

PR #122 状态为 closed,按 Webhook 审查规则(closed/labeled → 停止,不发布审查意见),本次不做代码审查,也不输出审查评论。

如需重新审查,请在该 PR 重新打开或推送新提交后触发。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants