@defold-typescript/types 0.50.0 → 0.51.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 (88) hide show
  1. package/api-signatures.json +286 -232
  2. package/examples/translations.json +15 -15
  3. package/fixtures/messages_doc.json +20 -39
  4. package/generated/builtin-messages.d.ts +16 -14
  5. package/generated/collectionfactory.d.ts +1 -1
  6. package/generated/collectionproxy.d.ts +2 -2
  7. package/generated/factory.d.ts +4 -2
  8. package/generated/graphics.d.ts +4 -0
  9. package/generated/gui.d.ts +2 -2
  10. package/generated/http.d.ts +3 -1
  11. package/generated/image.d.ts +1 -1
  12. package/generated/kinds/gui-script.d.ts +1 -0
  13. package/generated/kinds/render-script.d.ts +1 -0
  14. package/generated/kinds/script.d.ts +1 -0
  15. package/generated/model.d.ts +4 -5
  16. package/generated/physics.d.ts +3 -3
  17. package/generated/render.d.ts +41 -13
  18. package/generated/sys.d.ts +8 -4
  19. package/generated/tilemap.d.ts +4 -4
  20. package/generated/versions/defold-1.12.4/collectionfactory.d.ts +1 -1
  21. package/generated/versions/defold-1.12.4/collectionproxy.d.ts +1 -1
  22. package/generated/versions/defold-1.12.4/factory.d.ts +4 -2
  23. package/generated/versions/defold-1.12.4/gui.d.ts +2 -2
  24. package/generated/versions/defold-1.12.4/http.d.ts +3 -1
  25. package/generated/versions/defold-1.12.4/image.d.ts +1 -1
  26. package/generated/versions/defold-1.12.4/index.d.ts +1 -0
  27. package/generated/versions/defold-1.12.4/model.d.ts +4 -5
  28. package/generated/versions/defold-1.12.4/physics.d.ts +2 -2
  29. package/generated/versions/defold-1.12.4/render.d.ts +41 -13
  30. package/generated/versions/defold-1.12.4/sys.d.ts +1 -1
  31. package/generated/versions/defold-1.12.4/tilemap.d.ts +4 -4
  32. package/generated/versions/defold-1.13.0/collectionfactory.d.ts +1 -1
  33. package/generated/versions/defold-1.13.0/collectionproxy.d.ts +1 -1
  34. package/generated/versions/defold-1.13.0/factory.d.ts +4 -2
  35. package/generated/versions/defold-1.13.0/gui.d.ts +2 -2
  36. package/generated/versions/defold-1.13.0/http.d.ts +3 -1
  37. package/generated/versions/defold-1.13.0/image.d.ts +1 -1
  38. package/generated/versions/defold-1.13.0/index.d.ts +1 -0
  39. package/generated/versions/defold-1.13.0/model.d.ts +4 -5
  40. package/generated/versions/defold-1.13.0/physics.d.ts +2 -2
  41. package/generated/versions/defold-1.13.0/render.d.ts +41 -13
  42. package/generated/versions/defold-1.13.0/sys.d.ts +1 -1
  43. package/generated/versions/defold-1.13.0/tilemap.d.ts +4 -4
  44. package/generated/versions/defold-1.13.1/collectionfactory.d.ts +1 -1
  45. package/generated/versions/defold-1.13.1/collectionproxy.d.ts +1 -1
  46. package/generated/versions/defold-1.13.1/factory.d.ts +4 -2
  47. package/generated/versions/defold-1.13.1/gui.d.ts +2 -2
  48. package/generated/versions/defold-1.13.1/http.d.ts +3 -1
  49. package/generated/versions/defold-1.13.1/image.d.ts +1 -1
  50. package/generated/versions/defold-1.13.1/index.d.ts +1 -0
  51. package/generated/versions/defold-1.13.1/model.d.ts +4 -5
  52. package/generated/versions/defold-1.13.1/physics.d.ts +2 -2
  53. package/generated/versions/defold-1.13.1/render.d.ts +41 -13
  54. package/generated/versions/defold-1.13.1/sys.d.ts +1 -1
  55. package/generated/versions/defold-1.13.1/tilemap.d.ts +4 -4
  56. package/generated/versions/defold-1.13.2/collectionfactory.d.ts +1 -1
  57. package/generated/versions/defold-1.13.2/collectionproxy.d.ts +2 -2
  58. package/generated/versions/defold-1.13.2/factory.d.ts +4 -2
  59. package/generated/versions/defold-1.13.2/gui.d.ts +2 -2
  60. package/generated/versions/defold-1.13.2/http.d.ts +3 -1
  61. package/generated/versions/defold-1.13.2/image.d.ts +1 -1
  62. package/generated/versions/defold-1.13.2/index.d.ts +1 -0
  63. package/generated/versions/defold-1.13.2/model.d.ts +4 -5
  64. package/generated/versions/defold-1.13.2/physics.d.ts +3 -3
  65. package/generated/versions/defold-1.13.2/render.d.ts +41 -13
  66. package/generated/versions/defold-1.13.2/sys.d.ts +8 -4
  67. package/generated/versions/defold-1.13.2/tilemap.d.ts +4 -4
  68. package/generated/versions/defold-1.13.2/window.d.ts +1 -1
  69. package/generated/window.d.ts +1 -1
  70. package/identity-vocabulary.json +46 -0
  71. package/index.d.ts +1 -0
  72. package/package.json +2 -1
  73. package/scripts/generate-api-availability.ts +5 -1
  74. package/scripts/generate-api-signatures.ts +16 -11
  75. package/scripts/materialize-version.ts +7 -1
  76. package/scripts/regen.ts +42 -25
  77. package/src/component-properties.d.ts +137 -6
  78. package/src/core-types.ts +46 -11
  79. package/src/emit-dts.ts +555 -84
  80. package/src/emit-messages.ts +289 -0
  81. package/src/engine-globals.d.ts +3 -1
  82. package/src/go-overloads.d.ts +13 -3
  83. package/src/lifecycle.ts +16 -16
  84. package/src/luals-type-expr.ts +6 -1
  85. package/src/physics-event-guard.d.ts +39 -0
  86. package/src/render-overloads.d.ts +2 -2
  87. package/src/return-correction-provenance.ts +36 -6
  88. package/src/window-event-guard.d.ts +5 -3
package/src/emit-dts.ts CHANGED
@@ -21,6 +21,7 @@ import {
21
21
  splitExampleSources,
22
22
  summaryFor,
23
23
  } from "./doc-comment";
24
+ import { applyMessageOverlays, parseMessagesDoc } from "./emit-messages";
24
25
  import type { TranslationStore } from "./example-store";
25
26
  import { lookupExampleSegments, translateProseFences } from "./example-store";
26
27
  import {
@@ -30,7 +31,12 @@ import {
30
31
  indexBaseNoteResolver,
31
32
  OVERRIDE_RETURN_SLOT,
32
33
  } from "./index-slot-classifications";
33
- import { type LualsPosition, mapLualsExpression, needsArrayParens } from "./luals-type-expr";
34
+ import {
35
+ isLualsFunctionToken,
36
+ type LualsPosition,
37
+ mapLualsExpression,
38
+ needsArrayParens,
39
+ } from "./luals-type-expr";
34
40
  import { classifyUrlParameter, type UrlParameterTable } from "./url-parameters";
35
41
 
36
42
  export { summaryFor };
@@ -75,6 +81,14 @@ export interface EmitOptions {
75
81
  // `UNDOCUMENTED_CONSTANTS` back-fill. Absent for a surface with no vendored
76
82
  // bindings (a ref-doc target), where every entry the ref-doc omits is emitted.
77
83
  registeredConstants?: ReadonlySet<string>;
84
+ // The `RETURN_TYPE_CORRECTIONS` and `STRUCT_MEMBER_ADDITIONS` keys whose binding
85
+ // pin holds in the target's vendored engine source. A binding-pinned entry
86
+ // applies only when its key is here, so a surface with no vendored bindings
87
+ // (absent) applies none of them.
88
+ bindingEvidence?: ReadonlySet<string>;
89
+ // The functions a target's skip rules removed from the module, read only to
90
+ // type an `OVERLOAD_SLOT_ALIASES` slot.
91
+ withheldFunctions?: readonly ApiFunction[];
78
92
  }
79
93
 
80
94
  export const TS_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
@@ -362,8 +376,22 @@ export const RETURN_TYPE_OVERRIDES: ReadonlyMap<string, string> = new Map([
362
376
  // destructured into separate arguments; a boxed `LuaMultiReturn` is rejected
363
377
  // because TSTL passes it as one table, which is always truthy. `reason` quotes
364
378
  // the slot prose the provenance test searches for, as for the `nil` entries.
365
- export interface ReturnTypeCorrection {
366
- readonly ts: string;
379
+ //
380
+ // An entry with a `binding` rests on the engine source instead of the prose:
381
+ // it applies only on a target whose vendored binding shows the pin (the
382
+ // `EmitOptions.bindingEvidence` keys), and `reason` names that binding in words.
383
+ // A nil push proves nil-ability and nothing about the base type, so such an
384
+ // entry usually carries `nullable` rather than `ts`.
385
+ export interface BindingPin {
386
+ // Relative to the target's vendored `engine/` tree.
387
+ readonly file: string;
388
+ readonly cFunction: string;
389
+ // Token snippets the function body must contain and must lack; at least one.
390
+ readonly contains?: string;
391
+ readonly lacks?: string;
392
+ }
393
+
394
+ interface ReturnCorrectionBase {
367
395
  readonly upstream: readonly string[];
368
396
  // A later release's spelling of the same defect; see `correction-pins.ts`.
369
397
  readonly retypedUpstream?: readonly (readonly string[])[];
@@ -371,10 +399,33 @@ export interface ReturnTypeCorrection {
371
399
  // The named value of a multi-return the correction rewrites; `ts` is then
372
400
  // that value's type. Absent, the correction rewrites a single return.
373
401
  readonly slot?: string;
402
+ readonly binding?: BindingPin;
403
+ }
404
+
405
+ // `ts` replaces the emitted type. `nullable` keeps the type each target maps
406
+ // from its own tokens and only adds or drops the `nil`, so a target whose
407
+ // spelling differs keeps its own base.
408
+ export type ReturnTypeCorrection = ReturnCorrectionBase &
409
+ (
410
+ | { readonly ts: string; readonly nullable?: undefined }
411
+ | { readonly nullable: boolean; readonly ts?: undefined }
412
+ );
413
+
414
+ // The early nil return both tilemap readers take for an unknown layer, and
415
+ // `TileMap_Get` for a cell out of range too.
416
+ function tilemapEarlyNil(cFunction: string): BindingPin {
417
+ return {
418
+ file: "gamesys/src/gamesys/scripts/script_tilemap.cpp",
419
+ cFunction,
420
+ contains: "lua_pushnil(L); assert(top + 1 == lua_gettop(L)); return 1;",
421
+ };
374
422
  }
375
423
 
376
424
  // Keyed by FQN, mirroring PROPERTY_TYPE_CORRECTIONS.
377
- export const RETURN_TYPE_CORRECTIONS: ReadonlyMap<string, ReturnTypeCorrection> = new Map([
425
+ export const RETURN_TYPE_CORRECTIONS: ReadonlyMap<string, ReturnTypeCorrection> = new Map<
426
+ string,
427
+ ReturnTypeCorrection
428
+ >([
378
429
  [
379
430
  "b2d.get_body",
380
431
  {
@@ -439,6 +490,147 @@ export const RETURN_TYPE_CORRECTIONS: ReadonlyMap<string, ReturnTypeCorrection>
439
490
  reason: "the customized try function.",
440
491
  },
441
492
  ],
493
+ [
494
+ "factory.create",
495
+ {
496
+ nullable: true,
497
+ upstream: ["hash"],
498
+ reason: "script_factory.cpp:FactoryComp_Create pushes nil when the spawn fails",
499
+ binding: {
500
+ file: "gamesys/src/gamesys/scripts/script_factory.cpp",
501
+ cFunction: "FactoryComp_Create",
502
+ contains: "dmScript::PushHash(L, id); } else { lua_pushnil(L); }",
503
+ },
504
+ },
505
+ ],
506
+ // 1.13.2 retypes each tilemap return to a precise spelling of the same value
507
+ // and still omits the nil.
508
+ ...(
509
+ [
510
+ ["tilemap.get_tile", ["number"], ["integer"]],
511
+ [
512
+ "tilemap.get_tile_info",
513
+ ["table"],
514
+ ["{ index:integer, h_flip:boolean, v_flip:boolean, rotate_90:boolean }"],
515
+ ],
516
+ ] as const
517
+ ).map(
518
+ ([key, upstream, retyped]) =>
519
+ [
520
+ key,
521
+ {
522
+ nullable: true,
523
+ upstream,
524
+ retypedUpstream: [retyped],
525
+ reason:
526
+ "script_tilemap.cpp:TileMap_Get pushes nil for an unknown layer or a cell out of range",
527
+ binding: tilemapEarlyNil("TileMap_Get"),
528
+ },
529
+ ] as const,
530
+ ),
531
+ [
532
+ "tilemap.get_tiles",
533
+ {
534
+ nullable: true,
535
+ upstream: ["table"],
536
+ retypedUpstream: [["table<integer, table<integer, integer>>"]],
537
+ reason: "script_tilemap.cpp:TileMap_GetTiles pushes nil for an unknown layer",
538
+ binding: tilemapEarlyNil("TileMap_GetTiles"),
539
+ },
540
+ ],
541
+ [
542
+ "collectionproxy.set_collection",
543
+ {
544
+ nullable: true,
545
+ upstream: ["number"],
546
+ retypedUpstream: [["collectionproxy.RESULT"]],
547
+ reason:
548
+ "script_collectionproxy.cpp:CollectionProxy_SetCollection pushes true, nil when the change succeeds",
549
+ slot: "code",
550
+ binding: {
551
+ file: "gamesys/src/gamesys/scripts/script_collectionproxy.cpp",
552
+ cFunction: "CollectionProxy_SetCollection",
553
+ contains: "case SET_COLLECTION_PATH_RESULT_OK: lua_pushboolean(L, 1); lua_pushnil(L);",
554
+ },
555
+ },
556
+ ],
557
+ [
558
+ "image.get_astc_header",
559
+ {
560
+ nullable: false,
561
+ upstream: ["table", "nil"],
562
+ retypedUpstream: [["image.astc_header", "nil"]],
563
+ reason: "script_image.cpp:Image_GetAstcHeader raises on invalid data and never pushes nil",
564
+ binding: {
565
+ file: "gamesys/src/gamesys/scripts/script_image.cpp",
566
+ cFunction: "Image_GetAstcHeader",
567
+ contains: 'return luaL_error(L, "Data is not a valid .astc file");',
568
+ lacks: "lua_pushnil(L);",
569
+ },
570
+ },
571
+ ],
572
+ // `PushRayCastResponse` has two definitions; `Physics_RayCast` calls the one
573
+ // that takes the world, which sets every payload field but `request_id`.
574
+ [
575
+ "physics.raycast",
576
+ {
577
+ ts: 'Omit<BuiltinMessages["ray_cast_response"], "request_id">[] | Omit<BuiltinMessages["ray_cast_response"], "request_id"> | undefined',
578
+ upstream: ["table", "nil"],
579
+ retypedUpstream: [
580
+ ["message.physics.ray_cast_response[]", "message.physics.ray_cast_response", "nil"],
581
+ ],
582
+ reason:
583
+ "script_physics.cpp:Physics_RayCast pushes each hit through PushRayCastResponse(L, world, hit), which sets no request_id",
584
+ binding: {
585
+ file: "gamesys/src/gamesys/scripts/script_physics.cpp",
586
+ cFunction: "Physics_RayCast",
587
+ contains: "PushRayCastResponse(L, world, hits[i]);",
588
+ lacks: '"request_id"',
589
+ },
590
+ },
591
+ ],
592
+ ...(
593
+ [
594
+ [
595
+ "collectionfactory.create",
596
+ "Hash",
597
+ ["table<hash, hash>"],
598
+ "gamesys/src/gamesys/scripts/script_collection_factory.cpp",
599
+ "CollectionFactoryComp_Create",
600
+ ],
601
+ [
602
+ "gui.get_tree",
603
+ 'Opaque<"node">',
604
+ ["table<hash, node>"],
605
+ "gui/src/gui_script.cpp",
606
+ "LuaGetTree",
607
+ ],
608
+ [
609
+ "gui.clone_tree",
610
+ 'Opaque<"node">',
611
+ ["table<hash, node>"],
612
+ "gui/src/gui_script.cpp",
613
+ "LuaCloneTree",
614
+ ],
615
+ ] as const
616
+ ).map(
617
+ ([key, value, retyped, file, cFunction]) =>
618
+ [
619
+ key,
620
+ {
621
+ ts: `LuaTable<Hash, ${value}> & { get: LuaTableGetMethod<string, ${value}> }`,
622
+ upstream: ["table"],
623
+ retypedUpstream: [retyped],
624
+ reason: `${file.slice(file.lastIndexOf("/") + 1)}:${cFunction} sets HashTableIndex as the id table's __index, which hashes a string key`,
625
+ binding: {
626
+ file,
627
+ cFunction,
628
+ contains:
629
+ 'lua_pushcfunction(L, HashTableIndex); lua_setfield(L, -2, "__index"); lua_setmetatable(L, -2);',
630
+ },
631
+ },
632
+ ] as const,
633
+ ),
442
634
  ]);
443
635
 
444
636
  // A parameter whose declared `types` disagree with what the same element's
@@ -1553,6 +1745,13 @@ function unionAliasDeclaration(
1553
1745
  return `type ${name} = ${members.map(arm).join(" | ")};`;
1554
1746
  }
1555
1747
 
1748
+ // A slot of a function the authored-overload lane declares, published under
1749
+ // one alias on every target so the version-independent overload file can
1750
+ // name the type each target's own reference gives that slot.
1751
+ export const OVERLOAD_SLOT_ALIASES: ReadonlyMap<string, string> = new Map([
1752
+ ["render.render_target:param:parameters", "RenderTargetParams"],
1753
+ ]);
1754
+
1556
1755
  const DOC_CONSTANT_TOKEN =
1557
1756
  /(?<![A-Za-z0-9_.])([a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)*\.[A-Z][A-Z0-9_]*)(\*)?/g;
1558
1757
 
@@ -2267,10 +2466,13 @@ export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new
2267
2466
  // buildTableDocResolver resolves only FUNCTION elements so the cross-ref
2268
2467
  // cannot fire (the message payload lives in messages_doc.json, not the
2269
2468
  // FUNCTION slot pool). The shape is curated directly from the message
2270
- // catalog (the six ray_cast_response payload fields). The doc declares a
2469
+ // catalog: the ray_cast_response payload fields but `request_id`, which
2470
+ // `Physics_RayCast` never sets. The doc declares a
2271
2471
  // `nil` branch ("If missed it returns <code>nil</code>"), so the existing
2272
2472
  // nil-aware emit already projects the curated array onto the
2273
2473
  // `array-object | nil` return type — no curation change needed there.
2474
+ // Where its binding pin holds, the `physics.raycast` RETURN_TYPE_CORRECTIONS
2475
+ // entry replaces the whole return.
2274
2476
  [
2275
2477
  "physics.raycast:return:result",
2276
2478
  {
@@ -2281,7 +2483,6 @@ export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new
2281
2483
  { name: "normal", types: ["vector3"] },
2282
2484
  { name: "id", types: ["hash"] },
2283
2485
  { name: "group", types: ["hash"] },
2284
- { name: "request_id", types: ["number"] },
2285
2486
  ],
2286
2487
  },
2287
2488
  ],
@@ -2704,41 +2905,103 @@ export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new
2704
2905
  ]);
2705
2906
 
2706
2907
  // 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.
2908
+ // for a callback parameter's signature. Each entry carries two forms, chosen by
2909
+ // the token the reference gives the slot. `untyped` answers a bare
2910
+ // `function(a, b, c)` token, where the generic `recoverCallbackSignature`
2911
+ // `unknown`-everywhere form leaves real engine type information on the table.
2912
+ // `typed` answers a `fun(a:T, ...)` token, where the reference names a type for
2913
+ // each parameter; `null` there emits the reference's own signature. Each form
2914
+ // is the full emitted function type. Honest only.
2715
2915
  //
2716
- // The other entries type what the slot's own prose lists and nothing more.
2916
+ // `window.set_listener` discriminates `event` by the union of the
2917
+ // `WINDOW_EVENT_*` constants the surface's own `window` namespace declares, on
2918
+ // both forms: the union is derived from the surface, so each version accepts
2919
+ // exactly the constants it declares. Its untyped `data` is a bare record (only
2920
+ // resize carries fields) and its typed `data` is the reference's
2921
+ // `window.event_data`; `isWindowEvent` narrows either to the event's payload,
2922
+ // mirroring the `msg.post` (send, typed) / `isMessage` (receive, narrow) split.
2923
+ //
2924
+ // The other untyped forms type what the slot's own prose lists and nothing more.
2717
2925
  // `http.request`'s response fields are the `<ul>` under "The response data.
2718
2926
  // Contains the fields:"; each is conditional ("if not saved on disc", "only if
2719
2927
  // 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
2928
+ // string map the request sends. The prose omits two fields the engine sets:
2929
+ // `url`, which every final response carries (script_http_util.h), and
2930
+ // `bytes_sent`, which a report_progress event carries beside `bytes_received`,
2931
+ // `bytes_total` and `url` (http_ddf.proto `HttpRequestProgress`). That event has
2932
+ // no `status`; typing it as its own shape needs a per-target authored overload,
2933
+ // so `status` stays required and the slot's `note` says how to tell the two
2934
+ // apart. Its typed form is the same signature: the typed reference names
2935
+ // `http.response`, which the runtime surface withholds.
2936
+ // `model.play_anim`'s completion callback receives self, the message id and the
2937
+ // message only: script_model.cpp calls it with `PCall(L, 3, 0)`, so both forms
2938
+ // drop the `sender` every reference lists and type the message as the typed
2939
+ // reference's `model_animation_done`.
2940
+ // `sprite.play_flipbook`'s message is the "Information about the completion"
2941
+ // list. Its typed form is the same signature: the typed reference names the
2942
+ // built-in `animation_done` message, which has this shape, and the inline form
2943
+ // keeps the fields visible in the signature. `collectionproxy.load` documents
2722
2944
  // 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([
2945
+ // `proxy_error` messages and lists no fields on the untyped reference, so its
2946
+ // untyped message is the bare record `window.set_listener`'s untyped `data` is;
2947
+ // the typed reference names `collectionproxy.load_data`, so its typed form
2948
+ // defers to the reference.
2949
+ export interface CallbackSignatureCuration {
2950
+ /** Emitted for a bare `function(...)` token. */
2951
+ readonly untyped: string;
2952
+ /** Emitted for a typed `fun(...)` token; `null` emits the reference's own signature. */
2953
+ readonly typed: string | null;
2954
+ /** Appended to the parameter's doc, for what the signature cannot express. */
2955
+ readonly note?: string;
2956
+ }
2957
+
2958
+ const onBothForms = (signature: string, note?: string): CallbackSignatureCuration => ({
2959
+ untyped: signature,
2960
+ typed: signature,
2961
+ ...(note === undefined ? {} : { note }),
2962
+ });
2963
+
2964
+ export const CALLBACK_SIGNATURE_CURATIONS: ReadonlyMap<string, CallbackSignatureCuration> = new Map<
2965
+ string,
2966
+ CallbackSignatureCuration
2967
+ >([
2726
2968
  [
2727
2969
  "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",
2970
+ {
2971
+ untyped:
2972
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: emitted verbatim as a TS template literal type
2973
+ "(self: unknown, event: (typeof window)[Extract<keyof typeof window, `WINDOW_EVENT_${string}`>], data: Record<string | number, unknown>) => void",
2974
+ typed:
2975
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: emitted verbatim as a TS template literal type
2976
+ "(self: unknown, event: (typeof window)[Extract<keyof typeof window, `WINDOW_EVENT_${string}`>], data: window.event_data) => void",
2977
+ },
2730
2978
  ],
2731
2979
  [
2732
2980
  "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",
2981
+ onBothForms(
2982
+ "(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 }) => void",
2983
+ "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.",
2984
+ ),
2985
+ ],
2986
+ [
2987
+ "model.play_anim:param:complete_function",
2988
+ onBothForms(
2989
+ '(self: unknown, message_id: Hash, message: BuiltinMessages["model_animation_done"]) => void',
2990
+ ),
2734
2991
  ],
2735
2992
  [
2736
2993
  "sprite.play_flipbook:param:complete_function",
2737
- "(self: unknown, message_id: Hash, message: { current_tile: number; id: Hash }, sender: Url) => void",
2994
+ onBothForms(
2995
+ "(self: unknown, message_id: Hash, message: { current_tile: number; id: Hash }, sender: Url) => void",
2996
+ ),
2738
2997
  ],
2739
2998
  [
2740
2999
  "collectionproxy.load:param:callback",
2741
- "(self: unknown, message_id: Hash, message: Record<string | number, unknown>, sender: Url) => void",
3000
+ {
3001
+ untyped:
3002
+ "(self: unknown, message_id: Hash, message: Record<string | number, unknown>, sender: Url) => void",
3003
+ typed: null,
3004
+ },
2742
3005
  ],
2743
3006
  ]);
2744
3007
 
@@ -2899,32 +3162,13 @@ export interface ReturnFieldOptionalityCorrection {
2899
3162
  readonly evidence: string;
2900
3163
  }
2901
3164
 
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.
3165
+ // A platform gate alone is not evidence: `sys.get_sys_info` gates four fields
3166
+ // on a platform, yet `Sys_GetSysInfo` pushes each from a zero-filled char array,
3167
+ // so off-platform they are empty strings, never nil.
2905
3168
  export const RETURN_FIELD_OPTIONALITY_CORRECTIONS: ReadonlyMap<
2906
3169
  string,
2907
3170
  ReturnFieldOptionalityCorrection
2908
3171
  > = 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
3172
  [
2929
3173
  "sys.get_ifaddrs:return:ifaddrs:address",
2930
3174
  { upstream: ["string"], evidence: "might be nil if not available." },
@@ -2975,8 +3219,10 @@ export function applyFieldOptionalityCorrections(
2975
3219
  // `<element>:param:<slot>:<field>`, the TABLE_FIELD_TYPE_OVERRIDES shape, with
2976
3220
  // `<field>.<member>` or `<field>[].<member>` one level down. A `STRUCT` member
2977
3221
  // 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.
3222
+ // (`field-correction-coverage.ts` pairs the two), or for a returned struct whose
3223
+ // binding always sets a member upstream marks optional. The value is the
3224
+ // evidence: the C++ binding, `<file>:<cFunction>`, and how it reads or sets the
3225
+ // field.
2980
3226
  const CHECK_SHAPE_DEF = "CheckShapeDef reads the shape table with luaL_checkinteger(type)";
2981
3227
  const V2_FILTER_DATA =
2982
3228
  "v2/script_box2d_fixture_v2.cpp:CheckFilterData reads each with luaL_checkinteger";
@@ -3000,6 +3246,49 @@ export const OPTIONAL_MEMBER_CORRECTIONS: ReadonlyMap<string, string> = new Map(
3000
3246
  ],
3001
3247
  ]);
3002
3248
 
3249
+ // A `STRUCT` member the engine binding sets while the target's reference omits
3250
+ // it, keyed `<struct>:member:<field>`. It applies only where its binding pin
3251
+ // holds (the `EmitOptions.bindingEvidence` keys), and a struct that already
3252
+ // declares the member throws, so no entry outlives upstream's fix.
3253
+ export interface StructMemberAddition {
3254
+ readonly member: Omit<ApiStructMember, "name">;
3255
+ readonly binding: BindingPin;
3256
+ }
3257
+
3258
+ export const STRUCT_MEMBER_ADDITIONS: ReadonlyMap<string, StructMemberAddition> = new Map([
3259
+ [
3260
+ memberCorrectionKey("graphics.adapter_limits", "max_storage_buffers_per_stage"),
3261
+ {
3262
+ member: {
3263
+ type: "integer",
3264
+ isOptional: false,
3265
+ doc: "Maximum number of storage buffers per shader stage.",
3266
+ },
3267
+ binding: {
3268
+ file: "script/src/script_graphics.cpp",
3269
+ cFunction: "Graphics_GetAdapterInfo",
3270
+ contains: 'lua_setfield(L, -2, "max_storage_buffers_per_stage");',
3271
+ },
3272
+ },
3273
+ ],
3274
+ [
3275
+ memberCorrectionKey("sys.interface_info", "family"),
3276
+ {
3277
+ member: {
3278
+ type: '"ipv4"|"ipv6"',
3279
+ isOptional: true,
3280
+ doc: "Address family, absent for an interface that is neither IPv4 nor IPv6.",
3281
+ },
3282
+ binding: {
3283
+ file: "script/src/script_sys.cpp",
3284
+ cFunction: "Sys_GetIfaddrs",
3285
+ contains:
3286
+ 'lua_pushliteral(L, "ipv6"); } else { lua_pushnil(L); } lua_setfield(L, -2, "family");',
3287
+ },
3288
+ },
3289
+ ],
3290
+ ]);
3291
+
3003
3292
  export const REQUIRED_FIELD_CORRECTIONS: ReadonlyMap<string, string> = new Map<string, string>([
3004
3293
  ...(["mass", "center", "inertia"] as const).map(
3005
3294
  (field) =>
@@ -3093,6 +3382,13 @@ export const REQUIRED_FIELD_CORRECTIONS: ReadonlyMap<string, string> = new Map<s
3093
3382
  "resource.sound_data_options:member:data",
3094
3383
  "script_resource.cpp:CreateSoundData reads the data with CheckBufferOrString",
3095
3384
  ],
3385
+ ...(["device_model", "manufacturer", "device_ident", "user_agent"] as const).map(
3386
+ (field) =>
3387
+ [
3388
+ `sys.sys_info:member:${field}`,
3389
+ 'script_sys.cpp:Sys_GetSysInfo pushes it with lua_pushstring from a zero-filled char array (user_agent falls back to "")',
3390
+ ] as const,
3391
+ ),
3096
3392
  ]);
3097
3393
 
3098
3394
  // Mark the fields REQUIRED_FIELD_CORRECTIONS names for this slot required,
@@ -3611,7 +3907,10 @@ function typedefAlias(
3611
3907
  const brand = `Opaque<"${typedef.name}">`;
3612
3908
  const aliasOf = typedef.aliasOf ?? [];
3613
3909
  if (aliasOf.length > 0 && !isHandleAlias(aliasOf)) {
3614
- return `type ${local} = ${unionFromTokens(aliasOf, mapType)};`;
3910
+ const armType: DefoldTypeMapper = DISCRIMINATED_MESSAGE_UNIONS.has(typedef.name)
3911
+ ? (token, position) => discriminatedMessageArm(token) ?? mapType(token, position)
3912
+ : mapType;
3913
+ return `type ${local} = ${unionFromTokens(aliasOf, armType)};`;
3615
3914
  }
3616
3915
  const mapped = rawMapType(typedef.name);
3617
3916
  if (mapped.startsWith(brand)) return `type ${local} = ${mapped};`;
@@ -3642,6 +3941,7 @@ function collectTypeDeclarations(
3642
3941
  skip: ReadonlySet<string>,
3643
3942
  rawMapType: (token: string) => string,
3644
3943
  mapType: DefoldTypeMapper,
3944
+ bindingEvidence: ReadonlySet<string>,
3645
3945
  ): TypeDeclaration[] {
3646
3946
  const prefix = `${module.namespace}.`;
3647
3947
  const out: TypeDeclaration[] = [];
@@ -3665,7 +3965,7 @@ function collectTypeDeclarations(
3665
3965
  for (const struct of module.structs ?? []) {
3666
3966
  const local = localOf(struct.name);
3667
3967
  if (local === null || skip.has(local)) continue;
3668
- out.push(structDeclaration(struct, local, mapType, module.structs ?? []));
3968
+ out.push(structDeclaration(struct, local, mapType, module.structs ?? [], bindingEvidence));
3669
3969
  }
3670
3970
  return out;
3671
3971
  }
@@ -3718,10 +4018,22 @@ function structDeclaration(
3718
4018
  path: string,
3719
4019
  mapType: DefoldTypeMapper,
3720
4020
  structs: readonly ApiStruct[],
4021
+ bindingEvidence: ReadonlySet<string>,
3721
4022
  ): TypeDeclaration {
3722
4023
  const name = path.slice(path.lastIndexOf(".") + 1);
3723
4024
  const memberPrefix = memberCorrectionKey(struct.name, "");
3724
4025
  const declared = new Set(struct.members.map((member) => member.name));
4026
+ const redundant = [...STRUCT_MEMBER_ADDITIONS.keys()].filter(
4027
+ (key) => key.startsWith(memberPrefix) && declared.has(key.slice(memberPrefix.length)),
4028
+ );
4029
+ if (redundant.length > 0) {
4030
+ throw new Error(
4031
+ `struct member addition names a declared member: ${redundant.sort().join(", ")}`,
4032
+ );
4033
+ }
4034
+ const added = [...STRUCT_MEMBER_ADDITIONS]
4035
+ .filter(([key]) => key.startsWith(memberPrefix) && bindingEvidence.has(key))
4036
+ .map(([key, addition]) => ({ ...addition.member, name: key.slice(memberPrefix.length) }));
3725
4037
  const stale = [
3726
4038
  ...REQUIRED_FIELD_CORRECTIONS.keys(),
3727
4039
  ...LIST_FIELD_CORRECTIONS.keys(),
@@ -3731,7 +4043,7 @@ function structDeclaration(
3731
4043
  if (stale.length > 0) {
3732
4044
  throw new Error(`member-keyed field correction names no member: ${stale.sort().join(", ")}`);
3733
4045
  }
3734
- const members = struct.members.map((member) => {
4046
+ const members = [...struct.members, ...added].map((member) => {
3735
4047
  const key = memberCorrectionKey(struct.name, member.name);
3736
4048
  const list = LIST_FIELD_CORRECTIONS.get(key);
3737
4049
  const retyped = TABLE_FIELD_TYPE_OVERRIDES.get(key);
@@ -3935,6 +4247,7 @@ export function emitDeclarations(documented: ApiModule, options?: EmitOptions):
3935
4247
  const knownConstantFqns = options?.knownConstantFqns;
3936
4248
  const translations = options?.translations ?? {};
3937
4249
  const urlParameters = options?.urlParameters ?? [];
4250
+ const bindingEvidence = options?.bindingEvidence ?? new Set<string>();
3938
4251
  const baseNotes = indexBaseNoteResolver(options?.indexBaseSource ?? "engine");
3939
4252
  const { rawMapType, mapType } = moduleTypeMappers(module, constantsByFqn, options);
3940
4253
  const constantTokens = constantSlotTokenResolver(
@@ -3991,7 +4304,13 @@ export function emitDeclarations(documented: ApiModule, options?: EmitOptions):
3991
4304
  // A typedef that also has colon methods is emitted as a method-bearing
3992
4305
  // interface below, not an opaque brand alias.
3993
4306
  const typeDeclarations = placeTypeDeclarations(
3994
- collectTypeDeclarations(module, new Set(handleGroups.keys()), rawMapType, mapType),
4307
+ collectTypeDeclarations(
4308
+ module,
4309
+ new Set(handleGroups.keys()),
4310
+ rawMapType,
4311
+ mapType,
4312
+ bindingEvidence,
4313
+ ),
3995
4314
  nestedRoot,
3996
4315
  );
3997
4316
 
@@ -4013,6 +4332,16 @@ export function emitDeclarations(documented: ApiModule, options?: EmitOptions):
4013
4332
  for (const aliasDecl of constantUnionAliasDeclarations(module.namespace)) {
4014
4333
  lines.push(`${INDENT}${decl}${aliasDecl}`);
4015
4334
  }
4335
+ for (const aliasDecl of overloadSlotAliasDeclarations(
4336
+ [...module.functions, ...(options?.withheldFunctions ?? [])],
4337
+ prefix,
4338
+ mapType,
4339
+ constantTokens,
4340
+ urlParameters,
4341
+ bindingEvidence,
4342
+ )) {
4343
+ lines.push(`${INDENT}${decl}${aliasDecl}`);
4344
+ }
4016
4345
  for (const alias of enumAliases.topLevel) {
4017
4346
  lines.push(
4018
4347
  `${INDENT}${decl}${unionAliasDeclaration(alias.name, alias.members, alias.nilable)}`,
@@ -4027,7 +4356,7 @@ export function emitDeclarations(documented: ApiModule, options?: EmitOptions):
4027
4356
  lines.push(docLine);
4028
4357
  }
4029
4358
  lines.push(
4030
- `${handleIndent}${emitMethod(fn, mapType, resolver, constantTokens, urlParameters)}`,
4359
+ `${handleIndent}${emitMethod(fn, mapType, resolver, constantTokens, urlParameters, bindingEvidence)}`,
4031
4360
  );
4032
4361
  }
4033
4362
  lines.push(`${INDENT}}`);
@@ -4058,7 +4387,15 @@ export function emitDeclarations(documented: ApiModule, options?: EmitOptions):
4058
4387
  const emitName = aliasName(fn.name, aliases);
4059
4388
  for (const docLine of functionDocLines(fn.original, translations, baseNotes))
4060
4389
  lines.push(docLine);
4061
- const line = emitFunction(fn, emitName, mapType, resolver, constantTokens, urlParameters);
4390
+ const line = emitFunction(
4391
+ fn,
4392
+ emitName,
4393
+ mapType,
4394
+ resolver,
4395
+ constantTokens,
4396
+ urlParameters,
4397
+ bindingEvidence,
4398
+ );
4062
4399
  lines.push(`${INDENT}${reserved ? "" : decl}${line}`);
4063
4400
  const alternative = firstSlotAlternative(fn, module.namespace);
4064
4401
  if (alternative !== null) {
@@ -4074,6 +4411,7 @@ export function emitDeclarations(documented: ApiModule, options?: EmitOptions):
4074
4411
  resolver,
4075
4412
  constantTokens,
4076
4413
  urlParameters,
4414
+ bindingEvidence,
4077
4415
  );
4078
4416
  lines.push(`${INDENT}${reserved ? "" : decl}${overload}`);
4079
4417
  }
@@ -4148,7 +4486,7 @@ export function emitDeclarations(documented: ApiModule, options?: EmitOptions):
4148
4486
  lines.push(docLine);
4149
4487
  }
4150
4488
  lines.push(
4151
- `${bodyIndent}${reserved ? "" : segmentDecl}${emitFunction(fn, emitName, mapType, resolver, constantTokens, urlParameters)}`,
4489
+ `${bodyIndent}${reserved ? "" : segmentDecl}${emitFunction(fn, emitName, mapType, resolver, constantTokens, urlParameters, bindingEvidence)}`,
4152
4490
  );
4153
4491
  }
4154
4492
  aliasReservedSegments(group.children.keys(), segmentAliases);
@@ -4179,6 +4517,55 @@ export function emitDeclarations(documented: ApiModule, options?: EmitOptions):
4179
4517
  return `${lines.join("\n")}\n`;
4180
4518
  }
4181
4519
 
4520
+ // One `type <alias> = <slot type>;` per `OVERLOAD_SLOT_ALIASES` slot these
4521
+ // functions carry, in alias order. The type is the one `emitFunction` reports
4522
+ // for the slot, so it reads as the function's own signature would. The
4523
+ // resolver spans these functions alone, leaving the module's own untouched.
4524
+ function overloadSlotAliasDeclarations(
4525
+ functions: readonly ApiFunction[],
4526
+ prefix: string,
4527
+ mapType: DefoldTypeMapper,
4528
+ constantTokens: ConstantSlotTokens,
4529
+ urlParameters: UrlParameterTable,
4530
+ bindingEvidence: ReadonlySet<string>,
4531
+ ): string[] {
4532
+ const resolver = buildTableDocResolver(
4533
+ functions.map((fn) => ({ name: fn.name, slots: [...fn.parameters, ...fn.returnValues] })),
4534
+ );
4535
+ const types = new Map<string, string>();
4536
+ for (const fn of functions) {
4537
+ for (const [position, p] of fn.parameters.entries()) {
4538
+ const alias = OVERLOAD_SLOT_ALIASES.get(tableSlotKey(fn.name, "param", p.name));
4539
+ if (alias === undefined) continue;
4540
+ const prepared = prepareFunction(fn, prefix);
4541
+ if (prepared === null) continue;
4542
+ const slots: Record<string, SlotType> = {};
4543
+ emitFunction(
4544
+ prepared,
4545
+ prepared.name,
4546
+ mapType,
4547
+ resolver,
4548
+ constantTokens,
4549
+ urlParameters,
4550
+ bindingEvidence,
4551
+ slots,
4552
+ );
4553
+ const slot = slots[slotKey("param", position, p.name)];
4554
+ if (slot === undefined) {
4555
+ throw new Error(`${fn.name} reports no type for its aliased slot "${p.name}"`);
4556
+ }
4557
+ const seen = types.get(alias);
4558
+ if (seen !== undefined && seen !== slot.type) {
4559
+ throw new Error(`${alias} is typed both ${seen} and ${slot.type}`);
4560
+ }
4561
+ types.set(alias, slot.type);
4562
+ }
4563
+ }
4564
+ return [...types]
4565
+ .sort(([a], [b]) => a.localeCompare(b))
4566
+ .map(([alias, type]) => `type ${alias} = ${type};`);
4567
+ }
4568
+
4182
4569
  /**
4183
4570
  * Every documented slot's rendered TS type and optionality, keyed
4184
4571
  * `param|return:<position>:<raw ref-doc name>`. The position is part of the key
@@ -4232,6 +4619,7 @@ export function emitSymbolSignatures(
4232
4619
  const constantsByFqn = new Map(module.constants.map((c) => [c.name, c]));
4233
4620
  const knownConstantFqns = options?.knownConstantFqns;
4234
4621
  const urlParameters = options?.urlParameters ?? [];
4622
+ const bindingEvidence = options?.bindingEvidence ?? new Set<string>();
4235
4623
  const { rawMapType, mapType } = moduleTypeMappers(module, constantsByFqn, options);
4236
4624
  const constantTokens = constantSlotTokenResolver(
4237
4625
  module,
@@ -4269,6 +4657,7 @@ export function emitSymbolSignatures(
4269
4657
  resolver,
4270
4658
  constantTokens,
4271
4659
  urlParameters,
4660
+ bindingEvidence,
4272
4661
  slotTypes,
4273
4662
  );
4274
4663
  const alternative = firstSlotAlternative(prepared, module.namespace);
@@ -4282,6 +4671,7 @@ export function emitSymbolSignatures(
4282
4671
  resolver,
4283
4672
  constantTokens,
4284
4673
  urlParameters,
4674
+ bindingEvidence,
4285
4675
  )}`;
4286
4676
  out.push({ identity: fnIdentity(fn), tsSignature, slotTypes });
4287
4677
  }
@@ -4323,6 +4713,7 @@ export function emitSymbolSignatures(
4323
4713
  resolver,
4324
4714
  constantTokens,
4325
4715
  urlParameters,
4716
+ bindingEvidence,
4326
4717
  slotTypes,
4327
4718
  ),
4328
4719
  slotTypes,
@@ -4341,6 +4732,7 @@ export function emitSymbolSignatures(
4341
4732
  resolver,
4342
4733
  constantTokens,
4343
4734
  urlParameters,
4735
+ bindingEvidence,
4344
4736
  slotTypes,
4345
4737
  ),
4346
4738
  slotTypes,
@@ -4389,6 +4781,7 @@ export function emitSymbolSignatures(
4389
4781
  new Set(handleGroups.keys()),
4390
4782
  rawMapType,
4391
4783
  mapType,
4784
+ bindingEvidence,
4392
4785
  )) {
4393
4786
  out.push({
4394
4787
  identity: {
@@ -4846,6 +5239,7 @@ function memberSignature(
4846
5239
  resolver: TableDocResolver,
4847
5240
  constantTokens: ConstantSlotTokens,
4848
5241
  urlParameters: UrlParameterTable,
5242
+ bindingEvidence: ReadonlySet<string>,
4849
5243
  slotSink?: Record<string, SlotType>,
4850
5244
  ): string {
4851
5245
  const original = prepared.original.parameters;
@@ -4874,6 +5268,7 @@ function memberSignature(
4874
5268
  resolver,
4875
5269
  constantTokens,
4876
5270
  elementName,
5271
+ bindingEvidence,
4877
5272
  );
4878
5273
  const predicateToken = TYPE_PREDICATES.get(elementName);
4879
5274
  const soleParam = original[0];
@@ -4911,9 +5306,10 @@ function emitFunction(
4911
5306
  resolver: TableDocResolver,
4912
5307
  constantTokens: ConstantSlotTokens,
4913
5308
  urlParameters: UrlParameterTable,
5309
+ bindingEvidence: ReadonlySet<string>,
4914
5310
  slotSink?: Record<string, SlotType>,
4915
5311
  ): string {
4916
- return `function ${memberSignature(prepared, name, mapType, resolver, constantTokens, urlParameters, slotSink)}`;
5312
+ return `function ${memberSignature(prepared, name, mapType, resolver, constantTokens, urlParameters, bindingEvidence, slotSink)}`;
4917
5313
  }
4918
5314
 
4919
5315
  // A colon-method member of a handle interface: identical signature machinery to a
@@ -4925,6 +5321,7 @@ function emitMethod(
4925
5321
  resolver: TableDocResolver,
4926
5322
  constantTokens: ConstantSlotTokens,
4927
5323
  urlParameters: UrlParameterTable,
5324
+ bindingEvidence: ReadonlySet<string>,
4928
5325
  slotSink?: Record<string, SlotType>,
4929
5326
  ): string {
4930
5327
  return memberSignature(
@@ -4934,6 +5331,7 @@ function emitMethod(
4934
5331
  resolver,
4935
5332
  constantTokens,
4936
5333
  urlParameters,
5334
+ bindingEvidence,
4937
5335
  slotSink,
4938
5336
  );
4939
5337
  }
@@ -4956,13 +5354,18 @@ function functionDocLines(
4956
5354
  baseNotes: IndexBaseNoteResolver,
4957
5355
  indent: string = INDENT,
4958
5356
  ): 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
- }));
5357
+ const params = fn.parameters.map((p, index) => {
5358
+ const curationNote = CALLBACK_SIGNATURE_CURATIONS.get(
5359
+ tableSlotKey(fn.name, "param", p.name),
5360
+ )?.note;
5361
+ return {
5362
+ name: emittedParamName(p, index),
5363
+ doc: appendNotes(translateProseFences(htmlToDocText(p.doc), fn.name, translations), [
5364
+ ...baseNotes(fn.name, "param", p.name),
5365
+ ...(curationNote === undefined ? [] : [curationNote]),
5366
+ ]),
5367
+ };
5368
+ });
4966
5369
  const onlyReturn = fn.returnValues.length === 1 ? fn.returnValues[0] : undefined;
4967
5370
  const notedReturns =
4968
5371
  multiReturnDoc(fn, translations, baseNotes) ?? overrideReturnDoc(fn, baseNotes);
@@ -5273,12 +5676,24 @@ function emitParameter(
5273
5676
  return { text: `${name}${optional ? "?" : ""}: ${type}`, type, optional };
5274
5677
  }
5275
5678
 
5679
+ // A `nullable` correction rewrites only the `nil` in the declared tokens and
5680
+ // leaves the rest to the ordinary mapping and its top-level `nil` projection.
5681
+ function correctedReturnTokens(
5682
+ types: readonly string[],
5683
+ correction: ReturnTypeCorrection | undefined,
5684
+ ): readonly string[] {
5685
+ if (correction?.nullable === undefined) return types;
5686
+ const base = types.filter((token) => token !== "nil");
5687
+ return correction.nullable ? [...base, "nil"] : base;
5688
+ }
5689
+
5276
5690
  function emitReturn(
5277
5691
  returnValues: ApiParameter[],
5278
5692
  mapType: DefoldTypeMapper,
5279
5693
  resolver: TableDocResolver,
5280
5694
  constantTokens: ConstantSlotTokens,
5281
5695
  elementName: string,
5696
+ bindingEvidence: ReadonlySet<string>,
5282
5697
  ): { type: string; trailing: string; slots: EmittedSlot[] } {
5283
5698
  // An authored whole-return override is not a per-slot render, so it reports no
5284
5699
  // slots and leaves the consumer on its token-derived fallback.
@@ -5287,23 +5702,30 @@ function emitReturn(
5287
5702
  // Kept beside the override lookup so the two stay visibly distinct: this one
5288
5703
  // contradicts a declared token rather than filling an empty `returnvalues`,
5289
5704
  // and it is a per-slot render, so it reports its slot.
5290
- const correction = RETURN_TYPE_CORRECTIONS.get(elementName);
5705
+ const listed = RETURN_TYPE_CORRECTIONS.get(elementName);
5706
+ const correction =
5707
+ listed?.binding === undefined || bindingEvidence.has(elementName) ? listed : undefined;
5291
5708
  if (returnValues.length === 0) return { type: "void", trailing: "", slots: [] };
5292
5709
  if (returnValues.length > 1) {
5293
5710
  // Defold multi-returns are positional and always present; each slot maps
5294
5711
  // straight through (unknown when the doc lists no type) into a tuple that
5295
5712
  // 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:
5713
+ const emitted = returnValues.map((rv, index) => {
5714
+ const applied =
5300
5715
  correction?.slot !== undefined &&
5301
5716
  correction.slot === rv.name &&
5302
5717
  pinMatches(rv.types, correctionPins(correction))
5303
- ? correction.ts
5304
- : rv.types.length > 0
5718
+ ? correction
5719
+ : undefined;
5720
+ const types = correctedReturnTokens(rv.types, applied);
5721
+ return {
5722
+ position: index,
5723
+ name: rv.name,
5724
+ ts:
5725
+ applied?.ts ??
5726
+ (types.length > 0
5305
5727
  ? mapSlotUnion(
5306
- rv.types,
5728
+ types,
5307
5729
  rv.doc,
5308
5730
  mapType,
5309
5731
  false,
@@ -5313,8 +5735,9 @@ function emitReturn(
5313
5735
  "return",
5314
5736
  rv.name,
5315
5737
  )
5316
- : "unknown",
5317
- }));
5738
+ : "unknown"),
5739
+ };
5740
+ });
5318
5741
  return {
5319
5742
  type: `LuaMultiReturn<[${emitted.map((slot) => slot.ts).join(", ")}]>`,
5320
5743
  trailing: "",
@@ -5323,21 +5746,24 @@ function emitReturn(
5323
5746
  }
5324
5747
  const first = returnValues[0];
5325
5748
  if (!first) return { type: "void", trailing: "", slots: [] };
5326
- if (
5749
+ const applied =
5327
5750
  correction !== undefined &&
5328
5751
  correction.slot === undefined &&
5329
5752
  pinMatches(first.types, correctionPins(correction))
5330
- ) {
5753
+ ? correction
5754
+ : undefined;
5755
+ if (applied?.ts !== undefined) {
5331
5756
  return {
5332
- type: correction.ts,
5757
+ type: applied.ts,
5333
5758
  trailing: "",
5334
- slots: [{ position: 0, name: first.name, ts: correction.ts }],
5759
+ slots: [{ position: 0, name: first.name, ts: applied.ts }],
5335
5760
  };
5336
5761
  }
5762
+ const types = correctedReturnTokens(first.types, applied);
5337
5763
  const ts =
5338
- first.types.length > 0
5764
+ types.length > 0
5339
5765
  ? mapSlotUnion(
5340
- first.types,
5766
+ types,
5341
5767
  first.doc,
5342
5768
  mapType,
5343
5769
  false,
@@ -5361,6 +5787,26 @@ function emitReturn(
5361
5787
  // only".
5362
5788
  const READ_ONLY_MARK = /^\s*<span class="mark">READ ONLY<\/span>/;
5363
5789
 
5790
+ // A property the engine reads but refuses to write while upstream omits the
5791
+ // READ ONLY mark. `evidence` names the component source that shows the missing
5792
+ // write branch. The emit throws once upstream adds the mark, so the entry is
5793
+ // deleted rather than outliving the defect it corrects.
5794
+ export interface ReadOnlyPropertyCorrection {
5795
+ readonly evidence: string;
5796
+ }
5797
+
5798
+ // Keyed `<namespace>.<property>`, mirroring PROPERTY_TYPE_CORRECTIONS.
5799
+ export const READ_ONLY_PROPERTY_CORRECTIONS: ReadonlyMap<string, ReadOnlyPropertyCorrection> =
5800
+ new Map([
5801
+ [
5802
+ "model.animation",
5803
+ {
5804
+ evidence:
5805
+ "comp_model.cpp:CompModelSetProperty has no animation branch, so the write falls through to the material-constant lookup and fails",
5806
+ },
5807
+ ],
5808
+ ]);
5809
+
5364
5810
  function emitPropertyMembers(
5365
5811
  p: ApiProperty,
5366
5812
  mapType: DefoldTypeMapper,
@@ -5381,7 +5827,14 @@ function emitPropertyMembers(
5381
5827
  : p.types.length > 0
5382
5828
  ? unionFromTokens(p.types, mapType)
5383
5829
  : "unknown";
5384
- const modifier = READ_ONLY_MARK.test(p.description) ? "readonly " : "";
5830
+ const marked = READ_ONLY_MARK.test(p.description);
5831
+ const readOnlyCorrected = READ_ONLY_PROPERTY_CORRECTIONS.has(`${namespace}.${p.name}`);
5832
+ if (marked && readOnlyCorrected) {
5833
+ throw new Error(
5834
+ `read-only property correction ${namespace}.${p.name}: upstream marks the property READ ONLY`,
5835
+ );
5836
+ }
5837
+ const modifier = marked || readOnlyCorrected ? "readonly " : "";
5385
5838
  return keys.map((key) => `${modifier}${key}: ${ts};`);
5386
5839
  }
5387
5840
 
@@ -5521,11 +5974,17 @@ function mapSlotUnion(
5521
5974
  // return slots.
5522
5975
  ts = "undefined";
5523
5976
  } else {
5524
- const curated =
5977
+ const curation =
5525
5978
  slotKind !== undefined && slotName !== undefined
5526
5979
  ? CALLBACK_SIGNATURE_CURATIONS.get(tableSlotKey(elementName, slotKind, slotName))
5527
5980
  : undefined;
5528
- ts = curated ?? mapType(token);
5981
+ if (curation === undefined) {
5982
+ ts = mapType(token);
5983
+ } else if (isLualsFunctionToken(token)) {
5984
+ ts = curation.typed ?? mapType(token);
5985
+ } else {
5986
+ ts = curation.untyped;
5987
+ }
5529
5988
  }
5530
5989
  push(ts);
5531
5990
  }
@@ -5690,7 +6149,7 @@ function joinUnionArms(mapped: readonly string[]): string {
5690
6149
  }
5691
6150
 
5692
6151
  const BUILTIN_MESSAGE_NAMES: ReadonlySet<string> = new Set(
5693
- messagesDoc.messages.map((message) => message.name),
6152
+ applyMessageOverlays(parseMessagesDoc(messagesDoc)).entries.map((entry) => entry.name),
5694
6153
  );
5695
6154
  const MESSAGE_REFERENCE = /^message\.[a-z_]+\.([a-z_]+)$/;
5696
6155
 
@@ -5706,6 +6165,18 @@ function resolveDefoldLeaf(leaf: string): string | undefined {
5706
6165
  return undefined;
5707
6166
  }
5708
6167
 
6168
+ // The union typedefs whose message arms each carry the engine's message-name
6169
+ // hash in `type`: script_physics.cpp sets it on every `physics.set_event_listener`
6170
+ // event. The same message referenced anywhere else (`physics.raycast`) has none.
6171
+ const DISCRIMINATED_MESSAGE_UNIONS: ReadonlySet<string> = new Set(["physics.event"]);
6172
+
6173
+ function discriminatedMessageArm(token: string): string | undefined {
6174
+ const name = MESSAGE_REFERENCE.exec(token)?.[1];
6175
+ const payload = resolveDefoldLeaf(token);
6176
+ if (name === undefined || payload === undefined) return undefined;
6177
+ return `${payload} & { type: Hash<"${name}"> }`;
6178
+ }
6179
+
5709
6180
  // Whole-token lookup and callback recovery run first, so the grammar sees only
5710
6181
  // what they miss; every pre-1.13.2 token resolves before it is reached.
5711
6182
  function wholeTokenMapping(token: string): string | undefined {