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,1576 @@
1
+ export class PhpGenerator {
2
+ constructor(options = {}) {
3
+ this.indent = 0;
4
+ this.output = [];
5
+ this.models = new Map();
6
+ this.schemas = new Map();
7
+ this.usesLaravel = false;
8
+ this.authSecret = null;
9
+ this.dbVar = null;
10
+ this.sourceMap = [];
11
+ this.currentSourceLine = 0;
12
+ this.sourceFile = options.sourceFile || null;
13
+ }
14
+
15
+ generate(ast) {
16
+ this.visitProgram(ast);
17
+
18
+ // Prepend <?php
19
+ this.output.unshift('<?php');
20
+ this.output.splice(1, 0, '');
21
+
22
+ return this.output.join('\n');
23
+ }
24
+
25
+ emit(line) {
26
+ this.output.push(' '.repeat(this.indent) + line);
27
+ this.sourceMap.push(this.currentSourceLine);
28
+ }
29
+
30
+ emitRaw(line) {
31
+ this.output.push(line);
32
+ this.sourceMap.push(this.currentSourceLine);
33
+ }
34
+
35
+ visitProgram(node) {
36
+ for (const stmt of node.body) {
37
+ this.visitStatement(stmt);
38
+ }
39
+ }
40
+
41
+ visitStatement(node) {
42
+ if (node._line) this.currentSourceLine = node._line;
43
+ switch (node.type) {
44
+ case 'Use': return this.visitUse(node);
45
+ case 'UseDestructured': return this.visitUseDestructured(node);
46
+ case 'Function': return this.visitFunction(node);
47
+ case 'Return': return this.visitReturn(node);
48
+ case 'ReturnStatus': return this.visitReturnStatus(node);
49
+ case 'ReturnMethod': return this.visitReturnMethod(node);
50
+ case 'TypedVar': return this.visitTypedVar(node);
51
+ case 'If': return this.visitIf(node);
52
+ case 'Each': return this.visitEach(node);
53
+ case 'For': return this.visitFor(node);
54
+ case 'While': return this.visitWhile(node);
55
+ case 'Match': return this.visitMatch(node);
56
+ case 'Try': return this.visitTry(node);
57
+ case 'Server': return this.visitServer(node);
58
+ case 'Bot': return this.visitBot(node);
59
+ case 'Model': return this.visitModel(node);
60
+ case 'On': return this.visitOn(node);
61
+ case 'Log': return this.visitLog(node);
62
+ case 'Throw': return this.visitThrow(node);
63
+ case 'Break': this.emit('break;'); return;
64
+ case 'Continue': this.emit('continue;'); return;
65
+ case 'Assignment': return this.visitAssignment(node);
66
+ case 'CompoundAssign': return this.visitCompoundAssign(node);
67
+ case 'ExprStatement': this.emit(this.expr(node.expression) + ';'); return;
68
+ case 'DbConnect': return this.visitDbConnect(node);
69
+ case 'DbDir': return this.visitDbDir(node);
70
+ case 'DbSql': return this.visitDbSql(node);
71
+ case 'AwaitAll': return this.visitAwaitAllStatement(node);
72
+ case 'SchemaDecl': return this.visitSchema(node);
73
+ case 'AuthDecl': return this.visitAuth('$app', node);
74
+ case 'CorsDecl': return this.visitCors('$app', node);
75
+ case 'EnvDecl': return this.visitEnv(node);
76
+ case 'TestDecl': return this.visitTest(node);
77
+ case 'AssertStmt': return this.visitAssert(node);
78
+ case 'ErrorHandler': return this.visitErrorHandler(node);
79
+ case 'CrudDecl': return this.visitCrud(node);
80
+ case 'StaticDecl': return this.visitStatic(node);
81
+ case 'LimitDecl': return this.visitLimit(node);
82
+ case 'WsDecl': return this.visitWs(node);
83
+ case 'SessionDecl': return this.visitSession(node);
84
+ case 'CacheDecl': return this.visitCache(node);
85
+ case 'EveryDecl': return this.visitEvery(node);
86
+ case 'PromptDecl': return this.visitPrompt(node);
87
+ case 'Page': return this.visitPage(node);
88
+ case 'CliApp': return this.visitCli(node);
89
+ case 'MailConfig': return this.visitMail(node);
90
+ case 'DesktopApp': return this.visitDesktop(node);
91
+ case 'Screen': return this.visitScreen(node);
92
+ case 'OauthDecl': return this.visitOauth(node);
93
+ case 'PayDecl': return this.visitPay(node);
94
+ case 'StorageDecl': return this.visitStorage(node);
95
+ case 'PdfDecl': return this.visitPdf(node);
96
+ case 'I18nDecl': return this.visitI18n(node);
97
+ case 'PushDecl': return this.visitPush(node);
98
+ case 'SearchDecl': return this.visitSearch(node);
99
+ case 'ImageDecl': return this.visitImage(node);
100
+ case 'CsvDecl': return this.visitCsv(node);
101
+ case 'LoggingDecl': return this.visitLogging(node);
102
+ case 'MigrateDecl': return this.visitMigrate(node);
103
+ case 'GrpcDecl': return this.visitGrpc(node);
104
+ case 'WebrtcDecl': return this.visitWebrtc(node);
105
+ case 'BlockchainDecl': return this.visitBlockchain(node);
106
+ default:
107
+ this.emit(`/* unknown: ${node.type} */`);
108
+ }
109
+ }
110
+
111
+ // ===== Imports =====
112
+
113
+ visitUse(node) {
114
+ const raw = node.source ? this.rawString(node.source) : node.name;
115
+ const alias = node.alias || node.name;
116
+ const clean = raw.replace(/^['"]|['"]$/g, '');
117
+ this.emit(`// use ${clean}`);
118
+ this.emit(`require_once '${clean}.php';`);
119
+ }
120
+
121
+ visitUseDestructured(node) {
122
+ const raw = this.rawString(node.source);
123
+ const clean = raw.replace(/^['"]|['"]$/g, '');
124
+ this.emit(`// use { ${node.names.map(n => n.name).join(', ')} } from ${clean}`);
125
+ this.emit(`require_once '${clean}.php';`);
126
+ }
127
+
128
+ // ===== Functions =====
129
+
130
+ visitFunction(node) {
131
+ const params = node.params.map(p => {
132
+ let s = '';
133
+ if (p.spread) s += '...';
134
+ s += '$' + p.name;
135
+ if (p.defaultValue) s += ` = ${this.expr(p.defaultValue)}`;
136
+ return s;
137
+ }).join(', ');
138
+
139
+ const retType = node.returnType ? `: ${this.mapType(node.returnType)}` : '';
140
+
141
+ this.emit(`function ${node.name}(${params})${retType} {`);
142
+ this.indent++;
143
+ if (node.body.length === 0) {
144
+ this.emit('// empty');
145
+ } else {
146
+ for (const stmt of node.body) {
147
+ this.visitStatement(stmt);
148
+ }
149
+ }
150
+ this.indent--;
151
+ this.emit('}');
152
+ this.emitRaw('');
153
+ }
154
+
155
+ visitReturn(node) {
156
+ if (node.value === null) {
157
+ this.emit('return;');
158
+ } else {
159
+ this.emit(`return ${this.expr(node.value)};`);
160
+ }
161
+ }
162
+
163
+ visitReturnStatus(node) {
164
+ this.emit(`http_response_code(${this.expr(node.statusCode)});`);
165
+ this.emit(`echo json_encode(${this.expr(node.body)});`);
166
+ this.emit('return;');
167
+ }
168
+
169
+ visitReturnMethod(node) {
170
+ const val = this.expr(node.value);
171
+ switch (node.method) {
172
+ case 'redirect':
173
+ this.emit(`header("Location: " . ${val});`);
174
+ this.emit('exit;');
175
+ break;
176
+ case 'html':
177
+ this.emit(`header("Content-Type: text/html");`);
178
+ this.emit(`echo ${val};`);
179
+ break;
180
+ case 'text':
181
+ this.emit(`header("Content-Type: text/plain");`);
182
+ this.emit(`echo ${val};`);
183
+ break;
184
+ case 'file':
185
+ this.emit(`readfile(${val});`);
186
+ break;
187
+ default:
188
+ this.emit(`echo ${val};`);
189
+ }
190
+ }
191
+
192
+ // ===== Variables =====
193
+
194
+ visitTypedVar(node) {
195
+ this.emit(`$${node.name} = ${this.expr(node.value)};`);
196
+ }
197
+
198
+ visitAssignment(node) {
199
+ const value = this.expr(node.value);
200
+
201
+ if (node.target.type === 'Array') {
202
+ const names = node.target.elements.map(e => this.expr(e)).join(', ');
203
+ this.emit(`[${names}] = ${value};`);
204
+ } else if (node.target.type === 'Object') {
205
+ const tempVar = '$__d';
206
+ this.emit(`${tempVar} = ${value};`);
207
+ for (const p of node.target.properties) {
208
+ const key = p.type === 'shorthand' ? p.key : p.key;
209
+ const varName = p.type === 'shorthand' ? '$' + p.key : this.expr(p.value);
210
+ this.emit(`${varName} = ${tempVar}[${JSON.stringify(key)}];`);
211
+ }
212
+ } else {
213
+ this.emit(`${this.expr(node.target)} = ${value};`);
214
+ }
215
+ }
216
+
217
+ visitCompoundAssign(node) {
218
+ this.emit(`${this.expr(node.target)} ${node.op} ${this.expr(node.value)};`);
219
+ }
220
+
221
+ // ===== Control flow =====
222
+
223
+ visitIf(node) {
224
+ this.emit(`if (${this.expr(node.condition)}) {`);
225
+ this.indent++;
226
+ for (const stmt of node.body) this.visitStatement(stmt);
227
+ this.indent--;
228
+
229
+ for (const elif of node.elifs) {
230
+ this.emit(`} elseif (${this.expr(elif.condition)}) {`);
231
+ this.indent++;
232
+ for (const stmt of elif.body) this.visitStatement(stmt);
233
+ this.indent--;
234
+ }
235
+
236
+ if (node.elseBody) {
237
+ this.emit('} else {');
238
+ this.indent++;
239
+ for (const stmt of node.elseBody) this.visitStatement(stmt);
240
+ this.indent--;
241
+ }
242
+ this.emit('}');
243
+ }
244
+
245
+ visitEach(node) {
246
+ const collection = this.expr(node.collection);
247
+ if (node.key) {
248
+ this.emit(`foreach (${collection} as $${node.key} => $${node.value}) {`);
249
+ } else {
250
+ this.emit(`foreach (${collection} as $${node.value}) {`);
251
+ }
252
+ this.indent++;
253
+ for (const stmt of node.body) this.visitStatement(stmt);
254
+ this.indent--;
255
+ this.emit('}');
256
+ }
257
+
258
+ visitFor(node) {
259
+ const varName = node.varName;
260
+ const start = this.expr(node.start);
261
+ const end = this.expr(node.end);
262
+ this.emit(`for ($${varName} = ${start}; $${varName} < ${end}; $${varName}++) {`);
263
+ this.indent++;
264
+ for (const stmt of node.body) this.visitStatement(stmt);
265
+ this.indent--;
266
+ this.emit('}');
267
+ }
268
+
269
+ visitWhile(node) {
270
+ this.emit(`while (${this.expr(node.condition)}) {`);
271
+ this.indent++;
272
+ for (const stmt of node.body) this.visitStatement(stmt);
273
+ this.indent--;
274
+ this.emit('}');
275
+ }
276
+
277
+ visitMatch(node) {
278
+ this.emit(`$__match_result = match(${this.expr(node.value)}) {`);
279
+ this.indent++;
280
+ for (const c of node.cases) {
281
+ if (c.pattern.type === 'DefaultPattern') {
282
+ this.emit(`default => (function() {`);
283
+ } else {
284
+ this.emit(`${this.expr(c.pattern)} => (function() {`);
285
+ }
286
+ this.indent++;
287
+ for (const stmt of c.body) this.visitStatement(stmt);
288
+ this.indent--;
289
+ this.emit('})(),');
290
+ }
291
+ this.indent--;
292
+ this.emit('};');
293
+ }
294
+
295
+ visitTry(node) {
296
+ this.emit('try {');
297
+ this.indent++;
298
+ for (const stmt of node.body) this.visitStatement(stmt);
299
+ this.indent--;
300
+
301
+ if (node.catchBody) {
302
+ const catchParam = node.catchVar || 'e';
303
+ this.emit(`} catch (\\Exception $${catchParam}) {`);
304
+ this.indent++;
305
+ for (const stmt of node.catchBody) this.visitStatement(stmt);
306
+ this.indent--;
307
+ }
308
+
309
+ if (node.ensureBody) {
310
+ this.emit('} finally {');
311
+ this.indent++;
312
+ for (const stmt of node.ensureBody) this.visitStatement(stmt);
313
+ this.indent--;
314
+ }
315
+ this.emit('}');
316
+ }
317
+
318
+ // ===== Server (Laravel-style) =====
319
+
320
+ visitServer(node) {
321
+ this.usesLaravel = true;
322
+ this.emit(`// Server: ${node.name}`);
323
+ this.emit(`// Laravel-style route definitions`);
324
+ this.emitRaw('');
325
+
326
+ this.emit(`use Illuminate\\Support\\Facades\\Route;`);
327
+ this.emit(`use Illuminate\\Http\\Request;`);
328
+ this.emitRaw('');
329
+
330
+ for (const child of node.routes) {
331
+ if (child.type === 'Route') {
332
+ this.visitRoute(child);
333
+ } else if (child.type === 'AuthDecl') {
334
+ this.visitAuth('Route', child);
335
+ } else if (child.type === 'CorsDecl') {
336
+ this.visitCors('Route', child);
337
+ } else if (child.type === 'CrudDecl') {
338
+ this.visitCrud(child);
339
+ } else if (child.type === 'GroupDecl') {
340
+ this.visitGroup(child);
341
+ } else if (child.type === 'ErrorHandler') {
342
+ this.visitErrorHandler(child);
343
+ } else if (child.type === 'SchemaDecl') {
344
+ this.visitSchema(child);
345
+ } else if (child.type === 'StaticDecl') {
346
+ this.visitStatic(child);
347
+ } else if (child.type === 'SessionDecl') {
348
+ this.visitSession(child);
349
+ } else if (child.type === 'GraphqlDecl') {
350
+ this.visitGraphql(child);
351
+ } else {
352
+ this.visitStatement(child);
353
+ }
354
+ }
355
+
356
+ const port = node.port ? this.expr(node.port) : '8000';
357
+ this.emitRaw('');
358
+ this.emit(`// To run: php artisan serve --port=${port}`);
359
+ }
360
+
361
+ visitRoute(route) {
362
+ const method = route.method === 'del' ? 'delete' : route.method;
363
+ const path = this.phpStringValue(route.path);
364
+
365
+ this.emit(`Route::${method}(${path}, function (Request $request) {`);
366
+ this.indent++;
367
+
368
+ for (let i = 0; i < route.body.length; i++) {
369
+ const stmt = route.body[i];
370
+ if (stmt.type === 'Return' && stmt.value !== null) {
371
+ this.emit(`return response()->json(${this.expr(stmt.value)});`);
372
+ } else {
373
+ this.visitStatement(stmt);
374
+ }
375
+ }
376
+
377
+ this.indent--;
378
+ this.emit('});');
379
+ this.emitRaw('');
380
+ }
381
+
382
+ visitGroup(node) {
383
+ const prefix = this.phpStringValue(node.prefix);
384
+ this.emit(`Route::prefix(${prefix})->group(function () {`);
385
+ this.indent++;
386
+
387
+ for (const child of node.routes) {
388
+ if (child.type === 'Route') {
389
+ this.visitRoute(child);
390
+ } else if (child.type === 'CrudDecl') {
391
+ this.visitCrud(child);
392
+ } else {
393
+ this.visitStatement(child);
394
+ }
395
+ }
396
+
397
+ this.indent--;
398
+ this.emit('});');
399
+ this.emitRaw('');
400
+ }
401
+
402
+ // ===== Model (class) =====
403
+
404
+ visitModel(node) {
405
+ this.models.set(node.name, node);
406
+
407
+ const ext = node.parent ? ` extends ${node.parent}` : '';
408
+ this.emit(`class ${node.name}${ext} {`);
409
+ this.indent++;
410
+
411
+ // Properties
412
+ for (const f of node.fields) {
413
+ const phpType = this.mapType(f.type);
414
+ this.emit(`public ${phpType} $${f.name};`);
415
+ }
416
+
417
+ if (node.fields.length > 0) {
418
+ this.emitRaw('');
419
+ }
420
+
421
+ // Constructor
422
+ if (node.fields.length > 0 || node.parent) {
423
+ const parentModel = node.parent ? this.models.get(node.parent) : null;
424
+ const parentFields = parentModel ? parentModel.fields : [];
425
+ const allFields = [...parentFields, ...node.fields];
426
+
427
+ const constructorParams = allFields.map(f => {
428
+ const phpType = this.mapType(f.type);
429
+ if (f.defaultValue) return `${phpType} $${f.name} = ${this.expr(f.defaultValue)}`;
430
+ return `${phpType} $${f.name}`;
431
+ }).join(', ');
432
+
433
+ this.emit(`public function __construct(${constructorParams}) {`);
434
+ this.indent++;
435
+ if (node.parent) {
436
+ const superArgs = parentFields.map(f => '$' + f.name).join(', ');
437
+ this.emit(`parent::__construct(${superArgs});`);
438
+ }
439
+ for (const f of node.fields) {
440
+ this.emit(`$this->${f.name} = $${f.name};`);
441
+ }
442
+ this.indent--;
443
+ this.emit('}');
444
+ this.emitRaw('');
445
+ }
446
+
447
+ // Methods
448
+ for (const method of node.methods) {
449
+ const params = method.params.map(p => {
450
+ let s = '';
451
+ if (p.spread) s += '...';
452
+ s += '$' + p.name;
453
+ if (p.defaultValue) s += ` = ${this.expr(p.defaultValue)}`;
454
+ return s;
455
+ }).join(', ');
456
+
457
+ this.emit(`public function ${method.name}(${params}) {`);
458
+ this.indent++;
459
+ if (method.body.length === 0) {
460
+ this.emit('// empty');
461
+ } else {
462
+ for (const stmt of method.body) {
463
+ this.visitStatement(stmt);
464
+ }
465
+ }
466
+ this.indent--;
467
+ this.emit('}');
468
+ this.emitRaw('');
469
+ }
470
+
471
+ this.indent--;
472
+ this.emit('}');
473
+ this.emitRaw('');
474
+ }
475
+
476
+ // ===== Events =====
477
+
478
+ visitOn(node) {
479
+ const event = this.expr(node.event);
480
+ this.emit(`// on ${event}`);
481
+ this.emit(`// Event handler`);
482
+ this.emit(`(function () {`);
483
+ this.indent++;
484
+ for (const stmt of node.body) this.visitStatement(stmt);
485
+ this.indent--;
486
+ this.emit('})();');
487
+ }
488
+
489
+ // ===== Logging =====
490
+
491
+ visitLog(node) {
492
+ const args = node.args.map(a => this.expr(a));
493
+ if (args.length === 1) {
494
+ if (node.level === 'error') {
495
+ this.emit(`error_log(${args[0]});`);
496
+ } else {
497
+ this.emit(`echo ${args[0]} . "\\n";`);
498
+ }
499
+ } else {
500
+ if (node.level === 'error') {
501
+ this.emit(`error_log(implode(' ', [${args.join(', ')}]));`);
502
+ } else {
503
+ this.emit(`echo implode(' ', [${args.join(', ')}]) . "\\n";`);
504
+ }
505
+ }
506
+ }
507
+
508
+ visitThrow(node) {
509
+ const value = this.expr(node.value);
510
+ if (node.value.type === 'String') {
511
+ this.emit(`throw new \\Exception(${value});`);
512
+ } else {
513
+ this.emit(`throw ${value};`);
514
+ }
515
+ }
516
+
517
+ // ===== Database =====
518
+
519
+ visitDbConnect(node) {
520
+ this.dbVar = '$db';
521
+ this.emit(`$db = new PDO(${this.expr(node.connectionString)});`);
522
+ this.emit(`$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);`);
523
+ }
524
+
525
+ visitDbDir(node) {
526
+ const raw = node.path.raw || node.path.parts?.map(p => p.value).join('') || 'data/';
527
+ this.emit(`// db dir: ${raw}`);
528
+ }
529
+
530
+ visitDbSql(node) {
531
+ const driverRaw = node.driver.raw || node.driver.parts?.map(p => p.value).join('') || 'sqlite';
532
+ this.dbVar = '$__db';
533
+
534
+ if (driverRaw === 'sqlite') {
535
+ const conn = node.connection ? this.phpStringValue(node.connection) : '"data.db"';
536
+ this.emit(`$__db = new PDO("sqlite:" . ${conn});`);
537
+ } else if (driverRaw === 'postgres' || driverRaw === 'pg') {
538
+ const conn = node.connection ? this.phpStringValue(node.connection) : 'getenv("DATABASE_URL")';
539
+ this.emit(`$__db = new PDO(${conn});`);
540
+ } else {
541
+ const conn = node.connection ? this.phpStringValue(node.connection) : '"localhost"';
542
+ this.emit(`$__db = new PDO(${conn});`);
543
+ }
544
+ this.emit(`$__db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);`);
545
+ this.emitRaw('');
546
+ }
547
+
548
+ visitAwaitAllStatement(node) {
549
+ this.emit(`// await.all - PHP is synchronous; executing sequentially`);
550
+ for (const e of node.expressions) {
551
+ this.emit(`${this.expr(e)};`);
552
+ }
553
+ }
554
+
555
+ // ===== Schema =====
556
+
557
+ visitSchema(node) {
558
+ this.emit(`// Schema: ${node.name}`);
559
+ this.emit(`class ${node.name} {`);
560
+ this.indent++;
561
+
562
+ for (const field of node.fields) {
563
+ const phpType = this.mapSchemaType(field.type);
564
+ const defaultVal = this.schemaDefault(field);
565
+ if (defaultVal !== null) {
566
+ this.emit(`public ${phpType} $${field.name} = ${defaultVal};`);
567
+ } else {
568
+ this.emit(`public ${phpType} $${field.name};`);
569
+ }
570
+ }
571
+
572
+ this.indent--;
573
+ this.emit('}');
574
+
575
+ this.schemas.set(node.name, node);
576
+ this.emitRaw('');
577
+ }
578
+
579
+ mapSchemaType(type) {
580
+ const map = {
581
+ 'str': 'string', 'string': 'string',
582
+ 'int': 'int', 'integer': 'int',
583
+ 'num': 'float', 'number': 'float',
584
+ 'bool': 'bool', 'boolean': 'bool',
585
+ 'auto': 'int', 'timestamp': 'string',
586
+ 'enum': 'string',
587
+ };
588
+ return map[type] || 'mixed';
589
+ }
590
+
591
+ schemaDefault(fieldNode) {
592
+ for (const mod of fieldNode.modifiers) {
593
+ if (mod.name === 'default') {
594
+ return this.expr(mod.args[0]);
595
+ }
596
+ }
597
+ for (const mod of fieldNode.modifiers) {
598
+ if (mod.name === 'optional') return 'null';
599
+ }
600
+ if (fieldNode.type === 'auto') return 'null';
601
+ if (fieldNode.type === 'timestamp') return 'null';
602
+ return null;
603
+ }
604
+
605
+ // ===== Auth =====
606
+
607
+ visitAuth(appName, node) {
608
+ const secret = this.expr(node.secret);
609
+ this.authSecret = secret;
610
+ this.emit(`$__authSecret = ${secret};`);
611
+ this.emit(`// JWT authentication middleware`);
612
+ this.emit(`function requireAuth() {`);
613
+ this.indent++;
614
+ this.emit(`global $__authSecret;`);
615
+ this.emit(`$token = $_SERVER['HTTP_AUTHORIZATION'] ?? '';`);
616
+ this.emit(`$token = str_replace('Bearer ', '', $token);`);
617
+ this.emit(`if (!$token) {`);
618
+ this.indent++;
619
+ this.emit(`http_response_code(401);`);
620
+ this.emit(`echo json_encode(['error' => 'No token provided']);`);
621
+ this.emit(`exit;`);
622
+ this.indent--;
623
+ this.emit('}');
624
+ this.emit('try {');
625
+ this.indent++;
626
+ this.emit(`$decoded = json_decode(base64_decode(explode('.', $token)[1]), true);`);
627
+ this.emit(`return $decoded;`);
628
+ this.indent--;
629
+ this.emit('} catch (\\Exception $e) {');
630
+ this.indent++;
631
+ this.emit(`http_response_code(401);`);
632
+ this.emit(`echo json_encode(['error' => 'Invalid token']);`);
633
+ this.emit(`exit;`);
634
+ this.indent--;
635
+ this.emit('}');
636
+ this.indent--;
637
+ this.emit('}');
638
+ this.emitRaw('');
639
+ }
640
+
641
+ // ===== CORS =====
642
+
643
+ visitCors(appName, node) {
644
+ const origins = this.expr(node.origins);
645
+ this.emit(`// CORS`);
646
+ this.emit(`header("Access-Control-Allow-Origin: " . ${origins});`);
647
+ this.emit(`header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");`);
648
+ this.emit(`header("Access-Control-Allow-Headers: Content-Type, Authorization");`);
649
+ this.emitRaw('');
650
+ }
651
+
652
+ // ===== Env =====
653
+
654
+ visitEnv(node) {
655
+ this.emitRaw('');
656
+ for (const v of node.vars) {
657
+ let defaultVal = null;
658
+ for (const mod of v.modifiers) {
659
+ if (mod.name === 'default') {
660
+ defaultVal = this.expr(mod.args[0]);
661
+ }
662
+ }
663
+
664
+ if (defaultVal !== null) {
665
+ this.emit(`$${v.name} = getenv(${JSON.stringify(v.name)}) ?: ${defaultVal};`);
666
+ } else {
667
+ this.emit(`$${v.name} = getenv(${JSON.stringify(v.name)});`);
668
+ }
669
+
670
+ if (v.type === 'int') {
671
+ this.emit(`$${v.name} = (int)$${v.name};`);
672
+ } else if (v.type === 'num') {
673
+ this.emit(`$${v.name} = (float)$${v.name};`);
674
+ } else if (v.type === 'bool') {
675
+ this.emit(`$${v.name} = filter_var($${v.name}, FILTER_VALIDATE_BOOLEAN);`);
676
+ }
677
+ }
678
+ this.emitRaw('');
679
+ }
680
+
681
+ // ===== Tests =====
682
+
683
+ visitTest(node) {
684
+ const name = this.rawString(node.name);
685
+ const funcName = `test_${name.replace(/[^a-zA-Z0-9]/g, '_').replace(/_+/g, '_')}`;
686
+
687
+ this.emit(`// Test: ${name}`);
688
+ this.emit(`function ${funcName}() {`);
689
+ this.indent++;
690
+ if (node.body.length === 0) {
691
+ this.emit('// empty test');
692
+ } else {
693
+ for (const stmt of node.body) this.visitStatement(stmt);
694
+ }
695
+ this.indent--;
696
+ this.emit('}');
697
+ this.emitRaw('');
698
+ }
699
+
700
+ visitAssert(node) {
701
+ const exprNode = node.expr;
702
+ if (exprNode.type === 'Binary' && (exprNode.op === '===' || exprNode.op === '==')) {
703
+ this.emit(`assert(${this.expr(exprNode.left)} === ${this.expr(exprNode.right)}, "Assertion failed");`);
704
+ } else if (exprNode.type === 'Binary' && (exprNode.op === '!==' || exprNode.op === '!=')) {
705
+ this.emit(`assert(${this.expr(exprNode.left)} !== ${this.expr(exprNode.right)}, "Assertion failed");`);
706
+ } else {
707
+ this.emit(`assert(${this.expr(exprNode)}, "Assertion failed");`);
708
+ }
709
+ }
710
+
711
+ // ===== CRUD =====
712
+
713
+ visitCrud(node) {
714
+ const path = this.rawString(node.path);
715
+ const schema = node.schemaName;
716
+ this.emit(`// CRUD for ${schema} at ${path}`);
717
+ this.emit(`Route::get('${path}', function () { return response()->json([]); });`);
718
+ this.emit(`Route::get('${path}/{id}', function ($id) { return response()->json(['id' => $id]); });`);
719
+ this.emit(`Route::post('${path}', function (Request $request) { return response()->json($request->all(), 201); });`);
720
+ this.emit(`Route::put('${path}/{id}', function (Request $request, $id) { return response()->json(array_merge(['id' => $id], $request->all())); });`);
721
+ this.emit(`Route::delete('${path}/{id}', function ($id) { return response()->json(['deleted' => true]); });`);
722
+ this.emitRaw('');
723
+ }
724
+
725
+ // ===== Static =====
726
+
727
+ visitStatic(node) {
728
+ let raw = node.path.raw || node.path.parts?.map(p => p.value).join('') || 'public';
729
+ this.emit(`// Static files from: ${raw}`);
730
+ }
731
+
732
+ // ===== Rate limit =====
733
+
734
+ visitLimit(node) {
735
+ const path = this.rawString(node.path);
736
+ const max = this.expr(node.max);
737
+ const window = this.expr(node.window);
738
+ this.emit(`// Rate limit: ${max} requests per ${window} on ${path}`);
739
+ }
740
+
741
+ // ===== WebSocket =====
742
+
743
+ visitWs(node) {
744
+ this.emit(`// WebSocket - use Ratchet or Laravel WebSockets`);
745
+ this.emit(`// ws path: ${this.phpStringValue(node.path)}`);
746
+ for (const evt of (node.events || [])) {
747
+ const evtName = evt.name.raw || evt.name.parts?.map(p => p.value).join('');
748
+ this.emit(`// on ${evtName}`);
749
+ }
750
+ this.emitRaw('');
751
+ }
752
+
753
+ // ===== Session =====
754
+
755
+ visitSession(node) {
756
+ this.emit(`session_start();`);
757
+ this.emitRaw('');
758
+ }
759
+
760
+ // ===== Cache =====
761
+
762
+ visitCache(node) {
763
+ const path = this.rawString(node.path);
764
+ this.emit(`// Cache: ${path}`);
765
+ }
766
+
767
+ // ===== Error handler =====
768
+
769
+ visitErrorHandler(node) {
770
+ this.emit(`set_exception_handler(function ($exception) {`);
771
+ this.indent++;
772
+ this.emit(`http_response_code(500);`);
773
+ this.emit(`echo json_encode(['error' => $exception->getMessage()]);`);
774
+ this.indent--;
775
+ this.emit('});');
776
+ this.emitRaw('');
777
+ }
778
+
779
+ // ===== Bot =====
780
+
781
+ visitBot(node) {
782
+ const tokenExpr = node.token ? this.expr(node.token) : 'getenv("BOT_TOKEN")';
783
+ this.emit(`// Bot: ${node.name}`);
784
+ this.emit(`$${node.name}_token = ${tokenExpr};`);
785
+ this.emit(`// Bot implementation requires a PHP bot library`);
786
+ this.emitRaw('');
787
+ }
788
+
789
+ // ===== Every (cron) =====
790
+
791
+ visitEvery(node) {
792
+ const interval = this.expr(node.interval);
793
+ this.emit(`// Cron job: every ${interval}`);
794
+ this.emit(`// Schedule via: * * * * * php artisan schedule:run`);
795
+ this.emit(`(function () {`);
796
+ this.indent++;
797
+ for (const stmt of node.body) this.visitStatement(stmt);
798
+ this.indent--;
799
+ this.emit('})();');
800
+ this.emitRaw('');
801
+ }
802
+
803
+ // ===== Prompt =====
804
+
805
+ visitPrompt(node) {
806
+ const lines = node.lines.map(l => this.generateString(l));
807
+ const template = lines.length === 1 ? lines[0] : `implode("\\n", [${lines.join(', ')}])`;
808
+ const defaults = node.defaults ? this.expr(node.defaults) : '[]';
809
+
810
+ this.emit(`function ${node.name}($vars = []) {`);
811
+ this.indent++;
812
+ this.emit(`$defaults = ${defaults};`);
813
+ this.emit(`$merged = array_merge($defaults, $vars);`);
814
+ this.emit(`$template = ${template};`);
815
+ this.emit(`foreach ($merged as $k => $v) {`);
816
+ this.indent++;
817
+ this.emit(`$template = str_replace('{' . $k . '}', (string)$v, $template);`);
818
+ this.indent--;
819
+ this.emit('}');
820
+ this.emit('return $template;');
821
+ this.indent--;
822
+ this.emit('}');
823
+ this.emitRaw('');
824
+ }
825
+
826
+ // ===== Page =====
827
+
828
+ visitPage(node) {
829
+ const filename = this.rawString(node.filename);
830
+ const headParts = [];
831
+ const bodyParts = [];
832
+ let pageTitle = 'NAIDE Page';
833
+
834
+ for (const el of node.elements) {
835
+ this.classifyPageElement(el, headParts, bodyParts, (t) => { pageTitle = t; });
836
+ }
837
+
838
+ this.emit(`$__html = '<!DOCTYPE html>`);
839
+ this.emit(`<html lang="en">`);
840
+ this.emit(`<head>`);
841
+ this.emit(`<meta charset="UTF-8">`);
842
+ this.emit(`<meta name="viewport" content="width=device-width, initial-scale=1.0">`);
843
+ this.emit(`<title>${pageTitle}</title>`);
844
+ for (const h of headParts) this.emit(h);
845
+ this.emit(`</head>`);
846
+ this.emit(`<body>`);
847
+ for (const b of bodyParts) this.emit(b);
848
+ this.emit(`</body>`);
849
+ this.emit(`</html>';`);
850
+ this.emit(`file_put_contents(${JSON.stringify(filename)}, $__html);`);
851
+ this.emitRaw('');
852
+ }
853
+
854
+ classifyPageElement(el, head, body, setTitle) {
855
+ const tag = el.tag;
856
+ const arg0 = el.args[0] ? this.rawString(el.args[0]) : '';
857
+ const arg1 = el.args[1] ? this.rawString(el.args[1]) : '';
858
+ if (tag === 'title') { setTitle(arg0); return; }
859
+ if (tag === 'style' || tag === 'link') { head.push(`<link rel="stylesheet" href="${arg0}">`); return; }
860
+ if (tag === 'meta') { head.push(`<meta name="${arg0}" content="${arg1}">`); return; }
861
+ if (tag === 'script') { body.push(`<script src="${arg0}"></script>`); return; }
862
+ if (tag === 'img') { body.push(`<img src="${arg0}" alt="${arg1}">`); return; }
863
+ if (tag === 'a') { body.push(`<a href="${arg0}">${arg1}</a>`); return; }
864
+ if (tag === 'input') { body.push(`<input type="${arg0}" name="${arg1}">`); return; }
865
+ if (el.children && el.children.length > 0) {
866
+ const cls = arg0 ? ` class="${arg0}"` : '';
867
+ body.push(`<${tag}${cls}>`);
868
+ for (const child of el.children) this.classifyPageElement(child, head, body, setTitle);
869
+ body.push(`</${tag}>`);
870
+ } else {
871
+ body.push(`<${tag}>${arg0}</${tag}>`);
872
+ }
873
+ }
874
+
875
+ // ===== CLI =====
876
+
877
+ visitCli(node) {
878
+ const desc = node.description ? this.rawString(node.description) : node.name;
879
+ this.emit(`// CLI: ${node.name} - ${desc}`);
880
+ this.emit(`$args = [];`);
881
+ this.emit(`$argv = array_slice($argv ?? [], 1);`);
882
+ this.emitRaw('');
883
+
884
+ this.emit(`for ($__i = 0; $__i < count($argv); $__i++) {`);
885
+ this.indent++;
886
+ for (const arg of node.args) {
887
+ const argName = this.rawString(arg.name);
888
+ this.emit(`if ($argv[$__i] === '--${argName}') { $args['${argName}'] = $argv[++$__i]; continue; }`);
889
+ }
890
+ for (const flag of node.flags) {
891
+ const s = this.rawString(flag.short);
892
+ const l = this.rawString(flag.long);
893
+ this.emit(`if ($argv[$__i] === '-${s}' || $argv[$__i] === '--${l}') { $args['${l}'] = true; continue; }`);
894
+ }
895
+ this.indent--;
896
+ this.emit('}');
897
+ this.emitRaw('');
898
+
899
+ if (node.run) {
900
+ const runParam = node.run.params.length > 0 ? node.run.params[0] : 'args';
901
+ if (runParam !== 'args') {
902
+ this.emit(`$${runParam} = $args;`);
903
+ }
904
+ for (const stmt of node.run.body) this.visitStatement(stmt);
905
+ }
906
+ this.emitRaw('');
907
+ }
908
+
909
+ // ===== Mail =====
910
+
911
+ visitMail(node) {
912
+ const host = this.generateString(node.host);
913
+ const port = this.expr(node.port);
914
+ this.emit(`// Mail configuration`);
915
+ this.emit(`// Use PHPMailer or Laravel Mail`);
916
+ this.emit(`$mailConfig = ['host' => ${host}, 'port' => ${port}];`);
917
+ this.emitRaw('');
918
+ }
919
+
920
+ // ===== Desktop =====
921
+
922
+ visitDesktop(node) {
923
+ this.emit(`// Desktop app: ${node.name}`);
924
+ this.emit(`// PHP is not typically used for desktop apps`);
925
+ this.emit(`// Consider using PHP-GTK or a web-based approach`);
926
+ this.emitRaw('');
927
+ }
928
+
929
+ // ===== Screen =====
930
+
931
+ visitScreen(node) {
932
+ this.emit(`// Mobile screen: ${node.name}`);
933
+ this.emit(`// PHP is not used for mobile development`);
934
+ this.emitRaw('');
935
+ }
936
+
937
+ // ===== GraphQL =====
938
+
939
+ visitGraphql(node) {
940
+ const path = this.phpStringValue(node.path);
941
+ this.emit(`// GraphQL endpoint at ${path}`);
942
+ this.emit(`// Use webonyx/graphql-php for GraphQL support`);
943
+ this.emitRaw('');
944
+ }
945
+
946
+ // ===== OAuth =====
947
+
948
+ visitOauth(node) {
949
+ const provider = this.rawString(node.provider);
950
+ const clientId = this.expr(node.clientId);
951
+ const clientSecret = this.expr(node.clientSecret);
952
+ this.emit(`// OAuth: ${provider}`);
953
+ this.emit(`$oauthConfig = [`);
954
+ this.indent++;
955
+ this.emit(`'provider' => ${JSON.stringify(provider)},`);
956
+ this.emit(`'client_id' => ${clientId},`);
957
+ this.emit(`'client_secret' => ${clientSecret},`);
958
+ this.indent--;
959
+ this.emit(`];`);
960
+ this.emitRaw('');
961
+ }
962
+
963
+ // ===== Pay =====
964
+
965
+ visitPay(node) {
966
+ const provider = this.rawString(node.provider);
967
+ const secretKey = this.expr(node.secretKey);
968
+ if (provider === 'stripe') {
969
+ this.emit(`// Stripe payment`);
970
+ this.emit(`// composer require stripe/stripe-php`);
971
+ this.emit(`\\Stripe\\Stripe::setApiKey(${secretKey});`);
972
+ } else {
973
+ this.emit(`// ${provider} payment integration`);
974
+ }
975
+ this.emitRaw('');
976
+ }
977
+
978
+ // ===== Storage =====
979
+
980
+ visitStorage(node) {
981
+ const provider = this.rawString(node.provider);
982
+ const bucket = this.expr(node.bucket);
983
+ this.emit(`// Storage: ${provider}`);
984
+ this.emit(`$storageBucket = ${bucket};`);
985
+ this.emit(`// Use league/flysystem for storage abstraction`);
986
+ this.emitRaw('');
987
+ }
988
+
989
+ // ===== PDF =====
990
+
991
+ visitPdf(node) {
992
+ const filename = this.rawString(node.filename);
993
+ this.emit(`// PDF generation: ${filename}`);
994
+ this.emit(`// Use TCPDF or FPDF`);
995
+ this.emit(`// composer require tecnickcom/tcpdf`);
996
+ this.emit(`$__pdf = new TCPDF();`);
997
+ this.emit(`$__pdf->AddPage();`);
998
+
999
+ for (const el of node.elements) {
1000
+ const tag = el.tag;
1001
+ const arg0 = el.args[0] ? this.rawString(el.args[0]) : '';
1002
+ if (tag === 'title') {
1003
+ this.emit(`$__pdf->SetFont('helvetica', 'B', 24);`);
1004
+ this.emit(`$__pdf->Cell(0, 15, ${JSON.stringify(arg0)}, 0, 1);`);
1005
+ } else if (tag === 'text' || tag === 'p') {
1006
+ this.emit(`$__pdf->SetFont('helvetica', '', 12);`);
1007
+ this.emit(`$__pdf->MultiCell(0, 8, ${JSON.stringify(arg0)});`);
1008
+ } else {
1009
+ this.emit(`$__pdf->Cell(0, 8, ${JSON.stringify(arg0)}, 0, 1);`);
1010
+ }
1011
+ }
1012
+
1013
+ this.emit(`$__pdf->Output(${JSON.stringify(filename)}, 'F');`);
1014
+ this.emit(`echo "Generated: ${filename}\\n";`);
1015
+ this.emitRaw('');
1016
+ }
1017
+
1018
+ // ===== i18n =====
1019
+
1020
+ visitI18n(node) {
1021
+ const dir = this.rawString(node.dir);
1022
+ const defaultLang = node.defaultLang ? this.rawString(node.defaultLang) : 'en';
1023
+ this.emit(`$__i18nData = [];`);
1024
+ for (const lang of node.langs) {
1025
+ const code = this.rawString(lang.code);
1026
+ const file = this.rawString(lang.file);
1027
+ this.emit(`$__i18nData[${JSON.stringify(code)}] = json_decode(file_get_contents(${JSON.stringify(dir)}. '/' . ${JSON.stringify(file)}), true);`);
1028
+ }
1029
+ this.emit(`$__i18nLang = ${JSON.stringify(defaultLang)};`);
1030
+ this.emitRaw('');
1031
+ this.emit(`function t($key, $params = []) {`);
1032
+ this.indent++;
1033
+ this.emit(`global $__i18nData, $__i18nLang;`);
1034
+ this.emit(`$data = $__i18nData[$__i18nLang] ?? [];`);
1035
+ this.emit(`foreach (explode('.', $key) as $k) { $data = $data[$k] ?? $key; if (!is_array($data)) break; }`);
1036
+ this.emit(`$text = (string)$data;`);
1037
+ this.emit(`foreach ($params as $k => $v) { $text = str_replace('{' . $k . '}', (string)$v, $text); }`);
1038
+ this.emit(`return $text;`);
1039
+ this.indent--;
1040
+ this.emit('}');
1041
+ this.emitRaw('');
1042
+ }
1043
+
1044
+ // ===== Push =====
1045
+
1046
+ visitPush(node) {
1047
+ this.emit(`// Push notifications - use minishlink/web-push`);
1048
+ this.emit(`$vapidPublicKey = ${this.expr(node.publicKey)};`);
1049
+ this.emit(`$vapidPrivateKey = ${this.expr(node.privateKey)};`);
1050
+ this.emitRaw('');
1051
+ }
1052
+
1053
+ // ===== Search =====
1054
+
1055
+ visitSearch(node) {
1056
+ const engine = this.rawString(node.engine);
1057
+ const host = this.expr(node.host);
1058
+ const apiKey = this.expr(node.apiKey);
1059
+ this.emit(`// Search engine: ${engine}`);
1060
+ this.emit(`$searchHost = ${host};`);
1061
+ this.emit(`$searchApiKey = ${apiKey};`);
1062
+ this.emitRaw('');
1063
+ }
1064
+
1065
+ // ===== Image =====
1066
+
1067
+ visitImage(node) {
1068
+ const input = this.expr(node.input);
1069
+ const output = node.output ? this.expr(node.output) : input;
1070
+ this.emit(`// Image processing with GD or Intervention Image`);
1071
+ this.emit(`$__img = imagecreatefromstring(file_get_contents(${input}));`);
1072
+ for (const op of node.operations) {
1073
+ if (op.op === 'resize') {
1074
+ const w = op.args[0] || 800;
1075
+ const h = op.args[1] || 600;
1076
+ this.emit(`$__img = imagescale($__img, ${w}, ${h});`);
1077
+ } else if (op.op === 'rotate') {
1078
+ this.emit(`$__img = imagerotate($__img, ${op.args[0] || 90}, 0);`);
1079
+ } else if (op.op === 'flip') {
1080
+ this.emit(`imageflip($__img, IMG_FLIP_VERTICAL);`);
1081
+ } else if (op.op === 'grayscale' || op.op === 'greyscale') {
1082
+ this.emit(`imagefilter($__img, IMG_FILTER_GRAYSCALE);`);
1083
+ }
1084
+ }
1085
+ this.emit(`imagepng($__img, ${output});`);
1086
+ this.emit(`echo "Processed: " . ${output} . "\\n";`);
1087
+ this.emitRaw('');
1088
+ }
1089
+
1090
+ // ===== CSV =====
1091
+
1092
+ visitCsv(node) {
1093
+ const name = this.rawString(node.name);
1094
+ const columns = node.columns.map(c => this.rawString(c));
1095
+ const source = node.source ? this.expr(node.source) : '[]';
1096
+ const output = node.output ? this.rawString(node.output) : `${name}.csv`;
1097
+
1098
+ this.emit(`$__fp = fopen(${JSON.stringify(output)}, 'w');`);
1099
+ if (columns.length > 0) {
1100
+ this.emit(`fputcsv($__fp, ${JSON.stringify(columns)});`);
1101
+ }
1102
+ this.emit(`foreach (${source} as $row) {`);
1103
+ this.indent++;
1104
+ this.emit(`fputcsv($__fp, is_array($row) ? array_values($row) : [$row]);`);
1105
+ this.indent--;
1106
+ this.emit('}');
1107
+ this.emit(`fclose($__fp);`);
1108
+ this.emit(`echo "Exported: ${output}\\n";`);
1109
+ this.emitRaw('');
1110
+ }
1111
+
1112
+ // ===== Logging =====
1113
+
1114
+ visitLogging(node) {
1115
+ const name = this.rawString(node.name);
1116
+ const level = node.level ? this.rawString(node.level) : 'info';
1117
+ const file = node.file ? this.rawString(node.file) : null;
1118
+ this.emit(`// Logger: ${name} (level: ${level})`);
1119
+ if (file) {
1120
+ this.emit(`// Log file: ${file}`);
1121
+ this.emit(`function logger($msg, $level = '${level}') {`);
1122
+ this.indent++;
1123
+ this.emit(`$line = date('Y-m-d H:i:s') . " [{$level}] {$msg}\\n";`);
1124
+ this.emit(`file_put_contents(${JSON.stringify(file)}, $line, FILE_APPEND);`);
1125
+ this.indent--;
1126
+ this.emit('}');
1127
+ } else {
1128
+ this.emit(`function logger($msg, $level = '${level}') {`);
1129
+ this.indent++;
1130
+ this.emit(`error_log(date('Y-m-d H:i:s') . " [{$level}] {$msg}");`);
1131
+ this.indent--;
1132
+ this.emit('}');
1133
+ }
1134
+ this.emitRaw('');
1135
+ }
1136
+
1137
+ // ===== Migration =====
1138
+
1139
+ visitMigrate(node) {
1140
+ const name = this.rawString(node.name);
1141
+ this.emit(`// Migration: ${name}`);
1142
+ this.emit(`class Migration_${name.replace(/\W/g, '_')} {`);
1143
+ this.indent++;
1144
+ this.emit(`public function up($db) {`);
1145
+ this.indent++;
1146
+ if (node.up.length > 0) {
1147
+ for (const stmt of node.up) this.visitStatement(stmt);
1148
+ } else {
1149
+ this.emit('// empty');
1150
+ }
1151
+ this.indent--;
1152
+ this.emit('}');
1153
+ this.emit(`public function down($db) {`);
1154
+ this.indent++;
1155
+ if (node.down.length > 0) {
1156
+ for (const stmt of node.down) this.visitStatement(stmt);
1157
+ } else {
1158
+ this.emit('// empty');
1159
+ }
1160
+ this.indent--;
1161
+ this.emit('}');
1162
+ this.indent--;
1163
+ this.emit('}');
1164
+ this.emitRaw('');
1165
+ }
1166
+
1167
+ // ===== gRPC =====
1168
+
1169
+ visitGrpc(node) {
1170
+ const name = this.rawString(node.name);
1171
+ const port = node.port ? this.expr(node.port) : '50051';
1172
+ this.emit(`// gRPC server: ${name} on port ${port}`);
1173
+ this.emit(`// Use grpc/grpc PHP extension`);
1174
+ this.emitRaw('');
1175
+ }
1176
+
1177
+ // ===== WebRTC =====
1178
+
1179
+ visitWebrtc(node) {
1180
+ this.emit(`// WebRTC signaling - use Ratchet WebSocket`);
1181
+ this.emitRaw('');
1182
+ }
1183
+
1184
+ // ===== Blockchain =====
1185
+
1186
+ visitBlockchain(node) {
1187
+ const name = this.rawString(node.name);
1188
+ const provider = node.provider ? this.expr(node.provider) : "'http://localhost:8545'";
1189
+ this.emit(`// Blockchain: ${name}`);
1190
+ this.emit(`// Use web3.php or similar`);
1191
+ this.emit(`$${name.replace(/\W/g, '_')}_provider = ${provider};`);
1192
+ this.emitRaw('');
1193
+ }
1194
+
1195
+ // ===== Expression generation =====
1196
+
1197
+ expr(node) {
1198
+ if (!node) return 'null';
1199
+
1200
+ switch (node.type) {
1201
+ case 'Number': return node.value;
1202
+ case 'String': return this.generateString(node.value);
1203
+ case 'Bool': return node.value ? 'true' : 'false';
1204
+ case 'Null': return 'null';
1205
+ case 'Self': return '$this';
1206
+ case 'Identifier': return '$' + node.name;
1207
+
1208
+ case 'Binary': {
1209
+ const op = this.mapBinaryOp(node.op);
1210
+ const l = this.expr(node.left);
1211
+ const r = this.expr(node.right);
1212
+ if (node.op === 'instanceof') {
1213
+ return `${l} instanceof ${r}`;
1214
+ }
1215
+ if (node.op === 'in') {
1216
+ return `in_array(${l}, ${r})`;
1217
+ }
1218
+ const simple = node.left.type !== 'Binary' && node.right.type !== 'Binary';
1219
+ return simple ? `${l} ${op} ${r}` : `(${l} ${op} ${r})`;
1220
+ }
1221
+
1222
+ case 'Unary': {
1223
+ const op = node.op === '!' ? '!' : node.op;
1224
+ return `${op}${this.expr(node.expr)}`;
1225
+ }
1226
+
1227
+ case 'Await':
1228
+ // PHP is synchronous; just return the expression
1229
+ return this.expr(node.expr);
1230
+
1231
+ case 'AwaitAllExpr':
1232
+ return this.expr(node.expr);
1233
+
1234
+ case 'Spread':
1235
+ return `...${this.expr(node.expr)}`;
1236
+
1237
+ case 'New':
1238
+ return `new ${this.exprNoPrefix(node.expr)}`;
1239
+
1240
+ case 'TypeOf':
1241
+ return `gettype(${this.expr(node.expr)})`;
1242
+
1243
+ case 'MemberAccess':
1244
+ return this.mapMemberAccess(node);
1245
+
1246
+ case 'OptionalAccess':
1247
+ return `${this.expr(node.object)}?->${node.property}`;
1248
+
1249
+ case 'Call':
1250
+ return this.generateCall(node);
1251
+
1252
+ case 'IndexAccess':
1253
+ return `${this.expr(node.object)}[${this.expr(node.index)}]`;
1254
+
1255
+ case 'Array':
1256
+ return `[${node.elements.map(e => this.expr(e)).join(', ')}]`;
1257
+
1258
+ case 'Object': {
1259
+ const props = node.properties.map(p => {
1260
+ if (p.type === 'spread') return `...${this.expr(p.value)}`;
1261
+ if (p.type === 'shorthand') return `${JSON.stringify(p.key)} => $${p.key}`;
1262
+ const key = typeof p.key === 'string' ? JSON.stringify(p.key) :
1263
+ (p.key.type === 'String' ? this.generateString(p.key.value) :
1264
+ (p.key.type === 'Computed' ? this.expr(p.key.expr) : JSON.stringify(p.key)));
1265
+ return `${key} => ${this.expr(p.value)}`;
1266
+ }).join(', ');
1267
+ return `[${props}]`;
1268
+ }
1269
+
1270
+ case 'ArrowFn': {
1271
+ const params = node.params.map(p => '$' + p.name).join(', ');
1272
+ const body = this.expr(node.body);
1273
+ return `fn(${params}) => ${body}`;
1274
+ }
1275
+
1276
+ case 'Lambda': {
1277
+ const params = node.params.map(p => '$' + p.name).join(', ');
1278
+ const bodyLines = [];
1279
+ const savedOutput = this.output;
1280
+ const savedIndent = this.indent;
1281
+ this.output = bodyLines;
1282
+ this.indent = 0;
1283
+ for (const stmt of node.body) this.visitStatement(stmt);
1284
+ this.output = savedOutput;
1285
+ this.indent = savedIndent;
1286
+ return `function(${params}) { ${bodyLines.join(' ')} }`;
1287
+ }
1288
+
1289
+ case 'Ternary':
1290
+ return `(${this.expr(node.condition)} ? ${this.expr(node.consequent)} : ${this.expr(node.alternate)})`;
1291
+
1292
+ case 'Pipe':
1293
+ return this.generatePipe(node);
1294
+
1295
+ default:
1296
+ return `null /* expr:${node.type} */`;
1297
+ }
1298
+ }
1299
+
1300
+ // Generate expression without $ prefix for class names (new Foo, not new $Foo)
1301
+ exprNoPrefix(node) {
1302
+ if (!node) return 'null';
1303
+ if (node.type === 'Call') {
1304
+ const callee = node.callee.type === 'Identifier' ? node.callee.name : this.exprNoPrefix(node.callee);
1305
+ const args = node.args.map(a => this.expr(a)).join(', ');
1306
+ return `${callee}(${args})`;
1307
+ }
1308
+ if (node.type === 'Identifier') return node.name;
1309
+ if (node.type === 'MemberAccess') {
1310
+ return `${this.exprNoPrefix(node.object)}->${node.property}`;
1311
+ }
1312
+ return this.expr(node);
1313
+ }
1314
+
1315
+ mapBinaryOp(op) {
1316
+ const map = {
1317
+ '===': '===', '!==': '!==',
1318
+ '==': '==', '!=': '!=',
1319
+ '&&': '&&', '||': '||',
1320
+ '??': '??',
1321
+ '>': '>', '<': '<', '>=': '>=', '<=': '<=',
1322
+ '+': '+', '-': '-', '*': '*', '/': '/', '%': '%',
1323
+ '**': '**',
1324
+ 'and': '&&', 'or': '||',
1325
+ '.': '.',
1326
+ };
1327
+ return map[op] || op;
1328
+ }
1329
+
1330
+ mapMemberAccess(node) {
1331
+ const obj = this.expr(node.object);
1332
+ const prop = node.property;
1333
+
1334
+ // Map common JS patterns to PHP
1335
+ if (prop === 'length') return `count(${obj})`;
1336
+ if (prop === 'toString') return `(string)${obj}`;
1337
+
1338
+ // Request mappings
1339
+ if ((obj === '$request' || obj === '$req') && prop === 'body') return '$_POST';
1340
+ if ((obj === '$request' || obj === '$req') && prop === 'query') return '$_GET';
1341
+ if ((obj === '$request' || obj === '$req') && prop === 'params') return '$_GET';
1342
+ if ((obj === '$request' || obj === '$req') && prop === 'headers') return 'getallheaders()';
1343
+ if ((obj === '$request' || obj === '$req') && prop === 'cookies') return '$_COOKIE';
1344
+ if ((obj === '$request' || obj === '$req') && prop === 'method') return '$_SERVER["REQUEST_METHOD"]';
1345
+ if ((obj === '$request' || obj === '$req') && prop === 'url') return '$_SERVER["REQUEST_URI"]';
1346
+
1347
+ // JSON
1348
+ if (obj === '$JSON' && prop === 'parse') return 'json_decode';
1349
+ if (obj === '$JSON' && prop === 'stringify') return 'json_encode';
1350
+
1351
+ // Math
1352
+ if (obj === '$Math' && prop === 'floor') return 'floor';
1353
+ if (obj === '$Math' && prop === 'ceil') return 'ceil';
1354
+ if (obj === '$Math' && prop === 'round') return 'round';
1355
+ if (obj === '$Math' && prop === 'abs') return 'abs';
1356
+ if (obj === '$Math' && prop === 'min') return 'min';
1357
+ if (obj === '$Math' && prop === 'max') return 'max';
1358
+ if (obj === '$Math' && prop === 'random') return '(mt_rand() / mt_getrandmax())';
1359
+ if (obj === '$Math' && prop === 'PI') return 'M_PI';
1360
+
1361
+ // Console
1362
+ if (obj === '$console' && prop === 'log') return 'print_r';
1363
+ if (obj === '$console' && prop === 'error') return 'error_log';
1364
+
1365
+ return `${obj}->${prop}`;
1366
+ }
1367
+
1368
+ generateCall(node) {
1369
+ const args = node.args.map(a => this.expr(a)).join(', ');
1370
+
1371
+ // Map global function calls
1372
+ if (node.callee.type === 'Identifier') {
1373
+ const name = node.callee.name;
1374
+ if (name === 'parseInt') return `(int)(${args})`;
1375
+ if (name === 'parseFloat') return `(float)(${args})`;
1376
+ if (name === 'String') return `(string)(${args})`;
1377
+ if (name === 'Number') return `(float)(${args})`;
1378
+ if (name === 'Boolean') return `(bool)(${args})`;
1379
+ if (name === 'isNaN') return `is_nan(${args})`;
1380
+ if (name === 'setTimeout') return `/* setTimeout not available in PHP */`;
1381
+ if (name === 'setInterval') return `/* setInterval not available in PHP */`;
1382
+ // Don't add $ prefix for function calls
1383
+ return `${name}(${args})`;
1384
+ }
1385
+
1386
+ // Map method calls
1387
+ if (node.callee.type === 'MemberAccess') {
1388
+ const obj = this.expr(node.callee.object);
1389
+ const prop = node.callee.property;
1390
+
1391
+ // Array methods
1392
+ if (prop === 'push') return `${obj}[] = ${args}`;
1393
+ if (prop === 'pop') return `array_pop(${obj})`;
1394
+ if (prop === 'shift') return `array_shift(${obj})`;
1395
+ if (prop === 'unshift') return `array_unshift(${obj}, ${args})`;
1396
+ if (prop === 'indexOf') return `array_search(${args}, ${obj})`;
1397
+ if (prop === 'includes') return `in_array(${args}, ${obj})`;
1398
+ if (prop === 'join') return `implode(${args}, ${obj})`;
1399
+ if (prop === 'slice') return `array_slice(${obj}, ${args})`;
1400
+ if (prop === 'forEach') { return `array_map(${args}, ${obj})`; }
1401
+ if (prop === 'map') return `array_map(${args}, ${obj})`;
1402
+ if (prop === 'filter') return `array_filter(${obj}, ${args})`;
1403
+ if (prop === 'find') return `current(array_filter(${obj}, ${args}))`;
1404
+ if (prop === 'some') return `count(array_filter(${obj}, ${args})) > 0`;
1405
+ if (prop === 'every') return `count(array_filter(${obj}, ${args})) === count(${obj})`;
1406
+ if (prop === 'reduce') return `array_reduce(${obj}, ${args})`;
1407
+ if (prop === 'reverse') return `array_reverse(${obj})`;
1408
+ if (prop === 'sort') { return `(function(&$a) { sort($a); return $a; })(${obj})`; }
1409
+ if (prop === 'concat') return `array_merge(${obj}, ${args})`;
1410
+ if (prop === 'flat') return `array_merge(...${obj})`;
1411
+
1412
+ // String methods
1413
+ if (prop === 'split') return `explode(${args}, ${obj})`;
1414
+ if (prop === 'trim') return `trim(${obj})`;
1415
+ if (prop === 'trimStart') return `ltrim(${obj})`;
1416
+ if (prop === 'trimEnd') return `rtrim(${obj})`;
1417
+ if (prop === 'toUpperCase') return `strtoupper(${obj})`;
1418
+ if (prop === 'toLowerCase') return `strtolower(${obj})`;
1419
+ if (prop === 'startsWith') return `str_starts_with(${obj}, ${args})`;
1420
+ if (prop === 'endsWith') return `str_ends_with(${obj}, ${args})`;
1421
+ if (prop === 'replace') return `str_replace(${args}, ${obj})`;
1422
+ if (prop === 'replaceAll') return `str_replace(${args}, ${obj})`;
1423
+ if (prop === 'charAt') return `${obj}[${args}]`;
1424
+ if (prop === 'substring') return `substr(${obj}, ${args})`;
1425
+ if (prop === 'repeat') return `str_repeat(${obj}, ${args})`;
1426
+ if (prop === 'padStart') return `str_pad(${obj}, ${args}, ' ', STR_PAD_LEFT)`;
1427
+ if (prop === 'padEnd') return `str_pad(${obj}, ${args})`;
1428
+
1429
+ // Object methods
1430
+ if (obj === '$Object' && prop === 'keys') return `array_keys(${args})`;
1431
+ if (obj === '$Object' && prop === 'values') return `array_values(${args})`;
1432
+ if (obj === '$Object' && prop === 'entries') return `array_map(null, array_keys(${args}), array_values(${args}))`;
1433
+ if (obj === '$Object' && prop === 'assign') return `array_merge(${args})`;
1434
+
1435
+ // Array static
1436
+ if (obj === '$Array' && prop === 'isArray') return `is_array(${args})`;
1437
+ if (obj === '$Array' && prop === 'from') return `(array)(${args})`;
1438
+
1439
+ // Date
1440
+ if (obj === '$Date' && prop === 'now') return `(int)(microtime(true) * 1000)`;
1441
+
1442
+ // Console
1443
+ if (obj === '$console' && prop === 'log') return `print_r(${args})`;
1444
+ if (obj === '$console' && prop === 'error') return `error_log(${args})`;
1445
+ }
1446
+
1447
+ const callee = this.expr(node.callee);
1448
+ return `${callee}(${args})`;
1449
+ }
1450
+
1451
+ generateString(strData) {
1452
+ if (!strData || !strData.parts) return '""';
1453
+
1454
+ const hasInterpolation = strData.parts.some(p => p.type === 'expr');
1455
+
1456
+ if (!hasInterpolation) {
1457
+ const raw = strData.parts.map(p => p.value).join('');
1458
+ return JSON.stringify(raw);
1459
+ }
1460
+
1461
+ // Use double-quoted string with {$var} interpolation
1462
+ let result = '"';
1463
+ for (const part of strData.parts) {
1464
+ if (part.type === 'text') {
1465
+ result += part.value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\$/g, '\\$');
1466
+ } else {
1467
+ const exprCode = part.value.replace(/\bthis\b/g, 'this');
1468
+ // Wrap in braces for interpolation
1469
+ result += '{$' + exprCode + '}';
1470
+ }
1471
+ }
1472
+ result += '"';
1473
+ return result;
1474
+ }
1475
+
1476
+ rawString(strData) {
1477
+ if (!strData) return '';
1478
+ if (strData.raw !== null && strData.raw !== undefined) return strData.raw;
1479
+ if (strData.parts) return strData.parts.map(p => p.value).join('');
1480
+ return '';
1481
+ }
1482
+
1483
+ phpStringValue(strData) {
1484
+ if (!strData || !strData.parts) return '""';
1485
+ if (strData.raw !== null && strData.raw !== undefined) {
1486
+ return JSON.stringify(strData.raw);
1487
+ }
1488
+ return this.generateString(strData);
1489
+ }
1490
+
1491
+ generatePipe(node) {
1492
+ const steps = [];
1493
+ let current = node;
1494
+ while (current.type === 'Pipe') {
1495
+ steps.unshift(current.right);
1496
+ current = current.left;
1497
+ }
1498
+ steps.unshift(current);
1499
+
1500
+ const ARRAY_METHODS = new Set(['filter', 'map', 'reduce', 'sort', 'reverse', 'join']);
1501
+
1502
+ let result = this.expr(steps[0]);
1503
+ for (let i = 1; i < steps.length; i++) {
1504
+ const step = steps[i];
1505
+ if (step.type === 'Call' && step.callee.type === 'Identifier') {
1506
+ const name = step.callee.name;
1507
+ const args = step.args.map(a => this.expr(a)).join(', ');
1508
+
1509
+ if (name === 'filter') {
1510
+ result = `array_filter(${result}, ${args})`;
1511
+ } else if (name === 'map') {
1512
+ result = `array_map(${args}, ${result})`;
1513
+ } else if (name === 'reduce') {
1514
+ result = `array_reduce(${result}, ${args})`;
1515
+ } else if (name === 'sort') {
1516
+ result = `(function($a) { sort($a); return $a; })(${result})`;
1517
+ } else if (name === 'reverse') {
1518
+ result = `array_reverse(${result})`;
1519
+ } else if (name === 'join') {
1520
+ result = `implode(${args}, ${result})`;
1521
+ } else {
1522
+ result = `${name}(${result}${args ? ', ' + args : ''})`;
1523
+ }
1524
+ } else if (step.type === 'Identifier') {
1525
+ result = `${step.name}(${result})`;
1526
+ } else if (step.type === 'Call') {
1527
+ const callee = this.exprNoPrefix(step.callee);
1528
+ const args = step.args.map(a => this.expr(a)).join(', ');
1529
+ result = `${callee}(${result}${args ? ', ' + args : ''})`;
1530
+ } else {
1531
+ result = `(${this.expr(step)})(${result})`;
1532
+ }
1533
+ }
1534
+ return result;
1535
+ }
1536
+
1537
+ mapType(type) {
1538
+ if (!type) return 'mixed';
1539
+ const map = {
1540
+ 'str': 'string', 'string': 'string',
1541
+ 'int': 'int', 'integer': 'int',
1542
+ 'num': 'float', 'number': 'float',
1543
+ 'bool': 'bool', 'boolean': 'bool',
1544
+ 'list': 'array', 'array': 'array',
1545
+ 'map': 'array',
1546
+ 'any': 'mixed', 'void': 'void',
1547
+ 'json': 'mixed',
1548
+ };
1549
+ return map[type] || type;
1550
+ }
1551
+
1552
+ bodyUsesAwait(body) {
1553
+ for (const stmt of body) {
1554
+ if (this.stmtUsesAwait(stmt)) return true;
1555
+ }
1556
+ return false;
1557
+ }
1558
+
1559
+ stmtUsesAwait(node) {
1560
+ if (!node) return false;
1561
+ const json = JSON.stringify(node);
1562
+ return json.includes('"Await"') || json.includes('"AwaitAll"');
1563
+ }
1564
+
1565
+ statementToString(stmt) {
1566
+ const saved = this.output;
1567
+ const savedIndent = this.indent;
1568
+ this.output = [];
1569
+ this.indent = 0;
1570
+ this.visitStatement(stmt);
1571
+ const result = this.output.join('\n');
1572
+ this.output = saved;
1573
+ this.indent = savedIndent;
1574
+ return result;
1575
+ }
1576
+ }