naider 1.14.0 → 1.16.0

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,1781 @@
1
+ export class GoGenerator {
2
+ constructor(options = {}) {
3
+ this.indent = 0;
4
+ this.output = [];
5
+ this.imports = new Set();
6
+ this.models = new Map();
7
+ this.schemas = new Map();
8
+ this.sourceMap = [];
9
+ this.currentSourceLine = 0;
10
+ this.sourceFile = options.sourceFile || null;
11
+ this.authSecret = null;
12
+ this.dbVar = null;
13
+ this.varTypes = new Map();
14
+ this.declaredVars = new Set();
15
+ }
16
+
17
+ generate(ast) {
18
+ this.visitProgram(ast);
19
+
20
+ const preamble = ['package main', ''];
21
+ if (this.imports.size > 0) {
22
+ preamble.push('import (');
23
+ for (const imp of [...this.imports].sort()) {
24
+ preamble.push(`\t"${imp}"`);
25
+ }
26
+ preamble.push(')');
27
+ preamble.push('');
28
+ }
29
+
30
+ this.output.unshift(...preamble);
31
+ return this.output.join('\n');
32
+ }
33
+
34
+ emit(line) {
35
+ this.output.push('\t'.repeat(this.indent) + line);
36
+ this.sourceMap.push(this.currentSourceLine);
37
+ }
38
+
39
+ emitRaw(line) {
40
+ this.output.push(line);
41
+ this.sourceMap.push(this.currentSourceLine);
42
+ }
43
+
44
+ addImport(mod) {
45
+ this.imports.add(mod);
46
+ }
47
+
48
+ visitProgram(node) {
49
+ for (const stmt of node.body) {
50
+ this.visitStatement(stmt);
51
+ }
52
+ }
53
+
54
+ visitStatement(node) {
55
+ if (node._line) this.currentSourceLine = node._line;
56
+ switch (node.type) {
57
+ case 'Use': return this.visitUse(node);
58
+ case 'UseDestructured': return this.visitUseDestructured(node);
59
+ case 'Function': return this.visitFunction(node);
60
+ case 'Return': return this.visitReturn(node);
61
+ case 'ReturnStatus': return this.visitReturnStatus(node);
62
+ case 'ReturnMethod': return this.visitReturnMethod(node);
63
+ case 'TypedVar': return this.visitTypedVar(node);
64
+ case 'If': return this.visitIf(node);
65
+ case 'Each': return this.visitEach(node);
66
+ case 'For': return this.visitFor(node);
67
+ case 'While': return this.visitWhile(node);
68
+ case 'Match': return this.visitMatch(node);
69
+ case 'Try': return this.visitTry(node);
70
+ case 'Server': return this.visitServer(node);
71
+ case 'Bot': return this.visitBot(node);
72
+ case 'Model': return this.visitModel(node);
73
+ case 'On': return this.visitOn(node);
74
+ case 'Log': return this.visitLog(node);
75
+ case 'Throw': return this.visitThrow(node);
76
+ case 'Break': this.emit('break'); return;
77
+ case 'Continue': this.emit('continue'); return;
78
+ case 'Assignment': return this.visitAssignment(node);
79
+ case 'CompoundAssign': return this.visitCompoundAssign(node);
80
+ case 'ExprStatement': this.emit(this.expr(node.expression)); return;
81
+ case 'DbConnect': return this.visitDbConnect(node);
82
+ case 'DbDir': return this.visitDbDir(node);
83
+ case 'DbSql': return this.visitDbSql(node);
84
+ case 'AwaitAll': return this.visitAwaitAllStatement(node);
85
+ case 'SchemaDecl': return this.visitSchema(node);
86
+ case 'AuthDecl': return this.visitAuth(node);
87
+ case 'CorsDecl': return this.visitCors(node);
88
+ case 'EnvDecl': return this.visitEnv(node);
89
+ case 'TestDecl': return this.visitTest(node);
90
+ case 'AssertStmt': return this.visitAssert(node);
91
+ case 'ErrorHandler': return this.visitErrorHandler(node);
92
+ case 'CrudDecl': return this.visitCrud(node);
93
+ case 'StaticDecl': return this.visitStatic(node);
94
+ case 'LimitDecl': return this.visitLimit(node);
95
+ case 'WsDecl': return this.visitWs(node);
96
+ case 'SessionDecl': return this.visitSession(node);
97
+ case 'CacheDecl': return this.visitCache(node);
98
+ case 'EveryDecl': return this.visitEvery(node);
99
+ case 'PromptDecl': return this.visitPrompt(node);
100
+ case 'Page': return this.visitPage(node);
101
+ case 'CliApp': return this.visitCli(node);
102
+ case 'MailConfig': return this.visitMail(node);
103
+ case 'DesktopApp': return this.visitDesktop(node);
104
+ case 'Screen': return this.visitScreen(node);
105
+ case 'OauthDecl': return this.visitOauth(node);
106
+ case 'PayDecl': return this.visitPay(node);
107
+ case 'StorageDecl': return this.visitStorage(node);
108
+ case 'PdfDecl': return this.visitPdf(node);
109
+ case 'I18nDecl': return this.visitI18n(node);
110
+ case 'PushDecl': return this.visitPush(node);
111
+ case 'SearchDecl': return this.visitSearch(node);
112
+ case 'ImageDecl': return this.visitImage(node);
113
+ case 'CsvDecl': return this.visitCsv(node);
114
+ case 'LoggingDecl': return this.visitLogging(node);
115
+ case 'MigrateDecl': return this.visitMigrate(node);
116
+ case 'GrpcDecl': return this.visitGrpc(node);
117
+ case 'WebrtcDecl': return this.visitWebrtc(node);
118
+ case 'BlockchainDecl': return this.visitBlockchain(node);
119
+ default:
120
+ this.emit(`// unknown: ${node.type}`);
121
+ }
122
+ }
123
+
124
+ // ===== Type mapping =====
125
+
126
+ mapType(naideType) {
127
+ const map = {
128
+ 'str': 'string',
129
+ 'string': 'string',
130
+ 'int': 'int',
131
+ 'integer': 'int',
132
+ 'num': 'float64',
133
+ 'number': 'float64',
134
+ 'float': 'float64',
135
+ 'bool': 'bool',
136
+ 'boolean': 'bool',
137
+ 'list': '[]interface{}',
138
+ 'map': 'map[string]interface{}',
139
+ 'json': 'interface{}',
140
+ 'any': 'interface{}',
141
+ 'void': '',
142
+ };
143
+ return map[naideType] || naideType;
144
+ }
145
+
146
+ // ===== Imports =====
147
+
148
+ visitUse(node) {
149
+ const raw = node.source ? this.rawString(node.source) : node.name;
150
+ const alias = node.alias || node.name;
151
+ const goModule = this.mapModuleName(raw);
152
+ this.emit(`// import: ${alias} from "${goModule}"`);
153
+ }
154
+
155
+ visitUseDestructured(node) {
156
+ const raw = this.rawString(node.source);
157
+ const goModule = this.mapModuleName(raw);
158
+ const names = node.names.map(n => n.alias ? `${n.name} as ${n.alias}` : n.name).join(', ');
159
+ this.emit(`// import { ${names} } from "${goModule}"`);
160
+ }
161
+
162
+ mapModuleName(name) {
163
+ const clean = name.replace(/^['"]|['"]$/g, '');
164
+ const map = {
165
+ 'express': 'net/http',
166
+ 'axios': 'net/http',
167
+ 'fs': 'os',
168
+ 'path': 'path/filepath',
169
+ 'crypto': 'crypto',
170
+ 'uuid': 'github.com/google/uuid',
171
+ };
172
+ return map[clean] || clean;
173
+ }
174
+
175
+ // ===== Functions =====
176
+
177
+ visitFunction(node) {
178
+ const params = node.params.map(p => {
179
+ let s = p.name;
180
+ if (p.type) {
181
+ s += ' ' + this.mapType(p.type);
182
+ } else {
183
+ s += ' interface{}';
184
+ }
185
+ return s;
186
+ }).join(', ');
187
+
188
+ const returnType = node.returnType ? ' ' + this.mapType(node.returnType) : '';
189
+
190
+ this.emit(`func ${node.name}(${params})${returnType} {`);
191
+ this.indent++;
192
+ if (node.body.length === 0) {
193
+ this.emit('// empty');
194
+ } else {
195
+ for (const stmt of node.body) {
196
+ this.visitStatement(stmt);
197
+ }
198
+ }
199
+ this.indent--;
200
+ this.emit('}');
201
+ this.emitRaw('');
202
+ }
203
+
204
+ visitReturn(node) {
205
+ if (node.value === null) {
206
+ this.emit('return');
207
+ } else {
208
+ this.emit(`return ${this.expr(node.value)}`);
209
+ }
210
+ }
211
+
212
+ visitReturnStatus(node) {
213
+ this.addImport('encoding/json');
214
+ this.addImport('net/http');
215
+ this.emit(`w.WriteHeader(${this.expr(node.statusCode)})`);
216
+ this.emit(`json.NewEncoder(w).Encode(${this.expr(node.body)})`);
217
+ this.emit('return');
218
+ }
219
+
220
+ visitReturnMethod(node) {
221
+ this.addImport('net/http');
222
+ const val = this.expr(node.value);
223
+ switch (node.method) {
224
+ case 'redirect':
225
+ this.emit(`http.Redirect(w, r, ${val}, http.StatusFound)`);
226
+ break;
227
+ case 'html':
228
+ this.emit(`w.Header().Set("Content-Type", "text/html")`);
229
+ this.emit(`fmt.Fprint(w, ${val})`);
230
+ this.addImport('fmt');
231
+ break;
232
+ case 'text':
233
+ this.emit(`w.Header().Set("Content-Type", "text/plain")`);
234
+ this.emit(`fmt.Fprint(w, ${val})`);
235
+ this.addImport('fmt');
236
+ break;
237
+ case 'file':
238
+ this.emit(`http.ServeFile(w, r, ${val})`);
239
+ break;
240
+ default:
241
+ this.emit(`fmt.Fprint(w, ${val})`);
242
+ this.addImport('fmt');
243
+ }
244
+ this.emit('return');
245
+ }
246
+
247
+ // ===== Variables =====
248
+
249
+ visitTypedVar(node) {
250
+ const value = this.expr(node.value);
251
+ if (node.varType) {
252
+ const goType = this.mapType(node.varType);
253
+ this.emit(`var ${node.name} ${goType} = ${value}`);
254
+ } else {
255
+ this.emit(`${node.name} := ${value}`);
256
+ }
257
+ this.declaredVars.add(node.name);
258
+ }
259
+
260
+ visitAssignment(node) {
261
+ const value = this.expr(node.value);
262
+
263
+ if (node.target.type === 'Array') {
264
+ // Go doesn't have destructuring; emit individual assignments
265
+ this.emit(`__tmp := ${value}`);
266
+ node.target.elements.forEach((e, i) => {
267
+ this.emit(`${this.expr(e)} := __tmp[${i}]`);
268
+ });
269
+ } else if (node.target.type === 'Object') {
270
+ this.emit(`__d := ${value}`);
271
+ for (const p of node.target.properties) {
272
+ const key = p.type === 'shorthand' ? p.key : p.key;
273
+ const varName = p.type === 'shorthand' ? p.key : this.expr(p.value);
274
+ this.emit(`${varName} := __d[${JSON.stringify(key)}]`);
275
+ }
276
+ } else {
277
+ const target = this.expr(node.target);
278
+ if (this.declaredVars.has(target)) {
279
+ this.emit(`${target} = ${value}`);
280
+ } else {
281
+ this.emit(`${target} := ${value}`);
282
+ this.declaredVars.add(target);
283
+ }
284
+ }
285
+ }
286
+
287
+ visitCompoundAssign(node) {
288
+ this.emit(`${this.expr(node.target)} ${node.op} ${this.expr(node.value)}`);
289
+ }
290
+
291
+ // ===== Control flow =====
292
+
293
+ visitIf(node) {
294
+ this.emit(`if ${this.expr(node.condition)} {`);
295
+ this.indent++;
296
+ for (const stmt of node.body) this.visitStatement(stmt);
297
+ this.indent--;
298
+
299
+ for (const elif of node.elifs) {
300
+ this.emit(`} else if ${this.expr(elif.condition)} {`);
301
+ this.indent++;
302
+ for (const stmt of elif.body) this.visitStatement(stmt);
303
+ this.indent--;
304
+ }
305
+
306
+ if (node.elseBody) {
307
+ this.emit('} else {');
308
+ this.indent++;
309
+ for (const stmt of node.elseBody) this.visitStatement(stmt);
310
+ this.indent--;
311
+ }
312
+ this.emit('}');
313
+ }
314
+
315
+ visitEach(node) {
316
+ const collection = this.expr(node.collection);
317
+ if (node.key) {
318
+ this.emit(`for ${node.key}, ${node.value} := range ${collection} {`);
319
+ } else {
320
+ this.emit(`for _, ${node.value} := range ${collection} {`);
321
+ }
322
+ this.indent++;
323
+ for (const stmt of node.body) this.visitStatement(stmt);
324
+ this.indent--;
325
+ this.emit('}');
326
+ }
327
+
328
+ visitFor(node) {
329
+ const varName = node.varName;
330
+ const start = this.expr(node.start);
331
+ const end = this.expr(node.end);
332
+ this.emit(`for ${varName} := ${start}; ${varName} < ${end}; ${varName}++ {`);
333
+ this.indent++;
334
+ for (const stmt of node.body) this.visitStatement(stmt);
335
+ this.indent--;
336
+ this.emit('}');
337
+ }
338
+
339
+ visitWhile(node) {
340
+ this.emit(`for ${this.expr(node.condition)} {`);
341
+ this.indent++;
342
+ for (const stmt of node.body) this.visitStatement(stmt);
343
+ this.indent--;
344
+ this.emit('}');
345
+ }
346
+
347
+ visitMatch(node) {
348
+ this.emit(`switch ${this.expr(node.value)} {`);
349
+ for (const c of node.cases) {
350
+ if (c.pattern.type === 'DefaultPattern') {
351
+ this.emit('default:');
352
+ } else {
353
+ this.emit(`case ${this.expr(c.pattern)}:`);
354
+ }
355
+ this.indent++;
356
+ for (const stmt of c.body) this.visitStatement(stmt);
357
+ this.indent--;
358
+ }
359
+ this.emit('}');
360
+ }
361
+
362
+ visitTry(node) {
363
+ // Go uses error-returning pattern instead of try/catch.
364
+ // We wrap in a func with deferred recover for panic-based errors.
365
+ this.emit('func() {');
366
+ this.indent++;
367
+ if (node.catchBody) {
368
+ const catchParam = node.catchVar || 'err';
369
+ this.emit('defer func() {');
370
+ this.indent++;
371
+ this.emit(`if r := recover(); r != nil {`);
372
+ this.indent++;
373
+ this.addImport('fmt');
374
+ this.emit(`${catchParam} := fmt.Errorf("%v", r)`);
375
+ for (const stmt of node.catchBody) this.visitStatement(stmt);
376
+ this.indent--;
377
+ this.emit('}');
378
+ this.indent--;
379
+ this.emit('}()');
380
+ }
381
+ if (node.ensureBody) {
382
+ this.emit('defer func() {');
383
+ this.indent++;
384
+ for (const stmt of node.ensureBody) this.visitStatement(stmt);
385
+ this.indent--;
386
+ this.emit('}()');
387
+ }
388
+ for (const stmt of node.body) this.visitStatement(stmt);
389
+ this.indent--;
390
+ this.emit('}()');
391
+ }
392
+
393
+ // ===== Server (net/http) =====
394
+
395
+ visitServer(node) {
396
+ this.addImport('net/http');
397
+ this.addImport('fmt');
398
+ this.addImport('encoding/json');
399
+
400
+ this.emitRaw('');
401
+
402
+ // Emit helper function for JSON responses
403
+ this.emit('func jsonResponse(w http.ResponseWriter, data interface{}) {');
404
+ this.indent++;
405
+ this.emit('w.Header().Set("Content-Type", "application/json")');
406
+ this.emit('json.NewEncoder(w).Encode(data)');
407
+ this.indent--;
408
+ this.emit('}');
409
+ this.emitRaw('');
410
+
411
+ // Emit middleware
412
+ for (const mid of node.middleware) {
413
+ this.emit(`// middleware`);
414
+ }
415
+
416
+ const errorHandlers = [];
417
+
418
+ for (const child of node.routes) {
419
+ if (child.type === 'Route') {
420
+ this.visitRoute(child);
421
+ } else if (child.type === 'AuthDecl') {
422
+ this.visitAuth(child);
423
+ } else if (child.type === 'CorsDecl') {
424
+ this.visitCors(child);
425
+ } else if (child.type === 'ErrorHandler') {
426
+ errorHandlers.push(child);
427
+ } else if (child.type === 'SchemaDecl') {
428
+ this.visitSchema(child);
429
+ } else if (child.type === 'GroupDecl') {
430
+ this.visitGroup(child);
431
+ } else if (child.type === 'CrudDecl') {
432
+ this.visitCrud(child);
433
+ } else if (child.type === 'StaticDecl') {
434
+ this.visitStatic(child);
435
+ } else if (child.type === 'LimitDecl') {
436
+ this.visitLimit(child);
437
+ } else if (child.type === 'WsDecl') {
438
+ this.visitWs(child);
439
+ } else if (child.type === 'SessionDecl') {
440
+ this.visitSession(child);
441
+ } else if (child.type === 'CacheDecl') {
442
+ this.visitCache(child);
443
+ } else if (child.type === 'GraphqlDecl') {
444
+ this.emit(`// GraphQL: use github.com/graphql-go/graphql`);
445
+ } else {
446
+ this.visitStatement(child);
447
+ }
448
+ }
449
+
450
+ for (const eh of errorHandlers) {
451
+ this.visitErrorHandler(eh);
452
+ }
453
+
454
+ const port = node.port ? this.rawExprValue(node.port) : '3000';
455
+ this.emitRaw('');
456
+ this.emit(`func main() {`);
457
+ this.indent++;
458
+ this.emit(`fmt.Println("Server running on port ${port}")`);
459
+ this.emit(`http.ListenAndServe(":${port}", nil)`);
460
+ this.indent--;
461
+ this.emit('}');
462
+ this.emitRaw('');
463
+ }
464
+
465
+ visitRoute(route) {
466
+ this.addImport('net/http');
467
+ this.addImport('encoding/json');
468
+
469
+ const method = route.method === 'del' ? 'DELETE' : route.method.toUpperCase();
470
+ const path = this.rawString(route.path);
471
+
472
+ this.emit(`http.HandleFunc("${path}", func(w http.ResponseWriter, r *http.Request) {`);
473
+ this.indent++;
474
+ if (method !== 'GET') {
475
+ this.emit(`if r.Method != "${method}" {`);
476
+ this.indent++;
477
+ this.emit(`http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)`);
478
+ this.emit('return');
479
+ this.indent--;
480
+ this.emit('}');
481
+ }
482
+
483
+ for (const stmt of route.body) {
484
+ if (stmt.type === 'Return' && stmt.value !== null) {
485
+ this.emit(`jsonResponse(w, ${this.expr(stmt.value)})`);
486
+ this.emit('return');
487
+ } else {
488
+ this.visitStatement(stmt);
489
+ }
490
+ }
491
+
492
+ this.indent--;
493
+ this.emit('})');
494
+ this.emitRaw('');
495
+ }
496
+
497
+ visitGroup(node) {
498
+ const prefix = this.rawString(node.prefix);
499
+ this.emit(`// group: ${prefix}`);
500
+ for (const child of node.routes) {
501
+ if (child.type === 'Route') {
502
+ // Prepend group prefix to route path
503
+ const origPath = this.rawString(child.path);
504
+ const fullPath = prefix + origPath;
505
+ this.emit(`http.HandleFunc("${fullPath}", func(w http.ResponseWriter, r *http.Request) {`);
506
+ this.indent++;
507
+ const method = child.method === 'del' ? 'DELETE' : child.method.toUpperCase();
508
+ if (method !== 'GET') {
509
+ this.emit(`if r.Method != "${method}" {`);
510
+ this.indent++;
511
+ this.emit(`http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)`);
512
+ this.emit('return');
513
+ this.indent--;
514
+ this.emit('}');
515
+ }
516
+ for (const stmt of child.body) {
517
+ if (stmt.type === 'Return' && stmt.value !== null) {
518
+ this.emit(`jsonResponse(w, ${this.expr(stmt.value)})`);
519
+ this.emit('return');
520
+ } else {
521
+ this.visitStatement(stmt);
522
+ }
523
+ }
524
+ this.indent--;
525
+ this.emit('})');
526
+ this.emitRaw('');
527
+ } else {
528
+ this.visitStatement(child);
529
+ }
530
+ }
531
+ }
532
+
533
+ // ===== Bot =====
534
+
535
+ visitBot(node) {
536
+ const tokenExpr = node.token ? this.expr(node.token) : 'os.Getenv("BOT_TOKEN")';
537
+ const botType = this.rawBotType(node.botType);
538
+
539
+ if (botType === 'discord') {
540
+ this.emit(`// Discord bot: use github.com/bwmarrin/discordgo`);
541
+ this.emit(`// token: ${tokenExpr}`);
542
+ } else if (botType === 'slack') {
543
+ this.emit(`// Slack bot: use github.com/slack-go/slack`);
544
+ this.emit(`// token: ${tokenExpr}`);
545
+ } else if (botType === 'telegram') {
546
+ this.emit(`// Telegram bot: use github.com/go-telegram-bot-api/telegram-bot-api`);
547
+ this.emit(`// token: ${tokenExpr}`);
548
+ } else if (botType === 'line') {
549
+ this.emit(`// LINE bot: use github.com/line/line-bot-sdk-go`);
550
+ this.emit(`// token: ${tokenExpr}`);
551
+ } else {
552
+ this.emit(`// ${botType} bot: configure appropriate Go library`);
553
+ this.emit(`// token: ${tokenExpr}`);
554
+ }
555
+ this.emitRaw('');
556
+
557
+ for (const handler of node.handlers) {
558
+ if (handler.type === 'BotEvent') {
559
+ const evtName = handler.event.raw || handler.event.parts?.map(p => p.value).join('');
560
+ this.emit(`// on "${evtName}":`);
561
+ this.indent++;
562
+ for (const stmt of handler.body) this.visitStatement(stmt);
563
+ this.indent--;
564
+ this.emitRaw('');
565
+ } else if (handler.type === 'BotSlashCmd') {
566
+ const cmdName = handler.name.raw || handler.name.parts?.map(p => p.value).join('');
567
+ this.emit(`// slash command "/${cmdName}":`);
568
+ this.indent++;
569
+ for (const stmt of handler.body) this.visitStatement(stmt);
570
+ this.indent--;
571
+ this.emitRaw('');
572
+ }
573
+ }
574
+ }
575
+
576
+ rawBotType(bt) {
577
+ if (!bt) return 'discord';
578
+ return bt.raw || bt.parts?.map(p => p.value).join('') || 'discord';
579
+ }
580
+
581
+ // ===== Model (struct) =====
582
+
583
+ visitModel(node) {
584
+ this.models.set(node.name, node);
585
+
586
+ this.emit(`type ${node.name} struct {`);
587
+ this.indent++;
588
+
589
+ if (node.parent) {
590
+ this.emit(`${node.parent} // embedded parent`);
591
+ }
592
+
593
+ for (const f of node.fields) {
594
+ const goType = f.type ? this.mapType(f.type) : 'interface{}';
595
+ const fieldName = f.name.charAt(0).toUpperCase() + f.name.slice(1);
596
+ this.emit(`${fieldName} ${goType} \`json:"${f.name}"\``);
597
+ }
598
+
599
+ this.indent--;
600
+ this.emit('}');
601
+ this.emitRaw('');
602
+
603
+ // Constructor function
604
+ if (node.fields.length > 0) {
605
+ const parentModel = node.parent ? this.models.get(node.parent) : null;
606
+ const parentFields = parentModel ? parentModel.fields : [];
607
+ const allFields = [...parentFields, ...node.fields];
608
+
609
+ const params = allFields.map(f => {
610
+ const goType = f.type ? this.mapType(f.type) : 'interface{}';
611
+ return `${f.name} ${goType}`;
612
+ }).join(', ');
613
+
614
+ this.emit(`func New${node.name}(${params}) ${node.name} {`);
615
+ this.indent++;
616
+ this.emit(`return ${node.name}{`);
617
+ this.indent++;
618
+ for (const f of node.fields) {
619
+ const fieldName = f.name.charAt(0).toUpperCase() + f.name.slice(1);
620
+ this.emit(`${fieldName}: ${f.name},`);
621
+ }
622
+ this.indent--;
623
+ this.emit('}');
624
+ this.indent--;
625
+ this.emit('}');
626
+ this.emitRaw('');
627
+ }
628
+
629
+ // Methods
630
+ for (const method of node.methods) {
631
+ const params = method.params.map(p => {
632
+ const goType = p.type ? this.mapType(p.type) : 'interface{}';
633
+ return `${p.name} ${goType}`;
634
+ }).join(', ');
635
+
636
+ const receiver = node.name.charAt(0).toLowerCase();
637
+ this.emit(`func (${receiver} *${node.name}) ${method.name.charAt(0).toUpperCase() + method.name.slice(1)}(${params}) {`);
638
+ this.indent++;
639
+ if (method.body.length === 0) {
640
+ this.emit('// empty');
641
+ } else {
642
+ for (const stmt of method.body) {
643
+ this.visitStatement(stmt);
644
+ }
645
+ }
646
+ this.indent--;
647
+ this.emit('}');
648
+ this.emitRaw('');
649
+ }
650
+ }
651
+
652
+ // ===== Events =====
653
+
654
+ visitOn(node) {
655
+ const event = this.expr(node.event);
656
+ this.emit(`// on ${event}`);
657
+ this.emit(`func() {`);
658
+ this.indent++;
659
+ for (const stmt of node.body) this.visitStatement(stmt);
660
+ this.indent--;
661
+ this.emit('}()');
662
+ this.emitRaw('');
663
+ }
664
+
665
+ // ===== Logging and errors =====
666
+
667
+ visitLog(node) {
668
+ this.addImport('fmt');
669
+ const args = node.args.map(a => this.expr(a)).join(', ');
670
+ if (node.level === 'warn' || node.level === 'error') {
671
+ this.addImport('log');
672
+ this.emit(`log.Println(${args})`);
673
+ } else {
674
+ this.emit(`fmt.Println(${args})`);
675
+ }
676
+ }
677
+
678
+ visitThrow(node) {
679
+ const value = this.expr(node.value);
680
+ if (node.value.type === 'String') {
681
+ this.emit(`panic(${value})`);
682
+ } else {
683
+ this.emit(`panic(${value})`);
684
+ }
685
+ }
686
+
687
+ // ===== Database =====
688
+
689
+ visitDbConnect(node) {
690
+ this.addImport('database/sql');
691
+ this.emit(`db, err := sql.Open("sqlite3", ${this.expr(node.connectionString)})`);
692
+ this.emit('if err != nil {');
693
+ this.indent++;
694
+ this.emit('panic(err)');
695
+ this.indent--;
696
+ this.emit('}');
697
+ this.emit('defer db.Close()');
698
+ this.dbVar = 'db';
699
+ }
700
+
701
+ visitDbDir(node) {
702
+ const raw = node.path.raw || node.path.parts?.map(p => p.value).join('') || 'data/';
703
+ this.emit(`// db dir: ${raw}`);
704
+ }
705
+
706
+ visitDbSql(node) {
707
+ this.addImport('database/sql');
708
+ const driverRaw = node.driver.raw || node.driver.parts?.map(p => p.value).join('') || 'sqlite';
709
+
710
+ if (driverRaw === 'sqlite') {
711
+ this.emit(`// import _ "github.com/mattn/go-sqlite3"`);
712
+ const conn = node.connection ? this.goStringValue(node.connection) : '"data.db"';
713
+ this.emit(`__db, err := sql.Open("sqlite3", ${conn})`);
714
+ } else if (driverRaw === 'postgres' || driverRaw === 'pg') {
715
+ this.emit(`// import _ "github.com/lib/pq"`);
716
+ const conn = node.connection ? this.goStringValue(node.connection) : 'os.Getenv("DATABASE_URL")';
717
+ this.emit(`__db, err := sql.Open("postgres", ${conn})`);
718
+ this.addImport('os');
719
+ } else {
720
+ const conn = node.connection ? this.goStringValue(node.connection) : '""';
721
+ this.emit(`__db, err := sql.Open("${driverRaw}", ${conn})`);
722
+ }
723
+ this.emit('if err != nil {');
724
+ this.indent++;
725
+ this.emit('panic(err)');
726
+ this.indent--;
727
+ this.emit('}');
728
+ this.emit('defer __db.Close()');
729
+ this.dbVar = '__db';
730
+ this.emitRaw('');
731
+ }
732
+
733
+ visitAwaitAllStatement(node) {
734
+ this.addImport('sync');
735
+ this.emit('var wg sync.WaitGroup');
736
+ for (const e of node.expressions) {
737
+ this.emit('wg.Add(1)');
738
+ this.emit('go func() {');
739
+ this.indent++;
740
+ this.emit('defer wg.Done()');
741
+ this.emit(this.expr(e));
742
+ this.indent--;
743
+ this.emit('}()');
744
+ }
745
+ this.emit('wg.Wait()');
746
+ }
747
+
748
+ // ===== Schema (struct) =====
749
+
750
+ visitSchema(node) {
751
+ this.emit(`type ${node.name} struct {`);
752
+ this.indent++;
753
+
754
+ for (const field of node.fields) {
755
+ const goType = this.mapSchemaType(field.type);
756
+ const fieldName = field.name.charAt(0).toUpperCase() + field.name.slice(1);
757
+ const tags = [`json:"${field.name}"`];
758
+ this.emit(`${fieldName} ${goType} \`${tags.join(' ')}\``);
759
+ }
760
+
761
+ this.indent--;
762
+ this.emit('}');
763
+ this.emitRaw('');
764
+
765
+ this.schemas.set(node.name, node);
766
+ }
767
+
768
+ mapSchemaType(type) {
769
+ const map = {
770
+ 'str': 'string',
771
+ 'string': 'string',
772
+ 'int': 'int',
773
+ 'integer': 'int',
774
+ 'num': 'float64',
775
+ 'number': 'float64',
776
+ 'bool': 'bool',
777
+ 'boolean': 'bool',
778
+ 'auto': 'int',
779
+ 'timestamp': 'string',
780
+ 'enum': 'string',
781
+ };
782
+ return map[type] || 'interface{}';
783
+ }
784
+
785
+ // ===== Auth =====
786
+
787
+ visitAuth(node) {
788
+ const secret = this.expr(node.secret);
789
+ this.authSecret = secret;
790
+ this.emit(`// JWT auth middleware`);
791
+ this.emit(`// secret: ${secret}`);
792
+ this.emit(`// Use github.com/golang-jwt/jwt/v5`);
793
+ this.emitRaw('');
794
+ }
795
+
796
+ // ===== CORS =====
797
+
798
+ visitCors(node) {
799
+ const origins = this.expr(node.origins);
800
+ this.emit(`// CORS: allow origins ${origins}`);
801
+ this.emit(`// Use github.com/rs/cors`);
802
+ this.emitRaw('');
803
+ }
804
+
805
+ // ===== Env =====
806
+
807
+ visitEnv(node) {
808
+ this.addImport('os');
809
+ this.emitRaw('');
810
+ for (const v of node.vars) {
811
+ let defaultVal = null;
812
+ for (const mod of v.modifiers) {
813
+ if (mod.name === 'default') {
814
+ defaultVal = this.expr(mod.args[0]);
815
+ }
816
+ }
817
+
818
+ if (defaultVal !== null) {
819
+ this.emit(`${v.name} := os.Getenv(${JSON.stringify(v.name)})`);
820
+ this.emit(`if ${v.name} == "" {`);
821
+ this.indent++;
822
+ this.emit(`${v.name} = ${defaultVal}`);
823
+ this.indent--;
824
+ this.emit('}');
825
+ } else {
826
+ this.emit(`${v.name} := os.Getenv(${JSON.stringify(v.name)})`);
827
+ }
828
+
829
+ // Type coercion
830
+ if (v.type === 'int') {
831
+ this.addImport('strconv');
832
+ const tmpName = `__${v.name}_int`;
833
+ this.emit(`${tmpName}, _ := strconv.Atoi(${v.name})`);
834
+ this.emit(`_ = ${tmpName} // use ${tmpName} as int`);
835
+ } else if (v.type === 'num') {
836
+ this.addImport('strconv');
837
+ const tmpName = `__${v.name}_float`;
838
+ this.emit(`${tmpName}, _ := strconv.ParseFloat(${v.name}, 64)`);
839
+ this.emit(`_ = ${tmpName} // use ${tmpName} as float64`);
840
+ } else if (v.type === 'bool') {
841
+ this.addImport('strconv');
842
+ const tmpName = `__${v.name}_bool`;
843
+ this.emit(`${tmpName}, _ := strconv.ParseBool(${v.name})`);
844
+ this.emit(`_ = ${tmpName} // use ${tmpName} as bool`);
845
+ }
846
+ }
847
+ this.emitRaw('');
848
+ }
849
+
850
+ // ===== Tests =====
851
+
852
+ visitTest(node) {
853
+ this.addImport('testing');
854
+ const name = this.rawString(node.name);
855
+ const funcName = 'Test_' + name.replace(/[^a-zA-Z0-9]/g, '_').replace(/_+/g, '_');
856
+
857
+ this.emit(`func ${funcName}(t *testing.T) {`);
858
+ this.indent++;
859
+ if (node.body.length === 0) {
860
+ this.emit('// empty test');
861
+ } else {
862
+ for (const stmt of node.body) this.visitStatement(stmt);
863
+ }
864
+ this.indent--;
865
+ this.emit('}');
866
+ this.emitRaw('');
867
+ }
868
+
869
+ visitAssert(node) {
870
+ this.addImport('testing');
871
+ const exprNode = node.expr;
872
+ if (exprNode.type === 'Binary' && (exprNode.op === '===' || exprNode.op === '==')) {
873
+ this.emit(`if ${this.expr(exprNode.left)} != ${this.expr(exprNode.right)} {`);
874
+ this.indent++;
875
+ this.emit(`t.Errorf("expected %v, got %v", ${this.expr(exprNode.right)}, ${this.expr(exprNode.left)})`);
876
+ this.indent--;
877
+ this.emit('}');
878
+ } else if (exprNode.type === 'Binary' && (exprNode.op === '!==' || exprNode.op === '!=')) {
879
+ this.emit(`if ${this.expr(exprNode.left)} == ${this.expr(exprNode.right)} {`);
880
+ this.indent++;
881
+ this.emit(`t.Errorf("expected not equal: %v", ${this.expr(exprNode.left)})`);
882
+ this.indent--;
883
+ this.emit('}');
884
+ } else {
885
+ this.emit(`if !(${this.expr(exprNode)}) {`);
886
+ this.indent++;
887
+ this.emit(`t.Errorf("assertion failed: ${this.expr(exprNode)}")`);
888
+ this.indent--;
889
+ this.emit('}');
890
+ }
891
+ }
892
+
893
+ // ===== Error handler =====
894
+
895
+ visitErrorHandler(node) {
896
+ this.emit(`// error handler`);
897
+ this.emit(`func errorHandler(w http.ResponseWriter, r *http.Request, err error) {`);
898
+ this.indent++;
899
+ if (node.body.length === 0) {
900
+ this.emit('http.Error(w, err.Error(), http.StatusInternalServerError)');
901
+ } else {
902
+ for (const stmt of node.body) this.visitStatement(stmt);
903
+ }
904
+ this.indent--;
905
+ this.emit('}');
906
+ this.emitRaw('');
907
+ }
908
+
909
+ // ===== CRUD =====
910
+
911
+ visitCrud(node) {
912
+ this.addImport('net/http');
913
+ this.addImport('encoding/json');
914
+ this.addImport('sync');
915
+
916
+ const path = this.rawString(node.path);
917
+ const schema = node.schemaName;
918
+ const storeName = `__${schema.toLowerCase()}Store`;
919
+ const mutexName = `__${schema.toLowerCase()}Mu`;
920
+
921
+ this.emitRaw('');
922
+ this.emit(`var ${storeName} = make([]${schema}, 0)`);
923
+ this.emit(`var ${mutexName} sync.Mutex`);
924
+ this.emit(`var __${schema.toLowerCase()}ID = 0`);
925
+ this.emitRaw('');
926
+
927
+ // List all
928
+ this.emit(`http.HandleFunc("${path}", func(w http.ResponseWriter, r *http.Request) {`);
929
+ this.indent++;
930
+ this.emit('if r.Method == "GET" {');
931
+ this.indent++;
932
+ this.emit(`jsonResponse(w, ${storeName})`);
933
+ this.emit('return');
934
+ this.indent--;
935
+ this.emit('}');
936
+
937
+ // Create
938
+ this.emit('if r.Method == "POST" {');
939
+ this.indent++;
940
+ this.emit(`var item ${schema}`);
941
+ this.emit('json.NewDecoder(r.Body).Decode(&item)');
942
+ this.emit(`${mutexName}.Lock()`);
943
+ this.emit(`__${schema.toLowerCase()}ID++`);
944
+ this.emit(`${storeName} = append(${storeName}, item)`);
945
+ this.emit(`${mutexName}.Unlock()`);
946
+ this.emit('w.WriteHeader(http.StatusCreated)');
947
+ this.emit('jsonResponse(w, item)');
948
+ this.emit('return');
949
+ this.indent--;
950
+ this.emit('}');
951
+
952
+ this.indent--;
953
+ this.emit('})');
954
+ this.emitRaw('');
955
+ }
956
+
957
+ // ===== Static files =====
958
+
959
+ visitStatic(node) {
960
+ this.addImport('net/http');
961
+ let raw = node.path.raw || node.path.parts?.map(p => p.value).join('') || 'public';
962
+ if (raw.startsWith('/')) raw = raw.slice(1);
963
+ this.emit(`http.Handle("/", http.FileServer(http.Dir(${JSON.stringify(raw)})))`);
964
+ this.emitRaw('');
965
+ }
966
+
967
+ // ===== Rate limiting =====
968
+
969
+ visitLimit(node) {
970
+ const path = this.rawString(node.path);
971
+ const max = this.expr(node.max);
972
+ const window = this.expr(node.window);
973
+ this.emit(`// rate limit: ${max} requests per ${window} on ${path}`);
974
+ this.emit(`// Use golang.org/x/time/rate`);
975
+ this.emitRaw('');
976
+ }
977
+
978
+ // ===== WebSocket =====
979
+
980
+ visitWs(node) {
981
+ this.emit(`// WebSocket: use github.com/gorilla/websocket`);
982
+ const path = this.rawString(node.path);
983
+ this.emit(`// path: ${path}`);
984
+ this.emitRaw('');
985
+
986
+ for (const evt of (node.events || [])) {
987
+ const evtName = evt.name.raw || evt.name.parts?.map(p => p.value).join('');
988
+ this.emit(`// ws event: ${evtName}`);
989
+ this.indent++;
990
+ for (const stmt of evt.body) this.visitStatement(stmt);
991
+ this.indent--;
992
+ }
993
+ this.emitRaw('');
994
+ }
995
+
996
+ // ===== Session =====
997
+
998
+ visitSession(node) {
999
+ this.emit(`// Session: use github.com/gorilla/sessions`);
1000
+ this.emitRaw('');
1001
+ }
1002
+
1003
+ // ===== Cache =====
1004
+
1005
+ visitCache(node) {
1006
+ const path = this.rawString(node.path);
1007
+ this.emit(`// cache: ${path}`);
1008
+ this.emit(`// Use github.com/patrickmn/go-cache`);
1009
+ this.emitRaw('');
1010
+ }
1011
+
1012
+ // ===== Every (cron/scheduler) =====
1013
+
1014
+ visitEvery(node) {
1015
+ const raw = node.interval?.value?.raw || node.interval?.value?.parts?.map(p => p.value).join('') || '';
1016
+ const isCron = /^[\d*\/,-]+\s+[\d*\/,-]+\s+[\d*\/,-]+\s+[\d*\/,-]+\s+[\d*\/,-]+/.test(raw.trim());
1017
+
1018
+ if (isCron) {
1019
+ this.emit(`// Cron: use github.com/robfig/cron/v3`);
1020
+ this.emit(`// schedule: ${raw}`);
1021
+ } else {
1022
+ this.addImport('time');
1023
+ const interval = this.expr(node.interval);
1024
+ this.emit(`go func() {`);
1025
+ this.indent++;
1026
+ this.emit(`for {`);
1027
+ this.indent++;
1028
+ for (const stmt of node.body) this.visitStatement(stmt);
1029
+ this.emit(`time.Sleep(${interval})`);
1030
+ this.indent--;
1031
+ this.emit('}');
1032
+ this.indent--;
1033
+ this.emit('}()');
1034
+ }
1035
+ this.emitRaw('');
1036
+ }
1037
+
1038
+ // ===== Prompt template =====
1039
+
1040
+ visitPrompt(node) {
1041
+ this.addImport('strings');
1042
+ const name = node.name;
1043
+ const lines = node.lines.map(l => this.generateString(l));
1044
+ const template = lines.length === 1 ? lines[0] : lines.join(` + "\\n" + `);
1045
+
1046
+ this.emit(`func ${name}(vars map[string]string) string {`);
1047
+ this.indent++;
1048
+ this.emit(`template := ${template}`);
1049
+ this.emit('for k, v := range vars {');
1050
+ this.indent++;
1051
+ this.emit('template = strings.ReplaceAll(template, "{" + k + "}", v)');
1052
+ this.indent--;
1053
+ this.emit('}');
1054
+ this.emit('return template');
1055
+ this.indent--;
1056
+ this.emit('}');
1057
+ this.emitRaw('');
1058
+ }
1059
+
1060
+ // ===== Page (HTML generation) =====
1061
+
1062
+ visitPage(node) {
1063
+ this.addImport('os');
1064
+ const filename = this.rawPageString(node.filename);
1065
+ const headParts = [];
1066
+ const bodyParts = [];
1067
+ let pageTitle = 'NAIDE Page';
1068
+
1069
+ for (const el of node.elements) {
1070
+ this.classifyPageElement(el, headParts, bodyParts, (t) => { pageTitle = t; });
1071
+ }
1072
+
1073
+ this.emit(`func() {`);
1074
+ this.indent++;
1075
+ this.emit(`f, err := os.Create(${JSON.stringify(filename)})`);
1076
+ this.emit('if err != nil { panic(err) }');
1077
+ this.emit('defer f.Close()');
1078
+ this.addImport('fmt');
1079
+ this.emit(`fmt.Fprint(f, \`<!DOCTYPE html>`);
1080
+ this.emit(`<html lang="en">`);
1081
+ this.emit(`<head>`);
1082
+ this.emit(`<meta charset="UTF-8">`);
1083
+ this.emit(`<meta name="viewport" content="width=device-width, initial-scale=1.0">`);
1084
+ this.emit(`<title>${pageTitle}</title>`);
1085
+ for (const h of headParts) this.emit(h);
1086
+ this.emit(`</head>`);
1087
+ this.emit(`<body>`);
1088
+ for (const b of bodyParts) this.emit(b);
1089
+ this.emit(`</body>`);
1090
+ this.emit(`</html>\`)`);
1091
+ this.indent--;
1092
+ this.emit('}()');
1093
+ this.emitRaw('');
1094
+ }
1095
+
1096
+ classifyPageElement(el, head, body, setTitle) {
1097
+ const tag = el.tag;
1098
+ const arg0 = el.args[0] ? this.rawPageString(el.args[0]) : '';
1099
+ const arg1 = el.args[1] ? this.rawPageString(el.args[1]) : '';
1100
+ if (tag === 'title') { setTitle(arg0); return; }
1101
+ if (tag === 'style' || tag === 'link') { head.push(`<link rel="stylesheet" href="${arg0}">`); return; }
1102
+ if (tag === 'meta') { head.push(`<meta name="${arg0}" content="${arg1}">`); return; }
1103
+ if (tag === 'script') { body.push(`<script src="${arg0}"></script>`); return; }
1104
+ if (tag === 'img') { body.push(`<img src="${arg0}" alt="${arg1}">`); return; }
1105
+ if (tag === 'a') { body.push(`<a href="${arg0}">${arg1}</a>`); return; }
1106
+ if (tag === 'input') { body.push(`<input type="${arg0}" name="${arg1}">`); return; }
1107
+ if (el.children && el.children.length > 0) {
1108
+ const cls = arg0 ? ` class="${arg0}"` : '';
1109
+ body.push(`<${tag}${cls}>`);
1110
+ for (const child of el.children) this.classifyPageElement(child, head, body, setTitle);
1111
+ body.push(`</${tag}>`);
1112
+ } else {
1113
+ body.push(`<${tag}>${arg0}</${tag}>`);
1114
+ }
1115
+ }
1116
+
1117
+ // ===== CLI App =====
1118
+
1119
+ visitCli(node) {
1120
+ this.addImport('flag');
1121
+ this.addImport('fmt');
1122
+ this.emitRaw('');
1123
+
1124
+ const desc = node.description ? this.rawPageString(node.description) : node.name;
1125
+ this.emit(`// CLI: ${desc}`);
1126
+ this.emitRaw('');
1127
+
1128
+ for (const arg of node.args) {
1129
+ const argName = this.rawPageString(arg.name);
1130
+ if (arg.type === 'int') {
1131
+ this.emit(`var __arg_${argName} = flag.Int(${JSON.stringify(argName)}, 0, ${JSON.stringify(this.rawPageString(arg.description) || argName)})`);
1132
+ } else if (arg.type === 'num') {
1133
+ this.emit(`var __arg_${argName} = flag.Float64(${JSON.stringify(argName)}, 0, ${JSON.stringify(this.rawPageString(arg.description) || argName)})`);
1134
+ } else if (arg.type === 'bool') {
1135
+ this.emit(`var __arg_${argName} = flag.Bool(${JSON.stringify(argName)}, false, ${JSON.stringify(this.rawPageString(arg.description) || argName)})`);
1136
+ } else {
1137
+ this.emit(`var __arg_${argName} = flag.String(${JSON.stringify(argName)}, "", ${JSON.stringify(this.rawPageString(arg.description) || argName)})`);
1138
+ }
1139
+ }
1140
+
1141
+ for (const fl of node.flags) {
1142
+ const l = this.rawPageString(fl.long);
1143
+ const desc = fl.description ? this.rawPageString(fl.description) : l;
1144
+ this.emit(`var __flag_${l} = flag.Bool(${JSON.stringify(l)}, false, ${JSON.stringify(desc)})`);
1145
+ }
1146
+
1147
+ this.emit('flag.Parse()');
1148
+ this.emitRaw('');
1149
+
1150
+ if (node.run) {
1151
+ for (const stmt of node.run.body) this.visitStatement(stmt);
1152
+ }
1153
+ this.emitRaw('');
1154
+ }
1155
+
1156
+ // ===== Mail =====
1157
+
1158
+ visitMail(node) {
1159
+ this.addImport('net/smtp');
1160
+ this.emitRaw('');
1161
+
1162
+ const host = this.generateString(node.host);
1163
+ const port = this.expr(node.port);
1164
+ const user = node.user ? this.expr(node.user) : 'os.Getenv("MAIL_USER")';
1165
+ const pass = node.pass ? this.expr(node.pass) : 'os.Getenv("MAIL_PASS")';
1166
+
1167
+ this.emit(`// Mail config`);
1168
+ this.emit(`// host: ${host}, port: ${port}`);
1169
+ this.emit(`// Use net/smtp.SendMail or github.com/jordan-wright/email`);
1170
+ this.emit(`func sendMail(to string, subject string, body string) error {`);
1171
+ this.indent++;
1172
+ this.emit(`auth := smtp.PlainAuth("", ${user}, ${pass}, ${host})`);
1173
+ this.addImport('fmt');
1174
+ this.emit(`msg := []byte(fmt.Sprintf("Subject: %s\\r\\n\\r\\n%s", subject, body))`);
1175
+ this.emit(`return smtp.SendMail(fmt.Sprintf("%s:%s", ${host}, ${port}), auth, ${user}, []string{to}, msg)`);
1176
+ this.indent--;
1177
+ this.emit('}');
1178
+ this.emitRaw('');
1179
+ }
1180
+
1181
+ // ===== Desktop =====
1182
+
1183
+ visitDesktop(node) {
1184
+ const title = node.title ? this.rawPageString(node.title) : node.name;
1185
+ const width = node.width ? this.expr(node.width) : '800';
1186
+ const height = node.height ? this.expr(node.height) : '600';
1187
+ this.emit(`// Desktop app: use github.com/nicholasgasior/gowl or github.com/nicholasgasior/fyne`);
1188
+ this.emit(`// title: ${JSON.stringify(title)}, width: ${width}, height: ${height}`);
1189
+ this.emit(`// Go desktop frameworks: Fyne (github.com/fyne-io/fyne), Wails (github.com/wailsapp/wails)`);
1190
+ this.emitRaw('');
1191
+ }
1192
+
1193
+ // ===== Screen (mobile) =====
1194
+
1195
+ visitScreen(node) {
1196
+ this.emit(`// Mobile screen: ${node.name}`);
1197
+ this.emit(`// Go mobile: use golang.org/x/mobile or github.com/nicholasgasior/fyne for cross-platform`);
1198
+
1199
+ for (const el of node.elements) {
1200
+ const tag = el.tag;
1201
+ const arg0 = el.args[0] ? this.rawPageString(el.args[0]) : '';
1202
+ this.emit(`// element: <${tag}> ${arg0}`);
1203
+ }
1204
+ this.emitRaw('');
1205
+ }
1206
+
1207
+ // ===== OAuth =====
1208
+
1209
+ visitOauth(node) {
1210
+ const provider = this.rawPageString(node.provider);
1211
+ const clientId = this.expr(node.clientId);
1212
+ const clientSecret = this.expr(node.clientSecret);
1213
+ const callback = node.callback ? this.rawPageString(node.callback) : '/auth/callback';
1214
+
1215
+ this.emit(`// OAuth: ${provider}`);
1216
+ this.emit(`// Use golang.org/x/oauth2`);
1217
+ this.addImport('os');
1218
+ this.emit(`// clientID: ${clientId}`);
1219
+ this.emit(`// clientSecret: ${clientSecret}`);
1220
+ this.emit(`// callback: ${callback}`);
1221
+ this.emitRaw('');
1222
+ }
1223
+
1224
+ // ===== Pay =====
1225
+
1226
+ visitPay(node) {
1227
+ const provider = this.rawPageString(node.provider);
1228
+ const secretKey = this.expr(node.secretKey);
1229
+
1230
+ if (provider === 'stripe') {
1231
+ this.emit(`// Stripe payments: use github.com/stripe/stripe-go/v76`);
1232
+ this.emit(`// secretKey: ${secretKey}`);
1233
+ } else {
1234
+ this.emit(`// ${provider} payment integration`);
1235
+ }
1236
+ this.emitRaw('');
1237
+ }
1238
+
1239
+ // ===== Storage =====
1240
+
1241
+ visitStorage(node) {
1242
+ const provider = this.rawPageString(node.provider);
1243
+ const bucket = this.expr(node.bucket);
1244
+
1245
+ if (provider === 's3') {
1246
+ this.emit(`// S3 storage: use github.com/aws/aws-sdk-go-v2`);
1247
+ this.emit(`// bucket: ${bucket}`);
1248
+ } else if (provider === 'gcs') {
1249
+ this.emit(`// GCS storage: use cloud.google.com/go/storage`);
1250
+ this.emit(`// bucket: ${bucket}`);
1251
+ } else {
1252
+ this.emit(`// ${provider} storage integration`);
1253
+ }
1254
+ this.emitRaw('');
1255
+ }
1256
+
1257
+ // ===== PDF =====
1258
+
1259
+ visitPdf(node) {
1260
+ const filename = this.rawPageString(node.filename);
1261
+ this.emit(`// PDF generation: use github.com/jung-kurt/gofpdf`);
1262
+ this.emit(`// output: ${filename}`);
1263
+
1264
+ for (const el of node.elements) {
1265
+ const tag = el.tag;
1266
+ const arg0 = el.args[0] ? this.rawPageString(el.args[0]) : '';
1267
+ this.emit(`// pdf element: <${tag}> ${arg0}`);
1268
+ }
1269
+ this.emitRaw('');
1270
+ }
1271
+
1272
+ // ===== i18n =====
1273
+
1274
+ visitI18n(node) {
1275
+ const dir = this.rawPageString(node.dir);
1276
+ const defaultLang = node.defaultLang ? this.rawPageString(node.defaultLang) : 'en';
1277
+
1278
+ this.emit(`// i18n: dir=${dir}, default=${defaultLang}`);
1279
+ this.emit(`// Use github.com/nicksnyder/go-i18n/v2`);
1280
+ for (const lang of node.langs) {
1281
+ const code = this.rawPageString(lang.code);
1282
+ const file = this.rawPageString(lang.file);
1283
+ this.emit(`// lang: ${code} -> ${file}`);
1284
+ }
1285
+ this.emitRaw('');
1286
+ }
1287
+
1288
+ // ===== Push =====
1289
+
1290
+ visitPush(node) {
1291
+ this.emit(`// Push notifications: use github.com/SherClockHolmes/webpush-go`);
1292
+ this.emitRaw('');
1293
+ }
1294
+
1295
+ // ===== Search =====
1296
+
1297
+ visitSearch(node) {
1298
+ const engine = this.rawPageString(node.engine);
1299
+ if (engine === 'meilisearch') {
1300
+ this.emit(`// Search: Meilisearch - use github.com/meilisearch/meilisearch-go`);
1301
+ } else {
1302
+ this.emit(`// Search: Elasticsearch - use github.com/elastic/go-elasticsearch/v8`);
1303
+ }
1304
+ this.emitRaw('');
1305
+ }
1306
+
1307
+ // ===== Image =====
1308
+
1309
+ visitImage(node) {
1310
+ const input = this.expr(node.input);
1311
+ this.emit(`// Image processing: use github.com/disintegration/imaging`);
1312
+ this.emit(`// input: ${input}`);
1313
+
1314
+ for (const op of node.operations) {
1315
+ this.emit(`// operation: ${op.op}(${(op.args || []).join(', ')})`);
1316
+ }
1317
+ this.emitRaw('');
1318
+ }
1319
+
1320
+ // ===== CSV =====
1321
+
1322
+ visitCsv(node) {
1323
+ const name = this.rawPageString(node.name);
1324
+ const format = node.format ? this.rawPageString(node.format) : 'csv';
1325
+ const output = node.output ? this.rawPageString(node.output) : `${name}.${format}`;
1326
+
1327
+ if (format === 'xlsx' || format === 'excel') {
1328
+ this.emit(`// Excel export: use github.com/xuri/excelize/v2`);
1329
+ } else {
1330
+ this.addImport('encoding/csv');
1331
+ this.addImport('os');
1332
+ this.emit(`// CSV export: ${output}`);
1333
+ }
1334
+ this.emit(`// output: ${output}`);
1335
+ this.emitRaw('');
1336
+ }
1337
+
1338
+ // ===== Logging =====
1339
+
1340
+ visitLogging(node) {
1341
+ this.addImport('log');
1342
+ const name = this.rawPageString(node.name);
1343
+ const level = node.level ? this.rawPageString(node.level) : 'info';
1344
+ const file = node.file ? this.rawPageString(node.file) : null;
1345
+
1346
+ this.emit(`// Logger: ${name} (level: ${level})`);
1347
+ this.emit(`// Use log/slog for structured logging or github.com/sirupsen/logrus`);
1348
+ if (file) {
1349
+ this.addImport('os');
1350
+ this.emit(`__logFile, err := os.OpenFile(${JSON.stringify(file)}, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)`);
1351
+ this.emit('if err != nil { panic(err) }');
1352
+ this.emit(`defer __logFile.Close()`);
1353
+ this.emit(`logger := log.New(__logFile, "", log.LstdFlags)`);
1354
+ } else {
1355
+ this.emit(`logger := log.Default()`);
1356
+ }
1357
+ this.emit(`_ = logger`);
1358
+ this.emitRaw('');
1359
+ }
1360
+
1361
+ // ===== DB Migration =====
1362
+
1363
+ visitMigrate(node) {
1364
+ const name = this.rawPageString(node.name);
1365
+ this.emit(`// DB Migration: ${name}`);
1366
+ this.emit(`// Use github.com/golang-migrate/migrate/v4`);
1367
+
1368
+ this.emit(`// up:`);
1369
+ this.indent++;
1370
+ for (const stmt of node.up) this.visitStatement(stmt);
1371
+ this.indent--;
1372
+
1373
+ this.emit(`// down:`);
1374
+ this.indent++;
1375
+ for (const stmt of node.down) this.visitStatement(stmt);
1376
+ this.indent--;
1377
+ this.emitRaw('');
1378
+ }
1379
+
1380
+ // ===== gRPC =====
1381
+
1382
+ visitGrpc(node) {
1383
+ const name = this.rawPageString(node.name);
1384
+ const port = node.port ? this.expr(node.port) : '50051';
1385
+
1386
+ this.emit(`// gRPC server: ${name}`);
1387
+ this.emit(`// Use google.golang.org/grpc`);
1388
+ this.emit(`// port: ${port}`);
1389
+
1390
+ for (const rpc of node.rpcs) {
1391
+ this.emit(`// rpc ${rpc.method}:`);
1392
+ this.indent++;
1393
+ for (const stmt of rpc.body) this.visitStatement(stmt);
1394
+ this.indent--;
1395
+ }
1396
+ this.emitRaw('');
1397
+ }
1398
+
1399
+ // ===== WebRTC =====
1400
+
1401
+ visitWebrtc(node) {
1402
+ const name = this.rawPageString(node.name);
1403
+ this.emit(`// WebRTC: ${name}`);
1404
+ this.emit(`// Use github.com/pion/webrtc/v3`);
1405
+
1406
+ for (const evt of node.events) {
1407
+ this.emit(`// event: ${evt.event}`);
1408
+ this.indent++;
1409
+ for (const stmt of evt.body) this.visitStatement(stmt);
1410
+ this.indent--;
1411
+ }
1412
+ this.emitRaw('');
1413
+ }
1414
+
1415
+ // ===== Blockchain =====
1416
+
1417
+ visitBlockchain(node) {
1418
+ const name = this.rawPageString(node.name);
1419
+ const provider = node.provider ? this.expr(node.provider) : '"http://localhost:8545"';
1420
+
1421
+ this.emit(`// Blockchain: ${name}`);
1422
+ this.emit(`// Use github.com/ethereum/go-ethereum`);
1423
+ this.emit(`// provider: ${provider}`);
1424
+ this.emitRaw('');
1425
+ }
1426
+
1427
+ // ===== Expression generation =====
1428
+
1429
+ expr(node) {
1430
+ if (!node) return 'nil';
1431
+
1432
+ switch (node.type) {
1433
+ case 'Number': return node.value;
1434
+ case 'String': return this.generateString(node.value);
1435
+ case 'Bool': return node.value ? 'true' : 'false';
1436
+ case 'Null': return 'nil';
1437
+ case 'Self': return 'self'; // receiver in methods
1438
+ case 'Identifier': return this.mapIdentifier(node.name);
1439
+
1440
+ case 'Binary': {
1441
+ const op = this.mapBinaryOp(node.op);
1442
+ const l = this.expr(node.left);
1443
+ const r = this.expr(node.right);
1444
+ const simple = node.left.type !== 'Binary' && node.right.type !== 'Binary';
1445
+ return simple ? `${l} ${op} ${r}` : `(${l} ${op} ${r})`;
1446
+ }
1447
+
1448
+ case 'Unary': {
1449
+ const op = this.mapUnaryOp(node.op);
1450
+ return `${op}${this.expr(node.expr)}`;
1451
+ }
1452
+
1453
+ case 'Await':
1454
+ // Go doesn't have await; it uses goroutines. Just emit the expression.
1455
+ return this.expr(node.expr);
1456
+
1457
+ case 'AwaitAllExpr':
1458
+ return this.expr(node.expr);
1459
+
1460
+ case 'Spread':
1461
+ return `${this.expr(node.expr)}...`;
1462
+
1463
+ case 'New':
1464
+ return this.expr(node.expr);
1465
+
1466
+ case 'TypeOf':
1467
+ this.addImport('fmt');
1468
+ return `fmt.Sprintf("%T", ${this.expr(node.expr)})`;
1469
+
1470
+ case 'MemberAccess':
1471
+ return this.mapMemberAccess(node);
1472
+
1473
+ case 'OptionalAccess':
1474
+ // Go doesn't have ?. — just access directly
1475
+ return `${this.expr(node.object)}.${node.property.charAt(0).toUpperCase() + node.property.slice(1)}`;
1476
+
1477
+ case 'Call':
1478
+ return this.generateCall(node);
1479
+
1480
+ case 'IndexAccess':
1481
+ return `${this.expr(node.object)}[${this.expr(node.index)}]`;
1482
+
1483
+ case 'Array': {
1484
+ const elements = node.elements.map(e => this.expr(e)).join(', ');
1485
+ return `[]interface{}{${elements}}`;
1486
+ }
1487
+
1488
+ case 'Object': {
1489
+ const props = node.properties.map(p => {
1490
+ if (p.type === 'spread') return `// spread: ${this.expr(p.value)}`;
1491
+ const key = p.type === 'shorthand' ? p.key :
1492
+ (typeof p.key === 'string' ? p.key :
1493
+ (p.key.type === 'String' ? this.rawString(p.key.value) :
1494
+ (p.key.type === 'Computed' ? this.expr(p.key.expr) : this.expr(p.key))));
1495
+ const val = p.type === 'shorthand' ? p.key : this.expr(p.value);
1496
+ return `${JSON.stringify(key)}: ${val}`;
1497
+ }).join(', ');
1498
+ return `map[string]interface{}{${props}}`;
1499
+ }
1500
+
1501
+ case 'ArrowFn': {
1502
+ const params = node.params.map(p => `${p.name} interface{}`).join(', ');
1503
+ const body = this.expr(node.body);
1504
+ return `func(${params}) interface{} { return ${body} }`;
1505
+ }
1506
+
1507
+ case 'Lambda': {
1508
+ const params = node.params.map(p => `${p.name} interface{}`).join(', ');
1509
+ const bodyLines = [];
1510
+ const savedOutput = this.output;
1511
+ const savedIndent = this.indent;
1512
+ this.output = bodyLines;
1513
+ this.indent = 0;
1514
+ for (const stmt of node.body) this.visitStatement(stmt);
1515
+ this.output = savedOutput;
1516
+ this.indent = savedIndent;
1517
+ return `func(${params}) { ${bodyLines.join(' ')} }`;
1518
+ }
1519
+
1520
+ case 'Ternary': {
1521
+ // Go doesn't have ternary. Use an IIFE.
1522
+ const cond = this.expr(node.condition);
1523
+ const consequent = this.expr(node.consequent);
1524
+ const alternate = this.expr(node.alternate);
1525
+ return `func() interface{} { if ${cond} { return ${consequent} }; return ${alternate} }()`;
1526
+ }
1527
+
1528
+ case 'Pipe':
1529
+ return this.generatePipe(node);
1530
+
1531
+ default:
1532
+ return `nil /* expr:${node.type} */`;
1533
+ }
1534
+ }
1535
+
1536
+ mapIdentifier(name) {
1537
+ const map = {
1538
+ 'null': 'nil',
1539
+ 'undefined': 'nil',
1540
+ 'true': 'true',
1541
+ 'false': 'false',
1542
+ 'this': 'self',
1543
+ 'console': 'fmt',
1544
+ };
1545
+ return map[name] || name;
1546
+ }
1547
+
1548
+ mapBinaryOp(op) {
1549
+ const map = {
1550
+ '===': '==',
1551
+ '!==': '!=',
1552
+ '==': '==',
1553
+ '!=': '!=',
1554
+ '&&': '&&',
1555
+ '||': '||',
1556
+ 'and': '&&',
1557
+ 'or': '||',
1558
+ '>': '>',
1559
+ '<': '<',
1560
+ '>=': '>=',
1561
+ '<=': '<=',
1562
+ '+': '+',
1563
+ '-': '-',
1564
+ '*': '*',
1565
+ '/': '/',
1566
+ '%': '%',
1567
+ };
1568
+ return map[op] || op;
1569
+ }
1570
+
1571
+ mapUnaryOp(op) {
1572
+ if (op === '!' || op === 'not') return '!';
1573
+ return op;
1574
+ }
1575
+
1576
+ mapMemberAccess(node) {
1577
+ const obj = this.expr(node.object);
1578
+ const prop = node.property;
1579
+
1580
+ // Map common JS properties to Go equivalents
1581
+ if (prop === 'length') return `len(${obj})`;
1582
+ if (prop === 'toString') return `fmt.Sprint(${obj})`;
1583
+
1584
+ // Request properties for net/http
1585
+ if ((obj === 'request' || obj === 'req' || obj === 'r') && prop === 'body') return 'r.Body';
1586
+ if ((obj === 'request' || obj === 'req' || obj === 'r') && prop === 'query') return 'r.URL.Query()';
1587
+ if ((obj === 'request' || obj === 'req' || obj === 'r') && prop === 'params') return 'r.URL.Query()';
1588
+ if ((obj === 'request' || obj === 'req' || obj === 'r') && prop === 'headers') return 'r.Header';
1589
+ if ((obj === 'request' || obj === 'req' || obj === 'r') && prop === 'method') return 'r.Method';
1590
+ if ((obj === 'request' || obj === 'req' || obj === 'r') && prop === 'url') return 'r.URL.String()';
1591
+
1592
+ // JSON
1593
+ if (obj === 'JSON' && prop === 'parse') return 'json.Unmarshal';
1594
+ if (obj === 'JSON' && prop === 'stringify') return 'json.Marshal';
1595
+
1596
+ // Math
1597
+ if (obj === 'Math' && prop === 'floor') { this.addImport('math'); return 'math.Floor'; }
1598
+ if (obj === 'Math' && prop === 'ceil') { this.addImport('math'); return 'math.Ceil'; }
1599
+ if (obj === 'Math' && prop === 'round') { this.addImport('math'); return 'math.Round'; }
1600
+ if (obj === 'Math' && prop === 'abs') { this.addImport('math'); return 'math.Abs'; }
1601
+ if (obj === 'Math' && prop === 'min') { this.addImport('math'); return 'math.Min'; }
1602
+ if (obj === 'Math' && prop === 'max') { this.addImport('math'); return 'math.Max'; }
1603
+ if (obj === 'Math' && prop === 'random') { this.addImport('math/rand'); return 'rand.Float64'; }
1604
+ if (obj === 'Math' && prop === 'PI') { this.addImport('math'); return 'math.Pi'; }
1605
+
1606
+ // console methods
1607
+ if (obj === 'console' && prop === 'log') { this.addImport('fmt'); return 'fmt.Println'; }
1608
+ if (obj === 'console' && prop === 'error') { this.addImport('log'); return 'log.Println'; }
1609
+ if (obj === 'console' && prop === 'warn') { this.addImport('log'); return 'log.Println'; }
1610
+
1611
+ // Capitalize first letter for Go exported fields
1612
+ const goProp = prop.charAt(0).toUpperCase() + prop.slice(1);
1613
+ return `${obj}.${goProp}`;
1614
+ }
1615
+
1616
+ generateCall(node) {
1617
+ const args = node.args.map(a => this.expr(a)).join(', ');
1618
+
1619
+ // Map common JS global functions to Go
1620
+ if (node.callee.type === 'Identifier') {
1621
+ const name = node.callee.name;
1622
+
1623
+ if (name === 'parseInt') { this.addImport('strconv'); return `func() int { v, _ := strconv.Atoi(${args}); return v }()`; }
1624
+ if (name === 'parseFloat') { this.addImport('strconv'); return `func() float64 { v, _ := strconv.ParseFloat(${args}, 64); return v }()`; }
1625
+ if (name === 'String') { this.addImport('fmt'); return `fmt.Sprint(${args})`; }
1626
+ if (name === 'Number') { this.addImport('strconv'); return `func() float64 { v, _ := strconv.ParseFloat(fmt.Sprint(${args}), 64); return v }()`; }
1627
+ if (name === 'Boolean') return `(${args}) != nil && (${args}) != false && (${args}) != 0`;
1628
+ }
1629
+
1630
+ // Map common method calls
1631
+ if (node.callee.type === 'MemberAccess') {
1632
+ const obj = this.expr(node.callee.object);
1633
+ const prop = node.callee.property;
1634
+
1635
+ // Array/slice methods
1636
+ if (prop === 'push') return `${obj} = append(${obj}, ${args})`;
1637
+ if (prop === 'join') { this.addImport('strings'); return `strings.Join(${obj}, ${args})`; }
1638
+ if (prop === 'includes') return `/* contains: */ func() bool { for _, v := range ${obj} { if v == ${args} { return true } }; return false }()`;
1639
+ if (prop === 'indexOf') return `/* indexOf: */ func() int { for i, v := range ${obj} { if v == ${args} { return i } }; return -1 }()`;
1640
+ if (prop === 'reverse') { this.addImport('slices'); return `slices.Reverse(${obj})`; }
1641
+ if (prop === 'sort') { this.addImport('sort'); return `sort.Slice(${obj}, func(i, j int) bool { return ${obj}[i] < ${obj}[j] })`; }
1642
+
1643
+ // String methods
1644
+ if (prop === 'split') { this.addImport('strings'); return `strings.Split(${obj}, ${args})`; }
1645
+ if (prop === 'trim') { this.addImport('strings'); return `strings.TrimSpace(${obj})`; }
1646
+ if (prop === 'trimStart') { this.addImport('strings'); return `strings.TrimLeft(${obj}, " ")`; }
1647
+ if (prop === 'trimEnd') { this.addImport('strings'); return `strings.TrimRight(${obj}, " ")`; }
1648
+ if (prop === 'toUpperCase') { this.addImport('strings'); return `strings.ToUpper(${obj})`; }
1649
+ if (prop === 'toLowerCase') { this.addImport('strings'); return `strings.ToLower(${obj})`; }
1650
+ if (prop === 'startsWith') { this.addImport('strings'); return `strings.HasPrefix(${obj}, ${args})`; }
1651
+ if (prop === 'endsWith') { this.addImport('strings'); return `strings.HasSuffix(${obj}, ${args})`; }
1652
+ if (prop === 'replace') { this.addImport('strings'); return `strings.Replace(${obj}, ${args}, 1)`; }
1653
+ if (prop === 'replaceAll') { this.addImport('strings'); return `strings.ReplaceAll(${obj}, ${args})`; }
1654
+ if (prop === 'charAt') return `string(${obj}[${args}])`;
1655
+ if (prop === 'repeat') { this.addImport('strings'); return `strings.Repeat(${obj}, ${args})`; }
1656
+
1657
+ // Object methods
1658
+ if (obj === 'Object' && prop === 'keys') return `/* Object.keys */ func() []string { keys := make([]string, 0); for k := range ${args} { keys = append(keys, k) }; return keys }()`;
1659
+
1660
+ // Promise / concurrency
1661
+ if (obj === 'Promise' && prop === 'all') return `/* Promise.all: use sync.WaitGroup */ ${args}`;
1662
+ if (obj === 'Promise' && prop === 'resolve') return args;
1663
+
1664
+ // Date
1665
+ if (obj === 'Date' && prop === 'now') { this.addImport('time'); return 'time.Now().UnixMilli()'; }
1666
+ }
1667
+
1668
+ const callee = this.expr(node.callee);
1669
+ return `${callee}(${args})`;
1670
+ }
1671
+
1672
+ generateString(strData) {
1673
+ if (!strData || !strData.parts) return '""';
1674
+
1675
+ const hasInterpolation = strData.parts.some(p => p.type === 'expr');
1676
+
1677
+ if (!hasInterpolation) {
1678
+ const raw = strData.parts.map(p => p.value).join('');
1679
+ return JSON.stringify(raw);
1680
+ }
1681
+
1682
+ // Use fmt.Sprintf for interpolation
1683
+ this.addImport('fmt');
1684
+ let format = '';
1685
+ const args = [];
1686
+
1687
+ for (const part of strData.parts) {
1688
+ if (part.type === 'text') {
1689
+ format += part.value.replace(/%/g, '%%').replace(/"/g, '\\"');
1690
+ } else {
1691
+ format += '%v';
1692
+ args.push(part.value);
1693
+ }
1694
+ }
1695
+
1696
+ if (args.length === 0) {
1697
+ return `"${format}"`;
1698
+ }
1699
+
1700
+ return `fmt.Sprintf("${format}", ${args.join(', ')})`;
1701
+ }
1702
+
1703
+ rawString(strData) {
1704
+ if (!strData) return '';
1705
+ if (strData.raw !== null && strData.raw !== undefined) return strData.raw;
1706
+ if (strData.parts) return strData.parts.map(p => p.value).join('');
1707
+ return '';
1708
+ }
1709
+
1710
+ rawPageString(strData) {
1711
+ if (!strData) return '';
1712
+ if (strData.raw !== null && strData.raw !== undefined) return strData.raw;
1713
+ if (strData.parts) return strData.parts.map(p => p.value).join('');
1714
+ return '';
1715
+ }
1716
+
1717
+ goStringValue(strData) {
1718
+ if (!strData || !strData.parts) return '""';
1719
+ if (strData.raw !== null && strData.raw !== undefined) {
1720
+ return JSON.stringify(strData.raw);
1721
+ }
1722
+ return this.generateString(strData);
1723
+ }
1724
+
1725
+ rawExprValue(node) {
1726
+ if (!node) return '';
1727
+ if (node.type === 'Number') return node.value;
1728
+ if (node.type === 'String') return this.rawString(node.value);
1729
+ return this.expr(node);
1730
+ }
1731
+
1732
+ generatePipe(node) {
1733
+ const steps = [];
1734
+ let current = node;
1735
+ while (current.type === 'Pipe') {
1736
+ steps.unshift(current.right);
1737
+ current = current.left;
1738
+ }
1739
+ steps.unshift(current);
1740
+
1741
+ let result = this.expr(steps[0]);
1742
+ for (let i = 1; i < steps.length; i++) {
1743
+ const step = steps[i];
1744
+ if (step.type === 'Call') {
1745
+ const callee = this.expr(step.callee);
1746
+ const args = step.args.map(a => this.expr(a)).join(', ');
1747
+ result = `${callee}(${result}${args ? ', ' + args : ''})`;
1748
+ } else if (step.type === 'Identifier') {
1749
+ result = `${step.name}(${result})`;
1750
+ } else {
1751
+ result = `(${this.expr(step)})(${result})`;
1752
+ }
1753
+ }
1754
+ return result;
1755
+ }
1756
+
1757
+ bodyUsesAwait(body) {
1758
+ for (const stmt of body) {
1759
+ if (this.stmtUsesAwait(stmt)) return true;
1760
+ }
1761
+ return false;
1762
+ }
1763
+
1764
+ stmtUsesAwait(node) {
1765
+ if (!node) return false;
1766
+ const json = JSON.stringify(node);
1767
+ return json.includes('"Await"') || json.includes('"AwaitAll"');
1768
+ }
1769
+
1770
+ statementToString(stmt) {
1771
+ const saved = this.output;
1772
+ const savedIndent = this.indent;
1773
+ this.output = [];
1774
+ this.indent = 0;
1775
+ this.visitStatement(stmt);
1776
+ const result = this.output.join('\n');
1777
+ this.output = saved;
1778
+ this.indent = savedIndent;
1779
+ return result;
1780
+ }
1781
+ }