@defold-typescript/types 0.50.1 → 0.52.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 (166) hide show
  1. package/api-availability.json +4 -58
  2. package/api-signatures.json +1128 -1552
  3. package/api-targets.json +15 -5
  4. package/examples/translations.json +19 -71
  5. package/fixtures/messages_doc.json +20 -39
  6. package/generated/b2d_world.d.ts +10 -10
  7. package/generated/buffer.d.ts +9 -9
  8. package/generated/builtin-messages.d.ts +16 -14
  9. package/generated/bullet3d_collision_object.d.ts +1 -1
  10. package/generated/bullet3d_constraint.d.ts +109 -7
  11. package/generated/bullet3d_shape.d.ts +2 -2
  12. package/generated/collectionfactory.d.ts +1 -1
  13. package/generated/collectionproxy.d.ts +2 -2
  14. package/generated/factory.d.ts +4 -2
  15. package/generated/go.d.ts +114 -220
  16. package/generated/graphics.d.ts +5 -0
  17. package/generated/gui.d.ts +148 -371
  18. package/generated/http.d.ts +4 -2
  19. package/generated/image.d.ts +56 -4
  20. package/generated/kinds/gui-script.d.ts +3 -0
  21. package/generated/kinds/render-script.d.ts +15 -15
  22. package/generated/kinds/script.d.ts +1 -0
  23. package/generated/liveupdate.d.ts +5 -1
  24. package/generated/material.d.ts +3 -3
  25. package/generated/model.d.ts +4 -5
  26. package/generated/particlefx.d.ts +20 -1
  27. package/generated/physics.d.ts +48 -4
  28. package/generated/render-script-modules/b2d.d.ts +711 -0
  29. package/generated/render-script-modules/bullet3d.d.ts +73 -0
  30. package/generated/render-script-modules/collectionfactory.d.ts +25 -0
  31. package/generated/render-script-modules/collectionproxy.d.ts +39 -0
  32. package/generated/render-script-modules/factory.d.ts +25 -0
  33. package/generated/render-script-modules/go.d.ts +436 -0
  34. package/generated/render-script-modules/label.d.ts +105 -0
  35. package/generated/render-script-modules/model.d.ts +89 -0
  36. package/generated/render-script-modules/particlefx.d.ts +59 -0
  37. package/generated/render-script-modules/physics.d.ts +333 -0
  38. package/generated/render-script-modules/resource.d.ts +1233 -0
  39. package/generated/render-script-modules/sound.d.ts +227 -0
  40. package/generated/render-script-modules/sprite.d.ts +68 -0
  41. package/generated/render-script-modules/tilemap.d.ts +43 -0
  42. package/generated/render.d.ts +175 -21
  43. package/generated/resource.d.ts +31 -10
  44. package/generated/sprite.d.ts +49 -1
  45. package/generated/sys.d.ts +78 -10
  46. package/generated/tilemap.d.ts +4 -4
  47. package/generated/versions/defold-1.12.4/buffer.d.ts +8 -8
  48. package/generated/versions/defold-1.12.4/collectionfactory.d.ts +1 -1
  49. package/generated/versions/defold-1.12.4/collectionproxy.d.ts +1 -1
  50. package/generated/versions/defold-1.12.4/factory.d.ts +4 -2
  51. package/generated/versions/defold-1.12.4/go.d.ts +115 -343
  52. package/generated/versions/defold-1.12.4/graphics.d.ts +1 -0
  53. package/generated/versions/defold-1.12.4/gui.d.ts +151 -513
  54. package/generated/versions/defold-1.12.4/http.d.ts +4 -2
  55. package/generated/versions/defold-1.12.4/image.d.ts +83 -3
  56. package/generated/versions/defold-1.12.4/index.d.ts +2 -0
  57. package/generated/versions/defold-1.12.4/liveupdate.d.ts +4 -0
  58. package/generated/versions/defold-1.12.4/model.d.ts +4 -5
  59. package/generated/versions/defold-1.12.4/particlefx.d.ts +22 -1
  60. package/generated/versions/defold-1.12.4/physics.d.ts +47 -3
  61. package/generated/versions/defold-1.12.4/render.d.ts +119 -15
  62. package/generated/versions/defold-1.12.4/resource.d.ts +43 -10
  63. package/generated/versions/defold-1.12.4/sprite.d.ts +58 -1
  64. package/generated/versions/defold-1.12.4/sys.d.ts +101 -6
  65. package/generated/versions/defold-1.12.4/tilemap.d.ts +4 -4
  66. package/generated/versions/defold-1.12.4/vmath.d.ts +0 -24
  67. package/generated/versions/defold-1.13.0/buffer.d.ts +8 -8
  68. package/generated/versions/defold-1.13.0/collectionfactory.d.ts +1 -1
  69. package/generated/versions/defold-1.13.0/collectionproxy.d.ts +1 -1
  70. package/generated/versions/defold-1.13.0/factory.d.ts +4 -2
  71. package/generated/versions/defold-1.13.0/go.d.ts +115 -370
  72. package/generated/versions/defold-1.13.0/graphics.d.ts +1 -0
  73. package/generated/versions/defold-1.13.0/gui.d.ts +151 -513
  74. package/generated/versions/defold-1.13.0/http.d.ts +4 -2
  75. package/generated/versions/defold-1.13.0/image.d.ts +83 -3
  76. package/generated/versions/defold-1.13.0/index.d.ts +2 -0
  77. package/generated/versions/defold-1.13.0/liveupdate.d.ts +4 -0
  78. package/generated/versions/defold-1.13.0/material.d.ts +3 -1
  79. package/generated/versions/defold-1.13.0/model.d.ts +4 -5
  80. package/generated/versions/defold-1.13.0/particlefx.d.ts +22 -1
  81. package/generated/versions/defold-1.13.0/physics.d.ts +47 -3
  82. package/generated/versions/defold-1.13.0/render.d.ts +182 -16
  83. package/generated/versions/defold-1.13.0/resource.d.ts +43 -10
  84. package/generated/versions/defold-1.13.0/sprite.d.ts +58 -1
  85. package/generated/versions/defold-1.13.0/sys.d.ts +101 -6
  86. package/generated/versions/defold-1.13.0/tilemap.d.ts +4 -4
  87. package/generated/versions/defold-1.13.0/vmath.d.ts +0 -24
  88. package/generated/versions/defold-1.13.1/buffer.d.ts +8 -8
  89. package/generated/versions/defold-1.13.1/collectionfactory.d.ts +1 -1
  90. package/generated/versions/defold-1.13.1/collectionproxy.d.ts +1 -1
  91. package/generated/versions/defold-1.13.1/factory.d.ts +4 -2
  92. package/generated/versions/defold-1.13.1/go.d.ts +115 -370
  93. package/generated/versions/defold-1.13.1/graphics.d.ts +1 -0
  94. package/generated/versions/defold-1.13.1/gui.d.ts +151 -529
  95. package/generated/versions/defold-1.13.1/http.d.ts +4 -2
  96. package/generated/versions/defold-1.13.1/image.d.ts +83 -3
  97. package/generated/versions/defold-1.13.1/index.d.ts +3 -0
  98. package/generated/versions/defold-1.13.1/liveupdate.d.ts +4 -0
  99. package/generated/versions/defold-1.13.1/material.d.ts +3 -1
  100. package/generated/versions/defold-1.13.1/model.d.ts +4 -5
  101. package/generated/versions/defold-1.13.1/particlefx.d.ts +22 -1
  102. package/generated/versions/defold-1.13.1/physics.d.ts +47 -3
  103. package/generated/versions/defold-1.13.1/render.d.ts +182 -16
  104. package/generated/versions/defold-1.13.1/resource.d.ts +43 -10
  105. package/generated/versions/defold-1.13.1/sprite.d.ts +58 -1
  106. package/generated/versions/defold-1.13.1/sys.d.ts +101 -6
  107. package/generated/versions/defold-1.13.1/tilemap.d.ts +4 -4
  108. package/generated/versions/defold-1.13.1/vmath.d.ts +0 -24
  109. package/generated/versions/defold-1.13.2/b2d_world.d.ts +10 -10
  110. package/generated/versions/defold-1.13.2/buffer.d.ts +9 -9
  111. package/generated/versions/defold-1.13.2/bullet3d_collision_object.d.ts +1 -1
  112. package/generated/versions/defold-1.13.2/bullet3d_constraint.d.ts +109 -7
  113. package/generated/versions/defold-1.13.2/bullet3d_shape.d.ts +2 -2
  114. package/generated/versions/defold-1.13.2/collectionfactory.d.ts +1 -1
  115. package/generated/versions/defold-1.13.2/collectionproxy.d.ts +2 -2
  116. package/generated/versions/defold-1.13.2/factory.d.ts +4 -2
  117. package/generated/versions/defold-1.13.2/go.d.ts +114 -220
  118. package/generated/versions/defold-1.13.2/graphics.d.ts +1 -0
  119. package/generated/versions/defold-1.13.2/gui.d.ts +148 -371
  120. package/generated/versions/defold-1.13.2/http.d.ts +4 -2
  121. package/generated/versions/defold-1.13.2/image.d.ts +56 -4
  122. package/generated/versions/defold-1.13.2/index.d.ts +3 -0
  123. package/generated/versions/defold-1.13.2/liveupdate.d.ts +5 -1
  124. package/generated/versions/defold-1.13.2/material.d.ts +3 -3
  125. package/generated/versions/defold-1.13.2/model.d.ts +4 -5
  126. package/generated/versions/defold-1.13.2/particlefx.d.ts +20 -1
  127. package/generated/versions/defold-1.13.2/physics.d.ts +48 -4
  128. package/generated/versions/defold-1.13.2/render.d.ts +164 -20
  129. package/generated/versions/defold-1.13.2/resource.d.ts +31 -10
  130. package/generated/versions/defold-1.13.2/sprite.d.ts +49 -1
  131. package/generated/versions/defold-1.13.2/sys.d.ts +78 -10
  132. package/generated/versions/defold-1.13.2/tilemap.d.ts +4 -4
  133. package/generated/versions/defold-1.13.2/vmath.d.ts +0 -24
  134. package/generated/versions/defold-1.13.2/window.d.ts +1 -1
  135. package/generated/vmath.d.ts +0 -24
  136. package/generated/window.d.ts +1 -1
  137. package/identity-vocabulary.json +98 -7
  138. package/index.d.ts +3 -0
  139. package/package.json +3 -1
  140. package/scripts/generate-api-availability.ts +6 -22
  141. package/scripts/generate-api-signatures.ts +87 -21
  142. package/scripts/materialize-version.ts +53 -6
  143. package/scripts/regen.ts +256 -44
  144. package/scripts/signature-store-fs.ts +1 -0
  145. package/scripts/target-version.ts +26 -0
  146. package/src/api-doc.ts +26 -0
  147. package/src/component-properties.d.ts +226 -6
  148. package/src/core-types.ts +82 -20
  149. package/src/doc-corrections.ts +53 -5
  150. package/src/emit-dts.ts +1300 -269
  151. package/src/emit-messages.ts +289 -0
  152. package/src/engine-globals.d.ts +8 -2
  153. package/src/go-overloads.d.ts +80 -15
  154. package/src/gui-overloads.d.ts +489 -0
  155. package/src/gui-texture-removal.d.ts +41 -0
  156. package/src/index.ts +2 -0
  157. package/src/lifecycle.ts +46 -37
  158. package/src/luals-type-expr.ts +6 -1
  159. package/src/msg-overloads.d.ts +25 -13
  160. package/src/physics-event-guard.d.ts +39 -0
  161. package/src/render-overloads.d.ts +2 -2
  162. package/src/return-correction-provenance.ts +36 -6
  163. package/src/signature-store.ts +50 -0
  164. package/src/timers.d.ts +2 -1
  165. package/src/vmath-overloads.d.ts +26 -0
  166. package/src/window-event-guard.d.ts +5 -3
package/src/emit-dts.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  import messagesDoc from "../fixtures/messages_doc.json" with { type: "json" };
2
- import { type ApiSymbolIdentity, normalizedFunctionSignature } from "./api-availability";
2
+ import {
3
+ type ApiSymbolIdentity,
4
+ type Box2dBackend,
5
+ normalizedFunctionSignature,
6
+ } from "./api-availability";
3
7
  import type {
4
8
  ApiConstant,
5
9
  ApiFunction,
@@ -21,6 +25,7 @@ import {
21
25
  splitExampleSources,
22
26
  summaryFor,
23
27
  } from "./doc-comment";
28
+ import { applyMessageOverlays, parseMessagesDoc } from "./emit-messages";
24
29
  import type { TranslationStore } from "./example-store";
25
30
  import { lookupExampleSegments, translateProseFences } from "./example-store";
26
31
  import {
@@ -30,7 +35,13 @@ import {
30
35
  indexBaseNoteResolver,
31
36
  OVERRIDE_RETURN_SLOT,
32
37
  } from "./index-slot-classifications";
33
- import { type LualsPosition, mapLualsExpression, needsArrayParens } from "./luals-type-expr";
38
+ import {
39
+ isLualsFunctionToken,
40
+ type LualsPosition,
41
+ mapLualsExpression,
42
+ needsArrayParens,
43
+ splitTopLevel,
44
+ } from "./luals-type-expr";
34
45
  import { classifyUrlParameter, type UrlParameterTable } from "./url-parameters";
35
46
 
36
47
  export { summaryFor };
@@ -75,6 +86,21 @@ export interface EmitOptions {
75
86
  // `UNDOCUMENTED_CONSTANTS` back-fill. Absent for a surface with no vendored
76
87
  // bindings (a ref-doc target), where every entry the ref-doc omits is emitted.
77
88
  registeredConstants?: ReadonlySet<string>;
89
+ // The `RETURN_TYPE_CORRECTIONS` and `STRUCT_MEMBER_ADDITIONS` keys whose binding
90
+ // pin holds in the target's vendored engine source. A binding-pinned entry
91
+ // applies only when its key is here, so a surface with no vendored bindings
92
+ // (absent) applies none of them.
93
+ bindingEvidence?: ReadonlySet<string>;
94
+ // The 1-based parameter slots, keyed by function FQN, that the target's engine
95
+ // bindings let a call omit but raise on when passed nil. Each optional one
96
+ // splits its function into a call without the slot and a call requiring it,
97
+ // so a possibly-undefined value is a compile error. Absent for a surface with
98
+ // no vendored bindings (a ref-doc target), which keeps every `?`.
99
+ nilRaisingSlots?: ReadonlyMap<string, readonly number[]>;
100
+ // The functions a target's skip rules removed from the module, read only to
101
+ // type an `OVERLOAD_SLOT_ALIASES` slot and to resolve a
102
+ // `CONSTANT_SLOT_RESOLUTIONS` borrow from their docs.
103
+ withheldFunctions?: readonly ApiFunction[];
78
104
  }
79
105
 
80
106
  export const TS_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
@@ -165,14 +191,12 @@ export const ARBITRARY_TABLE_SLOTS = new Set([
165
191
  // runtime-owned passthrough: the table is filled by user code at runtime, never
166
192
  // by Defold's API contract. factory.create/collectionfactory.create `properties`
167
193
  // are spawn-time overrides keyed by the spawned object's own script-property
168
- // names; bare `on_message`'s receive `message` is the pre-hashed, user-routed
169
- // payload (mitigated for authored code by isMessage/onMessage). No static shape
170
- // exists, so `Record<string | number, unknown>` is faithful, not a loss.
171
- // collectionfactory.create curates the instance-id layer of `properties` as a
172
- // mapping; each per-instance property table stays that faithful `Record`.
194
+ // names. No static shape exists, so `Record<string | number, unknown>` is
195
+ // faithful, not a loss. collectionfactory.create curates the instance-id layer
196
+ // of `properties` as a mapping; each per-instance property table stays that
197
+ // faithful `Record`.
173
198
  "factory.create",
174
199
  "collectionfactory.create",
175
- "on_message",
176
200
  // engine-formatted blob: the engine produces the value at runtime and no
177
201
  // field list is documented, so the whole-slot `Record` is faithful, not a
178
202
  // loss. crash.get_backtrace is "table containing the backtrace",
@@ -245,6 +269,11 @@ export const OVERLOAD_COVERED_SKIPS = new Set([
245
269
  "go.get",
246
270
  "go.set",
247
271
  "go.property",
272
+ // gui-overloads.d.ts closes the node property names and types each value per
273
+ // name, with a hash form for material constants.
274
+ "gui.animate",
275
+ "gui.get",
276
+ "gui.set",
248
277
  // msg-overloads.d.ts supplies the typed msg.post shape.
249
278
  "msg.post",
250
279
  // msg-overloads.d.ts supplies the typed msg.url shape (0 / 1-string / 3-arg
@@ -263,6 +292,7 @@ export const OVERLOAD_COVERED_SKIPS = new Set([
263
292
  "vmath.slerp",
264
293
  "vmath.mul_per_elem",
265
294
  "vmath.normalize",
295
+ "vmath.dot",
266
296
  // sys-overloads.d.ts supplies the disabling form and the form that takes the
267
297
  // cooldown the engine requires when enabling.
268
298
  "sys.set_engine_throttle",
@@ -320,13 +350,10 @@ export const MAPPING_TABLE_SLOTS: ReadonlyMap<string, { key: string; value: stri
320
350
  ]);
321
351
 
322
352
  // FQN-keyed return-type overrides for functions whose doc `returnvalues` are
323
- // empty yet the engine returns a value. `gui.get`'s doc lists no return, but it
324
- // yields the property value (a vmath.vector4 or a number); `unknown` is the
325
- // honest, ts-defold-matching shape. Per-FQN, not a blanket "empty returnvalues
326
- // -> unknown" rule: `gui.set` also has empty returnvalues and `void` is correct
327
- // there.
353
+ // empty yet the engine returns a value. Per-FQN, not a blanket "empty
354
+ // returnvalues -> unknown" rule: most functions with empty returnvalues return
355
+ // nothing, and `void` is correct there.
328
356
  export const RETURN_TYPE_OVERRIDES: ReadonlyMap<string, string> = new Map([
329
- ["gui.get", "unknown"],
330
357
  // Upstream documents the returned transaction step in prose only — its
331
358
  // `returnvalues` is empty while every sibling `editor.tx.*` builder declares
332
359
  // one, and `editor.transact()` takes exactly that.
@@ -362,8 +389,22 @@ export const RETURN_TYPE_OVERRIDES: ReadonlyMap<string, string> = new Map([
362
389
  // destructured into separate arguments; a boxed `LuaMultiReturn` is rejected
363
390
  // because TSTL passes it as one table, which is always truthy. `reason` quotes
364
391
  // the slot prose the provenance test searches for, as for the `nil` entries.
365
- export interface ReturnTypeCorrection {
366
- readonly ts: string;
392
+ //
393
+ // An entry with a `binding` rests on the engine source instead of the prose:
394
+ // it applies only on a target whose vendored binding shows the pin (the
395
+ // `EmitOptions.bindingEvidence` keys), and `reason` names that binding in words.
396
+ // A nil push proves nil-ability and nothing about the base type, so such an
397
+ // entry usually carries `nullable` rather than `ts`.
398
+ export interface BindingPin {
399
+ // Relative to the target's vendored `engine/` tree.
400
+ readonly file: string;
401
+ readonly cFunction: string;
402
+ // Token snippets the function body must contain and must lack; at least one.
403
+ readonly contains?: string;
404
+ readonly lacks?: string;
405
+ }
406
+
407
+ interface ReturnCorrectionBase {
367
408
  readonly upstream: readonly string[];
368
409
  // A later release's spelling of the same defect; see `correction-pins.ts`.
369
410
  readonly retypedUpstream?: readonly (readonly string[])[];
@@ -371,10 +412,33 @@ export interface ReturnTypeCorrection {
371
412
  // The named value of a multi-return the correction rewrites; `ts` is then
372
413
  // that value's type. Absent, the correction rewrites a single return.
373
414
  readonly slot?: string;
415
+ readonly binding?: BindingPin;
416
+ }
417
+
418
+ // `ts` replaces the emitted type. `nullable` keeps the type each target maps
419
+ // from its own tokens and only adds or drops the `nil`, so a target whose
420
+ // spelling differs keeps its own base.
421
+ export type ReturnTypeCorrection = ReturnCorrectionBase &
422
+ (
423
+ | { readonly ts: string; readonly nullable?: undefined }
424
+ | { readonly nullable: boolean; readonly ts?: undefined }
425
+ );
426
+
427
+ // The early nil return both tilemap readers take for an unknown layer, and
428
+ // `TileMap_Get` for a cell out of range too.
429
+ function tilemapEarlyNil(cFunction: string): BindingPin {
430
+ return {
431
+ file: "gamesys/src/gamesys/scripts/script_tilemap.cpp",
432
+ cFunction,
433
+ contains: "lua_pushnil(L); assert(top + 1 == lua_gettop(L)); return 1;",
434
+ };
374
435
  }
375
436
 
376
437
  // Keyed by FQN, mirroring PROPERTY_TYPE_CORRECTIONS.
377
- export const RETURN_TYPE_CORRECTIONS: ReadonlyMap<string, ReturnTypeCorrection> = new Map([
438
+ export const RETURN_TYPE_CORRECTIONS: ReadonlyMap<string, ReturnTypeCorrection> = new Map<
439
+ string,
440
+ ReturnTypeCorrection
441
+ >([
378
442
  [
379
443
  "b2d.get_body",
380
444
  {
@@ -408,8 +472,8 @@ export const RETURN_TYPE_CORRECTIONS: ReadonlyMap<string, ReturnTypeCorrection>
408
472
  slot: "code",
409
473
  },
410
474
  ],
411
- // 1.13.2 declares `render_predicate`, whose TYPEDEF aliases `userdata`, and
412
- // that still emits `number`.
475
+ // 1.13.2 retypes the return to `render_predicate`, whose TYPEDEF aliases
476
+ // `userdata`; its type mapping emits the same brand.
413
477
  [
414
478
  "render.predicate",
415
479
  {
@@ -439,6 +503,161 @@ export const RETURN_TYPE_CORRECTIONS: ReadonlyMap<string, ReturnTypeCorrection>
439
503
  reason: "the customized try function.",
440
504
  },
441
505
  ],
506
+ [
507
+ "factory.create",
508
+ {
509
+ nullable: true,
510
+ upstream: ["hash"],
511
+ reason: "script_factory.cpp:FactoryComp_Create pushes nil when the spawn fails",
512
+ binding: {
513
+ file: "gamesys/src/gamesys/scripts/script_factory.cpp",
514
+ cFunction: "FactoryComp_Create",
515
+ contains: "dmScript::PushHash(L, id); } else { lua_pushnil(L); }",
516
+ },
517
+ },
518
+ ],
519
+ // 1.13.2 retypes each tilemap return to a precise spelling of the same value
520
+ // and still omits the nil.
521
+ ...(
522
+ [
523
+ ["tilemap.get_tile", ["number"], ["integer"]],
524
+ [
525
+ "tilemap.get_tile_info",
526
+ ["table"],
527
+ ["{ index:integer, h_flip:boolean, v_flip:boolean, rotate_90:boolean }"],
528
+ ],
529
+ ] as const
530
+ ).map(
531
+ ([key, upstream, retyped]) =>
532
+ [
533
+ key,
534
+ {
535
+ nullable: true,
536
+ upstream,
537
+ retypedUpstream: [retyped],
538
+ reason:
539
+ "script_tilemap.cpp:TileMap_Get pushes nil for an unknown layer or a cell out of range",
540
+ binding: tilemapEarlyNil("TileMap_Get"),
541
+ },
542
+ ] as const,
543
+ ),
544
+ [
545
+ "tilemap.get_tiles",
546
+ {
547
+ nullable: true,
548
+ upstream: ["table"],
549
+ retypedUpstream: [["table<integer, table<integer, integer>>"]],
550
+ reason: "script_tilemap.cpp:TileMap_GetTiles pushes nil for an unknown layer",
551
+ binding: tilemapEarlyNil("TileMap_GetTiles"),
552
+ },
553
+ ],
554
+ [
555
+ "bullet3d.shape.get_shape",
556
+ {
557
+ ts: "bullet3d.shape.definition | { type: typeof bullet3d.shape.SHAPE_TYPE_MESH }",
558
+ upstream: ["bullet3d.shape.definition"],
559
+ reason:
560
+ "script_bullet3d_shape.cpp:Shape_GetShape sets only `type` for a triangle mesh, which no shape definition arm matches",
561
+ binding: {
562
+ file: "gamesys/src/gamesys/scripts/bullet3d/script_bullet3d_shape.cpp",
563
+ cFunction: "Shape_GetShape",
564
+ contains: "case TRIANGLE_MESH_SHAPE_PROXYTYPE: case GIMPACT_SHAPE_PROXYTYPE: break;",
565
+ },
566
+ },
567
+ ],
568
+ [
569
+ "collectionproxy.set_collection",
570
+ {
571
+ nullable: true,
572
+ upstream: ["number"],
573
+ retypedUpstream: [["collectionproxy.RESULT"]],
574
+ reason:
575
+ "script_collectionproxy.cpp:CollectionProxy_SetCollection pushes true, nil when the change succeeds",
576
+ slot: "code",
577
+ binding: {
578
+ file: "gamesys/src/gamesys/scripts/script_collectionproxy.cpp",
579
+ cFunction: "CollectionProxy_SetCollection",
580
+ contains: "case SET_COLLECTION_PATH_RESULT_OK: lua_pushboolean(L, 1); lua_pushnil(L);",
581
+ },
582
+ },
583
+ ],
584
+ [
585
+ "image.get_astc_header",
586
+ {
587
+ nullable: false,
588
+ upstream: ["table", "nil"],
589
+ retypedUpstream: [["image.astc_header", "nil"]],
590
+ reason: "script_image.cpp:Image_GetAstcHeader raises on invalid data and never pushes nil",
591
+ binding: {
592
+ file: "gamesys/src/gamesys/scripts/script_image.cpp",
593
+ cFunction: "Image_GetAstcHeader",
594
+ contains: 'return luaL_error(L, "Data is not a valid .astc file");',
595
+ lacks: "lua_pushnil(L);",
596
+ },
597
+ },
598
+ ],
599
+ // `PushRayCastResponse` has two definitions; `Physics_RayCast` calls the one
600
+ // that takes the world, which sets every payload field but `request_id`.
601
+ [
602
+ "physics.raycast",
603
+ {
604
+ ts: 'Omit<BuiltinMessages["ray_cast_response"], "request_id">[] | Omit<BuiltinMessages["ray_cast_response"], "request_id"> | undefined',
605
+ upstream: ["table", "nil"],
606
+ retypedUpstream: [
607
+ ["message.physics.ray_cast_response[]", "message.physics.ray_cast_response", "nil"],
608
+ ],
609
+ reason:
610
+ "script_physics.cpp:Physics_RayCast pushes each hit through PushRayCastResponse(L, world, hit), which sets no request_id",
611
+ binding: {
612
+ file: "gamesys/src/gamesys/scripts/script_physics.cpp",
613
+ cFunction: "Physics_RayCast",
614
+ contains: "PushRayCastResponse(L, world, hits[i]);",
615
+ lacks: '"request_id"',
616
+ },
617
+ },
618
+ ],
619
+ ...(
620
+ [
621
+ [
622
+ "collectionfactory.create",
623
+ "Hash",
624
+ ["table<hash, hash>"],
625
+ "gamesys/src/gamesys/scripts/script_collection_factory.cpp",
626
+ "CollectionFactoryComp_Create",
627
+ ],
628
+ [
629
+ "gui.get_tree",
630
+ 'Opaque<"node">',
631
+ ["table<hash, node>"],
632
+ "gui/src/gui_script.cpp",
633
+ "LuaGetTree",
634
+ ],
635
+ [
636
+ "gui.clone_tree",
637
+ 'Opaque<"node">',
638
+ ["table<hash, node>"],
639
+ "gui/src/gui_script.cpp",
640
+ "LuaCloneTree",
641
+ ],
642
+ ] as const
643
+ ).map(
644
+ ([key, value, retyped, file, cFunction]) =>
645
+ [
646
+ key,
647
+ {
648
+ ts: `LuaTable<Hash, ${value}> & { get: LuaTableGetMethod<string, ${value}> }`,
649
+ upstream: ["table"],
650
+ retypedUpstream: [retyped],
651
+ reason: `${file.slice(file.lastIndexOf("/") + 1)}:${cFunction} sets HashTableIndex as the id table's __index, which hashes a string key`,
652
+ binding: {
653
+ file,
654
+ cFunction,
655
+ contains:
656
+ 'lua_pushcfunction(L, HashTableIndex); lua_setfield(L, -2, "__index"); lua_setmetatable(L, -2);',
657
+ },
658
+ },
659
+ ] as const,
660
+ ),
442
661
  ]);
443
662
 
444
663
  // A parameter whose declared `types` disagree with what the same element's
@@ -565,6 +784,35 @@ export const PARAM_TYPE_CORRECTIONS: ReadonlyMap<string, ParamTypeCorrection> =
565
784
  },
566
785
  ] as const,
567
786
  ),
787
+ ...(
788
+ [
789
+ ["clone_tree", "LuaCloneTree"],
790
+ ["get_tree", "LuaGetTree"],
791
+ ] as const
792
+ ).map(
793
+ ([name, cFunction]) =>
794
+ [
795
+ `gui.${name}:param:node`,
796
+ {
797
+ adds: "undefined",
798
+ upstream: ["node"],
799
+ reason: `gui_script.cpp:${cFunction} pushes its result table before testing argument 1 with lua_isnil, so nil selects the whole scene and an absent argument reaches LuaCheckNodeInternal and raises`,
800
+ },
801
+ ] as const,
802
+ ),
803
+ ...(["get_slider_motor", "set_slider_motor"] as const).map(
804
+ (name) =>
805
+ [
806
+ `bullet3d.constraint.${name}:param:motor`,
807
+ {
808
+ adds: '"linear" | "angular"',
809
+ removes: ["string"],
810
+ upstream: ["string"],
811
+ reason:
812
+ "script_bullet3d_constraint.cpp:CheckSliderMotorAxis raises \"motor must be 'linear' or 'angular'\" on any other string",
813
+ },
814
+ ] as const,
815
+ ),
568
816
  [
569
817
  "sound.get_group_name:param:group",
570
818
  {
@@ -643,9 +891,10 @@ export const PARAM_TYPE_CORRECTIONS: ReadonlyMap<string, ParamTypeCorrection> =
643
891
  "render.disable_texture:param:binding",
644
892
  {
645
893
  adds: "number",
894
+ removes: ['Opaque<"texture">'],
646
895
  upstream: ["texture", "string", "hash"],
647
896
  reason:
648
- '"texture binding, either by texture unit, string or hash"; example `render.disable_texture(0)`',
897
+ "render_script.cpp:RenderScript_DisableTexture reads a number as a texture unit with lua_isnumber and anything else with CheckHashOrString; a texture handle is a number, so it is read as a unit",
649
898
  },
650
899
  ],
651
900
  [
@@ -663,11 +912,42 @@ export const PARAM_TYPE_CORRECTIONS: ReadonlyMap<string, ParamTypeCorrection> =
663
912
  adds: 'Opaque<"render_predicate">',
664
913
  removes: ["number"],
665
914
  upstream: ["number"],
666
- retypedUpstream: [["render_predicate"]],
667
915
  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`",
916
+ "render_script.cpp:RenderScript_Draw reads the predicate with RenderScriptPredicate_Check, the userdata render.predicate returns; 1.13.2 types the slot `render_predicate`, whose type mapping is that brand",
669
917
  },
670
918
  ],
919
+ ...(["enable_state", "disable_state"] as const).map(
920
+ (name) =>
921
+ [
922
+ `render.${name}:param:state`,
923
+ {
924
+ adds: "graphics.State",
925
+ removes: ["graphics.STATE"],
926
+ upstream: ["graphics.STATE"],
927
+ reason: `render_script.cpp:RenderScript_${name === "enable_state" ? "Enable" : "Disable"}State raises "Invalid state" on any state but the six it lists, so graphics.STATE_SCISSOR_TEST and graphics.STATE_ALPHA_TEST_SUPPORTED are rejected`,
928
+ },
929
+ ] as const,
930
+ ),
931
+ ...(
932
+ [
933
+ "source_factor_color",
934
+ "destination_factor_color",
935
+ "source_factor_alpha",
936
+ "destination_factor_alpha",
937
+ ] as const
938
+ ).map(
939
+ (slot) =>
940
+ [
941
+ `render.set_blend_func_separate:param:${slot}`,
942
+ {
943
+ adds: "graphics.BlendFactor",
944
+ removes: ["number"],
945
+ upstream: ["number"],
946
+ reason:
947
+ 'render_script.cpp:RenderScript_SetBlendFuncSeparate raises "Invalid blend factor" on any value CheckBlendFactor does not list',
948
+ },
949
+ ] as const,
950
+ ),
671
951
  [
672
952
  "go.delete:param:id",
673
953
  {
@@ -686,15 +966,6 @@ export const PARAM_TYPE_CORRECTIONS: ReadonlyMap<string, ParamTypeCorrection> =
686
966
  reason: 'sys.load_buffer example `buffer.get_bytes(my_buffer, "data")`',
687
967
  },
688
968
  ],
689
- [
690
- "gui.set:param:value",
691
- {
692
- adds: "Matrix4 | Hash",
693
- upstream: ["number", "vector4", "vector3", "quaternion"],
694
- reason:
695
- 'example `gui.set(node, "light_matrix", vmath.matrix4())` ("matrix4 is also supported"); example `gui.set(msg.url(), "textures", atlas_id, {key = "runtime_texture"})` passes the hash `resource.create_atlas` returns',
696
- },
697
- ],
698
969
  [
699
970
  "gui.set_rotation:param:rotation",
700
971
  {
@@ -903,6 +1174,10 @@ export const OPTIONAL_SLOT_CORRECTIONS: ReadonlyMap<string, string> = new Map([
903
1174
  "render.set_render_target:param:render_target",
904
1175
  "render_script.cpp:RenderScript_SetRenderTarget sets the default target when lua_gettop(L) == 0",
905
1176
  ],
1177
+ [
1178
+ "buffer.get_bytes:param:stream_name",
1179
+ "script_buffer.cpp:GetBytes reads only argument 1 with CheckBufferUnpack and returns every byte of the buffer",
1180
+ ],
906
1181
  ["socket.newtry:param:finalizer", "socket.protect's example `local try = socket.newtry()`"],
907
1182
  ...(
908
1183
  [
@@ -1045,13 +1320,6 @@ export const REQUIRED_SLOT_CORRECTIONS: ReadonlyMap<string, RequiredSlotCorrecti
1045
1320
  evidence: "gui_script.cpp:LuaSetParent reads a node from argument 2 unless lua_isnil(L, 2)",
1046
1321
  },
1047
1322
  ],
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
1323
  [
1056
1324
  "window.set_listener:param:callback",
1057
1325
  {
@@ -1141,6 +1409,335 @@ function firstSlotAlternative(
1141
1409
  return original === null ? null : { ...prepared, original };
1142
1410
  }
1143
1411
 
1412
+ // The calls a function splits into at its optional nil-raising slots
1413
+ // (`EmitOptions.nilRaisingSlots`): one ending before each such slot, then one
1414
+ // taking them all, every such slot before an arm's end declared required. A
1415
+ // required slot, or one a rest parameter folds in, splits nothing.
1416
+ function nilRaisingArms(
1417
+ prepared: PreparedFunction,
1418
+ nilRaisingSlots: ReadonlyMap<string, readonly number[]>,
1419
+ ): PreparedFunction[] {
1420
+ const { name, parameters } = prepared.original;
1421
+ const firstVararg = parameters.findIndex(isVarargParameter);
1422
+ const positional = firstVararg === -1 ? parameters.length : firstVararg;
1423
+ const split = [...new Set(nilRaisingSlots.get(name) ?? [])]
1424
+ .map((slot) => slot - 1)
1425
+ .filter((index) => {
1426
+ const p = parameters[index];
1427
+ return index < positional && p !== undefined && isDocOptional(p, name);
1428
+ })
1429
+ .sort((a, b) => a - b);
1430
+ if (split.length === 0) return [prepared];
1431
+ const shorter = split.map((end, k) => ({
1432
+ ...prepared,
1433
+ original: { ...prepared.original, parameters: parameters.slice(0, end) },
1434
+ required: new Set(split.slice(0, k)),
1435
+ }));
1436
+ return [...shorter, { ...prepared, required: new Set(split) }];
1437
+ }
1438
+
1439
+ // Table fields a binding reads as required only when `argument` is non-nil,
1440
+ // keyed by FQN. A curated function splits into a call whose `table` slot requires
1441
+ // `fields`, with `argument` as declared, then a call whose `argument` is nil, so
1442
+ // a possibly-nil argument must carry them.
1443
+ export interface PresenceRequiredFields {
1444
+ readonly argument: string;
1445
+ readonly table: string;
1446
+ readonly fields: readonly string[];
1447
+ readonly reason: string;
1448
+ }
1449
+
1450
+ const FRAME_B_FIELDS = ["frame_b_position", "frame_b_rotation"] as const;
1451
+
1452
+ export const PRESENCE_REQUIRED_FIELDS: ReadonlyMap<string, PresenceRequiredFields> = new Map<
1453
+ string,
1454
+ PresenceRequiredFields
1455
+ >([
1456
+ [
1457
+ "bullet3d.constraint.create_point_to_point",
1458
+ {
1459
+ argument: "body_b",
1460
+ table: "params",
1461
+ fields: ["pivot_b"],
1462
+ reason:
1463
+ "script_bullet3d_constraint.cpp:Constraint_CreatePointToPoint reads pivot_b as required when input.m_BodyB != 0",
1464
+ },
1465
+ ],
1466
+ ...(
1467
+ [
1468
+ ["create_hinge", "Constraint_CreateHinge"],
1469
+ ["create_cone_twist", "Constraint_CreateConeTwist"],
1470
+ ["create_generic_6dof", "Constraint_CreateGeneric6Dof"],
1471
+ ["create_slider", "Constraint_CreateSlider"],
1472
+ ] as const
1473
+ ).map(
1474
+ ([name, cFunction]) =>
1475
+ [
1476
+ `bullet3d.constraint.${name}`,
1477
+ {
1478
+ argument: "body_b",
1479
+ table: "params",
1480
+ fields: FRAME_B_FIELDS,
1481
+ reason: `script_bullet3d_constraint.cpp:${cFunction} reads frame B as required under if (input.m_BodyB)`,
1482
+ },
1483
+ ] as const,
1484
+ ),
1485
+ ]);
1486
+
1487
+ // The calls PRESENCE_REQUIRED_FIELDS splits `prepared` into: the declared one
1488
+ // with its fields required, then the one whose argument is nil.
1489
+ function presenceArms(
1490
+ prepared: PreparedFunction,
1491
+ structs: readonly ApiStruct[],
1492
+ ): PreparedFunction[] {
1493
+ const { name, parameters } = prepared.original;
1494
+ const entry = PRESENCE_REQUIRED_FIELDS.get(name);
1495
+ if (entry === undefined) return [prepared];
1496
+ const argument = parameters.findIndex((p) => p.name === entry.argument);
1497
+ const table = parameters.findIndex((p) => p.name === entry.table);
1498
+ const struct = structs.find((s) => parameters[table]?.types.includes(s.name));
1499
+ if (argument === -1 || struct === undefined) {
1500
+ throw new Error(
1501
+ `presence-required fields ${name}: no ${entry.argument} parameter or ${entry.table} table`,
1502
+ );
1503
+ }
1504
+ const missing = entry.fields.filter((field) => !struct.members.some((m) => m.name === field));
1505
+ if (missing.length > 0) {
1506
+ throw new Error(`presence-required fields ${name}: ${struct.name} lacks ${missing.join(", ")}`);
1507
+ }
1508
+ const picked = entry.fields.map((field) => `"${field}"`).join(" | ");
1509
+ const requireFields = (type: string): string => {
1510
+ const operand = /^[\w$.]+$/.test(type) ? type : `(${type})`;
1511
+ return `${operand} & Required<Pick<${operand}, ${picked}>>`;
1512
+ };
1513
+ const strict: PreparedFunction = { ...prepared, retype: new Map([[table, requireFields]]) };
1514
+ // Declared required, the nil argument emits as `undefined` alone rather than
1515
+ // gaining the `| undefined` of an interior optional slot.
1516
+ const absent: PreparedFunction = {
1517
+ ...prepared,
1518
+ required: new Set([...(prepared.required ?? []), argument]),
1519
+ retype: new Map([[argument, () => "undefined"]]),
1520
+ };
1521
+ return [strict, absent];
1522
+ }
1523
+
1524
+ // A rule an engine binding enforces on one parameter that no ref-doc token
1525
+ // states, keyed `<fqn>:param:<name>`. Each rewrites the slot's mapped type, so
1526
+ // one entry holds on every target whatever token spelling produced that type.
1527
+ export type EngineSlotRule =
1528
+ // `E[]` becomes `[E, ...E[]]`: the binding raises on an empty list.
1529
+ | { readonly kind: "non-empty"; readonly reason: string }
1530
+ // The constants the binding raises on leave the slot's type.
1531
+ | { readonly kind: "excludes"; readonly constants: readonly string[]; readonly reason: string }
1532
+ // With `flag` true the table must carry `fields`, which the binding then reads
1533
+ // without a default.
1534
+ | {
1535
+ readonly kind: "flag-requires";
1536
+ readonly flag: string;
1537
+ readonly fields: readonly string[];
1538
+ readonly reason: string;
1539
+ }
1540
+ // In each call that requires `slot`, this slot is required too and its
1541
+ // top-level union loses `drops`.
1542
+ | {
1543
+ readonly kind: "alongside";
1544
+ readonly slot: string;
1545
+ readonly drops: readonly string[];
1546
+ readonly reason: string;
1547
+ };
1548
+
1549
+ export const ENGINE_SLOT_RULES: ReadonlyMap<string, EngineSlotRule> = new Map<
1550
+ string,
1551
+ EngineSlotRule
1552
+ >([
1553
+ [
1554
+ "buffer.create:param:declaration",
1555
+ {
1556
+ kind: "non-empty",
1557
+ reason:
1558
+ 'script_buffer.cpp:Create raises "You must specify at least one stream declaration" when the list holds fewer than one entry',
1559
+ },
1560
+ ],
1561
+ [
1562
+ "buffer.set_metadata:param:values",
1563
+ {
1564
+ kind: "non-empty",
1565
+ reason:
1566
+ 'script_buffer.cpp:SetMetadata raises "invalid metadata" when lua_objlen of values is 0',
1567
+ },
1568
+ ],
1569
+ [
1570
+ "buffer.set_metadata:param:value_type",
1571
+ {
1572
+ kind: "excludes",
1573
+ constants: ["buffer.VALUE_TYPE_INT64", "buffer.VALUE_TYPE_UINT64"],
1574
+ reason:
1575
+ 'script_buffer.cpp:SetMetadata raises "64 bit integer metadata are not supported." on VALUE_TYPE_UINT64 and VALUE_TYPE_INT64',
1576
+ },
1577
+ ],
1578
+ [
1579
+ "resource.create_sound_data:param:options",
1580
+ {
1581
+ kind: "flag-requires",
1582
+ flag: "partial",
1583
+ fields: ["filesize"],
1584
+ reason:
1585
+ "script_resource.cpp:CreateSoundData reads filesize with the no-default CheckFieldValue<int> when partial is true, and CheckValue<int> raises on a non-number",
1586
+ },
1587
+ ],
1588
+ [
1589
+ "go.delete:param:id",
1590
+ {
1591
+ kind: "alongside",
1592
+ slot: "recursive",
1593
+ drops: ["boolean"],
1594
+ reason:
1595
+ "gameobject_script.cpp:Script_Delete raises on a boolean id with a second argument, and on a nil id once recursive is popped",
1596
+ },
1597
+ ],
1598
+ ]);
1599
+
1600
+ // `prepared` with `rewrite` applied after any rewrite it already makes at `index`.
1601
+ function withRetype(
1602
+ prepared: PreparedFunction,
1603
+ index: number,
1604
+ rewrite: (type: string) => string,
1605
+ ): PreparedFunction {
1606
+ const retype = new Map(prepared.retype);
1607
+ const prior = retype.get(index);
1608
+ retype.set(index, prior === undefined ? rewrite : (type) => rewrite(prior(type)));
1609
+ return { ...prepared, retype };
1610
+ }
1611
+
1612
+ // `E[]` as `[E, ...E[]]`; the element keeps its own parentheses.
1613
+ function nonEmptyList(type: string, key: string): string {
1614
+ const element = type.endsWith("[]") ? type.slice(0, -2) : "";
1615
+ const single =
1616
+ splitTopLevel(type, "|").length === 1 &&
1617
+ splitTopLevel(type, "&").length === 1 &&
1618
+ !isFunctionTypeText(type);
1619
+ if (element === "" || !single) {
1620
+ throw new Error(`engine slot rule ${key}: ${type} is not one array`);
1621
+ }
1622
+ return `[${element}, ...${element}[]]`;
1623
+ }
1624
+
1625
+ function withoutMembers(type: string, drops: readonly string[], key: string): string {
1626
+ const members = splitTopLevel(type, "|").map((member) => member.trim());
1627
+ const missing = drops.filter((drop) => !members.includes(drop));
1628
+ if (missing.length > 0) {
1629
+ throw new Error(`engine slot rule ${key}: ${type} has no ${missing.join(", ")} to drop`);
1630
+ }
1631
+ return members.filter((member) => !drops.includes(member)).join(" | ");
1632
+ }
1633
+
1634
+ // The field names a table slot declares: its STRUCT's members, else the fields
1635
+ // parsed from its own doc.
1636
+ function slotFieldNames(parameter: ApiParameter, structs: readonly ApiStruct[]): string[] {
1637
+ const struct = structs.find((s) => parameter.types.includes(s.name));
1638
+ if (struct !== undefined) return struct.members.map((member) => member.name);
1639
+ const fields = parameter.fields ?? parseTableFields(parameter.doc) ?? [];
1640
+ return fields.map((field) => field.name);
1641
+ }
1642
+
1643
+ function engineSlotRuleArms(
1644
+ prepared: PreparedFunction,
1645
+ arms: readonly PreparedFunction[],
1646
+ key: string,
1647
+ rule: EngineSlotRule,
1648
+ index: number,
1649
+ structs: readonly ApiStruct[],
1650
+ bindingsRead: boolean,
1651
+ ): PreparedFunction[] {
1652
+ const { name, parameters } = prepared.original;
1653
+ const takes = (arm: PreparedFunction): boolean => index < arm.original.parameters.length;
1654
+ const rewriteEach = (rewrite: (type: string) => string): PreparedFunction[] =>
1655
+ arms.map((arm) => (takes(arm) ? withRetype(arm, index, rewrite) : arm));
1656
+ switch (rule.kind) {
1657
+ case "non-empty":
1658
+ return rewriteEach((type) => nonEmptyList(type, key));
1659
+ case "excludes": {
1660
+ const excluded = rule.constants.map((constant) => `typeof ${constant}`).join(" | ");
1661
+ return rewriteEach((type) => `Exclude<${type}, ${excluded}>`);
1662
+ }
1663
+ case "flag-requires": {
1664
+ const declared = slotFieldNames(parameters[index] as ApiParameter, structs);
1665
+ const missing = [rule.flag, ...rule.fields].filter((field) => !declared.includes(field));
1666
+ if (missing.length > 0) {
1667
+ throw new Error(`engine slot rule ${key}: the table declares no ${missing.join(", ")}`);
1668
+ }
1669
+ const picked = rule.fields.map((field) => `"${field}"`).join(" | ");
1670
+ return rewriteEach((type) => {
1671
+ const operand =
1672
+ splitTopLevel(type, "|").length > 1 || isFunctionTypeText(type) ? `(${type})` : type;
1673
+ return `${operand} & ({ ${rule.flag}?: false } | ({ ${rule.flag}: true } & Required<Pick<${type}, ${picked}>>))`;
1674
+ });
1675
+ }
1676
+ case "alongside": {
1677
+ const partner = parameters.findIndex((p) => p.name === rule.slot);
1678
+ const partnerParameter = parameters[partner];
1679
+ const requires = (arm: PreparedFunction): boolean =>
1680
+ partnerParameter !== undefined &&
1681
+ partner < arm.original.parameters.length &&
1682
+ (arm.required?.has(partner) === true || !isDocOptional(partnerParameter, name));
1683
+ if (!arms.some(requires)) {
1684
+ // Without the target's nil-raising slots no call was split out to
1685
+ // require the partner, so there is no binding to have stopped marking it.
1686
+ if (!bindingsRead) return [...arms];
1687
+ throw new Error(
1688
+ partnerParameter === undefined
1689
+ ? `engine slot rule ${key}: ${name} has no ${rule.slot} parameter`
1690
+ : `engine slot rule ${key}: no call of ${name} requires ${rule.slot}`,
1691
+ );
1692
+ }
1693
+ return arms.map((arm) =>
1694
+ requires(arm)
1695
+ ? {
1696
+ ...withRetype(arm, index, (type) => withoutMembers(type, rule.drops, key)),
1697
+ required: new Set([...(arm.required ?? []), index]),
1698
+ }
1699
+ : arm,
1700
+ );
1701
+ }
1702
+ }
1703
+ }
1704
+
1705
+ // `arms` with every ENGINE_SLOT_RULES entry keyed to `prepared` applied.
1706
+ // `bindingsRead` says the emit was given the target's nil-raising slots.
1707
+ function applyEngineSlotRules(
1708
+ prepared: PreparedFunction,
1709
+ arms: readonly PreparedFunction[],
1710
+ structs: readonly ApiStruct[],
1711
+ bindingsRead: boolean,
1712
+ ): PreparedFunction[] {
1713
+ const { name, parameters } = prepared.original;
1714
+ const prefix = `${name}:param:`;
1715
+ let ruled = [...arms];
1716
+ for (const [key, rule] of ENGINE_SLOT_RULES) {
1717
+ if (!key.startsWith(prefix)) continue;
1718
+ const slot = key.slice(prefix.length);
1719
+ const index = parameters.findIndex((p) => p.name === slot);
1720
+ if (index === -1) {
1721
+ throw new Error(`engine slot rule ${key}: ${name} has no ${slot} parameter`);
1722
+ }
1723
+ ruled = engineSlotRuleArms(prepared, ruled, key, rule, index, structs, bindingsRead);
1724
+ }
1725
+ return ruled;
1726
+ }
1727
+
1728
+ // Every call a function declares, in order: its nil-raising arms, each split
1729
+ // at its presence-required fields, then narrowed by its engine slot rules.
1730
+ function callArms(
1731
+ prepared: PreparedFunction,
1732
+ nilRaisingSlots: ReadonlyMap<string, readonly number[]> | undefined,
1733
+ structs: readonly ApiStruct[],
1734
+ ): PreparedFunction[] {
1735
+ const arms = nilRaisingArms(prepared, nilRaisingSlots ?? new Map()).flatMap((arm) =>
1736
+ presenceArms(arm, structs),
1737
+ );
1738
+ return applyEngineSlotRules(prepared, arms, structs, nilRaisingSlots !== undefined);
1739
+ }
1740
+
1144
1741
  export type ConstantSlotResolution =
1145
1742
  | { readonly borrow: string }
1146
1743
  | { readonly family: string; readonly evidence: string }
@@ -1155,8 +1752,6 @@ export const CONSTANT_SLOT_RESOLUTIONS: ReadonlyMap<string, ConstantSlotResoluti
1155
1752
  string,
1156
1753
  ConstantSlotResolution
1157
1754
  >([
1158
- ["gui.get:param:property", { borrow: "gui.animate:param:property" }],
1159
- ["gui.set:param:property", { borrow: "gui.animate:param:property" }],
1160
1755
  ["gui.cancel_animations:param:property", { borrow: "gui.animate:param:property" }],
1161
1756
  [
1162
1757
  "buffer.set_metadata:param:value_type",
@@ -1285,6 +1880,29 @@ export const CONSTANT_UNION_ALIASES: readonly ConstantUnionAlias[] = [
1285
1880
  "go.PLAYBACK_LOOP_PINGPONG",
1286
1881
  ],
1287
1882
  },
1883
+ // The factors render_script.cpp:CheckBlendFactor accepts, in its order. Every
1884
+ // target defines the constants; only the newer ones name the union.
1885
+ {
1886
+ home: "graphics",
1887
+ name: "BlendFactor",
1888
+ members: [
1889
+ "graphics.BLEND_FACTOR_ZERO",
1890
+ "graphics.BLEND_FACTOR_ONE",
1891
+ "graphics.BLEND_FACTOR_SRC_COLOR",
1892
+ "graphics.BLEND_FACTOR_ONE_MINUS_SRC_COLOR",
1893
+ "graphics.BLEND_FACTOR_DST_COLOR",
1894
+ "graphics.BLEND_FACTOR_ONE_MINUS_DST_COLOR",
1895
+ "graphics.BLEND_FACTOR_SRC_ALPHA",
1896
+ "graphics.BLEND_FACTOR_ONE_MINUS_SRC_ALPHA",
1897
+ "graphics.BLEND_FACTOR_DST_ALPHA",
1898
+ "graphics.BLEND_FACTOR_ONE_MINUS_DST_ALPHA",
1899
+ "graphics.BLEND_FACTOR_SRC_ALPHA_SATURATE",
1900
+ "graphics.BLEND_FACTOR_CONSTANT_COLOR",
1901
+ "graphics.BLEND_FACTOR_ONE_MINUS_CONSTANT_COLOR",
1902
+ "graphics.BLEND_FACTOR_CONSTANT_ALPHA",
1903
+ "graphics.BLEND_FACTOR_ONE_MINUS_CONSTANT_ALPHA",
1904
+ ],
1905
+ },
1288
1906
  {
1289
1907
  home: "graphics",
1290
1908
  name: "BufferType",
@@ -1498,6 +2116,12 @@ export const CONSTANT_UNION_ALIASES: readonly ConstantUnionAlias[] = [
1498
2116
  "graphics.BUFFER_TYPE_STENCIL_BIT",
1499
2117
  ],
1500
2118
  },
2119
+ // The render-target creator raises on any other texture type.
2120
+ {
2121
+ home: "render",
2122
+ name: "RenderTargetTextureType",
2123
+ members: ["graphics.TEXTURE_TYPE_2D", "graphics.TEXTURE_TYPE_CUBE_MAP"],
2124
+ },
1501
2125
  {
1502
2126
  home: "sys",
1503
2127
  name: "NetworkConnectivity",
@@ -1553,6 +2177,13 @@ function unionAliasDeclaration(
1553
2177
  return `type ${name} = ${members.map(arm).join(" | ")};`;
1554
2178
  }
1555
2179
 
2180
+ // A slot of a function the authored-overload lane declares, published under
2181
+ // one alias on every target so the version-independent overload file can
2182
+ // name the type each target's own reference gives that slot.
2183
+ export const OVERLOAD_SLOT_ALIASES: ReadonlyMap<string, string> = new Map([
2184
+ ["render.render_target:param:parameters", "RenderTargetParams"],
2185
+ ]);
2186
+
1556
2187
  const DOC_CONSTANT_TOKEN =
1557
2188
  /(?<![A-Za-z0-9_.])([a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)*\.[A-Z][A-Z0-9_]*)(\*)?/g;
1558
2189
 
@@ -1602,9 +2233,14 @@ function constantSlotTokens(
1602
2233
  return documentedConstantTokens(entry.family, universe);
1603
2234
  }
1604
2235
 
1605
- function slotDocLookup(module: ApiModule): SlotDocLookup {
2236
+ // A borrow reads the ref-doc, so a slot of a function the skip rules withheld
2237
+ // (`gui.animate`, which gui-overloads.d.ts declares) still lends its doc.
2238
+ function slotDocLookup(
2239
+ module: ApiModule,
2240
+ withheldFunctions: readonly ApiFunction[] = [],
2241
+ ): SlotDocLookup {
1606
2242
  const docs = new Map<string, string>();
1607
- for (const fn of module.functions) {
2243
+ for (const fn of [...module.functions, ...withheldFunctions]) {
1608
2244
  for (const p of fn.parameters) {
1609
2245
  const key = tableSlotKey(fn.name, "param", p.name);
1610
2246
  if (!docs.has(key)) docs.set(key, p.doc);
@@ -1624,8 +2260,9 @@ export function resolveConstantSlotTokens(
1624
2260
  module: ApiModule,
1625
2261
  slotKey: string,
1626
2262
  universe: ReadonlySet<string>,
2263
+ withheldFunctions: readonly ApiFunction[] = [],
1627
2264
  ): string[] {
1628
- const lookupDoc = slotDocLookup(module);
2265
+ const lookupDoc = slotDocLookup(module, withheldFunctions);
1629
2266
  return constantSlotTokens(lookupDoc(slotKey) ?? "", slotKey, lookupDoc, universe);
1630
2267
  }
1631
2268
 
@@ -1639,21 +2276,23 @@ type ConstantSlotTokens = (
1639
2276
  function constantSlotTokenResolver(
1640
2277
  module: ApiModule,
1641
2278
  universe: ReadonlySet<string>,
2279
+ withheldFunctions: readonly ApiFunction[] = [],
1642
2280
  ): ConstantSlotTokens {
1643
- const lookupDoc = slotDocLookup(module);
2281
+ const lookupDoc = slotDocLookup(module, withheldFunctions);
1644
2282
  return (elementName, slotKind, slotName, doc) =>
1645
2283
  constantSlotTokens(doc, tableSlotKey(elementName, slotKind, slotName), lookupDoc, universe);
1646
2284
  }
1647
2285
 
1648
- // FQN-keyed allowlist of the `types.is_*` checks that genuinely narrow their
1649
- // argument, mapped to the `DEFOLD_TYPE_MAP` token whose interface they prove.
1650
- // Emitting these as user-defined type guards (`var_ is Vector3`) is the only way
1651
- // to narrow an engine userdata value, which `typeof` cannot. An allowlist, not a
1652
- // blanket `is_*` match: `b2d.body.is_active`, `gui.is_enabled`,
1653
- // `sound.is_music_playing`, `go.exists`, `sys.exists` are runtime-state /
1654
- // existence checks on already-typed arguments and must stay `boolean`. An
1655
- // unlisted future `types.is_*` also stays a visible `boolean` until curated here,
1656
- // matching the ARBITRARY_TABLE_SLOTS deliberate-opt-in precedent.
2286
+ // FQN-keyed allowlist of the checks that genuinely narrow their argument, mapped
2287
+ // to the `DEFOLD_TYPE_MAP` token whose type they prove: the `types.is_*` checks,
2288
+ // and `bullet3d.collision_object.is_rigid_body`, whose binding is a
2289
+ // `btRigidBody::upcast` test. Emitting these as user-defined type guards
2290
+ // (`var_ is Vector3`) is the only way to narrow an engine userdata value, which
2291
+ // `typeof` cannot. An allowlist, not a blanket `is_*` match: `b2d.body.is_active`,
2292
+ // `gui.is_enabled`, `sound.is_music_playing`, `go.exists`, `sys.exists` are
2293
+ // runtime-state / existence checks on already-typed arguments and must stay
2294
+ // `boolean`. An unlisted future `types.is_*` also stays a visible `boolean` until
2295
+ // curated here, matching the ARBITRARY_TABLE_SLOTS deliberate-opt-in precedent.
1657
2296
  export const TYPE_PREDICATES: ReadonlyMap<string, string> = new Map([
1658
2297
  ["types.is_hash", "hash"],
1659
2298
  ["types.is_matrix4", "matrix4"],
@@ -1662,13 +2301,47 @@ export const TYPE_PREDICATES: ReadonlyMap<string, string> = new Map([
1662
2301
  ["types.is_vector", "vector"],
1663
2302
  ["types.is_vector3", "vector3"],
1664
2303
  ["types.is_vector4", "vector4"],
2304
+ ["bullet3d.collision_object.is_rigid_body", "btRigidBody"],
2305
+ ]);
2306
+
2307
+ // The merged ref-doc gives each Box2D backend's form of these queries one
2308
+ // parameter list, so the first (v2) arm always wins and a v3 hit is unreachable.
2309
+ // A type parameter tells them apart: v2/script_box2d_world_v2.cpp:PushCastHit
2310
+ // sets `fixture` and `shape` to one PushFixtureInfo table, while
2311
+ // v3/script_box2d_world_v3.cpp:PushCastHit sets `shape` alone from
2312
+ // PushShapeInfoForShape. The v2 arm is declared first, so a call with no type
2313
+ // argument keeps the v2 hits; its `= "v2"` default only states that. Keyed by
2314
+ // `box2dBackendArmKey`.
2315
+ export const BOX2D_BACKEND_ARMS: ReadonlyMap<string, Box2dBackend> = new Map<string, Box2dBackend>([
2316
+ ["b2d.world.cast_ray:b2d.fixture_cast_hit[]", "v2"],
2317
+ ["b2d.world.cast_ray:b2d.shape_cast_hit[]", "v3"],
2318
+ ["b2d.world.cast_ray_closest:b2d.fixture_cast_hit", "v2"],
2319
+ ["b2d.world.cast_ray_closest:b2d.shape_cast_hit", "v3"],
2320
+ ["b2d.world.cast_shape:b2d.fixture_cast_hit[]", "v2"],
2321
+ ["b2d.world.cast_shape:b2d.shape_cast_hit[]", "v3"],
2322
+ ["b2d.world.overlap_aabb:b2d.fixture_info[]", "v2"],
2323
+ ["b2d.world.overlap_aabb:b2d.shape_info[]", "v3"],
2324
+ ["b2d.world.overlap_shape:b2d.fixture_info[]", "v2"],
2325
+ ["b2d.world.overlap_shape:b2d.shape_info[]", "v3"],
1665
2326
  ]);
1666
2327
 
2328
+ // `<fqn>:<first non-nil type of the first return>`: the two arms of each
2329
+ // `BOX2D_BACKEND_ARMS` query share every parameter and differ only there.
2330
+ export function box2dBackendArmKey(fn: ApiFunction): string {
2331
+ const first = fn.returnValues[0]?.types.find((type) => type !== "nil") ?? "";
2332
+ return `${fn.name}:${first}`;
2333
+ }
2334
+
2335
+ function box2dBackendTypeParameters(fn: ApiFunction): string {
2336
+ const backend = BOX2D_BACKEND_ARMS.get(box2dBackendArmKey(fn));
2337
+ if (backend === undefined) return "";
2338
+ return backend === "v2" ? '<Backend extends "v2" = "v2">' : '<Backend extends "v3">';
2339
+ }
2340
+
1667
2341
  // Element names whose `table` slot is a prose-only `array/list/table of <T>` shape
1668
2342
  // the field-list parser cannot read, but whose element type a human curated from
1669
2343
  // the doc. The value is a single element token (`T[]`) or a token list when the
1670
- // element is itself a union — `go.delete`'s "table of id's" is the id union
1671
- // `string | hash | url`, emitted `(string | Hash | Url)[]`. Emitted as a plain
2344
+ // element is itself a union, emitted `(A | B)[]`. Emitted as a plain
1672
2345
  // array, never `LuaMap`: an array element carries no illegal-index-key problem so
1673
2346
  // no TSTL wiring is needed. The audit consults this map too, so the gate and the
1674
2347
  // emitted surface stay coupled; a slot not curated here still surfaces under
@@ -1683,7 +2356,6 @@ export const HOMOGENEOUS_ARRAY_SLOTS: ReadonlyMap<string, string | readonly stri
1683
2356
  ["vmath.vector", "number"],
1684
2357
  ["sound.get_groups", "hash"],
1685
2358
  ["iap.list", "string"],
1686
- ["go.delete", ["string", "hash", "url"]],
1687
2359
  // push.register's `notifications` is a prose-only array of push.NOTIFICATION_*
1688
2360
  // bitmask constants (the ref example sums NOTIFICATION_BADGE/SOUND/ALERT). The
1689
2361
  // vendored push_doc.json fixture declares no NOTIFICATION_* constant elements, so
@@ -1706,7 +2378,11 @@ export type NestedMapping = { key: string; value: string };
1706
2378
  // for `collectionfactory.create`'s properties.
1707
2379
  export type TableSlotCuration = (
1708
2380
  | { kind: "mapping"; key: string; value: string | readonly TableField[] | NestedMapping }
1709
- | { kind: "array"; element: string | readonly string[] }
2381
+ // `pairs` also admits any table whose values are the element, for a binding
2382
+ // that walks the table with lua_next and reads only its values. The extra arm
2383
+ // is `LuaPairsIterable`, which is covariant in its value; `LuaTable` is
2384
+ // invariant through `set`, so it would reject a `LuaTable<Hash, Hash>`.
2385
+ | { kind: "array"; element: string | readonly string[]; pairs?: true }
1710
2386
  | { kind: "object"; fields: readonly TableField[] }
1711
2387
  | { kind: "array-object"; fields: readonly TableField[] }
1712
2388
  // A `<key> -> <args table>` slot: the doc's `<dl>` describes the *value* shape,
@@ -1883,25 +2559,6 @@ export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new
1883
2559
  string,
1884
2560
  TableSlotCuration
1885
2561
  >([
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
- ),
1905
2562
  // buffer.create's `declaration` is "a table where each entry (table) describes
1906
2563
  // a stream", and upstream's example passes a list of stream records whose
1907
2564
  // `type` is a buffer.VALUE_TYPE_* constant.
@@ -1929,6 +2586,19 @@ export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new
1929
2586
  },
1930
2587
  ],
1931
2588
  ["collectionfactory.create:return:ids", { kind: "mapping", key: "hash", value: "hash" }],
2589
+ // go.delete's "table of id's": gameobject_script.cpp:DeleteGOTable walks any
2590
+ // table with lua_next and resolves each value with ResolveURL, so the id table
2591
+ // collectionfactory.create returns deletes as it is. 1.13.2 retypes the slot
2592
+ // to an id array, which still rejects that table.
2593
+ [
2594
+ "go.delete:param:id",
2595
+ {
2596
+ kind: "array",
2597
+ element: ["string", "hash", "url"],
2598
+ pairs: true,
2599
+ retypedUpstream: ["(string|hash|url)[]"],
2600
+ },
2601
+ ],
1932
2602
  // font.get_info's `info` return is a `<dl>` with `path: hash` and a nested
1933
2603
  // `fonts` field whose `<dd>` is itself a `<dl>` (`path: string`, `path_hash:
1934
2604
  // hash`). parseTableFields flattens the inner `<dl>` to the top level, which
@@ -2186,62 +2856,6 @@ export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new
2186
2856
  value: "number | vector4",
2187
2857
  },
2188
2858
  ],
2189
- // go.on_input and gui.on_input share the same well-known InputAction shape
2190
- // (the on_input function description lists the well-known fields: value,
2191
- // pressed, released, repeated, x/y/screen_x/screen_y/dx/dy/screen_dx/screen_dy,
2192
- // plus gamepad/touch/text multi-touch fields). The doc markup carries no
2193
- // field list of its own, so the shape is curated verbatim from the prose.
2194
- // Param-side optional fields: each input kind (mouse/key/text/gamepad) sets a
2195
- // different subset, so the call site always sees an `?` on every field.
2196
- // `touch` stays `table` (the nested multi-touch table has no machine-readable
2197
- // field list; mirrors the render.set_render_target.transient recursive case).
2198
- // The element is the bare `on_input` script hook (no namespace prefix), the
2199
- // same form the `on_message` arbitrary-table entry uses. The doc carries a
2200
- // well-formed touch input table, so `touch` is curated as a nested
2201
- // array-of-touch-records (the field has its own `<table>` listing id,
2202
- // pressed, released, tap_count, x, y, dx, dy, acc_x/y/z).
2203
- [
2204
- "on_input:param:action",
2205
- {
2206
- kind: "object",
2207
- fields: [
2208
- { name: "value", types: ["number"], optional: true },
2209
- { name: "pressed", types: ["boolean"], optional: true },
2210
- { name: "released", types: ["boolean"], optional: true },
2211
- { name: "repeated", types: ["boolean"], optional: true },
2212
- { name: "x", types: ["number"], optional: true },
2213
- { name: "y", types: ["number"], optional: true },
2214
- { name: "screen_x", types: ["number"], optional: true },
2215
- { name: "screen_y", types: ["number"], optional: true },
2216
- { name: "dx", types: ["number"], optional: true },
2217
- { name: "dy", types: ["number"], optional: true },
2218
- { name: "screen_dx", types: ["number"], optional: true },
2219
- { name: "screen_dy", types: ["number"], optional: true },
2220
- { name: "gamepad", types: ["number"], optional: true },
2221
- { name: "gamepad_axis", types: ["vector3"], optional: true },
2222
- {
2223
- name: "touch",
2224
- types: ["table"],
2225
- optional: true,
2226
- isList: true,
2227
- fields: [
2228
- { name: "id", types: ["number"] },
2229
- { name: "pressed", types: ["boolean"] },
2230
- { name: "released", types: ["boolean"] },
2231
- { name: "tap_count", types: ["number"] },
2232
- { name: "x", types: ["number"] },
2233
- { name: "y", types: ["number"] },
2234
- { name: "dx", types: ["number"] },
2235
- { name: "dy", types: ["number"] },
2236
- { name: "acc_x", types: ["number"] },
2237
- { name: "acc_y", types: ["number"] },
2238
- { name: "acc_z", types: ["number"] },
2239
- ],
2240
- },
2241
- { name: "text", types: ["string"], optional: true },
2242
- ],
2243
- },
2244
- ],
2245
2859
  // physics.create_joint and physics.set_joint_properties both declare
2246
2860
  // `properties` as a joint-type-specific table; the only universal field is
2247
2861
  // `collide_connected` (parseTableFields already recovers it from the
@@ -2267,10 +2881,13 @@ export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new
2267
2881
  // buildTableDocResolver resolves only FUNCTION elements so the cross-ref
2268
2882
  // cannot fire (the message payload lives in messages_doc.json, not the
2269
2883
  // FUNCTION slot pool). The shape is curated directly from the message
2270
- // catalog (the six ray_cast_response payload fields). The doc declares a
2884
+ // catalog: the ray_cast_response payload fields but `request_id`, which
2885
+ // `Physics_RayCast` never sets. The doc declares a
2271
2886
  // `nil` branch ("If missed it returns <code>nil</code>"), so the existing
2272
2887
  // nil-aware emit already projects the curated array onto the
2273
2888
  // `array-object | nil` return type — no curation change needed there.
2889
+ // Where its binding pin holds, the `physics.raycast` RETURN_TYPE_CORRECTIONS
2890
+ // entry replaces the whole return.
2274
2891
  [
2275
2892
  "physics.raycast:return:result",
2276
2893
  {
@@ -2281,7 +2898,6 @@ export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new
2281
2898
  { name: "normal", types: ["vector3"] },
2282
2899
  { name: "id", types: ["hash"] },
2283
2900
  { name: "group", types: ["hash"] },
2284
- { name: "request_id", types: ["number"] },
2285
2901
  ],
2286
2902
  },
2287
2903
  ],
@@ -2704,41 +3320,105 @@ export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new
2704
3320
  ]);
2705
3321
 
2706
3322
  // Slot-keyed (`element:param:name`, mirroring TABLE_SLOT_CURATIONS) replacements
2707
- // for a callback parameter's recovered signature, used where the generic
2708
- // `recoverCallbackSignature` `unknown`-everywhere form leaves real engine type
2709
- // information on the table. The value is the full emitted function type. Honest
2710
- // only: `window.set_listener` discriminates `event` by the union of the
2711
- // `WINDOW_EVENT_*` constants the surface's own `window` namespace declares, and
2712
- // its `data` is a bare record (only resize carries fields — `isWindowEvent` is the
2713
- // path to typed `data`), mirroring the `msg.post` (send, typed) / `isMessage`
2714
- // (receive, narrow) split.
3323
+ // for a callback parameter's signature. Each entry carries two forms, chosen by
3324
+ // the token the reference gives the slot. `untyped` answers a bare
3325
+ // `function(a, b, c)` token, where the generic `recoverCallbackSignature`
3326
+ // `unknown`-everywhere form leaves real engine type information on the table.
3327
+ // `typed` answers a `fun(a:T, ...)` token, where the reference names a type for
3328
+ // each parameter; `null` there emits the reference's own signature. Each form
3329
+ // is the full emitted function type. Honest only.
2715
3330
  //
2716
- // The other entries type what the slot's own prose lists and nothing more.
3331
+ // `window.set_listener` discriminates `event` by the union of the
3332
+ // `WINDOW_EVENT_*` constants the surface's own `window` namespace declares, on
3333
+ // both forms: the union is derived from the surface, so each version accepts
3334
+ // exactly the constants it declares. Its untyped `data` is a bare record (only
3335
+ // resize carries fields) and its typed `data` is the reference's
3336
+ // `window.event_data`; `isWindowEvent` narrows either to the event's payload,
3337
+ // mirroring the `msg.post` (send, typed) / `isMessage` (receive, narrow) split.
3338
+ //
3339
+ // The other untyped forms type what the slot's own prose lists and nothing more.
2717
3340
  // `http.request`'s response fields are the `<ul>` under "The response data.
2718
3341
  // Contains the fields:"; each is conditional ("if not saved on disc", "only if
2719
3342
  // option report_progress is true") except `status`, and `headers` is the same
2720
- // string map the request sends. `sprite.play_flipbook`'s message is the
2721
- // "Information about the completion" list. `collectionproxy.load` documents
3343
+ // string map the request sends. The prose omits two fields the engine sets:
3344
+ // `url`, which every final response carries (script_http_util.h), and
3345
+ // `bytes_sent`, which a report_progress event carries beside `bytes_received`,
3346
+ // `bytes_total` and `url` (http_ddf.proto `HttpRequestProgress`). That event has
3347
+ // no `status`; typing it as its own shape needs a per-target authored overload,
3348
+ // so `status` stays required and the slot's `note` says how to tell the two
3349
+ // apart. Its typed form is the same signature: the typed reference names
3350
+ // `http.response`, which the runtime surface withholds. Both forms add the
3351
+ // `sender` no reference lists: comp_script.cpp delivers the response as a
3352
+ // message, with `PushURL(L, message->m_Sender)` then `PCall(L, 4, 0)`.
3353
+ // `model.play_anim`'s completion callback receives self, the message id and the
3354
+ // message only: script_model.cpp calls it with `PCall(L, 3, 0)`, so both forms
3355
+ // drop the `sender` every reference lists and type the message as the typed
3356
+ // reference's `model_animation_done`.
3357
+ // `sprite.play_flipbook`'s message is the "Information about the completion"
3358
+ // list. Its typed form is the same signature: the typed reference names the
3359
+ // built-in `animation_done` message, which has this shape, and the inline form
3360
+ // keeps the fields visible in the signature. `collectionproxy.load` documents
2722
3361
  // only that the callback receives `proxy_loading`, `proxy_ready` or
2723
- // `proxy_error` messages and lists no fields, so its message is the bare record
2724
- // `window.set_listener`'s `data` is.
2725
- export const CALLBACK_SIGNATURE_CURATIONS: ReadonlyMap<string, string> = new Map([
3362
+ // `proxy_error` messages and lists no fields on the untyped reference, so its
3363
+ // untyped message is the bare record `window.set_listener`'s untyped `data` is;
3364
+ // the typed reference names `collectionproxy.load_data`, so its typed form
3365
+ // defers to the reference.
3366
+ export interface CallbackSignatureCuration {
3367
+ /** Emitted for a bare `function(...)` token. */
3368
+ readonly untyped: string;
3369
+ /** Emitted for a typed `fun(...)` token; `null` emits the reference's own signature. */
3370
+ readonly typed: string | null;
3371
+ /** Appended to the parameter's doc, for what the signature cannot express. */
3372
+ readonly note?: string;
3373
+ }
3374
+
3375
+ const onBothForms = (signature: string, note?: string): CallbackSignatureCuration => ({
3376
+ untyped: signature,
3377
+ typed: signature,
3378
+ ...(note === undefined ? {} : { note }),
3379
+ });
3380
+
3381
+ export const CALLBACK_SIGNATURE_CURATIONS: ReadonlyMap<string, CallbackSignatureCuration> = new Map<
3382
+ string,
3383
+ CallbackSignatureCuration
3384
+ >([
2726
3385
  [
2727
3386
  "window.set_listener:param:callback",
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",
3387
+ {
3388
+ untyped:
3389
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: emitted verbatim as a TS template literal type
3390
+ "(self: unknown, event: (typeof window)[Extract<keyof typeof window, `WINDOW_EVENT_${string}`>], data: Record<string | number, unknown>) => void",
3391
+ typed:
3392
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: emitted verbatim as a TS template literal type
3393
+ "(self: unknown, event: (typeof window)[Extract<keyof typeof window, `WINDOW_EVENT_${string}`>], data: window.event_data) => void",
3394
+ },
2730
3395
  ],
2731
3396
  [
2732
3397
  "http.request:param:callback",
2733
- "(self: unknown, id: Hash, response: { status: number; response?: string; headers?: LuaMap<string, string>; path?: string; error?: string; bytes_received?: number; bytes_total?: number; range_start?: number; range_end?: number; document_size?: number }) => void",
3398
+ onBothForms(
3399
+ "(self: unknown, id: Hash, response: { status: number; url: string; response?: string; headers?: LuaMap<string, string>; path?: string; error?: string; bytes_sent?: number; bytes_received?: number; bytes_total?: number; range_start?: number; range_end?: number; document_size?: number }, sender: Url) => void",
3400
+ "With `report_progress` set, the callback also receives progress events carrying `bytes_sent`, `bytes_received`, `bytes_total` and `url` but no `status`; `response.bytes_received !== undefined` marks a progress event, since a final response never carries it.",
3401
+ ),
3402
+ ],
3403
+ [
3404
+ "model.play_anim:param:complete_function",
3405
+ onBothForms(
3406
+ '(self: unknown, message_id: Hash, message: BuiltinMessages["model_animation_done"]) => void',
3407
+ ),
2734
3408
  ],
2735
3409
  [
2736
3410
  "sprite.play_flipbook:param:complete_function",
2737
- "(self: unknown, message_id: Hash, message: { current_tile: number; id: Hash }, sender: Url) => void",
3411
+ onBothForms(
3412
+ "(self: unknown, message_id: Hash, message: { current_tile: number; id: Hash }, sender: Url) => void",
3413
+ ),
2738
3414
  ],
2739
3415
  [
2740
3416
  "collectionproxy.load:param:callback",
2741
- "(self: unknown, message_id: Hash, message: Record<string | number, unknown>, sender: Url) => void",
3417
+ {
3418
+ untyped:
3419
+ "(self: unknown, message_id: Hash, message: Record<string | number, unknown>, sender: Url) => void",
3420
+ typed: null,
3421
+ },
2742
3422
  ],
2743
3423
  ]);
2744
3424
 
@@ -2801,11 +3481,11 @@ export const TABLE_FIELD_TYPE_OVERRIDES: ReadonlyMap<string, string> = new Map([
2801
3481
  ["resource.set_atlas:param:table:animations[].playback", "go.Playback"],
2802
3482
  ["resource.create_atlas:param:table:animations[].playback", "go.Playback"],
2803
3483
  ["resource.get_atlas:return:data:animations[].playback", "go.Playback"],
2804
- ["gui.set:param:options:key", "string | Hash"],
2805
3484
  ["render.set_render_target:param:options:transient", "graphics.BufferType[]"],
2806
3485
  ["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.
3486
+ // The option bags 1.13.2 types as a STRUCT, whose members still carry the
3487
+ // narrower upstream types: `gui.set`'s (its slot is declared in
3488
+ // gui-overloads.d.ts) and `resource.create_sound_data`'s.
2809
3489
  ["gui.set_options:member:key", "string | Hash"],
2810
3490
  ["resource.sound_data_options:member:data", 'string | Opaque<"buffer">'],
2811
3491
  // Bitmasks typed as their flag enum: the create_texture example passes
@@ -2820,6 +3500,11 @@ export const TABLE_FIELD_TYPE_OVERRIDES: ReadonlyMap<string, string> = new Map([
2820
3500
  ] as const
2821
3501
  ).map((struct) => [`${struct}:member:flags`, "graphics.TEXTURE_USAGE_FLAG | number"] as const),
2822
3502
  ["render.render_target_buffer_params:member:flags", "render.RENDER_TARGET_FLAG | number"],
3503
+ // gamesys_script.cpp:PushSampler, behind both get_samplers, pushes the raw
3504
+ // texture type; NormalizeBindableTextureType never runs there. A separate
3505
+ // sampler reports TEXTURE_TYPE_SAMPLER (6) and a separate texture binding one
3506
+ // of the four TEXTURE_TYPE_TEXTURE_* (7-10), none of which has a Lua constant.
3507
+ ["material.sampler_info:member:type", "graphics.TEXTURE_TYPE | 6 | 7 | 8 | 9 | 10"],
2823
3508
  ]);
2824
3509
 
2825
3510
  // A field the engine accepts on an option bag whose `<dl>` upstream otherwise
@@ -2839,13 +3524,6 @@ export interface TableFieldAddition {
2839
3524
  readonly evidence: readonly string[];
2840
3525
  }
2841
3526
 
2842
- const PROPERTY_OPTION_KEYS: TableField = {
2843
- name: "keys",
2844
- types: ["table"],
2845
- optional: true,
2846
- tsType: "(Hash | string)[]",
2847
- };
2848
-
2849
3527
  export const TABLE_SLOT_FIELD_ADDITIONS: ReadonlyMap<string, TableFieldAddition> = new Map<
2850
3528
  string,
2851
3529
  TableFieldAddition
@@ -2860,24 +3538,6 @@ export const TABLE_SLOT_FIELD_ADDITIONS: ReadonlyMap<string, TableFieldAddition>
2860
3538
  ],
2861
3539
  },
2862
3540
  ],
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
- ],
2881
3541
  ]);
2882
3542
 
2883
3543
  // A return-slot field upstream documents as platform-gated or nil-valued while
@@ -2899,32 +3559,13 @@ export interface ReturnFieldOptionalityCorrection {
2899
3559
  readonly evidence: string;
2900
3560
  }
2901
3561
 
2902
- // The platform-gate markers upstream renders as `<span class="icon-*">` decode
2903
- // to `[ios]`, `[android]`, `[html5]` — `user_agent` states its gate through the
2904
- // marker alone, with no availability sentence to pin.
3562
+ // A platform gate alone is not evidence: `sys.get_sys_info` gates four fields
3563
+ // on a platform, yet `Sys_GetSysInfo` pushes each from a zero-filled char array,
3564
+ // so off-platform they are empty strings, never nil.
2905
3565
  export const RETURN_FIELD_OPTIONALITY_CORRECTIONS: ReadonlyMap<
2906
3566
  string,
2907
3567
  ReturnFieldOptionalityCorrection
2908
3568
  > = new Map([
2909
- [
2910
- "sys.get_sys_info:return:sys_info:device_model",
2911
- { upstream: ["string"], evidence: "[ios][android] Only available on iOS and Android." },
2912
- ],
2913
- [
2914
- "sys.get_sys_info:return:sys_info:manufacturer",
2915
- { upstream: ["string"], evidence: "[ios][android] Only available on iOS and Android." },
2916
- ],
2917
- [
2918
- "sys.get_sys_info:return:sys_info:device_ident",
2919
- {
2920
- upstream: ["string"],
2921
- evidence: '[ios] "identifierForVendor" on iOS. [android] "android_id" on Android.',
2922
- },
2923
- ],
2924
- [
2925
- "sys.get_sys_info:return:sys_info:user_agent",
2926
- { upstream: ["string"], evidence: "[html5] The HTTP user agent" },
2927
- ],
2928
3569
  [
2929
3570
  "sys.get_ifaddrs:return:ifaddrs:address",
2930
3571
  { upstream: ["string"], evidence: "might be nil if not available." },
@@ -2975,8 +3616,10 @@ export function applyFieldOptionalityCorrections(
2975
3616
  // `<element>:param:<slot>:<field>`, the TABLE_FIELD_TYPE_OVERRIDES shape, with
2976
3617
  // `<field>.<member>` or `<field>[].<member>` one level down. A `STRUCT` member
2977
3618
  // 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.
3619
+ // (`field-correction-coverage.ts` pairs the two), or for a returned struct whose
3620
+ // binding always sets a member upstream marks optional. The value is the
3621
+ // evidence: the C++ binding, `<file>:<cFunction>`, and how it reads or sets the
3622
+ // field.
2980
3623
  const CHECK_SHAPE_DEF = "CheckShapeDef reads the shape table with luaL_checkinteger(type)";
2981
3624
  const V2_FILTER_DATA =
2982
3625
  "v2/script_box2d_fixture_v2.cpp:CheckFilterData reads each with luaL_checkinteger";
@@ -3000,6 +3643,49 @@ export const OPTIONAL_MEMBER_CORRECTIONS: ReadonlyMap<string, string> = new Map(
3000
3643
  ],
3001
3644
  ]);
3002
3645
 
3646
+ // A `STRUCT` member the engine binding sets while the target's reference omits
3647
+ // it, keyed `<struct>:member:<field>`. It applies only where its binding pin
3648
+ // holds (the `EmitOptions.bindingEvidence` keys), and a struct that already
3649
+ // declares the member throws, so no entry outlives upstream's fix.
3650
+ export interface StructMemberAddition {
3651
+ readonly member: Omit<ApiStructMember, "name">;
3652
+ readonly binding: BindingPin;
3653
+ }
3654
+
3655
+ export const STRUCT_MEMBER_ADDITIONS: ReadonlyMap<string, StructMemberAddition> = new Map([
3656
+ [
3657
+ memberCorrectionKey("graphics.adapter_limits", "max_storage_buffers_per_stage"),
3658
+ {
3659
+ member: {
3660
+ type: "integer",
3661
+ isOptional: false,
3662
+ doc: "Maximum number of storage buffers per shader stage.",
3663
+ },
3664
+ binding: {
3665
+ file: "script/src/script_graphics.cpp",
3666
+ cFunction: "Graphics_GetAdapterInfo",
3667
+ contains: 'lua_setfield(L, -2, "max_storage_buffers_per_stage");',
3668
+ },
3669
+ },
3670
+ ],
3671
+ [
3672
+ memberCorrectionKey("sys.interface_info", "family"),
3673
+ {
3674
+ member: {
3675
+ type: '"ipv4"|"ipv6"',
3676
+ isOptional: true,
3677
+ doc: "Address family, absent for an interface that is neither IPv4 nor IPv6.",
3678
+ },
3679
+ binding: {
3680
+ file: "script/src/script_sys.cpp",
3681
+ cFunction: "Sys_GetIfaddrs",
3682
+ contains:
3683
+ 'lua_pushliteral(L, "ipv6"); } else { lua_pushnil(L); } lua_setfield(L, -2, "family");',
3684
+ },
3685
+ },
3686
+ ],
3687
+ ]);
3688
+
3003
3689
  export const REQUIRED_FIELD_CORRECTIONS: ReadonlyMap<string, string> = new Map<string, string>([
3004
3690
  ...(["mass", "center", "inertia"] as const).map(
3005
3691
  (field) =>
@@ -3093,6 +3779,13 @@ export const REQUIRED_FIELD_CORRECTIONS: ReadonlyMap<string, string> = new Map<s
3093
3779
  "resource.sound_data_options:member:data",
3094
3780
  "script_resource.cpp:CreateSoundData reads the data with CheckBufferOrString",
3095
3781
  ],
3782
+ ...(["device_model", "manufacturer", "device_ident", "user_agent"] as const).map(
3783
+ (field) =>
3784
+ [
3785
+ `sys.sys_info:member:${field}`,
3786
+ 'script_sys.cpp:Sys_GetSysInfo pushes it with lua_pushstring from a zero-filled char array (user_agent falls back to "")',
3787
+ ] as const,
3788
+ ),
3096
3789
  ]);
3097
3790
 
3098
3791
  // Mark the fields REQUIRED_FIELD_CORRECTIONS names for this slot required,
@@ -3594,6 +4287,58 @@ function groupFlattenedTableFields(fields: readonly TableField[]): TableField[]
3594
4287
  return grouped;
3595
4288
  }
3596
4289
 
4290
+ // A TYPEDEF whose aliased type the engine binding contradicts, replaced verbatim.
4291
+ // Member overrides reach only a STRUCT, and these alias inline LuaLS tables.
4292
+ // Each applies only while the typedef still aliases exactly its `upstream` tokens,
4293
+ // in order, so a changed upstream typedef is emitted as written rather than
4294
+ // shadowed; `typedef-correction-provenance.test.ts` pins every entry against the
4295
+ // retained ref-docs. Keyed by the typedef's ref-doc name.
4296
+ export interface TypedefAliasCorrection {
4297
+ readonly upstream: readonly string[];
4298
+ readonly ts: string;
4299
+ readonly reason: string;
4300
+ }
4301
+
4302
+ // One arm per shape a script can build, in upstream's order, each keyed by its
4303
+ // own shape-type constant.
4304
+ const BULLET3D_SHAPE_ARMS = [
4305
+ ["SPHERE", "diameter: number"],
4306
+ ["BOX", "dimensions: Vector3"],
4307
+ ["CAPSULE", "diameter: number; height: number"],
4308
+ ["HULL", "vertices: Vector3[]"],
4309
+ ] as const;
4310
+
4311
+ export const TYPEDEF_ALIAS_CORRECTIONS: ReadonlyMap<string, TypedefAliasCorrection> = new Map([
4312
+ [
4313
+ "render.render_target_params",
4314
+ {
4315
+ upstream: [
4316
+ "{ type?:graphics.TEXTURE_TYPE, sample_count?:integer, [graphics.BUFFER_TYPE]:render.render_target_buffer_params }",
4317
+ ],
4318
+ ts: "{ type?: render.RenderTargetTextureType; sample_count?: number } & { [key: number]: render.render_target_buffer_params }",
4319
+ reason:
4320
+ 'render_script.cpp:RenderScript_RenderTarget raises "Invalid render target texture type" on any `type` but graphics.TEXTURE_TYPE_2D and graphics.TEXTURE_TYPE_CUBE_MAP',
4321
+ },
4322
+ ],
4323
+ [
4324
+ "bullet3d.shape.definition",
4325
+ {
4326
+ upstream: [
4327
+ "{ type:bullet3d.shape.SHAPE_TYPE, diameter:number, position?:vector3, rotation?:quaternion, target_rotation?:quaternion }",
4328
+ "{ type:bullet3d.shape.SHAPE_TYPE, dimensions:vector3, position?:vector3, rotation?:quaternion, target_rotation?:quaternion }",
4329
+ "{ type:bullet3d.shape.SHAPE_TYPE, diameter:number, height:number, position?:vector3, rotation?:quaternion, target_rotation?:quaternion }",
4330
+ "{ type:bullet3d.shape.SHAPE_TYPE, vertices:vector3[], position?:vector3, rotation?:quaternion, target_rotation?:quaternion }",
4331
+ ],
4332
+ ts: BULLET3D_SHAPE_ARMS.map(
4333
+ ([kind, fields]) =>
4334
+ `{ type: typeof bullet3d.shape.SHAPE_TYPE_${kind}; ${fields}; position?: Vector3; rotation?: Quaternion; target_rotation?: Quaternion }`,
4335
+ ).join(" | "),
4336
+ reason:
4337
+ 'script_bullet3d_shape.cpp:CheckBullet3DShapeDef reads only the fields of the table\'s own `type` and raises "Unsupported shape type" on any other, a mesh included',
4338
+ },
4339
+ ],
4340
+ ]);
4341
+
3597
4342
  // A handle the type map widens (`constant_buffer` takes named constants) keeps
3598
4343
  // that shape, so annotating with the alias loses nothing the handle-returning
3599
4344
  // function yields.
@@ -3610,8 +4355,19 @@ function typedefAlias(
3610
4355
  ): string {
3611
4356
  const brand = `Opaque<"${typedef.name}">`;
3612
4357
  const aliasOf = typedef.aliasOf ?? [];
4358
+ const correction = TYPEDEF_ALIAS_CORRECTIONS.get(typedef.name);
4359
+ if (
4360
+ correction !== undefined &&
4361
+ aliasOf.length === correction.upstream.length &&
4362
+ aliasOf.every((token, i) => token === correction.upstream[i])
4363
+ ) {
4364
+ return `type ${local} = ${correction.ts};`;
4365
+ }
3613
4366
  if (aliasOf.length > 0 && !isHandleAlias(aliasOf)) {
3614
- return `type ${local} = ${unionFromTokens(aliasOf, mapType)};`;
4367
+ const armType: DefoldTypeMapper = DISCRIMINATED_MESSAGE_UNIONS.has(typedef.name)
4368
+ ? (token, position) => discriminatedMessageArm(token) ?? mapType(token, position)
4369
+ : mapType;
4370
+ return `type ${local} = ${unionFromTokens(aliasOf, armType)};`;
3615
4371
  }
3616
4372
  const mapped = rawMapType(typedef.name);
3617
4373
  if (mapped.startsWith(brand)) return `type ${local} = ${mapped};`;
@@ -3642,6 +4398,7 @@ function collectTypeDeclarations(
3642
4398
  skip: ReadonlySet<string>,
3643
4399
  rawMapType: (token: string) => string,
3644
4400
  mapType: DefoldTypeMapper,
4401
+ bindingEvidence: ReadonlySet<string>,
3645
4402
  ): TypeDeclaration[] {
3646
4403
  const prefix = `${module.namespace}.`;
3647
4404
  const out: TypeDeclaration[] = [];
@@ -3665,7 +4422,7 @@ function collectTypeDeclarations(
3665
4422
  for (const struct of module.structs ?? []) {
3666
4423
  const local = localOf(struct.name);
3667
4424
  if (local === null || skip.has(local)) continue;
3668
- out.push(structDeclaration(struct, local, mapType, module.structs ?? []));
4425
+ out.push(structDeclaration(struct, local, mapType, module.structs ?? [], bindingEvidence));
3669
4426
  }
3670
4427
  return out;
3671
4428
  }
@@ -3718,10 +4475,22 @@ function structDeclaration(
3718
4475
  path: string,
3719
4476
  mapType: DefoldTypeMapper,
3720
4477
  structs: readonly ApiStruct[],
4478
+ bindingEvidence: ReadonlySet<string>,
3721
4479
  ): TypeDeclaration {
3722
4480
  const name = path.slice(path.lastIndexOf(".") + 1);
3723
4481
  const memberPrefix = memberCorrectionKey(struct.name, "");
3724
4482
  const declared = new Set(struct.members.map((member) => member.name));
4483
+ const redundant = [...STRUCT_MEMBER_ADDITIONS.keys()].filter(
4484
+ (key) => key.startsWith(memberPrefix) && declared.has(key.slice(memberPrefix.length)),
4485
+ );
4486
+ if (redundant.length > 0) {
4487
+ throw new Error(
4488
+ `struct member addition names a declared member: ${redundant.sort().join(", ")}`,
4489
+ );
4490
+ }
4491
+ const added = [...STRUCT_MEMBER_ADDITIONS]
4492
+ .filter(([key]) => key.startsWith(memberPrefix) && bindingEvidence.has(key))
4493
+ .map(([key, addition]) => ({ ...addition.member, name: key.slice(memberPrefix.length) }));
3725
4494
  const stale = [
3726
4495
  ...REQUIRED_FIELD_CORRECTIONS.keys(),
3727
4496
  ...LIST_FIELD_CORRECTIONS.keys(),
@@ -3731,7 +4500,7 @@ function structDeclaration(
3731
4500
  if (stale.length > 0) {
3732
4501
  throw new Error(`member-keyed field correction names no member: ${stale.sort().join(", ")}`);
3733
4502
  }
3734
- const members = struct.members.map((member) => {
4503
+ const members = [...struct.members, ...added].map((member) => {
3735
4504
  const key = memberCorrectionKey(struct.name, member.name);
3736
4505
  const list = LIST_FIELD_CORRECTIONS.get(key);
3737
4506
  const retyped = TABLE_FIELD_TYPE_OVERRIDES.get(key);
@@ -3935,11 +4704,14 @@ export function emitDeclarations(documented: ApiModule, options?: EmitOptions):
3935
4704
  const knownConstantFqns = options?.knownConstantFqns;
3936
4705
  const translations = options?.translations ?? {};
3937
4706
  const urlParameters = options?.urlParameters ?? [];
4707
+ const bindingEvidence = options?.bindingEvidence ?? new Set<string>();
4708
+ const nilRaisingSlots = options?.nilRaisingSlots;
3938
4709
  const baseNotes = indexBaseNoteResolver(options?.indexBaseSource ?? "engine");
3939
4710
  const { rawMapType, mapType } = moduleTypeMappers(module, constantsByFqn, options);
3940
4711
  const constantTokens = constantSlotTokenResolver(
3941
4712
  module,
3942
4713
  new Set([...constantsByFqn.keys(), ...(knownConstantFqns ?? [])]),
4714
+ options?.withheldFunctions,
3943
4715
  );
3944
4716
 
3945
4717
  const constants = module.constants
@@ -3991,7 +4763,13 @@ export function emitDeclarations(documented: ApiModule, options?: EmitOptions):
3991
4763
  // A typedef that also has colon methods is emitted as a method-bearing
3992
4764
  // interface below, not an opaque brand alias.
3993
4765
  const typeDeclarations = placeTypeDeclarations(
3994
- collectTypeDeclarations(module, new Set(handleGroups.keys()), rawMapType, mapType),
4766
+ collectTypeDeclarations(
4767
+ module,
4768
+ new Set(handleGroups.keys()),
4769
+ rawMapType,
4770
+ mapType,
4771
+ bindingEvidence,
4772
+ ),
3995
4773
  nestedRoot,
3996
4774
  );
3997
4775
 
@@ -4013,6 +4791,16 @@ export function emitDeclarations(documented: ApiModule, options?: EmitOptions):
4013
4791
  for (const aliasDecl of constantUnionAliasDeclarations(module.namespace)) {
4014
4792
  lines.push(`${INDENT}${decl}${aliasDecl}`);
4015
4793
  }
4794
+ for (const aliasDecl of overloadSlotAliasDeclarations(
4795
+ [...module.functions, ...(options?.withheldFunctions ?? [])],
4796
+ prefix,
4797
+ mapType,
4798
+ constantTokens,
4799
+ urlParameters,
4800
+ bindingEvidence,
4801
+ )) {
4802
+ lines.push(`${INDENT}${decl}${aliasDecl}`);
4803
+ }
4016
4804
  for (const alias of enumAliases.topLevel) {
4017
4805
  lines.push(
4018
4806
  `${INDENT}${decl}${unionAliasDeclaration(alias.name, alias.members, alias.nilable)}`,
@@ -4027,7 +4815,7 @@ export function emitDeclarations(documented: ApiModule, options?: EmitOptions):
4027
4815
  lines.push(docLine);
4028
4816
  }
4029
4817
  lines.push(
4030
- `${handleIndent}${emitMethod(fn, mapType, resolver, constantTokens, urlParameters)}`,
4818
+ `${handleIndent}${emitMethod(fn, mapType, resolver, constantTokens, urlParameters, bindingEvidence)}`,
4031
4819
  );
4032
4820
  }
4033
4821
  lines.push(`${INDENT}}`);
@@ -4056,10 +4844,20 @@ export function emitDeclarations(documented: ApiModule, options?: EmitOptions):
4056
4844
  for (const fn of functions) {
4057
4845
  const reserved = TS_RESERVED_NAMES.has(fn.name);
4058
4846
  const emitName = aliasName(fn.name, aliases);
4059
- for (const docLine of functionDocLines(fn.original, translations, baseNotes))
4060
- lines.push(docLine);
4061
- const line = emitFunction(fn, emitName, mapType, resolver, constantTokens, urlParameters);
4062
- lines.push(`${INDENT}${reserved ? "" : decl}${line}`);
4847
+ for (const arm of callArms(fn, nilRaisingSlots, module.structs ?? [])) {
4848
+ for (const docLine of functionDocLines(fn.original, translations, baseNotes))
4849
+ lines.push(docLine);
4850
+ const line = emitFunction(
4851
+ arm,
4852
+ emitName,
4853
+ mapType,
4854
+ resolver,
4855
+ constantTokens,
4856
+ urlParameters,
4857
+ bindingEvidence,
4858
+ );
4859
+ lines.push(`${INDENT}${reserved ? "" : decl}${line}`);
4860
+ }
4063
4861
  const alternative = firstSlotAlternative(fn, module.namespace);
4064
4862
  if (alternative !== null) {
4065
4863
  // The overload documents its own parameters, so its hover names the base
@@ -4074,6 +4872,7 @@ export function emitDeclarations(documented: ApiModule, options?: EmitOptions):
4074
4872
  resolver,
4075
4873
  constantTokens,
4076
4874
  urlParameters,
4875
+ bindingEvidence,
4077
4876
  );
4078
4877
  lines.push(`${INDENT}${reserved ? "" : decl}${overload}`);
4079
4878
  }
@@ -4148,7 +4947,7 @@ export function emitDeclarations(documented: ApiModule, options?: EmitOptions):
4148
4947
  lines.push(docLine);
4149
4948
  }
4150
4949
  lines.push(
4151
- `${bodyIndent}${reserved ? "" : segmentDecl}${emitFunction(fn, emitName, mapType, resolver, constantTokens, urlParameters)}`,
4950
+ `${bodyIndent}${reserved ? "" : segmentDecl}${emitFunction(fn, emitName, mapType, resolver, constantTokens, urlParameters, bindingEvidence)}`,
4152
4951
  );
4153
4952
  }
4154
4953
  aliasReservedSegments(group.children.keys(), segmentAliases);
@@ -4179,6 +4978,55 @@ export function emitDeclarations(documented: ApiModule, options?: EmitOptions):
4179
4978
  return `${lines.join("\n")}\n`;
4180
4979
  }
4181
4980
 
4981
+ // One `type <alias> = <slot type>;` per `OVERLOAD_SLOT_ALIASES` slot these
4982
+ // functions carry, in alias order. The type is the one `emitFunction` reports
4983
+ // for the slot, so it reads as the function's own signature would. The
4984
+ // resolver spans these functions alone, leaving the module's own untouched.
4985
+ function overloadSlotAliasDeclarations(
4986
+ functions: readonly ApiFunction[],
4987
+ prefix: string,
4988
+ mapType: DefoldTypeMapper,
4989
+ constantTokens: ConstantSlotTokens,
4990
+ urlParameters: UrlParameterTable,
4991
+ bindingEvidence: ReadonlySet<string>,
4992
+ ): string[] {
4993
+ const resolver = buildTableDocResolver(
4994
+ functions.map((fn) => ({ name: fn.name, slots: [...fn.parameters, ...fn.returnValues] })),
4995
+ );
4996
+ const types = new Map<string, string>();
4997
+ for (const fn of functions) {
4998
+ for (const [position, p] of fn.parameters.entries()) {
4999
+ const alias = OVERLOAD_SLOT_ALIASES.get(tableSlotKey(fn.name, "param", p.name));
5000
+ if (alias === undefined) continue;
5001
+ const prepared = prepareFunction(fn, prefix);
5002
+ if (prepared === null) continue;
5003
+ const slots: Record<string, SlotType> = {};
5004
+ emitFunction(
5005
+ prepared,
5006
+ prepared.name,
5007
+ mapType,
5008
+ resolver,
5009
+ constantTokens,
5010
+ urlParameters,
5011
+ bindingEvidence,
5012
+ slots,
5013
+ );
5014
+ const slot = slots[slotKey("param", position, p.name)];
5015
+ if (slot === undefined) {
5016
+ throw new Error(`${fn.name} reports no type for its aliased slot "${p.name}"`);
5017
+ }
5018
+ const seen = types.get(alias);
5019
+ if (seen !== undefined && seen !== slot.type) {
5020
+ throw new Error(`${alias} is typed both ${seen} and ${slot.type}`);
5021
+ }
5022
+ types.set(alias, slot.type);
5023
+ }
5024
+ }
5025
+ return [...types]
5026
+ .sort(([a], [b]) => a.localeCompare(b))
5027
+ .map(([alias, type]) => `type ${alias} = ${type};`);
5028
+ }
5029
+
4182
5030
  /**
4183
5031
  * Every documented slot's rendered TS type and optionality, keyed
4184
5032
  * `param|return:<position>:<raw ref-doc name>`. The position is part of the key
@@ -4232,10 +5080,13 @@ export function emitSymbolSignatures(
4232
5080
  const constantsByFqn = new Map(module.constants.map((c) => [c.name, c]));
4233
5081
  const knownConstantFqns = options?.knownConstantFqns;
4234
5082
  const urlParameters = options?.urlParameters ?? [];
5083
+ const bindingEvidence = options?.bindingEvidence ?? new Set<string>();
5084
+ const nilRaisingSlots = options?.nilRaisingSlots;
4235
5085
  const { rawMapType, mapType } = moduleTypeMappers(module, constantsByFqn, options);
4236
5086
  const constantTokens = constantSlotTokenResolver(
4237
5087
  module,
4238
5088
  new Set([...constantsByFqn.keys(), ...(knownConstantFqns ?? [])]),
5089
+ options?.withheldFunctions,
4239
5090
  );
4240
5091
  const resolver = buildTableDocResolver(
4241
5092
  module.functions.map((fn) => ({
@@ -4262,27 +5113,30 @@ export function emitSymbolSignatures(
4262
5113
  const prepared = prepareFunction(fn, prefix);
4263
5114
  if (prepared === null) continue;
4264
5115
  const slotTypes: Record<string, SlotType> = {};
5116
+ const [first = prepared, ...later] = callArms(prepared, nilRaisingSlots, module.structs ?? []);
4265
5117
  const primary = emitFunction(
4266
- prepared,
5118
+ first,
4267
5119
  emitName(prepared.name),
4268
5120
  mapType,
4269
5121
  resolver,
4270
5122
  constantTokens,
4271
5123
  urlParameters,
5124
+ bindingEvidence,
4272
5125
  slotTypes,
4273
5126
  );
4274
5127
  const alternative = firstSlotAlternative(prepared, module.namespace);
4275
- const tsSignature =
4276
- alternative === null
4277
- ? primary
4278
- : `${primary}\n${emitFunction(
4279
- alternative,
4280
- emitName(prepared.name),
4281
- mapType,
4282
- resolver,
4283
- constantTokens,
4284
- urlParameters,
4285
- )}`;
5128
+ const arms = [...later, ...(alternative === null ? [] : [alternative])].map((arm) =>
5129
+ emitFunction(
5130
+ arm,
5131
+ emitName(prepared.name),
5132
+ mapType,
5133
+ resolver,
5134
+ constantTokens,
5135
+ urlParameters,
5136
+ bindingEvidence,
5137
+ ),
5138
+ );
5139
+ const tsSignature = [primary, ...arms].join("\n");
4286
5140
  out.push({ identity: fnIdentity(fn), tsSignature, slotTypes });
4287
5141
  }
4288
5142
 
@@ -4323,6 +5177,7 @@ export function emitSymbolSignatures(
4323
5177
  resolver,
4324
5178
  constantTokens,
4325
5179
  urlParameters,
5180
+ bindingEvidence,
4326
5181
  slotTypes,
4327
5182
  ),
4328
5183
  slotTypes,
@@ -4341,6 +5196,7 @@ export function emitSymbolSignatures(
4341
5196
  resolver,
4342
5197
  constantTokens,
4343
5198
  urlParameters,
5199
+ bindingEvidence,
4344
5200
  slotTypes,
4345
5201
  ),
4346
5202
  slotTypes,
@@ -4389,6 +5245,7 @@ export function emitSymbolSignatures(
4389
5245
  new Set(handleGroups.keys()),
4390
5246
  rawMapType,
4391
5247
  mapType,
5248
+ bindingEvidence,
4392
5249
  )) {
4393
5250
  out.push({
4394
5251
  identity: {
@@ -4436,6 +5293,12 @@ interface PreparedConstant {
4436
5293
  interface PreparedFunction {
4437
5294
  name: string;
4438
5295
  original: ApiFunction;
5296
+ // Parameter positions declared required whatever their doc says: the
5297
+ // nil-raising slots an arm of `nilRaisingArms` takes.
5298
+ required?: ReadonlySet<number>;
5299
+ // A rewrite of the mapped type at a parameter position: the fields an arm of
5300
+ // `presenceArms` requires.
5301
+ retype?: ReadonlyMap<number, (type: string) => string>;
4439
5302
  }
4440
5303
 
4441
5304
  interface PreparedVariable {
@@ -4719,10 +5582,13 @@ export const NIL_CONSTANTS: ReadonlyMap<string, string> = new Map([
4719
5582
  // `undocumented-constant-provenance.test.ts` reds an entry no committed target
4720
5583
  // still needs. `sameValueAs` names the documented constant the engine registers
4721
5584
  // with the same value; where the surface declares it, the back-fill takes its
4722
- // type, so a value compares equal to either spelling.
5585
+ // type, so a value compares equal to either spelling. `memberOf` names the
5586
+ // ref-doc `ENUM` the engine returns the constant through; the back-fill joins it
5587
+ // only on a surface whose ref-doc declares that enum, so an older surface
5588
+ // without one keeps a plain constant.
4723
5589
  export const UNDOCUMENTED_CONSTANTS: ReadonlyMap<
4724
5590
  string,
4725
- { readonly doc: string; readonly sameValueAs?: string }
5591
+ { readonly doc: string; readonly sameValueAs?: string; readonly memberOf?: string }
4726
5592
  > = new Map([
4727
5593
  [
4728
5594
  "render.CONTEXT_EVENT_CONTEXT_LOST",
@@ -4743,6 +5609,13 @@ export const UNDOCUMENTED_CONSTANTS: ReadonlyMap<
4743
5609
  sameValueAs: "window.WINDOW_EVENT_ICONIFIED",
4744
5610
  },
4745
5611
  ],
5612
+ [
5613
+ "liveupdate.LIVEUPDATE_NOT_INITIALIZED",
5614
+ {
5615
+ doc: "Live update is not initialized. `liveupdate.add_mount` and `liveupdate.remove_mount` return it while live update is disabled.",
5616
+ memberOf: "liveupdate.LIVEUPDATE",
5617
+ },
5618
+ ],
4746
5619
  ]);
4747
5620
 
4748
5621
  function withUndocumentedConstants(
@@ -4752,7 +5625,8 @@ function withUndocumentedConstants(
4752
5625
  const prefix = `${module.namespace}.`;
4753
5626
  const documented = new Set(module.constants.map((c) => c.name));
4754
5627
  const backfill: ApiConstant[] = [];
4755
- for (const [fqn, { doc, sameValueAs }] of UNDOCUMENTED_CONSTANTS) {
5628
+ const joined = new Map<string, string[]>();
5629
+ for (const [fqn, { doc, sameValueAs, memberOf }] of UNDOCUMENTED_CONSTANTS) {
4756
5630
  if (!fqn.startsWith(prefix) || fqn.slice(prefix.length).includes(".")) continue;
4757
5631
  if (documented.has(fqn)) continue;
4758
5632
  if (registered !== undefined && !registered.has(fqn)) continue;
@@ -4764,10 +5638,18 @@ function withUndocumentedConstants(
4764
5638
  description: "",
4765
5639
  ...(aliasOf === undefined ? {} : { aliasOf }),
4766
5640
  });
5641
+ if (memberOf !== undefined) joined.set(memberOf, [...(joined.get(memberOf) ?? []), fqn]);
4767
5642
  }
4768
- return backfill.length === 0
4769
- ? module
4770
- : { ...module, constants: [...module.constants, ...backfill] };
5643
+ if (backfill.length === 0) return module;
5644
+ const enums = module.enums?.map((e) => {
5645
+ const members = joined.get(e.name);
5646
+ return members === undefined ? e : { ...e, members: [...e.members, ...members] };
5647
+ });
5648
+ return {
5649
+ ...module,
5650
+ constants: [...module.constants, ...backfill],
5651
+ ...(enums === undefined ? {} : { enums }),
5652
+ };
4771
5653
  }
4772
5654
 
4773
5655
  // `constant` is the module's own element for `fqn`, absent for a constant another
@@ -4846,14 +5728,30 @@ function memberSignature(
4846
5728
  resolver: TableDocResolver,
4847
5729
  constantTokens: ConstantSlotTokens,
4848
5730
  urlParameters: UrlParameterTable,
5731
+ bindingEvidence: ReadonlySet<string>,
4849
5732
  slotSink?: Record<string, SlotType>,
4850
5733
  ): string {
4851
5734
  const original = prepared.original.parameters;
4852
5735
  const elementName = prepared.original.name;
4853
- const cutoff = trailingOptionalCutoff(original, elementName);
5736
+ const required = prepared.required ?? new Set<number>();
5737
+ const cutoff = Math.max(
5738
+ trailingOptionalCutoff(original, elementName),
5739
+ ...[...required].map((index) => index + 1),
5740
+ );
4854
5741
  const varargIndex = original.findIndex(isVarargParameter);
4855
5742
  const positional = (varargIndex === -1 ? original : original.slice(0, varargIndex)).map((p, i) =>
4856
- emitParameter(p, i, i >= cutoff, mapType, resolver, constantTokens, elementName, urlParameters),
5743
+ emitParameter(
5744
+ p,
5745
+ i,
5746
+ i >= cutoff,
5747
+ mapType,
5748
+ resolver,
5749
+ constantTokens,
5750
+ elementName,
5751
+ urlParameters,
5752
+ required.has(i),
5753
+ prepared.retype?.get(i),
5754
+ ),
4857
5755
  );
4858
5756
  const rest =
4859
5757
  varargIndex === -1
@@ -4874,6 +5772,7 @@ function memberSignature(
4874
5772
  resolver,
4875
5773
  constantTokens,
4876
5774
  elementName,
5775
+ bindingEvidence,
4877
5776
  );
4878
5777
  const predicateToken = TYPE_PREDICATES.get(elementName);
4879
5778
  const soleParam = original[0];
@@ -4901,7 +5800,7 @@ function memberSignature(
4901
5800
  if (isPredicate && soleParam !== undefined && predicateToken !== undefined) {
4902
5801
  return `${name}(${params}): ${safeParamName(soleParam.name, 0)} is ${mapType(predicateToken)};`;
4903
5802
  }
4904
- return `${name}(${params}): ${ret.type};${ret.trailing}`;
5803
+ return `${name}${box2dBackendTypeParameters(prepared.original)}(${params}): ${ret.type};${ret.trailing}`;
4905
5804
  }
4906
5805
 
4907
5806
  function emitFunction(
@@ -4911,9 +5810,10 @@ function emitFunction(
4911
5810
  resolver: TableDocResolver,
4912
5811
  constantTokens: ConstantSlotTokens,
4913
5812
  urlParameters: UrlParameterTable,
5813
+ bindingEvidence: ReadonlySet<string>,
4914
5814
  slotSink?: Record<string, SlotType>,
4915
5815
  ): string {
4916
- return `function ${memberSignature(prepared, name, mapType, resolver, constantTokens, urlParameters, slotSink)}`;
5816
+ return `function ${memberSignature(prepared, name, mapType, resolver, constantTokens, urlParameters, bindingEvidence, slotSink)}`;
4917
5817
  }
4918
5818
 
4919
5819
  // A colon-method member of a handle interface: identical signature machinery to a
@@ -4925,6 +5825,7 @@ function emitMethod(
4925
5825
  resolver: TableDocResolver,
4926
5826
  constantTokens: ConstantSlotTokens,
4927
5827
  urlParameters: UrlParameterTable,
5828
+ bindingEvidence: ReadonlySet<string>,
4928
5829
  slotSink?: Record<string, SlotType>,
4929
5830
  ): string {
4930
5831
  return memberSignature(
@@ -4934,6 +5835,7 @@ function emitMethod(
4934
5835
  resolver,
4935
5836
  constantTokens,
4936
5837
  urlParameters,
5838
+ bindingEvidence,
4937
5839
  slotSink,
4938
5840
  );
4939
5841
  }
@@ -4956,14 +5858,20 @@ function functionDocLines(
4956
5858
  baseNotes: IndexBaseNoteResolver,
4957
5859
  indent: string = INDENT,
4958
5860
  ): string[] {
4959
- const params = fn.parameters.map((p, index) => ({
4960
- name: emittedParamName(p, index),
4961
- doc: appendNotes(
4962
- translateProseFences(htmlToDocText(p.doc), fn.name, translations),
4963
- baseNotes(fn.name, "param", p.name),
4964
- ),
4965
- }));
5861
+ const params = fn.parameters.map((p, index) => {
5862
+ const curationNote = CALLBACK_SIGNATURE_CURATIONS.get(
5863
+ tableSlotKey(fn.name, "param", p.name),
5864
+ )?.note;
5865
+ return {
5866
+ name: emittedParamName(p, index),
5867
+ doc: appendNotes(translateProseFences(htmlToDocText(p.doc), fn.name, translations), [
5868
+ ...baseNotes(fn.name, "param", p.name),
5869
+ ...(curationNote === undefined ? [] : [curationNote]),
5870
+ ]),
5871
+ };
5872
+ });
4966
5873
  const onlyReturn = fn.returnValues.length === 1 ? fn.returnValues[0] : undefined;
5874
+ const sparseNote = SPARSE_ARRAY_RETURNS.get(fn.name)?.note;
4967
5875
  const notedReturns =
4968
5876
  multiReturnDoc(fn, translations, baseNotes) ?? overrideReturnDoc(fn, baseNotes);
4969
5877
  const lua = htmlToCodeText(fn.examples ?? "");
@@ -4996,7 +5904,10 @@ function functionDocLines(
4996
5904
  ? {
4997
5905
  returns: appendNotes(
4998
5906
  translateProseFences(htmlToDocText(onlyReturn.doc), fn.name, translations),
4999
- baseNotes(fn.name, "return", onlyReturn.name),
5907
+ [
5908
+ ...baseNotes(fn.name, "return", onlyReturn.name),
5909
+ ...(sparseNote === undefined ? [] : [sparseNote]),
5910
+ ],
5000
5911
  ),
5001
5912
  }
5002
5913
  : notedReturns !== undefined
@@ -5258,27 +6169,43 @@ function emitParameter(
5258
6169
  constantTokens: ConstantSlotTokens,
5259
6170
  elementName: string,
5260
6171
  urlParameters: UrlParameterTable,
6172
+ required = false,
6173
+ retype: (type: string) => string = (type) => type,
5261
6174
  ): { text: string; type: string; optional: boolean } {
5262
6175
  const name = safeParamName(p.name, index);
5263
- const ts = parameterType(p, mapType, resolver, constantTokens, elementName, urlParameters);
6176
+ const ts = retype(
6177
+ parameterType(p, mapType, resolver, constantTokens, elementName, urlParameters),
6178
+ );
5264
6179
  // An interior doc-optional param (a required param follows, so the trailing-`?`
5265
6180
  // projection cannot mark it) keeps its optionality as `| undefined` — TSTL
5266
6181
  // lowers `undefined` to `nil`, the faithful call. Trailing optionals keep the
5267
6182
  // `?` form; required params are untouched.
5268
6183
  const nilRequired = requiredSlotCorrection(p, elementName)?.nil === true;
5269
- const interiorOptional = !optional && (isDocOptional(p, elementName) || nilRequired);
6184
+ const interiorOptional = !optional && !required && (isDocOptional(p, elementName) || nilRequired);
5270
6185
  // `(a) => void | undefined` reads as a function returning `void | undefined`.
5271
6186
  const operand = interiorOptional && isFunctionTypeText(ts) ? `(${ts})` : ts;
5272
6187
  const type = `${operand}${interiorOptional ? " | undefined" : ""}`;
5273
6188
  return { text: `${name}${optional ? "?" : ""}: ${type}`, type, optional };
5274
6189
  }
5275
6190
 
6191
+ // A `nullable` correction rewrites only the `nil` in the declared tokens and
6192
+ // leaves the rest to the ordinary mapping and its top-level `nil` projection.
6193
+ function correctedReturnTokens(
6194
+ types: readonly string[],
6195
+ correction: ReturnTypeCorrection | undefined,
6196
+ ): readonly string[] {
6197
+ if (correction?.nullable === undefined) return types;
6198
+ const base = types.filter((token) => token !== "nil");
6199
+ return correction.nullable ? [...base, "nil"] : base;
6200
+ }
6201
+
5276
6202
  function emitReturn(
5277
6203
  returnValues: ApiParameter[],
5278
6204
  mapType: DefoldTypeMapper,
5279
6205
  resolver: TableDocResolver,
5280
6206
  constantTokens: ConstantSlotTokens,
5281
6207
  elementName: string,
6208
+ bindingEvidence: ReadonlySet<string>,
5282
6209
  ): { type: string; trailing: string; slots: EmittedSlot[] } {
5283
6210
  // An authored whole-return override is not a per-slot render, so it reports no
5284
6211
  // slots and leaves the consumer on its token-derived fallback.
@@ -5287,23 +6214,30 @@ function emitReturn(
5287
6214
  // Kept beside the override lookup so the two stay visibly distinct: this one
5288
6215
  // contradicts a declared token rather than filling an empty `returnvalues`,
5289
6216
  // and it is a per-slot render, so it reports its slot.
5290
- const correction = RETURN_TYPE_CORRECTIONS.get(elementName);
6217
+ const listed = RETURN_TYPE_CORRECTIONS.get(elementName);
6218
+ const correction =
6219
+ listed?.binding === undefined || bindingEvidence.has(elementName) ? listed : undefined;
5291
6220
  if (returnValues.length === 0) return { type: "void", trailing: "", slots: [] };
5292
6221
  if (returnValues.length > 1) {
5293
6222
  // Defold multi-returns are positional and always present; each slot maps
5294
6223
  // straight through (unknown when the doc lists no type) into a tuple that
5295
6224
  // typescript-to-lua erases to `local a, b = fn()`.
5296
- const emitted = returnValues.map((rv, index) => ({
5297
- position: index,
5298
- name: rv.name,
5299
- ts:
6225
+ const emitted = returnValues.map((rv, index) => {
6226
+ const applied =
5300
6227
  correction?.slot !== undefined &&
5301
6228
  correction.slot === rv.name &&
5302
6229
  pinMatches(rv.types, correctionPins(correction))
5303
- ? correction.ts
5304
- : rv.types.length > 0
6230
+ ? correction
6231
+ : undefined;
6232
+ const types = correctedReturnTokens(rv.types, applied);
6233
+ return {
6234
+ position: index,
6235
+ name: rv.name,
6236
+ ts:
6237
+ applied?.ts ??
6238
+ (types.length > 0
5305
6239
  ? mapSlotUnion(
5306
- rv.types,
6240
+ types,
5307
6241
  rv.doc,
5308
6242
  mapType,
5309
6243
  false,
@@ -5313,8 +6247,9 @@ function emitReturn(
5313
6247
  "return",
5314
6248
  rv.name,
5315
6249
  )
5316
- : "unknown",
5317
- }));
6250
+ : "unknown"),
6251
+ };
6252
+ });
5318
6253
  return {
5319
6254
  type: `LuaMultiReturn<[${emitted.map((slot) => slot.ts).join(", ")}]>`,
5320
6255
  trailing: "",
@@ -5323,21 +6258,24 @@ function emitReturn(
5323
6258
  }
5324
6259
  const first = returnValues[0];
5325
6260
  if (!first) return { type: "void", trailing: "", slots: [] };
5326
- if (
6261
+ const applied =
5327
6262
  correction !== undefined &&
5328
6263
  correction.slot === undefined &&
5329
6264
  pinMatches(first.types, correctionPins(correction))
5330
- ) {
6265
+ ? correction
6266
+ : undefined;
6267
+ if (applied?.ts !== undefined) {
5331
6268
  return {
5332
- type: correction.ts,
6269
+ type: applied.ts,
5333
6270
  trailing: "",
5334
- slots: [{ position: 0, name: first.name, ts: correction.ts }],
6271
+ slots: [{ position: 0, name: first.name, ts: applied.ts }],
5335
6272
  };
5336
6273
  }
6274
+ const types = correctedReturnTokens(first.types, applied);
5337
6275
  const ts =
5338
- first.types.length > 0
6276
+ types.length > 0
5339
6277
  ? mapSlotUnion(
5340
- first.types,
6278
+ types,
5341
6279
  first.doc,
5342
6280
  mapType,
5343
6281
  false,
@@ -5361,6 +6299,26 @@ function emitReturn(
5361
6299
  // only".
5362
6300
  const READ_ONLY_MARK = /^\s*<span class="mark">READ ONLY<\/span>/;
5363
6301
 
6302
+ // A property the engine reads but refuses to write while upstream omits the
6303
+ // READ ONLY mark. `evidence` names the component source that shows the missing
6304
+ // write branch. The emit throws once upstream adds the mark, so the entry is
6305
+ // deleted rather than outliving the defect it corrects.
6306
+ export interface ReadOnlyPropertyCorrection {
6307
+ readonly evidence: string;
6308
+ }
6309
+
6310
+ // Keyed `<namespace>.<property>`, mirroring PROPERTY_TYPE_CORRECTIONS.
6311
+ export const READ_ONLY_PROPERTY_CORRECTIONS: ReadonlyMap<string, ReadOnlyPropertyCorrection> =
6312
+ new Map([
6313
+ [
6314
+ "model.animation",
6315
+ {
6316
+ evidence:
6317
+ "comp_model.cpp:CompModelSetProperty has no animation branch, so the write falls through to the material-constant lookup and fails",
6318
+ },
6319
+ ],
6320
+ ]);
6321
+
5364
6322
  function emitPropertyMembers(
5365
6323
  p: ApiProperty,
5366
6324
  mapType: DefoldTypeMapper,
@@ -5381,10 +6339,59 @@ function emitPropertyMembers(
5381
6339
  : p.types.length > 0
5382
6340
  ? unionFromTokens(p.types, mapType)
5383
6341
  : "unknown";
5384
- const modifier = READ_ONLY_MARK.test(p.description) ? "readonly " : "";
6342
+ const marked = READ_ONLY_MARK.test(p.description);
6343
+ const readOnlyCorrected = READ_ONLY_PROPERTY_CORRECTIONS.has(`${namespace}.${p.name}`);
6344
+ if (marked && readOnlyCorrected) {
6345
+ throw new Error(
6346
+ `read-only property correction ${namespace}.${p.name}: upstream marks the property READ ONLY`,
6347
+ );
6348
+ }
6349
+ const modifier = marked || readOnlyCorrected ? "readonly " : "";
5385
6350
  return keys.map((key) => `${modifier}${key}: ${ts};`);
5386
6351
  }
5387
6352
 
6353
+ // A function whose returned table the engine binding keys by a sparse integer,
6354
+ // so the sequence its reference declares is a map with holes: `ipairs` stops at
6355
+ // the first hole, and `#t` may return any border. Keyed by function name, since the return slot is named
6356
+ // `table` in 1.13.0/1.13.1 and `textures` from 1.13.2. The return's `T[]` member
6357
+ // emits as `LuaMap<number, T>`, which `for...of` lowers to `pairs`, and `note`
6358
+ // joins its `@returns`.
6359
+ export const SPARSE_ARRAY_RETURNS: ReadonlyMap<string, { note: string; reason: string }> = new Map([
6360
+ [
6361
+ "material.get_textures",
6362
+ {
6363
+ note: "Keyed by sampler slot from 1; a slot with no texture has no entry, so iterate with `for (const [slot, info] of textures)`.",
6364
+ reason:
6365
+ "script_material.cpp:Material_GetTextures pushes key i+1 only for a slot that has a texture, unlike Compute_GetTextures, which packs its keys",
6366
+ },
6367
+ ],
6368
+ ]);
6369
+
6370
+ function sparseArrayMap(ts: string, elementName: string): string {
6371
+ const members = unionMembers(ts);
6372
+ const arrays = members.filter((member) => member.endsWith("[]"));
6373
+ if (arrays.length === 0) {
6374
+ throw new Error(`SPARSE_ARRAY_RETURNS ${elementName}: the return emits no array (${ts})`);
6375
+ }
6376
+ return members
6377
+ .map((member) => {
6378
+ if (!member.endsWith("[]")) return member;
6379
+ const element = member.slice(0, -2);
6380
+ return `LuaMap<number, ${wrappedInParens(element) ? element.slice(1, -1) : element}>`;
6381
+ })
6382
+ .join(" | ");
6383
+ }
6384
+
6385
+ function wrappedInParens(text: string): boolean {
6386
+ if (!text.startsWith("(")) return false;
6387
+ let depth = 0;
6388
+ for (let i = 0; i < text.length; i++) {
6389
+ if (text[i] === "(") depth++;
6390
+ else if (text[i] === ")" && --depth === 0) return i === text.length - 1;
6391
+ }
6392
+ return false;
6393
+ }
6394
+
5388
6395
  // Like `unionFromTokens`, but a `table` token whose slot doc carries a parseable
5389
6396
  // `<dl>` field list emits an inline object type instead of the opaque `Record`
5390
6397
  // fallback. Other tokens in the union map unchanged. `optionalFields` marks the
@@ -5465,6 +6472,10 @@ function mapSlotUnion(
5465
6472
  }
5466
6473
  } else if (element !== undefined) {
5467
6474
  ts = arrayTypeFromTokens(element, mapType);
6475
+ if (curation?.kind === "array" && curation.pairs === true) {
6476
+ const tokens = typeof element === "string" ? [element] : element;
6477
+ ts = `${ts} | LuaPairsIterable<AnyNotNil, ${unionFromTokens(tokens, mapType)}>`;
6478
+ }
5468
6479
  } else if (curation?.kind === "verbatim") {
5469
6480
  ts = curation.ts;
5470
6481
  } else if (curation?.kind === "object" || curation?.kind === "array-object") {
@@ -5521,19 +6532,27 @@ function mapSlotUnion(
5521
6532
  // return slots.
5522
6533
  ts = "undefined";
5523
6534
  } else {
5524
- const curated =
6535
+ const curation =
5525
6536
  slotKind !== undefined && slotName !== undefined
5526
6537
  ? CALLBACK_SIGNATURE_CURATIONS.get(tableSlotKey(elementName, slotKind, slotName))
5527
6538
  : undefined;
5528
- ts = curated ?? mapType(token);
6539
+ if (curation === undefined) {
6540
+ ts = mapType(token);
6541
+ } else if (isLualsFunctionToken(token)) {
6542
+ ts = curation.typed ?? mapType(token);
6543
+ } else {
6544
+ ts = curation.untyped;
6545
+ }
5529
6546
  }
5530
6547
  push(ts);
5531
6548
  }
5532
6549
  // A genuine `unknown` member (an `any` slot, including `any | nil`) absorbs
5533
6550
  // every other union member, so collapse to exactly `unknown` rather than emit
5534
6551
  // a redundant `T | unknown` or `unknown | undefined`.
5535
- if (mapped.includes("unknown")) return "unknown";
5536
- return joinUnionArms(mapped);
6552
+ const joined = mapped.includes("unknown") ? "unknown" : joinUnionArms(mapped);
6553
+ return slotKind === "return" && SPARSE_ARRAY_RETURNS.has(elementName)
6554
+ ? sparseArrayMap(joined, elementName)
6555
+ : joined;
5537
6556
  }
5538
6557
 
5539
6558
  function tableSlotKey(elementName: string, slotKind: "param" | "return", slotName: string): string {
@@ -5690,7 +6709,7 @@ function joinUnionArms(mapped: readonly string[]): string {
5690
6709
  }
5691
6710
 
5692
6711
  const BUILTIN_MESSAGE_NAMES: ReadonlySet<string> = new Set(
5693
- messagesDoc.messages.map((message) => message.name),
6712
+ applyMessageOverlays(parseMessagesDoc(messagesDoc)).entries.map((entry) => entry.name),
5694
6713
  );
5695
6714
  const MESSAGE_REFERENCE = /^message\.[a-z_]+\.([a-z_]+)$/;
5696
6715
 
@@ -5706,6 +6725,18 @@ function resolveDefoldLeaf(leaf: string): string | undefined {
5706
6725
  return undefined;
5707
6726
  }
5708
6727
 
6728
+ // The union typedefs whose message arms each carry the engine's message-name
6729
+ // hash in `type`: script_physics.cpp sets it on every `physics.set_event_listener`
6730
+ // event. The same message referenced anywhere else (`physics.raycast`) has none.
6731
+ const DISCRIMINATED_MESSAGE_UNIONS: ReadonlySet<string> = new Set(["physics.event"]);
6732
+
6733
+ function discriminatedMessageArm(token: string): string | undefined {
6734
+ const name = MESSAGE_REFERENCE.exec(token)?.[1];
6735
+ const payload = resolveDefoldLeaf(token);
6736
+ if (name === undefined || payload === undefined) return undefined;
6737
+ return `${payload} & { type: Hash<"${name}"> }`;
6738
+ }
6739
+
5709
6740
  // Whole-token lookup and callback recovery run first, so the grammar sees only
5710
6741
  // what they miss; every pre-1.13.2 token resolves before it is reached.
5711
6742
  function wholeTokenMapping(token: string): string | undefined {