@defold-typescript/types 0.40.0 → 0.42.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 (224) hide show
  1. package/api-availability.json +3963 -772
  2. package/api-migrations.json +27 -0
  3. package/api-signatures.json +18069 -10240
  4. package/api-targets.json +448 -11
  5. package/examples/translations.json +560 -89
  6. package/fixtures/messages_doc.json +96 -0
  7. package/generated/b2d.d.ts +680 -4
  8. package/generated/b2d_body.d.ts +29 -103
  9. package/generated/b2d_chain.d.ts +5 -6
  10. package/generated/b2d_fixture.d.ts +34 -38
  11. package/generated/b2d_joint.d.ts +503 -25
  12. package/generated/b2d_shape.d.ts +183 -20
  13. package/generated/b2d_world.d.ts +57 -63
  14. package/generated/buffer.d.ts +16 -15
  15. package/generated/builtin-messages.d.ts +7 -1
  16. package/generated/bullet3d.d.ts +91 -0
  17. package/generated/bullet3d_collision_object.d.ts +433 -0
  18. package/generated/bullet3d_constraint.d.ts +818 -0
  19. package/generated/bullet3d_rigid_body.d.ts +429 -0
  20. package/generated/bullet3d_shape.d.ts +123 -0
  21. package/generated/bullet3d_world.d.ts +492 -0
  22. package/generated/camera.d.ts +29 -32
  23. package/generated/collectionfactory.d.ts +15 -18
  24. package/generated/collectionproxy.d.ts +22 -6
  25. package/generated/compute.d.ts +12 -238
  26. package/generated/crash.d.ts +24 -10
  27. package/generated/editor-vm/http.d.ts +90 -7
  28. package/generated/editor-vm/image.d.ts +7 -7
  29. package/generated/editor-vm/json.d.ts +11 -0
  30. package/generated/editor-vm/localization.d.ts +7 -7
  31. package/generated/editor-vm/tilemap_tiles.d.ts +56 -10
  32. package/generated/editor-vm/zip.d.ts +28 -0
  33. package/generated/editor.d.ts +1496 -430
  34. package/generated/factory.d.ts +21 -25
  35. package/generated/font.d.ts +64 -22
  36. package/generated/go.d.ts +235 -182
  37. package/generated/graphics.d.ts +335 -97
  38. package/generated/gui.d.ts +240 -471
  39. package/generated/html5.d.ts +2 -2
  40. package/generated/http.d.ts +28 -35
  41. package/generated/image.d.ts +105 -72
  42. package/generated/json.d.ts +34 -16
  43. package/generated/kinds/gui-script.d.ts +7 -0
  44. package/generated/kinds/render-script.d.ts +7 -0
  45. package/generated/kinds/script.d.ts +7 -0
  46. package/generated/label.d.ts +77 -7
  47. package/generated/liveupdate.d.ts +13 -10
  48. package/generated/material.d.ts +312 -360
  49. package/generated/model.d.ts +40 -39
  50. package/generated/particlefx.d.ts +19 -28
  51. package/generated/physics.d.ts +365 -133
  52. package/generated/profiler.d.ts +13 -23
  53. package/generated/render.d.ts +237 -297
  54. package/generated/resource.d.ts +664 -583
  55. package/generated/socket.d.ts +120 -133
  56. package/generated/sound.d.ts +62 -31
  57. package/generated/sprite.d.ts +31 -17
  58. package/generated/sys.d.ts +234 -190
  59. package/generated/tilemap.d.ts +21 -16
  60. package/generated/timer.d.ts +34 -21
  61. package/generated/versions/defold-1.12.4/b2d_body.d.ts +0 -7
  62. package/generated/versions/defold-1.12.4/buffer.d.ts +1 -3
  63. package/generated/versions/defold-1.12.4/camera.d.ts +21 -21
  64. package/generated/versions/defold-1.12.4/collectionfactory.d.ts +9 -9
  65. package/generated/versions/defold-1.12.4/collectionproxy.d.ts +2 -2
  66. package/generated/versions/defold-1.12.4/crash.d.ts +2 -2
  67. package/generated/versions/defold-1.12.4/factory.d.ts +15 -16
  68. package/generated/versions/defold-1.12.4/go.d.ts +6 -12
  69. package/generated/versions/defold-1.12.4/gui.d.ts +77 -175
  70. package/generated/versions/defold-1.12.4/html5.d.ts +1 -1
  71. package/generated/versions/defold-1.12.4/http.d.ts +0 -13
  72. package/generated/versions/defold-1.12.4/image.d.ts +16 -33
  73. package/generated/versions/defold-1.12.4/index.d.ts +1 -0
  74. package/generated/versions/defold-1.12.4/json.d.ts +10 -6
  75. package/generated/versions/defold-1.12.4/label.d.ts +2 -3
  76. package/generated/versions/defold-1.12.4/liveupdate.d.ts +5 -3
  77. package/generated/versions/defold-1.12.4/model.d.ts +2 -10
  78. package/generated/versions/defold-1.12.4/particlefx.d.ts +2 -5
  79. package/generated/versions/defold-1.12.4/physics.d.ts +69 -67
  80. package/generated/versions/defold-1.12.4/profiler.d.ts +2 -5
  81. package/generated/versions/defold-1.12.4/render.d.ts +99 -128
  82. package/generated/versions/defold-1.12.4/resource.d.ts +120 -148
  83. package/generated/versions/defold-1.12.4/socket.d.ts +34 -60
  84. package/generated/versions/defold-1.12.4/sound.d.ts +8 -8
  85. package/generated/versions/defold-1.12.4/sprite.d.ts +15 -5
  86. package/generated/versions/defold-1.12.4/sys.d.ts +37 -71
  87. package/generated/versions/defold-1.12.4/tilemap.d.ts +18 -11
  88. package/generated/versions/defold-1.12.4/timer.d.ts +5 -3
  89. package/generated/versions/defold-1.12.4/vmath.d.ts +0 -21
  90. package/generated/versions/defold-1.12.4/window.d.ts +1 -16
  91. package/generated/versions/defold-1.13.0/b2d_body.d.ts +10 -55
  92. package/generated/versions/defold-1.13.0/b2d_fixture.d.ts +25 -25
  93. package/generated/versions/defold-1.13.0/b2d_shape.d.ts +169 -8
  94. package/generated/versions/defold-1.13.0/b2d_world.d.ts +16 -10
  95. package/generated/versions/defold-1.13.0/buffer.d.ts +1 -3
  96. package/generated/versions/defold-1.13.0/camera.d.ts +25 -25
  97. package/generated/versions/defold-1.13.0/collectionfactory.d.ts +9 -9
  98. package/generated/versions/defold-1.13.0/collectionproxy.d.ts +2 -2
  99. package/generated/versions/defold-1.13.0/compute.d.ts +0 -69
  100. package/generated/versions/defold-1.13.0/crash.d.ts +2 -2
  101. package/generated/versions/defold-1.13.0/editor-vm/tilemap_tiles.d.ts +3 -3
  102. package/generated/versions/defold-1.13.0/editor.d.ts +34 -24
  103. package/generated/versions/defold-1.13.0/factory.d.ts +15 -16
  104. package/generated/versions/defold-1.13.0/go.d.ts +6 -12
  105. package/generated/versions/defold-1.13.0/graphics.d.ts +73 -37
  106. package/generated/versions/defold-1.13.0/gui.d.ts +77 -175
  107. package/generated/versions/defold-1.13.0/html5.d.ts +1 -1
  108. package/generated/versions/defold-1.13.0/http.d.ts +0 -13
  109. package/generated/versions/defold-1.13.0/image.d.ts +16 -33
  110. package/generated/versions/defold-1.13.0/index.d.ts +1 -0
  111. package/generated/versions/defold-1.13.0/json.d.ts +10 -6
  112. package/generated/versions/defold-1.13.0/label.d.ts +2 -3
  113. package/generated/versions/defold-1.13.0/liveupdate.d.ts +7 -5
  114. package/generated/versions/defold-1.13.0/material.d.ts +68 -106
  115. package/generated/versions/defold-1.13.0/model.d.ts +2 -10
  116. package/generated/versions/defold-1.13.0/particlefx.d.ts +2 -5
  117. package/generated/versions/defold-1.13.0/physics.d.ts +69 -67
  118. package/generated/versions/defold-1.13.0/profiler.d.ts +2 -5
  119. package/generated/versions/defold-1.13.0/render.d.ts +99 -128
  120. package/generated/versions/defold-1.13.0/resource.d.ts +126 -150
  121. package/generated/versions/defold-1.13.0/socket.d.ts +34 -60
  122. package/generated/versions/defold-1.13.0/sound.d.ts +8 -8
  123. package/generated/versions/defold-1.13.0/sprite.d.ts +15 -5
  124. package/generated/versions/defold-1.13.0/sys.d.ts +37 -71
  125. package/generated/versions/defold-1.13.0/tilemap.d.ts +18 -11
  126. package/generated/versions/defold-1.13.0/timer.d.ts +5 -3
  127. package/generated/versions/defold-1.13.0/vmath.d.ts +0 -21
  128. package/generated/versions/defold-1.13.0/window.d.ts +1 -16
  129. package/generated/versions/defold-1.13.1/b2d.d.ts +33 -0
  130. package/generated/versions/defold-1.13.1/b2d_body.d.ts +636 -0
  131. package/generated/versions/defold-1.13.1/b2d_chain.d.ts +106 -0
  132. package/generated/versions/defold-1.13.1/b2d_fixture.d.ts +157 -0
  133. package/generated/versions/defold-1.13.1/b2d_joint.d.ts +823 -0
  134. package/generated/versions/defold-1.13.1/b2d_shape.d.ts +400 -0
  135. package/generated/versions/defold-1.13.1/b2d_world.d.ts +320 -0
  136. package/generated/versions/defold-1.13.1/buffer.d.ts +189 -0
  137. package/generated/versions/defold-1.13.1/camera.d.ts +303 -0
  138. package/generated/versions/defold-1.13.1/collectionfactory.d.ts +151 -0
  139. package/generated/versions/defold-1.13.1/collectionproxy.d.ts +93 -0
  140. package/generated/versions/defold-1.13.1/compute.d.ts +237 -0
  141. package/generated/versions/defold-1.13.1/crash.d.ts +143 -0
  142. package/generated/versions/defold-1.13.1/editor-vm/http.d.ts +56 -0
  143. package/generated/versions/defold-1.13.1/editor-vm/image.d.ts +48 -0
  144. package/generated/versions/defold-1.13.1/editor-vm/json.d.ts +10 -0
  145. package/generated/versions/defold-1.13.1/editor-vm/localization.d.ts +42 -0
  146. package/generated/versions/defold-1.13.1/editor-vm/tilemap_tiles.d.ts +80 -0
  147. package/generated/versions/defold-1.13.1/editor-vm/zip.d.ts +10 -0
  148. package/generated/versions/defold-1.13.1/editor-vm/zlib.d.ts +24 -0
  149. package/generated/versions/defold-1.13.1/editor.d.ts +1121 -0
  150. package/generated/versions/defold-1.13.1/factory.d.ts +124 -0
  151. package/generated/versions/defold-1.13.1/font.d.ts +81 -0
  152. package/generated/versions/defold-1.13.1/go.d.ts +1107 -0
  153. package/generated/versions/defold-1.13.1/graphics.d.ts +411 -0
  154. package/generated/versions/defold-1.13.1/gui.d.ts +2353 -0
  155. package/generated/versions/defold-1.13.1/html5.d.ts +55 -0
  156. package/generated/versions/defold-1.13.1/http.d.ts +77 -0
  157. package/generated/versions/defold-1.13.1/image.d.ts +131 -0
  158. package/generated/versions/defold-1.13.1/index.d.ts +57 -0
  159. package/generated/versions/defold-1.13.1/json.d.ts +80 -0
  160. package/generated/versions/defold-1.13.1/kinds/editor-script.d.ts +14 -0
  161. package/generated/versions/defold-1.13.1/label.d.ts +93 -0
  162. package/generated/versions/defold-1.13.1/liveupdate.d.ts +142 -0
  163. package/generated/versions/defold-1.13.1/material.d.ts +408 -0
  164. package/generated/versions/defold-1.13.1/model.d.ts +272 -0
  165. package/generated/versions/defold-1.13.1/msg.d.ts +10 -0
  166. package/generated/versions/defold-1.13.1/particlefx.d.ts +163 -0
  167. package/generated/versions/defold-1.13.1/physics.d.ts +559 -0
  168. package/generated/versions/defold-1.13.1/profiler.d.ts +235 -0
  169. package/generated/versions/defold-1.13.1/render.d.ts +1190 -0
  170. package/generated/versions/defold-1.13.1/resource.d.ts +1523 -0
  171. package/generated/versions/defold-1.13.1/socket.d.ts +784 -0
  172. package/generated/versions/defold-1.13.1/sound.d.ts +292 -0
  173. package/generated/versions/defold-1.13.1/sprite.d.ts +175 -0
  174. package/generated/versions/defold-1.13.1/sys.d.ts +678 -0
  175. package/generated/versions/defold-1.13.1/tilemap.d.ts +208 -0
  176. package/generated/versions/defold-1.13.1/timer.d.ts +125 -0
  177. package/generated/versions/defold-1.13.1/types.d.ts +61 -0
  178. package/generated/versions/defold-1.13.1/vmath.d.ts +774 -0
  179. package/generated/versions/defold-1.13.1/window.d.ts +183 -0
  180. package/generated/versions/defold-1.13.1/zlib.d.ts +41 -0
  181. package/generated/vmath.d.ts +1 -21
  182. package/generated/window.d.ts +71 -71
  183. package/identity-vocabulary.json +561 -0
  184. package/index.d.ts +7 -0
  185. package/package.json +18 -2
  186. package/scripts/example-store-io.ts +12 -1
  187. package/scripts/extension-goldens.ts +1 -0
  188. package/scripts/fidelity-baseline.json +146 -46
  189. package/scripts/generate-api-availability.ts +90 -12
  190. package/scripts/generate-api-signatures.ts +12 -2
  191. package/scripts/import-defold-release.ts +106 -26
  192. package/scripts/prose-constants.ts +77 -16
  193. package/scripts/regen.ts +391 -77
  194. package/scripts/shared-handle-methods.ts +53 -0
  195. package/scripts/signature-store-fs.ts +1 -0
  196. package/scripts/sync-api-docs.ts +48 -10
  197. package/src/api-availability.ts +0 -0
  198. package/src/api-doc.ts +201 -3
  199. package/src/api-identity.ts +252 -0
  200. package/src/core-types.ts +26 -3
  201. package/src/correction-pins.ts +49 -0
  202. package/src/doc-comment.ts +104 -11
  203. package/src/doc-corrections.ts +108 -0
  204. package/src/editor-vm-globals.d.ts +5 -2
  205. package/src/emit-dts.ts +2064 -281
  206. package/src/emit-messages.ts +52 -0
  207. package/src/example-store.ts +125 -14
  208. package/src/field-correction-coverage.ts +204 -0
  209. package/src/fnv1a.ts +16 -0
  210. package/src/go-overloads.d.ts +25 -6
  211. package/src/index-base-table.ts +145 -0
  212. package/src/index-slot-classifications.ts +588 -0
  213. package/src/index.ts +38 -5
  214. package/src/library-index-slot-classifications.ts +332 -0
  215. package/src/lifecycle.ts +61 -199
  216. package/src/luals-type-expr.ts +354 -0
  217. package/src/msg-overloads.d.ts +0 -1
  218. package/src/param-correction-provenance.ts +8 -11
  219. package/src/render-overloads.d.ts +0 -15
  220. package/src/return-correction-provenance.ts +58 -16
  221. package/src/sys-overloads.d.ts +48 -0
  222. package/src/vmath-overloads.d.ts +27 -1
  223. package/src/window-event-guard.d.ts +6 -9
  224. package/url-parameters.json +1 -2
package/src/emit-dts.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import messagesDoc from "../fixtures/messages_doc.json" with { type: "json" };
1
2
  import { type ApiSymbolIdentity, normalizedFunctionSignature } from "./api-availability";
2
3
  import type {
3
4
  ApiConstant,
@@ -5,22 +6,41 @@ import type {
5
6
  ApiModule,
6
7
  ApiParameter,
7
8
  ApiProperty,
9
+ ApiStruct,
10
+ ApiStructMember,
11
+ ApiTypedef,
8
12
  ApiVariable,
9
13
  } from "./api-doc";
10
14
  import { DEFOLD_TYPE_MAP } from "./core-types";
15
+ import { correctionPins, pinMatches, propertyCorrectionPins } from "./correction-pins";
11
16
  import {
12
17
  type DocCommentParts,
13
18
  htmlToCodeText,
14
19
  htmlToDocText,
15
20
  renderDocComment,
16
21
  splitExampleSources,
22
+ summaryFor,
17
23
  } from "./doc-comment";
18
24
  import type { TranslationStore } from "./example-store";
19
- import { hashExampleSource, lookupExampleTranslations, lookupTranslation } from "./example-store";
25
+ import { lookupExampleSegments, translateProseFences } from "./example-store";
26
+ import {
27
+ appendNotes,
28
+ type IndexBaseNoteResolver,
29
+ type IndexBaseSource,
30
+ indexBaseNoteResolver,
31
+ OVERRIDE_RETURN_SLOT,
32
+ } from "./index-slot-classifications";
33
+ import { type LualsPosition, mapLualsExpression, needsArrayParens } from "./luals-type-expr";
20
34
  import { classifyUrlParameter, type UrlParameterTable } from "./url-parameters";
21
35
 
36
+ export { summaryFor };
37
+
38
+ // Maps one ref-doc type token. `position` says whether the caller supplies the
39
+ // value (a parameter) or the engine hands it back; see `LualsPosition`.
40
+ export type DefoldTypeMapper = (token: string, position?: LualsPosition) => string;
41
+
22
42
  export interface EmitOptions {
23
- mapType?: (defoldType: string) => string;
43
+ mapType?: DefoldTypeMapper;
24
44
  // Constant FQNs from *other* modules, so a foreign token like
25
45
  // `graphics.BUFFER_TYPE_COLOR0_BIT` used as a param type inside `render`
26
46
  // brands to the same FQN-keyed type its owning module's `const` emits,
@@ -38,6 +58,23 @@ export interface EmitOptions {
38
58
  // emit. `regen` supplies the committed `url-parameters.json`; the table
39
59
  // arrives as data so this module stays free of `node:fs` (bug-88).
40
60
  urlParameters?: UrlParameterTable;
61
+ // Which classification map the index-base notes read. Defaults to the engine
62
+ // map; a library or extension declaration passes its page keys (`[]` for one
63
+ // no page documents), so it never names Defold as the receiver.
64
+ indexBaseSource?: IndexBaseSource;
65
+ // The TS path of each `ENUM`, `STRUCT` and `TYPEDEF` another module declares
66
+ // (`declaredTypePaths`), so a token naming one resolves instead of widening to
67
+ // `unknown`. The module's own declarations resolve without it. Read only by the
68
+ // default engine mapper; a caller-supplied `mapType` keeps its own vocabulary.
69
+ declaredTypes?: ReadonlyMap<string, string>;
70
+ // Tokens a surface names that the engine vocabulary does not (the editor's
71
+ // handles), resolved ahead of it both as whole tokens and as leaves inside a
72
+ // composite. Read by the default mapper together with the declared types.
73
+ typeLeaves?: Readonly<Record<string, string>>;
74
+ // Constant FQNs the target's own engine bindings register, which gate the
75
+ // `UNDOCUMENTED_CONSTANTS` back-fill. Absent for a surface with no vendored
76
+ // bindings (a ref-doc target), where every entry the ref-doc omits is emitted.
77
+ registeredConstants?: ReadonlySet<string>;
41
78
  }
42
79
 
43
80
  export const TS_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
@@ -174,11 +211,6 @@ export const ARBITRARY_TABLE_SLOTS = new Set([
174
211
  // not a loss. The audit skips counting these; the emitter already leaves an
175
212
  // uncurated slot as `Record`. Add deliberately, one rationale per line.
176
213
  export const ARBITRARY_TABLE_SLOT_KEYS = new Set([
177
- // "the chain definition" / "the shape definition" / "explosion definition" —
178
- // create-side option tables with no documented field list at this slot.
179
- "b2d.body.create_chain:param:definition",
180
- "b2d.body.create_shape:param:definition",
181
- "b2d.world.explode:param:definition",
182
214
  "b2d.joint.create_filter:param:definition", // "optional definition table"
183
215
  // opaque Box2D result structs: the doc names no fields.
184
216
  "b2d.chain.get_geometry:return:geometry", // "chain geometry table"
@@ -196,6 +228,18 @@ export const ARBITRARY_TABLE_SLOT_KEYS = new Set([
196
228
  // set to keep the `droppedMembers` count honest; an uncovered skip (e.g. a new
197
229
  // `skipFunctions` entry with no matching overload) still counts, so the gate
198
230
  // stays a real signal. Mirrors the ARBITRARY_TABLE_SLOTS audit-honesty pattern.
231
+ // A slot the engine treats as omissible on a function OVERLOAD_COVERED_SKIPS
232
+ // hands to a `*-overloads.d.ts` form without it, where no generated declaration
233
+ // exists for OPTIONAL_SLOT_CORRECTIONS to reach. Availability identity reads it
234
+ // as optional, so a ref-doc that starts marking it is no change. The value is
235
+ // the evidence, as in OPTIONAL_SLOT_CORRECTIONS.
236
+ export const HAND_DECLARED_OPTIONAL_SLOTS: ReadonlyMap<string, string> = new Map(
237
+ (["y", "z"] as const).map((slot) => [
238
+ `vmath.euler_to_quat:param:${slot}`,
239
+ "example `vmath.euler_to_quat(v)` passes a vector3 alone; vmath-overloads.d.ts declares that form",
240
+ ]),
241
+ );
242
+
199
243
  export const OVERLOAD_COVERED_SKIPS = new Set([
200
244
  // go-overloads.d.ts supplies the typed go.get / go.set / go.property shapes.
201
245
  "go.get",
@@ -210,14 +254,55 @@ export const OVERLOAD_COVERED_SKIPS = new Set([
210
254
  "render.render_target",
211
255
  // vmath-overloads.d.ts supplies the vector3 and three-number euler_to_quat arities.
212
256
  "vmath.euler_to_quat",
257
+ // vmath-overloads.d.ts supplies the empty form and the table form; the engine
258
+ // raises on an explicit nil, so no optional parameter admits undefined.
259
+ "vmath.vector",
213
260
  // vmath-overloads.d.ts supplies the generic covariant signatures.
214
261
  "vmath.clamp",
215
262
  "vmath.lerp",
216
263
  "vmath.slerp",
217
264
  "vmath.mul_per_elem",
218
265
  "vmath.normalize",
266
+ // sys-overloads.d.ts supplies the disabling form and the form that takes the
267
+ // cooldown the engine requires when enabling.
268
+ "sys.set_engine_throttle",
269
+ // sys-overloads.d.ts supplies the form without a default, which returns nil
270
+ // for a missing key, and the form with one, which always returns a string.
271
+ "sys.get_config_string",
272
+ ]);
273
+
274
+ // A declaration upstream files under another function's name: its return slot
275
+ // and prose are the named sibling's, so the element keeps only its own
276
+ // declarations. Keyed `<fqn>:<first return name>`, which tells the misfiled
277
+ // declaration from the real one. `return-correction-provenance.test.ts` reds
278
+ // once no retained target still files it, and the entry is then deleted.
279
+ export const MISFILED_DECLARATIONS: ReadonlyMap<string, string> = new Map([
280
+ // b2d_body_doc.json repeats `get_angle`'s `angle` return under `get_world_center`.
281
+ ["b2d.body.get_world_center:angle", "b2d.body.get_angle"],
282
+ ]);
283
+
284
+ function isMisfiled(fn: ApiFunction): boolean {
285
+ return MISFILED_DECLARATIONS.has(`${fn.name}:${fn.returnValues[0]?.name ?? ""}`);
286
+ }
287
+
288
+ // skipFunction FQNs withheld because no engine binding registers them in the
289
+ // target that documents them, so the declared call would raise. Like
290
+ // OVERLOAD_COVERED_SKIPS, the audit leaves them out of `droppedMembers`: the
291
+ // surface loses nothing a script could call. The engine-binding diff reports a
292
+ // missing binding if one is declared again.
293
+ export const UNBOUND_SKIPS = new Set([
294
+ "b2d.body.get_contact_list",
295
+ "b2d.body.get_user_data",
296
+ "b2d.body.set_user_data",
297
+ "b2d.body.reset_mass_data",
219
298
  ]);
220
299
 
300
+ // skipFunction FQNs withheld because another lane's declaration takes the same
301
+ // global name: the runtime `http.response` table beside the editor VM's
302
+ // `http.response` handle. The audit leaves them out of `droppedMembers`, since
303
+ // a slot that needs the shape types it inline.
304
+ export const NAME_CLASH_SKIPS = new Set(["http.response"]);
305
+
221
306
  // Element names whose `table` slot is a prose-only `a table mapping X to Y`
222
307
  // shape the field-list parser cannot read, but whose key/value a human curated
223
308
  // from the doc. Emitted as `LuaMap<K, V>` because the key is a branded `Hash`,
@@ -246,6 +331,17 @@ export const RETURN_TYPE_OVERRIDES: ReadonlyMap<string, string> = new Map([
246
331
  // `returnvalues` is empty while every sibling `editor.tx.*` builder declares
247
332
  // one, and `editor.transact()` takes exactly that.
248
333
  ["editor.tx.add", 'Opaque<"transaction_step">'],
334
+ // The engine binding pushes a value the ref-doc never documents:
335
+ // script_sys.cpp:Sys_Save pushes whether the write succeeded,
336
+ // script_tilemap.cpp:TileMap_SetTile pushes false for an unknown layer or an
337
+ // out-of-range cell, and script_box2d_body_v3.cpp:Body_CreateShape pushes the
338
+ // created shape's info table through PushShapeInfo.
339
+ ["sys.save", "boolean"],
340
+ ["tilemap.set_tile", "boolean"],
341
+ [
342
+ "b2d.body.create_shape",
343
+ '{ index: number; shape_id: Opaque<"b2Shape">; type: number; sensor: boolean; density: number; friction: number; restitution: number; material: number; child_count: number; is_chain_segment: boolean }',
344
+ ],
249
345
  ]);
250
346
 
251
347
  // A function whose `returnvalues` declare a concrete type while the same slot's
@@ -269,7 +365,12 @@ export const RETURN_TYPE_OVERRIDES: ReadonlyMap<string, string> = new Map([
269
365
  export interface ReturnTypeCorrection {
270
366
  readonly ts: string;
271
367
  readonly upstream: readonly string[];
368
+ // A later release's spelling of the same defect; see `correction-pins.ts`.
369
+ readonly retypedUpstream?: readonly (readonly string[])[];
272
370
  readonly reason: string;
371
+ // The named value of a multi-return the correction rewrites; `ts` is then
372
+ // that value's type. Absent, the correction rewrites a single return.
373
+ readonly slot?: string;
273
374
  }
274
375
 
275
376
  // Keyed by FQN, mirroring PROPERTY_TYPE_CORRECTIONS.
@@ -282,11 +383,50 @@ export const RETURN_TYPE_CORRECTIONS: ReadonlyMap<string, ReturnTypeCorrection>
282
383
  reason: "the body if successful. Otherwise nil.",
283
384
  },
284
385
  ],
386
+ [
387
+ "b2d.shape.ray_cast",
388
+ {
389
+ ts: "{ point: Vector3; normal: Vector3; fraction: number; iterations: number } | undefined",
390
+ upstream: ["table"],
391
+ reason: "and iterations, or nil",
392
+ },
393
+ ],
394
+ [
395
+ "b2d.world.cast_ray_closest",
396
+ {
397
+ ts: "{ fixture: number; shape: number; point: Vector3; normal: Vector3; fraction: number; node_visits: number; leaf_visits: number } | undefined",
398
+ upstream: ["table"],
399
+ reason: "and leaf_visits, or nil",
400
+ },
401
+ ],
402
+ [
403
+ "gui.new_texture",
404
+ {
405
+ ts: "number | undefined",
406
+ upstream: ["number"],
407
+ reason: "codes if unsuccessful",
408
+ slot: "code",
409
+ },
410
+ ],
411
+ // 1.13.2 declares `render_predicate`, whose TYPEDEF aliases `userdata`, and
412
+ // that still emits `number`.
413
+ [
414
+ "render.predicate",
415
+ {
416
+ ts: 'Opaque<"render_predicate">',
417
+ upstream: ["number"],
418
+ retypedUpstream: [["render_predicate"]],
419
+ reason: "new predicate",
420
+ },
421
+ ],
422
+ // 1.13.2 declares both as `fun(...:any):any`, which emits
423
+ // `(...args: unknown[]) => unknown` and still drops the contract.
285
424
  [
286
425
  "socket.protect",
287
426
  {
288
427
  ts: "(...args: unknown[]) => LuaMultiReturn<[unknown, string | undefined]>",
289
428
  upstream: ["function(function())"],
429
+ retypedUpstream: [["fun(...:any):any"]],
290
430
  reason: "returns nil followed by an error message.",
291
431
  },
292
432
  ],
@@ -295,38 +435,172 @@ export const RETURN_TYPE_CORRECTIONS: ReadonlyMap<string, ReturnTypeCorrection>
295
435
  {
296
436
  ts: "<T>(value: T | undefined, ...rest: T extends { readonly __tstlMultiReturn: unknown } ? [boxedMultiReturn: never] : unknown[]) => T",
297
437
  upstream: ["function"],
438
+ retypedUpstream: [["fun(...:any):any"]],
298
439
  reason: "the customized try function.",
299
440
  },
300
441
  ],
301
442
  ]);
302
443
 
303
- // A parameter whose declared `types` reject what the same element's prose, or
304
- // an upstream example, demonstrably passes — a render target by resource name,
305
- // a texture unit, a Lua sequence where `table` is declared. Admission rule: only
306
- // a slot upstream's own prose or example contradicts; a slot that is merely
307
- // imprecise is not a candidate. Every such slot is too narrow, never wrong, so
308
- // `adds` is appended to the mapped union rather than replacing it: the URL
309
- // address alias, array curation and constant mapping stay live, and optionality
310
- // stays with `isDocOptional`. Each entry records the upstream tokens it
311
- // contradicts and `param-correction-provenance.test.ts` pins them against every
312
- // retained vendored ref-doc. Once no retained target still declares those
313
- // tokens the entry is deleted, never re-pinned.
444
+ // A parameter whose declared `types` disagree with what the same element's
445
+ // prose, an upstream example, or the engine binding demonstrably takes — a render
446
+ // target by resource name, a texture unit, a Lua sequence where `table` is
447
+ // declared, a hash the binding refuses as a string. Admission rule: only a slot
448
+ // that evidence contradicts; a slot that is merely imprecise is not a candidate.
449
+ // The correction edits the mapped union rather than replacing it: `adds` appends
450
+ // what the slot also takes and `removes` drops members the binding raises on, so
451
+ // the URL address alias, array curation and constant mapping stay live, and
452
+ // optionality stays with `isDocOptional`. Each entry records the upstream tokens
453
+ // it contradicts, plus any later spelling of the same defect in
454
+ // `retypedUpstream`, and applies only to a slot still declaring one of them;
455
+ // `param-correction-provenance.test.ts` pins them against every retained
456
+ // vendored ref-doc. Once no retained target still declares any of them the
457
+ // entry is deleted, never re-pinned.
314
458
  export interface ParamTypeCorrection {
315
- readonly adds: string;
459
+ readonly adds?: string;
460
+ readonly removes?: readonly string[];
316
461
  readonly upstream: readonly string[];
462
+ readonly retypedUpstream?: readonly (readonly string[])[];
317
463
  readonly reason: string;
318
464
  }
319
465
 
466
+ const CAMERA_ADDRESS_REASON =
467
+ "render_script_camera.cpp:CheckRenderCamera resolves anything but a number handle through ResolveURL, which takes a string or a hash";
468
+
469
+ const CAMERA_SLOT_FUNCTIONS = [
470
+ "get_aspect_ratio",
471
+ "get_auto_aspect_ratio",
472
+ "get_enabled",
473
+ "get_far_z",
474
+ "get_fov",
475
+ "get_near_z",
476
+ "get_orthographic_auto_zoom",
477
+ "get_orthographic_mode",
478
+ "get_orthographic_zoom",
479
+ "get_projection",
480
+ "get_view",
481
+ "screen_to_world",
482
+ "screen_xy_to_world",
483
+ "set_aspect_ratio",
484
+ "set_auto_aspect_ratio",
485
+ "set_far_z",
486
+ "set_fov",
487
+ "set_near_z",
488
+ "set_orthographic_mode",
489
+ "set_orthographic_zoom",
490
+ "world_to_screen",
491
+ ] as const;
492
+
320
493
  // Keyed `<element>:param:<slot>` like OPTIONAL_SLOT_CORRECTIONS, so a
321
494
  // same-named slot on another element is untouched.
322
- export const PARAM_TYPE_CORRECTIONS: ReadonlyMap<string, ParamTypeCorrection> = new Map([
495
+ export const PARAM_TYPE_CORRECTIONS: ReadonlyMap<string, ParamTypeCorrection> = new Map<
496
+ string,
497
+ ParamTypeCorrection
498
+ >([
499
+ ...CAMERA_SLOT_FUNCTIONS.map(
500
+ (name) =>
501
+ [
502
+ `camera.${name}:param:camera`,
503
+ {
504
+ adds: "string | Hash",
505
+ upstream: ["url", "number", "nil"],
506
+ reason: CAMERA_ADDRESS_REASON,
507
+ },
508
+ ] as const,
509
+ ),
510
+ ...(["set_group", "get_maskbit", "set_maskbit"] as const).map(
511
+ (name) =>
512
+ [
513
+ `physics.${name}:param:group`,
514
+ {
515
+ adds: "Hash",
516
+ upstream: ["string"],
517
+ reason: `script_physics.cpp reads the ${name} group with CheckHashOrString(L, 2)`,
518
+ },
519
+ ] as const,
520
+ ),
521
+ [
522
+ "resource.get_text_metrics:param:url",
523
+ {
524
+ adds: "string",
525
+ upstream: ["hash"],
526
+ reason: "script_resource.cpp:GetTextMetrics reads the font path with CheckHashOrString(L, 1)",
527
+ },
528
+ ],
529
+ [
530
+ "collectionproxy.get_resources:param:collectionproxy",
531
+ {
532
+ adds: "string | Hash",
533
+ upstream: ["url"],
534
+ reason:
535
+ "script_collectionproxy.cpp:CollectionProxy_GetResourcesInternal resolves the proxy through GetComponentFromLua, which takes a string or a hash",
536
+ },
537
+ ],
538
+ ...(["screen_to_local", "set_screen_position"] as const).map(
539
+ (name) =>
540
+ [
541
+ `gui.${name}:param:screen_position`,
542
+ {
543
+ adds: "Vector4",
544
+ upstream: ["vector3"],
545
+ reason: "gui_script.cpp:GetPositionFromArgumentIndex takes a vector3 or a vector4",
546
+ },
547
+ ] as const,
548
+ ),
549
+ [
550
+ "particlefx.set_constant:param:value",
551
+ {
552
+ adds: "Matrix4",
553
+ upstream: ["vector4"],
554
+ reason: "script_particlefx.cpp:ParticleFX_SetConstant takes a vector4 or a matrix4 value",
555
+ },
556
+ ],
557
+ ...(["animate", "cancel_animations"] as const).map(
558
+ (name) =>
559
+ [
560
+ `gui.${name}:param:property`,
561
+ {
562
+ adds: "Hash",
563
+ upstream: name === "animate" ? ["string", "constant"] : ["nil", "string", "constant"],
564
+ reason: `gui_script.cpp reads the ${name} property with CheckHashOrString(L, 2)`,
565
+ },
566
+ ] as const,
567
+ ),
568
+ [
569
+ "sound.get_group_name:param:group",
570
+ {
571
+ removes: ["string"],
572
+ upstream: ["string", "hash"],
573
+ reason: "script_sound.cpp:Sound_GetGroupName reads CheckHash(L, 1), which refuses a string",
574
+ },
575
+ ],
576
+ ...(["get_mesh_enabled", "set_mesh_enabled"] as const).map(
577
+ (name) =>
578
+ [
579
+ `model.${name}:param:mesh_id`,
580
+ {
581
+ removes: ["Url"],
582
+ upstream: ["string", "hash", "url"],
583
+ reason: `script_model.cpp reads the ${name} mesh id with CheckHashOrString(L, 2), which refuses a url`,
584
+ },
585
+ ] as const,
586
+ ),
587
+ [
588
+ "resource.create_texture_async:param:path",
589
+ {
590
+ removes: ["Hash"],
591
+ upstream: ["string", "hash"],
592
+ reason:
593
+ "script_resource.cpp:CheckCreateTextureResourceParams reads the path with luaL_checkstring(L, 1), which refuses a hash",
594
+ },
595
+ ],
323
596
  [
324
597
  "render.set_render_target:param:render_target",
325
598
  {
326
599
  adds: "string | Hash | typeof render.RENDER_TARGET_DEFAULT",
327
600
  upstream: ["render_target"],
601
+ retypedUpstream: [["render_target", "string", "hash", "nil"]],
328
602
  reason:
329
- "\"render.RENDER_TARGET_DEFAULT to set the default render target\"; example `render.set_render_target('my_rt_resource')`",
603
+ "\"render.RENDER_TARGET_DEFAULT to set the default render target\"; example `render.set_render_target('my_rt_resource')`; 1.13.2 adds the names but still omits the constant",
330
604
  },
331
605
  ],
332
606
  [
@@ -377,9 +651,21 @@ export const PARAM_TYPE_CORRECTIONS: ReadonlyMap<string, ParamTypeCorrection> =
377
651
  [
378
652
  "render.set_camera:param:camera",
379
653
  {
380
- adds: "string",
654
+ adds: "string | Hash",
381
655
  upstream: ["url", "number", "nil"],
382
- reason: 'example `render.set_camera("main:/my_go#camera")`',
656
+ reason:
657
+ 'example `render.set_camera("main:/my_go#camera")`; render_script.cpp:RenderScript_SetCamera resolves a non-number camera through ResolveURL, which also takes a hash',
658
+ },
659
+ ],
660
+ [
661
+ "render.draw:param:predicate",
662
+ {
663
+ adds: 'Opaque<"render_predicate">',
664
+ removes: ["number"],
665
+ upstream: ["number"],
666
+ retypedUpstream: [["render_predicate"]],
667
+ reason:
668
+ "render_script.cpp:RenderScript_Draw reads the predicate with RenderScriptPredicate_Check, the userdata render.predicate returns; 1.13.2's `render_predicate` still emits `number`",
383
669
  },
384
670
  ],
385
671
  [
@@ -387,7 +673,9 @@ export const PARAM_TYPE_CORRECTIONS: ReadonlyMap<string, ParamTypeCorrection> =
387
673
  {
388
674
  adds: "boolean",
389
675
  upstream: ["string", "hash", "url", "table"],
390
- reason: "example `go.delete(true)` deletes the script game object and its children",
676
+ retypedUpstream: [["string", "hash", "url", "(string|hash|url)[]"]],
677
+ reason:
678
+ "example `go.delete(true)` deletes the script game object and its children; 1.13.2 types the id list but still omits `boolean`",
391
679
  },
392
680
  ],
393
681
  [
@@ -420,8 +708,9 @@ export const PARAM_TYPE_CORRECTIONS: ReadonlyMap<string, ParamTypeCorrection> =
420
708
  {
421
709
  adds: "readonly unknown[]",
422
710
  upstream: ["table"],
711
+ retypedUpstream: [["table<any, any>"]],
423
712
  reason:
424
- 'example builds `my_table` with `table.insert(my_table, "my_value")` and passes it to `sys.save`',
713
+ 'example builds `my_table` with `table.insert(my_table, "my_value")` and passes it to `sys.save`; 1.13.2\'s `table<any, any>` emits a `LuaTable`, which refuses an array',
425
714
  },
426
715
  ],
427
716
  [
@@ -429,46 +718,41 @@ export const PARAM_TYPE_CORRECTIONS: ReadonlyMap<string, ParamTypeCorrection> =
429
718
  {
430
719
  adds: "readonly unknown[]",
431
720
  upstream: ["table"],
721
+ retypedUpstream: [["table<any, any>"]],
432
722
  reason:
433
- 'example builds `my_table` with `table.insert(my_table, "my_value")` and passes it to `sys.serialize`',
723
+ 'example builds `my_table` with `table.insert(my_table, "my_value")` and passes it to `sys.serialize`; 1.13.2\'s `table<any, any>` emits a `LuaTable`, which refuses an array',
724
+ },
725
+ ],
726
+ [
727
+ "window.set_dim_mode:param:mode",
728
+ {
729
+ removes: ["window.DIMMING"],
730
+ adds: "window.DimModeStateSettable",
731
+ upstream: ["window.DIMMING"],
732
+ reason:
733
+ 'script_window.cpp:SetDimMode raises "The dim mode specified is not supported." for anything but DIMMING_ON and DIMMING_OFF',
734
+ },
735
+ ],
736
+ ...(["texture", "render_target"] as const).map((handle): [string, ParamTypeCorrection] => [
737
+ `resource.get_${handle}_info:param:path`,
738
+ {
739
+ adds: "number",
740
+ upstream: ["hash", "string", handle],
741
+ reason:
742
+ "script_resource.cpp reads any number in slot 1 as the handle, which `info.handle` returns and older targets declared as `number`",
743
+ },
744
+ ]),
745
+ [
746
+ "tilemap.set_tile:param:transform_bitmask",
747
+ {
748
+ adds: "number",
749
+ upstream: ["tilemap.TRANSFORM"],
750
+ reason:
751
+ "a bitmask: the ref-doc example passes tilemap.H_FLIP + tilemap.V_FLIP + tilemap.ROTATE_90, a sum no single TRANSFORM member names",
434
752
  },
435
753
  ],
436
754
  ]);
437
755
 
438
- // Parameter doc text whose upstream wording describes Lua rather than the
439
- // TypeScript a user writes. The transpiler adds one to a literal `options.index`
440
- // at these calls, so the 1-based wording would tell the author to be off by one.
441
- // The `.d.ts` emit and the API reference both apply it, and applying it throws
442
- // when `from` is absent: an upstream rewording fails the build instead of
443
- // shipping the stale text alongside the lowering.
444
- export interface ParamDocRewrite {
445
- readonly from: string;
446
- readonly to: string;
447
- }
448
-
449
- const ZERO_BASED_INDEX_OPTION_DOC: ParamDocRewrite = {
450
- from: "index into array property (1 based)",
451
- to: "zero-based index into array property; the transpiler emits Defold's 1-based index",
452
- };
453
-
454
- // Keyed `<element>:param:<slot>` like PARAM_TYPE_CORRECTIONS.
455
- export const PARAM_DOC_REWRITES: ReadonlyMap<string, ParamDocRewrite> = new Map([
456
- ["go.get:param:options", ZERO_BASED_INDEX_OPTION_DOC],
457
- ["go.set:param:options", ZERO_BASED_INDEX_OPTION_DOC],
458
- ["gui.get:param:options", ZERO_BASED_INDEX_OPTION_DOC],
459
- ["gui.set:param:options", ZERO_BASED_INDEX_OPTION_DOC],
460
- ]);
461
-
462
- export function rewriteParamDoc(elementName: string, slotName: string, doc: string): string {
463
- const key = tableSlotKey(elementName, "param", slotName);
464
- const rewrite = PARAM_DOC_REWRITES.get(key);
465
- if (rewrite === undefined) return doc;
466
- if (!doc.includes(rewrite.from)) {
467
- throw new Error(`PARAM_DOC_REWRITES ${key}: upstream doc no longer contains "${rewrite.from}"`);
468
- }
469
- return doc.replaceAll(rewrite.from, rewrite.to);
470
- }
471
-
472
756
  // A property whose upstream `<span class="type">` states a type the engine does
473
757
  // not use. Unlike every other override here, which fills a gap upstream left
474
758
  // empty, these contradict a token upstream explicitly declares — so each entry
@@ -482,6 +766,8 @@ export function rewriteParamDoc(elementName: string, slotName: string, doc: stri
482
766
  export interface PropertyTypeCorrection {
483
767
  readonly ts: string;
484
768
  readonly upstream: string;
769
+ // A later release's span for the same defect; see `correction-pins.ts`.
770
+ readonly retypedUpstream?: readonly string[];
485
771
  readonly reason: string;
486
772
  }
487
773
 
@@ -497,6 +783,24 @@ export const PROPERTY_TYPE_CORRECTIONS: ReadonlyMap<string, PropertyTypeCorrecti
497
783
  "a count of animation frames, which the prose confirms; `hash` is a copy-paste of the sibling animation-id spans",
498
784
  },
499
785
  ],
786
+ [
787
+ "go.scale",
788
+ {
789
+ ts: "number | Vector3",
790
+ upstream: "number",
791
+ reason:
792
+ "the engine reads a vector3 back from `go.get`, and `go.set` also takes a number, which it applies as a uniform scale",
793
+ },
794
+ ],
795
+ [
796
+ "label.scale",
797
+ {
798
+ ts: "Vector3",
799
+ upstream: "number | vector3",
800
+ reason:
801
+ 'the engine reads a vector3 back from `go.get`, and `go.set` raises "must be a vmath.vector3" when given a number',
802
+ },
803
+ ],
500
804
  [
501
805
  "camera.projection",
502
806
  {
@@ -543,9 +847,10 @@ export const PROPERTY_KEY_RANGES: ReadonlyMap<string, PropertyKeyRange> = new Ma
543
847
  // examples skip — so the emitted declaration rejects the call the docs teach.
544
848
  //
545
849
  // Keyed `<element>:param:<slot>`, the shape `tableSlotKey` builds, so a
546
- // same-named slot on another element is untouched. The value is the upstream
547
- // evidence for that one entry: the example call that omits it, or the doc phrase
548
- // that names it optional. Both emitted shapes follow from one predicate — a
850
+ // same-named slot on another element is untouched. The value is the evidence for
851
+ // that one entry: the example call that omits it, the doc phrase that names it
852
+ // optional, or the engine binding that reads it as omissible
853
+ // (`<file>:<cFunction>`). Both emitted shapes follow from one predicate — a
549
854
  // trailing slot becomes `slot?:`, an interior one `slot: T | undefined`.
550
855
  //
551
856
  // Demonstrated omissions only. `resource.set_texture`'s buffer and
@@ -557,10 +862,6 @@ export const OPTIONAL_SLOT_CORRECTIONS: ReadonlyMap<string, string> = new Map([
557
862
  'example `resource.create_texture("/my_custom_texture.texturec", tparams)`; "optional buffer of precreated pixel data"',
558
863
  ],
559
864
  ["resource.create_texture_async:param:buffer", '"optional buffer of precreated pixel data"'],
560
- [
561
- "resource.create_texture_async:param:callback",
562
- 'example `resource.create_texture_async("/my_texture.texturec", tparams, tbuffer)`',
563
- ],
564
865
  [
565
866
  "gui.new_texture:param:flip",
566
867
  'example `gui.new_texture("orange_tx", w, h, "rgb", string.rep(orange, w * h))`',
@@ -569,7 +870,6 @@ export const OPTIONAL_SLOT_CORRECTIONS: ReadonlyMap<string, string> = new Map([
569
870
  "gui.set_texture_data:param:flip",
570
871
  'example `gui.set_texture_data("dynamic_tx", w, h, "rgb", string.rep(orange, w * h))`',
571
872
  ],
572
- ["sys.set_engine_throttle:param:cooldown", "example `sys.set_engine_throttle(false)`"],
573
873
  [
574
874
  "b2d.fixture.set_shape:param:update_mass",
575
875
  '2 of 3 examples omit it; "The body mass is not updated unless update_mass is true"',
@@ -583,6 +883,26 @@ export const OPTIONAL_SLOT_CORRECTIONS: ReadonlyMap<string, string> = new Map([
583
883
  '"optional maximum translation fraction, defaults to 1"',
584
884
  ],
585
885
  ["iap.buy:param:options", '"optional parameters as properties"'],
886
+ [
887
+ "go.world_to_local_position:param:url",
888
+ "gameobject_script.cpp:Script_WorldToLocalPosition resolves argument 2 with ResolveInstance, which takes the calling instance when it is absent",
889
+ ],
890
+ [
891
+ "go.world_to_local_transform:param:url",
892
+ "gameobject_script.cpp:Script_WorldToLocalTransfrom resolves argument 2 with ResolveInstance, which takes the calling instance when it is absent",
893
+ ],
894
+ [
895
+ "go.cancel_animations:param:url",
896
+ "gameobject_script.cpp:Script_CancelAnimations resolves argument 1 with ResolveURL, which takes the calling instance when it is absent",
897
+ ],
898
+ [
899
+ "b2d.fixture.set_density:param:update_mass",
900
+ "script_box2d_fixture_v2.cpp:Fixture_SetDensity reads argument 4 only when lua_gettop(L) >= 4",
901
+ ],
902
+ [
903
+ "render.set_render_target:param:render_target",
904
+ "render_script.cpp:RenderScript_SetRenderTarget sets the default target when lua_gettop(L) == 0",
905
+ ],
586
906
  ["socket.newtry:param:finalizer", "socket.protect's example `local try = socket.newtry()`"],
587
907
  ...(
588
908
  [
@@ -605,6 +925,159 @@ export const OPTIONAL_SLOT_CORRECTIONS: ReadonlyMap<string, string> = new Map([
605
925
  ),
606
926
  ]);
607
927
 
928
+ // A parameter the ref-doc marks omissible — `[name]` or a `nil` type — that the
929
+ // engine binding refuses to go without, so the emitted declaration accepts a call
930
+ // that raises. It applies only where a ref-doc marks the slot, so it follows each
931
+ // target's own markup, and it overrides OPTIONAL_SLOT_CORRECTIONS: the slot is
932
+ // never emitted with `?`. `nil` records whether the binding still takes an
933
+ // explicit nil there — `lua_isnil` answers false past the stack top, so such a
934
+ // slot must be passed yet may be nil — which emits `slot: T | undefined`.
935
+ //
936
+ // Keyed `<element>:param:<slot>` like OPTIONAL_SLOT_CORRECTIONS. `evidence` is the
937
+ // C++ binding, `<file>:<cFunction>`, and what it does with the slot;
938
+ // `required-correction-provenance.test.ts` deletes an entry once no retained
939
+ // target marks the slot omissible.
940
+ export interface RequiredSlotCorrection {
941
+ readonly nil: boolean;
942
+ readonly evidence: string;
943
+ }
944
+
945
+ const COMPONENT_URL_EVIDENCE =
946
+ "resolves the url through GetComponentFromLua, which refuses the calling script's own url";
947
+
948
+ export const REQUIRED_SLOT_CORRECTIONS: ReadonlyMap<string, RequiredSlotCorrection> = new Map<
949
+ string,
950
+ RequiredSlotCorrection
951
+ >([
952
+ [
953
+ "resource.create_texture_async:param:callback",
954
+ {
955
+ nil: false,
956
+ evidence:
957
+ "script_resource.cpp:CreateTextureAsync hands argument 4 to dmScript::CreateCallback, which raises through luaL_checktype(L, 4, LUA_TFUNCTION)",
958
+ },
959
+ ],
960
+ ...(["factory", "collectionfactory"] as const).flatMap((namespace) => {
961
+ const file = namespace === "factory" ? "script_factory.cpp" : "script_collection_factory.cpp";
962
+ const prefix = namespace === "factory" ? "FactoryComp" : "CollectionFactoryComp";
963
+ return [
964
+ ...(
965
+ [
966
+ ["get_status", "GetStatus"],
967
+ ["unload", "Unload"],
968
+ ["set_prototype", "SetPrototype"],
969
+ ["load", "Load"],
970
+ ] as const
971
+ ).map(
972
+ ([name, cFunction]) =>
973
+ [
974
+ `${namespace}.${name}:param:url`,
975
+ { nil: false, evidence: `${file}:${prefix}_${cFunction} ${COMPONENT_URL_EVIDENCE}` },
976
+ ] as const,
977
+ ),
978
+ [
979
+ `${namespace}.load:param:complete_function`,
980
+ {
981
+ nil: false,
982
+ evidence: `${file}:${prefix}_Load raises unless argument 2 is a function`,
983
+ },
984
+ ] as const,
985
+ [
986
+ `${namespace}.set_prototype:param:prototype`,
987
+ {
988
+ nil: true,
989
+ evidence: `${file}:${prefix}_SetPrototype reads luaL_checkstring(L, 2) unless lua_isnil(L, 2)`,
990
+ },
991
+ ] as const,
992
+ ];
993
+ }),
994
+ [
995
+ "collectionproxy.set_collection:param:url",
996
+ {
997
+ nil: false,
998
+ evidence: `script_collectionproxy.cpp:CollectionProxy_SetCollection ${COMPONENT_URL_EVIDENCE}`,
999
+ },
1000
+ ],
1001
+ [
1002
+ "collectionproxy.set_collection:param:prototype",
1003
+ {
1004
+ nil: true,
1005
+ evidence:
1006
+ "script_collectionproxy.cpp:CollectionProxy_SetCollection reads luaL_checkstring(L, 2) unless lua_isnil(L, 2)",
1007
+ },
1008
+ ],
1009
+ [
1010
+ "sound.set_gain:param:gain",
1011
+ { nil: false, evidence: "script_sound.cpp:Sound_SetGain reads luaL_checknumber(L, 2)" },
1012
+ ],
1013
+ [
1014
+ "sound.set_pan:param:pan",
1015
+ { nil: false, evidence: "script_sound.cpp:Sound_SetPan reads luaL_checknumber(L, 2)" },
1016
+ ],
1017
+ [
1018
+ "resource.create_buffer:param:table",
1019
+ {
1020
+ nil: false,
1021
+ evidence: "script_resource.cpp:CreateBuffer checks luaL_checktype(L, 2, LUA_TTABLE)",
1022
+ },
1023
+ ],
1024
+ [
1025
+ "resource.create_sound_data:param:options",
1026
+ {
1027
+ nil: false,
1028
+ evidence: "script_resource.cpp:CreateSoundData checks luaL_checktype(L, 2, LUA_TTABLE)",
1029
+ },
1030
+ ],
1031
+ ...(["move_above", "move_below"] as const).map(
1032
+ (name) =>
1033
+ [
1034
+ `gui.${name}:param:reference`,
1035
+ {
1036
+ nil: true,
1037
+ evidence: `gui_script.cpp:Lua${name === "move_above" ? "MoveAbove" : "MoveBelow"} reads a node from argument 2 unless lua_isnil(L, 2)`,
1038
+ },
1039
+ ] as const,
1040
+ ),
1041
+ [
1042
+ "gui.set_parent:param:parent",
1043
+ {
1044
+ nil: true,
1045
+ evidence: "gui_script.cpp:LuaSetParent reads a node from argument 2 unless lua_isnil(L, 2)",
1046
+ },
1047
+ ],
1048
+ [
1049
+ "gui.set:param:value",
1050
+ {
1051
+ nil: true,
1052
+ evidence: "gui_script.cpp:LuaSet converts argument 3 with LuaToVar unless lua_isnil(L, 3)",
1053
+ },
1054
+ ],
1055
+ [
1056
+ "window.set_listener:param:callback",
1057
+ {
1058
+ nil: true,
1059
+ evidence: "script_window.cpp:SetListener checks luaL_checkany(L, 1); nil clears the listener",
1060
+ },
1061
+ ],
1062
+ [
1063
+ "html5.set_interaction_listener:param:callback",
1064
+ {
1065
+ nil: true,
1066
+ evidence:
1067
+ "script_html5_js.cpp:Html5_SetInteractionListener checks luaL_checkany(L, 1); nil clears the listener",
1068
+ },
1069
+ ],
1070
+ ]);
1071
+
1072
+ function requiredSlotCorrection(
1073
+ p: ApiParameter,
1074
+ elementName: string,
1075
+ ): RequiredSlotCorrection | undefined {
1076
+ return isMarkedOptional(p)
1077
+ ? REQUIRED_SLOT_CORRECTIONS.get(tableSlotKey(elementName, "param", p.name))
1078
+ : undefined;
1079
+ }
1080
+
608
1081
  // A first slot upstream documents as standing in for a second argument list:
609
1082
  // every `b2d.shape` function takes a shape handle "or pass `body, shape_index`".
610
1083
  // The element keeps its declared signature and gains an overload with
@@ -633,7 +1106,7 @@ export const FIRST_SLOT_ALTERNATIVES: ReadonlyMap<string, FirstSlotAlternative>
633
1106
  },
634
1107
  {
635
1108
  name: "shape_index",
636
- doc: "The shape's 1-based index on that body, as <code>b2d.body.get_shapes</code> lists them.",
1109
+ doc: "The shape's position on that body, in the order <code>b2d.body.get_shapes</code> lists them.",
637
1110
  types: ["number"],
638
1111
  isOptional: false,
639
1112
  },
@@ -1067,9 +1540,17 @@ export function constantUnionAlias(fqns: readonly string[]): ConstantUnionAlias
1067
1540
  export function constantUnionAliasDeclarations(namespace: string): string[] {
1068
1541
  return CONSTANT_UNION_ALIASES.filter((row) => row.home === namespace)
1069
1542
  .sort((a, b) => a.name.localeCompare(b.name))
1070
- .map(
1071
- (row) => `type ${row.name} = ${row.members.map((member) => `typeof ${member}`).join(" | ")};`,
1072
- );
1543
+ .map((row) => unionAliasDeclaration(row.name, row.members));
1544
+ }
1545
+
1546
+ function unionAliasDeclaration(
1547
+ name: string,
1548
+ members: readonly string[],
1549
+ nilable: ReadonlySet<string> = new Set(),
1550
+ ): string {
1551
+ const arm = (member: string) =>
1552
+ nilable.has(member) ? `NonNullable<typeof ${member}>` : `typeof ${member}`;
1553
+ return `type ${name} = ${members.map(arm).join(" | ")};`;
1073
1554
  }
1074
1555
 
1075
1556
  const DOC_CONSTANT_TOKEN =
@@ -1219,7 +1700,11 @@ export const HOMOGENEOUS_ARRAY_SLOTS: ReadonlyMap<string, string | readonly stri
1219
1700
  // (`tilemap.get_tiles` → `tiles[row][col]`).
1220
1701
  export type NestedMapping = { key: string; value: string };
1221
1702
 
1222
- export type TableSlotCuration =
1703
+ // A curation applies where the slot declares the prose `table` token, or a later
1704
+ // spelling a newer release retypes the same slot to without fixing it
1705
+ // (`retypedUpstream`, matched whole): 1.13.2's `table<hash, table<string|hash, any>>`
1706
+ // for `collectionfactory.create`'s properties.
1707
+ export type TableSlotCuration = (
1223
1708
  | { kind: "mapping"; key: string; value: string | readonly TableField[] | NestedMapping }
1224
1709
  | { kind: "array"; element: string | readonly string[] }
1225
1710
  | { kind: "object"; fields: readonly TableField[] }
@@ -1228,7 +1713,11 @@ export type TableSlotCuration =
1228
1713
  // keyed by an arbitrary name (constant/sampler/attribute). The parser recovers
1229
1714
  // the inner args fields; emit wraps them as `Record<string, { <fields> }>`,
1230
1715
  // restoring the keyed-by-name layer the flat object curation drops.
1231
- | { kind: "keyed-object" };
1716
+ | { kind: "keyed-object" }
1717
+ // A slot whose faithful shape no field list can carry, such as a union
1718
+ // discriminated on `type`. `ts` is emitted verbatim, already mapped.
1719
+ | { kind: "verbatim"; ts: string }
1720
+ ) & { readonly retypedUpstream?: readonly string[] };
1232
1721
 
1233
1722
  export const SOCKET_HANDLE_TOKENS = ["client", "master", "unconnected"] as const;
1234
1723
 
@@ -1237,8 +1726,10 @@ export const SOCKET_HANDLE_TOKENS = ["client", "master", "unconnected"] as const
1237
1726
  // b2d submodule curations below so a single shape is defined once.
1238
1727
  //
1239
1728
  // The shape table is a discriminated union of circle/edge/polygon/box/chain
1240
- // forms keyed by `type` (a b2d.shape.SHAPE_TYPE_* constant); every kind-specific
1241
- // field is optional because only one form's fields are present per value.
1729
+ // forms, plus the Box2D v3 capsule, keyed by `type` (a b2d.shape.SHAPE_TYPE_*
1730
+ // constant); every kind-specific field is optional because only one form's
1731
+ // fields are present per value. The capsule's `center1`/`center2` come from
1732
+ // script_box2d_shape_v3.cpp, which reads them; the ref-doc prose never names them.
1242
1733
  const B2D_SHAPE_TABLE_FIELDS: readonly TableField[] = [
1243
1734
  { name: "type", types: ["number"] },
1244
1735
  { name: "radius", types: ["number"], optional: true },
@@ -1254,7 +1745,56 @@ const B2D_SHAPE_TABLE_FIELDS: readonly TableField[] = [
1254
1745
  { name: "loop", types: ["boolean"], optional: true },
1255
1746
  { name: "prev_vertex", types: ["vector3"], optional: true },
1256
1747
  { name: "next_vertex", types: ["vector3"], optional: true },
1748
+ { name: "center1", types: ["vector3"], optional: true },
1749
+ { name: "center2", types: ["vector3"], optional: true },
1257
1750
  ];
1751
+ // The shape table a Box2D binding reads, one arm per `type`. CheckShapeDef
1752
+ // (v2/script_box2d_shape_v2.cpp, v3/script_box2d_shape_v3.cpp) raises without
1753
+ // `type` and without the fields that type reads with luaL_checknumber or
1754
+ // CheckVec2; the rest go through TryGet*Field. A polygon takes `hx`/`hy` or
1755
+ // `vertices`. `SHAPE_TYPE_BOX` is the polygon value and `SHAPE_TYPE_EDGE` the v3
1756
+ // segment value, so each shares its arm. Each backend raises "Unsupported shape
1757
+ // type" on the kinds it does not build: `create_fixture` and `fixture.set_shape`
1758
+ // are registered only by v2/script_box2d_*_v2.cpp and take B2D_V2_SHAPE_TS (no
1759
+ // capsule, and an edge typed `SHAPE_TYPE_EDGE` with optional ghost vertices);
1760
+ // `create_shape` and `shape.set_shape` are registered only by
1761
+ // v3/script_box2d_*_v3.cpp and take B2D_V3_SHAPE_TS (no chain, and a segment
1762
+ // that reads no ghost vertices). `world.cast_shape` and `world.overlap_shape`
1763
+ // are registered by both, so they keep the full union.
1764
+ // The box arm leads: a v2 fixture keeps its shape type, and the runtime probe
1765
+ // witnesses a union by its first arm against a box collision shape.
1766
+ const B2D_SHAPE = "typeof b2d.shape";
1767
+ const B2D_BOX_ARM = `{ type: ${B2D_SHAPE}.SHAPE_TYPE_POLYGON | ${B2D_SHAPE}.SHAPE_TYPE_BOX; hx: number; hy: number; center?: Vector3; angle?: number }`;
1768
+ const B2D_POLYGON_ARM = `{ type: ${B2D_SHAPE}.SHAPE_TYPE_POLYGON | ${B2D_SHAPE}.SHAPE_TYPE_BOX; vertices: Vector3[] }`;
1769
+ const B2D_CIRCLE_ARM = `{ type: ${B2D_SHAPE}.SHAPE_TYPE_CIRCLE; radius: number; center?: Vector3 }`;
1770
+ const B2D_CAPSULE_ARM = `{ type: ${B2D_SHAPE}.SHAPE_TYPE_CAPSULE; radius: number; center1: Vector3; center2: Vector3 }`;
1771
+ const B2D_EDGE_ARM = `{ type: ${B2D_SHAPE}.SHAPE_TYPE_EDGE | ${B2D_SHAPE}.SHAPE_TYPE_SEGMENT; v1: Vector3; v2: Vector3; v0?: Vector3; v3?: Vector3 }`;
1772
+ const B2D_V2_EDGE_ARM = `{ type: ${B2D_SHAPE}.SHAPE_TYPE_EDGE; v1: Vector3; v2: Vector3; v0?: Vector3; v3?: Vector3 }`;
1773
+ const B2D_V3_SEGMENT_ARM = `{ type: ${B2D_SHAPE}.SHAPE_TYPE_EDGE | ${B2D_SHAPE}.SHAPE_TYPE_SEGMENT; v1: Vector3; v2: Vector3 }`;
1774
+ const B2D_CHAIN_ARM = `{ type: ${B2D_SHAPE}.SHAPE_TYPE_CHAIN; vertices: Vector3[]; loop?: boolean; prev_vertex?: Vector3; next_vertex?: Vector3 }`;
1775
+ const B2D_SHAPE_DEFINITION_TS = [
1776
+ B2D_BOX_ARM,
1777
+ B2D_POLYGON_ARM,
1778
+ B2D_CIRCLE_ARM,
1779
+ B2D_CAPSULE_ARM,
1780
+ B2D_EDGE_ARM,
1781
+ B2D_CHAIN_ARM,
1782
+ ].join(" | ");
1783
+ const B2D_V2_SHAPE_TS = [
1784
+ B2D_BOX_ARM,
1785
+ B2D_POLYGON_ARM,
1786
+ B2D_CIRCLE_ARM,
1787
+ B2D_V2_EDGE_ARM,
1788
+ B2D_CHAIN_ARM,
1789
+ ].join(" | ");
1790
+ const B2D_V3_SHAPE_TS = [
1791
+ B2D_BOX_ARM,
1792
+ B2D_POLYGON_ARM,
1793
+ B2D_CIRCLE_ARM,
1794
+ B2D_CAPSULE_ARM,
1795
+ B2D_V3_SEGMENT_ARM,
1796
+ ].join(" | ");
1797
+ const B2D_SHAPE_DEFINITION: TableSlotCuration = { kind: "verbatim", ts: B2D_SHAPE_DEFINITION_TS };
1258
1798
  // A fixture info entry (index/type/sensor/density/friction/restitution/child_count).
1259
1799
  const B2D_FIXTURE_INFO_FIELDS: readonly TableField[] = [
1260
1800
  { name: "index", types: ["number"] },
@@ -1330,8 +1870,38 @@ const PHYSICS_SHAPE_TABLE_FIELDS: readonly TableField[] = [
1330
1870
  { name: "dimensions", types: ["vector3"], optional: true },
1331
1871
  { name: "height", types: ["number"], optional: true },
1332
1872
  ];
1333
-
1334
- export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new Map([
1873
+ // The same record as `physics.set_shape` takes it: script_physics.cpp:
1874
+ // Physics_SetShape reads `type` and then each field its kind needs with
1875
+ // luaL_checknumber or CheckVector3, so each kind is its own arm.
1876
+ const PHYSICS_SHAPE_DEFINITION_TS = [
1877
+ "{ type: typeof physics.SHAPE_TYPE_SPHERE; diameter: number }",
1878
+ "{ type: typeof physics.SHAPE_TYPE_BOX; dimensions: Vector3 }",
1879
+ "{ type: typeof physics.SHAPE_TYPE_CAPSULE; diameter: number; height: number }",
1880
+ ].join(" | ");
1881
+
1882
+ export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new Map<
1883
+ string,
1884
+ TableSlotCuration
1885
+ >([
1886
+ // gui_script.cpp:LuaGet and LuaSet read the `key` and `keys` options. On a
1887
+ // prose `table` slot TABLE_SLOT_FIELD_ADDITIONS supplies them; 1.13.2 types the
1888
+ // slots as an inline record and a struct that omit them.
1889
+ ...(
1890
+ [
1891
+ ["gui.get:param:options", "{ index?:integer }"],
1892
+ ["gui.set:param:options", "gui.set_options"],
1893
+ ] as const
1894
+ ).map(
1895
+ ([key, retyped]) =>
1896
+ [
1897
+ key,
1898
+ {
1899
+ kind: "verbatim",
1900
+ ts: "{ index?: number; key?: string | Hash; keys?: (Hash | string)[] }",
1901
+ retypedUpstream: [retyped],
1902
+ },
1903
+ ] as const,
1904
+ ),
1335
1905
  // buffer.create's `declaration` is "a table where each entry (table) describes
1336
1906
  // a stream", and upstream's example passes a list of stream records whose
1337
1907
  // `type` is a buffer.VALUE_TYPE_* constant.
@@ -1351,7 +1921,12 @@ export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new
1351
1921
  // the object-literal form this slot accepted as a bare `table`.
1352
1922
  [
1353
1923
  "collectionfactory.create:param:properties",
1354
- { kind: "mapping", key: "hash | string", value: "table" },
1924
+ {
1925
+ kind: "mapping",
1926
+ key: "hash | string",
1927
+ value: "table",
1928
+ retypedUpstream: ["table<hash, table<string|hash, any>>"],
1929
+ },
1355
1930
  ],
1356
1931
  ["collectionfactory.create:return:ids", { kind: "mapping", key: "hash", value: "hash" }],
1357
1932
  // font.get_info's `info` return is a `<dl>` with `path: hash` and a nested
@@ -1482,8 +2057,22 @@ export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new
1482
2057
  ],
1483
2058
  ["socket.select:param:recvt", { kind: "array", element: SOCKET_HANDLE_TOKENS }],
1484
2059
  ["socket.select:param:sendt", { kind: "array", element: SOCKET_HANDLE_TOKENS }],
1485
- ["socket.select:return:sockets_r", { kind: "array", element: SOCKET_HANDLE_TOKENS }],
1486
- ["socket.select:return:sockets_w", { kind: "array", element: SOCKET_HANDLE_TOKENS }],
2060
+ [
2061
+ "socket.select:return:sockets_r",
2062
+ {
2063
+ kind: "array",
2064
+ element: SOCKET_HANDLE_TOKENS,
2065
+ retypedUpstream: ["table<integer|socket_selectable, socket_selectable|integer>"],
2066
+ },
2067
+ ],
2068
+ [
2069
+ "socket.select:return:sockets_w",
2070
+ {
2071
+ kind: "array",
2072
+ element: SOCKET_HANDLE_TOKENS,
2073
+ retypedUpstream: ["table<integer|socket_selectable, socket_selectable|integer>"],
2074
+ },
2075
+ ],
1487
2076
  [
1488
2077
  "tilemap.get_tile_info:return:tile_info",
1489
2078
  {
@@ -1575,8 +2164,10 @@ export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new
1575
2164
  // `headers`; the sibling `options` table stays parser-recovered.
1576
2165
  ["http.request:param:headers", { kind: "mapping", key: "string", value: "string" }],
1577
2166
  // collectionproxy.get_resources returns "the resources, or an empty list", a
1578
- // homogeneous list of resource-path hashes (prose only, no field list).
1579
- ["collectionproxy.get_resources:return:resources", { kind: "array", element: "hash" }],
2167
+ // homogeneous list of resource paths (prose only, no field list), each a
2168
+ // hexadecimal string: script_collectionproxy.cpp:GetResourceHashCallback pushes
2169
+ // every entry with lua_pushlstring.
2170
+ ["collectionproxy.get_resources:return:resources", { kind: "array", element: "string" }],
1580
2171
  // render.predicate's `tags` is "table of tags ... can be of either hash or
1581
2172
  // string type", a homogeneous array whose element is the string|hash union.
1582
2173
  ["render.predicate:param:tags", { kind: "array", element: ["string", "hash"] }],
@@ -1589,6 +2180,7 @@ export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new
1589
2180
  [
1590
2181
  "render.clear:param:buffers",
1591
2182
  {
2183
+ retypedUpstream: ["table<graphics.BUFFER_TYPE, number|vector4>"],
1592
2184
  kind: "mapping",
1593
2185
  key: "graphics.BUFFER_TYPE_COLOR0_BIT | graphics.BUFFER_TYPE_DEPTH_BIT | graphics.BUFFER_TYPE_STENCIL_BIT",
1594
2186
  value: "number | vector4",
@@ -1780,14 +2372,63 @@ export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new
1780
2372
  [
1781
2373
  "b2d.body.create_fixture:param:definition",
1782
2374
  {
2375
+ retypedUpstream: ["b2d.fixture_definition"],
1783
2376
  kind: "object",
1784
2377
  fields: [
1785
- { name: "shape", types: ["table"], fields: [...B2D_SHAPE_TABLE_FIELDS] },
2378
+ { name: "shape", types: ["table"], tsType: B2D_V2_SHAPE_TS },
1786
2379
  { name: "friction", types: ["number"] },
1787
2380
  { name: "restitution", types: ["number"] },
1788
2381
  { name: "density", types: ["number"] },
1789
2382
  { name: "sensor", types: ["boolean"] },
2383
+ // v2/script_box2d_fixture_v2.cpp:CheckFixtureDef reads is_sensor when
2384
+ // sensor is absent.
2385
+ { name: "is_sensor", types: ["boolean"] },
2386
+ { name: "filter", types: ["table"], fields: [...B2D_FILTER_DATA_FIELDS] },
2387
+ ],
2388
+ },
2389
+ ],
2390
+ // The creation-time definitions the v3 bindings document in their C++ doc
2391
+ // comments rather than the ref-doc: Body_CreateChain
2392
+ // (v3/script_box2d_chain_v3.cpp), CheckShapeCreateDef
2393
+ // (v3/script_box2d_shape_v3.cpp) and World_Explode
2394
+ // (v3/script_box2d_world_v3.cpp).
2395
+ [
2396
+ "b2d.body.create_chain:param:definition",
2397
+ {
2398
+ kind: "object",
2399
+ fields: [
2400
+ { name: "vertices", types: ["table"], tsType: "Vector3[]" },
2401
+ { name: "loop", types: ["boolean"] },
2402
+ { name: "prev_vertex", types: ["vector3"] },
2403
+ { name: "next_vertex", types: ["vector3"] },
2404
+ { name: "friction", types: ["number"] },
2405
+ { name: "restitution", types: ["number"] },
2406
+ { name: "material", types: ["number"] },
1790
2407
  { name: "filter", types: ["table"], fields: [...B2D_FILTER_DATA_FIELDS] },
2408
+ { name: "enable_sensor_events", types: ["boolean"] },
2409
+ ],
2410
+ },
2411
+ ],
2412
+ // The shape table sits in `definition.shape` or is the definition itself, and
2413
+ // a filter, when given, needs all three members CheckFilterData reads.
2414
+ [
2415
+ "b2d.body.create_shape:param:definition",
2416
+ {
2417
+ retypedUpstream: ["b2d.shape_create_definition"],
2418
+ kind: "verbatim",
2419
+ ts: `({ shape: ${B2D_V3_SHAPE_TS} } | ${B2D_V3_SHAPE_TS}) & { density?: number; friction?: number; restitution?: number; material?: number; sensor?: boolean; is_sensor?: boolean; filter?: { category_bits: number; mask_bits: number; group_index: number } }`,
2420
+ },
2421
+ ],
2422
+ [
2423
+ "b2d.world.explode:param:definition",
2424
+ {
2425
+ kind: "object",
2426
+ fields: [
2427
+ { name: "position", types: ["vector3"] },
2428
+ { name: "radius", types: ["number"] },
2429
+ { name: "falloff", types: ["number"] },
2430
+ { name: "impulse_per_length", types: ["number"] },
2431
+ { name: "mask_bits", types: ["number"] },
1791
2432
  ],
1792
2433
  },
1793
2434
  ],
@@ -1825,10 +2466,16 @@ export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new
1825
2466
  ["b2d.fixture.get_filter_data:return:filter", { kind: "object", fields: B2D_FILTER_DATA_FIELDS }],
1826
2467
  ["b2d.fixture.set_filter_data:param:filter", { kind: "object", fields: B2D_FILTER_DATA_FIELDS }],
1827
2468
  ["b2d.fixture.get_shape:return:shape", { kind: "object", fields: B2D_SHAPE_TABLE_FIELDS }],
1828
- ["b2d.fixture.set_shape:param:shape", { kind: "object", fields: B2D_SHAPE_TABLE_FIELDS }],
2469
+ [
2470
+ "b2d.fixture.set_shape:param:shape",
2471
+ { kind: "verbatim", ts: B2D_V2_SHAPE_TS, retypedUpstream: ["b2d.shape.definition"] },
2472
+ ],
1829
2473
  // b2d.shape
1830
2474
  ["b2d.shape.get_shape:return:shape", { kind: "object", fields: B2D_SHAPE_TABLE_FIELDS }],
1831
- ["b2d.shape.set_shape:param:definition", { kind: "object", fields: B2D_SHAPE_TABLE_FIELDS }],
2475
+ [
2476
+ "b2d.shape.set_shape:param:definition",
2477
+ { kind: "verbatim", ts: B2D_V3_SHAPE_TS, retypedUpstream: ["b2d.shape.definition"] },
2478
+ ],
1832
2479
  ["b2d.shape.get_mass_data:return:data", { kind: "object", fields: B2D_MASS_DATA_FIELDS }],
1833
2480
  [
1834
2481
  "b2d.shape.get_sensor_overlaps:return:overlaps",
@@ -2011,7 +2658,10 @@ export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new
2011
2658
  ],
2012
2659
  ["b2d.world.overlap_aabb:return:hits", { kind: "array-object", fields: B2D_SHAPE_INFO_FIELDS }],
2013
2660
  ["b2d.world.overlap_aabb:return:stats", { kind: "object", fields: B2D_QUERY_STATS_FIELDS }],
2014
- ["b2d.world.overlap_shape:param:shape", { kind: "object", fields: B2D_SHAPE_TABLE_FIELDS }],
2661
+ [
2662
+ "b2d.world.overlap_shape:param:shape",
2663
+ { ...B2D_SHAPE_DEFINITION, retypedUpstream: ["b2d.shape.definition"] },
2664
+ ],
2015
2665
  ["b2d.world.overlap_shape:param:filter", { kind: "object", fields: B2D_QUERY_FILTER_FIELDS }],
2016
2666
  [
2017
2667
  "b2d.world.overlap_shape:return:fixtures",
@@ -2038,7 +2688,10 @@ export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new
2038
2688
  ],
2039
2689
  },
2040
2690
  ],
2041
- ["b2d.world.cast_shape:param:shape", { kind: "object", fields: B2D_SHAPE_TABLE_FIELDS }],
2691
+ [
2692
+ "b2d.world.cast_shape:param:shape",
2693
+ { ...B2D_SHAPE_DEFINITION, retypedUpstream: ["b2d.shape.definition"] },
2694
+ ],
2042
2695
  ["b2d.world.cast_shape:param:filter", { kind: "object", fields: B2D_QUERY_FILTER_FIELDS }],
2043
2696
  ["b2d.world.cast_shape:return:hits", { kind: "array-object", fields: B2D_CAST_HIT_FIELDS }],
2044
2697
  ["b2d.world.cast_shape:return:stats", { kind: "object", fields: B2D_QUERY_STATS_FIELDS }],
@@ -2047,14 +2700,15 @@ export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new
2047
2700
  ["b2d.world.collide_mover:param:capsule", { kind: "object", fields: B2D_CAPSULE_FIELDS }],
2048
2701
  ["b2d.world.collide_mover:param:filter", { kind: "object", fields: B2D_MOVER_FILTER_FIELDS }],
2049
2702
  ["physics.get_shape:return:table", { kind: "object", fields: PHYSICS_SHAPE_TABLE_FIELDS }],
2050
- ["physics.set_shape:param:table", { kind: "object", fields: PHYSICS_SHAPE_TABLE_FIELDS }],
2703
+ ["physics.set_shape:param:table", { kind: "verbatim", ts: PHYSICS_SHAPE_DEFINITION_TS }],
2051
2704
  ]);
2052
2705
 
2053
2706
  // Slot-keyed (`element:param:name`, mirroring TABLE_SLOT_CURATIONS) replacements
2054
2707
  // for a callback parameter's recovered signature, used where the generic
2055
2708
  // `recoverCallbackSignature` `unknown`-everywhere form leaves real engine type
2056
2709
  // information on the table. The value is the full emitted function type. Honest
2057
- // only: `window.set_listener` discriminates `event` by a known constant union and
2710
+ // only: `window.set_listener` discriminates `event` by the union of the
2711
+ // `WINDOW_EVENT_*` constants the surface's own `window` namespace declares, and
2058
2712
  // its `data` is a bare record (only resize carries fields — `isWindowEvent` is the
2059
2713
  // path to typed `data`), mirroring the `msg.post` (send, typed) / `isMessage`
2060
2714
  // (receive, narrow) split.
@@ -2071,7 +2725,8 @@ export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new
2071
2725
  export const CALLBACK_SIGNATURE_CURATIONS: ReadonlyMap<string, string> = new Map([
2072
2726
  [
2073
2727
  "window.set_listener:param:callback",
2074
- "(self: unknown, event: typeof WINDOW_EVENT_FOCUS_LOST | typeof WINDOW_EVENT_FOCUS_GAINED | typeof WINDOW_EVENT_RESIZED | typeof WINDOW_EVENT_ICONFIED | typeof WINDOW_EVENT_DEICONIFIED, data: Record<string | number, unknown>) => void",
2728
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: emitted verbatim as a TS template literal type
2729
+ "(self: unknown, event: (typeof window)[Extract<keyof typeof window, `WINDOW_EVENT_${string}`>], data: Record<string | number, unknown>) => void",
2075
2730
  ],
2076
2731
  [
2077
2732
  "http.request:param:callback",
@@ -2096,9 +2751,11 @@ export const CALLBACK_SIGNATURE_CURATIONS: ReadonlyMap<string, string> = new Map
2096
2751
  // hand-curated. Each geometry is the triangle data (the create-input form
2097
2752
  // get_atlas mirrors via its cross-ref), and `vertices`/`uvs`/`indices` are
2098
2753
  // brace-form number-lists, hence `numberList: true` (emitted `number[]`).
2099
- // Injected onto the already-parsed field list (every sibling stays
2100
- // parser-authoritative), so this is a nested-field curation, not a whole-slot
2101
- // object curation.
2754
+ // resource.create_atlas documents a geometry's `id`/`width`/`height`/pivot
2755
+ // members, so the three arrays join those: script_resource.cpp reads them from
2756
+ // each `geometries` entry. Injected onto the already-parsed field list (every
2757
+ // sibling stays parser-authoritative), so this is a nested-field curation, not a
2758
+ // whole-slot object curation.
2102
2759
  const ATLAS_GEOMETRY_MEMBERS: readonly TableField[] = [
2103
2760
  { name: "vertices", types: ["table"], numberList: true },
2104
2761
  { name: "uvs", types: ["table"], numberList: true },
@@ -2107,6 +2764,7 @@ const ATLAS_GEOMETRY_MEMBERS: readonly TableField[] = [
2107
2764
 
2108
2765
  export const NESTED_FIELD_CURATIONS: ReadonlyMap<string, readonly TableField[]> = new Map([
2109
2766
  ["resource.set_atlas:param:table:geometries", ATLAS_GEOMETRY_MEMBERS],
2767
+ ["resource.create_atlas:param:table:geometries", ATLAS_GEOMETRY_MEMBERS],
2110
2768
  ["resource.get_atlas:return:data:geometries", ATLAS_GEOMETRY_MEMBERS],
2111
2769
  ]);
2112
2770
 
@@ -2130,6 +2788,9 @@ export const NESTED_FIELD_CURATIONS: ReadonlyMap<string, readonly TableField[]>
2130
2788
  // `render.set_render_target`'s `options.transient` is documented as `table`
2131
2789
  // with the prose "a list of buffer types", and upstream's example passes
2132
2790
  // graphics.BUFFER_TYPE_* constants.
2791
+ //
2792
+ // `resource.create_sound_data`'s `options.data` is documented as `string`, while
2793
+ // script_resource.cpp:CreateSoundData reads it with CheckBufferOrString.
2133
2794
  const CONSTANT_VALUE_TS = "Vector4 | Vector3 | Matrix4 | number | (Vector4 | Matrix4)[]";
2134
2795
  const ATTRIBUTE_VALUE_TS = "Vector4 | Vector3 | Matrix4 | number | number[]";
2135
2796
  export const TABLE_FIELD_TYPE_OVERRIDES: ReadonlyMap<string, string> = new Map([
@@ -2142,6 +2803,23 @@ export const TABLE_FIELD_TYPE_OVERRIDES: ReadonlyMap<string, string> = new Map([
2142
2803
  ["resource.get_atlas:return:data:animations[].playback", "go.Playback"],
2143
2804
  ["gui.set:param:options:key", "string | Hash"],
2144
2805
  ["render.set_render_target:param:options:transient", "graphics.BufferType[]"],
2806
+ ["resource.create_sound_data:param:options:data", 'string | Opaque<"buffer">'],
2807
+ // The same two slots where 1.13.2 types the option bag as a STRUCT, whose
2808
+ // members still carry the narrower upstream types.
2809
+ ["gui.set_options:member:key", "string | Hash"],
2810
+ ["resource.sound_data_options:member:data", 'string | Opaque<"buffer">'],
2811
+ // Bitmasks typed as their flag enum: the create_texture example passes
2812
+ // TEXTURE_USAGE_FLAG_STORAGE + TEXTURE_USAGE_FLAG_SAMPLE, a sum no single member
2813
+ // names, and a reported value combines them the same way.
2814
+ ...(
2815
+ [
2816
+ "resource.texture_creation_params",
2817
+ "resource.texture_info",
2818
+ "resource.render_target_attachment_info",
2819
+ "material.texture_info",
2820
+ ] as const
2821
+ ).map((struct) => [`${struct}:member:flags`, "graphics.TEXTURE_USAGE_FLAG | number"] as const),
2822
+ ["render.render_target_buffer_params:member:flags", "render.RENDER_TARGET_FLAG | number"],
2145
2823
  ]);
2146
2824
 
2147
2825
  // A field the engine accepts on an option bag whose `<dl>` upstream otherwise
@@ -2150,27 +2828,56 @@ export const TABLE_FIELD_TYPE_OVERRIDES: ReadonlyMap<string, string> = new Map([
2150
2828
  // upstream omits, leaving the documented fields parser-authoritative so an
2151
2829
  // upstream edit to any of them lands without a hand-maintained restatement.
2152
2830
  //
2153
- // `evidence` pins where upstream itself shows the field. applyFieldAdditions
2154
- // throws once the parse recovers it, so upstream documenting the field reds
2155
- // here and the entry is deleted rather than re-pinned.
2831
+ // `evidence` pins where upstream itself shows each field, or the engine binding
2832
+ // that reads it. applyFieldAdditions throws once the parse recovers one, so
2833
+ // upstream documenting a field reds here and the entry is deleted rather than
2834
+ // re-pinned.
2156
2835
  //
2157
2836
  // Keyed `<element>:<kind>:<slot>`, the TABLE_SLOT_CURATIONS shape.
2158
2837
  export interface TableFieldAddition {
2159
- readonly field: TableField;
2838
+ readonly fields: readonly TableField[];
2160
2839
  readonly evidence: readonly string[];
2161
2840
  }
2162
2841
 
2163
- export const TABLE_SLOT_FIELD_ADDITIONS: ReadonlyMap<string, TableFieldAddition> = new Map([
2842
+ const PROPERTY_OPTION_KEYS: TableField = {
2843
+ name: "keys",
2844
+ types: ["table"],
2845
+ optional: true,
2846
+ tsType: "(Hash | string)[]",
2847
+ };
2848
+
2849
+ export const TABLE_SLOT_FIELD_ADDITIONS: ReadonlyMap<string, TableFieldAddition> = new Map<
2850
+ string,
2851
+ TableFieldAddition
2852
+ >([
2164
2853
  [
2165
2854
  "resource.create_texture:param:table",
2166
2855
  {
2167
- field: { name: "page_count", types: ["number"], optional: true },
2856
+ fields: [{ name: "page_count", types: ["number"], optional: true }],
2168
2857
  evidence: [
2169
2858
  "the array-texture example on this element creates with `page_count = 5`",
2170
2859
  "resource.get_texture_info documents `page_count` as the texture array's page count",
2171
2860
  ],
2172
2861
  },
2173
2862
  ],
2863
+ [
2864
+ "gui.get:param:options",
2865
+ {
2866
+ fields: [{ name: "key", types: ["string", "hash"], optional: true }, PROPERTY_OPTION_KEYS],
2867
+ evidence: [
2868
+ "gui_script.cpp:LuaGet reads its options through dmGameObject::LuaToPropertyOptions, which reads index, key and keys",
2869
+ ],
2870
+ },
2871
+ ],
2872
+ [
2873
+ "gui.set:param:options",
2874
+ {
2875
+ fields: [PROPERTY_OPTION_KEYS],
2876
+ evidence: [
2877
+ "gui_script.cpp:LuaSet reads its options through dmGameObject::LuaToPropertyOptions, which reads index, key and keys",
2878
+ ],
2879
+ },
2880
+ ],
2174
2881
  ]);
2175
2882
 
2176
2883
  // A return-slot field upstream documents as platform-gated or nil-valued while
@@ -2262,6 +2969,247 @@ export function applyFieldOptionalityCorrections(
2262
2969
  return corrected;
2263
2970
  }
2264
2971
 
2972
+ // A param-side table field the engine binding refuses to go without. A `<dl>`
2973
+ // carries no per-field optionality, so every field of an input option bag is
2974
+ // emitted `?`; an entry here keeps one field required. Keyed
2975
+ // `<element>:param:<slot>:<field>`, the TABLE_FIELD_TYPE_OVERRIDES shape, with
2976
+ // `<field>.<member>` or `<field>[].<member>` one level down. A `STRUCT` member
2977
+ // is keyed `<struct>:member:<field>`, for a target whose slot names that struct
2978
+ // (`field-correction-coverage.ts` pairs the two). The value is the evidence: the
2979
+ // C++ binding, `<file>:<cFunction>`, and how it reads the field.
2980
+ const CHECK_SHAPE_DEF = "CheckShapeDef reads the shape table with luaL_checkinteger(type)";
2981
+ const V2_FILTER_DATA =
2982
+ "v2/script_box2d_fixture_v2.cpp:CheckFilterData reads each with luaL_checkinteger";
2983
+ const TEXTURE_PARAMS =
2984
+ "script_resource.cpp:CheckCreateTextureResourceParams reads CheckTableInteger";
2985
+ const ATLAS_ARGUMENTS =
2986
+ "script_resource.cpp:CheckAtlasArguments raises unless the atlas has at least one geometry and one animation";
2987
+
2988
+ const filterDataFields = (slot: string, evidence: string) =>
2989
+ (["category_bits", "mask_bits", "group_index"] as const).map(
2990
+ (field) => [`${slot}:filter.${field}`, evidence] as const,
2991
+ );
2992
+
2993
+ // A `STRUCT` member upstream marks required while the engine binding reads it
2994
+ // only when present, keyed `<struct>:member:<field>` like REQUIRED_FIELD_CORRECTIONS
2995
+ // and valued with the same binding evidence.
2996
+ export const OPTIONAL_MEMBER_CORRECTIONS: ReadonlyMap<string, string> = new Map([
2997
+ [
2998
+ "sound.stop_properties:member:play_id",
2999
+ "script_sound.cpp:Sound_Stop reads play_id with luaL_checknumber only when it is not nil",
3000
+ ],
3001
+ ]);
3002
+
3003
+ export const REQUIRED_FIELD_CORRECTIONS: ReadonlyMap<string, string> = new Map<string, string>([
3004
+ ...(["mass", "center", "inertia"] as const).map(
3005
+ (field) =>
3006
+ [
3007
+ `b2d.body.set_mass_data:param:data:${field}`,
3008
+ "script_box2d_body_v2/v3.cpp:CheckMassData reads mass and inertia with luaL_checknumber and center with CheckVec2",
3009
+ ] as const,
3010
+ ),
3011
+ ...(["category_bits", "mask_bits", "group_index"] as const).map(
3012
+ (field) => [`b2d.fixture.set_filter_data:param:filter:${field}`, V2_FILTER_DATA] as const,
3013
+ ),
3014
+ [
3015
+ "b2d.body.create_fixture:param:definition:shape",
3016
+ `v2/script_box2d_fixture_v2.cpp:CheckFixtureDef passes definition.shape to ${CHECK_SHAPE_DEF}`,
3017
+ ],
3018
+ ...filterDataFields("b2d.body.create_fixture:param:definition", V2_FILTER_DATA),
3019
+ [
3020
+ "b2d.body.create_chain:param:definition:vertices",
3021
+ "v3/script_box2d_chain_v3.cpp:Body_CreateChain reads vertices with CheckVerticesTable, at least 2 (4 for a loop)",
3022
+ ],
3023
+ ...(["cast_mover", "collide_mover"] as const).flatMap((name) =>
3024
+ (["center1", "center2", "radius"] as const).map(
3025
+ (field) =>
3026
+ [
3027
+ `b2d.world.${name}:param:capsule:${field}`,
3028
+ "v3/script_box2d_world_v3.cpp:CheckCapsule reads center1 and center2 with CheckVec2 and radius with luaL_checknumber",
3029
+ ] as const,
3030
+ ),
3031
+ ),
3032
+ ...(["lower", "upper"] as const).map(
3033
+ (field) =>
3034
+ [
3035
+ `b2d.world.overlap_aabb:param:aabb:${field}`,
3036
+ "script_box2d_world_v2/v3.cpp:CheckAABB reads lower and upper with CheckVec2",
3037
+ ] as const,
3038
+ ),
3039
+ ...(["position", "radius", "falloff", "impulse_per_length"] as const).map(
3040
+ (field) =>
3041
+ [
3042
+ `b2d.world.explode:param:definition:${field}`,
3043
+ "v3/script_box2d_world_v3.cpp:World_Explode reads position with CheckVec2 and the rest with luaL_checknumber",
3044
+ ] as const,
3045
+ ),
3046
+ [
3047
+ "buffer.create:param:declaration:type",
3048
+ "script_buffer.cpp:ParseStreamDeclaration raises on a stream with no value type",
3049
+ ],
3050
+ ...(["create_texture", "create_texture_async"] as const).flatMap((name) =>
3051
+ (["type", "format", "width", "height"] as const).map(
3052
+ (field) => [`resource.${name}:param:table:${field}`, TEXTURE_PARAMS] as const,
3053
+ ),
3054
+ ),
3055
+ ...(["type", "format", "width", "height"] as const).map(
3056
+ (field) =>
3057
+ [
3058
+ `resource.set_texture:param:table:${field}`,
3059
+ "script_resource.cpp:SetTexture reads CheckTableInteger",
3060
+ ] as const,
3061
+ ),
3062
+ ...(["create_atlas", "set_atlas"] as const).flatMap((name) => [
3063
+ [
3064
+ `resource.${name}:param:table:texture`,
3065
+ "script_resource.cpp:CheckTextureResource reads the texture with CheckHashOrString",
3066
+ ] as const,
3067
+ [`resource.${name}:param:table:geometries`, ATLAS_ARGUMENTS] as const,
3068
+ [`resource.${name}:param:table:animations`, ATLAS_ARGUMENTS] as const,
3069
+ ...(["vertices", "uvs", "indices"] as const).map(
3070
+ (field) =>
3071
+ [
3072
+ `resource.${name}:param:table:geometries[].${field}`,
3073
+ "script_resource.cpp:CheckAtlasArguments checks each geometry stream with luaL_checktype(LUA_TTABLE)",
3074
+ ] as const,
3075
+ ),
3076
+ ...(["id", "width", "height"] as const).map(
3077
+ (field) =>
3078
+ [
3079
+ `resource.${name}:param:table:animations[].${field}`,
3080
+ "script_resource.cpp:CheckAtlasArguments raises on an animation with no id, width or height",
3081
+ ] as const,
3082
+ ),
3083
+ ]),
3084
+ [
3085
+ "resource.create_buffer:param:table:buffer",
3086
+ "script_resource.cpp:CreateBuffer reads the buffer with dmScript::CheckBuffer",
3087
+ ],
3088
+ [
3089
+ "resource.create_sound_data:param:options:data",
3090
+ "script_resource.cpp:CreateSoundData reads the data with CheckBufferOrString",
3091
+ ],
3092
+ [
3093
+ "resource.sound_data_options:member:data",
3094
+ "script_resource.cpp:CreateSoundData reads the data with CheckBufferOrString",
3095
+ ],
3096
+ ]);
3097
+
3098
+ // Mark the fields REQUIRED_FIELD_CORRECTIONS names for this slot required,
3099
+ // leaving every other field and every type untouched. An entry keyed to this
3100
+ // slot that names no field throws, so a renamed field cannot leave a stale
3101
+ // correction looking applied. Returns a new array; never mutates its input.
3102
+ export function applyRequiredFieldCorrections(
3103
+ elementName: string,
3104
+ slotKind: "param" | "return" | undefined,
3105
+ slotName: string | undefined,
3106
+ fields: readonly TableField[],
3107
+ ): TableField[] {
3108
+ if (slotKind !== "param" || slotName === undefined) return [...fields];
3109
+ const prefix = `${tableSlotKey(elementName, slotKind, slotName)}:`;
3110
+ const applied = new Set<string>();
3111
+ const mark = (field: TableField, path: string): TableField => {
3112
+ if (!REQUIRED_FIELD_CORRECTIONS.has(`${prefix}${path}`)) return field;
3113
+ applied.add(path);
3114
+ return { ...field, required: true };
3115
+ };
3116
+ const corrected = fields.map((field) => {
3117
+ const own = mark(field, field.name);
3118
+ if (own.fields === undefined) return own;
3119
+ const parent = `${field.name}${field.isList === true ? "[]" : ""}`;
3120
+ return { ...own, fields: own.fields.map((child) => mark(child, `${parent}.${child.name}`)) };
3121
+ });
3122
+ const stale = [...REQUIRED_FIELD_CORRECTIONS.keys()].filter(
3123
+ (key) => key.startsWith(prefix) && !applied.has(key.slice(prefix.length)),
3124
+ );
3125
+ if (stale.length > 0) {
3126
+ throw new Error(
3127
+ `required-field correction names no recovered field: ${stale.sort().join(", ")}`,
3128
+ );
3129
+ }
3130
+ return corrected;
3131
+ }
3132
+
3133
+ // A param-side list field the engine binding refuses empty. Every entry emits
3134
+ // the list as `[E, ...E[]]`; `replaces` drops members the binding reads only as
3135
+ // one of several alternatives, and `alternatives` restores them as a union the
3136
+ // element is intersected with. Keyed `<element>:param:<slot>:<field>` or
3137
+ // `<struct>:member:<field>`, the REQUIRED_FIELD_CORRECTIONS shapes.
3138
+ export interface ListFieldCorrection {
3139
+ readonly evidence: string;
3140
+ readonly replaces?: readonly string[];
3141
+ readonly alternatives?: string;
3142
+ }
3143
+
3144
+ const ATLAS_FRAMES =
3145
+ "script_resource.cpp:CheckAtlasArguments reads a non-empty frames table, else both frame_start and frame_end with CheckFieldValue<int>";
3146
+
3147
+ const ATLAS_GEOMETRIES: ListFieldCorrection = { evidence: ATLAS_ARGUMENTS };
3148
+ const ATLAS_ANIMATIONS: ListFieldCorrection = {
3149
+ evidence: `${ATLAS_ARGUMENTS}; ${ATLAS_FRAMES}`,
3150
+ replaces: ["frame_start", "frame_end"],
3151
+ alternatives: "{ frames: [number, ...number[]] } | { frame_start: number; frame_end: number }",
3152
+ };
3153
+
3154
+ export const LIST_FIELD_CORRECTIONS: ReadonlyMap<string, ListFieldCorrection> = new Map<
3155
+ string,
3156
+ ListFieldCorrection
3157
+ >([
3158
+ ...(["create_atlas", "set_atlas"] as const).flatMap((name) => [
3159
+ [`resource.${name}:param:table:geometries`, ATLAS_GEOMETRIES] as const,
3160
+ [`resource.${name}:param:table:animations`, ATLAS_ANIMATIONS] as const,
3161
+ ]),
3162
+ ["resource.atlas:member:geometries", ATLAS_GEOMETRIES],
3163
+ ["resource.atlas:member:animations", ATLAS_ANIMATIONS],
3164
+ ]);
3165
+
3166
+ // Apply LIST_FIELD_CORRECTIONS to this slot's fields. An entry keyed to this
3167
+ // slot that names no field, names a field that is not a list, or replaces a
3168
+ // member the field does not carry throws, so a renamed field or member cannot
3169
+ // leave a stale correction looking applied. Returns a new array; never mutates
3170
+ // its input.
3171
+ export function applyListFieldCorrections(
3172
+ elementName: string,
3173
+ slotKind: "param" | "return" | undefined,
3174
+ slotName: string | undefined,
3175
+ fields: readonly TableField[],
3176
+ ): TableField[] {
3177
+ if (slotKind !== "param" || slotName === undefined) return [...fields];
3178
+ const prefix = `${tableSlotKey(elementName, slotKind, slotName)}:`;
3179
+ const applied = new Set<string>();
3180
+ const corrected = fields.map((field): TableField => {
3181
+ const key = `${prefix}${field.name}`;
3182
+ const correction = LIST_FIELD_CORRECTIONS.get(key);
3183
+ if (correction === undefined) return field;
3184
+ applied.add(field.name);
3185
+ if (field.isList !== true) {
3186
+ throw new Error(`list-field correction names a field that is not a list: ${key}`);
3187
+ }
3188
+ const replaces = correction.replaces ?? [];
3189
+ const missing = replaces.filter(
3190
+ (name) => !(field.fields ?? []).some((member) => member.name === name),
3191
+ );
3192
+ if (missing.length > 0) {
3193
+ throw new Error(`list-field correction ${key} replaces no member: ${missing.join(", ")}`);
3194
+ }
3195
+ return {
3196
+ ...field,
3197
+ nonEmpty: true,
3198
+ ...(field.fields === undefined
3199
+ ? {}
3200
+ : { fields: field.fields.filter((member) => !replaces.includes(member.name)) }),
3201
+ ...(correction.alternatives === undefined ? {} : { alternatives: correction.alternatives }),
3202
+ };
3203
+ });
3204
+ const stale = [...LIST_FIELD_CORRECTIONS.keys()].filter(
3205
+ (key) => key.startsWith(prefix) && !applied.has(key.slice(prefix.length)),
3206
+ );
3207
+ if (stale.length > 0) {
3208
+ throw new Error(`list-field correction names no recovered field: ${stale.sort().join(", ")}`);
3209
+ }
3210
+ return corrected;
3211
+ }
3212
+
2265
3213
  // Append the member TABLE_SLOT_FIELD_ADDITIONS supplies for this slot, after
2266
3214
  // every field the parser recovered. Throws on an addition whose field the parse
2267
3215
  // already produced: upstream has documented it, and the entry is stale.
@@ -2276,10 +3224,12 @@ export function applyFieldAdditions(
2276
3224
  const key = tableSlotKey(elementName, slotKind, slotName);
2277
3225
  const addition = TABLE_SLOT_FIELD_ADDITIONS.get(key);
2278
3226
  if (addition === undefined) return [...fields];
2279
- if (fields.some((field) => field.name === addition.field.name)) {
2280
- throw new Error(`field addition names a recovered field: ${key}:${addition.field.name}`);
3227
+ for (const added of addition.fields) {
3228
+ if (fields.some((field) => field.name === added.name)) {
3229
+ throw new Error(`field addition names a recovered field: ${key}:${added.name}`);
3230
+ }
2281
3231
  }
2282
- return [...fields, addition.field];
3232
+ return [...fields, ...addition.fields];
2283
3233
  }
2284
3234
 
2285
3235
  // Pin a parser-recovered field's TS type from TABLE_FIELD_TYPE_OVERRIDES,
@@ -2448,6 +3398,14 @@ export interface TableField {
2448
3398
  isList?: boolean;
2449
3399
  numberList?: boolean;
2450
3400
  optional?: boolean;
3401
+ // Set only by REQUIRED_FIELD_CORRECTIONS: the engine raises without the field,
3402
+ // so it keeps no `?` even on a param-side option bag.
3403
+ required?: boolean;
3404
+ // Set only by LIST_FIELD_CORRECTIONS: the engine raises on an empty list, so
3405
+ // the list emits as `[E, ...E[]]`, and its element is intersected with the
3406
+ // alternative field sets the binding accepts.
3407
+ nonEmpty?: boolean;
3408
+ alternatives?: string;
2451
3409
  // A hand-curated field whose faithful TS shape is not a plain token union
2452
3410
  // (e.g. a nested vector array, or a documented value union with an array
2453
3411
  // branch): inlineTableType emits this verbatim and the audit treats it as
@@ -2639,27 +3597,349 @@ function groupFlattenedTableFields(fields: readonly TableField[]): TableField[]
2639
3597
  // A handle the type map widens (`constant_buffer` takes named constants) keeps
2640
3598
  // that shape, so annotating with the alias loses nothing the handle-returning
2641
3599
  // function yields.
2642
- function typedefAlias(name: string, mapType: (token: string) => string): string {
2643
- const brand = `Opaque<"${name}">`;
2644
- const mapped = mapType(name);
2645
- return `type ${name} = ${mapped.startsWith(brand) ? mapped : brand};`;
3600
+ //
3601
+ // An engine typedef that names its aliased type: a `userdata`/`number` handle the
3602
+ // type map resolves (`vector`, `socket_client`) aliases that mapping, an
3603
+ // unresolved one keeps the brand, and any other aliased type (a union, literals, a
3604
+ // record) aliases the type itself.
3605
+ function typedefAlias(
3606
+ typedef: ApiTypedef,
3607
+ local: string,
3608
+ rawMapType: (token: string) => string,
3609
+ mapType: DefoldTypeMapper,
3610
+ ): string {
3611
+ const brand = `Opaque<"${typedef.name}">`;
3612
+ const aliasOf = typedef.aliasOf ?? [];
3613
+ if (aliasOf.length > 0 && !isHandleAlias(aliasOf)) {
3614
+ return `type ${local} = ${unionFromTokens(aliasOf, mapType)};`;
3615
+ }
3616
+ const mapped = rawMapType(typedef.name);
3617
+ if (mapped.startsWith(brand)) return `type ${local} = ${mapped};`;
3618
+ const resolved = aliasOf.length > 0 && mapped !== "unknown" && mapped !== local;
3619
+ return `type ${local} = ${resolved ? mapped : brand};`;
3620
+ }
3621
+
3622
+ function isHandleAlias(aliasOf: readonly string[]): boolean {
3623
+ return aliasOf.length === 1 && (aliasOf[0] === "userdata" || aliasOf[0] === "number");
2646
3624
  }
2647
3625
 
2648
- export function emitDeclarations(module: ApiModule, options?: EmitOptions): string {
3626
+ // A `STRUCT` or aliasing `TYPEDEF` placed at its local path (`shape_data`,
3627
+ // `on_input.action`) under the module that emits it.
3628
+ interface TypeDeclaration {
3629
+ name: string;
3630
+ path: string;
3631
+ // The declaration's lines at `indent`, its keyword line led by `decl`.
3632
+ lines: (indent: string, decl: string) => string[];
3633
+ signature: string;
3634
+ }
3635
+
3636
+ // A typedef with no aliased type keeps the flat-name rule it always had; one that
3637
+ // names its aliased type, and every struct, sits at its name less the module
3638
+ // prefix, nested when that local is dotted. Typedefs come before structs, each in
3639
+ // document order.
3640
+ function collectTypeDeclarations(
3641
+ module: ApiModule,
3642
+ skip: ReadonlySet<string>,
3643
+ rawMapType: (token: string) => string,
3644
+ mapType: DefoldTypeMapper,
3645
+ ): TypeDeclaration[] {
2649
3646
  const prefix = `${module.namespace}.`;
3647
+ const out: TypeDeclaration[] = [];
3648
+ const localOf = (name: string): string | null => {
3649
+ const local = stripPrefix(name, prefix);
3650
+ return local.split(".").every((segment) => TS_IDENTIFIER.test(segment)) ? local : null;
3651
+ };
3652
+ for (const t of module.typedefs) {
3653
+ const local =
3654
+ t.aliasOf === undefined ? (TS_IDENTIFIER.test(t.name) ? t.name : null) : localOf(t.name);
3655
+ if (local === null || skip.has(local)) continue;
3656
+ const name = local.slice(local.lastIndexOf(".") + 1);
3657
+ const signature = typedefAlias(t, name, rawMapType, mapType);
3658
+ out.push({
3659
+ name,
3660
+ path: local,
3661
+ lines: (indent, decl) => [`${indent}${decl}${signature}`],
3662
+ signature,
3663
+ });
3664
+ }
3665
+ for (const struct of module.structs ?? []) {
3666
+ const local = localOf(struct.name);
3667
+ if (local === null || skip.has(local)) continue;
3668
+ out.push(structDeclaration(struct, local, mapType, module.structs ?? []));
3669
+ }
3670
+ return out;
3671
+ }
3672
+
3673
+ export function memberCorrectionKey(struct: string, field: string): string {
3674
+ return `${struct}:member:${field}`;
3675
+ }
2650
3676
 
2651
- const constantFqns = new Set(module.constants.map((c) => c.name));
3677
+ // A member-keyed LIST_FIELD_CORRECTIONS entry over a `T[]` member: `T` with the
3678
+ // replaced members omitted, rendered by the prose-field list renderer.
3679
+ function correctedListMemberType(
3680
+ struct: ApiStruct,
3681
+ member: ApiStructMember,
3682
+ correction: ListFieldCorrection,
3683
+ structs: readonly ApiStruct[],
3684
+ mapType: DefoldTypeMapper,
3685
+ ): string {
3686
+ const key = memberCorrectionKey(struct.name, member.name);
3687
+ const element = member.type.endsWith("[]") ? member.type.slice(0, -2) : undefined;
3688
+ if (element === undefined || element.length === 0) {
3689
+ throw new Error(`list-field correction names a member that is not a list: ${key}`);
3690
+ }
3691
+ const replaces = correction.replaces ?? [];
3692
+ const elementStruct = structs.find((candidate) => candidate.name === element);
3693
+ const missing = replaces.filter(
3694
+ (name) => !(elementStruct?.members ?? []).some((candidate) => candidate.name === name),
3695
+ );
3696
+ if (missing.length > 0) {
3697
+ throw new Error(`list-field correction ${key} replaces no member: ${missing.join(", ")}`);
3698
+ }
3699
+ const mapped = mapType(element);
3700
+ const object =
3701
+ replaces.length === 0
3702
+ ? mapped
3703
+ : `Omit<${mapped}, ${replaces.map((name) => JSON.stringify(name)).join(" | ")}>`;
3704
+ return nestedFieldType(
3705
+ {
3706
+ name: member.name,
3707
+ types: [],
3708
+ isList: true,
3709
+ nonEmpty: true,
3710
+ ...(correction.alternatives === undefined ? {} : { alternatives: correction.alternatives }),
3711
+ },
3712
+ object,
3713
+ );
3714
+ }
3715
+
3716
+ function structDeclaration(
3717
+ struct: ApiStruct,
3718
+ path: string,
3719
+ mapType: DefoldTypeMapper,
3720
+ structs: readonly ApiStruct[],
3721
+ ): TypeDeclaration {
3722
+ const name = path.slice(path.lastIndexOf(".") + 1);
3723
+ const memberPrefix = memberCorrectionKey(struct.name, "");
3724
+ const declared = new Set(struct.members.map((member) => member.name));
3725
+ const stale = [
3726
+ ...REQUIRED_FIELD_CORRECTIONS.keys(),
3727
+ ...LIST_FIELD_CORRECTIONS.keys(),
3728
+ ...TABLE_FIELD_TYPE_OVERRIDES.keys(),
3729
+ ...OPTIONAL_MEMBER_CORRECTIONS.keys(),
3730
+ ].filter((key) => key.startsWith(memberPrefix) && !declared.has(key.slice(memberPrefix.length)));
3731
+ if (stale.length > 0) {
3732
+ throw new Error(`member-keyed field correction names no member: ${stale.sort().join(", ")}`);
3733
+ }
3734
+ const members = struct.members.map((member) => {
3735
+ const key = memberCorrectionKey(struct.name, member.name);
3736
+ const list = LIST_FIELD_CORRECTIONS.get(key);
3737
+ const retyped = TABLE_FIELD_TYPE_OVERRIDES.get(key);
3738
+ const optional =
3739
+ (member.isOptional || OPTIONAL_MEMBER_CORRECTIONS.has(key)) &&
3740
+ !REQUIRED_FIELD_CORRECTIONS.has(key);
3741
+ const type =
3742
+ list !== undefined
3743
+ ? correctedListMemberType(struct, member, list, structs, mapType)
3744
+ : retyped !== undefined
3745
+ ? retyped
3746
+ : member.type === ""
3747
+ ? "unknown"
3748
+ : mapType(member.type);
3749
+ return {
3750
+ doc: member.doc,
3751
+ line: `${TS_IDENTIFIER.test(member.name) ? member.name : JSON.stringify(member.name)}${optional ? "?" : ""}: ${type};`,
3752
+ };
3753
+ });
3754
+ return {
3755
+ name,
3756
+ path,
3757
+ lines: (indent, decl) => {
3758
+ const memberIndent = `${indent}${INDENT}`;
3759
+ return [
3760
+ ...summaryDocLines(struct.brief, struct.description, indent),
3761
+ `${indent}${decl}interface ${name} {`,
3762
+ ...members.flatMap((member) => [
3763
+ ...summaryDocLines(member.doc, "", memberIndent),
3764
+ `${memberIndent}${member.line}`,
3765
+ ]),
3766
+ `${indent}}`,
3767
+ ];
3768
+ },
3769
+ signature: `interface ${name} { ${members.map((member) => `${member.line} `).join("")}}`,
3770
+ };
3771
+ }
3772
+
3773
+ // Nested declarations join the group at their path, which is created when no
3774
+ // member lives there; the flat ones are returned in name order.
3775
+ function placeTypeDeclarations(
3776
+ declarations: readonly TypeDeclaration[],
3777
+ nestedRoot: Map<string, NestedGroup>,
3778
+ ): TypeDeclaration[] {
3779
+ const byName = (a: TypeDeclaration, b: TypeDeclaration) => a.name.localeCompare(b.name);
3780
+ const topLevel: TypeDeclaration[] = [];
3781
+ for (const declaration of declarations) {
3782
+ if (declaration.path === declaration.name) {
3783
+ topLevel.push(declaration);
3784
+ continue;
3785
+ }
3786
+ const group = nestedGroupAt(nestedRoot, declaration.path.split(".").slice(0, -1));
3787
+ group.typeDeclarations.push(declaration);
3788
+ group.typeDeclarations.sort(byName);
3789
+ }
3790
+ return topLevel.sort(byName);
3791
+ }
3792
+
3793
+ // The mappers both emit surfaces read. `mapType` is the engine vocabulary with
3794
+ // the surface's leaves, the module's own declared types, those `declaredTypes`
3795
+ // names from other modules, and constant tokens branded. `rawMapType`, read only
3796
+ // for a typedef's own name, leaves the declared paths out: a handle typedef
3797
+ // (`editor.schema`) would otherwise map to its own path and alias itself.
3798
+ function moduleTypeMappers(
3799
+ module: ApiModule,
3800
+ constantsByFqn: ReadonlyMap<string, ApiConstant>,
3801
+ options: EmitOptions | undefined,
3802
+ ): { rawMapType: (token: string) => string; mapType: DefoldTypeMapper } {
3803
+ const leaves = options?.typeLeaves;
3804
+ const rawMapType = options?.mapType ?? mapTypeWithVocabulary({ leaves });
3805
+ const baseMapType =
3806
+ options?.mapType ??
3807
+ mapTypeWithVocabulary({
3808
+ leaves,
3809
+ declared: new Map([...declaredTypePaths([module]), ...(options?.declaredTypes ?? [])]),
3810
+ });
3811
+ const knownConstantFqns = options?.knownConstantFqns;
3812
+ const mapType: DefoldTypeMapper = (token, position) =>
3813
+ constantsByFqn.has(token) || knownConstantFqns?.has(token)
3814
+ ? brandType(token, constantsByFqn.get(token))
3815
+ : baseMapType(token, position);
3816
+ return { rawMapType, mapType };
3817
+ }
3818
+
3819
+ /**
3820
+ * Move each `STRUCT` and aliasing `TYPEDEF` to the module whose namespace is the
3821
+ * longest prefix of its name (`b2d.joint.revolute_definition`, declared in the
3822
+ * `b2d` doc, moves to `b2d.joint`). A name no module prefixes stays with the
3823
+ * module that declares it. A declaration the home already declares under the
3824
+ * same kind and name, or that an earlier module already routed there, is
3825
+ * dropped, so the home's own copy wins; a type sharing a value's name
3826
+ * (`resource.atlas`) is untouched. A function moves the same way (1.13.2 files
3827
+ * `bullet3d.collision_object.get_shapes` in the shape doc), since the emitter
3828
+ * declares only the functions under a module's own namespace. Modules with
3829
+ * nothing moved in or out are returned as-is.
3830
+ */
3831
+ export function routeTypeDeclarations(modules: readonly ApiModule[]): ApiModule[] {
3832
+ const homeOf = (name: string, declaring: string): string => {
3833
+ let home = declaring;
3834
+ let longest = -1;
3835
+ for (const { namespace } of modules) {
3836
+ if (name.startsWith(`${namespace}.`) && namespace.length > longest) {
3837
+ home = namespace;
3838
+ longest = namespace.length;
3839
+ }
3840
+ }
3841
+ return home;
3842
+ };
3843
+ const incoming = new Map<
3844
+ string,
3845
+ { structs: ApiStruct[]; typedefs: ApiTypedef[]; functions: ApiFunction[] }
3846
+ >();
3847
+ const into = (namespace: string) => {
3848
+ let bucket = incoming.get(namespace);
3849
+ if (bucket === undefined) {
3850
+ bucket = { structs: [], typedefs: [], functions: [] };
3851
+ incoming.set(namespace, bucket);
3852
+ }
3853
+ return bucket;
3854
+ };
3855
+ const kept = modules.map((module) => {
3856
+ const structs = (module.structs ?? []).filter((struct) => {
3857
+ const home = homeOf(struct.name, module.namespace);
3858
+ if (home === module.namespace) return true;
3859
+ into(home).structs.push(struct);
3860
+ return false;
3861
+ });
3862
+ const typedefs = module.typedefs.filter((typedef) => {
3863
+ if (typedef.aliasOf === undefined) return true;
3864
+ const home = homeOf(typedef.name, module.namespace);
3865
+ if (home === module.namespace) return true;
3866
+ into(home).typedefs.push(typedef);
3867
+ return false;
3868
+ });
3869
+ const functions = module.functions.filter((fn) => {
3870
+ const home = homeOf(fn.name, module.namespace);
3871
+ if (home === module.namespace) return true;
3872
+ into(home).functions.push(fn);
3873
+ return false;
3874
+ });
3875
+ return structs.length === (module.structs ?? []).length &&
3876
+ typedefs.length === module.typedefs.length &&
3877
+ functions.length === module.functions.length
3878
+ ? module
3879
+ : { ...module, structs, typedefs, functions };
3880
+ });
3881
+ return kept.map((module) => {
3882
+ const bucket = incoming.get(module.namespace);
3883
+ if (bucket === undefined) return module;
3884
+ const unclaimed = (names: Set<string>) => (declaration: { name: string }) => {
3885
+ if (names.has(declaration.name)) return false;
3886
+ names.add(declaration.name);
3887
+ return true;
3888
+ };
3889
+ const structNames = new Set((module.structs ?? []).map((struct) => struct.name));
3890
+ const typedefNames = new Set(module.typedefs.map((typedef) => typedef.name));
3891
+ // A function keeps every overload one doc files, so the home's own name wins
3892
+ // over another doc's copy of it without collapsing the incoming overloads.
3893
+ const functionNames = new Set(module.functions.map((fn) => fn.name));
3894
+ return {
3895
+ ...module,
3896
+ structs: [...(module.structs ?? []), ...bucket.structs.filter(unclaimed(structNames))],
3897
+ typedefs: [...module.typedefs, ...bucket.typedefs.filter(unclaimed(typedefNames))],
3898
+ functions: [
3899
+ ...module.functions,
3900
+ ...bucket.functions.filter((fn) => !functionNames.has(fn.name)),
3901
+ ],
3902
+ };
3903
+ });
3904
+ }
3905
+
3906
+ /**
3907
+ * The TS path of every `ENUM`, `STRUCT` and aliasing `TYPEDEF` the (routed)
3908
+ * modules declare, keyed by its Lua name: the name itself when it sits under its
3909
+ * module's namespace, else that name nested under the module
3910
+ * (`on_input.action` in `go` is `go.on_input.action`).
3911
+ */
3912
+ export function declaredTypePaths(modules: readonly ApiModule[]): Map<string, string> {
3913
+ const paths = new Map<string, string>();
3914
+ for (const module of modules) {
3915
+ const names = [
3916
+ ...(module.enums ?? []).map((e) => e.name),
3917
+ ...(module.structs ?? []).map((struct) => struct.name),
3918
+ ...module.typedefs.filter((t) => t.aliasOf !== undefined).map((t) => t.name),
3919
+ ];
3920
+ for (const name of names) {
3921
+ paths.set(
3922
+ name,
3923
+ name.startsWith(`${module.namespace}.`) ? name : `${module.namespace}.${name}`,
3924
+ );
3925
+ }
3926
+ }
3927
+ return paths;
3928
+ }
3929
+
3930
+ export function emitDeclarations(documented: ApiModule, options?: EmitOptions): string {
3931
+ const module = withUndocumentedConstants(documented, options?.registeredConstants);
3932
+ const prefix = `${module.namespace}.`;
3933
+
3934
+ const constantsByFqn = new Map(module.constants.map((c) => [c.name, c]));
2652
3935
  const knownConstantFqns = options?.knownConstantFqns;
2653
3936
  const translations = options?.translations ?? {};
2654
3937
  const urlParameters = options?.urlParameters ?? [];
2655
- const baseMapType = options?.mapType ?? defaultMapType;
2656
- const mapType = (token: string): string =>
2657
- constantFqns.has(token) || knownConstantFqns?.has(token)
2658
- ? brandType(token)
2659
- : baseMapType(token);
3938
+ const baseNotes = indexBaseNoteResolver(options?.indexBaseSource ?? "engine");
3939
+ const { rawMapType, mapType } = moduleTypeMappers(module, constantsByFqn, options);
2660
3940
  const constantTokens = constantSlotTokenResolver(
2661
3941
  module,
2662
- new Set([...constantFqns, ...(knownConstantFqns ?? [])]),
3942
+ new Set([...constantsByFqn.keys(), ...(knownConstantFqns ?? [])]),
2663
3943
  );
2664
3944
 
2665
3945
  const constants = module.constants
@@ -2673,6 +3953,7 @@ export function emitDeclarations(module: ApiModule, options?: EmitOptions): stri
2673
3953
  .sort((a, b) => a.name.localeCompare(b.name));
2674
3954
 
2675
3955
  const functions = module.functions
3956
+ .filter((fn) => !isMisfiled(fn))
2676
3957
  .map((fn) => prepareFunction(fn, prefix))
2677
3958
  .filter((entry): entry is PreparedFunction => entry !== null)
2678
3959
  .sort((a, b) =>
@@ -2689,6 +3970,7 @@ export function emitDeclarations(module: ApiModule, options?: EmitOptions): stri
2689
3970
  // segment. One or two leading identifier segments qualify; anything deeper,
2690
3971
  // and any non-identifier segment, stays dropped.
2691
3972
  const nestedRoot = collectNestedGroups(module, prefix);
3973
+ const enumAliases = collectEnumAliases(module, constants, nestedRoot);
2692
3974
 
2693
3975
  // Colon methods (`client:send`) are FUNCTION elements named `<receiver>:<method>`
2694
3976
  // and are NOT namespace-prefixed, so they fail the flat-identifier test in
@@ -2706,9 +3988,12 @@ export function emitDeclarations(module: ApiModule, options?: EmitOptions): stri
2706
3988
  })),
2707
3989
  );
2708
3990
 
2709
- const typedefs = module.typedefs
2710
- .filter((t) => TS_IDENTIFIER.test(t.name))
2711
- .sort((a, b) => a.name.localeCompare(b.name));
3991
+ // A typedef that also has colon methods is emitted as a method-bearing
3992
+ // interface below, not an opaque brand alias.
3993
+ const typeDeclarations = placeTypeDeclarations(
3994
+ collectTypeDeclarations(module, new Set(handleGroups.keys()), rawMapType, mapType),
3995
+ nestedRoot,
3996
+ );
2712
3997
 
2713
3998
  // A re-export alias (`export { _x as x }`) switches the ambient namespace out
2714
3999
  // of its implicit-export mode, so once any alias is present every sibling
@@ -2716,28 +4001,29 @@ export function emitDeclarations(module: ApiModule, options?: EmitOptions): stri
2716
4001
  // keyword is otherwise omitted to keep every alias-free module byte-identical.
2717
4002
  const hasAliases =
2718
4003
  variables.some((v) => TS_RESERVED_NAMES.has(v.name)) ||
2719
- functions.some((fn) => TS_RESERVED_NAMES.has(fn.name));
4004
+ functions.some((fn) => TS_RESERVED_NAMES.has(fn.name)) ||
4005
+ [...nestedRoot.keys()].some((segment) => TS_RESERVED_NAMES.has(segment));
2720
4006
  const decl = hasAliases ? "export " : "";
2721
4007
 
2722
4008
  const lines: string[] = [];
2723
- for (const docLine of namespaceDocLines(module)) lines.push(docLine);
4009
+ for (const docLine of namespaceDocLines(module, translations)) lines.push(docLine);
2724
4010
  lines.push(`declare namespace ${module.namespace} {`);
2725
4011
 
2726
- for (const t of typedefs) {
2727
- // A typedef that also has colon methods is emitted as a method-bearing
2728
- // interface below, not an opaque brand alias.
2729
- if (handleGroups.has(t.name)) continue;
2730
- lines.push(`${INDENT}${decl}${typedefAlias(t.name, baseMapType)}`);
2731
- }
4012
+ for (const declaration of typeDeclarations) lines.push(...declaration.lines(INDENT, decl));
2732
4013
  for (const aliasDecl of constantUnionAliasDeclarations(module.namespace)) {
2733
4014
  lines.push(`${INDENT}${decl}${aliasDecl}`);
2734
4015
  }
4016
+ for (const alias of enumAliases.topLevel) {
4017
+ lines.push(
4018
+ `${INDENT}${decl}${unionAliasDeclaration(alias.name, alias.members, alias.nilable)}`,
4019
+ );
4020
+ }
2735
4021
  const handleIndent = `${INDENT}${INDENT}`;
2736
4022
  for (const receiver of handleReceivers) {
2737
4023
  const group = handleGroups.get(receiver) ?? [];
2738
4024
  lines.push(`${INDENT}${decl}interface ${receiver} {`);
2739
4025
  for (const fn of group) {
2740
- for (const docLine of functionDocLines(fn.original, translations, handleIndent)) {
4026
+ for (const docLine of functionDocLines(fn.original, translations, baseNotes, handleIndent)) {
2741
4027
  lines.push(docLine);
2742
4028
  }
2743
4029
  lines.push(
@@ -2750,7 +4036,7 @@ export function emitDeclarations(module: ApiModule, options?: EmitOptions): stri
2750
4036
  for (const docLine of summaryDocLines(c.original.brief, c.original.description, INDENT)) {
2751
4037
  lines.push(docLine);
2752
4038
  }
2753
- lines.push(`${INDENT}${decl}const ${c.name}: ${brandType(c.fqn)};`);
4039
+ lines.push(`${INDENT}${decl}const ${c.name}: ${constantType(c)};`);
2754
4040
  }
2755
4041
  const aliases: { internal: string; public: string }[] = [];
2756
4042
  for (const v of variables) {
@@ -2770,11 +4056,17 @@ export function emitDeclarations(module: ApiModule, options?: EmitOptions): stri
2770
4056
  for (const fn of functions) {
2771
4057
  const reserved = TS_RESERVED_NAMES.has(fn.name);
2772
4058
  const emitName = aliasName(fn.name, aliases);
2773
- for (const docLine of functionDocLines(fn.original, translations)) lines.push(docLine);
4059
+ for (const docLine of functionDocLines(fn.original, translations, baseNotes))
4060
+ lines.push(docLine);
2774
4061
  const line = emitFunction(fn, emitName, mapType, resolver, constantTokens, urlParameters);
2775
4062
  lines.push(`${INDENT}${reserved ? "" : decl}${line}`);
2776
4063
  const alternative = firstSlotAlternative(fn, module.namespace);
2777
4064
  if (alternative !== null) {
4065
+ // The overload documents its own parameters, so its hover names the base
4066
+ // of the index it takes. Its examples are the primary signature's.
4067
+ const { examples: _examples, ...overloadDoc } = alternative.original;
4068
+ for (const docLine of functionDocLines(overloadDoc, translations, baseNotes))
4069
+ lines.push(docLine);
2778
4070
  const overload = emitFunction(
2779
4071
  alternative,
2780
4072
  emitName,
@@ -2787,6 +4079,7 @@ export function emitDeclarations(module: ApiModule, options?: EmitOptions): stri
2787
4079
  }
2788
4080
  }
2789
4081
 
4082
+ aliasReservedSegments(nestedRoot.keys(), aliases);
2790
4083
  for (const alias of [...aliases].sort((a, b) => a.public.localeCompare(b.public))) {
2791
4084
  lines.push(`${INDENT}export { ${alias.internal} as ${alias.public} };`);
2792
4085
  }
@@ -2802,14 +4095,38 @@ export function emitDeclarations(module: ApiModule, options?: EmitOptions): stri
2802
4095
  // A reserved-name member inside a nested namespace gets the same recovery as
2803
4096
  // a top-level one: emitted un-exported as `_<name>` and re-exported under the
2804
4097
  // reserved name. The alias switches this namespace out of implicit-export mode,
2805
- // so its siblings then need an explicit `export` to stay reachable.
4098
+ // so its siblings then need an explicit `export` to stay reachable. A
4099
+ // reserved segment is itself declared as `_<segment>`, merging with a
4100
+ // same-named function, and reached through the parent's alias.
2806
4101
  const segmentAliases: { internal: string; public: string }[] = [];
2807
4102
  const segmentDecl =
2808
4103
  group.variables.some((v) => TS_RESERVED_NAMES.has(v.name)) ||
2809
- group.functions.some((fn) => TS_RESERVED_NAMES.has(fn.name))
4104
+ group.functions.some((fn) => TS_RESERVED_NAMES.has(fn.name)) ||
4105
+ [...group.children.keys()].some((child) => TS_RESERVED_NAMES.has(child))
2810
4106
  ? "export "
2811
4107
  : "";
2812
- lines.push(`${indent}${outerDecl}namespace ${segment} {`);
4108
+ const reservedSegment = TS_RESERVED_NAMES.has(segment);
4109
+ lines.push(
4110
+ `${indent}${reservedSegment ? "" : outerDecl}namespace ${reservedSegment ? `_${segment}` : segment} {`,
4111
+ );
4112
+ for (const declaration of group.typeDeclarations) {
4113
+ lines.push(...declaration.lines(bodyIndent, segmentDecl));
4114
+ }
4115
+ for (const alias of group.enumAliases) {
4116
+ lines.push(
4117
+ `${bodyIndent}${segmentDecl}${unionAliasDeclaration(alias.name, alias.members, alias.nilable)}`,
4118
+ );
4119
+ }
4120
+ for (const c of group.constants) {
4121
+ for (const docLine of summaryDocLines(
4122
+ c.original.brief,
4123
+ c.original.description,
4124
+ bodyIndent,
4125
+ )) {
4126
+ lines.push(docLine);
4127
+ }
4128
+ lines.push(`${bodyIndent}${segmentDecl}const ${c.name}: ${constantType(c)};`);
4129
+ }
2813
4130
  for (const v of group.variables) {
2814
4131
  const reserved = TS_RESERVED_NAMES.has(v.name);
2815
4132
  const emitName = aliasName(v.name, segmentAliases);
@@ -2827,13 +4144,14 @@ export function emitDeclarations(module: ApiModule, options?: EmitOptions): stri
2827
4144
  for (const fn of group.functions) {
2828
4145
  const reserved = TS_RESERVED_NAMES.has(fn.name);
2829
4146
  const emitName = aliasName(fn.name, segmentAliases);
2830
- for (const docLine of functionDocLines(fn.original, translations, bodyIndent)) {
4147
+ for (const docLine of functionDocLines(fn.original, translations, baseNotes, bodyIndent)) {
2831
4148
  lines.push(docLine);
2832
4149
  }
2833
4150
  lines.push(
2834
4151
  `${bodyIndent}${reserved ? "" : segmentDecl}${emitFunction(fn, emitName, mapType, resolver, constantTokens, urlParameters)}`,
2835
4152
  );
2836
4153
  }
4154
+ aliasReservedSegments(group.children.keys(), segmentAliases);
2837
4155
  for (const alias of [...segmentAliases].sort((a, b) => a.public.localeCompare(b.public))) {
2838
4156
  lines.push(`${bodyIndent}export { ${alias.internal} as ${alias.public} };`);
2839
4157
  }
@@ -2862,14 +4180,22 @@ export function emitDeclarations(module: ApiModule, options?: EmitOptions): stri
2862
4180
  }
2863
4181
 
2864
4182
  /**
2865
- * Every documented slot's rendered TS type, keyed `param|return:<position>:<raw
2866
- * ref-doc name>`. The position is part of the key because a multi-return can
2867
- * carry several unnamed slots (`push.schedule` renders two), which a name-only
2868
- * key would collapse into one. Each value appears verbatim inside the symbol's
2869
- * own `tsSignature`, so a consumer rendering a slot's type beside that signature
2870
- * cannot contradict it.
4183
+ * Every documented slot's rendered TS type and optionality, keyed
4184
+ * `param|return:<position>:<raw ref-doc name>`. The position is part of the key
4185
+ * because a multi-return can carry several unnamed slots (`push.schedule`
4186
+ * renders two), which a name-only key would collapse into one. Each `type`
4187
+ * appears verbatim inside the symbol's own `tsSignature` as the slot's printed
4188
+ * operand (an interior omissible slot keeps its `| undefined` arm), and
4189
+ * `optional` is whether the name carries `?` there, so a consumer rendering a
4190
+ * slot beside that signature cannot contradict it. Returns and slots folded into
4191
+ * a rest element are never `optional`.
2871
4192
  */
2872
- export type SlotTypes = Readonly<Record<string, string>>;
4193
+ export interface SlotType {
4194
+ readonly type: string;
4195
+ readonly optional: boolean;
4196
+ }
4197
+
4198
+ export type SlotTypes = Readonly<Record<string, SlotType>>;
2873
4199
 
2874
4200
  function slotKey(kind: "param" | "return", position: number, name: string): string {
2875
4201
  return `${kind}:${position}:${name}`;
@@ -2897,19 +4223,19 @@ export interface SymbolSignature {
2897
4223
  * line, primary first, the same convention as the authored fold; its
2898
4224
  * `slotTypes` describe the primary arm only.
2899
4225
  */
2900
- export function emitSymbolSignatures(module: ApiModule, options?: EmitOptions): SymbolSignature[] {
4226
+ export function emitSymbolSignatures(
4227
+ documented: ApiModule,
4228
+ options?: EmitOptions,
4229
+ ): SymbolSignature[] {
4230
+ const module = withUndocumentedConstants(documented, options?.registeredConstants);
2901
4231
  const prefix = `${module.namespace}.`;
2902
- const constantFqns = new Set(module.constants.map((c) => c.name));
4232
+ const constantsByFqn = new Map(module.constants.map((c) => [c.name, c]));
2903
4233
  const knownConstantFqns = options?.knownConstantFqns;
2904
4234
  const urlParameters = options?.urlParameters ?? [];
2905
- const baseMapType = options?.mapType ?? defaultMapType;
2906
- const mapType = (token: string): string =>
2907
- constantFqns.has(token) || knownConstantFqns?.has(token)
2908
- ? brandType(token)
2909
- : baseMapType(token);
4235
+ const { rawMapType, mapType } = moduleTypeMappers(module, constantsByFqn, options);
2910
4236
  const constantTokens = constantSlotTokenResolver(
2911
4237
  module,
2912
- new Set([...constantFqns, ...(knownConstantFqns ?? [])]),
4238
+ new Set([...constantsByFqn.keys(), ...(knownConstantFqns ?? [])]),
2913
4239
  );
2914
4240
  const resolver = buildTableDocResolver(
2915
4241
  module.functions.map((fn) => ({
@@ -2932,9 +4258,10 @@ export function emitSymbolSignatures(module: ApiModule, options?: EmitOptions):
2932
4258
  const NO_SLOTS: SlotTypes = {};
2933
4259
 
2934
4260
  for (const fn of module.functions) {
4261
+ if (isMisfiled(fn)) continue;
2935
4262
  const prepared = prepareFunction(fn, prefix);
2936
4263
  if (prepared === null) continue;
2937
- const slotTypes: Record<string, string> = {};
4264
+ const slotTypes: Record<string, SlotType> = {};
2938
4265
  const primary = emitFunction(
2939
4266
  prepared,
2940
4267
  emitName(prepared.name),
@@ -2959,7 +4286,20 @@ export function emitSymbolSignatures(module: ApiModule, options?: EmitOptions):
2959
4286
  out.push({ identity: fnIdentity(fn), tsSignature, slotTypes });
2960
4287
  }
2961
4288
 
2962
- const nested = flattenNestedGroups(collectNestedGroups(module, prefix));
4289
+ const nestedRoot = collectNestedGroups(module, prefix);
4290
+ const nested = flattenNestedGroups(nestedRoot);
4291
+ for (const c of nested.constants) {
4292
+ out.push({
4293
+ identity: {
4294
+ namespace: module.namespace,
4295
+ kind: "CONSTANT",
4296
+ name: c.fqn,
4297
+ signature: "",
4298
+ },
4299
+ tsSignature: `const ${c.name}: ${constantType(c)};`,
4300
+ slotTypes: NO_SLOTS,
4301
+ });
4302
+ }
2963
4303
  for (const v of nested.variables) {
2964
4304
  out.push({
2965
4305
  identity: {
@@ -2973,7 +4313,7 @@ export function emitSymbolSignatures(module: ApiModule, options?: EmitOptions):
2973
4313
  });
2974
4314
  }
2975
4315
  for (const fn of nested.functions) {
2976
- const slotTypes: Record<string, string> = {};
4316
+ const slotTypes: Record<string, SlotType> = {};
2977
4317
  out.push({
2978
4318
  identity: fnIdentity(fn.original),
2979
4319
  tsSignature: emitFunction(
@@ -2992,7 +4332,7 @@ export function emitSymbolSignatures(module: ApiModule, options?: EmitOptions):
2992
4332
  const handleGroups = collectHandleMethodGroups(module);
2993
4333
  for (const group of handleGroups.values()) {
2994
4334
  for (const prepared of group) {
2995
- const slotTypes: Record<string, string> = {};
4335
+ const slotTypes: Record<string, SlotType> = {};
2996
4336
  out.push({
2997
4337
  identity: fnIdentity(prepared.original),
2998
4338
  tsSignature: emitMethod(
@@ -3008,12 +4348,14 @@ export function emitSymbolSignatures(module: ApiModule, options?: EmitOptions):
3008
4348
  }
3009
4349
  }
3010
4350
 
4351
+ const constants: PreparedConstant[] = [];
3011
4352
  for (const c of module.constants) {
3012
4353
  const prepared = prepareConstant(c, prefix);
3013
4354
  if (prepared === null) continue;
4355
+ constants.push(prepared);
3014
4356
  out.push({
3015
4357
  identity: { namespace: module.namespace, kind: "CONSTANT", name: c.name, signature: "" },
3016
- tsSignature: `const ${prepared.name}: ${brandType(prepared.fqn)};`,
4358
+ tsSignature: `const ${prepared.name}: ${constantType(prepared)};`,
3017
4359
  slotTypes: NO_SLOTS,
3018
4360
  });
3019
4361
  }
@@ -3040,11 +4382,22 @@ export function emitSymbolSignatures(module: ApiModule, options?: EmitOptions):
3040
4382
  });
3041
4383
  }
3042
4384
 
3043
- for (const t of module.typedefs) {
3044
- if (!TS_IDENTIFIER.test(t.name) || handleGroups.has(t.name)) continue;
4385
+ // A struct signs as its interface on one line; each member line is verbatim in
4386
+ // the declarations.
4387
+ for (const declaration of collectTypeDeclarations(
4388
+ module,
4389
+ new Set(handleGroups.keys()),
4390
+ rawMapType,
4391
+ mapType,
4392
+ )) {
3045
4393
  out.push({
3046
- identity: { namespace: module.namespace, kind: "TYPEDEF", name: t.name, signature: "" },
3047
- tsSignature: typedefAlias(t.name, baseMapType),
4394
+ identity: {
4395
+ namespace: module.namespace,
4396
+ kind: "TYPEDEF",
4397
+ name: declaration.path,
4398
+ signature: "",
4399
+ },
4400
+ tsSignature: declaration.signature,
3048
4401
  slotTypes: NO_SLOTS,
3049
4402
  });
3050
4403
  }
@@ -3055,9 +4408,18 @@ export function emitSymbolSignatures(module: ApiModule, options?: EmitOptions):
3055
4408
  for (const row of CONSTANT_UNION_ALIASES.filter((r) => r.home === module.namespace)) {
3056
4409
  out.push({
3057
4410
  identity: { namespace: module.namespace, kind: "TYPEDEF", name: row.name, signature: "" },
3058
- tsSignature: `type ${row.name} = ${row.members
3059
- .map((member) => `typeof ${member}`)
3060
- .join(" | ")};`,
4411
+ tsSignature: unionAliasDeclaration(row.name, row.members),
4412
+ slotTypes: NO_SLOTS,
4413
+ });
4414
+ }
4415
+
4416
+ // An enum alias is keyed by its local path (`STATUS`, `ui.COLOR`), as the
4417
+ // constant-union aliases above are keyed by their local name.
4418
+ const enumAliases = collectEnumAliases(module, constants, nestedRoot);
4419
+ for (const alias of enumAliases.all) {
4420
+ out.push({
4421
+ identity: { namespace: module.namespace, kind: "TYPEDEF", name: alias.path, signature: "" },
4422
+ tsSignature: unionAliasDeclaration(alias.name, alias.members, alias.nilable),
3061
4423
  slotTypes: NO_SLOTS,
3062
4424
  });
3063
4425
  }
@@ -3084,35 +4446,67 @@ interface PreparedVariable {
3084
4446
  // One level of the nested-member tree: the members declared directly under a
3085
4447
  // segment, plus the segments declared beneath it.
3086
4448
  interface NestedGroup {
4449
+ constants: PreparedConstant[];
3087
4450
  variables: PreparedVariable[];
3088
4451
  functions: PreparedFunction[];
4452
+ enumAliases: EnumAlias[];
4453
+ typeDeclarations: TypeDeclaration[];
3089
4454
  children: Map<string, NestedGroup>;
3090
4455
  }
3091
4456
 
4457
+ // One `ENUM`'s union alias: `name` is the declared identifier, `path` the local
4458
+ // path under the module namespace (`ui.COLOR`), `members` the member FQNs.
4459
+ interface EnumAlias {
4460
+ name: string;
4461
+ path: string;
4462
+ members: readonly string[];
4463
+ // Members typed `<ENUM>|nil`, whose arm drops the `undefined` their `const`
4464
+ // admits, as a slot naming one does.
4465
+ nilable: ReadonlySet<string>;
4466
+ }
4467
+
3092
4468
  // A stripped local carrying one or two leading identifier segments before the
3093
4469
  // member name (`dns.toip`, `COLOR.TEXT`, `schema.integer`). Three levels deep is
3094
4470
  // beyond anything the vendored documents describe, so it stays dropped rather
3095
4471
  // than half-emitted.
3096
4472
  const NESTED_MEMBER_LOCAL = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*){1,2}$/;
3097
4473
 
4474
+ // The group at `segments` under `root`, creating each missing level.
4475
+ function nestedGroupAt(root: Map<string, NestedGroup>, segments: readonly string[]): NestedGroup {
4476
+ let level = root;
4477
+ let group: NestedGroup | undefined;
4478
+ for (const segment of segments) {
4479
+ let next = level.get(segment);
4480
+ if (next === undefined) {
4481
+ next = {
4482
+ constants: [],
4483
+ variables: [],
4484
+ functions: [],
4485
+ enumAliases: [],
4486
+ typeDeclarations: [],
4487
+ children: new Map(),
4488
+ };
4489
+ level.set(segment, next);
4490
+ }
4491
+ group = next;
4492
+ level = next.children;
4493
+ }
4494
+ return group as NestedGroup;
4495
+ }
4496
+
3098
4497
  function collectNestedGroups(module: ApiModule, prefix: string): Map<string, NestedGroup> {
3099
4498
  const root = new Map<string, NestedGroup>();
3100
- const groupAt = (segments: readonly string[]): NestedGroup => {
3101
- let level = root;
3102
- let group: NestedGroup | undefined;
3103
- for (const segment of segments) {
3104
- let next = level.get(segment);
3105
- if (next === undefined) {
3106
- next = { variables: [], functions: [], children: new Map() };
3107
- level.set(segment, next);
3108
- }
3109
- group = next;
3110
- level = next.children;
3111
- }
3112
- return group as NestedGroup;
3113
- };
4499
+ const groupAt = (segments: readonly string[]): NestedGroup => nestedGroupAt(root, segments);
3114
4500
  const pathOf = (local: string): string[] => local.split(".").slice(0, -1);
3115
4501
 
4502
+ for (const c of module.constants) {
4503
+ const local = stripPrefix(c.name, prefix);
4504
+ if (!NESTED_MEMBER_LOCAL.test(local)) continue;
4505
+ const segments = pathOf(local);
4506
+ const prepared = prepareConstant(c, `${module.namespace}.${segments.join(".")}.`);
4507
+ if (prepared === null) continue;
4508
+ groupAt(segments).constants.push(prepared);
4509
+ }
3116
4510
  for (const v of module.variables) {
3117
4511
  const local = stripPrefix(v.name, prefix);
3118
4512
  if (!NESTED_MEMBER_LOCAL.test(local)) continue;
@@ -3132,6 +4526,7 @@ function collectNestedGroups(module: ApiModule, prefix: string): Map<string, Nes
3132
4526
 
3133
4527
  const sortLevel = (level: Map<string, NestedGroup>): void => {
3134
4528
  for (const group of level.values()) {
4529
+ group.constants.sort((a, b) => a.name.localeCompare(b.name));
3135
4530
  group.variables.sort((a, b) => a.name.localeCompare(b.name));
3136
4531
  group.functions.sort((a, b) =>
3137
4532
  a.name === b.name
@@ -3150,21 +4545,71 @@ function collectNestedGroups(module: ApiModule, prefix: string): Map<string, Nes
3150
4545
  // children). Callers that need no per-segment container read the tree through
3151
4546
  // this rather than re-deriving which members are nested and how deep.
3152
4547
  function flattenNestedGroups(level: ReadonlyMap<string, NestedGroup>): {
4548
+ constants: PreparedConstant[];
3153
4549
  variables: PreparedVariable[];
3154
4550
  functions: PreparedFunction[];
3155
4551
  } {
4552
+ const constants: PreparedConstant[] = [];
3156
4553
  const variables: PreparedVariable[] = [];
3157
4554
  const functions: PreparedFunction[] = [];
3158
4555
  const walk = (current: ReadonlyMap<string, NestedGroup>): void => {
3159
4556
  for (const segment of [...current.keys()].sort((a, b) => a.localeCompare(b))) {
3160
4557
  const group = current.get(segment) as NestedGroup;
4558
+ constants.push(...group.constants);
3161
4559
  variables.push(...group.variables);
3162
4560
  functions.push(...group.functions);
3163
4561
  walk(group.children);
3164
4562
  }
3165
4563
  };
3166
4564
  walk(level);
3167
- return { variables, functions };
4565
+ return { constants, variables, functions };
4566
+ }
4567
+
4568
+ // Each `ENUM`'s union alias, placed where its name lands: a flat local
4569
+ // (`STATUS`) at the top of the namespace, a nested one (`ui.COLOR`) in the
4570
+ // nested group `ui`, which the tree already holds when the members are nested
4571
+ // beside it. An enum is aliased only when every member is declared, so the
4572
+ // alias never names a constant the module does not emit. The nested groups
4573
+ // gain their aliases in place; `topLevel` and `all` are in name order.
4574
+ function collectEnumAliases(
4575
+ module: ApiModule,
4576
+ topLevelConstants: readonly PreparedConstant[],
4577
+ nestedRoot: Map<string, NestedGroup>,
4578
+ ): { topLevel: EnumAlias[]; all: EnumAlias[] } {
4579
+ const prefix = `${module.namespace}.`;
4580
+ const declared = new Set([
4581
+ ...topLevelConstants.map((c) => c.fqn),
4582
+ ...flattenNestedGroups(nestedRoot).constants.map((c) => c.fqn),
4583
+ ]);
4584
+ const nilable = new Set(module.constants.filter((c) => c.nilable).map((c) => c.name));
4585
+ const topLevel: EnumAlias[] = [];
4586
+ const all: EnumAlias[] = [];
4587
+ const enums = [...(module.enums ?? [])].sort((a, b) => a.name.localeCompare(b.name));
4588
+ for (const e of enums) {
4589
+ if (e.members.length === 0 || !e.members.every((m) => declared.has(m))) continue;
4590
+ const path = stripPrefix(e.name, prefix);
4591
+ if (TS_IDENTIFIER.test(path)) {
4592
+ const alias = { name: path, path, members: e.members, nilable };
4593
+ topLevel.push(alias);
4594
+ all.push(alias);
4595
+ continue;
4596
+ }
4597
+ if (!NESTED_MEMBER_LOCAL.test(path)) continue;
4598
+ const segments = path.split(".");
4599
+ const name = segments.pop() as string;
4600
+ let level = nestedRoot;
4601
+ let group: NestedGroup | undefined;
4602
+ for (const segment of segments) {
4603
+ group = level.get(segment);
4604
+ if (group === undefined) break;
4605
+ level = group.children;
4606
+ }
4607
+ if (group === undefined) continue;
4608
+ const alias = { name, path, members: e.members, nilable };
4609
+ group.enumAliases.push(alias);
4610
+ all.push(alias);
4611
+ }
4612
+ return { topLevel, all };
3168
4613
  }
3169
4614
 
3170
4615
  function prepareFunction(fn: ApiFunction, prefix: string): PreparedFunction | null {
@@ -3210,8 +4655,137 @@ function prepareConstant(c: ApiConstant, prefix: string): PreparedConstant | nul
3210
4655
  return { name: stripped, fqn: c.name, original: c };
3211
4656
  }
3212
4657
 
3213
- function brandType(fqn: string): string {
3214
- return `number & { readonly __brand: "${fqn}" }`;
4658
+ // Where the engine registers a constant as a string: the vendored binding file
4659
+ // and the call there whose macro pushes the literal.
4660
+ export interface StringConstantSource {
4661
+ readonly binding: string;
4662
+ readonly macro: string;
4663
+ readonly call: string;
4664
+ }
4665
+
4666
+ const GUI_SCRIPT = "engine/gui/src/gui_script.cpp";
4667
+ const IMAGE_SCRIPT = "engine/gamesys/src/gamesys/scripts/script_image.cpp";
4668
+
4669
+ // Constants the engine registers with `lua_pushliteral`, keyed by FQN. The
4670
+ // ref-docs give constants no type, and every other constant is a number;
4671
+ // `string-constant-provenance.test.ts` reds when a vendored binding stops
4672
+ // pushing one as a string.
4673
+ export const STRING_CONSTANTS: ReadonlyMap<string, StringConstantSource> = new Map([
4674
+ ...[
4675
+ "position",
4676
+ "rotation",
4677
+ "euler",
4678
+ "scale",
4679
+ "color",
4680
+ "outline",
4681
+ "shadow",
4682
+ "size",
4683
+ "fill_angle",
4684
+ "inner_radius",
4685
+ "leading",
4686
+ "tracking",
4687
+ "slice9",
4688
+ ].map((prop): [string, StringConstantSource] => {
4689
+ const name = prop.toUpperCase();
4690
+ const call = `SETPROP(${prop}, ${name})`;
4691
+ return [`gui.PROP_${name}`, { binding: GUI_SCRIPT, macro: "SETPROP", call }];
4692
+ }),
4693
+ ...(
4694
+ [
4695
+ ["TYPE_RGB", "rgb"],
4696
+ ["TYPE_RGBA", "rgba"],
4697
+ ["TYPE_LUMINANCE", "l"],
4698
+ ["TYPE_LUMINANCE_ALPHA", "la"],
4699
+ ] as const
4700
+ ).map(([name, value]): [string, StringConstantSource] => [
4701
+ `image.${name}`,
4702
+ { binding: IMAGE_SCRIPT, macro: "SETCONSTANT", call: `SETCONSTANT(${name}, "${value}")` },
4703
+ ]),
4704
+ ]);
4705
+
4706
+ // Documented constants the engine never registers, keyed by FQN with the
4707
+ // reason, so Lua reads each as nil. `nil-constant-provenance.test.ts` pins the
4708
+ // keys to the accepted `constant-missing-binding` binding verdicts.
4709
+ export const NIL_CONSTANTS: ReadonlyMap<string, string> = new Map([
4710
+ [
4711
+ "render.RENDER_TARGET_DEFAULT",
4712
+ "render_script.cpp registers no such field; render.set_render_target reads the nil as the default target",
4713
+ ],
4714
+ ]);
4715
+
4716
+ // Constants the engine registers but some release's ref-doc omits, keyed by FQN
4717
+ // with the doc the emit carries. A surface gains an entry only where its ref-doc
4718
+ // leaves the constant out and its engine bindings register it.
4719
+ // `undocumented-constant-provenance.test.ts` reds an entry no committed target
4720
+ // still needs. `sameValueAs` names the documented constant the engine registers
4721
+ // with the same value; where the surface declares it, the back-fill takes its
4722
+ // type, so a value compares equal to either spelling.
4723
+ export const UNDOCUMENTED_CONSTANTS: ReadonlyMap<
4724
+ string,
4725
+ { readonly doc: string; readonly sameValueAs?: string }
4726
+ > = new Map([
4727
+ [
4728
+ "render.CONTEXT_EVENT_CONTEXT_LOST",
4729
+ {
4730
+ doc: "The rendering context was lost. Rendering is paused and all graphics resources become invalid. Passed to the `render.set_listener` callback.",
4731
+ },
4732
+ ],
4733
+ [
4734
+ "render.CONTEXT_EVENT_CONTEXT_RESTORED",
4735
+ {
4736
+ doc: "The rendering context was restored. Rendering is still paused and graphics resources are still invalid, but can be reloaded. Passed to the `render.set_listener` callback.",
4737
+ },
4738
+ ],
4739
+ [
4740
+ "window.WINDOW_EVENT_ICONFIED",
4741
+ {
4742
+ doc: "Deprecated misspelling of `window.WINDOW_EVENT_ICONIFIED`, with the same value. The engine still registers it.",
4743
+ sameValueAs: "window.WINDOW_EVENT_ICONIFIED",
4744
+ },
4745
+ ],
4746
+ ]);
4747
+
4748
+ function withUndocumentedConstants(
4749
+ module: ApiModule,
4750
+ registered: ReadonlySet<string> | undefined,
4751
+ ): ApiModule {
4752
+ const prefix = `${module.namespace}.`;
4753
+ const documented = new Set(module.constants.map((c) => c.name));
4754
+ const backfill: ApiConstant[] = [];
4755
+ for (const [fqn, { doc, sameValueAs }] of UNDOCUMENTED_CONSTANTS) {
4756
+ if (!fqn.startsWith(prefix) || fqn.slice(prefix.length).includes(".")) continue;
4757
+ if (documented.has(fqn)) continue;
4758
+ if (registered !== undefined && !registered.has(fqn)) continue;
4759
+ const aliasOf =
4760
+ sameValueAs !== undefined && documented.has(sameValueAs) ? sameValueAs : undefined;
4761
+ backfill.push({
4762
+ name: fqn,
4763
+ brief: doc,
4764
+ description: "",
4765
+ ...(aliasOf === undefined ? {} : { aliasOf }),
4766
+ });
4767
+ }
4768
+ return backfill.length === 0
4769
+ ? module
4770
+ : { ...module, constants: [...module.constants, ...backfill] };
4771
+ }
4772
+
4773
+ // `constant` is the module's own element for `fqn`, absent for a constant another
4774
+ // module declares; an `ENUM` member's value type rides on it.
4775
+ function brandType(fqn: string, constant?: ApiConstant): string {
4776
+ // `undefined & { __brand }` collapses to `never`, so a nil constant takes no brand.
4777
+ if (NIL_CONSTANTS.has(fqn)) return "undefined";
4778
+ const base = constant?.valueType === "string" || STRING_CONSTANTS.has(fqn) ? "string" : "number";
4779
+ return `${base} & { readonly __brand: "${fqn}" }`;
4780
+ }
4781
+
4782
+ // A declared constant's type. A nilable `ENUM` member admits `undefined` here
4783
+ // only: a slot naming it still takes the brand, as the value it is documented to
4784
+ // accept.
4785
+ function constantType(c: PreparedConstant): string {
4786
+ if (c.original.aliasOf !== undefined) return `typeof ${c.original.aliasOf}`;
4787
+ const brand = brandType(c.fqn, c.original);
4788
+ return c.original.nilable === true && brand !== "undefined" ? `${brand} | undefined` : brand;
3215
4789
  }
3216
4790
 
3217
4791
  function prepareVariable(v: ApiVariable, prefix: string): PreparedVariable | null {
@@ -3235,26 +4809,44 @@ function aliasName(name: string, aliases: { internal: string; public: string }[]
3235
4809
  return internal;
3236
4810
  }
3237
4811
 
3238
- function emitVariable(
3239
- prepared: PreparedVariable,
3240
- name: string,
3241
- mapType: (t: string) => string,
3242
- ): string {
4812
+ // A nested namespace named by a reserved word is emitted as `_<segment>`; its
4813
+ // parent re-exports it under the reserved name, sharing the alias a same-named
4814
+ // function already recorded.
4815
+ function aliasReservedSegments(
4816
+ segments: Iterable<string>,
4817
+ aliases: { internal: string; public: string }[],
4818
+ ): void {
4819
+ for (const segment of segments) {
4820
+ if (!TS_RESERVED_NAMES.has(segment)) continue;
4821
+ if (aliases.some((alias) => alias.public === segment)) continue;
4822
+ aliases.push({ internal: `_${segment}`, public: segment });
4823
+ }
4824
+ }
4825
+
4826
+ // A variable whose ref-doc states its type only in prose, keyed by FQN. Each
4827
+ // entry fills a slot upstream left empty; `emit-dts.test.ts` reds once the
4828
+ // default target's ref-doc types it.
4829
+ export const VARIABLE_TYPE_OVERRIDES: ReadonlyMap<string, string> = new Map([
4830
+ // "A `string`, either:" followed by the four platform ids.
4831
+ ["editor.platform", '"x86_64-win32" | "x86_64-macos" | "arm64-macos" | "x86_64-linux"'],
4832
+ ]);
4833
+
4834
+ function emitVariable(prepared: PreparedVariable, name: string, mapType: DefoldTypeMapper): string {
3243
4835
  const ts =
3244
4836
  prepared.original.types.length > 0
3245
4837
  ? unionFromTokens(prepared.original.types, mapType)
3246
- : "unknown";
4838
+ : (VARIABLE_TYPE_OVERRIDES.get(prepared.original.name) ?? "unknown");
3247
4839
  return `const ${name}: ${ts};`;
3248
4840
  }
3249
4841
 
3250
4842
  function memberSignature(
3251
4843
  prepared: PreparedFunction,
3252
4844
  name: string,
3253
- mapType: (t: string) => string,
4845
+ mapType: DefoldTypeMapper,
3254
4846
  resolver: TableDocResolver,
3255
4847
  constantTokens: ConstantSlotTokens,
3256
4848
  urlParameters: UrlParameterTable,
3257
- slotSink?: Record<string, string>,
4849
+ slotSink?: Record<string, SlotType>,
3258
4850
  ): string {
3259
4851
  const original = prepared.original.parameters;
3260
4852
  const elementName = prepared.original.name;
@@ -3293,16 +4885,16 @@ function memberSignature(
3293
4885
  if (slotSink !== undefined) {
3294
4886
  for (const [i, p] of positional.entries()) {
3295
4887
  const raw = original[i];
3296
- if (raw) slotSink[slotKey("param", i, raw.name)] = p.ts;
4888
+ if (raw) slotSink[slotKey("param", i, raw.name)] = { type: p.type, optional: p.optional };
3297
4889
  }
3298
4890
  for (const slot of rest?.slots ?? []) {
3299
- slotSink[slotKey("param", slot.position, slot.name)] = slot.ts;
4891
+ slotSink[slotKey("param", slot.position, slot.name)] = { type: slot.ts, optional: false };
3300
4892
  }
3301
4893
  // A predicate renders `arg is T` in place of the mapped `boolean`, so its
3302
4894
  // return slot has no text in the signature to stand on and is not reported.
3303
4895
  if (!isPredicate) {
3304
4896
  for (const slot of ret.slots) {
3305
- slotSink[slotKey("return", slot.position, slot.name)] = slot.ts;
4897
+ slotSink[slotKey("return", slot.position, slot.name)] = { type: slot.ts, optional: false };
3306
4898
  }
3307
4899
  }
3308
4900
  }
@@ -3315,11 +4907,11 @@ function memberSignature(
3315
4907
  function emitFunction(
3316
4908
  prepared: PreparedFunction,
3317
4909
  name: string,
3318
- mapType: (t: string) => string,
4910
+ mapType: DefoldTypeMapper,
3319
4911
  resolver: TableDocResolver,
3320
4912
  constantTokens: ConstantSlotTokens,
3321
4913
  urlParameters: UrlParameterTable,
3322
- slotSink?: Record<string, string>,
4914
+ slotSink?: Record<string, SlotType>,
3323
4915
  ): string {
3324
4916
  return `function ${memberSignature(prepared, name, mapType, resolver, constantTokens, urlParameters, slotSink)}`;
3325
4917
  }
@@ -3329,11 +4921,11 @@ function emitFunction(
3329
4921
  // member carries no `self` parameter.
3330
4922
  function emitMethod(
3331
4923
  prepared: PreparedFunction,
3332
- mapType: (t: string) => string,
4924
+ mapType: DefoldTypeMapper,
3333
4925
  resolver: TableDocResolver,
3334
4926
  constantTokens: ConstantSlotTokens,
3335
4927
  urlParameters: UrlParameterTable,
3336
- slotSink?: Record<string, string>,
4928
+ slotSink?: Record<string, SlotType>,
3337
4929
  ): string {
3338
4930
  return memberSignature(
3339
4931
  prepared,
@@ -3352,57 +4944,103 @@ function emitMethod(
3352
4944
  // fallback applies to non-identifier names and a vararg drops its dots, matching
3353
4945
  // `emitParameter` and `emitRestParameter`) so the tag resolves on hover; a
3354
4946
  // parameter folded into a rest element union keeps its own tag, so the prose
3355
- // documenting it survives. A single documented return becomes `@returns`. Returns `[]`
3356
- // for a fully-undocumented function, leaving its emission byte-identical.
4947
+ // documenting it survives. A single documented return becomes `@returns`; a
4948
+ // multi-return function gains one only when a slot carries a classified index
4949
+ // (see `multiReturnDoc`). A return authored in `RETURN_TYPE_OVERRIDES` has no
4950
+ // ref-doc slot, so it gains `@returns` only for the base notes of its classified
4951
+ // fields (see `overrideReturnDoc`). Returns `[]` for a fully-undocumented
4952
+ // function, leaving its emission byte-identical.
3357
4953
  function functionDocLines(
3358
4954
  fn: ApiFunction,
3359
4955
  translations: TranslationStore,
4956
+ baseNotes: IndexBaseNoteResolver,
3360
4957
  indent: string = INDENT,
3361
4958
  ): string[] {
3362
4959
  const params = fn.parameters.map((p, index) => ({
3363
4960
  name: emittedParamName(p, index),
3364
- doc: rewriteParamDoc(fn.name, p.name, htmlToDocText(p.doc)),
4961
+ doc: appendNotes(
4962
+ translateProseFences(htmlToDocText(p.doc), fn.name, translations),
4963
+ baseNotes(fn.name, "param", p.name),
4964
+ ),
3365
4965
  }));
3366
4966
  const onlyReturn = fn.returnValues.length === 1 ? fn.returnValues[0] : undefined;
4967
+ const notedReturns =
4968
+ multiReturnDoc(fn, translations, baseNotes) ?? overrideReturnDoc(fn, baseNotes);
3367
4969
  const lua = htmlToCodeText(fn.examples ?? "");
3368
- // A blob carrying several examples documents them as several, but only when
3369
- // every segment has an authored body: a partial resolve would drop the rest.
3370
- // Otherwise the whole-blob ladder stands — a hand-authored TS translation
3371
- // pinned to this exact Lua flips the fence to ```ts; any hash mismatch (or
3372
- // absent translation) keeps the Lua fallback.
4970
+ // Each example resolves by its own segment hash, a single-example element's
4971
+ // one segment being the whole blob with no prose. The blob documents its
4972
+ // examples as TypeScript only when every segment has an authored body: a
4973
+ // partial resolve would drop the rest, so any miss keeps the Lua fallback.
3373
4974
  const segments = splitExampleSources(fn.examples ?? "");
3374
- const perSegment =
3375
- segments.length > 1
3376
- ? lookupExampleTranslations(
3377
- translations,
3378
- fn.name,
3379
- segments.map((segment) => hashExampleSource(segment.code)),
3380
- )
3381
- : null;
3382
- const ts = lua === "" ? null : lookupTranslation(translations, fn.name, hashExampleSource(lua));
4975
+ const resolved = lookupExampleSegments(translations, fn.name, segments);
3383
4976
  const exampleParts: Pick<DocCommentParts, "examples"> =
3384
- perSegment !== null
4977
+ resolved !== null
3385
4978
  ? {
3386
- examples: perSegment.map((text, index) => ({
4979
+ examples: resolved.map(({ ts: text, prose }) => ({
3387
4980
  text,
3388
4981
  lang: "ts" as const,
3389
- ...(segments[index]?.prose ? { prose: segments[index]?.prose } : {}),
4982
+ ...(prose ? { prose } : {}),
3390
4983
  })),
3391
4984
  }
3392
- : ts !== null
3393
- ? { examples: [{ text: ts, lang: "ts" }] }
3394
- : lua !== ""
3395
- ? { examples: [{ text: lua, lang: "lua" }] }
3396
- : {};
4985
+ : lua !== ""
4986
+ ? { examples: [{ text: lua, lang: "lua" }] }
4987
+ : {};
3397
4988
  const parts: DocCommentParts = {
3398
- summary: htmlToDocText(summaryFor(fn.brief, fn.description)),
4989
+ summary: translateProseFences(
4990
+ htmlToDocText(summaryFor(fn.brief, fn.description)),
4991
+ fn.name,
4992
+ translations,
4993
+ ),
3399
4994
  params,
3400
- ...(onlyReturn ? { returns: htmlToDocText(onlyReturn.doc) } : {}),
4995
+ ...(onlyReturn
4996
+ ? {
4997
+ returns: appendNotes(
4998
+ translateProseFences(htmlToDocText(onlyReturn.doc), fn.name, translations),
4999
+ baseNotes(fn.name, "return", onlyReturn.name),
5000
+ ),
5001
+ }
5002
+ : notedReturns !== undefined
5003
+ ? { returns: notedReturns }
5004
+ : {}),
3401
5005
  ...exampleParts,
3402
5006
  };
3403
5007
  return indentDocLines(parts, indent);
3404
5008
  }
3405
5009
 
5010
+ // A multi-return function documents its tuple slot by slot, in tuple order, so
5011
+ // the hover can name each classified index's base. Every other multi-return
5012
+ // function keeps no `@returns`, leaving its emission byte-identical.
5013
+ function multiReturnDoc(
5014
+ fn: ApiFunction,
5015
+ translations: TranslationStore,
5016
+ baseNotes: IndexBaseNoteResolver,
5017
+ ): string | undefined {
5018
+ if (fn.returnValues.length < 2) return undefined;
5019
+ const classified = fn.returnValues.some(
5020
+ (slot) => baseNotes(fn.name, "return", slot.name).length > 0,
5021
+ );
5022
+ if (!classified) return undefined;
5023
+ const bullets = fn.returnValues.map((slot) => {
5024
+ const doc = appendNotes(
5025
+ translateProseFences(htmlToDocText(slot.doc), fn.name, translations),
5026
+ baseNotes(fn.name, "return", slot.name),
5027
+ );
5028
+ const [first = "", ...rest] = doc.split("\n");
5029
+ return [
5030
+ `- \`${slot.name}\` — ${first}`,
5031
+ ...rest.map((line) => (line === "" ? "" : ` ${line}`)),
5032
+ ];
5033
+ });
5034
+ const names = fn.returnValues.map((slot) => slot.name).join(", ");
5035
+ return [`\`[${names}]\`:`, ...bullets.flat()].join("\n");
5036
+ }
5037
+
5038
+ function overrideReturnDoc(fn: ApiFunction, baseNotes: IndexBaseNoteResolver): string | undefined {
5039
+ if (fn.returnValues.length > 0 || !RETURN_TYPE_OVERRIDES.has(fn.name)) return undefined;
5040
+ const notes = baseNotes(fn.name, "return", OVERRIDE_RETURN_SLOT);
5041
+ return notes.length > 0 ? notes.join(" ") : undefined;
5042
+ }
5043
+
3406
5044
  // Summary-only doc lines for a member that carries no params or returns
3407
5045
  // (constants, variables, `properties` members). Reuses the same renderer and
3408
5046
  // summary precedence as functions; returns `[]` for an undocumented member so
@@ -3412,11 +5050,11 @@ function summaryDocLines(brief: string, description: string, indent: string): st
3412
5050
  return indentDocLines({ summary: htmlToDocText(summaryFor(brief, description)) }, indent);
3413
5051
  }
3414
5052
 
3415
- function namespaceDocLines(module: ApiModule): string[] {
5053
+ function namespaceDocLines(module: ApiModule, translations: TranslationStore): string[] {
3416
5054
  const official = summaryFor(module.brief, module.description).trim();
3417
5055
  const summary =
3418
5056
  official.length > 0
3419
- ? htmlToDocText(official)
5057
+ ? translateProseFences(htmlToDocText(official), module.namespace, translations)
3420
5058
  : `(synthesized)\nDefold \`${module.namespace}\` API namespace.`;
3421
5059
  return indentDocLines({ summary }, "");
3422
5060
  }
@@ -3425,13 +5063,6 @@ function indentDocLines(parts: DocCommentParts, indent: string): string[] {
3425
5063
  return renderDocComment(parts).map((line) => `${indent}${line}`);
3426
5064
  }
3427
5065
 
3428
- // Prefer the full `description`; fall back to the one-line `brief` when prose is
3429
- // absent. Shared by every documented member kind so the summary source is
3430
- // consistent across functions, constants, variables, and properties.
3431
- export function summaryFor(brief: string, description: string): string {
3432
- return description.trim() !== "" ? description : brief;
3433
- }
3434
-
3435
5066
  export function isMarkedOptional(p: ApiParameter): boolean {
3436
5067
  return p.isOptional || p.types.includes("nil");
3437
5068
  }
@@ -3445,6 +5076,7 @@ export function isMarkedOptional(p: ApiParameter): boolean {
3445
5076
  // second copy of the rule. The fidelity audit reads it too, to tell which slots
3446
5077
  // the emitted surface still makes required.
3447
5078
  export function isDocOptional(p: ApiParameter, elementName: string): boolean {
5079
+ if (requiredSlotCorrection(p, elementName)) return false;
3448
5080
  if (isMarkedOptional(p)) return true;
3449
5081
  return OPTIONAL_SLOT_CORRECTIONS.has(tableSlotKey(elementName, "param", p.name));
3450
5082
  }
@@ -3472,7 +5104,7 @@ const SCENE_ADDRESS_ALIASES: Readonly<Record<string, string>> = {
3472
5104
  // becomes the matching scene-derived alias. Wrapping `mapType` rather than
3473
5105
  // rewriting the finished union leaves `mapSlotUnion`'s member ordering and
3474
5106
  // de-duplication in charge, so only the one token moves.
3475
- function addressMapType(mapType: (t: string) => string, alias: string): (t: string) => string {
5107
+ function addressMapType(mapType: DefoldTypeMapper, alias: string): (t: string) => string {
3476
5108
  return (token) => (token === "string" ? alias : mapType(token));
3477
5109
  }
3478
5110
 
@@ -3481,17 +5113,20 @@ function addressMapType(mapType: (t: string) => string, alias: string): (t: stri
3481
5113
  // type alone.
3482
5114
  function parameterType(
3483
5115
  p: ApiParameter,
3484
- mapType: (t: string) => string,
5116
+ mapType: DefoldTypeMapper,
3485
5117
  resolver: TableDocResolver,
3486
5118
  constantTokens: ConstantSlotTokens,
3487
5119
  elementName: string,
3488
5120
  urlParameters: UrlParameterTable,
3489
5121
  ): string {
3490
5122
  const concrete = p.types.filter((t) => t !== "nil");
5123
+ // The caller supplies a parameter, so a string-keyed table in it also takes an
5124
+ // object literal.
5125
+ const inputMapType = (token: string): string => mapType(token, "input");
3491
5126
  // The table is keyed by the *raw* ref-doc parameter name, not the emitted
3492
5127
  // `safeParamName` fallback.
3493
5128
  const alias = SCENE_ADDRESS_ALIASES[classifyUrlParameter(urlParameters, elementName, p.name)];
3494
- const slotMapType = alias === undefined ? mapType : addressMapType(mapType, alias);
5129
+ const slotMapType = alias === undefined ? inputMapType : addressMapType(inputMapType, alias);
3495
5130
  const mapped =
3496
5131
  concrete.length > 0
3497
5132
  ? mapSlotUnion(
@@ -3507,7 +5142,55 @@ function parameterType(
3507
5142
  )
3508
5143
  : "unknown";
3509
5144
  const correction = PARAM_TYPE_CORRECTIONS.get(tableSlotKey(elementName, "param", p.name));
3510
- return correction === undefined ? mapped : `${mapped} | ${correction.adds}`;
5145
+ if (correction === undefined || !pinMatches(p.types, correctionPins(correction))) return mapped;
5146
+ const kept =
5147
+ correction.removes === undefined ? mapped : withoutUnionMembers(mapped, correction.removes);
5148
+ if (correction.adds === undefined) return kept;
5149
+ if (kept === "") return correction.adds;
5150
+ const present = new Set(unionMembers(kept));
5151
+ const added = unionMembers(correction.adds).filter((member) => !present.has(member));
5152
+ return added.length === 0 ? kept : `${kept} | ${added.join(" | ")}`;
5153
+ }
5154
+
5155
+ function isFunctionTypeText(ts: string): boolean {
5156
+ let depth = 0;
5157
+ for (let i = 0; i < ts.length - 1; i++) {
5158
+ const c = ts[i];
5159
+ if (c === "(" || c === "[" || c === "{" || c === "<") depth++;
5160
+ else if (c === ")" || c === "]" || c === "}" || (c === ">" && ts[i - 1] !== "=")) depth--;
5161
+ else if (c === "=" && ts[i + 1] === ">" && depth === 0) return true;
5162
+ }
5163
+ return false;
5164
+ }
5165
+
5166
+ // The top-level members of a rendered union, leaving nested unions inside
5167
+ // brackets, braces or generics whole.
5168
+ function unionMembers(union: string): string[] {
5169
+ const members: string[] = [];
5170
+ let depth = 0;
5171
+ let start = 0;
5172
+ for (let i = 0; i < union.length; i++) {
5173
+ const c = union[i];
5174
+ if (c === "(" || c === "[" || c === "{" || c === "<") depth++;
5175
+ else if (c === ")" || c === "]" || c === "}" || (c === ">" && union[i - 1] !== "=")) depth--;
5176
+ else if (c === "|" && depth === 0) {
5177
+ members.push(union.slice(start, i).trim());
5178
+ start = i + 1;
5179
+ }
5180
+ }
5181
+ members.push(union.slice(start).trim());
5182
+ return members;
5183
+ }
5184
+
5185
+ // Drops whole top-level members from a rendered union. Called only once the
5186
+ // slot's pin matched, so a removal that finds nothing is a real defect.
5187
+ function withoutUnionMembers(union: string, removed: readonly string[]): string {
5188
+ const members = unionMembers(union);
5189
+ const kept = members.filter((member) => !removed.includes(member));
5190
+ if (kept.length === members.length) {
5191
+ throw new Error(`withoutUnionMembers: none of ${removed.join(", ")} is a member of ${union}`);
5192
+ }
5193
+ return kept.join(" | ");
3511
5194
  }
3512
5195
 
3513
5196
  const VARARG_PREFIX = "...";
@@ -3536,7 +5219,7 @@ function emittedParamName(p: ApiParameter, index: number): string {
3536
5219
  function emitRestParameter(
3537
5220
  params: readonly ApiParameter[],
3538
5221
  varargIndex: number,
3539
- mapType: (t: string) => string,
5222
+ mapType: DefoldTypeMapper,
3540
5223
  resolver: TableDocResolver,
3541
5224
  constantTokens: ConstantSlotTokens,
3542
5225
  elementName: string,
@@ -3570,25 +5253,29 @@ function emitParameter(
3570
5253
  p: ApiParameter,
3571
5254
  index: number,
3572
5255
  optional: boolean,
3573
- mapType: (t: string) => string,
5256
+ mapType: DefoldTypeMapper,
3574
5257
  resolver: TableDocResolver,
3575
5258
  constantTokens: ConstantSlotTokens,
3576
5259
  elementName: string,
3577
5260
  urlParameters: UrlParameterTable,
3578
- ): { text: string; ts: string } {
5261
+ ): { text: string; type: string; optional: boolean } {
3579
5262
  const name = safeParamName(p.name, index);
3580
5263
  const ts = parameterType(p, mapType, resolver, constantTokens, elementName, urlParameters);
3581
5264
  // An interior doc-optional param (a required param follows, so the trailing-`?`
3582
5265
  // projection cannot mark it) keeps its optionality as `| undefined` — TSTL
3583
5266
  // lowers `undefined` to `nil`, the faithful call. Trailing optionals keep the
3584
5267
  // `?` form; required params are untouched.
3585
- const interiorOptional = !optional && isDocOptional(p, elementName) ? " | undefined" : "";
3586
- return { text: `${name}${optional ? "?" : ""}: ${ts}${interiorOptional}`, ts };
5268
+ const nilRequired = requiredSlotCorrection(p, elementName)?.nil === true;
5269
+ const interiorOptional = !optional && (isDocOptional(p, elementName) || nilRequired);
5270
+ // `(a) => void | undefined` reads as a function returning `void | undefined`.
5271
+ const operand = interiorOptional && isFunctionTypeText(ts) ? `(${ts})` : ts;
5272
+ const type = `${operand}${interiorOptional ? " | undefined" : ""}`;
5273
+ return { text: `${name}${optional ? "?" : ""}: ${type}`, type, optional };
3587
5274
  }
3588
5275
 
3589
5276
  function emitReturn(
3590
5277
  returnValues: ApiParameter[],
3591
- mapType: (t: string) => string,
5278
+ mapType: DefoldTypeMapper,
3592
5279
  resolver: TableDocResolver,
3593
5280
  constantTokens: ConstantSlotTokens,
3594
5281
  elementName: string,
@@ -3610,19 +5297,23 @@ function emitReturn(
3610
5297
  position: index,
3611
5298
  name: rv.name,
3612
5299
  ts:
3613
- rv.types.length > 0
3614
- ? mapSlotUnion(
3615
- rv.types,
3616
- rv.doc,
3617
- mapType,
3618
- false,
3619
- resolver,
3620
- constantTokens,
3621
- elementName,
3622
- "return",
3623
- rv.name,
3624
- )
3625
- : "unknown",
5300
+ correction?.slot !== undefined &&
5301
+ correction.slot === rv.name &&
5302
+ pinMatches(rv.types, correctionPins(correction))
5303
+ ? correction.ts
5304
+ : rv.types.length > 0
5305
+ ? mapSlotUnion(
5306
+ rv.types,
5307
+ rv.doc,
5308
+ mapType,
5309
+ false,
5310
+ resolver,
5311
+ constantTokens,
5312
+ elementName,
5313
+ "return",
5314
+ rv.name,
5315
+ )
5316
+ : "unknown",
3626
5317
  }));
3627
5318
  return {
3628
5319
  type: `LuaMultiReturn<[${emitted.map((slot) => slot.ts).join(", ")}]>`,
@@ -3632,7 +5323,11 @@ function emitReturn(
3632
5323
  }
3633
5324
  const first = returnValues[0];
3634
5325
  if (!first) return { type: "void", trailing: "", slots: [] };
3635
- if (correction !== undefined) {
5326
+ if (
5327
+ correction !== undefined &&
5328
+ correction.slot === undefined &&
5329
+ pinMatches(first.types, correctionPins(correction))
5330
+ ) {
3636
5331
  return {
3637
5332
  type: correction.ts,
3638
5333
  trailing: "",
@@ -3653,12 +5348,22 @@ function emitReturn(
3653
5348
  first.name,
3654
5349
  )
3655
5350
  : "unknown";
5351
+ // A LuaLS vararg return (`...`) is every value the call returns, however many.
5352
+ if (first.name === "...") {
5353
+ const type = `LuaMultiReturn<${needsArrayParens(ts) ? `(${ts})` : ts}[]>`;
5354
+ return { type, trailing: "", slots: [{ position: 0, name: first.name, ts }] };
5355
+ }
3656
5356
  return { type: ts, trailing: "", slots: [{ position: 0, name: first.name, ts }] };
3657
5357
  }
3658
5358
 
5359
+ // The mark upstream opens a property's description with when the engine refuses
5360
+ // to write it: `go.set` raises "Unable to set the property ... since it is read
5361
+ // only".
5362
+ const READ_ONLY_MARK = /^\s*<span class="mark">READ ONLY<\/span>/;
5363
+
3659
5364
  function emitPropertyMembers(
3660
5365
  p: ApiProperty,
3661
- mapType: (t: string) => string,
5366
+ mapType: DefoldTypeMapper,
3662
5367
  namespace: string,
3663
5368
  ): readonly string[] {
3664
5369
  const keyRange = PROPERTY_KEY_RANGES.get(`${namespace}.${p.name}`);
@@ -3671,12 +5376,13 @@ function emitPropertyMembers(
3671
5376
  }
3672
5377
  const correction = PROPERTY_TYPE_CORRECTIONS.get(`${namespace}.${p.name}`);
3673
5378
  const ts =
3674
- correction !== undefined
5379
+ correction !== undefined && pinMatches(p.types, propertyCorrectionPins(correction))
3675
5380
  ? correction.ts
3676
5381
  : p.types.length > 0
3677
5382
  ? unionFromTokens(p.types, mapType)
3678
5383
  : "unknown";
3679
- return keys.map((key) => `${key}: ${ts};`);
5384
+ const modifier = READ_ONLY_MARK.test(p.description) ? "readonly " : "";
5385
+ return keys.map((key) => `${modifier}${key}: ${ts};`);
3680
5386
  }
3681
5387
 
3682
5388
  // Like `unionFromTokens`, but a `table` token whose slot doc carries a parseable
@@ -3689,7 +5395,7 @@ function emitPropertyMembers(
3689
5395
  function mapSlotUnion(
3690
5396
  types: readonly string[],
3691
5397
  doc: string,
3692
- mapType: (t: string) => string,
5398
+ mapType: DefoldTypeMapper,
3693
5399
  optionalFields: boolean,
3694
5400
  resolver: TableDocResolver,
3695
5401
  constantTokens: ConstantSlotTokens,
@@ -3713,11 +5419,12 @@ function mapSlotUnion(
3713
5419
  }
3714
5420
  }
3715
5421
  let ts: string;
3716
- if (token === "table") {
3717
- const curation =
3718
- slotKind !== undefined && slotName !== undefined
3719
- ? TABLE_SLOT_CURATIONS.get(tableSlotKey(elementName, slotKind, slotName))
3720
- : undefined;
5422
+ const slotCuration =
5423
+ slotKind !== undefined && slotName !== undefined
5424
+ ? TABLE_SLOT_CURATIONS.get(tableSlotKey(elementName, slotKind, slotName))
5425
+ : undefined;
5426
+ if (token === "table" || slotCuration?.retypedUpstream?.includes(token) === true) {
5427
+ const curation = slotCuration;
3721
5428
  const mapping =
3722
5429
  curation?.kind === "mapping" ? curation : MAPPING_TABLE_SLOTS.get(elementName);
3723
5430
  const element =
@@ -3758,8 +5465,16 @@ function mapSlotUnion(
3758
5465
  }
3759
5466
  } else if (element !== undefined) {
3760
5467
  ts = arrayTypeFromTokens(element, mapType);
5468
+ } else if (curation?.kind === "verbatim") {
5469
+ ts = curation.ts;
3761
5470
  } else if (curation?.kind === "object" || curation?.kind === "array-object") {
3762
- const object = inlineTableType(curation.fields, mapType, optionalFields);
5471
+ const fields = applyRequiredFieldCorrections(
5472
+ elementName,
5473
+ slotKind,
5474
+ slotName,
5475
+ curation.fields,
5476
+ );
5477
+ const object = inlineTableType(fields, mapType, optionalFields);
3763
5478
  ts = curation.kind === "array-object" ? `${object}[]` : object;
3764
5479
  } else if (curation?.kind === "keyed-object") {
3765
5480
  // The doc's `<dl>` describes the value shape; parse it and re-key by an
@@ -3785,7 +5500,13 @@ function mapSlotUnion(
3785
5500
  const added = applyFieldAdditions(elementName, slotKind, slotName, parsed);
3786
5501
  const nested = applyNestedFieldCurations(elementName, slotKind, slotName, added);
3787
5502
  const typed = applyFieldTypeOverrides(elementName, slotKind, slotName, nested);
3788
- const fields = applyFieldOptionalityCorrections(elementName, slotKind, slotName, typed);
5503
+ const required = applyRequiredFieldCorrections(
5504
+ elementName,
5505
+ slotKind,
5506
+ slotName,
5507
+ applyFieldOptionalityCorrections(elementName, slotKind, slotName, typed),
5508
+ );
5509
+ const fields = applyListFieldCorrections(elementName, slotKind, slotName, required);
3789
5510
  const object = inlineTableType(fields, mapType, optionalFields);
3790
5511
  ts = isSlotLevelList(doc) ? `${object}[]` : object;
3791
5512
  } else {
@@ -3840,18 +5561,21 @@ export function applyNestedFieldCurations(
3840
5561
  ): TableField[] {
3841
5562
  if (slotKind === undefined || slotName === undefined) return [...fields];
3842
5563
  return fields.map((field) => {
3843
- if (field.fields !== undefined) return field;
3844
5564
  const curated = NESTED_FIELD_CURATIONS.get(
3845
5565
  nestedFieldKey(elementName, slotKind, slotName, field.name),
3846
5566
  );
3847
5567
  if (curated === undefined) return field;
3848
- return { ...field, fields: [...curated], isList: true };
5568
+ // A parsed member list keeps its members; the curation supplies the ones
5569
+ // upstream flattened to the slot's top level.
5570
+ const parsed = field.fields ?? [];
5571
+ const missing = curated.filter((member) => !parsed.some((p) => p.name === member.name));
5572
+ return { ...field, fields: [...parsed, ...missing], isList: true };
3849
5573
  });
3850
5574
  }
3851
5575
 
3852
5576
  function arrayTypeFromTokens(
3853
5577
  element: string | readonly string[],
3854
- mapType: (t: string) => string,
5578
+ mapType: DefoldTypeMapper,
3855
5579
  ): string {
3856
5580
  const tokens = typeof element === "string" ? [element] : element;
3857
5581
  return tokens.length > 1
@@ -3861,7 +5585,7 @@ function arrayTypeFromTokens(
3861
5585
 
3862
5586
  export function inlineTableType(
3863
5587
  fields: readonly TableField[],
3864
- mapType: (t: string) => string,
5588
+ mapType: DefoldTypeMapper,
3865
5589
  optionalFields: boolean,
3866
5590
  ): string {
3867
5591
  const members = fields.map((field) => {
@@ -3879,7 +5603,7 @@ export function inlineTableType(
3879
5603
  field.tsType !== undefined
3880
5604
  ? field.tsType
3881
5605
  : field.fields !== undefined
3882
- ? `${inlineTableType(field.fields, mapType, optionalFields)}${field.isList ? "[]" : ""}`
5606
+ ? nestedFieldType(field, inlineTableType(field.fields, mapType, optionalFields))
3883
5607
  : field.numberList === true
3884
5608
  ? "number[]"
3885
5609
  : field.types.length > 0
@@ -3888,12 +5612,21 @@ export function inlineTableType(
3888
5612
  // A field is optional when the whole slot is param-side (optionalFields) or
3889
5613
  // when the curation marks that individual field optional (a return-side
3890
5614
  // variant field present only for some shape kinds, e.g. a b2d shape table).
3891
- return `${key}${optionalFields || field.optional === true ? "?" : ""}: ${ts}`;
5615
+ // A param-side field the engine requires stays required.
5616
+ const optional = (optionalFields && field.required !== true) || field.optional === true;
5617
+ return `${key}${optional ? "?" : ""}: ${ts}`;
3892
5618
  });
3893
5619
  return `{ ${members.join("; ")} }`;
3894
5620
  }
3895
5621
 
3896
- function unionFromTokens(tokens: readonly string[], mapType: (t: string) => string): string {
5622
+ function nestedFieldType(field: TableField, object: string): string {
5623
+ const element = field.alternatives === undefined ? object : `${object} & (${field.alternatives})`;
5624
+ if (field.isList !== true) return element;
5625
+ const rest = field.alternatives === undefined ? element : `(${element})`;
5626
+ return field.nonEmpty === true ? `[${element}, ...${rest}[]]` : `${rest}[]`;
5627
+ }
5628
+
5629
+ function unionFromTokens(tokens: readonly string[], mapType: DefoldTypeMapper): string {
3897
5630
  const mapped: string[] = [];
3898
5631
  const seen = new Set<string>();
3899
5632
  for (const token of tokens) {
@@ -3905,7 +5638,7 @@ function unionFromTokens(tokens: readonly string[], mapType: (t: string) => stri
3905
5638
  return joinUnionArms(mapped);
3906
5639
  }
3907
5640
 
3908
- const BRAND_ARM = /^number & \{ readonly __brand: "([^"]+)" \}$/;
5641
+ const BRAND_ARM = /^(?:number|string) & \{ readonly __brand: "([^"]+)" \}$/;
3909
5642
 
3910
5643
  // Joins already-mapped arms, collapsing the branded ones to their alias when
3911
5644
  // they are exactly a `CONSTANT_UNION_ALIASES` member set. The alias takes the
@@ -3934,20 +5667,70 @@ function joinUnionArms(mapped: readonly string[]): string {
3934
5667
  return out.join(" | ");
3935
5668
  }
3936
5669
 
3937
- export function isKnownDefoldTypeToken(token: string): boolean {
3938
- return (
3939
- token === "nil" ||
3940
- Object.hasOwn(DEFOLD_TYPE_MAP, token) ||
3941
- recoverCallbackSignature(token) !== null
3942
- );
5670
+ const BUILTIN_MESSAGE_NAMES: ReadonlySet<string> = new Set(
5671
+ messagesDoc.messages.map((message) => message.name),
5672
+ );
5673
+ const MESSAGE_REFERENCE = /^message\.[a-z_]+\.([a-z_]+)$/;
5674
+
5675
+ // The leaf vocabulary the LuaLS grammar resolves an engine type expression
5676
+ // against. `message.<ns>.<name>` names a documented message payload, typed only
5677
+ // when `<name>` is one `BuiltinMessages` declares.
5678
+ function resolveDefoldLeaf(leaf: string): string | undefined {
5679
+ if (leaf === "nil") return "undefined";
5680
+ if (Object.hasOwn(DEFOLD_TYPE_MAP, leaf)) return DEFOLD_TYPE_MAP[leaf];
5681
+ const message = MESSAGE_REFERENCE.exec(leaf);
5682
+ const name = message?.[1];
5683
+ if (name !== undefined && BUILTIN_MESSAGE_NAMES.has(name)) return `BuiltinMessages["${name}"]`;
5684
+ return undefined;
3943
5685
  }
3944
5686
 
3945
- export function defaultMapType(token: string): string {
5687
+ // Whole-token lookup and callback recovery run first, so the grammar sees only
5688
+ // what they miss; every pre-1.13.2 token resolves before it is reached.
5689
+ function wholeTokenMapping(token: string): string | undefined {
3946
5690
  if (Object.hasOwn(DEFOLD_TYPE_MAP, token)) {
3947
5691
  const mapped = DEFOLD_TYPE_MAP[token];
3948
5692
  if (typeof mapped === "string") return mapped;
3949
5693
  }
3950
- const callback = recoverCallbackSignature(token);
3951
- if (callback !== null) return callback;
3952
- return "unknown";
5694
+ return recoverCallbackSignature(token) ?? undefined;
5695
+ }
5696
+
5697
+ /** The leaves of `token` that resolve to nothing; empty when the whole token maps. */
5698
+ export function unknownDefoldTypeLeaves(token: string): string[] {
5699
+ if (token === "nil" || wholeTokenMapping(token) !== undefined) return [];
5700
+ if (token.trim() === "") return [token];
5701
+ return mapLualsExpression(token, resolveDefoldLeaf).unknowns;
5702
+ }
5703
+
5704
+ export function isKnownDefoldTypeToken(token: string): boolean {
5705
+ return unknownDefoldTypeLeaves(token).length === 0;
5706
+ }
5707
+
5708
+ export function defaultMapType(token: string, position?: LualsPosition): string {
5709
+ return wholeTokenMapping(token) ?? mapLualsExpression(token, resolveDefoldLeaf, position).ts;
5710
+ }
5711
+
5712
+ /**
5713
+ * The engine mapper extended with `leaves`, resolved ahead of the engine
5714
+ * vocabulary, and `declared` (Lua name -> TS path), consulted after it. A whole
5715
+ * token checks `leaves`, the engine's whole-token mapping, then `declared`; a
5716
+ * leaf inside a composite checks `leaves`, the engine leaves, then `declared`.
5717
+ * So a surface's handle brand (`editor.message`) outranks any declared path.
5718
+ */
5719
+ export function mapTypeWithVocabulary(vocabulary: {
5720
+ leaves?: Readonly<Record<string, string>> | undefined;
5721
+ declared?: ReadonlyMap<string, string> | undefined;
5722
+ }): DefoldTypeMapper {
5723
+ const leaves = vocabulary.leaves ?? {};
5724
+ const declared = vocabulary.declared ?? new Map<string, string>();
5725
+ if (Object.keys(leaves).length === 0 && declared.size === 0) return defaultMapType;
5726
+ const resolveLeaf = (leaf: string): string | undefined =>
5727
+ Object.hasOwn(leaves, leaf) ? leaves[leaf] : (resolveDefoldLeaf(leaf) ?? declared.get(leaf));
5728
+ return (token, position) => {
5729
+ if (Object.hasOwn(leaves, token)) return leaves[token] as string;
5730
+ return (
5731
+ wholeTokenMapping(token) ??
5732
+ declared.get(token) ??
5733
+ mapLualsExpression(token, resolveLeaf, position).ts
5734
+ );
5735
+ };
3953
5736
  }