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,798 @@
1
+ import * as os from 'os'
2
+ import * as gui from 'gui'
3
+ import { Tester } from './test_helper.js'
4
+ import { struct, union } from '../lib/ffi/struct.js'
5
+ import { bind, } from '../lib/ffi/bind.js'
6
+ import { type MaybePtr, Ptr, PtrArrayBuffer } from '../lib/ffi/ctype.js'
7
+ import type { C_Union } from '../lib/ffi/ctype.js'
8
+
9
+ // 编译期断言工具(仅类型层,运行时无开销)
10
+ type Equal<A, B> = (<G>() => G extends A ? 1 : 2) extends (<G>() => G extends B ? 1 : 2) ? true : false
11
+ function expectType<T extends true>(_value?: T): void { }
12
+
13
+ // Ptr 定义:'' 判右侧(不作 naked 分布)——字面品牌照旧收窄、Ptr<''> 归一 number、
14
+ // never 不再塌缩成 never(旧定义下 Ptr<never> 即 never,decode(… | Ptr<N>) 会在 N=never 时堵死)
15
+ expectType<Equal<Ptr<''>, number>>(true)
16
+ expectType<Equal<Ptr<never>, never> extends false ? true : false>(true)
17
+
18
+ const RECT = /* @__PURE__ */ struct('RECT', {
19
+ left: 'i32',
20
+ top: 'i32',
21
+ right: 'i32',
22
+ bottom: 'i32',
23
+ })
24
+
25
+ const TVITEM = /* @__PURE__ */ struct('TVITEM', {
26
+ mask: 'u32',
27
+ hItem: '<>ptr',
28
+ state: 'u32',
29
+ stateMask: 'u32',
30
+ pszText: '<>ptr',
31
+ cchTextMax: 'i32',
32
+ iImage: 'i32',
33
+ iSelectedImage: 'i32',
34
+ cChildren: 'i32',
35
+ lParam: '<>ptr',
36
+ })
37
+ const TVINSERTSTRUCT = /* @__PURE__ */ struct('TVINSERTSTRUCT', {
38
+ hParent: '<>ptr',
39
+ hInsertAfter: '<>ptr',
40
+ item: TVITEM.__struct,
41
+ })
42
+
43
+ export const suite = {
44
+ name: 'ffi-struct',
45
+ run: (t: Tester) => {
46
+ const is64 = os.arch === 'x64'
47
+
48
+ t.section('RECT layout & roundtrip')
49
+ t.check('RECT.size == 16', 16, RECT.size)
50
+ const rbuf = RECT.encode({ left: 10, top: 20, right: 30, bottom: 40 })
51
+ const r = RECT.decode(rbuf)
52
+ t.check('left', 10, r.left)
53
+ t.check('top', 20, r.top)
54
+ t.check('right', 30, r.right)
55
+ t.check('bottom', 40, r.bottom)
56
+ t.check('offsetOf left', 0, RECT.offsetOf('left'))
57
+ t.check('offsetOf bottom', 12, RECT.offsetOf('bottom'))
58
+
59
+ // decode 入参随 N 品牌化:命名 struct 收 ArrayBuffer | Ptr<其名字>,
60
+ // 未命名(N='')才归一 ArrayBuffer | number
61
+ expectType<Equal<Parameters<typeof RECT.decode>[0], ArrayBuffer | Ptr<'RECT'>>>(true)
62
+ expectType<Equal<Parameters<typeof TVITEM.decode>[0], ArrayBuffer | Ptr<'TVITEM'>>>(true)
63
+ if (false) {
64
+ // @ts-expect-error plain number is not assignable to ArrayBuffer | Ptr<'RECT'>
65
+ RECT.decode(123)
66
+ }
67
+
68
+ t.section('GetWindowRect fills RECT buffer')
69
+ const getWindowRect = bind('user32.dll', 'GetWindowRect', '<>ptr <BYTE>ptr -> i32')
70
+ const getWindowRectLayout = bind('user32.dll', 'GetWindowRect', '<>ptr <RECT>ptr -> i32', { RECT })
71
+ const getDesktopWindow = bind('user32.dll', 'GetDesktopWindow', ' -> <>ptr')
72
+ const hwnd = getDesktopWindow()
73
+ const wrect = new PtrArrayBuffer(16)
74
+ const ok = getWindowRect(hwnd, wrect)
75
+ t.checkTrue('GetWindowRect succeeds', ok !== 0)
76
+ const wr = RECT.decode(wrect)
77
+ t.checkTrue('screen RECT non-empty', wr.right > 0 && wr.bottom > 0)
78
+
79
+ // out 参数:encode() 新建零 buffer 返回 PtrArrayBuffer<'RECT'> → .ptr 直接喂
80
+ // <RECT>ptr,读回走 def.decode(实例)(ArrayBuffer 分支直读)
81
+ t.section('RECT.alloc() out-param → PtrArrayBuffer')
82
+ const out = RECT.alloc()
83
+ t.checkTrue('encode() exposes ArrayBuffer subclass + ptr', out instanceof ArrayBuffer && typeof out.ptr === 'number')
84
+ t.checkTrue('GetWindowRect(hwnd, out.ptr) succeeds', getWindowRectLayout(hwnd, out.ptr) !== 0)
85
+ const or = RECT.decode(out)
86
+ t.checkTrue('RECT.decode(encode()) decodes out-param', or.right > 0 && or.bottom > 0)
87
+
88
+ t.section('ptr layout matches arch')
89
+ t.check(`TVITEM.size (${os.arch})`, is64 ? 56 : 40, TVITEM.size)
90
+ t.check('TVITEM.offsetOf pszText', is64 ? 24 : 16, TVITEM.offsetOf('pszText'))
91
+ t.check('TVINSERTSTRUCT.size', is64 ? 72 : 48, TVINSERTSTRUCT.size)
92
+ t.check('TVINSERTSTRUCT.offsetOf item', is64 ? 16 : 8, TVINSERTSTRUCT.offsetOf('item'))
93
+
94
+ t.section('nested struct roundtrip')
95
+ const ins = TVINSERTSTRUCT.encode({
96
+ hParent: 0x11111111,
97
+ hInsertAfter: 0x22222222,
98
+ item: {
99
+ mask: 1, hItem: 0x33333333, state: 0, stateMask: 0,
100
+ pszText: 0x44444444, cchTextMax: 100,
101
+ iImage: 0, iSelectedImage: 0, cChildren: 5, lParam: 0x55555555,
102
+ },
103
+ })
104
+ const got = TVINSERTSTRUCT.decode(ins)
105
+ t.check('nested hParent', 0x11111111, got.hParent)
106
+ t.check('nested hInsertAfter', 0x22222222, got.hInsertAfter)
107
+ t.check('nested cChildren', 5, got.item.cChildren)
108
+ t.check('nested ptr pszText', 0x44444444, got.item.pszText)
109
+
110
+ t.section('mixed scalar layout (i32/u32/i16/<>ptr;别名机制已移除)')
111
+ const M = struct('M', {
112
+ n: 'i32',
113
+ d: 'u32',
114
+ w: '<>ptr',
115
+ s: 'i16',
116
+ q: '<>ptr',
117
+ })
118
+ t.check(`mixed size (${os.arch})`, is64 ? 32 : 20, M.size)
119
+ const mb = M.encode({ n: -5, d: 0xFFFFFFFF, w: 0xAABBCCDD, s: -7, q: 0x11223344 })
120
+ const m = M.decode(mb)
121
+ t.check('i32 -5', -5, m.n)
122
+ t.check('u32 0xFFFFFFFF', 0xFFFFFFFF, m.d)
123
+ // ptr 位有符号读(对齐 C 版 JS_NewInt64):0xAABBCCDD 在 ia32 按 4 字节
124
+ // 存取 → 符号扩展读成负;x64 8 字节高位 0 → 仍为正
125
+ t.check('<>ptr 符号读', is64 ? 0xAABBCCDD : -1430532899, m.w)
126
+ t.check('i16 -7', -7, m.s)
127
+ t.check('<>ptr 0x11223344', 0x11223344, m.q)
128
+ // 别名机制已移除:typedef 名塌出词汇表 —— struct 字段值只收规范 token
129
+ // (仅类型层断言,包成函数体不执行,避免运行时 fail-loud 崩掉 suite)
130
+ // @ts-expect-error typedef 别名 'DWORD' 已移除
131
+ void (() => struct('XD', { d: 'DWORD' }))
132
+
133
+ t.section('array sugar + trailing bitfield(数组糖与尾随位域单元并存)')
134
+ const AS = struct('AS', {
135
+ v: 'u32[4]',
136
+ f: 'u32:3',
137
+ })
138
+ t.check('size == 20 (16 + 位域单元)', 20, AS.size)
139
+ t.check('f offset == 16', 16, AS.offsetOf('f'))
140
+ const asd = AS.decode(AS.encode({ v: [1, 2, 3, 4], f: 5 }))
141
+ t.check('u32[4][1]', 2, asd.v[1])
142
+ t.check('u32:3', 5, asd.f)
143
+ expectType<Equal<ReturnType<typeof AS.decode>['f'], number>>()
144
+
145
+ t.section('union() entry roundtrip')
146
+ const U = union({ a: 'u8', b: 'u32' })
147
+ t.check('union size == 4', 4, U.size)
148
+ t.check('union offsetOf a', 0, U.offsetOf('a'))
149
+ t.check('union offsetOf b', 0, U.offsetOf('b'))
150
+ const ud = U.decode(U.encode({ a: 0, b: 0x01020304 }))
151
+ t.check('union a 与 b 同偏移(低字节别名)', 0x04, ud.a)
152
+ t.check('union b', 0x01020304, ud.b)
153
+
154
+ t.section('union 位域完全别名 roundtrip(mingw 实测语义)')
155
+ {
156
+ const UB = union({ a: 'u32:1', b: 'u32:1' })
157
+ t.check('union bitfield size == 4', 4, UB.size)
158
+ const ud1 = UB.decode(UB.encode({ a: 1, b: 1 }))
159
+ t.check('a=1,b=1 roundtrip a', 1, ud1.a)
160
+ t.check('a=1,b=1 roundtrip b', 1, ud1.b)
161
+ // 同位别名:字段按声明序写入,后写字段覆盖先写字段
162
+ const ud2 = UB.decode(UB.encode({ a: 1, b: 0 }))
163
+ t.check('别名覆盖:b=0 后写 → a 读 0', 0, ud2.a)
164
+ t.check('别名覆盖:b == 0', 0, ud2.b)
165
+
166
+ // 溢出形状(30+4>32):union 每成员回 offset 0,曾开新单元越出 size
167
+ const OF = union({ a: 'u32:30', b: 'u32:4' })
168
+ t.check('overflow union size == 4', 4, OF.size)
169
+ const od1 = OF.decode(OF.encode({ a: 0x3FFFFFFF, b: 0xF }))
170
+ t.check('overflow roundtrip a', 0x3FFFFFFF, od1.a)
171
+ t.check('overflow roundtrip b', 0xF, od1.b)
172
+ const od2 = OF.decode(OF.encode({ a: 0x3FFFFFFF, b: 0 }))
173
+ t.check('overflow 别名覆盖 → a 低 4 位被清', 0x3FFFFFF0, od2.a)
174
+
175
+ // 位域 + 普通成员混合:b 曾被 flushUnit 推到 offset 4(越出 size 4)
176
+ const MX = union({ a: 'u32:1', b: 'u8' })
177
+ t.check('mixed union size == 4', 4, MX.size)
178
+ t.check('mixed offsetOf b == 0(曾为 4)', 0, MX.offsetOf('b'))
179
+ const md = MX.decode(MX.encode({ a: 1, b: 0x54 }))
180
+ t.check('mixed: b 覆盖 a 所在字节 → a 读 0', 0, md.a)
181
+ t.check('mixed: b == 0x54', 0x54, md.b)
182
+
183
+ // 嵌套回归:struct 内嵌该 union(raw '#' 形式;union() 装饰对象不能嵌套,
184
+ // 嵌套 union 的解码类型推导会 TS2589,沿用旧行例 as any——本条是运行时断言),
185
+ // tail 不被 union 内错位写污染
186
+ const nestedU: C_Union = { '#': 'union', a: 'u32:1', b: 'u8' }
187
+ const NS = (struct as any)({ tag: 'u32', u: nestedU, tail: 'u16' })
188
+ t.check('nested size == 12', 12, NS.size)
189
+ const nd = NS.decode(NS.encode({ tag: 0xAABBCCDD, u: { a: 1, b: 0x54 }, tail: 0x1234 }))
190
+ t.check('nested tag', 0xAABBCCDD, nd.tag)
191
+ t.check('nested u.b == 0x54', 0x54, nd.u.b)
192
+ t.check('nested tail 不被污染', 0x1234, nd.tail)
193
+ }
194
+
195
+ t.section('struct() entry: pack 选项 + 键尾 @N(alignas)')
196
+ const P = struct('P', { a: 'u8', b: 'u32', c: 'u8' }, { pack: 1 })
197
+ t.check('pack(1) size == 6', 6, P.size)
198
+ t.check('pack(1) offsetOf b', 1, P.offsetOf('b'))
199
+ const AL = struct('AL', { a: 'u8', 'b@8': 'u32' })
200
+ t.check('@N offsetOf a', 0, AL.offsetOf('a'))
201
+ t.check('@N offsetOf b(字段名已剥离 @N)', 8, AL.offsetOf('b'))
202
+
203
+ t.section('utf-16le string[8] roundtrip & truncation')
204
+ const W = struct('W', {name: 'u16[8]@utf-16le' })
205
+ t.check('utf16[8].size == 16', 16, W.size)
206
+ const wb = W.encode({ name: 'hello' })
207
+ t.check('read back "hello"', 'hello', W.decode(wb).name)
208
+ const lb = W.encode({ name: 'a very long string over' })
209
+ t.check('utf16[8]: payload 7 + NUL', 'a very ', W.decode(lb).name)
210
+ t.check('utf16 terminator[14..15] == 0', 0, new DataView(lb).getUint16(14, true))
211
+
212
+ t.section("utf-8 string[8] roundtrip & truncation")
213
+ const C = struct('C', {name: 'u8[8]@utf-8' })
214
+ t.check('char[8].size == 8', 8, C.size)
215
+ const cb = C.encode({ name: 'hi' })
216
+ t.check('read back "hi"', 'hi', C.decode(cb).name)
217
+ t.check('NUL at [2]', 0, new DataView(cb).getUint8(2))
218
+ const ctrunc = C.encode({ name: '1234567890' })
219
+ t.check('char[8]: payload 7 + NUL', '1234567', C.decode(ctrunc).name)
220
+ t.check('char terminator[7] == 0', 0, new DataView(ctrunc).getUint8(7))
221
+
222
+ t.section('string sugar: 布局与解释正交(错配 roundtrip)')
223
+ const XS = struct('XS', { buf: 'u8[8]@utf-16le' })
224
+ t.check('u8[8]@utf-16le size == 8(encoding 不参与布局)', 8, XS.size)
225
+ const xsd = XS.decode(XS.encode({ buf: 'AB' }))
226
+ t.check('8 字节按 utf-16le 解出 "AB"', 'AB', xsd.buf)
227
+
228
+ t.section('string sugar as array element')
229
+ const SA = struct('SA', { rows: { '#': 'array', element: 'u16[4]@utf-16le', length: 3 } })
230
+ t.check('string[4]×3 size == 24', 24, SA.size)
231
+ const sad = SA.decode(SA.encode({ rows: ['ab', 'cd', 'ef'] }))
232
+ t.check('rows[0]', 'ab', sad.rows[0])
233
+ t.check('rows[1]', 'cd', sad.rows[1])
234
+ expectType<Equal<ReturnType<typeof SA.decode>['rows'], [string, string, string]>>()
235
+
236
+ t.section('键侧 alignas @N 与值侧 encoding @ 并存')
237
+ const KV = struct('KV', { a: 'u8', 's@8': 'u16[4]@utf-8' })
238
+ t.check('offsetOf a', 0, KV.offsetOf('a'))
239
+ t.check('offsetOf s(键尾 @8 生效)', 8, KV.offsetOf('s'))
240
+ t.check('KV size == 16', 16, KV.size)
241
+
242
+ t.section('numeric arrays roundtrip (CArray)')
243
+ const A = struct('A', {
244
+ v: 'u16[4]',
245
+ k: 'i32[2]',
246
+ })
247
+ t.check('size == 16', 16, A.size)
248
+ const ab = A.encode({ v: [1, 2, 3, 4], k: [-1, 300000] })
249
+ const ad = A.decode(ab)
250
+ t.check('u16[0]', 1, ad.v[0])
251
+ t.check('u16[3]', 4, ad.v[3])
252
+ t.check('i32[0] -1', -1, ad.k[0])
253
+ t.check('i32[1]', 300000, ad.k[1])
254
+
255
+ t.section('T[1] stays an array (no scalar degradation)')
256
+ const ONE = struct('ONE', {v: 'u16[1]'})
257
+ t.check('size == 2', 2, ONE.size)
258
+ const one = ONE.decode(ONE.encode({ v: [42] }))
259
+ t.check('length 1', 1, one.v.length)
260
+ t.check('[0]', 42, one.v[0])
261
+
262
+ t.section('array of struct roundtrip (stride = child size)')
263
+ const PT = struct('PT', {
264
+ x: 'i32',
265
+ y: 'i32',
266
+ })
267
+ const POLY = struct('POLY', {
268
+ count: 'u32',
269
+ pts: { '#': 'array', element: PT.__struct, length: 3 },
270
+ })
271
+ t.check('POLY.size == 28', 28, POLY.size)
272
+ t.check('POLY.offsetOf pts', 4, POLY.offsetOf('pts'))
273
+ const poly = POLY.decode(POLY.encode({ count: 3, pts: [{ x: 1, y: 2 }, { x: 3, y: 4 }, { x: 5, y: 6 }] }))
274
+ t.check('count', 3, poly.count)
275
+ t.check('pts[0].x', 1, poly.pts[0].x)
276
+ t.check('pts[2].y', 6, poly.pts[2].y)
277
+
278
+ // 编译期断言:CType IR 推导 — string / 定长数组元组 / 嵌套 struct 递归
279
+ expectType<Equal<ReturnType<typeof W.decode>['name'], string>>()
280
+ expectType<Equal<ReturnType<typeof A.decode>['v'], [number, number, number, number]>>()
281
+ expectType<Equal<ReturnType<typeof A.decode>['k'], [number, number]>>()
282
+ expectType<Equal<ReturnType<typeof ONE.decode>['v'], [number]>>()
283
+ expectType<Equal<ReturnType<typeof POLY.decode>['pts']['length'], 3>>()
284
+ expectType<Equal<ReturnType<typeof POLY.decode>['pts'][0], ReturnType<typeof PT.decode>>>()
285
+
286
+ let rejected = false
287
+ try { struct({v: 'i32[0]'}) } catch { rejected = true }
288
+ t.check('array length 0 rejected', true, rejected)
289
+
290
+ let rejectedBare = false
291
+ try { (struct as any)({ p: 'ptr' }) } catch { rejectedBare = true }
292
+ t.check('bare "ptr" member rejected', true, rejectedBare)
293
+
294
+ // '#' 必填:字段值漏写 '#'(裸嵌套 map)不再默认 struct,直接 throw
295
+ let rejectedMap = false
296
+ try { (struct as any)({ outer: { inner: 'u32' } }) } catch { rejectedMap = true }
297
+ t.check('bare nested map rejected(漏写 #)', true, rejectedMap)
298
+
299
+ // 顶层 '#' 由构造器写入:字段表混入的 '#' 指令键被剔除(运行时兜底,类型层已禁)
300
+ const H = (struct as any)({ '#': 'union', a: 'u8', b: 'u32' })
301
+ t.check('top-level # stripped(仍按函数名 struct 布局)', 8, H.size)
302
+
303
+ t.section("pointer member '<>ptr' raw vs '<T>ptr' branded")
304
+ const PTR = struct('PTR', {
305
+ raw: '<>ptr',
306
+ r: '<RECT>ptr',
307
+ tag: 'u32',
308
+ })
309
+ t.check('PTR.size', is64 ? 24 : 12, PTR.size)
310
+ t.check('offsetOf raw', 0, PTR.offsetOf('raw'))
311
+ t.check('offsetOf r', is64 ? 8 : 4, PTR.offsetOf('r'))
312
+ t.check('offsetOf tag', is64 ? 16 : 8, PTR.offsetOf('tag'))
313
+ const rawOut = RECT.alloc()
314
+ const pd = PTR.decode(PTR.encode({ raw: rawOut.ptr, r: rawOut.ptr, tag: 7 }))
315
+ t.check('raw roundtrip', rawOut.ptr, pd.raw)
316
+ t.check('r roundtrip', rawOut.ptr, pd.r)
317
+ t.check('tag', 7, pd.tag)
318
+ // 编译期:'<>ptr' → MaybePtr<''>(NULL|number,与 number 双向可赋值,NULL 即 0 保真);
319
+ // '<RECT>ptr' → MaybePtr<'RECT'>(NULL|Ptr,0 保真,传参前收窄)。
320
+ expectType<Equal<ReturnType<typeof PTR.decode>['raw'], MaybePtr<''>>>()
321
+ expectType<[ReturnType<typeof PTR.decode>['raw']] extends [number] ? true : false>()
322
+ expectType<Equal<ReturnType<typeof PTR.decode>['r'], MaybePtr<'RECT'>>>()
323
+
324
+ t.section('f32/f64 layout (MSVC: f@0, double@8)')
325
+ const FL = struct('FL', {
326
+ f: 'f32', // float
327
+ d: 'f64', // double
328
+ })
329
+ t.check('size == 16 (f@0, double aligned @8)', 16, FL.size)
330
+ const fb = FL.encode({ f: 1.5, d: -2.25 })
331
+ const fr = FL.decode(fb)
332
+ t.check('float', 1.5, fr.f)
333
+ t.check('double', -2.25, fr.d)
334
+
335
+ t.section('comctl dialog structs track 32/64-bit ABI')
336
+ const TCITEMW = struct('TCITEMW', {
337
+ mask: 'u32',
338
+ dwState: 'u32',
339
+ dwStateMask: 'u32',
340
+ pszText: '<>ptr',
341
+ cchTextMax: 'i32',
342
+ iImage: 'i32',
343
+ lParam: '<>ptr',
344
+ })
345
+ t.check('TCITEMW.size', is64 ? 40 : 28, TCITEMW.size)
346
+ t.check('TCITEMW.offsetOf pszText', is64 ? 16 : 12, TCITEMW.offsetOf('pszText'))
347
+ t.check('TCITEMW.offsetOf cchTextMax', is64 ? 24 : 16, TCITEMW.offsetOf('cchTextMax'))
348
+
349
+ const TTTOOLINFOW = struct('TTTOOLINFOW', {
350
+ cbSize: 'u32',
351
+ uFlags: 'u32',
352
+ hwnd: '<>ptr',
353
+ uId: '<>ptr',
354
+ rect: 'i32[4]',
355
+ hinst: '<>ptr',
356
+ lpszText: '<>ptr',
357
+ lParam: '<>ptr',
358
+ lpReserved: '<>ptr', // WinXP+ 追加
359
+ })
360
+ t.check('TTTOOLINFOW.size', is64 ? 72 : 48, TTTOOLINFOW.size)
361
+ t.check('TTTOOLINFOW.offsetOf lpszText', is64 ? 48 : 36, TTTOOLINFOW.offsetOf('lpszText'))
362
+
363
+ const OPENFILENAMEW = struct('OPENFILENAMEW', {
364
+ lStructSize: 'u32',
365
+ hwndOwner: '<>ptr',
366
+ hInstance: '<>ptr',
367
+ lpstrFilter: '<>ptr',
368
+ lpstrCustomFilter: '<>ptr',
369
+ nMaxCustFilter: 'u32',
370
+ nFilterIndex: 'u32',
371
+ lpstrFile: '<>ptr',
372
+ nMaxFile: 'u32',
373
+ lpstrFileTitle: '<>ptr',
374
+ nMaxFileTitle: 'u32',
375
+ lpstrInitialDir: '<>ptr',
376
+ lpstrTitle: '<>ptr',
377
+ Flags: 'u32',
378
+ nFileOffset: 'u16',
379
+ nFileExtension: 'u16',
380
+ lpstrDefExt: '<>ptr',
381
+ lCustData: '<>ptr',
382
+ lpfnHook: '<>ptr',
383
+ lpTemplateName: '<>ptr',
384
+ pvReserved: '<>ptr',
385
+ dwReserved: 'u32',
386
+ FlagsEx: 'u32', // Win2000+ 追加
387
+ })
388
+ t.check('OPENFILENAMEW.size', is64 ? 152 : 88, OPENFILENAMEW.size)
389
+ t.check('OPENFILENAMEW.offsetOf lpstrFile', is64 ? 48 : 28, OPENFILENAMEW.offsetOf('lpstrFile'))
390
+ t.check('OPENFILENAMEW.offsetOf Flags', is64 ? 96 : 52, OPENFILENAMEW.offsetOf('Flags'))
391
+ t.check('OPENFILENAMEW.offsetOf lpstrTitle', is64 ? 88 : 48, OPENFILENAMEW.offsetOf('lpstrTitle'))
392
+
393
+ const BROWSEINFOW = struct('BROWSEINFOW', {
394
+ hwndOwner: '<>ptr',
395
+ pidlRoot: '<>ptr',
396
+ pszDisplayName: '<>ptr',
397
+ lpszTitle: '<>ptr',
398
+ ulFlags: 'u32',
399
+ lpfn: '<>ptr',
400
+ lParam: '<>ptr',
401
+ iImage: 'i32',
402
+ })
403
+ t.check('BROWSEINFOW.size', is64 ? 64 : 32, BROWSEINFOW.size)
404
+ t.check('BROWSEINFOW.offsetOf lpszTitle', is64 ? 24 : 12, BROWSEINFOW.offsetOf('lpszTitle'))
405
+ t.check('BROWSEINFOW.offsetOf ulFlags', is64 ? 32 : 16, BROWSEINFOW.offsetOf('ulFlags'))
406
+
407
+ t.section('NMHDR(嵌套时 MSVC 尾 padding 传染)')
408
+ const NMHDR = struct('NMHDR', {
409
+ hwndFrom: '<>ptr',
410
+ idFrom: '<>ptr',
411
+ code: 'i32',
412
+ })
413
+ t.check('NMHDR.size', is64 ? 24 : 12, NMHDR.size)
414
+ t.check('NMHDR.offsetOf code', is64 ? 16 : 8, NMHDR.offsetOf('code'))
415
+
416
+ t.section('NMCUSTOMDRAW / NMLVCUSTOMDRAW (ListView custom draw)')
417
+ const NMCUSTOMDRAW = struct('NMCUSTOMDRAW', {
418
+ hdr: NMHDR.__struct,
419
+ dwDrawStage: 'u32',
420
+ hdc: '<>ptr',
421
+ rc: 'i32[4]',
422
+ dwItemSpec: '<>ptr',
423
+ uItemState: 'u32',
424
+ lItemlParam: '<>ptr',
425
+ })
426
+ t.check('NMCUSTOMDRAW.size', is64 ? 80 : 48, NMCUSTOMDRAW.size)
427
+ t.check('NMCUSTOMDRAW.offsetOf dwDrawStage', is64 ? 24 : 12, NMCUSTOMDRAW.offsetOf('dwDrawStage'))
428
+ t.check('NMCUSTOMDRAW.offsetOf hdc', is64 ? 32 : 16, NMCUSTOMDRAW.offsetOf('hdc'))
429
+ t.check('NMCUSTOMDRAW.offsetOf dwItemSpec', is64 ? 56 : 36, NMCUSTOMDRAW.offsetOf('dwItemSpec'))
430
+ const NMLVCUSTOMDRAW = struct('NMLVCUSTOMDRAW', {
431
+ hdr: NMHDR.__struct,
432
+ dwDrawStage: 'u32',
433
+ hdc: '<>ptr',
434
+ rc: 'i32[4]',
435
+ dwItemSpec: '<>ptr',
436
+ uItemState: 'u32',
437
+ lItemlParam: '<>ptr',
438
+ clrText: 'u32',
439
+ clrTextBk: 'u32',
440
+ iSubItem: 'i32',
441
+ dwItemType: 'u32',
442
+ })
443
+ t.check('NMLVCUSTOMDRAW.size', is64 ? 96 : 64, NMLVCUSTOMDRAW.size)
444
+ t.check('NMLVCUSTOMDRAW.offsetOf clrText', is64 ? 80 : 48, NMLVCUSTOMDRAW.offsetOf('clrText'))
445
+ t.check('NMLVCUSTOMDRAW.offsetOf clrTextBk', is64 ? 84 : 52, NMLVCUSTOMDRAW.offsetOf('clrTextBk'))
446
+ t.check('NMLVCUSTOMDRAW.offsetOf iSubItem', is64 ? 88 : 56, NMLVCUSTOMDRAW.offsetOf('iSubItem'))
447
+
448
+ t.section('NMLISTVIEW / NMITEMACTIVATE')
449
+ const NMLISTVIEW = struct('NMLISTVIEW', {
450
+ hdr: NMHDR.__struct,
451
+ iItem: 'i32',
452
+ iSubItem: 'i32',
453
+ uNewState: 'u32',
454
+ uOldState: 'u32',
455
+ uChanged: 'u32',
456
+ ptAction: 'i32[2]',
457
+ lParam: '<>ptr',
458
+ })
459
+ t.check('NMLISTVIEW.size', is64 ? 64 : 44, NMLISTVIEW.size)
460
+ t.check('NMLISTVIEW.offsetOf iItem', is64 ? 24 : 12, NMLISTVIEW.offsetOf('iItem'))
461
+ t.check('NMLISTVIEW.offsetOf iSubItem', is64 ? 28 : 16, NMLISTVIEW.offsetOf('iSubItem'))
462
+ t.check('NMLISTVIEW.offsetOf uNewState', is64 ? 32 : 20, NMLISTVIEW.offsetOf('uNewState'))
463
+ t.check('NMLISTVIEW.offsetOf uOldState', is64 ? 36 : 24, NMLISTVIEW.offsetOf('uOldState'))
464
+
465
+ t.section('LVITEMW / LVCOLUMNW')
466
+ const LVITEMW = struct('LVITEMW', {
467
+ mask: 'u32',
468
+ iItem: 'i32',
469
+ iSubItem: 'i32',
470
+ state: 'u32',
471
+ stateMask: 'u32',
472
+ pszText: '<>ptr',
473
+ cchTextMax: 'i32',
474
+ iImage: 'i32',
475
+ lParam: '<>ptr',
476
+ iIndent: 'i32',
477
+ iGroupId: 'i32',
478
+ cColumns: 'u32',
479
+ puColumns: '<>ptr',
480
+ piColFmt: '<>ptr',
481
+ iGroup: 'i32',
482
+ })
483
+ t.check('LVITEMW.size', is64 ? 88 : 60, LVITEMW.size)
484
+ t.check('LVITEMW.offsetOf iItem', 4, LVITEMW.offsetOf('iItem'))
485
+ t.check('LVITEMW.offsetOf pszText', is64 ? 24 : 20, LVITEMW.offsetOf('pszText'))
486
+ t.check('LVITEMW.offsetOf iImage', is64 ? 36 : 28, LVITEMW.offsetOf('iImage'))
487
+ t.check('LVITEMW.offsetOf lParam', is64 ? 40 : 32, LVITEMW.offsetOf('lParam'))
488
+ const LVCOLUMNW = struct('LVCOLUMNW', {
489
+ mask: 'u32',
490
+ fmt: 'i32',
491
+ cx: 'i32',
492
+ pszText: '<>ptr',
493
+ cchTextMax: 'i32',
494
+ iSubItem: 'i32',
495
+ iImage: 'i32',
496
+ iOrder: 'i32',
497
+ cxMin: 'i32',
498
+ cxDefault: 'i32',
499
+ cxIdeal: 'i32',
500
+ })
501
+ t.check('LVCOLUMNW.size', is64 ? 56 : 44, LVCOLUMNW.size)
502
+ t.check('LVCOLUMNW.offsetOf pszText', is64 ? 16 : 12, LVCOLUMNW.offsetOf('pszText'))
503
+ t.check('LVCOLUMNW.offsetOf iSubItem', is64 ? 28 : 20, LVCOLUMNW.offsetOf('iSubItem'))
504
+ t.check('LVCOLUMNW.offsetOf iOrder', is64 ? 36 : 28, LVCOLUMNW.offsetOf('iOrder'))
505
+
506
+ t.section('NMDATETIMECHANGE')
507
+ const SYSTEMTIME = struct('SYSTEMTIME', {
508
+ wYear: 'u16',
509
+ wMonth: 'u16',
510
+ wDayOfWeek: 'u16',
511
+ wDay: 'u16',
512
+ wHour: 'u16',
513
+ wMinute: 'u16',
514
+ wSecond: 'u16',
515
+ wMilliseconds: 'u16',
516
+ })
517
+ const NMDATETIMECHANGE = struct('NMDATETIMECHANGE', {
518
+ hdr: NMHDR.__struct,
519
+ dwFlags: 'u32',
520
+ st: SYSTEMTIME.__struct,
521
+ })
522
+ t.check('NMDATETIMECHANGE.size', is64 ? 48 : 32, NMDATETIMECHANGE.size)
523
+ t.check('NMDATETIMECHANGE.offsetOf dwFlags', is64 ? 24 : 12, NMDATETIMECHANGE.offsetOf('dwFlags'))
524
+ t.check('NMDATETIMECHANGE.offsetOf st', is64 ? 28 : 16, NMDATETIMECHANGE.offsetOf('st'))
525
+
526
+ t.section('NMLINK szUrl offset')
527
+ const LITEM = struct('LITEM', {
528
+ mask: 'u32',
529
+ iLink: 'i32',
530
+ state: 'u32',
531
+ stateMask: 'u32',
532
+ szID: 'u16[48]@utf-16le',
533
+ })
534
+ const NMLINK = struct('NMLINK', {
535
+ hdr: NMHDR.__struct,
536
+ item: LITEM.__struct,
537
+ szUrl: 'u16[2084]@utf-16le',
538
+ })
539
+ t.check('LITEM.offsetOf szID', 16, LITEM.offsetOf('szID'))
540
+ t.check('NMLINK.offsetOf szUrl', is64 ? 136 : 124, NMLINK.offsetOf('szUrl'))
541
+
542
+ // === 位域读/写往返 ===
543
+ // 布局 ground truth 在 test_ffi_struct_layout.ts(mingw 交叉实测),这里只测读写。
544
+
545
+ t.section('bitfield roundtrip: LSB→MSB 打包 + 读改写隔离')
546
+ {
547
+ // 字段按序写入同一 buffer:写 d 时单元里已有 a/b/c 的位,
548
+ // 若 writeBitfield 不做读改写(直接 val<<bit)会把 a/b/c 清零。
549
+ const BF = struct('BF', {
550
+ a: 'u32:3',
551
+ b: 'u32:5',
552
+ c: 'u32:4',
553
+ d: 'u32:10', // 3+5+4+10=22 <= 32
554
+ tail: 'u32',
555
+ })
556
+ t.check('BF.size == 8', 8, BF.size)
557
+ const bd = BF.decode(BF.encode({ a: 5, b: 31, c: 15, d: 1023, tail: 0xDEAD }))
558
+ t.check('a (LSB)', 5, bd.a)
559
+ t.check('b', 31, bd.b)
560
+ t.check('c', 15, bd.c)
561
+ t.check('d (max 10-bit)', 1023, bd.d)
562
+ t.check('tail survives bitfield unit', 0xDEAD, bd.tail)
563
+ // 位域写不破坏同单元的邻居:a=5 后写 b=31,a 仍为 5
564
+ t.check('a survives b write', 5, bd.a)
565
+ }
566
+
567
+ t.section('bitfield roundtrip: signed sign extension')
568
+ {
569
+ const SB = struct('SB', {
570
+ s: 'i32:8',
571
+ u: 'u32:4',
572
+ })
573
+ t.check('SB.size == 4 (同宽不分签别,共单元)', 4, SB.size)
574
+ const sbd = SB.decode(SB.encode({ s: -5, u: 9 }))
575
+ t.check('signed -5', -5, sbd.s)
576
+ t.check('unsigned 9', 9, sbd.u)
577
+ t.check('signed -1', -1, SB.decode(SB.encode({ s: -1, u: 0 })).s)
578
+ t.check('signed 127 (min int8)', 127, SB.decode(SB.encode({ s: 127, u: 0 })).s)
579
+ t.check('signed 128 → -128', -128, SB.decode(SB.encode({ s: 128, u: 0 })).s)
580
+ }
581
+
582
+ t.section('bitfield roundtrip: overflow starts a new unit')
583
+ {
584
+ const OV = struct('OV', {
585
+ a: 'u32:30',
586
+ b: 'u32:4', // 30+4=34 > 32 → 新单元,剩余 2 位废弃
587
+ c: 'u32',
588
+ })
589
+ t.check('OV.size == 12', 12, OV.size)
590
+ t.check('OV.offsetOf b (new unit)', 4, OV.offsetOf('b'))
591
+ t.check('OV.offsetOf c', 8, OV.offsetOf('c'))
592
+ const ovd = OV.decode(OV.encode({ a: 0x3FFFFFFF, b: 15, c: 7 }))
593
+ t.check('OV a (max 30-bit)', 0x3FFFFFFF, ovd.a)
594
+ t.check('OV b', 15, ovd.b)
595
+ t.check('OV c', 7, ovd.c)
596
+ }
597
+
598
+ t.section('bitfield: DCB roundtrip (Windows serial port config)')
599
+ {
600
+ const DCB = struct('DCB', {
601
+ DCBlength: 'u32',
602
+ BaudRate: 'u32',
603
+ fBinary: 'u32:1',
604
+ fParity: 'u32:1',
605
+ fOutxCtsFlow: 'u32:1',
606
+ fOutxDsrFlow: 'u32:1',
607
+ fDtrControl: 'u32:2',
608
+ fDsrSensitivity: 'u32:1',
609
+ fTXContinueOnXoff: 'u32:1',
610
+ fOutX: 'u32:1',
611
+ fInX: 'u32:1',
612
+ fErrorChar: 'u32:1',
613
+ fNull: 'u32:1',
614
+ fRtsControl: 'u32:2',
615
+ fAbortOnError: 'u32:1',
616
+ fDummy2: 'u32:17',
617
+ wReserved: 'u16',
618
+ XonLim: 'u16',
619
+ XoffLim: 'u16',
620
+ ByteSize: 'u8',
621
+ Parity: 'u8',
622
+ StopBits: 'u8',
623
+ XonChar: 'i8',
624
+ XoffChar: 'i8',
625
+ ErrorChar: 'i8',
626
+ EofChar: 'i8',
627
+ EvtChar: 'i8',
628
+ wReserved1: 'u16',
629
+ })
630
+ t.check('DCB.size == 28', 28, DCB.size)
631
+ const d = DCB.decode(DCB.encode({
632
+ DCBlength: 28, BaudRate: 0x1C200,
633
+ fBinary: 1, fParity: 0, fOutxCtsFlow: 0, fOutxDsrFlow: 0,
634
+ fDtrControl: 2, fDsrSensitivity: 1, fTXContinueOnXoff: 0,
635
+ fOutX: 1, fInX: 0, fErrorChar: 1, fNull: 0,
636
+ fRtsControl: 2, fAbortOnError: 1, fDummy2: 0,
637
+ wReserved: 0, XonLim: 200, XoffLim: 100,
638
+ ByteSize: 8, Parity: 0, StopBits: 0,
639
+ XonChar: 3, XoffChar: 4, ErrorChar: 0xFF,
640
+ EofChar: 3, EvtChar: 0x4, wReserved1: 0,
641
+ }))
642
+ t.check('DCBlength', 28, d.DCBlength)
643
+ t.check('BaudRate', 0x1C200, d.BaudRate)
644
+ t.check('fBinary', 1, d.fBinary)
645
+ t.check('fParity', 0, d.fParity)
646
+ t.check('fDtrControl', 2, d.fDtrControl)
647
+ t.check('fDsrSensitivity', 1, d.fDsrSensitivity)
648
+ t.check('fOutX', 1, d.fOutX)
649
+ t.check('fRtsControl', 2, d.fRtsControl)
650
+ t.check('fAbortOnError', 1, d.fAbortOnError)
651
+ t.check('fDummy2', 0, d.fDummy2)
652
+ t.check('wReserved', 0, d.wReserved)
653
+ t.check('XonLim', 200, d.XonLim)
654
+ t.check('XoffLim', 100, d.XoffLim)
655
+ t.check('ByteSize', 8, d.ByteSize)
656
+ t.check('Parity', 0, d.Parity)
657
+ t.check('StopBits', 0, d.StopBits)
658
+ t.check('XonChar', 3, d.XonChar)
659
+ t.check('XoffChar', 4, d.XoffChar)
660
+ t.check('ErrorChar (i8)', -1, d.ErrorChar)
661
+ t.check('EofChar', 3, d.EofChar)
662
+ t.check('EvtChar', 4, d.EvtChar)
663
+ t.check('wReserved1', 0, d.wReserved1)
664
+ }
665
+
666
+ // 类型层:位域成员值类型是 number
667
+ {
668
+ const BF = struct('BF', {
669
+ a: 'u32:3',
670
+ s: 'i32:8',
671
+ })
672
+ expectType<Equal<ReturnType<typeof BF.decode>['a'], number>>()
673
+ expectType<Equal<ReturnType<typeof BF.decode>['s'], number>>()
674
+ }
675
+
676
+ // 位域 width>53:值域装不进安全整数 → bigint(与类型层 BitShape 同界)
677
+ t.section('bitfield width>53: bigint roundtrip (u64/i64)')
678
+ {
679
+ // u64:60 + u64:4 同单元混合形态:>53 的字段 bigint、≤53 的字段 number
680
+ const W64 = struct('W64', {
681
+ u: 'u64:60',
682
+ v: 'u64:4',
683
+ })
684
+ t.check('u64:60+u64:4 size == 8(同单元)', 8, W64.size)
685
+ t.check('v 与 u 同单元 offset == 0', 0, W64.offsetOf('v'))
686
+ const big = 0x0FFFFFFFFFFFFFFFn // 60 位内大值 > 2^53
687
+ const d1 = W64.decode(W64.encode({ u: big, v: 15 }))
688
+ t.check('u64:60 大值精确往返', big, d1.u)
689
+ t.check('u64:4 同单元往返', 15, d1.v)
690
+ expectType<Equal<ReturnType<typeof W64.decode>['u'], bigint>>()
691
+ expectType<Equal<ReturnType<typeof W64.decode>['v'], number>>()
692
+
693
+ // i64:60 负值符号扩展 → 精确 bigint(回归:曾被有符号分支的 Number() 截断)
694
+ const I64 = struct('I64', { s: 'i64:60' })
695
+ const neg = -12345678901234567n
696
+ t.check('i64:60 负值精确往返', neg, I64.decode(I64.encode({ s: neg })).s)
697
+ // 符号位为 0 的正值:width>53 → 依然 bigint
698
+ t.check('i64:60 正值 bigint', 100n, I64.decode(I64.encode({ s: 100n })).s)
699
+ expectType<Equal<ReturnType<typeof I64.decode>['s'], bigint>>()
700
+ if (false) {
701
+ // @ts-expect-error width>53 位域收 bigint,number 被类型层拒绝
702
+ I64.encode({ s: 42 })
703
+ }
704
+
705
+ // 分界:53 → number(2^53-1 精确),54 → bigint(2^54-1 精确)
706
+ const B53 = struct('B53', { a: 'u64:53' })
707
+ expectType<Equal<ReturnType<typeof B53.decode>['a'], number>>()
708
+ t.check('u64:53 边界 number 精确', 9007199254740991,
709
+ B53.decode(B53.encode({ a: 9007199254740991 })).a)
710
+ const B54 = struct('B54', { a: 'u64:54' })
711
+ expectType<Equal<ReturnType<typeof B54.decode>['a'], bigint>>()
712
+ t.check('u64:54 边界 bigint 精确', 18014398509481983n,
713
+ B54.decode(B54.encode({ a: 18014398509481983n })).a)
714
+ }
715
+
716
+ // ============ token @Enum:struct 字段值域 ============
717
+ t.section('struct 字段 @Enum: 词汇定义点拒绝 + 值域收窄 + 必填判定')
718
+ {
719
+ // 定义点拒绝(∉ SimpleValue —— 未知枚举名与拼错 token 同一道闸)。if(false)
720
+ // 纯类型层断言:位域反例运行时 normToken 也会 throw,不执行。
721
+ if (false) {
722
+ // @ts-expect-error 未知枚举名(TypoEnum ∉ keyof EnumMap)
723
+ struct('Bad1', { m: 'u32@TypoEnum' })
724
+ // @ts-expect-error 位域单元不收 @(值域标注对位域无意义,词汇拒)
725
+ struct('Bad2', { m: 'u32@WmMsg:3' })
726
+ // @ts-expect-error ptr 位未知枚举名
727
+ struct('Bad3', { p: '<>ptr@TypoEnum' })
728
+ // @ts-expect-error 数组糖元素同样受词汇守卫
729
+ struct('Bad4', { a: 'u32@TypoEnum[4]' })
730
+ }
731
+
732
+ // 合法定义:数字位 + ptr 位 @ 标注
733
+ const EV = struct('EV', {
734
+ msg: 'u32@WmMsg', // WM_NULL = 0 ∈ 值域 → encode 可省
735
+ key: 'u32@MouseKeyFlag', // 仅 MK_SHIFT = 4,0 ∉ 值域 → encode 必填
736
+ hp: '<>ptr@SetWindowPosHwnd', // ptr 位(HWND_TOP = 0 ∈ → 可省)
737
+ })
738
+
739
+ // decode:值域收窄(与 bind 签名位同构;异种枚举/域外字面量由枚举 nominal 拦)
740
+ const dev = EV.decode(EV.encode({ key: 4 }))
741
+ expectType<Equal<typeof dev.msg, EnumMap['WmMsg']>>()
742
+ expectType<Equal<typeof dev.key, EnumMap['MouseKeyFlag']>>()
743
+ expectType<Equal<typeof dev.hp, EnumMap['SetWindowPosHwnd']>>()
744
+ t.check('数字位 @ 字段归一往返', 4, dev.key)
745
+
746
+ // encode 必填判定:0 ∉ 值域的 @ 字段必填(省略 = 新槽写 0 = 静默违约);
747
+ // 0 ∈ 值域可省。下面 { key } 行能编译即 msg/hp 可省的断言。
748
+ EV.encode({ key: gui.MouseKeyFlag.MK_SHIFT })
749
+ // @ts-expect-error key 必填:省略即编译错(0 ∉ MouseKeyFlag)
750
+ EV.encode({ msg: 0 })
751
+ if (false) {
752
+ // @ts-expect-error 域外字面量(3 不是任何 MK_ 标志)
753
+ EV.encode({ key: 3 })
754
+ // @ts-expect-error 跨枚举误用(WmMsg 成员不是 MouseKeyFlag)
755
+ EV.encode({ key: gui.WmMsg.CREATE })
756
+ }
757
+
758
+ // 数组糖元素带 @:lower 递归归一(normToken 剥 @)+ 元素值域
759
+ const EA = struct('EA', { msgs: 'u32@WmMsg[4]' })
760
+ const dea = EA.decode(EA.encode({ msgs: [0, 1, 2, 0] }))
761
+ expectType<Equal<(typeof dea.msgs)[number], EnumMap['WmMsg']>>()
762
+ t.check('数组 @ 元素运行时归一往返', 2, dea.msgs[2])
763
+ }
764
+
765
+ // ============ EncodeIn 后序穿透:嵌套/数组子树含必填 ⇒ 父键必给 ============
766
+ t.section('EncodeIn DFS 后序:子树含必填 ⇒ 键必填;值后序合成(非必填兄弟可省)')
767
+ {
768
+ // 嵌套子结构含 '!'(内层强制)+ 标量/字符串糖(内层可省:''/0 是合法零值)
769
+ const NSUB = struct('NSUB', { hp: '<X>ptr!', rest: 'u32', s: 'u16[4]@utf-8' })
770
+ const NOUT = struct('NOUT', { top: 'u32', sub: NSUB.__struct })
771
+ // decode 造全量对象(hp 槽预填非零,绕 '!' 的 decode 0 断言)
772
+ const nbuf = new PtrArrayBuffer(NOUT.size)
773
+ new DataView(nbuf).setUint32(NOUT.offsetOf('sub') + NSUB.offsetOf('hp'), 0x1234, true)
774
+ const dn = NOUT.decode(nbuf)
775
+ NOUT.encode(dn) // 全量回喂 ✓
776
+ NOUT.encode({ sub: dn.sub }) // 顶层 top 可省 ✓
777
+ // B 形态关键钉:内层非必填兄弟 rest/s 可省(A「全量严格形」形态此行编译错)
778
+ NOUT.encode({ sub: { hp: dn.sub.hp } })
779
+ // @ts-expect-error 嵌套穿透:sub 子树含 '!' ⇒ sub 必填 —— 省略即编译错
780
+ NOUT.encode({ top: 1 })
781
+ // @ts-expect-error 内层 '!' 必填:sub.hp 省略即编译错,不静默写 0
782
+ NOUT.encode({ sub: { rest: 1 } })
783
+
784
+ // 数组穿透:元素 0 ∉ 值域 ⇒ 整组必填(省 = 元素全 0 静默违约)
785
+ const ARMAND = struct('ARMAND', { keys: 'u32@MouseKeyFlag[2]' })
786
+ ARMAND.encode({ keys: [4, 4] })
787
+ // @ts-expect-error 数组含必填元素:整组省略即编译错
788
+ ARMAND.encode({})
789
+
790
+ // weak-type 防线在「无必填」分支保持:裸 number 与全可选形无公共键(TS2559)。
791
+ // if(false) 纯类型层断言(运行时 encode(123) 会炸,不执行)。
792
+ if (false) {
793
+ // @ts-expect-error 无必填 struct 的入参退化纯 DeepPartial,裸 number 被拦
794
+ RECT.encode(123)
795
+ }
796
+ }
797
+ },
798
+ }