goscript 0.0.33 → 0.0.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/compiler/analysis.go +2 -2
  2. package/compiler/assignment.go +26 -0
  3. package/compiler/builtin_test.go +2 -0
  4. package/compiler/compiler.go +12 -2
  5. package/compiler/compiler_test.go +0 -53
  6. package/compiler/expr-call.go +121 -2
  7. package/compiler/expr.go +66 -1
  8. package/compiler/lit.go +1 -1
  9. package/compiler/stmt-assign.go +106 -90
  10. package/compiler/stmt-for.go +78 -1
  11. package/compiler/stmt-range.go +333 -461
  12. package/compiler/stmt.go +20 -0
  13. package/compiler/type.go +11 -8
  14. package/dist/gs/builtin/builtin.d.ts +7 -0
  15. package/dist/gs/builtin/builtin.js +30 -0
  16. package/dist/gs/builtin/builtin.js.map +1 -1
  17. package/dist/gs/builtin/map.d.ts +4 -4
  18. package/dist/gs/builtin/map.js +12 -6
  19. package/dist/gs/builtin/map.js.map +1 -1
  20. package/dist/gs/builtin/slice.d.ts +7 -7
  21. package/dist/gs/builtin/slice.js +19 -9
  22. package/dist/gs/builtin/slice.js.map +1 -1
  23. package/dist/gs/maps/index.d.ts +2 -0
  24. package/dist/gs/maps/index.js +3 -0
  25. package/dist/gs/maps/index.js.map +1 -0
  26. package/dist/gs/maps/iter.gs.d.ts +7 -0
  27. package/dist/gs/maps/iter.gs.js +65 -0
  28. package/dist/gs/maps/iter.gs.js.map +1 -0
  29. package/dist/gs/maps/maps.gs.d.ts +7 -0
  30. package/dist/gs/maps/maps.gs.js +79 -0
  31. package/dist/gs/maps/maps.gs.js.map +1 -0
  32. package/dist/gs/slices/slices.d.ts +6 -0
  33. package/dist/gs/slices/slices.js +8 -0
  34. package/dist/gs/slices/slices.js.map +1 -1
  35. package/gs/builtin/builtin.ts +38 -0
  36. package/gs/builtin/map.ts +10 -9
  37. package/gs/builtin/slice.ts +23 -11
  38. package/gs/maps/index.ts +2 -0
  39. package/gs/maps/iter.gs.ts +71 -0
  40. package/gs/maps/maps.gs.ts +87 -0
  41. package/gs/slices/slices.ts +9 -0
  42. package/package.json +1 -1
@@ -864,8 +864,8 @@ func AnalyzeFile(file *ast.File, pkg *packages.Package, analysis *Analysis, cmap
864
864
  importVars: make(map[string]struct{}),
865
865
  }
866
866
 
867
- // Use the import name or path as the key
868
- key := path
867
+ // Use the import name or package name as the key
868
+ key := packageNameFromGoPath(path)
869
869
  if name != "" {
870
870
  key = name
871
871
  }
@@ -197,6 +197,7 @@ func (c *GoToTSCompiler) writeAssignmentCore(lhs, rhs []ast.Expr, tok token.Toke
197
197
  currentIsMapIndex := false
198
198
  if indexExpr, ok := l.(*ast.IndexExpr); ok {
199
199
  if tv, ok := c.pkg.TypesInfo.Types[indexExpr.X]; ok {
200
+ // Check if it's a concrete map type
200
201
  if _, isMap := tv.Type.Underlying().(*types.Map); isMap {
201
202
  currentIsMapIndex = true
202
203
  if i == 0 {
@@ -213,6 +214,31 @@ func (c *GoToTSCompiler) writeAssignmentCore(lhs, rhs []ast.Expr, tok token.Toke
213
214
  }
214
215
  c.tsw.WriteLiterally(", ")
215
216
  // Value will be added after operator and RHS
217
+ } else if typeParam, isTypeParam := tv.Type.(*types.TypeParam); isTypeParam {
218
+ // Check if the type parameter is constrained to be a map type
219
+ constraint := typeParam.Constraint()
220
+ if constraint != nil {
221
+ underlying := constraint.Underlying()
222
+ if iface, isInterface := underlying.(*types.Interface); isInterface {
223
+ if hasMapConstraint(iface) {
224
+ currentIsMapIndex = true
225
+ if i == 0 {
226
+ isMapIndexLHS = true
227
+ }
228
+ // Use mapSet helper for type parameter constrained to map
229
+ c.tsw.WriteLiterally("$.mapSet(")
230
+ if err := c.WriteValueExpr(indexExpr.X); err != nil { // Map
231
+ return err
232
+ }
233
+ c.tsw.WriteLiterally(", ")
234
+ if err := c.WriteValueExpr(indexExpr.Index); err != nil { // Key
235
+ return err
236
+ }
237
+ c.tsw.WriteLiterally(", ")
238
+ // Value will be added after operator and RHS
239
+ }
240
+ }
241
+ }
216
242
  }
217
243
  }
218
244
  }
@@ -1,3 +1,5 @@
1
+ //go:build test_emit_builtin
2
+
1
3
  package compiler
2
4
 
3
5
  import (
@@ -892,14 +892,24 @@ func (c *GoToTSCompiler) WriteDoc(doc *ast.CommentGroup) {
892
892
  }
893
893
 
894
894
  // sanitizeIdentifier checks if an identifier is a JavaScript/TypeScript reserved word
895
- // and prefixes it with an underscore if it is. This prevents compilation errors
896
- // when Go identifiers conflict with JS/TS keywords.
895
+ // or conflicts with built-in types, and transforms it if needed. This prevents
896
+ // compilation errors when Go identifiers conflict with JS/TS keywords or built-ins.
897
897
  func (c *GoToTSCompiler) sanitizeIdentifier(name string) string {
898
898
  // Don't sanitize boolean literals - they are valid in both Go and JS/TS
899
899
  if name == "true" || name == "false" {
900
900
  return name
901
901
  }
902
902
 
903
+ // Handle TypeScript built-in types that conflict with Go type parameter names
904
+ builtinTypes := map[string]string{
905
+ "Map": "MapType",
906
+ // Add other built-in types as needed
907
+ }
908
+
909
+ if replacement, exists := builtinTypes[name]; exists {
910
+ return replacement
911
+ }
912
+
903
913
  // List of JavaScript/TypeScript reserved words that could conflict
904
914
  reservedWords := map[string]bool{
905
915
  "abstract": true,
@@ -1,7 +1,6 @@
1
1
  package compiler_test
2
2
 
3
3
  import (
4
- "context"
5
4
  "os"
6
5
  "os/exec"
7
6
  "path/filepath"
@@ -12,9 +11,7 @@ import (
12
11
  "sync/atomic"
13
12
  "testing"
14
13
 
15
- "github.com/aperturerobotics/goscript/compiler"
16
14
  "github.com/aperturerobotics/goscript/compliance"
17
- "github.com/sirupsen/logrus"
18
15
  )
19
16
 
20
17
  // NOTE: this is here instead of compliance/compliance_test.go so coverage ends up in this package.
@@ -140,53 +137,3 @@ func getParentGoModulePath() (string, error) {
140
137
  }
141
138
  return strings.TrimSpace(string(output)), nil
142
139
  }
143
-
144
- func TestUnsafePackageCompilation(t *testing.T) {
145
- // Create a temporary directory for the test output
146
- tempDir, err := os.MkdirTemp("", "goscript-test-unsafe")
147
- if err != nil {
148
- t.Fatalf("Failed to create temp dir: %v", err)
149
- }
150
- defer os.RemoveAll(tempDir)
151
-
152
- // Setup logger
153
- log := logrus.New()
154
- log.SetLevel(logrus.DebugLevel)
155
- le := logrus.NewEntry(log)
156
-
157
- // Test with AllDependencies=true and DisableEmitBuiltin=false to ensure handwritten packages are copied
158
- config := &compiler.Config{
159
- OutputPath: tempDir,
160
- AllDependencies: true,
161
- DisableEmitBuiltin: false, // This ensures handwritten packages are copied to output
162
- }
163
-
164
- comp, err := compiler.NewCompiler(config, le, nil)
165
- if err != nil {
166
- t.Fatalf("Failed to create compiler: %v", err)
167
- }
168
-
169
- // Try to compile a package that has dependencies that import unsafe
170
- // We'll use "sync/atomic" which imports unsafe but doesn't have a handwritten equivalent
171
- result, err := comp.CompilePackages(context.Background(), "sync/atomic")
172
- // This should now succeed since we have a handwritten unsafe package
173
- if err != nil {
174
- t.Fatalf("Expected compilation to succeed with handwritten unsafe package, but it failed: %v", err)
175
- }
176
-
177
- // Verify that the unsafe package was copied (not compiled) since it has a handwritten equivalent
178
- if !slices.Contains(result.CopiedPackages, "unsafe") {
179
- t.Errorf("Expected unsafe package to be in CopiedPackages, but it wasn't. CopiedPackages: %v", result.CopiedPackages)
180
- }
181
-
182
- // Verify that sync/atomic was compiled
183
- if !slices.Contains(result.CompiledPackages, "sync/atomic") {
184
- t.Errorf("Expected sync/atomic package to be in CompiledPackages, but it wasn't. CompiledPackages: %v", result.CompiledPackages)
185
- }
186
-
187
- // Check that the unsafe package directory exists in the output
188
- unsafePath := filepath.Join(tempDir, "@goscript/unsafe")
189
- if _, err := os.Stat(unsafePath); os.IsNotExist(err) {
190
- t.Errorf("unsafe package directory was not created at %s", unsafePath)
191
- }
192
- }
@@ -545,6 +545,114 @@ func (c *GoToTSCompiler) WriteCallExpr(exp *ast.CallExpr) error {
545
545
  }
546
546
  }
547
547
  }
548
+ // Handle selector expressions: make(pkg.TypeName, ...)
549
+ // This handles cases like: make(fstest.MapFS) where fstest.MapFS is map[string]*MapFile
550
+ if selectorExpr, ok := exp.Args[0].(*ast.SelectorExpr); ok {
551
+ // Get the type information for the selector expression
552
+ if typ := c.pkg.TypesInfo.TypeOf(selectorExpr); typ != nil {
553
+ // Check the underlying type of the selector expression
554
+ underlying := typ.Underlying()
555
+
556
+ // Handle selector expression map types: make(pkg.MapType)
557
+ if mapType, isMap := underlying.(*types.Map); isMap {
558
+ c.tsw.WriteLiterally("$.makeMap<")
559
+ c.WriteGoType(mapType.Key(), GoTypeContextGeneral) // Write the key type
560
+ c.tsw.WriteLiterally(", ")
561
+ c.WriteGoType(mapType.Elem(), GoTypeContextGeneral) // Write the value type
562
+ c.tsw.WriteLiterally(">()")
563
+ return nil // Handled make for selector expression map type
564
+ }
565
+
566
+ // Handle selector expression slice types: make(pkg.SliceType, len, cap)
567
+ if sliceType, isSlice := underlying.(*types.Slice); isSlice {
568
+ goElemType := sliceType.Elem()
569
+
570
+ // Check if it's a selector expression with []byte underlying type
571
+ if basicElem, isBasic := goElemType.(*types.Basic); isBasic && basicElem.Kind() == types.Uint8 {
572
+ c.tsw.WriteLiterally("new Uint8Array(")
573
+ if len(exp.Args) >= 2 {
574
+ if err := c.WriteValueExpr(exp.Args[1]); err != nil { // Length
575
+ return err
576
+ }
577
+ // Capacity argument for make([]byte, len, cap) is ignored for new Uint8Array(len)
578
+ } else {
579
+ // If no length is provided, default to 0
580
+ c.tsw.WriteLiterally("0")
581
+ }
582
+ c.tsw.WriteLiterally(")")
583
+ return nil // Handled make for selector expression []byte type
584
+ }
585
+
586
+ // Handle other selector expression slice types
587
+ c.tsw.WriteLiterally("$.makeSlice<")
588
+ c.WriteGoType(goElemType, GoTypeContextGeneral) // Write the element type
589
+ c.tsw.WriteLiterally(">(")
590
+
591
+ if len(exp.Args) >= 2 {
592
+ if err := c.WriteValueExpr(exp.Args[1]); err != nil { // Length
593
+ return err
594
+ }
595
+ if len(exp.Args) == 3 {
596
+ c.tsw.WriteLiterally(", ")
597
+ if err := c.WriteValueExpr(exp.Args[2]); err != nil { // Capacity
598
+ return err
599
+ }
600
+ } else if len(exp.Args) > 3 {
601
+ return errors.New("makeSlice expects 2 or 3 arguments")
602
+ }
603
+ } else {
604
+ // If no length is provided, default to 0
605
+ c.tsw.WriteLiterally("0")
606
+ }
607
+ c.tsw.WriteLiterally(")")
608
+ return nil // Handled make for selector expression slice type
609
+ }
610
+
611
+ // Handle selector expression channel types: make(pkg.ChannelType, bufferSize)
612
+ if chanType, isChan := underlying.(*types.Chan); isChan {
613
+ c.tsw.WriteLiterally("$.makeChannel<")
614
+ c.WriteGoType(chanType.Elem(), GoTypeContextGeneral)
615
+ c.tsw.WriteLiterally(">(")
616
+
617
+ // If buffer size is provided, add it
618
+ if len(exp.Args) >= 2 {
619
+ if err := c.WriteValueExpr(exp.Args[1]); err != nil {
620
+ return fmt.Errorf("failed to write buffer size in makeChannel: %w", err)
621
+ }
622
+ } else {
623
+ // Default to 0 (unbuffered channel)
624
+ c.tsw.WriteLiterally("0")
625
+ }
626
+
627
+ c.tsw.WriteLiterally(", ") // Add comma for zero value argument
628
+
629
+ // Write the zero value for the channel's element type
630
+ if chanType.Elem().String() == "struct{}" {
631
+ c.tsw.WriteLiterally("{}")
632
+ } else {
633
+ c.WriteZeroValueForType(chanType.Elem())
634
+ }
635
+
636
+ // Add direction parameter
637
+ c.tsw.WriteLiterally(", ")
638
+
639
+ // Determine channel direction
640
+ switch chanType.Dir() {
641
+ case types.SendRecv:
642
+ c.tsw.WriteLiterally("'both'")
643
+ case types.SendOnly:
644
+ c.tsw.WriteLiterally("'send'")
645
+ case types.RecvOnly:
646
+ c.tsw.WriteLiterally("'receive'")
647
+ default:
648
+ c.tsw.WriteLiterally("'both'") // Default to bidirectional
649
+ }
650
+
651
+ c.tsw.WriteLiterally(")")
652
+ return nil // Handled make for selector expression channel type
653
+ }
654
+ }
655
+ }
548
656
  // Fallthrough for unhandled make calls (e.g., channels)
549
657
  return errors.New("unhandled make call")
550
658
  case "string":
@@ -879,12 +987,16 @@ func (c *GoToTSCompiler) WriteCallExpr(exp *ast.CallExpr) error {
879
987
  }
880
988
  c.tsw.WriteLiterally(")")
881
989
  } else {
882
- // Not an identifier (e.g., method call on a value)
990
+ // Not an identifier (e.g., method call on a value, function call result)
883
991
  if err := c.WriteValueExpr(expFun); err != nil {
884
992
  return fmt.Errorf("failed to write method expression in call: %w", err)
885
993
  }
886
994
 
887
- if funType := c.pkg.TypesInfo.TypeOf(expFun); funType != nil {
995
+ // Check if this is a function call that returns a function (e.g., simpleIterator(m)())
996
+ // Add non-null assertion since the returned function could be null
997
+ if _, isCallExpr := expFun.(*ast.CallExpr); isCallExpr {
998
+ c.tsw.WriteLiterally("!")
999
+ } else if funType := c.pkg.TypesInfo.TypeOf(expFun); funType != nil {
888
1000
  if _, ok := funType.Underlying().(*types.Signature); ok {
889
1001
  // Check if this is a function parameter identifier that needs not-null assertion
890
1002
  if ident, isIdent := expFun.(*ast.Ident); isIdent {
@@ -899,6 +1011,13 @@ func (c *GoToTSCompiler) WriteCallExpr(exp *ast.CallExpr) error {
899
1011
  } else if _, isNamed := funType.(*types.Named); isNamed {
900
1012
  c.tsw.WriteLiterally("!")
901
1013
  }
1014
+ } else {
1015
+ // Check if the function type is nullable (e.g., func(...) | null)
1016
+ // This handles cases where a function call returns a nullable function
1017
+ funTypeStr := funType.String()
1018
+ if strings.Contains(funTypeStr, "| null") || strings.Contains(funTypeStr, "null |") {
1019
+ c.tsw.WriteLiterally("!")
1020
+ }
902
1021
  }
903
1022
  }
904
1023
  }
package/compiler/expr.go CHANGED
@@ -52,7 +52,7 @@ func (c *GoToTSCompiler) WriteIndexExpr(exp *ast.IndexExpr) error {
52
52
 
53
53
  // Generate the zero value as the default value for mapGet
54
54
  c.WriteZeroValueForType(mapType.Elem())
55
- c.tsw.WriteLiterally(")")
55
+ c.tsw.WriteLiterally(")[0]") // Extract the value from the tuple
56
56
  return nil
57
57
  }
58
58
 
@@ -77,6 +77,31 @@ func (c *GoToTSCompiler) WriteIndexExpr(exp *ast.IndexExpr) error {
77
77
  if constraint != nil {
78
78
  underlying := constraint.Underlying()
79
79
  if iface, isInterface := underlying.(*types.Interface); isInterface {
80
+ // Check if this is a map constraint (like ~map[K]V)
81
+ if hasMapConstraint(iface) {
82
+ // This is a map type parameter, use map access
83
+ c.tsw.WriteLiterally("$.mapGet(")
84
+ if err := c.WriteValueExpr(exp.X); err != nil {
85
+ return err
86
+ }
87
+ c.tsw.WriteLiterally(", ")
88
+ if err := c.WriteValueExpr(exp.Index); err != nil {
89
+ return err
90
+ }
91
+ c.tsw.WriteLiterally(", ")
92
+
93
+ // Generate the zero value as the default value for mapGet
94
+ // For type parameters, we need to get the value type from the constraint
95
+ mapValueType := getMapValueTypeFromConstraint(iface)
96
+ if mapValueType != nil {
97
+ c.WriteZeroValueForType(mapValueType)
98
+ } else {
99
+ c.tsw.WriteLiterally("null")
100
+ }
101
+ c.tsw.WriteLiterally(")[0]") // Extract the value from the tuple
102
+ return nil
103
+ }
104
+
80
105
  // Check if this is a mixed string/byte constraint (like string | []byte)
81
106
  if hasMixedStringByteConstraint(iface) {
82
107
  // For mixed constraints, use specialized function that returns number (byte value)
@@ -694,3 +719,43 @@ func (c *GoToTSCompiler) WriteKeyValueExpr(exp *ast.KeyValueExpr) error {
694
719
  }
695
720
  return nil
696
721
  }
722
+
723
+ // hasMapConstraint checks if an interface constraint includes map types
724
+ // For constraints like ~map[K]V, this returns true
725
+ func hasMapConstraint(iface *types.Interface) bool {
726
+ // Check if the interface has type terms that include map types
727
+ for i := 0; i < iface.NumEmbeddeds(); i++ {
728
+ embedded := iface.EmbeddedType(i)
729
+ if union, ok := embedded.(*types.Union); ok {
730
+ for j := 0; j < union.Len(); j++ {
731
+ term := union.Term(j)
732
+ if _, isMap := term.Type().Underlying().(*types.Map); isMap {
733
+ return true
734
+ }
735
+ }
736
+ } else if _, isMap := embedded.Underlying().(*types.Map); isMap {
737
+ return true
738
+ }
739
+ }
740
+ return false
741
+ }
742
+
743
+ // getMapValueTypeFromConstraint extracts the value type from a map constraint
744
+ // For constraints like ~map[K]V, this returns V
745
+ func getMapValueTypeFromConstraint(iface *types.Interface) types.Type {
746
+ // Check if the interface has type terms that include map types
747
+ for i := 0; i < iface.NumEmbeddeds(); i++ {
748
+ embedded := iface.EmbeddedType(i)
749
+ if union, ok := embedded.(*types.Union); ok {
750
+ for j := 0; j < union.Len(); j++ {
751
+ term := union.Term(j)
752
+ if mapType, isMap := term.Type().Underlying().(*types.Map); isMap {
753
+ return mapType.Elem()
754
+ }
755
+ }
756
+ } else if mapType, isMap := embedded.Underlying().(*types.Map); isMap {
757
+ return mapType.Elem()
758
+ }
759
+ }
760
+ return nil
761
+ }
package/compiler/lit.go CHANGED
@@ -118,7 +118,7 @@ func (c *GoToTSCompiler) WriteFuncLitValue(exp *ast.FuncLit) error {
118
118
  }
119
119
 
120
120
  // Write function body
121
- if err := c.WriteStmtBlock(exp.Body, false); err != nil {
121
+ if err := c.WriteStmtBlock(exp.Body, true); err != nil {
122
122
  return fmt.Errorf("failed to write block statement: %w", err)
123
123
  }
124
124
 
@@ -276,122 +276,106 @@ func (c *GoToTSCompiler) WriteStmtAssign(exp *ast.AssignStmt) error {
276
276
  }
277
277
 
278
278
  // writeMapLookupWithExists handles the map comma-ok idiom: value, exists := myMap[key]
279
- // Note: We don't use WriteIndexExpr here because we need to handle .has() and .get() separately
279
+ // Uses array destructuring with the tuple-returning $.mapGet function
280
280
  writeMapLookupWithExists := func(lhs []ast.Expr, indexExpr *ast.IndexExpr, tok token.Token) error {
281
281
  // First check that we have exactly two LHS expressions (value and exists)
282
282
  if len(lhs) != 2 {
283
283
  return fmt.Errorf("map comma-ok idiom requires exactly 2 variables on LHS, got %d", len(lhs))
284
284
  }
285
285
 
286
- // Check for blank identifiers and get variable names
286
+ // Check for blank identifiers
287
287
  valueIsBlank := false
288
288
  existsIsBlank := false
289
- var valueName string
290
- var existsName string
291
289
 
292
- if valIdent, ok := lhs[0].(*ast.Ident); ok {
293
- if valIdent.Name == "_" {
294
- valueIsBlank = true
295
- } else {
296
- valueName = valIdent.Name
297
- }
298
- } else {
299
- return fmt.Errorf("unhandled LHS expression type for value in map comma-ok: %T", lhs[0])
290
+ if valIdent, ok := lhs[0].(*ast.Ident); ok && valIdent.Name == "_" {
291
+ valueIsBlank = true
292
+ }
293
+ if existsIdent, ok := lhs[1].(*ast.Ident); ok && existsIdent.Name == "_" {
294
+ existsIsBlank = true
300
295
  }
301
296
 
302
- if existsIdent, ok := lhs[1].(*ast.Ident); ok {
303
- if existsIdent.Name == "_" {
304
- existsIsBlank = true
305
- } else {
306
- existsName = existsIdent.Name
307
- }
297
+ // Use array destructuring with mapGet tuple return
298
+ if tok == token.DEFINE {
299
+ c.tsw.WriteLiterally("let ")
308
300
  } else {
309
- return fmt.Errorf("unhandled LHS expression type for exists in map comma-ok: %T", lhs[1])
301
+ // Add semicolon before destructuring assignment to prevent TypeScript
302
+ // from interpreting it as array access on the previous line
303
+ c.tsw.WriteLiterally(";")
310
304
  }
311
305
 
312
- // Declare variables if using := and not blank
313
- if tok == token.DEFINE {
314
- if !valueIsBlank {
315
- c.tsw.WriteLiterally("let ")
316
- c.tsw.WriteLiterally(valueName)
317
- // TODO: Add type annotation based on map value type
318
- c.tsw.WriteLine("")
319
- }
320
- if !existsIsBlank {
321
- c.tsw.WriteLiterally("let ")
322
- c.tsw.WriteLiterally(existsName)
323
- c.tsw.WriteLiterally(": boolean") // exists is always boolean
324
- c.tsw.WriteLine("")
306
+ c.tsw.WriteLiterally("[")
307
+
308
+ // Write LHS variables, handling blanks
309
+ if !valueIsBlank {
310
+ if err := c.WriteValueExpr(lhs[0]); err != nil {
311
+ return err
325
312
  }
326
313
  }
314
+ // Note: for blank identifiers, we just omit the variable name entirely
315
+
316
+ c.tsw.WriteLiterally(", ")
327
317
 
328
- // Assign 'exists'
329
318
  if !existsIsBlank {
330
- c.tsw.WriteLiterally(existsName)
331
- c.tsw.WriteLiterally(" = ")
332
- c.tsw.WriteLiterally("$.mapHas(")
333
- if err := c.WriteValueExpr(indexExpr.X); err != nil { // Map
334
- return err
335
- }
336
- c.tsw.WriteLiterally(", ")
337
- if err := c.WriteValueExpr(indexExpr.Index); err != nil { // Key
319
+ if err := c.WriteValueExpr(lhs[1]); err != nil {
338
320
  return err
339
321
  }
340
- c.tsw.WriteLiterally(")")
341
- c.tsw.WriteLine("")
342
322
  }
323
+ // Note: for blank identifiers, we just omit the variable name entirely
343
324
 
344
- // Assign 'value'
345
- if !valueIsBlank {
346
- c.tsw.WriteLiterally(valueName)
347
- c.tsw.WriteLiterally(" = ")
348
- c.tsw.WriteLiterally("$.mapGet(")
349
- if err := c.WriteValueExpr(indexExpr.X); err != nil { // Map
350
- return err
351
- }
352
- c.tsw.WriteLiterally(", ")
353
- if err := c.WriteValueExpr(indexExpr.Index); err != nil { // Key
354
- return err
355
- }
356
- c.tsw.WriteLiterally(", ")
357
- // Write the zero value for the map's value type
358
- if tv, ok := c.pkg.TypesInfo.Types[indexExpr.X]; ok {
359
- if mapType, isMap := tv.Type.Underlying().(*types.Map); isMap {
360
- c.WriteZeroValueForType(mapType.Elem())
325
+ c.tsw.WriteLiterally("] = $.mapGet(")
326
+
327
+ // Write map expression
328
+ if err := c.WriteValueExpr(indexExpr.X); err != nil {
329
+ return err
330
+ }
331
+
332
+ c.tsw.WriteLiterally(", ")
333
+
334
+ // Write key expression
335
+ if err := c.WriteValueExpr(indexExpr.Index); err != nil {
336
+ return err
337
+ }
338
+
339
+ c.tsw.WriteLiterally(", ")
340
+
341
+ // Write the zero value for the map's value type
342
+ if tv, ok := c.pkg.TypesInfo.Types[indexExpr.X]; ok {
343
+ if mapType, isMap := tv.Type.Underlying().(*types.Map); isMap {
344
+ c.WriteZeroValueForType(mapType.Elem())
345
+ } else if typeParam, isTypeParam := tv.Type.(*types.TypeParam); isTypeParam {
346
+ // Handle type parameter constrained to be a map type
347
+ constraint := typeParam.Constraint()
348
+ if constraint != nil {
349
+ underlying := constraint.Underlying()
350
+ if iface, isInterface := underlying.(*types.Interface); isInterface {
351
+ if hasMapConstraint(iface) {
352
+ // Get the value type from the constraint
353
+ mapValueType := getMapValueTypeFromConstraint(iface)
354
+ if mapValueType != nil {
355
+ c.WriteZeroValueForType(mapValueType)
356
+ } else {
357
+ c.tsw.WriteLiterally("null")
358
+ }
359
+ } else {
360
+ c.tsw.WriteLiterally("null")
361
+ }
362
+ } else {
363
+ c.tsw.WriteLiterally("null")
364
+ }
361
365
  } else {
362
- // Fallback zero value if type info is missing or not a map
363
366
  c.tsw.WriteLiterally("null")
364
367
  }
365
368
  } else {
369
+ // Fallback zero value if type info is missing or not a map
366
370
  c.tsw.WriteLiterally("null")
367
371
  }
368
- c.tsw.WriteLiterally(")")
369
- c.tsw.WriteLine("")
370
- } else if existsIsBlank {
371
- // If both are blank, still evaluate for side effects (though .has/.get are usually pure)
372
- // We add a ; otherwise TypeScript thinks we are invoking a function.
373
- c.tsw.WriteLiterally(";(") // Wrap in parens to make it an expression statement
374
- c.tsw.WriteLiterally("$.mapHas(")
375
- if err := c.WriteValueExpr(indexExpr.X); err != nil { // Map
376
- return err
377
- }
378
- c.tsw.WriteLiterally(", ")
379
- if err := c.WriteValueExpr(indexExpr.Index); err != nil { // Key
380
- return err
381
- }
382
- c.tsw.WriteLiterally("), ") // Evaluate .has
383
- c.tsw.WriteLiterally("$.mapGet(")
384
- if err := c.WriteValueExpr(indexExpr.X); err != nil { // Map
385
- return err
386
- }
387
- c.tsw.WriteLiterally(", ")
388
- if err := c.WriteValueExpr(indexExpr.Index); err != nil { // Key
389
- return err
390
- }
391
- c.tsw.WriteLiterally(", null))") // Evaluate .get with null as default
392
- c.tsw.WriteLine("")
372
+ } else {
373
+ c.tsw.WriteLiterally("null")
393
374
  }
394
375
 
376
+ c.tsw.WriteLiterally(")")
377
+ c.tsw.WriteLine("")
378
+
395
379
  return nil
396
380
  }
397
381
 
@@ -417,6 +401,8 @@ func (c *GoToTSCompiler) WriteStmtAssign(exp *ast.AssignStmt) error {
417
401
  }
418
402
  return nil
419
403
  }
404
+ // Handle general function calls that return multiple values
405
+ return writeMultiVarAssignFromCall(exp.Lhs, callExpr, exp.Tok)
420
406
  }
421
407
 
422
408
  if typeAssertExpr, ok := rhsExpr.(*ast.TypeAssertExpr); ok {
@@ -428,10 +414,22 @@ func (c *GoToTSCompiler) WriteStmtAssign(exp *ast.AssignStmt) error {
428
414
  if c.pkg != nil && c.pkg.TypesInfo != nil {
429
415
  tv, ok := c.pkg.TypesInfo.Types[indexExpr.X]
430
416
  if ok {
431
- // Check if it's a map type
417
+ // Check if it's a concrete map type
432
418
  if _, isMap := tv.Type.Underlying().(*types.Map); isMap {
433
419
  return writeMapLookupWithExists(exp.Lhs, indexExpr, exp.Tok)
434
420
  }
421
+ // Check if it's a type parameter constrained to be a map type
422
+ if typeParam, isTypeParam := tv.Type.(*types.TypeParam); isTypeParam {
423
+ constraint := typeParam.Constraint()
424
+ if constraint != nil {
425
+ underlying := constraint.Underlying()
426
+ if iface, isInterface := underlying.(*types.Interface); isInterface {
427
+ if hasMapConstraint(iface) {
428
+ return writeMapLookupWithExists(exp.Lhs, indexExpr, exp.Tok)
429
+ }
430
+ }
431
+ }
432
+ }
435
433
  }
436
434
  }
437
435
  }
@@ -441,8 +439,6 @@ func (c *GoToTSCompiler) WriteStmtAssign(exp *ast.AssignStmt) error {
441
439
  return c.writeChannelReceiveWithOk(exp.Lhs, unaryExpr, exp.Tok)
442
440
  }
443
441
  // If LHS count is not 2, fall through to error or other handling
444
- } else if callExpr, ok := rhsExpr.(*ast.CallExpr); ok {
445
- return writeMultiVarAssignFromCall(exp.Lhs, callExpr, exp.Tok)
446
442
  }
447
443
  // If none of the specific multi-assign patterns match, fall through to the error check below
448
444
  }
@@ -463,7 +459,27 @@ func (c *GoToTSCompiler) WriteStmtAssign(exp *ast.AssignStmt) error {
463
459
 
464
460
  // Ensure LHS and RHS have the same length for valid Go code in these cases
465
461
  if len(exp.Lhs) != len(exp.Rhs) {
466
- return fmt.Errorf("invalid assignment statement: LHS count (%d) != RHS count (%d)", len(exp.Lhs), len(exp.Rhs))
462
+ // Special case: allow multiple LHS with single RHS if RHS can produce multiple values
463
+ // This handles cases like: x, y := getValue() where getValue() returns multiple values
464
+ // or other expressions that can produce multiple values
465
+ if len(exp.Rhs) == 1 {
466
+ // Allow single RHS expressions that can produce multiple values:
467
+ // - Function calls that return multiple values
468
+ // - Type assertions with comma-ok
469
+ // - Map lookups with comma-ok
470
+ // - Channel receives with comma-ok
471
+ // The Go type checker should have already verified this is valid
472
+ rhsExpr := exp.Rhs[0]
473
+ switch rhsExpr.(type) {
474
+ case *ast.CallExpr, *ast.TypeAssertExpr, *ast.IndexExpr, *ast.UnaryExpr:
475
+ // These expression types can potentially produce multiple values
476
+ // Let the general assignment logic handle them
477
+ default:
478
+ return fmt.Errorf("invalid assignment statement: LHS count (%d) != RHS count (%d)", len(exp.Lhs), len(exp.Rhs))
479
+ }
480
+ } else {
481
+ return fmt.Errorf("invalid assignment statement: LHS count (%d) != RHS count (%d)", len(exp.Lhs), len(exp.Rhs))
482
+ }
467
483
  }
468
484
 
469
485
  // Handle multi-variable assignment (e.g., swaps) using writeAssignmentCore