Skip to content

Commit aaee5f5

Browse files
committed
add docs
1 parent cabf4b8 commit aaee5f5

8 files changed

Lines changed: 960 additions & 20 deletions

File tree

.github/workflows/CI.yml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# https://github.com/Alexander-Barth/NCDatasets.jl/blob/master/.github/workflows/ci.yml
2+
name: CI
3+
on:
4+
- push
5+
- pull_request
6+
jobs:
7+
test:
8+
name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }}
9+
runs-on: ${{ matrix.os }}
10+
strategy:
11+
fail-fast: false
12+
matrix:
13+
version:
14+
# - '1.3'
15+
- '1'
16+
# - 'nightly'
17+
os:
18+
- ubuntu-latest
19+
# - macOS-latest
20+
# - windows-latest
21+
arch:
22+
- x64
23+
steps:
24+
- uses: actions/checkout@v4
25+
- uses: julia-actions/setup-julia@v2
26+
with:
27+
version: ${{ matrix.version }}
28+
arch: ${{ matrix.arch }}
29+
- uses: julia-actions/cache@v2
30+
- uses: julia-actions/julia-buildpkg@latest
31+
- uses: julia-actions/julia-runtest@latest
32+
continue-on-error: ${{ matrix.version == 'nightly' }}
33+
34+
- uses: julia-actions/julia-processcoverage@v1
35+
- uses: codecov/codecov-action@v4
36+
with:
37+
token: ${{ secrets.CODECOV_TOKEN }}
38+
file: lcov.info

.github/workflows/TagBot.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# https://github.com/JuliaRegistries/TagBot
2+
name: TagBot
3+
on:
4+
issue_comment:
5+
types:
6+
- created
7+
workflow_dispatch:
8+
jobs:
9+
TagBot:
10+
if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot'
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: JuliaRegistries/TagBot@v1
14+
with:
15+
token: ${{ secrets.GITHUB_TOKEN }}
16+
# Edit the following line to reflect the actual name of the GitHub Secret containing your private key
17+
ssh: ${{ secrets.DOCUMENTER_KEY }}
18+
# ssh: ${{ secrets.NAME_OF_MY_SSH_PRIVATE_KEY_SECRET }}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
Manifest.toml
2+
*.pdf

README.md

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# FieldMeta.jl
22

3+
<!-- [![Stable](https://img.shields.io/badge/docs-stable-blue.svg)](https://jl-pkgs.github.io/FieldMeta.jl/stable) -->
4+
[![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://jl-pkgs.github.io/FieldMeta.jl/dev)
5+
[![CI](https://github.com/jl-pkgs/FieldMeta.jl/actions/workflows/CI.yml/badge.svg)](https://github.com/jl-pkgs/FieldMeta.jl/actions/workflows/CI.yml)
6+
[![Codecov](https://codecov.io/gh/jl-pkgs/FieldMeta.jl/branch/master/graph/badge.svg)](https://app.codecov.io/gh/jl-pkgs/FieldMeta.jl/tree/master)
7+
8+
39
为 struct 字段附加元数据(bounds、units、description …)。`FieldMetadata.jl` 的重新设计版本,保留堆叠写法的可读性,去掉它内部的脆弱性。
410

511
## 快速上手
@@ -39,21 +45,21 @@ end
3945

4046
### 1. 元数据存储:每 key 一份方法集 → 单一统一 dispatcher
4147

42-
| | FieldMetadata | FieldMeta |
43-
|---|---|---|
44-
| 每个 `@metadata` key | 生成约 10 个 dispatch 方法 | 共享单一 `_meta(::Type{<:T}, ::Val{field}, ::Val{key})` |
45-
| 默认值与类型检查 | 烧进每个生成的方法 | 集中在 `REGISTRY::Dict{Symbol,(default,check)}`,与字段方法解耦 |
46-
| 后加新 key | 需要重新展开 struct 的元数据宏 | 直接 `@metadata newkey ...`,旧 struct 立即可用,返回默认值 |
48+
| | FieldMetadata | FieldMeta |
49+
| -------------------- | ------------------------------ | --------------------------------------------------------------- |
50+
| 每个 `@metadata` key | 生成约 10 个 dispatch 方法 | 共享单一 `_meta(::Type{<:T}, ::Val{field}, ::Val{key})` |
51+
| 默认值与类型检查 | 烧进每个生成的方法 | 集中在 `REGISTRY::Dict{Symbol,(default,check)}`,与字段方法解耦 |
52+
| 后加新 key | 需要重新展开 struct 的元数据宏 | 直接 `@metadata newkey ...`,旧 struct 立即可用,返回默认值 |
4753

4854
后果:`FieldMetadata``@generated fieldname_vals` 一旦返回 `Val(:x)` 实例而不是 `Val{:x}` 类型,10 个方法分支瞬间全失配 — 这是这次修复的根因之一。`FieldMeta` 没有这条多分支链。
4955

5056
### 2. 堆叠语义:跨宏共享状态 → 每宏独立剥一层
5157

52-
| | FieldMetadata | FieldMeta |
53-
|---|---|---|
54-
| 宏栈层数 vs `\|` 数量 | 必须严格相等,少一根/多一根错位静默 | 同上(语义未变),但每宏内部只关心自己那一层,不需要追踪整条管道 |
55-
|`@with_kw` 协作 | `@with_kw` 必须在最内层;外层宏看到尚未展开的 macrocall | 同左;处理逻辑用统一递归找 `:struct`,不依赖宏栈对齐 |
56-
| 错误处理 | 在某些组合下静默丢弃字段元数据 | 显式 `error()`:找不到 struct / pipe 解析失败立刻报 |
58+
| | FieldMetadata | FieldMeta |
59+
| --------------------- | ------------------------------------------------------- | ---------------------------------------------------------------- |
60+
| 宏栈层数 vs `\|` 数量 | 必须严格相等,少一根/多一根错位静默 | 同上(语义未变),但每宏内部只关心自己那一层,不需要追踪整条管道 |
61+
|`@with_kw` 协作 | `@with_kw` 必须在最内层;外层宏看到尚未展开的 macrocall | 同左;处理逻辑用统一递归找 `:struct`,不依赖宏栈对齐 |
62+
| 错误处理 | 在某些组合下静默丢弃字段元数据 | 显式 `error()`:找不到 struct / pipe 解析失败立刻报 |
5763

5864
### 3. `@chain` 的多重坑
5965

@@ -65,19 +71,19 @@ end
6571

6672
### 4. 类型参数与子类型分发
6773

68-
| | FieldMetadata | FieldMeta |
69-
|---|---|---|
74+
| | FieldMetadata | FieldMeta |
75+
| ------------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------ |
7076
| `Muskingum{Float64}` 的字段查找 | 依赖 `fieldname_vals` 返回 `Val{...}` 类型元组,并经过 5 跳 dispatch | 直接 `_meta(::Type{<:Muskingum}, Val{:x}, Val{:bounds})`,1 跳命中 |
71-
| inference | 容易因任一中间方法不稳定而退化 | accessor 全部 `@inferred` 通过(测试覆盖) |
77+
| inference | 容易因任一中间方法不稳定而退化 | accessor 全部 `@inferred` 通过(测试覆盖) |
7278

7379
### 5. 字段名解析
7480

75-
| 形式 | FieldMetadata | FieldMeta |
76-
|---|---|---|
77-
| `a::T \| v` |||
78-
| `a::T = d \| v` || ✓(递归到最内层 lhs 取字段名) |
79-
| 链式 `a::T \| v1 \| v2 \| v3` |||
80-
| `a::T \| (k1=v1, k2=v2)` || ✓(`@fields` 专用) |
81+
| 形式 | FieldMetadata | FieldMeta |
82+
| ----------------------------- | ------------- | ------------------------------ |
83+
| `a::T \| v` | | |
84+
| `a::T = d \| v` | | ✓(递归到最内层 lhs 取字段名) |
85+
| 链式 `a::T \| v1 \| v2 \| v3` | | |
86+
| `a::T \| (k1=v1, k2=v2)` | | ✓(`@fields` 专用) |
8187

8288
## 迁移指南(从 FieldMetadata.jl)
8389

@@ -137,5 +143,5 @@ name(x, :field)
137143
运行:
138144

139145
```julia
140-
julia --project=. -e 'using Pkg; Pkg.test()'
146+
julia --project test/runtests.jl
141147
```

docs/implementation.typ

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
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

Comments
 (0)