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.
- package/compiler/analysis.go +2 -2
- package/compiler/assignment.go +26 -0
- package/compiler/builtin_test.go +2 -0
- package/compiler/compiler.go +12 -2
- package/compiler/compiler_test.go +0 -53
- package/compiler/expr-call.go +121 -2
- package/compiler/expr.go +66 -1
- package/compiler/lit.go +1 -1
- package/compiler/stmt-assign.go +106 -90
- package/compiler/stmt-for.go +78 -1
- package/compiler/stmt-range.go +333 -461
- package/compiler/stmt.go +20 -0
- package/compiler/type.go +11 -8
- package/dist/gs/builtin/builtin.d.ts +7 -0
- package/dist/gs/builtin/builtin.js +30 -0
- package/dist/gs/builtin/builtin.js.map +1 -1
- package/dist/gs/builtin/map.d.ts +4 -4
- package/dist/gs/builtin/map.js +12 -6
- package/dist/gs/builtin/map.js.map +1 -1
- package/dist/gs/builtin/slice.d.ts +7 -7
- package/dist/gs/builtin/slice.js +19 -9
- package/dist/gs/builtin/slice.js.map +1 -1
- package/dist/gs/maps/index.d.ts +2 -0
- package/dist/gs/maps/index.js +3 -0
- package/dist/gs/maps/index.js.map +1 -0
- package/dist/gs/maps/iter.gs.d.ts +7 -0
- package/dist/gs/maps/iter.gs.js +65 -0
- package/dist/gs/maps/iter.gs.js.map +1 -0
- package/dist/gs/maps/maps.gs.d.ts +7 -0
- package/dist/gs/maps/maps.gs.js +79 -0
- package/dist/gs/maps/maps.gs.js.map +1 -0
- package/dist/gs/slices/slices.d.ts +6 -0
- package/dist/gs/slices/slices.js +8 -0
- package/dist/gs/slices/slices.js.map +1 -1
- package/gs/builtin/builtin.ts +38 -0
- package/gs/builtin/map.ts +10 -9
- package/gs/builtin/slice.ts +23 -11
- package/gs/maps/index.ts +2 -0
- package/gs/maps/iter.gs.ts +71 -0
- package/gs/maps/maps.gs.ts +87 -0
- package/gs/slices/slices.ts +9 -0
- package/package.json +1 -1
package/compiler/analysis.go
CHANGED
|
@@ -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
|
|
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
|
}
|
package/compiler/assignment.go
CHANGED
|
@@ -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
|
}
|
package/compiler/builtin_test.go
CHANGED
package/compiler/compiler.go
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
}
|
package/compiler/expr-call.go
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
|
package/compiler/stmt-assign.go
CHANGED
|
@@ -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
|
-
//
|
|
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
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
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
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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.
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
-
|
|
369
|
-
c.tsw.
|
|
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
|
-
|
|
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
|