imxc 0.1.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,788 @@
1
+ import ts from 'typescript';
2
+ import { HOST_COMPONENTS, isHostComponent } from './components.js';
3
+ export function lowerComponent(parsed, validation) {
4
+ const func = parsed.component;
5
+ const name = func.name.text;
6
+ // Build state slots
7
+ const stateSlots = validation.useStateCalls.map(u => ({
8
+ name: u.name,
9
+ setter: u.setter,
10
+ type: inferTypeFromExpr(u.initializer),
11
+ initialValue: exprToLiteral(u.initializer),
12
+ index: u.index,
13
+ }));
14
+ // Build state lookup maps
15
+ const stateVars = new Map();
16
+ const setterMap = new Map();
17
+ for (const slot of stateSlots) {
18
+ stateVars.set(slot.name, slot);
19
+ setterMap.set(slot.setter, slot.name);
20
+ }
21
+ // Detect props parameter
22
+ let propsParam = null;
23
+ const params = [];
24
+ if (func.parameters.length > 0) {
25
+ const param = func.parameters[0];
26
+ if (ts.isIdentifier(param.name)) {
27
+ propsParam = param.name.text;
28
+ }
29
+ // Extract prop types from type annotation
30
+ if (param.type && ts.isTypeLiteralNode(param.type)) {
31
+ for (const member of param.type.members) {
32
+ if (ts.isPropertySignature(member) && member.name && ts.isIdentifier(member.name)) {
33
+ const propName = member.name.text;
34
+ const propType = inferPropType(member);
35
+ params.push({ name: propName, type: propType });
36
+ }
37
+ }
38
+ }
39
+ }
40
+ const ctx = {
41
+ stateVars,
42
+ setterMap,
43
+ propsParam,
44
+ bufferIndex: 0,
45
+ sourceFile: parsed.sourceFile,
46
+ };
47
+ // Find return statement and lower its JSX
48
+ const body = [];
49
+ if (func.body) {
50
+ const returnStmt = func.body.statements.find(ts.isReturnStatement);
51
+ if (returnStmt && returnStmt.expression) {
52
+ lowerJsxExpression(returnStmt.expression, body, ctx);
53
+ }
54
+ }
55
+ return {
56
+ name,
57
+ stateSlots,
58
+ bufferCount: ctx.bufferIndex,
59
+ params,
60
+ body,
61
+ };
62
+ }
63
+ function inferPropType(member) {
64
+ if (!member.type)
65
+ return 'string';
66
+ if (ts.isFunctionTypeNode(member.type))
67
+ return 'callback';
68
+ const text = member.type.getText();
69
+ if (text === 'number')
70
+ return 'int';
71
+ if (text === 'boolean')
72
+ return 'bool';
73
+ if (text === 'string')
74
+ return 'string';
75
+ return 'string';
76
+ }
77
+ function inferTypeFromExpr(expr) {
78
+ if (ts.isNumericLiteral(expr)) {
79
+ // Use getText() to check original source text, since TS normalizes 5.0 to "5" in .text
80
+ const rawText = expr.getText();
81
+ return rawText.includes('.') ? 'float' : 'int';
82
+ }
83
+ if (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr))
84
+ return 'string';
85
+ if (expr.kind === ts.SyntaxKind.TrueKeyword || expr.kind === ts.SyntaxKind.FalseKeyword)
86
+ return 'bool';
87
+ if (ts.isArrayLiteralExpression(expr))
88
+ return 'color';
89
+ // Default to int
90
+ return 'int';
91
+ }
92
+ function exprToLiteral(expr) {
93
+ if (ts.isNumericLiteral(expr)) {
94
+ // Use getText() to preserve original source (e.g., "5.0" instead of "5")
95
+ return expr.getText();
96
+ }
97
+ if (ts.isStringLiteral(expr))
98
+ return JSON.stringify(expr.text);
99
+ if (expr.kind === ts.SyntaxKind.TrueKeyword)
100
+ return 'true';
101
+ if (expr.kind === ts.SyntaxKind.FalseKeyword)
102
+ return 'false';
103
+ if (ts.isArrayLiteralExpression(expr)) {
104
+ const elements = expr.elements.map(e => {
105
+ const text = e.getText();
106
+ // Ensure float suffix for color array elements
107
+ if (ts.isNumericLiteral(e)) {
108
+ return text.includes('.') ? `${text}f` : `${text}.0f`;
109
+ }
110
+ return text;
111
+ }).join(', ');
112
+ return `{${elements}}`;
113
+ }
114
+ // Fallback: use text as-is
115
+ return expr.getText();
116
+ }
117
+ /**
118
+ * Convert a TypeScript expression to C++ code string.
119
+ */
120
+ export function exprToCpp(node, ctx) {
121
+ // Numeric literal
122
+ if (ts.isNumericLiteral(node)) {
123
+ return node.text;
124
+ }
125
+ // String literal
126
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
127
+ return JSON.stringify(node.text);
128
+ }
129
+ // Boolean literals
130
+ if (node.kind === ts.SyntaxKind.TrueKeyword)
131
+ return 'true';
132
+ if (node.kind === ts.SyntaxKind.FalseKeyword)
133
+ return 'false';
134
+ // Identifier
135
+ if (ts.isIdentifier(node)) {
136
+ const name = node.text;
137
+ if (ctx.stateVars.has(name)) {
138
+ return `${name}.get()`;
139
+ }
140
+ return name;
141
+ }
142
+ // Property access (e.g., props.name)
143
+ if (ts.isPropertyAccessExpression(node)) {
144
+ const obj = exprToCpp(node.expression, ctx);
145
+ const prop = node.name.text;
146
+ return `${obj}.${prop}`;
147
+ }
148
+ // Parenthesized expression
149
+ if (ts.isParenthesizedExpression(node)) {
150
+ return `(${exprToCpp(node.expression, ctx)})`;
151
+ }
152
+ // Binary expression
153
+ if (ts.isBinaryExpression(node)) {
154
+ const left = exprToCpp(node.left, ctx);
155
+ const right = exprToCpp(node.right, ctx);
156
+ const op = node.operatorToken.getText();
157
+ return `${left} ${op} ${right}`;
158
+ }
159
+ // Prefix unary expression (e.g., !show)
160
+ if (ts.isPrefixUnaryExpression(node)) {
161
+ const operand = exprToCpp(node.operand, ctx);
162
+ const opStr = node.operator === ts.SyntaxKind.ExclamationToken ? '!' :
163
+ node.operator === ts.SyntaxKind.MinusToken ? '-' :
164
+ node.operator === ts.SyntaxKind.PlusToken ? '+' : '';
165
+ return `${opStr}${operand}`;
166
+ }
167
+ // Call expression
168
+ if (ts.isCallExpression(node)) {
169
+ const callee = node.expression;
170
+ // Check for state setter: setCount(x) -> count.set(x)
171
+ if (ts.isIdentifier(callee)) {
172
+ const setterName = callee.text;
173
+ const stateVarName = ctx.setterMap.get(setterName);
174
+ if (stateVarName !== undefined) {
175
+ const arg = node.arguments.length > 0 ? exprToCpp(node.arguments[0], ctx) : '';
176
+ return `${stateVarName}.set(${arg})`;
177
+ }
178
+ }
179
+ // General call expression
180
+ const fn = exprToCpp(callee, ctx);
181
+ const args = node.arguments.map(a => exprToCpp(a, ctx)).join(', ');
182
+ return `${fn}(${args})`;
183
+ }
184
+ // Conditional (ternary) expression: a ? b : c
185
+ if (ts.isConditionalExpression(node)) {
186
+ const condition = exprToCpp(node.condition, ctx);
187
+ const whenTrue = exprToCpp(node.whenTrue, ctx);
188
+ const whenFalse = exprToCpp(node.whenFalse, ctx);
189
+ return `${condition} ? ${whenTrue} : ${whenFalse}`;
190
+ }
191
+ // Arrow function -> C++ lambda
192
+ if (ts.isArrowFunction(node)) {
193
+ if (ts.isBlock(node.body)) {
194
+ const stmts = [];
195
+ for (const stmt of node.body.statements) {
196
+ stmts.push(stmtToCpp(stmt, ctx));
197
+ }
198
+ return `[&]() { ${stmts.join(' ')} }`;
199
+ }
200
+ else {
201
+ const bodyExpr = exprToCpp(node.body, ctx);
202
+ return `[&]() { ${bodyExpr}; }`;
203
+ }
204
+ }
205
+ // Template literal
206
+ if (ts.isTemplateExpression(node)) {
207
+ let result = JSON.stringify(node.head.text);
208
+ for (const span of node.templateSpans) {
209
+ const expr = exprToCpp(span.expression, ctx);
210
+ result = `${result} + std::to_string(${expr}) + ${JSON.stringify(span.literal.text)}`;
211
+ }
212
+ return result;
213
+ }
214
+ // Array literal: ["a", "b"] -> "\"a\", \"b\""
215
+ if (ts.isArrayLiteralExpression(node)) {
216
+ return node.elements.map(e => exprToCpp(e, ctx)).join(', ');
217
+ }
218
+ // Fallback: use text representation
219
+ return node.getText();
220
+ }
221
+ function stmtToCpp(stmt, ctx) {
222
+ if (ts.isExpressionStatement(stmt)) {
223
+ return exprToCpp(stmt.expression, ctx) + ';';
224
+ }
225
+ if (ts.isReturnStatement(stmt)) {
226
+ if (stmt.expression) {
227
+ return 'return ' + exprToCpp(stmt.expression, ctx) + ';';
228
+ }
229
+ return 'return;';
230
+ }
231
+ return stmt.getText() + ';';
232
+ }
233
+ /**
234
+ * Extract action statements from an arrow function callback for button onPress etc.
235
+ */
236
+ function extractActionStatements(expr, ctx) {
237
+ if (ts.isArrowFunction(expr)) {
238
+ if (ts.isBlock(expr.body)) {
239
+ return expr.body.statements.map(s => stmtToCpp(s, ctx));
240
+ }
241
+ else {
242
+ // Expression body: () => setCount(count + 1)
243
+ return [exprToCpp(expr.body, ctx) + ';'];
244
+ }
245
+ }
246
+ // If not an arrow function, just call it
247
+ return [exprToCpp(expr, ctx) + ';'];
248
+ }
249
+ /**
250
+ * Lower a JSX expression (possibly wrapped in parenthesized expr) into IR nodes.
251
+ */
252
+ function lowerJsxExpression(node, body, ctx) {
253
+ if (ts.isParenthesizedExpression(node)) {
254
+ lowerJsxExpression(node.expression, body, ctx);
255
+ return;
256
+ }
257
+ if (ts.isJsxElement(node)) {
258
+ lowerJsxElement(node, body, ctx);
259
+ return;
260
+ }
261
+ if (ts.isJsxSelfClosingElement(node)) {
262
+ lowerJsxSelfClosing(node, body, ctx);
263
+ return;
264
+ }
265
+ if (ts.isJsxFragment(node)) {
266
+ for (const child of node.children) {
267
+ lowerJsxChild(child, body, ctx);
268
+ }
269
+ return;
270
+ }
271
+ // Conditional: condition && <Element/>
272
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
273
+ const condition = exprToCpp(node.left, ctx);
274
+ const condBody = [];
275
+ lowerJsxExpression(node.right, condBody, ctx);
276
+ body.push({ kind: 'conditional', condition, body: condBody });
277
+ return;
278
+ }
279
+ // Ternary: condition ? <A/> : <B/>
280
+ if (ts.isConditionalExpression(node)) {
281
+ const condition = exprToCpp(node.condition, ctx);
282
+ const thenBody = [];
283
+ const elseBody = [];
284
+ lowerJsxExpression(node.whenTrue, thenBody, ctx);
285
+ lowerJsxExpression(node.whenFalse, elseBody, ctx);
286
+ body.push({ kind: 'conditional', condition, body: thenBody, elseBody });
287
+ return;
288
+ }
289
+ // items.map(item => <Comp/>)
290
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === 'map') {
291
+ lowerListMap(node, body, ctx);
292
+ return;
293
+ }
294
+ // JsxExpression wrapper
295
+ if (ts.isJsxExpression(node) && node.expression) {
296
+ lowerJsxExpression(node.expression, body, ctx);
297
+ return;
298
+ }
299
+ }
300
+ function lowerJsxElement(node, body, ctx) {
301
+ const tagName = node.openingElement.tagName;
302
+ if (!ts.isIdentifier(tagName))
303
+ return;
304
+ const name = tagName.text;
305
+ if (name === 'Text') {
306
+ lowerTextElement(node, body, ctx);
307
+ return;
308
+ }
309
+ if (isHostComponent(name)) {
310
+ const def = HOST_COMPONENTS[name];
311
+ const attrs = getAttributes(node.openingElement.attributes, ctx);
312
+ if (def.isContainer) {
313
+ const containerTag = name;
314
+ body.push({ kind: 'begin_container', tag: containerTag, props: attrs });
315
+ for (const child of node.children) {
316
+ lowerJsxChild(child, body, ctx);
317
+ }
318
+ body.push({ kind: 'end_container', tag: containerTag });
319
+ return;
320
+ }
321
+ // Popup
322
+ if (name === 'Popup') {
323
+ const id = attrs['id'] ?? '';
324
+ body.push({ kind: 'begin_popup', id });
325
+ for (const child of node.children) {
326
+ lowerJsxChild(child, body, ctx);
327
+ }
328
+ body.push({ kind: 'end_popup' });
329
+ return;
330
+ }
331
+ }
332
+ // Custom component with children - treat as container-like (not common but handle gracefully)
333
+ if (!isHostComponent(name)) {
334
+ lowerCustomComponent(name, node.openingElement.attributes, body, ctx);
335
+ return;
336
+ }
337
+ }
338
+ function lowerJsxSelfClosing(node, body, ctx) {
339
+ const tagName = node.tagName;
340
+ if (!ts.isIdentifier(tagName))
341
+ return;
342
+ const name = tagName.text;
343
+ if (!isHostComponent(name)) {
344
+ lowerCustomComponent(name, node.attributes, body, ctx);
345
+ return;
346
+ }
347
+ const attrs = getAttributes(node.attributes, ctx);
348
+ const rawAttrs = getRawAttributes(node.attributes);
349
+ switch (name) {
350
+ case 'Button':
351
+ lowerButton(attrs, rawAttrs, body, ctx);
352
+ break;
353
+ case 'TextInput':
354
+ lowerTextInput(attrs, rawAttrs, body, ctx);
355
+ break;
356
+ case 'Checkbox':
357
+ lowerCheckbox(attrs, rawAttrs, body, ctx);
358
+ break;
359
+ case 'MenuItem':
360
+ lowerMenuItem(attrs, rawAttrs, body, ctx);
361
+ break;
362
+ case 'SliderFloat':
363
+ lowerSliderFloat(attrs, rawAttrs, body, ctx);
364
+ break;
365
+ case 'SliderInt':
366
+ lowerSliderInt(attrs, rawAttrs, body, ctx);
367
+ break;
368
+ case 'DragFloat':
369
+ lowerDragFloat(attrs, rawAttrs, body, ctx);
370
+ break;
371
+ case 'DragInt':
372
+ lowerDragInt(attrs, rawAttrs, body, ctx);
373
+ break;
374
+ case 'Combo':
375
+ lowerCombo(attrs, rawAttrs, body, ctx);
376
+ break;
377
+ case 'InputInt':
378
+ lowerInputInt(attrs, rawAttrs, body, ctx);
379
+ break;
380
+ case 'InputFloat':
381
+ lowerInputFloat(attrs, rawAttrs, body, ctx);
382
+ break;
383
+ case 'ColorEdit':
384
+ lowerColorEdit(attrs, rawAttrs, body, ctx);
385
+ break;
386
+ case 'ListBox':
387
+ lowerListBox(attrs, rawAttrs, body, ctx);
388
+ break;
389
+ case 'ProgressBar':
390
+ lowerProgressBar(attrs, rawAttrs, body, ctx);
391
+ break;
392
+ case 'Tooltip':
393
+ lowerTooltip(attrs, body, ctx);
394
+ break;
395
+ case 'Separator':
396
+ body.push({ kind: 'separator' });
397
+ break;
398
+ case 'Text':
399
+ // Self-closing <Text /> - empty text
400
+ body.push({ kind: 'text', format: '', args: [] });
401
+ break;
402
+ default:
403
+ // Container self-closing (e.g., <Window title="X"/>)
404
+ if (HOST_COMPONENTS[name]?.isContainer) {
405
+ const containerTag = name;
406
+ body.push({ kind: 'begin_container', tag: containerTag, props: attrs });
407
+ body.push({ kind: 'end_container', tag: containerTag });
408
+ }
409
+ break;
410
+ }
411
+ }
412
+ function lowerButton(attrs, rawAttrs, body, ctx) {
413
+ const title = attrs['title'] ?? '""';
414
+ const onPressExpr = rawAttrs.get('onPress');
415
+ let action = [];
416
+ if (onPressExpr) {
417
+ action = extractActionStatements(onPressExpr, ctx);
418
+ }
419
+ const style = attrs['style'];
420
+ body.push({ kind: 'button', title, action, style });
421
+ }
422
+ function lowerTextInput(attrs, rawAttrs, body, ctx) {
423
+ const label = attrs['label'] ?? '""';
424
+ const bufferIndex = ctx.bufferIndex++;
425
+ // Detect bound state variable from value prop
426
+ let stateVar = '';
427
+ const valueExpr = rawAttrs.get('value');
428
+ if (valueExpr && ts.isIdentifier(valueExpr)) {
429
+ const varName = valueExpr.text;
430
+ if (ctx.stateVars.has(varName)) {
431
+ stateVar = varName;
432
+ }
433
+ }
434
+ const style = attrs['style'];
435
+ body.push({ kind: 'text_input', label, bufferIndex, stateVar, style });
436
+ }
437
+ function lowerCheckbox(attrs, rawAttrs, body, ctx) {
438
+ const label = attrs['label'] ?? '""';
439
+ // Detect bound state variable from value prop
440
+ let stateVar = '';
441
+ let valueExprStr;
442
+ let onChangeExprStr;
443
+ const valueExpr = rawAttrs.get('value');
444
+ if (valueExpr && ts.isIdentifier(valueExpr)) {
445
+ const varName = valueExpr.text;
446
+ if (ctx.stateVars.has(varName)) {
447
+ stateVar = varName;
448
+ }
449
+ else {
450
+ // Non-state value (e.g., props.done passed as identifier)
451
+ valueExprStr = exprToCpp(valueExpr, ctx);
452
+ }
453
+ }
454
+ else if (valueExpr) {
455
+ // Non-state value expression (e.g., props.done)
456
+ valueExprStr = exprToCpp(valueExpr, ctx);
457
+ }
458
+ // If not state-bound, get onChange expression
459
+ if (!stateVar) {
460
+ const onChangeRaw = rawAttrs.get('onChange');
461
+ if (onChangeRaw) {
462
+ onChangeExprStr = exprToCpp(onChangeRaw, ctx);
463
+ // If it's not already a lambda/call, make it a call
464
+ if (!onChangeExprStr.startsWith('[') && !onChangeExprStr.endsWith(')')) {
465
+ onChangeExprStr = `${onChangeExprStr}()`;
466
+ }
467
+ }
468
+ }
469
+ const style = attrs['style'];
470
+ body.push({ kind: 'checkbox', label, stateVar, valueExpr: valueExprStr, onChangeExpr: onChangeExprStr, style });
471
+ }
472
+ function lowerMenuItem(attrs, rawAttrs, body, ctx) {
473
+ const label = attrs['label'] ?? '""';
474
+ const shortcut = attrs['shortcut'];
475
+ const onPressExpr = rawAttrs.get('onPress');
476
+ let action = [];
477
+ if (onPressExpr) {
478
+ action = extractActionStatements(onPressExpr, ctx);
479
+ }
480
+ body.push({ kind: 'menu_item', label, shortcut, action });
481
+ }
482
+ function lowerTextElement(node, body, ctx) {
483
+ let format = '';
484
+ const args = [];
485
+ for (const child of node.children) {
486
+ if (ts.isJsxText(child)) {
487
+ // Collapse whitespace (newlines, tabs, runs of spaces) into single spaces,
488
+ // matching JSX semantics. Only fully-blank segments are dropped.
489
+ const text = child.text.replace(/%/g, '%%').replace(/\s+/g, ' ');
490
+ // Drop segments that are purely whitespace at the very start or end of children
491
+ const isFirst = child === node.children[0];
492
+ const isLast = child === node.children[node.children.length - 1];
493
+ const trimmed = isFirst && isLast ? text.trim()
494
+ : isFirst ? text.trimStart()
495
+ : isLast ? text.trimEnd()
496
+ : text;
497
+ if (trimmed)
498
+ format += trimmed;
499
+ }
500
+ else if (ts.isJsxExpression(child) && child.expression) {
501
+ const expr = child.expression;
502
+ const cppExpr = exprToCpp(expr, ctx);
503
+ const exprType = inferExprType(expr, ctx);
504
+ switch (exprType) {
505
+ case 'int':
506
+ format += '%d';
507
+ args.push(cppExpr);
508
+ break;
509
+ case 'float':
510
+ format += '%.2f';
511
+ args.push(cppExpr);
512
+ break;
513
+ case 'bool':
514
+ format += '%s';
515
+ args.push(`${cppExpr} ? "true" : "false"`);
516
+ break;
517
+ case 'string':
518
+ format += '%s';
519
+ // String literals and ternaries of literals are already const char*
520
+ if (cppExpr.startsWith('"') || isCharPtrExpression(expr)) {
521
+ args.push(cppExpr);
522
+ }
523
+ else {
524
+ args.push(`${cppExpr}.c_str()`);
525
+ }
526
+ break;
527
+ default:
528
+ format += '%s';
529
+ args.push(`std::to_string(${cppExpr}).c_str()`);
530
+ break;
531
+ }
532
+ }
533
+ }
534
+ body.push({ kind: 'text', format, args });
535
+ }
536
+ /**
537
+ * Check if an expression will produce a const char* in C++ (not std::string).
538
+ * String literals and ternaries where both branches are string literals qualify.
539
+ */
540
+ function isCharPtrExpression(expr) {
541
+ if (ts.isStringLiteral(expr))
542
+ return true;
543
+ if (ts.isConditionalExpression(expr)) {
544
+ return isCharPtrExpression(expr.whenTrue) && isCharPtrExpression(expr.whenFalse);
545
+ }
546
+ return false;
547
+ }
548
+ function inferExprType(expr, ctx) {
549
+ if (ts.isNumericLiteral(expr)) {
550
+ return expr.text.includes('.') ? 'float' : 'int';
551
+ }
552
+ if (ts.isStringLiteral(expr))
553
+ return 'string';
554
+ if (expr.kind === ts.SyntaxKind.TrueKeyword || expr.kind === ts.SyntaxKind.FalseKeyword)
555
+ return 'bool';
556
+ // Identifier: check state var type
557
+ if (ts.isIdentifier(expr)) {
558
+ const slot = ctx.stateVars.get(expr.text);
559
+ if (slot)
560
+ return slot.type;
561
+ }
562
+ // Property access: props.name -> look for string by default
563
+ if (ts.isPropertyAccessExpression(expr)) {
564
+ return 'string';
565
+ }
566
+ // Binary expression: infer from operands
567
+ if (ts.isBinaryExpression(expr)) {
568
+ const op = expr.operatorToken.kind;
569
+ // Comparison operators return bool
570
+ if (op === ts.SyntaxKind.EqualsEqualsToken || op === ts.SyntaxKind.EqualsEqualsEqualsToken ||
571
+ op === ts.SyntaxKind.ExclamationEqualsToken || op === ts.SyntaxKind.ExclamationEqualsEqualsToken ||
572
+ op === ts.SyntaxKind.LessThanToken || op === ts.SyntaxKind.LessThanEqualsToken ||
573
+ op === ts.SyntaxKind.GreaterThanToken || op === ts.SyntaxKind.GreaterThanEqualsToken ||
574
+ op === ts.SyntaxKind.AmpersandAmpersandToken || op === ts.SyntaxKind.BarBarToken) {
575
+ return 'bool';
576
+ }
577
+ // Arithmetic: infer from left side
578
+ return inferExprType(expr.left, ctx);
579
+ }
580
+ // Conditional (ternary): infer from the result branches
581
+ if (ts.isConditionalExpression(expr)) {
582
+ return inferExprType(expr.whenTrue, ctx);
583
+ }
584
+ // Call expression that is a state getter
585
+ if (ts.isCallExpression(expr) && ts.isIdentifier(expr.expression)) {
586
+ const name = expr.expression.text;
587
+ if (ctx.setterMap.has(name)) {
588
+ const stateVarName = ctx.setterMap.get(name);
589
+ const slot = ctx.stateVars.get(stateVarName);
590
+ if (slot)
591
+ return slot.type;
592
+ }
593
+ }
594
+ return 'int'; // default
595
+ }
596
+ function lowerListMap(node, body, ctx) {
597
+ const propAccess = node.expression;
598
+ const array = exprToCpp(propAccess.expression, ctx);
599
+ const callback = node.arguments[0];
600
+ let itemVar = 'item';
601
+ let mapBody = [];
602
+ if (callback && (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback))) {
603
+ if (callback.parameters.length > 0 && ts.isIdentifier(callback.parameters[0].name)) {
604
+ itemVar = callback.parameters[0].name.text;
605
+ }
606
+ if (ts.isBlock(callback.body)) {
607
+ const ret = callback.body.statements.find(ts.isReturnStatement);
608
+ if (ret?.expression) {
609
+ lowerJsxExpression(ret.expression, mapBody, ctx);
610
+ }
611
+ }
612
+ else if (callback.body) {
613
+ lowerJsxExpression(callback.body, mapBody, ctx);
614
+ }
615
+ }
616
+ body.push({
617
+ kind: 'list_map',
618
+ array,
619
+ itemVar,
620
+ key: 'i',
621
+ componentName: 'ListItem',
622
+ stateCount: 0,
623
+ bufferCount: 0,
624
+ body: mapBody,
625
+ });
626
+ }
627
+ function lowerCustomComponent(name, attributes, body, ctx) {
628
+ const attrs = getAttributes(attributes, ctx);
629
+ body.push({
630
+ kind: 'custom_component',
631
+ name,
632
+ props: attrs,
633
+ stateCount: 0,
634
+ bufferCount: 0,
635
+ });
636
+ }
637
+ function lowerJsxChild(child, body, ctx) {
638
+ if (ts.isJsxElement(child)) {
639
+ lowerJsxElement(child, body, ctx);
640
+ }
641
+ else if (ts.isJsxSelfClosingElement(child)) {
642
+ lowerJsxSelfClosing(child, body, ctx);
643
+ }
644
+ else if (ts.isJsxExpression(child)) {
645
+ if (child.expression) {
646
+ lowerJsxExpression(child.expression, body, ctx);
647
+ }
648
+ }
649
+ else if (ts.isJsxText(child)) {
650
+ // Standalone text not inside <Text> — usually whitespace, skip
651
+ }
652
+ }
653
+ function getAttributes(attributes, ctx) {
654
+ const result = {};
655
+ for (const attr of attributes.properties) {
656
+ if (ts.isJsxAttribute(attr) && attr.name && ts.isIdentifier(attr.name)) {
657
+ const name = attr.name.text;
658
+ if (attr.initializer) {
659
+ if (ts.isStringLiteral(attr.initializer)) {
660
+ result[name] = JSON.stringify(attr.initializer.text);
661
+ }
662
+ else if (ts.isJsxExpression(attr.initializer) && attr.initializer.expression) {
663
+ result[name] = exprToCpp(attr.initializer.expression, ctx);
664
+ }
665
+ }
666
+ else {
667
+ // Boolean shorthand: <X disabled /> means disabled={true}
668
+ result[name] = 'true';
669
+ }
670
+ }
671
+ }
672
+ return result;
673
+ }
674
+ function getRawAttributes(attributes) {
675
+ const result = new Map();
676
+ for (const attr of attributes.properties) {
677
+ if (ts.isJsxAttribute(attr) && attr.name && ts.isIdentifier(attr.name)) {
678
+ const name = attr.name.text;
679
+ if (attr.initializer && ts.isJsxExpression(attr.initializer)) {
680
+ result.set(name, attr.initializer.expression ?? null);
681
+ }
682
+ else if (attr.initializer && ts.isStringLiteral(attr.initializer)) {
683
+ result.set(name, attr.initializer);
684
+ }
685
+ else {
686
+ result.set(name, null);
687
+ }
688
+ }
689
+ }
690
+ return result;
691
+ }
692
+ function lowerValueOnChange(rawAttrs, ctx) {
693
+ let stateVar = '';
694
+ let valueExpr;
695
+ let onChangeExpr;
696
+ const valueRaw = rawAttrs.get('value');
697
+ if (valueRaw && ts.isIdentifier(valueRaw) && ctx.stateVars.has(valueRaw.text)) {
698
+ stateVar = valueRaw.text;
699
+ }
700
+ else if (valueRaw) {
701
+ valueExpr = exprToCpp(valueRaw, ctx);
702
+ const onChangeRaw = rawAttrs.get('onChange');
703
+ if (onChangeRaw) {
704
+ onChangeExpr = exprToCpp(onChangeRaw, ctx);
705
+ if (!onChangeExpr.startsWith('[') && !onChangeExpr.endsWith(')')) {
706
+ onChangeExpr = `${onChangeExpr}()`;
707
+ }
708
+ }
709
+ }
710
+ return { stateVar, valueExpr, onChangeExpr };
711
+ }
712
+ function lowerSliderFloat(attrs, rawAttrs, body, ctx) {
713
+ const label = attrs['label'] ?? '""';
714
+ const min = attrs['min'] ?? '0.0f';
715
+ const max = attrs['max'] ?? '1.0f';
716
+ const style = attrs['style'];
717
+ const { stateVar, valueExpr, onChangeExpr } = lowerValueOnChange(rawAttrs, ctx);
718
+ body.push({ kind: 'slider_float', label, stateVar, valueExpr, onChangeExpr, min, max, style });
719
+ }
720
+ function lowerSliderInt(attrs, rawAttrs, body, ctx) {
721
+ const label = attrs['label'] ?? '""';
722
+ const min = attrs['min'] ?? '0';
723
+ const max = attrs['max'] ?? '100';
724
+ const style = attrs['style'];
725
+ const { stateVar, valueExpr, onChangeExpr } = lowerValueOnChange(rawAttrs, ctx);
726
+ body.push({ kind: 'slider_int', label, stateVar, valueExpr, onChangeExpr, min, max, style });
727
+ }
728
+ function lowerDragFloat(attrs, rawAttrs, body, ctx) {
729
+ const label = attrs['label'] ?? '""';
730
+ const speed = attrs['speed'] ?? '1.0f';
731
+ const style = attrs['style'];
732
+ const { stateVar, valueExpr, onChangeExpr } = lowerValueOnChange(rawAttrs, ctx);
733
+ body.push({ kind: 'drag_float', label, stateVar, valueExpr, onChangeExpr, speed, style });
734
+ }
735
+ function lowerDragInt(attrs, rawAttrs, body, ctx) {
736
+ const label = attrs['label'] ?? '""';
737
+ const speed = attrs['speed'] ?? '1.0f';
738
+ const style = attrs['style'];
739
+ const { stateVar, valueExpr, onChangeExpr } = lowerValueOnChange(rawAttrs, ctx);
740
+ body.push({ kind: 'drag_int', label, stateVar, valueExpr, onChangeExpr, speed, style });
741
+ }
742
+ function lowerCombo(attrs, rawAttrs, body, ctx) {
743
+ const label = attrs['label'] ?? '""';
744
+ const items = attrs['items'] ?? '';
745
+ const style = attrs['style'];
746
+ const { stateVar, valueExpr, onChangeExpr } = lowerValueOnChange(rawAttrs, ctx);
747
+ body.push({ kind: 'combo', label, stateVar, valueExpr, onChangeExpr, items, style });
748
+ }
749
+ function lowerInputInt(attrs, rawAttrs, body, ctx) {
750
+ const label = attrs['label'] ?? '""';
751
+ const style = attrs['style'];
752
+ const { stateVar, valueExpr, onChangeExpr } = lowerValueOnChange(rawAttrs, ctx);
753
+ body.push({ kind: 'input_int', label, stateVar, valueExpr, onChangeExpr, style });
754
+ }
755
+ function lowerInputFloat(attrs, rawAttrs, body, ctx) {
756
+ const label = attrs['label'] ?? '""';
757
+ const style = attrs['style'];
758
+ const { stateVar, valueExpr, onChangeExpr } = lowerValueOnChange(rawAttrs, ctx);
759
+ body.push({ kind: 'input_float', label, stateVar, valueExpr, onChangeExpr, style });
760
+ }
761
+ function lowerColorEdit(attrs, rawAttrs, body, ctx) {
762
+ const label = attrs['label'] ?? '""';
763
+ const style = attrs['style'];
764
+ // ColorEdit only supports state-bound values
765
+ let stateVar = '';
766
+ const valueRaw = rawAttrs.get('value');
767
+ if (valueRaw && ts.isIdentifier(valueRaw) && ctx.stateVars.has(valueRaw.text)) {
768
+ stateVar = valueRaw.text;
769
+ }
770
+ body.push({ kind: 'color_edit', label, stateVar, style });
771
+ }
772
+ function lowerListBox(attrs, rawAttrs, body, ctx) {
773
+ const label = attrs['label'] ?? '""';
774
+ const items = attrs['items'] ?? '';
775
+ const style = attrs['style'];
776
+ const { stateVar, valueExpr, onChangeExpr } = lowerValueOnChange(rawAttrs, ctx);
777
+ body.push({ kind: 'list_box', label, stateVar, valueExpr, onChangeExpr, items, style });
778
+ }
779
+ function lowerProgressBar(attrs, rawAttrs, body, ctx) {
780
+ const value = attrs['value'] ?? '0.0f';
781
+ const overlay = attrs['overlay'];
782
+ const style = attrs['style'];
783
+ body.push({ kind: 'progress_bar', value, overlay, style });
784
+ }
785
+ function lowerTooltip(attrs, body, ctx) {
786
+ const text = attrs['text'] ?? '""';
787
+ body.push({ kind: 'tooltip', text });
788
+ }