@defold-typescript/types 0.34.0 → 0.36.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 (67) hide show
  1. package/api-signatures.json +9547 -186
  2. package/api-targets.json +9 -66
  3. package/examples/translations.json +6 -6
  4. package/generated/buffer.d.ts +2 -2
  5. package/generated/collectionfactory.d.ts +2 -2
  6. package/generated/factory.d.ts +13 -12
  7. package/generated/gui.d.ts +23 -23
  8. package/generated/kinds/gui-script.d.ts +0 -4
  9. package/generated/kinds/render-script.d.ts +1 -4
  10. package/generated/kinds/script.d.ts +0 -4
  11. package/generated/model.d.ts +2 -2
  12. package/generated/profiler.d.ts +2 -4
  13. package/generated/render.d.ts +7 -155
  14. package/generated/sys.d.ts +1 -1
  15. package/generated/versions/defold-1.12.4/buffer.d.ts +2 -2
  16. package/generated/versions/defold-1.12.4/collectionfactory.d.ts +2 -2
  17. package/generated/versions/defold-1.12.4/factory.d.ts +13 -12
  18. package/generated/versions/defold-1.12.4/gui.d.ts +23 -23
  19. package/generated/versions/defold-1.12.4/index.d.ts +9 -4
  20. package/generated/versions/defold-1.12.4/model.d.ts +2 -2
  21. package/generated/versions/defold-1.12.4/profiler.d.ts +2 -4
  22. package/generated/versions/defold-1.12.4/render.d.ts +7 -155
  23. package/generated/versions/defold-1.12.4/sys.d.ts +1 -1
  24. package/generated/versions/defold-1.12.4/vmath.d.ts +0 -19
  25. package/generated/versions/defold-1.12.4/window.d.ts +2 -4
  26. package/generated/versions/defold-1.12.4/zlib.d.ts +2 -1
  27. package/generated/versions/defold-1.13.0/buffer.d.ts +2 -2
  28. package/generated/versions/defold-1.13.0/collectionfactory.d.ts +2 -2
  29. package/generated/versions/defold-1.13.0/factory.d.ts +13 -12
  30. package/generated/versions/defold-1.13.0/gui.d.ts +23 -23
  31. package/generated/versions/defold-1.13.0/index.d.ts +9 -4
  32. package/generated/versions/defold-1.13.0/model.d.ts +2 -2
  33. package/generated/versions/defold-1.13.0/profiler.d.ts +2 -4
  34. package/generated/versions/defold-1.13.0/render.d.ts +7 -155
  35. package/generated/versions/defold-1.13.0/sys.d.ts +1 -1
  36. package/generated/versions/defold-1.13.0/vmath.d.ts +0 -19
  37. package/generated/versions/defold-1.13.0/window.d.ts +2 -4
  38. package/generated/versions/defold-1.13.0/zlib.d.ts +2 -1
  39. package/generated/vmath.d.ts +0 -19
  40. package/generated/window.d.ts +2 -4
  41. package/generated/zlib.d.ts +2 -1
  42. package/index.d.ts +2 -4
  43. package/package.json +7 -2
  44. package/scripts/extension-goldens.ts +23 -0
  45. package/scripts/generate-api-signatures.ts +72 -8
  46. package/scripts/materialize-version.ts +15 -1
  47. package/scripts/regen.ts +109 -27
  48. package/scripts/signature-store-fs.ts +1 -0
  49. package/scripts/sync-api-docs.ts +5 -61
  50. package/src/emit-dts.ts +359 -58
  51. package/src/index.ts +1 -0
  52. package/src/optional-correction-provenance.ts +13 -2
  53. package/src/render-overloads.d.ts +46 -0
  54. package/src/vmath-overloads.d.ts +19 -0
  55. package/generated/iac.d.ts +0 -18
  56. package/generated/iap.d.ts +0 -60
  57. package/generated/push.d.ts +0 -66
  58. package/generated/versions/defold-1.12.4/iac.d.ts +0 -18
  59. package/generated/versions/defold-1.12.4/iap.d.ts +0 -60
  60. package/generated/versions/defold-1.12.4/push.d.ts +0 -66
  61. package/generated/versions/defold-1.12.4/webview.d.ts +0 -71
  62. package/generated/versions/defold-1.13.0/iac.d.ts +0 -18
  63. package/generated/versions/defold-1.13.0/iap.d.ts +0 -60
  64. package/generated/versions/defold-1.13.0/push.d.ts +0 -66
  65. package/generated/versions/defold-1.13.0/webview.d.ts +0 -71
  66. package/generated/webview.d.ts +0 -71
  67. package/scripts/fidelity-audit.ts +0 -684
package/src/emit-dts.ts CHANGED
@@ -151,11 +151,10 @@ export const ARBITRARY_TABLE_SLOTS = new Set([
151
151
  "socket.dns.toip",
152
152
  "socket.dns.getaddrinfo",
153
153
  "socket.dns.getnameinfo",
154
- // engine-built render-target tables: the "see the description" cross-refs
155
- // are dead (the function's own description is empty) or carry a typed
154
+ // engine-built render-target tables: the "see the description" cross-ref is
155
+ // dead (the function's own description is empty) or carries a typed
156
156
  // `transient: table` field whose sub-doc is prose-only, so the nested table
157
157
  // is opaque to the field-list parser and the whole-slot `Record` is faithful.
158
- "render.render_target",
159
158
  "render.set_render_target",
160
159
  // reserved-but-undocumented options bag: `collectionproxy.load`'s `options`
161
160
  // slot is documented verbatim as "options table, currently unused" and accepts
@@ -204,6 +203,10 @@ export const OVERLOAD_COVERED_SKIPS = new Set([
204
203
  // msg-overloads.d.ts supplies the typed msg.url shape (0 / 1-string / 3-arg
205
204
  // arities; the runtime-invalid two-arg form is intentionally omitted).
206
205
  "msg.url",
206
+ // render-overloads.d.ts supplies the name-less and named render_target arities.
207
+ "render.render_target",
208
+ // vmath-overloads.d.ts supplies the vector3 and three-number euler_to_quat arities.
209
+ "vmath.euler_to_quat",
207
210
  // vmath-overloads.d.ts supplies the generic covariant signatures.
208
211
  "vmath.clamp",
209
212
  "vmath.lerp",
@@ -219,7 +222,9 @@ export const OVERLOAD_COVERED_SKIPS = new Set([
219
222
  // is the TSTL non-string-key Lua-table idiom and resolves the same way the
220
223
  // already-emitted `LuaMultiReturn` does. The audit consults this map too, so the
221
224
  // gate and the emitted surface stay coupled; a slot not curated here still
222
- // surfaces under `recordTables` until a human adds it.
225
+ // surfaces under `recordTables` until a human adds it. Key and value are both
226
+ // `A | B` union tokens split per token, so a key whose doc names constants
227
+ // brands each of them rather than widening to `number`.
223
228
  export const MAPPING_TABLE_SLOTS: ReadonlyMap<string, { key: string; value: string }> = new Map([
224
229
  ["gui.clone_tree", { key: "hash", value: "node" }],
225
230
  ["gui.get_tree", { key: "hash", value: "node" }],
@@ -351,6 +356,131 @@ export const OPTIONAL_SLOT_CORRECTIONS: ReadonlyMap<string, string> = new Map([
351
356
  ),
352
357
  ]);
353
358
 
359
+ export type ConstantSlotResolution =
360
+ | { readonly borrow: string }
361
+ | { readonly family: string; readonly evidence: string }
362
+ | { readonly residual: string };
363
+
364
+ // A top-level `constant` slot takes the constants its own doc names. This table
365
+ // fills only a slot whose own doc names none: a `borrow` reads another slot's doc
366
+ // (same key shape), a `family` expands a `<prefix>*` wildcard evidenced by the
367
+ // named element's example, and a `residual` records why nothing can brand it.
368
+ // Constants always come from the brand universe, never from a hand-typed list.
369
+ export const CONSTANT_SLOT_RESOLUTIONS: ReadonlyMap<string, ConstantSlotResolution> = new Map<
370
+ string,
371
+ ConstantSlotResolution
372
+ >([
373
+ ["gui.get:param:property", { borrow: "gui.animate:param:property" }],
374
+ ["gui.set:param:property", { borrow: "gui.animate:param:property" }],
375
+ ["gui.cancel_animations:param:property", { borrow: "gui.animate:param:property" }],
376
+ [
377
+ "buffer.set_metadata:param:value_type",
378
+ { family: "buffer.VALUE_TYPE_*", evidence: "buffer.set_metadata" },
379
+ ],
380
+ [
381
+ "buffer.get_metadata:return:value_type",
382
+ { family: "buffer.VALUE_TYPE_*", evidence: "buffer.set_metadata" },
383
+ ],
384
+ ["gui.new_texture:param:type", { residual: "the doc lists texture type strings only" }],
385
+ ["gui.set_texture_data:param:type", { residual: "the doc lists texture type strings only" }],
386
+ [
387
+ "iap.get_provider_id:return:provider_id",
388
+ { residual: "the doc names iap.PROVIDER_ID_*, which no ref-doc defines as a constant" },
389
+ ],
390
+ ]);
391
+
392
+ const DOC_CONSTANT_TOKEN =
393
+ /(?<![A-Za-z0-9_.])([a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)*\.[A-Z][A-Z0-9_]*)(\*)?/g;
394
+
395
+ // The constant FQNs a piece of ref-doc text names, in doc order: exact tokens kept
396
+ // only when they are in `universe`, and each `<prefix>*` wildcard expanded to every
397
+ // universe FQN with that prefix, sorted.
398
+ export function documentedConstantTokens(text: string, universe: ReadonlySet<string>): string[] {
399
+ const out = new Set<string>();
400
+ for (const match of text.replace(/<[^>]*>/g, "").matchAll(DOC_CONSTANT_TOKEN)) {
401
+ const token = match[1] as string;
402
+ if (match[2] === undefined) {
403
+ if (universe.has(token)) out.add(token);
404
+ continue;
405
+ }
406
+ for (const fqn of [...universe].filter((candidate) => candidate.startsWith(token)).sort()) {
407
+ out.add(fqn);
408
+ }
409
+ }
410
+ return [...out];
411
+ }
412
+
413
+ // The constants a `table` slot's doc names as the table's *keys*, as opposed to
414
+ // the far commoner shape where a field's *value* is a constant
415
+ // (`physics.get_shape`'s `type`, `resource.create_texture`'s `format`). Upstream
416
+ // marks the difference in prose — render.clear reads "table with keys specifying
417
+ // which buffers to clear ... Available keys are:" — so a key mention alongside a
418
+ // named constant is the discriminator. Empty means the slot has no documented
419
+ // constant keys, which is true of every `table` slot but one today.
420
+ export function documentedConstantKeyTokens(doc: string, universe: ReadonlySet<string>): string[] {
421
+ if (!/\bkeys?\b/i.test(doc.replace(/<[^>]*>/g, ""))) return [];
422
+ return documentedConstantTokens(doc, universe);
423
+ }
424
+
425
+ type SlotDocLookup = (slotKey: string) => string | undefined;
426
+
427
+ function constantSlotTokens(
428
+ ownDoc: string,
429
+ slotKey: string,
430
+ lookupDoc: SlotDocLookup,
431
+ universe: ReadonlySet<string>,
432
+ ): string[] {
433
+ const own = documentedConstantTokens(ownDoc, universe);
434
+ if (own.length > 0) return own;
435
+ const entry = CONSTANT_SLOT_RESOLUTIONS.get(slotKey);
436
+ if (entry === undefined || "residual" in entry) return [];
437
+ if ("borrow" in entry) return documentedConstantTokens(lookupDoc(entry.borrow) ?? "", universe);
438
+ return documentedConstantTokens(entry.family, universe);
439
+ }
440
+
441
+ function slotDocLookup(module: ApiModule): SlotDocLookup {
442
+ const docs = new Map<string, string>();
443
+ for (const fn of module.functions) {
444
+ for (const p of fn.parameters) {
445
+ const key = tableSlotKey(fn.name, "param", p.name);
446
+ if (!docs.has(key)) docs.set(key, p.doc);
447
+ }
448
+ for (const rv of fn.returnValues) {
449
+ const key = tableSlotKey(fn.name, "return", rv.name);
450
+ if (!docs.has(key)) docs.set(key, rv.doc);
451
+ }
452
+ }
453
+ return (slotKey) => docs.get(slotKey);
454
+ }
455
+
456
+ // The constants a module's top-level `constant` slot expands into: its own doc
457
+ // first, then its CONSTANT_SLOT_RESOLUTIONS entry. Empty means the slot keeps
458
+ // `Opaque<"constant">`.
459
+ export function resolveConstantSlotTokens(
460
+ module: ApiModule,
461
+ slotKey: string,
462
+ universe: ReadonlySet<string>,
463
+ ): string[] {
464
+ const lookupDoc = slotDocLookup(module);
465
+ return constantSlotTokens(lookupDoc(slotKey) ?? "", slotKey, lookupDoc, universe);
466
+ }
467
+
468
+ type ConstantSlotTokens = (
469
+ elementName: string,
470
+ slotKind: "param" | "return",
471
+ slotName: string,
472
+ doc: string,
473
+ ) => readonly string[];
474
+
475
+ function constantSlotTokenResolver(
476
+ module: ApiModule,
477
+ universe: ReadonlySet<string>,
478
+ ): ConstantSlotTokens {
479
+ const lookupDoc = slotDocLookup(module);
480
+ return (elementName, slotKind, slotName, doc) =>
481
+ constantSlotTokens(doc, tableSlotKey(elementName, slotKind, slotName), lookupDoc, universe);
482
+ }
483
+
354
484
  // FQN-keyed allowlist of the `types.is_*` checks that genuinely narrow their
355
485
  // argument, mapped to the `DEFOLD_TYPE_MAP` token whose interface they prove.
356
486
  // Emitting these as user-defined type guards (`var_ is Vector3`) is the only way
@@ -714,11 +844,20 @@ export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new
714
844
  // render.predicate's `tags` is "table of tags ... can be of either hash or
715
845
  // string type", a homogeneous array whose element is the string|hash union.
716
846
  ["render.predicate:param:tags", { kind: "array", element: ["string", "hash"] }],
717
- // render.clear's `buffers` maps the numeric graphics.BUFFER_* constants to
718
- // their clear value: vector4 for the color buffer, number for depth/stencil.
719
- // The mapping value is the `number | vector4` union; the mapping string branch
720
- // splits the union token, so the value emits `number | Vector4`.
721
- ["render.clear:param:buffers", { kind: "mapping", key: "number", value: "number | vector4" }],
847
+ // render.clear's `buffers` maps the graphics.BUFFER_TYPE_* constants its doc
848
+ // names as keys to their clear value: vector4 for the color buffer, number for
849
+ // depth/stencil. Both sides are union tokens the mapping string branch splits,
850
+ // so the key emits the three branded constants and the value `number | Vector4`.
851
+ // The key is branded rather than `number`, which is the narrowing users migrate
852
+ // to via the authored `render.ClearBufferKey` alias.
853
+ [
854
+ "render.clear:param:buffers",
855
+ {
856
+ kind: "mapping",
857
+ key: "graphics.BUFFER_TYPE_COLOR0_BIT | graphics.BUFFER_TYPE_DEPTH_BIT | graphics.BUFFER_TYPE_STENCIL_BIT",
858
+ value: "number | vector4",
859
+ },
860
+ ],
722
861
  // go.on_input and gui.on_input share the same well-known InputAction shape
723
862
  // (the on_input function description lists the well-known fields: value,
724
863
  // pressed, released, repeated, x/y/screen_x/screen_y/dx/dy/screen_dx/screen_dy,
@@ -1580,6 +1719,10 @@ export function emitDeclarations(module: ApiModule, options?: EmitOptions): stri
1580
1719
  constantFqns.has(token) || knownConstantFqns?.has(token)
1581
1720
  ? brandType(token)
1582
1721
  : baseMapType(token);
1722
+ const constantTokens = constantSlotTokenResolver(
1723
+ module,
1724
+ new Set([...constantFqns, ...(knownConstantFqns ?? [])]),
1725
+ );
1583
1726
 
1584
1727
  const constants = module.constants
1585
1728
  .map((c) => prepareConstant(c, prefix))
@@ -1656,7 +1799,9 @@ export function emitDeclarations(module: ApiModule, options?: EmitOptions): stri
1656
1799
  for (const docLine of functionDocLines(fn.original, translations, handleIndent)) {
1657
1800
  lines.push(docLine);
1658
1801
  }
1659
- lines.push(`${handleIndent}${emitMethod(fn, mapType, resolver, urlParameters)}`);
1802
+ lines.push(
1803
+ `${handleIndent}${emitMethod(fn, mapType, resolver, constantTokens, urlParameters)}`,
1804
+ );
1660
1805
  }
1661
1806
  lines.push(`${INDENT}}`);
1662
1807
  }
@@ -1685,7 +1830,7 @@ export function emitDeclarations(module: ApiModule, options?: EmitOptions): stri
1685
1830
  const reserved = TS_RESERVED_NAMES.has(fn.name);
1686
1831
  const emitName = aliasName(fn.name, aliases);
1687
1832
  for (const docLine of functionDocLines(fn.original, translations)) lines.push(docLine);
1688
- const line = emitFunction(fn, emitName, mapType, resolver, urlParameters);
1833
+ const line = emitFunction(fn, emitName, mapType, resolver, constantTokens, urlParameters);
1689
1834
  lines.push(`${INDENT}${reserved ? "" : decl}${line}`);
1690
1835
  }
1691
1836
 
@@ -1733,7 +1878,7 @@ export function emitDeclarations(module: ApiModule, options?: EmitOptions): stri
1733
1878
  lines.push(docLine);
1734
1879
  }
1735
1880
  lines.push(
1736
- `${bodyIndent}${reserved ? "" : segmentDecl}${emitFunction(fn, emitName, mapType, resolver, urlParameters)}`,
1881
+ `${bodyIndent}${reserved ? "" : segmentDecl}${emitFunction(fn, emitName, mapType, resolver, constantTokens, urlParameters)}`,
1737
1882
  );
1738
1883
  }
1739
1884
  for (const alias of [...segmentAliases].sort((a, b) => a.public.localeCompare(b.public))) {
@@ -1761,10 +1906,25 @@ export function emitDeclarations(module: ApiModule, options?: EmitOptions): stri
1761
1906
  return `${lines.join("\n")}\n`;
1762
1907
  }
1763
1908
 
1909
+ /**
1910
+ * Every documented slot's rendered TS type, keyed `param|return:<position>:<raw
1911
+ * ref-doc name>`. The position is part of the key because a multi-return can
1912
+ * carry several unnamed slots (`push.schedule` renders two), which a name-only
1913
+ * key would collapse into one. Each value appears verbatim inside the symbol's
1914
+ * own `tsSignature`, so a consumer rendering a slot's type beside that signature
1915
+ * cannot contradict it.
1916
+ */
1917
+ export type SlotTypes = Readonly<Record<string, string>>;
1918
+
1919
+ function slotKey(kind: "param" | "return", position: number, name: string): string {
1920
+ return `${kind}:${position}:${name}`;
1921
+ }
1922
+
1764
1923
  /** One emitted symbol's identity paired with its rendered TS signature text. */
1765
1924
  export interface SymbolSignature {
1766
1925
  readonly identity: ApiSymbolIdentity;
1767
1926
  readonly tsSignature: string;
1927
+ readonly slotTypes: SlotTypes;
1768
1928
  }
1769
1929
 
1770
1930
  /**
@@ -1789,6 +1949,10 @@ export function emitSymbolSignatures(module: ApiModule, options?: EmitOptions):
1789
1949
  constantFqns.has(token) || knownConstantFqns?.has(token)
1790
1950
  ? brandType(token)
1791
1951
  : baseMapType(token);
1952
+ const constantTokens = constantSlotTokenResolver(
1953
+ module,
1954
+ new Set([...constantFqns, ...(knownConstantFqns ?? [])]),
1955
+ );
1792
1956
  const resolver = buildTableDocResolver(
1793
1957
  module.functions.map((fn) => ({
1794
1958
  name: fn.name,
@@ -1805,9 +1969,14 @@ export function emitSymbolSignatures(module: ApiModule, options?: EmitOptions):
1805
1969
  });
1806
1970
  const emitName = (name: string): string => (TS_RESERVED_NAMES.has(name) ? `_${name}` : name);
1807
1971
 
1972
+ // A member with no call list (constant, variable, property, typedef) documents
1973
+ // no slot, so it reports an empty map rather than being absent from the ledger.
1974
+ const NO_SLOTS: SlotTypes = {};
1975
+
1808
1976
  for (const fn of module.functions) {
1809
1977
  const prepared = prepareFunction(fn, prefix);
1810
1978
  if (prepared === null) continue;
1979
+ const slotTypes: Record<string, string> = {};
1811
1980
  out.push({
1812
1981
  identity: fnIdentity(fn),
1813
1982
  tsSignature: emitFunction(
@@ -1815,8 +1984,11 @@ export function emitSymbolSignatures(module: ApiModule, options?: EmitOptions):
1815
1984
  emitName(prepared.name),
1816
1985
  mapType,
1817
1986
  resolver,
1987
+ constantTokens,
1818
1988
  urlParameters,
1989
+ slotTypes,
1819
1990
  ),
1991
+ slotTypes,
1820
1992
  });
1821
1993
  }
1822
1994
 
@@ -1830,21 +2002,41 @@ export function emitSymbolSignatures(module: ApiModule, options?: EmitOptions):
1830
2002
  signature: "",
1831
2003
  },
1832
2004
  tsSignature: emitVariable(v, emitName(v.name), mapType),
2005
+ slotTypes: NO_SLOTS,
1833
2006
  });
1834
2007
  }
1835
2008
  for (const fn of nested.functions) {
2009
+ const slotTypes: Record<string, string> = {};
1836
2010
  out.push({
1837
2011
  identity: fnIdentity(fn.original),
1838
- tsSignature: emitFunction(fn, emitName(fn.name), mapType, resolver, urlParameters),
2012
+ tsSignature: emitFunction(
2013
+ fn,
2014
+ emitName(fn.name),
2015
+ mapType,
2016
+ resolver,
2017
+ constantTokens,
2018
+ urlParameters,
2019
+ slotTypes,
2020
+ ),
2021
+ slotTypes,
1839
2022
  });
1840
2023
  }
1841
2024
 
1842
2025
  const handleGroups = collectHandleMethodGroups(module);
1843
2026
  for (const group of handleGroups.values()) {
1844
2027
  for (const prepared of group) {
2028
+ const slotTypes: Record<string, string> = {};
1845
2029
  out.push({
1846
2030
  identity: fnIdentity(prepared.original),
1847
- tsSignature: emitMethod(prepared, mapType, resolver, urlParameters),
2031
+ tsSignature: emitMethod(
2032
+ prepared,
2033
+ mapType,
2034
+ resolver,
2035
+ constantTokens,
2036
+ urlParameters,
2037
+ slotTypes,
2038
+ ),
2039
+ slotTypes,
1848
2040
  });
1849
2041
  }
1850
2042
  }
@@ -1855,6 +2047,7 @@ export function emitSymbolSignatures(module: ApiModule, options?: EmitOptions):
1855
2047
  out.push({
1856
2048
  identity: { namespace: module.namespace, kind: "CONSTANT", name: c.name, signature: "" },
1857
2049
  tsSignature: `const ${prepared.name}: ${brandType(prepared.fqn)};`,
2050
+ slotTypes: NO_SLOTS,
1858
2051
  });
1859
2052
  }
1860
2053
 
@@ -1864,6 +2057,7 @@ export function emitSymbolSignatures(module: ApiModule, options?: EmitOptions):
1864
2057
  out.push({
1865
2058
  identity: { namespace: module.namespace, kind: "VARIABLE", name: v.name, signature: "" },
1866
2059
  tsSignature: emitVariable(prepared, emitName(prepared.name), mapType),
2060
+ slotTypes: NO_SLOTS,
1867
2061
  });
1868
2062
  }
1869
2063
 
@@ -1871,6 +2065,7 @@ export function emitSymbolSignatures(module: ApiModule, options?: EmitOptions):
1871
2065
  out.push({
1872
2066
  identity: { namespace: module.namespace, kind: "PROPERTY", name: p.name, signature: "" },
1873
2067
  tsSignature: emitPropertyMember(p, mapType, module.namespace),
2068
+ slotTypes: NO_SLOTS,
1874
2069
  });
1875
2070
  }
1876
2071
 
@@ -1879,6 +2074,7 @@ export function emitSymbolSignatures(module: ApiModule, options?: EmitOptions):
1879
2074
  out.push({
1880
2075
  identity: { namespace: module.namespace, kind: "TYPEDEF", name: t.name, signature: "" },
1881
2076
  tsSignature: `type ${t.name} = Opaque<"${t.name}">;`,
2077
+ slotTypes: NO_SLOTS,
1882
2078
  });
1883
2079
  }
1884
2080
 
@@ -2072,32 +2268,61 @@ function memberSignature(
2072
2268
  name: string,
2073
2269
  mapType: (t: string) => string,
2074
2270
  resolver: TableDocResolver,
2271
+ constantTokens: ConstantSlotTokens,
2075
2272
  urlParameters: UrlParameterTable,
2273
+ slotSink?: Record<string, string>,
2076
2274
  ): string {
2077
2275
  const original = prepared.original.parameters;
2078
2276
  const elementName = prepared.original.name;
2079
2277
  const cutoff = trailingOptionalCutoff(original, elementName);
2080
2278
  const varargIndex = original.findIndex(isVarargParameter);
2081
2279
  const positional = (varargIndex === -1 ? original : original.slice(0, varargIndex)).map((p, i) =>
2082
- emitParameter(p, i, i >= cutoff, mapType, resolver, elementName, urlParameters),
2280
+ emitParameter(p, i, i >= cutoff, mapType, resolver, constantTokens, elementName, urlParameters),
2083
2281
  );
2084
- const params = (
2282
+ const rest =
2085
2283
  varargIndex === -1
2086
- ? positional
2087
- : [
2088
- ...positional,
2089
- emitRestParameter(original, varargIndex, mapType, resolver, elementName, urlParameters),
2090
- ]
2091
- ).join(", ");
2092
- const ret = emitReturn(prepared.original.returnValues, mapType, resolver, elementName);
2284
+ ? null
2285
+ : emitRestParameter(
2286
+ original,
2287
+ varargIndex,
2288
+ mapType,
2289
+ resolver,
2290
+ constantTokens,
2291
+ elementName,
2292
+ urlParameters,
2293
+ );
2294
+ const params = [...positional.map((p) => p.text), ...(rest ? [rest.text] : [])].join(", ");
2295
+ const ret = emitReturn(
2296
+ prepared.original.returnValues,
2297
+ mapType,
2298
+ resolver,
2299
+ constantTokens,
2300
+ elementName,
2301
+ );
2093
2302
  const predicateToken = TYPE_PREDICATES.get(elementName);
2094
2303
  const soleParam = original[0];
2095
- if (
2304
+ const isPredicate =
2096
2305
  predicateToken !== undefined &&
2097
2306
  soleParam !== undefined &&
2098
2307
  original.length === 1 &&
2099
- ret.type === "boolean"
2100
- ) {
2308
+ ret.type === "boolean";
2309
+ if (slotSink !== undefined) {
2310
+ for (const [i, p] of positional.entries()) {
2311
+ const raw = original[i];
2312
+ if (raw) slotSink[slotKey("param", i, raw.name)] = p.ts;
2313
+ }
2314
+ for (const slot of rest?.slots ?? []) {
2315
+ slotSink[slotKey("param", slot.position, slot.name)] = slot.ts;
2316
+ }
2317
+ // A predicate renders `arg is T` in place of the mapped `boolean`, so its
2318
+ // return slot has no text in the signature to stand on and is not reported.
2319
+ if (!isPredicate) {
2320
+ for (const slot of ret.slots) {
2321
+ slotSink[slotKey("return", slot.position, slot.name)] = slot.ts;
2322
+ }
2323
+ }
2324
+ }
2325
+ if (isPredicate && soleParam !== undefined && predicateToken !== undefined) {
2101
2326
  return `${name}(${params}): ${safeParamName(soleParam.name, 0)} is ${mapType(predicateToken)};`;
2102
2327
  }
2103
2328
  return `${name}(${params}): ${ret.type};${ret.trailing}`;
@@ -2108,9 +2333,11 @@ function emitFunction(
2108
2333
  name: string,
2109
2334
  mapType: (t: string) => string,
2110
2335
  resolver: TableDocResolver,
2336
+ constantTokens: ConstantSlotTokens,
2111
2337
  urlParameters: UrlParameterTable,
2338
+ slotSink?: Record<string, string>,
2112
2339
  ): string {
2113
- return `function ${memberSignature(prepared, name, mapType, resolver, urlParameters)}`;
2340
+ return `function ${memberSignature(prepared, name, mapType, resolver, constantTokens, urlParameters, slotSink)}`;
2114
2341
  }
2115
2342
 
2116
2343
  // A colon-method member of a handle interface: identical signature machinery to a
@@ -2120,9 +2347,19 @@ function emitMethod(
2120
2347
  prepared: PreparedFunction,
2121
2348
  mapType: (t: string) => string,
2122
2349
  resolver: TableDocResolver,
2350
+ constantTokens: ConstantSlotTokens,
2123
2351
  urlParameters: UrlParameterTable,
2352
+ slotSink?: Record<string, string>,
2124
2353
  ): string {
2125
- return memberSignature(prepared, prepared.name, mapType, resolver, urlParameters);
2354
+ return memberSignature(
2355
+ prepared,
2356
+ prepared.name,
2357
+ mapType,
2358
+ resolver,
2359
+ constantTokens,
2360
+ urlParameters,
2361
+ slotSink,
2362
+ );
2126
2363
  }
2127
2364
 
2128
2365
  // Build the indented JSDoc lines for a function from its ref-doc prose. The
@@ -2238,6 +2475,7 @@ function parameterType(
2238
2475
  p: ApiParameter,
2239
2476
  mapType: (t: string) => string,
2240
2477
  resolver: TableDocResolver,
2478
+ constantTokens: ConstantSlotTokens,
2241
2479
  elementName: string,
2242
2480
  urlParameters: UrlParameterTable,
2243
2481
  ): string {
@@ -2247,13 +2485,25 @@ function parameterType(
2247
2485
  const alias = SCENE_ADDRESS_ALIASES[classifyUrlParameter(urlParameters, elementName, p.name)];
2248
2486
  const slotMapType = alias === undefined ? mapType : addressMapType(mapType, alias);
2249
2487
  return concrete.length > 0
2250
- ? mapSlotUnion(concrete, p.doc, slotMapType, true, resolver, elementName, "param", p.name)
2488
+ ? mapSlotUnion(
2489
+ concrete,
2490
+ p.doc,
2491
+ slotMapType,
2492
+ true,
2493
+ resolver,
2494
+ constantTokens,
2495
+ elementName,
2496
+ "param",
2497
+ p.name,
2498
+ )
2251
2499
  : "unknown";
2252
2500
  }
2253
2501
 
2254
2502
  const VARARG_PREFIX = "...";
2255
2503
 
2256
- function isVarargParameter(p: ApiParameter): boolean {
2504
+ // Exported because the fidelity audit must ask the same question about the
2505
+ // same input: what the emitted declaration accepts is the emitter's to decide.
2506
+ export function isVarargParameter(p: ApiParameter): boolean {
2257
2507
  return p.name.startsWith(VARARG_PREFIX);
2258
2508
  }
2259
2509
 
@@ -2277,21 +2527,32 @@ function emitRestParameter(
2277
2527
  varargIndex: number,
2278
2528
  mapType: (t: string) => string,
2279
2529
  resolver: TableDocResolver,
2530
+ constantTokens: ConstantSlotTokens,
2280
2531
  elementName: string,
2281
2532
  urlParameters: UrlParameterTable,
2282
- ): string {
2533
+ ): { text: string; slots: EmittedSlot[] } {
2283
2534
  const vararg = params[varargIndex];
2284
- if (vararg === undefined) return "";
2285
- const members = [
2286
- ...new Set(
2287
- params
2288
- .slice(varargIndex)
2289
- .map((p) => parameterType(p, mapType, resolver, elementName, urlParameters)),
2290
- ),
2291
- ];
2535
+ if (vararg === undefined) return { text: "", slots: [] };
2536
+ const folded = params.slice(varargIndex).map((p, offset) => ({
2537
+ position: varargIndex + offset,
2538
+ name: p.name,
2539
+ ts: parameterType(p, mapType, resolver, constantTokens, elementName, urlParameters),
2540
+ }));
2541
+ const members = [...new Set(folded.map((slot) => slot.ts))];
2292
2542
  const first = members[0] ?? "unknown";
2293
2543
  const element = members.length > 1 ? `(${members.join(" | ")})` : first;
2294
- return `...${varargParamName(vararg.name, varargIndex)}: ${element}[]`;
2544
+ return {
2545
+ text: `...${varargParamName(vararg.name, varargIndex)}: ${element}[]`,
2546
+ slots: folded,
2547
+ };
2548
+ }
2549
+
2550
+ // A slot's rendered type paired with the position and raw ref-doc name that
2551
+ // identify it, so the signature text and the per-slot map come out of one call.
2552
+ interface EmittedSlot {
2553
+ readonly position: number;
2554
+ readonly name: string;
2555
+ readonly ts: string;
2295
2556
  }
2296
2557
 
2297
2558
  function emitParameter(
@@ -2300,41 +2561,62 @@ function emitParameter(
2300
2561
  optional: boolean,
2301
2562
  mapType: (t: string) => string,
2302
2563
  resolver: TableDocResolver,
2564
+ constantTokens: ConstantSlotTokens,
2303
2565
  elementName: string,
2304
2566
  urlParameters: UrlParameterTable,
2305
- ): string {
2567
+ ): { text: string; ts: string } {
2306
2568
  const name = safeParamName(p.name, index);
2307
- const ts = parameterType(p, mapType, resolver, elementName, urlParameters);
2569
+ const ts = parameterType(p, mapType, resolver, constantTokens, elementName, urlParameters);
2308
2570
  // An interior doc-optional param (a required param follows, so the trailing-`?`
2309
2571
  // projection cannot mark it) keeps its optionality as `| undefined` — TSTL
2310
2572
  // lowers `undefined` to `nil`, the faithful call. Trailing optionals keep the
2311
2573
  // `?` form; required params are untouched.
2312
2574
  const interiorOptional = !optional && isDocOptional(p, elementName) ? " | undefined" : "";
2313
- return `${name}${optional ? "?" : ""}: ${ts}${interiorOptional}`;
2575
+ return { text: `${name}${optional ? "?" : ""}: ${ts}${interiorOptional}`, ts };
2314
2576
  }
2315
2577
 
2316
2578
  function emitReturn(
2317
2579
  returnValues: ApiParameter[],
2318
2580
  mapType: (t: string) => string,
2319
2581
  resolver: TableDocResolver,
2582
+ constantTokens: ConstantSlotTokens,
2320
2583
  elementName: string,
2321
- ): { type: string; trailing: string } {
2584
+ ): { type: string; trailing: string; slots: EmittedSlot[] } {
2585
+ // An authored whole-return override is not a per-slot render, so it reports no
2586
+ // slots and leaves the consumer on its token-derived fallback.
2322
2587
  const override = RETURN_TYPE_OVERRIDES.get(elementName);
2323
- if (override !== undefined) return { type: override, trailing: "" };
2324
- if (returnValues.length === 0) return { type: "void", trailing: "" };
2588
+ if (override !== undefined) return { type: override, trailing: "", slots: [] };
2589
+ if (returnValues.length === 0) return { type: "void", trailing: "", slots: [] };
2325
2590
  if (returnValues.length > 1) {
2326
2591
  // Defold multi-returns are positional and always present; each slot maps
2327
2592
  // straight through (unknown when the doc lists no type) into a tuple that
2328
2593
  // typescript-to-lua erases to `local a, b = fn()`.
2329
- const slots = returnValues.map((rv) =>
2330
- rv.types.length > 0
2331
- ? mapSlotUnion(rv.types, rv.doc, mapType, false, resolver, elementName, "return", rv.name)
2332
- : "unknown",
2333
- );
2334
- return { type: `LuaMultiReturn<[${slots.join(", ")}]>`, trailing: "" };
2594
+ const emitted = returnValues.map((rv, index) => ({
2595
+ position: index,
2596
+ name: rv.name,
2597
+ ts:
2598
+ rv.types.length > 0
2599
+ ? mapSlotUnion(
2600
+ rv.types,
2601
+ rv.doc,
2602
+ mapType,
2603
+ false,
2604
+ resolver,
2605
+ constantTokens,
2606
+ elementName,
2607
+ "return",
2608
+ rv.name,
2609
+ )
2610
+ : "unknown",
2611
+ }));
2612
+ return {
2613
+ type: `LuaMultiReturn<[${emitted.map((slot) => slot.ts).join(", ")}]>`,
2614
+ trailing: "",
2615
+ slots: emitted,
2616
+ };
2335
2617
  }
2336
2618
  const first = returnValues[0];
2337
- if (!first) return { type: "void", trailing: "" };
2619
+ if (!first) return { type: "void", trailing: "", slots: [] };
2338
2620
  const ts =
2339
2621
  first.types.length > 0
2340
2622
  ? mapSlotUnion(
@@ -2343,12 +2625,13 @@ function emitReturn(
2343
2625
  mapType,
2344
2626
  false,
2345
2627
  resolver,
2628
+ constantTokens,
2346
2629
  elementName,
2347
2630
  "return",
2348
2631
  first.name,
2349
2632
  )
2350
2633
  : "unknown";
2351
- return { type: ts, trailing: "" };
2634
+ return { type: ts, trailing: "", slots: [{ position: 0, name: first.name, ts }] };
2352
2635
  }
2353
2636
 
2354
2637
  function emitPropertyMember(
@@ -2380,13 +2663,26 @@ function mapSlotUnion(
2380
2663
  mapType: (t: string) => string,
2381
2664
  optionalFields: boolean,
2382
2665
  resolver: TableDocResolver,
2666
+ constantTokens: ConstantSlotTokens,
2383
2667
  elementName: string,
2384
2668
  slotKind?: "param" | "return",
2385
2669
  slotName?: string,
2386
2670
  ): string {
2387
2671
  const mapped: string[] = [];
2388
2672
  const seen = new Set<string>();
2673
+ const push = (ts: string): void => {
2674
+ if (seen.has(ts)) return;
2675
+ seen.add(ts);
2676
+ mapped.push(ts);
2677
+ };
2389
2678
  for (const token of types) {
2679
+ if (token === "constant" && slotKind !== undefined && slotName !== undefined) {
2680
+ const expanded = constantTokens(elementName, slotKind, slotName, doc);
2681
+ if (expanded.length > 0) {
2682
+ for (const fqn of expanded) push(mapType(fqn));
2683
+ continue;
2684
+ }
2685
+ }
2390
2686
  let ts: string;
2391
2687
  if (token === "table") {
2392
2688
  const curation =
@@ -2413,7 +2709,14 @@ function mapSlotUnion(
2413
2709
  const nested = mapping.value as NestedMapping;
2414
2710
  value = `LuaMap<${mapType(nested.key)}, ${mapType(nested.value)}>`;
2415
2711
  }
2416
- ts = `LuaMap<${mapType(mapping.key)}, ${value}>`;
2712
+ // A mapping key may be a `A | B` union token too (render.clear's three
2713
+ // documented graphics.BUFFER_TYPE_* constants); split it the same way
2714
+ // the value above is split. A single-token key (no `|`) is unaffected.
2715
+ const key = unionFromTokens(
2716
+ mapping.key.split("|").map((token) => token.trim()),
2717
+ mapType,
2718
+ );
2719
+ ts = `LuaMap<${key}, ${value}>`;
2417
2720
  } else if (element !== undefined) {
2418
2721
  ts = arrayTypeFromTokens(element, mapType);
2419
2722
  } else if (curation?.kind === "object" || curation?.kind === "array-object") {
@@ -2457,9 +2760,7 @@ function mapSlotUnion(
2457
2760
  : undefined;
2458
2761
  ts = curated ?? mapType(token);
2459
2762
  }
2460
- if (seen.has(ts)) continue;
2461
- seen.add(ts);
2462
- mapped.push(ts);
2763
+ push(ts);
2463
2764
  }
2464
2765
  // A genuine `unknown` member (an `any` slot, including `any | nil`) absorbs
2465
2766
  // every other union member, so collapse to exactly `unknown` rather than emit