typenative 0.0.21 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/transpiler.js CHANGED
@@ -9,7 +9,7 @@ function isArrayLiteralExpression(n) {
9
9
  return n?.kind === 'ArrayLiteralExpression';
10
10
  }
11
11
  function isArrayTypeNode(n) {
12
- return n?.kind === 'ArrayTypeNode';
12
+ return n?.kind === 'ArrayType';
13
13
  }
14
14
  function isArrowFunction(n) {
15
15
  return n?.kind === 'ArrowFunction';
@@ -45,7 +45,7 @@ function isConditionalExpression(n) {
45
45
  return n?.kind === 'ConditionalExpression';
46
46
  }
47
47
  function isConstructorDeclaration(n) {
48
- return n?.kind === 'ConstructorDeclaration';
48
+ return n?.kind === 'Constructor';
49
49
  }
50
50
  function isDefaultClause(n) {
51
51
  return n?.kind === 'DefaultClause';
@@ -84,7 +84,7 @@ function isFunctionExpression(n) {
84
84
  return n?.kind === 'FunctionExpression';
85
85
  }
86
86
  function isFunctionTypeNode(n) {
87
- return n?.kind === 'FunctionTypeNode';
87
+ return n?.kind === 'FunctionType';
88
88
  }
89
89
  function isGetAccessor(n) {
90
90
  return n?.kind === 'GetAccessor';
@@ -102,7 +102,7 @@ function isInterfaceDeclaration(n) {
102
102
  return n?.kind === 'InterfaceDeclaration';
103
103
  }
104
104
  function isLiteralTypeNode(n) {
105
- return n?.kind === 'LiteralTypeNode';
105
+ return n?.kind === 'LiteralType';
106
106
  }
107
107
  function isMethodDeclaration(n) {
108
108
  return n?.kind === 'MethodDeclaration';
@@ -198,10 +198,10 @@ function isTypeAssertionExpression(n) {
198
198
  return n?.kind === 'TypeAssertionExpression';
199
199
  }
200
200
  function isTypeReferenceNode(n) {
201
- return n?.kind === 'TypeReferenceNode';
201
+ return n?.kind === 'TypeReference';
202
202
  }
203
203
  function isUnionTypeNode(n) {
204
- return n?.kind === 'UnionTypeNode';
204
+ return n?.kind === 'UnionType';
205
205
  }
206
206
  function isVariableDeclaration(n) {
207
207
  return n?.kind === 'VariableDeclaration';
@@ -250,6 +250,15 @@ function goSafeId() {
250
250
  return id;
251
251
  }
252
252
  let parseFunction;
253
+ // Declarations of every parsed file, collected before visiting (functions are
254
+ // hoisted, so call sites may precede them) for contextual typing
255
+ const declaredFunctions = new Map();
256
+ const declaredInterfaces = new Map();
257
+ const declaredTypeAliases = new Map();
258
+ // Package-level declarations of the main file's top-level variables
259
+ const mainPackageVariables = [];
260
+ // True while emitting a separate Go file, whose top-level statements sit at package scope
261
+ let emittingModuleFile = false;
253
262
  const importedPackages = new Set();
254
263
  let outsideNodes = [];
255
264
  const classNames = new Set();
@@ -265,6 +274,21 @@ const dangerousNames = new Set([
265
274
  'select', 'struct', 'switch', 'type', 'var',
266
275
  ]);
267
276
  const renamedFunctions = new Map();
277
+ // Go type of the receiver of the method call being emitted (for method handlers)
278
+ let currentReceiverGoType;
279
+ // Struct field / member names: Go keywords get a trailing underscore
280
+ function goFieldName(name) {
281
+ return name !== 'main' && dangerousNames.has(name) ? `${name}_` : name;
282
+ }
283
+ function isFieldNameIdentifier(node) {
284
+ const parent = node.parent;
285
+ if (!parent || parent.name !== node)
286
+ return false;
287
+ return (isPropertyAccessExpression(parent) ||
288
+ isPropertyAssignment(parent) ||
289
+ isPropertySignature(parent) ||
290
+ isPropertyDeclaration(parent));
291
+ }
268
292
  const variableTypes = new Map();
269
293
  const variableGoTypes = new Map();
270
294
  const variableClassNames = new Map();
@@ -329,7 +353,44 @@ function operatorTokenText(token) {
329
353
  function defaultParseFunction(_code) {
330
354
  throw new Error('transpileToNative: no parse function injected (options.parse)');
331
355
  }
356
+ // Parses via the injected parser and links each node to its parent: the JSON
357
+ // AST carries no parent pointers, but the transpiler walks up via node.parent.
358
+ function parseSource(code) {
359
+ const sourceFile = parseFunction(code);
360
+ linkParents(sourceFile, undefined);
361
+ for (const stmt of sourceFile.statements ?? []) {
362
+ if (!stmt.name || !isIdentifier(stmt.name))
363
+ continue;
364
+ if (isFunctionDeclaration(stmt))
365
+ declaredFunctions.set(stmt.name.text, stmt);
366
+ else if (isInterfaceDeclaration(stmt))
367
+ declaredInterfaces.set(stmt.name.text, stmt);
368
+ else if (isTypeAliasDeclaration(stmt))
369
+ declaredTypeAliases.set(stmt.name.text, stmt.type);
370
+ }
371
+ return sourceFile;
372
+ }
373
+ function linkParents(node, parent) {
374
+ node.parent = parent;
375
+ for (const key of Object.keys(node)) {
376
+ if (key === 'parent')
377
+ continue;
378
+ const value = node[key];
379
+ if (Array.isArray(value)) {
380
+ for (const child of value) {
381
+ if (child && typeof child === 'object' && child.kind)
382
+ linkParents(child, node);
383
+ }
384
+ }
385
+ else if (value && typeof value === 'object' && value.kind) {
386
+ linkParents(value, node);
387
+ }
388
+ }
389
+ }
332
390
  // Renders a type node's source text syntactically (no typechecker needed).
391
+ function isAnyTypeNode(n) {
392
+ return n?.kind === 'AnyKeyword' || n?.kind === 'UnknownKeyword';
393
+ }
333
394
  function typeNodeToText(node) {
334
395
  if (!node)
335
396
  return 'any';
@@ -349,13 +410,13 @@ function typeNodeToText(node) {
349
410
  };
350
411
  if (keywordMap[node.kind])
351
412
  return keywordMap[node.kind];
352
- if (node.kind === 'TypeReferenceNode' && node.typeName) {
413
+ if (node.kind === 'TypeReference' && node.typeName) {
353
414
  return typeNodeToText(node.typeName);
354
415
  }
355
416
  if (node.kind === 'Identifier')
356
417
  return node.text;
357
- if (node.kind === 'ArrayTypeNode' && node.elementType) {
358
- return `${'{'}${''}}typeNodeToText(node.elementType)[]`;
418
+ if (node.kind === 'ArrayType' && node.elementType) {
419
+ return `${typeNodeToText(node.elementType)}[]`;
359
420
  }
360
421
  return 'any';
361
422
  }
@@ -363,7 +424,7 @@ function typeNodeToText(node) {
363
424
  function childNodes(node) {
364
425
  const out = [];
365
426
  for (const key of Object.keys(node ?? {})) {
366
- if (key === 'kind' || key === 'text')
427
+ if (key === 'kind' || key === 'text' || key === 'parent')
367
428
  continue;
368
429
  const v = node[key];
369
430
  if (v && typeof v === 'object' && typeof v.kind === 'string') {
@@ -391,8 +452,14 @@ const helperProvidedPackages = new Set();
391
452
  export function transpileToNative(code, options) {
392
453
  fileResolver = options?.readFile ?? null;
393
454
  parseFunction = options?.parse ?? defaultParseFunction;
455
+ declaredFunctions.clear();
456
+ declaredInterfaces.clear();
457
+ declaredTypeAliases.clear();
458
+ variableTypeNodes.clear();
459
+ narrowedVariables.clear();
460
+ nodeCallResultTypes.clear();
394
461
  currentFileDir = null;
395
- const sourceFile = parseFunction(code);
462
+ const sourceFile = parseSource(code);
396
463
  importedPackages.clear();
397
464
  outsideNodes = [];
398
465
  classNames.clear();
@@ -415,11 +482,14 @@ export function transpileToNative(code, options) {
415
482
  classStaticProps.clear();
416
483
  usedHelpers.clear();
417
484
  helperProvidedPackages.clear();
485
+ mainPackageVariables.length = 0;
418
486
  const transpiledCode = visit(sourceFile, { addFunctionOutside: true });
419
487
  const transpiledCodeOutside = outsideNodes.map((n) => visit(n, { isOutside: true })).join('\n');
420
488
  const main = `package main
421
489
 
422
- ${[...importedPackages].map((pkg) => `import "${pkg}"`).join('\n')}
490
+ ${[...importedPackages].sort().map((pkg) => `import "${pkg}"`).join('\n')}
491
+
492
+ ${mainPackageVariables.join('\n')}
423
493
 
424
494
  func main() {
425
495
  ${transpiledCode.trim()}
@@ -431,6 +501,13 @@ ${emitGoHelpers()}`.trimEnd();
431
501
  }
432
502
  export function visit(node, options = {}) {
433
503
  let code = '';
504
+ // Reads of a narrowed nullable property path dereference it
505
+ if (narrowedVariables.size > 0 && isPropertyAccessExpression(node) && !options.skipNarrowing) {
506
+ const key = getNarrowingKey(node);
507
+ if (key && narrowedVariables.has(key) && isNarrowableReference(node)) {
508
+ return `(*${visit(node, { ...options, skipNarrowing: true })})`;
509
+ }
510
+ }
434
511
  if (isSourceFile(node)) {
435
512
  return (node.statements ?? [])
436
513
  .map((n) => visit(n, { addFunctionOutside: true }))
@@ -438,18 +515,34 @@ export function visit(node, options = {}) {
438
515
  .join(options.inline ? '' : '\n\t');
439
516
  }
440
517
  else if (isIdentifier(node)) {
518
+ if (isFieldNameIdentifier(node))
519
+ return goFieldName(node.text);
520
+ if (node.text === 'Boolean' && isCallExpression(node.parent) && node.parent.arguments?.[0] === node) {
521
+ const receiver = isPropertyAccessExpression(node.parent.expression)
522
+ ? inferExpressionType(node.parent.expression.expression)
523
+ : undefined;
524
+ const elementType = receiver?.startsWith('[]') ? receiver.slice(2) : 'interface{}';
525
+ return `func(__v ${elementType}) bool { return ${truthinessCheck('__v', elementType)} }`;
526
+ }
441
527
  if (node.text === 'undefined')
442
528
  return 'nil';
529
+ if (node.text === 'NaN' || node.text === 'Infinity') {
530
+ importedPackages.add('math');
531
+ return node.text === 'NaN' ? 'math.NaN()' : 'math.Inf(1)';
532
+ }
443
533
  const goAlias = importAliases.get(node.text);
444
534
  if (goAlias)
445
535
  return goAlias;
536
+ if (narrowedVariables.has(node.text) && isNarrowableReference(node)) {
537
+ return `(*${getSafeName(node.text)})`;
538
+ }
446
539
  return getSafeName(node.text);
447
540
  }
448
541
  else if (isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node)) {
449
542
  return toGoStringLiteral(node.text);
450
543
  }
451
544
  else if (isAsExpression(node)) {
452
- return visit(node.expression);
545
+ return toGoValueOfType(node.expression, getAsExpressionType(node));
453
546
  }
454
547
  else if (isTypeAssertionExpression(node)) {
455
548
  return visit(node.expression);
@@ -480,7 +573,7 @@ export function visit(node, options = {}) {
480
573
  return `regexp.MustCompile("${goFlags}${escaped}")`;
481
574
  }
482
575
  else if (isArrayLiteralExpression(node)) {
483
- const type = isVariableDeclaration(node.parent) ? getType(node.parent.type, true) : '';
576
+ const type = getArrayLiteralElementType(node);
484
577
  const hasSpread = (node.elements ?? []).some((e) => isSpreadElement(e));
485
578
  if (hasSpread) {
486
579
  return visitSpreadArrayLiteral(node, type);
@@ -488,17 +581,50 @@ export function visit(node, options = {}) {
488
581
  return `[]${type} {${(node.elements ?? []).map((e) => visit(e)).join(', ')}}`;
489
582
  }
490
583
  else if (isBlock(node)) {
491
- return `{\n\t\t${options.prefixBlockContent ?? ''}${(node.statements ?? [])
492
- .map((n) => visit(n))
493
- .join('\t')}${options.extraBlockContent ?? ''}}${options.inline ? '' : '\n\t'}`;
584
+ return `{\n\t\t${options.prefixBlockContent ?? ''}${visitBlockStatements(node.statements ?? [])}${options.extraBlockContent ?? ''}}${options.inline ? '' : '\n\t'}`;
494
585
  }
495
586
  else if (isElementAccessExpression(node)) {
496
587
  if (hasQuestionDot(node)) {
497
588
  return visitOptionalElementAccess(node);
498
589
  }
590
+ if (isDynamicValue(node.expression)) {
591
+ useHelper('dynamic');
592
+ return `TnIndex(${visit(node.expression)}, ${visit(node.argumentExpression)})`;
593
+ }
594
+ // process.env['X'] → TnGetenv("X")
595
+ if (isProcessEnv(node.expression)) {
596
+ useHelper('getenv');
597
+ return `TnGetenv(${visit(node.argumentExpression)})`;
598
+ }
599
+ // Maps (Record/Map) and string keys index directly; arrays/strings need an int index
600
+ const targetType = inferExpressionType(node.expression);
601
+ if (targetType === 'string') {
602
+ // s[i] in JS is a one-character string ("" when out of range)
603
+ useHelper('dynamic');
604
+ return `TnCharAt(${visit(node.expression)}, ${visit(node.argumentExpression)})`;
605
+ }
606
+ if (targetType?.startsWith('*TnMap[')) {
607
+ return `${visit(node.expression)}.Get(${toGoValueOfType(node.argumentExpression, extractMapKeyType(targetType))})`;
608
+ }
609
+ if (isStringLiteral(node.argumentExpression)) {
610
+ return `${visit(node.expression)}[${visit(node.argumentExpression)}]`;
611
+ }
612
+ // Reading past the end of an array gives undefined in JS: the zero value here
613
+ if (targetType?.startsWith('[]') && !isAssignmentTarget(node)) {
614
+ useHelper('dynamic');
615
+ return `TnAt(${visit(node.expression)}, ${visit(node.argumentExpression)})`;
616
+ }
499
617
  return `${visit(node.expression)}[int(${visit(node.argumentExpression)})]`;
500
618
  }
501
619
  else if (isPropertyAccessExpression(node)) {
620
+ if (isDynamicValue(node.expression) && node.name.text === 'length' && !isCallee(node)) {
621
+ useHelper('dynamic');
622
+ return `TnLength(${visit(node.expression)})`;
623
+ }
624
+ if (isDynamicValue(node.expression) && !isCallee(node) && !isProcessEnv(node.expression)) {
625
+ useHelper('dynamic');
626
+ return `TnGet(${visit(node.expression)}, "${node.name.text}")`;
627
+ }
502
628
  if (hasQuestionDot(node)) {
503
629
  return visitOptionalPropertyAccess(node);
504
630
  }
@@ -538,7 +664,9 @@ export function visit(node, options = {}) {
538
664
  }
539
665
  // Array destructuring: const [a, b] = arr
540
666
  if (isArrayBindingPattern(node.name) && node.initializer) {
541
- const initExpr = visit(node.initializer);
667
+ const initExpr = isDynamicValue(node.initializer)
668
+ ? toGoValueOfType(node.initializer, '[]interface{}')
669
+ : visit(node.initializer);
542
670
  const tmpVar = getTempName('arr');
543
671
  const parts = [`${tmpVar} := ${initExpr}`];
544
672
  node.name.elements.forEach((el, idx) => {
@@ -556,12 +684,31 @@ export function visit(node, options = {}) {
556
684
  });
557
685
  return parts.join(';\n\t');
558
686
  }
559
- const type = getType(node.type);
687
+ // `const x: any = expr` keeps inference (x := expr); `any` elsewhere is interface{}
688
+ const isInferredAny = isAnyTypeNode(node.type) && !!node.initializer;
689
+ const type = isInferredAny ? ':' : getType(node.type);
560
690
  // Track variable type for type-aware method dispatch
561
691
  if (isIdentifier(node.name)) {
562
- if (node.type) {
692
+ // A declaration replaces whatever an earlier variable of the same name recorded
693
+ const inferredType = node.initializer && (!node.type || isInferredAny)
694
+ ? inferExpressionType(node.initializer)
695
+ : undefined;
696
+ variableTypes.delete(node.name.text);
697
+ variableClassNames.delete(node.name.text);
698
+ narrowedVariables.delete(node.name.text);
699
+ if (node.type && !isInferredAny)
700
+ variableTypeNodes.set(node.name.text, node.type);
701
+ else
702
+ variableTypeNodes.delete(node.name.text);
703
+ if (node.type && !isInferredAny) {
563
704
  variableGoTypes.set(node.name.text, getType(node.type));
564
705
  }
706
+ else if (inferredType) {
707
+ variableGoTypes.set(node.name.text, inferredType);
708
+ }
709
+ else {
710
+ variableGoTypes.delete(node.name.text);
711
+ }
565
712
  const cat = node.type ? getTypeCategory(node.type) : undefined;
566
713
  if (cat) {
567
714
  variableTypes.set(node.name.text, cat);
@@ -586,22 +733,25 @@ export function visit(node, options = {}) {
586
733
  else if (node.initializer && isRegularExpressionLiteral(node.initializer)) {
587
734
  variableTypes.set(node.name.text, 'RegExp');
588
735
  }
589
- if (!variableGoTypes.has(node.name.text) && node.initializer) {
590
- const inferredType = inferExpressionType(node.initializer);
591
- if (inferredType) {
592
- variableGoTypes.set(node.name.text, inferredType);
593
- }
594
- }
595
736
  }
596
737
  let initializer = node.initializer ? `= ${visit(node.initializer)}` : '';
597
738
  // Wrap non-nil values assigned to nullable primitive pointer types
598
739
  // Only wrap for primitive pointers (*string, *float64, *bool), not class pointers
599
- if (node.initializer &&
600
- type.startsWith('*') &&
601
- !isNilLiteral(node.initializer) &&
602
- ['*string', '*float64', '*bool'].includes(type)) {
603
- const value = visit(node.initializer);
604
- initializer = `= func() ${type} { v := ${value}; return &v }()`;
740
+ if (node.initializer && (type.startsWith('*') || isDynamicValue(node.initializer))) {
741
+ initializer = `= ${toGoValueOfType(node.initializer, type)}`;
742
+ }
743
+ // Package scope has no `:=`: module-level declarations need `var x = expr`
744
+ const isTopLevelStatement = isVariableStatement(node.parent?.parent) && isSourceFile(node.parent?.parent?.parent);
745
+ const isPackageScope = emittingModuleFile && isTopLevelStatement;
746
+ if (type === ':' && isPackageScope) {
747
+ return `var ${visit(node.name)} ${initializer}`;
748
+ }
749
+ const isMainTopLevel = !emittingModuleFile && isTopLevelStatement;
750
+ const packageType = type === ':' ? variableGoTypes.get(node.name.text) : type;
751
+ if (isMainTopLevel && packageType && packageType !== 'nil' && isIdentifier(node.name)) {
752
+ const name = visit(node.name);
753
+ mainPackageVariables.push(`var ${name} ${packageType}`);
754
+ return initializer ? `${name} ${initializer}` : '';
605
755
  }
606
756
  return `${type === ':' ? '' : 'var '}${visit(node.name)} ${type}${type === ':' ? '' : ' '}${initializer}`;
607
757
  }
@@ -610,6 +760,18 @@ export function visit(node, options = {}) {
610
760
  (isPropertyAccessExpression(node.expression) && hasQuestionDot(node.expression))) {
611
761
  return visitOptionalCall(node);
612
762
  }
763
+ if (isPropertyAccessExpression(node.expression) && isOptionalChain(node.expression.expression)) {
764
+ const receiverType = inferExpressionType(node.expression.expression);
765
+ if (receiverType && NULLABLE_PRIMITIVE_TYPES.includes(receiverType)) {
766
+ return visitNullablePrimitiveOptionalCall(node, visit(node.expression.expression), receiverType);
767
+ }
768
+ }
769
+ const dynamicCall = visitDynamicMethodCall(node);
770
+ if (dynamicCall)
771
+ return dynamicCall;
772
+ const regexReplace = visitRegexReplace(node);
773
+ if (regexReplace)
774
+ return regexReplace;
613
775
  // IIFE with named function expression: (function name() { ... })()
614
776
  if (isParenthesizedExpression(node.expression) &&
615
777
  isFunctionExpression(node.expression.expression)) {
@@ -621,7 +783,7 @@ export function visit(node, options = {}) {
621
783
  const inferredRetType = inferFunctionBodyReturnType(fn);
622
784
  const returnType = inferredRetType ? ` ${inferredRetType}` : '';
623
785
  const args = (node.arguments ?? []).map((a) => visit(a)).join(', ');
624
- return `func(${parameterInfo.signature})${returnType} ${visit(fn.body, { prefixBlockContent: parameterInfo.prefixBlockContent })}(${args})`;
786
+ return `func(${parameterInfo.signature})${returnType} ${visit(fn.body, { prefixBlockContent: parameterInfo.prefixBlockContent }).trimEnd()}(${args})`;
625
787
  }
626
788
  // Handle setTimeout specially to get raw delay value
627
789
  if (isIdentifier(node.expression) && node.expression.text === 'setTimeout') {
@@ -640,21 +802,37 @@ export function visit(node, options = {}) {
640
802
  const typeArgs = getTypeArguments((node.typeArguments ?? []));
641
803
  // Handle spread arguments: fn(...arr) → fn(arr...)
642
804
  const hasSpreadArg = (node.arguments ?? []).some((a) => isSpreadElement(a));
643
- const args = hasSpreadArg
644
- ? (node.arguments ?? []).map((a) => isSpreadElement(a) ? `${visit(a.expression)}...` : visit(a))
645
- : (node.arguments ?? []).map((a) => visit(a));
805
+ const args = hasSpreadArg ? visitSpreadCallArguments(node) : visitCallArguments(node);
646
806
  // Resolve object type for type-aware method dispatch
647
807
  let objectType;
648
808
  if (isPropertyAccessExpression(node.expression)) {
649
809
  objectType = resolveExpressionType(node.expression.expression);
650
810
  }
811
+ currentReceiverGoType = isPropertyAccessExpression(node.expression)
812
+ ? inferExpressionType(node.expression.expression)
813
+ : undefined;
651
814
  return getCallString(safeCaller, args, typeArgs, objectType);
652
815
  }
653
816
  else if (isPrefixUnaryExpression(node)) {
817
+ if (node.operator === 'ExclamationToken') {
818
+ return `!${wrapCondition(toGoCondition(node.operand))}`;
819
+ }
820
+ if (isIncrementOrDecrement(node))
821
+ return visitIncrementOrDecrement(node, true);
822
+ if (node.operator === 'TildeToken') {
823
+ useHelper('dynamic');
824
+ return `float64(^TnInt32(${visit(node.operand)}))`;
825
+ }
654
826
  return `${getOperatorText(node.operator)}${visit(node.operand)}`;
655
827
  }
656
828
  else if (isPostfixUnaryExpression(node)) {
657
- return `${visit(node.operand, { inline: true })}${getOperatorText(node.operator)}`;
829
+ return visitIncrementOrDecrement(node, false);
830
+ }
831
+ else if (node.kind === 'DeleteExpression') {
832
+ return visitDelete(node);
833
+ }
834
+ else if (node.kind === 'TypeOfExpression') {
835
+ return visitTypeOf(node);
658
836
  }
659
837
  else if (isConditionalExpression(node)) {
660
838
  return visitConditionalExpression(node);
@@ -663,12 +841,57 @@ export function visit(node, options = {}) {
663
841
  if (node.operatorToken.kind === 'QuestionQuestionToken') {
664
842
  return visitNullishCoalescingExpression(node);
665
843
  }
844
+ if (node.operatorToken.kind === 'InstanceOfKeyword' && isIdentifier(node.right)) {
845
+ // x instanceof C: the runtime type is C's pointer type
846
+ return `func() bool { _, ok := interface{}(${visit(node.left)}).(*${getSafeName(node.right.text)}); return ok }()`;
847
+ }
848
+ if (node.operatorToken.kind === 'InKeyword') {
849
+ if (isDynamicValue(node.right)) {
850
+ useHelper('dynamic');
851
+ return `TnHas(${visit(node.right)}, ${visit(node.left)})`;
852
+ }
853
+ return `${visit(node.right)}.Has(${visit(node.left)})`;
854
+ }
855
+ if (isLogicalOperator(node.operatorToken)) {
856
+ const logical = visitLogicalExpression(node);
857
+ if (logical)
858
+ return logical;
859
+ }
860
+ // x ??= v → assign only when x is nil
861
+ if (node.operatorToken.kind === 'BarBarEqualsToken' || node.operatorToken.kind === 'AmpersandAmpersandEqualsToken') {
862
+ // a ||= b assigns when a is falsy; a &&= b when a is truthy
863
+ const target = visit(node.left);
864
+ const condition = toGoCondition(node.left);
865
+ const value = toGoValueOfType(node.right, inferExpressionType(node.left));
866
+ const test = node.operatorToken.kind === 'BarBarEqualsToken' ? `!${wrapCondition(condition)}` : condition;
867
+ return `if ${test} { ${target} = ${value} }`;
868
+ }
869
+ if (node.operatorToken.kind === 'QuestionQuestionEqualsToken') {
870
+ const target = visit(node.left);
871
+ const value = toGoValueOfType(node.right, inferExpressionType(node.left));
872
+ return `if ${target} == nil { ${target} = ${value} }`;
873
+ }
666
874
  let op = operatorTokenText(node.operatorToken);
667
875
  if (op === '===')
668
876
  op = '==';
669
877
  if (op === '!==')
670
878
  op = '!=';
671
879
  // Go's % is not defined on float64 (TS numbers all map to float64)
880
+ if (op === '/' && isNumericLiteral(node.right) && Number(node.right.text) === 0) {
881
+ return `func(a, b float64) float64 { return a / b }(${visit(node.left)}, ${visit(node.right)})`;
882
+ }
883
+ const bitwise = visitBitwise(node, op);
884
+ if (bitwise)
885
+ return bitwise;
886
+ if (op === '**') {
887
+ importedPackages.add('math');
888
+ return `math.Pow(${visit(node.left)}, ${visit(node.right)})`;
889
+ }
890
+ if (op === '**=') {
891
+ importedPackages.add('math');
892
+ const target = visit(node.left);
893
+ return `${target} = math.Pow(${target}, ${visit(node.right)})`;
894
+ }
672
895
  if (op === '%') {
673
896
  importedPackages.add('math');
674
897
  return `math.Mod(${visit(node.left)}, ${visit(node.right)})`;
@@ -678,6 +901,75 @@ export function visit(node, options = {}) {
678
901
  const left = visit(node.left);
679
902
  return `${left} = math.Mod(${left}, ${visit(node.right)})`;
680
903
  }
904
+ if (['<', '>', '<=', '>=', '-', '*', '/', '+'].includes(op)) {
905
+ const dynamicArithmetic = visitDynamicArithmetic(node, op);
906
+ if (dynamicArithmetic)
907
+ return dynamicArithmetic;
908
+ }
909
+ if (op === '==' || op === '!=') {
910
+ // any values: JS equality (objects by identity) without Go's uncomparable-type panic
911
+ if ((isDynamicValue(node.left) || isDynamicValue(node.right)) &&
912
+ !isNilLiteral(node.left) &&
913
+ !isNilLiteral(node.right)) {
914
+ useHelper('dynamic');
915
+ const same = `TnSame(${visit(node.left)}, ${visit(node.right)})`;
916
+ return op === '==' ? same : `!${same}`;
917
+ }
918
+ const nullableComparison = visitNullableComparison(node, op);
919
+ if (nullableComparison)
920
+ return nullableComparison;
921
+ // A value type (string, number, struct…) is never null/undefined
922
+ const otherSide = isNilLiteral(node.right) ? node.left : isNilLiteral(node.left) ? node.right : undefined;
923
+ const otherType = otherSide ? inferExpressionType(otherSide) : undefined;
924
+ if (otherType && otherType !== 'nil' && !isNilableGoType(otherType)) {
925
+ return op === '!=' ? 'true' : 'false';
926
+ }
927
+ }
928
+ if (op === '=' && isPropertyAccessExpression(node.left) && isDynamicValue(node.left.expression)) {
929
+ useHelper('dynamic');
930
+ return `TnSet(${visit(node.left.expression)}, "${node.left.name.text}", ${visit(node.right)})`;
931
+ }
932
+ // m[k] = v on an ordered map
933
+ if (isElementAccessExpression(node.left) && isAssignmentTarget(node.left)) {
934
+ const mapType = inferExpressionType(node.left.expression);
935
+ if (mapType?.startsWith('*TnMap[')) {
936
+ const map = visit(node.left.expression);
937
+ const key = toGoValueOfType(node.left.argumentExpression, extractMapKeyType(mapType));
938
+ const value = op === '='
939
+ ? toGoValueOfType(node.right, extractMapValueType(mapType))
940
+ : `${map}.Get(${key}) ${op.slice(0, -1)} ${visit(node.right)}`;
941
+ return `${map}.Set(${key}, ${value})`;
942
+ }
943
+ }
944
+ // arr.length = n truncates the slice
945
+ if (op === '=' &&
946
+ isPropertyAccessExpression(node.left) &&
947
+ node.left.name.text === 'length' &&
948
+ inferExpressionType(node.left.expression)?.startsWith('[]')) {
949
+ const array = visit(node.left.expression);
950
+ return `${array} = ${array}[:int(${visit(node.right)})]`;
951
+ }
952
+ // Assigning to a nullable primitive (*T) boxes the value
953
+ if (op === '=' && isIdentifier(node.left)) {
954
+ const leftType = variableGoTypes.get(node.left.text);
955
+ if (leftType && (leftType.startsWith('*') || isDynamicValue(node.right))) {
956
+ return `${getSafeName(node.left.text)} = ${toGoValueOfType(node.right, leftType)}`;
957
+ }
958
+ }
959
+ if (op === '&&' || op === '||') {
960
+ const right = withNarrowing(getNarrowedNames(node.left, op === '&&'), () => visit(node.right));
961
+ return `${visit(node.left)} ${op} ${right}`;
962
+ }
963
+ if (op === '+') {
964
+ const leftType = inferExpressionType(node.left);
965
+ const rightType = inferExpressionType(node.right);
966
+ if (leftType === 'string' && rightType !== 'string') {
967
+ return `${visit(node.left)} + ${jsStringOf(visit(node.right), rightType)}`;
968
+ }
969
+ if (rightType === 'string' && leftType !== 'string') {
970
+ return `${jsStringOf(visit(node.left), leftType)} + ${visit(node.right)}`;
971
+ }
972
+ }
681
973
  return `${visit(node.left)} ${op} ${visit(node.right)}`;
682
974
  }
683
975
  else if (isParenthesizedExpression(node)) {
@@ -694,15 +986,15 @@ export function visit(node, options = {}) {
694
986
  return visit(node.expression) + (options.inline ? '' : ';\n\t');
695
987
  }
696
988
  else if (isForStatement(node)) {
697
- return `for ${visit(node.initializer, { inline: true })}; ${visit(node.condition, {
698
- inline: true
699
- })}; ${visit(node.incrementor, { inline: true })}${visit(node.statement)}`;
989
+ return `for ${visit(node.initializer, { inline: true })}; ${node.condition ? toGoCondition(node.condition) : ''}; ${visit(node.incrementor, { inline: true })}${visitLoopBody(node.statement)}`;
700
990
  }
701
991
  else if (isForInStatement(node)) {
702
992
  const varName = isVariableDeclarationList(node.initializer)
703
993
  ? visit(node.initializer.declarations[0].name)
704
994
  : visit(node.initializer);
705
- return `for ${varName} := range ${visit(node.expression, { inline: true })}${visit(node.statement)}`;
995
+ const iterated = visit(node.expression, { inline: true });
996
+ const isOrderedMap = inferExpressionType(node.expression)?.startsWith('*TnMap[');
997
+ return `for ${varName} := range ${iterated}${isOrderedMap ? '.All()' : ''}${visitLoopBody(node.statement)}`;
706
998
  }
707
999
  else if (isForOfStatement(node)) {
708
1000
  // Unwrap Object.entries(x) → treat x as the iterable
@@ -715,31 +1007,38 @@ export function visit(node, options = {}) {
715
1007
  iterNode.arguments.length > 0) {
716
1008
  iterNode = iterNode.arguments[0];
717
1009
  }
718
- const iterExpr = visit(iterNode, { inline: true });
1010
+ const iterExpr = isDynamicValue(iterNode)
1011
+ ? toGoValueOfType(iterNode, '[]interface{}')
1012
+ : visit(iterNode, { inline: true });
719
1013
  const iterType = inferExpressionType(iterNode);
720
- if (iterType && iterType.startsWith('map[')) {
1014
+ if (iterType && iterType.startsWith('*TnMap[')) {
721
1015
  const valueType = extractMapValueType(iterType);
722
1016
  const isSet = valueType === 'struct{}';
723
1017
  const varInfo = getForOfVarNames(node.initializer);
724
- if (isSet) {
725
- return `for ${varInfo[0]} := range ${iterExpr}${visit(node.statement)}`;
726
- }
727
- else if (varInfo.length >= 2) {
728
- return `for ${varInfo[0]}, ${varInfo[1]} := range ${iterExpr}${visit(node.statement)}`;
729
- }
730
- else {
731
- return `for ${varInfo[0]} := range ${iterExpr}${visit(node.statement)}`;
1018
+ registerLocalVariable(varInfo[0], extractMapKeyType(iterType));
1019
+ if (varInfo.length >= 2 && !isSet)
1020
+ registerLocalVariable(varInfo[1], valueType);
1021
+ if (varInfo.length >= 2 && !isSet) {
1022
+ return `for ${varInfo[0]}, ${varInfo[1]} := range ${iterExpr}.All()${visitLoopBody(node.statement)}`;
732
1023
  }
1024
+ return `for ${varInfo[0]} := range ${iterExpr}.All()${visitLoopBody(node.statement)}`;
733
1025
  }
734
- return `for _,${visit(node.initializer, { inline: true })}= range ${iterExpr}${visit(node.statement)}`;
1026
+ return visitForOfSequence(node, iterExpr, iterType);
735
1027
  }
736
1028
  else if (isWhileStatement(node)) {
737
- return `for ${visit(node.expression, { inline: true })}${visit(node.statement)}`;
1029
+ // while ((x = next()) !== null) → for { x = next(); if !(x != nil) { break }; … }
1030
+ const hoisted = getHoistedConditionAssignment(node.expression);
1031
+ if (hoisted) {
1032
+ const condition = { ...node.expression, left: hoisted.left };
1033
+ const prefix = `${visit(hoisted, { inline: true })};\n\t\tif !(${visit(condition, {
1034
+ inline: true
1035
+ })}) {\n\t\t\tbreak\n\t\t}\n\t\t`;
1036
+ return `for ${visitLoopBody(node.statement, prefix)}`;
1037
+ }
1038
+ return `for ${toGoCondition(node.expression)}${visitLoopBody(node.statement)}`;
738
1039
  }
739
1040
  else if (isDoStatement(node)) {
740
- const condition = `\tif !(${visit(node.expression, {
741
- inline: true
742
- })}) {\n\t\t\tbreak \n\t\t}\n\t`;
1041
+ const condition = `\tif !(${toGoCondition(node.expression)}) {\n\t\t\tbreak \n\t\t}\n\t`;
743
1042
  return `for ${visit(node.statement, { inline: true, extraBlockContent: condition })}`;
744
1043
  }
745
1044
  else if (isIfStatement(node)) {
@@ -747,18 +1046,15 @@ export function visit(node, options = {}) {
747
1046
  // Go requires a block body; `} else` must stay on the same line, so the
748
1047
  // terminating ';' is only added when no else branch follows
749
1048
  const thenTerm = node.elseStatement ? '' : ';';
750
- const thenCode = isBlock(node.thenStatement)
1049
+ const thenCode = withNarrowing(getNarrowedNames(node.expression, true), () => isBlock(node.thenStatement)
751
1050
  ? visit(node.thenStatement, { inline: !!node.elseStatement })
752
- : `{\n${visit(node.thenStatement)}\n}${thenTerm}`;
753
- const condition = `if ${visit(node.expression, { inline: true })} ${thenCode}`;
1051
+ : `{\n${visit(node.thenStatement)}\n}${thenTerm}`);
1052
+ const condition = `if ${toGoCondition(node.expression)} ${thenCode}`;
754
1053
  if (node.elseStatement) {
755
- if (isBlock(node.elseStatement)) {
756
- return `${condition} else ${visit(node.elseStatement)}`;
757
- }
758
1054
  // else-if chains stay chained; other single statements get a block
759
- const elseCode = isIfStatement(node.elseStatement)
1055
+ const elseCode = withNarrowing(getNarrowedNames(node.expression, false), () => isBlock(node.elseStatement) || isIfStatement(node.elseStatement)
760
1056
  ? visit(node.elseStatement)
761
- : `{\n${visit(node.elseStatement)}\n}`;
1057
+ : `{\n${visit(node.elseStatement)}\n};\n\t`);
762
1058
  return `${condition} else ${elseCode}`;
763
1059
  }
764
1060
  return condition;
@@ -785,6 +1081,9 @@ export function visit(node, options = {}) {
785
1081
  else if (isBreakStatement(node)) {
786
1082
  return 'break';
787
1083
  }
1084
+ else if (node.kind === 'ContinueStatement') {
1085
+ return 'continue;\n\t';
1086
+ }
788
1087
  else if (isThrowStatement(node)) {
789
1088
  const expr = node.expression;
790
1089
  if (isNewExpression(expr) &&
@@ -807,7 +1106,14 @@ export function visit(node, options = {}) {
807
1106
  node.expression.expression.text === 'Promise') {
808
1107
  return visitPromiseReturn(node.expression, options);
809
1108
  }
810
- return (`return ${node.expression ? visit(node.expression) : ''}` + (options.inline ? '' : ';\n\t'));
1109
+ const enclosingFn = getEnclosingFunction(node);
1110
+ const isAsync = enclosingFn?.modifiers?.some((m) => m.kind === 'AsyncKeyword');
1111
+ const returnGoType = enclosingFn && !isAsync ? inferFunctionBodyReturnType(enclosingFn) : undefined;
1112
+ const value = node.expression ? toGoValueOfType(node.expression, returnGoType) : '';
1113
+ const terminator = options.inline ? '' : ';\n\t';
1114
+ if (tryReturn && enclosingFn === tryReturn.fn)
1115
+ return tryAwareReturn(value) + terminator;
1116
+ return `return ${value}` + terminator;
811
1117
  }
812
1118
  else if (isFunctionDeclaration(node) || isFunctionExpression(node)) {
813
1119
  if (options.addFunctionOutside) {
@@ -815,7 +1121,7 @@ export function visit(node, options = {}) {
815
1121
  return '';
816
1122
  }
817
1123
  const typeParams = getTypeParameters(node.typeParameters);
818
- const parameterInfo = getFunctionParametersInfo((node.parameters ?? []));
1124
+ const parameterInfo = withContextualParameters(node, getFunctionParametersInfo(node.parameters ?? []));
819
1125
  if (node.body && isBlock(node.body)) {
820
1126
  prescanVariableDeclarations(node.body);
821
1127
  }
@@ -831,7 +1137,7 @@ export function visit(node, options = {}) {
831
1137
  if (!node.name) {
832
1138
  return `func${typeParams}(${parameterInfo.signature})${returnType} ${visit(node.body, {
833
1139
  prefixBlockContent: parameterInfo.prefixBlockContent
834
- })}`;
1140
+ }).trimEnd()}`;
835
1141
  }
836
1142
  const name = visit(node.name, { inline: true });
837
1143
  const safeName = getSafeName(name);
@@ -840,7 +1146,7 @@ export function visit(node, options = {}) {
840
1146
  })}`;
841
1147
  }
842
1148
  else if (isArrowFunction(node)) {
843
- const parameterInfo = getFunctionParametersInfo((node.parameters ?? []));
1149
+ const parameterInfo = withContextualParameters(node, getFunctionParametersInfo(node.parameters ?? []));
844
1150
  const inferredRetType = inferFunctionBodyReturnType(node);
845
1151
  const returnType = inferredRetType ? ` ${inferredRetType}` : '';
846
1152
  if (parameterInfo.prefixBlockContent && !isBlock(node.body)) {
@@ -851,7 +1157,7 @@ export function visit(node, options = {}) {
851
1157
  }
852
1158
  return `func(${parameterInfo.signature})${returnType} ${visit(node.body, {
853
1159
  prefixBlockContent: parameterInfo.prefixBlockContent
854
- })}`;
1160
+ }).trimEnd()}`;
855
1161
  }
856
1162
  else if (node.kind === 'ThisKeyword') {
857
1163
  return 'self';
@@ -868,7 +1174,34 @@ export function visit(node, options = {}) {
868
1174
  }
869
1175
  else if (isTypeAliasDeclaration(node)) {
870
1176
  typeAliases.set(node.name.text, node.type);
871
- return '';
1177
+ if (node.type?.kind !== 'TypeLiteral')
1178
+ return '';
1179
+ // type X = { a: T } → type X struct { a T }
1180
+ if (options.addFunctionOutside) {
1181
+ outsideNodes.push(node);
1182
+ const properties = new Map();
1183
+ for (const member of node.type.members ?? []) {
1184
+ if (isPropertySignature(member) && isIdentifier(member.name)) {
1185
+ properties.set(member.name.text, getOptionalNodeType(member.type, !!member.questionToken));
1186
+ }
1187
+ }
1188
+ if (properties.size > 0)
1189
+ interfacePropertyTypes.set(visit(node.name), properties);
1190
+ return '';
1191
+ }
1192
+ const localProperties = new Map();
1193
+ for (const member of node.type.members ?? []) {
1194
+ if (isPropertySignature(member) && isIdentifier(member.name)) {
1195
+ localProperties.set(member.name.text, getOptionalNodeType(member.type, !!member.questionToken));
1196
+ }
1197
+ }
1198
+ if (localProperties.size > 0)
1199
+ interfacePropertyTypes.set(visit(node.name), localProperties);
1200
+ const fields = (node.type.members ?? [])
1201
+ .filter((m) => isPropertySignature(m) && isIdentifier(m.name))
1202
+ .map((m) => `\t${goFieldName(m.name.text)} ${getOptionalNodeType(m.type, !!m.questionToken)}`);
1203
+ const terminator = options.isOutside ? '' : ';\n\t';
1204
+ return `type ${visit(node.name)}${getTypeParameters(node.typeParameters)} struct {\n${fields.join('\n')}\n}${terminator}`;
872
1205
  }
873
1206
  else if (isInterfaceDeclaration(node)) {
874
1207
  if (options.addFunctionOutside) {
@@ -889,7 +1222,7 @@ export function visit(node, options = {}) {
889
1222
  const extendedInterfaces = [];
890
1223
  if ((node.heritageClauses ?? [])) {
891
1224
  for (const clause of (node.heritageClauses ?? [])) {
892
- if (clause.token?.kind === 'ExtendsKeyword') {
1225
+ if (clause.token === 'ExtendsKeyword') {
893
1226
  for (const type of (clause.types ?? [])) {
894
1227
  extendedInterfaces.push(visit(type.expression));
895
1228
  }
@@ -908,7 +1241,7 @@ export function visit(node, options = {}) {
908
1241
  methods.push(`\t${methodName}(${params})${returnType}`);
909
1242
  }
910
1243
  else if (isPropertySignature(member) && isIdentifier(member.name)) {
911
- properties.push(`\t${member.name.text} ${getOptionalNodeType(member.type, !!member.questionToken)}`);
1244
+ properties.push(`\t${goFieldName(member.name.text)} ${getOptionalNodeType(member.type, !!member.questionToken)}`);
912
1245
  }
913
1246
  }
914
1247
  if (properties.length > 0 && methods.length === 0) {
@@ -933,7 +1266,7 @@ export function visit(node, options = {}) {
933
1266
  classStaticProps.add(`${className}.${member.name.text}`);
934
1267
  }
935
1268
  else {
936
- properties.set(member.name.text, getOptionalNodeType(member.type, !!member.questionToken));
1269
+ properties.set(member.name.text, getPropertyDeclarationType(member));
937
1270
  }
938
1271
  }
939
1272
  if (isMethodDeclaration(member) && isIdentifier(member.name)) {
@@ -945,6 +1278,9 @@ export function visit(node, options = {}) {
945
1278
  }
946
1279
  }
947
1280
  }
1281
+ for (const param of getParameterProperties(node)) {
1282
+ properties.set(param.name.text, getParameterGoType(param));
1283
+ }
948
1284
  classPropertyTypes.set(className, properties);
949
1285
  classMethodReturnTypes.set(className, methods);
950
1286
  return '';
@@ -955,7 +1291,7 @@ export function visit(node, options = {}) {
955
1291
  let parentClass = null;
956
1292
  if ((node.heritageClauses ?? [])) {
957
1293
  for (const clause of (node.heritageClauses ?? [])) {
958
- if (clause.token?.kind === 'ExtendsKeyword') {
1294
+ if (clause.token === 'ExtendsKeyword') {
959
1295
  parentClass = visit(clause.types[0].expression);
960
1296
  }
961
1297
  }
@@ -972,16 +1308,19 @@ export function visit(node, options = {}) {
972
1308
  fieldType = `[]${getType(member.type, true)}`;
973
1309
  }
974
1310
  else {
975
- fieldType = getOptionalNodeType(member.type, !!member.questionToken);
1311
+ fieldType = getPropertyDeclarationType(member);
976
1312
  }
977
1313
  fields.push(`\t${fieldName} ${fieldType}`);
978
1314
  }
979
1315
  }
1316
+ for (const param of getParameterProperties(node)) {
1317
+ fields.push(`\t${goFieldName(param.name.text)} ${getParameterGoType(param)}`);
1318
+ }
980
1319
  let result = `type ${name}${typeParams} struct {\n${fields.join('\n')}\n}\n\n`;
981
1320
  const ctor = (node.members ?? []).find((m) => isConstructorDeclaration(m));
982
1321
  if (ctor) {
983
1322
  const ctorParameterInfo = getFunctionParametersInfo(ctor.parameters ?? []);
984
- const bodyStatements = ctor.body?.statements
1323
+ const bodyStatements = (ctor.body?.statements ?? [])
985
1324
  .filter((s) => {
986
1325
  if (isExpressionStatement(s) && isCallExpression(s.expression)) {
987
1326
  return s.expression.expression.kind !== 'SuperKeyword';
@@ -990,10 +1329,13 @@ export function visit(node, options = {}) {
990
1329
  })
991
1330
  .map((s) => visit(s))
992
1331
  .join('\t') ?? '';
993
- result += `func New${name}${typeParams}(${ctorParameterInfo.signature}) *${name}${typeParamNames} {\n\t\tself := &${name}${typeParamNames}{}\n\t\t${ctorParameterInfo.prefixBlockContent}${bodyStatements}return self;\n\t}\n\n`;
1332
+ const parameterAssignments = getParameterProperties(node)
1333
+ .map((p) => `self.${goFieldName(p.name.text)} = ${visit(p.name)}\n\t\t`)
1334
+ .join('');
1335
+ result += `func New${name}${typeParams}(${ctorParameterInfo.signature}) *${name}${typeParamNames} {\n\t\tself := &${name}${typeParamNames}{}\n\t\t${ctorParameterInfo.prefixBlockContent}${fieldInitializers(node)}${parameterAssignments}${bodyStatements}return self;\n\t}\n\n`;
994
1336
  }
995
1337
  else {
996
- result += `func New${name}${typeParams}() *${name}${typeParamNames} {\n\t\treturn &${name}${typeParamNames}{}\n\t}\n\n`;
1338
+ result += `func New${name}${typeParams}() *${name}${typeParamNames} {\n\t\tself := &${name}${typeParamNames}{}\n\t\t${fieldInitializers(node)}return self\n\t}\n\n`;
997
1339
  }
998
1340
  for (const member of (node.members ?? [])) {
999
1341
  if (isMethodDeclaration(member)) {
@@ -1063,31 +1405,81 @@ export function visit(node, options = {}) {
1063
1405
  return `New${className}${typeArgs}(${args.join(', ')})`;
1064
1406
  }
1065
1407
  else if (isObjectLiteralExpression(node)) {
1066
- let typeName = '';
1067
- if (isVariableDeclaration(node.parent) && node.parent.type) {
1068
- typeName = getTypeText(node.parent.type);
1408
+ if (isDynamicObjectLiteral(node)) {
1409
+ // { ...a, k: v } → copy a's properties, then set k (in source order)
1410
+ if ((node.properties ?? []).some((p) => p.kind === 'SpreadAssignment')) {
1411
+ useHelper('dynamic');
1412
+ const steps = (node.properties ?? []).map((p) => {
1413
+ if (p.kind === 'SpreadAssignment')
1414
+ return `TnAssign(__obj, ${visit(p.expression)})`;
1415
+ if (isShorthandPropertyAssignment(p))
1416
+ return `__obj[${mapKeyText(p.name)}] = ${visit(p.name)}`;
1417
+ return `__obj[${mapKeyText(p.name)}] = ${visit(p.initializer)}`;
1418
+ });
1419
+ return `func() map[string]interface{} { __obj := map[string]interface{}{}; ${steps.join('; ')}; return __obj }()`;
1420
+ }
1421
+ const entries = (node.properties ?? [])
1422
+ .map((p) => {
1423
+ if (isPropertyAssignment(p))
1424
+ return `${mapKeyText(p.name)}: ${visit(p.initializer)}`;
1425
+ if (isShorthandPropertyAssignment(p))
1426
+ return `${mapKeyText(p.name)}: ${visit(p.name)}`;
1427
+ return '';
1428
+ })
1429
+ .filter((e) => e);
1430
+ return `map[string]interface{}${compositeBody(entries)}`;
1431
+ }
1432
+ const contextualType = resolveTypeNode(getContextualTypeNode(node));
1433
+ if (isRecordTypeNode(contextualType)) {
1434
+ return visitMapLiteral(node, contextualType);
1435
+ }
1436
+ const typeName = contextualType ? getTypeText(contextualType) : '';
1437
+ if (!typeName || typeName === 'interface{}') {
1438
+ return visitAnonymousStructLiteral(node);
1439
+ }
1440
+ // Objects are pointers: the struct type is typeName without its *
1441
+ const structName = typeName.replace(/^\*/, '');
1442
+ // { ...base, x: 1 } → a copy of *base with fields set
1443
+ if ((node.properties ?? []).some((p) => p.kind === 'SpreadAssignment')) {
1444
+ const steps = (node.properties ?? []).map((p) => {
1445
+ if (p.kind === 'SpreadAssignment')
1446
+ return `__obj = *(${visit(p.expression)})`;
1447
+ if (isShorthandPropertyAssignment(p))
1448
+ return `__obj.${goFieldName(p.name.text)} = ${visit(p.name)}`;
1449
+ const spreadFieldType = isIdentifier(p.name)
1450
+ ? (interfacePropertyTypes.get(structName)?.get(p.name.text) ??
1451
+ getStructFieldGoType(structName, p.name.text))
1452
+ : undefined;
1453
+ return `__obj.${visit(p.name)} = ${toGoValueOfType(p.initializer, spreadFieldType)}`;
1454
+ });
1455
+ return `func() *${structName} { var __obj ${structName}; ${steps.join('; ')}; return &__obj }()`;
1069
1456
  }
1457
+ const fieldTypes = interfacePropertyTypes.get(structName) ?? classPropertyTypes.get(structName);
1070
1458
  const properties = (node.properties ?? [])
1071
1459
  .map((p) => {
1072
1460
  if (isPropertyAssignment(p)) {
1073
- return `${visit(p.name)}: ${visit(p.initializer)}`;
1461
+ const fieldType = isIdentifier(p.name)
1462
+ ? (fieldTypes?.get(p.name.text) ?? getStructFieldGoType(typeName, p.name.text))
1463
+ : undefined;
1464
+ return `${visit(p.name)}: ${toGoValueOfType(p.initializer, fieldType)}`;
1074
1465
  }
1075
1466
  // Shorthand: { name } → name: name
1076
1467
  if (isShorthandPropertyAssignment(p)) {
1077
- const name = visit(p.name);
1078
- return `${name}: ${name}`;
1468
+ return `${goFieldName(p.name.text)}: ${toGoValueOfType(p.name, fieldTypes?.get(p.name.text) ?? getStructFieldGoType(typeName, p.name.text))}`;
1079
1469
  }
1080
1470
  // Spread: { ...obj } — not easily supported in Go structs, omit
1081
1471
  return '';
1082
1472
  })
1083
- .filter((p) => p)
1084
- .join(', ');
1085
- return `${typeName}{${properties}}`;
1473
+ .filter((p) => p);
1474
+ return `&${structName}${compositeBody(properties)}`;
1086
1475
  }
1087
1476
  else if (isPropertyAssignment(node)) {
1088
1477
  return `${visit(node.name)}: ${visit(node.initializer)}`;
1089
1478
  }
1090
1479
  else if (isNonNullExpression(node)) {
1480
+ const innerType = inferExpressionType(node.expression);
1481
+ if (innerType && NULLABLE_PRIMITIVE_TYPES.includes(innerType))
1482
+ return `(*${visit(node.expression)})`;
1091
1483
  return visit(node.expression);
1092
1484
  }
1093
1485
  else if (isImportDeclaration(node)) {
@@ -1097,7 +1489,7 @@ export function visit(node, options = {}) {
1097
1489
  return '';
1098
1490
  }
1099
1491
  const syntaxKind = node.kind;
1100
- if (!['FirstStatement', 'EndOfFileToken'].includes(syntaxKind)) {
1492
+ if (!['VariableStatement', 'EndOfFileToken'].includes(syntaxKind)) {
1101
1493
  const snippet = String(node.text ?? node.kind).substring(0, 60).replace(/\n/g, ' ');
1102
1494
  console.warn(`[TypeNative] Unsupported syntax: ${syntaxKind} — "${snippet}"`);
1103
1495
  }
@@ -1106,6 +1498,850 @@ export function visit(node, options = {}) {
1106
1498
  }
1107
1499
  return code;
1108
1500
  }
1501
+ // Renders composite literal entries; multi-line entries need trailing commas
1502
+ // (Go inserts a semicolon after a newline that ends in `}`)
1503
+ // Visits a value going into a slot of Go type `goType`; non-nil values for
1504
+ // nullable primitives (*string/*float64/*bool) are boxed into pointers
1505
+ function toGoValueOfType(expr, goType) {
1506
+ if (goType &&
1507
+ ((isObjectLiteralExpression(expr) && (expr.properties ?? []).length === 0 && goType.startsWith('*TnMap[')) ||
1508
+ (isArrayLiteralExpression(expr) && (expr.elements ?? []).length === 0 && goType.startsWith('[]')))) {
1509
+ const mapArgs = splitMapTypeArgs(goType);
1510
+ if (mapArgs)
1511
+ return orderedMapLiteral(mapArgs[0], mapArgs[1], []);
1512
+ return `${goType}{}`;
1513
+ }
1514
+ const code = visit(expr);
1515
+ if (goType && goType !== 'interface{}' && goType !== ':' && isDynamicValue(expr)) {
1516
+ useHelper('dynamic');
1517
+ // T | undefined: box the dynamic value (nil when missing or of another type)
1518
+ if (goType.startsWith('*'))
1519
+ return `TnAsPtr[${goType.slice(1)}](${code})`;
1520
+ return `TnAs[${goType}](${code})`;
1521
+ }
1522
+ // *Struct slot: take the address (shares the value, like a JS object reference)
1523
+ if (goType?.startsWith('*') && isStructGoType(goType.slice(1)) && !isNilLiteral(expr)) {
1524
+ // an untyped literal (&struct{...}) converts to the named pointer type
1525
+ if (isObjectLiteralExpression(expr))
1526
+ return code.startsWith('&struct{') ? `(${goType})(${code})` : code;
1527
+ if (inferExpressionType(expr) !== goType.slice(1))
1528
+ return code;
1529
+ // the element itself (not a bounds-checked copy), so mutations are shared
1530
+ if (isElementAccessExpression(expr) && inferExpressionType(expr.expression)?.startsWith('[]')) {
1531
+ return `&${visit(expr.expression)}[int(${visit(expr.argumentExpression)})]`;
1532
+ }
1533
+ if (isIdentifier(expr) || isPropertyAccessExpression(expr)) {
1534
+ return `&${code}`;
1535
+ }
1536
+ return `func() ${goType} { v := ${code}; return &v }()`;
1537
+ }
1538
+ if (!goType || !NULLABLE_PRIMITIVE_TYPES.includes(goType) || isNilLiteral(expr)) {
1539
+ return code;
1540
+ }
1541
+ if (isConditionalExpression(expr))
1542
+ return code;
1543
+ if (isBinaryExpression(expr) &&
1544
+ expr.operatorToken.kind === 'QuestionQuestionToken' &&
1545
+ inferExpressionType(expr.left)?.startsWith('*')) {
1546
+ return code;
1547
+ }
1548
+ if (inferExpressionType(expr) !== goType.slice(1))
1549
+ return code;
1550
+ return `func() ${goType} { v := ${code}; return &v }()`;
1551
+ }
1552
+ const NULLABLE_PRIMITIVE_TYPES = ['*string', '*float64', '*bool'];
1553
+ // Nullable primitives (T | null → *T) that a guard has narrowed to T in the
1554
+ // code being emitted; reads of them dereference the pointer
1555
+ const narrowedVariables = new Set();
1556
+ function isNullablePrimitiveVariable(name) {
1557
+ return NULLABLE_PRIMITIVE_TYPES.includes(variableGoTypes.get(name) ?? '');
1558
+ }
1559
+ // Narrowing key of a variable or property path (`opts.source`), if it has one
1560
+ function getNarrowingKey(expr) {
1561
+ if (isIdentifier(expr))
1562
+ return expr.text;
1563
+ if (expr.kind === 'ThisKeyword')
1564
+ return 'this';
1565
+ if (isPropertyAccessExpression(expr) && !hasQuestionDot(expr)) {
1566
+ const objectKey = getNarrowingKey(expr.expression);
1567
+ return objectKey ? `${objectKey}.${expr.name.text}` : undefined;
1568
+ }
1569
+ return undefined;
1570
+ }
1571
+ // Emits `emit()` with `names` narrowed
1572
+ function withNarrowing(names, emit) {
1573
+ const added = names.filter((n) => !narrowedVariables.has(n));
1574
+ for (const name of added)
1575
+ narrowedVariables.add(name);
1576
+ const code = emit();
1577
+ for (const name of added)
1578
+ narrowedVariables.delete(name);
1579
+ return code;
1580
+ }
1581
+ function isProcessEnv(expr) {
1582
+ while (isParenthesizedExpression(expr) || isAsExpression(expr) || isNonNullExpression(expr)) {
1583
+ expr = expr.expression;
1584
+ }
1585
+ return (isPropertyAccessExpression(expr) &&
1586
+ isIdentifier(expr.expression) &&
1587
+ expr.expression.text === 'process' &&
1588
+ expr.name.text === 'env');
1589
+ }
1590
+ // Whether an expression is (part of) an optional chain: a?.b, a?.b(), a?.b().c
1591
+ function isOptionalChain(expr) {
1592
+ if (isCallExpression(expr)) {
1593
+ return hasQuestionDot(expr) || isOptionalChain(expr.expression);
1594
+ }
1595
+ if (isPropertyAccessExpression(expr) || isElementAccessExpression(expr)) {
1596
+ return hasQuestionDot(expr) || isOptionalChain(expr.expression);
1597
+ }
1598
+ return false;
1599
+ }
1600
+ // x in `x?.m(...)` / `x?.p`
1601
+ function getOptionalChainBase(expr) {
1602
+ const access = isCallExpression(expr) ? expr.expression : expr;
1603
+ if (isPropertyAccessExpression(access) && hasQuestionDot(access))
1604
+ return access.expression;
1605
+ return undefined;
1606
+ }
1607
+ // Variables known non-null when `condition` evaluates to `whenTrue`
1608
+ function getNarrowedNames(condition, whenTrue) {
1609
+ if (isParenthesizedExpression(condition))
1610
+ return getNarrowedNames(condition.expression, whenTrue);
1611
+ if (whenTrue) {
1612
+ const chainBase = getOptionalChainBase(condition);
1613
+ if (chainBase)
1614
+ return nullableKeys([chainBase]);
1615
+ }
1616
+ if (isIdentifier(condition) || isPropertyAccessExpression(condition)) {
1617
+ return whenTrue ? nullableKeys([condition]) : [];
1618
+ }
1619
+ if (isPrefixUnaryExpression(condition) && condition.operator === 'ExclamationToken') {
1620
+ return getNarrowedNames(condition.operand, !whenTrue);
1621
+ }
1622
+ if (!isBinaryExpression(condition))
1623
+ return [];
1624
+ const kind = condition.operatorToken.kind;
1625
+ if (kind === 'AmpersandAmpersandToken') {
1626
+ return whenTrue
1627
+ ? [...getNarrowedNames(condition.left, true), ...getNarrowedNames(condition.right, true)]
1628
+ : [];
1629
+ }
1630
+ if (kind === 'BarBarToken') {
1631
+ return whenTrue
1632
+ ? []
1633
+ : [...getNarrowedNames(condition.left, false), ...getNarrowedNames(condition.right, false)];
1634
+ }
1635
+ const isNotEqual = kind === 'ExclamationEqualsEqualsToken' || kind === 'ExclamationEqualsToken';
1636
+ const isEqual = kind === 'EqualsEqualsEqualsToken' || kind === 'EqualsEqualsToken';
1637
+ if (!isNotEqual && !isEqual)
1638
+ return [];
1639
+ const checked = isNilLiteral(condition.right)
1640
+ ? condition.left
1641
+ : isNilLiteral(condition.left)
1642
+ ? condition.right
1643
+ : undefined;
1644
+ if (!checked) {
1645
+ // x === <non-null value>
1646
+ if (!isEqual || !whenTrue)
1647
+ return [];
1648
+ const leftType = inferExpressionType(condition.left);
1649
+ const rightType = inferExpressionType(condition.right);
1650
+ if (rightType && !NULLABLE_PRIMITIVE_TYPES.includes(rightType))
1651
+ return nullableKeys([condition.left]);
1652
+ if (leftType && !NULLABLE_PRIMITIVE_TYPES.includes(leftType))
1653
+ return nullableKeys([condition.right]);
1654
+ return [];
1655
+ }
1656
+ return isNotEqual === whenTrue ? nullableKeys([checked]) : [];
1657
+ }
1658
+ // Narrowing keys of the given expressions that are nullable primitives (*T)
1659
+ function nullableKeys(exprs) {
1660
+ const keys = [];
1661
+ for (const expr of exprs) {
1662
+ const key = getNarrowingKey(expr);
1663
+ if (!key || narrowedVariables.has(key))
1664
+ continue;
1665
+ if (NULLABLE_PRIMITIVE_TYPES.includes(inferExpressionType(expr) ?? ''))
1666
+ keys.push(key);
1667
+ }
1668
+ return keys;
1669
+ }
1670
+ // Identifier occurrences that read the variable (not declarations, member
1671
+ // names, or assignment targets)
1672
+ function isNarrowableReference(node) {
1673
+ const parent = node.parent;
1674
+ if (!parent)
1675
+ return false;
1676
+ if (isPropertyAccessExpression(parent) && parent.name === node)
1677
+ return false;
1678
+ if (parent.name === node)
1679
+ return false;
1680
+ if (isBinaryExpression(parent) && parent.left === node) {
1681
+ const op = parent.operatorToken.kind;
1682
+ if (op === 'EqualsToken' || op === 'QuestionQuestionEqualsToken')
1683
+ return false;
1684
+ }
1685
+ return true;
1686
+ }
1687
+ function alwaysExits(statement) {
1688
+ if (isReturnStatement(statement) ||
1689
+ isThrowStatement(statement) ||
1690
+ isBreakStatement(statement) ||
1691
+ statement.kind === 'ContinueStatement') {
1692
+ return true;
1693
+ }
1694
+ if (isBlock(statement)) {
1695
+ const statements = statement.statements ?? [];
1696
+ return statements.length > 0 && alwaysExits(statements[statements.length - 1]);
1697
+ }
1698
+ return false;
1699
+ }
1700
+ // Statements of a block; after `if (!x) return;`, x stays narrowed for the rest
1701
+ function visitBlockStatements(statements) {
1702
+ const added = [];
1703
+ const parts = [];
1704
+ for (const statement of statements) {
1705
+ parts.push(visit(statement));
1706
+ if (isIfStatement(statement) && !statement.elseStatement && alwaysExits(statement.thenStatement)) {
1707
+ for (const name of getNarrowedNames(statement.expression, false)) {
1708
+ if (!narrowedVariables.has(name)) {
1709
+ narrowedVariables.add(name);
1710
+ added.push(name);
1711
+ }
1712
+ }
1713
+ }
1714
+ }
1715
+ for (const name of added)
1716
+ narrowedVariables.delete(name);
1717
+ return parts.join('\t');
1718
+ }
1719
+ function visitDynamicArithmetic(node, op) {
1720
+ const leftDynamic = isDynamicValue(node.left);
1721
+ const rightDynamic = isDynamicValue(node.right);
1722
+ if (!leftDynamic && !rightDynamic)
1723
+ return undefined;
1724
+ const isConcat = op === '+' && [node.left, node.right].some((side) => inferExpressionType(side) === 'string');
1725
+ const operand = (side, dynamic) => {
1726
+ if (!dynamic)
1727
+ return visit(side);
1728
+ if (isConcat) {
1729
+ importedPackages.add('fmt');
1730
+ return `fmt.Sprint(${visit(side)})`;
1731
+ }
1732
+ useHelper('dynamic');
1733
+ return `TnAs[float64](${visit(side)})`;
1734
+ };
1735
+ return `${operand(node.left, leftDynamic)} ${op} ${operand(node.right, rightDynamic)}`;
1736
+ }
1737
+ // `x === v` where x is *T and v is T: equal only when x is non-nil and *x == v
1738
+ function visitNullableComparison(node, op) {
1739
+ const leftType = inferExpressionType(node.left);
1740
+ const rightType = inferExpressionType(node.right);
1741
+ let pointerSide;
1742
+ let valueSide;
1743
+ if (leftType && NULLABLE_PRIMITIVE_TYPES.includes(leftType) && rightType === leftType.slice(1)) {
1744
+ pointerSide = node.left;
1745
+ valueSide = node.right;
1746
+ }
1747
+ else if (rightType &&
1748
+ NULLABLE_PRIMITIVE_TYPES.includes(rightType) &&
1749
+ leftType === rightType.slice(1)) {
1750
+ pointerSide = node.right;
1751
+ valueSide = node.left;
1752
+ }
1753
+ else {
1754
+ return undefined;
1755
+ }
1756
+ const tmp = getTempName('cmp');
1757
+ const equal = `${tmp} != nil && *${tmp} == ${visit(valueSide)}`;
1758
+ return `func() bool { ${tmp} := ${visit(pointerSide)}; return ${op === '==' ? equal : `!(${equal})`} }()`;
1759
+ }
1760
+ // Go type of a Map/Set/Record: an insertion-ordered map
1761
+ function mapGoType(keyType, valueType) {
1762
+ useHelper('orderedMap');
1763
+ return `*TnMap[${keyType}, ${valueType}]`;
1764
+ }
1765
+ // [K, V] of a *TnMap[K, V] type string
1766
+ function splitMapTypeArgs(mapType) {
1767
+ if (!mapType.startsWith('*TnMap['))
1768
+ return undefined;
1769
+ const body = mapType.slice(7, -1);
1770
+ let depth = 0;
1771
+ for (let i = 0; i < body.length; i++) {
1772
+ const ch = body[i];
1773
+ if (ch === '[' || ch === '(' || ch === '{')
1774
+ depth++;
1775
+ else if (ch === ']' || ch === ')' || ch === '}')
1776
+ depth--;
1777
+ else if (ch === ',' && depth === 0)
1778
+ return [body.slice(0, i).trim(), body.slice(i + 1).trim()];
1779
+ }
1780
+ return undefined;
1781
+ }
1782
+ // Key type of a map type string: *TnMap[K, V] → K
1783
+ function extractMapKeyType(mapType) {
1784
+ const args = splitMapTypeArgs(mapType);
1785
+ if (args)
1786
+ return args[0];
1787
+ let depth = 0;
1788
+ for (let i = 4; i < mapType.length; i++) {
1789
+ if (mapType[i] === '[')
1790
+ depth++;
1791
+ else if (mapType[i] === ']') {
1792
+ if (depth === 0)
1793
+ return mapType.slice(4, i);
1794
+ depth--;
1795
+ }
1796
+ }
1797
+ return 'interface{}';
1798
+ }
1799
+ // Field type of an anonymous struct type string: struct{ a T; b U } → field b → U
1800
+ function getStructFieldGoType(structType, field) {
1801
+ structType = structType.replace(/^\*/, '');
1802
+ if (!structType.startsWith('struct{'))
1803
+ return undefined;
1804
+ const body = structType.slice(7, structType.lastIndexOf('}'));
1805
+ let depth = 0;
1806
+ let start = 0;
1807
+ const fields = [];
1808
+ for (let i = 0; i <= body.length; i++) {
1809
+ const ch = i < body.length ? body[i] : ';';
1810
+ if (ch === '{' || ch === '(' || ch === '[')
1811
+ depth++;
1812
+ else if (ch === '}' || ch === ')' || ch === ']')
1813
+ depth--;
1814
+ else if (ch === ';' && depth === 0) {
1815
+ fields.push(body.slice(start, i).trim());
1816
+ start = i + 1;
1817
+ }
1818
+ }
1819
+ for (const entry of fields) {
1820
+ const space = entry.indexOf(' ');
1821
+ if (space > 0 && entry.slice(0, space) === goFieldName(field))
1822
+ return entry.slice(space + 1).trim();
1823
+ }
1824
+ return undefined;
1825
+ }
1826
+ // A Go expression as the string JS would produce for it (numbers like JS:
1827
+ // 1000000, 0.5, NaN; not Go's 1e+06)
1828
+ function jsStringOf(code, goType) {
1829
+ if (goType === 'string')
1830
+ return code;
1831
+ if (goType === 'float64') {
1832
+ useHelper('dynamic');
1833
+ return `TnNumStr(${code})`;
1834
+ }
1835
+ if (goType === 'bool') {
1836
+ importedPackages.add('strconv');
1837
+ return `strconv.FormatBool(${code})`;
1838
+ }
1839
+ importedPackages.add('fmt');
1840
+ return `fmt.Sprintf("%v", ${code})`;
1841
+ }
1842
+ // JS bitwise operators work on 32-bit integers (ToInt32 / ToUint32)
1843
+ function visitBitwise(node, op) {
1844
+ const kind = node.operatorToken.kind;
1845
+ const base = {
1846
+ AmpersandToken: '&',
1847
+ BarToken: '|',
1848
+ CaretToken: '^',
1849
+ LessThanLessThanToken: '<<',
1850
+ GreaterThanGreaterThanToken: '>>',
1851
+ GreaterThanGreaterThanGreaterThanToken: '>>>',
1852
+ AmpersandEqualsToken: '&',
1853
+ BarEqualsToken: '|',
1854
+ CaretEqualsToken: '^',
1855
+ LessThanLessThanEqualsToken: '<<',
1856
+ GreaterThanGreaterThanEqualsToken: '>>',
1857
+ GreaterThanGreaterThanGreaterThanEqualsToken: '>>>'
1858
+ };
1859
+ const bitOp = base[kind];
1860
+ if (!bitOp)
1861
+ return undefined;
1862
+ useHelper('dynamic');
1863
+ const left = visit(node.left);
1864
+ const right = visit(node.right);
1865
+ let value;
1866
+ if (bitOp === '>>>')
1867
+ value = `float64(TnUint32(${left}) >> (TnUint32(${right}) & 31))`;
1868
+ else if (bitOp === '<<' || bitOp === '>>') {
1869
+ value = `float64(TnInt32(${left}) ${bitOp} (TnUint32(${right}) & 31))`;
1870
+ }
1871
+ else
1872
+ value = `float64(TnInt32(${left}) ${bitOp} TnInt32(${right}))`;
1873
+ const isAssignment = kind.endsWith('EqualsToken');
1874
+ return isAssignment ? `${left} = ${value}` : value;
1875
+ }
1876
+ // delete obj.key / delete obj[key] on maps (Records and dynamic objects)
1877
+ function visitDelete(node) {
1878
+ const target = unwrapParentheses(node.expression);
1879
+ const object = target.expression;
1880
+ const key = isPropertyAccessExpression(target)
1881
+ ? toGoStringLiteral(target.name.text)
1882
+ : visit(target.argumentExpression);
1883
+ const objectType = inferExpressionType(object);
1884
+ if (objectType?.startsWith('*TnMap['))
1885
+ return `${visit(object)}.Delete(${key})`;
1886
+ useHelper('dynamic');
1887
+ return `TnDelete(${visit(object)}, ${key})`;
1888
+ }
1889
+ // f(...xs): variadic callees take xs...; fixed-parameter functions get
1890
+ // xs[0], xs[1], … for their remaining parameters
1891
+ function visitSpreadCallArguments(node) {
1892
+ const fn = isIdentifier(node.expression) ? declaredFunctions.get(node.expression.text) : undefined;
1893
+ const params = fn?.parameters ?? [];
1894
+ const isVariadic = !fn || params.some((p) => p.dotDotDotToken);
1895
+ const result = [];
1896
+ for (const arg of node.arguments ?? []) {
1897
+ if (!isSpreadElement(arg)) {
1898
+ result.push(visit(arg));
1899
+ }
1900
+ else if (isVariadic) {
1901
+ result.push(`${visit(arg.expression)}...`);
1902
+ }
1903
+ else {
1904
+ useHelper('dynamic');
1905
+ const source = visit(arg.expression);
1906
+ const start = result.length;
1907
+ for (let i = start; i < params.length; i++)
1908
+ result.push(`TnAt(${source}, ${i - start})`);
1909
+ }
1910
+ }
1911
+ return result;
1912
+ }
1913
+ // self.field = initializer for each instance field that has one
1914
+ function fieldInitializers(classNode) {
1915
+ return (classNode.members ?? [])
1916
+ .filter((m) => isPropertyDeclaration(m) &&
1917
+ m.initializer &&
1918
+ isIdentifier(m.name) &&
1919
+ !(m.modifiers ?? []).some((mod) => mod.kind === 'StaticKeyword'))
1920
+ .map((m) => `self.${goFieldName(m.name.text)} = ${toGoValueOfType(m.initializer, getPropertyDeclarationType(m))}\n\t\t`)
1921
+ .join('');
1922
+ }
1923
+ // A class field's type: declared, else inferred from its initializer (n = 1 → float64)
1924
+ function getPropertyDeclarationType(member) {
1925
+ if (member.type || !member.initializer)
1926
+ return getOptionalNodeType(member.type, !!member.questionToken);
1927
+ return toFieldGoType(inferExpressionType(member.initializer));
1928
+ }
1929
+ function isIncrementOrDecrement(node) {
1930
+ return node.operator === 'PlusPlusToken' || node.operator === 'MinusMinusToken';
1931
+ }
1932
+ // x++ / ++x: Go only has the statement form, so used as a value they become
1933
+ // a closure returning the old (postfix) or new (prefix) value
1934
+ function visitIncrementOrDecrement(node, isPrefix) {
1935
+ const target = visit(node.operand, { inline: true });
1936
+ const op = node.operator === 'PlusPlusToken' ? '++' : '--';
1937
+ const parent = node.parent;
1938
+ const isStatement = isExpressionStatement(parent) || (isForStatement(parent) && parent.incrementor === node);
1939
+ if (isStatement)
1940
+ return `${target}${op}`;
1941
+ if (isPrefix)
1942
+ return `func() float64 { ${target}${op}; return ${target} }()`;
1943
+ return `func() float64 { __old := ${target}; ${target}${op}; return __old }()`;
1944
+ }
1945
+ function compositeBody(rawEntries) {
1946
+ const entries = rawEntries.map((e) => e.trimEnd());
1947
+ if (!entries.some((e) => e.includes('\n')))
1948
+ return `{${entries.join(', ')}}`;
1949
+ return `{\n\t${entries.join(',\n\t')},\n}`;
1950
+ }
1951
+ // Struct field type for an inferred value type (null/unknown → interface{})
1952
+ function toFieldGoType(goType) {
1953
+ return !goType || goType === 'nil' ? 'interface{}' : goType;
1954
+ }
1955
+ function isDictionaryLiteral(node) {
1956
+ const declaration = node.parent;
1957
+ if (!isVariableDeclaration(declaration) || !isIdentifier(declaration.name))
1958
+ return false;
1959
+ let scope = declaration.parent;
1960
+ while (scope && !isBlock(scope) && !isSourceFile(scope))
1961
+ scope = scope.parent;
1962
+ if (!scope)
1963
+ return false;
1964
+ const name = declaration.name.text;
1965
+ return containsNode(scope, (n) => isElementAccessExpression(n) &&
1966
+ isIdentifier(n.expression) &&
1967
+ n.expression.text === name &&
1968
+ !isStringLiteral(n.argumentExpression));
1969
+ }
1970
+ function containsNode(root, predicate) {
1971
+ for (const child of childNodes(root)) {
1972
+ if (predicate(child) || containsNode(child, predicate))
1973
+ return true;
1974
+ }
1975
+ return false;
1976
+ }
1977
+ function getDictionaryValueType(node) {
1978
+ let valueType;
1979
+ for (const p of node.properties ?? []) {
1980
+ if (!isPropertyAssignment(p))
1981
+ continue;
1982
+ const t = inferExpressionType(p.initializer);
1983
+ if (!t || (valueType && t !== valueType))
1984
+ return 'interface{}';
1985
+ valueType = t;
1986
+ }
1987
+ return valueType ?? 'interface{}';
1988
+ }
1989
+ // Go type of an object literal, matching what the visitor emits
1990
+ // Object literals that are dynamic objects: in an any context, or spreading an any value
1991
+ function isDynamicObjectLiteral(node) {
1992
+ if (isAnyContext(getContextualTypeNode(node)))
1993
+ return true;
1994
+ return (node.properties ?? []).some((p) => p.kind === 'SpreadAssignment' && isDynamicValue(p.expression));
1995
+ }
1996
+ function getObjectLiteralGoType(node) {
1997
+ if (isDynamicObjectLiteral(node))
1998
+ return 'interface{}';
1999
+ const contextualType = resolveTypeNode(getContextualTypeNode(node));
2000
+ if (isRecordTypeNode(contextualType))
2001
+ return getType(contextualType);
2002
+ const typeName = contextualType ? getType(contextualType) : '';
2003
+ if (typeName && typeName !== 'interface{}' && typeName !== ':')
2004
+ return typeName;
2005
+ if (isDictionaryLiteral(node))
2006
+ return mapGoType('string', getDictionaryValueType(node));
2007
+ const fields = (node.properties ?? [])
2008
+ .filter((p) => (isPropertyAssignment(p) || isShorthandPropertyAssignment(p)) && isIdentifier(p.name))
2009
+ .map((p) => {
2010
+ const value = isPropertyAssignment(p) ? p.initializer : p.name;
2011
+ return `${goFieldName(p.name.text)} ${toFieldGoType(inferExpressionType(value))}`;
2012
+ });
2013
+ return `*struct{ ${fields.join('; ')} }`;
2014
+ }
2015
+ // Values typed `any`/`unknown` are handled at runtime (TnGet/TnSet/...),
2016
+ // never by guessing their type
2017
+ function isDynamicValue(expr) {
2018
+ return inferExpressionType(expr) === 'interface{}';
2019
+ }
2020
+ // Whether an expression is written to (x = …, x += …, x++)
2021
+ function isAssignmentTarget(node) {
2022
+ const parent = node.parent;
2023
+ if (isBinaryExpression(parent) && parent.left === node) {
2024
+ return /^(EqualsToken|.*EqualsToken)$/.test(parent.operatorToken.kind) &&
2025
+ !['EqualsEqualsToken', 'EqualsEqualsEqualsToken', 'ExclamationEqualsToken', 'ExclamationEqualsEqualsToken', 'LessThanEqualsToken', 'GreaterThanEqualsToken'].includes(parent.operatorToken.kind);
2026
+ }
2027
+ return ((isPrefixUnaryExpression(parent) || isPostfixUnaryExpression(parent)) &&
2028
+ ['PlusPlusToken', 'MinusMinusToken'].includes(parent.operator));
2029
+ }
2030
+ // `x as T`: an any value cast to an object type stays dynamic (a JS object
2031
+ // cannot become a Go struct); other casts convert to T
2032
+ function getAsExpressionType(node) {
2033
+ const target = getType(node.type);
2034
+ if (isDynamicValue(node.expression) && (isStructGoType(target) || target.startsWith('*'))) {
2035
+ return 'interface{}';
2036
+ }
2037
+ return target;
2038
+ }
2039
+ function isCallee(node) {
2040
+ return isCallExpression(node.parent) && node.parent.expression === node;
2041
+ }
2042
+ // Whether a type node is `any`/`unknown`, directly or through aliases
2043
+ function isAnyContext(typeNode) {
2044
+ for (let depth = 0; typeNode && depth < 10; depth++) {
2045
+ if (isAnyTypeNode(typeNode))
2046
+ return true;
2047
+ // any | undefined is still any
2048
+ if (isUnionTypeNode(typeNode)) {
2049
+ return (typeNode.types ?? []).some((t) => isAnyContext(t));
2050
+ }
2051
+ if (!isTypeReferenceNode(typeNode) || !isIdentifier(typeNode.typeName))
2052
+ return false;
2053
+ typeNode = declaredTypeAliases.get(typeNode.typeName.text);
2054
+ }
2055
+ return false;
2056
+ }
2057
+ function isRecordTypeNode(typeNode) {
2058
+ return (isTypeReferenceNode(typeNode) &&
2059
+ isIdentifier(typeNode.typeName) &&
2060
+ typeNode.typeName.text === 'Record' &&
2061
+ (typeNode.typeArguments ?? []).length === 2);
2062
+ }
2063
+ // Object literal keys as Go map keys: identifiers become string literals
2064
+ function mapKeyText(name) {
2065
+ if (isIdentifier(name))
2066
+ return toGoStringLiteral(name.text);
2067
+ if (name.kind === 'ComputedPropertyName')
2068
+ return visit(name.expression);
2069
+ return visit(name);
2070
+ }
2071
+ // { a: 1 } typed as Record<K, V> → map[K]V{"a": 1}
2072
+ // An ordered map built from "key, value" entries (in source order)
2073
+ function orderedMapLiteral(keyType, valueType, entries) {
2074
+ useHelper('orderedMap');
2075
+ const constructor = `TnNewMap[${keyType}, ${valueType}]()`;
2076
+ if (entries.length === 0)
2077
+ return constructor;
2078
+ const sets = entries.map((e) => `.Set(${e})`).join('');
2079
+ return `${constructor}${sets}`;
2080
+ }
2081
+ function visitMapLiteral(node, recordType) {
2082
+ const keyType = getType(recordType.typeArguments[0]);
2083
+ const valueType = getType(recordType.typeArguments[1]);
2084
+ const entries = [];
2085
+ for (const p of node.properties ?? []) {
2086
+ if (isPropertyAssignment(p))
2087
+ entries.push(`${mapKeyText(p.name)}, ${toGoValueOfType(p.initializer, valueType)}`);
2088
+ else if (isShorthandPropertyAssignment(p))
2089
+ entries.push(`${mapKeyText(p.name)}, ${visit(p.name)}`);
2090
+ }
2091
+ return orderedMapLiteral(keyType, valueType, entries);
2092
+ }
2093
+ // Object literal with no contextual type → anonymous struct with inferred fields,
2094
+ // or a map when the variable holding it is indexed dynamically (obj[key])
2095
+ function visitAnonymousStructLiteral(node) {
2096
+ if (isDictionaryLiteral(node)) {
2097
+ const valueType = getDictionaryValueType(node);
2098
+ const entries = (node.properties ?? [])
2099
+ .filter((p) => isPropertyAssignment(p))
2100
+ .map((p) => `${mapKeyText(p.name)}, ${visit(p.initializer)}`);
2101
+ return orderedMapLiteral('string', valueType, entries);
2102
+ }
2103
+ const fields = [];
2104
+ const values = [];
2105
+ for (const p of node.properties ?? []) {
2106
+ if (isPropertyAssignment(p) && isIdentifier(p.name)) {
2107
+ const field = goFieldName(p.name.text);
2108
+ fields.push(`${field} ${toFieldGoType(inferExpressionType(p.initializer))}`);
2109
+ values.push(`${field}: ${visit(p.initializer)}`);
2110
+ }
2111
+ else if (isShorthandPropertyAssignment(p)) {
2112
+ const field = goFieldName(p.name.text);
2113
+ fields.push(`${field} ${toFieldGoType(inferExpressionType(p.name))}`);
2114
+ values.push(`${field}: ${visit(p.name)}`);
2115
+ }
2116
+ }
2117
+ return `&struct{ ${fields.join('; ')} }${compositeBody(values)}`;
2118
+ }
2119
+ // Follows type aliases and strips null/undefined from unions
2120
+ function resolveTypeNode(typeNode, depth = 0) {
2121
+ if (!typeNode || depth > 10)
2122
+ return typeNode;
2123
+ if (isAnyTypeNode(typeNode))
2124
+ return undefined;
2125
+ if (typeNode.kind === 'ParenthesizedType')
2126
+ return resolveTypeNode(typeNode.type, depth + 1);
2127
+ if (isUnionTypeNode(typeNode)) {
2128
+ const nonNull = (typeNode.types ?? []).filter((t) => t.kind !== 'NullKeyword' &&
2129
+ t.kind !== 'UndefinedKeyword' &&
2130
+ !(isLiteralTypeNode(t) && t.literal?.kind === 'NullKeyword'));
2131
+ return nonNull.length === 1 ? resolveTypeNode(nonNull[0], depth + 1) : typeNode;
2132
+ }
2133
+ if (isTypeReferenceNode(typeNode) && isIdentifier(typeNode.typeName)) {
2134
+ const alias = declaredTypeAliases.get(typeNode.typeName.text);
2135
+ if (alias && alias.kind !== 'TypeLiteral')
2136
+ return resolveTypeNode(alias, depth + 1);
2137
+ }
2138
+ return typeNode;
2139
+ }
2140
+ function getEnclosingFunction(node) {
2141
+ let current = node.parent;
2142
+ while (current) {
2143
+ if (isFunctionDeclaration(current) ||
2144
+ isFunctionExpression(current) ||
2145
+ isArrowFunction(current) ||
2146
+ isMethodDeclaration(current) ||
2147
+ isGetAccessor(current)) {
2148
+ return current;
2149
+ }
2150
+ current = current.parent;
2151
+ }
2152
+ return undefined;
2153
+ }
2154
+ // Declared return type of a function, unwrapping Promise<T> for async functions
2155
+ function getReturnTypeNode(fn) {
2156
+ const typeNode = fn?.type ?? (fn ? getContextualFunctionType(fn)?.type : undefined);
2157
+ if (isTypeReferenceNode(typeNode) &&
2158
+ isIdentifier(typeNode.typeName) &&
2159
+ typeNode.typeName.text === 'Promise') {
2160
+ return typeNode.typeArguments?.[0];
2161
+ }
2162
+ return typeNode;
2163
+ }
2164
+ // Type of member `name` within an object type (interface, type literal, Record)
2165
+ function getMemberTypeNode(typeNode, name) {
2166
+ const resolved = resolveTypeNode(typeNode);
2167
+ if (!resolved)
2168
+ return undefined;
2169
+ if (isRecordTypeNode(resolved))
2170
+ return resolved.typeArguments[1];
2171
+ let members = [];
2172
+ if (resolved.kind === 'TypeLiteral') {
2173
+ members = resolved.members ?? [];
2174
+ }
2175
+ else if (isTypeReferenceNode(resolved) && isIdentifier(resolved.typeName)) {
2176
+ const aliasLiteral = declaredTypeAliases.get(resolved.typeName.text);
2177
+ if (aliasLiteral?.kind === 'TypeLiteral')
2178
+ members = aliasLiteral.members ?? [];
2179
+ const iface = declaredInterfaces.get(resolved.typeName.text);
2180
+ if (iface) {
2181
+ members = iface.members ?? [];
2182
+ for (const clause of iface.heritageClauses ?? []) {
2183
+ for (const base of clause.types ?? []) {
2184
+ const baseType = { kind: 'TypeReference', typeName: base.expression };
2185
+ const inherited = getMemberTypeNode(baseType, name);
2186
+ if (inherited)
2187
+ return inherited;
2188
+ }
2189
+ }
2190
+ }
2191
+ }
2192
+ for (const member of members) {
2193
+ if (isPropertySignature(member) && isIdentifier(member.name) && member.name.text === name) {
2194
+ return member.type;
2195
+ }
2196
+ }
2197
+ return undefined;
2198
+ }
2199
+ // The type an expression is expected to have from where it appears
2200
+ // (TypeScript's "contextual type"), derived syntactically from declarations
2201
+ // Declared type nodes of variables/parameters in scope (latest declaration wins)
2202
+ const variableTypeNodes = new Map();
2203
+ // Declared type (as a type node) of an assignable expression, when known
2204
+ function getExpressionTypeNode(expr) {
2205
+ if (isIdentifier(expr))
2206
+ return variableTypeNodes.get(expr.text);
2207
+ if (isParenthesizedExpression(expr))
2208
+ return getExpressionTypeNode(expr.expression);
2209
+ if (isPropertyAccessExpression(expr)) {
2210
+ if (expr.expression.kind === 'ThisKeyword') {
2211
+ let current = expr.parent;
2212
+ while (current && !isClassDeclaration(current))
2213
+ current = current.parent;
2214
+ const member = (current?.members ?? []).find((m) => isPropertyDeclaration(m) && isIdentifier(m.name) && m.name.text === expr.name.text);
2215
+ return member?.type;
2216
+ }
2217
+ const objectType = getExpressionTypeNode(expr.expression);
2218
+ return objectType ? getMemberTypeNode(objectType, expr.name.text) : undefined;
2219
+ }
2220
+ if (isElementAccessExpression(expr)) {
2221
+ const objectType = resolveTypeNode(getExpressionTypeNode(expr.expression));
2222
+ if (isArrayTypeNode(objectType))
2223
+ return objectType.elementType;
2224
+ if (isTypeReferenceNode(objectType) &&
2225
+ isIdentifier(objectType.typeName) &&
2226
+ ['Record', 'Map'].includes(objectType.typeName.text)) {
2227
+ return objectType.typeArguments?.[1];
2228
+ }
2229
+ }
2230
+ return undefined;
2231
+ }
2232
+ function getContextualTypeNode(node) {
2233
+ const parent = node.parent;
2234
+ if (!parent)
2235
+ return undefined;
2236
+ if (isParenthesizedExpression(parent))
2237
+ return getContextualTypeNode(parent);
2238
+ if (isConditionalExpression(parent) && parent.condition !== node) {
2239
+ return getContextualTypeNode(parent);
2240
+ }
2241
+ if (isBinaryExpression(parent)) {
2242
+ const op = parent.operatorToken.kind;
2243
+ if ((op === 'QuestionQuestionToken' || op === 'BarBarToken') && parent.right === node) {
2244
+ return getContextualTypeNode(parent);
2245
+ }
2246
+ if (op === 'EqualsToken' && parent.right === node) {
2247
+ return getExpressionTypeNode(parent.left);
2248
+ }
2249
+ }
2250
+ if (isAsExpression(parent) || isTypeAssertionExpression(parent))
2251
+ return parent.type;
2252
+ if ((isVariableDeclaration(parent) || isPropertyDeclaration(parent) || parent.kind === 'Parameter') &&
2253
+ parent.initializer === node) {
2254
+ return parent.type;
2255
+ }
2256
+ if (isReturnStatement(parent)) {
2257
+ return getReturnTypeNode(getEnclosingFunction(parent));
2258
+ }
2259
+ if (isArrowFunction(parent) && parent.body === node) {
2260
+ return getReturnTypeNode(parent);
2261
+ }
2262
+ if (isCallExpression(parent) && isIdentifier(parent.expression)) {
2263
+ const index = (parent.arguments ?? []).indexOf(node);
2264
+ const fn = declaredFunctions.get(parent.expression.text);
2265
+ if (index > -1 && fn)
2266
+ return fn.parameters?.[index]?.type;
2267
+ }
2268
+ if (isPropertyAssignment(parent) &&
2269
+ parent.initializer === node &&
2270
+ (isIdentifier(parent.name) || isStringLiteral(parent.name))) {
2271
+ const objectType = getContextualTypeNode(parent.parent);
2272
+ return objectType ? getMemberTypeNode(objectType, parent.name.text) : undefined;
2273
+ }
2274
+ if (isArrayLiteralExpression(parent)) {
2275
+ const arrayType = resolveTypeNode(getContextualTypeNode(parent));
2276
+ if (isArrayTypeNode(arrayType))
2277
+ return arrayType.elementType;
2278
+ }
2279
+ return undefined;
2280
+ }
2281
+ // Element type of an array literal: from its contextual type (declared
2282
+ // variable/parameter/return type), else inferred from its first typed element
2283
+ function getArrayLiteralElementType(node) {
2284
+ const contextual = resolveTypeNode(getContextualTypeNode(node));
2285
+ if (isArrayTypeNode(contextual))
2286
+ return getType(contextual.elementType);
2287
+ if (contextual?.kind === 'TupleType')
2288
+ return getTupleElementType(contextual);
2289
+ if (isTypeReferenceNode(contextual) &&
2290
+ isIdentifier(contextual.typeName) &&
2291
+ contextual.typeName.text === 'Array' &&
2292
+ contextual.typeArguments?.length === 1) {
2293
+ return getType(contextual.typeArguments[0]);
2294
+ }
2295
+ for (const element of node.elements ?? []) {
2296
+ if (isSpreadElement(element)) {
2297
+ const spreadType = inferExpressionType(element.expression);
2298
+ if (spreadType?.startsWith('[]'))
2299
+ return spreadType.slice(2);
2300
+ if (spreadType?.startsWith('*TnMap[') && extractMapValueType(spreadType) === 'struct{}') {
2301
+ return extractMapKeyType(spreadType);
2302
+ }
2303
+ continue;
2304
+ }
2305
+ const elementType = inferExpressionType(element);
2306
+ if (elementType && elementType !== 'nil')
2307
+ return elementType;
2308
+ }
2309
+ return 'interface{}';
2310
+ }
2311
+ // Loop bodies must be blocks in Go; `prefix` is emitted before the body's statements
2312
+ function visitLoopBody(statement, prefix = '') {
2313
+ if (isBlock(statement))
2314
+ return visit(statement, { prefixBlockContent: prefix });
2315
+ return `{\n\t\t${prefix}${visit(statement)}}\n\t`;
2316
+ }
2317
+ // `(x = expr) op rhs` as a loop condition: returns the assignment to hoist
2318
+ function getHoistedConditionAssignment(condition) {
2319
+ if (!isBinaryExpression(condition))
2320
+ return undefined;
2321
+ let left = condition.left;
2322
+ while (isParenthesizedExpression(left))
2323
+ left = left.expression;
2324
+ if (isBinaryExpression(left) && left.operatorToken.kind === 'EqualsToken')
2325
+ return left;
2326
+ return undefined;
2327
+ }
2328
+ // Function type a function literal is expected to have (e.g. from a declared
2329
+ // variable, parameter, or Record value type)
2330
+ function getContextualFunctionType(fn) {
2331
+ if (!isArrowFunction(fn) && !isFunctionExpression(fn))
2332
+ return undefined;
2333
+ const contextual = resolveTypeNode(getContextualTypeNode(fn));
2334
+ return isFunctionTypeNode(contextual) ? contextual : undefined;
2335
+ }
2336
+ function unwrapParentheses(expr) {
2337
+ while (isParenthesizedExpression(expr))
2338
+ expr = expr.expression;
2339
+ return expr;
2340
+ }
2341
+ function getTupleElementType(tuple) {
2342
+ const types = (tuple.elements ?? []).map((t) => getType(t.kind === 'NamedTupleMember' ? t.type : t));
2343
+ return types.length > 0 && types.every((t) => t === types[0]) ? types[0] : 'interface{}';
2344
+ }
1109
2345
  function getTypeText(typeNode) {
1110
2346
  if (!typeNode)
1111
2347
  return ':';
@@ -1117,6 +2353,14 @@ function getTypeText(typeNode) {
1117
2353
  function toGoStringLiteral(value) {
1118
2354
  return JSON.stringify(value);
1119
2355
  }
2356
+ // A spread source as a Go slice; Sets spread their elements (map keys)
2357
+ function visitSpreadSource(expr) {
2358
+ const sourceType = inferExpressionType(expr);
2359
+ if (sourceType?.startsWith('*TnMap[') && extractMapValueType(sourceType) === 'struct{}') {
2360
+ return `${visit(expr)}.Keys()`;
2361
+ }
2362
+ return visit(expr);
2363
+ }
1120
2364
  function visitSpreadArrayLiteral(node, elemType) {
1121
2365
  const chunks = [];
1122
2366
  for (const el of (node.elements ?? [])) {
@@ -1124,7 +2368,7 @@ function visitSpreadArrayLiteral(node, elemType) {
1124
2368
  chunks.push({ isSpread: true, items: [el.expression] });
1125
2369
  }
1126
2370
  else {
1127
- const last = chunks[chunks.length - 1];
2371
+ const last = chunks.length > 0 ? chunks[chunks.length - 1] : undefined;
1128
2372
  if (last && !last.isSpread) {
1129
2373
  last.items.push(el);
1130
2374
  }
@@ -1137,7 +2381,7 @@ function visitSpreadArrayLiteral(node, elemType) {
1137
2381
  let result = `[]${baseType}{}`;
1138
2382
  for (const chunk of chunks) {
1139
2383
  if (chunk.isSpread) {
1140
- result = `append(${result}, ${visit(chunk.items[0])}...)`;
2384
+ result = `append(${result}, ${visitSpreadSource(chunk.items[0])}...)`;
1141
2385
  }
1142
2386
  else {
1143
2387
  result = `append(${result}, ${chunk.items.map((e) => visit(e)).join(', ')})`;
@@ -1151,8 +2395,14 @@ function visitTemplateExpression(node) {
1151
2395
  parts.push(toGoStringLiteral(node.head.text));
1152
2396
  }
1153
2397
  for (const span of node.templateSpans) {
1154
- importedPackages.add('fmt');
1155
- parts.push(`fmt.Sprintf("%v", ${visit(span.expression)})`);
2398
+ const spanType = inferExpressionType(span.expression);
2399
+ if (spanType && NULLABLE_PRIMITIVE_TYPES.includes(spanType)) {
2400
+ useHelper('dynamic');
2401
+ parts.push(`TnFormat(${visit(span.expression)})`);
2402
+ }
2403
+ else {
2404
+ parts.push(jsStringOf(visit(span.expression), spanType));
2405
+ }
1156
2406
  if (span.literal.text.length > 0) {
1157
2407
  parts.push(toGoStringLiteral(span.literal.text));
1158
2408
  }
@@ -1211,6 +2461,9 @@ function prescanVariableDeclarations(block) {
1211
2461
  function inferFunctionBodyReturnType(node) {
1212
2462
  if (node.type)
1213
2463
  return getType(node.type);
2464
+ const contextualFn = getContextualFunctionType(node);
2465
+ if (contextualFn?.type)
2466
+ return getType(contextualFn.type);
1214
2467
  if (isArrowFunction(node) && !isBlock(node.body)) {
1215
2468
  return inferExpressionType(node.body);
1216
2469
  }
@@ -1226,7 +2479,15 @@ function inferFunctionBodyReturnType(node) {
1226
2479
  return undefined;
1227
2480
  }
1228
2481
  function inferArrowFunctionGoType(node) {
1229
- const params = (node.parameters ?? []).map((p) => (p.type ? getType(p.type) : 'interface{}')).join(', ');
2482
+ // Mirrors getFunctionParametersInfo: trailing defaulted parameters become
2483
+ // a variadic `...interface{}`
2484
+ const parameters = node.parameters ?? [];
2485
+ const firstDefaultIndex = parameters.findIndex((p) => !!p.initializer);
2486
+ const hasTrailingDefaults = firstDefaultIndex > -1 && !parameters.slice(firstDefaultIndex).some((p) => !p.initializer);
2487
+ const paramTypes = (hasTrailingDefaults ? parameters.slice(0, firstDefaultIndex) : parameters).map((p) => getParameterGoType(p));
2488
+ if (hasTrailingDefaults)
2489
+ paramTypes.push('...interface{}');
2490
+ const params = paramTypes.join(', ');
1230
2491
  const retType = inferFunctionBodyReturnType(node);
1231
2492
  return `func(${params})${retType ? ` ${retType}` : ''}`;
1232
2493
  }
@@ -1236,10 +2497,12 @@ function inferExpressionType(expr) {
1236
2497
  }
1237
2498
  if (isParenthesizedExpression(expr))
1238
2499
  return inferExpressionType(expr.expression);
1239
- if (isNonNullExpression(expr))
1240
- return inferExpressionType(expr.expression);
2500
+ if (isNonNullExpression(expr)) {
2501
+ const innerType = inferExpressionType(expr.expression);
2502
+ return innerType && NULLABLE_PRIMITIVE_TYPES.includes(innerType) ? innerType.slice(1) : innerType;
2503
+ }
1241
2504
  if (isAsExpression(expr))
1242
- return getType(expr.type);
2505
+ return getAsExpressionType(expr);
1243
2506
  if (isTypeAssertionExpression(expr))
1244
2507
  return getType(expr.type);
1245
2508
  if (isStringLiteral(expr) ||
@@ -1249,29 +2512,144 @@ function inferExpressionType(expr) {
1249
2512
  }
1250
2513
  if (isNumericLiteral(expr))
1251
2514
  return 'float64';
2515
+ if (expr.kind === 'TypeOfExpression')
2516
+ return 'string';
2517
+ if (isRegularExpressionLiteral(expr))
2518
+ return '*regexp.Regexp';
2519
+ if (isNewExpression(expr) && isIdentifier(expr.expression) && expr.expression.text === 'RegExp') {
2520
+ return '*regexp.Regexp';
2521
+ }
1252
2522
  if (expr.kind === 'TrueKeyword' || expr.kind === 'FalseKeyword')
1253
2523
  return 'bool';
1254
2524
  if (expr.kind === 'NullKeyword')
1255
2525
  return 'nil';
1256
- if (isIdentifier(expr))
1257
- return variableGoTypes.get(expr.text);
2526
+ if (isIdentifier(expr) && expr.text === 'undefined')
2527
+ return 'nil';
2528
+ if (isIdentifier(expr)) {
2529
+ const varType = variableGoTypes.get(expr.text);
2530
+ return narrowedVariables.has(expr.text) && varType?.startsWith('*') ? varType.slice(1) : varType;
2531
+ }
1258
2532
  if (isArrayLiteralExpression(expr)) {
1259
- const elements = expr.elements ?? [];
1260
- if (elements.length === 0)
1261
- return '[]interface{}';
1262
- const firstElementType = inferExpressionType(elements[0]) ?? 'interface{}';
1263
- return `[]${firstElementType}`;
2533
+ return `[]${getArrayLiteralElementType(expr)}`;
1264
2534
  }
2535
+ if (isObjectLiteralExpression(expr))
2536
+ return getObjectLiteralGoType(expr);
1265
2537
  if (isNewExpression(expr) && isIdentifier(expr.expression)) {
1266
2538
  const ctorName = expr.expression.text;
1267
- if (ctorName === 'Map' && expr.typeArguments && expr.typeArguments.length === 2) {
1268
- return `map[${getType(expr.typeArguments[0])}]${getType(expr.typeArguments[1])}`;
2539
+ const typeArguments = getCollectionTypeArguments(expr);
2540
+ if (ctorName === 'Map' && typeArguments.length === 2) {
2541
+ return mapGoType(getType(typeArguments[0]), getType(typeArguments[1]));
1269
2542
  }
1270
- if (ctorName === 'Set' && expr.typeArguments && expr.typeArguments.length === 1) {
1271
- return `map[${getType(expr.typeArguments[0])}]struct{}`;
2543
+ if (ctorName === 'Set' && typeArguments.length === 1) {
2544
+ return mapGoType(getType(typeArguments[0]), 'struct{}');
1272
2545
  }
2546
+ if (ctorName === 'Set' && expr.arguments?.length && isArrayLiteralExpression(expr.arguments[0])) {
2547
+ return mapGoType(getArrayLiteralElementType(expr.arguments[0]), 'struct{}');
2548
+ }
2549
+ }
2550
+ if (isElementAccessExpression(expr)) {
2551
+ const objectType = inferExpressionType(expr.expression);
2552
+ if (objectType === 'string')
2553
+ return 'string';
2554
+ if (objectType?.startsWith('[]'))
2555
+ return objectType.slice(2);
2556
+ if (objectType?.startsWith('*TnMap['))
2557
+ return extractMapValueType(objectType);
2558
+ const declared = getExpressionTypeNode(expr);
2559
+ if (declared)
2560
+ return getType(declared);
2561
+ }
2562
+ if (isPropertyAccessExpression(expr) &&
2563
+ isIdentifier(expr.expression) &&
2564
+ expr.expression.text === 'process' &&
2565
+ expr.name.text === 'argv') {
2566
+ return '[]string';
2567
+ }
2568
+ if (isIdentifier(expr) && (expr.text === 'NaN' || expr.text === 'Infinity'))
2569
+ return 'float64';
2570
+ if (isPropertyAccessExpression(expr) && isIdentifier(expr.expression) && expr.expression.text === 'Number') {
2571
+ return 'float64';
2572
+ }
2573
+ // process.env.X (also through casts: (process.env as any).X)
2574
+ if (isPropertyAccessExpression(expr) && isProcessEnv(expr.expression))
2575
+ return 'string';
2576
+ if ((isPrefixUnaryExpression(expr) || isPostfixUnaryExpression(expr)) &&
2577
+ ['MinusToken', 'PlusToken', 'TildeToken', 'PlusPlusToken', 'MinusMinusToken'].includes(expr.operator)) {
2578
+ return 'float64';
2579
+ }
2580
+ if (isCallExpression(expr)) {
2581
+ const callee = isIdentifier(expr.expression)
2582
+ ? expr.expression.text
2583
+ : isPropertyAccessExpression(expr.expression) && isIdentifier(expr.expression.expression)
2584
+ ? `${expr.expression.expression.text}.${expr.expression.name.text}`
2585
+ : undefined;
2586
+ const nodeResultType = callee ? nodeCallResultTypes.get(callee) : undefined;
2587
+ if (callee && BUILTIN_CALL_TYPES[callee])
2588
+ return BUILTIN_CALL_TYPES[callee];
2589
+ if (callee?.startsWith('Math.'))
2590
+ return 'float64';
2591
+ if (nodeResultType)
2592
+ return nodeResultType;
2593
+ }
2594
+ // Object.keys(map) / Object.values(map)
2595
+ if (isCallExpression(expr) &&
2596
+ isPropertyAccessExpression(expr.expression) &&
2597
+ isIdentifier(expr.expression.expression) &&
2598
+ expr.expression.expression.text === 'Object') {
2599
+ const mapType = inferExpressionType(expr.arguments?.[0]);
2600
+ if (mapType?.startsWith('*TnMap[')) {
2601
+ if (expr.expression.name.text === 'keys')
2602
+ return `[]${extractMapKeyType(mapType)}`;
2603
+ if (expr.expression.name.text === 'values')
2604
+ return `[]${extractMapValueType(mapType)}`;
2605
+ if (expr.expression.name.text === 'entries')
2606
+ return '[][]interface{}';
2607
+ }
2608
+ }
2609
+ if (isCallExpression(expr) && isIdentifier(expr.expression)) {
2610
+ const builtinType = BUILTIN_FUNCTION_TYPES[expr.expression.text];
2611
+ if (builtinType && !declaredFunctions.has(expr.expression.text))
2612
+ return builtinType;
2613
+ }
2614
+ if (isPropertyAccessExpression(expr) && expr.name.text === 'length' && isDynamicValue(expr.expression)) {
2615
+ return 'float64';
2616
+ }
2617
+ if ((isPropertyAccessExpression(expr) || isElementAccessExpression(expr)) && isDynamicValue(expr.expression)) {
2618
+ return 'interface{}';
2619
+ }
2620
+ if (isCallExpression(expr) && isObjectKeysOfDynamic(expr))
2621
+ return '[]string';
2622
+ if (isCallExpression(expr) &&
2623
+ isPropertyAccessExpression(expr.expression) &&
2624
+ isIdentifier(expr.expression.expression) &&
2625
+ expr.expression.expression.text === 'JSON' &&
2626
+ expr.expression.name.text === 'parse') {
2627
+ return 'interface{}';
2628
+ }
2629
+ if (isCallExpression(expr) &&
2630
+ isPropertyAccessExpression(expr.expression) &&
2631
+ isDynamicValue(expr.expression.expression)) {
2632
+ const method = expr.expression.name.text;
2633
+ const castCall = castDynamicReceiverCall(expr);
2634
+ if (castCall)
2635
+ return inferExpressionType(castCall);
2636
+ if (method === 'includes')
2637
+ return 'bool';
2638
+ if (method === 'indexOf')
2639
+ return 'float64';
2640
+ if (method === 'slice' || method === 'concat')
2641
+ return 'interface{}';
1273
2642
  }
1274
2643
  if (isPropertyAccessExpression(expr)) {
2644
+ const narrowingKey = getNarrowingKey(expr);
2645
+ if (narrowingKey && narrowedVariables.has(narrowingKey)) {
2646
+ // Narrowed: the declared (pointer) type without its pointer
2647
+ narrowedVariables.delete(narrowingKey);
2648
+ const nullableType = inferExpressionType(expr);
2649
+ narrowedVariables.add(narrowingKey);
2650
+ if (nullableType?.startsWith('*'))
2651
+ return nullableType.slice(1);
2652
+ }
1275
2653
  if (isIdentifier(expr.expression) && enumNames.has(expr.expression.text)) {
1276
2654
  const enumType = getSafeName(expr.expression.text);
1277
2655
  return enumType;
@@ -1286,7 +2664,9 @@ function inferExpressionType(expr) {
1286
2664
  : undefined;
1287
2665
  if (hasQuestionDot(expr)) {
1288
2666
  if (leftType && leftType.startsWith('*')) {
1289
- const memberType = resolvedPropertyType ?? 'interface{}';
2667
+ const memberType = resolvedPropertyType ??
2668
+ getStructFieldGoType(leftType.slice(1), expr.name.text) ??
2669
+ 'interface{}';
1290
2670
  return makeNullableType(memberType);
1291
2671
  }
1292
2672
  return 'interface{}';
@@ -1294,6 +2674,9 @@ function inferExpressionType(expr) {
1294
2674
  if (resolvedPropertyType) {
1295
2675
  return resolvedPropertyType;
1296
2676
  }
2677
+ const structFieldType = leftType ? getStructFieldGoType(leftType.replace(/^\*/, ''), expr.name.text) : undefined;
2678
+ if (structFieldType)
2679
+ return structFieldType;
1297
2680
  if (isIdentifier(expr.expression)) {
1298
2681
  const className = variableClassNames.get(expr.expression.text);
1299
2682
  const memberType = className
@@ -1303,14 +2686,46 @@ function inferExpressionType(expr) {
1303
2686
  return memberType;
1304
2687
  }
1305
2688
  }
2689
+ if (isCallExpression(expr) && isIdentifier(expr.expression)) {
2690
+ const fn = declaredFunctions.get(expr.expression.text);
2691
+ if (fn?.type)
2692
+ return getType(fn.type);
2693
+ const fnValueType = variableGoTypes.get(expr.expression.text);
2694
+ if (fnValueType?.startsWith('func('))
2695
+ return goFuncReturnType(fnValueType);
2696
+ }
1306
2697
  if (isCallExpression(expr) && isPropertyAccessExpression(expr.expression)) {
1307
2698
  const methodName = expr.expression.name.text;
1308
2699
  const ownerType = inferExpressionType(expr.expression.expression);
1309
- if (ownerType && ownerType.startsWith('map[')) {
2700
+ if (ownerType &&
2701
+ NULLABLE_PRIMITIVE_TYPES.includes(ownerType) &&
2702
+ (hasQuestionDot(expr) ||
2703
+ hasQuestionDot(expr.expression) ||
2704
+ isOptionalChain(expr.expression.expression))) {
2705
+ const tmp = getTempName('optt');
2706
+ const call = makeNarrowedReceiverCall(expr, tmp, ownerType);
2707
+ const resultType = withNarrowingType([tmp], () => inferExpressionType(call));
2708
+ variableGoTypes.delete(tmp);
2709
+ return resultType ? makeNullableType(resultType) : undefined;
2710
+ }
2711
+ if (ownerType === 'string') {
2712
+ const stringMethodType = STRING_METHOD_RETURN_TYPES[methodName];
2713
+ if (stringMethodType)
2714
+ return stringMethodType;
2715
+ }
2716
+ if (ownerType === '*regexp.Regexp') {
2717
+ if (methodName === 'exec')
2718
+ return '[]string';
2719
+ if (methodName === 'test')
2720
+ return 'bool';
2721
+ }
2722
+ if (ownerType && ownerType.startsWith('*TnMap[')) {
1310
2723
  if (methodName === 'has')
1311
2724
  return 'bool';
1312
- if (methodName === 'get')
1313
- return extractMapValueType(ownerType);
2725
+ if (methodName === 'get') {
2726
+ const valueType = extractMapValueType(ownerType);
2727
+ return isStructGoType(valueType) ? `*${valueType}` : valueType;
2728
+ }
1314
2729
  }
1315
2730
  if (isArrayLikeGoType(ownerType)) {
1316
2731
  const elementType = getArrayElementTypeFromGoType(ownerType);
@@ -1321,10 +2736,17 @@ function inferExpressionType(expr) {
1321
2736
  : elementType;
1322
2737
  return `[]${mappedType}`;
1323
2738
  }
1324
- if (methodName === 'filter')
2739
+ if (['filter', 'slice', 'concat', 'reverse', 'sort'].includes(methodName)) {
1325
2740
  return `[]${elementType}`;
1326
- if (methodName === 'some')
2741
+ }
2742
+ if (['some', 'every', 'includes'].includes(methodName))
1327
2743
  return 'bool';
2744
+ if (['indexOf', 'lastIndexOf', 'findIndex'].includes(methodName))
2745
+ return 'float64';
2746
+ if (['pop', 'shift', 'at'].includes(methodName))
2747
+ return elementType;
2748
+ if (methodName === 'flat' && elementType.startsWith('[]'))
2749
+ return elementType;
1328
2750
  if (methodName === 'find')
1329
2751
  return elementType;
1330
2752
  if (methodName === 'join')
@@ -1356,55 +2778,352 @@ function inferExpressionType(expr) {
1356
2778
  if (isConditionalExpression(expr)) {
1357
2779
  const whenTrueType = inferExpressionType(expr.whenTrue);
1358
2780
  const whenFalseType = inferExpressionType(expr.whenFalse);
2781
+ // `c ? value : undefined` is nullable
2782
+ if (whenTrueType === 'nil' && whenFalseType)
2783
+ return makeNullableType(whenFalseType);
2784
+ if (whenFalseType === 'nil' && whenTrueType)
2785
+ return makeNullableType(whenTrueType);
1359
2786
  if (whenTrueType && whenTrueType === whenFalseType)
1360
2787
  return whenTrueType;
1361
- return whenTrueType ?? whenFalseType;
2788
+ if (whenTrueType && whenFalseType === `*${whenTrueType}`)
2789
+ return whenFalseType;
2790
+ if (whenFalseType && whenTrueType === `*${whenFalseType}`)
2791
+ return whenTrueType;
2792
+ const branchType = whenTrueType ?? whenFalseType;
2793
+ return branchType === 'nil' ? 'interface{}' : branchType;
1362
2794
  }
1363
2795
  if (isBinaryExpression(expr) &&
1364
2796
  expr.operatorToken.kind === 'QuestionQuestionToken') {
1365
- const leftType = inferExpressionType(expr.left);
1366
- const rightType = inferExpressionType(expr.right);
1367
- if (leftType && leftType.startsWith('*') && rightType === leftType.slice(1)) {
1368
- return rightType;
2797
+ return getNullishResultType(expr);
2798
+ }
2799
+ if (isPrefixUnaryExpression(expr) && expr.operator === 'ExclamationToken')
2800
+ return 'bool';
2801
+ if (isBinaryExpression(expr)) {
2802
+ const kind = expr.operatorToken.kind;
2803
+ if (COMPARISON_OPERATORS.has(kind) || kind === 'InKeyword' || kind === 'InstanceOfKeyword') {
2804
+ return 'bool';
2805
+ }
2806
+ if (isLogicalOperator(expr.operatorToken)) {
2807
+ return isLogicalValueExpression(expr) ? getLogicalValueType(expr) : 'bool';
2808
+ }
2809
+ if ([
2810
+ 'MinusToken',
2811
+ 'AsteriskToken',
2812
+ 'SlashToken',
2813
+ 'PercentToken',
2814
+ 'AsteriskAsteriskToken',
2815
+ 'AmpersandToken',
2816
+ 'BarToken',
2817
+ 'CaretToken',
2818
+ 'LessThanLessThanToken',
2819
+ 'GreaterThanGreaterThanToken',
2820
+ 'GreaterThanGreaterThanGreaterThanToken'
2821
+ ].includes(kind)) {
2822
+ return 'float64';
2823
+ }
2824
+ if (kind === 'PlusToken') {
2825
+ const isString = [expr.left, expr.right].some((side) => inferExpressionType(side) === 'string');
2826
+ return isString ? 'string' : 'float64';
2827
+ }
2828
+ if (['LessThanToken', 'GreaterThanToken', 'LessThanEqualsToken', 'GreaterThanEqualsToken'].includes(kind)) {
2829
+ return 'bool';
2830
+ }
2831
+ }
2832
+ return undefined;
2833
+ }
2834
+ const BUILTIN_CALL_TYPES = {
2835
+ 'Number.isNaN': 'bool',
2836
+ 'Number.isFinite': 'bool',
2837
+ 'Number.isInteger': 'bool',
2838
+ isNaN: 'bool'
2839
+ };
2840
+ const BUILTIN_FUNCTION_TYPES = {
2841
+ parseFloat: 'float64',
2842
+ parseInt: 'float64',
2843
+ Number: 'float64',
2844
+ String: 'string'
2845
+ };
2846
+ const STRING_METHOD_RETURN_TYPES = {
2847
+ trim: 'string',
2848
+ trimStart: 'string',
2849
+ trimEnd: 'string',
2850
+ toUpperCase: 'string',
2851
+ toLowerCase: 'string',
2852
+ slice: 'string',
2853
+ substring: 'string',
2854
+ replace: 'string',
2855
+ replaceAll: 'string',
2856
+ padStart: 'string',
2857
+ padEnd: 'string',
2858
+ repeat: 'string',
2859
+ charAt: 'string',
2860
+ concat: 'string',
2861
+ at: 'string',
2862
+ split: '[]string',
2863
+ match: '[]string',
2864
+ matchAll: '[][]string',
2865
+ includes: 'bool',
2866
+ startsWith: 'bool',
2867
+ endsWith: 'bool',
2868
+ indexOf: 'float64',
2869
+ lastIndexOf: 'float64',
2870
+ search: 'float64',
2871
+ charCodeAt: 'float64'
2872
+ };
2873
+ // Return type of a Go func type string: `func(a T) R` → `R`
2874
+ function goFuncReturnType(funcType) {
2875
+ let depth = 0;
2876
+ for (let i = 4; i < funcType.length; i++) {
2877
+ const ch = funcType[i];
2878
+ if (ch === '(')
2879
+ depth++;
2880
+ else if (ch === ')') {
2881
+ depth--;
2882
+ if (depth === 0)
2883
+ return funcType.slice(i + 1).trim() || undefined;
1369
2884
  }
1370
- return rightType ?? leftType;
1371
2885
  }
1372
2886
  return undefined;
1373
2887
  }
2888
+ const COMPARISON_OPERATORS = new Set([
2889
+ 'EqualsEqualsEqualsToken',
2890
+ 'ExclamationEqualsEqualsToken',
2891
+ 'EqualsEqualsToken',
2892
+ 'ExclamationEqualsToken',
2893
+ 'LessThanToken',
2894
+ 'LessThanEqualsToken',
2895
+ 'GreaterThanToken',
2896
+ 'GreaterThanEqualsToken'
2897
+ ]);
2898
+ function isLogicalOperator(token) {
2899
+ return token?.kind === 'AmpersandAmpersandToken' || token?.kind === 'BarBarToken';
2900
+ }
2901
+ // JS truthiness of an expression as a Go bool, by its Go type
2902
+ function toGoCondition(expr) {
2903
+ if (isParenthesizedExpression(expr))
2904
+ return `(${toGoCondition(expr.expression)})`;
2905
+ if (isPrefixUnaryExpression(expr) && expr.operator === 'ExclamationToken') {
2906
+ return `!${wrapCondition(toGoCondition(expr.operand))}`;
2907
+ }
2908
+ if (isBinaryExpression(expr) && isLogicalOperator(expr.operatorToken)) {
2909
+ const isAnd = expr.operatorToken.kind === 'AmpersandAmpersandToken';
2910
+ const right = withNarrowing(getNarrowedNames(expr.left, isAnd), () => toGoCondition(expr.right));
2911
+ return `${toGoCondition(expr.left)} ${isAnd ? '&&' : '||'} ${right}`;
2912
+ }
2913
+ return truthinessCheck(visit(expr, { inline: true }), inferExpressionType(expr));
2914
+ }
2915
+ function truthinessCheck(code, goType) {
2916
+ if (!goType || goType === 'bool' || goType === ':')
2917
+ return code;
2918
+ if (goType === 'interface{}') {
2919
+ useHelper('dynamic');
2920
+ return `TnTruthy(${code})`;
2921
+ }
2922
+ if (NULLABLE_PRIMITIVE_TYPES.includes(goType) && !/^[\w.()*]+$/.test(code)) {
2923
+ return `func() bool { __t := ${code}; return ${truthinessCheck('__t', goType)} }()`;
2924
+ }
2925
+ // structs (value types) are always truthy
2926
+ if (isStructGoType(goType))
2927
+ return 'true';
2928
+ if (goType === 'string')
2929
+ return `${code} != ""`;
2930
+ if (goType === 'float64')
2931
+ return `${code} != 0`;
2932
+ if (goType === '*bool')
2933
+ return `(${code} != nil && *${code})`;
2934
+ if (goType === '*string')
2935
+ return `(${code} != nil && *${code} != "")`;
2936
+ if (goType === '*float64')
2937
+ return `(${code} != nil && *${code} != 0)`;
2938
+ if (goType.startsWith('*') ||
2939
+ goType.startsWith('[]') ||
2940
+ goType.startsWith('map[') ||
2941
+ goType.startsWith('func') ||
2942
+ goType.startsWith('chan ') ||
2943
+ goType === 'interface{}') {
2944
+ return `${code} != nil`;
2945
+ }
2946
+ return code;
2947
+ }
2948
+ // Parenthesizes a condition unless it is already atomic
2949
+ function wrapCondition(condition) {
2950
+ return /^[\w.]+$/.test(condition) || /^\(.*\)$/.test(condition) ? condition : `(${condition})`;
2951
+ }
2952
+ // `a || b` with non-bool operands is a value (JS returns an operand), not a bool
2953
+ function isLogicalValueExpression(expr) {
2954
+ if (expr.operatorToken.kind !== 'BarBarToken')
2955
+ return false;
2956
+ const leftType = inferExpressionType(expr.left);
2957
+ const rightType = inferExpressionType(expr.right);
2958
+ return !!leftType && leftType !== 'bool' && !!rightType && rightType !== 'bool';
2959
+ }
2960
+ function getLogicalValueType(expr) {
2961
+ const leftType = inferExpressionType(expr.left);
2962
+ const rightType = inferExpressionType(expr.right);
2963
+ if (rightType === 'nil')
2964
+ return makeNullableType(leftType);
2965
+ if (leftType.startsWith('*') && rightType === leftType.slice(1))
2966
+ return rightType;
2967
+ return leftType;
2968
+ }
2969
+ // && / || : boolean form when any operand is non-bool (truthiness), value form for
2970
+ // `a || fallback`; undefined keeps the plain Go operator (both operands bool/unknown)
2971
+ function visitLogicalExpression(expr) {
2972
+ const leftType = inferExpressionType(expr.left);
2973
+ const rightType = inferExpressionType(expr.right);
2974
+ const isBoolOrUnknown = (t) => !t || t === 'bool' || t === ':';
2975
+ if (isBoolOrUnknown(leftType) && isBoolOrUnknown(rightType))
2976
+ return undefined;
2977
+ if (!isLogicalValueExpression(expr))
2978
+ return toGoCondition(expr);
2979
+ // a || b → func() T { if truthy(a) { return a }; return b }()
2980
+ const resultType = getLogicalValueType(expr);
2981
+ const tmp = getTempName('or');
2982
+ let leftValue = tmp;
2983
+ if (leftType.startsWith('*') && resultType === leftType.slice(1))
2984
+ leftValue = `*${tmp}`;
2985
+ else if (resultType === `*${leftType}`)
2986
+ leftValue = `&${tmp}`;
2987
+ return `func() ${resultType} { ${tmp} := ${visit(expr.left)}; if ${truthinessCheck(tmp, leftType)} { return ${leftValue} }; return ${toGoValueOfType(expr.right, resultType)} }()`;
2988
+ }
1374
2989
  function makeNullableType(typeName) {
1375
2990
  if (!typeName || typeName === 'interface{}' || typeName.startsWith('*'))
1376
2991
  return typeName || 'interface{}';
1377
2992
  if (['string', 'float64', 'bool'].includes(typeName))
1378
2993
  return `*${typeName}`;
2994
+ if (isStructGoType(typeName))
2995
+ return `*${typeName}`;
1379
2996
  return typeName;
1380
2997
  }
2998
+ // Go struct types (value types that cannot be nil): anonymous structs and
2999
+ // property-only interfaces / object type aliases
3000
+ // Object types that compile to Go structs: property-only interfaces and
3001
+ // object type aliases (values of these types are *Name, like JS references)
3002
+ function isObjectTypeName(name) {
3003
+ const alias = declaredTypeAliases.get(name) ?? typeAliases.get(name);
3004
+ if (alias)
3005
+ return alias.kind === 'TypeLiteral';
3006
+ const iface = declaredInterfaces.get(name);
3007
+ if (iface) {
3008
+ const members = iface.members ?? [];
3009
+ return members.some((m) => isPropertySignature(m)) && !members.some((m) => isMethodSignature(m));
3010
+ }
3011
+ return interfacePropertyTypes.has(name);
3012
+ }
3013
+ function isStructGoType(goType) {
3014
+ return goType.startsWith('struct{') || interfacePropertyTypes.has(goType);
3015
+ }
1381
3016
  function visitConditionalExpression(node) {
1382
- const whenTrue = visit(node.whenTrue);
1383
- const whenFalse = visit(node.whenFalse);
1384
- const resultType = inferExpectedTypeFromContext(node) ||
1385
- (() => {
1386
- const whenTrueType = inferExpressionType(node.whenTrue);
1387
- const whenFalseType = inferExpressionType(node.whenFalse);
1388
- if (whenTrueType && whenTrueType === whenFalseType)
1389
- return whenTrueType;
1390
- return whenTrueType ?? whenFalseType ?? 'interface{}';
1391
- })();
1392
- return `func() ${resultType} { if ${visit(node.condition)} { return ${whenTrue} }; return ${whenFalse} }()`;
3017
+ const resultType = inferExpectedTypeFromContext(node) || inferExpressionType(node) || 'interface{}';
3018
+ const whenTrue = withNarrowing(getNarrowedNames(node.condition, true), () => toGoValueOfType(node.whenTrue, resultType));
3019
+ const whenFalse = withNarrowing(getNarrowedNames(node.condition, false), () => toGoValueOfType(node.whenFalse, resultType));
3020
+ return `func() ${resultType} { if ${toGoCondition(node.condition)} { return ${whenTrue} }; return ${whenFalse} }()`;
3021
+ }
3022
+ // Type of `a ?? b`: NonNullable<A> | B — nullable only when b can be null too
3023
+ function getNullishResultType(expr) {
3024
+ const leftType = inferExpressionType(expr.left);
3025
+ const rightType = inferExpressionType(expr.right);
3026
+ const leftValueType = leftType && NULLABLE_PRIMITIVE_TYPES.includes(leftType) ? leftType.slice(1) : leftType;
3027
+ if (!rightType || rightType === 'nil')
3028
+ return leftType;
3029
+ if (rightType === leftValueType)
3030
+ return rightType;
3031
+ if (leftValueType && rightType === `*${leftValueType}`)
3032
+ return rightType;
3033
+ if (leftValueType === 'interface{}')
3034
+ return 'interface{}';
3035
+ if (!leftValueType)
3036
+ return rightType;
3037
+ return leftValueType;
3038
+ }
3039
+ // Inside a ?? chain an operand keeps its own type; otherwise the context decides
3040
+ function getNullishEmitType(node) {
3041
+ const parent = node.parent;
3042
+ const inChain = isBinaryExpression(parent) && parent.operatorToken.kind === 'QuestionQuestionToken';
3043
+ return (inChain ? undefined : inferExpectedTypeFromContext(node)) || getNullishResultType(node);
1393
3044
  }
3045
+ // a ?? b ?? c: operands are tried in order; map lookups (comma-ok) and nil-able
3046
+ // values may be missing, anything else is always defined and ends the chain
1394
3047
  function visitNullishCoalescingExpression(node) {
1395
- const leftType = inferExpressionType(node.left);
1396
- const rightType = inferExpressionType(node.right);
1397
- if (leftType && leftType.startsWith('*')) {
1398
- const leftValueType = leftType.slice(1);
1399
- const expectedType = inferExpectedTypeFromContext(node);
1400
- const resultType = expectedType || (rightType === leftValueType ? leftValueType : (rightType ?? leftType));
3048
+ const operands = flattenNullishChain(node);
3049
+ const resultType = getNullishEmitType(node) || 'interface{}';
3050
+ const steps = [];
3051
+ for (let i = 0; i < operands.length; i++) {
3052
+ const operand = operands[i];
3053
+ const isLast = i === operands.length - 1;
1401
3054
  const tmp = getTempName('nullish');
1402
- const leftExpr = visit(node.left);
1403
- const rightExpr = visit(node.right);
1404
- const returnLeft = resultType === leftValueType ? `*${tmp}` : tmp;
1405
- return `func() ${resultType} { ${tmp} := ${leftExpr}; if ${tmp} == nil { return ${rightExpr} }; return ${returnLeft} }()`;
3055
+ const lookup = isLast ? undefined : getMapLookup(operand);
3056
+ const operandType = inferExpressionType(operand);
3057
+ const arrayType = !isLast && isElementAccessExpression(operand) ? inferExpressionType(operand.expression) : undefined;
3058
+ if (arrayType?.startsWith('[]')) {
3059
+ // arr[i] ?? x: out of range is undefined in JS
3060
+ const array = getTempName('arr');
3061
+ const index = getTempName('idx');
3062
+ steps.push(`if ${array}, ${index} := ${visit(operand.expression)}, int(${visit(operand.argumentExpression)}); ${index} >= 0 && ${index} < len(${array}) { return ${convertGoValue(`${array}[${index}]`, arrayType.slice(2), resultType)} }`);
3063
+ }
3064
+ else if (lookup) {
3065
+ const valueType = extractMapValueType(inferExpressionType(lookup.mapNode) ?? '');
3066
+ steps.push(`if ${tmp}, ok := ${lookup.map}.Lookup(${lookup.key}); ok { return ${convertGoValue(tmp, valueType, resultType)} }`);
3067
+ }
3068
+ else if (!isLast && operandType && isNilableGoType(operandType)) {
3069
+ steps.push(`if ${tmp} := ${visit(operand)}; ${tmp} != nil { return ${convertGoValue(tmp, operandType, resultType)} }`);
3070
+ }
3071
+ else {
3072
+ steps.push(`return ${toGoValueOfType(operand, resultType)}`);
3073
+ break;
3074
+ }
3075
+ }
3076
+ return `func() ${resultType} { ${steps.join('; ')} }()`;
3077
+ }
3078
+ function flattenNullishChain(node) {
3079
+ if (isBinaryExpression(node) && node.operatorToken.kind === 'QuestionQuestionToken') {
3080
+ return [...flattenNullishChain(node.left), node.right];
3081
+ }
3082
+ if (isParenthesizedExpression(node))
3083
+ return flattenNullishChain(node.expression);
3084
+ return [node];
3085
+ }
3086
+ // Converts a Go variable between T and *T as the target type requires
3087
+ function convertGoValue(variable, fromType, toType) {
3088
+ if (toType === `*${fromType}`)
3089
+ return `&${variable}`;
3090
+ if (fromType === `*${toType}`)
3091
+ return `*${variable}`;
3092
+ // dynamic (any) values hold the concrete type at runtime
3093
+ if (fromType === 'interface{}' && toType !== 'interface{}')
3094
+ return `${variable}.(${toType})`;
3095
+ return variable;
3096
+ }
3097
+ // Map lookups (m.get(k) on a Map, m[k] on a Record): Go code of the map and key
3098
+ function getMapLookup(expr) {
3099
+ if (isCallExpression(expr) &&
3100
+ isPropertyAccessExpression(expr.expression) &&
3101
+ expr.expression.name.text === 'get' &&
3102
+ inferExpressionType(expr.expression.expression)?.startsWith('*TnMap[')) {
3103
+ const mapType = inferExpressionType(expr.expression.expression);
3104
+ return {
3105
+ map: visit(expr.expression.expression),
3106
+ key: toGoValueOfType(expr.arguments[0], extractMapKeyType(mapType)),
3107
+ mapNode: expr.expression.expression
3108
+ };
3109
+ }
3110
+ if (isElementAccessExpression(expr) && inferExpressionType(expr.expression)?.startsWith('*TnMap[')) {
3111
+ const mapType = inferExpressionType(expr.expression);
3112
+ return {
3113
+ map: visit(expr.expression),
3114
+ key: toGoValueOfType(expr.argumentExpression, extractMapKeyType(mapType)),
3115
+ mapNode: expr.expression
3116
+ };
1406
3117
  }
1407
- return visit(node.left);
3118
+ return undefined;
3119
+ }
3120
+ function isNilableGoType(goType) {
3121
+ return (goType.startsWith('*') ||
3122
+ goType.startsWith('[]') ||
3123
+ goType.startsWith('map[') ||
3124
+ goType.startsWith('func') ||
3125
+ goType.startsWith('chan ') ||
3126
+ goType === 'interface{}');
1408
3127
  }
1409
3128
  function visitOptionalPropertyAccess(node) {
1410
3129
  const baseExpr = visit(node.expression);
@@ -1414,7 +3133,10 @@ function visitOptionalPropertyAccess(node) {
1414
3133
  return getAcessString(baseExpr, visit(node.name), objectType);
1415
3134
  }
1416
3135
  const className = baseType.replace(/^\*/, '').replace(/\[.*\]$/, '');
1417
- const propertyType = classPropertyTypes.get(className)?.get(node.name.text) ?? 'interface{}';
3136
+ const propertyType = classPropertyTypes.get(className)?.get(node.name.text) ??
3137
+ interfacePropertyTypes.get(className)?.get(node.name.text) ??
3138
+ getStructFieldGoType(className, node.name.text) ??
3139
+ 'interface{}';
1418
3140
  const nullableType = makeNullableType(propertyType);
1419
3141
  const tmp = getTempName('opt');
1420
3142
  const propertyAccess = `${tmp}.${visit(node.name)}`;
@@ -1427,8 +3149,9 @@ function visitOptionalPropertyAccess(node) {
1427
3149
  function visitOptionalElementAccess(node) {
1428
3150
  const baseExpr = visit(node.expression);
1429
3151
  const baseType = inferExpressionType(node.expression);
1430
- if (!baseType || !baseType.startsWith('*')) {
1431
- return `${baseExpr}[int(${visit(node.argumentExpression)})]`;
3152
+ if (!baseType || !baseType.startsWith('*') || baseType.startsWith('*TnMap[')) {
3153
+ const plainAccess = { ...node, questionDotToken: undefined };
3154
+ return visit(plainAccess);
1432
3155
  }
1433
3156
  const valueType = inferExpectedTypeFromContext(node) ?? 'interface{}';
1434
3157
  const nullableType = makeNullableType(valueType);
@@ -1440,6 +3163,33 @@ function visitOptionalElementAccess(node) {
1440
3163
  }
1441
3164
  return `func() ${nullableType} { ${tmp} := ${baseExpr}; if ${tmp} == nil { var __zero ${nullableType}; return __zero }; return ${elementExpr} }()`;
1442
3165
  }
3166
+ // The call with its receiver replaced by a narrowed temporary of the receiver's type
3167
+ function makeNarrowedReceiverCall(node, tmp, baseType) {
3168
+ variableGoTypes.set(tmp, baseType);
3169
+ const receiver = { kind: 'Identifier', text: tmp };
3170
+ const access = { ...node.expression, questionDotToken: undefined, expression: receiver };
3171
+ const call = { ...node, questionDotToken: undefined, expression: access, parent: node.parent };
3172
+ receiver.parent = access;
3173
+ access.parent = call;
3174
+ return call;
3175
+ }
3176
+ function visitNullablePrimitiveOptionalCall(node, baseExpr, baseType) {
3177
+ const tmp = getTempName('optc');
3178
+ const call = makeNarrowedReceiverCall(node, tmp, baseType);
3179
+ const resultType = makeNullableType(withNarrowingType([tmp], () => inferExpressionType(call)) ?? 'interface{}');
3180
+ const callCode = withNarrowing([tmp], () => visit(call));
3181
+ const value = resultType.startsWith('*') ? `func() ${resultType} { v := ${callCode}; return &v }()` : callCode;
3182
+ return `func() ${resultType} { ${tmp} := ${baseExpr}; if ${tmp} == nil { return nil }; return ${value} }()`;
3183
+ }
3184
+ function withNarrowingType(names, infer) {
3185
+ const added = names.filter((n) => !narrowedVariables.has(n));
3186
+ for (const name of added)
3187
+ narrowedVariables.add(name);
3188
+ const type = infer();
3189
+ for (const name of added)
3190
+ narrowedVariables.delete(name);
3191
+ return type;
3192
+ }
1443
3193
  function visitOptionalCall(node) {
1444
3194
  if (!isPropertyAccessExpression(node.expression)) {
1445
3195
  return `${visit(node.expression)}(${(node.arguments ?? []).map((a) => visit(a)).join(', ')})`;
@@ -1449,8 +3199,14 @@ function visitOptionalCall(node) {
1449
3199
  const baseExpr = visit(baseNode);
1450
3200
  const baseType = inferExpressionType(baseNode);
1451
3201
  const args = (node.arguments ?? []).map((a) => visit(a)).join(', ');
1452
- if (!baseType || !baseType.startsWith('*')) {
1453
- return `${baseExpr}.${methodName}(${args})`;
3202
+ if (baseType && NULLABLE_PRIMITIVE_TYPES.includes(baseType)) {
3203
+ return visitNullablePrimitiveOptionalCall(node, baseExpr, baseType);
3204
+ }
3205
+ if (!baseType || !baseType.startsWith('*') || baseType.startsWith('*TnMap[')) {
3206
+ const plainAccess = { ...node.expression, questionDotToken: undefined };
3207
+ const plainCall = { ...node, questionDotToken: undefined, expression: plainAccess };
3208
+ plainAccess.parent = plainCall;
3209
+ return visit(plainCall);
1454
3210
  }
1455
3211
  const className = baseType.replace(/^\*/, '').replace(/\[.*\]$/, '');
1456
3212
  const returnType = classMethodReturnTypes.get(className)?.get(methodName) ?? 'interface{}';
@@ -1479,7 +3235,7 @@ function inferArrayCallbackReturnType(callback, elementType, fallbackType) {
1479
3235
  return explicitType || fallbackType;
1480
3236
  }
1481
3237
  if (isBlock(callback.body)) {
1482
- return fallbackType;
3238
+ return inferFunctionBodyReturnType(callback) ?? fallbackType;
1483
3239
  }
1484
3240
  const inferred = inferExpressionType(callback.body);
1485
3241
  return inferred ?? fallbackType;
@@ -1491,23 +3247,24 @@ function inferArrayCallbackReturnType(callback, elementType, fallbackType) {
1491
3247
  }
1492
3248
  return fallbackType;
1493
3249
  }
1494
- function buildArrayCallbackInfo(callback, elementType, forcedReturnType) {
3250
+ function buildArrayCallbackInfo(callback, elementType, forcedReturnType, paramTypes = [elementType, 'float64', `[]${elementType}`]) {
1495
3251
  if (isArrowFunction(callback) || isFunctionExpression(callback)) {
1496
- const paramCount = callback.parameters.length;
1497
- const callbackReturnType = forcedReturnType ?? inferArrayCallbackReturnType(callback, elementType, 'interface{}');
1498
- const params = [];
1499
- if (paramCount > 0) {
1500
- params.push(`${visit(callback.parameters[0].name)} ${elementType}`);
1501
- }
1502
- if (paramCount > 1) {
1503
- params.push(`${visit(callback.parameters[1].name)} float64`);
1504
- }
1505
- if (paramCount > 2) {
1506
- params.push(`${visit(callback.parameters[2].name)} []${elementType}`);
1507
- }
3252
+ const paramCount = (callback.parameters ?? []).length;
3253
+ (callback.parameters ?? []).slice(0, paramTypes.length).forEach((p, index) => {
3254
+ if (isIdentifier(p.name))
3255
+ registerLocalVariable(p.name.text, paramTypes[index]);
3256
+ });
3257
+ const callbackReturnType = forcedReturnType ?? inferArrayCallbackReturnType(callback, elementType, elementType);
3258
+ const params = (callback.parameters ?? [])
3259
+ .slice(0, paramTypes.length)
3260
+ .map((p, index) => `${visit(p.name)} ${paramTypes[index]}`);
1508
3261
  const body = isBlock(callback.body)
1509
3262
  ? visit(callback.body, { inline: true })
1510
- : `{ return ${visit(callback.body)}; }`;
3263
+ : forcedReturnType === 'bool'
3264
+ ? `{ return ${toGoCondition(callback.body)}; }`
3265
+ : forcedReturnType === ''
3266
+ ? `{ ${visit(unwrapParentheses(callback.body))}; }`
3267
+ : `{ return ${visit(callback.body)}; }`;
1511
3268
  return {
1512
3269
  fnExpr: `func(${params.join(', ')}) ${callbackReturnType} ${body}`,
1513
3270
  paramCount,
@@ -1528,7 +3285,12 @@ function buildArrayCallbackInvocation(callbackInfo, itemVar, indexVar, arrayVar)
1528
3285
  args.push(`float64(${indexVar})`);
1529
3286
  if (callbackInfo.paramCount > 2)
1530
3287
  args.push(arrayVar);
1531
- return `(${callbackInfo.fnExpr})(${args.join(', ')})`;
3288
+ const call = `(${callbackInfo.fnExpr})(${args.join(', ')})`;
3289
+ if (callbackInfo.paramCount > 0)
3290
+ return call;
3291
+ const returnType = callbackInfo.returnType ? ` ${callbackInfo.returnType}` : '';
3292
+ const ret = callbackInfo.returnType ? 'return ' : '';
3293
+ return `func()${returnType} { _ = ${itemVar}; ${ret}${call} }()`;
1532
3294
  }
1533
3295
  // Like buildArrayCallbackInvocation but for reduce: (acc, item, idx, arr)
1534
3296
  function buildArrayCallbackInvocationReduce(callbackInfo, accVar, itemVar, indexVar, arrayVar) {
@@ -1556,6 +3318,21 @@ function visitArrayHigherOrderCall(node) {
1556
3318
  const elementType = isArrayLikeGoType(ownerType)
1557
3319
  ? getArrayElementTypeFromGoType(ownerType)
1558
3320
  : 'interface{}';
3321
+ if (methodName === 'forEach' && ownerType?.startsWith('*TnMap[')) {
3322
+ const callback = (node.arguments ?? [])[0];
3323
+ if (!callback)
3324
+ return undefined;
3325
+ const keyType = extractMapKeyType(ownerType);
3326
+ const valueType = extractMapValueType(ownerType);
3327
+ const info = buildArrayCallbackInfo(callback, valueType, '', [
3328
+ valueType === 'struct{}' ? keyType : valueType,
3329
+ keyType
3330
+ ]);
3331
+ const key = getTempName('key');
3332
+ const value = getTempName('value');
3333
+ const args = [valueType === 'struct{}' ? key : value, key].slice(0, info.paramCount).join(', ');
3334
+ return `func() { for ${key}, ${value} := range ${arrayExpr}.All() { _, _ = ${key}, ${value}; (${info.fnExpr})(${args}) } }()`;
3335
+ }
1559
3336
  if (methodName === 'join') {
1560
3337
  // Only intercept array.join — if owner type is unknown/not array, fall through to callHandlers
1561
3338
  if (!isArrayLikeGoType(ownerType))
@@ -1575,7 +3352,7 @@ function visitArrayHigherOrderCall(node) {
1575
3352
  const idxVar = getTempName('i');
1576
3353
  const itemVar = getTempName('item');
1577
3354
  if (methodName === 'map') {
1578
- const callbackInfo = buildArrayCallbackInfo(callback, elementType, elementType);
3355
+ const callbackInfo = buildArrayCallbackInfo(callback, elementType);
1579
3356
  const mappedType = callbackInfo.returnType || 'interface{}';
1580
3357
  const resultVar = getTempName('mapres');
1581
3358
  const rangeIndexVar = callbackInfo.paramCount > 1 ? idxVar : '_';
@@ -1622,7 +3399,12 @@ function visitArrayHigherOrderCall(node) {
1622
3399
  : elementType;
1623
3400
  const accVar = getTempName('acc');
1624
3401
  // Build a callback that takes (acc, item, idx, arr) — param count determines what gets passed
1625
- const cbInfo = buildArrayCallbackInfo(reduceCallback, elementType, accType ?? 'interface{}');
3402
+ const cbInfo = buildArrayCallbackInfo(reduceCallback, elementType, accType, [
3403
+ accType,
3404
+ elementType,
3405
+ 'float64',
3406
+ `[]${elementType}`
3407
+ ]);
1626
3408
  const cbCall = buildArrayCallbackInvocationReduce(cbInfo, accVar, itemVar, idxVar, arrVar);
1627
3409
  // Only include the index loop var if the callback uses it (paramCount > 2)
1628
3410
  const reduceIdxVar = cbInfo.paramCount > 2 ? idxVar : '_';
@@ -1736,10 +3518,17 @@ function visitEnumDeclaration(node) {
1736
3518
  function getType(typeNode, getArrayType = false) {
1737
3519
  if (!typeNode)
1738
3520
  return ':';
3521
+ if (typeNode.kind === 'ParenthesizedType')
3522
+ return getType(typeNode.type, getArrayType);
1739
3523
  if (isArrayTypeNode(typeNode)) {
1740
3524
  const elementType = getType(typeNode.elementType);
1741
3525
  return getArrayType ? elementType : `[]${elementType}`;
1742
3526
  }
3527
+ // [A, B] tuples are slices: []A when all elements share a type, else []interface{}
3528
+ if (typeNode.kind === 'TupleType') {
3529
+ const elementType = getTupleElementType(typeNode);
3530
+ return getArrayType ? elementType : `[]${elementType}`;
3531
+ }
1743
3532
  // Handle union types (e.g. string | null, number | undefined)
1744
3533
  if (isUnionTypeNode(typeNode)) {
1745
3534
  const nonNullTypes = typeNode.types.filter((t) => t.kind !== 'NullKeyword' &&
@@ -1748,7 +3537,7 @@ function getType(typeNode, getArrayType = false) {
1748
3537
  if (nonNullTypes.length === 1 && nonNullTypes.length < typeNode.types.length) {
1749
3538
  // This is a nullable type T | null or T | undefined
1750
3539
  const innerType = getType(nonNullTypes[0]);
1751
- if (['float64', 'string', 'bool'].includes(innerType)) {
3540
+ if (['float64', 'string', 'bool'].includes(innerType) || isStructGoType(innerType)) {
1752
3541
  return `*${innerType}`;
1753
3542
  }
1754
3543
  // Pointer/interface types already support nil
@@ -1758,7 +3547,7 @@ function getType(typeNode, getArrayType = false) {
1758
3547
  return 'interface{}';
1759
3548
  }
1760
3549
  if (isFunctionTypeNode(typeNode)) {
1761
- const params = typeNode.parameters
3550
+ const params = (typeNode.parameters ?? [])
1762
3551
  .map((p) => (p.type ? getType(p.type) : 'interface{}'))
1763
3552
  .join(', ');
1764
3553
  const ret = typeNode.type ? ` ${getType(typeNode.type)}` : '';
@@ -1770,7 +3559,7 @@ function getType(typeNode, getArrayType = false) {
1770
3559
  return getSafeName(name);
1771
3560
  }
1772
3561
  const aliasType = getAliasType(name);
1773
- if (aliasType) {
3562
+ if (aliasType && aliasType.kind !== 'TypeLiteral') {
1774
3563
  return getType(aliasType, getArrayType);
1775
3564
  }
1776
3565
  if (name === 'Promise' && typeNode.typeArguments && typeNode.typeArguments.length > 0) {
@@ -1779,21 +3568,30 @@ function getType(typeNode, getArrayType = false) {
1779
3568
  if (name === 'RegExp') {
1780
3569
  return '*regexp.Regexp';
1781
3570
  }
1782
- if (name === 'Map' && typeNode.typeArguments && typeNode.typeArguments.length === 2) {
1783
- const keyType = getType(typeNode.typeArguments[0]);
1784
- const valueType = getType(typeNode.typeArguments[1]);
1785
- return `map[${keyType}]${valueType}`;
3571
+ if (name === 'RegExpExecArray' || name === 'RegExpMatchArray') {
3572
+ return '[]string';
3573
+ }
3574
+ if ((name === 'Map' || name === 'Record') &&
3575
+ typeNode.typeArguments &&
3576
+ typeNode.typeArguments.length === 2) {
3577
+ return mapGoType(getType(typeNode.typeArguments[0]), getType(typeNode.typeArguments[1]));
1786
3578
  }
1787
3579
  if (name === 'Set' && typeNode.typeArguments && typeNode.typeArguments.length === 1) {
1788
- const elementType = getType(typeNode.typeArguments[0]);
1789
- return `map[${elementType}]struct{}`;
3580
+ return mapGoType(getType(typeNode.typeArguments[0]), 'struct{}');
1790
3581
  }
1791
3582
  const typeArgs = getTypeArguments(typeNode.typeArguments);
1792
- if (classNames.has(name)) {
3583
+ if (classNames.has(name) || isObjectTypeName(name)) {
1793
3584
  return `*${name}${typeArgs}`;
1794
3585
  }
1795
3586
  return `${name}${typeArgs}`;
1796
3587
  }
3588
+ // { a: T; b?: U } → struct{ a T; b *U }
3589
+ if (typeNode.kind === 'TypeLiteral') {
3590
+ const fields = (typeNode.members ?? [])
3591
+ .filter((m) => isPropertySignature(m) && isIdentifier(m.name))
3592
+ .map((m) => `${goFieldName(m.name.text)} ${getOptionalNodeType(m.type, !!m.questionToken)}`);
3593
+ return fields.length > 0 ? `*struct{ ${fields.join('; ')} }` : 'interface{}';
3594
+ }
1797
3595
  // Syntactic replacement for the ts typechecker: render the type node's text
1798
3596
  let typeName = typeNodeToText(typeNode);
1799
3597
  const isArray = typeName.includes('[]');
@@ -1811,7 +3609,7 @@ function getType(typeNode, getArrayType = false) {
1811
3609
  case 'boolean':
1812
3610
  return 'bool';
1813
3611
  case 'any':
1814
- return ':';
3612
+ return 'interface{}';
1815
3613
  case 'void':
1816
3614
  return '';
1817
3615
  default:
@@ -1864,11 +3662,25 @@ function getClassNameFromTypeNode(typeNode) {
1864
3662
  }
1865
3663
  function resolveExpressionType(expr) {
1866
3664
  if (isIdentifier(expr)) {
1867
- return variableTypes.get(expr.text);
3665
+ return variableTypes.get(expr.text) ?? goTypeCategory(variableGoTypes.get(expr.text));
1868
3666
  }
1869
3667
  if (expr.kind === 'ThisKeyword') {
1870
3668
  return 'class';
1871
3669
  }
3670
+ return goTypeCategory(inferExpressionType(expr));
3671
+ }
3672
+ // Method-dispatch category of an inferred Go type (e.g. `const s = new Set<T>()`)
3673
+ function goTypeCategory(goType) {
3674
+ if (!goType)
3675
+ return undefined;
3676
+ if (goType.startsWith('*TnMap['))
3677
+ return extractMapValueType(goType) === 'struct{}' ? 'Set' : 'Map';
3678
+ if (goType.startsWith('[]'))
3679
+ return 'array';
3680
+ if (goType === 'string')
3681
+ return 'string';
3682
+ if (goType === '*regexp.Regexp')
3683
+ return 'RegExp';
1872
3684
  return undefined;
1873
3685
  }
1874
3686
  function isNilLiteral(node) {
@@ -1883,7 +3695,7 @@ function getAcessString(leftSide, rightSide, objectType) {
1883
3695
  return `float64(len(${leftSide}))`;
1884
3696
  }
1885
3697
  if (rightSide === 'size' && (objectType === 'Map' || objectType === 'Set')) {
1886
- return `float64(len(${leftSide}))`;
3698
+ return `float64(${leftSide}.Len())`;
1887
3699
  }
1888
3700
  // process global properties
1889
3701
  if (leftSide === 'process') {
@@ -1892,16 +3704,32 @@ function getAcessString(leftSide, rightSide, objectType) {
1892
3704
  return 'os.Args';
1893
3705
  }
1894
3706
  if (rightSide === 'platform') {
1895
- importedPackages.add('runtime');
1896
- return 'runtime.GOOS';
3707
+ // Node's names: win32 / darwin / linux
3708
+ useHelper('osPlatform');
3709
+ return 'TnOsPlatform()';
1897
3710
  }
1898
3711
  if (rightSide === 'env') {
1899
3712
  // process.env.X is detected via the nested access below
1900
3713
  return 'process.env';
1901
3714
  }
1902
3715
  }
3716
+ if (leftSide === 'Number') {
3717
+ const constants = {
3718
+ MAX_SAFE_INTEGER: 'float64(9007199254740991)',
3719
+ MIN_SAFE_INTEGER: 'float64(-9007199254740991)',
3720
+ EPSILON: 'float64(2.220446049250313e-16)'
3721
+ };
3722
+ if (constants[rightSide])
3723
+ return constants[rightSide];
3724
+ if (rightSide === 'MAX_VALUE' || rightSide === 'POSITIVE_INFINITY' || rightSide === 'NaN') {
3725
+ importedPackages.add('math');
3726
+ if (rightSide === 'MAX_VALUE')
3727
+ return 'math.MaxFloat64';
3728
+ return rightSide === 'NaN' ? 'math.NaN()' : 'math.Inf(1)';
3729
+ }
3730
+ }
1903
3731
  // process.env.X → TnGetenv("X") (os.Environ() is a []string in Go, not a map)
1904
- if (leftSide === 'process.env') {
3732
+ if (leftSide === 'process.env' || leftSide === '(process.env)') {
1905
3733
  useHelper('getenv');
1906
3734
  return `TnGetenv("${rightSide}")`;
1907
3735
  }
@@ -1945,13 +3773,20 @@ const callHandlers = {
1945
3773
  importedPackages.add('math');
1946
3774
  return `math.Abs(${args[0]})`;
1947
3775
  },
3776
+ // Math.max(...arr) → slices.Max(arr); otherwise Go's variadic builtin
1948
3777
  'Math.max': (_caller, args) => {
1949
- importedPackages.add('math');
1950
- return `math.Max(${args[0]}, ${args[1]})`;
3778
+ if (args.length === 1 && args[0].endsWith('...')) {
3779
+ importedPackages.add('slices');
3780
+ return `slices.Max(${args[0].slice(0, -3)})`;
3781
+ }
3782
+ return `max(${args.join(', ')})`;
1951
3783
  },
1952
3784
  'Math.min': (_caller, args) => {
1953
- importedPackages.add('math');
1954
- return `math.Min(${args[0]}, ${args[1]})`;
3785
+ if (args.length === 1 && args[0].endsWith('...')) {
3786
+ importedPackages.add('slices');
3787
+ return `slices.Min(${args[0].slice(0, -3)})`;
3788
+ }
3789
+ return `min(${args.join(', ')})`;
1955
3790
  },
1956
3791
  'Math.sqrt': (_caller, args) => {
1957
3792
  importedPackages.add('math');
@@ -1962,12 +3797,12 @@ const callHandlers = {
1962
3797
  return `math.Pow(${args[0]}, ${args[1]})`;
1963
3798
  },
1964
3799
  parseInt: (_caller, args) => {
1965
- importedPackages.add('strconv');
1966
- return `func() float64 { v, _ := strconv.Atoi(${args[0]}); return float64(v) }()`;
3800
+ useHelper('dynamic');
3801
+ return `TnParseInt(${args[0]}, ${args[1] ?? '10'})`;
1967
3802
  },
1968
3803
  parseFloat: (_caller, args) => {
1969
- importedPackages.add('strconv');
1970
- return `func() float64 { v, _ := strconv.ParseFloat(${args[0]}, 64); return v }()`;
3804
+ useHelper('dynamic');
3805
+ return `TnParseFloat(${args[0]})`;
1971
3806
  },
1972
3807
  'process.exit': (_caller, args) => {
1973
3808
  importedPackages.add('os');
@@ -1978,27 +3813,17 @@ const callHandlers = {
1978
3813
  return `func() string { __d, _ := os.Getwd(); return __d }()`;
1979
3814
  },
1980
3815
  'JSON.stringify': (_caller, args) => {
1981
- importedPackages.add('encoding/json');
1982
- if (args.length >= 3) {
1983
- return `func() string { __b, _ := json.MarshalIndent(${args[0]}, "", " "); return string(__b) }()`;
1984
- }
1985
- return `func() string { __b, _ := json.Marshal(${args[0]}); return string(__b) }()`;
3816
+ useHelper('jsonStringify');
3817
+ return `TnJSONStringify(${args[0]}, ${args.length >= 3 ? '" "' : '""'})`;
1986
3818
  },
1987
3819
  'JSON.parse': (_caller, args) => {
1988
3820
  importedPackages.add('encoding/json');
1989
3821
  return `func() interface{} { var __v interface{}; json.Unmarshal([]byte(${args[0]}), &__v); return __v }()`;
1990
3822
  },
1991
- 'Object.keys': (_caller, args) => {
1992
- importedPackages.add('maps');
1993
- return `func() []string { __k := make([]string, 0); for k := range ${args[0]} { __k = append(__k, k) }; return __k }()`;
1994
- },
1995
- 'Object.values': (_caller, args) => {
1996
- return `func() []interface{} { __v := make([]interface{}, 0); for _, v := range ${args[0]} { __v = append(__v, v) }; return __v }()`;
1997
- },
1998
- 'Object.entries': (_caller, args) => {
1999
- // When used outside for...of, produce a slice of [key, value] pairs — rarely needed
2000
- return args[0];
2001
- },
3823
+ 'Object.keys': (_caller, args) => `${args[0]}.Keys()`,
3824
+ 'Object.values': (_caller, args) => `${args[0]}.Values()`,
3825
+ // Outside for...of (which iterates the map directly): [key, value] pairs
3826
+ 'Object.entries': (_caller, args) => `${args[0]}.Entries()`,
2002
3827
  'Math.log': (_caller, args) => {
2003
3828
  importedPackages.add('math');
2004
3829
  return `math.Log(${args[0]})`;
@@ -2035,13 +3860,38 @@ const callHandlers = {
2035
3860
  importedPackages.add('os');
2036
3861
  return `fmt.Fprintln(os.Stderr, ${args.join(', ')})`;
2037
3862
  },
3863
+ 'console.warn': (_caller, args) => {
3864
+ importedPackages.add('fmt');
3865
+ importedPackages.add('os');
3866
+ return `fmt.Fprintln(os.Stderr, ${args.join(', ')})`;
3867
+ },
3868
+ 'Array.isArray': (_caller, args) => {
3869
+ importedPackages.add('reflect');
3870
+ return `(reflect.ValueOf(${args[0]}).Kind() == reflect.Slice)`;
3871
+ },
2038
3872
  'String': (_caller, args) => {
2039
3873
  importedPackages.add('fmt');
2040
3874
  return `fmt.Sprintf("%v", ${args[0]})`;
2041
3875
  },
2042
3876
  'Number': (_caller, args) => {
2043
- importedPackages.add('strconv');
2044
- return `func() float64 { v, _ := strconv.ParseFloat(fmt.Sprintf("%v", ${args[0]}), 64); return v }()`;
3877
+ useHelper('dynamic');
3878
+ return `TnNumber(${args[0]})`;
3879
+ },
3880
+ 'Number.isNaN': (_caller, args) => {
3881
+ importedPackages.add('math');
3882
+ return `math.IsNaN(${args[0]})`;
3883
+ },
3884
+ isNaN: (_caller, args) => {
3885
+ importedPackages.add('math');
3886
+ return `math.IsNaN(${args[0]})`;
3887
+ },
3888
+ 'Number.isFinite': (_caller, args) => {
3889
+ importedPackages.add('math');
3890
+ return `!math.IsInf(${args[0]}, 0) && !math.IsNaN(${args[0]})`;
3891
+ },
3892
+ 'Number.isInteger': (_caller, args) => {
3893
+ importedPackages.add('math');
3894
+ return `(${args[0]} == math.Trunc(${args[0]}) && !math.IsInf(${args[0]}, 0))`;
2045
3895
  },
2046
3896
  'Boolean': (_caller, args) => {
2047
3897
  return `(${args[0]} != nil && ${args[0]} != false && ${args[0]} != 0 && ${args[0]} != "")`;
@@ -2049,6 +3899,8 @@ const callHandlers = {
2049
3899
  };
2050
3900
  const stringMethodHandlers = {
2051
3901
  split: (obj, args) => {
3902
+ if (args[0]?.startsWith('regexp.MustCompile('))
3903
+ return `${args[0]}.Split(${obj}, -1)`;
2052
3904
  importedPackages.add('strings');
2053
3905
  return `strings.Split(${obj}, ${args[0]})`;
2054
3906
  },
@@ -2103,17 +3955,16 @@ const stringMethodHandlers = {
2103
3955
  charAt: (obj, args) => `string(${obj}[int(${args[0]})])`,
2104
3956
  substring: (obj, args) => {
2105
3957
  if (args.length >= 2)
2106
- return `${obj}[int(${args[0]}):int(${args[1]})]`;
2107
- return `${obj}[int(${args[0]}):]`;
3958
+ return `${obj}[${sliceIndex(obj, args[0])}:${sliceIndex(obj, args[1])}]`;
3959
+ return `${obj}[${sliceIndex(obj, args[0])}:]`;
2108
3960
  },
2109
3961
  slice: (obj, args) => {
2110
3962
  if (args.length >= 2)
2111
- return `${obj}[int(${args[0]}):int(${args[1]})]`;
2112
- return `${obj}[int(${args[0]}):]`;
3963
+ return `${obj}[${sliceIndex(obj, args[0])}:${sliceIndex(obj, args[1])}]`;
3964
+ return `${obj}[${sliceIndex(obj, args[0])}:]`;
2113
3965
  },
2114
3966
  concat: (obj, args) => `${obj} + ${args.join(' + ')}`,
2115
3967
  padStart: (obj, args) => {
2116
- importedPackages.add('fmt');
2117
3968
  importedPackages.add('strings');
2118
3969
  const pad = args[1] ?? '" "';
2119
3970
  return `func() string { __s := ${obj}; __n := int(${args[0]}) - len(__s); if __n > 0 { __s = strings.Repeat(${pad}, __n)[:__n] + __s }; return __s }()`;
@@ -2123,17 +3974,12 @@ const stringMethodHandlers = {
2123
3974
  const pad = args[1] ?? '" "';
2124
3975
  return `func() string { __s := ${obj}; __n := int(${args[0]}) - len(__s); if __n > 0 { __s = __s + strings.Repeat(${pad}, __n)[:__n] }; return __s }()`;
2125
3976
  },
2126
- match: (obj, args) => {
2127
- importedPackages.add('regexp');
2128
- return `regexp.MustCompile(${args[0]}).FindStringSubmatch(${obj})`;
2129
- },
2130
- matchAll: (obj, args) => {
2131
- importedPackages.add('regexp');
2132
- return `regexp.MustCompile(${args[0]}).FindAllStringSubmatch(${obj}, -1)`;
2133
- },
2134
- search: (obj, args) => {
2135
- importedPackages.add('regexp');
2136
- return `float64(regexp.MustCompile(${args[0]}).FindStringIndex(${obj})[0])`;
3977
+ match: (obj, args) => `${toGoRegexp(args[0])}.FindStringSubmatch(${obj})`,
3978
+ matchAll: (obj, args) => `${toGoRegexp(args[0])}.FindAllStringSubmatch(${obj}, -1)`,
3979
+ search: (obj, args) => `func() float64 { __loc := ${toGoRegexp(args[0])}.FindStringIndex(${obj}); if __loc == nil { return -1 }; return float64(__loc[0]) }()`,
3980
+ lastIndexOf: (obj, args) => {
3981
+ importedPackages.add('strings');
3982
+ return `float64(strings.LastIndex(${obj}, ${args[0]}))`;
2137
3983
  },
2138
3984
  at: (obj, args) => {
2139
3985
  return `func() string { __i := int(${args[0]}); if __i < 0 { __i = len(${obj}) + __i }; return string(${obj}[__i]) }()`;
@@ -2150,37 +3996,54 @@ const regexpMethodHandlers = {
2150
3996
  const arrayMethodHandlers = {
2151
3997
  push: (obj, args) => `${obj} = append(${obj}, ${args.join(', ')})`,
2152
3998
  pop: (obj) => {
2153
- return `func() interface{} { if len(${obj}) == 0 { return nil }; __last := ${obj}[len(${obj})-1]; ${obj} = ${obj}[:len(${obj})-1]; return __last }()`;
3999
+ const elementType = receiverElementType();
4000
+ if (!isAddressable(obj)) {
4001
+ return `func() ${elementType} { __s := ${obj}; if len(__s) == 0 { var __zero ${elementType}; return __zero }; return __s[len(__s)-1] }()`;
4002
+ }
4003
+ return `func() ${elementType} { if len(${obj}) == 0 { var __zero ${elementType}; return __zero }; __last := ${obj}[len(${obj})-1]; ${obj} = ${obj}[:len(${obj})-1]; return __last }()`;
2154
4004
  },
2155
4005
  shift: (obj) => {
2156
- return `func() interface{} { if len(${obj}) == 0 { return nil }; __first := ${obj}[0]; ${obj} = ${obj}[1:]; return __first }()`;
4006
+ const elementType = receiverElementType();
4007
+ if (!isAddressable(obj)) {
4008
+ return `func() ${elementType} { __s := ${obj}; if len(__s) == 0 { var __zero ${elementType}; return __zero }; return __s[0] }()`;
4009
+ }
4010
+ return `func() ${elementType} { if len(${obj}) == 0 { var __zero ${elementType}; return __zero }; __first := ${obj}[0]; ${obj} = ${obj}[1:]; return __first }()`;
2157
4011
  },
2158
- unshift: (obj, args) => `${obj} = append([]interface{}{${args.join(', ')}}, ${obj}...)`,
4012
+ unshift: (obj, args) => `${obj} = append([]${receiverElementType()}{${args.join(', ')}}, ${obj}...)`,
2159
4013
  join: (obj, args) => {
2160
4014
  importedPackages.add('strings');
2161
4015
  return `strings.Join(${obj}, ${args[0] ?? '""'})`;
2162
4016
  },
2163
4017
  slice: (obj, args) => {
2164
4018
  if (args.length >= 2)
2165
- return `${obj}[int(${args[0]}):int(${args[1]})]`;
2166
- return `${obj}[int(${args[0]}):]`;
4019
+ return `${obj}[${sliceIndex(obj, args[0])}:${sliceIndex(obj, args[1])}]`;
4020
+ return `${obj}[${sliceIndex(obj, args[0])}:]`;
2167
4021
  },
2168
4022
  reverse: (obj) => {
2169
4023
  importedPackages.add('slices');
2170
4024
  return `func() interface{} { slices.Reverse(${obj}); return nil }()`;
2171
4025
  },
2172
- sort: (obj) => {
4026
+ // Sorts in place and returns the array; without a comparator, compares as strings (like JS)
4027
+ sort: (obj, args) => {
2173
4028
  importedPackages.add('sort');
2174
- return `func() interface{} { sort.Slice(${obj}, func(i, j int) bool { return fmt.Sprintf("%v", ${obj}[i]) < fmt.Sprintf("%v", ${obj}[j]) }); return nil }()`;
2175
- },
2176
- indexOf: (obj, args) => {
2177
- return `func() float64 { for __i, __v := range ${obj} { if fmt.Sprintf("%v", __v) == fmt.Sprintf("%v", ${args[0]}) { return float64(__i) } }; return float64(-1) }()`;
2178
- },
2179
- includes: (obj, args) => {
2180
- return `func() bool { for _, __v := range ${obj} { if fmt.Sprintf("%v", __v) == fmt.Sprintf("%v", ${args[0]}) { return true } }; return false }()`;
4029
+ const arrayType = currentReceiverGoType?.startsWith('[]') ? currentReceiverGoType : '[]interface{}';
4030
+ let less = `(${args[0]})(__s[i], __s[j]) < 0`;
4031
+ if (!args[0]) {
4032
+ importedPackages.add('fmt');
4033
+ less = `fmt.Sprintf("%v", __s[i]) < fmt.Sprintf("%v", __s[j])`;
4034
+ }
4035
+ return `func() ${arrayType} { __s := ${obj}; sort.SliceStable(__s, func(i, j int) bool { return ${less} }); return __s }()`;
2181
4036
  },
4037
+ indexOf: (obj, args) => `func() float64 { __x := ${args[0]}; for __i, __v := range ${obj} { if ${elementEquals('__v', '__x')} { return float64(__i) } }; return float64(-1) }()`,
4038
+ includes: (obj, args) => `func() bool { __x := ${args[0]}; for _, __v := range ${obj} { if ${elementEquals('__v', '__x')} { return true } }; return false }()`,
2182
4039
  concat: (obj, args) => `append(${obj}, ${args.join(', ')}...)`,
2183
- flat: (obj) => obj,
4040
+ // [][]T → []T (one level, like JS's default depth)
4041
+ flat: (obj) => {
4042
+ const elementType = receiverElementType();
4043
+ if (!elementType.startsWith('[]'))
4044
+ return obj;
4045
+ return `func() ${elementType} { var __flat ${elementType}; for _, __inner := range ${obj} { __flat = append(__flat, __inner...) }; return __flat }()`;
4046
+ },
2184
4047
  toString: (obj) => {
2185
4048
  importedPackages.add('fmt');
2186
4049
  return `fmt.Sprintf("%v", ${obj})`;
@@ -2190,24 +4053,52 @@ const arrayMethodHandlers = {
2190
4053
  return `func() interface{} { __i := int(${args[0]}); if __i < 0 { __i = len(${obj}) + __i }; if __i < 0 || __i >= len(${obj}) { return nil }; return ${obj}[__i] }()`;
2191
4054
  },
2192
4055
  };
4056
+ function receiverElementType() {
4057
+ return currentReceiverGoType?.startsWith('[]') ? currentReceiverGoType.slice(2) : 'interface{}';
4058
+ }
4059
+ // Go expressions that can be assigned to (variables, fields, index expressions)
4060
+ function isAddressable(code) {
4061
+ return /^[\w.]+(\[[^\]]*\])*$/.test(code);
4062
+ }
4063
+ // A regex argument as a *regexp.Regexp: compiled literals pass through, pattern
4064
+ // strings are compiled
4065
+ function toGoRegexp(arg) {
4066
+ importedPackages.add('regexp');
4067
+ if (arg.startsWith('regexp.MustCompile(') || variableGoTypes.get(arg) === '*regexp.Regexp')
4068
+ return arg;
4069
+ return `regexp.MustCompile(${arg})`;
4070
+ }
4071
+ // A JS slice index: negative values count from the end
4072
+ // A JS slice index: negative values count from the end; out-of-range values clamp
4073
+ function sliceIndex(obj, index) {
4074
+ if (index.startsWith('-'))
4075
+ return `max(0, int(float64(len(${obj})) + ${index}))`;
4076
+ return `min(int(${index}), len(${obj}))`;
4077
+ }
4078
+ // Equality of two array elements of the current receiver's element type
4079
+ function elementEquals(a, b) {
4080
+ const elementType = receiverElementType();
4081
+ if (elementType === 'interface{}' || elementType.startsWith('*TnMap[') || elementType.startsWith('[]')) {
4082
+ useHelper('dynamic');
4083
+ return `TnSame(${a}, ${b})`;
4084
+ }
4085
+ return `${a} == ${b}`;
4086
+ }
2193
4087
  const mapMethodHandlers = {
2194
- set: (obj, args) => `${obj}[${args[0]}] = ${args[1]}`,
2195
- get: (obj, args) => `${obj}[${args[0]}]`,
2196
- has: (obj, args) => {
2197
- const tmp = getTempName('ok');
2198
- return `func() bool { _, ${tmp} := ${obj}[${args[0]}]; return ${tmp} }()`;
2199
- },
2200
- delete: (obj, args) => `delete(${obj}, ${args[0]})`,
2201
- clear: (obj) => `clear(${obj})`
4088
+ set: (obj, args) => `${obj}.Set(${args[0]}, ${args[1]})`,
4089
+ get: (obj, args) => `${obj}.Get(${args[0]})`,
4090
+ has: (obj, args) => `${obj}.Has(${args[0]})`,
4091
+ delete: (obj, args) => `${obj}.Delete(${args[0]})`,
4092
+ clear: (obj) => `${obj}.Clear()`,
4093
+ keys: (obj) => `${obj}.Keys()`,
4094
+ values: (obj) => `${obj}.Values()`
2202
4095
  };
2203
4096
  const setMethodHandlers = {
2204
- add: (obj, args) => `${obj}[${args[0]}] = struct{}{}`,
2205
- has: (obj, args) => {
2206
- const tmp = getTempName('ok');
2207
- return `func() bool { _, ${tmp} := ${obj}[${args[0]}]; return ${tmp} }()`;
2208
- },
2209
- delete: (obj, args) => `delete(${obj}, ${args[0]})`,
2210
- clear: (obj) => `clear(${obj})`
4097
+ add: (obj, args) => `${obj}.Set(${args[0]}, struct{}{})`,
4098
+ has: (obj, args) => `${obj}.Has(${args[0]})`,
4099
+ delete: (obj, args) => `${obj}.Delete(${args[0]})`,
4100
+ clear: (obj) => `${obj}.Clear()`,
4101
+ values: (obj) => `${obj}.Keys()`
2211
4102
  };
2212
4103
  function getDynamicCallHandler(caller, objectType) {
2213
4104
  if (promiseResolveName && caller === promiseResolveName) {
@@ -2218,10 +4109,20 @@ function getDynamicCallHandler(caller, objectType) {
2218
4109
  const methodName = caller.substring(dotIndex + 1);
2219
4110
  // toString() is universal — works for any type including numbers and objects
2220
4111
  if (methodName === 'toString') {
2221
- return (c) => {
4112
+ return (c, args) => {
2222
4113
  const obj = c.substring(0, dotIndex);
2223
- importedPackages.add('fmt');
2224
- return `fmt.Sprintf("%v", ${obj})`;
4114
+ if (args.length > 0) {
4115
+ importedPackages.add('strconv');
4116
+ return `strconv.FormatInt(int64(${obj}), int(${args[0]}))`;
4117
+ }
4118
+ return jsStringOf(obj, objectType === 'number' || objectType === 'float64' ? 'float64' : undefined);
4119
+ };
4120
+ }
4121
+ // number.toFixed(digits)
4122
+ if (methodName === 'toFixed') {
4123
+ return (c, args) => {
4124
+ importedPackages.add('strconv');
4125
+ return `strconv.FormatFloat(${c.substring(0, dotIndex)}, 'f', int(${args[0] ?? '0'}), 64)`;
2225
4126
  };
2226
4127
  }
2227
4128
  // Class instances use their own methods — never intercept
@@ -2290,6 +4191,177 @@ function getOperatorText(operator) {
2290
4191
  function getTimerName(name) {
2291
4192
  return `__timer_${name.replaceAll(' ', '_').replaceAll('"', '')}__`;
2292
4193
  }
4194
+ // typeof x: a constant when the Go type is known, a runtime check for any values
4195
+ function visitTypeOf(node) {
4196
+ const goType = inferExpressionType(node.expression);
4197
+ const known = { string: 'string', float64: 'number', bool: 'boolean', nil: 'undefined' };
4198
+ if (goType && known[goType])
4199
+ return toGoStringLiteral(known[goType]);
4200
+ if (goType?.startsWith('func'))
4201
+ return '"function"';
4202
+ if (goType && goType !== 'interface{}' && !NULLABLE_PRIMITIVE_TYPES.includes(goType))
4203
+ return '"object"';
4204
+ useHelper('typeOf');
4205
+ return `TnTypeOf(${visit(node.expression)})`;
4206
+ }
4207
+ // x.method(...) with an any receiver, as a call on the receiver cast to the type
4208
+ // that defines the method (array or string); undefined for other methods
4209
+ function castDynamicReceiverCall(node) {
4210
+ if (!isPropertyAccessExpression(node.expression) || isAsExpression(node.expression.expression)) {
4211
+ return undefined;
4212
+ }
4213
+ if (!isDynamicValue(node.expression.expression))
4214
+ return undefined;
4215
+ const method = node.expression.name.text;
4216
+ let castType;
4217
+ if (ARRAY_ONLY_METHODS.has(method))
4218
+ castType = { kind: 'ArrayType', elementType: { kind: 'AnyKeyword' } };
4219
+ else if (STRING_ONLY_METHODS.has(method))
4220
+ castType = { kind: 'StringKeyword' };
4221
+ if (!castType)
4222
+ return undefined;
4223
+ const cast = { kind: 'AsExpression', expression: node.expression.expression, type: castType };
4224
+ const access = { ...node.expression, expression: cast };
4225
+ const call = { ...node, expression: access };
4226
+ cast.parent = access;
4227
+ access.parent = call;
4228
+ return call;
4229
+ }
4230
+ function isObjectKeysOfDynamic(node) {
4231
+ return (isPropertyAccessExpression(node.expression) &&
4232
+ isIdentifier(node.expression.expression) &&
4233
+ node.expression.expression.text === 'Object' &&
4234
+ node.expression.name.text === 'keys' &&
4235
+ !!node.arguments?.[0] &&
4236
+ isDynamicValue(node.arguments[0]));
4237
+ }
4238
+ const ARRAY_ONLY_METHODS = new Set([
4239
+ 'map', 'filter', 'some', 'every', 'find', 'findIndex', 'forEach', 'reduce', 'join', 'flat', 'sort', 'reverse'
4240
+ ]);
4241
+ const STRING_ONLY_METHODS = new Set([
4242
+ 'startsWith', 'endsWith', 'trim', 'trimStart', 'trimEnd', 'toUpperCase', 'toLowerCase', 'split',
4243
+ 'replace', 'replaceAll', 'charAt', 'charCodeAt', 'padStart', 'padEnd', 'repeat', 'match', 'matchAll', 'search',
4244
+ 'substring', 'lastIndexOf'
4245
+ ]);
4246
+ // x.method(...) where x is any: re-visit with the receiver cast to the type the
4247
+ // method belongs to; methods of both strings and arrays dispatch at runtime
4248
+ function visitDynamicMethodCall(node) {
4249
+ if (isObjectKeysOfDynamic(node)) {
4250
+ useHelper('dynamic');
4251
+ return `TnKeys(${visit(node.arguments[0])})`;
4252
+ }
4253
+ if (!isPropertyAccessExpression(node.expression) || !isDynamicValue(node.expression.expression)) {
4254
+ return undefined;
4255
+ }
4256
+ const method = node.expression.name.text;
4257
+ const castCall = castDynamicReceiverCall(node);
4258
+ if (castCall)
4259
+ return visit(castCall);
4260
+ const dynamicHelpers = {
4261
+ includes: 'TnIncludes',
4262
+ indexOf: 'TnIndexOf',
4263
+ slice: 'TnSlice',
4264
+ concat: 'TnConcat'
4265
+ };
4266
+ const helper = dynamicHelpers[method];
4267
+ if (!helper)
4268
+ return undefined;
4269
+ useHelper('dynamic');
4270
+ const args = (node.arguments ?? []).map((a) => visit(a));
4271
+ return `${helper}(${[visit(node.expression.expression), ...args].join(', ')})`;
4272
+ }
4273
+ function isRegexReplaceCall(node) {
4274
+ if (!isCallExpression(node) || !isPropertyAccessExpression(node.expression))
4275
+ return false;
4276
+ const method = node.expression.name.text;
4277
+ if (method !== 'replace' && method !== 'replaceAll')
4278
+ return false;
4279
+ const pattern = (node.arguments ?? [])[0];
4280
+ if (!pattern || (node.arguments ?? []).length < 2)
4281
+ return false;
4282
+ return (isRegularExpressionLiteral(pattern) ||
4283
+ inferExpressionType(pattern) === '*regexp.Regexp' ||
4284
+ resolveExpressionType(pattern) === 'RegExp');
4285
+ }
4286
+ // Element type of the array whose .sort(fn) receives this comparator
4287
+ function getSortComparatorElementType(fn) {
4288
+ const call = fn.parent;
4289
+ if (!isCallExpression(call) || call.arguments?.[0] !== fn)
4290
+ return undefined;
4291
+ if (!isPropertyAccessExpression(call.expression) || call.expression.name.text !== 'sort') {
4292
+ return undefined;
4293
+ }
4294
+ const arrayType = inferExpressionType(call.expression.expression);
4295
+ return arrayType?.startsWith('[]') ? arrayType.slice(2) : undefined;
4296
+ }
4297
+ function isRegexReplacerCallback(fn) {
4298
+ if (!isArrowFunction(fn) && !isFunctionExpression(fn))
4299
+ return false;
4300
+ const call = fn.parent;
4301
+ return !!call && isRegexReplaceCall(call) && call.arguments[1] === fn;
4302
+ }
4303
+ // JS replacement patterns → Go: $& (whole match) → ${0}, $1 → ${1}
4304
+ function jsReplacementToGo(replacement) {
4305
+ if (!isStringLiteral(replacement) && !isNoSubstitutionTemplateLiteral(replacement)) {
4306
+ return visit(replacement);
4307
+ }
4308
+ const text = replacement.text;
4309
+ let goPattern = '';
4310
+ for (let i = 0; i < text.length; i++) {
4311
+ const ch = text[i];
4312
+ const next = i + 1 < text.length ? text[i + 1] : '';
4313
+ if (ch !== '$') {
4314
+ goPattern += ch;
4315
+ }
4316
+ else if (next === '$') {
4317
+ goPattern += '$$';
4318
+ i++;
4319
+ }
4320
+ else if (next === '&') {
4321
+ goPattern += '${0}';
4322
+ i++;
4323
+ }
4324
+ else if (next >= '0' && next <= '9') {
4325
+ let digits = next;
4326
+ i++;
4327
+ while (i + 1 < text.length && text[i + 1] >= '0' && text[i + 1] <= '9') {
4328
+ digits += text[i + 1];
4329
+ i++;
4330
+ }
4331
+ goPattern += '${' + digits + '}';
4332
+ }
4333
+ else {
4334
+ goPattern += '$$';
4335
+ }
4336
+ }
4337
+ return toGoStringLiteral(goPattern);
4338
+ }
4339
+ // str.replace(/re/g, x) → re.ReplaceAllString; without g only the first match is
4340
+ // replaced; a function replacer receives (match, ...groups)
4341
+ function visitRegexReplace(node) {
4342
+ if (!isRegexReplaceCall(node))
4343
+ return undefined;
4344
+ const [pattern, replacement] = node.arguments;
4345
+ const isGlobal = node.expression.name.text === 'replaceAll' ||
4346
+ (isRegularExpressionLiteral(pattern) &&
4347
+ pattern.text.substring(pattern.text.lastIndexOf('/') + 1).includes('g'));
4348
+ const target = toGoValueOfType(node.expression.expression, 'string');
4349
+ const re = visit(pattern);
4350
+ importedPackages.add('regexp');
4351
+ if (isArrowFunction(replacement) || isFunctionExpression(replacement)) {
4352
+ useHelper('regexReplaceFunc');
4353
+ const callback = visit(replacement);
4354
+ const groupArgs = (replacement.parameters ?? [])
4355
+ .map((_p, index) => `TnGroup(__m, ${index})`)
4356
+ .join(', ');
4357
+ return `TnRegexReplaceFunc(${re}, ${target}, func(__m []string) string { return (${callback})(${groupArgs}) }, ${isGlobal})`;
4358
+ }
4359
+ const goReplacement = jsReplacementToGo(replacement);
4360
+ if (isGlobal)
4361
+ return `${re}.ReplaceAllString(${target}, ${goReplacement})`;
4362
+ useHelper('regexReplaceFirst');
4363
+ return `TnRegexReplaceFirst(${re}, ${target}, ${goReplacement})`;
4364
+ }
2293
4365
  function jsRegexFlagsToGo(flags) {
2294
4366
  let goFlags = '';
2295
4367
  if (flags.includes('i'))
@@ -2336,7 +4408,9 @@ function getParameterGoType(param) {
2336
4408
  }
2337
4409
  if (param.type) {
2338
4410
  const explicitType = getType(param.type);
2339
- return explicitType === ':' ? 'interface{}' : explicitType;
4411
+ if (explicitType === ':')
4412
+ return 'interface{}';
4413
+ return param.questionToken ? makeNullableType(explicitType) : explicitType;
2340
4414
  }
2341
4415
  if (param.initializer) {
2342
4416
  const inferredType = inferExpressionType(param.initializer);
@@ -2344,29 +4418,174 @@ function getParameterGoType(param) {
2344
4418
  return inferredType;
2345
4419
  }
2346
4420
  }
4421
+ if (param.parent && isRegexReplacerCallback(param.parent))
4422
+ return 'string';
4423
+ const sortElementType = param.parent ? getSortComparatorElementType(param.parent) : undefined;
4424
+ if (sortElementType)
4425
+ return sortElementType;
4426
+ const contextualFn = param.parent ? getContextualFunctionType(param.parent) : undefined;
4427
+ if (contextualFn) {
4428
+ const index = (param.parent.parameters ?? []).indexOf(param);
4429
+ const contextualParamType = contextualFn.parameters?.[index]?.type;
4430
+ if (contextualParamType)
4431
+ return getType(contextualParamType);
4432
+ }
2347
4433
  return 'interface{}';
2348
4434
  }
4435
+ // Parameters are variables of the function body: record their types (replacing
4436
+ // whatever an earlier variable of the same name had)
4437
+ function registerParameterType(param) {
4438
+ if (!isIdentifier(param.name))
4439
+ return;
4440
+ const name = param.name.text;
4441
+ const goType = getParameterGoType(param);
4442
+ variableGoTypes.set(name, goType.startsWith('...') ? `[]${goType.slice(3)}` : goType);
4443
+ const category = param.type ? getTypeCategory(param.type) : undefined;
4444
+ if (category)
4445
+ variableTypes.set(name, category);
4446
+ else
4447
+ variableTypes.delete(name);
4448
+ const className = param.type ? getClassNameFromTypeNode(param.type) : undefined;
4449
+ if (className)
4450
+ variableClassNames.set(name, className);
4451
+ else
4452
+ variableClassNames.delete(name);
4453
+ narrowedVariables.delete(name);
4454
+ if (param.type)
4455
+ variableTypeNodes.set(name, param.type);
4456
+ else
4457
+ variableTypeNodes.delete(name);
4458
+ }
4459
+ // Constructor parameter properties: constructor(private x: T) declares field x
4460
+ function getParameterProperties(classNode) {
4461
+ const ctor = (classNode.members ?? []).find((m) => isConstructorDeclaration(m));
4462
+ return (ctor?.parameters ?? []).filter((p) => isIdentifier(p.name) && (p.modifiers ?? []).length > 0);
4463
+ }
4464
+ // Argument types of map.get/has/set/delete, set.add/has/delete, array.push
4465
+ function getCollectionArgumentTypes(node) {
4466
+ if (!isPropertyAccessExpression(node.expression))
4467
+ return undefined;
4468
+ const method = node.expression.name.text;
4469
+ const receiverType = inferExpressionType(node.expression.expression);
4470
+ if (receiverType?.startsWith('*TnMap[')) {
4471
+ const keyType = extractMapKeyType(receiverType);
4472
+ const valueType = extractMapValueType(receiverType);
4473
+ if (valueType === 'struct{}' && ['add', 'has', 'delete'].includes(method))
4474
+ return [keyType];
4475
+ if (['get', 'has', 'delete'].includes(method))
4476
+ return [keyType];
4477
+ if (method === 'set')
4478
+ return [keyType, valueType];
4479
+ }
4480
+ if (receiverType === '*regexp.Regexp' && (method === 'test' || method === 'exec'))
4481
+ return ['string'];
4482
+ if (receiverType?.startsWith('[]') && method === 'push') {
4483
+ return (node.arguments ?? []).map(() => receiverType.slice(2));
4484
+ }
4485
+ return undefined;
4486
+ }
4487
+ // TS lets a function literal omit trailing parameters of its contextual type;
4488
+ // Go func types must match exactly, so the omitted ones are added as unused `_`
4489
+ // Arguments of a call; for calls to declared functions, values are converted to
4490
+ // the parameter types (nullable primitives boxed) and omitted optional
4491
+ // parameters are passed as zero values (nil for pointers)
4492
+ function visitCallArguments(node) {
4493
+ const args = node.arguments ?? [];
4494
+ const collectionTypes = getCollectionArgumentTypes(node);
4495
+ if (collectionTypes)
4496
+ return args.map((arg, index) => toGoValueOfType(arg, collectionTypes[index]));
4497
+ let fn = isIdentifier(node.expression) ? declaredFunctions.get(node.expression.text) : undefined;
4498
+ if (!fn && isIdentifier(node.expression)) {
4499
+ const fnType = resolveTypeNode(variableTypeNodes.get(node.expression.text));
4500
+ if (isFunctionTypeNode(fnType))
4501
+ fn = fnType;
4502
+ }
4503
+ if (!fn)
4504
+ return args.map((a) => visit(a));
4505
+ const params = fn.parameters ?? [];
4506
+ const result = args.map((arg, index) => {
4507
+ const param = params[index];
4508
+ return param && !param.dotDotDotToken ? toGoValueOfType(arg, getParameterGoType(param)) : visit(arg);
4509
+ });
4510
+ for (let i = args.length; i < params.length; i++) {
4511
+ const param = params[i];
4512
+ if (!param.questionToken || param.initializer || param.dotDotDotToken)
4513
+ break;
4514
+ result.push(`*new(${getParameterGoType(param)})`);
4515
+ }
4516
+ return result;
4517
+ }
4518
+ function withContextualParameters(fn, info) {
4519
+ const contextualFn = getContextualFunctionType(fn);
4520
+ const declaredCount = (fn.parameters ?? []).length;
4521
+ const contextualParams = contextualFn?.parameters ?? [];
4522
+ if (contextualParams.length <= declaredCount || info.signature.includes('...'))
4523
+ return info;
4524
+ const extra = contextualParams
4525
+ .slice(declaredCount)
4526
+ .map((p) => `_ ${p.dotDotDotToken ? '...' : ''}${p.type ? getType(p.type) : 'interface{}'}`);
4527
+ const signature = [info.signature, ...extra].filter((part) => part).join(', ');
4528
+ return { ...info, signature };
4529
+ }
4530
+ // Destructured parameters ({ a, b }: T / [x, y]: T[]) get a temporary name
4531
+ function getParameterName(param, parameters) {
4532
+ if (isIdentifier(param.name))
4533
+ return visit(param.name);
4534
+ return `__param${parameters.indexOf(param)}`;
4535
+ }
4536
+ // Unpacks destructured parameters at the start of the function body
4537
+ function getParameterDestructuring(parameters) {
4538
+ let code = '';
4539
+ for (const param of parameters) {
4540
+ if (isIdentifier(param.name))
4541
+ continue;
4542
+ const source = getParameterName(param, parameters);
4543
+ const paramType = getParameterGoType(param);
4544
+ (param.name.elements ?? []).forEach((el, index) => {
4545
+ if (isOmittedExpression(el) || !isIdentifier(el.name))
4546
+ return;
4547
+ let value;
4548
+ let valueType;
4549
+ if (isObjectBindingPattern(param.name)) {
4550
+ const field = (el.propertyName ?? el.name).text;
4551
+ value = `${source}.${goFieldName(field)}`;
4552
+ valueType =
4553
+ interfacePropertyTypes.get(paramType)?.get(field) ?? getStructFieldGoType(paramType, field);
4554
+ }
4555
+ else {
4556
+ value = `${source}[${index}]`;
4557
+ valueType = paramType.startsWith('[]') ? paramType.slice(2) : undefined;
4558
+ }
4559
+ registerLocalVariable(el.name.text, valueType);
4560
+ code += `${visit(el.name)} := ${value}\n\t\t_ = ${visit(el.name)}\n\t\t`;
4561
+ });
4562
+ }
4563
+ return code;
4564
+ }
2349
4565
  function getFunctionParametersInfo(parameters) {
4566
+ for (const param of parameters)
4567
+ registerParameterType(param);
2350
4568
  if (parameters.length === 0) {
2351
4569
  return { signature: '', prefixBlockContent: '' };
2352
4570
  }
4571
+ const destructuring = getParameterDestructuring(parameters);
2353
4572
  const firstDefaultIndex = parameters.findIndex((p) => !!p.initializer);
2354
4573
  if (firstDefaultIndex === -1) {
2355
4574
  return {
2356
- signature: parameters.map((p) => `${visit(p.name)} ${getParameterGoType(p)}`).join(', '),
2357
- prefixBlockContent: ''
4575
+ signature: parameters.map((p) => `${getParameterName(p, parameters)} ${getParameterGoType(p)}`).join(', '),
4576
+ prefixBlockContent: destructuring
2358
4577
  };
2359
4578
  }
2360
4579
  const hasRequiredAfterDefault = parameters.slice(firstDefaultIndex).some((p) => !p.initializer);
2361
4580
  if (hasRequiredAfterDefault) {
2362
4581
  return {
2363
- signature: parameters.map((p) => `${visit(p.name)} ${getParameterGoType(p)}`).join(', '),
2364
- prefixBlockContent: ''
4582
+ signature: parameters.map((p) => `${getParameterName(p, parameters)} ${getParameterGoType(p)}`).join(', '),
4583
+ prefixBlockContent: destructuring
2365
4584
  };
2366
4585
  }
2367
4586
  const requiredParams = parameters.slice(0, firstDefaultIndex);
2368
4587
  const defaultedParams = parameters.slice(firstDefaultIndex);
2369
- const signatureParts = requiredParams.map((p) => `${visit(p.name)} ${getParameterGoType(p)}`);
4588
+ const signatureParts = requiredParams.map((p) => `${getParameterName(p, parameters)} ${getParameterGoType(p)}`);
2370
4589
  signatureParts.push('__defaultArgs ...interface{}');
2371
4590
  const prefixBlockContent = defaultedParams
2372
4591
  .map((param, index) => {
@@ -2381,7 +4600,7 @@ function getFunctionParametersInfo(parameters) {
2381
4600
  .join('');
2382
4601
  return {
2383
4602
  signature: signatureParts.join(', '),
2384
- prefixBlockContent
4603
+ prefixBlockContent: prefixBlockContent + destructuring
2385
4604
  };
2386
4605
  }
2387
4606
  function getSafeName(name) {
@@ -2444,7 +4663,9 @@ function visitNewPromise(node) {
2444
4663
  return `func() chan ${channelType} {\n\t\tch := make(chan ${channelType})\n\t\tgo func() ${body.trimEnd()}()\n\t\treturn ch;\n\t}()`;
2445
4664
  }
2446
4665
  function extractMapValueType(mapType) {
2447
- // mapType is "map[K]V" — find the closing bracket of K accounting for nesting
4666
+ const args = splitMapTypeArgs(mapType);
4667
+ if (args)
4668
+ return args[1];
2448
4669
  if (!mapType.startsWith('map['))
2449
4670
  return 'interface{}';
2450
4671
  let depth = 0;
@@ -2459,43 +4680,43 @@ function extractMapValueType(mapType) {
2459
4680
  }
2460
4681
  return 'interface{}';
2461
4682
  }
4683
+ // Type arguments of new Map/Set: explicit, else from the declared/contextual type
4684
+ function getCollectionTypeArguments(node) {
4685
+ if ((node.typeArguments ?? []).length > 0)
4686
+ return node.typeArguments;
4687
+ const contextual = resolveTypeNode(getContextualTypeNode(node));
4688
+ return isTypeReferenceNode(contextual) ? (contextual.typeArguments ?? []) : [];
4689
+ }
2462
4690
  function visitNewMap(node) {
2463
4691
  let keyType = 'interface{}';
2464
4692
  let valueType = 'interface{}';
2465
- if ((node.typeArguments ?? []) && (node.typeArguments ?? []).length === 2) {
2466
- keyType = getType((node.typeArguments ?? [])[0]);
2467
- valueType = getType((node.typeArguments ?? [])[1]);
2468
- }
2469
- const mapType = `map[${keyType}]${valueType}`;
2470
- const args = (node.arguments ?? []);
2471
- if (!args || args.length === 0 || !isArrayLiteralExpression(args[0])) {
2472
- return `make(${mapType})`;
2473
- }
2474
- const initArg = args[0];
2475
- const tmp = getTempName('map');
2476
- const entries = initArg.elements
2477
- .filter((el) => isArrayLiteralExpression(el) && el.elements.length >= 2)
2478
- .map((el) => {
2479
- const pair = el;
2480
- return `${tmp}[${visit(pair.elements[0])}] = ${visit(pair.elements[1])}`;
2481
- })
2482
- .join('; ');
2483
- return `func() ${mapType} { ${tmp} := make(${mapType}); ${entries}; return ${tmp} }()`;
4693
+ const typeArguments = getCollectionTypeArguments(node);
4694
+ if (typeArguments.length === 2) {
4695
+ keyType = getType(typeArguments[0]);
4696
+ valueType = getType(typeArguments[1]);
4697
+ }
4698
+ const args = node.arguments ?? [];
4699
+ const entries = args.length > 0 && isArrayLiteralExpression(args[0])
4700
+ ? args[0].elements
4701
+ .filter((el) => isArrayLiteralExpression(el) && el.elements.length >= 2)
4702
+ .map((pair) => `${visit(pair.elements[0])}, ${visit(pair.elements[1])}`)
4703
+ : [];
4704
+ return orderedMapLiteral(keyType, valueType, entries);
2484
4705
  }
2485
4706
  function visitNewSet(node) {
2486
4707
  let elementType = 'interface{}';
2487
- if ((node.typeArguments ?? []) && (node.typeArguments ?? []).length === 1) {
2488
- elementType = getType((node.typeArguments ?? [])[0]);
4708
+ const typeArguments = getCollectionTypeArguments(node);
4709
+ if (typeArguments.length === 1) {
4710
+ elementType = getType(typeArguments[0]);
2489
4711
  }
2490
- const setType = `map[${elementType}]struct{}`;
2491
- const args = (node.arguments ?? []);
2492
- if (!args || args.length === 0 || !isArrayLiteralExpression(args[0])) {
2493
- return `make(${setType})`;
4712
+ else if (node.arguments?.length && isArrayLiteralExpression(node.arguments[0])) {
4713
+ elementType = getArrayLiteralElementType(node.arguments[0]);
2494
4714
  }
2495
- const initArg = args[0];
2496
- const tmp = getTempName('set');
2497
- const values = initArg.elements.map((el) => `${tmp}[${visit(el)}] = struct{}{}`).join('; ');
2498
- return `func() ${setType} { ${tmp} := make(${setType}); ${values}; return ${tmp} }()`;
4715
+ const args = node.arguments ?? [];
4716
+ const values = args.length > 0 && isArrayLiteralExpression(args[0])
4717
+ ? args[0].elements.map((el) => `${visit(el)}, struct{}{}`)
4718
+ : [];
4719
+ return orderedMapLiteral(elementType, 'struct{}', values);
2499
4720
  }
2500
4721
  function specifierToGoFileName(specifier) {
2501
4722
  const segments = specifier.split(/[/\\]/);
@@ -2530,7 +4751,7 @@ function includeLocalImport(code, dir, goFileName) {
2530
4751
  }
2531
4752
  if (!goFileName) {
2532
4753
  // Inline mode (npm packages): pull declarations into the current transpilation context
2533
- const sf = parseFunction(code);
4754
+ const sf = parseSource(code);
2534
4755
  for (const stmt of sf.statements) {
2535
4756
  visit(stmt, { addFunctionOutside: true });
2536
4757
  }
@@ -2545,13 +4766,16 @@ function includeLocalImport(code, dir, goFileName) {
2545
4766
  outsideNodes = [];
2546
4767
  importedPackages.clear();
2547
4768
  helperProvidedPackages.clear();
2548
- const sf = parseFunction(code);
4769
+ const sf = parseSource(code);
2549
4770
  const inlineLines = [];
4771
+ const prevEmittingModuleFile = emittingModuleFile;
4772
+ emittingModuleFile = true;
2550
4773
  for (const stmt of sf.statements) {
2551
4774
  const result = visit(stmt, { addFunctionOutside: true });
2552
4775
  if (result.trim())
2553
4776
  inlineLines.push(result);
2554
4777
  }
4778
+ emittingModuleFile = prevEmittingModuleFile;
2555
4779
  const fileOutside = outsideNodes.map((n) => visit(n, { isOutside: true })).join('\n');
2556
4780
  const fileInline = inlineLines.join('\n');
2557
4781
  // Computed after all visits: function bodies (outside nodes) register
@@ -2565,8 +4789,9 @@ function includeLocalImport(code, dir, goFileName) {
2565
4789
  if (!helperProvidedPackages.has(pkg))
2566
4790
  return true;
2567
4791
  const name = pkg.split('/').pop();
2568
- return fileCode.includes(`${name}.`);
4792
+ return stripGoStrings(fileCode).includes(`${name}.`);
2569
4793
  })
4794
+ .sort()
2570
4795
  .map((pkg) => `import "${pkg}"`)
2571
4796
  .join('\n');
2572
4797
  const parts = ['package main'];
@@ -2587,6 +4812,10 @@ function includeLocalImport(code, dir, goFileName) {
2587
4812
  importedPackages.add(p);
2588
4813
  currentFileDir = prevDir;
2589
4814
  }
4815
+ // Go source without its string literals (for detecting real package use)
4816
+ function stripGoStrings(code) {
4817
+ return code.replace(/"(?:[^"\\\n]|\\.)*"|`[^`]*`/g, '""');
4818
+ }
2590
4819
  function registerGoPackageAliases(node, goPkg) {
2591
4820
  const pkgName = goPkg.split('/').pop();
2592
4821
  if (!node.importClause)
@@ -2710,6 +4939,10 @@ const nodeModuleMappings = {
2710
4939
  rmSync: (args) => {
2711
4940
  useHelper('removeAll');
2712
4941
  return `TnRemoveAll(${args[0]})`;
4942
+ },
4943
+ unlinkSync: (args) => {
4944
+ useHelper('removeAll');
4945
+ return `TnRemoveAll(${args[0]})`;
2713
4946
  }
2714
4947
  }
2715
4948
  },
@@ -2760,15 +4993,15 @@ const nodeModuleMappings = {
2760
4993
  spawnSync: (args) => {
2761
4994
  useHelper('exec');
2762
4995
  useHelper('runInherit');
2763
- if (args[2]?.includes('inherit')) {
2764
- return `TnRunInherit(${args[0]})`;
2765
- }
2766
4996
  // Untyped array literals visit as `[] {…}` — give them the []string
2767
4997
  // type the variadic helper needs (`[]string{…}...` is valid Go spread)
2768
4998
  const arrArg = args[1] ?? '[]string{}';
2769
4999
  const typedArg = arrArg.startsWith('[]') && !arrArg.startsWith('[]string')
2770
- ? `[]string ${arrArg.slice(2)}`
5000
+ ? arrArg.replace(/^\[\]\S* \{/, '[]string{')
2771
5001
  : arrArg;
5002
+ if (args[2]?.includes('inherit')) {
5003
+ return `TnRunInherit(${args[0]}, ${typedArg}...)`;
5004
+ }
2772
5005
  if (args[2]?.includes('input')) {
2773
5006
  // {input: expr, ...} → run with expr as stdin
2774
5007
  useHelper('execInput');
@@ -2792,6 +5025,12 @@ const nodeModuleMappings = {
2792
5025
  };
2793
5026
  // Go packages required by each helper, registered when the helper is used.
2794
5027
  const helperPackages = {
5028
+ regexReplaceFirst: ['regexp'],
5029
+ orderedMap: ['iter', 'encoding/json', 'bytes', 'fmt'],
5030
+ jsonStringify: ['encoding/json', 'bytes', 'strings'],
5031
+ typeOf: ['reflect'],
5032
+ dynamic: ['math', 'strings', 'fmt', 'sort', 'reflect', 'strconv', 'regexp'],
5033
+ regexReplaceFunc: ['regexp'],
2795
5034
  readFile: ['os'],
2796
5035
  writeFile: ['os'],
2797
5036
  appendFile: ['os'],
@@ -2813,6 +5052,535 @@ const helperPackages = {
2813
5052
  // Field names are intentionally lowercase: every generated file is `package main`,
2814
5053
  // so `result.stdout` in emitted code resolves to the struct field directly.
2815
5054
  const goHelpers = {
5055
+ dynamic: `// Dynamic (any) values: JSON-like objects are map[string]interface{},
5056
+ // arrays []interface{}, numbers float64
5057
+ func TnGet(obj interface{}, key string) interface{} {
5058
+ switch o := obj.(type) {
5059
+ case map[string]interface{}:
5060
+ return o[key]
5061
+ case []interface{}:
5062
+ if key == "length" {
5063
+ return float64(len(o))
5064
+ }
5065
+ case string:
5066
+ if key == "length" {
5067
+ return float64(len(o))
5068
+ }
5069
+ }
5070
+ return nil
5071
+ }
5072
+
5073
+ func TnSet(obj interface{}, key string, value interface{}) {
5074
+ if o, ok := obj.(map[string]interface{}); ok {
5075
+ o[key] = value
5076
+ return
5077
+ }
5078
+ panic("TnSet: cannot set property " + key)
5079
+ }
5080
+
5081
+ func TnIndex(obj interface{}, key interface{}) interface{} {
5082
+ switch o := obj.(type) {
5083
+ case map[string]interface{}:
5084
+ if k, ok := key.(string); ok {
5085
+ return o[k]
5086
+ }
5087
+ case []interface{}:
5088
+ if i, ok := key.(float64); ok && i >= 0 && int(i) < len(o) {
5089
+ return o[int(i)]
5090
+ }
5091
+ case string:
5092
+ if i, ok := key.(float64); ok && i >= 0 && int(i) < len(o) {
5093
+ return string(o[int(i)])
5094
+ }
5095
+ }
5096
+ return nil
5097
+ }
5098
+
5099
+ // JS ===: objects and arrays compare by identity, other values by value
5100
+ func TnSame(a interface{}, b interface{}) bool {
5101
+ if a == nil || b == nil {
5102
+ return a == nil && b == nil
5103
+ }
5104
+ va, vb := reflect.ValueOf(a), reflect.ValueOf(b)
5105
+ switch va.Kind() {
5106
+ case reflect.Map, reflect.Slice, reflect.Func, reflect.Pointer:
5107
+ return va.Kind() == vb.Kind() && va.Type() == vb.Type() && va.Pointer() == vb.Pointer() &&
5108
+ (va.Kind() != reflect.Slice || va.Len() == vb.Len())
5109
+ }
5110
+ if !va.Type().Comparable() || !vb.Type().Comparable() {
5111
+ return false
5112
+ }
5113
+ return a == b
5114
+ }
5115
+
5116
+ func TnAt[T any](s []T, i float64) T {
5117
+ if i < 0 || int(i) >= len(s) {
5118
+ var zero T
5119
+ return zero
5120
+ }
5121
+ return s[int(i)]
5122
+ }
5123
+
5124
+ func TnCharAt(s string, i float64) string {
5125
+ if i < 0 || int(i) >= len(s) {
5126
+ return ""
5127
+ }
5128
+ return string(s[int(i)])
5129
+ }
5130
+
5131
+ func TnLength(v interface{}) float64 {
5132
+ switch t := v.(type) {
5133
+ case string:
5134
+ return float64(len(t))
5135
+ case []interface{}:
5136
+ return float64(len(t))
5137
+ }
5138
+ return 0
5139
+ }
5140
+
5141
+ // A value as JS prints it in a template literal (nil pointers are "undefined")
5142
+ func TnFormat(v interface{}) string {
5143
+ rv := reflect.ValueOf(v)
5144
+ if !rv.IsValid() || (rv.Kind() == reflect.Pointer && rv.IsNil()) {
5145
+ return "undefined"
5146
+ }
5147
+ if rv.Kind() == reflect.Pointer {
5148
+ return fmt.Sprintf("%v", rv.Elem().Interface())
5149
+ }
5150
+ return fmt.Sprintf("%v", v)
5151
+ }
5152
+
5153
+ // Object spread: copies the properties of src into dst
5154
+ func TnAssign(dst map[string]interface{}, src interface{}) {
5155
+ if o, ok := src.(map[string]interface{}); ok {
5156
+ for k, v := range o {
5157
+ dst[k] = v
5158
+ }
5159
+ }
5160
+ }
5161
+
5162
+ // A number as JS prints it
5163
+ func TnNumStr(f float64) string {
5164
+ switch {
5165
+ case math.IsNaN(f):
5166
+ return "NaN"
5167
+ case math.IsInf(f, 1):
5168
+ return "Infinity"
5169
+ case math.IsInf(f, -1):
5170
+ return "-Infinity"
5171
+ case f == math.Trunc(f) && math.Abs(f) < 1e21:
5172
+ return strconv.FormatFloat(f, 'f', -1, 64)
5173
+ }
5174
+ return strconv.FormatFloat(f, 'g', -1, 64)
5175
+ }
5176
+
5177
+ // Number(x) like JS: "" → 0, invalid → NaN, booleans → 0/1
5178
+ func TnNumber(v interface{}) float64 {
5179
+ switch t := v.(type) {
5180
+ case float64:
5181
+ return t
5182
+ case bool:
5183
+ if t {
5184
+ return 1
5185
+ }
5186
+ return 0
5187
+ case string:
5188
+ s := strings.TrimSpace(t)
5189
+ if s == "" {
5190
+ return 0
5191
+ }
5192
+ if f, err := strconv.ParseFloat(s, 64); err == nil {
5193
+ return f
5194
+ }
5195
+ case nil:
5196
+ return 0
5197
+ }
5198
+ return math.NaN()
5199
+ }
5200
+
5201
+ var tnLeadingFloat = regexp.MustCompile("^[+-]?(Infinity|[0-9]+[.]?[0-9]*([eE][+-]?[0-9]+)?|[.][0-9]+([eE][+-]?[0-9]+)?)")
5202
+
5203
+ // parseFloat: the leading number of the string, NaN if none
5204
+ func TnParseFloat(s string) float64 {
5205
+ m := tnLeadingFloat.FindString(strings.TrimSpace(s))
5206
+ if m == "" {
5207
+ return math.NaN()
5208
+ }
5209
+ if strings.HasSuffix(m, "Infinity") {
5210
+ if strings.HasPrefix(m, "-") {
5211
+ return math.Inf(-1)
5212
+ }
5213
+ return math.Inf(1)
5214
+ }
5215
+ f, _ := strconv.ParseFloat(m, 64)
5216
+ return f
5217
+ }
5218
+
5219
+ // parseInt: the leading integer in the given radix, NaN if none
5220
+ func TnParseInt(s string, radix float64) float64 {
5221
+ s = strings.TrimSpace(s)
5222
+ base := int(radix)
5223
+ sign := 1.0
5224
+ if strings.HasPrefix(s, "-") || strings.HasPrefix(s, "+") {
5225
+ if s[0] == '-' {
5226
+ sign = -1
5227
+ }
5228
+ s = s[1:]
5229
+ }
5230
+ if (base == 16 || base == 0) && (strings.HasPrefix(s, "0x") || strings.HasPrefix(s, "0X")) {
5231
+ s, base = s[2:], 16
5232
+ }
5233
+ if base == 0 {
5234
+ base = 10
5235
+ }
5236
+ end := 0
5237
+ for end < len(s) {
5238
+ c := s[end]
5239
+ d := 99
5240
+ switch {
5241
+ case c >= '0' && c <= '9':
5242
+ d = int(c - '0')
5243
+ case c >= 'a' && c <= 'z':
5244
+ d = int(c-'a') + 10
5245
+ case c >= 'A' && c <= 'Z':
5246
+ d = int(c-'A') + 10
5247
+ }
5248
+ if d >= base {
5249
+ break
5250
+ }
5251
+ end++
5252
+ }
5253
+ if end == 0 {
5254
+ return math.NaN()
5255
+ }
5256
+ v, _ := strconv.ParseInt(s[:end], base, 64)
5257
+ return sign * float64(v)
5258
+ }
5259
+
5260
+ // JS ToInt32 / ToUint32 (wrapping modulo 2^32; NaN and Infinity → 0)
5261
+ func TnInt32(f float64) int32 {
5262
+ if math.IsNaN(f) || math.IsInf(f, 0) {
5263
+ return 0
5264
+ }
5265
+ return int32(int64(math.Trunc(math.Mod(f, 4294967296))))
5266
+ }
5267
+
5268
+ func TnUint32(f float64) uint32 { return uint32(TnInt32(f)) }
5269
+
5270
+ func TnDelete(obj interface{}, key interface{}) bool {
5271
+ o, ok := obj.(map[string]interface{})
5272
+ k, isString := key.(string)
5273
+ if !ok || !isString {
5274
+ return false
5275
+ }
5276
+ delete(o, k)
5277
+ return true
5278
+ }
5279
+
5280
+ func TnHas(obj interface{}, key interface{}) bool {
5281
+ if o, ok := obj.(map[string]interface{}); ok {
5282
+ if k, ok := key.(string); ok {
5283
+ _, found := o[k]
5284
+ return found
5285
+ }
5286
+ }
5287
+ return false
5288
+ }
5289
+
5290
+ func TnKeys(obj interface{}) []string {
5291
+ o, _ := obj.(map[string]interface{})
5292
+ keys := make([]string, 0, len(o))
5293
+ for k := range o {
5294
+ keys = append(keys, k)
5295
+ }
5296
+ sort.Strings(keys)
5297
+ return keys
5298
+ }
5299
+
5300
+ func TnIncludes(v interface{}, x interface{}) bool {
5301
+ return TnIndexOf(v, x) >= 0
5302
+ }
5303
+
5304
+ func TnIndexOf(v interface{}, x interface{}) float64 {
5305
+ switch t := v.(type) {
5306
+ case string:
5307
+ if s, ok := x.(string); ok {
5308
+ return float64(strings.Index(t, s))
5309
+ }
5310
+ case []interface{}:
5311
+ for i, item := range t {
5312
+ if TnSame(item, x) {
5313
+ return float64(i)
5314
+ }
5315
+ }
5316
+ }
5317
+ return -1
5318
+ }
5319
+
5320
+ func TnSlice(v interface{}, bounds ...float64) interface{} {
5321
+ length := 0
5322
+ switch t := v.(type) {
5323
+ case string:
5324
+ length = len(t)
5325
+ case []interface{}:
5326
+ length = len(t)
5327
+ default:
5328
+ return nil
5329
+ }
5330
+ clamp := func(i float64) int {
5331
+ n := int(i)
5332
+ if n < 0 {
5333
+ n += length
5334
+ }
5335
+ return max(0, min(n, length))
5336
+ }
5337
+ start, end := 0, length
5338
+ if len(bounds) > 0 {
5339
+ start = clamp(bounds[0])
5340
+ }
5341
+ if len(bounds) > 1 {
5342
+ end = clamp(bounds[1])
5343
+ }
5344
+ if start > end {
5345
+ start = end
5346
+ }
5347
+ if s, ok := v.(string); ok {
5348
+ return s[start:end]
5349
+ }
5350
+ return v.([]interface{})[start:end]
5351
+ }
5352
+
5353
+ func TnConcat(v interface{}, others ...interface{}) interface{} {
5354
+ if s, ok := v.(string); ok {
5355
+ for _, o := range others {
5356
+ s += fmt.Sprint(o)
5357
+ }
5358
+ return s
5359
+ }
5360
+ result := append([]interface{}{}, TnAs[[]interface{}](v)...)
5361
+ for _, o := range others {
5362
+ if items, ok := o.([]interface{}); ok {
5363
+ result = append(result, items...)
5364
+ } else {
5365
+ result = append(result, o)
5366
+ }
5367
+ }
5368
+ return result
5369
+ }
5370
+
5371
+ func TnAsPtr[T any](v interface{}) *T {
5372
+ if p, ok := v.(*T); ok {
5373
+ return p
5374
+ }
5375
+ if t, ok := v.(T); ok {
5376
+ return &t
5377
+ }
5378
+ return nil
5379
+ }
5380
+
5381
+ func TnAs[T any](v interface{}) T {
5382
+ if p, ok := v.(*T); ok && p != nil {
5383
+ return *p
5384
+ }
5385
+ t, _ := v.(T)
5386
+ return t
5387
+ }
5388
+
5389
+ func TnTruthy(v interface{}) bool {
5390
+ switch t := v.(type) {
5391
+ case nil:
5392
+ return false
5393
+ case bool:
5394
+ return t
5395
+ case float64:
5396
+ return t != 0 && !math.IsNaN(t)
5397
+ case string:
5398
+ return t != ""
5399
+ }
5400
+ return true
5401
+ }`,
5402
+ typeOf: `func TnTypeOf(v interface{}) string {
5403
+ switch v.(type) {
5404
+ case nil:
5405
+ return "undefined"
5406
+ case string, *string:
5407
+ return "string"
5408
+ case float64, *float64:
5409
+ return "number"
5410
+ case bool, *bool:
5411
+ return "boolean"
5412
+ }
5413
+ if reflect.ValueOf(v).Kind() == reflect.Func {
5414
+ return "function"
5415
+ }
5416
+ return "object"
5417
+ }`,
5418
+ jsonStringify: `func TnJSONStringify(v interface{}, indent string) string {
5419
+ var buf bytes.Buffer
5420
+ enc := json.NewEncoder(&buf)
5421
+ enc.SetEscapeHTML(false)
5422
+ enc.SetIndent("", indent)
5423
+ enc.Encode(v)
5424
+ return strings.TrimSuffix(buf.String(), "\\n")
5425
+ }`,
5426
+ orderedMap: `// TnMap: JS Map/Set/object semantics — keys iterate in insertion order
5427
+ type TnMap[K comparable, V any] struct {
5428
+ keys []K
5429
+ values map[K]V
5430
+ }
5431
+
5432
+ func TnNewMap[K comparable, V any]() *TnMap[K, V] {
5433
+ return &TnMap[K, V]{values: map[K]V{}}
5434
+ }
5435
+
5436
+ func (m *TnMap[K, V]) Set(k K, v V) *TnMap[K, V] {
5437
+ if _, ok := m.values[k]; !ok {
5438
+ m.keys = append(m.keys, k)
5439
+ }
5440
+ m.values[k] = v
5441
+ return m
5442
+ }
5443
+
5444
+ func (m *TnMap[K, V]) Get(k K) V {
5445
+ if m == nil {
5446
+ var zero V
5447
+ return zero
5448
+ }
5449
+ return m.values[k]
5450
+ }
5451
+
5452
+ func (m *TnMap[K, V]) Lookup(k K) (V, bool) {
5453
+ if m == nil {
5454
+ var zero V
5455
+ return zero, false
5456
+ }
5457
+ v, ok := m.values[k]
5458
+ return v, ok
5459
+ }
5460
+
5461
+ func (m *TnMap[K, V]) Has(k K) bool {
5462
+ if m == nil {
5463
+ return false
5464
+ }
5465
+ _, ok := m.values[k]
5466
+ return ok
5467
+ }
5468
+
5469
+ func (m *TnMap[K, V]) Delete(k K) bool {
5470
+ if _, ok := m.values[k]; !ok {
5471
+ return false
5472
+ }
5473
+ delete(m.values, k)
5474
+ for i, key := range m.keys {
5475
+ if key == k {
5476
+ m.keys = append(m.keys[:i], m.keys[i+1:]...)
5477
+ break
5478
+ }
5479
+ }
5480
+ return true
5481
+ }
5482
+
5483
+ func (m *TnMap[K, V]) Clear() {
5484
+ m.keys = nil
5485
+ m.values = map[K]V{}
5486
+ }
5487
+
5488
+ func (m *TnMap[K, V]) Len() int {
5489
+ if m == nil {
5490
+ return 0
5491
+ }
5492
+ return len(m.keys)
5493
+ }
5494
+
5495
+ func (m *TnMap[K, V]) Keys() []K {
5496
+ if m == nil {
5497
+ return nil
5498
+ }
5499
+ return append([]K{}, m.keys...)
5500
+ }
5501
+
5502
+ func (m *TnMap[K, V]) Values() []V {
5503
+ if m == nil {
5504
+ return nil
5505
+ }
5506
+ values := make([]V, 0, len(m.keys))
5507
+ for _, k := range m.keys {
5508
+ values = append(values, m.values[k])
5509
+ }
5510
+ return values
5511
+ }
5512
+
5513
+ func (m *TnMap[K, V]) Entries() [][]interface{} {
5514
+ entries := make([][]interface{}, 0, m.Len())
5515
+ for _, k := range m.Keys() {
5516
+ entries = append(entries, []interface{}{k, m.values[k]})
5517
+ }
5518
+ return entries
5519
+ }
5520
+
5521
+ func (m *TnMap[K, V]) All() iter.Seq2[K, V] {
5522
+ return func(yield func(K, V) bool) {
5523
+ for _, k := range m.Keys() {
5524
+ if v, ok := m.values[k]; ok && !yield(k, v) {
5525
+ return
5526
+ }
5527
+ }
5528
+ }
5529
+ }
5530
+
5531
+ // JSON objects keep insertion order
5532
+ func (m *TnMap[K, V]) MarshalJSON() ([]byte, error) {
5533
+ var buf bytes.Buffer
5534
+ buf.WriteByte('{')
5535
+ for i, k := range m.keys {
5536
+ if i > 0 {
5537
+ buf.WriteByte(',')
5538
+ }
5539
+ key, _ := json.Marshal(fmt.Sprint(k))
5540
+ value, err := json.Marshal(m.values[k])
5541
+ if err != nil {
5542
+ return nil, err
5543
+ }
5544
+ buf.Write(key)
5545
+ buf.WriteByte(':')
5546
+ buf.Write(value)
5547
+ }
5548
+ buf.WriteByte('}')
5549
+ return buf.Bytes(), nil
5550
+ }`,
5551
+ regexReplaceFirst: `func TnRegexReplaceFirst(re *regexp.Regexp, s string, repl string) string {
5552
+ loc := re.FindStringSubmatchIndex(s)
5553
+ if loc == nil {
5554
+ return s
5555
+ }
5556
+ return s[:loc[0]] + string(re.ExpandString(nil, repl, s, loc)) + s[loc[1]:]
5557
+ }`,
5558
+ regexReplaceFunc: `func TnRegexReplaceFunc(re *regexp.Regexp, s string, fn func([]string) string, all bool) string {
5559
+ limit := 1
5560
+ if all {
5561
+ limit = -1
5562
+ }
5563
+ result := ""
5564
+ last := 0
5565
+ for _, loc := range re.FindAllStringSubmatchIndex(s, limit) {
5566
+ groups := make([]string, len(loc)/2)
5567
+ for i := range groups {
5568
+ if loc[2*i] >= 0 {
5569
+ groups[i] = s[loc[2*i]:loc[2*i+1]]
5570
+ }
5571
+ }
5572
+ result += s[last:loc[0]] + fn(groups)
5573
+ last = loc[1]
5574
+ }
5575
+ return result + s[last:]
5576
+ }
5577
+
5578
+ func TnGroup(groups []string, i int) string {
5579
+ if i < len(groups) {
5580
+ return groups[i]
5581
+ }
5582
+ return ""
5583
+ }`,
2816
5584
  readFile: `func TnReadFile(path string) string {
2817
5585
  data, err := os.ReadFile(path)
2818
5586
  if err != nil {
@@ -2953,7 +5721,7 @@ func TnExecShell(command string) tnExecResult {
2953
5721
  }
2954
5722
  return tnExecResult{stdout: stdout.String(), stderr: stderr.String(), status: status}
2955
5723
  }`,
2956
- runInherit: `func TnRunInherit(name string, args ...string) float64 {
5724
+ runInherit: `func TnRunInherit(name string, args ...string) tnExecResult {
2957
5725
  cmd := exec.Command(name, args...)
2958
5726
  cmd.Stdin = os.Stdin
2959
5727
  cmd.Stdout = os.Stdout
@@ -2961,11 +5729,11 @@ func TnExecShell(command string) tnExecResult {
2961
5729
  err := cmd.Run()
2962
5730
  if err != nil {
2963
5731
  if exitErr, ok := err.(*exec.ExitError); ok {
2964
- return float64(exitErr.ExitCode())
5732
+ return tnExecResult{status: float64(exitErr.ExitCode())}
2965
5733
  }
2966
5734
  panic(err)
2967
5735
  }
2968
- return 0
5736
+ return tnExecResult{}
2969
5737
  }`,
2970
5738
  question: `var tnStdinReader = bufio.NewReader(os.Stdin)
2971
5739
 
@@ -2987,10 +5755,41 @@ func TnQuestion(prompt string) string {
2987
5755
  // so helpers are visible across files and must not be duplicated.
2988
5756
  function emitGoHelpers() {
2989
5757
  return [...usedHelpers]
5758
+ .sort()
2990
5759
  .map((id) => goHelpers[id])
2991
5760
  .filter(Boolean)
2992
5761
  .join('\n\n');
2993
5762
  }
5763
+ // Result types of the mapped Node.js stdlib functions ('' = no result)
5764
+ const NODE_FUNCTION_TYPES = {
5765
+ path: {
5766
+ join: 'string',
5767
+ resolve: 'string',
5768
+ dirname: 'string',
5769
+ basename: 'string',
5770
+ extname: 'string',
5771
+ relative: 'string',
5772
+ normalize: 'string'
5773
+ },
5774
+ fs: {
5775
+ readFileSync: 'string',
5776
+ existsSync: 'bool',
5777
+ readdirSync: '[]string',
5778
+ writeFileSync: '',
5779
+ appendFileSync: '',
5780
+ mkdirSync: '',
5781
+ copyFileSync: '',
5782
+ rmSync: '',
5783
+ unlinkSync: ''
5784
+ },
5785
+ os: { platform: 'string', homedir: 'string', tmpdir: 'string' },
5786
+ url: { fileURLToPath: 'string', pathToFileURL: 'string' },
5787
+ child_process: { execSync: 'string' },
5788
+ readline: { question: 'string' }
5789
+ };
5790
+ // Result types of imported Node.js functions by their local callee text
5791
+ // (`join`, `path.join`)
5792
+ const nodeCallResultTypes = new Map();
2994
5793
  // Mapping from Node.js stdlib module names to Go setup functions.
2995
5794
  // Each entry adds the required Go imports and registers call handlers for the local identifier.
2996
5795
  function setupNodeModuleImport(node, nodeModule) {
@@ -3009,6 +5808,9 @@ function setupNodeModuleImport(node, nodeModule) {
3009
5808
  continue;
3010
5809
  const localName = el.name.text;
3011
5810
  const importedName = el.propertyName?.text ?? localName;
5811
+ const resultType = NODE_FUNCTION_TYPES[nodeModule]?.[importedName];
5812
+ if (resultType !== undefined)
5813
+ nodeCallResultTypes.set(localName, resultType);
3012
5814
  const fn = mapping.functions[importedName];
3013
5815
  if (fn) {
3014
5816
  // Register a call handler keyed on the local name
@@ -3020,6 +5822,9 @@ function setupNodeModuleImport(node, nodeModule) {
3020
5822
  // Default import (`import path from 'node:path'`) or
3021
5823
  // namespace import (`import * as path from 'node:path'`)
3022
5824
  const localName = getImportLocalName(node) ?? nodeModule;
5825
+ for (const [funcName, resultType] of Object.entries(NODE_FUNCTION_TYPES[nodeModule] ?? {})) {
5826
+ nodeCallResultTypes.set(`${localName}.${funcName}`, resultType);
5827
+ }
3023
5828
  for (const [funcName, fn] of Object.entries(mapping.functions)) {
3024
5829
  callHandlers[`${localName}.${funcName}`] = (_caller, args) => fn(args);
3025
5830
  }
@@ -3082,6 +5887,93 @@ function visitImportDeclaration(node) {
3082
5887
  }
3083
5888
  return '';
3084
5889
  }
5890
+ // Records the type of a variable introduced by a loop or binding
5891
+ function registerLocalVariable(name, goType) {
5892
+ if (!name || name === '_')
5893
+ return;
5894
+ variableTypes.delete(name);
5895
+ variableClassNames.delete(name);
5896
+ variableTypeNodes.delete(name);
5897
+ narrowedVariables.delete(name);
5898
+ if (goType)
5899
+ variableGoTypes.set(name, goType);
5900
+ else
5901
+ variableGoTypes.delete(name);
5902
+ }
5903
+ // for...of over arrays and strings (a string yields one-character strings)
5904
+ function visitForOfSequence(node, iterExpr, iterType) {
5905
+ if (iterType === 'interface{}')
5906
+ iterType = '[]interface{}';
5907
+ const elementType = iterType === 'string' ? 'string' : iterType?.startsWith('[]') ? iterType.slice(2) : undefined;
5908
+ const declaration = isVariableDeclarationList(node.initializer)
5909
+ ? node.initializer.declarations[0]
5910
+ : undefined;
5911
+ const binding = declaration?.name;
5912
+ const item = getTempName('item');
5913
+ let prefix = '';
5914
+ let loopVar = item;
5915
+ if (binding && isArrayBindingPattern(binding)) {
5916
+ // for (const [a, b] of pairs) → a := item[0]; b := item[1]
5917
+ const memberType = elementType?.startsWith('[]') ? elementType.slice(2) : undefined;
5918
+ (binding.elements ?? []).forEach((el, index) => {
5919
+ if (isOmittedExpression(el))
5920
+ return;
5921
+ const name = visit(el.name);
5922
+ registerLocalVariable(name, memberType);
5923
+ prefix += `${name} := ${item}[${index}]\n\t\t_ = ${name}\n\t\t`;
5924
+ });
5925
+ }
5926
+ else if (binding && isIdentifier(binding)) {
5927
+ const name = visit(binding);
5928
+ registerLocalVariable(binding.text, elementType);
5929
+ if (iterType === 'string') {
5930
+ prefix = `${name} := string(${item})\n\t\t`;
5931
+ }
5932
+ else {
5933
+ loopVar = name;
5934
+ }
5935
+ }
5936
+ else {
5937
+ loopVar = visit(node.initializer, { inline: true });
5938
+ }
5939
+ return `for _, ${loopVar} := range ${iterExpr}${visitLoopBody(node.statement, prefix)}`;
5940
+ }
5941
+ // try/catch with returns: the closure reports (value, done) and the caller
5942
+ // returns when done
5943
+ function visitReturningTryStatement(node, fn, options) {
5944
+ const returnGoType = inferFunctionBodyReturnType(fn);
5945
+ const hasValue = !!returnGoType;
5946
+ const saved = tryReturn;
5947
+ const retVar = getTempName('tryret');
5948
+ const doneVar = getTempName('trydone');
5949
+ const blockCode = (block, inCatch) => {
5950
+ tryReturn = { fn, hasValue, inCatch, retVar, doneVar };
5951
+ const code = (block.statements ?? []).map((s) => visit(s)).join('\t');
5952
+ tryReturn = saved;
5953
+ return code;
5954
+ };
5955
+ const deferreds = [];
5956
+ if (node.finallyBlock) {
5957
+ const finallyBody = (node.finallyBlock.statements ?? []).map((s) => visit(s)).join('\t');
5958
+ deferreds.push(`defer func() {\n\t\t\t${finallyBody}\t\t\t}()`);
5959
+ }
5960
+ if (node.catchClause) {
5961
+ const varDecl = node.catchClause.variableDeclaration;
5962
+ const catchVar = varDecl ? visit(varDecl.name) : '_r';
5963
+ const catchBody = blockCode(node.catchClause.block, true);
5964
+ deferreds.push(`defer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\t${catchVar} := r\n\t\t\t\t_ = ${catchVar}\n\t\t\t\t${catchBody}\t\t\t}\n\t\t\t}()`);
5965
+ }
5966
+ const tryBody = blockCode(node.tryBlock, false);
5967
+ const results = hasValue
5968
+ ? `(${retVar} ${returnGoType}, ${doneVar} bool)`
5969
+ : `(${doneVar} bool)`;
5970
+ const body = [...deferreds, tryBody, 'return'].join('\n\t\t\t');
5971
+ const outer = hasValue ? `${retVar}, ${doneVar}` : doneVar;
5972
+ // Leaving through an enclosing try/catch closure of the same function passes the value on
5973
+ const exit = tryAwareReturn(hasValue ? retVar : '');
5974
+ return (`if ${outer} := func() ${results} {\n\t\t\t${body}\n\t\t\t}(); ${doneVar} { ${exit} }` +
5975
+ (options.inline ? '' : ';\n\t'));
5976
+ }
3085
5977
  function getForOfVarNames(initializer) {
3086
5978
  if (!isVariableDeclarationList(initializer) || initializer.declarations.length === 0) {
3087
5979
  return ['_'];
@@ -3096,7 +5988,27 @@ function getForOfVarNames(initializer) {
3096
5988
  }
3097
5989
  return [visit(decl.name)];
3098
5990
  }
5991
+ // Set while emitting a try/catch whose blocks return from the enclosing function
5992
+ let tryReturn;
5993
+ // `return value` from inside a try/catch closure: report (value, done)
5994
+ function tryAwareReturn(value) {
5995
+ if (!tryReturn)
5996
+ return `return ${value}`;
5997
+ const results = tryReturn.hasValue ? `${value}, true` : 'true';
5998
+ if (tryReturn.inCatch) {
5999
+ const targets = tryReturn.hasValue
6000
+ ? `${tryReturn.retVar}, ${tryReturn.doneVar}`
6001
+ : tryReturn.doneVar;
6002
+ return `${targets} = ${results}; return`;
6003
+ }
6004
+ return `return ${results}`;
6005
+ }
3099
6006
  function visitTryStatement(node, options) {
6007
+ const fn = getEnclosingFunction(node);
6008
+ const returnsFromFunction = (block) => !!block && containsNode(block, (n) => isReturnStatement(n) && getEnclosingFunction(n) === fn);
6009
+ if (fn && (returnsFromFunction(node.tryBlock) || returnsFromFunction(node.catchClause?.block))) {
6010
+ return visitReturningTryStatement(node, fn, options);
6011
+ }
3100
6012
  const deferreds = [];
3101
6013
  // Register finally first (LIFO: runs last after catch)
3102
6014
  if (node.finallyBlock) {