|
| 1 | +#import "@preview/modern-cug-report:0.1.3": * |
| 2 | +#show: doc => template(doc, size: 11.5pt, |
| 3 | + footer: "2026", 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 | +理解整个包,最快的方式是看最终生成了什么。 |
| 20 | + |
| 21 | +```julia |
| 22 | +@metadata bounds nothing Any |
| 23 | +@metadata units "-" String |
| 24 | +
|
| 25 | +@bounds @units struct Muskingum |
| 26 | + x::Float64 | (0.01, 0.5) | "m/s" |
| 27 | + dt::Float64 |
| 28 | +end |
| 29 | +``` |
| 30 | + |
| 31 | +等价于: |
| 32 | + |
| 33 | +```julia |
| 34 | +struct Muskingum # pipe 被剥干净 |
| 35 | + x::Float64 |
| 36 | + dt::Float64 |
| 37 | +end |
| 38 | +
|
| 39 | +# 每个 (类型, 字段, key) → 专属方法,直接返回常量 |
| 40 | +@inline _meta(::Type{<:Muskingum}, ::Val{:x}, ::Val{:bounds}) = (0.01, 0.5) |
| 41 | +@inline _meta(::Type{<:Muskingum}, ::Val{:x}, ::Val{:units}) = "m/s" |
| 42 | +
|
| 43 | +# @metadata 为每个 key 生成的默认 fallback |
| 44 | +@inline _meta(::Type, ::Val, ::Val{:bounds}) = nothing |
| 45 | +@inline _meta(::Type, ::Val, ::Val{:units}) = "-" |
| 46 | +``` |
| 47 | + |
| 48 | +用户调用 `bounds(model, :x)` 时,内部走 `_meta(Muskingum, Val{:x}(), Val{:bounds}())`。 |
| 49 | +三层优先级:*专属方法 > 按 key 默认 > MethodError(未声明的 key)*。 |
| 50 | +热路径全是方法派发,无 Dict 查询,完全类型稳定。 |
| 51 | + |
| 52 | += 2 pipe 链与堆叠宏 |
| 53 | + |
| 54 | +`|` 在 Julia 里是左结合运算符: |
| 55 | + |
| 56 | +``` |
| 57 | +x::T | v1 | v2 | v3 ≡ ((x::T | v1) | v2) | v3 |
| 58 | +``` |
| 59 | + |
| 60 | +最左值 `v1` 在最深的 `:|` 节点。Julia 宏从外向内展开,所以: |
| 61 | + |
| 62 | +- `@bounds`(最外层)剥走 `v1`,余下 `x::T | v2 | v3` 交给下一个宏。 |
| 63 | +- `@units` 剥走 `v2`,余下 `x::T | v3`。 |
| 64 | +- `@description` 剥走 `v3`,余下纯净的 `x::T`。 |
| 65 | + |
| 66 | +核心实现是 `_strip_leftmost!(args, i)`(7 行),递归找到最深的 `:|` 节点, |
| 67 | +原地替换并返回被剥掉的值。 |
| 68 | + |
| 69 | += 3 类型检查在何时发生 |
| 70 | + |
| 71 | +#table(columns: (auto, 1fr), |
| 72 | + align: (left, left), |
| 73 | + stroke: 0.5pt + gray, |
| 74 | + table.header[时机][做什么], |
| 75 | + [宏展开期], [从 `REGISTRY` 读约束 `c`,检查 key 是否已声明(未声明立即报错)], |
| 76 | + [结构体定义期], [`let v = $val; v isa c || _typeerror(...)` 执行一次], |
| 77 | + [访问期], [专属方法直接 `return $val`,*零额外开销*], |
| 78 | +) |
| 79 | + |
| 80 | +`REGISTRY` 只在 `@metadata` 执行时写入、宏展开时读出,运行期不接触。 |
| 81 | + |
| 82 | += 4 内部函数调用链 |
| 83 | + |
| 84 | +`@bounds struct ... end` 展开时,调用关系如下: |
| 85 | + |
| 86 | +``` |
| 87 | +macro bounds(ex) |
| 88 | + _stack(ex, :bounds, src) |
| 89 | + _process(ex, src, emit_fn) |
| 90 | + 1. _find_struct(ex) # 找到 :struct,穿过 @with_kw 等 macrocall |
| 91 | + 2. for each field line: |
| 92 | + a. _meta_slot(block, i) # 该行有 pipe 吗?→ (args, idx, fname) |
| 93 | + b. _strip_leftmost!(args, idx) # 原地剥最左 | 值,返回 val |
| 94 | + c. _emit!(methods, T, fname, key, val) |
| 95 | + - REGISTRY[key] # 读约束 c(宏展开期,一次性) |
| 96 | + - push! 生成的方法块: |
| 97 | + let v=val; v isa c || _typeerror(...) end # 定义期执行 |
| 98 | + @inline _meta(::Type{<:T}, ::Val{f}, ::Val{k}) = val |
| 99 | + 3. return Expr(:block, esc(ex), methods...) |
| 100 | +``` |
| 101 | + |
| 102 | +步骤 2 是顺序的三步(a → b → c),不是嵌套调用。`ex` 在步骤 2 里被原地修改(pipe 已剥),步骤 3 返回时它已经是干净的 struct。 |
| 103 | + |
| 104 | +== 多宏堆叠时的展开顺序 |
| 105 | + |
| 106 | +堆叠不靠代码里的循环,而是 Julia 宏展开机制本身驱动。 |
| 107 | +展开(编译期)和执行(运行期)是两个阶段: |
| 108 | + |
| 109 | +*展开阶段*(外 → 内,编译期): |
| 110 | +``` |
| 111 | +@bounds @units struct ... x | v1 | v2 end |
| 112 | + @bounds 展开 → block { esc(@units struct...x|v2 end), # ex 放前面 |
| 113 | + @inline _meta(...bounds...) = v1 } |
| 114 | + @units 展开 → block { esc(struct...x end), # ex 放前面 |
| 115 | + @inline _meta(...units...) = v2 } |
| 116 | +``` |
| 117 | + |
| 118 | +*执行阶段*(顺序执行,运行期): |
| 119 | +``` |
| 120 | +1. struct ... x end ← struct 先定义 |
| 121 | +2. @inline _meta(...units...) = v2 ← 方法引用 T,T 已存在 ✓ |
| 122 | +3. @inline _meta(...bounds...) = v1 |
| 123 | +``` |
| 124 | + |
| 125 | +`_process` 始终把 `esc(ex)` 放在 `methods` 之前返回, |
| 126 | +保证 struct 定义先于任何引用它的 `_meta` 方法。 |
| 127 | + |
| 128 | += 5 调试入口 |
| 129 | + |
| 130 | +```julia |
| 131 | +import FieldMeta: _ispipe, _find_struct, _fieldname, _strip_leftmost!, _meta_slot |
| 132 | +
|
| 133 | +# 直接喂 quote 调用内部函数 |
| 134 | +holder = Any[:(x::Int | (0, 1) | "m")] |
| 135 | +_strip_leftmost!(holder, 1) # => :((0, 1)) |
| 136 | +
|
| 137 | +# 观察完整展开 |
| 138 | +@macroexpand @bounds @units struct Foo; x::Int | 1 | "u"; end |
| 139 | +``` |
| 140 | + |
| 141 | +每个 helper 的行为在 `test/test-internals.jl` 里都有可执行示例。 |
0 commit comments