|
| 1 | +#import "@preview/modern-cug-report:0.1.3": * |
| 2 | +#show: doc => template(doc, size: 11.5pt, |
| 3 | + footer: "Dongdong Kong", header: "FieldMeta.jl 实现思路") |
| 4 | + |
| 5 | +#set par(leading: 1em, spacing: 1em) |
| 6 | + |
| 7 | +#align(center)[ |
| 8 | + #text(20pt, weight: "bold")[FieldMeta.jl 实现思路] |
| 9 | + #v(0.4em) |
| 10 | + #text(11pt)[一个比 FieldMetadata.jl 更紧凑、更可调试的 struct 字段元数据方案] |
| 11 | + #v(0.6em) |
| 12 | + #text(9.5pt, fill: gray)[版本 0.1.0 · 共 ~160 行] |
| 13 | +] |
| 14 | + |
| 15 | +#v(1em) |
| 16 | + |
| 17 | += 1 目标 |
| 18 | + |
| 19 | +为 Julia struct 的每个字段附加键值元数据(如 `bounds`、`units`、`description`), |
| 20 | +并通过函数式 API 在运行时查询: |
| 21 | + |
| 22 | +```julia |
| 23 | +bounds(model, :x) # (0.01, 0.5) |
| 24 | +description(model) # ("Muskingum x", "time step") |
| 25 | +``` |
| 26 | + |
| 27 | +要求: |
| 28 | + |
| 29 | +- *写法直观*:`@bounds @units @description struct ...` 堆叠,左右顺序与 `|` 值一一对应。 |
| 30 | +- *类型稳定*:accessor 全部 `@inferred` 通过。 |
| 31 | +- *分发简单*:不要为每个 key 各自生成一打 dispatch 方法(这是旧版 FieldMetadata.jl 的脆弱点之一)。 |
| 32 | +- *可调试*:内部 helper 都是小函数,能在 REPL 里独立喂 `:(...)` quote 调用。 |
| 33 | + |
| 34 | += 2 核心数据结构 |
| 35 | + |
| 36 | +整个包只有一个统一分发入口: |
| 37 | + |
| 38 | +```julia |
| 39 | +function _meta end |
| 40 | +@inline _meta(::Type, ::Val, ::Val{K}) where K = REGISTRY[K][1] |
| 41 | +``` |
| 42 | + |
| 43 | +- 由 `@fields` / 堆叠宏发射的特化方法形如: |
| 44 | + ```julia |
| 45 | + _meta(::Type{<:Muskingum}, ::Val{:x}, ::Val{:bounds}) = (0.01, 0.5) |
| 46 | + ``` |
| 47 | +- 命中 → 返回特化值(带类型检查);未命中 → fallback 取注册表的默认值。 |
| 48 | + |
| 49 | +注册表本身: |
| 50 | + |
| 51 | +```julia |
| 52 | +const REGISTRY = Dict{Symbol, Tuple{Any, Type}}() |
| 53 | +# REGISTRY[:bounds] = (nothing, Any) |
| 54 | +# REGISTRY[:units] = ("-", String) |
| 55 | +``` |
| 56 | + |
| 57 | +`@metadata` 只往 `REGISTRY` 写一行 `(default, check)`,然后生成 accessor 包装、 |
| 58 | +以及一个用户级的堆叠宏 `@bounds`。 |
| 59 | + |
| 60 | += 3 AST 层处理 |
| 61 | + |
| 62 | +== 3.1 `|` 是 Julia 自带的左结合运算符 |
| 63 | + |
| 64 | +``` |
| 65 | +a | b | c | d ≡ ((a | b) | c) | d |
| 66 | +``` |
| 67 | + |
| 68 | +解析树: |
| 69 | + |
| 70 | +``` |
| 71 | +:call :| |
| 72 | +├── :call :| |
| 73 | +│ ├── :call :| |
| 74 | +│ │ ├── a |
| 75 | +│ │ └── b ← 最左值 |
| 76 | +│ └── c |
| 77 | +└── d ← 最右值 |
| 78 | +``` |
| 79 | + |
| 80 | +要让"最外层宏对应最左 `|` 值",每个堆叠宏只需做一件事: |
| 81 | +*把最内层 `:|` 节点替换为它的 lhs*,并取走它的 rhs 作为本次元数据值。 |
| 82 | + |
| 83 | +// #pagebreak() |
| 84 | + |
| 85 | +== 3.2 关键 helper:`_strip_leftmost!` |
| 86 | + |
| 87 | +整个包最核心的一段代码(只有 7 行): |
| 88 | + |
| 89 | +```julia |
| 90 | +function _strip_leftmost!(args, i) |
| 91 | + e = args[i] |
| 92 | + _ispipe(e.args[2]) && return _strip_leftmost!(e.args, 2) |
| 93 | + val = e.args[3] |
| 94 | + args[i] = e.args[2] |
| 95 | + val |
| 96 | +end |
| 97 | +``` |
| 98 | + |
| 99 | +调用约定: |
| 100 | +- `args[i]` 是一个 `:|` Expr 链。 |
| 101 | +- 函数原地修改 `args[i]`(剥掉最内层),返回被剥掉的值。 |
| 102 | + |
| 103 | +REPL 演示: |
| 104 | + |
| 105 | +```julia |
| 106 | +julia> import FieldMeta: _strip_leftmost! |
| 107 | +julia> holder = Any[:(x::Float64 | (0.01, 0.5) | "m" | "label")] |
| 108 | +julia> _strip_leftmost!(holder, 1) |
| 109 | +:((0.01, 0.5)) |
| 110 | +julia> holder |
| 111 | +1-element Vector{Any}: |
| 112 | + :(x::Float64 | "m" | "label") |
| 113 | +julia> _strip_leftmost!(holder, 1), _strip_leftmost!(holder, 1) |
| 114 | +("m", "label") |
| 115 | +julia> holder[1] |
| 116 | +:(x::Float64) |
| 117 | +``` |
| 118 | + |
| 119 | +== 3.3 宏扩展顺序 |
| 120 | + |
| 121 | +Julia 宏扩展是*从外向内*。`@bounds @units @description struct ...` 的过程: |
| 122 | + |
| 123 | +#table(columns: (auto, 1fr, 1fr), |
| 124 | + align: (left, left, left), |
| 125 | + stroke: 0.5pt + gray, |
| 126 | + table.header[阶段][输入][剥离 / 输出], |
| 127 | + [`@bounds` 先跑], [`x::T \| (0.01, 0.5) \| "-" \| "lbl"`], [取 `(0.01, 0.5)`,余 `x::T \| "-" \| "lbl"`], |
| 128 | + [`@units` 跑], [`x::T \| "-" \| "lbl"`], [取 `"-"`,余 `x::T \| "lbl"`], |
| 129 | + [`@description`], [`x::T \| "lbl"`], [取 `"lbl"`,余 `x::T`], |
| 130 | + [最终 struct], [`struct ... x::T ... end`], [纯净 struct 进入编译] |
| 131 | +) |
| 132 | + |
| 133 | += 4 两种用户 API |
| 134 | + |
| 135 | +== 4.1 堆叠形式(`@bounds` / `@units` / ...) |
| 136 | + |
| 137 | +每个 `@metadata` 声明都顺带生成一个同名宏: |
| 138 | + |
| 139 | +```julia |
| 140 | +macro $name(ex) |
| 141 | + $FieldMeta._stack(ex, $q, __source__) |
| 142 | +end |
| 143 | +``` |
| 144 | + |
| 145 | +`_stack` 流程: |
| 146 | + |
| 147 | ++ `_find_struct(ex)` —— 递归找到 `:struct` Expr(穿过任意层 macrocall,如 `@with_kw`)。 |
| 148 | ++ 遍历字段块 `s.args[3].args`。 |
| 149 | ++ 每行用 `_meta_slot` 定位元数据所在的 `(args, idx, fname)`。 |
| 150 | ++ 调 `_strip_leftmost!` 剥一层,并 emit 一条 `_meta(::Type{<:T}, ::Val{f}, ::Val{k})` 方法。 |
| 151 | ++ 返回 `Expr(:block, src, esc(ex), methods...)`,把(已被原地修改的)`ex` 交还给下一个宏。 |
| 152 | + |
| 153 | +== 4.2 一次性命名形式(`@fields`) |
| 154 | + |
| 155 | +```julia |
| 156 | +@fields @with_kw struct Muskingum{FT} |
| 157 | + x::FT = 0.35 | (bounds=(0.01, 0.5), units="-", description="x") |
| 158 | + dt::FT = 1.0 | (units="h") |
| 159 | +end |
| 160 | +``` |
| 161 | + |
| 162 | +`@fields` 不剥层,而是直接把 `(k=v, k=v)` 这个 `:tuple` 展开成多条 `_emit!`。 |
| 163 | +和堆叠形式共用同一套 `_meta` 方法表,accessor 行为完全一致。 |
| 164 | + |
| 165 | +// #pagebreak() |
| 166 | + |
| 167 | += 5 形式 A 与 形式 B |
| 168 | + |
| 169 | +字段行有两种带元数据的形态: |
| 170 | + |
| 171 | +#table(columns: (1fr, 2fr), |
| 172 | + align: (left, left), |
| 173 | + stroke: 0.5pt + gray, |
| 174 | + table.header[形态][AST], |
| 175 | + [`a::T | v`], [`Expr(:call, :\|, :(a::T), v)`,行就是 `:|` 表达式], |
| 176 | + [`a::T = default | v`], [`Expr(:(=), :(a::T), Expr(:call, :\|, default, v))`,`:|` 嵌在 `:(=)` 内], |
| 177 | +) |
| 178 | + |
| 179 | +`_meta_slot` 用一个分支就把两种形态统一返回 `(slot_args, slot_idx, fieldname)`: |
| 180 | + |
| 181 | +```julia |
| 182 | +function _meta_slot(block_args, i) |
| 183 | + line = block_args[i] |
| 184 | + line isa Expr || return nothing |
| 185 | + _ispipe(line) && return (block_args, i, _fieldname(line)) |
| 186 | + line.head === :(=) && _ispipe(line.args[2]) && |
| 187 | + return (line.args, 2, _fieldname(line.args[1])) |
| 188 | + nothing |
| 189 | +end |
| 190 | +``` |
| 191 | + |
| 192 | +下游 `_strip_leftmost!` 和 `_emit!` 不需要知道是哪种形态。 |
| 193 | + |
| 194 | += 6 类型检查 |
| 195 | + |
| 196 | +每条 emit 的方法体里做一次 `isa` 检查,不命中时通过专门的 `@noinline` |
| 197 | +错误函数抛出,避免让正常路径吃错误处理的字节码: |
| 198 | + |
| 199 | +```julia |
| 200 | +@inline function _meta(::Type{<:T}, ::Val{f}, ::Val{k}) |
| 201 | + v = $val |
| 202 | + _, c = REGISTRY[k] |
| 203 | + v isa c || _typeerror(T, k, v, c) |
| 204 | + v |
| 205 | +end |
| 206 | +``` |
| 207 | + |
| 208 | += 7 与 `Parameters.@with_kw` 的协作 |
| 209 | + |
| 210 | +约定写法:`@bounds @units @description @with_kw struct ...`。 |
| 211 | + |
| 212 | +执行顺序(外→内): |
| 213 | + |
| 214 | ++ `@bounds` 看到 `@units @description @with_kw struct...`,递归找到 `:struct`, |
| 215 | + 剥掉它字段里的最左 `|`,发射 `bounds` 方法,返回的 block 中仍然包含未展开的 |
| 216 | + `@units @description @with_kw`。 |
| 217 | ++ `@units`、`@description` 依次同理。 |
| 218 | ++ 最后 `@with_kw` 看到的 struct 字段已经是纯净的 `x::FT = 0.35`,正常生成 |
| 219 | + 关键字构造器。 |
| 220 | + |
| 221 | +因此元数据宏完全感知不到 `@with_kw` 的存在,只关心找到 `:struct` 然后剥层。 |
| 222 | + |
| 223 | += 8 调试入口 |
| 224 | + |
| 225 | +`test/test-internals.jl` 里每个内部 helper 都附了 testset + 例子。 |
| 226 | +建议的调试流程: |
| 227 | + |
| 228 | ++ 在 REPL `using FieldMeta`,`import FieldMeta: _ispipe, _find_struct, _fieldname, _strip_leftmost!, _meta_slot`。 |
| 229 | ++ 用 `:(struct Foo; x::Int \| (0,1); end)` 这种 quote 直接喂内部函数。 |
| 230 | ++ `@macroexpand @bounds @units struct ... end` 观察展开树。 |
| 231 | ++ 配合 `docs/playground.ipynb` 里准备好的可执行 cell。 |
| 232 | + |
| 233 | += 9 设计取舍 |
| 234 | + |
| 235 | +#table(columns: (1fr, 1fr, 1fr), |
| 236 | + align: (left, left, left), |
| 237 | + stroke: 0.5pt + gray, |
| 238 | + table.header[决策][选择][代价 / 备注], |
| 239 | + [元数据存储], [单一 `_meta` + 注册表 fallback], [每次访问要查一次 `REGISTRY` 默认值(仅在 miss 路径)], |
| 240 | + [字段名解析], [`_fieldname` 递归穿 `::` / `=` / `\|`], [新增 wrapper 时需在此处加分支], |
| 241 | + [`_` 跳过], [`val === :_` 不 emit,落到 fallback], [字段不能用 `_` 当字面值], |
| 242 | + [类型检查时机], [访问时(命中 emit 方法里)], [声明 struct 时不立即报错,需要至少访问一次], |
| 243 | +) |
| 244 | + |
| 245 | += 10 后续可扩展 |
| 246 | + |
| 247 | +- `@fields T begin ... end` —— 给已有 struct 后期补 / 改元数据。 |
| 248 | +- `@inferred` 计入测试 —— 当前 `@inferred` 跑过但不计 `@test` 计数。 |
| 249 | +- 字段名重叠处理(同字段同 key 多次 emit 会触发 method overwrite warning)。 |
0 commit comments