quickwin 1.2.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/README.en.md +3 -3
  2. package/README.md +21 -24
  3. package/examples/PdfCanvas.js +8 -37
  4. package/examples/PdfCanvas.tsx +11 -41
  5. package/examples/exec_server.js +20054 -1423
  6. package/examples/exec_server.tsx +489 -0
  7. package/examples/exec_server_worker.js +383 -33
  8. package/examples/exec_server_worker.ts +408 -30
  9. package/examples/pdf_preview2.js +31 -73
  10. package/examples/pdf_preview2.ts +33 -76
  11. package/examples/pdf_viewer.js +1496 -616
  12. package/examples/pdf_viewer.tsx +23 -64
  13. package/examples/setres.js +22 -47
  14. package/examples/setres.ts +23 -57
  15. package/examples/test_gallery.js +1491 -525
  16. package/examples/test_react_lazy.js +1455 -500
  17. package/examples/test_react_listbox.js +1454 -499
  18. package/examples/test_root_window.js +1450 -495
  19. package/examples/test_tab.js +1455 -500
  20. package/lib/abort.js +70 -0
  21. package/lib/abort.ts +69 -0
  22. package/lib/certs.js +214 -0
  23. package/lib/certs.ts +224 -0
  24. package/lib/exec-log.js +15 -0
  25. package/lib/exec-log.ts +30 -0
  26. package/lib/fetch.js +37 -4
  27. package/lib/fetch.ts +35 -4
  28. package/lib/ffi/bind.js +352 -0
  29. package/lib/ffi/bind.ts +442 -0
  30. package/lib/ffi/ctype.js +168 -0
  31. package/lib/ffi/ctype.ts +331 -0
  32. package/lib/ffi/index.js +6 -0
  33. package/lib/ffi/index.ts +6 -0
  34. package/lib/ffi/struct.js +412 -0
  35. package/lib/ffi/struct.ts +664 -0
  36. package/lib/http-common.js +43 -5
  37. package/lib/http-common.ts +48 -6
  38. package/lib/http-server.js +187 -29
  39. package/lib/http-server.ts +177 -26
  40. package/lib/react-qw/components/DateTimePicker.js +20 -33
  41. package/lib/react-qw/components/Input.js +3 -10
  42. package/lib/react-qw/components/Link.js +7 -22
  43. package/lib/react-qw/components/ListView.js +80 -141
  44. package/lib/react-qw/components/PathPicker.js +36 -80
  45. package/lib/react-qw/components/ScrollView.js +15 -8
  46. package/lib/react-qw/components/Slider.js +2 -1
  47. package/lib/react-qw/components/Tab.js +15 -20
  48. package/lib/react-qw/components/Tooltip.js +21 -20
  49. package/lib/react-qw/components/TreeView.js +14 -47
  50. package/lib/react-qw/index.js +4 -2
  51. package/lib/react-qw/index.ts +4 -2
  52. package/lib/react-qw/nmhdr.js +5 -0
  53. package/lib/react-qw/nmhdr.ts +7 -0
  54. package/lib/react-qw/props.js +7 -2
  55. package/lib/react-qw/props.ts +7 -3
  56. package/lib/react-qw/reconciler.js +23 -13
  57. package/lib/react-qw/reconciler.ts +22 -13
  58. package/lib/text-measure.js +18 -35
  59. package/lib/text-measure.ts +20 -37
  60. package/lib/websocket.js +12 -2
  61. package/lib/websocket.ts +15 -2
  62. package/lib/windows/comctl32.js +18 -0
  63. package/lib/windows/comctl32.ts +20 -0
  64. package/lib/windows/comdlg32.js +7 -0
  65. package/lib/windows/comdlg32.ts +9 -0
  66. package/lib/windows/gdi32.js +90 -0
  67. package/lib/windows/gdi32.ts +97 -0
  68. package/lib/windows/kernel32.js +66 -0
  69. package/lib/windows/kernel32.ts +76 -0
  70. package/lib/windows/ntdll.js +8 -0
  71. package/lib/windows/ntdll.ts +10 -0
  72. package/lib/windows/ole32.js +7 -0
  73. package/lib/windows/ole32.ts +9 -0
  74. package/lib/windows/shell32.js +11 -0
  75. package/lib/windows/shell32.ts +14 -0
  76. package/lib/windows/structs.js +384 -0
  77. package/lib/windows/structs.ts +422 -0
  78. package/lib/windows/user32.js +469 -0
  79. package/lib/windows/user32.ts +502 -0
  80. package/package.json +1 -1
  81. package/quickwin.d.ts +22 -33
  82. package/quickwin_const.d.ts +1 -0
  83. package/qwin-nowasm-x86.exe +0 -0
  84. package/qwin-nowasm.exe +0 -0
  85. package/qwin-x86.exe +0 -0
  86. package/qwin.exe +0 -0
  87. package/test/bench_ffi_callraw.js +236 -0
  88. package/test/bench_ffi_callraw.ts +236 -0
  89. package/test/bench_ffi_overhead.js +139 -0
  90. package/test/bench_ffi_overhead.ts +141 -0
  91. package/test/bench_sendmessage.js +104 -0
  92. package/test/bench_sendmessage.ts +100 -0
  93. package/test/bench_sendmessage_nonnull.js +187 -0
  94. package/test/bench_sendmessage_nonnull.ts +188 -0
  95. package/test/run.js +14 -4
  96. package/test/run.ts +15 -5
  97. package/test/test_basic_fetch.js +8 -0
  98. package/test/test_basic_fetch.ts +9 -0
  99. package/test/test_ffi.js +33 -47
  100. package/test/test_ffi.ts +41 -60
  101. package/test/test_ffi_abi.js +235 -0
  102. package/test/test_ffi_abi.ts +218 -0
  103. package/test/test_ffi_bind.js +572 -0
  104. package/test/test_ffi_bind.ts +597 -0
  105. package/test/test_ffi_struct.js +745 -0
  106. package/test/test_ffi_struct.ts +798 -0
  107. package/test/test_ffi_struct_layout.js +495 -0
  108. package/test/test_ffi_struct_layout.ts +543 -0
  109. package/test/test_ffi_user32.js +103 -0
  110. package/test/test_ffi_user32.ts +113 -0
  111. package/test/test_ffi_windows.js +101 -0
  112. package/test/test_ffi_windows.ts +113 -0
  113. package/test/test_helper.js +8 -1
  114. package/test/test_helper.ts +9 -1
  115. package/test/test_http_import.js +18 -0
  116. package/test/test_http_import.ts +19 -0
  117. package/test/test_http_server.js +73 -1
  118. package/test/test_http_server.ts +75 -1
  119. package/test/test_ipv6.js +2 -2
  120. package/test/test_ipv6.ts +2 -2
  121. package/test/test_net_fetch.js +70 -8
  122. package/test/test_net_fetch.ts +69 -7
  123. package/test/test_net_websocket.js +1 -1
  124. package/test/test_net_websocket.ts +1 -1
  125. package/test/test_react_complex.js +1465 -510
  126. package/test/test_react_counter.js +1455 -500
  127. package/test/test_react_flex.js +1475 -513
  128. package/test/test_react_hidden.js +1458 -503
  129. package/test/test_react_listview_link.js +1455 -500
  130. package/test/test_react_notify.js +18534 -0
  131. package/test/test_react_render.js +1455 -500
  132. package/test/test_wolfssl_read.js +3 -2
  133. package/test/test_wolfssl_read.ts +3 -1
  134. package/test/test_worker_net.js +35 -17
  135. package/test/test_worker_net.ts +27 -15
  136. package/tsconfig.json +1 -1
  137. package/examples/exec_server.ts +0 -80
@@ -0,0 +1,664 @@
1
+ import * as ffi from 'ffi';
2
+ import '../text-codec.js';
3
+ import {
4
+ C_BasicType_No_Void,
5
+ C_TypeJsTypeMap,
6
+ isCPtrToken,
7
+ normToken,
8
+ MaybePtr,
9
+ PTR_SIZE,
10
+ PtrArrayBuffer,
11
+ readScalar,
12
+ SizeAlign,
13
+ writeScalar,
14
+ type C_Array,
15
+ type C_BasicType_Token,
16
+ type C_Integer,
17
+ type C_Number,
18
+ type C_Struct, type C_Union,
19
+ type Encoding,
20
+ type Ptr,
21
+ type SimpleMember,
22
+ type SimpleToken,
23
+ } from './ctype.js';
24
+
25
+ // ============================================================
26
+ // 聚合定义:唯一 API 是平铺语法(值词汇见 ctype.ts §4),签名与 pack 见末尾 struct()/union()。
27
+ // layout 采用 MSVC 对齐语义:
28
+ // i8/u8→1 i16/u16→2 i32/u32/f32→4 i64/u64/f64→8 '<>ptr'/'<T>ptr'→arch 宽(4/8)
29
+ // struct 对齐 = 最大字段对齐,总尺寸末尾补齐;pack 压上限、键尾 '@N'(alignas)抬下限
30
+ // 位域布局规则见下方 computeStructLayout 的状态机注释(mingw 实测 DCB/COMSTAT)。
31
+ // ============================================================
32
+
33
+ // ============================================================
34
+ // 类型推导 — 值词汇 → decode/encode 形状
35
+ // ============================================================
36
+
37
+ // 指针成员:'<>ptr' → number(T='' 品牌退化);'<NAME>ptr' → MaybePtr<NAME>(NULL | 品牌 number)。
38
+ // 该品牌 number 可直接喂 bind 的 <NAME>ptr 形参(裸地址透传);空位写 NULL。
39
+
40
+ type N =
41
+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
42
+ | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19
43
+ | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29
44
+ | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39
45
+ | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49
46
+ | 50 | 51 | 52 | 53;
47
+
48
+ // 规范 token(SimpleToken 守卫后,T 恒为规范形)→ JS 形状:'<T>ptr' → MaybePtr(0 保真,
49
+ // 对齐 C 版);'<T>ptr!' → Ptr(非空担保:decode 直出非空品牌且运行时 0 断言,见 doDecode;
50
+ // 下游 '! 消费位'由 bind 的 callPacked 兜底);'@E' → EnumMap 值域(与 bind 签名位同构:
51
+ // decode 出值域收窄、encode 收同型 —— 异种枚举/域外字面量由枚举 nominal 拦,运行时值域
52
+ // 校验按设计留 Phase 2);数字档 → JsType 映射。落不到的('void'、裸 'ptr'、拼错 token)
53
+ // 塌 never。
54
+ type ValOf<T> =
55
+ T extends `<${infer B}>ptr!` ? Ptr<B>
56
+ : T extends `<${string}>ptr@${infer E extends keyof EnumMap}` ? EnumMap[E]
57
+ : T extends `<${infer B}>ptr` ? MaybePtr<B>
58
+ : T extends `${infer U}@${infer E extends keyof EnumMap}` ? (U extends C_Number ? EnumMap[E] : never)
59
+ : T extends C_Number ? C_TypeJsTypeMap[T]
60
+ : never
61
+
62
+ // 位域糖 → 形状:unit 非整数档(如 'f32:3';T 已被 SimpleToken 守卫限定为规范 token)
63
+ // 塌 never(运行时同报错);width ≤53 可被 number 精确表示 → number,否则 bigint。
64
+ type BitShape<T extends string, W extends number> =
65
+ T extends C_Integer ? (W extends N ? number : bigint) : never
66
+
67
+ // 值 → 形状。对象值一律按 '#' 分派(无「裸嵌套 map 默认 struct」—— 漏写 '#' 是错误)。
68
+ // 字符串糖(值尾带 @encoding)在数组糖之前判别 —— 两形态本不相交(数组糖以 ] 收尾),
69
+ // 特判在前更可读。注意 infer 出的 L 是字符串字面('8'),不能写 `L extends number`
70
+ // (string 永不 extends number → 塌 never),要用 `L extends `${number}`` 检查;
71
+ // U/L/E 都在条件里被引用,避免未使用的 infer 触发 TS6196。
72
+ type ShapeOfValue<V> =
73
+ V extends infer T extends SimpleToken ? ValOf<T>
74
+ : V extends `${infer U}[${infer L}]@${infer E}`
75
+ ? (U extends 'u8' | 'u16' ? (L extends `${number}` ? (E extends Encoding ? string : never) : never) : never)
76
+ : V extends `${infer T extends SimpleToken}[${infer L extends number}]` ? Tuple<ShapeOfValue<T>, L>
77
+ : V extends `${infer T extends SimpleToken}:${infer W extends number}` ? BitShape<T, W>
78
+ : V extends { '#': 'array', element: infer E, length: infer L extends number } ? Tuple<ShapeOfValue<E>, L>
79
+ : V extends { '#': 'struct' | 'union' } ? ShapeOfC<V>
80
+ : never
81
+
82
+ type Tuple<T, N extends number, R extends T[] = []> =
83
+ number extends N ? T[]
84
+ : R['length'] extends N ? R
85
+ : R['length'] extends 64 ? T[]
86
+ : Tuple<T, N, [...R, T]>
87
+ type DollarKeys<T> = { [K in keyof T]: K extends `$${string}` ? K : never }[keyof T];
88
+ type UnionToIntersection<U> =
89
+ (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
90
+ type Lift<T> =
91
+ {
92
+ [K in keyof T as K extends `$${string}` ? never : K]:
93
+ T[K] extends readonly unknown[] ? T[K] :
94
+ // 品牌 number(NULL / Ptr = 0&{brand} 或 number&{brand})经自己的对象半边
95
+ // extends object,会被误当嵌套对象递归,把 number 榨成方法集对象(丧失
96
+ // number 属性)——number 档先跳过。只递归进真正嵌套的结构体形状。
97
+ T[K] extends number ? T[K] :
98
+ T[K] extends object ? Lift<T[K]> : T[K];
99
+ } & UnionToIntersection<
100
+ { [K in DollarKeys<T>]: Lift<T[K]> }[DollarKeys<T>]
101
+ >;
102
+ // 形状层刻意不设约束:M 常是泛型实例化里的 C_Struct 交叉(在 struct()/union() 声明处
103
+ // 无法证明满足模板索引签名),约束检查会在这里炸掉(TS2344/TS2589)。
104
+ // 非法 M 落到 ShapeOfValue 的 never 分支,由具体实例化点暴露。
105
+ // 指令键('#'/'#pack')不进形状;键尾 '@N'(alignas)拆掉 —— 对齐只影响布局、不影响读写形状。
106
+ type FieldKey<K> =
107
+ K extends `#${string}` ? never
108
+ : K extends `${infer B}@${string}` ? B
109
+ : K
110
+ type _ShapeOfC<M> = {
111
+ [K in keyof M as FieldKey<K>]: ShapeOfValue<M[K]>
112
+ }
113
+ type ShapeOfC<M> = Lift<_ShapeOfC<M>>
114
+
115
+ // encode 入参的深度可选形态:缺省字段运行时跳过(doEncode 的 undefined-continue——
116
+ // 不动 buffer 该字段,新建 buffer 上等价写 0),decode 返回仍是全字段 ShapeOfC。
117
+ // 数组元组排除在外:writeArray 是整体写入语义,"省略第 N 个元素"没有意义——
118
+ // 数组字段维持"要么给全、要么整个缺省"(整个缺省由外层 continue 拦下)。
119
+ export type DeepPartial<T> =
120
+ T extends readonly unknown[] ? T
121
+ : T extends number | string | boolean | bigint | symbol ? T
122
+ : { [K in keyof T]?: DeepPartial<T[K]> }
123
+
124
+ // encode 物化入参:缺省字段运行时跳过(doEncode 的 undefined-continue —— 不动 buffer,
125
+ // 新建槽等价写 0),decode 返回仍是全字段 ShapeOfC。
126
+ // 必填集 = '!' 字段(非空担保,省略 = 槽留 0 违约)∪ '@' 值域字段中 0 ∉ 值域的(省略 =
127
+ // 新槽写 0,0 不在枚举值域同样是静默违约;0 ∈ 值域或宽 number 值域可省 —— 省略产生的 0
128
+ // 本身就是合法枚举值)。判定从 T 的**原始 token** 按 DFS 后序递归展开(子先判、父取 OR):
129
+ // 原文 token 是标注的唯一权威,且 `<>ptr!` 的 '!' 在形上已塌进裸 number(Ptr<''> 空品牌
130
+ // 归一),只有 token 能看见。数组(`{'#':'array'}` 与 `u32@E[N]` 糖)按**元素**穿透 ——
131
+ // 整组省略 = 元素全 0,违约同理;嵌套 struct/union 递归字段 —— 子树含必填 ⇒ 本键必填,
132
+ // 堵掉「encode 通过、decode 0 断言炸」的嵌套/数组缺口。
133
+ // 必填键的值经 EncodeValOfT **后序合成**:嵌套 struct/union 再套同一 EncodeShapeOfC
134
+ // (内层必填键强制、非必填兄弟维持 DeepPartial 可省 —— 与运行时逐字段对应:省略兄弟 =
135
+ // 写 0,合法零值),标量/数组取全量形。其余字段维持 DeepPartial 缺省语义(省略 = 跳过,
136
+ // 新建槽写 0 / 复用槽留旧值)。
137
+ // 无必填字段时整体退化为纯 DeepPartial、**不进交集**:`{} & {全可选}` 会绕过 TS 的
138
+ // weak-type 检查(TS2559 'no properties in common' 不再触发),裸 number/string 就此
139
+ // 混进任何 codec 实参位(asctime(123) 回归实测)。元组包一层防 never 分发。
140
+ type IsSubtreeMandatory<D> =
141
+ // —— 结构形态:DFS 后序(先子后父)——
142
+ D extends { '#': 'array', element: infer E } ? IsSubtreeMandatory<E>
143
+ : D extends { '#': 'struct' | 'union' }
144
+ ? (true extends { [K in keyof D]-?: IsSubtreeMandatory<D[K]> extends true ? true : false }[keyof D] ? true : false)
145
+ // —— token 形态:分支序对齐 ShapeOfValue(字符串糖先于数组糖,编码 @ 先于枚举 @)——
146
+ : D extends `${string}[${number}]@${string}` ? false // 字符串糖('u16[4]@utf-8'):'' 合法 → 可省
147
+ : D extends `${infer TK extends SimpleToken}[${string}]` ? IsSubtreeMandatory<TK> // 数组糖穿透元素
148
+ : D extends `${string}:${number}` ? false // 位域:数值零合法 → 可省
149
+ : D extends `<${string}>ptr!` ? true // '!'(含裸 <>ptr! —— 形塌 number,token 是唯一判据)
150
+ : D extends `<${string}>ptr@${infer E extends keyof EnumMap}` ? (0 extends EnumMap[E] ? false : true)
151
+ : D extends `${infer U}@${infer E extends keyof EnumMap}`
152
+ ? (U extends C_Number ? (0 extends EnumMap[E] ? false : true) : false)
153
+ : false
154
+ // 必填键 → 其**原始声明值**的映射(`as` 重映射一步完成过滤 + alignas 归一 + 剔 '#')。
155
+ type MandatoryDeclKeys<T> = {
156
+ [K in keyof T]-?: IsSubtreeMandatory<T[K]> extends true ? FieldKey<K> : never
157
+ }[keyof T]
158
+ // 必填键的值:嵌套 struct/union 后序递归同一形态,其余(标量/数组/糖)取全量形。
159
+ type EncodeValOfT<D> =
160
+ D extends { '#': 'struct' | 'union' } ? EncodeShapeOfC<D>
161
+ : ShapeOfValue<D>
162
+ type EncodeShapeOfC<T> =
163
+ DeepPartial<ShapeOfC<T>> & {
164
+ [K in keyof T as IsSubtreeMandatory<T[K]> extends true ? FieldKey<K> : never]-?: EncodeValOfT<T[K]>
165
+ }
166
+ type EncodeIn<T extends { '#': 'struct' | 'union' }> =
167
+ [MandatoryDeclKeys<T>] extends [never]
168
+ ? DeepPartial<ShapeOfC<T>>
169
+ : EncodeShapeOfC<T>
170
+
171
+
172
+ // ============================================================
173
+ // 运行时类型
174
+ // ============================================================
175
+
176
+ // lower 后指针统一归一为内部 kind 'ptr'(用户面写 '<>ptr' 或 '<NAME>ptr')。
177
+
178
+ // kind → size/align(不含指针:指针统一 PTR_SIZE,见 lower)。
179
+
180
+
181
+ // 运行时 Field:IR 的 lowered 视图(独立于语法层,不保留 alignas)。
182
+ // computeStructLayout 在 lowering 时把子布局内嵌进 FieldType(struct/union→fields,
183
+ // array→elementType+elementSize,bitfield→bit 单元内偏移),运行期 read/write 零查表。
184
+ // offset 相对本层起点;位域成员的 offset 是其存储单元的基址(不是位域自己的地址)。
185
+ type FieldType =
186
+ { tag: 'basic', kind: C_BasicType_No_Void }
187
+ | { tag: 'bitfield', unit: C_Integer, bit: number, width: number }
188
+ | { tag: 'string', encoding: Encoding }
189
+ | { tag: 'struct' | 'union', fields: Field[] }
190
+ | { tag: 'array', elementType: FieldType, elementSize: number }
191
+ export type Field = {
192
+ name: string
193
+ offset: number
194
+ size: number
195
+ type: FieldType
196
+ /** '!' 标注字段(原文 token `<X>ptr!`):decode 读出 0 当场 throw(后置兜底 ——
197
+ * C 没填 / 拿未填充槽 decode 都在这里拦,见 doDecode)。 */
198
+ nonnull?: true
199
+ }
200
+ export type Fields = Field[]
201
+
202
+ // T 约束 = '#' 声明本身:struct()/union() 构造的 `{'#':'struct'} & M` 可证 —— M 是平铺
203
+ // 字段表(SimpleMember 无 '#' 键),交叉的 '#' 取字面声明。形状层不设约束的理由见 ShapeOfC。
204
+ // N = 布局品牌(struct(name, …) 传入):decode 入参与 alloc().ptr 同品牌。默认 '' ——
205
+ // `Ptr<''>` 归一裸 number(与旧 never 守卫结果相同),品牌化 def 未来收窄时签名不用再动。
206
+ export type StructDef<T extends { '#': 'struct' | 'union' }, N extends string = ''> = {
207
+ readonly __struct: T
208
+ readonly size: number
209
+ readonly structAlign: number
210
+ decode(buf: ArrayBuffer | Ptr<N>): ShapeOfC<T>
211
+ // alloc = 出参槽分配(全新全 0:'!' 字段的 0 是出参哨兵,decode 的 0 断言兜 C 没填);
212
+ // encode = 物化(C 要读的对象),带品牌 .ptr 直接喂 <N>ptr 形参。'!' 字段在 EncodeIn
213
+ // 里必填 —— 省略即编译错,不静默写 0 / 复用槽留旧值(DFS 后序穿透嵌套与数组:子树/
214
+ // 数组元素含必填 ⇒ 该键必给,值后序合成到内层)。带 buf = 写入调用方既有
215
+ // PtrArrayBuffer(返回其本身,供链式读回)——encode 出口一律 PAB,裸 ArrayBuffer
216
+ // 不出 codec(与 CodecMap 同约束)。动态调用方无参 encode 由运行时 throw 兜底。
217
+ alloc(): PtrArrayBuffer<N>
218
+ encode(v: EncodeIn<T>): PtrArrayBuffer<N>
219
+ // 带 buf = 写入调用方既有 PtrArrayBuffer(复用/预分配形态,返回其本身供链式读回)。
220
+ // 刻意没有 offset —— 与 decode 同理(bind.ts CodecMap 注释):PAB 的 .ptr 恒指向起点,
221
+ // 顶层再收 offset 只会掩盖"写错位置"且返回值 .ptr 对不上写入点;嵌套偏移由布局字段
222
+ // (doEncode 的 base)承担。
223
+ encode(v: EncodeIn<T>, buf: PtrArrayBuffer<any>): PtrArrayBuffer<any>
224
+ offsetOf(name: string): number
225
+ }
226
+
227
+ // ============================================================
228
+ // Layout IR —— computeStructLayout/lower 把 CType AST IR lower 到此。
229
+ // 自包含:子布局在 lowering 时内嵌、offset 相对本层起点,运行期 read/write 不回查 AST IR。
230
+ // ============================================================
231
+
232
+ /** 向上对齐(a 必须是 2 的幂)。 */
233
+ function alignUp(v: number, a: number): number {
234
+ return (v + a - 1) & ~(a - 1)
235
+ }
236
+
237
+ type Layout = {
238
+ size: number,
239
+ maxEffectiveAlign: number,
240
+ fields: Fields,
241
+ }
242
+
243
+ // 归一后 token 的档位检查:'void'/指针 token 查表为 undefined(SizeAlign 只收 C_Number)。
244
+ const isCNumberToken = (t: C_BasicType_Token): t is C_Number =>
245
+ SizeAlign[t as C_Number] !== undefined
246
+
247
+ // 值 → { size, align, FieldType }:字符串先试字符串糖/数组糖/位域糖,再按 token 归一;
248
+ // 对象值一律按 '#' 分派 —— 无「裸嵌套 map 默认 struct」,漏写 '#' 直接报错。
249
+ // 字符串无 '#' 声明形式(词汇只有糖,见 ctype.ts §4):旧写法落到末尾 unknown '#' 报错。
250
+ function lower(v: unknown): { size: number, align: number, type: FieldType } {
251
+ if (typeof v === 'string') {
252
+ const st = /^(u8|u16)\[(\d+)\]@(utf-8|utf-16le)$/.exec(v) // 'u16[128]@utf-16le' → 字符串糖
253
+ if (st) {
254
+ const length = Number(st[2])
255
+ if (!Number.isInteger(length) || length < 1)
256
+ throw new Error(`ffi-struct: string length must be an integer >= 1, got ${length}`)
257
+ // 布局 = unit × length(与 encoding 无关),encoding 只是读写时的解释 ——
258
+ // 'u8[128]@utf-16le'(128 字节按 utf-16le 解出 64 字符)之类的错配语义自洽。
259
+ const s = st[1] === 'u8' ? 1 : 2
260
+ return { size: s * length, align: s, type: { tag: 'string', encoding: st[3] as Encoding } }
261
+ }
262
+ const a = /^([^\[\]]+)\[(\d+)\]$/.exec(v) // 'u32[4]' / 'i3z[4]' → 数组糖(元素递归归一)
263
+ if (a) {
264
+ const length = Number(a[2])
265
+ if (!Number.isInteger(length) || length < 1)
266
+ throw new Error(`ffi-struct: array length must be an integer >= 1, got ${length}`)
267
+ const el = lower(a[1])
268
+ if (el.type.tag === 'bitfield')
269
+ throw new Error('ffi-struct: bitfields cannot be array elements')
270
+ return {
271
+ size: el.size * length,
272
+ align: el.align,
273
+ type: { tag: 'array', elementType: el.type, elementSize: el.size },
274
+ }
275
+ }
276
+ const b = /^([A-Za-z_]\w*):(\d+)$/.exec(v) // 'u32:3' → 位域糖('f32:3' 归一后非整数档被拒)
277
+ if (b) {
278
+ const rawUnit = b[1]!
279
+ const unit = normToken(rawUnit)
280
+ // 'void'/'<>ptr' 查表为 undefined、'float' 等别名 normToken 直接抛 —— 都不是合法位域单元。
281
+ if (!isCNumberToken(unit) || unit === 'f32' || unit === 'f64')
282
+ throw new Error(`ffi-struct: bitfield unit must be an integer type, got "${rawUnit}"`)
283
+ const width = Number(b[2])
284
+ const s = SizeAlign[unit]
285
+ const cap = s * 8
286
+ if (!Number.isInteger(width) || width < 1)
287
+ throw new Error(`ffi-struct: bitfield width must be an integer >= 1, got ${width}`)
288
+ if (width > cap)
289
+ throw new Error(`ffi-struct: bitfield width ${width} exceeds unit width ${cap}`)
290
+ // bit 是单元内偏移,由 computeStructLayout 的位域状态机填写;这里占位 0。
291
+ return { size: s, align: s, type: { tag: 'bitfield', unit, bit: 0, width } }
292
+ }
293
+ // 规范 token / 指针 token(别名机制已移除:'DWORD' 等 normToken 直接抛)。
294
+ // 归一后必须先判指针形,否则落 unknown kind;'void' 归一后无大小、
295
+ // 裸 'ptr'/'i3z' 未知 —— 都在这里抛(原先由 SizeAlign 查表兜住,现归一并入)。
296
+ const k = normToken(v)
297
+ if (isCPtrToken(k))
298
+ return { size: PTR_SIZE, align: PTR_SIZE, type: { tag: 'basic', kind: 'ptr' } }
299
+ if (!isCNumberToken(k)) throw new Error(`ffi-struct: unknown kind "${v}"`)
300
+ const s = SizeAlign[k]
301
+ return { size: s, align: s, type: { tag: 'basic', kind: k } }
302
+ }
303
+ if (v && typeof v === 'object') {
304
+ const o = v as Record<string, unknown>
305
+ const tag = o['#']
306
+ if (tag === undefined)
307
+ throw new Error(`ffi-struct: nested object value requires a '#' key ('struct' | 'union' | 'array')`)
308
+ if (tag === 'array' && '#pack' in o)
309
+ throw new Error(`ffi-struct: '#pack' is only allowed on struct/union, got '${tag}'`)
310
+ if (tag === 'array') {
311
+ const length = o['length']
312
+ if (!Number.isInteger(length) || (length as number) < 1)
313
+ throw new Error(`ffi-struct: array length must be an integer >= 1, got ${String(length)}`)
314
+ const el = lower(o['element'])
315
+ if (el.type.tag === 'bitfield')
316
+ throw new Error('ffi-struct: bitfields cannot be array elements')
317
+ return {
318
+ size: el.size * (length as number),
319
+ align: el.align,
320
+ type: { tag: 'array', elementType: el.type, elementSize: el.size },
321
+ }
322
+ }
323
+ if (tag === 'struct' || tag === 'union') {
324
+ const l = computeStructLayout(v as C_Struct | C_Union)
325
+ return { size: l.size, align: l.maxEffectiveAlign, type: { tag, fields: l.fields } }
326
+ }
327
+ throw new Error(`ffi-struct: unknown '#' declaration ${JSON.stringify(String(tag))}`)
328
+ }
329
+ throw new Error(`ffi-struct: invalid member value: ${String(v)}`)
330
+ }
331
+
332
+ export function computeArray(t: C_Array): { align: number, size: number } {
333
+ const { size, align } = lower(t)
334
+ return { align, size }
335
+ }
336
+
337
+ export function computeStructLayout(t: C_Struct | C_Union): Layout {
338
+ const isStruct = t['#'] === 'struct'
339
+ const pack = t['#pack'] ?? 8
340
+ let maxEffectiveAlign = 1
341
+ let maxMemberSize = 0
342
+ const fields: Fields = []
343
+ let cursor = 0
344
+
345
+ // 位域组状态:unitBase 是当前存储单元的字节基址(-1 = 不在位域组内),
346
+ // unitBits 是单元内已用位数,unitSize 是单元宽(字节)。
347
+ // MSVC 规则(mingw 交叉实测 DCB/COMSTAT 验证):
348
+ // - 连续同宽位域共单元,LSB→MSB(签别不参与分组)
349
+ // - 放不下的位域开新单元,单元剩余位废弃
350
+ // - 非位域成员打断位域组,游标跳到完整单元边界(不是按字节截断)
351
+ // 联合体位域不分组:每位域成员独立从 offset 0/bit 0 起算——完全别名
352
+ // (mingw 16 x64+ia32 实测:a=1→b=1、溢出成员回 offset 0、sizeof=单元宽)。
353
+ let unitBase = -1
354
+ let unitBits = 0
355
+ let unitSize = 0
356
+
357
+ // 落定当前位域组:游标 = 单元起点 + 已用字节数,向上取整到单元宽。
358
+ const flushUnit = () => {
359
+ if (unitBase < 0) return
360
+ cursor = alignUp(unitBase + Math.ceil(unitBits / 8), unitSize)
361
+ unitBase = -1
362
+ }
363
+
364
+ for (const [rawName, v] of Object.entries(t as unknown as Record<string, unknown>)) {
365
+ if (rawName.startsWith('#')) continue // '#' 指令键('#'/'#pack')不是字段
366
+ const m = /^([^@]+)@(\d+)$/.exec(rawName) // 键尾 '@N' = alignas(字段名不含 '@')
367
+ const name = m ? m[1]! : rawName
368
+ const alignas = m ? Number(m[2]) : 0
369
+ const { size, align: natural, type } = lower(v)
370
+ // '!' 非空担保标记(原文 `<X>ptr!`):lower 里 normToken 已拒掉 'u32!'/'HANDLE!' 等
371
+ // 非法尾缀,能走到这里的 '!' 尾必是指针 token —— 记到 Field 上供 decode 的 0 断言。
372
+ const nonnull = typeof v === 'string' && v.endsWith('!')
373
+ let align = natural
374
+ if (pack > 0) align = Math.min(pack, align)
375
+ if (alignas) align = Math.max(align, alignas)
376
+
377
+ if (type.tag === 'bitfield') {
378
+ if (name.startsWith('$')) throw new Error('ffi-struct: bitfield members must be named')
379
+
380
+ // union:成员同址(C 标准),每位域成员独立从 offset 0/bit 0 起算。
381
+ // 不顺序分组、不开新单元、不动游标(flushUnit 因 unitBase 恒 -1 而
382
+ // no-op,普通/匿名成员的 offset 也因此保持 0)——曾因游标推进致
383
+ // 溢出单元/后续成员落在 union 声明 size 之外(越界读写)。
384
+ if (!isStruct) {
385
+ fields.push({ name: name, offset: 0, size, type: { ...type, bit: 0 } })
386
+ maxEffectiveAlign = Math.max(maxEffectiveAlign, align)
387
+ maxMemberSize = Math.max(maxMemberSize, size)
388
+ continue
389
+ }
390
+
391
+ const width = type.width
392
+
393
+ // 新单元:无活动单元 / 单元宽不同 / 放不下。
394
+ if (unitBase < 0 || size !== unitSize || unitBits + width > unitSize * 8) {
395
+ // 单元宽变化或溢出时先把旧单元落定,新单元从落定点对齐。
396
+ if (unitBase >= 0) flushUnit()
397
+ const offset = alignUp(cursor, align)
398
+ unitBase = offset
399
+ unitBits = 0
400
+ unitSize = size
401
+ }
402
+
403
+ fields.push({
404
+ name: name,
405
+ offset: unitBase, // 单元基址(不是位域自己的地址)
406
+ size,
407
+ type: { ...type, bit: unitBits },
408
+ })
409
+ unitBits += width
410
+
411
+ maxEffectiveAlign = Math.max(maxEffectiveAlign, align)
412
+ maxMemberSize = Math.max(maxMemberSize, size)
413
+ continue
414
+ }
415
+
416
+ flushUnit()
417
+ const offset = alignUp(cursor, align)
418
+
419
+ if (!name.startsWith('$')) {
420
+ fields.push({ name: name, offset, size, type, ...(nonnull ? { nonnull: true as const } : {}) })
421
+ } else {
422
+ // 匿名聚合:内嵌 fields 已 lower 好,offset 平移到本成员起点。
423
+ if (type.tag !== 'struct' && type.tag !== 'union')
424
+ throw new Error(`ffi-struct: anonymous member "${name}" must be a struct/union`)
425
+ for (const cf of type.fields)
426
+ fields.push({ ...cf, offset: cf.offset + offset })
427
+ }
428
+
429
+ if (isStruct) cursor = offset + size
430
+ maxEffectiveAlign = Math.max(maxEffectiveAlign, align)
431
+ maxMemberSize = Math.max(maxMemberSize, size)
432
+ }
433
+
434
+ flushUnit()
435
+
436
+ return {
437
+ size: isStruct ? alignUp(cursor, maxEffectiveAlign) : alignUp(maxMemberSize, maxEffectiveAlign),
438
+ maxEffectiveAlign,
439
+ fields,
440
+ }
441
+ }
442
+
443
+ // ============================================================
444
+ // 位域读写
445
+ // ============================================================
446
+ // 存储单元统一按无符号读(避免单元高位的符号位干扰提取),再用掩码取出位域。
447
+ // 有符号单元在提取后做符号扩展。写是读-改-写:只改本位域占的位,其余位保留。
448
+ // 64 位单元往返走 BigInt;读回时 width>53 返回 bigint、否则 number(见 readBitfield)。
449
+ type BitfieldType = Extract<FieldType, { tag: 'bitfield' }>
450
+
451
+ function readUnitRaw(dv: DataView, off: number, unit: C_Integer): bigint {
452
+ const s = SizeAlign[unit]
453
+ if (s === 1) return BigInt(dv.getUint8(off))
454
+ if (s === 2) return BigInt(dv.getUint16(off, true))
455
+ if (s === 4) return BigInt(dv.getUint32(off, true))
456
+ return dv.getBigUint64(off, true)
457
+ }
458
+
459
+ function writeUnitRaw(dv: DataView, off: number, unit: C_Integer, v: bigint): void {
460
+ const s = SizeAlign[unit]
461
+ if (s === 1) { dv.setUint8(off, Number(v & 0xffn)); return }
462
+ if (s === 2) { dv.setUint16(off, Number(v & 0xffffn), true); return }
463
+ if (s === 4) { dv.setUint32(off, Number(v & 0xffffffffn), true); return }
464
+ dv.setBigUint64(off, v & 0xffffffffffffffffn, true)
465
+ }
466
+
467
+ function readBitfield(dv: DataView, off: number, t: BitfieldType): number | bigint {
468
+ const w = BigInt(t.width)
469
+ let v = (readUnitRaw(dv, off, t.unit) >> BigInt(t.bit)) & ((1n << w) - 1n)
470
+ if (t.unit[0] === 'i' && (v & (1n << (w - 1n)))) // 有符号单元且符号位为 1 → 先符号扩展
471
+ v -= 1n << w
472
+ if (t.width > 53) return v // 值域装不进安全整数 → bigint(与类型层 BitShape 同界)
473
+ return Number(v)
474
+ }
475
+
476
+ function writeBitfield(dv: DataView, off: number, t: BitfieldType, val: number | bigint): void {
477
+ const w = BigInt(t.width)
478
+ const bit = BigInt(t.bit)
479
+ const widthMask = (1n << w) - 1n
480
+ const posMask = widthMask << bit
481
+ const raw = readUnitRaw(dv, off, t.unit)
482
+ const bits = (typeof val === 'bigint' ? val : BigInt(Math.trunc(val))) & widthMask
483
+ writeUnitRaw(dv, off, t.unit, (raw & ~posMask) | (bits << bit))
484
+ }
485
+
486
+ // 字符串读写统一走 TextEncoder/TextDecoder(polyfill 见 lib/text-codec.js)。
487
+ const stringEncoders: Record<Encoding, TextEncoder> = {
488
+ 'utf-8': new TextEncoder('utf-8'),
489
+ 'utf-16le': new TextEncoder('utf-16le'),
490
+ }
491
+
492
+ const stringDecoders: Record<Encoding, TextDecoder> = {
493
+ 'utf-8': new TextDecoder('utf-8', { ignoreBOM: true }),
494
+ 'utf-16le': new TextDecoder('utf-16le', { ignoreBOM: true }),
495
+ }
496
+
497
+ function readString(dv: DataView, off: number, size: number, enc: Encoding): string {
498
+ const bytes = new Uint8Array(dv.buffer, dv.byteOffset + off, size)
499
+ const unit = enc === 'utf-16le' ? 2 : 1
500
+ let end = size
501
+ for (let i = 0; i + unit <= size; i += unit) {
502
+ if (bytes[i] === 0 && (unit === 1 || bytes[i + 1] === 0)) { end = i; break }
503
+ }
504
+ return stringDecoders[enc].decode(bytes.subarray(0, end))
505
+ }
506
+
507
+ function writeString(dv: DataView, off: number, size: number, enc: Encoding, v: unknown): void {
508
+ const bytes = new Uint8Array(dv.buffer, dv.byteOffset + off, size)
509
+ bytes.fill(0)
510
+ // 始终给 NUL 终止符留一个存储单元(宽度由 encoding 决定,见 readString):
511
+ // 末尾保持 0,payload 最多 size - unit 字节。这样 C 侧 strlen/wcslen 都不会越界。
512
+ const unit = enc === 'utf-16le' ? 2 : 1
513
+ const cap = size - unit
514
+ if (cap <= 0) return
515
+ const src = stringEncoders[enc].encode(String(v ?? ''))
516
+ bytes.set(src.subarray(0, Math.min(src.length, cap)))
517
+ }
518
+
519
+ type ArrayFieldType = Extract<FieldType, { tag: 'array' }>
520
+
521
+ function readArray(dv: DataView, off: number, size: number, t: ArrayFieldType): unknown[] {
522
+ const count = t.elementSize > 0 ? size / t.elementSize : 0
523
+ const out: unknown[] = []
524
+ for (let i = 0; i < count; i++)
525
+ out.push(readElement(dv, off + i * t.elementSize, t.elementType, t.elementSize))
526
+ return out
527
+ }
528
+
529
+ function writeArray(dv: DataView, off: number, size: number, t: ArrayFieldType, v: unknown): void {
530
+ const count = t.elementSize > 0 ? size / t.elementSize : 0
531
+ const list = (v ?? []) as unknown[]
532
+ for (let i = 0; i < count; i++)
533
+ writeElement(dv, off + i * t.elementSize, t.elementType, t.elementSize, list[i])
534
+ }
535
+
536
+ function readElement(dv: DataView, off: number, t: FieldType, size: number): unknown {
537
+ switch (t.tag) {
538
+ case 'basic': return readScalar(dv, off, t.kind)
539
+ case 'bitfield': return readBitfield(dv, off, t)
540
+ case 'string': return readString(dv, off, size, t.encoding)
541
+ case 'struct':
542
+ case 'union': return doDecode(dv, off, t.fields)
543
+ case 'array': return readArray(dv, off, size, t)
544
+ }
545
+ }
546
+
547
+ function writeElement(dv: DataView, off: number, t: FieldType, size: number, val: unknown): void {
548
+ switch (t.tag) {
549
+ // TODO: remove this any
550
+ case 'basic': writeScalar(dv, off, { k: t.kind, v: val as any }); break
551
+ // 直传(不 Number()):writeBitfield 的 val 是 number | bigint,
552
+ // 64 位单元走 bigint,先 Number() 会把 >2^53 的位域值截断。
553
+ case 'bitfield': writeBitfield(dv, off, t, (val ?? 0) as number | bigint); break
554
+ case 'string': writeString(dv, off, size, t.encoding, val); break
555
+ case 'struct':
556
+ case 'union': doEncode(dv, off, t.fields, (val ?? {}) as Record<string, unknown>); break
557
+ case 'array': writeArray(dv, off, size, t, val); break
558
+ }
559
+ }
560
+
561
+ function doDecode(dv: DataView, base: number, fields: Fields): Record<string, unknown> {
562
+ const out: Record<string, unknown> = {}
563
+ for (const f of fields) {
564
+ const v = readElement(dv, base + f.offset, f.type, f.size)
565
+ // '!' 非空担保的后置兜底:C 没填(忘判成功就 decode)或拿未填充槽(decode(alloc()))
566
+ // 读出 0 —— 当场 throw,不让 0 带 Ptr 品牌流到下游 '! 消费位'。成功路径必非零,不触发。
567
+ if (f.nonnull && v === 0)
568
+ throw new Error(`ffi-struct: decode: '!' field "${f.name}" got 0 ` +
569
+ `(C did not fill it, or decode was called on an unfilled slot)`)
570
+ out[f.name] = v
571
+ }
572
+ return out
573
+ }
574
+
575
+ function doEncode(dv: DataView, base: number, fields: Fields, v: Record<string, unknown>): void {
576
+ for (const f of fields) {
577
+ // DeepPartial 缺省:不动该字段(新建 buffer 上保持 0、复用 buffer 上保留旧值);
578
+ // 嵌套/数组字段整个缺失也在这里拦下(writeElement 的 struct 分支回到本函数)。
579
+ if (v[f.name] === undefined) continue
580
+ writeElement(dv, base + f.offset, f.type, f.size, v[f.name])
581
+ }
582
+ }
583
+
584
+ // ============================================================
585
+ // 内部构造器
586
+ // ============================================================
587
+
588
+ function createStruct(t: C_Struct | C_Union): any {
589
+ const { fields, size, maxEffectiveAlign } = computeStructLayout(t)
590
+ return {
591
+ __struct: t,
592
+ size,
593
+ structAlign: maxEffectiveAlign,
594
+ decode: (p: ArrayBuffer | number) => {
595
+ // number = native 拥有的品牌指针(Codec 的 decode(p) 形态):按布局 size
596
+ // 逐字节拷进临时 buffer 再解码;ArrayBuffer = 调用方持有的 out buffer 直读。
597
+ // 顶层不收 offset——out 参数恒从起点读,嵌套偏移由字段布局(doDecode 的 base)承担。
598
+ if (typeof p === 'number') {
599
+ const buf = new ArrayBuffer(size)
600
+ const u8 = new Uint8Array(buf)
601
+ for (let i = 0; i < size; i++) u8[i] = ffi.readByte(p + i)
602
+ return doDecode(new DataView(buf), 0, fields)
603
+ }
604
+ return doDecode(new DataView(p), 0, fields)
605
+ },
606
+ alloc: (() => new PtrArrayBuffer(size)) as StructDef<any, any>['alloc'],
607
+ encode: ((v: any, buf?: PtrArrayBuffer<any>) => {
608
+ // fail-loud:无参 = 分配语义,走 alloc();这里绝不静默零填充 —— 类型层由
609
+ // EncodeIn 拦 '!' 字段省略,动态/JS 调用方由本 throw 兜住。顶层不收 offset
610
+ // (与 decode 对齐,见 StructDef 注释),恒从 buffer 起点写。
611
+ if (v === undefined || v === null)
612
+ throw new Error(`ffi-struct: encode(v) requires an argument; ` +
613
+ `use alloc() for a zeroed out-param slot`)
614
+ const out = buf ?? new PtrArrayBuffer(size)
615
+ doEncode(new DataView(out), 0, fields, v)
616
+ return out
617
+ }) as StructDef<any, any>['encode'],
618
+ offsetOf: (name: string) => {
619
+ for (const f of fields) if (f.name === name) return f.offset
620
+ throw new Error(`ffi-struct: no field "${name}"`)
621
+ },
622
+ }
623
+ }
624
+
625
+ // ============================================================
626
+ // 公共 API
627
+ // ============================================================
628
+
629
+ /** 聚合对齐上限(pack 压 maxAlign,语义同 MSVC #pragma pack)。 */
630
+ export type AggOpts = { pack?: number }
631
+
632
+ /** 平铺字段表 → '#' 声明:'#'/`#pack` 由构造器写入(顶层 kind 由函数名表明,字段表本身
633
+ * 不收指令键)。member 里混进的 '#' 前缀键在此剔除 —— 类型层已禁(fresh literal excess
634
+ * check),这里运行时兜底,避免用户键覆盖构造器写入的 '#'/`#pack`。 */
635
+ function buildDecl(kind: 'struct' | 'union', member: Record<string, unknown>, pack?: number): C_Struct | C_Union {
636
+ const decl: Record<string, unknown> = { '#': kind }
637
+ if (pack !== undefined) decl['#pack'] = pack
638
+ for (const [k, v] of Object.entries(member))
639
+ if (!k.startsWith('#')) decl[k] = v
640
+ return decl as unknown as C_Struct | C_Union
641
+ }
642
+
643
+ /** 平铺字段表定义结构体(token/`X[n]`/`unit:width`/`'#'` 声明均可,见 ctype.ts §4)。
644
+ * 给 name 则 alloc().ptr 与 decode 入参带 Ptr<name> 品牌。 */
645
+ type StructOf<M, N extends string = ''> = StructDef<{ '#': 'struct', '#pack'?: number } & M, N>
646
+ type UnionOf<M, N extends string = ''> = StructDef<{ '#': 'union', '#pack'?: number } & M, N>
647
+ export function struct<const M extends SimpleMember>(member: M, opts?: AggOpts): StructOf<M>
648
+ export function struct<const N extends string, const M extends SimpleMember>(name: N, member: M, opts?: AggOpts): StructOf<M, N>
649
+ export function struct(a: string | SimpleMember, b?: SimpleMember | AggOpts, c?: AggOpts): any {
650
+ const isNamed = typeof a === 'string'
651
+ const raw = (isNamed ? b : a) as Record<string, unknown>
652
+ const opts = (isNamed ? c : b) as AggOpts | undefined
653
+ return createStruct(buildDecl('struct', raw, opts?.pack))
654
+ }
655
+
656
+ /** 平铺字段表定义联合体;布局/读写与 struct 相同,仅成员偏移重叠。 */
657
+ export function union<const M extends SimpleMember>(member: M, opts?: AggOpts): UnionOf<M>
658
+ export function union<const N extends string, const M extends SimpleMember>(name: N, member: M, opts?: AggOpts): UnionOf<M, N>
659
+ export function union(a: string | SimpleMember, b?: SimpleMember | AggOpts, c?: AggOpts): any {
660
+ const isNamed = typeof a === 'string'
661
+ const raw = (isNamed ? b : a) as Record<string, unknown>
662
+ const opts = (isNamed ? c : b) as AggOpts | undefined
663
+ return createStruct(buildDecl('union', raw, opts?.pack))
664
+ }