Skip to content

Commit d0d7e43

Browse files
committed
fix: correctly declare named return values
Signed-off-by: Christian Stewart <christian@aperture.us>
1 parent 6b848a6 commit d0d7e43

8 files changed

Lines changed: 290 additions & 40 deletions

File tree

compiler/decl.go

Lines changed: 42 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -101,37 +101,21 @@ func (c *GoToTSCompiler) WriteFuncDeclAsFunction(decl *ast.FuncDecl) error {
101101
c.WriteFuncType(decl.Type, isAsync) // Write signature (params, return type)
102102
c.tsw.WriteLiterally(" ")
103103

104-
hasNamedReturns := false
105-
if decl.Type.Results != nil {
106-
for _, field := range decl.Type.Results.List {
107-
if len(field.Names) > 0 {
108-
hasNamedReturns = true
109-
break
110-
}
111-
}
112-
}
113-
114-
if hasNamedReturns {
104+
if c.hasNamedReturns(decl.Type.Results) {
115105
c.tsw.WriteLine("{")
116106
c.tsw.Indent(1)
117107

118108
// Declare named return variables and initialize them to their zero values
119-
for _, field := range decl.Type.Results.List {
120-
for _, name := range field.Names {
121-
c.tsw.WriteLiterallyf("let %s: ", c.sanitizeIdentifier(name.Name))
122-
c.WriteTypeExpr(field.Type)
123-
c.tsw.WriteLiterally(" = ")
124-
c.WriteZeroValueForType(c.pkg.TypesInfo.TypeOf(field.Type))
125-
c.tsw.WriteLine("")
126-
}
109+
if err := c.writeNamedReturnDeclarations(decl.Type.Results); err != nil {
110+
return fmt.Errorf("failed to write named return declarations: %w", err)
127111
}
128112
}
129113

130114
if err := c.WriteStmt(decl.Body); err != nil {
131115
return fmt.Errorf("failed to write function body: %w", err)
132116
}
133117

134-
if hasNamedReturns {
118+
if c.hasNamedReturns(decl.Type.Results) {
135119
c.tsw.Indent(-1)
136120
c.tsw.WriteLine("}")
137121
}
@@ -244,6 +228,11 @@ func (c *GoToTSCompiler) WriteFuncDeclAsMethod(decl *ast.FuncDecl) error {
244228
}
245229
}
246230

231+
// Declare named return variables and initialize them to their zero values
232+
if err := c.writeNamedReturnDeclarations(decl.Type.Results); err != nil {
233+
return fmt.Errorf("failed to write named return declarations: %w", err)
234+
}
235+
247236
// write method body without outer braces
248237
for _, stmt := range decl.Body.List {
249238
if err := c.WriteStmt(stmt); err != nil {
@@ -263,3 +252,36 @@ func (c *GoToTSCompiler) WriteFuncDeclAsMethod(decl *ast.FuncDecl) error {
263252

264253
return nil
265254
}
255+
256+
// writeNamedReturnDeclarations generates TypeScript variable declarations for named return parameters.
257+
// It declares each named return variable with its appropriate type and zero value.
258+
func (c *GoToTSCompiler) writeNamedReturnDeclarations(results *ast.FieldList) error {
259+
if results == nil {
260+
return nil
261+
}
262+
263+
for _, field := range results.List {
264+
for _, name := range field.Names {
265+
c.tsw.WriteLiterallyf("let %s: ", c.sanitizeIdentifier(name.Name))
266+
c.WriteTypeExpr(field.Type)
267+
c.tsw.WriteLiterally(" = ")
268+
c.WriteZeroValueForType(c.pkg.TypesInfo.TypeOf(field.Type))
269+
c.tsw.WriteLine("")
270+
}
271+
}
272+
return nil
273+
}
274+
275+
// hasNamedReturns checks if a function type has any named return parameters.
276+
func (c *GoToTSCompiler) hasNamedReturns(results *ast.FieldList) bool {
277+
if results == nil {
278+
return false
279+
}
280+
281+
for _, field := range results.List {
282+
if len(field.Names) > 0 {
283+
return true
284+
}
285+
}
286+
return false
287+
}

compiler/lit.go

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -164,29 +164,13 @@ func (c *GoToTSCompiler) WriteFuncLitValue(exp *ast.FuncLit) error {
164164

165165
c.tsw.WriteLiterally(" => ")
166166

167-
hasNamedReturns := false
168-
if exp.Type.Results != nil {
169-
for _, field := range exp.Type.Results.List {
170-
if len(field.Names) > 0 {
171-
hasNamedReturns = true
172-
break
173-
}
174-
}
175-
}
176-
177-
if hasNamedReturns {
167+
if c.hasNamedReturns(exp.Type.Results) {
178168
c.tsw.WriteLine("{")
179169
c.tsw.Indent(1)
180170

181171
// Declare named return variables and initialize them to their zero values
182-
for _, field := range exp.Type.Results.List {
183-
for _, name := range field.Names {
184-
c.tsw.WriteLiterallyf("let %s: ", c.sanitizeIdentifier(name.Name))
185-
c.WriteTypeExpr(field.Type)
186-
c.tsw.WriteLiterally(" = ")
187-
c.WriteZeroValueForType(c.pkg.TypesInfo.TypeOf(field.Type))
188-
c.tsw.WriteLine("")
189-
}
172+
if err := c.writeNamedReturnDeclarations(exp.Type.Results); err != nil {
173+
return fmt.Errorf("failed to write named return declarations: %w", err)
190174
}
191175
}
192176

@@ -195,7 +179,7 @@ func (c *GoToTSCompiler) WriteFuncLitValue(exp *ast.FuncLit) error {
195179
return fmt.Errorf("failed to write block statement: %w", err)
196180
}
197181

198-
if hasNamedReturns {
182+
if c.hasNamedReturns(exp.Type.Results) {
199183
c.tsw.Indent(-1)
200184
c.tsw.WriteLiterally("}")
201185
}

compiler/spec.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,11 @@ func (c *GoToTSCompiler) writeNamedTypeMethod(decl *ast.FuncDecl) error {
364364
}
365365
}
366366

367+
// Declare named return variables and initialize them to their zero values
368+
if err := c.writeNamedReturnDeclarations(decl.Type.Results); err != nil {
369+
return fmt.Errorf("failed to write named return declarations: %w", err)
370+
}
371+
367372
// write method body without outer braces
368373
for _, stmt := range decl.Body.List {
369374
if err := c.WriteStmt(stmt); err != nil {
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
5
2+
nil
3+
Hello
4+
6
5+
nil
6+
World!
7+
30
8+
high
9+
true
10+
10
11+
low
12+
true
13+
-2
14+
invalid
15+
false

compliance/tests/named_return_method/index.ts

Whitespace-only changes.
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package main
2+
3+
type content struct {
4+
bytes []byte
5+
}
6+
7+
func (c *content) ReadAt(b []byte, off int64) (n int, err error) {
8+
if off < 0 || off >= int64(len(c.bytes)) {
9+
err = nil // Simulate an error scenario
10+
return
11+
}
12+
13+
l := int64(len(b))
14+
if off+l > int64(len(c.bytes)) {
15+
l = int64(len(c.bytes)) - off
16+
}
17+
18+
btr := c.bytes[off : off+l]
19+
n = copy(b, btr)
20+
return
21+
}
22+
23+
func (c *content) ProcessData(input int) (result int, status string, valid bool) {
24+
result = input * 2
25+
if input > 10 {
26+
status = "high"
27+
valid = true
28+
} else if input > 0 {
29+
status = "low"
30+
valid = true
31+
} else {
32+
// status and valid will be zero values
33+
status = "invalid"
34+
}
35+
return
36+
}
37+
38+
func main() {
39+
c := &content{
40+
bytes: []byte("Hello, World!"),
41+
}
42+
43+
// Test ReadAt method
44+
buf := make([]byte, 5)
45+
n1, err1 := c.ReadAt(buf, 0)
46+
println(n1) // Expected: 5
47+
if err1 == nil {
48+
println("nil") // Expected: nil
49+
} else {
50+
println("error")
51+
}
52+
println(string(buf)) // Expected: Hello
53+
54+
// Test ReadAt with different offset
55+
buf2 := make([]byte, 6)
56+
n2, err2 := c.ReadAt(buf2, 7)
57+
println(n2) // Expected: 6
58+
if err2 == nil {
59+
println("nil") // Expected: nil
60+
} else {
61+
println("error")
62+
}
63+
println(string(buf2)) // Expected: World!
64+
65+
// Test ProcessData method
66+
r1, s1, v1 := c.ProcessData(15)
67+
println(r1) // Expected: 30
68+
println(s1) // Expected: high
69+
println(v1) // Expected: true
70+
71+
r2, s2, v2 := c.ProcessData(5)
72+
println(r2) // Expected: 10
73+
println(s2) // Expected: low
74+
println(v2) // Expected: true
75+
76+
r3, s3, v3 := c.ProcessData(-1)
77+
println(r3) // Expected: -2
78+
println(s3) // Expected: invalid
79+
println(v3) // Expected: false
80+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
// Generated file based on named_return_method.go
2+
// Updated when compliance tests are re-run, DO NOT EDIT!
3+
4+
import * as $ from "@goscript/builtin/index.js";
5+
6+
export class content {
7+
public get bytes(): $.Bytes {
8+
return this._fields.bytes.value
9+
}
10+
public set bytes(value: $.Bytes) {
11+
this._fields.bytes.value = value
12+
}
13+
14+
public _fields: {
15+
bytes: $.VarRef<$.Bytes>;
16+
}
17+
18+
constructor(init?: Partial<{bytes?: $.Bytes}>) {
19+
this._fields = {
20+
bytes: $.varRef(init?.bytes ?? new Uint8Array(0))
21+
}
22+
}
23+
24+
public clone(): content {
25+
const cloned = new content()
26+
cloned._fields = {
27+
bytes: $.varRef(this._fields.bytes.value)
28+
}
29+
return cloned
30+
}
31+
32+
public ReadAt(b: $.Bytes, off: number): [number, $.GoError] {
33+
const c = this
34+
let n: number = 0
35+
let err: $.GoError = null
36+
if (off < 0 || off >= ($.len(c.bytes) as number)) {
37+
err = null // Simulate an error scenario
38+
return [n, err]
39+
}
40+
let l = ($.len(b) as number)
41+
if (off + l > ($.len(c.bytes) as number)) {
42+
l = ($.len(c.bytes) as number) - off
43+
}
44+
let btr = $.goSlice(c.bytes, off, off + l)
45+
n = $.copy(b, btr)
46+
return [n, err]
47+
}
48+
49+
public ProcessData(input: number): [number, string, boolean] {
50+
const c = this
51+
let result: number = 0
52+
let status: string = ""
53+
let valid: boolean = false
54+
result = input * 2
55+
if (input > 10) {
56+
status = "high"
57+
valid = true
58+
} else if (input > 0) {
59+
status = "low"
60+
valid = true
61+
} else {
62+
// status and valid will be zero values
63+
status = "invalid"
64+
}
65+
return [result, status, valid]
66+
}
67+
68+
// Register this type with the runtime type system
69+
static __typeInfo = $.registerStructType(
70+
'content',
71+
new content(),
72+
[{ name: "ReadAt", args: [{ name: "b", type: { kind: $.TypeKind.Slice, elemType: { kind: $.TypeKind.Basic, name: "number" } } }, { name: "off", type: { kind: $.TypeKind.Basic, name: "number" } }], returns: [{ type: { kind: $.TypeKind.Basic, name: "number" } }, { type: { kind: $.TypeKind.Interface, name: 'GoError', methods: [{ name: 'Error', args: [], returns: [{ type: { kind: $.TypeKind.Basic, name: 'string' } }] }] } }] }, { name: "ProcessData", args: [{ name: "input", type: { kind: $.TypeKind.Basic, name: "number" } }], returns: [{ type: { kind: $.TypeKind.Basic, name: "number" } }, { type: { kind: $.TypeKind.Basic, name: "string" } }, { type: { kind: $.TypeKind.Basic, name: "boolean" } }] }],
73+
content,
74+
{"bytes": { kind: $.TypeKind.Slice, elemType: { kind: $.TypeKind.Basic, name: "number" } }}
75+
);
76+
}
77+
78+
export async function main(): Promise<void> {
79+
let c = new content({bytes: $.stringToBytes("Hello, World!")})
80+
81+
// Test ReadAt method
82+
let buf = new Uint8Array(5)
83+
let [n1, err1] = c.ReadAt(buf, 0)
84+
console.log(n1) // Expected: 5
85+
86+
// Expected: nil
87+
if (err1 == null) {
88+
console.log("nil") // Expected: nil
89+
} else {
90+
console.log("error")
91+
}
92+
console.log($.bytesToString(buf)) // Expected: Hello
93+
94+
// Test ReadAt with different offset
95+
let buf2 = new Uint8Array(6)
96+
let [n2, err2] = c.ReadAt(buf2, 7)
97+
console.log(n2) // Expected: 6
98+
99+
// Expected: nil
100+
if (err2 == null) {
101+
console.log("nil") // Expected: nil
102+
} else {
103+
console.log("error")
104+
}
105+
console.log($.bytesToString(buf2)) // Expected: World!
106+
107+
// Test ProcessData method
108+
let [r1, s1, v1] = c.ProcessData(15)
109+
console.log(r1) // Expected: 30
110+
console.log(s1) // Expected: high
111+
console.log(v1) // Expected: true
112+
113+
let [r2, s2, v2] = c.ProcessData(5)
114+
console.log(r2) // Expected: 10
115+
console.log(s2) // Expected: low
116+
console.log(v2) // Expected: true
117+
118+
let [r3, s3, v3] = c.ProcessData(-1)
119+
console.log(r3) // Expected: -2
120+
console.log(s3) // Expected: invalid
121+
console.log(v3) // Expected: false
122+
}
123+

design/TODO.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Known TODOs
2+
3+
## More Specific Type Registrations
4+
5+
We register types like this:
6+
7+
```typescript
8+
// Register this type with the runtime type system
9+
static __typeInfo = $.registerStructType(
10+
'content',
11+
new content(),
12+
[{ name: "ReadAt", args: [{ name: "b", type: { kind: $.TypeKind.Slice, elemType: { kind: $.TypeKind.Basic, name: "number" } } }, { name: "off", type: { kind: $.TypeKind.Basic, name: "number" } }], returns: [{ type: { kind: $.TypeKind.Basic, name: "number" } }, { type: { kind: $.TypeKind.Interface, name: 'GoError', methods: [{ name: 'Error', args: [], returns: [{ type: { kind: $.TypeKind.Basic, name: 'string' } }] }] } }] }, { name: "ProcessData", args: [{ name: "input", type: { kind: $.TypeKind.Basic, name: "number" } }], returns: [{ type: { kind: $.TypeKind.Basic, name: "number" } }, { type: { kind: $.TypeKind.Basic, name: "string" } }, { type: { kind: $.TypeKind.Basic, name: "boolean" } }] }],
13+
content,
14+
{"bytes": { kind: $.TypeKind.Slice, elemType: { kind: $.TypeKind.Basic, name: "number" } }}
15+
);
16+
```
17+
18+
But we need to register with the full package name for each type, otherwise there will be collisions.
19+
20+
This registration is done to avoid circular references using string identifiers as aliases. Unfortunately I have not seen any alternative way to do this.
21+

0 commit comments

Comments
 (0)