typenative 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/transpiler.js CHANGED
@@ -276,6 +276,45 @@ const dangerousNames = new Set([
276
276
  const renamedFunctions = new Map();
277
277
  // Go type of the receiver of the method call being emitted (for method handlers)
278
278
  let currentReceiverGoType;
279
+ // Inside an object-literal method/getter, `this` refers to the instance variable
280
+ let thisOverrideName;
281
+ let thisOverrideFn;
282
+ // Classes with accessor members: "Class.prop" in these sets means reads/writes
283
+ // of the property are Get_prop() / Set_prop(...) calls
284
+ const classGetterMethods = new Set();
285
+ const classSetterMethods = new Set();
286
+ // Classes with a generator method named [Symbol.iterator]: className → yield type
287
+ const classIteratorMethods = new Map();
288
+ // Every parsed class declaration by name (for inheritance lookup)
289
+ const declaredClasses = new Map();
290
+ // Class → parent class name (skip the Error special case)
291
+ const classParentNames = new Map();
292
+ // Functions declared `async` (their calls at top level drain before exit)
293
+ const asyncFunctionNames = new Set();
294
+ // Generator functions (declared with `function*`)
295
+ const generatorFunctionNames = new Set();
296
+ // While emitting an async/generator function body: `return`/`yield` send here
297
+ let asyncChannelVar;
298
+ let asyncFunctionNode;
299
+ // Variables holding JS arrays: stored as *[]T so aliasing shares the slice
300
+ // header (push/splice through any alias are visible everywhere)
301
+ const referenceArrays = new Set();
302
+ // Registers a variable's Go type; array-typed variables become *[]T references
303
+ // (dynamic []interface{} arrays stay plain: they flow through runtime helpers)
304
+ function setVarGoType(name, goType) {
305
+ if (!name)
306
+ return;
307
+ if (goType && goType.startsWith('[]') && goType !== '[]interface{}') {
308
+ variableGoTypes.set(name, `*${goType}`);
309
+ referenceArrays.add(name);
310
+ return;
311
+ }
312
+ referenceArrays.delete(name);
313
+ if (goType)
314
+ variableGoTypes.set(name, goType);
315
+ else
316
+ variableGoTypes.delete(name);
317
+ }
279
318
  // Struct field / member names: Go keywords get a trailing underscore
280
319
  function goFieldName(name) {
281
320
  return name !== 'main' && dangerousNames.has(name) ? `${name}_` : name;
@@ -361,8 +400,14 @@ function parseSource(code) {
361
400
  for (const stmt of sourceFile.statements ?? []) {
362
401
  if (!stmt.name || !isIdentifier(stmt.name))
363
402
  continue;
364
- if (isFunctionDeclaration(stmt))
403
+ if (isFunctionDeclaration(stmt)) {
365
404
  declaredFunctions.set(stmt.name.text, stmt);
405
+ if ((stmt.modifiers ?? []).some((m) => m.kind === 'AsyncKeyword')) {
406
+ asyncFunctionNames.add(stmt.name.text);
407
+ }
408
+ if (stmt.asteriskToken)
409
+ generatorFunctionNames.add(stmt.name.text);
410
+ }
366
411
  else if (isInterfaceDeclaration(stmt))
367
412
  declaredInterfaces.set(stmt.name.text, stmt);
368
413
  else if (isTypeAliasDeclaration(stmt))
@@ -458,6 +503,8 @@ export function transpileToNative(code, options) {
458
503
  variableTypeNodes.clear();
459
504
  narrowedVariables.clear();
460
505
  nodeCallResultTypes.clear();
506
+ asyncFunctionNames.clear();
507
+ generatorFunctionNames.clear();
461
508
  currentFileDir = null;
462
509
  const sourceFile = parseSource(code);
463
510
  importedPackages.clear();
@@ -480,26 +527,65 @@ export function transpileToNative(code, options) {
480
527
  defaultImportNamespaces.clear();
481
528
  classStaticMethods.clear();
482
529
  classStaticProps.clear();
530
+ classGetterMethods.clear();
531
+ classSetterMethods.clear();
532
+ classIteratorMethods.clear();
533
+ declaredClasses.clear();
534
+ classParentNames.clear();
535
+ referenceArrays.clear();
483
536
  usedHelpers.clear();
484
537
  helperProvidedPackages.clear();
485
538
  mainPackageVariables.length = 0;
486
539
  const transpiledCode = visit(sourceFile, { addFunctionOutside: true });
487
540
  const transpiledCodeOutside = outsideNodes.map((n) => visit(n, { isOutside: true })).join('\n');
541
+ // Keep the process alive until detached async work and timers settle
542
+ const drains = [];
543
+ if (usedHelpers.has('asyncWait'))
544
+ drains.push('__tnWaitGroup.Wait()');
545
+ if (usedHelpers.has('timerWait'))
546
+ drains.push('__tnTimers.Wait()');
547
+ const drainCode = drains.length > 0 ? `\n\t${drains.join('\n\t')}` : '';
488
548
  const main = `package main
489
549
 
490
- ${[...importedPackages].sort().map((pkg) => `import "${pkg}"`).join('\n')}
550
+ ${[...importedPackages].sort().map((pkg) => goImportLine(pkg)).join('\n')}
491
551
 
492
552
  ${mainPackageVariables.join('\n')}
493
553
 
494
554
  func main() {
495
- ${transpiledCode.trim()}
555
+ ${transpiledCode.trim()}${drainCode}
496
556
  }
497
557
 
498
558
  ${transpiledCodeOutside.trim()}
499
559
  ${emitGoHelpers()}`.trimEnd();
500
560
  return { main, files: localImportFiles };
501
561
  }
502
- export function visit(node, options = {}) {
562
+ export // Property map of an interface, including inherited properties (flattened —
563
+ function collectInterfaceProperties(iface, into, seen = new Set()) {
564
+ let name;
565
+ if (iface.name && isIdentifier(iface.name))
566
+ name = iface.name.text;
567
+ if (name && seen.has(name))
568
+ return;
569
+ if (name)
570
+ seen.add(name);
571
+ for (const clause of iface.heritageClauses ?? []) {
572
+ if (clause.token === 'ExtendsKeyword') {
573
+ for (const type of clause.types ?? []) {
574
+ if (isIdentifier(type.expression)) {
575
+ const base = declaredInterfaces.get(type.expression.text);
576
+ if (base)
577
+ collectInterfaceProperties(base, into, seen);
578
+ }
579
+ }
580
+ }
581
+ }
582
+ for (const member of (iface.members ?? [])) {
583
+ if (isPropertySignature(member) && isIdentifier(member.name)) {
584
+ into.set(member.name.text, getOptionalNodeType(member.type, !!member.questionToken));
585
+ }
586
+ }
587
+ }
588
+ function visit(node, options = {}) {
503
589
  let code = '';
504
590
  // Reads of a narrowed nullable property path dereference it
505
591
  if (narrowedVariables.size > 0 && isPropertyAccessExpression(node) && !options.skipNarrowing) {
@@ -533,6 +619,10 @@ export function visit(node, options = {}) {
533
619
  const goAlias = importAliases.get(node.text);
534
620
  if (goAlias)
535
621
  return goAlias;
622
+ // Array references: reads go through the shared slice-header pointer
623
+ if (referenceArrays.has(node.text)) {
624
+ return `(*${getSafeName(node.text)})`;
625
+ }
536
626
  if (narrowedVariables.has(node.text) && isNarrowableReference(node)) {
537
627
  return `(*${getSafeName(node.text)})`;
538
628
  }
@@ -550,9 +640,31 @@ export function visit(node, options = {}) {
550
640
  else if (isTemplateExpression(node)) {
551
641
  return visitTemplateExpression(node);
552
642
  }
643
+ else if (node.kind === 'TaggedTemplateExpression') {
644
+ // tag`x${2}` → tag(["x", ""], 2)
645
+ const tag = visit(node.tag);
646
+ const template = node.template;
647
+ const parts = [];
648
+ const args = [];
649
+ if (template.kind === 'NoSubstitutionTemplateLiteral') {
650
+ parts.push(toGoStringLiteral(template.text));
651
+ }
652
+ else {
653
+ parts.push(toGoStringLiteral(template.head.text));
654
+ for (const span of template.templateSpans ?? []) {
655
+ args.push(visit(span.expression));
656
+ parts.push(toGoStringLiteral(span.literal.text));
657
+ }
658
+ }
659
+ return `${tag}([]string{${parts.join(', ')}}${args.length ? `, ${args.join(', ')}` : ''})`;
660
+ }
553
661
  else if (isNumericLiteral(node)) {
554
662
  return `float64(${node.text})`;
555
663
  }
664
+ else if (node.kind === 'BigIntLiteral') {
665
+ useHelper('bigint');
666
+ return `TnBigInt("${String(node.text).replace(/n$/, '')}")`;
667
+ }
556
668
  else if (isToken(node) && node.kind === 'TrueKeyword') {
557
669
  return `true`;
558
670
  }
@@ -563,14 +675,20 @@ export function visit(node, options = {}) {
563
675
  return `nil`;
564
676
  }
565
677
  else if (isRegularExpressionLiteral(node)) {
566
- importedPackages.add('regexp');
567
678
  const text = node.text; // e.g. /pattern/flags
568
679
  const lastSlash = text.lastIndexOf('/');
569
680
  const pattern = text.substring(1, lastSlash);
570
681
  const flags = text.substring(lastSlash + 1);
682
+ const goPattern = translateJsRegexPattern(pattern);
683
+ // /g needs lastIndex state; backreferences and lookarounds need the
684
+ // backtracking engine — both live in the TnRegex wrapper
685
+ if (needsStatefulRegex(pattern, flags)) {
686
+ useHelper('fancyRegex');
687
+ return `TnRegexCompile("${goPattern}", ${flags.includes('g') ? 'true' : 'false'})`;
688
+ }
689
+ importedPackages.add('regexp');
571
690
  const goFlags = jsRegexFlagsToGo(flags);
572
- const escaped = pattern.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
573
- return `regexp.MustCompile("${goFlags}${escaped}")`;
691
+ return `regexp.MustCompile("${goFlags}${goPattern}")`;
574
692
  }
575
693
  else if (isArrayLiteralExpression(node)) {
576
694
  const type = getArrayLiteralElementType(node);
@@ -617,6 +735,11 @@ export function visit(node, options = {}) {
617
735
  return `${visit(node.expression)}[int(${visit(node.argumentExpression)})]`;
618
736
  }
619
737
  else if (isPropertyAccessExpression(node)) {
738
+ // import.meta.url — the file URL of the running program
739
+ if (node.expression.kind === 'MetaProperty' && node.name.text === 'url') {
740
+ useHelper('moduleUrl');
741
+ return 'TnModuleURL()';
742
+ }
620
743
  if (isDynamicValue(node.expression) && node.name.text === 'length' && !isCallee(node)) {
621
744
  useHelper('dynamic');
622
745
  return `TnLength(${visit(node.expression)})`;
@@ -628,6 +751,15 @@ export function visit(node, options = {}) {
628
751
  if (hasQuestionDot(node)) {
629
752
  return visitOptionalPropertyAccess(node);
630
753
  }
754
+ // super.method() → the parent's implementation on the embedded struct
755
+ if (node.expression.kind === 'SuperKeyword') {
756
+ const ownerClass = getEnclosingClassName(node);
757
+ const parentName = ownerClass ? classParentNames.get(ownerClass) : undefined;
758
+ if (parentName) {
759
+ return `self.${goFieldName(parentName)}.${visit(node.name)}`;
760
+ }
761
+ return visit(node.name);
762
+ }
631
763
  if (isIdentifier(node.expression) && enumNames.has(node.expression.text)) {
632
764
  return `${getSafeName(node.expression.text)}_${getEnumMemberName(node.name)}`;
633
765
  }
@@ -645,48 +777,120 @@ export function visit(node, options = {}) {
645
777
  const leftSide = visit(node.expression);
646
778
  const rightSide = visit(node.name);
647
779
  const objectType = resolveExpressionType(node.expression);
780
+ // Anonymous-struct function fields (object-literal methods/getters): a
781
+ // property read invokes the closure, unless it is itself being called
782
+ const leftGoType = inferExpressionType(node.expression);
783
+ if (leftGoType?.includes('struct{') &&
784
+ !isCallee(node) &&
785
+ getStructFieldGoType(leftGoType.replace(/^\*/, ''), node.name.text)?.startsWith('func')) {
786
+ return `${leftSide}.${rightSide}()`;
787
+ }
788
+ // Class getter reads are accessor calls: c.v → c.Get_v()
789
+ if ((leftGoType?.startsWith('*') || node.expression.kind === 'ThisKeyword') &&
790
+ !leftGoType?.startsWith('*TnMap[') &&
791
+ !leftGoType?.includes('struct{') &&
792
+ !leftGoType?.startsWith('*[]')) {
793
+ const className = node.expression.kind === 'ThisKeyword'
794
+ ? getEnclosingClassName(node)
795
+ : leftGoType
796
+ ? leftGoType.slice(1).replace(/\[.*\]$/, '')
797
+ : undefined;
798
+ if (className && classGetterMethods.has(`${className}.${node.name.text}`)) {
799
+ return `${leftSide}.Get_${rightSide}()`;
800
+ }
801
+ }
802
+ // Record/Map reads via dot syntax are key lookups; method callees keep their name
803
+ if ((objectType === 'Map' || objectType === 'Record') &&
804
+ rightSide !== 'size' &&
805
+ rightSide !== 'length' &&
806
+ !isCallee(node) &&
807
+ !isAssignmentTarget(node)) {
808
+ return `${leftSide}.Get(${toGoStringLiteral(rightSide)})`;
809
+ }
648
810
  return getAcessString(leftSide, rightSide, objectType);
649
811
  }
650
812
  else if (isVariableDeclaration(node)) {
651
813
  // Object destructuring: const { x, y } = obj
652
814
  if (isObjectBindingPattern(node.name) && node.initializer) {
653
815
  const initExpr = visit(node.initializer);
654
- const parts = node.name.elements.map((el) => {
816
+ const initType = inferExpressionType(node.initializer);
817
+ const parts = [];
818
+ for (const el of node.name.elements ?? []) {
655
819
  const localName = visit(el.name);
656
- const propName = el.propertyName ? visit(el.propertyName) : localName;
657
- const defaultVal = el.initializer ? visit(el.initializer) : undefined;
820
+ const rawProp = el.propertyName ?? el.name;
821
+ const propName = isIdentifier(rawProp) ? rawProp.text : visit(rawProp);
822
+ const fieldType = initType
823
+ ? getStructFieldGoType(initType.replace(/^\*/, ''), propName)
824
+ : undefined;
825
+ const defaultVal = el.initializer;
658
826
  if (defaultVal) {
659
- return `${localName} := func() interface{} { if ${initExpr}.${propName} == nil { return ${defaultVal} }; return ${initExpr}.${propName} }()`;
827
+ // Missing property (nil) takes the default
828
+ const defType = inferExpressionType(defaultVal) ?? fieldType ?? 'interface{}';
829
+ const tmp = getTempName('destructure');
830
+ useHelper('dynamic');
831
+ parts.push(`${localName} := func() ${defType} { ${tmp} := interface{}((${initExpr}).${goFieldName(propName)}); if TnIsNil(${tmp}) { return ${visit(defaultVal)} }; return TnAs[${defType}](${tmp}) }()`);
660
832
  }
661
- return `${localName} := ${initExpr}.${propName}`;
662
- });
833
+ else {
834
+ parts.push(`${localName} := (${initExpr}).${goFieldName(propName)}`);
835
+ }
836
+ registerLocalVariable(localName, defaultVal ? (inferExpressionType(defaultVal) ?? fieldType) : fieldType);
837
+ }
663
838
  return parts.join(';\n\t');
664
839
  }
665
840
  // Array destructuring: const [a, b] = arr
666
841
  if (isArrayBindingPattern(node.name) && node.initializer) {
842
+ const initGoType = inferExpressionType(node.initializer);
667
843
  const initExpr = isDynamicValue(node.initializer)
668
844
  ? toGoValueOfType(node.initializer, '[]interface{}')
669
845
  : visit(node.initializer);
670
846
  const tmpVar = getTempName('arr');
847
+ const elemType = initGoType?.startsWith('[]') ? initGoType.slice(2) : undefined;
671
848
  const parts = [`${tmpVar} := ${initExpr}`];
672
849
  node.name.elements.forEach((el, idx) => {
673
850
  if (isOmittedExpression(el))
674
851
  return;
675
852
  const bindEl = el;
676
853
  const localName = visit(bindEl.name);
677
- // Variables starting with _ are intentionally unused — use blank identifier
678
- if (localName === '_' || localName.startsWith('_')) {
679
- parts.push(`_ = ${tmpVar}[${idx}]`);
854
+ const defaultVal = bindEl.initializer;
855
+ if (defaultVal) {
856
+ // Missing element (past the end) takes the default
857
+ const defType = inferExpressionType(defaultVal) ?? elemType ?? 'interface{}';
858
+ parts.push(`${localName} := func() ${defType} { if len(${tmpVar}) > ${idx} { return ${tmpVar}[${idx}] }; return ${visit(defaultVal)} }()`);
859
+ }
860
+ else if (localName === '_' || localName.startsWith('_')) {
861
+ parts.push(`_ = ${tmpVar}`);
680
862
  }
681
863
  else {
682
- parts.push(`${localName} := ${tmpVar}[${idx}]`);
864
+ // Reading past the end gives undefined in JS: the zero value here
865
+ useHelper('dynamic');
866
+ parts.push(`${localName} := TnAt(${tmpVar}, float64(${idx}))`);
683
867
  }
868
+ registerLocalVariable(localName, defaultVal ? inferExpressionType(defaultVal) : elemType);
684
869
  });
685
870
  return parts.join(';\n\t');
686
871
  }
687
872
  // `const x: any = expr` keeps inference (x := expr); `any` elsewhere is interface{}
688
873
  const isInferredAny = isAnyTypeNode(node.type) && !!node.initializer;
689
- const type = isInferredAny ? ':' : getType(node.type);
874
+ let type = isInferredAny ? ':' : getType(node.type);
875
+ // Array aliasing: b = a keeps pointing at the same backing array
876
+ if (isIdentifier(node.name) &&
877
+ node.initializer &&
878
+ isIdentifier(unwrapParentheses(node.initializer)) &&
879
+ referenceArrays.has(unwrapParentheses(node.initializer).text)) {
880
+ const srcName = unwrapParentheses(node.initializer).text;
881
+ const srcType = variableGoTypes.get(srcName) ?? inferExpressionType(node.initializer);
882
+ setVarGoType(node.name.text, srcType?.startsWith('*') ? srcType.slice(1) : srcType);
883
+ const stored = variableGoTypes.get(node.name.text) ?? '*[]interface{}';
884
+ const aliasTop = isVariableStatement(node.parent?.parent) && isSourceFile(node.parent?.parent?.parent);
885
+ if (aliasTop) {
886
+ if (emittingModuleFile) {
887
+ return `var ${getSafeName(node.name.text)} ${stored} = ${getSafeName(srcName)}`;
888
+ }
889
+ mainPackageVariables.push(`var ${getSafeName(node.name.text)} ${stored}`);
890
+ return `${getSafeName(node.name.text)} = ${getSafeName(srcName)}`;
891
+ }
892
+ return `${getSafeName(node.name.text)} := ${getSafeName(srcName)}`;
893
+ }
690
894
  // Track variable type for type-aware method dispatch
691
895
  if (isIdentifier(node.name)) {
692
896
  // A declaration replaces whatever an earlier variable of the same name recorded
@@ -725,6 +929,9 @@ export function visit(node, options = {}) {
725
929
  if (node.initializer.expression.text === 'RegExp') {
726
930
  variableTypes.set(node.name.text, 'RegExp');
727
931
  }
932
+ else if (node.initializer.expression.text === 'Date') {
933
+ variableTypes.set(node.name.text, 'Date');
934
+ }
728
935
  else if (classNames.has(node.initializer.expression.text)) {
729
936
  variableTypes.set(node.name.text, 'class');
730
937
  variableClassNames.set(node.name.text, node.initializer.expression.text);
@@ -733,6 +940,19 @@ export function visit(node, options = {}) {
733
940
  else if (node.initializer && isRegularExpressionLiteral(node.initializer)) {
734
941
  variableTypes.set(node.name.text, 'RegExp');
735
942
  }
943
+ // Array-typed variables hold references (*[]T)
944
+ if (node.type && !isInferredAny) {
945
+ setVarGoType(node.name.text, getType(node.type));
946
+ }
947
+ else if (inferredType) {
948
+ setVarGoType(node.name.text, inferredType);
949
+ }
950
+ else {
951
+ setVarGoType(node.name.text, undefined);
952
+ }
953
+ if (referenceArrays.has(node.name.text) && type.startsWith('[]')) {
954
+ type = variableGoTypes.get(node.name.text);
955
+ }
736
956
  }
737
957
  let initializer = node.initializer ? `= ${visit(node.initializer)}` : '';
738
958
  // Wrap non-nil values assigned to nullable primitive pointer types
@@ -740,20 +960,28 @@ export function visit(node, options = {}) {
740
960
  if (node.initializer && (type.startsWith('*') || isDynamicValue(node.initializer))) {
741
961
  initializer = `= ${toGoValueOfType(node.initializer, type)}`;
742
962
  }
963
+ // Array references take the address of the fresh array
964
+ if (node.initializer &&
965
+ isIdentifier(node.name) &&
966
+ referenceArrays.has(node.name.text) &&
967
+ type === ':') {
968
+ initializer = `= ${toGoValueOfType(node.initializer, variableGoTypes.get(node.name.text))}`;
969
+ }
743
970
  // Package scope has no `:=`: module-level declarations need `var x = expr`
744
971
  const isTopLevelStatement = isVariableStatement(node.parent?.parent) && isSourceFile(node.parent?.parent?.parent);
745
972
  const isPackageScope = emittingModuleFile && isTopLevelStatement;
746
973
  if (type === ':' && isPackageScope) {
747
- return `var ${visit(node.name)} ${initializer}`;
974
+ return `var ${getSafeName(node.name.text)} ${initializer}`;
748
975
  }
749
976
  const isMainTopLevel = !emittingModuleFile && isTopLevelStatement;
750
977
  const packageType = type === ':' ? variableGoTypes.get(node.name.text) : type;
751
978
  if (isMainTopLevel && packageType && packageType !== 'nil' && isIdentifier(node.name)) {
752
- const name = visit(node.name);
979
+ const name = getSafeName(node.name.text);
753
980
  mainPackageVariables.push(`var ${name} ${packageType}`);
754
981
  return initializer ? `${name} ${initializer}` : '';
755
982
  }
756
- return `${type === ':' ? '' : 'var '}${visit(node.name)} ${type}${type === ':' ? '' : ' '}${initializer}`;
983
+ const declName = isIdentifier(node.name) ? getSafeName(node.name.text) : visit(node.name);
984
+ return `${type === ':' ? '' : 'var '}${declName} ${type}${type === ':' ? '' : ' '}${initializer}`;
757
985
  }
758
986
  else if (isCallExpression(node)) {
759
987
  if (hasQuestionDot(node) ||
@@ -787,11 +1015,61 @@ export function visit(node, options = {}) {
787
1015
  }
788
1016
  // Handle setTimeout specially to get raw delay value
789
1017
  if (isIdentifier(node.expression) && node.expression.text === 'setTimeout') {
1018
+ useHelper('timerWait');
790
1019
  importedPackages.add('time');
791
1020
  const callback = visit((node.arguments ?? [])[0]);
792
1021
  const delayNode = (node.arguments ?? [])[1];
793
1022
  const delay = isNumericLiteral(delayNode) ? delayNode.text : visit(delayNode);
794
- return `time.AfterFunc(${delay} * time.Millisecond, ${callback.trimEnd()})`;
1023
+ // Registered on the wait group so the process lives until it fires
1024
+ return `__tnTimers.Add(1)\n\t\ttime.AfterFunc(${delay} * time.Millisecond, func() {\n\t\t\tdefer __tnTimers.Done()\n\t\t\t${callback.trim()}()\n\t\t})`;
1025
+ }
1026
+ // p.then(cb) on a channel-based Promise: wait for the value, run the callback
1027
+ if (isPropertyAccessExpression(node.expression) &&
1028
+ node.expression.name.text === 'then' &&
1029
+ !hasQuestionDot(node.expression)) {
1030
+ const recvType = inferExpressionType(node.expression.expression);
1031
+ if (recvType?.startsWith('chan ')) {
1032
+ const elemType = recvType.slice(5) || 'interface{}';
1033
+ const cb = (node.arguments ?? [])[0];
1034
+ if (cb && (isArrowFunction(cb) || isFunctionExpression(cb))) {
1035
+ for (const p of cb.parameters ?? []) {
1036
+ if (isIdentifier(p.name)) {
1037
+ variableGoTypes.set(p.name.text, elemType);
1038
+ variableTypeNodes.delete(p.name.text);
1039
+ variableTypes.delete(p.name.text);
1040
+ variableClassNames.delete(p.name.text);
1041
+ }
1042
+ }
1043
+ }
1044
+ useHelper('asyncWait');
1045
+ const recv = visit(node.expression.expression);
1046
+ const cbs = (node.arguments ?? []).map((a) => visit(a));
1047
+ const cbExpr = cbs[0] ?? 'func(interface{}) {}';
1048
+ return `__tnWaitGroup.Add(1)\n\t\tgo func() {\n\t\t\tdefer __tnWaitGroup.Done()\n\t\t\t__v := <-(${recv})\n\t\t\t(${cbExpr})(__v)\n\t\t}()`;
1049
+ }
1050
+ }
1051
+ // Array.from(arrayLike, mapFn) — array-likes are { length } structs
1052
+ if (isPropertyAccessExpression(node.expression) &&
1053
+ isIdentifier(node.expression.expression) &&
1054
+ node.expression.expression.text === 'Array' &&
1055
+ node.expression.name.text === 'from') {
1056
+ const sourceNode = (node.arguments ?? [])[0];
1057
+ const callbackNode = (node.arguments ?? [])[1];
1058
+ const paramTypes = ['interface{}', 'float64'];
1059
+ if (callbackNode && (isArrowFunction(callbackNode) || isFunctionExpression(callbackNode))) {
1060
+ (callbackNode.parameters ?? []).forEach((p, index) => {
1061
+ if (isIdentifier(p.name) && paramTypes[index])
1062
+ variableGoTypes.set(p.name.text, paramTypes[index]);
1063
+ });
1064
+ }
1065
+ const source = visit(sourceNode);
1066
+ const cb = callbackNode ? visit(callbackNode) : undefined;
1067
+ const srcVar = getTempName('from');
1068
+ const iVar = getTempName('i');
1069
+ const itemExpr = cb
1070
+ ? `(${cb})(${paramTypes.slice(0, (callbackNode?.parameters ?? []).length).map((t, i) => (i === 0 ? 'nil' : `float64(${iVar})`)).join(', ')})`
1071
+ : 'nil';
1072
+ return `func() []interface{} { ${srcVar} := ${source}; __n := int(${srcVar}.length); __out := make([]interface{}, 0, __n); for ${iVar} := 0; ${iVar} < __n; ${iVar}++ { __out = append(__out, ${itemExpr}) }; return __out }()`;
795
1073
  }
796
1074
  const arrayHigherOrderCall = visitArrayHigherOrderCall(node);
797
1075
  if (arrayHigherOrderCall) {
@@ -883,6 +1161,38 @@ export function visit(node, options = {}) {
883
1161
  const bitwise = visitBitwise(node, op);
884
1162
  if (bitwise)
885
1163
  return bitwise;
1164
+ // BigInt arithmetic and comparisons (*big.Int)
1165
+ const bigLeft = inferExpressionType(node.left);
1166
+ const bigRight = inferExpressionType(node.right);
1167
+ if (bigLeft === '*tnbig.Int' || bigRight === '*tnbig.Int') {
1168
+ useHelper('bigint');
1169
+ const bl = visit(node.left);
1170
+ const br = visit(node.right);
1171
+ if (op === '==')
1172
+ return `${bl}.Cmp(${br}) == 0`;
1173
+ if (op === '!=')
1174
+ return `${bl}.Cmp(${br}) != 0`;
1175
+ if (op === '<')
1176
+ return `${bl}.Cmp(${br}) < 0`;
1177
+ if (op === '>')
1178
+ return `${bl}.Cmp(${br}) > 0`;
1179
+ if (op === '<=')
1180
+ return `${bl}.Cmp(${br}) <= 0`;
1181
+ if (op === '>=')
1182
+ return `${bl}.Cmp(${br}) >= 0`;
1183
+ if (op === '+')
1184
+ return `new(tnbig.Int).Add(${bl}, ${br})`;
1185
+ if (op === '-')
1186
+ return `new(tnbig.Int).Sub(${bl}, ${br})`;
1187
+ if (op === '*')
1188
+ return `new(tnbig.Int).Mul(${bl}, ${br})`;
1189
+ if (op === '/')
1190
+ return `new(tnbig.Int).Div(${bl}, ${br})`;
1191
+ if (op === '%')
1192
+ return `new(tnbig.Int).Rem(${bl}, ${br})`;
1193
+ if (op === '**')
1194
+ return `new(tnbig.Int).Exp(${bl}, ${br}, nil)`;
1195
+ }
886
1196
  if (op === '**') {
887
1197
  importedPackages.add('math');
888
1198
  return `math.Pow(${visit(node.left)}, ${visit(node.right)})`;
@@ -901,7 +1211,7 @@ export function visit(node, options = {}) {
901
1211
  const left = visit(node.left);
902
1212
  return `${left} = math.Mod(${left}, ${visit(node.right)})`;
903
1213
  }
904
- if (['<', '>', '<=', '>=', '-', '*', '/', '+'].includes(op)) {
1214
+ if (['<', '>', '<=', '>=', '-', '*', '/', '+', '+=', '-=', '*=', '/='].includes(op)) {
905
1215
  const dynamicArithmetic = visitDynamicArithmetic(node, op);
906
1216
  if (dynamicArithmetic)
907
1217
  return dynamicArithmetic;
@@ -924,11 +1234,42 @@ export function visit(node, options = {}) {
924
1234
  if (otherType && otherType !== 'nil' && !isNilableGoType(otherType)) {
925
1235
  return op === '!=' ? 'true' : 'false';
926
1236
  }
1237
+ // JS loose equality across primitive types (ToNumber coercion)
1238
+ const leftType = inferExpressionType(node.left);
1239
+ const rightType = inferExpressionType(node.right);
1240
+ const hasBool = leftType === 'bool' || rightType === 'bool';
1241
+ const hasNumberOrString = leftType === 'float64' || leftType === 'string' || rightType === 'float64' || rightType === 'string';
1242
+ if (hasBool && hasNumberOrString) {
1243
+ const boolNode = leftType === 'bool' ? node.left : node.right;
1244
+ useHelper('dynamic');
1245
+ const eq = `TnNumber(${visit(boolNode)}) == ${visit(leftType === 'bool' ? node.right : node.left)}`;
1246
+ return op === '==' ? eq : `!(${eq})`;
1247
+ }
1248
+ if ((leftType === 'string' && rightType === 'float64') ||
1249
+ (leftType === 'float64' && rightType === 'string')) {
1250
+ const strNode = leftType === 'string' ? node.left : node.right;
1251
+ const numNode = leftType === 'string' ? node.right : node.left;
1252
+ useHelper('dynamic');
1253
+ const eq = `TnParseFloat(${visit(strNode)}) == ${visit(numNode)}`;
1254
+ return op === '==' ? eq : `!(${eq})`;
1255
+ }
927
1256
  }
928
1257
  if (op === '=' && isPropertyAccessExpression(node.left) && isDynamicValue(node.left.expression)) {
929
1258
  useHelper('dynamic');
930
1259
  return `TnSet(${visit(node.left.expression)}, "${node.left.name.text}", ${visit(node.right)})`;
931
1260
  }
1261
+ // Class setter writes are accessor calls: c.v = x → c.Set_v(x)
1262
+ if (op === '=' && isPropertyAccessExpression(node.left)) {
1263
+ const ownerGoType = inferExpressionType(node.left.expression);
1264
+ const setterClass = ownerGoType?.startsWith('*') && !ownerGoType.startsWith('*TnMap[') && !ownerGoType.includes('struct{')
1265
+ ? ownerGoType.slice(1).replace(/\[.*\]$/, '')
1266
+ : node.left.expression.kind === 'ThisKeyword'
1267
+ ? getEnclosingClassName(node)
1268
+ : undefined;
1269
+ if (setterClass && classSetterMethods.has(`${setterClass}.${node.left.name.text}`)) {
1270
+ return `${visit(node.left.expression)}.Set_${visit(node.left.name)}(${visit(node.right)})`;
1271
+ }
1272
+ }
932
1273
  // m[k] = v on an ordered map
933
1274
  if (isElementAccessExpression(node.left) && isAssignmentTarget(node.left)) {
934
1275
  const mapType = inferExpressionType(node.left.expression);
@@ -975,6 +1316,17 @@ export function visit(node, options = {}) {
975
1316
  else if (isParenthesizedExpression(node)) {
976
1317
  return `(${visit(node.expression)})`;
977
1318
  }
1319
+ else if (node.kind === 'YieldExpression') {
1320
+ // Inside a generator: yield sends the value over the generator's channel
1321
+ if (asyncChannelVar && getEnclosingFunction(node) === asyncFunctionNode) {
1322
+ if (node.asteriskToken) {
1323
+ // yield* other(): forward every value of the delegated generator
1324
+ return `for __yv := range ${visit(node.expression)} {\n\t\t\t${asyncChannelVar} <- __yv\n\t\t}`;
1325
+ }
1326
+ return `${asyncChannelVar} <- ${visit(node.expression ?? 'nil')}`;
1327
+ }
1328
+ return '';
1329
+ }
978
1330
  else if (isAwaitExpression(node)) {
979
1331
  return `<-${visit(node.expression)}`;
980
1332
  }
@@ -983,6 +1335,15 @@ export function visit(node, options = {}) {
983
1335
  (options.inline ? '' : ';\n\t'));
984
1336
  }
985
1337
  else if (isExpressionStatement(node)) {
1338
+ // A detached top-level async call keeps the process alive until it settles
1339
+ const exprStmt = node.expression;
1340
+ if (isCallExpression(exprStmt) &&
1341
+ isIdentifier(exprStmt.expression) &&
1342
+ asyncFunctionNames.has(exprStmt.expression.text) &&
1343
+ !emittingModuleFile) {
1344
+ useHelper('asyncWait');
1345
+ return `__tnWaitGroup.Add(1)\n\t\tgo func() {\n\t\t\tdefer __tnWaitGroup.Done()\n\t\t\t<-${getSafeName(exprStmt.expression.text)}()\n\t\t}();\n\t`;
1346
+ }
986
1347
  return visit(node.expression) + (options.inline ? '' : ';\n\t');
987
1348
  }
988
1349
  else if (isForStatement(node)) {
@@ -994,7 +1355,8 @@ export function visit(node, options = {}) {
994
1355
  : visit(node.initializer);
995
1356
  const iterated = visit(node.expression, { inline: true });
996
1357
  const isOrderedMap = inferExpressionType(node.expression)?.startsWith('*TnMap[');
997
- return `for ${varName} := range ${iterated}${isOrderedMap ? '.All()' : ''}${visitLoopBody(node.statement)}`;
1358
+ // A key the body never reads would be "declared and not used" in Go
1359
+ return `for ${varName} := range ${iterated}${isOrderedMap ? '.All()' : ''}${visitLoopBody(node.statement, `_ = ${varName}\n\t\t`)}`;
998
1360
  }
999
1361
  else if (isForOfStatement(node)) {
1000
1362
  // Unwrap Object.entries(x) → treat x as the iterable
@@ -1079,19 +1441,24 @@ export function visit(node, options = {}) {
1079
1441
  .join('')}`;
1080
1442
  }
1081
1443
  else if (isBreakStatement(node)) {
1082
- return 'break';
1444
+ return node.label ? `break ${getSafeName(node.label.text)}` : 'break';
1083
1445
  }
1084
1446
  else if (node.kind === 'ContinueStatement') {
1085
- return 'continue;\n\t';
1447
+ return node.label ? `continue ${getSafeName(node.label.text)};\n\t` : 'continue;\n\t';
1448
+ }
1449
+ else if (node.kind === 'LabeledStatement') {
1450
+ const label = getSafeName(node.label.text);
1451
+ return `${label}:\n\t\t${visit(node.statement)}` + (options.inline ? '' : ';\n\t');
1086
1452
  }
1087
1453
  else if (isThrowStatement(node)) {
1088
1454
  const expr = node.expression;
1089
1455
  if (isNewExpression(expr) &&
1090
1456
  isIdentifier(expr.expression) &&
1091
1457
  expr.expression.text === 'Error') {
1458
+ useHelper('error');
1092
1459
  const args = expr.arguments ?? [];
1093
1460
  const msg = args.length > 0 ? visit(args[0]) : '""';
1094
- return `panic(${msg})` + (options.inline ? '' : ';\n\t');
1461
+ return `panic(TnNewError(${msg}))` + (options.inline ? '' : ';\n\t');
1095
1462
  }
1096
1463
  return `panic(${visit(expr)})` + (options.inline ? '' : ';\n\t');
1097
1464
  }
@@ -1106,6 +1473,12 @@ export function visit(node, options = {}) {
1106
1473
  node.expression.expression.text === 'Promise') {
1107
1474
  return visitPromiseReturn(node.expression, options);
1108
1475
  }
1476
+ // Inside an async function or generator the channel carries the result
1477
+ if (asyncChannelVar && getEnclosingFunction(node) === asyncFunctionNode) {
1478
+ if (!node.expression)
1479
+ return 'return';
1480
+ return `${asyncChannelVar} <- ${visit(node.expression)}; return` + (options.inline ? '' : ';\n\t');
1481
+ }
1109
1482
  const enclosingFn = getEnclosingFunction(node);
1110
1483
  const isAsync = enclosingFn?.modifiers?.some((m) => m.kind === 'AsyncKeyword');
1111
1484
  const returnGoType = enclosingFn && !isAsync ? inferFunctionBodyReturnType(enclosingFn) : undefined;
@@ -1128,6 +1501,14 @@ export function visit(node, options = {}) {
1128
1501
  const inferredRetType = inferFunctionBodyReturnType(node);
1129
1502
  const returnType = inferredRetType ? ` ${inferredRetType}` : '';
1130
1503
  if (options.isOutside) {
1504
+ // async functions run their body in a goroutine sending over a channel
1505
+ const isAsync = (node.modifiers ?? []).some((m) => m.kind === 'AsyncKeyword');
1506
+ if (isAsync && node.name) {
1507
+ return emitChannelFunction(node, true);
1508
+ }
1509
+ if (node.asteriskToken && node.name) {
1510
+ return emitChannelFunction(node, false);
1511
+ }
1131
1512
  const name = node.name ? visit(node.name, { inline: true }) : '';
1132
1513
  const safeName = getSafeName(name);
1133
1514
  return `func ${safeName}${typeParams}(${parameterInfo.signature})${returnType} ${visit(node.body, {
@@ -1153,6 +1534,10 @@ export function visit(node, options = {}) {
1153
1534
  return `func(${parameterInfo.signature})${returnType} {\n\t\t${parameterInfo.prefixBlockContent}return ${visit(node.body)};\n\t}`;
1154
1535
  }
1155
1536
  if (!isBlock(node.body)) {
1537
+ // void-style calls (console.*, assert) emit statements, not values
1538
+ if (isVoidStatementCall(node.body)) {
1539
+ return `func(${parameterInfo.signature})${returnType} { ${visit(node.body)}; }`;
1540
+ }
1156
1541
  return `func(${parameterInfo.signature})${returnType} { return ${visit(node.body)}; }`;
1157
1542
  }
1158
1543
  return `func(${parameterInfo.signature})${returnType} ${visit(node.body, {
@@ -1160,8 +1545,14 @@ export function visit(node, options = {}) {
1160
1545
  }).trimEnd()}`;
1161
1546
  }
1162
1547
  else if (node.kind === 'ThisKeyword') {
1548
+ if (thisOverrideName && getEnclosingFunction(node) === thisOverrideFn)
1549
+ return thisOverrideName;
1163
1550
  return 'self';
1164
1551
  }
1552
+ else if (node.kind === 'PrivateIdentifier') {
1553
+ // #field → a plain (mangled) struct field
1554
+ return goFieldName(node.text.slice(1));
1555
+ }
1165
1556
  else if (isEnumDeclaration(node)) {
1166
1557
  const enumName = node.name.text;
1167
1558
  enumNames.add(enumName);
@@ -1207,11 +1598,7 @@ export function visit(node, options = {}) {
1207
1598
  if (options.addFunctionOutside) {
1208
1599
  outsideNodes.push(node);
1209
1600
  const properties = new Map();
1210
- for (const member of (node.members ?? [])) {
1211
- if (isPropertySignature(member) && isIdentifier(member.name)) {
1212
- properties.set(member.name.text, getOptionalNodeType(member.type, !!member.questionToken));
1213
- }
1214
- }
1601
+ collectInterfaceProperties(node, properties);
1215
1602
  if (properties.size > 0) {
1216
1603
  interfacePropertyTypes.set(visit(node.name), properties);
1217
1604
  }
@@ -1245,7 +1632,16 @@ export function visit(node, options = {}) {
1245
1632
  }
1246
1633
  }
1247
1634
  if (properties.length > 0 && methods.length === 0) {
1248
- const fields = [...extendedInterfaces.map((e) => `\t${e}`), ...properties];
1635
+ if (extendedInterfaces.length === 0) {
1636
+ return `type ${name}${typeParams} struct {\n${properties.join('\n')}\n}`;
1637
+ }
1638
+ // Flattened inherited properties: literals can set every field directly
1639
+ const flattened = new Map();
1640
+ collectInterfaceProperties(node, flattened);
1641
+ const fields = [];
1642
+ flattened.forEach((propType, prop) => {
1643
+ fields.push(`\t${goFieldName(prop)} ${propType}`);
1644
+ });
1249
1645
  return `type ${name}${typeParams} struct {\n${fields.join('\n')}\n}`;
1250
1646
  }
1251
1647
  const members = [...extendedInterfaces.map((e) => `\t${e}`), ...methods];
@@ -1256,31 +1652,81 @@ export function visit(node, options = {}) {
1256
1652
  outsideNodes.push(node);
1257
1653
  const className = visit(node.name);
1258
1654
  classNames.add(className);
1655
+ declaredClasses.set(className, node);
1259
1656
  const properties = new Map();
1260
1657
  const methods = new Map();
1261
1658
  for (const member of (node.members ?? [])) {
1262
1659
  const memberModifiers = member.modifiers;
1263
1660
  const isStatic = memberModifiers?.some((m) => m.kind === 'StaticKeyword');
1264
- if (isPropertyDeclaration(member) && isIdentifier(member.name)) {
1661
+ const isPrivate = member.name?.kind === 'PrivateIdentifier';
1662
+ const memberName = isPrivate ? member.name.text.slice(1) : member.name?.text;
1663
+ if (isPropertyDeclaration(member) && memberName) {
1265
1664
  if (isStatic) {
1266
- classStaticProps.add(`${className}.${member.name.text}`);
1665
+ classStaticProps.add(`${className}.${memberName}`);
1267
1666
  }
1268
1667
  else {
1269
- properties.set(member.name.text, getPropertyDeclarationType(member));
1668
+ properties.set(memberName, getPropertyDeclarationType(member));
1270
1669
  }
1271
1670
  }
1272
- if (isMethodDeclaration(member) && isIdentifier(member.name)) {
1671
+ if (isMethodDeclaration(member) && memberName) {
1273
1672
  if (isStatic) {
1274
- classStaticMethods.add(`${className}.${member.name.text}`);
1673
+ classStaticMethods.add(`${className}.${memberName}`);
1275
1674
  }
1276
1675
  else {
1277
- methods.set(member.name.text, member.type ? getType(member.type) : 'interface{}');
1676
+ methods.set(memberName, member.type ? getType(member.type) : 'interface{}');
1677
+ }
1678
+ }
1679
+ if (isGetAccessor(member) && isIdentifier(member.name)) {
1680
+ classGetterMethods.add(`${className}.${member.name.text}`);
1681
+ properties.set(member.name.text, member.type ? getType(member.type) : 'interface{}');
1682
+ }
1683
+ if (isSetAccessor(member) && isIdentifier(member.name)) {
1684
+ classSetterMethods.add(`${className}.${member.name.text}`);
1685
+ }
1686
+ // *[Symbol.iterator]() — the class is iterable via a generator channel
1687
+ if (isMethodDeclaration(member) && member.asteriskToken) {
1688
+ const computed = member.name?.kind === 'ComputedPropertyName' ? member.name.expression : undefined;
1689
+ if (computed &&
1690
+ isPropertyAccessExpression(computed) &&
1691
+ isIdentifier(computed.expression) &&
1692
+ computed.expression.text === 'Symbol' &&
1693
+ computed.name.text === 'iterator') {
1694
+ classIteratorMethods.set(className, getGeneratorYieldType(member));
1278
1695
  }
1279
1696
  }
1280
1697
  }
1281
1698
  for (const param of getParameterProperties(node)) {
1282
1699
  properties.set(param.name.text, getParameterGoType(param));
1283
1700
  }
1701
+ // Inherited members resolve through the subclass instance too
1702
+ let parentClassName = null;
1703
+ for (const clause of node.heritageClauses ?? []) {
1704
+ if (clause.token === 'ExtendsKeyword') {
1705
+ parentClassName = visit(clause.types[0].expression);
1706
+ }
1707
+ }
1708
+ if (parentClassName && parentClassName !== 'Error') {
1709
+ classParentNames.set(className, parentClassName);
1710
+ const parent = declaredClasses.get(parentClassName);
1711
+ if (parent) {
1712
+ const parentMethods = classMethodReturnTypes.get(parentClassName);
1713
+ if (parentMethods) {
1714
+ for (const inheritedMethod of parentMethods.keys()) {
1715
+ if (!methods.has(inheritedMethod)) {
1716
+ methods.set(inheritedMethod, parentMethods.get(inheritedMethod));
1717
+ }
1718
+ }
1719
+ }
1720
+ const parentProps = classPropertyTypes.get(parentClassName);
1721
+ if (parentProps) {
1722
+ for (const inheritedProp of parentProps.keys()) {
1723
+ if (!properties.has(inheritedProp)) {
1724
+ properties.set(inheritedProp, parentProps.get(inheritedProp));
1725
+ }
1726
+ }
1727
+ }
1728
+ }
1729
+ }
1284
1730
  classPropertyTypes.set(className, properties);
1285
1731
  classMethodReturnTypes.set(className, methods);
1286
1732
  return '';
@@ -1296,13 +1742,21 @@ export function visit(node, options = {}) {
1296
1742
  }
1297
1743
  }
1298
1744
  }
1745
+ const extendsError = parentClass === 'Error';
1746
+ if (extendsError)
1747
+ useHelper('error');
1748
+ if (parentClass && !extendsError)
1749
+ classParentNames.set(name, parentClass);
1750
+ const isAbstractClass = (node.modifiers ?? []).some((m) => m.kind === 'AbstractKeyword');
1299
1751
  const fields = [];
1300
1752
  if (parentClass) {
1301
- fields.push(`\t${parentClass}`);
1753
+ fields.push(`\t${extendsError ? 'TnError' : parentClass}`);
1302
1754
  }
1303
1755
  for (const member of (node.members ?? [])) {
1304
1756
  if (isPropertyDeclaration(member)) {
1305
- const fieldName = visit(member.name);
1757
+ const fieldName = member.name?.kind === 'PrivateIdentifier'
1758
+ ? goFieldName(member.name.text.slice(1))
1759
+ : visit(member.name);
1306
1760
  let fieldType;
1307
1761
  if (member.type && isArrayTypeNode(member.type)) {
1308
1762
  fieldType = `[]${getType(member.type, true)}`;
@@ -1318,31 +1772,136 @@ export function visit(node, options = {}) {
1318
1772
  }
1319
1773
  let result = `type ${name}${typeParams} struct {\n${fields.join('\n')}\n}\n\n`;
1320
1774
  const ctor = (node.members ?? []).find((m) => isConstructorDeclaration(m));
1775
+ const isCtorSuperCall = (s) => isExpressionStatement(s) &&
1776
+ isCallExpression(s.expression) &&
1777
+ s.expression.expression.kind === 'SuperKeyword';
1778
+ const superCalledInCtor = (ctor?.body?.statements ?? []).some(isCtorSuperCall);
1321
1779
  if (ctor) {
1322
1780
  const ctorParameterInfo = getFunctionParametersInfo(ctor.parameters ?? []);
1323
1781
  const bodyStatements = (ctor.body?.statements ?? [])
1324
- .filter((s) => {
1325
- if (isExpressionStatement(s) && isCallExpression(s.expression)) {
1326
- return s.expression.expression.kind !== 'SuperKeyword';
1782
+ .map((s) => {
1783
+ // super(...) initializes the embedded parent (or the Error message)
1784
+ if (isCtorSuperCall(s)) {
1785
+ const args = (s.expression.arguments ?? []).map((a) => visit(a));
1786
+ if (extendsError)
1787
+ return `self.message = ${args[0] ?? '""'}\n\t\t`;
1788
+ return `self.${goFieldName(parentClass)} = *New${parentClass}(${args.join(', ')})\n\t\t`;
1327
1789
  }
1328
- return true;
1790
+ return visit(s);
1329
1791
  })
1330
- .map((s) => visit(s))
1331
1792
  .join('\t') ?? '';
1332
1793
  const parameterAssignments = getParameterProperties(node)
1333
1794
  .map((p) => `self.${goFieldName(p.name.text)} = ${visit(p.name)}\n\t\t`)
1334
1795
  .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`;
1796
+ const parentInit = parentClass && !extendsError && !superCalledInCtor
1797
+ ? `self.${goFieldName(parentClass)} = *New${parentClass}()\n\t\t`
1798
+ : '';
1799
+ result += `func New${name}${typeParams}(${ctorParameterInfo.signature}) *${name}${typeParamNames} {\n\t\tself := &${name}${typeParamNames}{}\n\t\t${ctorParameterInfo.prefixBlockContent}${parentInit}${fieldInitializers(node)}${parameterAssignments}${bodyStatements}return self;\n\t}\n\n`;
1800
+ }
1801
+ else if (extendsError) {
1802
+ // class X extends Error {} — the constructor takes the message
1803
+ result += `func New${name}${typeParams}(message string) *${name}${typeParamNames} {\n\t\tself := &${name}${typeParamNames}{}\n\t\tself.message = message\n\t\t${fieldInitializers(node)}return self\n\t}\n\n`;
1336
1804
  }
1337
1805
  else {
1338
- result += `func New${name}${typeParams}() *${name}${typeParamNames} {\n\t\tself := &${name}${typeParamNames}{}\n\t\t${fieldInitializers(node)}return self\n\t}\n\n`;
1806
+ const parentInit = parentClass && !extendsError ? `self.${goFieldName(parentClass)} = *New${parentClass}()\n\t\t` : '';
1807
+ result += `func New${name}${typeParams}() *${name}${typeParamNames} {\n\t\tself := &${name}${typeParamNames}{}\n\t\t${parentInit}${fieldInitializers(node)}return self\n\t}\n\n`;
1808
+ }
1809
+ // Inherited concrete methods are copied onto the subclass: Go embedding
1810
+ // promotes the parent's receiver, which would break `this` dispatch
1811
+ const ownMethodNames = new Set();
1812
+ for (const member of (node.members ?? [])) {
1813
+ if (!isMethodDeclaration(member) && !isGetAccessor(member))
1814
+ continue;
1815
+ if (member.name?.kind === 'PrivateIdentifier')
1816
+ continue;
1817
+ if (!member.name?.text)
1818
+ continue;
1819
+ ownMethodNames.add(member.name.text);
1820
+ }
1821
+ const copiedMethods = [];
1822
+ if (parentClass && !extendsError) {
1823
+ // Walk the parent chain (iteratively — no local recursive closures)
1824
+ const pending = [parentClass];
1825
+ const seenClasses = new Set();
1826
+ const ancestors = [];
1827
+ while (pending.length > 0) {
1828
+ const nextName = pending.pop();
1829
+ if (!nextName || seenClasses.has(nextName))
1830
+ continue;
1831
+ seenClasses.add(nextName);
1832
+ const ancestor = declaredClasses.get(nextName);
1833
+ if (!ancestor)
1834
+ continue;
1835
+ ancestors.push(ancestor);
1836
+ for (const clause of ancestor.heritageClauses ?? []) {
1837
+ if (clause.token === 'ExtendsKeyword') {
1838
+ pending.push(visit(clause.types[0].expression));
1839
+ }
1840
+ }
1841
+ }
1842
+ // Copy from the topmost ancestor down so overrides shadow correctly
1843
+ let ancestorIndex = ancestors.length - 1;
1844
+ while (ancestorIndex >= 0) {
1845
+ const ancestor = ancestors[ancestorIndex];
1846
+ for (const member of (ancestor.members ?? [])) {
1847
+ if (!isMethodDeclaration(member))
1848
+ continue;
1849
+ if (!member.body || member.asteriskToken)
1850
+ continue; // abstract / generator
1851
+ if (member.modifiers?.some((m) => m.kind === 'StaticKeyword'))
1852
+ continue;
1853
+ if (member.modifiers?.some((m) => m.kind === 'AbstractKeyword'))
1854
+ continue;
1855
+ const methodName = member.name?.kind === 'PrivateIdentifier' ? member.name.text.slice(1) : member.name?.text;
1856
+ if (!methodName || ownMethodNames.has(methodName))
1857
+ continue;
1858
+ ownMethodNames.add(methodName);
1859
+ const methodParameterInfo = getFunctionParametersInfo(member.parameters ?? []);
1860
+ const returnType = member.type ? ` ${getType(member.type)}` : '';
1861
+ copiedMethods.push(`func (self *${name}${typeParamNames}) ${methodName}(${methodParameterInfo.signature})${returnType} ${visit(member.body, { prefixBlockContent: methodParameterInfo.prefixBlockContent })}\n`);
1862
+ }
1863
+ ancestorIndex--;
1864
+ }
1339
1865
  }
1340
1866
  for (const member of (node.members ?? [])) {
1867
+ if (isMethodDeclaration(member) && member.asteriskToken && member.body) {
1868
+ // Generator method: *[Symbol.iterator]() → Symbol_iterator() chan T
1869
+ let genName;
1870
+ if (member.name?.kind === 'ComputedPropertyName') {
1871
+ const expr = member.name.expression;
1872
+ if (isPropertyAccessExpression(expr) &&
1873
+ isIdentifier(expr.expression) &&
1874
+ expr.expression.text === 'Symbol' &&
1875
+ expr.name.text === 'iterator') {
1876
+ genName = 'Symbol_iterator';
1877
+ }
1878
+ }
1879
+ else if (member.name && member.name.text) {
1880
+ genName = member.name.kind === 'PrivateIdentifier' ? member.name.text.slice(1) : member.name.text;
1881
+ }
1882
+ if (genName) {
1883
+ const yieldType = getGeneratorYieldType(member);
1884
+ const prevChannel = asyncChannelVar;
1885
+ const prevFn = asyncFunctionNode;
1886
+ asyncChannelVar = '__ch';
1887
+ asyncFunctionNode = member;
1888
+ const methodParameterInfo = getFunctionParametersInfo(member.parameters ?? []);
1889
+ const genBody = visit(member.body, { prefixBlockContent: methodParameterInfo.prefixBlockContent });
1890
+ asyncChannelVar = prevChannel;
1891
+ asyncFunctionNode = prevFn;
1892
+ result += `func (self *${name}${typeParamNames}) ${genName}(${methodParameterInfo.signature}) chan ${yieldType} {\n\t__ch := make(chan ${yieldType})\n\tgo func() {\n\t\tdefer close(__ch)\n\t\t${genBody.trim()}\n\t}()\n\treturn __ch\n}\n\n`;
1893
+ }
1894
+ continue;
1895
+ }
1341
1896
  if (isMethodDeclaration(member)) {
1342
1897
  const methodName = visit(member.name);
1343
1898
  const methodParameterInfo = getFunctionParametersInfo(member.parameters ?? []);
1344
1899
  const returnType = member.type ? ` ${getType(member.type)}` : '';
1345
1900
  const isStatic = member.modifiers?.some((m) => m.kind === 'StaticKeyword');
1901
+ if (!member.body)
1902
+ continue; // abstract method — implemented by subclasses
1903
+ if (isAbstractClass && !isStatic)
1904
+ continue; // only reachable via subclasses
1346
1905
  if (isStatic) {
1347
1906
  // Static methods become package-level functions named ClassName_methodName
1348
1907
  result += `func ${name}_${methodName}(${methodParameterInfo.signature})${returnType} ${visit(member.body, { prefixBlockContent: methodParameterInfo.prefixBlockContent })}\n\n`;
@@ -1352,24 +1911,27 @@ export function visit(node, options = {}) {
1352
1911
  }
1353
1912
  }
1354
1913
  else if (isGetAccessor(member)) {
1355
- // Getter: get prop() { ... } → func (self *T) Prop() RetType { ... }
1914
+ // Getter: get prop() { ... } → func (self *T) Get_prop() RetType { ... }
1356
1915
  const getterName = visit(member.name);
1357
1916
  const returnType = member.type ? ` ${getType(member.type)}` : ' interface{}';
1358
1917
  result += `func (self *${name}${typeParamNames}) Get_${getterName}()${returnType} ${visit(member.body)}\n\n`;
1359
1918
  }
1360
1919
  else if (isSetAccessor(member)) {
1361
- // Setter: set prop(val) { ... } → func (self *T) SetProp(val ValType) { ... }
1920
+ // Setter: set prop(val) { ... } → func (self *T) Set_prop(val ValType) { ... }
1362
1921
  const setterName = visit(member.name);
1363
1922
  const parameterInfo = getFunctionParametersInfo(member.parameters ?? []);
1364
1923
  result += `func (self *${name}${typeParamNames}) Set_${setterName}(${parameterInfo.signature}) ${visit(member.body, { prefixBlockContent: parameterInfo.prefixBlockContent })}\n\n`;
1365
1924
  }
1366
1925
  }
1926
+ result += copiedMethods.join('\n');
1367
1927
  // Static property declarations → package-level vars named ClassName_propName
1368
1928
  for (const member of (node.members ?? [])) {
1369
1929
  if (isPropertyDeclaration(member) &&
1370
1930
  member.modifiers?.some((m) => m.kind === 'StaticKeyword')) {
1371
1931
  const fieldName = visit(member.name);
1372
- const fieldType = getOptionalNodeType(member.type, !!member.questionToken);
1932
+ const fieldType = member.type
1933
+ ? getOptionalNodeType(member.type, !!member.questionToken)
1934
+ : toFieldGoType(member.initializer ? inferExpressionType(member.initializer) : undefined);
1373
1935
  const initializer = member.initializer ? ` = ${visit(member.initializer)}` : '';
1374
1936
  result += `var ${name}_${fieldName} ${fieldType}${initializer}\n\n`;
1375
1937
  }
@@ -1400,6 +1962,25 @@ export function visit(node, options = {}) {
1400
1962
  if (className === 'Set') {
1401
1963
  return visitNewSet(node);
1402
1964
  }
1965
+ // new Error(message) → a TnError value with a .message
1966
+ if (className === 'Error') {
1967
+ useHelper('error');
1968
+ const msg = (node.arguments ?? [])[0] ? visit(node.arguments[0]) : '""';
1969
+ return `TnNewError(${msg})`;
1970
+ }
1971
+ // new Date() / new Date(ms) → Go time
1972
+ if (className === 'Date') {
1973
+ importedPackages.add('time');
1974
+ if ((node.arguments ?? []).length === 0)
1975
+ return 'time.Now()';
1976
+ return `time.UnixMilli(int64(${visit(node.arguments[0])})).UTC()`;
1977
+ }
1978
+ // new Array<number>(3) → make([]float64, 3)
1979
+ if (className === 'Array' && (node.typeArguments ?? []).length === 1) {
1980
+ const elementType = getType(node.typeArguments[0]);
1981
+ const length = (node.arguments ?? [])[0] ? visit(node.arguments[0]) : '0';
1982
+ return `make([]${elementType}, int(${length}))`;
1983
+ }
1403
1984
  const typeArgs = getTypeArguments((node.typeArguments ?? []));
1404
1985
  const args = node.arguments ? (node.arguments ?? []).map((a) => visit(a)) : [];
1405
1986
  return `New${className}${typeArgs}(${args.join(', ')})`;
@@ -1511,6 +2092,20 @@ function toGoValueOfType(expr, goType) {
1511
2092
  return orderedMapLiteral(mapArgs[0], mapArgs[1], []);
1512
2093
  return `${goType}{}`;
1513
2094
  }
2095
+ // Array reference slots (*[]T): literals take an address, aliases pass through
2096
+ if (goType && goType.startsWith('*[]')) {
2097
+ if (isIdentifier(expr) && referenceArrays.has(expr.text)) {
2098
+ return getSafeName(expr.text);
2099
+ }
2100
+ if (isArrayLiteralExpression(expr)) {
2101
+ const code = visit(expr);
2102
+ // Spread-built arrays (append chains) are not addressable
2103
+ return code.startsWith('[')
2104
+ ? `&${code}`
2105
+ : `func() ${goType} { __v := ${code}; return &__v }()`;
2106
+ }
2107
+ return `func() ${goType} { __v := ${visit(expr)}; return &__v }()`;
2108
+ }
1514
2109
  const code = visit(expr);
1515
2110
  if (goType && goType !== 'interface{}' && goType !== ':' && isDynamicValue(expr)) {
1516
2111
  useHelper('dynamic');
@@ -1732,7 +2327,16 @@ function visitDynamicArithmetic(node, op) {
1732
2327
  useHelper('dynamic');
1733
2328
  return `TnAs[float64](${visit(side)})`;
1734
2329
  };
1735
- return `${operand(node.left, leftDynamic)} ${op} ${operand(node.right, rightDynamic)}`;
2330
+ // Compound assignment with a dynamic left side reads through the interface
2331
+ if (op.endsWith('=') && leftDynamic) {
2332
+ const binOp = op.slice(0, -1);
2333
+ const converted = isConcat
2334
+ ? `${visit(node.left)} + ${visit(node.right)}`
2335
+ : `TnAs[float64](${visit(node.left)}) ${binOp} ${operand(node.right, rightDynamic)}`;
2336
+ useHelper('dynamic');
2337
+ return `${visit(node.left)} = ${converted}`;
2338
+ }
2339
+ return `${operand(node.left, leftDynamic && !op.endsWith('='))} ${op} ${operand(node.right, rightDynamic)}`;
1736
2340
  }
1737
2341
  // `x === v` where x is *T and v is T: equal only when x is non-nil and *x == v
1738
2342
  function visitNullableComparison(node, op) {
@@ -1915,9 +2519,12 @@ function fieldInitializers(classNode) {
1915
2519
  return (classNode.members ?? [])
1916
2520
  .filter((m) => isPropertyDeclaration(m) &&
1917
2521
  m.initializer &&
1918
- isIdentifier(m.name) &&
2522
+ (isIdentifier(m.name) || m.name?.kind === 'PrivateIdentifier') &&
1919
2523
  !(m.modifiers ?? []).some((mod) => mod.kind === 'StaticKeyword'))
1920
- .map((m) => `self.${goFieldName(m.name.text)} = ${toGoValueOfType(m.initializer, getPropertyDeclarationType(m))}\n\t\t`)
2524
+ .map((m) => {
2525
+ const fieldName = m.name.kind === 'PrivateIdentifier' ? m.name.text.slice(1) : m.name.text;
2526
+ return `self.${goFieldName(fieldName)} = ${toGoValueOfType(m.initializer, getPropertyDeclarationType(m))}\n\t\t`;
2527
+ })
1921
2528
  .join('');
1922
2529
  }
1923
2530
  // A class field's type: declared, else inferred from its initializer (n = 1 → float64)
@@ -2004,13 +2611,27 @@ function getObjectLiteralGoType(node) {
2004
2611
  return typeName;
2005
2612
  if (isDictionaryLiteral(node))
2006
2613
  return mapGoType('string', getDictionaryValueType(node));
2007
- const fields = (node.properties ?? [])
2008
- .filter((p) => (isPropertyAssignment(p) || isShorthandPropertyAssignment(p)) && isIdentifier(p.name))
2009
- .map((p) => {
2614
+ const fields = new Map();
2615
+ for (const p of node.properties ?? []) {
2616
+ if (p.kind === 'SpreadAssignment') {
2617
+ const srcType = inferExpressionType(p.expression);
2618
+ const parsedFields = parseStructFields((srcType ?? '').replace(/^\*/, ''));
2619
+ for (const entry of parsedFields) {
2620
+ if (!fields.has(entry[0]))
2621
+ fields.set(entry[0], entry[1]);
2622
+ }
2623
+ continue;
2624
+ }
2625
+ if ((isMethodDeclaration(p) || isGetAccessor(p)) && isIdentifier(p.name)) {
2626
+ fields.set(goFieldName(p.name.text), methodFuncType(p));
2627
+ continue;
2628
+ }
2629
+ if (!isPropertyAssignment(p) && !isShorthandPropertyAssignment(p))
2630
+ continue;
2010
2631
  const value = isPropertyAssignment(p) ? p.initializer : p.name;
2011
- return `${goFieldName(p.name.text)} ${toFieldGoType(inferExpressionType(value))}`;
2012
- });
2013
- return `*struct{ ${fields.join('; ')} }`;
2632
+ fields.set(goFieldName(p.name.text), toFieldGoType(inferExpressionType(value)));
2633
+ }
2634
+ return `*struct{ ${structFieldsBody(fields)} }`;
2014
2635
  }
2015
2636
  // Values typed `any`/`unknown` are handled at runtime (TnGet/TnSet/...),
2016
2637
  // never by guessing their type
@@ -2028,8 +2649,13 @@ function isAssignmentTarget(node) {
2028
2649
  ['PlusPlusToken', 'MinusMinusToken'].includes(parent.operator));
2029
2650
  }
2030
2651
  // `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
2652
+ // cannot become a Go struct); other casts convert to T; `as const` is erased
2032
2653
  function getAsExpressionType(node) {
2654
+ if (isTypeReferenceNode(node.type) &&
2655
+ isIdentifier(node.type.typeName) &&
2656
+ node.type.typeName.text === 'const') {
2657
+ return inferExpressionType(node.expression) ?? 'interface{}';
2658
+ }
2033
2659
  const target = getType(node.type);
2034
2660
  if (isDynamicValue(node.expression) && (isStructGoType(target) || target.startsWith('*'))) {
2035
2661
  return 'interface{}';
@@ -2090,8 +2716,44 @@ function visitMapLiteral(node, recordType) {
2090
2716
  }
2091
2717
  return orderedMapLiteral(keyType, valueType, entries);
2092
2718
  }
2719
+ // Field name → Go type entries of a struct type string: struct{ a T; b U }
2720
+ function parseStructFields(structType) {
2721
+ if (!structType.startsWith('struct{'))
2722
+ return [];
2723
+ const body = structType.slice(7, structType.lastIndexOf('}'));
2724
+ const out = [];
2725
+ let depth = 0;
2726
+ let start = 0;
2727
+ for (let i = 0; i <= body.length; i++) {
2728
+ const ch = i < body.length ? body[i] : ';';
2729
+ if (ch === '{' || ch === '(' || ch === '[')
2730
+ depth++;
2731
+ else if (ch === '}' || ch === ')' || ch === ']')
2732
+ depth--;
2733
+ else if (ch === ';' && depth === 0) {
2734
+ const entry = body.slice(start, i).trim();
2735
+ start = i + 1;
2736
+ if (!entry)
2737
+ continue;
2738
+ const space = entry.indexOf(' ');
2739
+ if (space > 0)
2740
+ out.push([entry.slice(0, space), entry.slice(space + 1).trim()]);
2741
+ }
2742
+ }
2743
+ return out;
2744
+ }
2745
+ // Go func type of an object-literal method or getter
2746
+ function methodFuncType(member) {
2747
+ const params = (member.parameters ?? []).map((p) => getParameterGoType(p)).join(', ');
2748
+ const ret = member.type
2749
+ ? getType(member.type)
2750
+ : inferFunctionBodyReturnType(member) ?? '';
2751
+ return `func(${params})${ret ? ` ${ret}` : ''}`;
2752
+ }
2093
2753
  // 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])
2754
+ // or a map when the variable holding it is indexed dynamically (obj[key]).
2755
+ // Methods and getters become function fields whose closures capture the
2756
+ // instance; spreads copy the source struct's fields.
2095
2757
  function visitAnonymousStructLiteral(node) {
2096
2758
  if (isDictionaryLiteral(node)) {
2097
2759
  const valueType = getDictionaryValueType(node);
@@ -2100,21 +2762,82 @@ function visitAnonymousStructLiteral(node) {
2100
2762
  .map((p) => `${mapKeyText(p.name)}, ${visit(p.initializer)}`);
2101
2763
  return orderedMapLiteral('string', valueType, entries);
2102
2764
  }
2103
- const fields = [];
2765
+ const fields = new Map();
2104
2766
  const values = [];
2767
+ const steps = [];
2768
+ let sawSpread = false;
2105
2769
  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)}`);
2770
+ if (p.kind === 'SpreadAssignment') {
2771
+ const srcType = inferExpressionType(p.expression);
2772
+ const srcFields = srcType?.includes('struct{')
2773
+ ? parseStructFields(srcType.replace(/^\*/, ''))
2774
+ : [];
2775
+ for (const entry of srcFields) {
2776
+ if (!fields.has(entry[0]))
2777
+ fields.set(entry[0], entry[1]);
2778
+ }
2779
+ if (!sawSpread) {
2780
+ sawSpread = true;
2781
+ steps.push(`__obj := *(${visit(p.expression)})`);
2782
+ }
2783
+ else {
2784
+ steps.push(srcFields
2785
+ .map((entry) => `__obj.${entry[0]} = (${visit(p.expression)}).${entry[0]}`)
2786
+ .join(';\n\t\t'));
2787
+ }
2788
+ continue;
2110
2789
  }
2111
- else if (isShorthandPropertyAssignment(p)) {
2790
+ if ((isMethodDeclaration(p) || isGetAccessor(p)) && isIdentifier(p.name)) {
2112
2791
  const field = goFieldName(p.name.text);
2113
- fields.push(`${field} ${toFieldGoType(inferExpressionType(p.name))}`);
2114
- values.push(`${field}: ${visit(p.name)}`);
2792
+ fields.set(field, methodFuncType(p));
2793
+ const prevName = thisOverrideName;
2794
+ const prevFn = thisOverrideFn;
2795
+ thisOverrideName = '__obj';
2796
+ thisOverrideFn = p;
2797
+ const paramInfo = getFunctionParametersInfo(p.parameters ?? []);
2798
+ const retType = methodFuncType(p).slice(methodFuncType(p).indexOf(')') + 1);
2799
+ const body = visit(p.body, { prefixBlockContent: paramInfo.prefixBlockContent });
2800
+ thisOverrideName = prevName;
2801
+ thisOverrideFn = prevFn;
2802
+ steps.push(`__obj.${field} = func(${paramInfo.signature})${retType} ${body.trimEnd()}`);
2803
+ continue;
2804
+ }
2805
+ if (isSetAccessor(p))
2806
+ continue;
2807
+ const isProp = isPropertyAssignment(p);
2808
+ if (!isProp && !isShorthandPropertyAssignment(p))
2809
+ continue;
2810
+ const field = goFieldName(p.name.text);
2811
+ const valueNode = isProp ? p.initializer : p.name;
2812
+ if (sawSpread) {
2813
+ steps.push(`__obj.${field} = ${toGoValueOfType(valueNode, fields.get(field))}`);
2814
+ }
2815
+ else {
2816
+ fields.set(field, toFieldGoType(inferExpressionType(valueNode)));
2817
+ values.push(`${field}: ${visit(valueNode)}`);
2818
+ }
2819
+ if (sawSpread && !fields.has(field)) {
2820
+ fields.set(field, toFieldGoType(inferExpressionType(valueNode)));
2115
2821
  }
2116
2822
  }
2117
- return `&struct{ ${fields.join('; ')} }${compositeBody(values)}`;
2823
+ const structType = `struct{ ${structFieldsBody(fields)} }`;
2824
+ if (steps.length === 0) {
2825
+ return `&${structType}${compositeBody(values)}`;
2826
+ }
2827
+ const self = sawSpread ? '__obj' : getTempName('obj');
2828
+ const lines = [];
2829
+ if (sawSpread) {
2830
+ for (const step of steps)
2831
+ lines.push(step);
2832
+ lines.push('return &__obj');
2833
+ }
2834
+ else {
2835
+ lines.push(`${self} := &${structType}${compositeBody(values)}`);
2836
+ for (const step of steps)
2837
+ lines.push(step.replace(/__obj\./g, `${self}.`));
2838
+ lines.push(`return ${self}`);
2839
+ }
2840
+ return `func() *${structType} {\n\t\t${lines.join(';\n\t\t')}\n\t}()`;
2118
2841
  }
2119
2842
  // Follows type aliases and strips null/undefined from unions
2120
2843
  function resolveTypeNode(typeNode, depth = 0) {
@@ -2151,6 +2874,17 @@ function getEnclosingFunction(node) {
2151
2874
  }
2152
2875
  return undefined;
2153
2876
  }
2877
+ // Name of the class declaration enclosing this node, if any
2878
+ function getEnclosingClassName(node) {
2879
+ let current = node;
2880
+ while (current) {
2881
+ if (isClassDeclaration(current) && current.name && isIdentifier(current.name)) {
2882
+ return current.name.text;
2883
+ }
2884
+ current = current.parent;
2885
+ }
2886
+ return undefined;
2887
+ }
2154
2888
  // Declared return type of a function, unwrapping Promise<T> for async functions
2155
2889
  function getReturnTypeNode(fn) {
2156
2890
  const typeNode = fn?.type ?? (fn ? getContextualFunctionType(fn)?.type : undefined);
@@ -2282,32 +3016,64 @@ function getContextualTypeNode(node) {
2282
3016
  // variable/parameter/return type), else inferred from its first typed element
2283
3017
  function getArrayLiteralElementType(node) {
2284
3018
  const contextual = resolveTypeNode(getContextualTypeNode(node));
2285
- if (isArrayTypeNode(contextual))
2286
- return getType(contextual.elementType);
3019
+ if (isArrayTypeNode(contextual)) {
3020
+ const elem = getType(contextual.elementType);
3021
+ // An unbound type parameter (call-site []T) resolves from the elements
3022
+ if (!isUnboundGoType(elem))
3023
+ return elem;
3024
+ }
2287
3025
  if (contextual?.kind === 'TupleType')
2288
3026
  return getTupleElementType(contextual);
2289
3027
  if (isTypeReferenceNode(contextual) &&
2290
3028
  isIdentifier(contextual.typeName) &&
2291
3029
  contextual.typeName.text === 'Array' &&
2292
3030
  contextual.typeArguments?.length === 1) {
2293
- return getType(contextual.typeArguments[0]);
3031
+ const elem = getType(contextual.typeArguments[0]);
3032
+ if (!isUnboundGoType(elem))
3033
+ return elem;
2294
3034
  }
2295
3035
  for (const element of node.elements ?? []) {
2296
3036
  if (isSpreadElement(element)) {
2297
3037
  const spreadType = inferExpressionType(element.expression);
2298
3038
  if (spreadType?.startsWith('[]'))
2299
3039
  return spreadType.slice(2);
3040
+ if (spreadType === 'string')
3041
+ return 'string';
2300
3042
  if (spreadType?.startsWith('*TnMap[') && extractMapValueType(spreadType) === 'struct{}') {
2301
3043
  return extractMapKeyType(spreadType);
2302
3044
  }
2303
3045
  continue;
2304
3046
  }
2305
3047
  const elementType = inferExpressionType(element);
2306
- if (elementType && elementType !== 'nil')
3048
+ if (elementType && elementType !== 'nil' && !isUnboundGoType(elementType))
2307
3049
  return elementType;
2308
3050
  }
2309
3051
  return 'interface{}';
2310
3052
  }
3053
+ // A Go type that is a generic type parameter of the enclosing function (T)
3054
+ // rather than a concrete type: unresolvable at the call site
3055
+ function isUnboundGoType(goType) {
3056
+ if (!goType)
3057
+ return false;
3058
+ // Only a bare identifier can be a type parameter; composite types are concrete
3059
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(goType))
3060
+ return false;
3061
+ if (['float64', 'string', 'bool', 'int', 'interface{}', 'byte', 'rune'].includes(goType))
3062
+ return false;
3063
+ return (!classNames.has(goType) &&
3064
+ !interfacePropertyTypes.has(goType) &&
3065
+ !enumNames.has(goType) &&
3066
+ !typeAliases.has(goType) &&
3067
+ !declaredInterfaces.has(goType));
3068
+ }
3069
+ // A field-name → type map rendered as a Go struct type body
3070
+ function structFieldsBody(fields) {
3071
+ const parts = [];
3072
+ fields.forEach((fieldType, fieldName) => {
3073
+ parts.push(`${fieldName} ${fieldType}`);
3074
+ });
3075
+ return parts.join('; ');
3076
+ }
2311
3077
  // Loop bodies must be blocks in Go; `prefix` is emitted before the body's statements
2312
3078
  function visitLoopBody(statement, prefix = '') {
2313
3079
  if (isBlock(statement))
@@ -2353,12 +3119,16 @@ function getTypeText(typeNode) {
2353
3119
  function toGoStringLiteral(value) {
2354
3120
  return JSON.stringify(value);
2355
3121
  }
2356
- // A spread source as a Go slice; Sets spread their elements (map keys)
3122
+ // A spread source as a Go slice; Sets spread their elements (map keys) and
3123
+ // strings spread into []string of runes (like JS [...str])
2357
3124
  function visitSpreadSource(expr) {
2358
3125
  const sourceType = inferExpressionType(expr);
2359
3126
  if (sourceType?.startsWith('*TnMap[') && extractMapValueType(sourceType) === 'struct{}') {
2360
3127
  return `${visit(expr)}.Keys()`;
2361
3128
  }
3129
+ if (sourceType === 'string') {
3130
+ return `func() []string { __parts := []string{}; for _, __r := range ${visit(expr)} { __parts = append(__parts, string(__r)) }; return __parts }()`;
3131
+ }
2362
3132
  return visit(expr);
2363
3133
  }
2364
3134
  function visitSpreadArrayLiteral(node, elemType) {
@@ -2412,6 +3182,17 @@ function visitTemplateExpression(node) {
2412
3182
  }
2413
3183
  return parts.join(' + ');
2414
3184
  }
3185
+ // An expression-bodied arrow whose callee emits Go statements (no value):
3186
+ // console.log(...), assert(...)
3187
+ function isVoidStatementCall(expr) {
3188
+ if (!isCallExpression(expr))
3189
+ return false;
3190
+ if (isIdentifier(expr.expression))
3191
+ return expr.expression.text === 'assert';
3192
+ return (isPropertyAccessExpression(expr.expression) &&
3193
+ isIdentifier(expr.expression.expression) &&
3194
+ expr.expression.expression.text === 'console');
3195
+ }
2415
3196
  function hasQuestionDot(node) {
2416
3197
  return ('questionDotToken' in node &&
2417
3198
  !!node.questionDotToken);
@@ -2446,12 +3227,12 @@ function prescanVariableDeclarations(block) {
2446
3227
  for (const decl of stmt.declarationList.declarations) {
2447
3228
  if (isIdentifier(decl.name) && !variableGoTypes.has(decl.name.text)) {
2448
3229
  if (decl.type) {
2449
- variableGoTypes.set(decl.name.text, getType(decl.type));
3230
+ setVarGoType(decl.name.text, getType(decl.type));
2450
3231
  }
2451
3232
  else if (decl.initializer) {
2452
3233
  const inferredType = inferExpressionType(decl.initializer);
2453
3234
  if (inferredType)
2454
- variableGoTypes.set(decl.name.text, inferredType);
3235
+ setVarGoType(decl.name.text, inferredType);
2455
3236
  }
2456
3237
  }
2457
3238
  }
@@ -2492,6 +3273,11 @@ function inferArrowFunctionGoType(node) {
2492
3273
  return `func(${params})${retType ? ` ${retType}` : ''}`;
2493
3274
  }
2494
3275
  function inferExpressionType(expr) {
3276
+ if (isAwaitExpression(expr)) {
3277
+ // await unwraps the promise channel
3278
+ const inner = inferExpressionType(expr.expression);
3279
+ return inner?.startsWith('chan ') ? inner.slice(5) || 'struct{}' : 'interface{}';
3280
+ }
2495
3281
  if (isArrowFunction(expr) || isFunctionExpression(expr)) {
2496
3282
  return inferArrowFunctionGoType(expr);
2497
3283
  }
@@ -2512,10 +3298,17 @@ function inferExpressionType(expr) {
2512
3298
  }
2513
3299
  if (isNumericLiteral(expr))
2514
3300
  return 'float64';
3301
+ if (expr.kind === 'BigIntLiteral')
3302
+ return '*tnbig.Int';
2515
3303
  if (expr.kind === 'TypeOfExpression')
2516
3304
  return 'string';
2517
- if (isRegularExpressionLiteral(expr))
3305
+ if (isRegularExpressionLiteral(expr)) {
3306
+ const lastSlash = expr.text.lastIndexOf('/');
3307
+ if (needsStatefulRegex(expr.text.substring(1, lastSlash), expr.text.substring(lastSlash + 1))) {
3308
+ return '*TnRegex';
3309
+ }
2518
3310
  return '*regexp.Regexp';
3311
+ }
2519
3312
  if (isNewExpression(expr) && isIdentifier(expr.expression) && expr.expression.text === 'RegExp') {
2520
3313
  return '*regexp.Regexp';
2521
3314
  }
@@ -2526,7 +3319,10 @@ function inferExpressionType(expr) {
2526
3319
  if (isIdentifier(expr) && expr.text === 'undefined')
2527
3320
  return 'nil';
2528
3321
  if (isIdentifier(expr)) {
2529
- const varType = variableGoTypes.get(expr.text);
3322
+ let varType = variableGoTypes.get(expr.text);
3323
+ // Array references are read as the slice they point to
3324
+ if (varType?.startsWith('*[]'))
3325
+ varType = varType.slice(1);
2530
3326
  return narrowedVariables.has(expr.text) && varType?.startsWith('*') ? varType.slice(1) : varType;
2531
3327
  }
2532
3328
  if (isArrayLiteralExpression(expr)) {
@@ -2536,7 +3332,20 @@ function inferExpressionType(expr) {
2536
3332
  return getObjectLiteralGoType(expr);
2537
3333
  if (isNewExpression(expr) && isIdentifier(expr.expression)) {
2538
3334
  const ctorName = expr.expression.text;
3335
+ if (classNames.has(ctorName))
3336
+ return `*${ctorName}`;
3337
+ if (ctorName === 'Error')
3338
+ return '*TnError';
3339
+ if (ctorName === 'Promise' && (expr.typeArguments ?? []).length > 0) {
3340
+ return `chan ${getType(expr.typeArguments[0]) || 'struct{}'}`;
3341
+ }
2539
3342
  const typeArguments = getCollectionTypeArguments(expr);
3343
+ if (ctorName === 'Array' && (expr.typeArguments ?? []).length === 1) {
3344
+ return `[]${getType(expr.typeArguments[0])}`;
3345
+ }
3346
+ if (ctorName === 'Date') {
3347
+ return 'time.Time';
3348
+ }
2540
3349
  if (ctorName === 'Map' && typeArguments.length === 2) {
2541
3350
  return mapGoType(getType(typeArguments[0]), getType(typeArguments[1]));
2542
3351
  }
@@ -2565,6 +3374,10 @@ function inferExpressionType(expr) {
2565
3374
  expr.name.text === 'argv') {
2566
3375
  return '[]string';
2567
3376
  }
3377
+ // import.meta.url is a string
3378
+ if (isPropertyAccessExpression(expr) && expr.expression.kind === 'MetaProperty' && expr.name.text === 'url') {
3379
+ return 'string';
3380
+ }
2568
3381
  if (isIdentifier(expr) && (expr.text === 'NaN' || expr.text === 'Infinity'))
2569
3382
  return 'float64';
2570
3383
  if (isPropertyAccessExpression(expr) && isIdentifier(expr.expression) && expr.expression.text === 'Number') {
@@ -2583,6 +3396,23 @@ function inferExpressionType(expr) {
2583
3396
  : isPropertyAccessExpression(expr.expression) && isIdentifier(expr.expression.expression)
2584
3397
  ? `${expr.expression.expression.text}.${expr.expression.name.text}`
2585
3398
  : undefined;
3399
+ // Promise.all([...]) resolves to a slice over a channel
3400
+ if (isPropertyAccessExpression(expr.expression) &&
3401
+ isIdentifier(expr.expression.expression) &&
3402
+ expr.expression.expression.text === 'Promise' &&
3403
+ expr.expression.name.text === 'all') {
3404
+ const elem = promiseAllElementType(expr);
3405
+ if (elem)
3406
+ return `chan []${elem}`;
3407
+ return 'chan []interface{}';
3408
+ }
3409
+ // Generator functions return a channel of yielded values
3410
+ if (isIdentifier(expr.expression) &&
3411
+ generatorFunctionNames.has(expr.expression.text)) {
3412
+ const fn = declaredFunctions.get(expr.expression.text);
3413
+ if (fn)
3414
+ return `chan ${getGeneratorYieldType(fn)}`;
3415
+ }
2586
3416
  const nodeResultType = callee ? nodeCallResultTypes.get(callee) : undefined;
2587
3417
  if (callee && BUILTIN_CALL_TYPES[callee])
2588
3418
  return BUILTIN_CALL_TYPES[callee];
@@ -2617,6 +3447,13 @@ function inferExpressionType(expr) {
2617
3447
  if ((isPropertyAccessExpression(expr) || isElementAccessExpression(expr)) && isDynamicValue(expr.expression)) {
2618
3448
  return 'interface{}';
2619
3449
  }
3450
+ if (isCallExpression(expr) &&
3451
+ isPropertyAccessExpression(expr.expression) &&
3452
+ isIdentifier(expr.expression.expression) &&
3453
+ expr.expression.expression.text === 'Array' &&
3454
+ expr.expression.name.text === 'from') {
3455
+ return '[]interface{}';
3456
+ }
2620
3457
  if (isCallExpression(expr) && isObjectKeysOfDynamic(expr))
2621
3458
  return '[]string';
2622
3459
  if (isCallExpression(expr) &&
@@ -2651,6 +3488,9 @@ function inferExpressionType(expr) {
2651
3488
  return nullableType.slice(1);
2652
3489
  }
2653
3490
  if (isIdentifier(expr.expression) && enumNames.has(expr.expression.text)) {
3491
+ // Numeric enums are float64 constants; string enums keep their type
3492
+ if (enumBaseTypes.get(expr.expression.text) === 'float64')
3493
+ return 'float64';
2654
3494
  const enumType = getSafeName(expr.expression.text);
2655
3495
  return enumType;
2656
3496
  }
@@ -2713,12 +3553,29 @@ function inferExpressionType(expr) {
2713
3553
  if (stringMethodType)
2714
3554
  return stringMethodType;
2715
3555
  }
2716
- if (ownerType === '*regexp.Regexp') {
2717
- if (methodName === 'exec')
3556
+ if (ownerType === '*regexp.Regexp' || ownerType === '*TnRegex') {
3557
+ if (methodName === 'exec') {
3558
+ // Named-group patterns return a match object with a .groups record
3559
+ const receiver = expr.expression.expression;
3560
+ if (isRegularExpressionLiteral(receiver) && /\(\?<[A-Za-z_]/.test(receiver.text ?? '')) {
3561
+ useHelper('namedGroups');
3562
+ return '*TnMatch';
3563
+ }
2718
3564
  return '[]string';
3565
+ }
2719
3566
  if (methodName === 'test')
2720
3567
  return 'bool';
2721
3568
  }
3569
+ if (ownerType === 'time.Time') {
3570
+ if (methodName === 'getTime' || methodName === 'valueOf' || methodName === 'getMilliseconds' ||
3571
+ methodName === 'getSeconds' || methodName === 'getMinutes' || methodName === 'getHours' ||
3572
+ methodName === 'getDate' || methodName === 'getDay' || methodName === 'getMonth' ||
3573
+ methodName === 'getFullYear')
3574
+ return 'float64';
3575
+ if (methodName === 'toISOString' || methodName === 'toJSON' || methodName === 'toString' ||
3576
+ methodName === 'toDateString' || methodName === 'toTimeString')
3577
+ return 'string';
3578
+ }
2722
3579
  if (ownerType && ownerType.startsWith('*TnMap[')) {
2723
3580
  if (methodName === 'has')
2724
3581
  return 'bool';
@@ -2747,6 +3604,10 @@ function inferExpressionType(expr) {
2747
3604
  return elementType;
2748
3605
  if (methodName === 'flat' && elementType.startsWith('[]'))
2749
3606
  return elementType;
3607
+ if (methodName === 'flat' || methodName === 'flatMap')
3608
+ return `[]${elementType}`;
3609
+ if (methodName === 'entries')
3610
+ return '[][]interface{}';
2750
3611
  if (methodName === 'find')
2751
3612
  return elementType;
2752
3613
  if (methodName === 'join')
@@ -2803,6 +3664,11 @@ function inferExpressionType(expr) {
2803
3664
  if (COMPARISON_OPERATORS.has(kind) || kind === 'InKeyword' || kind === 'InstanceOfKeyword') {
2804
3665
  return 'bool';
2805
3666
  }
3667
+ // BigInt operands stay bigint
3668
+ if (['AsteriskToken', 'SlashToken', 'PercentToken', 'AsteriskAsteriskToken', 'PlusToken', 'MinusToken'].includes(kind) &&
3669
+ (inferExpressionType(expr.left) === '*tnbig.Int' || inferExpressionType(expr.right) === '*tnbig.Int')) {
3670
+ return '*tnbig.Int';
3671
+ }
2806
3672
  if (isLogicalOperator(expr.operatorToken)) {
2807
3673
  return isLogicalValueExpression(expr) ? getLogicalValueType(expr) : 'bool';
2808
3674
  }
@@ -2835,8 +3701,17 @@ const BUILTIN_CALL_TYPES = {
2835
3701
  'Number.isNaN': 'bool',
2836
3702
  'Number.isFinite': 'bool',
2837
3703
  'Number.isInteger': 'bool',
2838
- isNaN: 'bool'
3704
+ isNaN: 'bool',
3705
+ 'Object.assign': '*TnMap[string, interface{}]',
3706
+ 'Date.now': 'float64'
2839
3707
  };
3708
+ // Element type of `await Promise.all([...])` from the first promise channel
3709
+ function promiseAllElementType(expr) {
3710
+ const arrayNode = (expr.arguments ?? [])[0];
3711
+ const first = arrayNode?.elements?.[0];
3712
+ const chanType = first ? inferExpressionType(first) : undefined;
3713
+ return chanType?.startsWith('chan ') ? chanType.slice(5) : undefined;
3714
+ }
2840
3715
  const BUILTIN_FUNCTION_TYPES = {
2841
3716
  parseFloat: 'float64',
2842
3717
  parseInt: 'float64',
@@ -2868,7 +3743,8 @@ const STRING_METHOD_RETURN_TYPES = {
2868
3743
  indexOf: 'float64',
2869
3744
  lastIndexOf: 'float64',
2870
3745
  search: 'float64',
2871
- charCodeAt: 'float64'
3746
+ charCodeAt: 'float64',
3747
+ localeCompare: 'float64'
2872
3748
  };
2873
3749
  // Return type of a Go func type string: `func(a T) R` → `R`
2874
3750
  function goFuncReturnType(funcType) {
@@ -3128,8 +4004,12 @@ function isNilableGoType(goType) {
3128
4004
  function visitOptionalPropertyAccess(node) {
3129
4005
  const baseExpr = visit(node.expression);
3130
4006
  const baseType = inferExpressionType(node.expression);
3131
- if (!baseType || !baseType.startsWith('*')) {
4007
+ if (!baseType || !baseType.startsWith('*') || baseType.startsWith('*TnMap[')) {
4008
+ // Record/Map members are key lookups even through optional chains
3132
4009
  const objectType = resolveExpressionType(node.expression);
4010
+ if (objectType === 'Map' || objectType === 'Record') {
4011
+ return `${baseExpr}.Get(${toGoStringLiteral(node.name.text)})`;
4012
+ }
3133
4013
  return getAcessString(baseExpr, visit(node.name), objectType);
3134
4014
  }
3135
4015
  const className = baseType.replace(/^\*/, '').replace(/\[.*\]$/, '');
@@ -3309,7 +4189,7 @@ function visitArrayHigherOrderCall(node) {
3309
4189
  if (!isPropertyAccessExpression(node.expression))
3310
4190
  return undefined;
3311
4191
  const methodName = node.expression.name.text;
3312
- if (!['map', 'filter', 'some', 'find', 'findIndex', 'every', 'forEach', 'reduce', 'join'].includes(methodName)) {
4192
+ if (!['map', 'filter', 'some', 'find', 'findIndex', 'findLast', 'findLastIndex', 'every', 'forEach', 'reduce', 'join', 'entries', 'flatMap'].includes(methodName)) {
3313
4193
  return undefined;
3314
4194
  }
3315
4195
  const arrayExprNode = node.expression.expression;
@@ -3339,11 +4219,18 @@ function visitArrayHigherOrderCall(node) {
3339
4219
  return undefined;
3340
4220
  importedPackages.add('strings');
3341
4221
  importedPackages.add('fmt');
3342
- const separator = (node.arguments ?? [])[0] ? visit((node.arguments ?? [])[0]) : '""';
4222
+ // join() with no argument defaults to "," in JS
4223
+ const separator = (node.arguments ?? [])[0] ? visit((node.arguments ?? [])[0]) : '","';
3343
4224
  const arrVar = getTempName('arrjoin');
3344
4225
  const partsVar = getTempName('parts');
3345
4226
  return `func() string { ${arrVar} := ${arrayExpr}; ${partsVar} := make([]string, len(${arrVar})); for i, v := range ${arrVar} { ${partsVar}[i] = fmt.Sprintf("%v", v) }; return strings.Join(${partsVar}, ${separator}) }()`;
3346
4227
  }
4228
+ if (methodName === 'entries') {
4229
+ // [[index, value], ...] pairs, destructured by for...of
4230
+ if (!isArrayLikeGoType(ownerType))
4231
+ return undefined;
4232
+ return `func() [][]interface{} { __out := [][]interface{}{}; for __i, __v := range ${arrayExpr} { __out = append(__out, []interface{}{float64(__i), __v}) }; return __out }()`;
4233
+ }
3347
4234
  const callback = (node.arguments ?? [])[0];
3348
4235
  if (!callback) {
3349
4236
  return undefined;
@@ -3351,6 +4238,16 @@ function visitArrayHigherOrderCall(node) {
3351
4238
  const arrVar = getTempName('arrhof');
3352
4239
  const idxVar = getTempName('i');
3353
4240
  const itemVar = getTempName('item');
4241
+ if (methodName === 'flatMap') {
4242
+ const callbackInfo = buildArrayCallbackInfo(callback, elementType);
4243
+ const innerType = callbackInfo.returnType.startsWith('[]')
4244
+ ? callbackInfo.returnType.slice(2)
4245
+ : elementType;
4246
+ const resultVar = getTempName('flatres');
4247
+ const callbackCall = buildArrayCallbackInvocation(callbackInfo, itemVar, idxVar, arrVar);
4248
+ const rangeIndexVar = callbackInfo.paramCount > 1 ? idxVar : '_';
4249
+ return `func() []${innerType} { ${arrVar} := ${arrayExpr}; ${resultVar} := []${innerType}{}; for ${rangeIndexVar}, ${itemVar} := range ${arrVar} { ${resultVar} = append(${resultVar}, (${callbackCall})...) }; return ${resultVar} }()`;
4250
+ }
3354
4251
  if (methodName === 'map') {
3355
4252
  const callbackInfo = buildArrayCallbackInfo(callback, elementType);
3356
4253
  const mappedType = callbackInfo.returnType || 'interface{}';
@@ -3377,6 +4274,14 @@ function visitArrayHigherOrderCall(node) {
3377
4274
  const callbackCall = buildArrayCallbackInvocation(callbackInfo, itemVar, idxVar, arrVar);
3378
4275
  return `func() float64 { ${arrVar} := ${arrayExpr}; for ${idxVar}, ${itemVar} := range ${arrVar} { if ${callbackCall} { return float64(${idxVar}) } }; return float64(-1) }()`;
3379
4276
  }
4277
+ if (methodName === 'findLastIndex' || methodName === 'findLast') {
4278
+ // Search from the end
4279
+ const callbackInfo = buildArrayCallbackInfo(callback, elementType, 'bool');
4280
+ const callbackCall = buildArrayCallbackInvocation(callbackInfo, itemVar, idxVar, arrVar);
4281
+ const retType = methodName === 'findLast' ? elementType : 'float64';
4282
+ const hit = methodName === 'findLast' ? itemVar : `float64(${idxVar})`;
4283
+ return `func() ${retType} { ${arrVar} := ${arrayExpr}; for ${idxVar} := len(${arrVar}) - 1; ${idxVar} >= 0; ${idxVar}-- { ${itemVar} := ${arrVar}[${idxVar}]; if ${callbackCall} { return ${hit} } }; ${methodName === 'findLast' ? `var __zero ${elementType}; return __zero` : 'return float64(-1)'} }()`;
4284
+ }
3380
4285
  if (methodName === 'every') {
3381
4286
  const callbackInfo = buildArrayCallbackInfo(callback, elementType, 'bool');
3382
4287
  const rangeIndexVar = callbackInfo.paramCount > 1 ? idxVar : '_';
@@ -3513,6 +4418,21 @@ function visitEnumDeclaration(node) {
3513
4418
  }
3514
4419
  members.push(`\t${symbolName} ${enumName} = ${enumName}(${valueExpr})`);
3515
4420
  }
4421
+ // Numeric enums are plain float64 constants so they compare against numbers;
4422
+ // string enums keep a named type
4423
+ if (baseType === 'float64') {
4424
+ return `const (\n${members
4425
+ .map((m) => {
4426
+ const name = m.slice(m.indexOf('\t') + 1, m.indexOf(' '));
4427
+ const value = m.slice(m.lastIndexOf('=') + 1).trim();
4428
+ // Strip the enum conversion wrapper: Dir(0) → 0
4429
+ const plain = value.startsWith(`${enumName}(`) && value.endsWith(')')
4430
+ ? value.slice(enumName.length + 1, -1)
4431
+ : value;
4432
+ return `\t${name} float64 = ${plain}`;
4433
+ })
4434
+ .join('\n')}\n)`;
4435
+ }
3516
4436
  return `type ${enumName} ${baseType}\n\nvar (\n${members.join('\n')}\n)`;
3517
4437
  }
3518
4438
  function getType(typeNode, getArrayType = false) {
@@ -3553,9 +4473,21 @@ function getType(typeNode, getArrayType = false) {
3553
4473
  const ret = typeNode.type ? ` ${getType(typeNode.type)}` : '';
3554
4474
  return `func(${params})${ret}`;
3555
4475
  }
4476
+ // Type guard: function isCat(p): p is Cat → Go bool
4477
+ if (typeNode.kind === 'TypePredicate') {
4478
+ return 'bool';
4479
+ }
4480
+ // symbol → a unique pointer type usable as a map key
4481
+ if (typeNode.kind === 'SymbolKeyword') {
4482
+ useHelper('symbol');
4483
+ return '*TnSymbol';
4484
+ }
3556
4485
  if (isTypeReferenceNode(typeNode) && isIdentifier(typeNode.typeName)) {
3557
4486
  const name = typeNode.typeName.text;
3558
4487
  if (enumNames.has(name)) {
4488
+ // Numeric enums lowered to float64 constants have no named type
4489
+ if (enumBaseTypes.get(name) === 'float64')
4490
+ return 'float64';
3559
4491
  return getSafeName(name);
3560
4492
  }
3561
4493
  const aliasType = getAliasType(name);
@@ -3563,14 +4495,21 @@ function getType(typeNode, getArrayType = false) {
3563
4495
  return getType(aliasType, getArrayType);
3564
4496
  }
3565
4497
  if (name === 'Promise' && typeNode.typeArguments && typeNode.typeArguments.length > 0) {
3566
- return `chan ${getType(typeNode.typeArguments[0])}`;
4498
+ const inner = getType(typeNode.typeArguments[0]);
4499
+ return `chan ${inner || 'struct{}'}`;
3567
4500
  }
3568
4501
  if (name === 'RegExp') {
3569
4502
  return '*regexp.Regexp';
3570
4503
  }
4504
+ if (name === 'Error') {
4505
+ return '*TnError';
4506
+ }
3571
4507
  if (name === 'RegExpExecArray' || name === 'RegExpMatchArray') {
3572
4508
  return '[]string';
3573
4509
  }
4510
+ if (name === 'TemplateStringsArray' || name === 'ReadonlyArray') {
4511
+ return name === 'TemplateStringsArray' ? '[]string' : getType(typeNode.typeArguments?.[0]);
4512
+ }
3574
4513
  if ((name === 'Map' || name === 'Record') &&
3575
4514
  typeNode.typeArguments &&
3576
4515
  typeNode.typeArguments.length === 2) {
@@ -3675,12 +4614,16 @@ function goTypeCategory(goType) {
3675
4614
  return undefined;
3676
4615
  if (goType.startsWith('*TnMap['))
3677
4616
  return extractMapValueType(goType) === 'struct{}' ? 'Set' : 'Map';
4617
+ if (goType.startsWith('*[]'))
4618
+ goType = goType.slice(1);
3678
4619
  if (goType.startsWith('[]'))
3679
4620
  return 'array';
3680
4621
  if (goType === 'string')
3681
4622
  return 'string';
3682
- if (goType === '*regexp.Regexp')
4623
+ if (goType === '*regexp.Regexp' || goType === '*TnRegex')
3683
4624
  return 'RegExp';
4625
+ if (goType === 'time.Time')
4626
+ return 'Date';
3684
4627
  return undefined;
3685
4628
  }
3686
4629
  function isNilLiteral(node) {
@@ -3691,6 +4634,11 @@ function isNilLiteral(node) {
3691
4634
  return false;
3692
4635
  }
3693
4636
  function getAcessString(leftSide, rightSide, objectType) {
4637
+ if (rightSide === 'length' && objectType === 'string') {
4638
+ // JS .length counts UTF-16 code units; rune count matches for the BMP
4639
+ importedPackages.add('unicode/utf8');
4640
+ return `float64(utf8.RuneCountInString(${leftSide}))`;
4641
+ }
3694
4642
  if (rightSide === 'length' && objectType !== 'class') {
3695
4643
  return `float64(len(${leftSide}))`;
3696
4644
  }
@@ -3814,6 +4762,7 @@ const callHandlers = {
3814
4762
  },
3815
4763
  'JSON.stringify': (_caller, args) => {
3816
4764
  useHelper('jsonStringify');
4765
+ useHelper('dynamic');
3817
4766
  return `TnJSONStringify(${args[0]}, ${args.length >= 3 ? '" "' : '""'})`;
3818
4767
  },
3819
4768
  'JSON.parse': (_caller, args) => {
@@ -3824,6 +4773,17 @@ const callHandlers = {
3824
4773
  'Object.values': (_caller, args) => `${args[0]}.Values()`,
3825
4774
  // Outside for...of (which iterates the map directly): [key, value] pairs
3826
4775
  'Object.entries': (_caller, args) => `${args[0]}.Entries()`,
4776
+ // Object.assign copies sources into a fresh ordered map (mutating structs is
4777
+ // not observable the way it is in JS)
4778
+ 'Object.assign': (_caller, args) => {
4779
+ useHelper('objectAssign');
4780
+ return `TnObjectAssign(${args.join(', ')})`;
4781
+ },
4782
+ // Promise.all: drain every promise channel, collect the results
4783
+ 'Promise.all': (_caller, args) => {
4784
+ useHelper('promiseAll');
4785
+ return `TnPromiseAll(${args[0] ?? '[]chan interface{}{}'})`;
4786
+ },
3827
4787
  'Math.log': (_caller, args) => {
3828
4788
  importedPackages.add('math');
3829
4789
  return `math.Log(${args[0]})`;
@@ -3855,6 +4815,10 @@ const callHandlers = {
3855
4815
  'Math.sign': (_caller, args) => {
3856
4816
  return `func() float64 { if ${args[0]} > 0 { return 1 }; if ${args[0]} < 0 { return -1 }; return 0 }()`;
3857
4817
  },
4818
+ 'Date.now': () => {
4819
+ importedPackages.add('time');
4820
+ return 'float64(time.Now().UnixMilli())';
4821
+ },
3858
4822
  'console.error': (_caller, args) => {
3859
4823
  importedPackages.add('fmt');
3860
4824
  importedPackages.add('os');
@@ -3873,6 +4837,12 @@ const callHandlers = {
3873
4837
  importedPackages.add('fmt');
3874
4838
  return `fmt.Sprintf("%v", ${args[0]})`;
3875
4839
  },
4840
+ 'String.fromCharCode': (_caller, args) => `string(rune(int(${args[0] ?? '0'})))`,
4841
+ // Symbol(description) — unique values for record keys
4842
+ Symbol: (_caller, args) => {
4843
+ useHelper('symbol');
4844
+ return `TnNewSymbol(${args[0] ?? '""'})`;
4845
+ },
3876
4846
  'Number': (_caller, args) => {
3877
4847
  useHelper('dynamic');
3878
4848
  return `TnNumber(${args[0]})`;
@@ -3901,6 +4871,10 @@ const stringMethodHandlers = {
3901
4871
  split: (obj, args) => {
3902
4872
  if (args[0]?.startsWith('regexp.MustCompile('))
3903
4873
  return `${args[0]}.Split(${obj}, -1)`;
4874
+ if (isTnRegexArg(args[0])) {
4875
+ useHelper('fancyRegex');
4876
+ return `${args[0]}.SplitStr(${obj}, -1)`;
4877
+ }
3904
4878
  importedPackages.add('strings');
3905
4879
  return `strings.Split(${obj}, ${args[0]})`;
3906
4880
  },
@@ -3928,6 +4902,11 @@ const stringMethodHandlers = {
3928
4902
  importedPackages.add('strings');
3929
4903
  return `float64(strings.Index(${obj}, ${args[0]}))`;
3930
4904
  },
4905
+ charCodeAt: (obj, args) => `float64(${obj}[int(${args[0]})])`,
4906
+ localeCompare: (obj, args) => {
4907
+ importedPackages.add('strings');
4908
+ return `float64(strings.Compare(${obj}, ${args[0]}))`;
4909
+ },
3931
4910
  includes: (obj, args) => {
3932
4911
  importedPackages.add('strings');
3933
4912
  return `strings.Contains(${obj}, ${args[0]})`;
@@ -3974,10 +4953,28 @@ const stringMethodHandlers = {
3974
4953
  const pad = args[1] ?? '" "';
3975
4954
  return `func() string { __s := ${obj}; __n := int(${args[0]}) - len(__s); if __n > 0 { __s = __s + strings.Repeat(${pad}, __n)[:__n] }; return __s }()`;
3976
4955
  },
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) => {
4956
+ match: (obj, args) => {
4957
+ if (isTnRegexArg(args[0])) {
4958
+ useHelper('fancyRegex');
4959
+ return `${args[0]}.FindSub(${obj})`;
4960
+ }
4961
+ return `${toGoRegexp(args[0])}.FindStringSubmatch(${obj})`;
4962
+ },
4963
+ matchAll: (obj, args) => {
4964
+ if (isTnRegexArg(args[0])) {
4965
+ useHelper('fancyRegex');
4966
+ return `${args[0]}.FindAllSub(${obj})`;
4967
+ }
4968
+ return `${toGoRegexp(args[0])}.FindAllStringSubmatch(${obj}, -1)`;
4969
+ },
4970
+ search: (obj, args) => {
4971
+ if (isTnRegexArg(args[0])) {
4972
+ useHelper('fancyRegex');
4973
+ return `func() float64 { __loc := ${args[0]}.FindIndexSub(${obj}); if __loc == nil { return -1 }; return float64(__loc[0]) }()`;
4974
+ }
4975
+ return `func() float64 { __loc := ${toGoRegexp(args[0])}.FindStringIndex(${obj}); if __loc == nil { return -1 }; return float64(__loc[0]) }()`;
4976
+ },
4977
+ lastIndexOf: (obj, args) => {
3981
4978
  importedPackages.add('strings');
3982
4979
  return `float64(strings.LastIndex(${obj}, ${args[0]}))`;
3983
4980
  },
@@ -3990,8 +4987,36 @@ const stringMethodHandlers = {
3990
4987
  }
3991
4988
  };
3992
4989
  const regexpMethodHandlers = {
3993
- test: (obj, args) => `${obj}.MatchString(${args[0]})`,
3994
- exec: (obj, args) => `${obj}.FindStringSubmatch(${args[0]})`
4990
+ test: (obj, args) => currentReceiverGoType === '*TnRegex'
4991
+ ? `${obj}.Test(${args[0]})`
4992
+ : `${obj}.MatchString(${args[0]})`,
4993
+ exec: (obj, args) => {
4994
+ if (currentReceiverGoType === '*TnRegex')
4995
+ return `${obj}.Find(${args[0]})`;
4996
+ // Named groups come back as a match object with a .groups record
4997
+ if (obj.includes('(?P<')) {
4998
+ useHelper('namedGroups');
4999
+ return `TnRegexExecNamed(${obj}, ${args[0]})`;
5000
+ }
5001
+ return `${obj}.FindStringSubmatch(${args[0]})`;
5002
+ }
5003
+ };
5004
+ const dateMethodHandlers = {
5005
+ getTime: (obj) => `float64((${obj}).UnixMilli())`,
5006
+ getMilliseconds: (obj) => `float64((${obj}).Millisecond())`,
5007
+ getSeconds: (obj) => `float64((${obj}).Second())`,
5008
+ getMinutes: (obj) => `float64((${obj}).Minute())`,
5009
+ getHours: (obj) => `float64((${obj}).Hour())`,
5010
+ getDate: (obj) => `float64((${obj}).Day())`,
5011
+ getDay: (obj) => `float64(int((${obj}).Weekday()))`,
5012
+ getMonth: (obj) => `float64(int((${obj}).Month()) - 1)`,
5013
+ getFullYear: (obj) => `float64((${obj}).Year())`,
5014
+ toISOString: (obj) => `(${obj}).UTC().Format("2006-01-01T15:04:05.000Z07:00")`,
5015
+ toJSON: (obj) => `(${obj}).UTC().Format("2006-01-01T15:04:05.000Z07:00")`,
5016
+ toString: (obj) => `(${obj}).UTC().Format("Mon Jan 02 2006 15:04:05 GMT+0000 (UTC)")`,
5017
+ toDateString: (obj) => `(${obj}).UTC().Format("Mon Jan 02 2006")`,
5018
+ toTimeString: (obj) => `(${obj}).UTC().Format("15:04:05 GMT+0000 (UTC)")`,
5019
+ valueOf: (obj) => `float64((${obj}).UnixMilli())`
3995
5020
  };
3996
5021
  const arrayMethodHandlers = {
3997
5022
  push: (obj, args) => `${obj} = append(${obj}, ${args.join(', ')})`,
@@ -4012,7 +5037,7 @@ const arrayMethodHandlers = {
4012
5037
  unshift: (obj, args) => `${obj} = append([]${receiverElementType()}{${args.join(', ')}}, ${obj}...)`,
4013
5038
  join: (obj, args) => {
4014
5039
  importedPackages.add('strings');
4015
- return `strings.Join(${obj}, ${args[0] ?? '""'})`;
5040
+ return `strings.Join(${obj}, ${args[0] ?? '","'})`;
4016
5041
  },
4017
5042
  slice: (obj, args) => {
4018
5043
  if (args.length >= 2)
@@ -4021,7 +5046,29 @@ const arrayMethodHandlers = {
4021
5046
  },
4022
5047
  reverse: (obj) => {
4023
5048
  importedPackages.add('slices');
4024
- return `func() interface{} { slices.Reverse(${obj}); return nil }()`;
5049
+ const arrayType = currentReceiverGoType?.startsWith('[]') ? currentReceiverGoType : '[]interface{}';
5050
+ return `func() ${arrayType} { __s := ${obj}; slices.Reverse(__s); return __s }()`;
5051
+ },
5052
+ // Removes `deleteCount` elements at `start`, returns them; extra args are inserted
5053
+ splice: (obj, args) => {
5054
+ const elementType = receiverElementType();
5055
+ if (args.length === 0)
5056
+ return `[]${elementType}{}`;
5057
+ const start = `${sliceIndex(obj, args[0])}`;
5058
+ if (args.length === 1) {
5059
+ return `func() []${elementType} { __rm := append([]${elementType}{}, ${obj}[${start}:]...); ${obj} = ${obj}[:${start}]; return __rm }()`;
5060
+ }
5061
+ const insert = args.slice(2).join(', ');
5062
+ return `func() []${elementType} { __rm := append([]${elementType}{}, ${obj}[${start}:(int(${args[1]}))+(${start})]...); __tail := append([]${elementType}{}, ${obj}[(int(${args[1]}))+(${start}):]...); ${obj} = append(${obj}[:${start}], ${insert}); ${obj} = append(${obj}, __tail...); return __rm }()`;
5063
+ },
5064
+ fill: (obj, args) => {
5065
+ const elementType = receiverElementType();
5066
+ const value = args[0] ?? '0';
5067
+ return `func() []${elementType} { __s := ${obj}; for __i := range __s { __s[__i] = ${value} }; return __s }()`;
5068
+ },
5069
+ lastIndexOf: (obj, args) => {
5070
+ const elementType = receiverElementType();
5071
+ return `func() float64 { __x := ${args[0]}; for __i := len(${obj}) - 1; __i >= 0; __i-- { if ${elementEqualsFor(elementType, `${obj}[__i]`, '__x')} { return float64(__i) } }; return float64(-1) }()`;
4025
5072
  },
4026
5073
  // Sorts in place and returns the array; without a comparator, compares as strings (like JS)
4027
5074
  sort: (obj, args) => {
@@ -4056,9 +5103,10 @@ const arrayMethodHandlers = {
4056
5103
  function receiverElementType() {
4057
5104
  return currentReceiverGoType?.startsWith('[]') ? currentReceiverGoType.slice(2) : 'interface{}';
4058
5105
  }
4059
- // Go expressions that can be assigned to (variables, fields, index expressions)
5106
+ // Go expressions that can be assigned to (variables, fields, index expressions,
5107
+ // dereferenced array references)
4060
5108
  function isAddressable(code) {
4061
- return /^[\w.]+(\[[^\]]*\])*$/.test(code);
5109
+ return /^[\w.]+(\[[^\]]*\])*$/.test(code) || /^\(\*[\w.]+\)$/.test(code);
4062
5110
  }
4063
5111
  // A regex argument as a *regexp.Regexp: compiled literals pass through, pattern
4064
5112
  // strings are compiled
@@ -4076,14 +5124,16 @@ function sliceIndex(obj, index) {
4076
5124
  return `min(int(${index}), len(${obj}))`;
4077
5125
  }
4078
5126
  // Equality of two array elements of the current receiver's element type
4079
- function elementEquals(a, b) {
4080
- const elementType = receiverElementType();
5127
+ function elementEqualsFor(elementType, a, b) {
4081
5128
  if (elementType === 'interface{}' || elementType.startsWith('*TnMap[') || elementType.startsWith('[]')) {
4082
5129
  useHelper('dynamic');
4083
5130
  return `TnSame(${a}, ${b})`;
4084
5131
  }
4085
5132
  return `${a} == ${b}`;
4086
5133
  }
5134
+ function elementEquals(a, b) {
5135
+ return elementEqualsFor(receiverElementType(), a, b);
5136
+ }
4087
5137
  const mapMethodHandlers = {
4088
5138
  set: (obj, args) => `${obj}.Set(${args[0]}, ${args[1]})`,
4089
5139
  get: (obj, args) => `${obj}.Get(${args[0]})`,
@@ -4138,6 +5188,9 @@ function getDynamicCallHandler(caller, objectType) {
4138
5188
  else if (objectType === 'RegExp') {
4139
5189
  handler = regexpMethodHandlers[methodName];
4140
5190
  }
5191
+ else if (objectType === 'Date') {
5192
+ handler = dateMethodHandlers[methodName];
5193
+ }
4141
5194
  else if (objectType === 'Map') {
4142
5195
  handler = mapMethodHandlers[methodName];
4143
5196
  }
@@ -4341,11 +5394,13 @@ function jsReplacementToGo(replacement) {
4341
5394
  function visitRegexReplace(node) {
4342
5395
  if (!isRegexReplaceCall(node))
4343
5396
  return undefined;
4344
- const [pattern, replacement] = node.arguments;
5397
+ const pattern = node.arguments[0];
5398
+ const replacement = node.arguments[1];
4345
5399
  const isGlobal = node.expression.name.text === 'replaceAll' ||
4346
5400
  (isRegularExpressionLiteral(pattern) &&
4347
5401
  pattern.text.substring(pattern.text.lastIndexOf('/') + 1).includes('g'));
4348
5402
  const target = toGoValueOfType(node.expression.expression, 'string');
5403
+ const reIsWrapper = inferExpressionType(pattern) === '*TnRegex';
4349
5404
  const re = visit(pattern);
4350
5405
  importedPackages.add('regexp');
4351
5406
  if (isArrowFunction(replacement) || isFunctionExpression(replacement)) {
@@ -4354,9 +5409,19 @@ function visitRegexReplace(node) {
4354
5409
  const groupArgs = (replacement.parameters ?? [])
4355
5410
  .map((_p, index) => `TnGroup(__m, ${index})`)
4356
5411
  .join(', ');
5412
+ if (reIsWrapper) {
5413
+ useHelper('fancyRegex');
5414
+ return `TnRegexReplaceFuncW(${re}, ${target}, func(__m []string) string { return (${callback})(${groupArgs}) }, ${isGlobal})`;
5415
+ }
4357
5416
  return `TnRegexReplaceFunc(${re}, ${target}, func(__m []string) string { return (${callback})(${groupArgs}) }, ${isGlobal})`;
4358
5417
  }
4359
5418
  const goReplacement = jsReplacementToGo(replacement);
5419
+ if (reIsWrapper) {
5420
+ useHelper('fancyRegex');
5421
+ return isGlobal
5422
+ ? `TnRegexWrapReplaceAll(${re}, ${target}, ${goReplacement})`
5423
+ : `TnRegexWrapReplaceFirst(${re}, ${target}, ${goReplacement})`;
5424
+ }
4360
5425
  if (isGlobal)
4361
5426
  return `${re}.ReplaceAllString(${target}, ${goReplacement})`;
4362
5427
  useHelper('regexReplaceFirst');
@@ -4372,6 +5437,28 @@ function jsRegexFlagsToGo(flags) {
4372
5437
  goFlags += 's';
4373
5438
  return goFlags ? `(?${goFlags})` : '';
4374
5439
  }
5440
+ // Escapes a JS regex pattern for a Go string literal and rewrites JS named
5441
+ // groups to RE2 syntax: (?<name> → (?P<name>
5442
+ function translateJsRegexPattern(pattern) {
5443
+ return pattern
5444
+ .replace(/\\/g, '\\\\')
5445
+ .replace(/"/g, '\\"')
5446
+ .replace(/\(\?<([A-Za-z_][A-Za-z0-9_]*)>/g, '(?P<$1>');
5447
+ }
5448
+ // Patterns the plain RE2 path cannot serve: /g state or RE2-unsupported syntax.
5449
+ // Named groups (?<name> are fine (translated to (?P<name>), so strip them
5450
+ // before testing for lookarounds (?= (?! (?<= (?<!
5451
+ function needsStatefulRegex(pattern, flags) {
5452
+ if (flags.includes('g'))
5453
+ return true;
5454
+ const stripped = pattern.replace(/\(\?<[A-Za-z_][A-Za-z0-9_]*>/g, '');
5455
+ // (?= (?! and any remaining (?< (lookbehind) need the backtracker
5456
+ return /\\[1-9]|\(\?[=!<]/.test(stripped);
5457
+ }
5458
+ // Whether a visited regex argument is the stateful wrapper type
5459
+ function isTnRegexArg(arg) {
5460
+ return arg.startsWith('TnRegexCompile(') || variableGoTypes.get(arg) === '*TnRegex';
5461
+ }
4375
5462
  function getTypeParameters(typeParameters) {
4376
5463
  if (!typeParameters || typeParameters.length === 0)
4377
5464
  return '';
@@ -4407,9 +5494,20 @@ function getParameterGoType(param) {
4407
5494
  return '...interface{}';
4408
5495
  }
4409
5496
  if (param.type) {
4410
- const explicitType = getType(param.type);
5497
+ let explicitType = getType(param.type);
4411
5498
  if (explicitType === ':')
4412
5499
  return 'interface{}';
5500
+ // Array parameters are references (mutations are visible to the caller) —
5501
+ // except unbound type parameters (Go generics), template-tag arrays, and
5502
+ // dynamic []interface{} (runtime helper arrays)
5503
+ if (explicitType.startsWith('[]') && explicitType !== '[]interface{}' && !param.dotDotDotToken) {
5504
+ const isTaggedTemplateArray = isTypeReferenceNode(param.type) &&
5505
+ isIdentifier(param.type.typeName) &&
5506
+ param.type.typeName.text === 'TemplateStringsArray';
5507
+ if (!isUnboundGoType(explicitType.slice(2)) && !isTaggedTemplateArray) {
5508
+ explicitType = `*${explicitType}`;
5509
+ }
5510
+ }
4413
5511
  return param.questionToken ? makeNullableType(explicitType) : explicitType;
4414
5512
  }
4415
5513
  if (param.initializer) {
@@ -4439,7 +5537,19 @@ function registerParameterType(param) {
4439
5537
  return;
4440
5538
  const name = param.name.text;
4441
5539
  const goType = getParameterGoType(param);
4442
- variableGoTypes.set(name, goType.startsWith('...') ? `[]${goType.slice(3)}` : goType);
5540
+ if (goType.startsWith('...')) {
5541
+ // Rest parameters own a fresh array; plain slice storage is fine
5542
+ variableGoTypes.set(name, `[]${goType.slice(3)}`);
5543
+ referenceArrays.delete(name);
5544
+ }
5545
+ else {
5546
+ // The signature type already carries the reference wrapper when needed
5547
+ variableGoTypes.set(name, goType);
5548
+ if (goType.startsWith('*[]'))
5549
+ referenceArrays.add(name);
5550
+ else
5551
+ referenceArrays.delete(name);
5552
+ }
4443
5553
  const category = param.type ? getTypeCategory(param.type) : undefined;
4444
5554
  if (category)
4445
5555
  variableTypes.set(name, category);
@@ -4530,7 +5640,7 @@ function withContextualParameters(fn, info) {
4530
5640
  // Destructured parameters ({ a, b }: T / [x, y]: T[]) get a temporary name
4531
5641
  function getParameterName(param, parameters) {
4532
5642
  if (isIdentifier(param.name))
4533
- return visit(param.name);
5643
+ return getSafeName(param.name.text);
4534
5644
  return `__param${parameters.indexOf(param)}`;
4535
5645
  }
4536
5646
  // Unpacks destructured parameters at the start of the function body
@@ -4589,9 +5699,12 @@ function getFunctionParametersInfo(parameters) {
4589
5699
  signatureParts.push('__defaultArgs ...interface{}');
4590
5700
  const prefixBlockContent = defaultedParams
4591
5701
  .map((param, index) => {
4592
- const paramName = visit(param.name);
5702
+ const paramName = getSafeName(param.name.text);
4593
5703
  const paramType = getParameterGoType(param);
4594
- const defaultValue = visit(param.initializer);
5704
+ // Array-reference parameters need the default boxed to *[]T
5705
+ const defaultValue = paramType.startsWith('*[]')
5706
+ ? toGoValueOfType(param.initializer, paramType)
5707
+ : visit(param.initializer);
4595
5708
  if (paramType === 'interface{}') {
4596
5709
  return `var ${paramName} interface{}\n\t\tif len(__defaultArgs) > ${index} {\n\t\t\t${paramName} = __defaultArgs[${index}]\n\t\t} else {\n\t\t\t${paramName} = ${defaultValue}\n\t\t}\n\t\t`;
4597
5710
  }
@@ -4613,7 +5726,14 @@ function getSafeName(name) {
4613
5726
  return renamedFunctions.get(name);
4614
5727
  }
4615
5728
  function getPromiseChannelType(node) {
4616
- let parent = node.parent;
5729
+ // An explicit new Promise<T> / async return type wins
5730
+ if (isNewExpression(node) &&
5731
+ isIdentifier(node.expression) &&
5732
+ node.expression.text === 'Promise' &&
5733
+ (node.typeArguments ?? []).length > 0) {
5734
+ return getType(node.typeArguments[0]) || 'struct{}';
5735
+ }
5736
+ let parent = node;
4617
5737
  while (parent) {
4618
5738
  if (isFunctionDeclaration(parent) ||
4619
5739
  isMethodDeclaration(parent) ||
@@ -4624,7 +5744,7 @@ function getPromiseChannelType(node) {
4624
5744
  if (parent.type.typeName.text === 'Promise' &&
4625
5745
  parent.type.typeArguments &&
4626
5746
  parent.type.typeArguments.length > 0) {
4627
- return getType(parent.type.typeArguments[0]);
5747
+ return getType(parent.type.typeArguments[0]) || 'struct{}';
4628
5748
  }
4629
5749
  }
4630
5750
  break;
@@ -4633,6 +5753,38 @@ function getPromiseChannelType(node) {
4633
5753
  }
4634
5754
  return 'interface{}';
4635
5755
  }
5756
+ // A function whose body runs in a goroutine and communicates over a channel:
5757
+ // async functions (return sends) and generators (yield sends)
5758
+ function emitChannelFunction(node, isAsync) {
5759
+ const name = getSafeName(visit(node.name, { inline: true }));
5760
+ const typeParams = getTypeParameters(node.typeParameters);
5761
+ const parameterInfo = withContextualParameters(node, getFunctionParametersInfo(node.parameters ?? []));
5762
+ if (node.body && isBlock(node.body)) {
5763
+ prescanVariableDeclarations(node.body);
5764
+ }
5765
+ const chanType = isAsync ? getPromiseChannelType(node) : getGeneratorYieldType(node);
5766
+ const channelVar = '__ch';
5767
+ const prevChannel = asyncChannelVar;
5768
+ const prevFn = asyncFunctionNode;
5769
+ asyncChannelVar = channelVar;
5770
+ asyncFunctionNode = node;
5771
+ const body = visit(node.body, { prefixBlockContent: parameterInfo.prefixBlockContent });
5772
+ asyncChannelVar = prevChannel;
5773
+ asyncFunctionNode = prevFn;
5774
+ return (`func ${name}${typeParams}(${parameterInfo.signature}) chan ${chanType} {\n\t${channelVar} := make(chan ${chanType})\n\tgo func() {\n\t\tdefer close(${channelVar})\n\t\t${body.trim()}\n\t}()\n\treturn ${channelVar}\n}`);
5775
+ }
5776
+ // The type a generator yields: from its first yield expression's operand
5777
+ function getGeneratorYieldType(fn) {
5778
+ let found;
5779
+ containsNode(fn, (n) => {
5780
+ if (n.kind === 'YieldExpression' && found === undefined) {
5781
+ found = n.expression ? inferExpressionType(n.expression) : undefined;
5782
+ return true;
5783
+ }
5784
+ return false;
5785
+ });
5786
+ return found && found !== 'nil' ? found : 'interface{}';
5787
+ }
4636
5788
  function visitPromiseReturn(node, options) {
4637
5789
  const callback = node.arguments?.[0];
4638
5790
  if (!callback || (!isArrowFunction(callback) && !isFunctionExpression(callback))) {
@@ -4713,6 +5865,13 @@ function visitNewSet(node) {
4713
5865
  elementType = getArrayLiteralElementType(node.arguments[0]);
4714
5866
  }
4715
5867
  const args = node.arguments ?? [];
5868
+ // new Set(otherSet/values()) — copy at runtime
5869
+ if (args.length > 0 && !isArrayLiteralExpression(args[0])) {
5870
+ useHelper('orderedMap');
5871
+ const src = getTempName('setsrc');
5872
+ const item = getTempName('item');
5873
+ return `func() *TnMap[${elementType}, struct{}] { ${src} := ${visit(args[0])}; __s := TnNewMap[${elementType}, struct{}](); for _, ${item} := range ${src} { __s.Set(${item}, struct{}{}) }; return __s }()`;
5874
+ }
4716
5875
  const values = args.length > 0 && isArrayLiteralExpression(args[0])
4717
5876
  ? args[0].elements.map((el) => `${visit(el)}, struct{}{}`)
4718
5877
  : [];
@@ -4788,11 +5947,11 @@ function includeLocalImport(code, dir, goFileName) {
4788
5947
  .filter((pkg) => {
4789
5948
  if (!helperProvidedPackages.has(pkg))
4790
5949
  return true;
4791
- const name = pkg.split('/').pop();
4792
- return stripGoStrings(fileCode).includes(`${name}.`);
5950
+ const pkgName = goImportAliases[pkg] ?? pkg.split('/').pop();
5951
+ return stripGoStrings(fileCode).includes(`${pkgName}.`);
4793
5952
  })
4794
5953
  .sort()
4795
- .map((pkg) => `import "${pkg}"`)
5954
+ .map((pkg) => goImportLine(pkg))
4796
5955
  .join('\n');
4797
5956
  const parts = ['package main'];
4798
5957
  if (fileImports)
@@ -4868,10 +6027,26 @@ function needPkg(pkg) {
4868
6027
  }
4869
6028
  // Marks a Go helper as used; also registers the packages the helper needs,
4870
6029
  // since the import list is serialized before helpers are emitted.
6030
+ const helperRequires = {
6031
+ objectAssign: ['orderedMap'],
6032
+ promiseAll: ['orderedMap', 'asyncWait'],
6033
+ namedGroups: ['orderedMap'],
6034
+ fancyRegex: ['orderedMap'],
6035
+ dynamic: ['error'],
6036
+ moduleUrl: ['pathToFileURL'],
6037
+ timerWait: [],
6038
+ asyncWait: []
6039
+ };
4871
6040
  function useHelper(id) {
4872
6041
  if (usedHelpers.has(id))
4873
6042
  return;
4874
6043
  usedHelpers.add(id);
6044
+ // The match object type the named-groups helper returns
6045
+ if (id === 'namedGroups') {
6046
+ interfacePropertyTypes.set('TnMatch', new Map([['groups', '*TnMap[string, string]']]));
6047
+ }
6048
+ for (const dep of helperRequires[id] ?? [])
6049
+ useHelper(dep);
4875
6050
  for (const pkg of helperPackages[id] ?? []) {
4876
6051
  importedPackages.add(pkg);
4877
6052
  helperProvidedPackages.add(pkg);
@@ -5027,7 +6202,8 @@ const nodeModuleMappings = {
5027
6202
  const helperPackages = {
5028
6203
  regexReplaceFirst: ['regexp'],
5029
6204
  orderedMap: ['iter', 'encoding/json', 'bytes', 'fmt'],
5030
- jsonStringify: ['encoding/json', 'bytes', 'strings'],
6205
+ jsonStringify: ['encoding/json', 'bytes', 'strings', 'unsafe'],
6206
+ objectAssign: ['reflect', 'unsafe'],
5031
6207
  typeOf: ['reflect'],
5032
6208
  dynamic: ['math', 'strings', 'fmt', 'sort', 'reflect', 'strconv', 'regexp'],
5033
6209
  regexReplaceFunc: ['regexp'],
@@ -5041,6 +6217,15 @@ const helperPackages = {
5041
6217
  fileURLToPath: ['net/url', 'path/filepath', 'strings'],
5042
6218
  pathToFileURL: ['net/url', 'path/filepath'],
5043
6219
  osPlatform: ['runtime'],
6220
+ error: [],
6221
+ bigint: ['math/big'],
6222
+ symbol: [],
6223
+ fancyRegex: ['regexp', 'strings', 'strconv', 'fmt'],
6224
+ namedGroups: ['regexp'],
6225
+ moduleUrl: ['os'],
6226
+ asyncWait: ['sync'],
6227
+ timerWait: ['sync', 'time'],
6228
+ promiseAll: [],
5044
6229
  exec: ['os/exec', 'bytes', 'runtime'],
5045
6230
  execSync: ['os/exec', 'bytes', 'runtime'],
5046
6231
  runInherit: ['os/exec', 'os'],
@@ -5066,6 +6251,10 @@ func TnGet(obj interface{}, key string) interface{} {
5066
6251
  if key == "length" {
5067
6252
  return float64(len(o))
5068
6253
  }
6254
+ case *TnError:
6255
+ if key == "message" {
6256
+ return o.message
6257
+ }
5069
6258
  }
5070
6259
  return nil
5071
6260
  }
@@ -5284,6 +6473,19 @@ func TnHas(obj interface{}, key interface{}) bool {
5284
6473
  return found
5285
6474
  }
5286
6475
  }
6476
+ // Struct objects: the in operator checks for a field
6477
+ rv := reflect.ValueOf(obj)
6478
+ if rv.Kind() == reflect.Pointer {
6479
+ if rv.IsNil() {
6480
+ return false
6481
+ }
6482
+ rv = rv.Elem()
6483
+ }
6484
+ if rv.Kind() == reflect.Struct {
6485
+ if k, ok := key.(string); ok {
6486
+ return rv.FieldByName(k).IsValid()
6487
+ }
6488
+ }
5287
6489
  return false
5288
6490
  }
5289
6491
 
@@ -5397,7 +6599,25 @@ func TnTruthy(v interface{}) bool {
5397
6599
  case string:
5398
6600
  return t != ""
5399
6601
  }
6602
+ rv := reflect.ValueOf(v)
6603
+ switch rv.Kind() {
6604
+ case reflect.Pointer, reflect.Interface, reflect.Map, reflect.Slice:
6605
+ return !rv.IsNil()
6606
+ }
5400
6607
  return true
6608
+ }
6609
+
6610
+ // nil or a typed nil pointer inside an interface (undefined vs defined)
6611
+ func TnIsNil(v interface{}) bool {
6612
+ if v == nil {
6613
+ return true
6614
+ }
6615
+ rv := reflect.ValueOf(v)
6616
+ switch rv.Kind() {
6617
+ case reflect.Pointer, reflect.Interface, reflect.Map, reflect.Slice, reflect.Func, reflect.Chan:
6618
+ return rv.IsNil()
6619
+ }
6620
+ return false
5401
6621
  }`,
5402
6622
  typeOf: `func TnTypeOf(v interface{}) string {
5403
6623
  switch v.(type) {
@@ -5415,13 +6635,124 @@ func TnTruthy(v interface{}) bool {
5415
6635
  }
5416
6636
  return "object"
5417
6637
  }`,
5418
- jsonStringify: `func TnJSONStringify(v interface{}, indent string) string {
6638
+ jsonStringify: `// JSON.stringify including unexported struct fields (Go's encoder skips them)
6639
+ func TnJSONStringify(v interface{}, indent string) string {
5419
6640
  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")
6641
+ tnJSONWrite(&buf, reflect.ValueOf(v), indent, 0)
6642
+ return buf.String()
6643
+ }
6644
+
6645
+ func tnJSONNewline(buf *bytes.Buffer, indent string, depth int) {
6646
+ if indent == "" {
6647
+ return
6648
+ }
6649
+ buf.WriteByte('\\n')
6650
+ for i := 0; i < depth; i++ {
6651
+ buf.WriteString(indent)
6652
+ }
6653
+ }
6654
+
6655
+ func tnJSONWrite(buf *bytes.Buffer, rv reflect.Value, indent string, depth int) {
6656
+ if !rv.IsValid() {
6657
+ buf.WriteString("null")
6658
+ return
6659
+ }
6660
+ switch rv.Kind() {
6661
+ case reflect.Pointer, reflect.Interface:
6662
+ if rv.IsNil() {
6663
+ buf.WriteString("null")
6664
+ return
6665
+ }
6666
+ tnJSONWrite(buf, rv.Elem(), indent, depth)
6667
+ case reflect.Bool:
6668
+ if rv.Bool() {
6669
+ buf.WriteString("true")
6670
+ } else {
6671
+ buf.WriteString("false")
6672
+ }
6673
+ case reflect.Float32, reflect.Float64:
6674
+ buf.WriteString(TnNumStr(rv.Float()))
6675
+ case reflect.String:
6676
+ data, _ := json.Marshal(rv.String())
6677
+ buf.Write(data)
6678
+ case reflect.Slice, reflect.Array:
6679
+ if rv.Kind() == reflect.Slice && rv.IsNil() {
6680
+ buf.WriteString("null")
6681
+ return
6682
+ }
6683
+ buf.WriteByte('[')
6684
+ for i := 0; i < rv.Len(); i++ {
6685
+ if i > 0 {
6686
+ buf.WriteByte(',')
6687
+ tnJSONNewline(buf, indent, depth+1)
6688
+ }
6689
+ tnJSONWrite(buf, rv.Index(i), indent, depth+1)
6690
+ }
6691
+ buf.WriteByte(']')
6692
+ case reflect.Map:
6693
+ buf.WriteByte('{')
6694
+ keys := make([]string, 0, rv.Len())
6695
+ byKey := map[string]reflect.Value{}
6696
+ for _, k := range rv.MapKeys() {
6697
+ ks := fmt.Sprint(k.Interface())
6698
+ keys = append(keys, ks)
6699
+ byKey[ks] = rv.MapIndex(k)
6700
+ }
6701
+ sort.Strings(keys)
6702
+ for i, ks := range keys {
6703
+ if i > 0 {
6704
+ buf.WriteByte(',')
6705
+ tnJSONNewline(buf, indent, depth+1)
6706
+ }
6707
+ kd, _ := json.Marshal(ks)
6708
+ buf.Write(kd)
6709
+ buf.WriteByte(':')
6710
+ tnJSONWrite(buf, byKey[ks], indent, depth+1)
6711
+ }
6712
+ buf.WriteByte('}')
6713
+ case reflect.Struct:
6714
+ if m, ok := rv.Interface().(json.Marshaler); ok {
6715
+ data, err := m.MarshalJSON()
6716
+ if err == nil {
6717
+ buf.Write(data)
6718
+ return
6719
+ }
6720
+ }
6721
+ buf.WriteByte('{')
6722
+ rt := rv.Type()
6723
+ first := true
6724
+ for i := 0; i < rt.NumField(); i++ {
6725
+ f := rt.Field(i)
6726
+ value := rv.Field(i)
6727
+ if value.Kind() == reflect.Func {
6728
+ continue
6729
+ }
6730
+ if !value.CanInterface() {
6731
+ // Unexported field: read it through a pointer to bypass the flag
6732
+ if !value.CanAddr() {
6733
+ continue
6734
+ }
6735
+ value = reflect.NewAt(value.Type(), unsafe.Pointer(value.UnsafeAddr())).Elem()
6736
+ }
6737
+ if !first {
6738
+ buf.WriteByte(',')
6739
+ tnJSONNewline(buf, indent, depth+1)
6740
+ }
6741
+ first = false
6742
+ kd, _ := json.Marshal(f.Name)
6743
+ buf.Write(kd)
6744
+ buf.WriteByte(':')
6745
+ tnJSONWrite(buf, value, indent, depth+1)
6746
+ }
6747
+ buf.WriteByte('}')
6748
+ default:
6749
+ data, err := json.Marshal(rv.Interface())
6750
+ if err != nil {
6751
+ buf.WriteString("null")
6752
+ return
6753
+ }
6754
+ buf.Write(data)
6755
+ }
5425
6756
  }`,
5426
6757
  orderedMap: `// TnMap: JS Map/Set/object semantics — keys iterate in insertion order
5427
6758
  type TnMap[K comparable, V any] struct {
@@ -5554,6 +6885,902 @@ func (m *TnMap[K, V]) MarshalJSON() ([]byte, error) {
5554
6885
  return s
5555
6886
  }
5556
6887
  return s[:loc[0]] + string(re.ExpandString(nil, repl, s, loc)) + s[loc[1]:]
6888
+ }`,
6889
+ namedGroups: `// TnMatch: the result of exec on a regex with named groups
6890
+ type TnMatch struct {
6891
+ groups *TnMap[string, string]
6892
+ }
6893
+
6894
+ func TnRegexExecNamed(re *regexp.Regexp, s string) *TnMatch {
6895
+ m := re.FindStringSubmatch(s)
6896
+ if m == nil {
6897
+ return nil
6898
+ }
6899
+ res := &TnMatch{groups: TnNewMap[string, string]()}
6900
+ for i, name := range re.SubexpNames() {
6901
+ if name != "" && i < len(m) {
6902
+ res.groups.Set(name, m[i])
6903
+ }
6904
+ }
6905
+ return res
6906
+ }`,
6907
+ fancyRegex: `// TnRegex: a regex with JS semantics the plain RE2 path cannot serve —
6908
+ // /g state (lastIndex) and backreference/lookaround patterns (a small
6909
+ // backtracking engine).
6910
+ type TnRegex struct {
6911
+ re *regexp.Regexp
6912
+ bt *tnBt
6913
+ lastIndex int
6914
+ global bool
6915
+ }
6916
+
6917
+ func TnRegexCompile(pattern string, global bool) *TnRegex {
6918
+ if re, err := regexp.Compile(pattern); err == nil {
6919
+ return &TnRegex{re: re, global: global}
6920
+ }
6921
+ return &TnRegex{bt: tnBtCompile(pattern), global: global}
6922
+ }
6923
+
6924
+ func (r *TnRegex) Test(s string) bool {
6925
+ groups, _ := r.find(s)
6926
+ return groups != nil
6927
+ }
6928
+
6929
+ func (r *TnRegex) Find(s string) []string {
6930
+ groups, _ := r.find(s)
6931
+ return groups
6932
+ }
6933
+
6934
+ // First match only, without advancing state (string .match)
6935
+ func (r *TnRegex) FindSub(s string) []string {
6936
+ savedG, savedI := r.global, r.lastIndex
6937
+ r.global, r.lastIndex = false, 0
6938
+ groups, _ := r.find(s)
6939
+ r.global, r.lastIndex = savedG, savedI
6940
+ return groups
6941
+ }
6942
+
6943
+ func (r *TnRegex) FindAllSub(s string) [][]string {
6944
+ savedG, savedI := r.global, r.lastIndex
6945
+ r.global, r.lastIndex = true, 0
6946
+ var out [][]string
6947
+ for {
6948
+ groups, _ := r.find(s)
6949
+ if groups == nil {
6950
+ break
6951
+ }
6952
+ out = append(out, groups)
6953
+ }
6954
+ r.global, r.lastIndex = savedG, savedI
6955
+ return out
6956
+ }
6957
+
6958
+ func (r *TnRegex) FindIndexSub(s string) []int {
6959
+ savedG, savedI := r.global, r.lastIndex
6960
+ r.global, r.lastIndex = false, 0
6961
+ _, loc := r.find(s)
6962
+ r.global, r.lastIndex = savedG, savedI
6963
+ if loc == nil {
6964
+ return nil
6965
+ }
6966
+ return loc[:2]
6967
+ }
6968
+
6969
+ func (r *TnRegex) SplitStr(s string, n int) []string {
6970
+ if r.bt == nil {
6971
+ return r.re.Split(s, n)
6972
+ }
6973
+ savedG, savedI := r.global, r.lastIndex
6974
+ r.global, r.lastIndex = true, 0
6975
+ parts := []string{}
6976
+ last := 0
6977
+ for {
6978
+ _, loc := r.find(s)
6979
+ if loc == nil || (n > 0 && len(parts) >= n-1) {
6980
+ break
6981
+ }
6982
+ parts = append(parts, s[last:loc[0]])
6983
+ last = loc[1]
6984
+ if loc[1] == loc[0] && loc[1] < len(s) {
6985
+ parts = append(parts, string(s[loc[1]]))
6986
+ last = loc[1] + 1
6987
+ }
6988
+ }
6989
+ r.global, r.lastIndex = savedG, savedI
6990
+ return append(parts, s[last:])
6991
+ }
6992
+
6993
+ func TnRegexWrapReplaceAll(r *TnRegex, s string, repl string) string {
6994
+ return r.replace(s, repl, true)
6995
+ }
6996
+
6997
+ func TnRegexWrapReplaceFirst(r *TnRegex, s string, repl string) string {
6998
+ return r.replace(s, repl, false)
6999
+ }
7000
+
7001
+ func TnRegexReplaceFuncW(r *TnRegex, s string, fn func([]string) string, all bool) string {
7002
+ savedG, savedI := r.global, r.lastIndex
7003
+ r.global, r.lastIndex = true, 0
7004
+ out := ""
7005
+ last := 0
7006
+ for {
7007
+ groups, loc := r.find(s)
7008
+ if groups == nil {
7009
+ break
7010
+ }
7011
+ out += s[last:loc[0]] + fn(groups)
7012
+ last = loc[1]
7013
+ if !all {
7014
+ break
7015
+ }
7016
+ }
7017
+ r.global, r.lastIndex = savedG, savedI
7018
+ return out + s[last:]
7019
+ }
7020
+
7021
+ func (r *TnRegex) replace(s string, repl string, all bool) string {
7022
+ savedG, savedI := r.global, r.lastIndex
7023
+ r.global, r.lastIndex = true, 0
7024
+ out := ""
7025
+ last := 0
7026
+ for {
7027
+ groups, loc := r.find(s)
7028
+ if groups == nil {
7029
+ break
7030
+ }
7031
+ out += s[last:loc[0]] + tnExpandGroups(repl, groups)
7032
+ last = loc[1]
7033
+ if !all {
7034
+ break
7035
+ }
7036
+ if loc[1] == loc[0] && loc[1] < len(s) {
7037
+ out += string(s[loc[1]])
7038
+ last = loc[1] + 1
7039
+ }
7040
+ }
7041
+ r.global, r.lastIndex = savedG, savedI
7042
+ return out + s[last:]
7043
+ }
7044
+
7045
+ // Replaces the numeric group placeholders in the replacement with captures
7046
+ func tnExpandGroups(repl string, groups []string) string {
7047
+ out := ""
7048
+ for i := 0; i < len(repl); i++ {
7049
+ if repl[i] == '$' && i+1 < len(repl) && repl[i+1] == '{' {
7050
+ if j := strings.IndexByte(repl[i+2:], '}'); j >= 0 {
7051
+ if n, err := strconv.Atoi(repl[i+2 : i+2+j]); err == nil && n < len(groups) {
7052
+ out += groups[n]
7053
+ i = i + 2 + j
7054
+ continue
7055
+ }
7056
+ }
7057
+ }
7058
+ out += string(repl[i])
7059
+ }
7060
+ return out
7061
+ }
7062
+
7063
+ // find: leftmost match at or after lastIndex (which /g advances in runes)
7064
+ func (r *TnRegex) find(s string) ([]string, []int) {
7065
+ runs := []rune(s)
7066
+ if r.bt != nil {
7067
+ start := 0
7068
+ if r.global {
7069
+ if r.lastIndex > len(runs) {
7070
+ r.lastIndex = 0
7071
+ return nil, nil
7072
+ }
7073
+ start = r.lastIndex
7074
+ }
7075
+ offs := tnByteOffsets(runs)
7076
+ for pos := start; pos <= len(runs); pos++ {
7077
+ caps := make([]int, (r.bt.ngroups+1)*2)
7078
+ for i := range caps {
7079
+ caps[i] = -1
7080
+ }
7081
+ m := &tnBtMatchState{runs: runs, caps: caps}
7082
+ end := -1
7083
+ if m.match(r.bt.root, pos, func(e int) bool { end = e; return true }) {
7084
+ caps[0], caps[1] = pos, end
7085
+ groups := make([]string, r.bt.ngroups+1)
7086
+ loc := make([]int, (r.bt.ngroups+1)*2)
7087
+ for i := 0; i <= r.bt.ngroups; i++ {
7088
+ si, ei := caps[2*i], caps[2*i+1]
7089
+ if si >= 0 && ei >= si {
7090
+ groups[i] = string(runs[si:ei])
7091
+ loc[2*i], loc[2*i+1] = offs[si], offs[ei]
7092
+ } else {
7093
+ loc[2*i], loc[2*i+1] = -1, -1
7094
+ }
7095
+ }
7096
+ if r.global {
7097
+ next := end
7098
+ if next == pos {
7099
+ next++
7100
+ }
7101
+ r.lastIndex = next
7102
+ }
7103
+ return groups, loc
7104
+ }
7105
+ }
7106
+ if r.global {
7107
+ r.lastIndex = 0
7108
+ }
7109
+ return nil, nil
7110
+ }
7111
+ if !r.global {
7112
+ loc := r.re.FindStringSubmatchIndex(s)
7113
+ if loc == nil {
7114
+ return nil, nil
7115
+ }
7116
+ groups := make([]string, len(loc)/2)
7117
+ for i := range groups {
7118
+ if loc[2*i] >= 0 {
7119
+ groups[i] = s[loc[2*i]:loc[2*i+1]]
7120
+ }
7121
+ }
7122
+ return groups, loc
7123
+ }
7124
+ if r.lastIndex >= len(runs) {
7125
+ r.lastIndex = 0
7126
+ return nil, nil
7127
+ }
7128
+ byteStart := 0
7129
+ for i := 0; i < r.lastIndex; i++ {
7130
+ byteStart += len(string(runs[i]))
7131
+ }
7132
+ loc := r.re.FindStringSubmatchIndex(s[byteStart:])
7133
+ if loc == nil {
7134
+ r.lastIndex = 0
7135
+ return nil, nil
7136
+ }
7137
+ for i := range loc {
7138
+ if loc[i] >= 0 {
7139
+ loc[i] += byteStart
7140
+ }
7141
+ }
7142
+ groups := make([]string, len(loc)/2)
7143
+ for i := range groups {
7144
+ if loc[2*i] >= 0 {
7145
+ groups[i] = s[loc[2*i]:loc[2*i+1]]
7146
+ }
7147
+ }
7148
+ endRune := r.lastIndex
7149
+ bo := byteStart
7150
+ for ri := r.lastIndex; ri < len(runs) && bo < loc[1]; ri++ {
7151
+ bo += len(string(runs[ri]))
7152
+ endRune = ri + 1
7153
+ }
7154
+ if endRune == r.lastIndex {
7155
+ endRune++
7156
+ }
7157
+ r.lastIndex = endRune
7158
+ return groups, loc
7159
+ }
7160
+
7161
+ func tnByteOffsets(runs []rune) []int {
7162
+ offs := make([]int, len(runs)+1)
7163
+ bo := 0
7164
+ for i, r := range runs {
7165
+ offs[i] = bo
7166
+ bo += len(string(r))
7167
+ }
7168
+ offs[len(runs)] = bo
7169
+ return offs
7170
+ }
7171
+
7172
+ // ---- Backtracking regex engine for patterns RE2 rejects ----
7173
+
7174
+ type tnBtNode struct {
7175
+ op string
7176
+ ch rune
7177
+ cls func(rune) bool
7178
+ negCls bool
7179
+ seq []*tnBtNode
7180
+ branches [][]*tnBtNode
7181
+ child *tnBtNode
7182
+ min, max int
7183
+ greedy bool
7184
+ anchor byte
7185
+ group int
7186
+ behind bool
7187
+ negative bool
7188
+ }
7189
+
7190
+ type tnBt struct {
7191
+ root *tnBtNode
7192
+ ngroups int
7193
+ }
7194
+
7195
+ type tnBtParser struct {
7196
+ src []rune
7197
+ pos int
7198
+ ngrp int
7199
+ }
7200
+
7201
+ func tnBtCompile(pattern string) *tnBt {
7202
+ p := &tnBtParser{src: []rune(pattern)}
7203
+ root, err := p.parseAlt()
7204
+ if err != nil || p.pos < len(p.src) {
7205
+ return nil
7206
+ }
7207
+ return &tnBt{root: root, ngroups: p.ngrp}
7208
+ }
7209
+
7210
+ func (p *tnBtParser) peek() (rune, bool) {
7211
+ if p.pos < len(p.src) {
7212
+ return p.src[p.pos], true
7213
+ }
7214
+ return 0, false
7215
+ }
7216
+
7217
+ func (p *tnBtParser) parseAlt() (*tnBtNode, error) {
7218
+ var branches [][]*tnBtNode
7219
+ for {
7220
+ seq, err := p.parseSeq()
7221
+ if err != nil {
7222
+ return nil, err
7223
+ }
7224
+ branches = append(branches, seq)
7225
+ if ch, ok := p.peek(); ok && ch == '|' {
7226
+ p.pos++
7227
+ continue
7228
+ }
7229
+ break
7230
+ }
7231
+ if len(branches) == 1 {
7232
+ return &tnBtNode{op: "cat", seq: branches[0]}, nil
7233
+ }
7234
+ return &tnBtNode{op: "alt", branches: branches}, nil
7235
+ }
7236
+
7237
+ func (p *tnBtParser) parseSeq() ([]*tnBtNode, error) {
7238
+ var seq []*tnBtNode
7239
+ for {
7240
+ ch, ok := p.peek()
7241
+ if !ok || ch == '|' || ch == ')' {
7242
+ break
7243
+ }
7244
+ atom, err := p.parseAtom()
7245
+ if err != nil {
7246
+ return nil, err
7247
+ }
7248
+ if atom == nil {
7249
+ continue
7250
+ }
7251
+ node, err := p.parseQuantifier(atom)
7252
+ if err != nil {
7253
+ return nil, err
7254
+ }
7255
+ seq = append(seq, node)
7256
+ }
7257
+ return seq, nil
7258
+ }
7259
+
7260
+ func (p *tnBtParser) parseQuantifier(atom *tnBtNode) (*tnBtNode, error) {
7261
+ ch, ok := p.peek()
7262
+ if !ok {
7263
+ return atom, nil
7264
+ }
7265
+ lo, hi := 0, 0
7266
+ switch ch {
7267
+ case '*':
7268
+ lo, hi = 0, -1
7269
+ case '+':
7270
+ lo, hi = 1, -1
7271
+ case '?':
7272
+ lo, hi = 0, 1
7273
+ case '{':
7274
+ save := p.pos
7275
+ p.pos++
7276
+ parsedLo, parsedHi, ok2 := p.parseBraces()
7277
+ if !ok2 {
7278
+ p.pos = save
7279
+ return atom, nil
7280
+ }
7281
+ lo, hi = parsedLo, parsedHi
7282
+ default:
7283
+ return atom, nil
7284
+ }
7285
+ p.pos++
7286
+ greedy := true
7287
+ if c, ok2 := p.peek(); ok2 && c == '?' {
7288
+ greedy = false
7289
+ p.pos++
7290
+ }
7291
+ return &tnBtNode{op: "rep", child: atom, min: lo, max: hi, greedy: greedy}, nil
7292
+ }
7293
+
7294
+ // {m}, {m,}, {m,n} — returns ok=false when the brace is a literal
7295
+ func (p *tnBtParser) parseBraces() (int, int, bool) {
7296
+ lo := 0
7297
+ digits := 0
7298
+ for {
7299
+ c, ok := p.peek()
7300
+ if !ok || c < '0' || c > '9' {
7301
+ break
7302
+ }
7303
+ lo = lo*10 + int(c-'0')
7304
+ digits++
7305
+ p.pos++
7306
+ }
7307
+ if digits == 0 {
7308
+ return 0, 0, false
7309
+ }
7310
+ hi := lo
7311
+ if c, ok := p.peek(); ok && c == ',' {
7312
+ p.pos++
7313
+ hi = -1
7314
+ hv := 0
7315
+ hd := 0
7316
+ for {
7317
+ c2, ok2 := p.peek()
7318
+ if !ok2 || c2 < '0' || c2 > '9' {
7319
+ break
7320
+ }
7321
+ hv = hv*10 + int(c2-'0')
7322
+ hd++
7323
+ p.pos++
7324
+ }
7325
+ if hd > 0 {
7326
+ hi = hv
7327
+ }
7328
+ }
7329
+ if c, ok := p.peek(); !ok || c != '}' {
7330
+ return 0, 0, false
7331
+ }
7332
+ return lo, hi, true
7333
+ }
7334
+
7335
+ func tnCharClass(escape rune) func(rune) bool {
7336
+ switch escape {
7337
+ case 'd':
7338
+ return func(r rune) bool { return r >= '0' && r <= '9' }
7339
+ case 'D':
7340
+ return func(r rune) bool { return !(r >= '0' && r <= '9') }
7341
+ case 'w':
7342
+ return func(r rune) bool {
7343
+ return r == '_' || (r >= '0' && r <= '9') || (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z')
7344
+ }
7345
+ case 'W':
7346
+ return func(r rune) bool {
7347
+ return !(r == '_' || (r >= '0' && r <= '9') || (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z'))
7348
+ }
7349
+ case 's':
7350
+ return func(r rune) bool { return r == ' ' || r == '\\t' || r == '\\n' || r == '\\r' || r == '\\f' || r == '\\v' }
7351
+ case 'S':
7352
+ return func(r rune) bool { return !(r == ' ' || r == '\\t' || r == '\\n' || r == '\\r' || r == '\\f' || r == '\\v') }
7353
+ }
7354
+ return nil
7355
+ }
7356
+
7357
+ func (p *tnBtParser) parseAtom() (*tnBtNode, error) {
7358
+ ch, ok := p.peek()
7359
+ if !ok {
7360
+ return nil, fmt.Errorf("unexpected end")
7361
+ }
7362
+ p.pos++
7363
+ switch ch {
7364
+ case '(':
7365
+ kind := "group"
7366
+ negative := false
7367
+ behind := false
7368
+ if c, ok2 := p.peek(); ok2 && c == '?' {
7369
+ p.pos++
7370
+ c2, _ := p.peek()
7371
+ switch c2 {
7372
+ case ':':
7373
+ p.pos++
7374
+ kind = "noncap"
7375
+ case '=':
7376
+ p.pos++
7377
+ kind = "look"
7378
+ case '!':
7379
+ p.pos++
7380
+ kind = "look"
7381
+ negative = true
7382
+ case '<':
7383
+ p.pos++
7384
+ c3, ok3 := p.peek()
7385
+ if !ok3 {
7386
+ return nil, fmt.Errorf("bad (?<")
7387
+ }
7388
+ if c3 == '=' || c3 == '!' {
7389
+ p.pos++
7390
+ kind = "look"
7391
+ behind = true
7392
+ negative = c3 == '!'
7393
+ } else {
7394
+ for {
7395
+ cn, okn := p.peek()
7396
+ if !okn {
7397
+ return nil, fmt.Errorf("unterminated group name")
7398
+ }
7399
+ p.pos++
7400
+ if cn == '>' {
7401
+ break
7402
+ }
7403
+ }
7404
+ }
7405
+ case 'P':
7406
+ p.pos++
7407
+ if c3, ok3 := p.peek(); ok3 && c3 == '<' {
7408
+ p.pos++
7409
+ for {
7410
+ cn, okn := p.peek()
7411
+ if !okn {
7412
+ return nil, fmt.Errorf("unterminated group name")
7413
+ }
7414
+ p.pos++
7415
+ if cn == '>' {
7416
+ break
7417
+ }
7418
+ }
7419
+ } else {
7420
+ return nil, fmt.Errorf("bad (?P")
7421
+ }
7422
+ default:
7423
+ return nil, fmt.Errorf("unsupported group")
7424
+ }
7425
+ }
7426
+ body, err := p.parseAlt()
7427
+ if err != nil {
7428
+ return nil, err
7429
+ }
7430
+ if c, ok2 := p.peek(); !ok2 || c != ')' {
7431
+ return nil, fmt.Errorf("missing )")
7432
+ }
7433
+ p.pos++
7434
+ switch kind {
7435
+ case "noncap":
7436
+ return body, nil
7437
+ case "look":
7438
+ return &tnBtNode{op: "look", child: body, behind: behind, negative: negative}, nil
7439
+ default:
7440
+ p.ngrp++
7441
+ return &tnBtNode{op: "group", group: p.ngrp, child: body}, nil
7442
+ }
7443
+ case '[':
7444
+ return p.parseClass()
7445
+ case '.':
7446
+ return &tnBtNode{op: "any"}, nil
7447
+ case '^':
7448
+ return &tnBtNode{op: "anchor", anchor: '^'}, nil
7449
+ case '$':
7450
+ return &tnBtNode{op: "anchor", anchor: '$'}, nil
7451
+ case '\\\\':
7452
+ return p.parseEscape()
7453
+ default:
7454
+ return &tnBtNode{op: "char", ch: ch}, nil
7455
+ }
7456
+ }
7457
+
7458
+ func (p *tnBtParser) parseEscape() (*tnBtNode, error) {
7459
+ c, ok := p.peek()
7460
+ if !ok {
7461
+ return nil, fmt.Errorf("trailing backslash")
7462
+ }
7463
+ p.pos++
7464
+ switch c {
7465
+ case 'b':
7466
+ return &tnBtNode{op: "anchor", anchor: 'b'}, nil
7467
+ case 'B':
7468
+ return &tnBtNode{op: "anchor", anchor: 'B'}, nil
7469
+ case 'n':
7470
+ return &tnBtNode{op: "char", ch: '\\n'}, nil
7471
+ case 't':
7472
+ return &tnBtNode{op: "char", ch: '\\t'}, nil
7473
+ case 'r':
7474
+ return &tnBtNode{op: "char", ch: '\\r'}, nil
7475
+ case 'f':
7476
+ return &tnBtNode{op: "char", ch: '\\f'}, nil
7477
+ case 'v':
7478
+ return &tnBtNode{op: "char", ch: '\\v'}, nil
7479
+ case '0':
7480
+ return &tnBtNode{op: "char", ch: 0}, nil
7481
+ case '1', '2', '3', '4', '5', '6', '7', '8', '9':
7482
+ return &tnBtNode{op: "backref", group: int(c - '0')}, nil
7483
+ default:
7484
+ if cls := tnCharClass(c); cls != nil {
7485
+ return &tnBtNode{op: "class", cls: cls}, nil
7486
+ }
7487
+ return &tnBtNode{op: "char", ch: c}, nil
7488
+ }
7489
+ }
7490
+
7491
+ func (p *tnBtParser) parseClass() (*tnBtNode, error) {
7492
+ negate := false
7493
+ if c, ok := p.peek(); ok && c == '^' {
7494
+ negate = true
7495
+ p.pos++
7496
+ }
7497
+ var items []func(rune) bool
7498
+ first := true
7499
+ for {
7500
+ c, ok := p.peek()
7501
+ if !ok {
7502
+ return nil, fmt.Errorf("unterminated class")
7503
+ }
7504
+ if c == ']' && !first {
7505
+ p.pos++
7506
+ break
7507
+ }
7508
+ first = false
7509
+ p.pos++
7510
+ var lo rune
7511
+ if c == '\\\\' {
7512
+ c2, ok2 := p.peek()
7513
+ if !ok2 {
7514
+ return nil, fmt.Errorf("trailing backslash in class")
7515
+ }
7516
+ p.pos++
7517
+ if cls := tnCharClass(c2); cls != nil {
7518
+ items = append(items, cls)
7519
+ continue
7520
+ }
7521
+ switch c2 {
7522
+ case 'n':
7523
+ lo = '\\n'
7524
+ case 't':
7525
+ lo = '\\t'
7526
+ case 'r':
7527
+ lo = '\\r'
7528
+ default:
7529
+ lo = c2
7530
+ }
7531
+ } else {
7532
+ lo = c
7533
+ }
7534
+ // range?
7535
+ if c3, ok3 := p.peek(); ok3 && c3 == '-' {
7536
+ if c4, ok4 := p.peekAt(1); ok4 && c4 != ']' {
7537
+ p.pos++ // consume '-'
7538
+ hiChar, _ := p.peek()
7539
+ p.pos++
7540
+ hi := hiChar
7541
+ if hiChar == '\\\\' {
7542
+ hc, ok5 := p.peek()
7543
+ if !ok5 {
7544
+ return nil, fmt.Errorf("trailing backslash in class")
7545
+ }
7546
+ p.pos++
7547
+ hi = hc
7548
+ }
7549
+ l, h := lo, hi
7550
+ items = append(items, func(r rune) bool { return r >= l && r <= h })
7551
+ continue
7552
+ }
7553
+ }
7554
+ l := lo
7555
+ items = append(items, func(r rune) bool { return r == l })
7556
+ }
7557
+ matchers := items
7558
+ return &tnBtNode{op: "class", cls: func(r rune) bool {
7559
+ for _, m := range matchers {
7560
+ if m(r) {
7561
+ return true
7562
+ }
7563
+ }
7564
+ return false
7565
+ }, negCls: negate}, nil
7566
+ }
7567
+
7568
+ func (p *tnBtParser) peekAt(offset int) (rune, bool) {
7569
+ if p.pos+offset < len(p.src) {
7570
+ return p.src[p.pos+offset], true
7571
+ }
7572
+ return 0, false
7573
+ }
7574
+
7575
+ type tnBtMatchState struct {
7576
+ runs []rune
7577
+ caps []int
7578
+ }
7579
+
7580
+ func (m *tnBtMatchState) seq(nodes []*tnBtNode, pos int, k func(int) bool) bool {
7581
+ if len(nodes) == 0 {
7582
+ return k(pos)
7583
+ }
7584
+ return m.match(nodes[0], pos, func(p2 int) bool {
7585
+ return m.seq(nodes[1:], p2, k)
7586
+ })
7587
+ }
7588
+
7589
+ func tnBtWord(r rune) bool {
7590
+ return r == '_' || (r >= '0' && r <= '9') || (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z')
7591
+ }
7592
+
7593
+ func (m *tnBtMatchState) match(node *tnBtNode, pos int, k func(int) bool) bool {
7594
+ switch node.op {
7595
+ case "char":
7596
+ if pos < len(m.runs) && m.runs[pos] == node.ch {
7597
+ return k(pos + 1)
7598
+ }
7599
+ return false
7600
+ case "any":
7601
+ if pos < len(m.runs) && m.runs[pos] != '\\n' {
7602
+ return k(pos + 1)
7603
+ }
7604
+ return false
7605
+ case "class":
7606
+ if pos < len(m.runs) && node.cls(m.runs[pos]) != node.negCls {
7607
+ return k(pos + 1)
7608
+ }
7609
+ return false
7610
+ case "anchor":
7611
+ switch node.anchor {
7612
+ case '^':
7613
+ if pos == 0 {
7614
+ return k(pos)
7615
+ }
7616
+ case '$':
7617
+ if pos == len(m.runs) {
7618
+ return k(pos)
7619
+ }
7620
+ case 'b':
7621
+ before := pos > 0 && tnBtWord(m.runs[pos-1])
7622
+ after := pos < len(m.runs) && tnBtWord(m.runs[pos])
7623
+ if before != after {
7624
+ return k(pos)
7625
+ }
7626
+ case 'B':
7627
+ before := pos > 0 && tnBtWord(m.runs[pos-1])
7628
+ after := pos < len(m.runs) && tnBtWord(m.runs[pos])
7629
+ if before == after {
7630
+ return k(pos)
7631
+ }
7632
+ }
7633
+ return false
7634
+ case "cat":
7635
+ return m.seq(node.seq, pos, k)
7636
+ case "alt":
7637
+ for _, br := range node.branches {
7638
+ if m.seq(br, pos, k) {
7639
+ return true
7640
+ }
7641
+ }
7642
+ return false
7643
+ case "group":
7644
+ si, ei := node.group*2, node.group*2+1
7645
+ savedS, savedE := m.caps[si], m.caps[ei]
7646
+ m.caps[si] = pos
7647
+ matched := m.match(node.child, pos, func(end int) bool {
7648
+ ps, pe := m.caps[si], m.caps[ei]
7649
+ m.caps[ei] = end
7650
+ if k(end) {
7651
+ return true
7652
+ }
7653
+ m.caps[si], m.caps[ei] = ps, pe
7654
+ return false
7655
+ })
7656
+ if !matched {
7657
+ m.caps[si], m.caps[ei] = savedS, savedE
7658
+ }
7659
+ return matched
7660
+ case "backref":
7661
+ s, e := m.caps[node.group*2], m.caps[node.group*2+1]
7662
+ if s < 0 || e < 0 {
7663
+ return k(pos)
7664
+ }
7665
+ n := e - s
7666
+ if pos+n > len(m.runs) {
7667
+ return false
7668
+ }
7669
+ for i := 0; i < n; i++ {
7670
+ if m.runs[pos+i] != m.runs[s+i] {
7671
+ return false
7672
+ }
7673
+ }
7674
+ return k(pos + n)
7675
+ case "look":
7676
+ if !node.behind {
7677
+ matched := false
7678
+ saved := append([]int(nil), m.caps...)
7679
+ if m.match(node.child, pos, func(int) bool { return true }) {
7680
+ matched = true
7681
+ }
7682
+ m.caps = saved
7683
+ if matched != node.negative {
7684
+ return k(pos)
7685
+ }
7686
+ return false
7687
+ }
7688
+ matched := false
7689
+ saved := append([]int(nil), m.caps...)
7690
+ for start := 0; start <= pos && !matched; start++ {
7691
+ if m.match(node.child, start, func(end int) bool { return end == pos }) {
7692
+ matched = true
7693
+ }
7694
+ }
7695
+ m.caps = saved
7696
+ if matched != node.negative {
7697
+ return k(pos)
7698
+ }
7699
+ return false
7700
+ case "rep":
7701
+ return m.rep(node, pos, 0, k)
7702
+ }
7703
+ return false
7704
+ }
7705
+
7706
+ func (m *tnBtMatchState) rep(node *tnBtNode, pos, count int, k func(int) bool) bool {
7707
+ if count < node.min {
7708
+ return m.match(node.child, pos, func(p2 int) bool {
7709
+ return m.rep(node, p2, count+1, k)
7710
+ })
7711
+ }
7712
+ tryMore := func() bool {
7713
+ if node.max >= 0 && count >= node.max {
7714
+ return false
7715
+ }
7716
+ return m.match(node.child, pos, func(p2 int) bool {
7717
+ if p2 == pos {
7718
+ return false
7719
+ }
7720
+ return m.rep(node, p2, count+1, k)
7721
+ })
7722
+ }
7723
+ if node.greedy {
7724
+ if tryMore() {
7725
+ return true
7726
+ }
7727
+ return k(pos)
7728
+ }
7729
+ if k(pos) {
7730
+ return true
7731
+ }
7732
+ return tryMore()
7733
+ }`,
7734
+ objectAssign: `// Object.assign(target, ...sources): copies every source's properties
7735
+ // (struct fields, maps, dynamic objects) into a fresh ordered map
7736
+ func TnObjectAssign(target interface{}, sources ...interface{}) *TnMap[string, interface{}] {
7737
+ result := TnNewMap[string, interface{}]()
7738
+ tnAssignInto(result, target)
7739
+ for _, src := range sources {
7740
+ tnAssignInto(result, src)
7741
+ }
7742
+ return result
7743
+ }
7744
+
7745
+ func tnAssignInto(dst *TnMap[string, interface{}], src interface{}) {
7746
+ if src == nil {
7747
+ return
7748
+ }
7749
+ switch s := src.(type) {
7750
+ case map[string]interface{}:
7751
+ for k, v := range s {
7752
+ dst.Set(k, v)
7753
+ }
7754
+ return
7755
+ case *TnMap[string, interface{}]:
7756
+ for _, k := range s.Keys() {
7757
+ dst.Set(k, s.Get(k))
7758
+ }
7759
+ return
7760
+ }
7761
+ rv := reflect.ValueOf(src)
7762
+ if rv.Kind() == reflect.Pointer {
7763
+ if rv.IsNil() {
7764
+ return
7765
+ }
7766
+ rv = rv.Elem()
7767
+ }
7768
+ if rv.Kind() == reflect.Struct {
7769
+ rt := rv.Type()
7770
+ for i := 0; i < rt.NumField(); i++ {
7771
+ if rv.Field(i).Kind() == reflect.Func {
7772
+ continue
7773
+ }
7774
+ if !rv.Field(i).CanInterface() {
7775
+ if !rv.Field(i).CanAddr() {
7776
+ continue
7777
+ }
7778
+ dst.Set(rt.Field(i).Name, reflect.NewAt(rv.Field(i).Type(), unsafe.Pointer(rv.Field(i).UnsafeAddr())).Elem().Interface())
7779
+ continue
7780
+ }
7781
+ dst.Set(rt.Field(i).Name, rv.Field(i).Interface())
7782
+ }
7783
+ }
5557
7784
  }`,
5558
7785
  regexReplaceFunc: `func TnRegexReplaceFunc(re *regexp.Regexp, s string, fn func([]string) string, all bool) string {
5559
7786
  limit := 1
@@ -5644,7 +7871,7 @@ func TnGroup(groups []string, i int) string {
5644
7871
  }`,
5645
7872
  fileURLToPath: `func TnFileURLToPath(u string) string {
5646
7873
  trimmed := strings.TrimPrefix(u, "file://")
5647
- if unescaped, err := url.PathUnescape(trimmed); err == nil {
7874
+ if unescaped, err := tnurl.PathUnescape(trimmed); err == nil {
5648
7875
  trimmed = unescaped
5649
7876
  }
5650
7877
  return filepath.FromSlash(trimmed)
@@ -5654,7 +7881,15 @@ func TnGroup(groups []string, i int) string {
5654
7881
  if err != nil {
5655
7882
  panic(err)
5656
7883
  }
5657
- return "file://" + url.PathEscape(filepath.ToSlash(abs))
7884
+ return "file://" + tnurl.PathEscape(filepath.ToSlash(abs))
7885
+ }`,
7886
+ moduleUrl: `// import.meta.url: the file URL of the running program
7887
+ func TnModuleURL() string {
7888
+ exe, err := os.Executable()
7889
+ if err != nil {
7890
+ panic(err)
7891
+ }
7892
+ return TnPathToFileURL(exe)
5658
7893
  }`,
5659
7894
  osPlatform: `func TnOsPlatform() string {
5660
7895
  if runtime.GOOS == "windows" {
@@ -5664,6 +7899,46 @@ func TnGroup(groups []string, i int) string {
5664
7899
  return "darwin"
5665
7900
  }
5666
7901
  return runtime.GOOS
7902
+ }`,
7903
+ error: `// TnError: JS Error values (thrown and caught) with a message
7904
+ type TnError struct {
7905
+ message string
7906
+ }
7907
+
7908
+ func (e *TnError) Error() string { return e.message }
7909
+
7910
+ func TnNewError(message string) *TnError {
7911
+ return &TnError{message: message}
7912
+ }`,
7913
+ asyncWait: `// Pending async work (promise callbacks, detached async calls) is drained
7914
+ // before the process exits
7915
+ var __tnWaitGroup sync.WaitGroup`,
7916
+ bigint: `func TnBigInt(s string) *tnbig.Int {
7917
+ v, _ := new(tnbig.Int).SetString(s, 10)
7918
+ return v
7919
+ }`,
7920
+ symbol: `// TnSymbol: JS symbol values (unique by identity)
7921
+ type TnSymbol struct {
7922
+ description string
7923
+ }
7924
+
7925
+ func TnNewSymbol(description string) *TnSymbol {
7926
+ return &TnSymbol{description: description}
7927
+ }`,
7928
+ timerWait: `// Pending setTimeout timers are waited on before the process exits
7929
+ var __tnTimers sync.WaitGroup`,
7930
+ promiseAll: `// Promise.all: every channel drained in order, results collected
7931
+ func TnPromiseAll[T any](chs []chan T) chan []T {
7932
+ out := make(chan []T)
7933
+ go func() {
7934
+ results := make([]T, len(chs))
7935
+ for i, ch := range chs {
7936
+ results[i] = <-ch
7937
+ }
7938
+ out <- results
7939
+ close(out)
7940
+ }()
7941
+ return out
5667
7942
  }`,
5668
7943
  exec: `type tnExecResult struct {
5669
7944
  stdout string
@@ -5887,6 +8162,16 @@ function visitImportDeclaration(node) {
5887
8162
  }
5888
8163
  return '';
5889
8164
  }
8165
+ // Go packages imported under a fixed alias to avoid collisions with user
8166
+ // variables (a variable named `big` would shadow the package name)
8167
+ const goImportAliases = {
8168
+ 'math/big': 'tnbig',
8169
+ 'net/url': 'tnurl'
8170
+ };
8171
+ function goImportLine(pkg) {
8172
+ const alias = goImportAliases[pkg];
8173
+ return alias ? `import ${alias} "${pkg}"` : `import "${pkg}"`;
8174
+ }
5890
8175
  // Records the type of a variable introduced by a loop or binding
5891
8176
  function registerLocalVariable(name, goType) {
5892
8177
  if (!name || name === '_')
@@ -5895,6 +8180,8 @@ function registerLocalVariable(name, goType) {
5895
8180
  variableClassNames.delete(name);
5896
8181
  variableTypeNodes.delete(name);
5897
8182
  narrowedVariables.delete(name);
8183
+ // Loop and binding variables hold plain values in our lowering
8184
+ referenceArrays.delete(name);
5898
8185
  if (goType)
5899
8186
  variableGoTypes.set(name, goType);
5900
8187
  else
@@ -5904,6 +8191,32 @@ function registerLocalVariable(name, goType) {
5904
8191
  function visitForOfSequence(node, iterExpr, iterType) {
5905
8192
  if (iterType === 'interface{}')
5906
8193
  iterType = '[]interface{}';
8194
+ // An iterable class instance: range its Symbol_iterator channel
8195
+ if (iterType?.startsWith('*')) {
8196
+ const iterClass = iterType.slice(1).replace(/\[.*\]$/, '');
8197
+ const yieldType = classIteratorMethods.get(iterClass);
8198
+ if (yieldType) {
8199
+ const declaration = isVariableDeclarationList(node.initializer)
8200
+ ? node.initializer.declarations[0]
8201
+ : undefined;
8202
+ if (declaration && isIdentifier(declaration.name)) {
8203
+ registerLocalVariable(declaration.name.text, yieldType);
8204
+ return `for ${visit(declaration.name)} := range ${iterExpr}.Symbol_iterator()${visitLoopBody(node.statement)}`;
8205
+ }
8206
+ return `for _ = range ${iterExpr}.Symbol_iterator()${visitLoopBody(node.statement)}`;
8207
+ }
8208
+ }
8209
+ if (iterType?.startsWith('chan ')) {
8210
+ const elemType = iterType.slice(5) || 'interface{}';
8211
+ const declaration = isVariableDeclarationList(node.initializer)
8212
+ ? node.initializer.declarations[0]
8213
+ : undefined;
8214
+ if (declaration && isIdentifier(declaration.name)) {
8215
+ registerLocalVariable(declaration.name.text, elemType);
8216
+ return `for ${visit(declaration.name)} := range ${iterExpr}${visitLoopBody(node.statement)}`;
8217
+ }
8218
+ return `for _ = range ${iterExpr}${visitLoopBody(node.statement)}`;
8219
+ }
5907
8220
  const elementType = iterType === 'string' ? 'string' : iterType?.startsWith('[]') ? iterType.slice(2) : undefined;
5908
8221
  const declaration = isVariableDeclarationList(node.initializer)
5909
8222
  ? node.initializer.declarations[0]
@@ -6019,6 +8332,9 @@ function visitTryStatement(node, options) {
6019
8332
  if (node.catchClause) {
6020
8333
  const varDecl = node.catchClause.variableDeclaration;
6021
8334
  const catchVar = varDecl ? visit(varDecl.name) : '_r';
8335
+ if (varDecl && isIdentifier(varDecl.name)) {
8336
+ registerLocalVariable(varDecl.name.text, 'interface{}');
8337
+ }
6022
8338
  const catchBody = (node.catchClause.block.statements ?? []).map((s) => visit(s)).join('\t');
6023
8339
  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}()`);
6024
8340
  }