goscript 0.0.20 → 0.0.22

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.
@@ -0,0 +1,907 @@
1
+ package compiler
2
+
3
+ import (
4
+ "fmt"
5
+ "go/ast"
6
+ "go/token"
7
+ "strings"
8
+
9
+ "github.com/pkg/errors"
10
+ )
11
+
12
+ // WriteStmt is a central dispatcher function that translates a Go statement
13
+ // (`ast.Stmt`) into its TypeScript equivalent by calling the appropriate
14
+ // specialized `WriteStmt*` or `write*` method.
15
+ // It handles a wide variety of Go statements:
16
+ // - Block statements (`ast.BlockStmt`): `WriteStmtBlock`.
17
+ // - Assignment statements (`ast.AssignStmt`): `WriteStmtAssign`.
18
+ // - Return statements (`ast.ReturnStmt`): `WriteStmtReturn`.
19
+ // - Defer statements (`ast.DeferStmt`): `WriteStmtDefer`.
20
+ // - If statements (`ast.IfStmt`): `WriteStmtIf`.
21
+ // - Expression statements (`ast.ExprStmt`): `WriteStmtExpr`.
22
+ // - Declaration statements (`ast.DeclStmt`): `WriteStmtDecl`.
23
+ // - For statements (`ast.ForStmt`): `WriteStmtFor`.
24
+ // - Range statements (`ast.RangeStmt`): `WriteStmtRange`.
25
+ // - Switch statements (`ast.SwitchStmt`): `WriteStmtSwitch`.
26
+ // - Increment/decrement statements (`ast.IncDecStmt`): `WriteStmtIncDec`.
27
+ // - Send statements (`ast.SendStmt`): `WriteStmtSend`.
28
+ // - Go statements (`ast.GoStmt`): `WriteStmtGo`.
29
+ // - Select statements (`ast.SelectStmt`): `WriteStmtSelect`.
30
+ // - Branch statements (`ast.BranchStmt`): `WriteStmtBranch`.
31
+ //
32
+ // If an unknown statement type is encountered, it returns an error.
33
+ func (c *GoToTSCompiler) WriteStmt(a ast.Stmt) error {
34
+ switch exp := a.(type) {
35
+ case *ast.BlockStmt:
36
+ // WriteStmtBlock does not currently return an error, assuming it's safe for now.
37
+ if err := c.WriteStmtBlock(exp, false); err != nil {
38
+ return fmt.Errorf("failed to write block statement: %w", err)
39
+ }
40
+ case *ast.AssignStmt:
41
+ // special case: if the left side of the assign has () we need a ; to prepend the line
42
+ // ;(myStruct!.value).MyInt = 5
43
+ // otherwise typescript assumes it is a function call
44
+ if len(exp.Lhs) == 1 {
45
+ if lhsSel, ok := exp.Lhs[0].(*ast.SelectorExpr); ok {
46
+ if _, ok := lhsSel.X.(*ast.ParenExpr); ok {
47
+ c.tsw.WriteLiterally(";")
48
+ }
49
+ }
50
+ }
51
+
52
+ if err := c.WriteStmtAssign(exp); err != nil {
53
+ return fmt.Errorf("failed to write assignment statement: %w", err)
54
+ }
55
+ case *ast.ReturnStmt:
56
+ if err := c.WriteStmtReturn(exp); err != nil {
57
+ return fmt.Errorf("failed to write return statement: %w", err)
58
+ }
59
+ case *ast.DeferStmt:
60
+ if err := c.WriteStmtDefer(exp); err != nil {
61
+ return fmt.Errorf("failed to write defer statement: %w", err)
62
+ }
63
+ case *ast.IfStmt:
64
+ if err := c.WriteStmtIf(exp); err != nil {
65
+ return fmt.Errorf("failed to write if statement: %w", err)
66
+ }
67
+ case *ast.ExprStmt:
68
+ if err := c.WriteStmtExpr(exp); err != nil {
69
+ return fmt.Errorf("failed to write expression statement: %w", err)
70
+ }
71
+ case *ast.DeclStmt:
72
+ if err := c.WriteStmtDecl(exp); err != nil {
73
+ return fmt.Errorf("failed to write declaration statement: %w", err)
74
+ }
75
+ case *ast.ForStmt:
76
+ // WriteStmtFor does not currently return an error, assuming it's safe for now.
77
+ if err := c.WriteStmtFor(exp); err != nil {
78
+ return fmt.Errorf("failed to write for statement: %w", err)
79
+ }
80
+ case *ast.RangeStmt:
81
+ // Generate TS for for…range loops, log if something goes wrong
82
+ if err := c.WriteStmtRange(exp); err != nil {
83
+ return fmt.Errorf("failed to write range statement: %w", err)
84
+ }
85
+ case *ast.SwitchStmt:
86
+ // WriteStmtSwitch does not currently return an error, assuming it's safe for now.
87
+ if err := c.WriteStmtSwitch(exp); err != nil {
88
+ return fmt.Errorf("failed to write switch statement: %w", err)
89
+ }
90
+ case *ast.IncDecStmt:
91
+ if err := c.WriteStmtIncDec(exp); err != nil {
92
+ return fmt.Errorf("failed to write increment/decrement statement: %w", err)
93
+ }
94
+ case *ast.SendStmt:
95
+ if err := c.WriteStmtSend(exp); err != nil {
96
+ return fmt.Errorf("failed to write send statement: %w", err)
97
+ }
98
+ case *ast.GoStmt:
99
+ if err := c.WriteStmtGo(exp); err != nil {
100
+ return fmt.Errorf("failed to write go statement: %w", err)
101
+ }
102
+ case *ast.SelectStmt:
103
+ // Handle select statement
104
+ if err := c.WriteStmtSelect(exp); err != nil {
105
+ return fmt.Errorf("failed to write select statement: %w", err)
106
+ }
107
+ case *ast.BranchStmt:
108
+ if err := c.WriteStmtBranch(exp); err != nil {
109
+ return fmt.Errorf("failed to write branch statement: %w", err)
110
+ }
111
+ default:
112
+ return errors.Errorf("unknown statement: %s\n", a)
113
+ }
114
+ return nil
115
+ }
116
+
117
+ // WriteStmtDecl handles declaration statements (`ast.DeclStmt`),
118
+ // such as short variable declarations or type declarations within a statement list.
119
+ // It processes `ValueSpec`s and `TypeSpec`s within the declaration.
120
+ func (c *GoToTSCompiler) WriteStmtDecl(stmt *ast.DeclStmt) error {
121
+ // This typically contains a GenDecl
122
+ if genDecl, ok := stmt.Decl.(*ast.GenDecl); ok {
123
+ for _, spec := range genDecl.Specs {
124
+ // Value specs within a declaration statement
125
+ if valueSpec, ok := spec.(*ast.ValueSpec); ok {
126
+ if err := c.WriteValueSpec(valueSpec); err != nil {
127
+ return fmt.Errorf("failed to write value spec in declaration statement: %w", err)
128
+ }
129
+ } else if typeSpec, ok := spec.(*ast.TypeSpec); ok {
130
+ if err := c.WriteTypeSpec(typeSpec); err != nil {
131
+ return fmt.Errorf("failed to write type spec in declaration statement: %w", err)
132
+ }
133
+ } else {
134
+ c.tsw.WriteCommentLinef("unhandled spec in DeclStmt: %T", spec)
135
+ }
136
+ }
137
+ } else {
138
+ return errors.Errorf("unhandled declaration type in DeclStmt: %T", stmt.Decl)
139
+ }
140
+ return nil
141
+ }
142
+
143
+ // WriteStmtIncDec handles increment and decrement statements (`ast.IncDecStmt`).
144
+ // It writes the expression followed by `++` or `--`.
145
+ func (c *GoToTSCompiler) WriteStmtIncDec(stmt *ast.IncDecStmt) error {
146
+ if err := c.WriteValueExpr(stmt.X); err != nil { // The expression (e.g., i)
147
+ return fmt.Errorf("failed to write increment/decrement expression: %w", err)
148
+ }
149
+ tokStr, ok := TokenToTs(stmt.Tok)
150
+ if !ok {
151
+ return errors.Errorf("unknown incdec token: %s", stmt.Tok.String())
152
+ }
153
+ c.tsw.WriteLiterally(tokStr) // The token (e.g., ++)
154
+ c.tsw.WriteLine("")
155
+ return nil
156
+ }
157
+
158
+ // WriteStmtBranch handles branch statements (`ast.BranchStmt`), such as `break` and `continue`.
159
+ func (c *GoToTSCompiler) WriteStmtBranch(stmt *ast.BranchStmt) error {
160
+ switch stmt.Tok {
161
+ case token.BREAK:
162
+ c.tsw.WriteLine("break") // No semicolon needed
163
+ case token.CONTINUE:
164
+ c.tsw.WriteLine("continue") // No semicolon needed
165
+ default:
166
+ // This case should ideally not be reached if the Go parser is correct,
167
+ // as ast.BranchStmt only covers break, continue, goto, fallthrough.
168
+ // 'goto' and 'fallthrough' are handled elsewhere or not supported.
169
+ c.tsw.WriteCommentLinef("unhandled branch statement token: %s", stmt.Tok.String())
170
+ }
171
+ return nil
172
+ }
173
+
174
+ // WriteStmtGo translates a Go statement (`ast.GoStmt`) into its TypeScript equivalent.
175
+ // It handles `go func(){...}()` and `go namedFunc(args)`.
176
+ func (c *GoToTSCompiler) WriteStmtGo(exp *ast.GoStmt) error {
177
+ // Handle goroutine statement
178
+ // Translate 'go func() { ... }()' to 'queueMicrotask(() => { ... compiled body ... })'
179
+
180
+ // The call expression's function is the function literal
181
+ callExpr := exp.Call
182
+ if funcLit, ok := callExpr.Fun.(*ast.FuncLit); ok {
183
+ // For function literals, we need to check if the function literal itself is async
184
+ // This happens during analysis in analysisVisitor.Visit for FuncLit nodes
185
+ isAsync := c.analysis.IsFuncLitAsync(funcLit)
186
+ if isAsync {
187
+ c.tsw.WriteLiterally("queueMicrotask(async () => ")
188
+ } else {
189
+ c.tsw.WriteLiterally("queueMicrotask(() => ")
190
+ }
191
+
192
+ // Compile the function literal's body directly
193
+ if err := c.WriteStmtBlock(funcLit.Body, true); err != nil {
194
+ return fmt.Errorf("failed to write goroutine function literal body: %w", err)
195
+ }
196
+
197
+ c.tsw.WriteLine(")") // Close the queueMicrotask statement
198
+
199
+ } else if ident, ok := callExpr.Fun.(*ast.Ident); ok {
200
+ // Handle named functions: go namedFunc(args)
201
+ // Get the object for this function
202
+ obj := c.pkg.TypesInfo.Uses[ident]
203
+ if obj == nil {
204
+ return errors.Errorf("could not find object for function: %s", ident.Name)
205
+ }
206
+
207
+ // Check if the function is async
208
+ isAsync := c.analysis.IsAsyncFunc(obj)
209
+ if isAsync {
210
+ c.tsw.WriteLiterally("queueMicrotask(async () => {")
211
+ } else {
212
+ c.tsw.WriteLiterally("queueMicrotask(() => {")
213
+ }
214
+
215
+ c.tsw.Indent(1)
216
+ c.tsw.WriteLine("")
217
+
218
+ // Write the function call, using await if the function is async
219
+ if isAsync {
220
+ c.tsw.WriteLiterally("await ")
221
+ }
222
+
223
+ // Write the function name
224
+ c.tsw.WriteLiterally(ident.Name)
225
+
226
+ // Write the function arguments
227
+ c.tsw.WriteLiterally("(")
228
+ for i, arg := range callExpr.Args {
229
+ if i != 0 {
230
+ c.tsw.WriteLiterally(", ")
231
+ }
232
+ if err := c.WriteValueExpr(arg); err != nil {
233
+ return fmt.Errorf("failed to write argument %d in goroutine function call: %w", i, err)
234
+ }
235
+ }
236
+ c.tsw.WriteLiterally(")")
237
+ c.tsw.WriteLine("")
238
+
239
+ c.tsw.Indent(-1)
240
+ c.tsw.WriteLine("})") // Close the queueMicrotask callback and the statement
241
+ } else {
242
+ return errors.Errorf("unhandled goroutine function type: %T", callExpr.Fun)
243
+ }
244
+ return nil
245
+ }
246
+
247
+ // WriteStmtExpr translates a Go expression statement (`ast.ExprStmt`) into
248
+ // its TypeScript equivalent. An expression statement in Go is an expression
249
+ // evaluated for its side effects (e.g., a function call).
250
+ // - A special case is a simple channel receive used as a statement (`<-ch`). This
251
+ // is translated to `await ch_ts.receive();` (the value is discarded).
252
+ // - For other expression statements, the underlying expression `exp.X` is translated
253
+ // using `WriteValueExpr`.
254
+ // - It attempts to preserve inline comments associated with the expression statement
255
+ // or its underlying expression `exp.X`.
256
+ //
257
+ // The translated statement is terminated with a newline.
258
+ func (c *GoToTSCompiler) WriteStmtExpr(exp *ast.ExprStmt) error {
259
+ // Handle simple channel receive used as a statement (<-ch)
260
+ if unaryExpr, ok := exp.X.(*ast.UnaryExpr); ok && unaryExpr.Op == token.ARROW {
261
+ // Translate <-ch to await ch.receive()
262
+ c.tsw.WriteLiterally("await ")
263
+ if err := c.WriteValueExpr(unaryExpr.X); err != nil { // Channel expression
264
+ return fmt.Errorf("failed to write channel expression in receive statement: %w", err)
265
+ }
266
+ c.tsw.WriteLiterally(".receive()") // Use receive() as the value is discarded
267
+ c.tsw.WriteLine("")
268
+ return nil
269
+ }
270
+
271
+ // Handle other expression statements
272
+ if err := c.WriteValueExpr(exp.X); err != nil { // Expression statement evaluates a value
273
+ return err
274
+ }
275
+
276
+ // Handle potential inline comment for ExprStmt
277
+ inlineCommentWritten := false
278
+ if c.pkg != nil && c.pkg.Fset != nil && exp.End().IsValid() {
279
+ if file := c.pkg.Fset.File(exp.End()); file != nil {
280
+ endLine := file.Line(exp.End())
281
+ // Check comments associated *directly* with the ExprStmt node
282
+ for _, cg := range c.analysis.Cmap[exp] {
283
+ if cg.Pos().IsValid() && file.Line(cg.Pos()) == endLine && cg.Pos() > exp.End() {
284
+ commentText := strings.TrimSpace(strings.TrimPrefix(cg.Text(), "//"))
285
+ c.tsw.WriteLiterally(" // " + commentText)
286
+ inlineCommentWritten = true
287
+ break
288
+ }
289
+ }
290
+ // Also check comments associated with the underlying expression X
291
+ // This might be necessary if the comment map links it to X instead of ExprStmt
292
+ if !inlineCommentWritten {
293
+ for _, cg := range c.analysis.Cmap[exp.X] {
294
+ if cg.Pos().IsValid() && file.Line(cg.Pos()) == endLine && cg.Pos() > exp.End() {
295
+ commentText := strings.TrimSpace(strings.TrimPrefix(cg.Text(), "//"))
296
+ c.tsw.WriteLiterally(" // " + commentText)
297
+ inlineCommentWritten = true //nolint:ineffassign
298
+ break
299
+ }
300
+ }
301
+ }
302
+ }
303
+ }
304
+
305
+ // Add semicolon according to design doc (omit semicolons) - REMOVED semicolon
306
+ c.tsw.WriteLine("") // Finish with a newline
307
+ return nil
308
+ }
309
+
310
+ // WriteStmtSend translates a Go channel send statement (`ast.SendStmt`),
311
+ // which has the form `ch <- value`, into its asynchronous TypeScript equivalent.
312
+ // The translation is `await ch_ts.send(value_ts)`.
313
+ // Both the channel expression (`exp.Chan`) and the value expression (`exp.Value`)
314
+ // are translated using `WriteValueExpr`. The `await` keyword is used because
315
+ // channel send operations are asynchronous in the TypeScript model.
316
+ // The statement is terminated with a newline.
317
+ func (c *GoToTSCompiler) WriteStmtSend(exp *ast.SendStmt) error {
318
+ // Translate ch <- value to await ch.send(value)
319
+ c.tsw.WriteLiterally("await ")
320
+ if err := c.WriteValueExpr(exp.Chan); err != nil { // The channel expression
321
+ return fmt.Errorf("failed to write channel expression in send statement: %w", err)
322
+ }
323
+ c.tsw.WriteLiterally(".send(")
324
+ if err := c.WriteValueExpr(exp.Value); err != nil { // The value expression
325
+ return fmt.Errorf("failed to write value expression in send statement: %w", err)
326
+ }
327
+ c.tsw.WriteLiterally(")")
328
+ c.tsw.WriteLine("") // Add newline after the statement
329
+ return nil
330
+ }
331
+
332
+ // WriteStmtIf translates a Go `if` statement (`ast.IfStmt`) into its
333
+ // TypeScript equivalent.
334
+ // - If the Go `if` has an initialization statement (`exp.Init`), it's wrapped
335
+ // in a TypeScript block `{...}` before the `if` keyword, and the initializer
336
+ // is translated within this block. This emulates Go's `if` statement scope.
337
+ // - The condition (`exp.Cond`) is translated using `WriteValueExpr` and placed
338
+ // within parentheses `(...)`.
339
+ // - The `if` body (`exp.Body`) is translated as a block statement using
340
+ // `WriteStmtBlock`. If `exp.Body` is nil, an empty block `{}` is written.
341
+ // - The `else` branch (`exp.Else`) is handled:
342
+ // - If `exp.Else` is a block statement (`ast.BlockStmt`), it's written as `else { ...body_ts... }`.
343
+ // - If `exp.Else` is another `if` statement (`ast.IfStmt`), it's written as `else if (...) ...`,
344
+ // recursively calling `WriteStmtIf`.
345
+ //
346
+ // The function aims to produce idiomatic TypeScript `if/else if/else` structures.
347
+ func (s *GoToTSCompiler) WriteStmtIf(exp *ast.IfStmt) error {
348
+ if exp.Init != nil {
349
+ s.tsw.WriteLiterally("{")
350
+ s.tsw.Indent(1)
351
+
352
+ if err := s.WriteStmt(exp.Init); err != nil {
353
+ return err
354
+ }
355
+
356
+ defer func() {
357
+ s.tsw.Indent(-1)
358
+ s.tsw.WriteLiterally("}")
359
+ }()
360
+ }
361
+
362
+ s.tsw.WriteLiterally("if (")
363
+ if err := s.WriteValueExpr(exp.Cond); err != nil { // Condition is a value
364
+ return err
365
+ }
366
+ s.tsw.WriteLiterally(") ")
367
+
368
+ if exp.Body != nil {
369
+ if err := s.WriteStmtBlock(exp.Body, exp.Else != nil); err != nil {
370
+ return fmt.Errorf("failed to write if body block statement: %w", err)
371
+ }
372
+ } else {
373
+ // Handle nil body case using WriteStmtBlock with an empty block
374
+ if err := s.WriteStmtBlock(&ast.BlockStmt{}, exp.Else != nil); err != nil {
375
+ return fmt.Errorf("failed to write empty block statement in if statement: %w", err)
376
+ }
377
+ }
378
+
379
+ // handle else branch
380
+ if exp.Else != nil {
381
+ s.tsw.WriteLiterally(" else ")
382
+ switch elseStmt := exp.Else.(type) {
383
+ case *ast.BlockStmt:
384
+ // Always pass false for suppressNewline here
385
+ if err := s.WriteStmtBlock(elseStmt, false); err != nil {
386
+ return fmt.Errorf("failed to write else block statement in if statement: %w", err)
387
+ }
388
+ case *ast.IfStmt:
389
+ // Recursive call handles its own block formatting
390
+ if err := s.WriteStmtIf(elseStmt); err != nil {
391
+ return fmt.Errorf("failed to write else if statement in if statement: %w", err)
392
+ }
393
+ }
394
+ }
395
+ return nil
396
+ }
397
+
398
+ // WriteStmtReturn translates a Go `return` statement (`ast.ReturnStmt`) into
399
+ // its TypeScript equivalent.
400
+ // - It writes the `return` keyword.
401
+ // - If there are multiple return values (`len(exp.Results) > 1`), the translated
402
+ // results are wrapped in a TypeScript array literal `[...]`.
403
+ // - Each result expression in `exp.Results` is translated using `WriteValueExpr`.
404
+ // - If there are no results, it simply writes `return`.
405
+ //
406
+ // The statement is terminated with a newline.
407
+ func (c *GoToTSCompiler) WriteStmtReturn(exp *ast.ReturnStmt) error {
408
+ c.tsw.WriteLiterally("return ")
409
+ if len(exp.Results) > 1 {
410
+ c.tsw.WriteLiterally("[")
411
+ }
412
+ for i, res := range exp.Results {
413
+ if i != 0 {
414
+ c.tsw.WriteLiterally(", ")
415
+ }
416
+ if err := c.WriteValueExpr(res); err != nil { // Return results are values
417
+ return err
418
+ }
419
+ }
420
+ if len(exp.Results) > 1 {
421
+ c.tsw.WriteLiterally("]")
422
+ }
423
+ c.tsw.WriteLine("") // Remove semicolon
424
+ return nil
425
+ }
426
+
427
+ // WriteStmtBlock translates a Go block statement (`ast.BlockStmt`), typically
428
+ // `{ ...stmts... }`, into its TypeScript equivalent, carefully preserving
429
+ // comments and blank lines to maintain code readability and structure.
430
+ // - It writes an opening brace `{` and indents.
431
+ // - If the analysis (`c.analysis.NeedsDefer`) indicates that the block (or a
432
+ // function it's part of) contains `defer` statements, it injects a
433
+ // `using __defer = new $.DisposableStack();` (or `AsyncDisposableStack` if
434
+ // the context is async or contains async defers) at the beginning of the block.
435
+ // This `__defer` stack is used by `WriteStmtDefer` to register cleanup actions.
436
+ // - It iterates through the statements (`exp.List`) in the block:
437
+ // - Leading comments associated with each statement are written first, with
438
+ // blank lines preserved based on original source line numbers.
439
+ // - The statement itself is then translated using `WriteStmt`.
440
+ // - Inline comments (comments on the same line after a statement) are expected
441
+ // to be handled by the individual statement writers (e.g., `WriteStmtExpr`).
442
+ // - Trailing comments within the block (after the last statement but before the
443
+ // closing brace) are written.
444
+ // - Blank lines before the closing brace are preserved.
445
+ // - Finally, it unindents and writes the closing brace `}`.
446
+ //
447
+ // If `suppressNewline` is true, the final newline after the closing brace is omitted
448
+ // (used, for example, when an `if` block is followed by an `else`).
449
+ func (c *GoToTSCompiler) WriteStmtBlock(exp *ast.BlockStmt, suppressNewline bool) error {
450
+ if exp == nil {
451
+ c.tsw.WriteLiterally("{}")
452
+ if !suppressNewline {
453
+ c.tsw.WriteLine("")
454
+ }
455
+ return nil
456
+ }
457
+
458
+ // Opening brace
459
+ c.tsw.WriteLine("{")
460
+ c.tsw.Indent(1)
461
+
462
+ // Determine if there is any defer to an async function literal in this block
463
+ hasAsyncDefer := false
464
+ for _, stmt := range exp.List {
465
+ if deferStmt, ok := stmt.(*ast.DeferStmt); ok {
466
+ if funcLit, ok := deferStmt.Call.Fun.(*ast.FuncLit); ok {
467
+ if c.analysis.IsInAsyncFunctionMap[funcLit] {
468
+ hasAsyncDefer = true
469
+ break
470
+ }
471
+ }
472
+ }
473
+ }
474
+
475
+ // Add using statement if needed, considering async function or async defer
476
+ if c.analysis.NeedsDefer(exp) {
477
+ if c.analysis.IsInAsyncFunction(exp) || hasAsyncDefer {
478
+ c.tsw.WriteLine("await using __defer = new $.AsyncDisposableStack();")
479
+ } else {
480
+ c.tsw.WriteLine("using __defer = new $.DisposableStack();")
481
+ }
482
+ }
483
+
484
+ // Prepare line info
485
+ var file *token.File
486
+ if c.pkg != nil && c.pkg.Fset != nil && exp.Lbrace.IsValid() {
487
+ file = c.pkg.Fset.File(exp.Lbrace)
488
+ }
489
+
490
+ // writeBlank emits a single blank line if gap > 1
491
+ writeBlank := func(prev, curr int) {
492
+ if file != nil && prev > 0 && curr > prev+1 {
493
+ c.tsw.WriteLine("")
494
+ }
495
+ }
496
+
497
+ // Track last printed line, start at opening brace
498
+ lastLine := 0
499
+ if file != nil {
500
+ lastLine = file.Line(exp.Lbrace)
501
+ }
502
+
503
+ // 1. For each statement: write its leading comments, blank space, then the stmt
504
+ for _, stmt := range exp.List {
505
+ // Get statement's end line and position for inline comment check
506
+ stmtEndLine := 0
507
+ stmtEndPos := token.NoPos
508
+ if file != nil && stmt.End().IsValid() {
509
+ stmtEndLine = file.Line(stmt.End())
510
+ stmtEndPos = stmt.End()
511
+ }
512
+
513
+ // Process leading comments for stmt
514
+ comments := c.analysis.Cmap.Filter(stmt).Comments()
515
+ for _, cg := range comments {
516
+ // Check if this comment group is an inline comment for the current statement
517
+ isInlineComment := false
518
+ if file != nil && cg.Pos().IsValid() && stmtEndPos.IsValid() {
519
+ commentStartLine := file.Line(cg.Pos())
520
+ // Inline if starts on same line as stmt end AND starts after stmt end position
521
+ if commentStartLine == stmtEndLine && cg.Pos() > stmtEndPos {
522
+ isInlineComment = true
523
+ }
524
+ }
525
+
526
+ // If it's NOT an inline comment for this statement, write it here
527
+ if !isInlineComment {
528
+ start := 0
529
+ if file != nil && cg.Pos().IsValid() {
530
+ start = file.Line(cg.Pos())
531
+ }
532
+ writeBlank(lastLine, start)
533
+ // WriteDoc does not currently return an error, assuming it's safe for now.
534
+ c.WriteDoc(cg) // WriteDoc will handle the actual comment text
535
+ if file != nil && cg.End().IsValid() {
536
+ lastLine = file.Line(cg.End())
537
+ }
538
+ }
539
+ // If it IS an inline comment, skip it. The statement writer will handle it.
540
+ }
541
+
542
+ // the statement itself
543
+ stmtStart := 0
544
+ if file != nil && stmt.Pos().IsValid() {
545
+ stmtStart = file.Line(stmt.Pos())
546
+ }
547
+ writeBlank(lastLine, stmtStart)
548
+ // Call the specific statement writer (e.g., WriteStmtAssign).
549
+ // It is responsible for handling its own inline comment.
550
+ if err := c.WriteStmt(stmt); err != nil {
551
+ return fmt.Errorf("failed to write statement in block: %w", err)
552
+ }
553
+
554
+ if file != nil && stmt.End().IsValid() {
555
+ // Update lastLine based on the statement's end, *including* potential inline comment handled by WriteStmt*
556
+ lastLine = file.Line(stmt.End())
557
+ }
558
+ }
559
+
560
+ // 2. Trailing comments on the block (after last stmt, before closing brace)
561
+ trailing := c.analysis.Cmap.Filter(exp).Comments()
562
+ for _, cg := range trailing {
563
+ start := 0
564
+ if file != nil && cg.Pos().IsValid() {
565
+ start = file.Line(cg.Pos())
566
+ }
567
+ // only emit if it follows the last content
568
+ if start > lastLine {
569
+ writeBlank(lastLine, start)
570
+ // WriteDoc does not currently return an error, assuming it's safe for now.
571
+ c.WriteDoc(cg)
572
+ if file != nil && cg.End().IsValid() {
573
+ lastLine = file.Line(cg.End())
574
+ }
575
+ }
576
+ }
577
+
578
+ // 3. Blank lines before closing brace
579
+ closing := 0
580
+ if file != nil && exp.Rbrace.IsValid() {
581
+ closing = file.Line(exp.Rbrace)
582
+ }
583
+ writeBlank(lastLine, closing)
584
+
585
+ // Closing brace
586
+ c.tsw.Indent(-1)
587
+ c.tsw.WriteLiterally("}")
588
+
589
+ if !suppressNewline {
590
+ c.tsw.WriteLine("")
591
+ }
592
+ return nil
593
+ }
594
+
595
+ // WriteStmtSwitch translates a Go `switch` statement into its TypeScript equivalent.
596
+ // - If the Go switch has an initialization statement (`exp.Init`), it's wrapped
597
+ // in a TypeScript block `{...}` before the `switch` keyword, and the
598
+ // initializer is translated within this block. This emulates Go's switch scope.
599
+ // - The switch condition (`exp.Tag`):
600
+ // - If `exp.Tag` is present, it's translated using `WriteValueExpr`.
601
+ // - If `exp.Tag` is nil (a "tagless" switch, like `switch { case cond1: ... }`),
602
+ // it's translated as `switch (true)` in TypeScript.
603
+ // - Each case clause (`ast.CaseClause`) in `exp.Body.List` is translated using
604
+ // `WriteCaseClause`.
605
+ //
606
+ // The overall structure is `[optional_init_block] switch (condition_ts) { ...cases_ts... }`.
607
+ func (c *GoToTSCompiler) WriteStmtSwitch(exp *ast.SwitchStmt) error {
608
+ // Handle optional initialization statement
609
+ if exp.Init != nil {
610
+ c.tsw.WriteLiterally("{")
611
+ c.tsw.Indent(1)
612
+ if err := c.WriteStmt(exp.Init); err != nil {
613
+ return fmt.Errorf("failed to write switch initialization statement: %w", err)
614
+ }
615
+ defer func() {
616
+ c.tsw.Indent(-1)
617
+ c.tsw.WriteLiterally("}")
618
+ }()
619
+ }
620
+
621
+ c.tsw.WriteLiterally("switch (")
622
+ // Handle the switch tag (the expression being switched on)
623
+ if exp.Tag != nil {
624
+ if err := c.WriteValueExpr(exp.Tag); err != nil {
625
+ return fmt.Errorf("failed to write switch tag expression: %w", err)
626
+ }
627
+ } else {
628
+ c.tsw.WriteLiterally("true") // Write 'true' for switch without expression
629
+ }
630
+ c.tsw.WriteLiterally(") {")
631
+ c.tsw.WriteLine("")
632
+ c.tsw.Indent(1)
633
+
634
+ // Handle case clauses
635
+ for _, stmt := range exp.Body.List {
636
+ if caseClause, ok := stmt.(*ast.CaseClause); ok {
637
+ // WriteCaseClause does not currently return an error, assuming it's safe for now.
638
+ if err := c.WriteCaseClause(caseClause); err != nil {
639
+ return fmt.Errorf("failed to write case clause in switch statement: %w", err)
640
+ }
641
+ } else {
642
+ c.tsw.WriteCommentLinef("unhandled statement in switch body: %T", stmt)
643
+ }
644
+ }
645
+
646
+ c.tsw.Indent(-1)
647
+ c.tsw.WriteLine("}")
648
+ return nil
649
+ }
650
+
651
+ // WriteStmtDefer translates a Go `defer` statement into TypeScript code that
652
+ // utilizes a disposable stack (`$.DisposableStack` or `$.AsyncDisposableStack`).
653
+ // The Go `defer` semantics (LIFO execution at function exit) are emulated by
654
+ // registering a cleanup function with this stack.
655
+ // - `defer funcCall()` becomes `__defer.defer(() => { funcCall_ts(); });`.
656
+ // - `defer func(){ ...body... }()` (an immediately-invoked function literal, IIFL)
657
+ // has its body inlined: `__defer.defer(() => { ...body_ts... });`.
658
+ // - If the deferred call is to an async function or an async function literal
659
+ // (determined by `c.analysis.IsInAsyncFunctionMap`), the registered callback
660
+ // is marked `async`: `__defer.defer(async () => { ... });`.
661
+ //
662
+ // The `__defer` variable is assumed to be declared at the beginning of the
663
+ // function scope (see `WriteStmtBlock` or `WriteFuncDeclAsMethod`) using
664
+ // `await using __defer = new $.AsyncDisposableStack();` for async functions/contexts
665
+ // or `using __defer = new $.DisposableStack();` for sync contexts.
666
+ func (c *GoToTSCompiler) WriteStmtDefer(exp *ast.DeferStmt) error {
667
+ // Determine if the deferred call is to an async function literal using analysis
668
+ isAsyncDeferred := false
669
+ if funcLit, ok := exp.Call.Fun.(*ast.FuncLit); ok {
670
+ isAsyncDeferred = c.analysis.IsInAsyncFunctionMap[funcLit]
671
+ }
672
+
673
+ // Set async prefix based on pre-computed async status
674
+ asyncPrefix := ""
675
+ if isAsyncDeferred {
676
+ asyncPrefix = "async "
677
+ }
678
+
679
+ // Set stack variable based on whether we are in an async function
680
+ stackVar := "__defer"
681
+ c.tsw.WriteLiterallyf("%s.defer(%s() => {", stackVar, asyncPrefix)
682
+ c.tsw.Indent(1)
683
+ c.tsw.WriteLine("")
684
+
685
+ // Write the deferred call or inline the body when it's an immediately-invoked
686
+ // function literal (defer func(){ ... }()).
687
+ if funcLit, ok := exp.Call.Fun.(*ast.FuncLit); ok && len(exp.Call.Args) == 0 {
688
+ // Inline the function literal's body to avoid nested arrow invocation.
689
+ for _, stmt := range funcLit.Body.List {
690
+ if err := c.WriteStmt(stmt); err != nil {
691
+ return fmt.Errorf("failed to write statement in deferred function body: %w", err)
692
+ }
693
+ }
694
+ } else {
695
+ // Write the call expression as-is.
696
+ if err := c.WriteValueExpr(exp.Call); err != nil {
697
+ return fmt.Errorf("failed to write deferred call: %w", err)
698
+ }
699
+ }
700
+
701
+ c.tsw.Indent(-1)
702
+ c.tsw.WriteLine("});")
703
+
704
+ return nil
705
+ }
706
+
707
+ // WriteStmtSelect translates a Go `select` statement into an asynchronous
708
+ // TypeScript operation using the `$.selectStatement` runtime helper.
709
+ // Go's `select` provides non-deterministic choice over channel operations.
710
+ // This is emulated by constructing an array of `SelectCase` objects, one for
711
+ // each `case` in the Go `select`, and passing it to `$.selectStatement`.
712
+ //
713
+ // Each `SelectCase` object includes:
714
+ // - `id`: A unique identifier for the case.
715
+ // - `isSend`: `true` for send operations (`case ch <- val:`), `false` for receives.
716
+ // - `channel`: The TypeScript channel object.
717
+ // - `value` (for sends): The value being sent.
718
+ // - `onSelected: async (result) => { ... }`: A callback executed when this case
719
+ // is chosen. `result` contains `{ value, ok }` for receives.
720
+ // - Inside `onSelected`, assignments for receive operations (e.g., `v := <-ch`,
721
+ // `v, ok := <-ch`) are handled by declaring/assigning variables from `result.value`
722
+ // and `result.ok`.
723
+ // - The original Go case body is then translated within this callback.
724
+ //
725
+ // A `default` case in Go `select` is translated to a `SelectCase` with `id: -1`
726
+ // and its body in the `onSelected` handler. The `$.selectStatement` helper
727
+ // is informed if a default case exists.
728
+ // The entire `$.selectStatement(...)` call is `await`ed because channel
729
+ // operations are asynchronous in the TypeScript model.
730
+ func (c *GoToTSCompiler) WriteStmtSelect(exp *ast.SelectStmt) error {
731
+ // This is our implementation of the select statement, which will use Promise.race
732
+ // to achieve the same semantics as Go's select statement.
733
+
734
+ // Variable to track whether we have a default case
735
+ hasDefault := false
736
+
737
+ // Start the selectStatement call and the array literal
738
+ c.tsw.WriteLiterally("await $.selectStatement(")
739
+ c.tsw.WriteLine("[") // Put bracket on new line
740
+ c.tsw.Indent(1)
741
+
742
+ // For each case clause, generate a SelectCase object directly into the array literal
743
+ for i, stmt := range exp.Body.List {
744
+ if commClause, ok := stmt.(*ast.CommClause); ok {
745
+ if commClause.Comm == nil {
746
+ // This is a default case
747
+ hasDefault = true
748
+ // Add a SelectCase object for the default case with a special ID
749
+ c.tsw.WriteLiterally("{") // Start object literal
750
+ c.tsw.Indent(1)
751
+ c.tsw.WriteLine("")
752
+ c.tsw.WriteLiterally("id: -1,") // Special ID for default case
753
+ c.tsw.WriteLine("")
754
+ c.tsw.WriteLiterally("isSend: false,") // Default case is neither send nor receive, but needs a value
755
+ c.tsw.WriteLine("")
756
+ c.tsw.WriteLiterally("channel: null,") // No channel for default case
757
+ c.tsw.WriteLine("")
758
+ c.tsw.WriteLiterally("onSelected: async (result) => {") // Mark as async because case body might contain await
759
+ c.tsw.Indent(1)
760
+ c.tsw.WriteLine("")
761
+ // Write the case body
762
+ for _, bodyStmt := range commClause.Body {
763
+ if err := c.WriteStmt(bodyStmt); err != nil {
764
+ return fmt.Errorf("failed to write statement in select default case body (onSelected): %w", err)
765
+ }
766
+ }
767
+ c.tsw.Indent(-1)
768
+ c.tsw.WriteLine("}") // Close onSelected handler
769
+ c.tsw.Indent(-1)
770
+ c.tsw.WriteLiterally("},") // Close SelectCase object and add comma
771
+ c.tsw.WriteLine("")
772
+
773
+ continue
774
+ }
775
+
776
+ // Generate a unique ID for this case
777
+ caseID := i
778
+
779
+ // Start writing the SelectCase object
780
+ c.tsw.WriteLiterally("{") // Start object literal
781
+ c.tsw.Indent(1)
782
+ c.tsw.WriteLine("")
783
+ c.tsw.WriteLiterallyf("id: %d,", caseID)
784
+ c.tsw.WriteLine("")
785
+
786
+ // Handle different types of comm statements
787
+ switch comm := commClause.Comm.(type) {
788
+ case *ast.AssignStmt:
789
+ // This is a receive operation with assignment: case v := <-ch: or case v, ok := <-ch:
790
+ if len(comm.Rhs) == 1 {
791
+ if unaryExpr, ok := comm.Rhs[0].(*ast.UnaryExpr); ok && unaryExpr.Op == token.ARROW {
792
+ // It's a receive operation
793
+ c.tsw.WriteLiterally("isSend: false,")
794
+ c.tsw.WriteLine("")
795
+ c.tsw.WriteLiterally("channel: ")
796
+ if err := c.WriteValueExpr(unaryExpr.X); err != nil { // The channel expression
797
+ return fmt.Errorf("failed to write channel expression in select receive case: %w", err)
798
+ }
799
+ c.tsw.WriteLiterally(",")
800
+ c.tsw.WriteLine("")
801
+ } else {
802
+ c.tsw.WriteCommentLinef("unhandled RHS in select assignment case: %T", comm.Rhs[0])
803
+ }
804
+ } else {
805
+ c.tsw.WriteCommentLinef("unhandled RHS count in select assignment case: %d", len(comm.Rhs))
806
+ }
807
+ case *ast.ExprStmt:
808
+ // This is a simple receive: case <-ch:
809
+ if unaryExpr, ok := comm.X.(*ast.UnaryExpr); ok && unaryExpr.Op == token.ARROW {
810
+ c.tsw.WriteLiterally("isSend: false,")
811
+ c.tsw.WriteLine("")
812
+ c.tsw.WriteLiterally("channel: ")
813
+ if err := c.WriteValueExpr(unaryExpr.X); err != nil { // The channel expression
814
+ return fmt.Errorf("failed to write channel expression in select receive case: %w", err)
815
+ }
816
+ c.tsw.WriteLiterally(",")
817
+ c.tsw.WriteLine("")
818
+ } else {
819
+ c.tsw.WriteCommentLinef("unhandled expression in select case: %T", comm.X)
820
+ }
821
+ case *ast.SendStmt:
822
+ // This is a send operation: case ch <- v:
823
+ c.tsw.WriteLiterally("isSend: true,")
824
+ c.tsw.WriteLine("")
825
+ c.tsw.WriteLiterally("channel: ")
826
+ if err := c.WriteValueExpr(comm.Chan); err != nil { // The channel expression
827
+ return fmt.Errorf("failed to write channel expression in select send case: %w", err)
828
+ }
829
+ c.tsw.WriteLiterally(",")
830
+ c.tsw.WriteLine("")
831
+ c.tsw.WriteLiterally("value: ")
832
+ if err := c.WriteValueExpr(comm.Value); err != nil { // The value expression
833
+ return fmt.Errorf("failed to write value expression in select send case: %w", err)
834
+ }
835
+ c.tsw.WriteLiterally(",")
836
+ c.tsw.WriteLine("")
837
+ default:
838
+ c.tsw.WriteCommentLinef("unhandled comm statement in select case: %T", comm)
839
+ }
840
+
841
+ // Add the onSelected handler to execute the case body after the select resolves
842
+ c.tsw.WriteLiterally("onSelected: async (result) => {") // Mark as async because case body might contain await
843
+ c.tsw.Indent(1)
844
+ c.tsw.WriteLine("")
845
+
846
+ // Handle assignment for channel receives if needed (inside the onSelected handler)
847
+ if assignStmt, ok := commClause.Comm.(*ast.AssignStmt); ok {
848
+ // This is a receive operation with assignment
849
+ if len(assignStmt.Lhs) == 1 {
850
+ // Simple receive: case v := <-ch:
851
+ valIdent, ok := assignStmt.Lhs[0].(*ast.Ident)
852
+ if ok && valIdent.Name != "_" { // Check for blank identifier
853
+ c.tsw.WriteLiterally("const ")
854
+ c.WriteIdent(valIdent, false)
855
+ c.tsw.WriteLiterally(" = result.value")
856
+ c.tsw.WriteLine("")
857
+ }
858
+ } else if len(assignStmt.Lhs) == 2 {
859
+ // Receive with ok: case v, ok := <-ch:
860
+ valIdent, valOk := assignStmt.Lhs[0].(*ast.Ident)
861
+ okIdent, okOk := assignStmt.Lhs[1].(*ast.Ident)
862
+
863
+ if valOk && valIdent.Name != "_" {
864
+ c.tsw.WriteLiterally("const ")
865
+ c.WriteIdent(valIdent, false)
866
+ c.tsw.WriteLiterally(" = result.value")
867
+ c.tsw.WriteLine("")
868
+ }
869
+
870
+ if okOk && okIdent.Name != "_" {
871
+ c.tsw.WriteLiterally("const ")
872
+ c.WriteIdent(okIdent, false)
873
+ c.tsw.WriteLiterally(" = result.ok")
874
+ c.tsw.WriteLine("")
875
+ }
876
+ }
877
+ }
878
+ // Note: Simple receive (case <-ch:) and send (case ch <- v:) don't require assignment here,
879
+ // as the operation was already performed by selectReceive/selectSend and the result is in 'result'.
880
+
881
+ // Write the case body
882
+ for _, bodyStmt := range commClause.Body {
883
+ if err := c.WriteStmt(bodyStmt); err != nil {
884
+ return fmt.Errorf("failed to write statement in select case body (onSelected): %w", err)
885
+ }
886
+ }
887
+
888
+ c.tsw.Indent(-1)
889
+ c.tsw.WriteLine("}") // Close onSelected handler
890
+ c.tsw.Indent(-1)
891
+ c.tsw.WriteLiterally("},") // Close SelectCase object and add comma
892
+ c.tsw.WriteLine("")
893
+
894
+ } else {
895
+ c.tsw.WriteCommentLinef("unknown statement in select body: %T", stmt)
896
+ }
897
+ }
898
+
899
+ // Close the array literal and the selectStatement call
900
+ c.tsw.Indent(-1)
901
+ c.tsw.WriteLiterally("], ")
902
+ c.tsw.WriteLiterallyf("%t", hasDefault)
903
+ c.tsw.WriteLiterally(")")
904
+ c.tsw.WriteLine("")
905
+
906
+ return nil
907
+ }