@runtime-type-inspector/transpiler 4.0.6 → 5.0.1

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.
package/index.mjs CHANGED
@@ -1,6 +1,52 @@
1
1
  import { parse } from '@babel/parser';
2
2
  import ts from 'typescript';
3
3
 
4
+ /**
5
+ * @typedef DocType
6
+ * @property {boolean} optional - Type is optional.
7
+ */
8
+ /**
9
+ * Annotates a type with optionality for internal use (parseJSDoc navigation).
10
+ * Sets optionality on object types and wraps bare strings that need to be
11
+ * expandable containers (e.g. 'object', 'object[]', 'union') into objects
12
+ * so that parseJSDoc can later navigate into their properties.
13
+ * Does NOT recurse into compound types or delete properties — those are
14
+ * kept fully expanded so that parseJSDoc and JSDocAnnotator can still
15
+ * mutate them (e.g. appending nested @param lines).
16
+ * Literal types (numbers/booleans) pass through bare unless optional,
17
+ * mirroring the string path.
18
+ * @param {string | number | boolean | DocType} type - The type.
19
+ * @param {boolean} optional - Optionality
20
+ * @returns {string | number | boolean | DocType} The annotated type.
21
+ */
22
+ function annotateOptional(type, optional) {
23
+ // If it's already an object, just set optionality.
24
+ if (type instanceof Object) {
25
+ type.optional = optional;
26
+ } else if (typeof type === 'number' || typeof type === 'boolean') {
27
+ if (!optional) {
28
+ return type;
29
+ }
30
+ type = {
31
+ type,
32
+ optional
33
+ };
34
+ } else if (typeof type === 'string') {
35
+ type = type.trim();
36
+ if (type !== 'object' && type !== 'object[]' && type !== 'union' && !optional) {
37
+ return type;
38
+ }
39
+ type = {
40
+ type,
41
+ optional
42
+ };
43
+ } else {
44
+ debugger;
45
+ console.warn("annotateOptional> neither object nor string for type", type);
46
+ }
47
+ return type;
48
+ }
49
+
4
50
  /**
5
51
  * Transforms a type string into a structured type representation.
6
52
  *
@@ -157,6 +203,10 @@ function toSourceTS(node) {
157
203
  // parseType('keyof typeof obj' ).kind === ts.SyntaxKind.TypeOperator
158
204
  KeyOfKeyword,
159
205
  // parseType('keyof typeof obj' ).operator === ts.SyntaxKind.KeyOfKeyword
206
+ ReadonlyKeyword,
207
+ // parseType('readonly number[]' ).operator === ts.SyntaxKind.ReadonlyKeyword
208
+ UniqueKeyword,
209
+ // parseType('unique symbol' ).operator === ts.SyntaxKind.UniqueKeyword
160
210
  ConstructorType,
161
211
  // parseType('new (...args: any[]) => any' ).kind === ts.SyntaxKind.ConstructorType
162
212
  NamedTupleMember,
@@ -165,7 +215,11 @@ function toSourceTS(node) {
165
215
  // parseType('{[K in TaskType]: 123}' ).kind === ts.SyntaxKind.MappedType
166
216
  TypeParameter,
167
217
  // parseType('{[K in TaskType]: 123}' ).typeParameter.kind === ts.SyntaxKind.TypeParameter
168
- QualifiedName // parseType("import('abc').x.y" ).qualifier.kind === ts.SyntaxKind.QualifiedName
218
+ QualifiedName,
219
+ // parseType("import('abc').x.y" ).qualifier.kind === ts.SyntaxKind.QualifiedName
220
+ TemplateLiteralType,
221
+ // parseType('`${A}_id`' ).kind === ts.SyntaxKind.TemplateLiteralType
222
+ NoSubstitutionTemplateLiteral // parseType('`id`' ).literal.kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral
169
223
  } = ts.SyntaxKind;
170
224
  // console.log({typeArguments, typeName, kind_, node});
171
225
  switch (node.kind) {
@@ -328,9 +382,15 @@ function toSourceTS(node) {
328
382
  argument: _argument
329
383
  };
330
384
  }
385
+ if (node.operator === ReadonlyKeyword) {
386
+ // readonly erased at runtime, same shape as the inner type.
387
+ return toSourceTS(node.type);
388
+ }
331
389
  console.warn("unimplemented TypeOperator", node);
390
+ return 'any';
332
391
  case TypeReference:
333
392
  {
393
+ var _typeName$text;
334
394
  if (!ts.isTypeReferenceNode(node)) {
335
395
  throw Error("Impossible");
336
396
  }
@@ -381,7 +441,8 @@ function toSourceTS(node) {
381
441
  if (!typeArguments) {
382
442
  return typeName.getText();
383
443
  }
384
- const _name = typeName.text;
444
+ // Qualified names (e.g. `some.name.space.Array<T>`) have no `.text`.
445
+ const _name = (_typeName$text = typeName.text) != null ? _typeName$text : typeName.getText();
385
446
  const args = typeArguments.map(toSourceTS);
386
447
  return {
387
448
  type: 'reference',
@@ -390,10 +451,25 @@ function toSourceTS(node) {
390
451
  };
391
452
  }
392
453
  case NamedTupleMember:
393
- if (!ts.isNamedTupleMember(node)) {
394
- throw Error("Impossible");
454
+ {
455
+ if (!ts.isNamedTupleMember(node)) {
456
+ throw Error("Impossible");
457
+ }
458
+ const nName = toSourceTS(node.name);
459
+ const nType = toSourceTS(node.type);
460
+ const opt = !!node.questionToken;
461
+ const dot = !!node.dotDotDotToken;
462
+ // Preserve label for docs: [a: string, b: number] -> keep name
463
+ // Use tupleMember shape so stringify can round-trip
464
+ const mem = {
465
+ type: 'tupleMember',
466
+ name: nName,
467
+ elementType: nType
468
+ };
469
+ if (opt) mem.optional = true;
470
+ if (dot) mem.dotDot = true;
471
+ return mem;
395
472
  }
396
- return toSourceTS(node.type);
397
473
  case IntersectionType:
398
474
  {
399
475
  if (!ts.isIntersectionTypeNode(node)) {
@@ -414,6 +490,34 @@ function toSourceTS(node) {
414
490
  type: 'tuple',
415
491
  elements
416
492
  };
493
+ case TemplateLiteralType:
494
+ {
495
+ if (!ts.isTemplateLiteralTypeNode(node)) {
496
+ throw Error("Impossible");
497
+ }
498
+ // A template literal type is a sequence of literal chunks (quasis) with
499
+ // type expressions (types) in between, e.g. `${A}_${B}` becomes:
500
+ // {quasis: ['', '_', ''], types: [<A>, <B>]}
501
+ const quasis = [node.head.text];
502
+ /** @type {any[]} */
503
+ const types = [];
504
+ for (const span of node.templateSpans) {
505
+ types.push(toSourceTS(span.type));
506
+ quasis.push(span.literal.text);
507
+ }
508
+ return {
509
+ type: 'templateLiteral',
510
+ quasis,
511
+ types
512
+ };
513
+ }
514
+ case NoSubstitutionTemplateLiteral:
515
+ // A template literal type without interpolations, e.g. `id`
516
+ return {
517
+ type: 'templateLiteral',
518
+ quasis: [node.text],
519
+ types: []
520
+ };
417
521
  case UnionType:
418
522
  if (!ts.isUnionTypeNode(node)) {
419
523
  throw Error("Impossible");
@@ -441,7 +545,13 @@ function toSourceTS(node) {
441
545
  throw Error("Impossible");
442
546
  }
443
547
  const name = toSourceTS(member.name);
444
- const type = toSourceTS(member.type);
548
+ let type = toSourceTS(member.type);
549
+ if (member.questionToken) {
550
+ if (type && typeof type === 'object') type.optional = true;else type = {
551
+ type,
552
+ optional: true
553
+ };
554
+ }
445
555
  properties[name] = type;
446
556
  } else {
447
557
  console.warn('TypeLiteral: unhandled member', member);
@@ -579,6 +689,71 @@ function toSourceTS(node) {
579
689
  * @property {(object | string)[]} [elements] - For tuples.
580
690
  * @property {object | string} [argument] - For typeof.
581
691
  */
692
+ /**
693
+ * Splits a string by a delimiter, ignoring delimiters nested inside <>, {}, [], ().
694
+ * @param {string} str - The string to split.
695
+ * @param {string} delimiter - Single character delimiter.
696
+ * @returns {string[]} Top-level split parts.
697
+ */
698
+ function splitTopLevel(str, delimiter) {
699
+ const parts = [];
700
+ let depthAngle = 0;
701
+ let depthCurly = 0;
702
+ let depthSquare = 0;
703
+ let depthParen = 0;
704
+ let current = '';
705
+ for (const c of str) {
706
+ if (c === '<') depthAngle++;else if (c === '>') depthAngle--;else if (c === '{') depthCurly++;else if (c === '}') depthCurly--;else if (c === '[') depthSquare++;else if (c === ']') depthSquare--;else if (c === '(') depthParen++;else if (c === ')') depthParen--;
707
+ if (c === delimiter && depthAngle === 0 && depthCurly === 0 && depthSquare === 0 && depthParen === 0) {
708
+ parts.push(current);
709
+ current = '';
710
+ } else {
711
+ current += c;
712
+ }
713
+ }
714
+ parts.push(current);
715
+ return parts;
716
+ }
717
+ /**
718
+ * Parses `Name<A, B>` into name + raw arg strings, respecting nested brackets.
719
+ * Returns undefined when input isn't a generic reference.
720
+ * @param {string} type - Trimmed type string.
721
+ * @returns {{name: string, args: string[]}|undefined} Parsed generic reference.
722
+ */
723
+ function parseGenericReference(type) {
724
+ const openIndex = type.indexOf('<');
725
+ if (openIndex === -1 || !type.endsWith('>')) {
726
+ return;
727
+ }
728
+ const name = type.slice(0, openIndex).trim();
729
+ if (!/^[A-Za-z_$][A-Za-z0-9_$.]*$/.test(name)) {
730
+ return;
731
+ }
732
+ // Find matching '>' for the first '<' to ensure outermost brackets wrap the whole type.
733
+ let depth = 0;
734
+ let closeIndex = -1;
735
+ for (let i = openIndex; i < type.length; i++) {
736
+ if (type[i] === '<') depth++;else if (type[i] === '>') {
737
+ depth--;
738
+ if (depth === 0) {
739
+ closeIndex = i;
740
+ break;
741
+ }
742
+ }
743
+ }
744
+ if (closeIndex !== type.length - 1) {
745
+ return;
746
+ }
747
+ const inner = type.slice(openIndex + 1, closeIndex);
748
+ const args = splitTopLevel(inner, ',').map(_ => _.trim()).filter(_ => _.length);
749
+ if (!args.length) {
750
+ return;
751
+ }
752
+ return {
753
+ name,
754
+ args
755
+ };
756
+ }
582
757
  /**
583
758
  * 'DepFree' refers to the fact that this function has no dependencies,
584
759
  * while `expandType` depends on TypeScript itself for maximum compatibility.
@@ -594,6 +769,26 @@ function toSourceTS(node) {
594
769
  */
595
770
  function expandTypeDepFree(type) {
596
771
  type = type.trim();
772
+ // JSDocNullableType (`T?` / `?T`): union with null, matching expandType().
773
+ if (type.endsWith('?') && type.length > 1) {
774
+ return {
775
+ type: 'union',
776
+ members: [expandTypeDepFree(type.slice(0, -1).trim()), 'null']
777
+ };
778
+ }
779
+ if (type.startsWith('?') && type.length > 1) {
780
+ return {
781
+ type: 'union',
782
+ members: [expandTypeDepFree(type.slice(1).trim()), 'null']
783
+ };
784
+ }
785
+ // `readonly T` erased at runtime, same shape as the inner type.
786
+ if (type.startsWith('readonly ') && type.length > 9) {
787
+ return expandTypeDepFree(type.slice(9).trim());
788
+ }
789
+ if (type === 'unique symbol') {
790
+ return 'any';
791
+ }
597
792
  // '(123)' -> '123'
598
793
  while (!type.includes('|') && type[0] === '(' && type[type.length - 1] === ')') {
599
794
  type = type.slice(1, -1).trim();
@@ -640,6 +835,39 @@ function expandTypeDepFree(type) {
640
835
  val: expandTypeDepFree(val)
641
836
  };
642
837
  }
838
+ // (3b) Map<...> / Set<...' for dep-free parity with expandType()
839
+ if (type.startsWith("Map<") && type.endsWith('>')) {
840
+ const inner = type.slice(4, -1);
841
+ const parts = splitTopLevel(inner, ',');
842
+ if (parts.length === 2) {
843
+ return {
844
+ type: "map",
845
+ key: expandTypeDepFree(parts[0].trim()),
846
+ val: expandTypeDepFree(parts[1].trim())
847
+ };
848
+ }
849
+ }
850
+ if (type.startsWith("Set<") && type.endsWith('>')) {
851
+ const inner = type.slice(4, -1);
852
+ return {
853
+ type: "set",
854
+ elementType: expandTypeDepFree(inner.trim())
855
+ };
856
+ }
857
+ // (3c) Generic reference types like ArrayLike<T>, ReadonlyArray<T> etc.
858
+ // Keep structured so the runtime can validate them instead of warning 'unchecked'.
859
+ const genericRef = parseGenericReference(type);
860
+ if (genericRef) {
861
+ const {
862
+ name,
863
+ args
864
+ } = genericRef;
865
+ return {
866
+ type: 'reference',
867
+ name,
868
+ args: args.map(expandTypeDepFree)
869
+ };
870
+ }
643
871
  // (4) {...}
644
872
  if (type[0] === '{' && type[type.length - 1] === '}') {
645
873
  const propertiesArray = type.slice(1, -1).split(','); // ['entity: Entity', ' app: AppBase']
@@ -697,96 +925,302 @@ function expandTypeDepFree(type) {
697
925
  properties: {}
698
926
  };
699
927
  }
928
+ // Literal normalization for parity with expandType():
929
+ // numeric literals become numbers, true/false become booleans.
930
+ if (type === 'true') {
931
+ return true;
932
+ }
933
+ if (type === 'false') {
934
+ return false;
935
+ }
936
+ if (/^-?\d+(\.\d+)?([eE][+-]?\d+)?$/.test(type)) {
937
+ return Number(type);
938
+ }
700
939
  return type;
701
940
  }
702
941
 
703
- /** @typedef {import('@babel/types').Node} Node */
704
- /** @typedef {import('@babel/types').Function} Function */
705
942
  /**
706
- * Checks if the provided node is a function-like structure.
707
- *
708
- * @param {Node} node - The Babel AST node to be tested.
709
- * @returns {node is Function} - `true` if the node is a function-like structure, otherwise `false`.
943
+ * Infers a parameter type from its default value AST node.
944
+ * Returns widened types like TypeScript does (`= 0` means `number`, not
945
+ * literal `0`; `= null` widens to `any`).
946
+ * Returns `undefined` when nothing useful can be inferred — the caller then
947
+ * emits no check, exactly like an undocumented parameter today.
948
+ * Shapes match what `expandType` produces so they can be embedded as-is.
949
+ * @param {import('@babel/types').Node} node - The default value AST node.
950
+ * @returns {string | object | undefined} Inferred type or `undefined` to skip.
710
951
  */
711
- function nodeIsFunction(node) {
952
+ function inferTypeFromDefault$1(node) {
953
+ if (!node) {
954
+ return;
955
+ }
712
956
  switch (node.type) {
957
+ case 'NumericLiteral':
958
+ return 'number';
959
+ case 'StringLiteral':
960
+ return 'string';
961
+ case 'BooleanLiteral':
962
+ return 'boolean';
963
+ case 'BigIntLiteral':
964
+ return {
965
+ type: 'bigint'
966
+ };
967
+ case 'RegExpLiteral':
968
+ return 'RegExp';
969
+ case 'TemplateLiteral':
970
+ return 'string';
971
+ case 'ArrayExpression':
972
+ return {
973
+ type: 'array',
974
+ elementType: 'any'
975
+ };
976
+ case 'ObjectExpression':
977
+ return {
978
+ type: 'object',
979
+ properties: {}
980
+ };
713
981
  case 'ArrowFunctionExpression':
714
- case 'ClassMethod':
715
- case 'ClassPrivateMethod':
716
- case 'FunctionDeclaration':
717
982
  case 'FunctionExpression':
718
- case 'ObjectMethod':
719
- return true;
983
+ return 'Function';
984
+ case 'NewExpression':
985
+ {
986
+ const {
987
+ callee
988
+ } = node;
989
+ if (callee.type === 'Identifier') {
990
+ return callee.name;
991
+ }
992
+ break;
993
+ }
994
+ case 'UnaryExpression':
995
+ {
996
+ const {
997
+ operator,
998
+ argument
999
+ } = node;
1000
+ if (operator === '!') {
1001
+ return 'boolean';
1002
+ }
1003
+ if (operator === 'void' || operator === 'typeof') {
1004
+ return operator === 'void' ? 'undefined' : 'string';
1005
+ }
1006
+ if ((operator === '-' || operator === '+') && argument.type === 'NumericLiteral') {
1007
+ return 'number';
1008
+ }
1009
+ if ((operator === '-' || operator === '+') && argument.type === 'BigIntLiteral') {
1010
+ return {
1011
+ type: 'bigint'
1012
+ };
1013
+ }
1014
+ break;
1015
+ }
720
1016
  }
721
- return false;
722
1017
  }
723
1018
 
724
1019
  /**
725
- * @typedef DocType
726
- * @property {boolean} optional - Type is optional.
727
- */
728
- /**
729
- * @param {string | DocType} type - The type.
730
- * @param {boolean} optional - Optionality
731
- * @returns {string | DocType} The simplified type.
1020
+ * Extracts the parameter name and its optionality from a JSDoc parameter string.
1021
+ *
1022
+ * This function takes a rest parameter string from a JSDoc comment, trims it, and determines the parameter's
1023
+ * name and whether it is optional. The optionality is inferred based on the presence of square brackets around
1024
+ * the parameter name.
1025
+ *
1026
+ * @param {string} rest - The rest part of a JSDoc parameter string to parse.
1027
+ * @returns {[string, boolean]} A tuple where the first element is the name of the parameter,
1028
+ * and the second element is a boolean indicating if the parameter is optional.
732
1029
  */
733
- function simplifyType(type, optional) {
734
- // If it's already an object, just set optionality.
735
- if (type instanceof Object) {
736
- type.optional = optional;
737
- } else if (typeof type === 'string') {
738
- type = type.trim();
739
- if (type !== 'object' && type !== 'object[]' && type !== 'union' && !optional) {
740
- // console.log("simplify", type);
741
- return type;
742
- }
743
- type = {
744
- type,
745
- optional
746
- };
747
- } else {
748
- debugger;
749
- console.warn("simplifyType> neither object nor string for type", type);
750
- }
751
- if (type.type === 'object' && type.properties && Object.keys(type.properties).length === 0) {
752
- delete type.properties;
753
- // console.log("delete empty", type);
1030
+ function extractNameAndOptionality(rest) {
1031
+ rest = rest.trim();
1032
+ let optional = false;
1033
+ // Examples:
1034
+ // name: [kwargs={}] The configuration parameters.
1035
+ // name: [d = 1.0] Sample spacing
1036
+ if (rest[0] === '[') {
1037
+ // Possible improvement: counting opening/closing brackets for perfect match
1038
+ const closer = rest.lastIndexOf(']');
1039
+ // Afterwards name will be: d = 1.0
1040
+ rest = rest.substring(1, closer);
1041
+ // mark it for the type:
1042
+ optional = true;
754
1043
  }
755
-
756
- return type;
1044
+ // Strip the rest (either leftover of optional value or description)
1045
+ const name = rest.split(' ')[0].split('=')[0].trim();
1046
+ return [name, optional];
757
1047
  }
758
1048
 
759
1049
  /**
760
- * @typedef {ReturnType<typeof parseJSDoc>} ParseJSDocReturnType
761
- */
762
- /**
763
- * @typedef {typeof expandTypeDepFree} ExpandType
764
- * @typedef {ReturnType<ExpandType>} ExpandTypeReturnType
1050
+ * Extracts the content of a string that is delimited by curly braces.
1051
+ * @example
1052
+ * extractCurlyContent('{ {inner} }'); // Returns: {content: ' {inner} ', nextIndex: 11}
1053
+ * @param {string} line - The string to extract from.
1054
+ * @returns {{content: string, nextIndex: number}} An object containing the extracted content,
1055
+ * and the index of the character immediately following the closing curly brace.
765
1056
  */
1057
+ function extractCurlyContent(line) {
1058
+ const firstCurly = line.indexOf('{');
1059
+ let k = firstCurly + 1;
1060
+ let count = 0;
1061
+ for (; k < line.length; k++) {
1062
+ const c = line[k];
1063
+ if (c === '{') {
1064
+ count++;
1065
+ } else if (c === '}') {
1066
+ count--;
1067
+ }
1068
+ if (count === -1) {
1069
+ break;
1070
+ }
1071
+ }
1072
+ const content = line.substring(firstCurly + 1, k);
1073
+ return {
1074
+ content,
1075
+ nextIndex: k + 1
1076
+ };
1077
+ }
766
1078
  /**
767
- * Parses JSDoc comments to extract parameter type information.
1079
+ * Parses JSDoc comments to extract and expand typedefs and their associated properties.
768
1080
  *
769
- * @param {string} src - The JSDoc comment string to parse.
770
- * @param {ExpandType} [expandType] - An optional function to process the types found in the JSDoc.
771
- * @returns {Record<string, ExpandTypeReturnType> | undefined} An object mapping parameter names to their parsed types, or undefined if no parameters are found.
1081
+ * It iterates through the lines of a `CommentBlock` from the Babel AST, looking for `@typedef` and `@property`
1082
+ * annotations. When it finds a typedef, it stores it in the `typedefs` record. When it finds a property,
1083
+ * it adds it to the last found typedef if it is an object type.
1084
+ * @param {Record<string, object>} typedefs - An object to store typedefs, mapping type names to their expanded definitions.
1085
+ * @param {Console["warn"]} warn - A warn function used for emitting warnings about non-extensible types.
1086
+ * @param {import("@babel/types").Comment} comment - A comment extracted from Babel's AST, expected to be a CommentBlock containing type definitions.
1087
+ * @param {Function} expandType - A function that takes a type expression as a string and returns a structured representation of the type.
772
1088
  */
773
- function parseJSDoc(src, expandType = expandTypeDepFree) {
774
- // Parse something like: @param {Object} [kwargs={}] Optional arguments.
775
- const regex = /@param \{(.*?)\} ([\[\]a-zA-Z0-9_$=\-\{\}\.'" ]+)/g;
776
- const matches = [...src.matchAll(regex)];
777
- /** @type {Record<string, ExpandTypeReturnType>} */
778
- const params = Object.create(null);
779
- matches.forEach(_ => {
780
- const type = expandType(_[1].trim());
781
- let name = _[2].trim();
782
- let optional = false;
783
- // Examples:
784
- // name: [kwargs={}] The configuration parameters.
785
- // name: [d = 1.0] Sample spacing
786
- if (name[0] === '[') {
787
- // Counting opening/closing brackets for perfect match
788
- let openCloseCount = 1;
789
- let i = 1;
1089
+ function parseJSDocTypedef(typedefs, warn, comment, expandType) {
1090
+ const {
1091
+ type,
1092
+ value
1093
+ } = comment;
1094
+ if (type !== 'CommentBlock') {
1095
+ return;
1096
+ }
1097
+ const lines = value.split('\n');
1098
+ let lastTypedef;
1099
+ for (let line of lines) {
1100
+ line = line.trim();
1101
+ if (line[0] === '*') {
1102
+ line = line.slice(1).trim();
1103
+ }
1104
+ if (line.startsWith('@typedef')) {
1105
+ const {
1106
+ content: def,
1107
+ nextIndex
1108
+ } = extractCurlyContent(line);
1109
+ let name = line.substring(nextIndex).trim();
1110
+ // Drop description
1111
+ name = name.split(' ')[0];
1112
+ lastTypedef = expandType(def);
1113
+ // Ignore @typedef's that only refer to themselves in another file (see typedef-overwrite test)
1114
+ if (lastTypedef !== name) {
1115
+ typedefs[name] = lastTypedef;
1116
+ }
1117
+ } else if (line.startsWith('@property')) {
1118
+ var _lastTypedef;
1119
+ // class @property
1120
+ if (!lastTypedef) {
1121
+ continue;
1122
+ }
1123
+ const {
1124
+ content,
1125
+ nextIndex
1126
+ } = extractCurlyContent(line);
1127
+ const rest = line.substring(nextIndex);
1128
+ const propType = expandType(content);
1129
+ const [name, optional] = extractNameAndOptionality(rest);
1130
+ // console.log({name, optional, propType});
1131
+ const finalType = annotateOptional(propType, optional);
1132
+ if (((_lastTypedef = lastTypedef) == null ? void 0 : _lastTypedef.type) === 'object') {
1133
+ lastTypedef.properties[name] = finalType;
1134
+ } else {
1135
+ warn("not an extensible type", lastTypedef);
1136
+ }
1137
+ } else if (line.startsWith('@callback')) {
1138
+ const name = line.substring(9).trim();
1139
+ typedefs[name] = 'Function';
1140
+ }
1141
+ }
1142
+ }
1143
+
1144
+ /**
1145
+ * Extracts an inline `/** @type {X} *\/` annotation from parameter comments,
1146
+ * e.g. `function add(/** @type {number} *\/ a) {...}`.
1147
+ * @param {Array<{value: string}>|undefined} leadingComments - Leading comments of a param node.
1148
+ * @param {Function} expandType - Function expanding a type string.
1149
+ * @returns {any} Expanded type or `undefined` when no inline `@type` found.
1150
+ */
1151
+ function parseInlineParamType(leadingComments, expandType) {
1152
+ if (!Array.isArray(leadingComments)) {
1153
+ return;
1154
+ }
1155
+ for (let i = leadingComments.length - 1; i >= 0; i--) {
1156
+ const {
1157
+ value
1158
+ } = leadingComments[i];
1159
+ if (!value || !value.includes('@type')) {
1160
+ continue;
1161
+ }
1162
+ const {
1163
+ content
1164
+ } = extractCurlyContent(value.slice(value.indexOf('@type')));
1165
+ if (!content || !content.trim()) {
1166
+ continue;
1167
+ }
1168
+ return expandType(content.trim());
1169
+ }
1170
+ }
1171
+
1172
+ /** @typedef {import('@babel/types').Node} Node */
1173
+ /** @typedef {import('@babel/types').Function} Function */
1174
+ /**
1175
+ * Checks if the provided node is a function-like structure.
1176
+ *
1177
+ * @param {Node} node - The Babel AST node to be tested.
1178
+ * @returns {node is Function} - `true` if the node is a function-like structure, otherwise `false`.
1179
+ */
1180
+ function nodeIsFunctionLike(node) {
1181
+ switch (node.type) {
1182
+ case 'ArrowFunctionExpression':
1183
+ case 'ClassMethod':
1184
+ case 'ClassPrivateMethod':
1185
+ case 'FunctionDeclaration':
1186
+ case 'FunctionExpression':
1187
+ case 'ObjectMethod':
1188
+ return true;
1189
+ }
1190
+ return false;
1191
+ }
1192
+
1193
+ /**
1194
+ * @typedef {ReturnType<typeof parseJSDoc>} ParseJSDocReturnType
1195
+ */
1196
+ /**
1197
+ * @typedef {typeof expandTypeDepFree} ExpandType
1198
+ * @typedef {ReturnType<ExpandType>} ExpandTypeReturnType
1199
+ */
1200
+ /**
1201
+ * Parses JSDoc comments to extract parameter type information.
1202
+ *
1203
+ * @param {string} src - The JSDoc comment string to parse.
1204
+ * @param {ExpandType} [expandType] - An optional function to process the types found in the JSDoc.
1205
+ * @returns {Record<string, ExpandTypeReturnType> | undefined} An object mapping parameter names to their parsed types, or undefined if no parameters are found.
1206
+ */
1207
+ function parseJSDoc(src, expandType = expandTypeDepFree) {
1208
+ // Parse something like: @param {Object} [kwargs={}] Optional arguments.
1209
+ const regex = /@param \{(.*?)\} ([\[\]a-zA-Z0-9_$=\-\{\}\.'" ]+)/g;
1210
+ const matches = [...src.matchAll(regex)];
1211
+ /** @type {Record<string, ExpandTypeReturnType>} */
1212
+ const params = Object.create(null);
1213
+ matches.forEach(_ => {
1214
+ const type = expandType(_[1].trim());
1215
+ let name = _[2].trim();
1216
+ let optional = false;
1217
+ // Examples:
1218
+ // name: [kwargs={}] The configuration parameters.
1219
+ // name: [d = 1.0] Sample spacing
1220
+ if (name[0] === '[') {
1221
+ // Counting opening/closing brackets for perfect match
1222
+ let openCloseCount = 1;
1223
+ let i = 1;
790
1224
  for (; i < name.length; i++) {
791
1225
  const c = name[i];
792
1226
  if (c === '[') {
@@ -805,16 +1239,16 @@ function parseJSDoc(src, expandType = expandTypeDepFree) {
805
1239
  }
806
1240
  // Strip the rest (either leftover of optional value or description)
807
1241
  name = name.split(' ')[0].split('=')[0].trim();
808
- const simplifiedType = simplifyType(type, optional);
1242
+ const annotatedType = annotateOptional(type, optional);
809
1243
  // Turn "options.stats[].unitsName" into ['options', 'stats', 'unitsName'].
810
1244
  const parts = name.split(/[\[\]]*\./);
811
1245
  let properties = params;
812
1246
  for (const part of parts) {
813
1247
  const toptype = properties[part];
814
1248
  if (!toptype) {
815
- // No toptype means we resolved as far as possible, now we can add `simplifiedType`.
1249
+ // No toptype means we resolved as far as possible, now we can add `annotatedType`.
816
1250
  console.assert(part === parts.at(-1), 'Current part and last part should be the same.');
817
- properties[part] = simplifiedType;
1251
+ properties[part] = annotatedType;
818
1252
  } else if (toptype.type === "union") {
819
1253
  const typeObject = toptype.members.find(_ => (_ == null ? void 0 : _.type) === 'object');
820
1254
  properties = typeObject.properties;
@@ -828,7 +1262,7 @@ function parseJSDoc(src, expandType = expandTypeDepFree) {
828
1262
  src,
829
1263
  toptype,
830
1264
  parts,
831
- simplifiedType
1265
+ annotatedType
832
1266
  });
833
1267
  }
834
1268
  }
@@ -851,8 +1285,8 @@ function parseJSDocSetter(src, expandType = expandTypeDepFree) {
851
1285
  if (matches.length === 1) {
852
1286
  const match = matches[0];
853
1287
  const type = expandType(match[1]);
854
- const simplifiedType = simplifyType(type, /* optional */false);
855
- return simplifiedType;
1288
+ const annotatedType = annotateOptional(type, /* optional */false);
1289
+ return annotatedType;
856
1290
  }
857
1291
  }
858
1292
 
@@ -884,129 +1318,99 @@ function parseJSDocTemplates(src, expandType = expandTypeDepFree) {
884
1318
  return templates;
885
1319
  }
886
1320
 
1321
+ function _extends() {
1322
+ _extends = Object.assign ? Object.assign.bind() : function (target) {
1323
+ for (var i = 1; i < arguments.length; i++) {
1324
+ var source = arguments[i];
1325
+ for (var key in source) {
1326
+ if (Object.prototype.hasOwnProperty.call(source, key)) {
1327
+ target[key] = source[key];
1328
+ }
1329
+ }
1330
+ }
1331
+ return target;
1332
+ };
1333
+ return _extends.apply(this, arguments);
1334
+ }
1335
+
887
1336
  /**
888
- * Extracts the parameter name and its optionality from a JSDoc parameter string.
889
- *
890
- * This function takes a rest parameter string from a JSDoc comment, trims it, and determines the parameter's
891
- * name and whether it is optional. The optionality is inferred based on the presence of square brackets around
892
- * the parameter name.
893
- *
894
- * @param {string} rest - The rest part of a JSDoc parameter string to parse.
895
- * @returns {[string, boolean]} A tuple where the first element is the name of the parameter,
896
- * and the second element is a boolean indicating if the parameter is optional.
1337
+ * Returns a new object with each value mapped by `fn`, leaving the original
1338
+ * untouched. Returns `obj` as-is if it is falsy.
1339
+ * @param {Record<string, any>} obj - The source object.
1340
+ * @param {(value: any, key: string) => any} fn - Mapping function.
1341
+ * @returns {Record<string, any>} A new object with mapped values.
897
1342
  */
898
- function extractNameAndOptionality(rest) {
899
- rest = rest.trim();
900
- let optional = false;
901
- // Examples:
902
- // name: [kwargs={}] The configuration parameters.
903
- // name: [d = 1.0] Sample spacing
904
- if (rest[0] === '[') {
905
- // Possible improvement: counting opening/closing brackets for perfect match
906
- const closer = rest.lastIndexOf(']');
907
- // Afterwards name will be: d = 1.0
908
- rest = rest.substring(1, closer);
909
- // mark it for the type:
910
- optional = true;
1343
+ function mapValues(obj, fn) {
1344
+ if (!obj) return obj;
1345
+ const result = {};
1346
+ for (const key in obj) {
1347
+ result[key] = fn(obj[key], key);
911
1348
  }
912
- // Strip the rest (either leftover of optional value or description)
913
- const name = rest.split(' ')[0].split('=')[0].trim();
914
- return [name, optional];
1349
+ return result;
915
1350
  }
916
1351
 
917
1352
  /**
918
- * Extracts the content of a string that is delimited by curly braces.
919
- * @example
920
- * extractCurlyContent('{ {inner} }'); // Returns: {content: ' {inner} ', nextIndex: 11}
921
- * @param {string} line - The string to extract from.
922
- * @returns {{content: string, nextIndex: number}} An object containing the extracted content,
923
- * and the index of the character immediately following the closing curly brace.
1353
+ * @typedef DocType
1354
+ * @property {boolean} optional - Type is optional.
924
1355
  */
925
- function extractCurlyContent(line) {
926
- const firstCurly = line.indexOf('{');
927
- let k = firstCurly + 1;
928
- let count = 0;
929
- for (; k < line.length; k++) {
930
- const c = line[k];
931
- if (c === '{') {
932
- count++;
933
- } else if (c === '}') {
934
- count--;
935
- }
936
- if (count === -1) {
937
- break;
938
- }
939
- }
940
- const content = line.substring(firstCurly + 1, k);
941
- return {
942
- content,
943
- nextIndex: k + 1
944
- };
945
- }
946
1356
  /**
947
- * Parses JSDoc comments to extract and expand typedefs and their associated properties.
948
- *
949
- * It iterates through the lines of a `CommentBlock` from the Babel AST, looking for `@typedef` and `@property`
950
- * annotations. When it finds a typedef, it stores it in the `typedefs` record. When it finds a property,
951
- * it adds it to the last found typedef if it is an object type.
952
- * @param {Record<string, object>} typedefs - An object to store typedefs, mapping type names to their expanded definitions.
953
- * @param {Console["warn"]} warn - A warn function used for emitting warnings about non-extensible types.
954
- * @param {import("@babel/types").Comment} comment - A comment extracted from Babel's AST, expected to be a CommentBlock containing type definitions.
955
- * @param {Function} expandType - A function that takes a type expression as a string and returns a structured representation of the type.
1357
+ * Recursively clones and simplifies a type for emission into source code.
1358
+ * Strips empty `properties` and collapses empty `object` types to the bare
1359
+ * string `'object'`. Numbers/booleans (literal types) pass through.
1360
+ * Non-destructive — the original type tree is never mutated.
1361
+ * @param {string | DocType | number | boolean} type - The type.
1362
+ * @returns {string | DocType | number | boolean} The simplified type.
956
1363
  */
957
- function parseJSDocTypedef(typedefs, warn, comment, expandType) {
958
- const {
959
- type,
960
- value
961
- } = comment;
962
- if (type !== 'CommentBlock') {
963
- return;
1364
+ function simplifyType(type) {
1365
+ if (!(type instanceof Object)) {
1366
+ return type;
964
1367
  }
965
- const lines = value.split('\n');
966
- let lastTypedef;
967
- for (let line of lines) {
968
- line = line.trim();
969
- if (line[0] === '*') {
970
- line = line.slice(1).trim();
971
- }
972
- if (line.startsWith('@typedef')) {
973
- const {
974
- content: def,
975
- nextIndex
976
- } = extractCurlyContent(line);
977
- let name = line.substring(nextIndex).trim();
978
- // Drop description
979
- name = name.split(' ')[0];
980
- lastTypedef = expandType(def);
981
- // Ignore @typedef's that only refer to themselves in another file (see typedef-overwrite test)
982
- if (lastTypedef !== name) {
983
- typedefs[name] = lastTypedef;
984
- }
985
- } else if (line.startsWith('@property')) {
986
- var _lastTypedef;
987
- // class @property
988
- if (!lastTypedef) {
989
- continue;
990
- }
991
- const {
992
- content,
993
- nextIndex
994
- } = extractCurlyContent(line);
995
- const rest = line.substring(nextIndex);
996
- const propType = expandType(content);
997
- const [name, optional] = extractNameAndOptionality(rest);
998
- // console.log({name, optional, propType});
999
- const finalType = simplifyType(propType, optional);
1000
- if (((_lastTypedef = lastTypedef) == null ? void 0 : _lastTypedef.type) === 'object') {
1001
- lastTypedef.properties[name] = finalType;
1002
- } else {
1003
- warn("not an extensible type", lastTypedef);
1004
- }
1005
- } else if (line.startsWith('@callback')) {
1006
- const name = line.substring(9).trim();
1007
- typedefs[name] = 'Function';
1368
+ const out = _extends({}, type);
1369
+ if (out.properties) {
1370
+ out.properties = mapValues(out.properties, simplifyType);
1371
+ if (out.type === 'object' && !Object.keys(out.properties).length) {
1372
+ delete out.properties;
1008
1373
  }
1009
1374
  }
1375
+ if (out.indexSignatures && Array.isArray(out.indexSignatures)) {
1376
+ out.indexSignatures = out.indexSignatures.map(simplifyType);
1377
+ }
1378
+ if (out.type === 'union' && out.members) {
1379
+ out.members = out.members.map(simplifyType);
1380
+ }
1381
+ if (out.type === 'array' && out.elementType) {
1382
+ out.elementType = simplifyType(out.elementType);
1383
+ }
1384
+ if (out.type === 'tuple' && out.elements) {
1385
+ out.elements = out.elements.map(simplifyType);
1386
+ }
1387
+ if (out.type === 'promise' && out.elementType) {
1388
+ out.elementType = simplifyType(out.elementType);
1389
+ }
1390
+ if (out.type === 'reference' && Array.isArray(out.args)) {
1391
+ out.args = out.args.map(simplifyType);
1392
+ }
1393
+ if (out.type === 'record') {
1394
+ if (out.key) out.key = simplifyType(out.key);
1395
+ if (out.val) out.val = simplifyType(out.val);
1396
+ }
1397
+ if (out.type === 'typeof' && out.argument) {
1398
+ out.argument = simplifyType(out.argument);
1399
+ }
1400
+ if (out.type === 'object' && !out.properties && !out.indexSignatures && !out.optional) {
1401
+ return 'object';
1402
+ }
1403
+ return out;
1404
+ }
1405
+
1406
+ /**
1407
+ * Returns the pretty-printed JSON of the simplified type for embedding
1408
+ * into generated source code.
1409
+ * @param {import('./simplifyType.js').DocType | string | number | boolean} type - The type.
1410
+ * @returns {string} JSON source ready for code generation.
1411
+ */
1412
+ function simplifyTypeToSource(type) {
1413
+ return JSON.stringify(simplifyType(type), null, 2);
1010
1414
  }
1011
1415
 
1012
1416
  /**
@@ -1345,7 +1749,8 @@ class Stringifier {
1345
1749
  return '';
1346
1750
  }
1347
1751
  /**
1348
- * @type {string} A string of two spaces per indentation.
1752
+ * A string of two spaces per indentation.
1753
+ * @type {string}
1349
1754
  */
1350
1755
  get spaces() {
1351
1756
  return ' '.repeat(this.numSpaces);
@@ -3111,6 +3516,9 @@ class Stringifier {
3111
3516
  * @typedef {object} Options
3112
3517
  * @property {boolean} [forceCurly] - Determines whether curly braces are enforced in Stringifier.
3113
3518
  * @property {boolean} [validateDivision] - Indicates whether division operations should be validated.
3519
+ * @property {boolean} [inspectIndexedAccess] - Indicates whether indexed accesses
3520
+ * like `arr[i]` should be wrapped for bounds and integer validation. Disable
3521
+ * to drop indexed access inspection entirely. Defaults to true.
3114
3522
  * @property {import('./parseJSDoc.js').ExpandType} [expandType] - A function that expands shorthand types into full descriptions.
3115
3523
  * @property {string} [filename] - The name of a file to which the instance pertains.
3116
3524
  * @property {boolean} [addHeader] - Whether to add import declarations headers. Defaults to true.
@@ -3123,6 +3531,7 @@ class Asserter extends Stringifier {
3123
3531
  constructor({
3124
3532
  forceCurly = true,
3125
3533
  validateDivision = true,
3534
+ inspectIndexedAccess = true,
3126
3535
  expandType = expandTypeDepFree,
3127
3536
  filename,
3128
3537
  addHeader = true,
@@ -3182,6 +3591,7 @@ class Asserter extends Stringifier {
3182
3591
  this.addLaterImportNamespaceSpecifier = [];
3183
3592
  this.forceCurly = forceCurly;
3184
3593
  this.validateDivision = validateDivision;
3594
+ this.inspectIndexedAccess = inspectIndexedAccess;
3185
3595
  // @todo collect every type + manually validate as test set
3186
3596
  // + implement expandType using Babel Flow type parser aswell
3187
3597
  this.expandType = expandType;
@@ -3271,7 +3681,7 @@ class Asserter extends Stringifier {
3271
3681
  return '';
3272
3682
  }
3273
3683
  let header = super.getHeader();
3274
- header += "import {inspectType, inspectTypeWithTemplates, youCanAddABreakpointHere, registerVariable";
3684
+ header += "import {inspectIndexedAccess, inspectType, inspectTypeWithTemplates, youCanAddABreakpointHere, registerVariable";
3275
3685
  if (this.validateDivision) {
3276
3686
  header += ", validateDivision";
3277
3687
  }
@@ -3308,10 +3718,21 @@ class Asserter extends Stringifier {
3308
3718
  //if (parent.type === 'CallExpression') {
3309
3719
  // break;
3310
3720
  //}
3311
- if (nodeIsFunction(parent)) {
3721
+ if (nodeIsFunctionLike(parent)) {
3312
3722
  break;
3313
3723
  }
3314
3724
  if (parent.leadingComments) {
3725
+ if (parent.type === 'VariableDeclaration') {
3726
+ const comments = parent.leadingComments;
3727
+ const lastComment = comments[comments.length - 1];
3728
+ const isStatementDoc = Boolean(parent.loc && (lastComment == null ? void 0 : lastComment.loc) && lastComment.loc.end.line + 1 === parent.loc.start.line);
3729
+ if (isStatementDoc) {
3730
+ const declarator = parents[i + 1];
3731
+ if (!declarator || declarator.type !== 'VariableDeclarator' || declarator.init !== node) {
3732
+ break;
3733
+ }
3734
+ }
3735
+ }
3315
3736
  return parent;
3316
3737
  }
3317
3738
  i--;
@@ -3346,10 +3767,24 @@ class Asserter extends Stringifier {
3346
3767
  //if (parent.type === 'CallExpression') {
3347
3768
  // break;
3348
3769
  //}
3349
- if (nodeIsFunction(parent)) {
3770
+ if (nodeIsFunctionLike(parent)) {
3350
3771
  break;
3351
3772
  }
3352
3773
  if (parent.leadingComments) {
3774
+ if (parent.type === 'VariableDeclaration') {
3775
+ const comments = parent.leadingComments;
3776
+ const lastComment = comments[comments.length - 1];
3777
+ const isStatementDoc = Boolean(parent.loc && (lastComment == null ? void 0 : lastComment.loc) && lastComment.loc.end.line + 1 === parent.loc.start.line);
3778
+ if (isStatementDoc) {
3779
+ // Docblock directly above the statement: TypeScript-like cascade,
3780
+ // only direct declarator initializers inherit it. Functions nested
3781
+ // deeper (e.g. inside object literals) do not.
3782
+ const declarator = parents[i + 1];
3783
+ if (!declarator || declarator.type !== 'VariableDeclarator' || declarator.init !== node) {
3784
+ break;
3785
+ }
3786
+ }
3787
+ }
3353
3788
  return parent;
3354
3789
  }
3355
3790
  i--;
@@ -3497,7 +3932,7 @@ class Asserter extends Stringifier {
3497
3932
  const {
3498
3933
  stats
3499
3934
  } = this;
3500
- const type = nodeIsFunction(node) ? node.type : this.parentType;
3935
+ const type = nodeIsFunctionLike(node) ? node.type : this.parentType;
3501
3936
  if (type === 'ClassMethod') {
3502
3937
  const parent = /** @type {ClassMethod} */
3503
3938
  this.parent;
@@ -3585,15 +4020,26 @@ class Asserter extends Stringifier {
3585
4020
  const {
3586
4021
  parent
3587
4022
  } = this;
3588
- if (node.type === 'BlockStatement' && !nodeIsFunction(parent)) {
4023
+ if (node.type === 'BlockStatement' && !nodeIsFunctionLike(parent)) {
3589
4024
  return '';
3590
4025
  }
3591
4026
  const jsdoc = this.getJSDoc(node);
3592
4027
  // return '// ' + JSON.stringify(jsdoc) + '\n';
3593
4028
  const stat = this.getStatsForNode(node);
3594
4029
  if (!jsdoc) {
3595
- stat.unchecked++;
3596
- return '';
4030
+ // No JSDoc at all: default-value inference is the only
4031
+ // source of types. Emit nothing when nothing is inferable.
4032
+ const inferred = this.collectDefaultChecks(node, new Set());
4033
+ if (!inferred.length) {
4034
+ stat.unchecked++;
4035
+ return '';
4036
+ }
4037
+ const _loc = this.getName(node);
4038
+ if (this.ignoreLocations.includes(_loc)) {
4039
+ return '// IGNORE RTI TYPE VALIDATIONS, KNOWN ISSUES\n';
4040
+ }
4041
+ stat.checked++;
4042
+ return this.emitDefaultChecks(node, inferred, true);
3597
4043
  }
3598
4044
  const {
3599
4045
  templates,
@@ -3659,7 +4105,7 @@ class Asserter extends Stringifier {
3659
4105
  // via arguments[paramIndex] anyway.
3660
4106
  name = `arguments[${paramIndex}]`;
3661
4107
  } else if (param.type === 'AssignmentPattern') {
3662
- const _loc = this.getName(node);
4108
+ const _loc2 = this.getName(node);
3663
4109
  if (param.left.type === 'ArrayPattern' && type.type === 'array') {
3664
4110
  // Add a type assertion for each element of the ArrayPattern
3665
4111
  for (const element of param.left.elements) {
@@ -3674,12 +4120,12 @@ class Asserter extends Stringifier {
3674
4120
  this.warn('Only Identifier case handled right now');
3675
4121
  continue;
3676
4122
  }
3677
- const _t = JSON.stringify(type.elementType, null, 2).replaceAll('\n', '\n' + spaces);
4123
+ const _t = simplifyTypeToSource(type.elementType).replaceAll('\n', '\n' + spaces);
3678
4124
  newlineBeforeFirst();
3679
4125
  if (templates) {
3680
- out += `${spaces}if (!inspectTypeWithTemplates(${element.name}, ${_t}, '${_loc}', '${nameFancy}', rtiTemplates)) {\n`;
4126
+ out += `${spaces}if (!inspectTypeWithTemplates(${element.name}, ${_t}, '${_loc2}', '${nameFancy}', rtiTemplates)) {\n`;
3681
4127
  } else {
3682
- out += `${spaces}if (!inspectType(${element.name}, ${_t}, '${_loc}', '${nameFancy}')) {\n`;
4128
+ out += `${spaces}if (!inspectType(${element.name}, ${_t}, '${_loc2}', '${nameFancy}')) {\n`;
3683
4129
  }
3684
4130
  out += `${spaces} youCanAddABreakpointHere();\n${spaces}}\n`;
3685
4131
  }
@@ -3708,12 +4154,12 @@ class Asserter extends Stringifier {
3708
4154
  this.warn("missing subtype information in JSDoc");
3709
4155
  continue;
3710
4156
  }
3711
- const _t2 = JSON.stringify(subType, null, 2).replaceAll('\n', '\n' + spaces);
4157
+ const _t2 = simplifyTypeToSource(subType).replaceAll('\n', '\n' + spaces);
3712
4158
  newlineBeforeFirst();
3713
4159
  if (templates) {
3714
- out += `${spaces}if (!inspectTypeWithTemplates(${keyName}, ${_t2}, '${_loc}', '${nameFancy}', rtiTemplates)) {\n`;
4160
+ out += `${spaces}if (!inspectTypeWithTemplates(${keyName}, ${_t2}, '${_loc2}', '${nameFancy}', rtiTemplates)) {\n`;
3715
4161
  } else {
3716
- out += `${spaces}if (!inspectType(${keyName}, ${_t2}, '${_loc}', '${nameFancy}')) {\n`;
4162
+ out += `${spaces}if (!inspectType(${keyName}, ${_t2}, '${_loc2}', '${nameFancy}')) {\n`;
3717
4163
  }
3718
4164
  out += `${spaces} youCanAddABreakpointHere();\n${spaces}}\n`;
3719
4165
  }
@@ -3723,17 +4169,17 @@ class Asserter extends Stringifier {
3723
4169
  name = `arguments[${paramIndex}]`;
3724
4170
  }
3725
4171
  } else {
3726
- this.warn(`generateTypeChecks> ${_loc}> todo implement`, `AssignmentPattern for parameter ${name}`);
4172
+ this.warn(`generateTypeChecks> ${_loc2}> todo implement`, `AssignmentPattern for parameter ${name}`);
3727
4173
  continue;
3728
4174
  }
3729
4175
  }
3730
4176
  } else {
3731
- const _loc2 = this.getName(node);
3732
- this.warn(`generateTypeChecks> ${_loc2}> Missing param: ${name}`);
4177
+ const _loc3 = this.getName(node);
4178
+ this.warn(`generateTypeChecks> ${_loc3}> Missing param: ${name}`);
3733
4179
  continue;
3734
4180
  }
3735
4181
  }
3736
- let t = JSON.stringify(type, null, 2).replaceAll('\n', '\n' + spaces);
4182
+ let t = simplifyTypeToSource(type).replaceAll('\n', '\n' + spaces);
3737
4183
  if (type === 'this') {
3738
4184
  const classDecl = this.findParentOfType(node, 'ClassDeclaration');
3739
4185
  if (!(classDecl != null && classDecl.id)) {
@@ -3741,19 +4187,107 @@ class Asserter extends Stringifier {
3741
4187
  }
3742
4188
  t = '"' + this.toSource(classDecl.id) + '"';
3743
4189
  }
3744
- const _loc3 = this.getName(node);
4190
+ const _loc4 = this.getName(node);
3745
4191
  let prevCheck = '';
3746
4192
  // JSDoc doesn't support multiple function signatures yet, but this is
3747
4193
  // exactly what we would need to deal with ObjectPool'ing
3748
- if (_loc3 === 'ContactPoint#constructor' || _loc3 === 'ContactResult#constructor' || _loc3 === 'SingleContactResult#constructor') {
4194
+ if (_loc4 === 'ContactPoint#constructor' || _loc4 === 'ContactResult#constructor' || _loc4 === 'SingleContactResult#constructor') {
3749
4195
  prevCheck = 'arguments.length !== 0 && ';
3750
4196
  }
3751
4197
  newlineBeforeFirst();
3752
4198
  if (templates) {
3753
- out += `${spaces}if (${prevCheck}!inspectTypeWithTemplates(${name}, ${t}, '${_loc3}', '${nameFancy}', rtiTemplates)) {\n`;
4199
+ out += `${spaces}if (${prevCheck}!inspectTypeWithTemplates(${name}, ${t}, '${_loc4}', '${nameFancy}', rtiTemplates)) {\n`;
3754
4200
  } else {
3755
- out += `${spaces}if (${prevCheck}!inspectType(${name}, ${t}, '${_loc3}', '${nameFancy}')) {\n`;
4201
+ out += `${spaces}if (${prevCheck}!inspectType(${name}, ${t}, '${_loc4}', '${nameFancy}')) {\n`;
4202
+ }
4203
+ out += `${spaces} youCanAddABreakpointHere();\n${spaces}}\n`;
4204
+ }
4205
+ // Params without JSDoc but with inferable defaults get synthesized
4206
+ // optional checks; JSDoc types always win on conflict.
4207
+ out += this.emitDefaultChecks(node, this.collectDefaultChecks(node, new Set(Object.keys(params))), out === '');
4208
+ return out;
4209
+ }
4210
+ /**
4211
+ * Collects type checks for undocumented params: inline
4212
+ * `/** @type *\/` param comments first, default-value inference second.
4213
+ * Only `Identifier` targets missing from `documented` are considered.
4214
+ * @param {Node} node - The Babel AST node for which to generate type checks.
4215
+ * @param {Set<string>} documented - Parameter names covered by JSDoc.
4216
+ * @returns {{name: string, type: any}[]} Checks with optional-annotated types.
4217
+ */
4218
+ collectDefaultChecks(node, documented) {
4219
+ let fnNode = node;
4220
+ if (fnNode.type === 'BlockStatement') {
4221
+ fnNode = this.parent;
4222
+ }
4223
+ const {
4224
+ params
4225
+ } = fnNode;
4226
+ if (!Array.isArray(params)) {
4227
+ return [];
4228
+ }
4229
+ if (fnNode.type === 'ArrowFunctionExpression' && !this.findParentOfType(fnNode, 'VariableDeclarator')) {
4230
+ // Bare callbacks (e.g. `.forEach((x = 0) => ...)`) can't be named,
4231
+ // synthesizing checks would spam unnameable-callback warnings.
4232
+ return [];
4233
+ }
4234
+ const checks = [];
4235
+ for (const param of params) {
4236
+ var _parseInlineParamType;
4237
+ if (!param) {
4238
+ continue;
4239
+ }
4240
+ const isAssignment = param.type === 'AssignmentPattern';
4241
+ const target = isAssignment ? param.left : param;
4242
+ if (!target || target.type !== 'Identifier' || documented.has(target.name)) {
4243
+ continue;
3756
4244
  }
4245
+ // Inline `/** @type *\/` wins over default inference;
4246
+ // a default still marks the check optional.
4247
+ const inline = (_parseInlineParamType = parseInlineParamType(param.leadingComments, this.expandType)) != null ? _parseInlineParamType : parseInlineParamType(target.leadingComments, this.expandType);
4248
+ if (inline !== undefined) {
4249
+ checks.push({
4250
+ name: target.name,
4251
+ type: annotateOptional(inline, isAssignment)
4252
+ });
4253
+ continue;
4254
+ }
4255
+ if (!isAssignment) {
4256
+ continue;
4257
+ }
4258
+ const inferred = inferTypeFromDefault$1(param.right);
4259
+ if (inferred === undefined) {
4260
+ continue;
4261
+ }
4262
+ checks.push({
4263
+ name: target.name,
4264
+ type: annotateOptional(inferred, true)
4265
+ });
4266
+ }
4267
+ return checks;
4268
+ }
4269
+ /**
4270
+ * Emits code for checks inferred from default values.
4271
+ * @param {Node} node - The Babel AST node for which to generate type checks.
4272
+ * @param {{name: string, type: any}[]} checks - Checks to emit.
4273
+ * @param {boolean} prefixNewline - Separate from preceding code with newline.
4274
+ * @returns {string} A string of code with type check assertions.
4275
+ */
4276
+ emitDefaultChecks(node, checks, prefixNewline) {
4277
+ if (!checks.length) {
4278
+ return '';
4279
+ }
4280
+ const {
4281
+ spaces
4282
+ } = this;
4283
+ const loc = this.getName(node);
4284
+ let out = prefixNewline ? '\n' : '';
4285
+ for (const {
4286
+ name,
4287
+ type
4288
+ } of checks) {
4289
+ const t = simplifyTypeToSource(type).replaceAll('\n', '\n' + spaces);
4290
+ out += `${spaces}if (!inspectType(${name}, ${t}, '${loc}', '${name}')) {\n`;
3757
4291
  out += `${spaces} youCanAddABreakpointHere();\n${spaces}}\n`;
3758
4292
  }
3759
4293
  return out;
@@ -3783,6 +4317,11 @@ class Asserter extends Stringifier {
3783
4317
  }
3784
4318
  console.warn("Asserter#getNameForFunctionExpression> expression without left");
3785
4319
  }
4320
+ const variableDeclarator = this.findParentOfType(node, 'VariableDeclarator');
4321
+ if (variableDeclarator) {
4322
+ // e.g. `var ScopeSpace = function (name) {...}` (issue #82)
4323
+ return this.toSource(variableDeclarator.id);
4324
+ }
3786
4325
  return 'unnamed function expression';
3787
4326
  }
3788
4327
  /**
@@ -3790,7 +4329,7 @@ class Asserter extends Stringifier {
3790
4329
  * @returns {string} Stringification of the node.
3791
4330
  */
3792
4331
  getName(node) {
3793
- var _node, _node2;
4332
+ var _node, _node2, _node3;
3794
4333
  const toSource = this.toSource.bind(this);
3795
4334
  if (node.type === 'BlockStatement') {
3796
4335
  node = this.parent;
@@ -3833,10 +4372,93 @@ class Asserter extends Stringifier {
3833
4372
  }
3834
4373
  // top scope
3835
4374
  return `${this.filename}:${(_node = node) == null || (_node = _node.loc) == null || (_node = _node.start) == null ? void 0 : _node.line}`;
4375
+ case 'MemberExpression':
4376
+ if (node.computed) {
4377
+ const memberGoodNames = ['FunctionDeclaration', 'ClassMethod', 'ClassPrivateMethod'];
4378
+ const memberGoodParent = this.parents.findLast(_ => memberGoodNames.includes(_.type));
4379
+ if (memberGoodParent) {
4380
+ return this.getName(memberGoodParent);
4381
+ }
4382
+ }
4383
+ // top scope
4384
+ return `${this.filename}:${(_node2 = node) == null || (_node2 = _node2.loc) == null || (_node2 = _node2.start) == null ? void 0 : _node2.line}`;
3836
4385
  }
3837
4386
  this.warn('getName> unhandled type', type, 'for', node, this.path);
3838
4387
  //debugger;
3839
- return `${this.filename}:${(_node2 = node) == null || (_node2 = _node2.loc) == null || (_node2 = _node2.start) == null ? void 0 : _node2.line}`;
4388
+ return `${this.filename}:${(_node3 = node) == null || (_node3 = _node3.loc) == null || (_node3 = _node3.start) == null ? void 0 : _node3.line}`;
4389
+ }
4390
+ /**
4391
+ * @override
4392
+ * @param {import("@babel/types").MemberExpression} node - The Babel AST node.
4393
+ * @returns {string} Stringification of the node.
4394
+ */
4395
+ MemberExpression(node) {
4396
+ const {
4397
+ computed,
4398
+ object,
4399
+ property
4400
+ } = node;
4401
+ if (!this.inspectIndexedAccess) {
4402
+ return super.MemberExpression(node);
4403
+ }
4404
+ if (!computed || object.type === 'Super') {
4405
+ return super.MemberExpression(node);
4406
+ }
4407
+ const parent = this.parent;
4408
+ if (parent) {
4409
+ // `a[i] = ...`, `a[i]++`/`--` and `delete a[i]` are handled by the statement
4410
+ // itself, wrapping them here would produce invalid assignment to a call.
4411
+ if (parent.type === 'AssignmentExpression' && parent.left === node) {
4412
+ return super.MemberExpression(node);
4413
+ }
4414
+ if (parent.type === 'UpdateExpression' && parent.argument === node) {
4415
+ return super.MemberExpression(node);
4416
+ }
4417
+ if (parent.type === 'UnaryExpression' && parent.operator === 'delete' && parent.argument === node) {
4418
+ return super.MemberExpression(node);
4419
+ }
4420
+ // Destructuring, rest and loop targets are assigned to, never read.
4421
+ if (parent.type === 'ArrayPattern') {
4422
+ return super.MemberExpression(node);
4423
+ }
4424
+ if (parent.type === 'RestElement' && parent.argument === node) {
4425
+ return super.MemberExpression(node);
4426
+ }
4427
+ if (parent.type === 'ObjectProperty' && parent.value === node) {
4428
+ // `({p: a[i]})` reads but `({p: a[i]} = ...)` writes: bypass inside patterns only.
4429
+ const grandparent = this.parents[this.parents.findLastIndex(_ => _ === parent) - 1];
4430
+ if ((grandparent == null ? void 0 : grandparent.type) === 'ObjectPattern') {
4431
+ return super.MemberExpression(node);
4432
+ }
4433
+ }
4434
+ if (parent.type === 'AssignmentPattern' && parent.left === node) {
4435
+ return super.MemberExpression(node);
4436
+ }
4437
+ if ((parent.type === 'ForOfStatement' || parent.type === 'ForInStatement') && parent.left === node) {
4438
+ return super.MemberExpression(node);
4439
+ }
4440
+ // Method calls and tags carry their base as `this`; wrapping the callee
4441
+ // would silently rebind it to undefined.
4442
+ if ((parent.type === 'CallExpression' || parent.type === 'OptionalCallExpression') && parent.callee === node) {
4443
+ return super.MemberExpression(node);
4444
+ }
4445
+ if (parent.type === 'TaggedTemplateExpression' && parent.tag === node) {
4446
+ return super.MemberExpression(node);
4447
+ }
4448
+ // `new a[i](...)` must stay `new (inspectIndexedAccess(...))(...)`:
4449
+ // without parens it parses as `(new inspectIndexedAccess(...))(...)`,
4450
+ // constructing the wrapper instead of the accessed value.
4451
+ if (parent.type === 'NewExpression' && parent.callee === node) {
4452
+ const _object_ = this.toSource(object);
4453
+ const _property_ = this.toSource(property);
4454
+ const _loc5 = this.getName(node);
4455
+ return `(inspectIndexedAccess(${_object_}, ${_property_}, ${JSON.stringify(_loc5)}))`;
4456
+ }
4457
+ }
4458
+ const object_ = this.toSource(object);
4459
+ const property_ = this.toSource(property);
4460
+ const loc = this.getName(node);
4461
+ return `inspectIndexedAccess(${object_}, ${property_}, ${JSON.stringify(loc)})`;
3840
4462
  }
3841
4463
  /**
3842
4464
  * @override
@@ -3881,7 +4503,7 @@ class Asserter extends Stringifier {
3881
4503
  let out = '';
3882
4504
  for (const name in this.typedefs) {
3883
4505
  const typedef = this.typedefs[name];
3884
- const json = JSON.stringify(typedef, null, 2);
4506
+ const json = simplifyTypeToSource(typedef);
3885
4507
  out += `registerTypedef('${name}', ${json});\n`;
3886
4508
  }
3887
4509
  const code = this.toSource(program) + '\n';
@@ -4066,6 +4688,21 @@ function compareAST(left, right) {
4066
4688
  * @returns {string|object|undefined} - See `toSourceBabelTS`.
4067
4689
  */
4068
4690
  function expandTypeBabelTS(type) {
4691
+ type = type.trim();
4692
+ // JSDocNullableType (`T?` / `?T`): Babel has no support (babel/babel#16073),
4693
+ // handle at string level to match expandType() union-with-null.
4694
+ if (type.endsWith('?') && type.length > 1) {
4695
+ return {
4696
+ type: 'union',
4697
+ members: [expandTypeBabelTS(type.slice(0, -1).trim()), 'null']
4698
+ };
4699
+ }
4700
+ if (type.startsWith('?') && type.length > 1) {
4701
+ return {
4702
+ type: 'union',
4703
+ members: [expandTypeBabelTS(type.slice(1).trim()), 'null']
4704
+ };
4705
+ }
4069
4706
  const ast = parseTypeBabelTS(type);
4070
4707
  return toSourceBabelTS(ast);
4071
4708
  }
@@ -4138,7 +4775,8 @@ function toSourceBabelTS(node) {
4138
4775
  const name = toSourceBabelTS(node.typeName);
4139
4776
  if (!node.typeParameters) {
4140
4777
  // console.log(`node.typeName.name=${node.typeName.name} name=${name}`, node);
4141
- return node.typeName.name;
4778
+ // Bare reference: Identifier gives name, TSQualifiedName gives dotted path.
4779
+ return name;
4142
4780
  }
4143
4781
  console.assert(node.typeParameters.type === 'TSTypeParameterInstantiation');
4144
4782
  const typeArguments = node.typeParameters.params;
@@ -4186,9 +4824,13 @@ function toSourceBabelTS(node) {
4186
4824
  elementType
4187
4825
  };
4188
4826
  }
4189
- console.warn('unhandled TypeReference', node);
4827
+ // Parity with expandType(): generic references like ArrayLike<T>,
4828
+ // ReadonlyArray<T>, NodeListOf<T> or user typedefs like MyBox<T>.
4829
+ const args = typeArguments.map(toSourceBabelTS);
4190
4830
  return {
4191
- type: 'unhandled TypeReference'
4831
+ type: 'reference',
4832
+ name,
4833
+ args
4192
4834
  };
4193
4835
  }
4194
4836
  case 'TSStringKeyword':
@@ -4241,7 +4883,7 @@ function toSourceBabelTS(node) {
4241
4883
  return 'boolean';
4242
4884
  // expandTypeBabelTS('true | false');
4243
4885
  case 'BooleanLiteral':
4244
- return node.value.toString();
4886
+ return node.value;
4245
4887
  // ts.SyntaxKind[parseType("*").kind] === 'JSDocAllType'
4246
4888
  // But Babel-TS doesn't parse it atm
4247
4889
  //case 'JSDocAllType':
@@ -4250,6 +4892,7 @@ function toSourceBabelTS(node) {
4250
4892
  return 'null';
4251
4893
  // expandTypeBabelTS('123')
4252
4894
  case 'NumericLiteral':
4895
+ return typeof node.value === 'number' ? node.value : Number(node.extra.raw);
4253
4896
  case 'StringLiteral':
4254
4897
  return node.extra.raw;
4255
4898
  // expandTypeBabelTS('undefined')
@@ -4281,10 +4924,3282 @@ function toSourceBabelTS(node) {
4281
4924
  type: 'typeof',
4282
4925
  argument
4283
4926
  };
4927
+ case 'TSTypeOperator':
4928
+ if (node.operator === 'readonly') {
4929
+ // readonly erased at runtime, same shape as the inner type.
4930
+ return toSourceBabelTS(node.typeAnnotation);
4931
+ }
4932
+ console.warn('unimplemented TSTypeOperator', node.operator);
4933
+ return 'any';
4934
+ case 'TSQualifiedName':
4935
+ return `${toSourceBabelTS(node.left)}.${toSourceBabelTS(node.right)}`;
4284
4936
  default:
4285
4937
  console.warn('toSourceBabelTS> unhandled type', node.type, node);
4286
4938
  debugger;
4287
4939
  }
4288
4940
  }
4289
4941
 
4290
- export { Asserter, Stringifier, addTypeChecks, ast2json, ast2jsonForComparison, capitalize, code2ast2code, compareAST, expandType, expandTypeBabelTS, expandTypeDepFree, extractCurlyContent, extractNameAndOptionality, nodeIsFunction, parseJSDoc, parseJSDocSetter, parseJSDocTemplates, parseJSDocTypedef, parseType, parseTypeBabelTS, parserOptions, requiredTypeofs, simplifyType, toSourceBabelTS, toSourceTS, trimEndSpaces };
4942
+ /**
4943
+ * Map of Babel node types to their child keys that contain traversable AST nodes.
4944
+ * @type {Record<string, string[]>}
4945
+ */
4946
+ const nodeChildren = {
4947
+ 'ArrayExpression': ['elements'],
4948
+ 'ArrayPattern': ['elements'],
4949
+ 'ArrowFunctionExpression': ['params', 'body'],
4950
+ 'AssignmentExpression': ['left', 'right'],
4951
+ 'AssignmentPattern': ['left', 'right'],
4952
+ 'AwaitExpression': ['argument'],
4953
+ 'BinaryExpression': ['left', 'right'],
4954
+ 'BlockStatement': ['directives', 'body'],
4955
+ 'BreakStatement': ['label'],
4956
+ 'CallExpression': ['callee', 'arguments'],
4957
+ 'CatchClause': ['param', 'body'],
4958
+ 'ClassBody': ['body'],
4959
+ 'ClassDeclaration': ['id', 'superClass', 'body'],
4960
+ 'ClassExpression': ['id', 'superClass', 'body'],
4961
+ 'ClassMethod': ['key', 'params', 'body'],
4962
+ 'ClassPrivateMethod': ['key', 'params', 'body'],
4963
+ 'ClassPrivateProperty': ['key', 'value'],
4964
+ 'ClassProperty': ['key', 'value'],
4965
+ 'ConditionalExpression': ['test', 'consequent', 'alternate'],
4966
+ 'ContinueStatement': ['label'],
4967
+ 'DebuggerStatement': [],
4968
+ 'Directive': ['value'],
4969
+ 'DirectiveLiteral': [],
4970
+ 'DoWhileStatement': ['body', 'test'],
4971
+ 'EmptyStatement': [],
4972
+ 'ExportAllDeclaration': ['source'],
4973
+ 'ExportDefaultDeclaration': ['declaration'],
4974
+ 'ExportNamedDeclaration': ['declaration', 'specifiers', 'source'],
4975
+ 'ExportNamespaceSpecifier': ['exported'],
4976
+ 'ExportSpecifier': ['local', 'exported'],
4977
+ 'ExpressionStatement': ['expression'],
4978
+ 'File': ['program'],
4979
+ 'ForInStatement': ['left', 'right', 'body'],
4980
+ 'ForOfStatement': ['left', 'right', 'body'],
4981
+ 'ForStatement': ['init', 'test', 'update', 'body'],
4982
+ 'FunctionDeclaration': ['id', 'params', 'body'],
4983
+ 'FunctionExpression': ['id', 'params', 'body'],
4984
+ 'IfStatement': ['test', 'consequent', 'alternate'],
4985
+ 'Import': [],
4986
+ 'ImportDeclaration': ['specifiers', 'source'],
4987
+ 'ImportDefaultSpecifier': ['local'],
4988
+ 'ImportExpression': ['source'],
4989
+ 'ImportNamespaceSpecifier': ['local'],
4990
+ 'ImportSpecifier': ['imported', 'local'],
4991
+ 'JSXAttribute': ['name', 'value'],
4992
+ 'JSXElement': ['openingElement', 'children', 'closingElement'],
4993
+ 'JSXExpressionContainer': ['expression'],
4994
+ 'JSXFragment': ['openingFragment', 'children', 'closingFragment'],
4995
+ 'JSXIdentifier': [],
4996
+ 'JSXMemberExpression': ['object', 'property'],
4997
+ 'JSXNamespacedName': ['namespace', 'name'],
4998
+ 'JSXText': [],
4999
+ 'LabeledStatement': ['label', 'body'],
5000
+ 'LogicalExpression': ['left', 'right'],
5001
+ 'MemberExpression': ['object', 'property'],
5002
+ 'MetaProperty': ['meta', 'property'],
5003
+ 'NewExpression': ['callee', 'arguments'],
5004
+ 'ObjectExpression': ['properties'],
5005
+ 'ObjectMethod': ['key', 'params', 'body'],
5006
+ 'ObjectPattern': ['properties'],
5007
+ 'ObjectProperty': ['key', 'value'],
5008
+ 'OptionalCallExpression': ['callee', 'arguments'],
5009
+ 'OptionalMemberExpression': ['object', 'property'],
5010
+ 'ParenthesizedExpression': ['expression'],
5011
+ 'PrivateName': ['id'],
5012
+ 'Program': ['directives', 'body'],
5013
+ 'RegExpLiteral': [],
5014
+ 'RestElement': ['argument'],
5015
+ 'ReturnStatement': ['argument'],
5016
+ 'SequenceExpression': ['expressions'],
5017
+ 'SpreadElement': ['argument'],
5018
+ 'Super': [],
5019
+ 'SwitchCase': ['test', 'consequent'],
5020
+ 'SwitchStatement': ['discriminant', 'cases'],
5021
+ 'TaggedTemplateExpression': ['tag', 'quasi'],
5022
+ 'TemplateElement': [],
5023
+ 'TemplateLiteral': ['quasis', 'expressions'],
5024
+ 'ThisExpression': [],
5025
+ 'ThrowStatement': ['argument'],
5026
+ 'TryStatement': ['block', 'handler', 'finalizer'],
5027
+ 'UnaryExpression': ['argument'],
5028
+ 'UpdateExpression': ['argument'],
5029
+ 'VariableDeclaration': ['declarations'],
5030
+ 'VariableDeclarator': ['id', 'init'],
5031
+ 'WhileStatement': ['test', 'body'],
5032
+ 'YieldExpression': ['argument'],
5033
+ 'TSAsExpression': ['expression', 'typeAnnotation'],
5034
+ 'TSDeclareFunction': ['id', 'params', 'body'],
5035
+ 'TSEnumDeclaration': ['id', 'members'],
5036
+ 'TSEnumMember': ['id', 'initializer'],
5037
+ 'TSInterfaceDeclaration': ['id', 'body'],
5038
+ 'TSInterfaceBody': ['body'],
5039
+ 'TSModuleDeclaration': ['id', 'body'],
5040
+ 'TSModuleBlock': ['body'],
5041
+ 'TSNonNullExpression': ['expression'],
5042
+ 'TSParameterProperty': ['parameter'],
5043
+ 'TSInstantiationExpression': ['expression'],
5044
+ 'TSTypeAliasDeclaration': ['id', 'typeAnnotation'],
5045
+ 'TSTypeAssertion': ['expression'],
5046
+ 'TSTypeCastExpression': ['expression'],
5047
+ 'TSTypeAnnotation': ['typeAnnotation'],
5048
+ 'TSTypeParameterDeclaration': ['params'],
5049
+ 'TSTypeParameterInstantiation': ['params']
5050
+ };
5051
+
5052
+ class JSDocAnnotator {
5053
+ /**
5054
+ * @param {Object} [options] - Options for the annotator.
5055
+ * @param {import('./parseJSDoc.js').ExpandType} [options.expandType] - Function to expand types.
5056
+ */
5057
+ constructor(options = {}) {
5058
+ /** @type {import('@babel/types').Node[]} */
5059
+ this.parents = [];
5060
+ /** @type {Record<string, object>} */
5061
+ this.typedefs = {};
5062
+ /** @type {import('./parseJSDoc.js').ExpandType} */
5063
+ this.expandType = options.expandType || expandTypeDepFree;
5064
+ }
5065
+ /**
5066
+ * Annotates the AST by adding 'jsdoc' properties to relevant nodes.
5067
+ * @param {import('@babel/types').File|import('@babel/types').Program} ast - The Babel AST to annotate.
5068
+ * @returns {import('@babel/types').File|import('@babel/types').Program} The annotated AST.
5069
+ */
5070
+ annotate(ast) {
5071
+ if (ast.type === 'File') {
5072
+ const {
5073
+ comments
5074
+ } = ast;
5075
+ if (comments) {
5076
+ for (const comment of comments) {
5077
+ const warn = console.warn.bind(console);
5078
+ parseJSDocTypedef(this.typedefs, warn, comment, this.expandType);
5079
+ }
5080
+ }
5081
+ this.traverse(ast.program);
5082
+ ast.program.typedefs = this.typedefs;
5083
+ } else {
5084
+ this.traverse(ast);
5085
+ ast.typedefs = this.typedefs;
5086
+ }
5087
+ return ast;
5088
+ }
5089
+ /**
5090
+ * Traverses the AST node and annotates where applicable.
5091
+ * @param {import('@babel/types').Node} node - The node to traverse.
5092
+ */
5093
+ traverse(node) {
5094
+ if (!node) return;
5095
+ this.parents.push(node);
5096
+ if (nodeIsFunctionLike(node)) {
5097
+ const jsdoc = this.getJSDoc(node);
5098
+ if (jsdoc) {
5099
+ node.jsdoc = jsdoc;
5100
+ const paramTypes = this.collectParamTypes(node);
5101
+ if (Object.keys(paramTypes).length > 0) {
5102
+ node.paramTypes = paramTypes;
5103
+ }
5104
+ }
5105
+ }
5106
+ const type = node.type;
5107
+ const childrenKeys = nodeChildren[type] || [];
5108
+ for (const key of childrenKeys) {
5109
+ const child = node[key];
5110
+ if (Array.isArray(child)) {
5111
+ for (const c of child) {
5112
+ this.traverse(c);
5113
+ }
5114
+ } else if (child && typeof child === 'object' && child.type) {
5115
+ this.traverse(child);
5116
+ }
5117
+ }
5118
+ this.parents.pop();
5119
+ }
5120
+ /**
5121
+ * Collects the parameter names into a map of names to types.
5122
+ * @param {import('@babel/types').ArrowFunctionExpression | import('@babel/types').FunctionDeclaration | import('@babel/types').FunctionExpression | import('@babel/types').ObjectMethod | import('@babel/types').ClassMethod | import('@babel/types').ClassPrivateMethod} node - The function-like node.
5123
+ * @returns {Record<string, string | object>} The map of parameter names to types.
5124
+ */
5125
+ collectParamTypes(node) {
5126
+ var _node$jsdoc;
5127
+ const jsdocParams = ((_node$jsdoc = node.jsdoc) == null ? void 0 : _node$jsdoc.params) || {};
5128
+ const paramNames = Object.keys(jsdocParams);
5129
+ const paramTypes = {};
5130
+ node.params.forEach((paramNode, index) => {
5131
+ const paramName = paramNames[index];
5132
+ if (paramName) {
5133
+ const typeInfo = jsdocParams[paramName];
5134
+ this.collectParamNames(paramNode, typeInfo, paramTypes);
5135
+ }
5136
+ });
5137
+ return paramTypes;
5138
+ }
5139
+ /**
5140
+ * Recursively collects parameter names from a pattern with their types.
5141
+ * @param {import('@babel/types').PatternLike} paramNode - The parameter node.
5142
+ * @param {string | object} typeInfo - The type information.
5143
+ * @param {Record<string, string | object>} map - The map to collect into.
5144
+ */
5145
+ collectParamNames(paramNode, typeInfo, map) {
5146
+ if (!typeInfo) {
5147
+ console.warn("!typeInfo", {
5148
+ paramNode,
5149
+ typeInfo
5150
+ });
5151
+ return;
5152
+ }
5153
+ const {
5154
+ type
5155
+ } = paramNode;
5156
+ if (type === 'Identifier') {
5157
+ map[paramNode.name] = typeInfo;
5158
+ } else if (type === 'ObjectPattern') {
5159
+ if (typeof typeInfo !== 'object' || typeInfo.type !== 'object' || !typeInfo.properties) {
5160
+ console.warn("typeof typeInfo !== 'object' || typeInfo.type !== 'object' || !typeInfo.properties", {
5161
+ paramNode,
5162
+ typeInfo
5163
+ });
5164
+ return;
5165
+ }
5166
+ const propTypes = typeInfo.properties;
5167
+ paramNode.properties.forEach(prop => {
5168
+ if (prop.type !== 'ObjectProperty') {
5169
+ console.warn("prop.type !== 'ObjectProperty'", {
5170
+ paramNode,
5171
+ typeInfo
5172
+ });
5173
+ return;
5174
+ }
5175
+ if (prop.key.type !== 'Identifier') {
5176
+ console.warn("prop.key.type !== 'Identifier'", {
5177
+ paramNode,
5178
+ typeInfo
5179
+ });
5180
+ return;
5181
+ }
5182
+ const keyName = prop.key.name;
5183
+ const subType = propTypes[keyName];
5184
+ if (!subType) {
5185
+ console.warn("!subType", {
5186
+ paramNode,
5187
+ typeInfo
5188
+ });
5189
+ return;
5190
+ }
5191
+ this.collectParamNames(prop.value, subType, map);
5192
+ });
5193
+ } else if (type === 'ArrayPattern') {
5194
+ // console.log("ARRAY PATTERN", {type, typeInfo});
5195
+ if (typeof typeInfo !== 'object') {
5196
+ console.warn("typeof typeInfo !== 'object'", {
5197
+ paramNode,
5198
+ typeInfo
5199
+ });
5200
+ return;
5201
+ }
5202
+ switch (typeInfo.type) {
5203
+ case 'array':
5204
+ if (!typeInfo.elementType) {
5205
+ console.warn("Expected array type, but missing 'elementType' property.", {
5206
+ paramNode,
5207
+ typeInfo
5208
+ });
5209
+ return;
5210
+ }
5211
+ const elementType = typeInfo.elementType;
5212
+ paramNode.elements.forEach(el => {
5213
+ if (!el) {
5214
+ console.warn("!el", {
5215
+ paramNode,
5216
+ typeInfo
5217
+ });
5218
+ return;
5219
+ }
5220
+ this.collectParamNames(el, elementType, map);
5221
+ });
5222
+ break;
5223
+ case 'tuple':
5224
+ // console.log("TUPLE PATTERN", {type, typeInfo});
5225
+ if (!typeInfo.elements) {
5226
+ console.warn("Expected tuple type, but missing 'elements' property.", {
5227
+ paramNode,
5228
+ typeInfo
5229
+ });
5230
+ return;
5231
+ }
5232
+ const elements = typeInfo.elements;
5233
+ paramNode.elements.forEach((el, i) => {
5234
+ const elType = elements[i];
5235
+ if (el === null) {
5236
+ // Example missing 'd' param: function addStr(a, {b}, [c], [, e])
5237
+ return;
5238
+ }
5239
+ // console.log(`paramNode.elements[${i}]`, el, "elType", elType);
5240
+ this.collectParamNames(el, elType, map);
5241
+ });
5242
+ break;
5243
+ default:
5244
+ console.warn('Unsupported type for ArrayPattern:', typeInfo.type, {
5245
+ paramNode,
5246
+ typeInfo
5247
+ });
5248
+ break;
5249
+ }
5250
+ } else if (type === 'AssignmentPattern') {
5251
+ this.collectParamNames(paramNode.left, typeInfo, map);
5252
+ } else if (type === 'RestElement') {
5253
+ this.collectParamNames(paramNode.argument, typeInfo, map);
5254
+ }
5255
+ }
5256
+ /**
5257
+ * Finds the closest ancestor of the given node that matches the specified type.
5258
+ * @param {import('@babel/types').Node} node - The starting node.
5259
+ * @param {string} type - The type to search for.
5260
+ * @returns {import('@babel/types').Node|undefined} The ancestor node or undefined.
5261
+ */
5262
+ findParentOfType(node, type) {
5263
+ const currentIndex = this.parents.findLastIndex(_ => _ === node);
5264
+ return this.parents.findLast((_, i) => i <= currentIndex && _.type === type);
5265
+ }
5266
+ /**
5267
+ * Gets the leading comment node for ArrowFunctionExpression.
5268
+ * @param {import('@babel/types').Node} node - The node.
5269
+ * @returns {import('@babel/types').Node|undefined} The node with leading comments.
5270
+ */
5271
+ getLeadingCommentsNodeForArrowFunctionExpression(node) {
5272
+ let i = this.parents.findLastIndex(_ => _ === node);
5273
+ let parent = this.parents[i];
5274
+ if (parent.leadingComments) {
5275
+ return parent;
5276
+ }
5277
+ i--;
5278
+ while (i >= 0) {
5279
+ parent = this.parents[i];
5280
+ if (nodeIsFunctionLike(parent)) {
5281
+ break;
5282
+ }
5283
+ if (parent.leadingComments) {
5284
+ return parent;
5285
+ }
5286
+ i--;
5287
+ }
5288
+ }
5289
+ /**
5290
+ * Gets the leading comment node for FunctionExpression.
5291
+ * @param {import('@babel/types').Node} node - The node.
5292
+ * @returns {import('@babel/types').Node|undefined} The node with leading comments.
5293
+ */
5294
+ getLeadingCommentsNodeForFunctionExpression(node) {
5295
+ let i = this.parents.findLastIndex(_ => _ === node);
5296
+ let parent = this.parents[i];
5297
+ if (parent.leadingComments) {
5298
+ return parent;
5299
+ }
5300
+ i--;
5301
+ while (i >= 0) {
5302
+ parent = this.parents[i];
5303
+ if (nodeIsFunctionLike(parent)) {
5304
+ break;
5305
+ }
5306
+ if (parent.leadingComments) {
5307
+ return parent;
5308
+ }
5309
+ i--;
5310
+ }
5311
+ }
5312
+ /**
5313
+ * Gets the leading comment string for a node.
5314
+ * @param {import('@babel/types').Node} node - The node.
5315
+ * @returns {string|undefined} The comment value.
5316
+ */
5317
+ getLeadingComment(node) {
5318
+ let leadingComments = node.leadingComments;
5319
+ if (!leadingComments) {
5320
+ if (node.type === 'FunctionDeclaration') {
5321
+ const exportNamedDeclaration = this.findParentOfType(node, 'ExportNamedDeclaration');
5322
+ leadingComments = exportNamedDeclaration == null ? void 0 : exportNamedDeclaration.leadingComments;
5323
+ } else if (node.type === 'ArrowFunctionExpression') {
5324
+ const tmp = this.getLeadingCommentsNodeForArrowFunctionExpression(node);
5325
+ leadingComments = tmp == null ? void 0 : tmp.leadingComments;
5326
+ } else if (node.type === 'FunctionExpression') {
5327
+ const tmp = this.getLeadingCommentsNodeForFunctionExpression(node);
5328
+ leadingComments = tmp == null ? void 0 : tmp.leadingComments;
5329
+ }
5330
+ }
5331
+ if (leadingComments && leadingComments.length) {
5332
+ const lastComment = leadingComments[leadingComments.length - 1];
5333
+ if (lastComment.type === 'CommentBlock') {
5334
+ return lastComment.value;
5335
+ }
5336
+ }
5337
+ }
5338
+ /**
5339
+ * Parses the JSDoc for a node.
5340
+ * @param {import('@babel/types').Node} node - The node.
5341
+ * @returns {{templates: any, params: any}|undefined} The parsed JSDoc.
5342
+ */
5343
+ getJSDoc(node) {
5344
+ const comment = this.getLeadingComment(node);
5345
+ if (!comment) {
5346
+ return;
5347
+ }
5348
+ if (comment.includes('@event') || comment.includes('@ignoreRTI')) {
5349
+ return;
5350
+ }
5351
+ if (node.type === 'ClassMethod' && node.kind === 'set') {
5352
+ if (node.params.length !== 1) {
5353
+ console.warn('getJSDoc> setters require exactly one argument');
5354
+ }
5355
+ const setterType = parseJSDocSetter(comment, this.expandType);
5356
+ if (!setterType) {
5357
+ return;
5358
+ }
5359
+ const paramName = node.params[0].type === 'Identifier' ? node.params[0].name : 'value';
5360
+ const _params = {
5361
+ [paramName]: setterType
5362
+ };
5363
+ return {
5364
+ templates: undefined,
5365
+ params: _params
5366
+ };
5367
+ }
5368
+ const templates = parseJSDocTemplates(comment);
5369
+ const params = parseJSDoc(comment, this.expandType);
5370
+ if (!templates && !params) {
5371
+ return;
5372
+ }
5373
+ return {
5374
+ templates,
5375
+ params
5376
+ };
5377
+ }
5378
+ }
5379
+
5380
+ /** @typedef {import("@babel/types").Node} Node */
5381
+ const CONST_TYPE = {
5382
+ f32: 'f32.const',
5383
+ f64: 'f64.const',
5384
+ i32: 'i32.const',
5385
+ i64: 'i64.const'
5386
+ };
5387
+ const NEG_OP = {
5388
+ f32: 'f32.neg',
5389
+ f64: 'f64.neg'
5390
+ };
5391
+ const EQZ_OP = {
5392
+ f32: 'f32.eqz',
5393
+ f64: 'f64.eqz',
5394
+ i32: 'i32.eqz',
5395
+ i64: 'i64.eqz'
5396
+ };
5397
+ const NE_OP = {
5398
+ f32: 'f32.ne',
5399
+ f64: 'f64.ne',
5400
+ i32: 'i32.ne',
5401
+ i64: 'i64.ne'
5402
+ };
5403
+ const ARITH_OP = {
5404
+ f32: {
5405
+ add: 'f32.add',
5406
+ sub: 'f32.sub',
5407
+ mul: 'f32.mul',
5408
+ div: 'f32.div'
5409
+ },
5410
+ f64: {
5411
+ add: 'f64.add',
5412
+ sub: 'f64.sub',
5413
+ mul: 'f64.mul',
5414
+ div: 'f64.div'
5415
+ },
5416
+ i32: {
5417
+ add: 'i32.add',
5418
+ sub: 'i32.sub',
5419
+ mul: 'i32.mul',
5420
+ div: 'i32.div_s'
5421
+ },
5422
+ i64: {
5423
+ add: 'i64.add',
5424
+ sub: 'i64.sub',
5425
+ mul: 'i64.mul',
5426
+ div: 'i64.div_s'
5427
+ }
5428
+ };
5429
+ const CMP_OP = {
5430
+ f32: {
5431
+ lt: 'f32.lt',
5432
+ gt: 'f32.gt',
5433
+ le: 'f32.le',
5434
+ ge: 'f32.ge',
5435
+ eq: 'f32.eq',
5436
+ ne: 'f32.ne'
5437
+ },
5438
+ f64: {
5439
+ lt: 'f64.lt',
5440
+ gt: 'f64.gt',
5441
+ le: 'f64.le',
5442
+ ge: 'f64.ge',
5443
+ eq: 'f64.eq',
5444
+ ne: 'f64.ne'
5445
+ },
5446
+ i32: {
5447
+ lt: 'i32.lt_s',
5448
+ gt: 'i32.gt_s',
5449
+ le: 'i32.le_s',
5450
+ ge: 'i32.ge_s',
5451
+ eq: 'i32.eq',
5452
+ ne: 'i32.ne'
5453
+ },
5454
+ i64: {
5455
+ lt: 'i64.lt_s',
5456
+ gt: 'i64.gt_s',
5457
+ le: 'i64.le_s',
5458
+ ge: 'i64.ge_s',
5459
+ eq: 'i64.eq',
5460
+ ne: 'i64.ne'
5461
+ }
5462
+ };
5463
+ const MATH_OP = {
5464
+ f32: {
5465
+ abs: 'f32.abs',
5466
+ sqrt: 'f32.sqrt',
5467
+ min: 'f32.min',
5468
+ max: 'f32.max',
5469
+ floor: 'f32.floor',
5470
+ ceil: 'f32.ceil',
5471
+ trunc: 'f32.trunc'
5472
+ },
5473
+ f64: {
5474
+ abs: 'f64.abs',
5475
+ sqrt: 'f64.sqrt',
5476
+ min: 'f64.min',
5477
+ max: 'f64.max',
5478
+ floor: 'f64.floor',
5479
+ ceil: 'f64.ceil',
5480
+ trunc: 'f64.trunc'
5481
+ }
5482
+ };
5483
+ const CMP_OPERATORS = ['<', '<=', '>', '>=', '==', '!='];
5484
+ /**
5485
+ * Class for converting a JavaScript AST into WebAssembly Text (WAT).
5486
+ */
5487
+ class WATConverter extends Stringifier {
5488
+ constructor(...args) {
5489
+ super(...args);
5490
+ /**
5491
+ * Current numeric WAT type for emitted operations.
5492
+ * @type {'f32'|'f64'|'i32'|'i64'}
5493
+ */
5494
+ this.currentType = 'f32';
5495
+ /**
5496
+ * Stack of enclosing loop labels used as break/continue targets.
5497
+ * @type {Array<{exit: string, top: string, updateSrc: string}>}
5498
+ */
5499
+ this.loops = [];
5500
+ /**
5501
+ * Counter for unique loop label names.
5502
+ * @type {number}
5503
+ */
5504
+ this.loopCounter = 0;
5505
+ /**
5506
+ * Linear-memory array allocations by variable name.
5507
+ * @type {Object<string, {offset: number, values: number[], isObject: boolean, keys?: Map<string, number>}>}
5508
+ */
5509
+ this.arrays = {};
5510
+ /**
5511
+ * Declared classes by name, each with its instance fields, per-field byte offsets, methods and total size.
5512
+ * @type {Object<string, {fields: string[], offsets: Object<string, number>, defaults: Object<string, Node>, methods: Set<string>, size: number}>}
5513
+ */
5514
+ this.classes = {};
5515
+ /**
5516
+ * Instance variable names mapped to their class names.
5517
+ * @type {Object<string, string>}
5518
+ */
5519
+ this.instances = {};
5520
+ /**
5521
+ * Name of the class currently being converted, if any.
5522
+ * @type {string|null}
5523
+ */
5524
+ this.currentClass = null;
5525
+ }
5526
+ /**
5527
+ * Converts a Babel AST node to WAT source.
5528
+ * @param {Node} node - The Babel AST node.
5529
+ * @returns {string} WAT representation of the node.
5530
+ */
5531
+ toSource(node) {
5532
+ if (node === null) {
5533
+ return '';
5534
+ }
5535
+ const {
5536
+ leadingComments,
5537
+ trailingComments
5538
+ } = node;
5539
+ if (node.type === 'File') {
5540
+ this.parents.length = 0;
5541
+ }
5542
+ this.parents.push(node);
5543
+ let out = '';
5544
+ if (leadingComments) {
5545
+ out += this.leadingCommentsToSource(leadingComments);
5546
+ }
5547
+ out += this.toSource_(node);
5548
+ if (trailingComments) {
5549
+ out += this.trailingCommentsToSource(trailingComments);
5550
+ }
5551
+ this.parents.pop();
5552
+ return out;
5553
+ }
5554
+ /**
5555
+ * Dispatches a node to its handler method, emitting a warning comment for unhandled types.
5556
+ * @param {Node} node - The Babel AST node.
5557
+ * @returns {string} WAT representation of the node.
5558
+ */
5559
+ toSource_(node) {
5560
+ if (!node) {
5561
+ return '';
5562
+ }
5563
+ const {
5564
+ type
5565
+ } = node;
5566
+ if (this[type]) {
5567
+ return this[type](node);
5568
+ }
5569
+ console.warn(`TODO ADD METHOD: ${type}`);
5570
+ return `;; Unhandled node type: ${type}\n`;
5571
+ }
5572
+ /**
5573
+ * Converts leading comments to WAT.
5574
+ * @param {import("@babel/types").Comment[]} comments - The comment nodes.
5575
+ * @returns {string} WAT representation of comments.
5576
+ */
5577
+ leadingCommentsToSource(comments) {
5578
+ return comments.map(comment => this.commentToSource(comment, 'leading')).join('');
5579
+ }
5580
+ /**
5581
+ * Converts trailing comments to WAT.
5582
+ * @param {import("@babel/types").Comment[]} comments - The comment nodes.
5583
+ * @returns {string} WAT representation of comments.
5584
+ */
5585
+ trailingCommentsToSource(comments) {
5586
+ return comments.map(comment => this.commentToSource(comment, 'trailing')).join('');
5587
+ }
5588
+ /**
5589
+ * Converts a comment to WAT.
5590
+ * @param {import("@babel/types").Comment} comment - The comment node.
5591
+ * @param {'leading' | 'trailing'} pos - The position of the comment.
5592
+ * @returns {string} WAT representation of the comment.
5593
+ */
5594
+ commentToSource(comment, pos) {
5595
+ if (comment.type === 'CommentBlock') {
5596
+ return this.CommentBlock(comment);
5597
+ } else if (comment.type === 'CommentLine') {
5598
+ return this.CommentLine(comment);
5599
+ }
5600
+ console.warn("Unknown comment type", comment);
5601
+ return '';
5602
+ }
5603
+ /**
5604
+ * Converts a CommentBlock to WAT.
5605
+ * @param {import("@babel/types").CommentBlock} node - The comment node.
5606
+ * @returns {string} WAT representation of the comment.
5607
+ */
5608
+ CommentBlock(node) {
5609
+ const {
5610
+ loc,
5611
+ value
5612
+ } = node;
5613
+ if (this.lastCommentBlockIndex === loc.start.index) {
5614
+ return '';
5615
+ }
5616
+ this.lastCommentBlockIndex = loc.start.index;
5617
+ const spaces = this.spaces;
5618
+ let out = '';
5619
+ const dedicatedLine = spaces.length === loc.start.column;
5620
+ if (dedicatedLine) {
5621
+ out += spaces;
5622
+ }
5623
+ const multiLine = loc.start.line !== loc.end.line;
5624
+ if (multiLine) {
5625
+ out += '\n' + spaces;
5626
+ }
5627
+ out += ';;' + value.replace(/\n\s*\*/g, '\n' + spaces + ';;') + '\n';
5628
+ if (!dedicatedLine) {
5629
+ out += ' ';
5630
+ }
5631
+ return out;
5632
+ }
5633
+ /**
5634
+ * Converts a CommentLine to WAT.
5635
+ * @param {import("@babel/types").CommentLine} node - The comment node.
5636
+ * @returns {string} WAT representation of the comment.
5637
+ */
5638
+ CommentLine(node) {
5639
+ const {
5640
+ value,
5641
+ loc
5642
+ } = node;
5643
+ if (this.lastCommentLineIndex === loc.start.index) {
5644
+ return '';
5645
+ }
5646
+ this.lastCommentLineIndex = loc.start.index;
5647
+ const spaces = this.spaces;
5648
+ const dedicatedLine = spaces.length === loc.start.column;
5649
+ let out = dedicatedLine ? '\n' + spaces : ' ';
5650
+ out += `;;${value}\n`;
5651
+ return out;
5652
+ }
5653
+ /**
5654
+ * Runs a function with a temporarily shifted indentation depth.
5655
+ * @param {number} extra - Additional indentation depth.
5656
+ * @param {() => string} fn - The function to run.
5657
+ * @returns {string} The result of the function.
5658
+ */
5659
+ atDepth(extra, fn) {
5660
+ this.numSpaces += extra;
5661
+ const ret = fn();
5662
+ this.numSpaces -= extra;
5663
+ return ret;
5664
+ }
5665
+ /**
5666
+ * Converts a Program node to WAT.
5667
+ * @param {import("@babel/types").Program} node - The Babel AST node.
5668
+ * @returns {string} WAT representation of the node.
5669
+ */
5670
+ Program(node) {
5671
+ const {
5672
+ body
5673
+ } = node;
5674
+ this.registerModuleData(body);
5675
+ this.registerClasses(body);
5676
+ const classNames = Object.keys(this.classes);
5677
+ const {
5678
+ spaces
5679
+ } = this;
5680
+ let out = `(module\n`;
5681
+ this.numSpaces++;
5682
+ const rest = body.filter(stmt => this.isModuleDataDeclaration(stmt) === false);
5683
+ out += this.mapToSource(rest).join('');
5684
+ const names = Object.keys(this.arrays);
5685
+ if (classNames.length) {
5686
+ out += `${this.spaces}(memory (export "m") 512)\n`;
5687
+ names.forEach(name => {
5688
+ const arr = this.arrays[name];
5689
+ out += `${this.spaces}(data (i32.const ${arr.offset}) ${this.f32DataBytes(arr.values)})\n`;
5690
+ });
5691
+ out += this.allocatorSource();
5692
+ } else if (names.length) {
5693
+ out += `${this.spaces}(memory (export "m") 1)\n`;
5694
+ names.forEach(name => {
5695
+ const arr = this.arrays[name];
5696
+ out += `${this.spaces}(data (i32.const ${arr.offset}) ${this.f32DataBytes(arr.values)})\n`;
5697
+ });
5698
+ }
5699
+ this.numSpaces--;
5700
+ out += `${spaces})\n`;
5701
+ return out;
5702
+ }
5703
+ /**
5704
+ * Registers top-level array/object literals as flat memory data.
5705
+ * @param {import("@babel/types").Statement[]} body - Top-level statements.
5706
+ */
5707
+ registerModuleData(body) {
5708
+ let offset = 0;
5709
+ body.forEach(stmt => {
5710
+ if (stmt.type !== 'VariableDeclaration' || stmt.kind !== 'const') {
5711
+ return;
5712
+ }
5713
+ const decl = stmt.declarations[0];
5714
+ if (!decl || decl.id.type !== 'Identifier') {
5715
+ return;
5716
+ }
5717
+ const {
5718
+ init
5719
+ } = decl;
5720
+ if (!init) {
5721
+ return;
5722
+ }
5723
+ if (init.type === 'ArrayExpression') {
5724
+ const values = init.elements.map(e => (e == null ? void 0 : e.type) === 'NumericLiteral' ? e.value : 0.0);
5725
+ this.arrays[decl.id.name] = {
5726
+ offset,
5727
+ values,
5728
+ isObject: false
5729
+ };
5730
+ offset += values.length * 4;
5731
+ } else if (init.type === 'ObjectExpression') {
5732
+ /** @type {Map<string, number>} */
5733
+ const keys = new Map();
5734
+ /** @type {number[]} */
5735
+ const values = [];
5736
+ init.properties.forEach((prop, i) => {
5737
+ if (prop.type !== 'ObjectProperty' || prop.key.type !== 'Identifier' || prop.value.type !== 'NumericLiteral') {
5738
+ return;
5739
+ }
5740
+ keys.set(prop.key.name, i);
5741
+ values.push(prop.value.value);
5742
+ });
5743
+ this.arrays[decl.id.name] = {
5744
+ offset,
5745
+ values,
5746
+ isObject: true,
5747
+ keys
5748
+ };
5749
+ offset += values.length * 4;
5750
+ }
5751
+ });
5752
+ }
5753
+ /**
5754
+ * Checks whether a statement is a module-level array/object constant declaration.
5755
+ * @param {import("@babel/types").Statement} stmt - The statement.
5756
+ * @returns {boolean} True if the statement is a module-level data declaration.
5757
+ */
5758
+ isModuleDataDeclaration(stmt) {
5759
+ if (stmt.type !== 'VariableDeclaration' || stmt.kind !== 'const') {
5760
+ return false;
5761
+ }
5762
+ const decl = stmt.declarations[0];
5763
+ return !!decl && decl.id.type === 'Identifier' && !!this.arrays[decl.id.name];
5764
+ }
5765
+ /**
5766
+ * Serializes the f32 values of an array into raw little-endian bytes for a data segment.
5767
+ * @param {number[]} values - The f32 values.
5768
+ * @returns {string} The escaped byte string.
5769
+ */
5770
+ f32DataBytes(values) {
5771
+ const bytes = new Uint8Array(new Float32Array(values).buffer);
5772
+ let out = '"';
5773
+ for (let i = 0; i < bytes.length; i++) {
5774
+ out += '\\' + bytes[i].toString(16).padStart(2, '0');
5775
+ }
5776
+ return out + '"';
5777
+ }
5778
+ /**
5779
+ * Registers top-level class declarations and their instance memory layout.
5780
+ * @param {import("@babel/types").Statement[]} body - Top-level statements.
5781
+ */
5782
+ registerClasses(body) {
5783
+ body.forEach(stmt => {
5784
+ let decl = stmt;
5785
+ if (stmt.type === 'ExportNamedDeclaration' && stmt.declaration) {
5786
+ decl = stmt.declaration;
5787
+ }
5788
+ if (decl.type === 'ClassDeclaration') {
5789
+ this.registerClass(decl);
5790
+ }
5791
+ });
5792
+ }
5793
+ /**
5794
+ * Scans one class for instance fields, defaults and methods and stores its memory layout.
5795
+ * @param {import("@babel/types").ClassDeclaration} node - The class declaration.
5796
+ */
5797
+ registerClass(node) {
5798
+ const {
5799
+ id,
5800
+ body
5801
+ } = node;
5802
+ const cls = {
5803
+ fields: [],
5804
+ offsets: {},
5805
+ defaults: {},
5806
+ methods: new Set(),
5807
+ size: 0
5808
+ };
5809
+ body.body.forEach(member => {
5810
+ if (member.static) {
5811
+ return;
5812
+ }
5813
+ if (member.type === 'ClassMethod' || member.type === 'MethodDefinition') {
5814
+ const name = member.key.type === 'Identifier' ? member.key.name : member.key.value;
5815
+ if (member.kind === 'constructor') {
5816
+ var _member$value;
5817
+ const methodBody = ((_member$value = member.value) == null ? void 0 : _member$value.body) || member.body;
5818
+ if (methodBody) {
5819
+ this.collectConstructorFields(methodBody, cls);
5820
+ }
5821
+ } else {
5822
+ cls.methods.add(name);
5823
+ }
5824
+ } else if (member.type === 'ClassProperty' || member.type === 'PropertyDefinition') {
5825
+ const name = member.key.type === 'Identifier' ? member.key.name : member.key.value;
5826
+ this.addClassField(cls, name);
5827
+ if (member.value) {
5828
+ cls.defaults[name] = member.value;
5829
+ }
5830
+ }
5831
+ });
5832
+ if (cls.size || cls.methods.size) {
5833
+ this.classes[id.name] = cls;
5834
+ }
5835
+ }
5836
+ /**
5837
+ * Adds a class field at the next available byte offset.
5838
+ * @param {Object} cls - The class layout.
5839
+ * @param {string} name - The field name.
5840
+ */
5841
+ addClassField(cls, name) {
5842
+ if (name in cls.offsets) {
5843
+ return;
5844
+ }
5845
+ const index = cls.fields.length;
5846
+ cls.offsets[name] = index * 4;
5847
+ cls.fields.push(name);
5848
+ cls.size = (index + 1) * 4;
5849
+ }
5850
+ /**
5851
+ * Collects class fields assigned in a constructor from `this.name = ...` stores.
5852
+ * @param {import("@babel/types").BlockStatement} body - The constructor body.
5853
+ * @param {Object} cls - The class layout.
5854
+ */
5855
+ collectConstructorFields(body, cls) {
5856
+ this.walk(body, node => {
5857
+ const target = node.type === 'AssignmentExpression' ? node.left : node.type === 'UpdateExpression' ? node.argument : null;
5858
+ if (!target || target.type !== 'MemberExpression') {
5859
+ return;
5860
+ }
5861
+ const isThis = target.object.type === 'ThisExpression' || target.object.type === 'Identifier' && target.object.name === 'this';
5862
+ if (!isThis) {
5863
+ return;
5864
+ }
5865
+ const name = target.property.type === 'Identifier' ? target.property.name : target.property.value;
5866
+ this.addClassField(cls, name);
5867
+ });
5868
+ }
5869
+ /**
5870
+ * Walks a Babel node tree, invoking a callback for every node.
5871
+ * @param {Node} node - The node to walk.
5872
+ * @param {(node: Node) => void} fn - The callback.
5873
+ */
5874
+ walk(node, fn) {
5875
+ if (!node) {
5876
+ return;
5877
+ }
5878
+ fn(node);
5879
+ for (const key of Object.keys(node)) {
5880
+ if (key === 'leadingComments' || key === 'trailingComments' || key === 'loc' || key === 'start' || key === 'end') {
5881
+ continue;
5882
+ }
5883
+ const child = node[key];
5884
+ if (Array.isArray(child)) {
5885
+ child.forEach(c => this.walk(c, fn));
5886
+ } else if (child && typeof child.type === 'string') {
5887
+ this.walk(child, fn);
5888
+ }
5889
+ }
5890
+ }
5891
+ /**
5892
+ * Computes the first unused byte, after all module-level array/object data.
5893
+ * @returns {number} The heap start offset.
5894
+ */
5895
+ heapStart() {
5896
+ let max = 0;
5897
+ Object.values(this.arrays).forEach(arr => {
5898
+ const end = arr.offset + arr.values.length * 4;
5899
+ if (end > max) {
5900
+ max = end;
5901
+ }
5902
+ });
5903
+ return max;
5904
+ }
5905
+ /**
5906
+ * Emits the bump allocator: a heap pointer global and an `$alloc` function.
5907
+ * @returns {string} WAT source of the heap global and allocator.
5908
+ */
5909
+ allocatorSource() {
5910
+ const {
5911
+ spaces
5912
+ } = this;
5913
+ let out = `${spaces}(global $heapPtr (mut i32) (i32.const ${this.heapStart()}))\n`;
5914
+ out += `${spaces}(func $alloc (param $size i32) (result i32)\n`;
5915
+ this.numSpaces++;
5916
+ out += `${this.spaces}(local $result i32)\n`;
5917
+ out += `${this.spaces}(local.set $result (global.get $heapPtr))\n`;
5918
+ out += `${this.spaces}(global.set $heapPtr (i32.add (global.get $heapPtr) (local.get $size)))\n`;
5919
+ out += `${this.spaces}(local.get $result)\n`;
5920
+ this.numSpaces--;
5921
+ out += `${spaces})\n`;
5922
+ return out;
5923
+ }
5924
+ /**
5925
+ * Converts an ExportNamedDeclaration node to WAT.
5926
+ * @param {import("@babel/types").ExportNamedDeclaration} node - The Babel AST node.
5927
+ * @returns {string} WAT representation of the node.
5928
+ */
5929
+ ExportNamedDeclaration(node) {
5930
+ const {
5931
+ declaration
5932
+ } = node;
5933
+ let out = '';
5934
+ if (declaration && declaration.type === 'FunctionDeclaration') {
5935
+ out += this.toSource(declaration);
5936
+ const funcName = declaration.id.name;
5937
+ out += `${this.spaces}(export "${funcName}" (func $${funcName}))\n`;
5938
+ } else if (declaration && declaration.type === 'ClassDeclaration') {
5939
+ out += this.toSource(declaration);
5940
+ } else {
5941
+ out += `;; Unsupported ExportNamedDeclaration: ${JSON.stringify(node)}\n`;
5942
+ }
5943
+ return out;
5944
+ }
5945
+ /**
5946
+ * Extracts the numeric WAT type from JSDoc comments (e.g. `@param {i32} n`).
5947
+ * Looks at the function itself and its ancestors, since Babel often attaches
5948
+ * the comment to the wrapping ExportNamedDeclaration instead.
5949
+ * @param {import("@babel/types").FunctionDeclaration} node - The function declaration.
5950
+ * @returns {'f32'|'f64'|'i32'|'i64'} The numeric type.
5951
+ */
5952
+ getFuncNumericType(node) {
5953
+ let comments = node.leadingComments || [];
5954
+ if (!comments.length) {
5955
+ for (const parent of this.parents) {
5956
+ var _parent$leadingCommen;
5957
+ if ((_parent$leadingCommen = parent.leadingComments) != null && _parent$leadingCommen.length) {
5958
+ comments = parent.leadingComments;
5959
+ break;
5960
+ }
5961
+ }
5962
+ }
5963
+ const text = comments.map(c => c.value).join('\n');
5964
+ const regex = /@(?:param|returns)\s*\{([^}]+)\}/g;
5965
+ let match;
5966
+ while ((match = regex.exec(text)) !== null) {
5967
+ const found = this.jsTypeToWat(match[1].trim().toLowerCase());
5968
+ if (found) {
5969
+ return found;
5970
+ }
5971
+ }
5972
+ return 'f32';
5973
+ }
5974
+ /**
5975
+ * Maps a JSDoc type name to a WAT numeric type.
5976
+ * @param {string} type - The JSDoc type name.
5977
+ * @returns {'f32'|'f64'|'i32'|'i64'|null} The WAT type or null if not numeric.
5978
+ */
5979
+ jsTypeToWat(type) {
5980
+ if (type === 'i32' || type === 'int' || type === 'integer') {
5981
+ return 'i32';
5982
+ }
5983
+ if (type === 'i64' || type === 'long') {
5984
+ return 'i64';
5985
+ }
5986
+ if (type === 'f64' || type === 'double') {
5987
+ return 'f64';
5988
+ }
5989
+ if (type === 'f32' || type === 'float' || type === 'number') {
5990
+ return 'f32';
5991
+ }
5992
+ return null;
5993
+ }
5994
+ /**
5995
+ * Converts a FunctionDeclaration node to WAT.
5996
+ * @param {import("@babel/types").FunctionDeclaration} node - The Babel AST node.
5997
+ * @returns {string} WAT representation of the node.
5998
+ */
5999
+ FunctionDeclaration(node) {
6000
+ const {
6001
+ id,
6002
+ params,
6003
+ body
6004
+ } = node;
6005
+ const funcName = id.name;
6006
+ const savedType = this.currentType;
6007
+ this.currentType = this.getFuncNumericType(node);
6008
+ const t = this.currentType;
6009
+ const paramList = params.map(param => `(param $${param.name} ${t})`).join(' ');
6010
+ const returnType = `(result ${t})`;
6011
+ let out = `${this.spaces}(func $${funcName}${paramList ? ' ' + paramList : ''} ${returnType}\n`;
6012
+ this.numSpaces++;
6013
+ const localNames = this.collectLocalNames(body);
6014
+ localNames.forEach(localName => {
6015
+ out += `${this.spaces}(local $${localName} ${t})\n`;
6016
+ });
6017
+ this.parents.push({
6018
+ type: 'BlockStatement'
6019
+ });
6020
+ out += `${this.spaces}(block $exit (result ${t})\n`;
6021
+ this.numSpaces++;
6022
+ out += body.body.map(statement => this.toSource(statement)).join('');
6023
+ this.numSpaces--;
6024
+ out += `${this.spaces})\n`;
6025
+ this.parents.pop();
6026
+ this.numSpaces--;
6027
+ out += `${this.spaces})\n`;
6028
+ this.currentType = savedType;
6029
+ return out;
6030
+ }
6031
+ /**
6032
+ * Converts a class constructor to a `$<Name>_new` factory that allocates a slot
6033
+ * in the bump heap and returns the instance pointer (as f32).
6034
+ * @param {import("@babel/types").ClassDeclaration} node - The class declaration.
6035
+ * @param {Object} cls - The registered class layout.
6036
+ * @returns {string} WAT source of the factory function.
6037
+ */
6038
+ classConstructorSource(node, cls) {
6039
+ var _ctor$value;
6040
+ const {
6041
+ spaces
6042
+ } = this;
6043
+ const {
6044
+ id,
6045
+ body
6046
+ } = node;
6047
+ const clsName = id.name;
6048
+ const ctor = body.body.find(m => (m.type === 'ClassMethod' || m.type === 'MethodDefinition') && m.kind === 'constructor' && !m.static);
6049
+ const savedType = this.currentType;
6050
+ this.currentType = 'f32';
6051
+ const savedClass = this.currentClass;
6052
+ this.currentClass = clsName;
6053
+ const ctorParams = ctor ? ((_ctor$value = ctor.value) == null ? void 0 : _ctor$value.params) || ctor.params : [];
6054
+ const paramList = ctorParams.map(p => `(param $${p.name} f32)`).join(' ');
6055
+ let out = `${spaces}(func $${clsName}_new${paramList ? ' ' + paramList : ''} (result f32)\n`;
6056
+ this.numSpaces++;
6057
+ out += `${this.spaces}(local $this f32)\n`;
6058
+ if (ctor) {
6059
+ var _ctor$value2;
6060
+ const ctorBody = ((_ctor$value2 = ctor.value) == null ? void 0 : _ctor$value2.body) || ctor.body;
6061
+ const localNames = this.collectLocalNames(ctorBody);
6062
+ localNames.forEach(name => {
6063
+ out += `${this.spaces}(local $${name} f32)\n`;
6064
+ });
6065
+ }
6066
+ out += `${this.spaces}(local.set $this\n`;
6067
+ this.numSpaces++;
6068
+ out += `${this.spaces}(f32.convert_i32_u\n`;
6069
+ this.numSpaces++;
6070
+ out += `${this.spaces}(call $alloc\n`;
6071
+ this.numSpaces++;
6072
+ out += `${this.spaces}(i32.const ${cls.size})\n`;
6073
+ this.numSpaces--;
6074
+ out += `${this.spaces})\n`;
6075
+ this.numSpaces--;
6076
+ out += `${this.spaces})\n`;
6077
+ this.numSpaces--;
6078
+ out += `${this.spaces})\n`;
6079
+ cls.fields.forEach(name => {
6080
+ if (cls.defaults[name]) {
6081
+ out += this.classFieldStoreSource('this', cls.defaults[name], name, cls);
6082
+ }
6083
+ });
6084
+ if (ctor) {
6085
+ var _ctor$value3;
6086
+ const ctorBody = ((_ctor$value3 = ctor.value) == null ? void 0 : _ctor$value3.body) || ctor.body;
6087
+ out += this.mapToSource(ctorBody.body).join('');
6088
+ }
6089
+ out += `${this.spaces}(local.get $this)\n`;
6090
+ this.numSpaces--;
6091
+ out += `${spaces})\n`;
6092
+ this.currentType = savedType;
6093
+ this.currentClass = savedClass;
6094
+ return out;
6095
+ }
6096
+ /**
6097
+ * Converts a class method to a `$<Name>_<method>` function taking `$this` first.
6098
+ * @param {string} clsName - The class name.
6099
+ * @param {import("@babel/types").ClassMethod} method - The method definition.
6100
+ * @returns {string} WAT source of the method function.
6101
+ */
6102
+ classMethodSource(clsName, method) {
6103
+ var _method$value, _method$value2;
6104
+ const {
6105
+ spaces
6106
+ } = this;
6107
+ const methodName = method.key.type === 'Identifier' ? method.key.name : method.key.value;
6108
+ const fnParams = ((_method$value = method.value) == null ? void 0 : _method$value.params) || method.params;
6109
+ const fnBody = ((_method$value2 = method.value) == null ? void 0 : _method$value2.body) || method.body;
6110
+ const savedType = this.currentType;
6111
+ this.currentType = 'f32';
6112
+ const paramNames = ['this', ...fnParams.map(p => p.name)];
6113
+ const paramList = paramNames.map(name => `(param $${name} f32)`).join(' ');
6114
+ let out = `${spaces}(func $${clsName}_${methodName} ${paramList} (result f32)\n`;
6115
+ this.numSpaces++;
6116
+ const localNames = this.collectLocalNames(fnBody);
6117
+ localNames.forEach(name => {
6118
+ out += `${this.spaces}(local $${name} f32)\n`;
6119
+ });
6120
+ out += `${this.spaces}(block $exit (result f32)\n`;
6121
+ this.numSpaces++;
6122
+ out += this.mapToSource(fnBody.body).join('');
6123
+ const last = fnBody.body[fnBody.body.length - 1];
6124
+ if (!(last && last.type === 'ReturnStatement')) {
6125
+ out += `${this.spaces}(f32.const 0)\n`;
6126
+ }
6127
+ this.numSpaces--;
6128
+ out += `${this.spaces})\n`;
6129
+ this.numSpaces--;
6130
+ out += `${this.spaces})\n`;
6131
+ this.currentType = savedType;
6132
+ return out;
6133
+ }
6134
+ /**
6135
+ * Converts a ClassDeclaration node to its factory and method functions.
6136
+ * @param {import("@babel/types").ClassDeclaration} node - The class declaration.
6137
+ * @returns {string} WAT representation of the class.
6138
+ */
6139
+ ClassDeclaration(node) {
6140
+ const clsName = node.id.name;
6141
+ const cls = this.classes[clsName];
6142
+ const {
6143
+ spaces
6144
+ } = this;
6145
+ if (!cls) {
6146
+ return `${spaces};; Unsupported class declaration: ${JSON.stringify(node)}\n`;
6147
+ }
6148
+ let out = this.classConstructorSource(node, cls);
6149
+ node.body.body.forEach(member => {
6150
+ if (member.type !== 'ClassMethod' && member.type !== 'MethodDefinition' || member.kind === 'constructor' || member.static) {
6151
+ return;
6152
+ }
6153
+ const savedClass = this.currentClass;
6154
+ this.currentClass = clsName;
6155
+ out += this.classMethodSource(clsName, member);
6156
+ this.currentClass = savedClass;
6157
+ });
6158
+ return out;
6159
+ }
6160
+ /**
6161
+ * Collects the names of all variables declared inside a function body.
6162
+ * @param {import("@babel/types").BlockStatement} body - The function body.
6163
+ * @returns {string[]} Names of the declared locals.
6164
+ */
6165
+ collectLocalNames(body) {
6166
+ /** @type {string[]} */
6167
+ const localNames = [];
6168
+ const visit = node => {
6169
+ if (!node) {
6170
+ return;
6171
+ }
6172
+ const {
6173
+ type
6174
+ } = node;
6175
+ if (type === 'VariableDeclaration') {
6176
+ node.declarations.forEach(decl => {
6177
+ if (decl.id.type === 'Identifier' && !localNames.includes(decl.id.name)) {
6178
+ localNames.push(decl.id.name);
6179
+ }
6180
+ });
6181
+ return;
6182
+ }
6183
+ for (const key of Object.keys(node)) {
6184
+ if (key === 'leadingComments' || key === 'trailingComments' || key === 'loc' || key === 'start' || key === 'end') {
6185
+ continue;
6186
+ }
6187
+ const child = node[key];
6188
+ if (Array.isArray(child)) {
6189
+ child.forEach(visit);
6190
+ } else if (child && typeof child.type === 'string') {
6191
+ visit(child);
6192
+ }
6193
+ }
6194
+ };
6195
+ visit(body);
6196
+ return localNames;
6197
+ }
6198
+ /**
6199
+ * Converts a BlockStatement node to WAT.
6200
+ * @param {import("@babel/types").BlockStatement} node - The Babel AST node.
6201
+ * @returns {string} WAT representation of the node.
6202
+ */
6203
+ BlockStatement(node) {
6204
+ const {
6205
+ body
6206
+ } = node;
6207
+ return this.mapToSource(body).join('');
6208
+ }
6209
+ /**
6210
+ * Checks whether an expression already produces an i32 boolean test result,
6211
+ * so it can be fed to `if`/`br_if` without a truthiness conversion.
6212
+ * @param {Node} expr - The expression.
6213
+ * @returns {boolean} True if the expression yields an i32 test result.
6214
+ */
6215
+ isBooleanI32(expr) {
6216
+ if (expr.type === 'BinaryExpression') {
6217
+ return CMP_OPERATORS.includes(expr.operator);
6218
+ }
6219
+ if (expr.type === 'UnaryExpression' && expr.operator === '!') {
6220
+ return true;
6221
+ }
6222
+ if (expr.type === 'LogicalExpression') {
6223
+ return this.isBooleanI32(expr.left) && this.isBooleanI32(expr.right);
6224
+ }
6225
+ return false;
6226
+ }
6227
+ /**
6228
+ * Produces a WAT i32 test expression for an arbitrary numeric condition.
6229
+ * @param {Node} expr - The condition expression.
6230
+ * @returns {string} WAT source that pushes an i32.
6231
+ */
6232
+ getTestSource(expr) {
6233
+ if (this.isBooleanI32(expr)) {
6234
+ return this.toSource(expr);
6235
+ }
6236
+ const {
6237
+ spaces
6238
+ } = this;
6239
+ const t = this.currentType;
6240
+ let out = `${spaces}(${NE_OP[t]}\n`;
6241
+ this.numSpaces++;
6242
+ out += this.toSource(expr);
6243
+ out += `${this.spaces}(${CONST_TYPE[t]} 0)\n`;
6244
+ this.numSpaces--;
6245
+ out += `${spaces})\n`;
6246
+ return out;
6247
+ }
6248
+ /**
6249
+ * Converts a loop/conditional body, accepting either a block or a single statement.
6250
+ * @param {Node} body - The body (BlockStatement or single statement).
6251
+ * @returns {string} WAT source of the body.
6252
+ */
6253
+ bodySource(body) {
6254
+ if (body.type === 'BlockStatement') {
6255
+ return this.mapToSource(body.body).join('');
6256
+ }
6257
+ return this.toSource(body);
6258
+ }
6259
+ /**
6260
+ * Converts an IfStatement node to WAT.
6261
+ * @param {import("@babel/types").IfStatement} node - The Babel AST node.
6262
+ * @returns {string} WAT representation of the node.
6263
+ */
6264
+ IfStatement(node) {
6265
+ const {
6266
+ test,
6267
+ consequent,
6268
+ alternate
6269
+ } = node;
6270
+ const {
6271
+ spaces
6272
+ } = this;
6273
+ let out = `${spaces}(if\n`;
6274
+ this.numSpaces++;
6275
+ out += this.getTestSource(test);
6276
+ out += `${this.spaces}(then\n`;
6277
+ this.numSpaces++;
6278
+ out += this.bodySource(consequent);
6279
+ this.numSpaces--;
6280
+ out += `${this.spaces})\n`;
6281
+ if (alternate) {
6282
+ out += `${this.spaces}(else\n`;
6283
+ this.numSpaces++;
6284
+ out += this.bodySource(alternate);
6285
+ this.numSpaces--;
6286
+ out += `${this.spaces})\n`;
6287
+ }
6288
+ this.numSpaces--;
6289
+ out += `${spaces})\n`;
6290
+ return out;
6291
+ }
6292
+ /**
6293
+ * Registers the current loop as innermost and returns its label names.
6294
+ * @param {'while'|'for'|'do'} kind - The loop kind.
6295
+ * @param {Node} [update] - The for-loop update expression, regenerated inline at each continue.
6296
+ * @returns {{exit: string, top: string, updateSrc: string}} The loop labels.
6297
+ */
6298
+ pushLoop(kind, update) {
6299
+ const id = this.loopCounter++;
6300
+ const entry = {
6301
+ exit: `$exit${id}`,
6302
+ top: `$top${id}`,
6303
+ updateSrc: kind === 'for' && update ? this.toSource(update) : ''
6304
+ };
6305
+ this.loops.push(entry);
6306
+ return entry;
6307
+ }
6308
+ /**
6309
+ * Removes the innermost loop from the stack.
6310
+ * @returns {{exit: string, top: string, updateSrc: string}|undefined} The popped loop labels.
6311
+ */
6312
+ popLoop() {
6313
+ return this.loops.pop();
6314
+ }
6315
+ /**
6316
+ * Converts a WhileStatement node to WAT.
6317
+ * @param {import("@babel/types").WhileStatement} node - The Babel AST node.
6318
+ * @returns {string} WAT representation of the node.
6319
+ */
6320
+ WhileStatement(node) {
6321
+ const {
6322
+ test,
6323
+ body
6324
+ } = node;
6325
+ const {
6326
+ spaces
6327
+ } = this;
6328
+ const labels = this.pushLoop('while');
6329
+ let out = `${spaces}(block ${labels.exit}\n`;
6330
+ this.numSpaces++;
6331
+ out += `${this.spaces}(loop ${labels.top}\n`;
6332
+ this.numSpaces++;
6333
+ out += this.getTestSource(test);
6334
+ out += `${this.spaces}(i32.eqz)\n`;
6335
+ out += `${this.spaces}(br_if ${labels.exit})\n`;
6336
+ out += this.bodySource(body);
6337
+ out += `${this.spaces}(br ${labels.top})\n`;
6338
+ this.numSpaces--;
6339
+ out += `${this.spaces})\n`;
6340
+ this.numSpaces--;
6341
+ out += `${spaces})\n`;
6342
+ this.popLoop();
6343
+ return out;
6344
+ }
6345
+ /**
6346
+ * Converts a ForStatement node to WAT.
6347
+ * @param {import("@babel/types").ForStatement} node - The Babel AST node.
6348
+ * @returns {string} WAT representation of the node.
6349
+ */
6350
+ ForStatement(node) {
6351
+ const {
6352
+ init,
6353
+ test,
6354
+ update,
6355
+ body
6356
+ } = node;
6357
+ const {
6358
+ spaces
6359
+ } = this;
6360
+ let out = init ? this.toSource(init) : '';
6361
+ const labels = this.pushLoop('for', update);
6362
+ out += `${spaces}(block ${labels.exit}\n`;
6363
+ this.numSpaces++;
6364
+ out += `${this.spaces}(loop ${labels.top}\n`;
6365
+ this.numSpaces++;
6366
+ if (test) {
6367
+ out += this.getTestSource(test);
6368
+ out += `${this.spaces}(i32.eqz)\n`;
6369
+ out += `${this.spaces}(br_if ${labels.exit})\n`;
6370
+ }
6371
+ out += this.bodySource(body);
6372
+ if (labels.updateSrc) {
6373
+ out += labels.updateSrc;
6374
+ }
6375
+ out += `${this.spaces}(br ${labels.top})\n`;
6376
+ this.numSpaces--;
6377
+ out += `${this.spaces})\n`;
6378
+ this.numSpaces--;
6379
+ out += `${spaces})\n`;
6380
+ this.popLoop();
6381
+ return out;
6382
+ }
6383
+ /**
6384
+ * Converts a DoWhileStatement node to WAT.
6385
+ * @param {import("@babel/types").DoWhileStatement} node - The Babel AST node.
6386
+ * @returns {string} WAT representation of the node.
6387
+ */
6388
+ DoWhileStatement(node) {
6389
+ const {
6390
+ test,
6391
+ body
6392
+ } = node;
6393
+ const {
6394
+ spaces
6395
+ } = this;
6396
+ const labels = this.pushLoop('do');
6397
+ let out = `${spaces}(block ${labels.exit}\n`;
6398
+ this.numSpaces++;
6399
+ out += `${this.spaces}(loop ${labels.top}\n`;
6400
+ this.numSpaces++;
6401
+ out += this.bodySource(body);
6402
+ out += this.getTestSource(test);
6403
+ out += `${this.spaces}(i32.eqz)\n`;
6404
+ out += `${this.spaces}(br_if ${labels.exit})\n`;
6405
+ out += `${this.spaces}(br ${labels.top})\n`;
6406
+ this.numSpaces--;
6407
+ out += `${this.spaces})\n`;
6408
+ this.numSpaces--;
6409
+ out += `${spaces})\n`;
6410
+ this.popLoop();
6411
+ return out;
6412
+ }
6413
+ /**
6414
+ * Converts a BreakStatement node to WAT.
6415
+ * @param {import("@babel/types").BreakStatement} node - The Babel AST node.
6416
+ * @returns {string} WAT representation of the node.
6417
+ */
6418
+ BreakStatement(node) {
6419
+ const {
6420
+ spaces
6421
+ } = this;
6422
+ if (node.label) {
6423
+ return `${spaces}(br $exit) ;; Unsupported labeled break "${node.label.name}"\n`;
6424
+ }
6425
+ const labels = this.loops[this.loops.length - 1];
6426
+ if (labels === undefined) {
6427
+ return `${spaces};; Unsupported break outside loop\n`;
6428
+ }
6429
+ return `${spaces}(br ${labels.exit})\n`;
6430
+ }
6431
+ /**
6432
+ * Converts a ContinueStatement node to WAT.
6433
+ * @param {import("@babel/types").ContinueStatement} node - The Babel AST node.
6434
+ * @returns {string} WAT representation of the node.
6435
+ */
6436
+ ContinueStatement(node) {
6437
+ const {
6438
+ spaces
6439
+ } = this;
6440
+ if (node.label) {
6441
+ return `${spaces};; Unsupported labeled continue "${node.label.name}"\n`;
6442
+ }
6443
+ const labels = this.loops[this.loops.length - 1];
6444
+ if (labels === undefined) {
6445
+ return `${spaces};; Unsupported continue outside loop\n`;
6446
+ }
6447
+ let out = '';
6448
+ if (labels.updateSrc) {
6449
+ out += labels.updateSrc;
6450
+ }
6451
+ out += `${spaces}(br ${labels.top})\n`;
6452
+ return out;
6453
+ }
6454
+ /**
6455
+ * Converts a ReturnStatement node to WAT.
6456
+ * @param {import("@babel/types").ReturnStatement} node - The Babel AST node.
6457
+ * @returns {string} WAT representation of the node.
6458
+ */
6459
+ ReturnStatement(node) {
6460
+ const {
6461
+ argument
6462
+ } = node;
6463
+ const {
6464
+ spaces
6465
+ } = this;
6466
+ let out = `${spaces}(return\n`;
6467
+ this.numSpaces++;
6468
+ let expr = '';
6469
+ if (!argument) {
6470
+ expr = `${this.spaces}(${CONST_TYPE[this.currentType]} 0)\n`;
6471
+ } else {
6472
+ expr = this.toSource(argument);
6473
+ }
6474
+ out += expr;
6475
+ this.numSpaces--;
6476
+ out += `${spaces})\n`;
6477
+ return out;
6478
+ }
6479
+ /**
6480
+ * Converts a BinaryExpression node to WAT.
6481
+ * @param {import("@babel/types").BinaryExpression} node - The Babel AST node.
6482
+ * @returns {string} WAT representation of the node.
6483
+ */
6484
+ BinaryExpression(node) {
6485
+ const {
6486
+ left,
6487
+ operator,
6488
+ right
6489
+ } = node;
6490
+ const {
6491
+ spaces
6492
+ } = this;
6493
+ const t = this.currentType;
6494
+ if (operator === '%') {
6495
+ return this.percentToSource(node);
6496
+ }
6497
+ let opCode = '';
6498
+ if (CMP_OPERATORS.includes(operator)) {
6499
+ const opName = {
6500
+ '<': 'lt',
6501
+ '<=': 'le',
6502
+ '>': 'gt',
6503
+ '>=': 'ge',
6504
+ '==': 'eq',
6505
+ '!=': 'ne'
6506
+ }[operator];
6507
+ opCode = CMP_OP[t][opName];
6508
+ } else {
6509
+ const opName = {
6510
+ '+': 'add',
6511
+ '-': 'sub',
6512
+ '*': 'mul',
6513
+ '/': 'div'
6514
+ }[operator];
6515
+ if (!opName) {
6516
+ return `${spaces}(${CONST_TYPE[t]} 0.0) ;; Unsupported operator: ${operator}\n`;
6517
+ }
6518
+ opCode = ARITH_OP[t][opName];
6519
+ }
6520
+ let out = `${spaces}(${opCode}\n`;
6521
+ this.numSpaces++;
6522
+ out += this.toSource(left) + this.toSource(right);
6523
+ this.numSpaces--;
6524
+ out += `${spaces})\n`;
6525
+ return out;
6526
+ }
6527
+ /**
6528
+ * Converts a `%` (modulo) expression, emulating float modulo via truncation.
6529
+ * @param {import("@babel/types").BinaryExpression} node - The Babel AST node.
6530
+ * @returns {string} WAT representation of the node.
6531
+ */
6532
+ percentToSource(node) {
6533
+ const {
6534
+ left,
6535
+ right
6536
+ } = node;
6537
+ const {
6538
+ spaces
6539
+ } = this;
6540
+ const t = this.currentType;
6541
+ if (t === 'i32' || t === 'i64') {
6542
+ let _out = `${spaces}(${t}.rem_s\n`;
6543
+ this.numSpaces++;
6544
+ _out += this.toSource(left) + this.toSource(right);
6545
+ this.numSpaces--;
6546
+ _out += `${spaces})\n`;
6547
+ return _out;
6548
+ }
6549
+ const a = ARITH_OP[t];
6550
+ const leftDiv = this.atDepth(5, () => this.toSource(left));
6551
+ const rightDiv = this.atDepth(5, () => this.toSource(right));
6552
+ const leftTop = this.atDepth(1, () => this.toSource(left));
6553
+ const rightMul = this.atDepth(2, () => this.toSource(right));
6554
+ const indent = n => ' '.repeat(this.numSpaces + n);
6555
+ let out = `${spaces}(${a.add}\n`;
6556
+ out += leftTop;
6557
+ out += `${indent(1)}(${NEG_OP[t]}\n`;
6558
+ out += `${indent(2)}(${a.mul}\n`;
6559
+ out += `${indent(3)}(${MATH_OP[t].trunc}\n`;
6560
+ out += `${indent(4)}(${a.div}\n`;
6561
+ out += leftDiv + rightDiv;
6562
+ out += `${indent(4)})\n`;
6563
+ out += `${indent(3)})\n`;
6564
+ out += rightMul;
6565
+ out += `${indent(2)})\n`;
6566
+ out += `${indent(1)})\n`;
6567
+ out += `${spaces})\n`;
6568
+ return out;
6569
+ }
6570
+ /**
6571
+ * Converts an UnaryExpression node to WAT.
6572
+ * @param {import("@babel/types").UnaryExpression} node - The Babel AST node.
6573
+ * @returns {string} WAT representation of the node.
6574
+ */
6575
+ UnaryExpression(node) {
6576
+ const {
6577
+ argument,
6578
+ operator
6579
+ } = node;
6580
+ const {
6581
+ spaces
6582
+ } = this;
6583
+ const t = this.currentType;
6584
+ if (operator === '-') {
6585
+ if (NEG_OP[t]) {
6586
+ let _out2 = `${spaces}(${NEG_OP[t]}\n`;
6587
+ this.numSpaces++;
6588
+ _out2 += this.toSource(argument);
6589
+ this.numSpaces--;
6590
+ _out2 += `${spaces})\n`;
6591
+ return _out2;
6592
+ }
6593
+ let out = `${spaces}(${ARITH_OP[t].sub}\n`;
6594
+ this.numSpaces++;
6595
+ out += `${this.spaces}(${CONST_TYPE[t]} 0)\n`;
6596
+ out += this.toSource(argument);
6597
+ this.numSpaces--;
6598
+ out += `${spaces})\n`;
6599
+ return out;
6600
+ }
6601
+ if (operator === '!') {
6602
+ const op = this.isBooleanI32(argument) ? 'i32.eqz' : EQZ_OP[t];
6603
+ let out = `${spaces}(${op}\n`;
6604
+ this.numSpaces++;
6605
+ out += this.toSource(argument);
6606
+ this.numSpaces--;
6607
+ out += `${spaces})\n`;
6608
+ return out;
6609
+ }
6610
+ return `${spaces}(${CONST_TYPE[t]} 0.0) ;; Unsupported unary operator: ${operator}\n`;
6611
+ }
6612
+ /**
6613
+ * Converts a VariableDeclaration node to WAT.
6614
+ * @param {import("@babel/types").VariableDeclaration} node - The Babel AST node.
6615
+ * @returns {string} WAT representation of the node.
6616
+ */
6617
+ VariableDeclaration(node) {
6618
+ const {
6619
+ declarations
6620
+ } = node;
6621
+ return declarations.map(decl => this.toSource(decl)).join('');
6622
+ }
6623
+ /**
6624
+ * Converts a VariableDeclarator node to WAT.
6625
+ * @param {import("@babel/types").VariableDeclarator} node - The Babel AST node.
6626
+ * @returns {string} WAT representation of the node.
6627
+ */
6628
+ VariableDeclarator(node) {
6629
+ const {
6630
+ id,
6631
+ init
6632
+ } = node;
6633
+ const {
6634
+ spaces
6635
+ } = this;
6636
+ if (id.type === 'Identifier' && init && init.type === 'NewExpression' && init.callee.type === 'Identifier' && this.classes[init.callee.name]) {
6637
+ this.instances[id.name] = init.callee.name;
6638
+ }
6639
+ let out = `${spaces}(local.set $${id.name}\n`;
6640
+ this.numSpaces++;
6641
+ if (init) {
6642
+ out += this.toSource(init);
6643
+ } else {
6644
+ out += `${this.spaces}(${CONST_TYPE[this.currentType]} 0) ;; Initialized as zero.\n`;
6645
+ }
6646
+ this.numSpaces--;
6647
+ out += `${spaces})\n`;
6648
+ return out;
6649
+ }
6650
+ /**
6651
+ * Converts an UpdateExpression node to WAT.
6652
+ * @param {import("@babel/types").UpdateExpression} node - The Babel AST node.
6653
+ * @returns {string} WAT representation of the node.
6654
+ */
6655
+ UpdateExpression(node) {
6656
+ const {
6657
+ argument,
6658
+ operator
6659
+ } = node;
6660
+ const {
6661
+ spaces
6662
+ } = this;
6663
+ const t = this.currentType;
6664
+ if (argument.type !== 'Identifier') {
6665
+ return `${spaces}(${CONST_TYPE[t]} 0.0) ;; Unsupported UpdateExpression target ${argument.type}\n`;
6666
+ }
6667
+ const op = operator === '++' ? ARITH_OP[t].add : ARITH_OP[t].sub;
6668
+ let out = `${spaces}(local.set $${argument.name}\n`;
6669
+ this.numSpaces++;
6670
+ out += `${this.spaces}(${op}\n`;
6671
+ this.numSpaces++;
6672
+ out += `${this.spaces}(local.get $${argument.name})\n`;
6673
+ out += `${this.spaces}(${CONST_TYPE[t]} 1)\n`;
6674
+ this.numSpaces--;
6675
+ out += `${this.spaces})\n`;
6676
+ this.numSpaces--;
6677
+ out += `${spaces})\n`;
6678
+ return out;
6679
+ }
6680
+ /**
6681
+ * Converts an AssignmentExpression node to WAT.
6682
+ * @param {import("@babel/types").AssignmentExpression} node - The Babel AST node.
6683
+ * @returns {string} WAT representation of the node.
6684
+ */
6685
+ AssignmentExpression(node) {
6686
+ const {
6687
+ left,
6688
+ operator,
6689
+ right
6690
+ } = node;
6691
+ const {
6692
+ spaces
6693
+ } = this;
6694
+ if (operator !== '=') {
6695
+ return `${spaces}(${CONST_TYPE[this.currentType]} 0.0) ;; Unsupported assignment operator: ${operator}\n`;
6696
+ }
6697
+ if (left.type === 'Identifier') {
6698
+ let out = `${spaces}(local.set $${left.name}\n`;
6699
+ this.numSpaces++;
6700
+ out += this.toSource(right);
6701
+ this.numSpaces--;
6702
+ out += `${spaces})\n`;
6703
+ return out;
6704
+ }
6705
+ if (left.type === 'MemberExpression') {
6706
+ const member = left;
6707
+ const pointerName = this.instancePointerName(member.object);
6708
+ const fieldName = member.property.type === 'Identifier' ? member.property.name : member.property.value;
6709
+ if (pointerName) {
6710
+ const clsName = this.resolveInstanceClass(member.object, fieldName);
6711
+ if (clsName && fieldName in this.classes[clsName].offsets) {
6712
+ return this.classFieldStoreSource(pointerName, right, fieldName, this.classes[clsName]);
6713
+ }
6714
+ }
6715
+ const arr = member.object.type === 'Identifier' ? this.arrays[member.object.name] : null;
6716
+ if (arr) {
6717
+ const indexSrc = this.arrayIndexSource(arr, member);
6718
+ let out = `${spaces}(f32.store\n`;
6719
+ this.numSpaces++;
6720
+ out += this.arrayAddressSource(arr, indexSrc);
6721
+ out += this.toSource(right);
6722
+ this.numSpaces--;
6723
+ out += `${spaces})\n`;
6724
+ return out;
6725
+ }
6726
+ const objectName = member.object.type === 'Identifier' ? member.object.name : member.object.type;
6727
+ return `${spaces}(${CONST_TYPE[this.currentType]} 0.0) ;; Unsupported store target ${objectName}\n`;
6728
+ }
6729
+ return `${spaces}(${CONST_TYPE[this.currentType]} 0.0) ;; Unsupported assignment target ${left.type}\n`;
6730
+ }
6731
+ /**
6732
+ * Produces an i32 source for the element index of an array/object member access.
6733
+ * @param {Object} arr - The registered array/object data.
6734
+ * @param {import("@babel/types").MemberExpression} member - The member expression.
6735
+ * @returns {string} WAT source that pushes the element index.
6736
+ */
6737
+ arrayIndexSource(arr, member) {
6738
+ if (!arr.isObject) {
6739
+ if (!member.computed) {
6740
+ return `${this.spaces};; Unsupported ${member.object.name}.${member.property.name} on array\n`;
6741
+ }
6742
+ return this.toI32(member.property);
6743
+ }
6744
+ const key = member.computed && member.property.type === 'StringLiteral' ? member.property.value : member.property.name;
6745
+ const index = arr.keys.get(key);
6746
+ if (index === undefined) {
6747
+ return `${this.spaces}; Unknown key "${member.property.name}"\n`;
6748
+ }
6749
+ return `${this.spaces}(i32.const ${index})\n`;
6750
+ }
6751
+ /**
6752
+ * Truncates a numeric index expression to i32, passing i32 values through.
6753
+ * @param {Node} expr - The index expression.
6754
+ * @returns {string} WAT source that pushes an i32 index.
6755
+ */
6756
+ toI32(expr) {
6757
+ const {
6758
+ spaces
6759
+ } = this;
6760
+ const t = this.currentType;
6761
+ if (t === 'i32') {
6762
+ return this.toSource(expr);
6763
+ }
6764
+ if (t !== 'f32' && t !== 'f64') {
6765
+ return `${spaces};; Unsupported index type ${t}\n`;
6766
+ }
6767
+ const truncExpr = t === 'f32' ? 'i32.trunc_f32_s' : 'i32.trunc_f64_s';
6768
+ let out = `${spaces}(${truncExpr}\n`;
6769
+ this.numSpaces++;
6770
+ out += this.toSource(expr);
6771
+ this.numSpaces--;
6772
+ out += `${spaces})\n`;
6773
+ return out;
6774
+ }
6775
+ /**
6776
+ * Computes the byte address of `baseOffset + elementIndex * 4`.
6777
+ * @param {Object} arr - The registered array/object data.
6778
+ * @param {string} indexSrc - The i32 element index source.
6779
+ * @returns {string} WAT source that pushes the byte address.
6780
+ */
6781
+ arrayAddressSource(arr, indexSrc) {
6782
+ const {
6783
+ spaces
6784
+ } = this;
6785
+ let out = `${spaces}(i32.add\n`;
6786
+ this.numSpaces++;
6787
+ out += `${this.spaces}(i32.const ${arr.offset})\n`;
6788
+ out += `${this.spaces}(i32.mul\n`;
6789
+ this.numSpaces++;
6790
+ out += indexSrc;
6791
+ out += `${this.spaces}(i32.const 4)\n`;
6792
+ this.numSpaces--;
6793
+ out += `${this.spaces})\n`;
6794
+ this.numSpaces--;
6795
+ out += `${spaces})\n`;
6796
+ return out;
6797
+ }
6798
+ /**
6799
+ * Returns the WAT local name holding an instance pointer.
6800
+ * @param {Node} objectNode - The object expression.
6801
+ * @returns {string|null} The local name.
6802
+ */
6803
+ instancePointerName(objectNode) {
6804
+ const isThis = objectNode.type === 'ThisExpression' || objectNode.type === 'Identifier' && objectNode.name === 'this';
6805
+ return isThis ? 'this' : objectNode.type === 'Identifier' ? objectNode.name : null;
6806
+ }
6807
+ /**
6808
+ * Finds the class of an instance via `this`, a tracked `new` variable, or a unique field-name match.
6809
+ * @param {Node} objectNode - The member/call object expression.
6810
+ * @param {string} propertyName - The accessed field or method name.
6811
+ * @returns {string|null} The class name.
6812
+ */
6813
+ resolveInstanceClass(objectNode, propertyName) {
6814
+ const pointerName = this.instancePointerName(objectNode);
6815
+ if (pointerName === 'this') {
6816
+ return this.currentClass;
6817
+ }
6818
+ if (pointerName === null) {
6819
+ return null;
6820
+ }
6821
+ if (this.instances[pointerName]) {
6822
+ return this.instances[pointerName];
6823
+ }
6824
+ if (pointerName in this.arrays) {
6825
+ return null;
6826
+ }
6827
+ const clsNames = Object.keys(this.classes).filter(n => propertyName in this.classes[n].offsets);
6828
+ return clsNames.length === 1 ? clsNames[0] : null;
6829
+ }
6830
+ /**
6831
+ * Finds the class of the object a method call is dispatched on.
6832
+ * @param {Node} objectNode - The call object expression.
6833
+ * @param {string} methodName - The called method name.
6834
+ * @returns {string|null} The class name.
6835
+ */
6836
+ resolveInstanceMethodClass(objectNode, methodName) {
6837
+ const pointerName = this.instancePointerName(objectNode);
6838
+ if (pointerName === 'this') {
6839
+ return this.currentClass && this.classes[this.currentClass].methods.has(methodName) ? this.currentClass : null;
6840
+ }
6841
+ if (pointerName === null) {
6842
+ return null;
6843
+ }
6844
+ if (this.instances[pointerName]) {
6845
+ return this.instances[pointerName];
6846
+ }
6847
+ if (pointerName in this.arrays) {
6848
+ return null;
6849
+ }
6850
+ const clsNames = Object.keys(this.classes).filter(n => this.classes[n].methods.has(methodName));
6851
+ return clsNames.length === 1 ? clsNames[0] : null;
6852
+ }
6853
+ /**
6854
+ * Produces the WAT source for the heap byte address of a class field.
6855
+ * @param {string} pointerName - The local holding the instance pointer.
6856
+ * @param {string} fieldName - The field name.
6857
+ * @param {Object} cls - The class layout.
6858
+ * @returns {string} WAT source that pushes the byte address.
6859
+ */
6860
+ classFieldAddressSource(pointerName, fieldName, cls) {
6861
+ const {
6862
+ spaces
6863
+ } = this;
6864
+ let out = `${spaces}(i32.add\n`;
6865
+ this.numSpaces++;
6866
+ out += `${this.spaces}(i32.trunc_f32_s\n`;
6867
+ this.numSpaces++;
6868
+ out += `${this.spaces}(local.get $${pointerName})\n`;
6869
+ this.numSpaces--;
6870
+ out += `${this.spaces})\n`;
6871
+ out += `${this.spaces}(i32.const ${cls.offsets[fieldName]})\n`;
6872
+ this.numSpaces--;
6873
+ out += `${spaces})\n`;
6874
+ return out;
6875
+ }
6876
+ /**
6877
+ * Produces the WAT source for loading a class field from the heap.
6878
+ * @param {string} pointerName - The local holding the instance pointer.
6879
+ * @param {string} fieldName - The field name.
6880
+ * @param {Object} cls - The class layout.
6881
+ * @returns {string} WAT source that pushes the field value.
6882
+ */
6883
+ classFieldLoadSource(pointerName, fieldName, cls) {
6884
+ const {
6885
+ spaces
6886
+ } = this;
6887
+ let out = `${spaces}(f32.load\n`;
6888
+ this.numSpaces++;
6889
+ out += this.classFieldAddressSource(pointerName, fieldName, cls);
6890
+ this.numSpaces--;
6891
+ out += `${spaces})\n`;
6892
+ return out;
6893
+ }
6894
+ /**
6895
+ * Produces the WAT source for storing a value into a class field.
6896
+ * @param {string} pointerName - The local holding the instance pointer.
6897
+ * @param {Node} valueNode - The value expression.
6898
+ * @param {string} fieldName - The field name.
6899
+ * @param {Object} cls - The class layout.
6900
+ * @returns {string} WAT source that performs the store.
6901
+ */
6902
+ classFieldStoreSource(pointerName, valueNode, fieldName, cls) {
6903
+ const {
6904
+ spaces
6905
+ } = this;
6906
+ let out = `${spaces}(f32.store\n`;
6907
+ this.numSpaces++;
6908
+ out += this.classFieldAddressSource(pointerName, fieldName, cls);
6909
+ out += this.toSource(valueNode);
6910
+ this.numSpaces--;
6911
+ out += `${spaces})\n`;
6912
+ return out;
6913
+ }
6914
+ /**
6915
+ * Converts a MemberExpression node (array/object element read) to WAT.
6916
+ * @param {import("@babel/types").MemberExpression} node - The Babel AST node.
6917
+ * @returns {string} WAT representation of the node.
6918
+ */
6919
+ MemberExpression(node) {
6920
+ const {
6921
+ object,
6922
+ property
6923
+ } = node;
6924
+ const {
6925
+ spaces
6926
+ } = this;
6927
+ const pointerName = this.instancePointerName(object);
6928
+ const fieldName = property.type === 'Identifier' ? property.name : property.value;
6929
+ if (pointerName === 'this') {
6930
+ const _clsName = this.resolveInstanceClass(object, fieldName);
6931
+ if (_clsName && fieldName in this.classes[_clsName].offsets) {
6932
+ return this.classFieldLoadSource('this', fieldName, this.classes[_clsName]);
6933
+ }
6934
+ }
6935
+ if (object.type !== 'Identifier') {
6936
+ return `${spaces}(f32.const 0.0) ;; Unsupported member access on ${object.type}\n`;
6937
+ }
6938
+ const arr = this.arrays[object.name];
6939
+ if (arr) {
6940
+ const indexSrc = this.arrayIndexSource(arr, node);
6941
+ let out = `${spaces}(f32.load\n`;
6942
+ this.numSpaces++;
6943
+ out += this.arrayAddressSource(arr, indexSrc);
6944
+ this.numSpaces--;
6945
+ out += `${spaces})\n`;
6946
+ return out;
6947
+ }
6948
+ const clsName = this.resolveInstanceClass(object, fieldName);
6949
+ if (clsName && fieldName in this.classes[clsName].offsets) {
6950
+ return this.classFieldLoadSource(object.name, fieldName, this.classes[clsName]);
6951
+ }
6952
+ return `${spaces}(f32.const 0.0) ;; Unsupported member access on unknown "${object.name}"\n`;
6953
+ }
6954
+ /**
6955
+ * Converts a ConditionalExpression (ternary) node to WAT.
6956
+ * @param {import("@babel/types").ConditionalExpression} node - The Babel AST node.
6957
+ * @returns {string} WAT representation of the node.
6958
+ */
6959
+ ConditionalExpression(node) {
6960
+ const {
6961
+ test,
6962
+ consequent,
6963
+ alternate
6964
+ } = node;
6965
+ const {
6966
+ spaces
6967
+ } = this;
6968
+ const t = this.currentType;
6969
+ let out = `${spaces}(if (result ${t})\n`;
6970
+ this.numSpaces++;
6971
+ out += this.getTestSource(test);
6972
+ out += `${this.spaces}(then\n`;
6973
+ this.numSpaces++;
6974
+ out += this.toSource(consequent);
6975
+ this.numSpaces--;
6976
+ out += `${this.spaces})\n`;
6977
+ out += `${this.spaces}(else\n`;
6978
+ this.numSpaces++;
6979
+ out += this.toSource(alternate);
6980
+ this.numSpaces--;
6981
+ out += `${this.spaces})\n`;
6982
+ this.numSpaces--;
6983
+ out += `${spaces})\n`;
6984
+ return out;
6985
+ }
6986
+ /**
6987
+ * Lowers `&&`/`||` to an if/then/else that short-circuits on the left operand.
6988
+ * @param {import("@babel/types").LogicalExpression} node - The Babel AST node.
6989
+ * @returns {string} WAT representation of the node.
6990
+ */
6991
+ LogicalExpression(node) {
6992
+ const {
6993
+ left,
6994
+ operator,
6995
+ right
6996
+ } = node;
6997
+ const {
6998
+ spaces
6999
+ } = this;
7000
+ const resultType = this.isBooleanI32(left) && this.isBooleanI32(right) ? 'i32' : this.currentType;
7001
+ const thenOperand = operator === '&&' ? right : left;
7002
+ const elseOperand = operator === '&&' ? left : right;
7003
+ let out = `${spaces}(if (result ${resultType})\n`;
7004
+ this.numSpaces++;
7005
+ out += this.getTestSource(left);
7006
+ out += `${this.spaces}(then\n`;
7007
+ this.numSpaces++;
7008
+ out += this.toSource(thenOperand);
7009
+ this.numSpaces--;
7010
+ out += `${this.spaces})\n`;
7011
+ out += `${this.spaces}(else\n`;
7012
+ this.numSpaces++;
7013
+ out += this.toSource(elseOperand);
7014
+ this.numSpaces--;
7015
+ out += `${this.spaces})\n`;
7016
+ this.numSpaces--;
7017
+ out += `${spaces})\n`;
7018
+ return out;
7019
+ }
7020
+ /**
7021
+ * Converts an ExpressionStatement node to WAT, dropping unused values.
7022
+ * @param {import("@babel/types").ExpressionStatement} node - The Babel AST node.
7023
+ * @returns {string} WAT representation of the node.
7024
+ */
7025
+ ExpressionStatement(node) {
7026
+ const {
7027
+ expression
7028
+ } = node;
7029
+ if (expression.type === 'AssignmentExpression' || expression.type === 'UpdateExpression') {
7030
+ return this.toSource(expression);
7031
+ }
7032
+ const {
7033
+ spaces
7034
+ } = this;
7035
+ let out = `${spaces}(drop\n`;
7036
+ this.numSpaces++;
7037
+ out += this.toSource(expression);
7038
+ this.numSpaces--;
7039
+ out += `${spaces})\n`;
7040
+ return out;
7041
+ }
7042
+ /**
7043
+ * Converts an Identifier node to WAT.
7044
+ * @param {import("@babel/types").Identifier} node - The Babel AST node.
7045
+ * @returns {string} WAT representation of the node.
7046
+ */
7047
+ Identifier(node) {
7048
+ const {
7049
+ spaces
7050
+ } = this;
7051
+ const t = this.currentType;
7052
+ if (node.name === 'this') {
7053
+ return `${spaces}(local.get $this)\n`;
7054
+ }
7055
+ if (t === 'f32' || t === 'f64') {
7056
+ if (node.name === 'Infinity') {
7057
+ return `${spaces}(${CONST_TYPE[t]} inf)\n`;
7058
+ }
7059
+ if (node.name === 'NaN') {
7060
+ return `${spaces}(${CONST_TYPE[t]} nan)\n`;
7061
+ }
7062
+ }
7063
+ return `${spaces}(local.get $${node.name})\n`;
7064
+ }
7065
+ /**
7066
+ * Converts a NumericLiteral node to WAT.
7067
+ * @param {import("@babel/types").NumericLiteral} node - The Babel AST node.
7068
+ * @returns {string} WAT representation of the node.
7069
+ */
7070
+ NumericLiteral(node) {
7071
+ const {
7072
+ spaces
7073
+ } = this;
7074
+ const t = this.currentType;
7075
+ const constExpr = CONST_TYPE[t];
7076
+ const value = parseFloat(node.value);
7077
+ if (isNaN(value)) {
7078
+ return `${spaces}(${constExpr} 0.0) ;; Invalid literal value: ${node.value}\n`;
7079
+ }
7080
+ if (t === 'i32' || t === 'i64') {
7081
+ return `${spaces}(${constExpr} ${Math.trunc(value)})\n`;
7082
+ }
7083
+ return `${spaces}(${constExpr} ${value})\n`;
7084
+ }
7085
+ /**
7086
+ * Converts a CallExpression node to WAT.
7087
+ * @param {import("@babel/types").CallExpression} node - The Babel AST node.
7088
+ * @returns {string} WAT representation of the node.
7089
+ */
7090
+ CallExpression(node) {
7091
+ const {
7092
+ callee,
7093
+ arguments: args
7094
+ } = node;
7095
+ const {
7096
+ spaces
7097
+ } = this;
7098
+ if (callee.type === 'MemberExpression') {
7099
+ const methodName = callee.property.type === 'Identifier' ? callee.property.name : callee.property.value;
7100
+ if (callee.object.type === 'Identifier' && callee.object.name === 'Math') {
7101
+ var _MATH_OP$this$current;
7102
+ const mathOp = (_MATH_OP$this$current = MATH_OP[this.currentType]) == null ? void 0 : _MATH_OP$this$current[methodName];
7103
+ if (!mathOp) {
7104
+ return `${spaces}(f32.const 0.0) ;; Unsupported Math.${methodName} for ${this.currentType}\n`;
7105
+ }
7106
+ let _out3 = `${spaces}(${mathOp}\n`;
7107
+ this.numSpaces++;
7108
+ _out3 += args.map(arg => this.toSource(arg)).join('');
7109
+ this.numSpaces--;
7110
+ _out3 += `${spaces})\n`;
7111
+ return _out3;
7112
+ }
7113
+ const clsName = this.resolveInstanceMethodClass(callee.object, methodName);
7114
+ if (clsName) {
7115
+ const pointerName = this.instancePointerName(callee.object);
7116
+ if (pointerName) {
7117
+ let _out4 = `${spaces}(call $${clsName}_${methodName}\n`;
7118
+ this.numSpaces++;
7119
+ _out4 += `${this.spaces}(local.get $${pointerName})\n`;
7120
+ _out4 += args.map(arg => this.toSource(arg)).join('');
7121
+ this.numSpaces--;
7122
+ _out4 += `${spaces})\n`;
7123
+ return _out4;
7124
+ }
7125
+ }
7126
+ }
7127
+ const funcName = callee.name;
7128
+ let out = `${spaces}(call $${funcName}\n`;
7129
+ this.numSpaces++;
7130
+ const argsCode = args.map(arg => this.toSource(arg)).join('');
7131
+ this.numSpaces--;
7132
+ out += argsCode;
7133
+ out += `${spaces})\n`;
7134
+ return out;
7135
+ }
7136
+ /**
7137
+ * Converts a NewExpression node to a $<ClassName>_new factory call.
7138
+ * @param {import("@babel/types").NewExpression} node - The Babel AST node.
7139
+ * @returns {string} WAT representation of the node.
7140
+ */
7141
+ NewExpression(node) {
7142
+ const {
7143
+ callee,
7144
+ arguments: args
7145
+ } = node;
7146
+ const {
7147
+ spaces
7148
+ } = this;
7149
+ if (!this.classes[callee.name]) {
7150
+ return `${spaces}(f32.const 0.0) ;; Unsupported new ${callee.name}\n`;
7151
+ }
7152
+ let out = `${spaces}(call $${callee.name}_new\n`;
7153
+ this.numSpaces++;
7154
+ const argsCode = args.map(arg => this.toSource(arg)).join('');
7155
+ this.numSpaces--;
7156
+ out += argsCode;
7157
+ out += `${spaces})\n`;
7158
+ return out;
7159
+ }
7160
+ }
7161
+
7162
+ /** @typedef {import('@babel/types').Node} Node */
7163
+ /**
7164
+ * Converts a Babel-TS type AST node into a JSDoc compatible type string,
7165
+ * e.g. `number`, `string[]`, `Map<string, number>` or `{a: number, b?: string}`.
7166
+ *
7167
+ * The produced strings are valid TypeScript types as well, so they can be fed
7168
+ * back into `expandType`/`parseJSDoc` of this very project.
7169
+ * @param {Node} node - The Babel-TS type node.
7170
+ * @returns {string} The JSDoc-compatible type string.
7171
+ */
7172
+ function tsTypeToJSDoc(node) {
7173
+ if (!node) {
7174
+ return 'any';
7175
+ }
7176
+ switch (node.type) {
7177
+ case 'TSAnyKeyword':
7178
+ return 'any';
7179
+ case 'TSBigIntKeyword':
7180
+ return 'bigint';
7181
+ case 'TSBooleanKeyword':
7182
+ return 'boolean';
7183
+ case 'TSNeverKeyword':
7184
+ return 'never';
7185
+ case 'TSNullKeyword':
7186
+ return 'null';
7187
+ case 'TSNumberKeyword':
7188
+ return 'number';
7189
+ case 'TSObjectKeyword':
7190
+ return 'object';
7191
+ case 'TSStringKeyword':
7192
+ return 'string';
7193
+ case 'TSSymbolKeyword':
7194
+ return 'symbol';
7195
+ case 'TSUndefinedKeyword':
7196
+ return 'undefined';
7197
+ case 'TSUnknownKeyword':
7198
+ return 'unknown';
7199
+ case 'TSVoidKeyword':
7200
+ return 'void';
7201
+ case 'TSThisType':
7202
+ return 'this';
7203
+ case 'TSIntrinsicKeyword':
7204
+ return 'intrinsic';
7205
+ case 'TSParenthesizedType':
7206
+ return '(' + tsTypeToJSDoc(node.typeAnnotation) + ')';
7207
+ case 'TSLiteralType':
7208
+ return literalToJSDoc(node.literal);
7209
+ case 'TSArrayType':
7210
+ return tsTypeToJSDoc(node.elementType) + '[]';
7211
+ case 'TSTupleType':
7212
+ return '[' + node.elementTypes.map(tsTypeToJSDoc).join(', ') + ']';
7213
+ case 'TSUnionType':
7214
+ return node.types.map(tsTypeToJSDoc).join('|');
7215
+ case 'TSIntersectionType':
7216
+ return node.types.map(tsTypeToJSDoc).join('&');
7217
+ case 'TSOptionalType':
7218
+ return tsTypeToJSDoc(node.typeAnnotation) + '?';
7219
+ case 'TSRestType':
7220
+ return '...' + tsTypeToJSDoc(node.typeAnnotation);
7221
+ case 'TSNamedTupleMember':
7222
+ return tsTypeToJSDoc(node.elementType);
7223
+ case 'TSTypeReference':
7224
+ {
7225
+ var _node$typeParameters;
7226
+ const name = simplifyReference(node.typeName);
7227
+ const params = (_node$typeParameters = node.typeParameters) == null ? void 0 : _node$typeParameters.params;
7228
+ if (params != null && params.length) {
7229
+ return `${name}<${params.map(tsTypeToJSDoc).join(', ')}>`;
7230
+ }
7231
+ return name;
7232
+ }
7233
+ case 'TSQualifiedName':
7234
+ return `${tsTypeToJSDoc(node.left)}.${tsTypeToJSDoc(node.right)}`;
7235
+ case 'TSExpressionWithTypeArguments':
7236
+ return simplifyReference(node.expression);
7237
+ case 'Identifier':
7238
+ return node.name;
7239
+ case 'TSTypeAnnotation':
7240
+ return tsTypeToJSDoc(node.typeAnnotation);
7241
+ case 'TSTypeQuery':
7242
+ return 'typeof ' + tsTypeToJSDoc(node.exprName);
7243
+ case 'TSTypeOperator':
7244
+ return node.operator + ' ' + tsTypeToJSDoc(node.typeAnnotation);
7245
+ case 'TSIndexedAccessType':
7246
+ return tsTypeToJSDoc(node.objectType) + '[' + tsTypeToJSDoc(node.indexType) + ']';
7247
+ case 'TSTypeLiteral':
7248
+ return typeLiteralMembersToJSDoc(node.members);
7249
+ case 'TSPropertySignature':
7250
+ return propertySignatureToJSDoc(node);
7251
+ case 'TSIndexSignature':
7252
+ {
7253
+ const {
7254
+ parameters,
7255
+ typeAnnotation
7256
+ } = node;
7257
+ const pad = parameters.length ? `${tsTypeToJSDoc(parameters[0].typeAnnotation)}` : 'any';
7258
+ return `[key: ${pad}]: ${tsTypeToJSDoc(typeAnnotation.typeAnnotation)}`;
7259
+ }
7260
+ case 'TSFunctionType':
7261
+ return functionSignatureToJSDoc(node);
7262
+ case 'TSConstructorType':
7263
+ return `new ${functionSignatureToJSDoc(node)}`;
7264
+ case 'TSCallSignatureDeclaration':
7265
+ return functionSignatureToJSDoc(node);
7266
+ case 'TSConstructSignatureDeclaration':
7267
+ return `new ${functionSignatureToJSDoc(node)}`;
7268
+ case 'TSTypePredicate':
7269
+ return `${tsTypeToJSDoc(node.parameterName)} is ${tsTypeToJSDoc(node.typeAnnotation)}`;
7270
+ case 'TSImportType':
7271
+ {
7272
+ var _typeArguments$params;
7273
+ const {
7274
+ argument,
7275
+ qualifier,
7276
+ typeArguments
7277
+ } = node;
7278
+ const arg = argument.type === 'StringLiteral' ? `'${jsImportSource(argument)}'` : tsTypeToJSDoc(argument);
7279
+ let out = 'import(' + arg + ')';
7280
+ if (qualifier) {
7281
+ out += '.' + tsTypeToJSDoc(qualifier);
7282
+ }
7283
+ if (typeArguments != null && (_typeArguments$params = typeArguments.params) != null && _typeArguments$params.length) {
7284
+ out += '<' + typeArguments.params.map(tsTypeToJSDoc).join(', ') + '>';
7285
+ }
7286
+ return out;
7287
+ }
7288
+ case 'TSMappedType':
7289
+ {
7290
+ const {
7291
+ typeParameter,
7292
+ typeAnnotation
7293
+ } = node;
7294
+ const name = tsTypeToJSDoc(typeParameter == null ? void 0 : typeParameter.name);
7295
+ const constraint = typeParameter != null && typeParameter.constraint ? ' in ' + tsTypeToJSDoc(typeParameter.constraint) : '';
7296
+ const optional = node.optional ? '?' : '';
7297
+ const value = typeAnnotation ? tsTypeToJSDoc(typeAnnotation) : 'any';
7298
+ return `{ [${name}${constraint}]${optional}: ${value} }`;
7299
+ }
7300
+ case 'TSConditionalType':
7301
+ return `${tsTypeToJSDoc(node.checkType)} extends ${tsTypeToJSDoc(node.extendsType)} ? ${tsTypeToJSDoc(node.trueType)} : ${tsTypeToJSDoc(node.falseType)}`;
7302
+ case 'TSTemplateLiteralType':
7303
+ return templateLiteralToJSDoc(node);
7304
+ default:
7305
+ console.warn('ts2js> tsTypeToJSDoc unhandled type', node.type, node);
7306
+ return 'any';
7307
+ }
7308
+ }
7309
+ /**
7310
+ * Reduces a possibly namespace-qualified type name to its local identifier,
7311
+ * e.g. `Validation.StringValidator` becomes `StringValidator`, since flattened
7312
+ * namespaces hoist their interfaces/classes to plain identifiers.
7313
+ * @param {Node} node - The referenced name node.
7314
+ * @returns {string} The simplified JSDoc type string.
7315
+ */
7316
+ function simplifyReference(node) {
7317
+ if ((node == null ? void 0 : node.type) === 'TSQualifiedName') {
7318
+ return simplifyReference(node.right);
7319
+ }
7320
+ return tsTypeToJSDoc(node);
7321
+ }
7322
+ /**
7323
+ * Converts a literal type node to its JSDoc representation.
7324
+ * @param {Node} literal - The literal node.
7325
+ * @returns {string} The JSDoc type string.
7326
+ */
7327
+ function literalToJSDoc(literal) {
7328
+ if (literal.type === 'UnaryExpression') {
7329
+ return literal.operator + tsTypeToJSDoc(literal.argument);
7330
+ }
7331
+ if (literal.type === 'StringLiteral' || literal.type === 'NumericLiteral' || literal.type === 'BooleanLiteral') {
7332
+ var _literal$extra$raw, _literal$extra;
7333
+ return (_literal$extra$raw = (_literal$extra = literal.extra) == null ? void 0 : _literal$extra.raw) != null ? _literal$extra$raw : String(literal.value);
7334
+ }
7335
+ if (literal.type === 'BigIntLiteral') {
7336
+ var _literal$extra$raw2, _literal$extra2;
7337
+ return (_literal$extra$raw2 = (_literal$extra2 = literal.extra) == null ? void 0 : _literal$extra2.raw) != null ? _literal$extra$raw2 : literal.value;
7338
+ }
7339
+ return tsTypeToJSDoc(literal);
7340
+ }
7341
+ /**
7342
+ * Converts a `TSTypeLiteral`'s members into a JSDoc object type string,
7343
+ * e.g. `{a: number, b?: string}`.
7344
+ * @param {Node[]} members - The members of the type literal.
7345
+ * @returns {string} The JSDoc object type string.
7346
+ */
7347
+ function typeLiteralMembersToJSDoc(members) {
7348
+ const props = members.map(tsTypeToJSDoc);
7349
+ return '{' + props.join(', ') + '}';
7350
+ }
7351
+ /**
7352
+ * @param {Node} node - The `TSPropertySignature` node.
7353
+ * @returns {string} The `name: type` (or optional `name?: type`) string.
7354
+ */
7355
+ function propertySignatureToJSDoc(node) {
7356
+ const key = tsTypeToJSDoc(node.key);
7357
+ const optional = node.optional ? '?' : '';
7358
+ const type = node.typeAnnotation ? tsTypeToJSDoc(node.typeAnnotation.typeAnnotation) : 'any';
7359
+ return `${key}${optional}: ${type}`;
7360
+ }
7361
+ /**
7362
+ * @param {Node} node - A function-like type node (`TSFunctionType`, `TSCallSignatureDeclaration`, ...).
7363
+ * @returns {string} The `(a: A) => R` style JSDoc type string.
7364
+ */
7365
+ function functionSignatureToJSDoc(node) {
7366
+ const params = node.parameters.map(param => {
7367
+ const name = param.type === 'Identifier' ? param.name : '';
7368
+ const type = param.typeAnnotation ? tsTypeToJSDoc(param.typeAnnotation.typeAnnotation) : 'any';
7369
+ return name && name !== 'this' ? `${name}: ${type}` : type;
7370
+ }).join(', ');
7371
+ const retType = node.typeAnnotation ? tsTypeToJSDoc(node.typeAnnotation.typeAnnotation) : 'void';
7372
+ return `(${params}) => ${retType}`;
7373
+ }
7374
+ /**
7375
+ * @param {Node} node - The `TSTemplateLiteralType` node.
7376
+ * @returns {string} The template literal type as string.
7377
+ */
7378
+ function templateLiteralToJSDoc(node) {
7379
+ const {
7380
+ quasis,
7381
+ types
7382
+ } = node;
7383
+ let out = '`';
7384
+ for (let i = 0; i < quasis.length; i++) {
7385
+ out += quasis[i].value.raw;
7386
+ if (types[i]) {
7387
+ out += '${' + tsTypeToJSDoc(types[i]) + '}';
7388
+ }
7389
+ }
7390
+ return out + '`';
7391
+ }
7392
+ /**
7393
+ * Extracts param information and produces a `@param` JSDoc line,
7394
+ * or nothing when nothing can be said about the parameter.
7395
+ * @param {Node} param - The parameter node.
7396
+ * @param {number} index - The index of the parameter.
7397
+ * @returns {string|undefined} The `@param` line.
7398
+ */
7399
+ function paramToJSDoc(param, index) {
7400
+ if (param.type === 'TSParameterProperty') {
7401
+ param = param.parameter;
7402
+ }
7403
+ let type;
7404
+ let name;
7405
+ let optional = false;
7406
+ let rest = false;
7407
+ let defaultText;
7408
+ const typeAnnotationOf = value => {
7409
+ var _value$typeAnnotation;
7410
+ return value == null || (_value$typeAnnotation = value.typeAnnotation) == null ? void 0 : _value$typeAnnotation.typeAnnotation;
7411
+ };
7412
+ if (param.type === 'Identifier') {
7413
+ name = param.name;
7414
+ optional = param.optional;
7415
+ type = typeAnnotationOf(param);
7416
+ } else if (param.type === 'AssignmentPattern') {
7417
+ var _param$right, _param$right2, _param$right3;
7418
+ optional = true;
7419
+ const left = param.left;
7420
+ if (left.type === 'Identifier') {
7421
+ name = left.name;
7422
+ type = typeAnnotationOf(left);
7423
+ } else {
7424
+ var _left$id;
7425
+ name = 'param' + index;
7426
+ if (((_left$id = left.id) == null ? void 0 : _left$id.type) === 'Identifier') {
7427
+ name = left.id.name;
7428
+ }
7429
+ type = typeAnnotationOf(left);
7430
+ }
7431
+ if (((_param$right = param.right) == null ? void 0 : _param$right.type) === 'StringLiteral' || ((_param$right2 = param.right) == null ? void 0 : _param$right2.type) === 'NumericLiteral' || ((_param$right3 = param.right) == null ? void 0 : _param$right3.type) === 'BooleanLiteral') {
7432
+ var _param$right$extra$ra, _param$right$extra;
7433
+ defaultText = (_param$right$extra$ra = (_param$right$extra = param.right.extra) == null ? void 0 : _param$right$extra.raw) != null ? _param$right$extra$ra : String(param.right.value);
7434
+ }
7435
+ if (!type) {
7436
+ type = inferTypeFromDefault(param.right);
7437
+ }
7438
+ } else if (param.type === 'RestElement') {
7439
+ rest = true;
7440
+ const argument = param.argument;
7441
+ if (argument.type === 'Identifier') {
7442
+ name = argument.name;
7443
+ type = typeAnnotationOf(param);
7444
+ } else {
7445
+ type = typeAnnotationOf(param);
7446
+ name = 'param' + index;
7447
+ }
7448
+ } else {
7449
+ // ObjectPattern / ArrayPattern
7450
+ name = 'param' + index;
7451
+ type = typeAnnotationOf(param);
7452
+ }
7453
+ let nameStr = name || 'param' + index;
7454
+ if (optional) {
7455
+ nameStr = '[' + nameStr + (defaultText !== undefined ? ' = ' + defaultText : '') + ']';
7456
+ }
7457
+ if (!type) {
7458
+ return `@param {any} ${nameStr}`;
7459
+ }
7460
+ const typeStr = rest ? '...' + restElementType(tsTypeToJSDoc(type)) : tsTypeToJSDoc(type);
7461
+ return `@param {${typeStr}} ${nameStr}`;
7462
+ }
7463
+ /**
7464
+ * Unwraps the trailing `[]` of an array type in a rest parameter context,
7465
+ * e.g. `@param {...string[]} rest` becomes `@param {...string} rest`.
7466
+ * @param {string} typeStr - The JSDoc type string.
7467
+ * @returns {string} The element type.
7468
+ */
7469
+ function restElementType(typeStr) {
7470
+ if (typeStr.endsWith('[]')) {
7471
+ return typeStr.slice(0, -2);
7472
+ }
7473
+ return typeStr;
7474
+ }
7475
+ /**
7476
+ * Infers a JSDoc type from a default value literal.
7477
+ * @param {Node} node - The default value node.
7478
+ * @returns {import('@babel/types').Node|undefined} A primitive keyword node or `undefined`.
7479
+ */
7480
+ function inferTypeFromDefault(node) {
7481
+ let keyword;
7482
+ switch (node == null ? void 0 : node.type) {
7483
+ case 'StringLiteral':
7484
+ keyword = 'TSStringKeyword';
7485
+ break;
7486
+ case 'NumericLiteral':
7487
+ keyword = 'TSNumberKeyword';
7488
+ break;
7489
+ case 'BooleanLiteral':
7490
+ keyword = 'TSBooleanKeyword';
7491
+ break;
7492
+ case 'NullLiteral':
7493
+ keyword = 'TSNullKeyword';
7494
+ break;
7495
+ case 'ArrayExpression':
7496
+ keyword = 'TSArrayType';
7497
+ break;
7498
+ default:
7499
+ return undefined;
7500
+ }
7501
+ return {
7502
+ type: keyword
7503
+ };
7504
+ }
7505
+ /**
7506
+ * @param {Node} node - The function-like node.
7507
+ * @returns {boolean} True if any parameter carries a type annotation.
7508
+ */
7509
+ function hasTypedParams(node) {
7510
+ return node.params.some(param => {
7511
+ var _param$left, _param$argument;
7512
+ if (param.type === 'TSParameterProperty') {
7513
+ param = param.parameter;
7514
+ }
7515
+ return param.typeAnnotation || ((_param$left = param.left) == null ? void 0 : _param$left.typeAnnotation) || ((_param$argument = param.argument) == null ? void 0 : _param$argument.typeAnnotation);
7516
+ });
7517
+ }
7518
+ /**
7519
+ * Returns the JSDoc lines for a function-like node.
7520
+ * @param {Node} node - The function-like node.
7521
+ * @returns {string[]} The JSDoc lines (without `@param`/`@returns` separators).
7522
+ */
7523
+ function jsdocLinesFromFunction(node) {
7524
+ var _node$typeParameters2, _node$returnType;
7525
+ const lines = [];
7526
+ if ((_node$typeParameters2 = node.typeParameters) != null && _node$typeParameters2.params) {
7527
+ for (const tp of node.typeParameters.params) {
7528
+ const name = tp.name;
7529
+ lines.push(`@template {${name}} ${name}`);
7530
+ }
7531
+ }
7532
+ const addParams = node.type !== 'TSDeclareFunction' && node.kind !== 'get';
7533
+ if (addParams && hasTypedParams(node)) {
7534
+ node.params.forEach((param, i) => lines.push(paramToJSDoc(param, i)));
7535
+ }
7536
+ if ((_node$returnType = node.returnType) != null && _node$returnType.typeAnnotation) {
7537
+ lines.push(`@returns {${tsTypeToJSDoc(node.returnType.typeAnnotation)}}`);
7538
+ }
7539
+ return lines;
7540
+ }
7541
+ /**
7542
+ * Injects `this.prop = prop;` statements for constructor parameter properties
7543
+ * (`constructor(public prop: number)`).
7544
+ * @param {import('@babel/types').ClassMethod|import('@babel/types').ClassPrivateMethod} node - The constructor node.
7545
+ */
7546
+ function injectParameterProperties(node) {
7547
+ var _node$body;
7548
+ const body = (_node$body = node.body) == null ? void 0 : _node$body.body;
7549
+ if (!Array.isArray(body)) {
7550
+ return;
7551
+ }
7552
+ const properties = node.params.filter(param => param.type === 'TSParameterProperty');
7553
+ for (const property of properties) {
7554
+ const parameter = property.parameter;
7555
+ if (parameter.type !== 'Identifier' && parameter.type !== 'AssignmentPattern') {
7556
+ console.warn('ts2js> unhandled parameter property', property);
7557
+ continue;
7558
+ }
7559
+ const target = parameter.type === 'AssignmentPattern' ? parameter.left : parameter;
7560
+ if (target.type !== 'Identifier') {
7561
+ console.warn('ts2js> unhandled parameter property target', property);
7562
+ continue;
7563
+ }
7564
+ const {
7565
+ name
7566
+ } = target;
7567
+ const alreadyAssigned = body.some(stmt => {
7568
+ if (stmt.type !== 'ExpressionStatement') return false;
7569
+ const expr = stmt.expression;
7570
+ return expr.type === 'AssignmentExpression' && expr.operator === '=' && expr.left.type === 'MemberExpression' && expr.left.object.type === 'ThisExpression' && expr.left.property.type === 'Identifier' && expr.left.property.name === name;
7571
+ });
7572
+ if (alreadyAssigned) {
7573
+ continue;
7574
+ }
7575
+ const rightNode = target;
7576
+ const assignment = {
7577
+ type: 'ExpressionStatement',
7578
+ expression: {
7579
+ type: 'AssignmentExpression',
7580
+ operator: '=',
7581
+ left: {
7582
+ type: 'MemberExpression',
7583
+ object: {
7584
+ type: 'ThisExpression'
7585
+ },
7586
+ property: {
7587
+ type: 'Identifier',
7588
+ name
7589
+ },
7590
+ computed: false,
7591
+ optional: null
7592
+ },
7593
+ right: rightNode
7594
+ }
7595
+ };
7596
+ body.unshift(assignment);
7597
+ }
7598
+ }
7599
+ /**
7600
+ * A Stringifier subclass which strips TypeScript-only syntax while emitting the
7601
+ * JSDoc comments that were attached during the `annotate` pre-pass.
7602
+ */
7603
+ class ToJS extends Stringifier {
7604
+ // --- Unwrap type-only expressions ---
7605
+ TSAsExpression(node) {
7606
+ return this.toSource(node.expression);
7607
+ }
7608
+ TSNonNullExpression(node) {
7609
+ return this.toSource(node.expression);
7610
+ }
7611
+ TSSatisfiesExpression(node) {
7612
+ return this.toSource(node.expression);
7613
+ }
7614
+ TSTypeAssertion(node) {
7615
+ return this.toSource(node.expression);
7616
+ }
7617
+ TSInstantiationExpression(node) {
7618
+ return this.toSource(node.expression);
7619
+ }
7620
+ TSTypeCastExpression(node) {
7621
+ return this.toSource(node.expression);
7622
+ }
7623
+ TSParameterProperty(node) {
7624
+ return this.toSource(node.parameter);
7625
+ }
7626
+ TSExpressionWithTypeArguments(node) {
7627
+ return this.toSource(node.expression);
7628
+ }
7629
+ TSImportEqualsDeclaration(node) {
7630
+ var _node$moduleReference;
7631
+ if (((_node$moduleReference = node.moduleReference) == null ? void 0 : _node$moduleReference.type) !== 'TSExternalModuleReference') {
7632
+ return '';
7633
+ }
7634
+ const name = this.toSource(node.id);
7635
+ const raw = jsImportSource(node.moduleReference.expression);
7636
+ if (node.importKind === 'type') {
7637
+ return `/** @import * as ${name} from '${raw}' */`;
7638
+ }
7639
+ return `import * as ${name} from '${raw}';`;
7640
+ }
7641
+ TSExportAssignment() {
7642
+ return '';
7643
+ }
7644
+ TSUndefinedKeyword() {
7645
+ return '';
7646
+ }
7647
+ // --- Drop type-only declarations, but keep interfaces/aliases as @typedef ---
7648
+ /**
7649
+ * @param {import('@babel/types').TSInterfaceDeclaration} node - The Babel AST node.
7650
+ * @returns {string} A JSDoc typedef comment.
7651
+ */
7652
+ TSInterfaceDeclaration(node) {
7653
+ return this.typedefComment(node);
7654
+ }
7655
+ /**
7656
+ * @param {import('@babel/types').TSTypeAliasDeclaration} node - The Babel AST node.
7657
+ * @returns {string} A JSDoc typedef comment.
7658
+ */
7659
+ TSTypeAliasDeclaration(node) {
7660
+ return this.typedefComment(node);
7661
+ }
7662
+ /**
7663
+ * Drop ambient declarations, they have no runtime representation.
7664
+ * @returns {string} An empty string.
7665
+ */
7666
+ TSDeclareFunction() {
7667
+ return '';
7668
+ }
7669
+ /**
7670
+ * Converts a namespace (`namespace X { ... }`) into the classic IIFE
7671
+ * pattern, exporting members via `X.member = member` assignments.
7672
+ * @param {import('@babel/types').TSModuleDeclaration} node - The namespace declaration.
7673
+ * @returns {string} The IIFE source.
7674
+ */
7675
+ TSModuleDeclaration(node) {
7676
+ var _this$namespaceLevel;
7677
+ const {
7678
+ id,
7679
+ body
7680
+ } = node;
7681
+ const name = (id == null ? void 0 : id.type) === 'Identifier' ? id.name : '';
7682
+ if (!name || node.declare || node.global || !body || body.type !== 'TSModuleBlock') {
7683
+ return '';
7684
+ }
7685
+ const saved = this.numSpaces;
7686
+ const level = (_this$namespaceLevel = this.namespaceLevel) != null ? _this$namespaceLevel : 0;
7687
+ this.namespaceLevel = level + 1;
7688
+ this.numSpaces = 0;
7689
+ const memberSource = this.namespaceMembers(body, name);
7690
+ this.numSpaces = saved;
7691
+ this.namespaceLevel = level;
7692
+ const spaces = this.spaces;
7693
+ const outer = ' '.repeat(level + 1);
7694
+ const inner = memberSource.replace(/ \*\/ +(?=[a-zA-Z_$])/g, '*/\n').split('\n').map(line => line.trim() ? outer + line.trimEnd() : line).join('\n');
7695
+ let out = spaces + `var ${name};\n`;
7696
+ out += spaces + `(function (${name}) {\n`;
7697
+ out += inner;
7698
+ out += '\n' + spaces + `})(${name} || (${name} = {}));`;
7699
+ return out;
7700
+ }
7701
+ /**
7702
+ * Renders the statements of a namespace body at base indentation, emitting
7703
+ * the local members and the trailing `Name.member = member;` assignments
7704
+ * for exported value members.
7705
+ * @param {import('@babel/types').TSModuleBlock} block - The namespace body.
7706
+ * @param {string} name - The namespace identifier.
7707
+ * @returns {string} The body source (joinable lines).
7708
+ */
7709
+ namespaceMembers(block, name) {
7710
+ const lines = [];
7711
+ const assignments = [];
7712
+ for (const statement of block.body) {
7713
+ if (statement.type === 'ExportNamedDeclaration') {
7714
+ const {
7715
+ code,
7716
+ names
7717
+ } = this.namespaceExport(statement);
7718
+ if (code) {
7719
+ lines.push(code.trimEnd());
7720
+ }
7721
+ for (const member of names) {
7722
+ assignments.push(`${name}.${member} = ${member};`);
7723
+ }
7724
+ } else {
7725
+ lines.push(this.toSource(statement).trimEnd());
7726
+ }
7727
+ }
7728
+ return lines.concat(assignments).join('\n');
7729
+ }
7730
+ /**
7731
+ * Renders an `export` statement inside a namespace: the declaration without
7732
+ * the `export` keyword and the list of exported value names.
7733
+ * @param {import('@babel/types').ExportNamedDeclaration} statement - The export statement.
7734
+ * @returns {{code: string, names: string[]}} The declaration source and exported names.
7735
+ */
7736
+ namespaceExport(statement) {
7737
+ var _declaration$id;
7738
+ const {
7739
+ declaration
7740
+ } = statement;
7741
+ if (!declaration) {
7742
+ return {
7743
+ code: '',
7744
+ names: []
7745
+ };
7746
+ }
7747
+ let names = [];
7748
+ switch (declaration.type) {
7749
+ case 'ClassDeclaration':
7750
+ case 'FunctionDeclaration':
7751
+ case 'TSEnumDeclaration':
7752
+ if (declaration.id) {
7753
+ names = [declaration.id.name];
7754
+ }
7755
+ break;
7756
+ case 'VariableDeclaration':
7757
+ names = declaration.declarations.map(declarator => declarator.id).filter(id => id && id.type === 'Identifier').map(id => id.name);
7758
+ break;
7759
+ case 'TSModuleDeclaration':
7760
+ if ((_declaration$id = declaration.id) != null && _declaration$id.name) {
7761
+ names = [declaration.id.name];
7762
+ }
7763
+ break;
7764
+ }
7765
+ return {
7766
+ code: this.toSource(declaration),
7767
+ names
7768
+ };
7769
+ }
7770
+ /**
7771
+ * Converts `enum` into a plain `const` object.
7772
+ * @param {import('@babel/types').TSEnumDeclaration} node - The Babel AST node.
7773
+ * @returns {string} The object literal.
7774
+ */
7775
+ TSEnumDeclaration(node) {
7776
+ const spaces = this.spaces;
7777
+ let running = 0;
7778
+ const entries = node.members.map(member => {
7779
+ const key = this.toSource(member.id);
7780
+ let value;
7781
+ if (member.initializer) {
7782
+ value = this.toSource(member.initializer);
7783
+ if (member.initializer.type === 'NumericLiteral') {
7784
+ running = member.initializer.value + 1;
7785
+ } else if (member.initializer.type === 'UnaryExpression') {
7786
+ var _member$initializer$e;
7787
+ const raw = (_member$initializer$e = member.initializer.extra) == null ? void 0 : _member$initializer$e.raw;
7788
+ if (raw) {
7789
+ const n = Number(raw);
7790
+ running = Number.isNaN(n) ? running : n + 1;
7791
+ }
7792
+ }
7793
+ } else {
7794
+ value = String(running);
7795
+ running++;
7796
+ }
7797
+ return `${key}: ${value}`;
7798
+ });
7799
+ let out = spaces + 'const ' + this.toSource(node.id) + ' = {\n';
7800
+ this.numSpaces++;
7801
+ const innerSpaces = this.spaces;
7802
+ out += entries.map(_ => innerSpaces + _).join(',\n');
7803
+ this.numSpaces--;
7804
+ out += '\n' + spaces + '};';
7805
+ return out;
7806
+ }
7807
+ /**
7808
+ * Renders a typedef comment for interfaces/type aliases.
7809
+ * @param {Node} node - The declaration node.
7810
+ * @returns {string} The comment.
7811
+ */
7812
+ typedefComment(node) {
7813
+ const lines = this.typedefLines(node);
7814
+ return this.jsdocComment(lines);
7815
+ }
7816
+ /**
7817
+ * Builds `@typedef` JSDoc lines for a declaration node.
7818
+ * @param {Node} node - The declaration node.
7819
+ * @returns {string[]} The JSDoc lines.
7820
+ */
7821
+ typedefLines(node) {
7822
+ if (node.type === 'TSTypeAliasDeclaration') {
7823
+ const _name = node.id.name;
7824
+ return [`@typedef {${tsTypeToJSDoc(node.typeAnnotation)}} ${_name}`];
7825
+ }
7826
+ const name = node.id.name;
7827
+ const lines = [`@typedef {Object} ${name}`];
7828
+ for (const member of (_node$body$body = (_node$body2 = node.body) == null ? void 0 : _node$body2.body) != null ? _node$body$body : []) {
7829
+ var _node$body$body, _node$body2;
7830
+ if (member.type === 'TSPropertySignature') {
7831
+ const propName = tsTypeToJSDoc(member.key);
7832
+ const optional = member.optional;
7833
+ const type = member.typeAnnotation ? tsTypeToJSDoc(member.typeAnnotation.typeAnnotation) : 'any';
7834
+ lines.push(`@property {${type}} ${optional ? '[' + propName + ']' : propName}`);
7835
+ } else if (member.type === 'TSMethodSignature') {
7836
+ const methodName = tsTypeToJSDoc(member.key);
7837
+ const optional = member.optional;
7838
+ const type = functionSignatureToJSDoc(member);
7839
+ lines.push(`@property {${type}} ${optional ? '[' + methodName + ']' : methodName}`);
7840
+ }
7841
+ }
7842
+ return lines;
7843
+ }
7844
+ /**
7845
+ * Builds a JSDoc comment block string with proper indentation.
7846
+ * @param {string[]} lines - The comment body lines (e.g. `@param {number} a`).
7847
+ * @returns {string} The comment string.
7848
+ */
7849
+ jsdocComment(lines) {
7850
+ const spaces = this.spaces;
7851
+ let out = spaces + '/**\n';
7852
+ for (const line of lines) {
7853
+ out += spaces + ' * ' + line + '\n';
7854
+ }
7855
+ out += spaces + ' */';
7856
+ return out;
7857
+ }
7858
+ /**
7859
+ * Converts type-only imports into `@import` JSDoc comments and keeps
7860
+ * the value imports of mixed imports (`import {Value, type AlsoType}`).
7861
+ * @override
7862
+ * @param {import('@babel/types').ImportDeclaration} node - The Babel AST node.
7863
+ * @returns {string} Stringification of the node.
7864
+ */
7865
+ ImportDeclaration(node) {
7866
+ const typeSpecifiers = node.specifiers.filter(_ => _.importKind === 'type');
7867
+ const valueSpecifiers = node.specifiers.filter(_ => _.importKind !== 'type');
7868
+ const fullyTypeOnly = node.importKind === 'type' || typeSpecifiers.length && valueSpecifiers.length === 0;
7869
+ if (fullyTypeOnly) {
7870
+ return importTypeToJSDoc(node.specifiers, node.source);
7871
+ }
7872
+ let out = '';
7873
+ if (typeSpecifiers.length) {
7874
+ out = importTypeToJSDoc(typeSpecifiers, node.source);
7875
+ }
7876
+ if (valueSpecifiers.length) {
7877
+ const valueImport = super.ImportDeclaration(_extends({}, node, {
7878
+ importKind: 'value',
7879
+ specifiers: valueSpecifiers
7880
+ }));
7881
+ out = out ? out + '\n' + valueImport : valueImport;
7882
+ }
7883
+ return out;
7884
+ }
7885
+ /**
7886
+ * Strips type-only exports and converts type declarations into typedef comments.
7887
+ * @override
7888
+ * @param {import('@babel/types').ExportNamedDeclaration} node - The Babel AST node.
7889
+ * @returns {string} Stringification of the node.
7890
+ */
7891
+ ExportNamedDeclaration(node) {
7892
+ if (node.exportKind === 'type') {
7893
+ if (node.declaration) {
7894
+ return this.toSource(node.declaration);
7895
+ }
7896
+ return '';
7897
+ }
7898
+ if (node.specifiers.some(_ => _.exportKind === 'type')) {
7899
+ const specifiers = node.specifiers.filter(_ => _.exportKind !== 'type');
7900
+ if (specifiers.length === 0) {
7901
+ return '';
7902
+ }
7903
+ return super.ExportNamedDeclaration(_extends({}, node, {
7904
+ specifiers
7905
+ }));
7906
+ }
7907
+ return super.ExportNamedDeclaration(node);
7908
+ }
7909
+ /**
7910
+ * Strips the `declare` modifier from variable declarations.
7911
+ * @override
7912
+ * @param {import('@babel/types').VariableDeclaration} node - The Babel AST node.
7913
+ * @returns {string} Stringification of the node.
7914
+ */
7915
+ VariableDeclaration(node) {
7916
+ if (node.declare) {
7917
+ return '';
7918
+ }
7919
+ return super.VariableDeclaration(_extends({}, node, {
7920
+ declare: false
7921
+ }));
7922
+ }
7923
+ }
7924
+ /**
7925
+ * Renders a JSDoc `@import` comment for type-only import specifiers,
7926
+ * e.g. `import type {OnlyType} from './types'` becomes the single line
7927
+ * `@import { OnlyType } from './types.js'` and carries the type
7928
+ * information without creating a runtime import.
7929
+ * @param {import('@babel/types').ImportSpecifier[]|import('@babel/types').ImportDefaultSpecifier[]|import('@babel/types').ImportNamespaceSpecifier[]} specifiers - The import specifiers.
7930
+ * @param {import('@babel/types').StringLiteral} source - The import source.
7931
+ * @returns {string} The `@import` JSDoc comment (or an empty string).
7932
+ */
7933
+ function importTypeToJSDoc(specifiers, source) {
7934
+ const named = [];
7935
+ let defaultName;
7936
+ let namespaceName;
7937
+ for (const specifier of specifiers) {
7938
+ if (specifier.type === 'ImportSpecifier') {
7939
+ var _specifier$local$name, _specifier$local, _specifier$imported;
7940
+ named.push((_specifier$local$name = (_specifier$local = specifier.local) == null ? void 0 : _specifier$local.name) != null ? _specifier$local$name : (_specifier$imported = specifier.imported) == null ? void 0 : _specifier$imported.name);
7941
+ } else if (specifier.type === 'ImportDefaultSpecifier') {
7942
+ var _specifier$local2;
7943
+ defaultName = (_specifier$local2 = specifier.local) == null ? void 0 : _specifier$local2.name;
7944
+ } else if (specifier.type === 'ImportNamespaceSpecifier') {
7945
+ var _specifier$local3;
7946
+ namespaceName = (_specifier$local3 = specifier.local) == null ? void 0 : _specifier$local3.name;
7947
+ }
7948
+ }
7949
+ const parts = [];
7950
+ if (defaultName) {
7951
+ parts.push(defaultName);
7952
+ }
7953
+ if (named.length) {
7954
+ parts.push('{ ' + named.join(', ') + ' }');
7955
+ }
7956
+ if (namespaceName) {
7957
+ parts.push('* as ' + namespaceName);
7958
+ }
7959
+ if (!parts.length) {
7960
+ return '';
7961
+ }
7962
+ return `/** @import ${parts.join(', ')} from '${jsImportSource(source)}' */`;
7963
+ }
7964
+ /**
7965
+ * Rewrites a TypeScript import specifier to its JavaScript counterpart,
7966
+ * e.g. `./types` becomes `./types.js`.
7967
+ * @param {import('@babel/types').StringLiteral} source - The import source.
7968
+ * @returns {string} The JavaScript module specifier.
7969
+ */
7970
+ function jsImportSource(source) {
7971
+ const raw = source.value;
7972
+ if (!raw.startsWith('.') && !raw.startsWith('/')) {
7973
+ return raw;
7974
+ }
7975
+ if (/\.(ts|tsx|mts|cts)$/.test(raw)) {
7976
+ return raw.replace(/\.(ts|tsx|mts|cts)$/, '.js');
7977
+ }
7978
+ if (!/\.[a-zA-Z][a-zA-Z0-9]*$/.test(raw)) {
7979
+ return raw + '.js';
7980
+ }
7981
+ return raw;
7982
+ }
7983
+ /**
7984
+ * Attaches JSDoc comment blocks to the AST nodes which carry TypeScript types.
7985
+ * @param {Node} node - The node to annotate recursively.
7986
+ * @param {Node[]} parents - The current parent stack.
7987
+ */
7988
+ function annotate(node, parents) {
7989
+ if (!node || typeof node !== 'object') {
7990
+ return;
7991
+ }
7992
+ parents.push(node);
7993
+ const {
7994
+ type
7995
+ } = node;
7996
+ if (nodeIsFunctionLike(node)) {
7997
+ if ((type === 'ClassMethod' || type === 'ClassPrivateMethod') && node.kind === 'constructor') {
7998
+ injectParameterProperties(node);
7999
+ }
8000
+ const lines = jsdocLinesFromFunction(node);
8001
+ if (lines.length) {
8002
+ attachComment(node, lines, parents);
8003
+ }
8004
+ } else if (type === 'ClassProperty' || type === 'ClassPrivateProperty') {
8005
+ var _node$typeAnnotation;
8006
+ if ((_node$typeAnnotation = node.typeAnnotation) != null && _node$typeAnnotation.typeAnnotation && !node.declare) {
8007
+ attachComment(node, [`@type {${tsTypeToJSDoc(node.typeAnnotation.typeAnnotation)}}`], parents);
8008
+ }
8009
+ } else if (type === 'ClassDeclaration' || type === 'ClassExpression') {
8010
+ const interfaces = node.implements || [];
8011
+ if (interfaces != null && interfaces.length) {
8012
+ const lines = interfaces.map(imp => `@implements {${simplifyReference(imp.expression)}}`);
8013
+ attachComment(node, lines, parents);
8014
+ }
8015
+ } else if (type === 'VariableDeclaration' && !node.declare) {
8016
+ collectVariableTypeComments(node, parents);
8017
+ }
8018
+ // recurse into children based on the annotated keys
8019
+ const keys = nodeChildren[type];
8020
+ for (const key of keys != null ? keys : []) {
8021
+ const child = node[key];
8022
+ if (Array.isArray(child)) {
8023
+ for (const entry of child) {
8024
+ annotate(entry, parents);
8025
+ }
8026
+ } else if (child && typeof child === 'object' && child.type) {
8027
+ annotate(child, parents);
8028
+ }
8029
+ }
8030
+ parents.pop();
8031
+ }
8032
+ /**
8033
+ * Attaches a JSDoc comment (built from `lines`) to a suitable statement-level host node.
8034
+ * @param {Node} node - The annotated node.
8035
+ * @param {string[]} lines - The JSDoc lines.
8036
+ * @param {Node[]} parents - The parent stack.
8037
+ */
8038
+ function attachComment(node, lines, parents) {
8039
+ let host = findCommentHost(node, parents);
8040
+ if (!host) {
8041
+ host = node;
8042
+ }
8043
+ const comment = makeComment(node, lines, parents);
8044
+ host.leadingComments = [...(host.leadingComments || []), comment];
8045
+ }
8046
+ /**
8047
+ * Finds the statement-ish node where a JSDoc comment should live.
8048
+ * For `const f = (a) => {}` the comment belongs on the VariableDeclaration,
8049
+ * for `export function f` it belongs on the ExportNamedDeclaration.
8050
+ * @param {Node} node - The function/property node.
8051
+ * @param {Node[]} parents - The parent stack.
8052
+ * @returns {Node} The host node.
8053
+ */
8054
+ function findCommentHost(node, parents) {
8055
+ const index = parents.findLastIndex(_ => _ === node);
8056
+ const parent = parents[index - 1];
8057
+ if ((parent == null ? void 0 : parent.type) === 'ExportNamedDeclaration') {
8058
+ var _parents;
8059
+ // Inside namespaces the export wrapper is consumed by the namespace
8060
+ // emitter, so the comment has to live on the declaration itself.
8061
+ if (((_parents = parents[index - 2]) == null ? void 0 : _parents.type) === 'TSModuleBlock') {
8062
+ return node;
8063
+ }
8064
+ return parent;
8065
+ }
8066
+ if ((parent == null ? void 0 : parent.type) === 'VariableDeclarator') {
8067
+ const grand = parents[index - 2];
8068
+ if ((grand == null ? void 0 : grand.type) === 'VariableDeclaration') {
8069
+ var _parents2;
8070
+ if (((_parents2 = parents[index - 3]) == null ? void 0 : _parents2.type) === 'ExportNamedDeclaration') {
8071
+ return parents[index - 3];
8072
+ }
8073
+ return grand;
8074
+ }
8075
+ }
8076
+ if ((parent == null ? void 0 : parent.type) === 'ExpressionStatement') {
8077
+ return parent;
8078
+ }
8079
+ // Class-methods/properties and object-methods host the comment themselves.
8080
+ return node;
8081
+ }
8082
+ /**
8083
+ * Creates a Babel CommentBlock node with a fabricated `loc`.
8084
+ * The column is derived from the renderer indentation depth of the annotated
8085
+ * node (2 spaces per indenting ancestor) so that the comment aligns with the
8086
+ * `spaces` of the node's rendered position, independent of the original
8087
+ * (namespace-shifted) source column.
8088
+ * @param {Node} node - The annotated node.
8089
+ * @param {string[]} lines - The JSDoc lines.
8090
+ * @param {Node[]} parents - The parent stack.
8091
+ * @returns {import('@babel/types').CommentBlock} The comment node.
8092
+ */
8093
+ function makeComment(node, lines, parents) {
8094
+ var _ref, _node$start, _node$loc, _node$loc$start$line, _node$loc2, _node$loc$start$line2, _node$loc3;
8095
+ const value = '*\n * ' + lines.join('\n * ');
8096
+ const start = (_ref = (_node$start = node.start) != null ? _node$start : (_node$loc = node.loc) == null || (_node$loc = _node$loc.start) == null ? void 0 : _node$loc.index) != null ? _ref : 0;
8097
+ const startColumn = 2 * renderIndentDepth(node, parents);
8098
+ return {
8099
+ type: 'CommentBlock',
8100
+ value,
8101
+ loc: {
8102
+ start: {
8103
+ index: start,
8104
+ line: (_node$loc$start$line = (_node$loc2 = node.loc) == null || (_node$loc2 = _node$loc2.start) == null ? void 0 : _node$loc2.line) != null ? _node$loc$start$line : 1,
8105
+ column: startColumn
8106
+ },
8107
+ end: {
8108
+ index: start,
8109
+ line: (_node$loc$start$line2 = (_node$loc3 = node.loc) == null || (_node$loc3 = _node$loc3.start) == null ? void 0 : _node$loc3.line) != null ? _node$loc$start$line2 : 1,
8110
+ column: startColumn
8111
+ }
8112
+ }
8113
+ };
8114
+ }
8115
+ /**
8116
+ * Node types whose renderer increases the indentation level by one (`this.numSpaces++`).
8117
+ * @type {Set<string>}
8118
+ */
8119
+ const indentingNodeTypes = new Set(['ClassDeclaration', 'ClassExpression', 'FunctionDeclaration', 'FunctionExpression', 'ArrowFunctionExpression', 'ObjectMethod', 'ClassMethod', 'ClassPrivateMethod', 'BlockStatement', 'IfStatement', 'ObjectExpression', 'ArrayExpression', 'ObjectPattern', 'ParenthesizedExpression', 'JSXElement', 'JSXFragment', 'TSEnumDeclaration']);
8120
+ /**
8121
+ * Counts the renderer indentation levels that nest `node`, so comments can be
8122
+ * placed at the column the stringifier will use (`2` spaces per level).
8123
+ * @param {Node} node - The annotated node.
8124
+ * @param {Node[]} parents - The parent stack.
8125
+ * @returns {number} The indentation depth of the node.
8126
+ */
8127
+ function renderIndentDepth(node, parents) {
8128
+ const index = parents.findLastIndex(_ => _ === node);
8129
+ let depth = 0;
8130
+ for (let i = index - 1; i >= 0; i--) {
8131
+ if (indentingNodeTypes.has(parents[i].type)) {
8132
+ depth++;
8133
+ }
8134
+ }
8135
+ return depth;
8136
+ }
8137
+ /**
8138
+ * Attaches `@type` comments for typed variable declarations.
8139
+ * @param {import('@babel/types').VariableDeclaration} node - The declaration node.
8140
+ * @param {Node[]} parents - The parent stack.
8141
+ */
8142
+ function collectVariableTypeComments(node, parents) {
8143
+ for (const declarator of node.declarations) {
8144
+ var _declarator$id;
8145
+ const type = (_declarator$id = declarator.id) == null || (_declarator$id = _declarator$id.typeAnnotation) == null ? void 0 : _declarator$id.typeAnnotation;
8146
+ if (!type) {
8147
+ continue;
8148
+ }
8149
+ attachComment(node, [`@type {${tsTypeToJSDoc(type)}}`], parents);
8150
+ break; // only annotate once per declaration statement
8151
+ }
8152
+ }
8153
+ /**
8154
+ * A roundtrip between TypeScript code -> JavaScript code with JSDoc types.
8155
+ * @param {string} code - The TypeScript code.
8156
+ * @param {object} [options] - Options for the conversion.
8157
+ * @param {boolean} [options.filename] - Unused placeholder, kept for API symmetry.
8158
+ * @returns {string} The converted JavaScript code.
8159
+ */
8160
+ function ts2js(code, options = {}) {
8161
+ const ast = parseTS(code);
8162
+ annotate(ast.program, []);
8163
+ const stringifier = new ToJS();
8164
+ const source = stringifier.toSource(ast);
8165
+ return formatCommentBreaks(stringifier.getHeader() + source);
8166
+ }
8167
+ /**
8168
+ * Moves a statement that follows a generated JSDoc block onto its own line,
8169
+ * e.g. a closing comment marker directly followed by `export function f() {}`
8170
+ * becomes the closing marker, a newline, then `export function f() {}`.
8171
+ * @param {string} source - The generated source code.
8172
+ * @returns {string} The source with fixed comment/statement breaks.
8173
+ */
8174
+ function formatCommentBreaks(source) {
8175
+ return source.replace(/ \*\/ +(?=[a-zA-Z_$])/g, '*/\n');
8176
+ }
8177
+ /**
8178
+ * Parses TypeScript code, automatically falling back to TSX (JSX) mode
8179
+ * when the input contains JSX elements.
8180
+ * @param {string} code - The TypeScript code.
8181
+ * @returns {import('@babel/parser').ParseResult<import('@babel/types').File>} The parsed AST.
8182
+ */
8183
+ function parseTS(code) {
8184
+ try {
8185
+ return parse(code, {
8186
+ sourceType: 'module',
8187
+ plugins: ['typescript']
8188
+ });
8189
+ } catch (e) {
8190
+ return parse(code, {
8191
+ sourceType: 'module',
8192
+ plugins: [['typescript', {
8193
+ isTSX: true
8194
+ }], 'jsx']
8195
+ });
8196
+ }
8197
+ }
8198
+
8199
+ /**
8200
+ * @file
8201
+ * @todo Consider rendering nested namespaces leading flat, so their member
8202
+ * columns align with the source indentation.
8203
+ */
8204
+
8205
+ export { Asserter, JSDocAnnotator, Stringifier, ToJS, WATConverter, addTypeChecks, annotateOptional, ast2json, ast2jsonForComparison, capitalize, code2ast2code, compareAST, expandType, expandTypeBabelTS, expandTypeDepFree, extractCurlyContent, extractNameAndOptionality, nodeChildren, nodeIsFunctionLike, parseJSDoc, parseJSDocSetter, parseJSDocTemplates, parseJSDocTypedef, parseType, parseTypeBabelTS, parserOptions, requiredTypeofs, simplifyType, simplifyTypeToSource, toSourceBabelTS, toSourceTS, trimEndSpaces, ts2js, tsTypeToJSDoc };