embedded-react 0.3.0 → 0.4.1

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.
package/aot/compile.mjs CHANGED
@@ -33,14 +33,31 @@
33
33
  //
34
34
  // npm run aot # default demo (thermostat) — but use a minimal demo for the slice
35
35
  // npm run aot -- music-player # a specific demo by folder name
36
- import { parse } from '@babel/parser';
37
- import { codeFrameColumns } from '@babel/code-frame';
38
- import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from 'node:fs';
39
- import { resolve, dirname } from 'node:path';
40
- import { fileURLToPath } from 'node:url';
41
- import { lowerStyle, NODE_TYPES, DYN_FIELDS, colorLiteral } from './style-map.mjs';
42
- import { flattenSvg, parseColor, parsePath } from '../src/embedded-react/svg-ops.js';
43
- import { bakeAssets } from '../assets/index.mjs';
36
+ import {parse} from '@babel/parser';
37
+ import {codeFrameColumns} from '@babel/code-frame';
38
+ import {
39
+ readFileSync,
40
+ writeFileSync,
41
+ mkdirSync,
42
+ existsSync,
43
+ readdirSync,
44
+ } from 'node:fs';
45
+ import {resolve, dirname} from 'node:path';
46
+ import {fileURLToPath} from 'node:url';
47
+ import {
48
+ lowerStyle,
49
+ NODE_TYPES,
50
+ DYN_FIELDS,
51
+ colorLiteral,
52
+ } from './style-map.mjs';
53
+ import {
54
+ flattenSvg,
55
+ parseColor,
56
+ parsePath,
57
+ PAINT_STRIDE,
58
+ scaleVectorArtifact,
59
+ } from '../src/embedded-react/svg-ops.js';
60
+ import {bakeAssets} from '../assets/index.mjs';
44
61
 
45
62
  const here = dirname(fileURLToPath(import.meta.url)); // bridges/quickjs/js/aot
46
63
  const repoRoot = resolve(here, '../../../..');
@@ -49,7 +66,9 @@ const distDir = resolve(here, '..', 'dist');
49
66
 
50
67
  // The compiler's own version (kept in lockstep with the engine via tools/sync-version.mjs). Stamped into the
51
68
  // generated app + asserted against the engine's er_version.h so a version mismatch fails at COMPILE time.
52
- const PKG_VERSION = JSON.parse(readFileSync(resolve(here, '..', 'package.json'), 'utf8')).version;
69
+ const PKG_VERSION = JSON.parse(
70
+ readFileSync(resolve(here, '..', 'package.json'), 'utf8'),
71
+ ).version;
53
72
  const [PKG_MAJOR, PKG_MINOR] = PKG_VERSION.split('.');
54
73
 
55
74
  // The core compiler is exported as compileSource(src) so it can be unit-tested on inline JSX. The CLI
@@ -93,7 +112,14 @@ function withLoc(fn) {
93
112
  try {
94
113
  return fn(node, ...rest);
95
114
  } catch (e) {
96
- if (e && typeof e.message === 'string' && e.message.startsWith('AOT:') && !e.aotLoc && node && node.loc) {
115
+ if (
116
+ e &&
117
+ typeof e.message === 'string' &&
118
+ e.message.startsWith('AOT:') &&
119
+ !e.aotLoc &&
120
+ node &&
121
+ node.loc
122
+ ) {
97
123
  e.aotLoc = node.loc.start; // babel loc: { line (1-based), column (0-based) }
98
124
  }
99
125
  throw e;
@@ -104,16 +130,18 @@ function withLoc(fn) {
104
130
  /** Re-throws an AOT error with `file:line:col`, a code-frame, and any hint folded into the message. */
105
131
  function formatAotError(e, src, filename) {
106
132
  if (!e || !e.aotLoc) return e; // nothing to locate — leave the bare message
107
- const { line, column } = e.aotLoc;
108
- const loc = { start: { line, column: column + 1 } }; // code-frame columns are 1-based
133
+ const {line, column} = e.aotLoc;
134
+ const loc = {start: {line, column: column + 1}}; // code-frame columns are 1-based
109
135
  let frame = '';
110
136
  try {
111
- frame = codeFrameColumns(src, loc, { highlightCode: false });
137
+ frame = codeFrameColumns(src, loc, {highlightCode: false});
112
138
  } catch {
113
139
  /* code-frame is best-effort */
114
140
  }
115
141
  const hint = e.aotHint ? `\n\nhint: ${e.aotHint}` : '';
116
- const out = new Error(`${e.message}\n at ${filename}:${line}:${column + 1}\n\n${frame}${hint}`);
142
+ const out = new Error(
143
+ `${e.message}\n at ${filename}:${line}:${column + 1}\n\n${frame}${hint}`,
144
+ );
117
145
  out.aotLoc = e.aotLoc;
118
146
  if (e.aotHint) out.aotHint = e.aotHint;
119
147
  return out;
@@ -157,19 +185,30 @@ function evalStatic(node, scope) {
157
185
  const l = evalStatic(node.left, scope);
158
186
  const r = evalStatic(node.right, scope);
159
187
  switch (node.operator) {
160
- case '+': return l + r;
161
- case '-': return l - r;
162
- case '*': return l * r;
163
- case '/': return l / r;
164
- case '%': return l % r;
165
- case '<': return l < r;
166
- case '>': return l > r;
167
- case '<=': return l <= r;
168
- case '>=': return l >= r;
188
+ case '+':
189
+ return l + r;
190
+ case '-':
191
+ return l - r;
192
+ case '*':
193
+ return l * r;
194
+ case '/':
195
+ return l / r;
196
+ case '%':
197
+ return l % r;
198
+ case '<':
199
+ return l < r;
200
+ case '>':
201
+ return l > r;
202
+ case '<=':
203
+ return l <= r;
204
+ case '>=':
205
+ return l >= r;
169
206
  case '==':
170
- case '===': return l === r;
207
+ case '===':
208
+ return l === r;
171
209
  case '!=':
172
- case '!==': return l !== r;
210
+ case '!==':
211
+ return l !== r;
173
212
  }
174
213
  break;
175
214
  }
@@ -180,36 +219,54 @@ function evalStatic(node, scope) {
180
219
  break;
181
220
  }
182
221
  case 'ConditionalExpression':
183
- return evalStatic(node.test, scope) ? evalStatic(node.consequent, scope) : evalStatic(node.alternate, scope);
222
+ return evalStatic(node.test, scope)
223
+ ? evalStatic(node.consequent, scope)
224
+ : evalStatic(node.alternate, scope);
184
225
  case 'Identifier':
185
226
  if (node.name in scope) return scope[node.name];
186
- throw new Error(`AOT: cannot statically resolve identifier "${node.name}"`);
227
+ throw new Error(
228
+ `AOT: cannot statically resolve identifier "${node.name}"`,
229
+ );
187
230
  case 'MemberExpression': {
188
231
  const obj = evalStatic(node.object, scope);
189
- const key = node.computed ? evalStatic(node.property, scope) : node.property.name;
190
- if (obj == null) throw new Error(`AOT: member access on null/undefined ("${key}")`);
232
+ const key = node.computed
233
+ ? evalStatic(node.property, scope)
234
+ : node.property.name;
235
+ if (obj == null)
236
+ throw new Error(`AOT: member access on null/undefined ("${key}")`);
191
237
  return obj[key];
192
238
  }
193
239
  case 'ObjectExpression': {
194
240
  const o = {};
195
241
  for (const prop of node.properties) {
196
- if (prop.type !== 'ObjectProperty') throw new Error('AOT: object spreads/methods not supported in static objects');
197
- const k = prop.computed ? evalStatic(prop.key, scope) : prop.key.name ?? prop.key.value;
242
+ if (prop.type !== 'ObjectProperty')
243
+ throw new Error(
244
+ 'AOT: object spreads/methods not supported in static objects',
245
+ );
246
+ const k = prop.computed
247
+ ? evalStatic(prop.key, scope)
248
+ : (prop.key.name ?? prop.key.value);
198
249
  o[k] = evalStatic(prop.value, scope);
199
250
  }
200
251
  return o;
201
252
  }
202
253
  case 'ArrayExpression':
203
- return node.elements.map((e) => (e ? evalStatic(e, scope) : null));
254
+ return node.elements.map(e => (e ? evalStatic(e, scope) : null));
204
255
  case 'CallExpression': {
205
256
  const c = node.callee;
206
- if (c.type === 'MemberExpression' && c.object.name === 'StyleSheet' && c.property.name === 'create') {
257
+ if (
258
+ c.type === 'MemberExpression' &&
259
+ c.object.name === 'StyleSheet' &&
260
+ c.property.name === 'create'
261
+ ) {
207
262
  return evalStatic(node.arguments[0], scope);
208
263
  }
209
264
  throw new Error(`AOT: cannot statically evaluate call expression`);
210
265
  }
211
266
  }
212
- throw new Error(`AOT: unsupported expression "${node.type}" in static context`);
267
+ throw new Error(
268
+ `AOT: unsupported expression "${node.type}" in static context`,
269
+ );
213
270
  }
214
271
 
215
272
  // ---------------------------------------------------------------------------------------------------
@@ -223,143 +280,258 @@ const COMPARE = new Set(['<', '>', '<=', '>=', '==', '!=', '===', '!==']);
223
280
  function emitExprImpl(node, env) {
224
281
  switch (node.type) {
225
282
  case 'NumericLiteral':
226
- return Number.isInteger(node.value) ? { code: String(node.value), cType: 'int' } : { code: `${node.value}f`, cType: 'float' };
283
+ return Number.isInteger(node.value)
284
+ ? {code: String(node.value), cType: 'int'}
285
+ : {code: `${node.value}f`, cType: 'float'};
227
286
  case 'StringLiteral':
228
- return { code: cstr(node.value), cType: 'string' };
287
+ return {code: cstr(node.value), cType: 'string'};
229
288
  case 'BooleanLiteral':
230
- return { code: node.value ? '1' : '0', cType: 'int' };
289
+ return {code: node.value ? '1' : '0', cType: 'int'};
231
290
  case 'Identifier': {
232
291
  if (env.locals.has(node.name)) return env.locals.get(node.name);
233
292
  if (env.state.has(node.name)) {
234
293
  const s = env.state.get(node.name);
235
- if (s.kind === 'list') throw new Error(`AOT: a list state ("${node.name}") can only be used via .length or .map`);
236
- return { code: s.cMember, cType: s.cType };
294
+ if (s.kind === 'list')
295
+ throw new Error(
296
+ `AOT: a list state ("${node.name}") can only be used via .length or .map`,
297
+ );
298
+ return {code: s.cMember, cType: s.cType};
237
299
  }
238
300
  if (node.name in env.consts) {
239
301
  const v = env.consts[node.name];
240
- if (typeof v === 'number') return Number.isInteger(v) ? { code: String(v), cType: 'int' } : { code: `${v}f`, cType: 'float' };
241
- if (typeof v === 'string') return { code: cstr(v), cType: 'string' };
302
+ if (typeof v === 'number')
303
+ return Number.isInteger(v)
304
+ ? {code: String(v), cType: 'int'}
305
+ : {code: `${v}f`, cType: 'float'};
306
+ if (typeof v === 'string') return {code: cstr(v), cType: 'string'};
242
307
  }
243
- throw new Error(`AOT: cannot resolve identifier "${node.name}" in a dynamic expression`);
308
+ throw new Error(
309
+ `AOT: cannot resolve identifier "${node.name}" in a dynamic expression`,
310
+ );
244
311
  }
245
312
  case 'UnaryExpression': {
246
313
  const a = emitExpr(node.argument, env);
247
314
  // Parenthesize the operand so `-` on a negative literal emits `(-(-135))`, not `(--135)` (a decrement).
248
- if (node.operator === '-' || node.operator === '+' || node.operator === '!') return { code: `(${node.operator}(${a.code}))`, cType: node.operator === '!' ? 'int' : a.cType };
315
+ if (
316
+ node.operator === '-' ||
317
+ node.operator === '+' ||
318
+ node.operator === '!'
319
+ )
320
+ return {
321
+ code: `(${node.operator}(${a.code}))`,
322
+ cType: node.operator === '!' ? 'int' : a.cType,
323
+ };
249
324
  throw new Error(`AOT: unsupported unary operator "${node.operator}"`);
250
325
  }
251
326
  case 'BinaryExpression': {
252
327
  const l = emitExpr(node.left, env);
253
328
  const r = emitExpr(node.right, env);
254
329
  if (ARITH.has(node.operator)) {
255
- const cType = l.cType === 'float' || r.cType === 'float' ? 'float' : 'int';
256
- return { code: `(${l.code} ${node.operator} ${r.code})`, cType };
330
+ const cType =
331
+ l.cType === 'float' || r.cType === 'float' ? 'float' : 'int';
332
+ return {code: `(${l.code} ${node.operator} ${r.code})`, cType};
257
333
  }
258
334
  if (COMPARE.has(node.operator)) {
259
- const eqOp = node.operator === '===' || node.operator === '==' ? '==' : node.operator === '!==' || node.operator === '!=' ? '!=' : null;
335
+ const eqOp =
336
+ node.operator === '===' || node.operator === '=='
337
+ ? '=='
338
+ : node.operator === '!==' || node.operator === '!='
339
+ ? '!='
340
+ : null;
260
341
  // String (in)equality → strcmp; the generated C already includes <string.h>.
261
- if (eqOp && (l.cType === 'string' || r.cType === 'string')) return { code: `(strcmp(${l.code}, ${r.code}) ${eqOp} 0)`, cType: 'int' };
262
- const op = node.operator === '===' ? '==' : node.operator === '!==' ? '!=' : node.operator;
263
- return { code: `(${l.code} ${op} ${r.code})`, cType: 'int' };
342
+ if (eqOp && (l.cType === 'string' || r.cType === 'string'))
343
+ return {
344
+ code: `(strcmp(${l.code}, ${r.code}) ${eqOp} 0)`,
345
+ cType: 'int',
346
+ };
347
+ const op =
348
+ node.operator === '==='
349
+ ? '=='
350
+ : node.operator === '!=='
351
+ ? '!='
352
+ : node.operator;
353
+ return {code: `(${l.code} ${op} ${r.code})`, cType: 'int'};
264
354
  }
265
355
  throw new Error(`AOT: unsupported binary operator "${node.operator}"`);
266
356
  }
267
357
  case 'LogicalExpression': {
268
- const op = node.operator === '&&' || node.operator === '||' ? node.operator : null;
269
- if (!op) throw new Error(`AOT: unsupported logical operator "${node.operator}"`);
358
+ const op =
359
+ node.operator === '&&' || node.operator === '||' ? node.operator : null;
360
+ if (!op)
361
+ throw new Error(`AOT: unsupported logical operator "${node.operator}"`);
270
362
  const l = emitExpr(node.left, env);
271
363
  const r = emitExpr(node.right, env);
272
- return { code: `(${l.code} ${op} ${r.code})`, cType: 'int' };
364
+ return {code: `(${l.code} ${op} ${r.code})`, cType: 'int'};
273
365
  }
274
366
  case 'ConditionalExpression': {
275
367
  const t = emitExpr(node.test, env);
276
368
  const c = emitExpr(node.consequent, env);
277
369
  const a = emitExpr(node.alternate, env);
278
- const cType = c.cType === 'float' || a.cType === 'float' ? 'float' : c.cType === a.cType ? c.cType : 'int';
279
- return { code: `(${t.code} ? ${c.code} : ${a.code})`, cType };
370
+ const cType =
371
+ c.cType === 'float' || a.cType === 'float'
372
+ ? 'float'
373
+ : c.cType === a.cType
374
+ ? c.cType
375
+ : 'int';
376
+ return {code: `(${t.code} ? ${c.code} : ${a.code})`, cType};
280
377
  }
281
378
  case 'MemberExpression': {
282
379
  // Static fold: member access that resolves to a compile-time constant (e.g. a .map item's `.key`).
283
380
  try {
284
381
  const v = evalStatic(node, env.consts ?? {});
285
- if (typeof v === 'number') return Number.isInteger(v) ? { code: String(v), cType: 'int' } : { code: `${v}f`, cType: 'float' };
286
- if (typeof v === 'string') return { code: cstr(v), cType: 'string' };
287
- if (typeof v === 'boolean') return { code: v ? '1' : '0', cType: 'int' };
382
+ if (typeof v === 'number')
383
+ return Number.isInteger(v)
384
+ ? {code: String(v), cType: 'int'}
385
+ : {code: `${v}f`, cType: 'float'};
386
+ if (typeof v === 'string') return {code: cstr(v), cType: 'string'};
387
+ if (typeof v === 'boolean') return {code: v ? '1' : '0', cType: 'int'};
288
388
  } catch {
289
389
  /* not static — fall through to the dynamic member forms below */
290
390
  }
291
391
  const obj = node.object;
292
392
  const prop = node.computed ? null : node.property.name;
293
393
  // `<list>.length` → the runtime count.
294
- if (obj.type === 'Identifier' && env.state.get(obj.name)?.kind === 'list' && prop === 'length') {
295
- return { code: env.state.get(obj.name).countMember, cType: 'int' };
394
+ if (
395
+ obj.type === 'Identifier' &&
396
+ env.state.get(obj.name)?.kind === 'list' &&
397
+ prop === 'length'
398
+ ) {
399
+ return {code: env.state.get(obj.name).countMember, cType: 'int'};
296
400
  }
297
401
  // `<item>.field` where item is a struct local (a list row's bound element).
298
- if (obj.type === 'Identifier' && env.locals.get(obj.name)?.struct && prop) {
299
- const f = env.locals.get(obj.name).struct.fields.find((x) => x.key === prop);
402
+ if (
403
+ obj.type === 'Identifier' &&
404
+ env.locals.get(obj.name)?.struct &&
405
+ prop
406
+ ) {
407
+ const f = env.locals
408
+ .get(obj.name)
409
+ .struct.fields.find(x => x.key === prop);
300
410
  if (!f) throw new Error(`AOT: unknown field "${prop}" on a list item`);
301
- return { code: `${env.locals.get(obj.name).code}.${f.key}`, cType: f.kind === 'string' ? 'string' : f.kind };
411
+ return {
412
+ code: `${env.locals.get(obj.name).code}.${f.key}`,
413
+ cType: f.kind === 'string' ? 'string' : f.kind,
414
+ };
302
415
  }
303
416
  // `<ref>.current` — a value ref's mutable C slot.
304
- if (obj.type === 'Identifier' && env.refs?.has(obj.name) && prop === 'current') {
417
+ if (
418
+ obj.type === 'Identifier' &&
419
+ env.refs?.has(obj.name) &&
420
+ prop === 'current'
421
+ ) {
305
422
  const r = env.refs.get(obj.name);
306
- return { code: r.cVar, cType: r.cType };
423
+ return {code: r.cVar, cType: r.cType};
307
424
  }
308
425
  // `<event>.x / .y / .dx / .dy` — touch fields of the handler's EREventData.
309
- if (obj.type === 'Identifier' && env.event === obj.name && (prop === 'x' || prop === 'y' || prop === 'dx' || prop === 'dy')) {
310
- return { code: `data->${prop}`, cType: 'int' };
426
+ if (
427
+ obj.type === 'Identifier' &&
428
+ env.event === obj.name &&
429
+ (prop === 'x' || prop === 'y' || prop === 'dx' || prop === 'dy')
430
+ ) {
431
+ return {code: `data->${prop}`, cType: 'int'};
311
432
  }
312
433
  // `<event>.layout.x / .y / .width / .height` — the onLayout rect (EREventData.layout_rect; ERRect uses w/h).
313
- if (obj.type === 'MemberExpression' && !obj.computed && obj.object.type === 'Identifier' && env.event === obj.object.name && obj.property.name === 'layout') {
314
- const RECT = { x: 'x', y: 'y', width: 'w', height: 'h' };
434
+ if (
435
+ obj.type === 'MemberExpression' &&
436
+ !obj.computed &&
437
+ obj.object.type === 'Identifier' &&
438
+ env.event === obj.object.name &&
439
+ obj.property.name === 'layout'
440
+ ) {
441
+ const RECT = {x: 'x', y: 'y', width: 'w', height: 'h'};
315
442
  const f = RECT[prop];
316
- if (!f) throw new Error(`AOT: unknown onLayout rect field "${prop}" (use x / y / width / height)`);
317
- return { code: `data->layout_rect.${f}`, cType: 'int' };
443
+ if (!f)
444
+ throw new Error(
445
+ `AOT: unknown onLayout rect field "${prop}" (use x / y / width / height)`,
446
+ );
447
+ return {code: `data->layout_rect.${f}`, cType: 'int'};
318
448
  }
319
- if (obj.type === 'Identifier' && obj.name === 'Math' && prop === 'PI') return { code: '(float)M_PI', cType: 'float' };
320
- throw aotError('AOT: unsupported member expression in a dynamic context', 'in a handler or dynamic expression you can read state, `ref.current`, a `.map` item field, event fields (e.x / e.y / e.dx / e.dy / e.layout.*), and Math.PI — other member access must be a compile-time constant.');
449
+ if (obj.type === 'Identifier' && obj.name === 'Math' && prop === 'PI')
450
+ return {code: '(float)M_PI', cType: 'float'};
451
+ throw aotError(
452
+ 'AOT: unsupported member expression in a dynamic context',
453
+ 'in a handler or dynamic expression you can read state, `ref.current`, a `.map` item field, event fields (e.x / e.y / e.dx / e.dy / e.layout.*), and Math.PI — other member access must be a compile-time constant.',
454
+ );
321
455
  }
322
456
  case 'CallExpression': {
323
457
  // Math.* helpers → libm (the generated C includes <math.h> when these appear).
324
458
  const c = node.callee;
325
459
  if (c.type === 'MemberExpression' && c.object.name === 'Math') {
326
460
  const fn = c.property.name;
327
- const a = node.arguments.map((x) => emitExpr(x, env));
328
- const UNARY = { sin: 'sinf', cos: 'cosf', tan: 'tanf', sqrt: 'sqrtf', abs: 'fabsf', round: 'roundf', floor: 'floorf', ceil: 'ceilf' };
461
+ const a = node.arguments.map(x => emitExpr(x, env));
462
+ const UNARY = {
463
+ sin: 'sinf',
464
+ cos: 'cosf',
465
+ tan: 'tanf',
466
+ sqrt: 'sqrtf',
467
+ abs: 'fabsf',
468
+ round: 'roundf',
469
+ floor: 'floorf',
470
+ ceil: 'ceilf',
471
+ };
329
472
  if (UNARY[fn] && a.length === 1) {
330
473
  const inner = `${UNARY[fn]}((float)(${a[0].code}))`;
331
474
  // round/floor/ceil yield a whole number — cast to int so %d / int assignments are correct.
332
- return fn === 'round' || fn === 'floor' || fn === 'ceil' ? { code: `((int)${inner})`, cType: 'int' } : { code: inner, cType: 'float' };
475
+ return fn === 'round' || fn === 'floor' || fn === 'ceil'
476
+ ? {code: `((int)${inner})`, cType: 'int'}
477
+ : {code: inner, cType: 'float'};
333
478
  }
334
- const BINARY = { min: 'fminf', max: 'fmaxf', atan2: 'atan2f', pow: 'powf' };
335
- if (BINARY[fn] && a.length === 2) return { code: `${BINARY[fn]}((float)(${a[0].code}), (float)(${a[1].code}))`, cType: 'float' };
479
+ const BINARY = {
480
+ min: 'fminf',
481
+ max: 'fmaxf',
482
+ atan2: 'atan2f',
483
+ pow: 'powf',
484
+ };
485
+ if (BINARY[fn] && a.length === 2)
486
+ return {
487
+ code: `${BINARY[fn]}((float)(${a[0].code}), (float)(${a[1].code}))`,
488
+ cType: 'float',
489
+ };
336
490
  throw new Error(`AOT: unsupported Math.${fn}(...) (arity ${a.length})`);
337
491
  }
338
- throw new Error('AOT: unsupported call expression in a dynamic expression');
492
+ throw new Error(
493
+ 'AOT: unsupported call expression in a dynamic expression',
494
+ );
339
495
  }
340
496
  }
341
- throw new Error(`AOT: unsupported expression "${node.type}" in a dynamic context`);
497
+ throw new Error(
498
+ `AOT: unsupported expression "${node.type}" in a dynamic context`,
499
+ );
342
500
  }
343
501
  const emitExpr = withLoc(emitExprImpl);
344
502
 
345
- const printfSpec = (cType) => (cType === 'string' ? '%s' : cType === 'float' ? '%g' : '%d');
346
- const cTypeOfValue = (v) => (typeof v === 'string' ? 'string' : typeof v === 'number' && !Number.isInteger(v) ? 'float' : 'int');
503
+ const printfSpec = cType =>
504
+ cType === 'string' ? '%s' : cType === 'float' ? '%g' : '%d';
505
+ const cTypeOfValue = v =>
506
+ typeof v === 'string'
507
+ ? 'string'
508
+ : typeof v === 'number' && !Number.isInteger(v)
509
+ ? 'float'
510
+ : 'int';
347
511
 
348
512
  // ---------------------------------------------------------------------------------------------------
349
513
  // AST helpers + collection passes — small predicates (isFn, fnReturnsJSX, …) and the up-front scans
350
514
  // that walk the component body ONCE to gather what later emission needs: the module scope, useState,
351
515
  // child components, and the useCallback / useMemo / useEffect / useRef hooks.
352
516
  // ---------------------------------------------------------------------------------------------------
353
- const isFn = (n) => n && (n.type === 'FunctionDeclaration' || n.type === 'FunctionExpression' || n.type === 'ArrowFunctionExpression');
517
+ const isFn = n =>
518
+ n &&
519
+ (n.type === 'FunctionDeclaration' ||
520
+ n.type === 'FunctionExpression' ||
521
+ n.type === 'ArrowFunctionExpression');
354
522
 
355
523
  function findComponent(program) {
356
524
  for (const stmt of program.body) {
357
525
  const d = stmt.type === 'ExportNamedDeclaration' ? stmt.declaration : stmt;
358
526
  if (!d) continue;
359
527
  if (d.type === 'FunctionDeclaration' && d.id?.name === 'App') return d;
360
- if (d.type === 'VariableDeclaration') for (const decl of d.declarations) if (decl.id?.name === 'App' && isFn(decl.init)) return decl.init;
528
+ if (d.type === 'VariableDeclaration')
529
+ for (const decl of d.declarations)
530
+ if (decl.id?.name === 'App' && isFn(decl.init)) return decl.init;
361
531
  }
362
- throw new Error('AOT: no `App` component found (expected `export function App() { ... }`)');
532
+ throw new Error(
533
+ 'AOT: no `App` component found (expected `export function App() { ... }`)',
534
+ );
363
535
  }
364
536
 
365
537
  // Target screen size, baked at compile time so the demo's responsive `screen.width`/`screen.height`
@@ -371,12 +543,18 @@ const SCREEN_H = Number(process.env.ER_AOT_SCREEN_H) || 480;
371
543
  function moduleScope(program, screen, seed = {}) {
372
544
  // `seed` pre-populates the scope (e.g. image imports as their asset-name strings) BEFORE module consts are
373
545
  // folded, so a const that references one — `const DAYS = [{ icon: wxSun }]` — folds correctly.
374
- const scope = { screen, ...seed };
546
+ const scope = {screen, ...seed};
375
547
  for (const stmt of program.body) {
376
548
  const d = stmt.type === 'ExportNamedDeclaration' ? stmt.declaration : stmt;
377
549
  if (d?.type !== 'VariableDeclaration') continue;
378
550
  for (const decl of d.declarations) {
379
- if (!decl.id || decl.id.type !== 'Identifier' || !decl.init || isFn(decl.init)) continue;
551
+ if (
552
+ !decl.id ||
553
+ decl.id.type !== 'Identifier' ||
554
+ !decl.init ||
555
+ isFn(decl.init)
556
+ )
557
+ continue;
380
558
  try {
381
559
  scope[decl.id.name] = evalStatic(decl.init, scope);
382
560
  } catch {
@@ -403,17 +581,27 @@ function inferItemStruct(items, name) {
403
581
  'a list state is a fixed-shape struct array: each element must be an OBJECT with the same string/number fields, ' +
404
582
  'e.g. useState([{ title: "A", n: 1 }, { title: "B", n: 2 }]). The first element defines the columns.';
405
583
  if (!Array.isArray(items) || !items.length)
406
- throw aotError(`AOT: list state "${name}" needs ≥1 initial element to infer its item shape`, shapeHint);
584
+ throw aotError(
585
+ `AOT: list state "${name}" needs ≥1 initial element to infer its item shape`,
586
+ shapeHint,
587
+ );
407
588
  const first = items[0];
408
589
  if (typeof first !== 'object' || first === null || Array.isArray(first))
409
- throw aotError(`AOT: list state "${name}" elements must be objects`, shapeHint);
410
- const fields = Object.keys(first).map((key) => {
590
+ throw aotError(
591
+ `AOT: list state "${name}" elements must be objects`,
592
+ shapeHint,
593
+ );
594
+ const fields = Object.keys(first).map(key => {
411
595
  const v = first[key];
412
- if (typeof v === 'string') return { key, kind: 'string' };
413
- if (typeof v === 'number') return { key, kind: Number.isInteger(v) ? 'int' : 'float' };
414
- throw aotError(`AOT: list state "${name}" field "${key}" must be a string or number`, shapeHint);
596
+ if (typeof v === 'string') return {key, kind: 'string'};
597
+ if (typeof v === 'number')
598
+ return {key, kind: Number.isInteger(v) ? 'int' : 'float'};
599
+ throw aotError(
600
+ `AOT: list state "${name}" field "${key}" must be a string or number`,
601
+ shapeHint,
602
+ );
415
603
  });
416
- return { fields };
604
+ return {fields};
417
605
  }
418
606
 
419
607
  /**
@@ -429,7 +617,12 @@ function collectState(fnBody, scope, prefix = '') {
429
617
  if (stmt.type !== 'VariableDeclaration') continue;
430
618
  for (const decl of stmt.declarations) {
431
619
  const init = decl.init;
432
- if (init?.type !== 'CallExpression' || init.callee.name !== 'useState' || decl.id.type !== 'ArrayPattern') continue;
620
+ if (
621
+ init?.type !== 'CallExpression' ||
622
+ init.callee.name !== 'useState' ||
623
+ decl.id.type !== 'ArrayPattern'
624
+ )
625
+ continue;
433
626
  const name = decl.id.elements[0]?.name;
434
627
  const setter = decl.id.elements[1]?.name;
435
628
  if (!name) continue;
@@ -452,7 +645,18 @@ function collectState(fnBody, scope, prefix = '') {
452
645
  if (initArg.loc && !e.aotLoc) e.aotLoc = initArg.loc.start; // collectState isn't withLoc-wrapped
453
646
  throw e;
454
647
  }
455
- rec = { name, cField, setter, kind: 'list', struct, items, cap: LIST_CAP, cTypeName: `ErItem_${cField}`, arrayName: `s_${cField}`, countMember: `s_${cField}_count` };
648
+ rec = {
649
+ name,
650
+ cField,
651
+ setter,
652
+ kind: 'list',
653
+ struct,
654
+ items,
655
+ cap: LIST_CAP,
656
+ cTypeName: `ErItem_${cField}`,
657
+ arrayName: `s_${cField}`,
658
+ countMember: `s_${cField}_count`,
659
+ };
456
660
  } else {
457
661
  const initVal = initArg
458
662
  ? evalStaticOrThrow(
@@ -466,41 +670,65 @@ function collectState(fnBody, scope, prefix = '') {
466
670
  // A numeric literal written with a decimal point or exponent (e.g. useState(70.0)) forces a FLOAT
467
671
  // slot even though the value is integral — lets the state hold sub-integer values (a smooth drag)
468
672
  // while the UI shows Math.round(value). (70.0 === 70 in JS, so we read the raw source to tell them apart.)
469
- if (cType === 'int' && initArg?.type === 'NumericLiteral' && typeof initArg.extra?.raw === 'string' && /[.eE]/.test(initArg.extra.raw)) {
673
+ if (
674
+ cType === 'int' &&
675
+ initArg?.type === 'NumericLiteral' &&
676
+ typeof initArg.extra?.raw === 'string' &&
677
+ /[.eE]/.test(initArg.extra.raw)
678
+ ) {
470
679
  cType = 'float';
471
680
  }
472
681
  // String scalar → a fixed char buffer in ErAppState; setters snprintf into it (see scalarAssign).
473
- const initCode = cType === 'string' ? cstr(String(initVal)) : cType === 'float' ? floatLit(initVal) : String(Number(initVal));
474
- rec = { name, cField, setter, kind: 'scalar', cType, cMember: `s_state.${cField}`, initCode };
682
+ const initCode =
683
+ cType === 'string'
684
+ ? cstr(String(initVal))
685
+ : cType === 'float'
686
+ ? floatLit(initVal)
687
+ : String(Number(initVal));
688
+ rec = {
689
+ name,
690
+ cField,
691
+ setter,
692
+ kind: 'scalar',
693
+ cType,
694
+ cMember: `s_state.${cField}`,
695
+ initCode,
696
+ };
475
697
  }
476
698
  byName.set(name, rec);
477
699
  if (setter) bySetter.set(setter, rec);
478
700
  }
479
701
  }
480
- return { byName, bySetter };
702
+ return {byName, bySetter};
481
703
  }
482
704
 
483
705
  function findReturnJSX(fnBody, scope = {}) {
484
706
  // Fold top-level `if (staticCond) return …` at compile time — responsive layouts switch on `screen`.
485
- const scan = (stmts) => {
707
+ const scan = stmts => {
486
708
  for (const stmt of stmts) {
487
709
  if (stmt.type === 'IfStatement') {
488
710
  let test;
489
711
  try {
490
712
  test = evalStatic(stmt.test, scope);
491
713
  } catch {
492
- throw new Error('AOT: a top-level `if` in the component must have a compile-time-constant test (e.g. on the `screen` global) — runtime layout branching is not supported');
714
+ throw new Error(
715
+ 'AOT: a top-level `if` in the component must have a compile-time-constant test (e.g. on the `screen` global) — runtime layout branching is not supported',
716
+ );
493
717
  }
494
718
  const branch = test ? stmt.consequent : stmt.alternate;
495
719
  if (branch) {
496
- const r = scan(branch.type === 'BlockStatement' ? branch.body : [branch]);
720
+ const r = scan(
721
+ branch.type === 'BlockStatement' ? branch.body : [branch],
722
+ );
497
723
  if (r) return r;
498
724
  }
499
725
  continue;
500
726
  }
501
727
  if (stmt.type === 'ReturnStatement' && stmt.argument) {
502
728
  if (stmt.argument.type === 'JSXElement') return stmt.argument;
503
- throw new Error(`AOT: the component must return a single JSX element (got ${stmt.argument.type})`);
729
+ throw new Error(
730
+ `AOT: the component must return a single JSX element (got ${stmt.argument.type})`,
731
+ );
504
732
  }
505
733
  }
506
734
  return null;
@@ -517,13 +745,25 @@ function componentReturnJSX(fn, scope = {}) {
517
745
  throw new Error('AOT: component body must return a JSX element');
518
746
  }
519
747
 
520
- const fnReturnsJSX = (fn) => fn.body.type === 'JSXElement' || (fn.body.type === 'BlockStatement' && fn.body.body.some((s) => s.type === 'ReturnStatement' && s.argument?.type === 'JSXElement'));
748
+ const fnReturnsJSX = fn =>
749
+ fn.body.type === 'JSXElement' ||
750
+ (fn.body.type === 'BlockStatement' &&
751
+ fn.body.body.some(
752
+ s => s.type === 'ReturnStatement' && s.argument?.type === 'JSXElement',
753
+ ));
521
754
 
522
755
  /** Collects top-level function components (name → fn node), excluding the `App` entry component. */
523
756
  /** Resolves a component definition expression to its function node, unwrapping memo(fn) / React.memo(fn). */
524
757
  function asComponentFn(node) {
525
758
  if (isFn(node)) return node;
526
- if (node?.type === 'CallExpression' && isFn(node.arguments[0]) && (node.callee.name === 'memo' || (node.callee.type === 'MemberExpression' && node.callee.property?.name === 'memo'))) return node.arguments[0];
759
+ if (
760
+ node?.type === 'CallExpression' &&
761
+ isFn(node.arguments[0]) &&
762
+ (node.callee.name === 'memo' ||
763
+ (node.callee.type === 'MemberExpression' &&
764
+ node.callee.property?.name === 'memo'))
765
+ )
766
+ return node.arguments[0];
527
767
  return null;
528
768
  }
529
769
 
@@ -532,7 +772,13 @@ function collectComponents(program) {
532
772
  for (const stmt of program.body) {
533
773
  const d = stmt.type === 'ExportNamedDeclaration' ? stmt.declaration : stmt;
534
774
  if (!d) continue;
535
- if (d.type === 'FunctionDeclaration' && d.id && d.id.name !== 'App' && fnReturnsJSX(d)) comps.set(d.id.name, d);
775
+ if (
776
+ d.type === 'FunctionDeclaration' &&
777
+ d.id &&
778
+ d.id.name !== 'App' &&
779
+ fnReturnsJSX(d)
780
+ )
781
+ comps.set(d.id.name, d);
536
782
  if (d.type === 'VariableDeclaration')
537
783
  for (const decl of d.declarations) {
538
784
  if (decl.id?.type !== 'Identifier' || decl.id.name === 'App') continue;
@@ -552,18 +798,22 @@ function collectComponents(program) {
552
798
  function collectHelpers(componentBody, program) {
553
799
  const helpers = new Map();
554
800
  const add = (name, fn) => {
555
- if (name && name !== 'App' && isFn(fn) && !fnReturnsJSX(fn)) helpers.set(name, fn);
801
+ if (name && name !== 'App' && isFn(fn) && !fnReturnsJSX(fn))
802
+ helpers.set(name, fn);
556
803
  };
557
804
  for (const stmt of program.body) {
558
805
  const d = stmt.type === 'ExportNamedDeclaration' ? stmt.declaration : stmt;
559
806
  if (!d) continue;
560
807
  if (d.type === 'FunctionDeclaration' && d.id) add(d.id.name, d);
561
- if (d.type === 'VariableDeclaration') for (const decl of d.declarations) if (decl.id?.type === 'Identifier') add(decl.id.name, decl.init);
808
+ if (d.type === 'VariableDeclaration')
809
+ for (const decl of d.declarations)
810
+ if (decl.id?.type === 'Identifier') add(decl.id.name, decl.init);
562
811
  }
563
812
  if (componentBody.type === 'BlockStatement') {
564
813
  for (const stmt of componentBody.body) {
565
814
  if (stmt.type !== 'VariableDeclaration') continue;
566
- for (const decl of stmt.declarations) if (decl.id?.type === 'Identifier') add(decl.id.name, decl.init);
815
+ for (const decl of stmt.declarations)
816
+ if (decl.id?.type === 'Identifier') add(decl.id.name, decl.init);
567
817
  }
568
818
  }
569
819
  return helpers;
@@ -580,17 +830,93 @@ const IMAGE_EXT_RE = /\.(png|jpe?g|webp|gif|bmp)$/i;
580
830
  function collectImageImports(program) {
581
831
  const byLocal = new Map();
582
832
  for (const stmt of program.body) {
583
- if (stmt.type !== 'ImportDeclaration' || typeof stmt.source.value !== 'string') continue;
833
+ if (
834
+ stmt.type !== 'ImportDeclaration' ||
835
+ typeof stmt.source.value !== 'string'
836
+ )
837
+ continue;
584
838
  const importPath = stmt.source.value;
585
839
  if (!IMAGE_EXT_RE.test(importPath)) continue;
586
840
  const name = importPath.split(/[\\/]/).pop().replace(IMAGE_EXT_RE, '');
587
841
  for (const spec of stmt.specifiers) {
588
- if (spec.type === 'ImportDefaultSpecifier') byLocal.set(spec.local.name, { name, importPath });
842
+ if (spec.type === 'ImportDefaultSpecifier')
843
+ byLocal.set(spec.local.name, {name, importPath});
589
844
  }
590
845
  }
591
846
  return byLocal;
592
847
  }
593
848
 
849
+ /** Collects `import x from './foo.svg'` → Map(localName → { name, importPath }). Unlike an image (whose
850
+ * bytes the CLI bakes AFTER compile, so compile only needs its asset name), an <Svg source> needs the
851
+ * GEOMETRY during compile — so the CLI bakes the .svg to a vector artifact up front (bakeSvgArtifacts) and
852
+ * passes it in as opts.svgArtifacts, keyed by `name`. emitSvgSource resolves the local → name → artifact. */
853
+ function collectSvgImports(program) {
854
+ const byLocal = new Map();
855
+ for (const stmt of program.body) {
856
+ if (
857
+ stmt.type !== 'ImportDeclaration' ||
858
+ typeof stmt.source.value !== 'string'
859
+ )
860
+ continue;
861
+ const importPath = stmt.source.value;
862
+ if (!/\.svg$/i.test(importPath)) continue;
863
+ const name = importPath
864
+ .split(/[\\/]/)
865
+ .pop()
866
+ .replace(/\.svg$/i, '');
867
+ for (const spec of stmt.specifiers) {
868
+ if (spec.type === 'ImportDefaultSpecifier')
869
+ byLocal.set(spec.local.name, {name, importPath});
870
+ }
871
+ }
872
+ return byLocal;
873
+ }
874
+
875
+ /** Bakes a Flow B app's <Svg source> imports → { assetName: artifact } via the SAME baker Flow A's esbuild
876
+ * .svg loader uses (svgToVector). This is the I/O half (reads .svg files) kept OUT of the pure compileSource;
877
+ * both CLI entries (aot/compile.mjs and the consumer cli.mjs) call it and hand the result to opts.svgArtifacts.
878
+ * @param {string} src App.jsx source text.
879
+ * @param {string} baseDir Directory the import paths are resolved against (the app's dir).
880
+ * @returns {Promise<Object>} name → { ops, paints, gradients, width, height }. */
881
+ export async function bakeSvgArtifacts(src, baseDir) {
882
+ const program = parse(src, {sourceType: 'module', plugins: ['jsx']}).program;
883
+ const imports = collectSvgImports(program);
884
+ if (imports.size === 0) return {};
885
+ const {svgToVector, svgToRaster, writeRasterPng} =
886
+ await import('../assets/bake-svg.mjs');
887
+ const artifacts = {};
888
+ for (const [, imp] of imports) {
889
+ const p = resolve(baseDir, imp.importPath);
890
+ if (!existsSync(p))
891
+ throw new Error(
892
+ `AOT: <Svg source> asset "${imp.name}" not found at ${p}`,
893
+ );
894
+ const svg = readFileSync(p, 'utf8');
895
+ const art = await svgToVector(svg);
896
+ // Raster fallback (Flow B): an SVG that uses features the vector baker can't represent is rasterized via
897
+ // resvg and baked as a PNG (emitSvgSource emits an Image node for a kind:'raster' artifact + registers the
898
+ // PNG into the AOT image baker), so the content renders instead of dropping.
899
+ if (art.dropped && art.dropped.length) {
900
+ console.warn(
901
+ `embedded-react: ${imp.name}.svg uses unsupported SVG feature(s) [${art.dropped.join(', ')}] — ` +
902
+ `rasterizing it as a fallback image (Flow B bakes the PNG into assets.generated.c). Simplify the SVG ` +
903
+ `to keep it a live vector.`,
904
+ );
905
+ const {width, height, png} = await svgToRaster(svg);
906
+ artifacts[imp.name] = {
907
+ kind: 'raster',
908
+ name: imp.name,
909
+ width,
910
+ height,
911
+ png: writeRasterPng(imp.name, png),
912
+ };
913
+ } else {
914
+ artifacts[imp.name] = art;
915
+ }
916
+ }
917
+ return artifacts;
918
+ }
919
+
594
920
  /** Collects `const fn = useCallback((...) => {...}, deps)` → Map(name → arrow fn node). Deps are ignored:
595
921
  * the AOT re-renders via its own dependency tracking, so useCallback only names a shared C handler. */
596
922
  function collectCallbacks(fnBody) {
@@ -600,7 +926,12 @@ function collectCallbacks(fnBody) {
600
926
  if (stmt.type !== 'VariableDeclaration') continue;
601
927
  for (const decl of stmt.declarations) {
602
928
  const init = decl.init;
603
- if (init?.type === 'CallExpression' && init.callee.name === 'useCallback' && decl.id.type === 'Identifier' && isFn(init.arguments[0])) {
929
+ if (
930
+ init?.type === 'CallExpression' &&
931
+ init.callee.name === 'useCallback' &&
932
+ decl.id.type === 'Identifier' &&
933
+ isFn(init.arguments[0])
934
+ ) {
604
935
  cbs.set(decl.id.name, init.arguments[0]);
605
936
  }
606
937
  }
@@ -616,9 +947,17 @@ function collectMemos(fnBody) {
616
947
  if (stmt.type !== 'VariableDeclaration') continue;
617
948
  for (const decl of stmt.declarations) {
618
949
  const init = decl.init;
619
- if (init?.type === 'CallExpression' && init.callee.name === 'useMemo' && decl.id.type === 'Identifier' && isFn(init.arguments[0])) {
950
+ if (
951
+ init?.type === 'CallExpression' &&
952
+ init.callee.name === 'useMemo' &&
953
+ decl.id.type === 'Identifier' &&
954
+ isFn(init.arguments[0])
955
+ ) {
620
956
  const body = init.arguments[0].body;
621
- if (body.type === 'BlockStatement') throw new Error(`AOT: useMemo for "${decl.id.name}" must be a single expression (for now)`);
957
+ if (body.type === 'BlockStatement')
958
+ throw new Error(
959
+ `AOT: useMemo for "${decl.id.name}" must be a single expression (for now)`,
960
+ );
622
961
  memos.set(decl.id.name, body);
623
962
  }
624
963
  }
@@ -633,9 +972,21 @@ function collectEffects(fnBody) {
633
972
  for (const stmt of fnBody.body) {
634
973
  if (stmt.type !== 'ExpressionStatement') continue;
635
974
  const call = stmt.expression;
636
- if (call?.type === 'CallExpression' && call.callee.type === 'Identifier' && call.callee.name === 'useEffect') {
637
- if (!isFn(call.arguments[0])) throw aotError('AOT: useEffect must take an inline function', 'write useEffect(() => { … }, []).');
638
- effects.push({ fn: call.arguments[0], deps: call.arguments[1], node: call });
975
+ if (
976
+ call?.type === 'CallExpression' &&
977
+ call.callee.type === 'Identifier' &&
978
+ call.callee.name === 'useEffect'
979
+ ) {
980
+ if (!isFn(call.arguments[0]))
981
+ throw aotError(
982
+ 'AOT: useEffect must take an inline function',
983
+ 'write useEffect(() => { … }, []).',
984
+ );
985
+ effects.push({
986
+ fn: call.arguments[0],
987
+ deps: call.arguments[1],
988
+ node: call,
989
+ });
639
990
  }
640
991
  }
641
992
  return effects;
@@ -644,7 +995,15 @@ function collectEffects(fnBody) {
644
995
  /** True if a function component declares any useState (per-instance child state — not yet supported). */
645
996
  function usesState(fn) {
646
997
  if (fn.body.type !== 'BlockStatement') return false;
647
- return fn.body.body.some((s) => s.type === 'VariableDeclaration' && s.declarations.some((d) => d.init?.type === 'CallExpression' && d.init.callee.name === 'useState'));
998
+ return fn.body.body.some(
999
+ s =>
1000
+ s.type === 'VariableDeclaration' &&
1001
+ s.declarations.some(
1002
+ d =>
1003
+ d.init?.type === 'CallExpression' &&
1004
+ d.init.callee.name === 'useState',
1005
+ ),
1006
+ );
648
1007
  }
649
1008
 
650
1009
  // ---------------------------------------------------------------------------------------------------
@@ -663,9 +1022,18 @@ function collectAnims(fnBody, scope, prefix = '') {
663
1022
  if (stmt.type !== 'VariableDeclaration') continue;
664
1023
  for (const decl of stmt.declarations) {
665
1024
  const init = decl.init;
666
- if (init?.type === 'CallExpression' && init.callee.name === 'useAnimatedValue' && decl.id.type === 'Identifier') {
667
- const initVal = init.arguments[0] ? evalStatic(init.arguments[0], scope) : 0;
668
- anims.set(decl.id.name, { cVar: `s_av_${prefix}${decl.id.name}`, initCode: floatLit(initVal) });
1025
+ if (
1026
+ init?.type === 'CallExpression' &&
1027
+ init.callee.name === 'useAnimatedValue' &&
1028
+ decl.id.type === 'Identifier'
1029
+ ) {
1030
+ const initVal = init.arguments[0]
1031
+ ? evalStatic(init.arguments[0], scope)
1032
+ : 0;
1033
+ anims.set(decl.id.name, {
1034
+ cVar: `s_av_${prefix}${decl.id.name}`,
1035
+ initCode: floatLit(initVal),
1036
+ });
669
1037
  }
670
1038
  }
671
1039
  }
@@ -686,17 +1054,34 @@ function collectRefs(fnBody, scope, prefix = '') {
686
1054
  if (stmt.type !== 'VariableDeclaration') continue;
687
1055
  for (const decl of stmt.declarations) {
688
1056
  const init = decl.init;
689
- if (init?.type === 'CallExpression' && init.callee.name === 'useRef' && decl.id.type === 'Identifier') {
1057
+ if (
1058
+ init?.type === 'CallExpression' &&
1059
+ init.callee.name === 'useRef' &&
1060
+ decl.id.type === 'Identifier'
1061
+ ) {
690
1062
  const cVar = `s_ref_${prefix}${decl.id.name}`;
691
1063
  const arg = init.arguments[0];
692
- if (!arg || (arg.type === 'NullLiteral')) {
693
- refs.set(decl.id.name, { cVar, cType: 'ERNode*', initCode: 'NULL', kind: 'node' });
1064
+ if (!arg || arg.type === 'NullLiteral') {
1065
+ refs.set(decl.id.name, {
1066
+ cVar,
1067
+ cType: 'ERNode*',
1068
+ initCode: 'NULL',
1069
+ kind: 'node',
1070
+ });
694
1071
  continue;
695
1072
  }
696
1073
  const v = evalStatic(arg, scope);
697
- if (typeof v !== 'number') throw new Error(`AOT: useRef initial for "${decl.id.name}" must be a number (value ref) or null/empty (node ref)`);
1074
+ if (typeof v !== 'number')
1075
+ throw new Error(
1076
+ `AOT: useRef initial for "${decl.id.name}" must be a number (value ref) or null/empty (node ref)`,
1077
+ );
698
1078
  const cType = Number.isInteger(v) ? 'int' : 'float';
699
- refs.set(decl.id.name, { cVar, cType, initCode: cType === 'float' ? `${v}f` : String(v), kind: 'value' });
1079
+ refs.set(decl.id.name, {
1080
+ cVar,
1081
+ cType,
1082
+ initCode: cType === 'float' ? `${v}f` : String(v),
1083
+ kind: 'value',
1084
+ });
700
1085
  }
701
1086
  }
702
1087
  }
@@ -707,12 +1092,17 @@ function collectRefs(fnBody, scope, prefix = '') {
707
1092
  function resolveTag(openingElement) {
708
1093
  const n = openingElement.name;
709
1094
  if (n.type === 'JSXIdentifier') return n.name;
710
- if (n.type === 'JSXMemberExpression' && n.object.name === 'Animated') return n.property.name; // Animated.View → View
1095
+ if (n.type === 'JSXMemberExpression' && n.object.name === 'Animated')
1096
+ return n.property.name; // Animated.View → View
711
1097
  throw new Error('AOT: unsupported JSX tag expression');
712
1098
  }
713
1099
 
714
1100
  /** Style key → the ERAnimProp(s) an animated value binds to. */
715
- const ANIM_STYLE_PROPS = { opacity: ['ER_PROP_OPACITY'], backgroundColor: ['ER_PROP_BACKGROUND_COLOR'], color: ['ER_PROP_COLOR'] };
1101
+ const ANIM_STYLE_PROPS = {
1102
+ opacity: ['ER_PROP_OPACITY'],
1103
+ backgroundColor: ['ER_PROP_BACKGROUND_COLOR'],
1104
+ color: ['ER_PROP_COLOR'],
1105
+ };
716
1106
  const ANIM_TRANSFORM_PROPS = {
717
1107
  scale: ['ER_PROP_SCALE_X', 'ER_PROP_SCALE_Y'],
718
1108
  scaleX: ['ER_PROP_SCALE_X'],
@@ -742,25 +1132,40 @@ function easingFamily(node) {
742
1132
  * (linear/ease/quad/cubic/bounce/elastic), the in/out/inOut wrappers around quad/cubic, and
743
1133
  * Easing.bezier(x1,y1,x2,y2). No easing → ER_EASE_EASE_IN_OUT (RN's timing default); unknown → same. */
744
1134
  function easingInfo(node, env) {
745
- const FALLBACK = { ease: 'ER_EASE_EASE_IN_OUT', bezier: null };
1135
+ const FALLBACK = {ease: 'ER_EASE_EASE_IN_OUT', bezier: null};
746
1136
  if (!node) return FALLBACK;
747
1137
  // Bare member: Easing.linear / Easing.ease / Easing.quad (== quad-in) / ...
748
1138
  if (node.type === 'MemberExpression' && node.object?.name === 'Easing') {
749
- const m = { linear: 'ER_EASE_LINEAR', ease: 'ER_EASE_EASE', quad: 'ER_EASE_QUAD_IN', cubic: 'ER_EASE_CUBIC_IN', bounce: 'ER_EASE_BOUNCE_OUT', elastic: 'ER_EASE_ELASTIC_OUT' };
750
- return { ease: m[node.property.name] || 'ER_EASE_EASE_IN_OUT', bezier: null };
1139
+ const m = {
1140
+ linear: 'ER_EASE_LINEAR',
1141
+ ease: 'ER_EASE_EASE',
1142
+ quad: 'ER_EASE_QUAD_IN',
1143
+ cubic: 'ER_EASE_CUBIC_IN',
1144
+ bounce: 'ER_EASE_BOUNCE_OUT',
1145
+ elastic: 'ER_EASE_ELASTIC_OUT',
1146
+ };
1147
+ return {ease: m[node.property.name] || 'ER_EASE_EASE_IN_OUT', bezier: null};
751
1148
  }
752
1149
  // Call: Easing.bezier(...), Easing.elastic(n), Easing.in/out/inOut(inner)
753
- if (node.type === 'CallExpression' && node.callee.type === 'MemberExpression' && node.callee.object?.name === 'Easing') {
1150
+ if (
1151
+ node.type === 'CallExpression' &&
1152
+ node.callee.type === 'MemberExpression' &&
1153
+ node.callee.object?.name === 'Easing'
1154
+ ) {
754
1155
  const fn = node.callee.property.name;
755
1156
  if (fn === 'bezier') {
756
- const cps = node.arguments.slice(0, 4).map((a) => Number(evalStaticOr(a, env, 0)));
757
- return cps.length === 4 ? { ease: 'ER_EASE_BEZIER', bezier: cps } : FALLBACK;
1157
+ const cps = node.arguments
1158
+ .slice(0, 4)
1159
+ .map(a => Number(evalStaticOr(a, env, 0)));
1160
+ return cps.length === 4
1161
+ ? {ease: 'ER_EASE_BEZIER', bezier: cps}
1162
+ : FALLBACK;
758
1163
  }
759
- if (fn === 'elastic') return { ease: 'ER_EASE_ELASTIC_OUT', bezier: null };
1164
+ if (fn === 'elastic') return {ease: 'ER_EASE_ELASTIC_OUT', bezier: null};
760
1165
  if (fn === 'in' || fn === 'out' || fn === 'inOut') {
761
1166
  const fam = easingFamily(node.arguments[0]);
762
1167
  const dir = fn === 'in' ? 'IN' : fn === 'out' ? 'OUT' : 'IN_OUT';
763
- return fam ? { ease: `ER_EASE_${fam}_${dir}`, bezier: null } : FALLBACK;
1168
+ return fam ? {ease: `ER_EASE_${fam}_${dir}`, bezier: null} : FALLBACK;
764
1169
  }
765
1170
  }
766
1171
  return FALLBACK;
@@ -768,33 +1173,57 @@ function easingInfo(node, env) {
768
1173
 
769
1174
  /** Pushes `cfg.easing = …;` (and bezier control points for Easing.bezier) onto a timing config's C lines. */
770
1175
  function pushEasing(lines, c, easingNode, env) {
771
- const { ease, bezier } = easingInfo(easingNode, env);
1176
+ const {ease, bezier} = easingInfo(easingNode, env);
772
1177
  lines.push(` ${c}.easing = ${ease};`);
773
1178
  if (bezier) {
774
- lines.push(` ${c}.bezier_x1 = ${floatLit(bezier[0])}; ${c}.bezier_y1 = ${floatLit(bezier[1])};`);
775
- lines.push(` ${c}.bezier_x2 = ${floatLit(bezier[2])}; ${c}.bezier_y2 = ${floatLit(bezier[3])};`);
1179
+ lines.push(
1180
+ ` ${c}.bezier_x1 = ${floatLit(bezier[0])}; ${c}.bezier_y1 = ${floatLit(bezier[1])};`,
1181
+ );
1182
+ lines.push(
1183
+ ` ${c}.bezier_x2 = ${floatLit(bezier[2])}; ${c}.bezier_y2 = ${floatLit(bezier[3])};`,
1184
+ );
776
1185
  }
777
1186
  }
778
1187
 
779
1188
  /** Parses a `.interpolate({ inputRange, outputRange, extrapolate })` config object → a static
780
1189
  * { input, output, exLeft, exRight } descriptor (ranges must be static, equal-length, 2..8 points). */
781
1190
  function parseInterp(cfgNode, env) {
782
- if (cfgNode?.type !== 'ObjectExpression') throw aotError('AOT: .interpolate() needs a config object literal { inputRange, outputRange }');
783
- const get = (k) => cfgNode.properties.find((p) => (p.key.name ?? p.key.value) === k)?.value;
1191
+ if (cfgNode?.type !== 'ObjectExpression')
1192
+ throw aotError(
1193
+ 'AOT: .interpolate() needs a config object literal { inputRange, outputRange }',
1194
+ );
1195
+ const get = k =>
1196
+ cfgNode.properties.find(p => (p.key.name ?? p.key.value) === k)?.value;
784
1197
  const arr = (node, name) => {
785
- if (node?.type !== 'ArrayExpression') throw aotError(`AOT: .interpolate() ${name} must be an array literal`);
786
- return node.elements.map((e) => Number(evalStatic(e, env.consts ?? {})));
1198
+ if (node?.type !== 'ArrayExpression')
1199
+ throw aotError(`AOT: .interpolate() ${name} must be an array literal`);
1200
+ return node.elements.map(e => Number(evalStatic(e, env.consts ?? {})));
787
1201
  };
788
1202
  const input = arr(get('inputRange'), 'inputRange');
789
1203
  const output = arr(get('outputRange'), 'outputRange');
790
- if (input.length < 2 || input.length !== output.length) throw aotError('AOT: .interpolate() inputRange and outputRange must be the same length (>= 2)');
791
- if (input.length > 8) throw aotError('AOT: .interpolate() supports up to 8 breakpoints (ER_INTERPOLATE_MAX_POINTS)');
792
- const ex = (node) => {
1204
+ if (input.length < 2 || input.length !== output.length)
1205
+ throw aotError(
1206
+ 'AOT: .interpolate() inputRange and outputRange must be the same length (>= 2)',
1207
+ );
1208
+ if (input.length > 8)
1209
+ throw aotError(
1210
+ 'AOT: .interpolate() supports up to 8 breakpoints (ER_INTERPOLATE_MAX_POINTS)',
1211
+ );
1212
+ const ex = node => {
793
1213
  const v = node ? String(evalStaticOr(node, env, 'extend')) : 'extend';
794
- return v === 'clamp' ? 'ER_EXTRAPOLATE_CLAMP' : v === 'identity' ? 'ER_EXTRAPOLATE_IDENTITY' : 'ER_EXTRAPOLATE_EXTEND';
1214
+ return v === 'clamp'
1215
+ ? 'ER_EXTRAPOLATE_CLAMP'
1216
+ : v === 'identity'
1217
+ ? 'ER_EXTRAPOLATE_IDENTITY'
1218
+ : 'ER_EXTRAPOLATE_EXTEND';
795
1219
  };
796
1220
  const both = get('extrapolate'); // RN: `extrapolate` sets both ends; extrapolateLeft/Right override.
797
- return { input, output, exLeft: ex(get('extrapolateLeft') ?? both), exRight: ex(get('extrapolateRight') ?? both) };
1221
+ return {
1222
+ input,
1223
+ output,
1224
+ exLeft: ex(get('extrapolateLeft') ?? both),
1225
+ exRight: ex(get('extrapolateRight') ?? both),
1226
+ };
798
1227
  }
799
1228
 
800
1229
  // ---------------------------------------------------------------------------------------------------
@@ -802,7 +1231,7 @@ function parseInterp(cfgNode, env) {
802
1231
  // ---------------------------------------------------------------------------------------------------
803
1232
  function attrExpr(attr) {
804
1233
  const v = attr.value;
805
- if (!v) return { type: 'BooleanLiteral', value: true };
1234
+ if (!v) return {type: 'BooleanLiteral', value: true};
806
1235
  if (v.type === 'StringLiteral') return v;
807
1236
  if (v.type === 'JSXExpressionContainer') return v.expression;
808
1237
  return v;
@@ -810,7 +1239,14 @@ function attrExpr(attr) {
810
1239
 
811
1240
  /** A static ARGB8888 C literal (`0xAARRGGBBu`) from a CSS color string or number. */
812
1241
  function argbLiteral(value) {
813
- return '0x' + (parseColor(String(value)) >>> 0).toString(16).padStart(8, '0').toUpperCase() + 'u';
1242
+ return (
1243
+ '0x' +
1244
+ (parseColor(String(value)) >>> 0)
1245
+ .toString(16)
1246
+ .padStart(8, '0')
1247
+ .toUpperCase() +
1248
+ 'u'
1249
+ );
814
1250
  }
815
1251
 
816
1252
  /** Lowers a dynamic (state-referencing) color expression to a C ARGB expression. */
@@ -827,7 +1263,9 @@ function emitColorExpr(node, env) {
827
1263
  } catch {
828
1264
  /* not static — fall through to the error below */
829
1265
  }
830
- throw new Error('AOT: a dynamic color must be a color string literal or a ternary of them');
1266
+ throw new Error(
1267
+ 'AOT: a dynamic color must be a color string literal or a ternary of them',
1268
+ );
831
1269
  }
832
1270
 
833
1271
  /** Lowers a dynamic enum-style expression (e.g. `flexDirection: row ? 'row' : 'column'`) to its ER_* constant
@@ -835,7 +1273,11 @@ function emitColorExpr(node, env) {
835
1273
  function emitEnumExpr(node, table, env) {
836
1274
  if (node.type === 'StringLiteral') {
837
1275
  const c = table[node.value];
838
- if (!c) throw aotError(`AOT: unsupported enum value "${node.value}"`, `one of: ${Object.keys(table).join(', ')}`);
1276
+ if (!c)
1277
+ throw aotError(
1278
+ `AOT: unsupported enum value "${node.value}"`,
1279
+ `one of: ${Object.keys(table).join(', ')}`,
1280
+ );
839
1281
  return c;
840
1282
  }
841
1283
  if (node.type === 'ConditionalExpression') {
@@ -849,17 +1291,36 @@ function emitEnumExpr(node, table, env) {
849
1291
  } catch {
850
1292
  /* not static — fall through */
851
1293
  }
852
- throw aotError('AOT: a state-driven enum style must be a string literal or a ternary of them', "e.g. flexDirection: wide ? 'row' : 'column'");
1294
+ throw aotError(
1295
+ 'AOT: a state-driven enum style must be a string literal or a ternary of them',
1296
+ "e.g. flexDirection: wide ? 'row' : 'column'",
1297
+ );
853
1298
  }
854
1299
 
855
1300
  /** Lowers one dynamic inline-style value to ERProps field assignment(s) (C expressions). */
856
1301
  function lowerDynamicStyleValue(key, valueNode, env) {
857
1302
  const meta = DYN_FIELDS[key];
858
- if (!meta) throw aotError(`AOT: a state-driven value for style "${key}" is not supported (static only)`, `state-driven styles supported: colors, opacity, sizes/margins/padding, and the layout enums (flexDirection, alignItems, alignSelf, justifyContent, position). Make "${key}" static, or drive the change another way.`);
859
- if (meta.kind === 'color') return [{ field: meta.field, code: emitColorExpr(valueNode, env) }];
860
- if (meta.kind === 'opacity') return [{ field: meta.field, code: `(uint8_t)((${emitExpr(valueNode, env).code}) * 255.0f)` }];
861
- if (meta.kind === 'enum') return [{ field: meta.field, code: emitEnumExpr(valueNode, meta.table, env) }];
862
- return [{ field: meta.field, code: `(int16_t)(${emitExpr(valueNode, env).code})` }]; /* num */
1303
+ if (!meta)
1304
+ throw aotError(
1305
+ `AOT: a state-driven value for style "${key}" is not supported (static only)`,
1306
+ `state-driven styles supported: colors, opacity, sizes/margins/padding, and the layout enums (flexDirection, alignItems, alignSelf, justifyContent, position). Make "${key}" static, or drive the change another way.`,
1307
+ );
1308
+ if (meta.kind === 'color')
1309
+ return [{field: meta.field, code: emitColorExpr(valueNode, env)}];
1310
+ if (meta.kind === 'opacity')
1311
+ return [
1312
+ {
1313
+ field: meta.field,
1314
+ code: `(uint8_t)((${emitExpr(valueNode, env).code}) * 255.0f)`,
1315
+ },
1316
+ ];
1317
+ if (meta.kind === 'enum')
1318
+ return [
1319
+ {field: meta.field, code: emitEnumExpr(valueNode, meta.table, env)},
1320
+ ];
1321
+ return [
1322
+ {field: meta.field, code: `(int16_t)(${emitExpr(valueNode, env).code})`},
1323
+ ]; /* num */
863
1324
  }
864
1325
 
865
1326
  /**
@@ -870,34 +1331,54 @@ function lowerDynamicStyleValue(key, valueNode, env) {
870
1331
  function collectStyleAssigns(openingElement, scope, env) {
871
1332
  const fields = new Map(); // ERProps field -> { dynamic: bool, code: string }
872
1333
  const binds = []; // [{ cVar, prop, interp? }] — animated values bound to node properties (native driver)
873
- const animRef = (node) => (node?.type === 'Identifier' && env.anims?.has(node.name) ? env.anims.get(node.name).cVar : null);
1334
+ const animRef = node =>
1335
+ node?.type === 'Identifier' && env.anims?.has(node.name)
1336
+ ? env.anims.get(node.name).cVar
1337
+ : null;
874
1338
  // `<animValue>.interpolate({ inputRange, outputRange, extrapolate })` → { cVar, interp } for a mapped bind.
875
- const animInterpRef = (node) => {
876
- if (node?.type === 'CallExpression' && node.callee.type === 'MemberExpression' && !node.callee.computed && node.callee.property.name === 'interpolate' && node.callee.object.type === 'Identifier' && env.anims?.has(node.callee.object.name)) {
877
- return { cVar: env.anims.get(node.callee.object.name).cVar, interp: parseInterp(node.arguments[0], env) };
1339
+ const animInterpRef = node => {
1340
+ if (
1341
+ node?.type === 'CallExpression' &&
1342
+ node.callee.type === 'MemberExpression' &&
1343
+ !node.callee.computed &&
1344
+ node.callee.property.name === 'interpolate' &&
1345
+ node.callee.object.type === 'Identifier' &&
1346
+ env.anims?.has(node.callee.object.name)
1347
+ ) {
1348
+ return {
1349
+ cVar: env.anims.get(node.callee.object.name).cVar,
1350
+ interp: parseInterp(node.arguments[0], env),
1351
+ };
878
1352
  }
879
1353
  return null;
880
1354
  };
881
- const apply = (expr) => {
1355
+ const apply = expr => {
882
1356
  if (expr.type === 'ArrayExpression') {
883
1357
  for (const e of expr.elements) if (e) apply(e);
884
1358
  return;
885
1359
  }
886
1360
  if (expr.type === 'ObjectExpression') {
887
1361
  for (const prop of expr.properties) {
888
- if (prop.type !== 'ObjectProperty') throw new Error('AOT: spread/method in an inline style object not supported');
889
- const key = prop.computed ? evalStatic(prop.key, scope) : prop.key.name ?? prop.key.value;
1362
+ if (prop.type !== 'ObjectProperty')
1363
+ throw new Error(
1364
+ 'AOT: spread/method in an inline style object not supported',
1365
+ );
1366
+ const key = prop.computed
1367
+ ? evalStatic(prop.key, scope)
1368
+ : (prop.key.name ?? prop.key.value);
890
1369
 
891
1370
  // Animated value bound directly to a prop (opacity / backgroundColor / color), optionally through
892
1371
  // an .interpolate({ inputRange, outputRange }) mapping.
893
1372
  const av = animRef(prop.value);
894
1373
  if (av && ANIM_STYLE_PROPS[key]) {
895
- for (const p of ANIM_STYLE_PROPS[key]) binds.push({ cVar: av, prop: p });
1374
+ for (const p of ANIM_STYLE_PROPS[key])
1375
+ binds.push({cVar: av, prop: p});
896
1376
  continue;
897
1377
  }
898
1378
  const ai = animInterpRef(prop.value);
899
1379
  if (ai && ANIM_STYLE_PROPS[key]) {
900
- for (const p of ANIM_STYLE_PROPS[key]) binds.push({ cVar: ai.cVar, prop: p, interp: ai.interp });
1380
+ for (const p of ANIM_STYLE_PROPS[key])
1381
+ binds.push({cVar: ai.cVar, prop: p, interp: ai.interp});
901
1382
  continue;
902
1383
  }
903
1384
  // transform: [{ scale: <anim> }, { translateX: <anim>.interpolate(...) }, ...] — bind each entry.
@@ -909,13 +1390,15 @@ function collectStyleAssigns(openingElement, scope, env) {
909
1390
  const tk = tp.key.name ?? tp.key.value;
910
1391
  const tav = animRef(tp.value);
911
1392
  if (tav && ANIM_TRANSFORM_PROPS[tk]) {
912
- for (const p of ANIM_TRANSFORM_PROPS[tk]) binds.push({ cVar: tav, prop: p });
1393
+ for (const p of ANIM_TRANSFORM_PROPS[tk])
1394
+ binds.push({cVar: tav, prop: p});
913
1395
  handled = true;
914
1396
  continue;
915
1397
  }
916
1398
  const tai = animInterpRef(tp.value);
917
1399
  if (tai && ANIM_TRANSFORM_PROPS[tk]) {
918
- for (const p of ANIM_TRANSFORM_PROPS[tk]) binds.push({ cVar: tai.cVar, prop: p, interp: tai.interp });
1400
+ for (const p of ANIM_TRANSFORM_PROPS[tk])
1401
+ binds.push({cVar: tai.cVar, prop: p, interp: tai.interp});
919
1402
  handled = true;
920
1403
  }
921
1404
  }
@@ -924,15 +1407,18 @@ function collectStyleAssigns(openingElement, scope, env) {
924
1407
  }
925
1408
 
926
1409
  try {
927
- for (const a of lowerStyle({ [key]: evalStatic(prop.value, scope) })) fields.set(a.field, { dynamic: false, code: a.expr });
1410
+ for (const a of lowerStyle({[key]: evalStatic(prop.value, scope)}))
1411
+ fields.set(a.field, {dynamic: false, code: a.expr});
928
1412
  } catch {
929
- for (const a of lowerDynamicStyleValue(key, prop.value, env)) fields.set(a.field, { dynamic: true, code: a.code });
1413
+ for (const a of lowerDynamicStyleValue(key, prop.value, env))
1414
+ fields.set(a.field, {dynamic: true, code: a.code});
930
1415
  }
931
1416
  }
932
1417
  return;
933
1418
  }
934
1419
  // A StyleSheet reference / identifier resolving to a static style object.
935
- for (const a of lowerStyle(evalStatic(expr, scope))) fields.set(a.field, { dynamic: false, code: a.expr });
1420
+ for (const a of lowerStyle(evalStatic(expr, scope)))
1421
+ fields.set(a.field, {dynamic: false, code: a.expr});
936
1422
  };
937
1423
  for (const attr of openingElement.attributes) {
938
1424
  if (attr.type !== 'JSXAttribute' || attr.name.name !== 'style') continue;
@@ -940,8 +1426,11 @@ function collectStyleAssigns(openingElement, scope, env) {
940
1426
  }
941
1427
  const staticAssigns = [];
942
1428
  const dynAssigns = [];
943
- for (const [field, v] of fields) (v.dynamic ? dynAssigns : staticAssigns).push(v.dynamic ? { field, code: v.code } : { field, expr: v.code });
944
- return { staticAssigns, dynAssigns, binds };
1429
+ for (const [field, v] of fields)
1430
+ (v.dynamic ? dynAssigns : staticAssigns).push(
1431
+ v.dynamic ? {field, code: v.code} : {field, expr: v.code},
1432
+ );
1433
+ return {staticAssigns, dynAssigns, binds};
945
1434
  }
946
1435
 
947
1436
  const EVENT_TYPES = {
@@ -955,7 +1444,8 @@ const EVENT_TYPES = {
955
1444
  onLayout: 'ER_EVENT_LAYOUT',
956
1445
  };
957
1446
 
958
- const cstr = (s) => `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n').replace(/\t/g, '\\t')}"`;
1447
+ const cstr = s =>
1448
+ `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n').replace(/\t/g, '\\t')}"`;
959
1449
 
960
1450
  /**
961
1451
  * Builds a Text node's content. Static interpolations fold into the literal; any that reference state
@@ -967,13 +1457,18 @@ function buildText(children, scope, env) {
967
1457
  let dynamic = false;
968
1458
  for (const child of children) {
969
1459
  if (child.type === 'JSXText') {
970
- const t = /\n/.test(child.value) ? child.value.replace(/\s+/g, ' ').trim() : child.value;
1460
+ const t = /\n/.test(child.value)
1461
+ ? child.value.replace(/\s+/g, ' ').trim()
1462
+ : child.value;
971
1463
  format += t.replace(/%/g, '%%');
972
1464
  } else if (child.type === 'JSXExpressionContainer') {
973
1465
  if (child.expression.type === 'JSXEmptyExpression') continue;
974
1466
  try {
975
1467
  const v = evalStatic(child.expression, scope); // constant → fold in
976
- format += (v === undefined || v === null ? '' : String(v)).replace(/%/g, '%%');
1468
+ format += (v === undefined || v === null ? '' : String(v)).replace(
1469
+ /%/g,
1470
+ '%%',
1471
+ );
977
1472
  } catch {
978
1473
  const e = emitExpr(child.expression, env); // references state → dynamic
979
1474
  format += printfSpec(e.cType);
@@ -981,10 +1476,12 @@ function buildText(children, scope, env) {
981
1476
  dynamic = true;
982
1477
  }
983
1478
  } else if (child.type === 'JSXElement') {
984
- throw new Error('AOT: nested <Text> / element children inside <Text> not yet supported (spans)');
1479
+ throw new Error(
1480
+ 'AOT: nested <Text> / element children inside <Text> not yet supported (spans)',
1481
+ );
985
1482
  }
986
1483
  }
987
- return { dynamic, format, args };
1484
+ return {dynamic, format, args};
988
1485
  }
989
1486
 
990
1487
  /** Normalises JSX text the way Babel does: trim per-line, drop blank lines, join with single spaces; a
@@ -1008,10 +1505,16 @@ function staticTextContent(children, scope) {
1008
1505
  let s = '';
1009
1506
  for (const c of children) {
1010
1507
  if (c.type === 'JSXText') s += cleanJsxText(c.value);
1011
- else if (c.type === 'JSXExpressionContainer' && c.expression.type !== 'JSXEmptyExpression') {
1508
+ else if (
1509
+ c.type === 'JSXExpressionContainer' &&
1510
+ c.expression.type !== 'JSXEmptyExpression'
1511
+ ) {
1012
1512
  const v = evalStatic(c.expression, scope); // throws if it references state
1013
1513
  if (v !== undefined && v !== null) s += String(v);
1014
- } else if (c.type === 'JSXElement') throw aotError('AOT: a nested <Text> span may not itself contain another <Text> (one level of spans only)');
1514
+ } else if (c.type === 'JSXElement')
1515
+ throw aotError(
1516
+ 'AOT: a nested <Text> span may not itself contain another <Text> (one level of spans only)',
1517
+ );
1015
1518
  }
1016
1519
  return s;
1017
1520
  }
@@ -1023,9 +1526,22 @@ function staticTextContent(children, scope) {
1023
1526
  * a dynamic {…} segment or a state-driven span style throws.
1024
1527
  */
1025
1528
  function collectTextSpans(children, scope, env) {
1026
- if (!children.some((c) => c.type === 'JSXElement' && c.openingElement.name.name === 'Text')) return null;
1529
+ if (
1530
+ !children.some(
1531
+ c => c.type === 'JSXElement' && c.openingElement.name.name === 'Text',
1532
+ )
1533
+ )
1534
+ return null;
1027
1535
  // Inherit sentinels (see ERTextSpan doc): color 0, font_size 0, weight/style/decoration 0xFF, spacing AUTO.
1028
- const inheritSpan = (text) => ({ text, color: '0u', font_size: '0', font_weight: '0xFF', font_style: '0xFF', text_decoration: '0xFF', letter_spacing: 'ER_LAYOUT_AUTO' });
1536
+ const inheritSpan = text => ({
1537
+ text,
1538
+ color: '0u',
1539
+ font_size: '0',
1540
+ font_weight: '0xFF',
1541
+ font_style: '0xFF',
1542
+ text_decoration: '0xFF',
1543
+ letter_spacing: 'ER_LAYOUT_AUTO',
1544
+ });
1029
1545
  const spans = [];
1030
1546
  for (const c of children) {
1031
1547
  if (c.type === 'JSXText') {
@@ -1037,13 +1553,29 @@ function collectTextSpans(children, scope, env) {
1037
1553
  try {
1038
1554
  v = evalStatic(c.expression, scope);
1039
1555
  } catch {
1040
- throw aotError('AOT: a dynamic {…} segment inside a multi-span <Text> is not supported', 'spans must be static; keep dynamic text in its own single <Text> (no nested <Text> siblings).');
1556
+ throw aotError(
1557
+ 'AOT: a dynamic {…} segment inside a multi-span <Text> is not supported',
1558
+ 'spans must be static; keep dynamic text in its own single <Text> (no nested <Text> siblings).',
1559
+ );
1041
1560
  }
1042
- if (v !== undefined && v !== null) spans.push(inheritSpan(cstr(String(v))));
1043
- } else if (c.type === 'JSXElement' && c.openingElement.name.name === 'Text') {
1044
- const { staticAssigns, dynAssigns } = collectStyleAssigns(c.openingElement, scope, env);
1045
- if (dynAssigns.length) throw aotError('AOT: a state-driven style on a nested <Text> span is not supported', 'give the span <Text> a static style.');
1046
- const field = (f, dflt) => staticAssigns.find((a) => a.field === f)?.expr ?? dflt;
1561
+ if (v !== undefined && v !== null)
1562
+ spans.push(inheritSpan(cstr(String(v))));
1563
+ } else if (
1564
+ c.type === 'JSXElement' &&
1565
+ c.openingElement.name.name === 'Text'
1566
+ ) {
1567
+ const {staticAssigns, dynAssigns} = collectStyleAssigns(
1568
+ c.openingElement,
1569
+ scope,
1570
+ env,
1571
+ );
1572
+ if (dynAssigns.length)
1573
+ throw aotError(
1574
+ 'AOT: a state-driven style on a nested <Text> span is not supported',
1575
+ 'give the span <Text> a static style.',
1576
+ );
1577
+ const field = (f, dflt) =>
1578
+ staticAssigns.find(a => a.field === f)?.expr ?? dflt;
1047
1579
  spans.push({
1048
1580
  text: cstr(staticTextContent(c.children, scope)),
1049
1581
  color: field('color', '0u'),
@@ -1053,11 +1585,18 @@ function collectTextSpans(children, scope, env) {
1053
1585
  text_decoration: field('text_decoration', '0xFF'),
1054
1586
  letter_spacing: field('letter_spacing', 'ER_LAYOUT_AUTO'),
1055
1587
  });
1056
- } else throw aotError('AOT: unsupported child inside a multi-span <Text>', 'a <Text> with a nested <Text> may contain text, {static expressions}, and nested <Text> only.');
1588
+ } else
1589
+ throw aotError(
1590
+ 'AOT: unsupported child inside a multi-span <Text>',
1591
+ 'a <Text> with a nested <Text> may contain text, {static expressions}, and nested <Text> only.',
1592
+ );
1057
1593
  }
1058
1594
  // The engine renders at most ER_TEXT_MAX_SPANS segments; refuse to silently drop the rest.
1059
1595
  if (spans.length > AOT_MAX_TEXT_SPANS) {
1060
- throw aotError(`AOT: a <Text> has ${spans.length} inline segments but the engine renders at most ${AOT_MAX_TEXT_SPANS}`, `combine adjacent plain-text segments, or end the sentence right after a styled <Text> (e.g. "A <b>B</b> C <b>D</b>" is 4). If your engine build raised ER_TEXT_MAX_SPANS, set ER_AOT_MAX_TEXT_SPANS to match when running the AOT.`);
1596
+ throw aotError(
1597
+ `AOT: a <Text> has ${spans.length} inline segments but the engine renders at most ${AOT_MAX_TEXT_SPANS}`,
1598
+ `combine adjacent plain-text segments, or end the sentence right after a styled <Text> (e.g. "A <b>B</b> C <b>D</b>" is 4). If your engine build raised ER_TEXT_MAX_SPANS, set ER_AOT_MAX_TEXT_SPANS to match when running the AOT.`,
1599
+ );
1061
1600
  }
1062
1601
  return spans;
1063
1602
  }
@@ -1067,22 +1606,31 @@ function collectTextSpans(children, scope, env) {
1067
1606
  // ---------------------------------------------------------------------------------------------------
1068
1607
  /** Compiles a list-state setter call (`setItems(...)`) to bounded C array mutations. */
1069
1608
  function compileListOp(rec, arg, env) {
1070
- const { arrayName: arr, countMember: cnt, cap, struct } = rec;
1609
+ const {arrayName: arr, countMember: cnt, cap, struct} = rec;
1071
1610
  // setItems([...items, a, b]) — append; setItems([]) — clear.
1072
1611
  if (arg.type === 'ArrayExpression') {
1073
1612
  if (arg.elements.length === 0) return [` ${cnt} = 0;`];
1074
1613
  const [head, ...rest] = arg.elements;
1075
- if (head?.type !== 'SpreadElement' || head.argument.name !== rec.name) throw new Error(`AOT: a list literal must spread the current list first: [...${rec.name}, item]`);
1614
+ if (head?.type !== 'SpreadElement' || head.argument.name !== rec.name)
1615
+ throw new Error(
1616
+ `AOT: a list literal must spread the current list first: [...${rec.name}, item]`,
1617
+ );
1076
1618
  const lines = [];
1077
1619
  for (const el of rest) {
1078
- if (el.type !== 'ObjectExpression') throw new Error('AOT: appended list items must be object literals');
1079
- const props = new Map(el.properties.map((p) => [p.key.name ?? p.key.value, p.value]));
1620
+ if (el.type !== 'ObjectExpression')
1621
+ throw new Error('AOT: appended list items must be object literals');
1622
+ const props = new Map(
1623
+ el.properties.map(p => [p.key.name ?? p.key.value, p.value]),
1624
+ );
1080
1625
  lines.push(` if (${cnt} < ${cap})`, ' {');
1081
1626
  for (const f of struct.fields) {
1082
1627
  const valNode = props.get(f.key);
1083
1628
  if (!valNode) continue;
1084
1629
  const e = emitExpr(valNode, env);
1085
- if (f.kind === 'string') lines.push(` snprintf(${arr}[${cnt}].${f.key}, sizeof(${arr}[${cnt}].${f.key}), "${printfSpec(e.cType)}", ${e.code});`);
1630
+ if (f.kind === 'string')
1631
+ lines.push(
1632
+ ` snprintf(${arr}[${cnt}].${f.key}, sizeof(${arr}[${cnt}].${f.key}), "${printfSpec(e.cType)}", ${e.code});`,
1633
+ );
1086
1634
  else lines.push(` ${arr}[${cnt}].${f.key} = ${e.code};`);
1087
1635
  }
1088
1636
  lines.push(` ${cnt}++;`, ' }');
@@ -1090,39 +1638,68 @@ function compileListOp(rec, arg, env) {
1090
1638
  return lines;
1091
1639
  }
1092
1640
  // setItems(items.slice(0, X)) — slice(0,-1) pops the last; slice(0,n) truncates to n.
1093
- if (arg.type === 'CallExpression' && arg.callee.type === 'MemberExpression' && arg.callee.object.name === rec.name && arg.callee.property.name === 'slice') {
1641
+ if (
1642
+ arg.type === 'CallExpression' &&
1643
+ arg.callee.type === 'MemberExpression' &&
1644
+ arg.callee.object.name === rec.name &&
1645
+ arg.callee.property.name === 'slice'
1646
+ ) {
1094
1647
  const end = arg.arguments[1];
1095
- if (end?.type === 'UnaryExpression' && end.operator === '-' && end.argument.value === 1) return [` if (${cnt} > 0) ${cnt}--;`];
1648
+ if (
1649
+ end?.type === 'UnaryExpression' &&
1650
+ end.operator === '-' &&
1651
+ end.argument.value === 1
1652
+ )
1653
+ return [` if (${cnt} > 0) ${cnt}--;`];
1096
1654
  const e = emitExpr(end, env);
1097
1655
  return [` ${cnt} = (${cnt} < (${e.code})) ? ${cnt} : (${e.code});`];
1098
1656
  }
1099
- throw new Error(`AOT: unsupported list operation on "${rec.name}" (use [...${rec.name}, item], ${rec.name}.slice(0, -1), or [])`);
1657
+ throw new Error(
1658
+ `AOT: unsupported list operation on "${rec.name}" (use [...${rec.name}, item], ${rec.name}.slice(0, -1), or [])`,
1659
+ );
1100
1660
  }
1101
1661
 
1102
1662
  /** Builds a `get(key)` accessor over an `Animated.*(value, config)` call's config object literal. */
1103
1663
  function animConfigGetter(cfgObj) {
1104
- return (k) => cfgObj?.properties?.find((p) => (p.key.name ?? p.key.value) === k)?.value;
1664
+ return k =>
1665
+ cfgObj?.properties?.find(p => (p.key.name ?? p.key.value) === k)?.value;
1105
1666
  }
1106
1667
 
1107
1668
  /** Emits one atomic animation (timing/spring/decay) → a scoped ERAnimConfig + er_anim_value_animate.
1108
1669
  * `delayMs` is the absolute start delay (how composition offsets are realised); `loop` repeats a timing;
1109
1670
  * `onCompleteCb` (optional) is a C function name set as cfg.on_complete — used to chain sequence steps. */
1110
1671
  function emitAnimEntry(entry, env, idx, onCompleteCb) {
1111
- const { cVar, kind, get, delayMs, loop } = entry;
1672
+ const {cVar, kind, get, delayMs, loop} = entry;
1112
1673
  const c = `cfg${idx}`;
1113
- const lines = [' {', ` ERAnimConfig ${c};`, ` memset(&${c}, 0, sizeof(${c}));`];
1674
+ const lines = [
1675
+ ' {',
1676
+ ` ERAnimConfig ${c};`,
1677
+ ` memset(&${c}, 0, sizeof(${c}));`,
1678
+ ];
1114
1679
  if (kind === 'spring') {
1115
1680
  lines.push(` ${c}.type = ER_ANIM_SPRING;`);
1116
- lines.push(` ${c}.stiffness = ${floatLit(evalStaticOr(get('stiffness'), env, 200))};`);
1117
- lines.push(` ${c}.damping = ${floatLit(evalStaticOr(get('damping'), env, 18))};`);
1118
- lines.push(` ${c}.mass = ${floatLit(evalStaticOr(get('mass'), env, 1))};`);
1681
+ lines.push(
1682
+ ` ${c}.stiffness = ${floatLit(evalStaticOr(get('stiffness'), env, 200))};`,
1683
+ );
1684
+ lines.push(
1685
+ ` ${c}.damping = ${floatLit(evalStaticOr(get('damping'), env, 18))};`,
1686
+ );
1687
+ lines.push(
1688
+ ` ${c}.mass = ${floatLit(evalStaticOr(get('mass'), env, 1))};`,
1689
+ );
1119
1690
  } else if (kind === 'decay') {
1120
1691
  lines.push(` ${c}.type = ER_ANIM_DECAY;`);
1121
- lines.push(` ${c}.deceleration = ${floatLit(evalStaticOr(get('deceleration'), env, 0.998))};`);
1122
- lines.push(` ${c}.velocity = ${floatLit(evalStaticOr(get('velocity'), env, 0))};`);
1692
+ lines.push(
1693
+ ` ${c}.deceleration = ${floatLit(evalStaticOr(get('deceleration'), env, 0.998))};`,
1694
+ );
1695
+ lines.push(
1696
+ ` ${c}.velocity = ${floatLit(evalStaticOr(get('velocity'), env, 0))};`,
1697
+ );
1123
1698
  } else {
1124
1699
  lines.push(` ${c}.type = ER_ANIM_TIMING;`);
1125
- lines.push(` ${c}.duration_ms = ${Math.round(Number(evalStaticOr(get('duration'), env, 250)))};`);
1700
+ lines.push(
1701
+ ` ${c}.duration_ms = ${Math.round(Number(evalStaticOr(get('duration'), env, 250)))};`,
1702
+ );
1126
1703
  pushEasing(lines, c, get('easing'), env);
1127
1704
  }
1128
1705
  if (delayMs > 0) lines.push(` ${c}.delay_ms = ${delayMs};`);
@@ -1130,9 +1707,15 @@ function emitAnimEntry(entry, env, idx, onCompleteCb) {
1130
1707
  if (onCompleteCb) lines.push(` ${c}.on_complete = ${onCompleteCb};`);
1131
1708
  // decay is velocity-driven and has no toValue target; every other type needs one.
1132
1709
  const toNode = get('toValue');
1133
- if (!toNode && kind !== 'decay') throw aotError(`AOT: Animated.${kind}() config needs a toValue`);
1134
- const toCode = toNode ? emitExpr(toNode, env).code : `er_anim_value_get(${cVar})`;
1135
- lines.push(` er_anim_value_animate(${cVar}, (float)(${toCode}), &${c});`, ' }');
1710
+ if (!toNode && kind !== 'decay')
1711
+ throw aotError(`AOT: Animated.${kind}() config needs a toValue`);
1712
+ const toCode = toNode
1713
+ ? emitExpr(toNode, env).code
1714
+ : `er_anim_value_get(${cVar})`;
1715
+ lines.push(
1716
+ ` er_anim_value_animate(${cVar}, (float)(${toCode}), &${c});`,
1717
+ ' }',
1718
+ );
1136
1719
  return lines;
1137
1720
  }
1138
1721
 
@@ -1142,30 +1725,52 @@ function emitAnimEntry(entry, env, idx, onCompleteCb) {
1142
1725
  * `duration` is this node's own run length in ms, used to offset later siblings in a sequence/stagger;
1143
1726
  * null = unknown (spring/decay/loop), which is illegal to sequence anything after. */
1144
1727
  function flattenAnim(node, env, baseDelay, loop) {
1145
- if (node?.type !== 'CallExpression' || node.callee.type !== 'MemberExpression' || node.callee.object?.name !== 'Animated')
1146
- throw aotError('AOT: an animation must be Animated.timing/spring/decay/sequence/parallel/stagger/delay/loop(...)');
1728
+ if (
1729
+ node?.type !== 'CallExpression' ||
1730
+ node.callee.type !== 'MemberExpression' ||
1731
+ node.callee.object?.name !== 'Animated'
1732
+ )
1733
+ throw aotError(
1734
+ 'AOT: an animation must be Animated.timing/spring/decay/sequence/parallel/stagger/delay/loop(...)',
1735
+ );
1147
1736
  const kind = node.callee.property.name;
1148
1737
  const args = node.arguments;
1149
1738
 
1150
1739
  if (kind === 'timing' || kind === 'spring' || kind === 'decay') {
1151
1740
  const valRef = args[0];
1152
- if (valRef?.type !== 'Identifier' || !env.anims?.has(valRef.name)) throw aotError(`AOT: Animated.${kind}() first argument must be a useAnimatedValue`);
1741
+ if (valRef?.type !== 'Identifier' || !env.anims?.has(valRef.name))
1742
+ throw aotError(
1743
+ `AOT: Animated.${kind}() first argument must be a useAnimatedValue`,
1744
+ );
1153
1745
  const cVar = env.anims.get(valRef.name).cVar;
1154
1746
  const get = animConfigGetter(args[1]);
1155
1747
  const ownDelay = Math.round(Number(evalStaticOr(get('delay'), env, 0)));
1156
- const duration = kind === 'timing' ? ownDelay + Math.round(Number(evalStaticOr(get('duration'), env, 250))) : null;
1157
- return { entries: [{ cVar, kind, get, delayMs: baseDelay + ownDelay, loop }], duration };
1748
+ const duration =
1749
+ kind === 'timing'
1750
+ ? ownDelay + Math.round(Number(evalStaticOr(get('duration'), env, 250)))
1751
+ : null;
1752
+ return {
1753
+ entries: [{cVar, kind, get, delayMs: baseDelay + ownDelay, loop}],
1754
+ duration,
1755
+ };
1158
1756
  }
1159
1757
  if (kind === 'delay') {
1160
- return { entries: [], duration: Math.round(Number(evalStaticOr(args[0], env, 0))) };
1758
+ return {
1759
+ entries: [],
1760
+ duration: Math.round(Number(evalStaticOr(args[0], env, 0))),
1761
+ };
1161
1762
  }
1162
1763
  if (kind === 'sequence' || kind === 'parallel' || kind === 'stagger') {
1163
1764
  const list = kind === 'stagger' ? args[1] : args[0];
1164
- const staggerMs = kind === 'stagger' ? Math.round(Number(evalStaticOr(args[0], env, 0))) : 0;
1165
- if (list?.type !== 'ArrayExpression') throw aotError(`AOT: Animated.${kind}(...) needs an array of animations`);
1765
+ const staggerMs =
1766
+ kind === 'stagger'
1767
+ ? Math.round(Number(evalStaticOr(args[0], env, 0)))
1768
+ : 0;
1769
+ if (list?.type !== 'ArrayExpression')
1770
+ throw aotError(`AOT: Animated.${kind}(...) needs an array of animations`);
1166
1771
  const entries = [];
1167
1772
  let off = baseDelay; // running offset (sequence)
1168
- let groupDur = 0; // max end-time relative to baseDelay (parallel/stagger)
1773
+ let groupDur = 0; // max end-time relative to baseDelay (parallel/stagger)
1169
1774
  let i = 0;
1170
1775
  for (const child of list.elements) {
1171
1776
  if (!child) continue;
@@ -1173,10 +1778,13 @@ function flattenAnim(node, env, baseDelay, loop) {
1173
1778
  const r = flattenAnim(child, env, start, loop);
1174
1779
  entries.push(...r.entries);
1175
1780
  if (kind === 'sequence') {
1176
- if (r.duration == null) throw aotError('AOT: an Animated.sequence entry needs a known duration — use Animated.timing / Animated.delay (a spring/decay/loop inside a sequence is not supported; it has no fixed length to offset the next entry by)');
1781
+ if (r.duration == null)
1782
+ throw aotError(
1783
+ 'AOT: an Animated.sequence entry needs a known duration — use Animated.timing / Animated.delay (a spring/decay/loop inside a sequence is not supported; it has no fixed length to offset the next entry by)',
1784
+ );
1177
1785
  off += r.duration;
1178
1786
  } else {
1179
- const end = (start - baseDelay) + (r.duration ?? 0);
1787
+ const end = start - baseDelay + (r.duration ?? 0);
1180
1788
  if (end > groupDur) groupDur = end;
1181
1789
  }
1182
1790
  i++;
@@ -1187,15 +1795,23 @@ function flattenAnim(node, env, baseDelay, loop) {
1187
1795
  const seen = new Set();
1188
1796
  for (const e of entries) {
1189
1797
  if (seen.has(e.cVar))
1190
- throw aotError('AOT: the same animated value is driven more than once in this composition — concurrent/flat same-value steps cancel each other. Use a top-level Animated.sequence(...) for multi-step animation of one value.');
1798
+ throw aotError(
1799
+ 'AOT: the same animated value is driven more than once in this composition — concurrent/flat same-value steps cancel each other. Use a top-level Animated.sequence(...) for multi-step animation of one value.',
1800
+ );
1191
1801
  seen.add(e.cVar);
1192
1802
  }
1193
- return { entries, duration: kind === 'sequence' ? off - baseDelay : groupDur };
1803
+ return {
1804
+ entries,
1805
+ duration: kind === 'sequence' ? off - baseDelay : groupDur,
1806
+ };
1194
1807
  }
1195
1808
  if (kind === 'loop') {
1196
1809
  const r = flattenAnim(args[0], env, baseDelay, true);
1197
- if (r.entries.length !== 1) throw aotError('AOT: Animated.loop currently wraps a single Animated.timing/spring/decay (looping a sequence/parallel is not yet supported)');
1198
- return { entries: r.entries, duration: null };
1810
+ if (r.entries.length !== 1)
1811
+ throw aotError(
1812
+ 'AOT: Animated.loop currently wraps a single Animated.timing/spring/decay (looping a sequence/parallel is not yet supported)',
1813
+ );
1814
+ return {entries: r.entries, duration: null};
1199
1815
  }
1200
1816
  throw aotError(`AOT: Animated.${kind}(...) is not a supported animation`);
1201
1817
  }
@@ -1207,25 +1823,45 @@ function flattenAnim(node, env, baseDelay, loop) {
1207
1823
  * delay() entries fold into the next step's delay_ms. Nested parallel/stagger/loop in a sequence throws. */
1208
1824
  function compileSequenceChain(seqNode, env, ctx, doneCb = null) {
1209
1825
  const list = seqNode.arguments[0];
1210
- if (list?.type !== 'ArrayExpression') throw aotError('AOT: Animated.sequence(...) needs an array of animations');
1826
+ if (list?.type !== 'ArrayExpression')
1827
+ throw aotError('AOT: Animated.sequence(...) needs an array of animations');
1211
1828
  const steps = [];
1212
1829
  let pendingDelay = 0;
1213
1830
  for (const child of list.elements) {
1214
1831
  if (!child) continue;
1215
- if (child.type !== 'CallExpression' || child.callee.type !== 'MemberExpression' || child.callee.object?.name !== 'Animated')
1216
- throw aotError('AOT: Animated.sequence entries must be Animated.timing/spring/decay/delay(...)');
1832
+ if (
1833
+ child.type !== 'CallExpression' ||
1834
+ child.callee.type !== 'MemberExpression' ||
1835
+ child.callee.object?.name !== 'Animated'
1836
+ )
1837
+ throw aotError(
1838
+ 'AOT: Animated.sequence entries must be Animated.timing/spring/decay/delay(...)',
1839
+ );
1217
1840
  const kind = child.callee.property.name;
1218
1841
  if (kind === 'delay') {
1219
- pendingDelay += Math.round(Number(evalStaticOr(child.arguments[0], env, 0)));
1842
+ pendingDelay += Math.round(
1843
+ Number(evalStaticOr(child.arguments[0], env, 0)),
1844
+ );
1220
1845
  continue;
1221
1846
  }
1222
1847
  if (kind !== 'timing' && kind !== 'spring' && kind !== 'decay')
1223
- throw aotError('AOT: Animated.sequence entries must be Animated.timing/spring/decay/delay (a nested parallel/stagger/loop inside a sequence is not yet supported — keep the sequence flat)');
1848
+ throw aotError(
1849
+ 'AOT: Animated.sequence entries must be Animated.timing/spring/decay/delay (a nested parallel/stagger/loop inside a sequence is not yet supported — keep the sequence flat)',
1850
+ );
1224
1851
  const valRef = child.arguments[0];
1225
- if (valRef?.type !== 'Identifier' || !env.anims?.has(valRef.name)) throw aotError(`AOT: Animated.${kind}() first argument must be a useAnimatedValue`);
1852
+ if (valRef?.type !== 'Identifier' || !env.anims?.has(valRef.name))
1853
+ throw aotError(
1854
+ `AOT: Animated.${kind}() first argument must be a useAnimatedValue`,
1855
+ );
1226
1856
  const get = animConfigGetter(child.arguments[1]);
1227
1857
  const ownDelay = Math.round(Number(evalStaticOr(get('delay'), env, 0)));
1228
- steps.push({ cVar: env.anims.get(valRef.name).cVar, kind, get, delayMs: pendingDelay + ownDelay, loop: false });
1858
+ steps.push({
1859
+ cVar: env.anims.get(valRef.name).cVar,
1860
+ kind,
1861
+ get,
1862
+ delayMs: pendingDelay + ownDelay,
1863
+ loop: false,
1864
+ });
1229
1865
  pendingDelay = 0;
1230
1866
  }
1231
1867
  if (!steps.length) return [];
@@ -1238,7 +1874,7 @@ function compileSequenceChain(seqNode, env, ctx, doneCb = null) {
1238
1874
  const nextCb = i < steps.length - 1 ? `er_seqcb_${seqId}_${i + 1}` : doneCb;
1239
1875
  const lines = emitAnimEntry(steps[i], env, `${seqId}_${i}`, nextCb);
1240
1876
  if (i === 0) firstLines = lines;
1241
- else ctx.out.animCbs.push({ name: `er_seqcb_${seqId}_${i}`, body: lines });
1877
+ else ctx.out.animCbs.push({name: `er_seqcb_${seqId}_${i}`, body: lines});
1242
1878
  }
1243
1879
  return firstLines;
1244
1880
  }
@@ -1256,32 +1892,56 @@ function emitCompletionCb(fnNode, env, state, ctx) {
1256
1892
  const locals = new Map(env.locals);
1257
1893
  const param = fnNode.params[0];
1258
1894
  if (param?.type === 'ObjectPattern') {
1259
- for (const p of param.properties) if ((p.key?.name ?? p.key?.value) === 'finished') locals.set(p.value?.name ?? 'finished', { code: 'finished', cType: 'int' });
1895
+ for (const p of param.properties)
1896
+ if ((p.key?.name ?? p.key?.value) === 'finished')
1897
+ locals.set(p.value?.name ?? 'finished', {
1898
+ code: 'finished',
1899
+ cType: 'int',
1900
+ });
1260
1901
  }
1261
1902
  const body = fnNode.body;
1262
- const list = body.type === 'BlockStatement' ? body.body : [{ type: 'ExpressionStatement', expression: body }];
1263
- const cctx = { stateChanged: false, animIdx: 0, out: ctx.out };
1264
- const lines = compileStmts(list, { ...env, locals }, state, cctx, ' ');
1903
+ const list =
1904
+ body.type === 'BlockStatement'
1905
+ ? body.body
1906
+ : [{type: 'ExpressionStatement', expression: body}];
1907
+ const cctx = {stateChanged: false, animIdx: 0, out: ctx.out};
1908
+ const lines = compileStmts(list, {...env, locals}, state, cctx, ' ');
1265
1909
  if (cctx.stateChanged) lines.push(' app_update();');
1266
- ctx.out.animCbs.push({ name, body: lines });
1910
+ ctx.out.animCbs.push({name, body: lines});
1267
1911
  return name;
1268
1912
  }
1269
1913
 
1270
1914
  function compileAnimateStart(expr, env, state, ctx) {
1271
1915
  // .start(onComplete?) — an optional completion callback fired when the animation finishes.
1272
- const doneCb = isFn(expr.arguments[0]) ? emitCompletionCb(expr.arguments[0], env, state, ctx) : null;
1916
+ const doneCb = isFn(expr.arguments[0])
1917
+ ? emitCompletionCb(expr.arguments[0], env, state, ctx)
1918
+ : null;
1273
1919
  const receiver = expr.callee.object;
1274
- if (receiver?.type === 'CallExpression' && receiver.callee.type === 'MemberExpression' && receiver.callee.object?.name === 'Animated' && receiver.callee.property.name === 'sequence') {
1920
+ if (
1921
+ receiver?.type === 'CallExpression' &&
1922
+ receiver.callee.type === 'MemberExpression' &&
1923
+ receiver.callee.object?.name === 'Animated' &&
1924
+ receiver.callee.property.name === 'sequence'
1925
+ ) {
1275
1926
  return compileSequenceChain(receiver, env, ctx, doneCb);
1276
1927
  }
1277
- const { entries } = flattenAnim(receiver, env, 0, false);
1928
+ const {entries} = flattenAnim(receiver, env, 0, false);
1278
1929
  if (doneCb && entries.length > 1)
1279
1930
  throw aotError(
1280
1931
  'AOT: a .start(onComplete) callback on a parallel/stagger animation is not yet supported',
1281
1932
  'attach the completion callback to a single animation or an Animated.sequence(...). For "after all parallel anims", restructure as a sequence.',
1282
1933
  );
1283
1934
  const lines = [];
1284
- entries.forEach((e, i) => lines.push(...emitAnimEntry(e, env, ctx.animIdx++, i === entries.length - 1 ? doneCb : null)));
1935
+ entries.forEach((e, i) =>
1936
+ lines.push(
1937
+ ...emitAnimEntry(
1938
+ e,
1939
+ env,
1940
+ ctx.animIdx++,
1941
+ i === entries.length - 1 ? doneCb : null,
1942
+ ),
1943
+ ),
1944
+ );
1285
1945
  return lines;
1286
1946
  }
1287
1947
 
@@ -1302,13 +1962,20 @@ function blockList(node) {
1302
1962
 
1303
1963
  /** Emits C to write a value into a scalar state slot: snprintf for a string buffer, plain assign else. */
1304
1964
  function scalarAssign(rec, e, indent) {
1305
- if (rec.cType === 'string') return `${indent}snprintf(${rec.cMember}, sizeof(${rec.cMember}), "${printfSpec(e.cType)}", ${e.code});`;
1965
+ if (rec.cType === 'string')
1966
+ return `${indent}snprintf(${rec.cMember}, sizeof(${rec.cMember}), "${printfSpec(e.cType)}", ${e.code});`;
1306
1967
  return `${indent}${rec.cMember} = ${e.code};`;
1307
1968
  }
1308
1969
 
1309
1970
  /** True if `node` is a `<ref>.current` member access on a known value ref. */
1310
1971
  function refTarget(node, env) {
1311
- if (node?.type === 'MemberExpression' && !node.computed && node.object.type === 'Identifier' && node.property.name === 'current' && env.refs?.has(node.object.name)) {
1972
+ if (
1973
+ node?.type === 'MemberExpression' &&
1974
+ !node.computed &&
1975
+ node.object.type === 'Identifier' &&
1976
+ node.property.name === 'current' &&
1977
+ env.refs?.has(node.object.name)
1978
+ ) {
1312
1979
  return env.refs.get(node.object.name);
1313
1980
  }
1314
1981
  return null;
@@ -1318,23 +1985,36 @@ function refTarget(node, env) {
1318
1985
  * → { entries (op-tape C exprs), paint (static 7-num record) }. Geometry coords may reference state/refs/
1319
1986
  * event fields (emitExpr); paint must be static. Shares the ...EntriesC geometry with the JSX path. */
1320
1987
  function imperativeShape(shapeNode, env) {
1321
- if (shapeNode?.type !== 'ObjectExpression') throw new Error('AOT: each updateVector shape must be an object literal');
1988
+ if (shapeNode?.type !== 'ObjectExpression')
1989
+ throw new Error('AOT: each updateVector shape must be an object literal');
1322
1990
  const props = {};
1323
1991
  for (const p of shapeNode.properties) {
1324
- if (p.type !== 'ObjectProperty') throw new Error('AOT: spread/method in an updateVector shape not supported');
1992
+ if (p.type !== 'ObjectProperty')
1993
+ throw new Error(
1994
+ 'AOT: spread/method in an updateVector shape not supported',
1995
+ );
1325
1996
  props[p.key.name ?? p.key.value] = p.value;
1326
1997
  }
1327
1998
  const arr = (key, n) => {
1328
- if (props[key].type !== 'ArrayExpression') throw new Error(`AOT: updateVector "${key}" must be an array literal`);
1329
- return props[key].elements.slice(0, n).map((el) => `(float)(${emitExpr(el, env).code})`);
1999
+ if (props[key].type !== 'ArrayExpression')
2000
+ throw new Error(`AOT: updateVector "${key}" must be an array literal`);
2001
+ return props[key].elements
2002
+ .slice(0, n)
2003
+ .map(el => `(float)(${emitExpr(el, env).code})`);
1330
2004
  };
1331
2005
  let entries;
1332
2006
  if (props.arc) entries = arcEntriesC(...arr('arc', 5));
1333
2007
  else if (props.circle) entries = circleEntriesC(...arr('circle', 3));
1334
2008
  else if (props.rect) entries = rectEntriesC(...arr('rect', 4));
1335
2009
  else if (props.line) entries = lineEntriesC(...arr('line', 4));
1336
- else if (props.path) entries = parsePath(String(evalStatic(props.path, env.consts ?? {}))).map(floatLit);
1337
- else throw new Error('AOT: an updateVector shape needs one of arc / circle / rect / line / path');
2010
+ else if (props.path)
2011
+ entries = parsePath(String(evalStatic(props.path, env.consts ?? {}))).map(
2012
+ floatLit,
2013
+ );
2014
+ else
2015
+ throw new Error(
2016
+ 'AOT: an updateVector shape needs one of arc / circle / rect / line / path',
2017
+ );
1338
2018
  const stat = (key, dflt) => {
1339
2019
  if (props[key] == null) return dflt;
1340
2020
  try {
@@ -1343,8 +2023,16 @@ function imperativeShape(shapeNode, env) {
1343
2023
  throw new Error(`AOT: updateVector paint "${key}" must be static`);
1344
2024
  }
1345
2025
  };
1346
- const paint = [parseColor(stat('fill', 'none')), parseColor(stat('stroke', 'none')), svgNum(stat('strokeWidth', 1), 1), svgNum(stat('miter', 4), 4), CAP_MAP[stat('cap', 'butt')] ?? 0, JOIN_MAP[stat('join', 'miter')] ?? 0, stat('fillRule', 'nonzero') === 'evenodd' ? 1 : 0];
1347
- return { entries, paint };
2026
+ const paint = [
2027
+ parseColor(stat('fill', 'none')),
2028
+ parseColor(stat('stroke', 'none')),
2029
+ svgNum(stat('strokeWidth', 1), 1),
2030
+ svgNum(stat('miter', 4), 4),
2031
+ CAP_MAP[stat('cap', 'butt')] ?? 0,
2032
+ JOIN_MAP[stat('join', 'miter')] ?? 0,
2033
+ stat('fillRule', 'nonzero') === 'evenodd' ? 1 : 0,
2034
+ ];
2035
+ return {entries, paint};
1348
2036
  }
1349
2037
 
1350
2038
  /** Lowers `updateVector(nodeRef, shapes, [x,y,w,h]?)` to: fill a mutable op-tape, push it to the node, and
@@ -1353,12 +2041,18 @@ function compileUpdateVector(expr, env, ctx, indent) {
1353
2041
  const out = ctx.out;
1354
2042
  const [refArg, shapesArg, dirtyArg] = expr.arguments;
1355
2043
  const ref = refArg?.type === 'Identifier' ? env.refs?.get(refArg.name) : null;
1356
- if (ref?.kind !== 'node') throw new Error('AOT: updateVector(ref, …) first arg must be a node ref (const r = useRef())');
1357
- if (shapesArg?.type !== 'ArrayExpression') throw new Error('AOT: updateVector(ref, shapes, …) shapes must be an array literal');
2044
+ if (ref?.kind !== 'node')
2045
+ throw new Error(
2046
+ 'AOT: updateVector(ref, …) first arg must be a node ref (const r = useRef())',
2047
+ );
2048
+ if (shapesArg?.type !== 'ArrayExpression')
2049
+ throw new Error(
2050
+ 'AOT: updateVector(ref, shapes, …) shapes must be an array literal',
2051
+ );
1358
2052
  const entries = [];
1359
2053
  const paints = [];
1360
2054
  for (const s of shapesArg.elements) {
1361
- const { entries: e, paint } = imperativeShape(s, env);
2055
+ const {entries: e, paint} = imperativeShape(s, env);
1362
2056
  entries.push('ER_VOP_SHAPE', floatLit(paints.length), ...e);
1363
2057
  paints.push(paint);
1364
2058
  }
@@ -1366,13 +2060,22 @@ function compileUpdateVector(expr, env, ctx, indent) {
1366
2060
  const len = entries.length;
1367
2061
  out.needsMath = true;
1368
2062
  out.vectorData.push(`static float s_uv${id}_ops[${len}];`);
1369
- out.vectorData.push(`static const ERVectorPaint s_uv${id}_paints[] = {\n${paints.map((p) => ' ' + emitVectorPaint(p)).join(',\n')}\n};`);
2063
+ out.vectorData.push(
2064
+ `static const ERVectorPaint s_uv${id}_paints[] = {\n${paints.map(p => ' ' + emitVectorPaint(p)).join(',\n')}\n};`,
2065
+ );
1370
2066
  const lines = entries.map((e, i) => `${indent}s_uv${id}_ops[${i}] = ${e};`);
1371
- lines.push(`${indent}er_node_set_vector_ops(${ref.cVar}, s_uv${id}_ops, ${len}, s_uv${id}_paints, ${paints.length});`);
2067
+ lines.push(
2068
+ `${indent}er_node_set_vector_ops(${ref.cVar}, s_uv${id}_ops, ${len}, s_uv${id}_paints, ${paints.length}, NULL, 0);`,
2069
+ );
1372
2070
  if (dirtyArg) {
1373
- if (dirtyArg.type !== 'ArrayExpression' || dirtyArg.elements.length < 4) throw new Error('AOT: updateVector dirtyRect must be a [x, y, w, h] array literal');
1374
- const [x, y, w, h] = dirtyArg.elements.map((el) => emitExpr(el, env).code);
1375
- lines.push(`${indent}er_node_set_vector_dirty_rect(${ref.cVar}, ${x}, ${y}, ${w}, ${h});`);
2071
+ if (dirtyArg.type !== 'ArrayExpression' || dirtyArg.elements.length < 4)
2072
+ throw new Error(
2073
+ 'AOT: updateVector dirtyRect must be a [x, y, w, h] array literal',
2074
+ );
2075
+ const [x, y, w, h] = dirtyArg.elements.map(el => emitExpr(el, env).code);
2076
+ lines.push(
2077
+ `${indent}er_node_set_vector_dirty_rect(${ref.cVar}, ${x}, ${y}, ${w}, ${h});`,
2078
+ );
1376
2079
  }
1377
2080
  return lines;
1378
2081
  }
@@ -1381,13 +2084,17 @@ function compileUpdateVector(expr, env, ctx, indent) {
1381
2084
  /** setInterval/setTimeout(cb, ms) → a C `er_timer_add(ms, repeat, fn)` expr; registers cb as a timer fn. */
1382
2085
  function compileTimerAdd(expr, env, state, ctx) {
1383
2086
  const cb = expr.arguments[0];
1384
- if (!isFn(cb)) throw aotError('AOT: a setInterval/setTimeout callback must be an inline function', 'pass an inline arrow, e.g. setInterval(() => setTick((t) => t + 1), 1000).');
2087
+ if (!isFn(cb))
2088
+ throw aotError(
2089
+ 'AOT: a setInterval/setTimeout callback must be an inline function',
2090
+ 'pass an inline arrow, e.g. setInterval(() => setTick((t) => t + 1), 1000).',
2091
+ );
1385
2092
  const ms = expr.arguments[1] ? emitExpr(expr.arguments[1], env).code : '0';
1386
2093
  const repeat = expr.callee.name === 'setInterval';
1387
2094
  const slot = ctx.out.timerFns.length;
1388
2095
  const name = `er_timer_fn_${slot}`;
1389
2096
  ctx.out.usesTimers = true;
1390
- ctx.out.timerFns.push({ name, body: null }); // reserve the slot before compiling the body (it may add more)
2097
+ ctx.out.timerFns.push({name, body: null}); // reserve the slot before compiling the body (it may add more)
1391
2098
  ctx.out.timerFns[slot].body = compileHandler(cb, env, state, ctx.out);
1392
2099
  return `er_timer_add((int)(${ms}), ${repeat ? 'true' : 'false'}, ${name})`;
1393
2100
  }
@@ -1410,51 +2117,95 @@ function inlineHelperCall(name, fn, args, env, state, ctx, indent) {
1410
2117
  );
1411
2118
  if (args[i]) {
1412
2119
  const e = emitExpr(args[i], env);
1413
- locals.set(p.name, { code: e.code, cType: e.cType });
2120
+ locals.set(p.name, {code: e.code, cType: e.cType});
1414
2121
  }
1415
2122
  });
1416
2123
  const body = fn.body;
1417
- const list = body.type === 'BlockStatement' ? body.body : [{ type: 'ExpressionStatement', expression: body }];
2124
+ const list =
2125
+ body.type === 'BlockStatement'
2126
+ ? body.body
2127
+ : [{type: 'ExpressionStatement', expression: body}];
1418
2128
  ctx.inlining.add(name);
1419
- const lines = compileStmts(list, { ...env, locals }, state, ctx, indent);
2129
+ const lines = compileStmts(list, {...env, locals}, state, ctx, indent);
1420
2130
  ctx.inlining.delete(name);
1421
2131
  return lines;
1422
2132
  }
1423
2133
 
1424
2134
  function compileHandlerExprImpl(expr, env, state, ctx, indent) {
1425
2135
  // updateVector(ref, shapes, dirtyRect?) — imperative vector redraw (no app_update).
1426
- if (expr.type === 'CallExpression' && expr.callee.type === 'Identifier' && expr.callee.name === 'updateVector') {
2136
+ if (
2137
+ expr.type === 'CallExpression' &&
2138
+ expr.callee.type === 'Identifier' &&
2139
+ expr.callee.name === 'updateVector'
2140
+ ) {
1427
2141
  return compileUpdateVector(expr, env, ctx, indent);
1428
2142
  }
1429
2143
  // setInterval / setTimeout(cb, ms) → register a host-tick timer (the returned id is discarded here).
1430
- if (expr.type === 'CallExpression' && expr.callee.type === 'Identifier' && (expr.callee.name === 'setInterval' || expr.callee.name === 'setTimeout')) {
2144
+ if (
2145
+ expr.type === 'CallExpression' &&
2146
+ expr.callee.type === 'Identifier' &&
2147
+ (expr.callee.name === 'setInterval' || expr.callee.name === 'setTimeout')
2148
+ ) {
1431
2149
  return [`${indent}${compileTimerAdd(expr, env, state, ctx)};`];
1432
2150
  }
1433
2151
  // clearInterval / clearTimeout(id) → deactivate the timer slot.
1434
- if (expr.type === 'CallExpression' && expr.callee.type === 'Identifier' && (expr.callee.name === 'clearInterval' || expr.callee.name === 'clearTimeout')) {
1435
- return [`${indent}er_timer_clear(${emitExpr(expr.arguments[0], env).code});`];
2152
+ if (
2153
+ expr.type === 'CallExpression' &&
2154
+ expr.callee.type === 'Identifier' &&
2155
+ (expr.callee.name === 'clearInterval' ||
2156
+ expr.callee.name === 'clearTimeout')
2157
+ ) {
2158
+ return [
2159
+ `${indent}er_timer_clear(${emitExpr(expr.arguments[0], env).code});`,
2160
+ ];
1436
2161
  }
1437
2162
  // `ref.current = expr` / `ref.current += expr` — a value ref write; does NOT trigger a re-render.
1438
2163
  if (expr.type === 'AssignmentExpression') {
1439
2164
  const r = refTarget(expr.left, env);
1440
- if (!r) throw new Error('AOT: the only assignment allowed in a handler is `ref.current = ...`');
1441
- return [`${indent}${r.cVar} ${expr.operator} ${emitExpr(expr.right, env).code};`];
2165
+ if (!r)
2166
+ throw new Error(
2167
+ 'AOT: the only assignment allowed in a handler is `ref.current = ...`',
2168
+ );
2169
+ return [
2170
+ `${indent}${r.cVar} ${expr.operator} ${emitExpr(expr.right, env).code};`,
2171
+ ];
1442
2172
  }
1443
2173
  // `ref.current++` / `ref.current--`.
1444
2174
  if (expr.type === 'UpdateExpression') {
1445
2175
  const r = refTarget(expr.argument, env);
1446
- if (!r) throw new Error('AOT: the only ++/-- allowed in a handler is on `ref.current`');
2176
+ if (!r)
2177
+ throw new Error(
2178
+ 'AOT: the only ++/-- allowed in a handler is on `ref.current`',
2179
+ );
1447
2180
  return [`${indent}${r.cVar}${expr.operator};`];
1448
2181
  }
1449
2182
  // Animated.*(…).start() — single timing/spring/decay OR a sequence/parallel/stagger/delay/loop
1450
2183
  // composition; native-driven, sets no React state, needs no app_update.
1451
- if (expr.type === 'CallExpression' && expr.callee.type === 'MemberExpression' && expr.callee.property.name === 'start') {
2184
+ if (
2185
+ expr.type === 'CallExpression' &&
2186
+ expr.callee.type === 'MemberExpression' &&
2187
+ expr.callee.property.name === 'start'
2188
+ ) {
1452
2189
  return compileAnimateStart(expr, env, state, ctx);
1453
2190
  }
1454
- if (expr.type !== 'CallExpression' || expr.callee.type !== 'Identifier') throw aotError('AOT: a handler statement must be a state setter, a ref write, or Animated.timing/spring(...).start()', 'each statement in a handler must be one of: setX(value) / setX(prev => …), a `ref.current = …` write, an `updateVector(…)` call, or `Animated.timing|spring(v, …).start()`. Wrap conditional logic in `if (…) { … }`.');
2191
+ if (expr.type !== 'CallExpression' || expr.callee.type !== 'Identifier')
2192
+ throw aotError(
2193
+ 'AOT: a handler statement must be a state setter, a ref write, or Animated.timing/spring(...).start()',
2194
+ 'each statement in a handler must be one of: setX(value) / setX(prev => …), a `ref.current = …` write, an `updateVector(…)` call, or `Animated.timing|spring(v, …).start()`. Wrap conditional logic in `if (…) { … }`.',
2195
+ );
1455
2196
  // A call to a helper / useCallback (e.g. `reset();`) → inline its body here so handlers can compose logic.
1456
- const helperFn = env.helpers?.get(expr.callee.name) ?? env.callbacks?.get(expr.callee.name);
1457
- if (helperFn) return inlineHelperCall(expr.callee.name, helperFn, expr.arguments, env, state, ctx, indent);
2197
+ const helperFn =
2198
+ env.helpers?.get(expr.callee.name) ?? env.callbacks?.get(expr.callee.name);
2199
+ if (helperFn)
2200
+ return inlineHelperCall(
2201
+ expr.callee.name,
2202
+ helperFn,
2203
+ expr.arguments,
2204
+ env,
2205
+ state,
2206
+ ctx,
2207
+ indent,
2208
+ );
1458
2209
  const rec = state.bySetter.get(expr.callee.name);
1459
2210
  if (!rec)
1460
2211
  throw aotError(
@@ -1464,13 +2215,20 @@ function compileHandlerExprImpl(expr, env, state, ctx, indent) {
1464
2215
  ctx.stateChanged = true;
1465
2216
  const arg = expr.arguments[0];
1466
2217
  if (rec.kind === 'list') return compileListOp(rec, arg, env);
1467
- if (arg && (arg.type === 'ArrowFunctionExpression' || arg.type === 'FunctionExpression')) {
2218
+ if (
2219
+ arg &&
2220
+ (arg.type === 'ArrowFunctionExpression' ||
2221
+ arg.type === 'FunctionExpression')
2222
+ ) {
1468
2223
  // setState(prev => expr): bind the param to the current value, assign the result.
1469
2224
  const param = arg.params[0]?.name;
1470
2225
  const locals = new Map(env.locals);
1471
- if (param) locals.set(param, { code: rec.cMember, cType: rec.cType });
1472
- if (arg.body.type === 'BlockStatement') throw new Error('AOT: updater function must be a single expression (for now)');
1473
- return [scalarAssign(rec, emitExpr(arg.body, { ...env, locals }), indent)];
2226
+ if (param) locals.set(param, {code: rec.cMember, cType: rec.cType});
2227
+ if (arg.body.type === 'BlockStatement')
2228
+ throw new Error(
2229
+ 'AOT: updater function must be a single expression (for now)',
2230
+ );
2231
+ return [scalarAssign(rec, emitExpr(arg.body, {...env, locals}), indent)];
1474
2232
  }
1475
2233
  return [scalarAssign(rec, emitExpr(arg, env), indent)];
1476
2234
  }
@@ -1486,19 +2244,45 @@ function compileStmts(list, env, state, ctx, indent) {
1486
2244
  for (const st of list) {
1487
2245
  if (st.type === 'VariableDeclaration') {
1488
2246
  for (const decl of st.declarations) {
1489
- if (decl.id.type !== 'Identifier') throw new Error('AOT: destructuring a handler local is not supported');
1490
- if (!decl.init) throw new Error('AOT: a handler local must have an initializer');
2247
+ if (decl.id.type !== 'Identifier')
2248
+ throw new Error(
2249
+ 'AOT: destructuring a handler local is not supported',
2250
+ );
2251
+ if (!decl.init)
2252
+ throw new Error('AOT: a handler local must have an initializer');
1491
2253
  const cName = `l_${decl.id.name}`;
1492
2254
  // `const id = setInterval/setTimeout(…)` → an int timer-id local (so a later clearInterval(id) resolves).
1493
- if (decl.init.type === 'CallExpression' && decl.init.callee.type === 'Identifier' && (decl.init.callee.name === 'setInterval' || decl.init.callee.name === 'setTimeout')) {
2255
+ if (
2256
+ decl.init.type === 'CallExpression' &&
2257
+ decl.init.callee.type === 'Identifier' &&
2258
+ (decl.init.callee.name === 'setInterval' ||
2259
+ decl.init.callee.name === 'setTimeout')
2260
+ ) {
1494
2261
  // The id is only needed for a later clear*(); a mount effect drops its cleanup, so mark it used.
1495
- lines.push(`${indent}int ${cName} = ${compileTimerAdd(decl.init, env, state, ctx)};`, `${indent}(void)${cName};`);
1496
- env = { ...env, locals: new Map(env.locals).set(decl.id.name, { code: cName, cType: 'int' }) };
2262
+ lines.push(
2263
+ `${indent}int ${cName} = ${compileTimerAdd(decl.init, env, state, ctx)};`,
2264
+ `${indent}(void)${cName};`,
2265
+ );
2266
+ env = {
2267
+ ...env,
2268
+ locals: new Map(env.locals).set(decl.id.name, {
2269
+ code: cName,
2270
+ cType: 'int',
2271
+ }),
2272
+ };
1497
2273
  continue;
1498
2274
  }
1499
2275
  const e = emitExpr(decl.init, env);
1500
- lines.push(`${indent}${e.cType === 'float' ? 'float' : 'int'} ${cName} = ${e.code};`);
1501
- env = { ...env, locals: new Map(env.locals).set(decl.id.name, { code: cName, cType: e.cType }) };
2276
+ lines.push(
2277
+ `${indent}${e.cType === 'float' ? 'float' : 'int'} ${cName} = ${e.code};`,
2278
+ );
2279
+ env = {
2280
+ ...env,
2281
+ locals: new Map(env.locals).set(decl.id.name, {
2282
+ code: cName,
2283
+ cType: e.cType,
2284
+ }),
2285
+ };
1502
2286
  }
1503
2287
  continue;
1504
2288
  }
@@ -1510,11 +2294,27 @@ function compileStmts(list, env, state, ctx, indent) {
1510
2294
  }
1511
2295
  if (st.type === 'IfStatement') {
1512
2296
  lines.push(`${indent}if (${emitExpr(st.test, env).code})`, `${indent}{`);
1513
- lines.push(...compileStmts(blockList(st.consequent), env, state, ctx, indent + ' '));
2297
+ lines.push(
2298
+ ...compileStmts(
2299
+ blockList(st.consequent),
2300
+ env,
2301
+ state,
2302
+ ctx,
2303
+ indent + ' ',
2304
+ ),
2305
+ );
1514
2306
  lines.push(`${indent}}`);
1515
2307
  if (st.alternate) {
1516
2308
  lines.push(`${indent}else`, `${indent}{`);
1517
- lines.push(...compileStmts(blockList(st.alternate), env, state, ctx, indent + ' '));
2309
+ lines.push(
2310
+ ...compileStmts(
2311
+ blockList(st.alternate),
2312
+ env,
2313
+ state,
2314
+ ctx,
2315
+ indent + ' ',
2316
+ ),
2317
+ );
1518
2318
  lines.push(`${indent}}`);
1519
2319
  }
1520
2320
  continue;
@@ -1539,37 +2339,69 @@ function compileStmts(list, env, state, ctx, indent) {
1539
2339
  */
1540
2340
  function compileEffect(eff, env, state, out) {
1541
2341
  const body = eff.fn.body;
1542
- const stmts = body.type === 'BlockStatement' ? body.body : [{ type: 'ExpressionStatement', expression: body }];
1543
- const isMount = !eff.deps || (eff.deps.type === 'ArrayExpression' && eff.deps.elements.length === 0);
2342
+ const stmts =
2343
+ body.type === 'BlockStatement'
2344
+ ? body.body
2345
+ : [{type: 'ExpressionStatement', expression: body}];
2346
+ const isMount =
2347
+ !eff.deps ||
2348
+ (eff.deps.type === 'ArrayExpression' && eff.deps.elements.length === 0);
1544
2349
  if (isMount) {
1545
- const ctx = { stateChanged: false, animIdx: 0, out, allowReturn: true };
2350
+ const ctx = {stateChanged: false, animIdx: 0, out, allowReturn: true};
1546
2351
  const lines = compileStmts(stmts, env, state, ctx, ' ');
1547
2352
  if (ctx.stateChanged) lines.push(' app_update();');
1548
2353
  out.mountEffects.push(...lines);
1549
2354
  return;
1550
2355
  }
1551
- if (eff.deps.type !== 'ArrayExpression') throw aotError('AOT: a useEffect dependency list must be an array literal', 'pass `[]` (run once) or `[a, b]` (re-run when a/b change).');
2356
+ if (eff.deps.type !== 'ArrayExpression')
2357
+ throw aotError(
2358
+ 'AOT: a useEffect dependency list must be an array literal',
2359
+ 'pass `[]` (run once) or `[a, b]` (re-run when a/b change).',
2360
+ );
1552
2361
  const id = out.effN++;
1553
2362
  const name = `er_effect_${id}`;
1554
- const deps = eff.deps.elements.map((d) => {
2363
+ const deps = eff.deps.elements.map(d => {
1555
2364
  if (!d) throw aotError('AOT: a useEffect dependency must be an expression');
1556
2365
  const e = emitExpr(d, env);
1557
2366
  if (e.cType !== 'int' && e.cType !== 'float' && e.cType !== 'string')
1558
- throw aotError('AOT: useEffect dependencies must be scalar (number / bool / string)', 'depend on scalar state values; object/array dependencies are not yet supported.');
2367
+ throw aotError(
2368
+ 'AOT: useEffect dependencies must be scalar (number / bool / string)',
2369
+ 'depend on scalar state values; object/array dependencies are not yet supported.',
2370
+ );
1559
2371
  return e;
1560
2372
  });
1561
- const ctx = { stateChanged: false, animIdx: 0, out, allowReturn: true };
1562
- out.effectFns.push({ name, body: compileStmts(stmts, env, state, ctx, ' ') });
2373
+ const ctx = {stateChanged: false, animIdx: 0, out, allowReturn: true};
2374
+ out.effectFns.push({
2375
+ name,
2376
+ body: compileStmts(stmts, env, state, ctx, ' '),
2377
+ });
1563
2378
  // A static "previous value" per dep; snapshot at mount, then app_update detects changes against it.
1564
- deps.forEach((d, j) => out.effectDecls.push(d.cType === 'string' ? `static char s_eff${id}_d${j}[${LIST_STR_CAP}];` : `static ${d.cType === 'float' ? 'float' : 'int'} s_eff${id}_d${j};`));
1565
- const snap = (j, d) => (d.cType === 'string' ? `snprintf(s_eff${id}_d${j}, sizeof(s_eff${id}_d${j}), "%s", ${d.code})` : `s_eff${id}_d${j} = ${d.code}`);
1566
- out.mountEffects.push(` ${name}();`, ...deps.map((d, j) => ` ${snap(j, d)};`));
2379
+ deps.forEach((d, j) =>
2380
+ out.effectDecls.push(
2381
+ d.cType === 'string'
2382
+ ? `static char s_eff${id}_d${j}[${LIST_STR_CAP}];`
2383
+ : `static ${d.cType === 'float' ? 'float' : 'int'} s_eff${id}_d${j};`,
2384
+ ),
2385
+ );
2386
+ const snap = (j, d) =>
2387
+ d.cType === 'string'
2388
+ ? `snprintf(s_eff${id}_d${j}, sizeof(s_eff${id}_d${j}), "%s", ${d.code})`
2389
+ : `s_eff${id}_d${j} = ${d.code}`;
2390
+ out.mountEffects.push(
2391
+ ` ${name}();`,
2392
+ ...deps.map((d, j) => ` ${snap(j, d)};`),
2393
+ );
1567
2394
  const check = [' {', ' int er_changed = 0;'];
1568
2395
  deps.forEach((d, j) => {
1569
- if (d.cType === 'string') check.push(` if (strcmp(s_eff${id}_d${j}, ${d.code}) != 0) { ${snap(j, d)}; er_changed = 1; }`);
2396
+ if (d.cType === 'string')
2397
+ check.push(
2398
+ ` if (strcmp(s_eff${id}_d${j}, ${d.code}) != 0) { ${snap(j, d)}; er_changed = 1; }`,
2399
+ );
1570
2400
  else {
1571
2401
  const t = d.cType === 'float' ? 'float' : 'int';
1572
- check.push(` ${t} er_d${j} = ${d.code}; if (er_d${j} != s_eff${id}_d${j}) { s_eff${id}_d${j} = er_d${j}; er_changed = 1; }`);
2402
+ check.push(
2403
+ ` ${t} er_d${j} = ${d.code}; if (er_d${j} != s_eff${id}_d${j}) { s_eff${id}_d${j} = er_d${j}; er_changed = 1; }`,
2404
+ );
1573
2405
  }
1574
2406
  });
1575
2407
  check.push(` if (er_changed) ${name}();`, ' }');
@@ -1578,11 +2410,15 @@ function compileEffect(eff, env, state, out) {
1578
2410
 
1579
2411
  function compileHandler(fnNode, env, state, out) {
1580
2412
  const body = fnNode.body;
1581
- const list = body.type === 'BlockStatement' ? body.body : [{ type: 'ExpressionStatement', expression: body }];
2413
+ const list =
2414
+ body.type === 'BlockStatement'
2415
+ ? body.body
2416
+ : [{type: 'ExpressionStatement', expression: body}];
1582
2417
  // The handler's first parameter is the event; `<event>.x/.y/.dx/.dy` map to EREventData fields.
1583
- const eventParam = fnNode.params[0]?.type === 'Identifier' ? fnNode.params[0].name : null;
1584
- const henv = eventParam ? { ...env, event: eventParam } : env;
1585
- const ctx = { stateChanged: false, animIdx: 0, out };
2418
+ const eventParam =
2419
+ fnNode.params[0]?.type === 'Identifier' ? fnNode.params[0].name : null;
2420
+ const henv = eventParam ? {...env, event: eventParam} : env;
2421
+ const ctx = {stateChanged: false, animIdx: 0, out};
1586
2422
  const stmts = compileStmts(list, henv, state, ctx, ' ');
1587
2423
  if (ctx.stateChanged) stmts.push(' app_update();'); // re-apply state-dependent props once
1588
2424
  return stmts;
@@ -1606,10 +2442,16 @@ function extractProps(openingElement, scope, env) {
1606
2442
  try {
1607
2443
  obj = evalStatic(attr.argument, scope);
1608
2444
  } catch {
1609
- throw new Error('AOT: only a compile-time-constant object can be spread to a component ({...obj})');
2445
+ throw new Error(
2446
+ 'AOT: only a compile-time-constant object can be spread to a component ({...obj})',
2447
+ );
1610
2448
  }
1611
- if (obj == null || typeof obj !== 'object') throw new Error('AOT: a component spread {...x} must resolve to an object');
1612
- for (const [k, v] of Object.entries(obj)) props[k] = { static: true, value: v };
2449
+ if (obj == null || typeof obj !== 'object')
2450
+ throw new Error(
2451
+ 'AOT: a component spread {...x} must resolve to an object',
2452
+ );
2453
+ for (const [k, v] of Object.entries(obj))
2454
+ props[k] = {static: true, value: v};
1613
2455
  continue;
1614
2456
  }
1615
2457
  if (attr.type !== 'JSXAttribute' || attr.name.name === 'key') continue;
@@ -1617,21 +2459,21 @@ function extractProps(openingElement, scope, env) {
1617
2459
  // Callback prop: a function passed to a child (inline arrow, or an identifier bound to a useCallback in
1618
2460
  // the caller). Captured as a `fn` descriptor and resolved where the child uses it as an event handler.
1619
2461
  if (isFn(node)) {
1620
- props[attr.name.name] = { fn: true, node };
2462
+ props[attr.name.name] = {fn: true, node};
1621
2463
  continue;
1622
2464
  }
1623
2465
  if (node.type === 'Identifier' && env.callbacks?.has(node.name)) {
1624
- props[attr.name.name] = { fn: true, node: env.callbacks.get(node.name) };
2466
+ props[attr.name.name] = {fn: true, node: env.callbacks.get(node.name)};
1625
2467
  continue;
1626
2468
  }
1627
2469
  if (node.type === 'Identifier' && env.fnProps?.has(node.name)) {
1628
- props[attr.name.name] = { fn: true, ...env.fnProps.get(node.name) }; // forward original {node, env, state}
2470
+ props[attr.name.name] = {fn: true, ...env.fnProps.get(node.name)}; // forward original {node, env, state}
1629
2471
  continue;
1630
2472
  }
1631
2473
  try {
1632
- props[attr.name.name] = { static: true, value: evalStatic(node, scope) };
2474
+ props[attr.name.name] = {static: true, value: evalStatic(node, scope)};
1633
2475
  } catch {
1634
- props[attr.name.name] = { static: false, ...emitExpr(node, env) };
2476
+ props[attr.name.name] = {static: false, ...emitExpr(node, env)};
1635
2477
  }
1636
2478
  }
1637
2479
  return props;
@@ -1645,18 +2487,30 @@ function bindParams(fn, props) {
1645
2487
  if (param.type === 'Identifier') {
1646
2488
  const obj = {};
1647
2489
  for (const [k, d] of Object.entries(props)) {
1648
- if (!d.static) throw new Error('AOT: dynamic props require a destructured component parameter (e.g. `function C({ x })`)');
2490
+ if (!d.static)
2491
+ throw new Error(
2492
+ 'AOT: dynamic props require a destructured component parameter (e.g. `function C({ x })`)',
2493
+ );
1649
2494
  obj[k] = d.value;
1650
2495
  }
1651
- out.set(param.name, { static: true, value: obj });
2496
+ out.set(param.name, {static: true, value: obj});
1652
2497
  } else if (param.type === 'ObjectPattern') {
1653
2498
  for (const p of param.properties) {
1654
- if (p.type === 'RestElement') throw new Error('AOT: rest props (...rest) in a component param not supported');
2499
+ if (p.type === 'RestElement')
2500
+ throw new Error(
2501
+ 'AOT: rest props (...rest) in a component param not supported',
2502
+ );
1655
2503
  const propName = p.key.name ?? p.key.value;
1656
- const bindName = p.value?.type === 'Identifier' ? p.value.name : p.value?.type === 'AssignmentPattern' ? p.value.left.name : propName;
2504
+ const bindName =
2505
+ p.value?.type === 'Identifier'
2506
+ ? p.value.name
2507
+ : p.value?.type === 'AssignmentPattern'
2508
+ ? p.value.left.name
2509
+ : propName;
1657
2510
  let d = props[propName];
1658
- if (!d && p.value?.type === 'AssignmentPattern') d = { static: true, value: evalStatic(p.value.right, {}) };
1659
- out.set(bindName, d ?? { static: true, value: undefined });
2511
+ if (!d && p.value?.type === 'AssignmentPattern')
2512
+ d = {static: true, value: evalStatic(p.value.right, {})};
2513
+ out.set(bindName, d ?? {static: true, value: undefined});
1660
2514
  }
1661
2515
  } else {
1662
2516
  throw new Error('AOT: unsupported component parameter pattern');
@@ -1668,8 +2522,15 @@ function bindParams(fn, props) {
1668
2522
  function isChildrenRef(expr, env) {
1669
2523
  const cr = env.children?.ref;
1670
2524
  if (!cr) return false;
1671
- if (cr.kind === 'local') return expr.type === 'Identifier' && expr.name === cr.name;
1672
- return expr.type === 'MemberExpression' && !expr.computed && expr.object.type === 'Identifier' && expr.object.name === cr.name && expr.property.name === 'children';
2525
+ if (cr.kind === 'local')
2526
+ return expr.type === 'Identifier' && expr.name === cr.name;
2527
+ return (
2528
+ expr.type === 'MemberExpression' &&
2529
+ !expr.computed &&
2530
+ expr.object.type === 'Identifier' &&
2531
+ expr.object.name === cr.name &&
2532
+ expr.property.name === 'children'
2533
+ );
1673
2534
  }
1674
2535
 
1675
2536
  /** Inlines a function component instance: bind props (static → scope, dynamic → locals), emit its JSX.
@@ -1677,29 +2538,47 @@ function isChildrenRef(expr, env) {
1677
2538
  function emitComponent(el, scope, out, env, state, opts) {
1678
2539
  const tag = el.openingElement.name.name;
1679
2540
  const fn = out.components.get(tag);
1680
- const childNodes = el.children.filter((c) => c.type === 'JSXElement' || (c.type === 'JSXExpressionContainer' && c.expression.type !== 'JSXEmptyExpression'));
2541
+ const childNodes = el.children.filter(
2542
+ c =>
2543
+ c.type === 'JSXElement' ||
2544
+ (c.type === 'JSXExpressionContainer' &&
2545
+ c.expression.type !== 'JSXEmptyExpression'),
2546
+ );
1681
2547
 
1682
2548
  // How the body refers to children: destructured `{ children }` (a local) or whole `props` → props.children.
1683
2549
  const param = fn.params[0];
1684
2550
  let childrenRef = null;
1685
2551
  if (param?.type === 'ObjectPattern') {
1686
- for (const p of param.properties) if ((p.key?.name ?? p.key?.value) === 'children') childrenRef = { kind: 'local', name: p.value?.name ?? 'children' };
2552
+ for (const p of param.properties)
2553
+ if ((p.key?.name ?? p.key?.value) === 'children')
2554
+ childrenRef = {kind: 'local', name: p.value?.name ?? 'children'};
1687
2555
  } else if (param?.type === 'Identifier') {
1688
- childrenRef = { kind: 'props', name: param.name };
2556
+ childrenRef = {kind: 'props', name: param.name};
1689
2557
  }
1690
2558
 
1691
- const childScope = { ...scope };
2559
+ const childScope = {...scope};
1692
2560
  const childLocals = new Map(env.locals);
1693
2561
  // Callback props bound here resolve to the CALLER's function (node + caller env/state) so the child can
1694
2562
  // use them as event handlers (onPress={onTap}); inherit any the caller itself received (forwarding).
1695
2563
  const fnProps = new Map(env.fnProps);
1696
- for (const [name, d] of bindParams(fn, extractProps(el.openingElement, scope, env))) {
2564
+ for (const [name, d] of bindParams(
2565
+ fn,
2566
+ extractProps(el.openingElement, scope, env),
2567
+ )) {
1697
2568
  if (childrenRef?.kind === 'local' && name === childrenRef.name) continue; // children come from the slot, not a value prop
1698
- if (d.fn) fnProps.set(name, { node: d.node, env: d.env ?? env, state: d.state ?? state });
2569
+ if (d.fn)
2570
+ fnProps.set(name, {
2571
+ node: d.node,
2572
+ env: d.env ?? env,
2573
+ state: d.state ?? state,
2574
+ });
1699
2575
  else if (d.static) childScope[name] = d.value;
1700
- else childLocals.set(name, { code: d.code, cType: d.cType, struct: d.struct });
2576
+ else
2577
+ childLocals.set(name, {code: d.code, cType: d.cType, struct: d.struct});
1701
2578
  }
1702
- const children = childNodes.length ? { nodes: childNodes, scope, env, ref: childrenRef } : null;
2579
+ const children = childNodes.length
2580
+ ? {nodes: childNodes, scope, env, ref: childrenRef}
2581
+ : null;
1703
2582
 
1704
2583
  // Per-instance hooks: a child component is inlined, so EACH instance gets its OWN state, refs, animated
1705
2584
  // values, callbacks, memos and mount-effects — namespaced by a unique prefix (`c<N>_`) so two instances
@@ -1740,19 +2619,27 @@ function emitComponent(el, scope, out, env, state, opts) {
1740
2619
  childScope[name] = evalStatic(expr, childScope);
1741
2620
  } catch {
1742
2621
  const e = emitExpr(expr, childEnv);
1743
- childLocals.set(name, { code: `(${e.code})`, cType: e.cType });
2622
+ childLocals.set(name, {code: `(${e.code})`, cType: e.cType});
1744
2623
  }
1745
2624
  }
1746
2625
  for (const eff of collectEffects(fn.body)) {
1747
2626
  compileEffect(eff, childEnv, childState, out);
1748
2627
  }
1749
2628
 
1750
- return emitNode(componentReturnJSX(fn, childScope), childScope, out, childEnv, childState, opts);
2629
+ return emitNode(
2630
+ componentReturnJSX(fn, childScope),
2631
+ childScope,
2632
+ out,
2633
+ childEnv,
2634
+ childState,
2635
+ opts,
2636
+ );
1751
2637
  }
1752
2638
 
1753
2639
  /** Emits an element / component child and appends it to the parent. opts.displayCode toggles its show. */
1754
2640
  function emitElementInto(node, parentVar, scope, out, env, state, opts) {
1755
- if (node.type !== 'JSXElement') throw new Error(`AOT: expected a JSX element here, got ${node.type}`);
2641
+ if (node.type !== 'JSXElement')
2642
+ throw new Error(`AOT: expected a JSX element here, got ${node.type}`);
1756
2643
  const cv = emitNode(node, scope, out, env, state, opts);
1757
2644
  out.build.push(` er_tree_append_child(${parentVar}, ${cv});`);
1758
2645
  }
@@ -1763,19 +2650,30 @@ function emitMap(call, parentVar, scope, out, env, state) {
1763
2650
  try {
1764
2651
  arr = evalStatic(call.callee.object, scope);
1765
2652
  } catch {
1766
- throw new Error('AOT: .map target must be a compile-time-constant array (dynamic lists not yet supported)');
2653
+ throw new Error(
2654
+ 'AOT: .map target must be a compile-time-constant array (dynamic lists not yet supported)',
2655
+ );
1767
2656
  }
1768
- if (!Array.isArray(arr)) throw new Error('AOT: .map target did not resolve to an array');
2657
+ if (!Array.isArray(arr))
2658
+ throw new Error('AOT: .map target did not resolve to an array');
1769
2659
  const cb = call.arguments[0];
1770
- if (!isFn(cb)) throw new Error('AOT: .map argument must be an inline function');
2660
+ if (!isFn(cb))
2661
+ throw new Error('AOT: .map argument must be an inline function');
1771
2662
  const itemName = cb.params[0]?.name;
1772
2663
  const idxName = cb.params[1]?.name;
1773
2664
  const retJSX = componentReturnJSX(cb);
1774
2665
  arr.forEach((item, i) => {
1775
- const iterScope = { ...scope };
2666
+ const iterScope = {...scope};
1776
2667
  if (itemName) iterScope[itemName] = item;
1777
2668
  if (idxName) iterScope[idxName] = i;
1778
- emitElementInto(retJSX, parentVar, iterScope, out, { ...env, consts: iterScope }, state);
2669
+ emitElementInto(
2670
+ retJSX,
2671
+ parentVar,
2672
+ iterScope,
2673
+ out,
2674
+ {...env, consts: iterScope},
2675
+ state,
2676
+ );
1779
2677
  });
1780
2678
  }
1781
2679
 
@@ -1786,16 +2684,31 @@ function emitMap(call, parentVar, scope, out, env, state) {
1786
2684
  */
1787
2685
  function emitDynamicMap(call, rec, parentVar, scope, out, env, state) {
1788
2686
  const cb = call.arguments[0];
1789
- if (!isFn(cb)) throw new Error('AOT: .map argument must be an inline function');
2687
+ if (!isFn(cb))
2688
+ throw new Error('AOT: .map argument must be an inline function');
1790
2689
  const itemName = cb.params[0]?.name;
1791
2690
  const idxName = cb.params[1]?.name;
1792
2691
  const retJSX = componentReturnJSX(cb);
1793
2692
  for (let k = 0; k < rec.cap; k++) {
1794
- const iterScope = { ...scope };
2693
+ const iterScope = {...scope};
1795
2694
  if (idxName) iterScope[idxName] = k; // the index is a compile-time literal per pooled row
1796
2695
  const locals = new Map(env.locals);
1797
- if (itemName) locals.set(itemName, { code: `${rec.arrayName}[${k}]`, struct: rec.struct });
1798
- emitElementInto(retJSX, parentVar, iterScope, out, { ...env, consts: iterScope, locals }, state, { displayCode: `(${k} < ${rec.countMember})` });
2696
+ if (itemName)
2697
+ locals.set(itemName, {
2698
+ code: `${rec.arrayName}[${k}]`,
2699
+ struct: rec.struct,
2700
+ });
2701
+ emitElementInto(
2702
+ retJSX,
2703
+ parentVar,
2704
+ iterScope,
2705
+ out,
2706
+ {...env, consts: iterScope, locals},
2707
+ state,
2708
+ {
2709
+ displayCode: `(${k} < ${rec.countMember})`,
2710
+ },
2711
+ );
1799
2712
  }
1800
2713
  }
1801
2714
 
@@ -1809,7 +2722,14 @@ function emitChildren(children, parentVar, scope, out, env, state) {
1809
2722
  if (expr.type === 'JSXEmptyExpression') continue;
1810
2723
  if (isChildrenRef(expr, env)) {
1811
2724
  // {children} / {props.children}: emit the captured call-site children, in the caller's scope/env.
1812
- emitChildren(env.children.nodes, parentVar, env.children.scope, out, env.children.env, state);
2725
+ emitChildren(
2726
+ env.children.nodes,
2727
+ parentVar,
2728
+ env.children.scope,
2729
+ out,
2730
+ env.children.env,
2731
+ state,
2732
+ );
1813
2733
  continue;
1814
2734
  }
1815
2735
  if (expr.type === 'LogicalExpression' && expr.operator === '&&') {
@@ -1818,26 +2738,57 @@ function emitChildren(children, parentVar, scope, out, env, state) {
1818
2738
  let cond;
1819
2739
  try {
1820
2740
  cond = evalStatic(expr.left, scope);
1821
- if (cond) emitElementInto(expr.right, parentVar, scope, out, env, state);
2741
+ if (cond)
2742
+ emitElementInto(expr.right, parentVar, scope, out, env, state);
1822
2743
  } catch {
1823
2744
  const code = emitExpr(expr.left, env).code;
1824
- emitElementInto(expr.right, parentVar, scope, out, env, state, { displayCode: code });
2745
+ emitElementInto(expr.right, parentVar, scope, out, env, state, {
2746
+ displayCode: code,
2747
+ });
1825
2748
  }
1826
- } else if (expr.type === 'ConditionalExpression' && (expr.consequent.type === 'JSXElement' || expr.alternate.type === 'JSXElement')) {
2749
+ } else if (
2750
+ expr.type === 'ConditionalExpression' &&
2751
+ (expr.consequent.type === 'JSXElement' ||
2752
+ expr.alternate.type === 'JSXElement')
2753
+ ) {
1827
2754
  // `{cond ? <A/> : <B/>}`. Static cond picks a branch; dynamic cond builds both and toggles each.
1828
2755
  let test;
1829
2756
  try {
1830
2757
  test = evalStatic(expr.test, scope);
1831
- emitElementInto(test ? expr.consequent : expr.alternate, parentVar, scope, out, env, state);
2758
+ emitElementInto(
2759
+ test ? expr.consequent : expr.alternate,
2760
+ parentVar,
2761
+ scope,
2762
+ out,
2763
+ env,
2764
+ state,
2765
+ );
1832
2766
  } catch {
1833
2767
  const code = emitExpr(expr.test, env).code;
1834
- if (expr.consequent.type === 'JSXElement') emitElementInto(expr.consequent, parentVar, scope, out, env, state, { displayCode: code });
1835
- if (expr.alternate.type === 'JSXElement') emitElementInto(expr.alternate, parentVar, scope, out, env, state, { displayCode: `!(${code})` });
2768
+ if (expr.consequent.type === 'JSXElement')
2769
+ emitElementInto(
2770
+ expr.consequent,
2771
+ parentVar,
2772
+ scope,
2773
+ out,
2774
+ env,
2775
+ state,
2776
+ {displayCode: code},
2777
+ );
2778
+ if (expr.alternate.type === 'JSXElement')
2779
+ emitElementInto(expr.alternate, parentVar, scope, out, env, state, {
2780
+ displayCode: `!(${code})`,
2781
+ });
1836
2782
  }
1837
- } else if (expr.type === 'CallExpression' && expr.callee.type === 'MemberExpression' && expr.callee.property.name === 'map') {
2783
+ } else if (
2784
+ expr.type === 'CallExpression' &&
2785
+ expr.callee.type === 'MemberExpression' &&
2786
+ expr.callee.property.name === 'map'
2787
+ ) {
1838
2788
  const obj = expr.callee.object;
1839
2789
  const rec = obj.type === 'Identifier' ? env.state.get(obj.name) : null;
1840
- if (rec?.kind === 'list') emitDynamicMap(expr, rec, parentVar, scope, out, env, state);
2790
+ if (rec?.kind === 'list')
2791
+ emitDynamicMap(expr, rec, parentVar, scope, out, env, state);
1841
2792
  else emitMap(expr, parentVar, scope, out, env, state);
1842
2793
  } else {
1843
2794
  // A constant that renders nothing (false/null/'') is fine; anything else is unsupported.
@@ -1847,13 +2798,16 @@ function emitChildren(children, parentVar, scope, out, env, state) {
1847
2798
  } catch {
1848
2799
  const e = aotError(
1849
2800
  `AOT: unsupported expression child "${expr.type}" in a container`,
1850
- 'a child expression must be a JSX element, `cond && <El/>` / a ternary of elements, or `list.map(item => <El/>)`. A bare variable holding JSX isn\'t inlined — write the element directly. (If this is a responsive Flow-A-only branch, compile at the target board size via ER_AOT_SCREEN_W/H so the AOT folds the supported branch.)',
2801
+ "a child expression must be a JSX element, `cond && <El/>` / a ternary of elements, or `list.map(item => <El/>)`. A bare variable holding JSX isn't inlined — write the element directly. (If this is a responsive Flow-A-only branch, compile at the target board size via ER_AOT_SCREEN_W/H so the AOT folds the supported branch.)",
1851
2802
  );
1852
2803
  if (expr.loc) e.aotLoc = expr.loc.start;
1853
2804
  throw e;
1854
2805
  }
1855
2806
  if (v !== false && v != null && v !== '') {
1856
- const e = aotError(`AOT: a non-element expression child (${JSON.stringify(v)}) cannot render here`, 'only JSX elements render as children; wrap text in a <Text>{…}</Text>.');
2807
+ const e = aotError(
2808
+ `AOT: a non-element expression child (${JSON.stringify(v)}) cannot render here`,
2809
+ 'only JSX elements render as children; wrap text in a <Text>{…}</Text>.',
2810
+ );
1857
2811
  if (expr.loc) e.aotLoc = expr.loc.start;
1858
2812
  throw e;
1859
2813
  }
@@ -1868,32 +2822,65 @@ function emitChildren(children, parentVar, scope, out, env, state) {
1868
2822
  // ---------------------------------------------------------------------------------------------------
1869
2823
 
1870
2824
  /** Converts an SVG JSX element (Svg/Circle/Path/Rect/Line/Arc/G/…) to flattenSvg's `{type, props}` shape,
1871
- * statically evaluating every attribute. Dynamic attrs/children throw (state-driven Svg is Phase 6b). */
2825
+ * statically evaluating every attribute. Dynamic attrs/children throw (a state-driven Svg is not supported). */
1872
2826
  function jsxToSvgElement(node, scope) {
1873
2827
  if (node.type !== 'JSXElement') return null;
1874
2828
  const type = node.openingElement.name.name;
1875
2829
  const props = {};
1876
2830
  for (const attr of node.openingElement.attributes) {
1877
- if (attr.type !== 'JSXAttribute') throw new Error('AOT: spread attributes on an <Svg> element not supported');
2831
+ if (attr.type !== 'JSXAttribute')
2832
+ throw new Error(
2833
+ 'AOT: spread attributes on an <Svg> element not supported',
2834
+ );
1878
2835
  const name = attr.name.name;
1879
2836
  if (name === 'ref' || name === 'key') continue; // not geometry/paint
1880
2837
  if (attr.value == null) props[name] = true;
1881
- else if (attr.value.type === 'StringLiteral') props[name] = attr.value.value;
1882
- else if (attr.value.type === 'JSXExpressionContainer') props[name] = evalStatic(attr.value.expression, scope);
1883
- else throw new Error(`AOT: unsupported <${type}> attribute value for "${name}"`);
2838
+ else if (attr.value.type === 'StringLiteral')
2839
+ props[name] = attr.value.value;
2840
+ else if (attr.value.type === 'JSXExpressionContainer')
2841
+ props[name] = evalStatic(attr.value.expression, scope);
2842
+ else
2843
+ throw new Error(
2844
+ `AOT: unsupported <${type}> attribute value for "${name}"`,
2845
+ );
1884
2846
  }
1885
2847
  const children = [];
1886
2848
  for (const c of node.children) {
1887
2849
  if (c.type === 'JSXElement') children.push(jsxToSvgElement(c, scope));
1888
- else if (c.type === 'JSXExpressionContainer' && c.expression.type !== 'JSXEmptyExpression') throw new Error('AOT: dynamic <Svg> children ({…}) not yet supported — use literal shape elements');
2850
+ else if (
2851
+ c.type === 'JSXExpressionContainer' &&
2852
+ c.expression.type !== 'JSXEmptyExpression'
2853
+ )
2854
+ throw new Error(
2855
+ 'AOT: dynamic <Svg> children ({…}) not yet supported — use literal shape elements',
2856
+ );
1889
2857
  }
1890
2858
  if (children.length) props.children = children;
1891
- return { type, props };
2859
+ return {type, props};
1892
2860
  }
1893
2861
 
1894
- /** Emits one ERVectorPaint initializer from a 7-number flattenSvg paint record [fill,stroke,w,miter,cap,join,rule]. */
2862
+ /** Emits one ERVectorPaint initializer from a flattenSvg paint record
2863
+ * [fill,stroke,w,miter,cap,join,rule, fill_grad, stroke_grad]. fill_grad/stroke_grad (1-based gradient-table
2864
+ * indices, 0 = solid) are absent on inline-<Svg> records (7-wide) → zero, and set on baked <Svg source>. */
1895
2865
  function emitVectorPaint(p) {
1896
- return `{ .fill = ${p[0] >>> 0}u, .stroke = ${p[1] >>> 0}u, .stroke_w = ${floatLit(p[2])}, .miter = ${floatLit(p[3])}, .cap = ${p[4] | 0}, .join = ${p[5] | 0}, .fill_rule = ${p[6] | 0} }`;
2866
+ return `{ .fill = ${p[0] >>> 0}u, .stroke = ${p[1] >>> 0}u, .stroke_w = ${floatLit(p[2])}, .miter = ${floatLit(p[3])}, .cap = ${p[4] | 0}, .join = ${p[5] | 0}, .fill_rule = ${p[6] | 0}, .fill_grad = ${(p[7] || 0) | 0}, .stroke_grad = ${(p[8] || 0) | 0} }`;
2867
+ }
2868
+
2869
+ /** Emits one ERVectorGradient initializer from a baked artifact gradient
2870
+ * { type, stops:[{color, offset}], ax, ay, bx, by, r }. Stops fill the C ERGradientStop[] positionally
2871
+ * ({color, position}); the engine zero-inits the rest of stops[ER_VGRAD_MAX_STOPS]. Geometry meaning per
2872
+ * type: linear axis (ax,ay)->(bx,by); radial centre (ax,ay)+radius r; conic centre (ax,ay)+start angle r. */
2873
+ function emitVectorGradient(g) {
2874
+ const inStops = g.stops || [];
2875
+ const capped = inStops.length > 8 ? inStops.slice(0, 8) : inStops;
2876
+ const stops = capped
2877
+ .map(s => `{ ${s.color >>> 0}u, ${floatLit(s.offset)} }`)
2878
+ .join(', ');
2879
+ return (
2880
+ `{ .type = ${g.type | 0}, .stop_count = ${capped.length}, .stops = { ${stops} }, ` +
2881
+ `.ax = ${floatLit(g.ax || 0)}, .ay = ${floatLit(g.ay || 0)}, .bx = ${floatLit(g.bx || 0)}, ` +
2882
+ `.by = ${floatLit(g.by || 0)}, .r = ${floatLit(g.r || 0)} }`
2883
+ );
1897
2884
  }
1898
2885
 
1899
2886
  /** Static numeric coercion for an SVG attribute value (mirrors svg-ops `num`). */
@@ -1902,15 +2889,19 @@ const svgNum = (v, d = 0) => {
1902
2889
  return Number.isNaN(n) ? d : n;
1903
2890
  };
1904
2891
  /** True if an attribute value is a state-driven C expression (vs a static number/string). */
1905
- const isDyn = (v) => v != null && typeof v === 'object' && 'dyn' in v;
2892
+ const isDyn = v => v != null && typeof v === 'object' && 'dyn' in v;
1906
2893
  /** Lowers an SVG coordinate attr to a C float expression (literal when static, cast expr when dynamic). */
1907
- const cf = (v, d = 0) => (isDyn(v) ? `(float)(${v.dyn})` : floatLit(svgNum(v, d)));
2894
+ const cf = (v, d = 0) =>
2895
+ isDyn(v) ? `(float)(${v.dyn})` : floatLit(svgNum(v, d));
1908
2896
 
1909
2897
  /** Reads an SVG element's attributes → { name: number|string|true | {dyn: cExpr} } (state attrs → {dyn}). */
1910
2898
  function svgAttrs(openingElement, scope, env) {
1911
2899
  const out = {};
1912
2900
  for (const attr of openingElement.attributes) {
1913
- if (attr.type !== 'JSXAttribute') throw new Error('AOT: spread attributes on an <Svg> element not supported');
2901
+ if (attr.type !== 'JSXAttribute')
2902
+ throw new Error(
2903
+ 'AOT: spread attributes on an <Svg> element not supported',
2904
+ );
1914
2905
  const name = attr.name.name;
1915
2906
  if (name === 'ref' || name === 'key') continue; // not geometry/paint
1916
2907
  const vn = attr.value;
@@ -1922,18 +2913,30 @@ function svgAttrs(openingElement, scope, env) {
1922
2913
  } catch {
1923
2914
  // Keep the raw expression node too: color paint attrs (fill/stroke) lower via emitColorExpr (→ ARGB),
1924
2915
  // not the generic numeric `dyn` code, so a dynamic color resolves to a uint, not a char*.
1925
- out[name] = { dyn: emitExpr(vn.expression, env).code, node: vn.expression };
2916
+ out[name] = {
2917
+ dyn: emitExpr(vn.expression, env).code,
2918
+ node: vn.expression,
2919
+ };
1926
2920
  }
1927
- } else throw new Error(`AOT: unsupported SVG attribute value for "${name}"`);
2921
+ } else
2922
+ throw new Error(`AOT: unsupported SVG attribute value for "${name}"`);
1928
2923
  }
1929
2924
  return out;
1930
2925
  }
1931
2926
 
1932
- const CAP_MAP = { butt: 0, round: 1, square: 2 };
1933
- const JOIN_MAP = { miter: 0, round: 1, bevel: 2 };
2927
+ const CAP_MAP = {butt: 0, round: 1, square: 2};
2928
+ const JOIN_MAP = {miter: 0, round: 1, bevel: 2};
1934
2929
 
1935
2930
  /** The ERVectorPaint members, in op-tape paint order [fill,stroke,stroke_w,miter,cap,join,fill_rule]. */
1936
- const PAINT_FIELDS = ['fill', 'stroke', 'stroke_w', 'miter', 'cap', 'join', 'fill_rule'];
2931
+ const PAINT_FIELDS = [
2932
+ 'fill',
2933
+ 'stroke',
2934
+ 'stroke_w',
2935
+ 'miter',
2936
+ 'cap',
2937
+ 'join',
2938
+ 'fill_rule',
2939
+ ];
1937
2940
 
1938
2941
  /**
1939
2942
  * A shape's paint as 7 C-expr fields (matching PAINT_FIELDS) + whether any is state-driven.
@@ -1956,10 +2959,26 @@ function paintSpec(a, env) {
1956
2959
  anyDynamic = true;
1957
2960
  strokeW = `(float)(${a.strokeWidth.dyn})`;
1958
2961
  } else strokeW = floatLit(svgNum(a.strokeWidth, 1));
1959
- for (const k of ['strokeLinecap', 'strokeLinejoin', 'strokeMiterlimit', 'fillRule'])
1960
- if (isDyn(a[k])) throw new Error(`AOT: a state-driven <Svg> "${k}" is not supported (only fill / stroke / strokeWidth can be state-driven)`);
1961
- const fields = [color(a.fill, 'black'), color(a.stroke, 'none'), strokeW, floatLit(svgNum(a.strokeMiterlimit, 4)), String(CAP_MAP[a.strokeLinecap] ?? 0), String(JOIN_MAP[a.strokeLinejoin] ?? 0), String(a.fillRule === 'evenodd' ? 1 : 0)];
1962
- return { fields, anyDynamic };
2962
+ for (const k of [
2963
+ 'strokeLinecap',
2964
+ 'strokeLinejoin',
2965
+ 'strokeMiterlimit',
2966
+ 'fillRule',
2967
+ ])
2968
+ if (isDyn(a[k]))
2969
+ throw new Error(
2970
+ `AOT: a state-driven <Svg> "${k}" is not supported (only fill / stroke / strokeWidth can be state-driven)`,
2971
+ );
2972
+ const fields = [
2973
+ color(a.fill, 'black'),
2974
+ color(a.stroke, 'none'),
2975
+ strokeW,
2976
+ floatLit(svgNum(a.strokeMiterlimit, 4)),
2977
+ String(CAP_MAP[a.strokeLinecap] ?? 0),
2978
+ String(JOIN_MAP[a.strokeLinejoin] ?? 0),
2979
+ String(a.fillRule === 'evenodd' ? 1 : 0),
2980
+ ];
2981
+ return {fields, anyDynamic};
1963
2982
  }
1964
2983
 
1965
2984
  /** A `{ .fill = …, … }` ERVectorPaint initializer from a paintSpec's C-expr fields (used for static paints). */
@@ -1973,31 +2992,91 @@ function paintInitFromSpec(ps) {
1973
2992
  const arcEntriesC = (cx, cy, r, a0deg, a1deg) => {
1974
2993
  const a0 = `((${a0deg} - 90.0f) * (float)M_PI / 180.0f)`;
1975
2994
  const a1 = `((${a1deg} - 90.0f) * (float)M_PI / 180.0f)`;
1976
- return ['ER_VOP_MOVE', `(${cx} + ${r} * cosf(${a0}))`, `(${cy} + ${r} * sinf(${a0}))`, 'ER_VOP_ARC', cx, cy, r, a0, a1, '0.0f'];
2995
+ return [
2996
+ 'ER_VOP_MOVE',
2997
+ `(${cx} + ${r} * cosf(${a0}))`,
2998
+ `(${cy} + ${r} * sinf(${a0}))`,
2999
+ 'ER_VOP_ARC',
3000
+ cx,
3001
+ cy,
3002
+ r,
3003
+ a0,
3004
+ a1,
3005
+ '0.0f',
3006
+ ];
1977
3007
  };
1978
- const circleEntriesC = (cx, cy, r) => ['ER_VOP_MOVE', `(${cx} + ${r})`, cy, 'ER_VOP_ARC', cx, cy, r, '0.0f', '(2.0f * (float)M_PI)', '0.0f', 'ER_VOP_CLOSE'];
1979
- const rectEntriesC = (x, y, w, h) => ['ER_VOP_MOVE', x, y, 'ER_VOP_LINE', `(${x} + ${w})`, y, 'ER_VOP_LINE', `(${x} + ${w})`, `(${y} + ${h})`, 'ER_VOP_LINE', x, `(${y} + ${h})`, 'ER_VOP_CLOSE'];
1980
- const lineEntriesC = (x1, y1, x2, y2) => ['ER_VOP_MOVE', x1, y1, 'ER_VOP_LINE', x2, y2];
3008
+ const circleEntriesC = (cx, cy, r) => [
3009
+ 'ER_VOP_MOVE',
3010
+ `(${cx} + ${r})`,
3011
+ cy,
3012
+ 'ER_VOP_ARC',
3013
+ cx,
3014
+ cy,
3015
+ r,
3016
+ '0.0f',
3017
+ '(2.0f * (float)M_PI)',
3018
+ '0.0f',
3019
+ 'ER_VOP_CLOSE',
3020
+ ];
3021
+ const rectEntriesC = (x, y, w, h) => [
3022
+ 'ER_VOP_MOVE',
3023
+ x,
3024
+ y,
3025
+ 'ER_VOP_LINE',
3026
+ `(${x} + ${w})`,
3027
+ y,
3028
+ 'ER_VOP_LINE',
3029
+ `(${x} + ${w})`,
3030
+ `(${y} + ${h})`,
3031
+ 'ER_VOP_LINE',
3032
+ x,
3033
+ `(${y} + ${h})`,
3034
+ 'ER_VOP_CLOSE',
3035
+ ];
3036
+ const lineEntriesC = (x1, y1, x2, y2) => [
3037
+ 'ER_VOP_MOVE',
3038
+ x1,
3039
+ y1,
3040
+ 'ER_VOP_LINE',
3041
+ x2,
3042
+ y2,
3043
+ ];
1981
3044
 
1982
3045
  // JSX-attribute wrappers (6b): resolve each attr to a C float via cf().
1983
- const arcEntries = (a) => arcEntriesC(cf(a.cx), cf(a.cy), cf(a.r), cf(a.startAngle), cf(a.endAngle));
1984
- const circleEntries = (a) => circleEntriesC(cf(a.cx), cf(a.cy), cf(a.r));
1985
- const rectEntries = (a) => rectEntriesC(cf(a.x), cf(a.y), cf(a.width), cf(a.height));
1986
- const lineEntries = (a) => lineEntriesC(cf(a.x1), cf(a.y1), cf(a.x2), cf(a.y2));
1987
- const pathEntries = (a) => {
3046
+ const arcEntries = a =>
3047
+ arcEntriesC(cf(a.cx), cf(a.cy), cf(a.r), cf(a.startAngle), cf(a.endAngle));
3048
+ const circleEntries = a => circleEntriesC(cf(a.cx), cf(a.cy), cf(a.r));
3049
+ const rectEntries = a =>
3050
+ rectEntriesC(cf(a.x), cf(a.y), cf(a.width), cf(a.height));
3051
+ const lineEntries = a => lineEntriesC(cf(a.x1), cf(a.y1), cf(a.x2), cf(a.y2));
3052
+ const pathEntries = a => {
1988
3053
  if (a.d == null) return [];
1989
- if (isDyn(a.d)) throw new Error('AOT: a state-driven <Path d=…> is not yet supported (use Arc/Circle/Rect/Line for dynamic shapes)');
3054
+ if (isDyn(a.d))
3055
+ throw new Error(
3056
+ 'AOT: a state-driven <Path d=…> is not yet supported (use Arc/Circle/Rect/Line for dynamic shapes)',
3057
+ );
1990
3058
  return parsePath(String(a.d)).map(floatLit); // opcodes are encoded as float values 0..6, like coords
1991
3059
  };
1992
- const SHAPE_ENTRIES = { Arc: arcEntries, Circle: circleEntries, Rect: rectEntries, Line: lineEntries, Path: pathEntries };
3060
+ const SHAPE_ENTRIES = {
3061
+ Arc: arcEntries,
3062
+ Circle: circleEntries,
3063
+ Rect: rectEntries,
3064
+ Line: lineEntries,
3065
+ Path: pathEntries,
3066
+ };
1993
3067
 
1994
3068
  /** True if any attribute anywhere in the <Svg> subtree references state (→ the state-driven path). */
1995
3069
  function svgHasDynamic(el, scope) {
1996
3070
  let dyn = false;
1997
- const walk = (node) => {
3071
+ const walk = node => {
1998
3072
  if (node.type !== 'JSXElement') return;
1999
3073
  for (const attr of node.openingElement.attributes) {
2000
- if (attr.type === 'JSXAttribute' && attr.name.name !== 'ref' && attr.name.name !== 'key' && attr.value?.type === 'JSXExpressionContainer') {
3074
+ if (
3075
+ attr.type === 'JSXAttribute' &&
3076
+ attr.name.name !== 'ref' &&
3077
+ attr.name.name !== 'key' &&
3078
+ attr.value?.type === 'JSXExpressionContainer'
3079
+ ) {
2001
3080
  try {
2002
3081
  evalStatic(attr.value.expression, scope);
2003
3082
  } catch {
@@ -2013,11 +3092,17 @@ function svgHasDynamic(el, scope) {
2013
3092
 
2014
3093
  /** Emits the vector node's box: create + props + width/height + optional style={}. */
2015
3094
  function emitSvgBox(v, width, height, openingElement, scope, out, env) {
2016
- const { staticAssigns } = collectStyleAssigns(openingElement, scope, env);
2017
- out.build.push(` ${v} = er_node_create(ER_NODE_VECTOR);`, ` er_props_default(&p);`);
2018
- if (typeof width === 'number') out.build.push(` p.width = (int16_t)${Math.round(width)};`);
2019
- if (typeof height === 'number') out.build.push(` p.height = (int16_t)${Math.round(height)};`);
2020
- for (const a of staticAssigns) out.build.push(` p.${a.field} = ${a.expr};`);
3095
+ const {staticAssigns} = collectStyleAssigns(openingElement, scope, env);
3096
+ out.build.push(
3097
+ ` ${v} = er_node_create(ER_NODE_VECTOR);`,
3098
+ ` er_props_default(&p);`,
3099
+ );
3100
+ if (typeof width === 'number')
3101
+ out.build.push(` p.width = (int16_t)${Math.round(width)};`);
3102
+ if (typeof height === 'number')
3103
+ out.build.push(` p.height = (int16_t)${Math.round(height)};`);
3104
+ for (const a of staticAssigns)
3105
+ out.build.push(` p.${a.field} = ${a.expr};`);
2021
3106
  out.build.push(` er_node_set_props(${v}, &p);`);
2022
3107
  emitRefBind(v, openingElement, out, env);
2023
3108
  }
@@ -2025,23 +3110,147 @@ function emitSvgBox(v, width, height, openingElement, scope, out, env) {
2025
3110
  /** <Svg> → ER_NODE_VECTOR. Static subtree → a baked const op-tape; any state-driven attr → a symbolic
2026
3111
  * op-tape rebuilt by a generated build_svgN() at build time and on every app_update. */
2027
3112
  function emitSvg(el, scope, out, env, state, opts) {
2028
- if (opts.displayCode) throw new Error('AOT: an <Svg> inside a dynamic conditional is not yet supported');
2029
- return svgHasDynamic(el, scope) ? emitSvgDynamic(el, scope, out, env, state) : emitSvgStatic(el, scope, out, env);
3113
+ if (opts.displayCode)
3114
+ throw new Error(
3115
+ 'AOT: an <Svg> inside a dynamic conditional is not yet supported',
3116
+ );
3117
+ const sourceAttr = el.openingElement.attributes.find(
3118
+ a => a.type === 'JSXAttribute' && a.name && a.name.name === 'source',
3119
+ );
3120
+ if (sourceAttr) return emitSvgSource(el, sourceAttr, scope, out, env);
3121
+ return svgHasDynamic(el, scope)
3122
+ ? emitSvgDynamic(el, scope, out, env, state)
3123
+ : emitSvgStatic(el, scope, out, env);
2030
3124
  }
2031
3125
 
2032
3126
  /** Static <Svg>: reuse flattenSvg (full feature set: viewBox, <G>, Path) and bake const arrays. */
2033
3127
  function emitSvgStatic(el, scope, out, env) {
2034
3128
  const svgEl = jsxToSvgElement(el, scope);
2035
- const { ops, paints } = flattenSvg(svgEl.props);
3129
+ const {ops, paints} = flattenSvg(svgEl.props);
3130
+ const v = `n${out.n++}`;
3131
+ const id = out.svgN++;
3132
+ const nPaints = paints.length / PAINT_STRIDE;
3133
+ if (ops.length) {
3134
+ out.vectorData.push(
3135
+ `static const float s_svg${id}_ops[] = {\n ${Array.from(ops, floatLit).join(', ')}\n};`,
3136
+ );
3137
+ // flattenSvg paints are PAINT_STRIDE-wide; inline <Svg> has no gradients so fill_grad/stroke_grad are 0.
3138
+ out.vectorData.push(
3139
+ `static const ERVectorPaint s_svg${id}_paints[] = {\n${Array.from({length: nPaints}, (_, i) => ' ' + emitVectorPaint(paints.slice(i * PAINT_STRIDE, i * PAINT_STRIDE + PAINT_STRIDE))).join(',\n')}\n};`,
3140
+ );
3141
+ }
3142
+ emitSvgBox(
3143
+ v,
3144
+ svgEl.props.width,
3145
+ svgEl.props.height,
3146
+ el.openingElement,
3147
+ scope,
3148
+ out,
3149
+ env,
3150
+ );
3151
+ if (ops.length)
3152
+ out.build.push(
3153
+ ` er_node_set_vector_ops(${v}, s_svg${id}_ops, ${ops.length}, s_svg${id}_paints, ${nPaints}, NULL, 0);`,
3154
+ );
3155
+ return v;
3156
+ }
3157
+
3158
+ /** Baked <Svg source={importedSvg}>: the .svg is pre-baked to a vector artifact (ops/paints/GRADIENTS) by the
3159
+ * CLI (bakeSvgArtifacts → opts.svgArtifacts) since compileSource is I/O-free. Scaled to the static width/height
3160
+ * at compile time, then emitted as const tables. This is the path that carries gradients into Flow B. */
3161
+ function emitSvgSource(el, sourceAttr, scope, out, env) {
3162
+ const expr =
3163
+ sourceAttr.value && sourceAttr.value.type === 'JSXExpressionContainer'
3164
+ ? sourceAttr.value.expression
3165
+ : null;
3166
+ if (!expr || expr.type !== 'Identifier')
3167
+ throw new Error(
3168
+ 'AOT: <Svg source> must reference an imported .svg (source={importedSvg})',
3169
+ );
3170
+ const imp = env.svgImports.get(expr.name);
3171
+ if (!imp)
3172
+ throw new Error(
3173
+ `AOT: <Svg source={${expr.name}}> — no matching \`import ${expr.name} from '...svg'\``,
3174
+ );
3175
+ const art = env.svgArtifacts[imp.name];
3176
+ if (!art)
3177
+ throw new Error(
3178
+ `AOT: vector artifact for "${imp.name}" was not baked (internal: opts.svgArtifacts missing it)`,
3179
+ );
3180
+
3181
+ // Static width/height (props or {expr} that folds) → scale the artifact at compile time; default = intrinsic.
3182
+ const numAttr = (name, dflt) => {
3183
+ const at = el.openingElement.attributes.find(
3184
+ a => a.type === 'JSXAttribute' && a.name && a.name.name === name,
3185
+ );
3186
+ if (!at || at.value == null) return dflt;
3187
+ if (at.value.type === 'StringLiteral') return svgNum(at.value.value, dflt);
3188
+ if (at.value.type === 'JSXExpressionContainer') {
3189
+ try {
3190
+ return svgNum(evalStatic(at.value.expression, scope), dflt);
3191
+ } catch {
3192
+ throw new Error(
3193
+ 'AOT: <Svg source> width/height must be a static number',
3194
+ );
3195
+ }
3196
+ }
3197
+ return dflt;
3198
+ };
3199
+ const w = numAttr('width', art.width);
3200
+ const h = numAttr('height', art.height);
3201
+
3202
+ // Raster fallback: the .svg used unsupported features and was rasterized to a PNG at bake time
3203
+ // (bakeSvgArtifacts). Emit it as an Image node sized to the box, and register the PNG so it bakes into
3204
+ // assets.generated.c (the CLI resolves importPath; an absolute temp path passes through resolve() as-is).
3205
+ if (art.kind === 'raster') {
3206
+ if (art.png) out.images.set(art.name, art.png);
3207
+ const vimg = `n${out.n++}`;
3208
+ const {staticAssigns} = collectStyleAssigns(el.openingElement, scope, env);
3209
+ out.build.push(
3210
+ ` ${vimg} = er_node_create(${NODE_TYPES.Image});`,
3211
+ ` er_props_default(&p);`,
3212
+ );
3213
+ if (typeof w === 'number')
3214
+ out.build.push(` p.width = (int16_t)${Math.round(w)};`);
3215
+ if (typeof h === 'number')
3216
+ out.build.push(` p.height = (int16_t)${Math.round(h)};`);
3217
+ for (const a of staticAssigns)
3218
+ out.build.push(` p.${a.field} = ${a.expr};`);
3219
+ out.build.push(
3220
+ ` snprintf(p.image_name, sizeof(p.image_name), "%s", ${cstr(art.name)});`,
3221
+ );
3222
+ out.build.push(` er_node_set_props(${vimg}, &p);`);
3223
+ emitRefBind(vimg, el.openingElement, out, env);
3224
+ return vimg;
3225
+ }
3226
+
3227
+ const scaled = scaleVectorArtifact(art, w, h);
3228
+ const ops = scaled.ops;
3229
+ const paints = scaled.paints;
3230
+ const gradients = scaled.gradients || [];
3231
+
2036
3232
  const v = `n${out.n++}`;
2037
3233
  const id = out.svgN++;
2038
- const nPaints = paints.length / 7;
3234
+ const nPaints = paints.length / PAINT_STRIDE;
3235
+ if (ops.length) {
3236
+ out.vectorData.push(
3237
+ `static const float s_svg${id}_ops[] = {\n ${Array.from(ops, floatLit).join(', ')}\n};`,
3238
+ );
3239
+ out.vectorData.push(
3240
+ `static const ERVectorPaint s_svg${id}_paints[] = {\n${Array.from({length: nPaints}, (_, i) => ' ' + emitVectorPaint(paints.slice(i * PAINT_STRIDE, i * PAINT_STRIDE + PAINT_STRIDE))).join(',\n')}\n};`,
3241
+ );
3242
+ if (gradients.length)
3243
+ out.vectorData.push(
3244
+ `static const ERVectorGradient s_svg${id}_grads[] = {\n${gradients.map(g => ' ' + emitVectorGradient(g)).join(',\n')}\n};`,
3245
+ );
3246
+ }
3247
+ emitSvgBox(v, w, h, el.openingElement, scope, out, env);
2039
3248
  if (ops.length) {
2040
- out.vectorData.push(`static const float s_svg${id}_ops[] = {\n ${Array.from(ops, floatLit).join(', ')}\n};`);
2041
- out.vectorData.push(`static const ERVectorPaint s_svg${id}_paints[] = {\n${Array.from({ length: nPaints }, (_, i) => ' ' + emitVectorPaint(paints.slice(i * 7, i * 7 + 7))).join(',\n')}\n};`);
3249
+ const gradsRef = gradients.length ? `s_svg${id}_grads` : 'NULL';
3250
+ out.build.push(
3251
+ ` er_node_set_vector_ops(${v}, s_svg${id}_ops, ${ops.length}, s_svg${id}_paints, ${nPaints}, ${gradsRef}, ${gradients.length});`,
3252
+ );
2042
3253
  }
2043
- emitSvgBox(v, svgEl.props.width, svgEl.props.height, el.openingElement, scope, out, env);
2044
- if (ops.length) out.build.push(` er_node_set_vector_ops(${v}, s_svg${id}_ops, ${ops.length}, s_svg${id}_paints, ${nPaints});`);
2045
3254
  return v;
2046
3255
  }
2047
3256
 
@@ -2049,14 +3258,20 @@ function emitSvgStatic(el, scope, out, env) {
2049
3258
  * + build_svgN() that recomputes it from state, called at build and re-called on each app_update. */
2050
3259
  function emitSvgDynamic(el, scope, out, env, state) {
2051
3260
  const svgA = svgAttrs(el.openingElement, scope, env);
2052
- if (svgA.viewBox != null) throw new Error('AOT: a viewBox on a state-driven <Svg> is not yet supported — size shapes in the width/height space');
3261
+ if (svgA.viewBox != null)
3262
+ throw new Error(
3263
+ 'AOT: a viewBox on a state-driven <Svg> is not yet supported — size shapes in the width/height space',
3264
+ );
2053
3265
  const entries = [];
2054
3266
  const specs = [];
2055
3267
  for (const c of el.children) {
2056
3268
  if (c.type !== 'JSXElement') continue;
2057
3269
  const type = c.openingElement.name.name;
2058
3270
  const fn = SHAPE_ENTRIES[type];
2059
- if (!fn) throw new Error(`AOT: <${type}> is not a supported shape in a state-driven <Svg> (no <G>/viewBox yet)`);
3271
+ if (!fn)
3272
+ throw new Error(
3273
+ `AOT: <${type}> is not a supported shape in a state-driven <Svg> (no <G>/viewBox yet)`,
3274
+ );
2060
3275
  const a = svgAttrs(c.openingElement, scope, env);
2061
3276
  const shape = fn(a);
2062
3277
  if (!shape.length) continue;
@@ -2067,20 +3282,39 @@ function emitSvgDynamic(el, scope, out, env, state) {
2067
3282
  const id = out.svgN++;
2068
3283
  const len = entries.length;
2069
3284
  const nPaints = specs.length;
2070
- const dynPaint = specs.some((p) => p.anyDynamic);
3285
+ const dynPaint = specs.some(p => p.anyDynamic);
2071
3286
  out.needsMath = true; // build_svg uses cosf/sinf/M_PI for arcs
2072
3287
  out.vectorData.push(`static float s_svg${id}_ops[${len}];`);
2073
3288
  // Dynamic paint → a MUTABLE paint table (re)filled by build_svg from state each update; else a const table.
2074
- if (dynPaint) out.vectorData.push(`static ERVectorPaint s_svg${id}_paints[${nPaints}];`);
2075
- else out.vectorData.push(`static const ERVectorPaint s_svg${id}_paints[] = {\n${specs.map((p) => ' ' + paintInitFromSpec(p)).join(',\n')}\n};`);
2076
- const builderLines = entries.map((e, i) => ` s_svg${id}_ops[${i}] = ${e};`);
2077
- if (dynPaint) specs.forEach((ps, pi) => ps.fields.forEach((f, fi) => builderLines.push(` s_svg${id}_paints[${pi}].${PAINT_FIELDS[fi]} = ${f};`)));
2078
- out.vectorBuilders.push(`static void build_svg${id}(void)\n{\n${builderLines.join('\n')}\n}`);
3289
+ if (dynPaint)
3290
+ out.vectorData.push(`static ERVectorPaint s_svg${id}_paints[${nPaints}];`);
3291
+ else
3292
+ out.vectorData.push(
3293
+ `static const ERVectorPaint s_svg${id}_paints[] = {\n${specs.map(p => ' ' + paintInitFromSpec(p)).join(',\n')}\n};`,
3294
+ );
3295
+ const builderLines = entries.map(
3296
+ (e, i) => ` s_svg${id}_ops[${i}] = ${e};`,
3297
+ );
3298
+ if (dynPaint)
3299
+ specs.forEach((ps, pi) =>
3300
+ ps.fields.forEach((f, fi) =>
3301
+ builderLines.push(
3302
+ ` s_svg${id}_paints[${pi}].${PAINT_FIELDS[fi]} = ${f};`,
3303
+ ),
3304
+ ),
3305
+ );
3306
+ out.vectorBuilders.push(
3307
+ `static void build_svg${id}(void)\n{\n${builderLines.join('\n')}\n}`,
3308
+ );
2079
3309
 
2080
3310
  emitSvgBox(v, svgA.width, svgA.height, el.openingElement, scope, out, env);
2081
- out.build.push(` build_svg${id}();`, ` er_node_set_vector_ops(${v}, s_svg${id}_ops, ${len}, s_svg${id}_paints, ${nPaints});`, ` s_${v} = ${v};`);
3311
+ out.build.push(
3312
+ ` build_svg${id}();`,
3313
+ ` er_node_set_vector_ops(${v}, s_svg${id}_ops, ${len}, s_svg${id}_paints, ${nPaints}, NULL, 0);`,
3314
+ ` s_${v} = ${v};`,
3315
+ );
2082
3316
  out.handles.push(v);
2083
- out.svgUpdates.push({ id, len, nPaints, nodeVar: `s_${v}` });
3317
+ out.svgUpdates.push({id, len, nPaints, nodeVar: `s_${v}`});
2084
3318
  return v;
2085
3319
  }
2086
3320
 
@@ -2095,21 +3329,39 @@ function emitSvgDynamic(el, scope, out, env, state) {
2095
3329
  function emitRefBind(v, openingElement, out, env) {
2096
3330
  for (const attr of openingElement.attributes) {
2097
3331
  if (attr.type !== 'JSXAttribute' || attr.name.name !== 'ref') continue;
2098
- const e = attr.value?.type === 'JSXExpressionContainer' ? attr.value.expression : null;
2099
- if (e?.type === 'Identifier' && env.refs?.get(e.name)?.kind === 'node') out.build.push(` ${env.refs.get(e.name).cVar} = ${v};`);
2100
- else throw new Error('AOT: ref={…} must reference a node ref declared with useRef()');
3332
+ const e =
3333
+ attr.value?.type === 'JSXExpressionContainer'
3334
+ ? attr.value.expression
3335
+ : null;
3336
+ if (e?.type === 'Identifier' && env.refs?.get(e.name)?.kind === 'node')
3337
+ out.build.push(` ${env.refs.get(e.name).cVar} = ${v};`);
3338
+ else
3339
+ throw new Error(
3340
+ 'AOT: ref={…} must reference a node ref declared with useRef()',
3341
+ );
2101
3342
  }
2102
3343
  }
2103
3344
 
2104
3345
  /** Compiles a value-callback (e.g. Switch onValueChange) — binds its first param to `valueCode`, not an event. */
2105
- function compileValueHandler(fnNode, valueCode, env, state, out, cType = 'int') {
2106
- const param = fnNode.params[0]?.type === 'Identifier' ? fnNode.params[0].name : null;
3346
+ function compileValueHandler(
3347
+ fnNode,
3348
+ valueCode,
3349
+ env,
3350
+ state,
3351
+ out,
3352
+ cType = 'int',
3353
+ ) {
3354
+ const param =
3355
+ fnNode.params[0]?.type === 'Identifier' ? fnNode.params[0].name : null;
2107
3356
  const locals = new Map(env.locals);
2108
- if (param) locals.set(param, { code: valueCode, cType });
2109
- const ctx = { stateChanged: false, animIdx: 0, out };
3357
+ if (param) locals.set(param, {code: valueCode, cType});
3358
+ const ctx = {stateChanged: false, animIdx: 0, out};
2110
3359
  const body = fnNode.body;
2111
- const list = body.type === 'BlockStatement' ? body.body : [{ type: 'ExpressionStatement', expression: body }];
2112
- const stmts = compileStmts(list, { ...env, locals }, state, ctx, ' ');
3360
+ const list =
3361
+ body.type === 'BlockStatement'
3362
+ ? body.body
3363
+ : [{type: 'ExpressionStatement', expression: body}];
3364
+ const stmts = compileStmts(list, {...env, locals}, state, ctx, ' ');
2113
3365
  if (ctx.stateChanged) stmts.push(' app_update();');
2114
3366
  return stmts;
2115
3367
  }
@@ -2122,36 +3374,65 @@ function compileValueHandler(fnNode, valueCode, env, state, out, cType = 'int')
2122
3374
  */
2123
3375
  function emitSwitch(el, scope, out, env, state) {
2124
3376
  const v = `n${out.n++}`;
2125
- const { staticAssigns, dynAssigns } = collectStyleAssigns(el.openingElement, scope, env);
2126
- const hasField = (f) => staticAssigns.some((a) => a.field === f) || dynAssigns.some((a) => a.field === f);
2127
- if (!hasField('width')) staticAssigns.push({ field: 'width', expr: '51' });
2128
- if (!hasField('height')) staticAssigns.push({ field: 'height', expr: '31' });
3377
+ const {staticAssigns, dynAssigns} = collectStyleAssigns(
3378
+ el.openingElement,
3379
+ scope,
3380
+ env,
3381
+ );
3382
+ const hasField = f =>
3383
+ staticAssigns.some(a => a.field === f) ||
3384
+ dynAssigns.some(a => a.field === f);
3385
+ if (!hasField('width')) staticAssigns.push({field: 'width', expr: '51'});
3386
+ if (!hasField('height')) staticAssigns.push({field: 'height', expr: '31'});
2129
3387
 
2130
3388
  let valueNode = null;
2131
3389
  let onChangeFn = null;
2132
3390
  for (const attr of el.openingElement.attributes) {
2133
- if (attr.type !== 'JSXAttribute') throw aotError('AOT: spread props on <Switch> are not supported');
3391
+ if (attr.type !== 'JSXAttribute')
3392
+ throw aotError('AOT: spread props on <Switch> are not supported');
2134
3393
  const name = attr.name.name;
2135
3394
  if (name === 'style' || name === 'ref' || name === 'key') continue;
2136
3395
  const node = attrExpr(attr);
2137
3396
  if (name === 'value') valueNode = node;
2138
3397
  else if (name === 'onValueChange') onChangeFn = node;
2139
- else if (name === 'thumbColor') staticAssigns.push({ field: 'thumb_color', expr: colorLiteral(String(evalStatic(node, scope))) });
3398
+ else if (name === 'thumbColor')
3399
+ staticAssigns.push({
3400
+ field: 'thumb_color',
3401
+ expr: colorLiteral(String(evalStatic(node, scope))),
3402
+ });
2140
3403
  else if (name === 'trackColor') {
2141
3404
  const tc = evalStatic(node, scope);
2142
- if (tc?.false != null) staticAssigns.push({ field: 'track_color_false', expr: colorLiteral(String(tc.false)) });
2143
- if (tc?.true != null) staticAssigns.push({ field: 'track_color_true', expr: colorLiteral(String(tc.true)) });
3405
+ if (tc?.false != null)
3406
+ staticAssigns.push({
3407
+ field: 'track_color_false',
3408
+ expr: colorLiteral(String(tc.false)),
3409
+ });
3410
+ if (tc?.true != null)
3411
+ staticAssigns.push({
3412
+ field: 'track_color_true',
3413
+ expr: colorLiteral(String(tc.true)),
3414
+ });
2144
3415
  } else if (name === 'disabled') {
2145
3416
  /* accepted; the AOT has no disabled-visual yet, so it is a no-op */
2146
- } else throw aotError(`AOT: <Switch> prop "${name}" is not supported`, 'supported props: value, onValueChange, trackColor, thumbColor, style.');
3417
+ } else
3418
+ throw aotError(
3419
+ `AOT: <Switch> prop "${name}" is not supported`,
3420
+ 'supported props: value, onValueChange, trackColor, thumbColor, style.',
3421
+ );
2147
3422
  }
2148
3423
 
2149
3424
  // value → switch_value (static or, when state-driven, recomputed in app_update).
2150
3425
  if (valueNode) {
2151
3426
  try {
2152
- staticAssigns.push({ field: 'switch_value', expr: evalStatic(valueNode, scope) ? '1' : '0' });
3427
+ staticAssigns.push({
3428
+ field: 'switch_value',
3429
+ expr: evalStatic(valueNode, scope) ? '1' : '0',
3430
+ });
2153
3431
  } catch {
2154
- dynAssigns.push({ field: 'switch_value', code: `(uint8_t)((${emitExpr(valueNode, env).code}) ? 1 : 0)` });
3432
+ dynAssigns.push({
3433
+ field: 'switch_value',
3434
+ code: `(uint8_t)((${emitExpr(valueNode, env).code}) ? 1 : 0)`,
3435
+ });
2155
3436
  }
2156
3437
  }
2157
3438
 
@@ -2160,20 +3441,34 @@ function emitSwitch(el, scope, out, env, state) {
2160
3441
  if (isDynamic) {
2161
3442
  out.build.push(` s_${v} = ${v};`);
2162
3443
  out.handles.push(v);
2163
- out.updates.push({ v, styleAssigns: staticAssigns, text: null, dynAssigns });
3444
+ out.updates.push({v, styleAssigns: staticAssigns, text: null, dynAssigns});
2164
3445
  } else {
2165
3446
  out.build.push(` er_props_default(&p);`);
2166
- for (const a of staticAssigns) out.build.push(` p.${a.field} = ${a.expr};`);
3447
+ for (const a of staticAssigns)
3448
+ out.build.push(` p.${a.field} = ${a.expr};`);
2167
3449
  out.build.push(` er_node_set_props(${v}, &p);`);
2168
3450
  }
2169
3451
 
2170
3452
  if (onChangeFn) {
2171
- if (!isFn(onChangeFn)) throw aotError('AOT: onValueChange must be an inline function', 'onValueChange={(v) => setX(v)}');
2172
- if (!valueNode) throw aotError('AOT: a <Switch> with onValueChange needs a value prop', 'controlled switch: <Switch value={on} onValueChange={(v) => setOn(v)} />');
3453
+ if (!isFn(onChangeFn))
3454
+ throw aotError(
3455
+ 'AOT: onValueChange must be an inline function',
3456
+ 'onValueChange={(v) => setX(v)}',
3457
+ );
3458
+ if (!valueNode)
3459
+ throw aotError(
3460
+ 'AOT: a <Switch> with onValueChange needs a value prop',
3461
+ 'controlled switch: <Switch value={on} onValueChange={(v) => setOn(v)} />',
3462
+ );
2173
3463
  const handlerName = `er_handler_${out.handlers.length}`;
2174
3464
  const toggled = `(!(${emitExpr(valueNode, env).code}))`; // the engine toggles on press → param is !value
2175
- out.handlers.push({ name: handlerName, body: compileValueHandler(onChangeFn, toggled, env, state, out) });
2176
- out.build.push(` er_event_set(${v}, ER_EVENT_PRESS, ${handlerName}, NULL);`);
3465
+ out.handlers.push({
3466
+ name: handlerName,
3467
+ body: compileValueHandler(onChangeFn, toggled, env, state, out),
3468
+ });
3469
+ out.build.push(
3470
+ ` er_event_set(${v}, ER_EVENT_PRESS, ${handlerName}, NULL);`,
3471
+ );
2177
3472
  }
2178
3473
  emitRefBind(v, el.openingElement, out, env);
2179
3474
  return v;
@@ -2189,29 +3484,67 @@ function emitSwitch(el, scope, out, env, state) {
2189
3484
  */
2190
3485
  function emitTextInput(el, scope, out, env, state) {
2191
3486
  const v = `n${out.n++}`;
2192
- const { staticAssigns, dynAssigns } = collectStyleAssigns(el.openingElement, scope, env);
3487
+ const {staticAssigns, dynAssigns} = collectStyleAssigns(
3488
+ el.openingElement,
3489
+ scope,
3490
+ env,
3491
+ );
2193
3492
  let valueNode = null;
2194
3493
  let onChangeFn = null;
2195
3494
  let placeholder = null;
2196
3495
  for (const attr of el.openingElement.attributes) {
2197
- if (attr.type !== 'JSXAttribute') throw aotError('AOT: spread props on <TextInput> are not supported');
3496
+ if (attr.type !== 'JSXAttribute')
3497
+ throw aotError('AOT: spread props on <TextInput> are not supported');
2198
3498
  const name = attr.name.name;
2199
3499
  if (name === 'style' || name === 'ref' || name === 'key') continue;
2200
3500
  const node = attrExpr(attr);
2201
3501
  if (name === 'value' || name === 'defaultValue') valueNode = node;
2202
3502
  else if (name === 'onChangeText') onChangeFn = node;
2203
- else if (name === 'placeholder') placeholder = String(evalStatic(node, scope));
2204
- else if (name === 'placeholderTextColor') staticAssigns.push({ field: 'placeholder_color', expr: colorLiteral(String(evalStatic(node, scope))) });
2205
- else if (name === 'cursorColor') staticAssigns.push({ field: 'cursor_color', expr: colorLiteral(String(evalStatic(node, scope))) });
3503
+ else if (name === 'placeholder')
3504
+ placeholder = String(evalStatic(node, scope));
3505
+ else if (name === 'placeholderTextColor')
3506
+ staticAssigns.push({
3507
+ field: 'placeholder_color',
3508
+ expr: colorLiteral(String(evalStatic(node, scope))),
3509
+ });
3510
+ else if (name === 'cursorColor')
3511
+ staticAssigns.push({
3512
+ field: 'cursor_color',
3513
+ expr: colorLiteral(String(evalStatic(node, scope))),
3514
+ });
2206
3515
  else if (name === 'editable') {
2207
3516
  try {
2208
- staticAssigns.push({ field: 'editable', expr: evalStatic(node, scope) ? '1' : '0' });
3517
+ staticAssigns.push({
3518
+ field: 'editable',
3519
+ expr: evalStatic(node, scope) ? '1' : '0',
3520
+ });
2209
3521
  } catch {
2210
- dynAssigns.push({ field: 'editable', code: `(uint8_t)((${emitExpr(node, env).code}) ? 1 : 0)` });
3522
+ dynAssigns.push({
3523
+ field: 'editable',
3524
+ code: `(uint8_t)((${emitExpr(node, env).code}) ? 1 : 0)`,
3525
+ });
2211
3526
  }
2212
- } else if (['autoFocus', 'keyboardType', 'secureTextEntry', 'maxLength', 'multiline', 'autoCapitalize', 'autoCorrect', 'returnKeyType', 'onSubmitEditing', 'onFocus', 'onBlur'].includes(name)) {
3527
+ } else if (
3528
+ [
3529
+ 'autoFocus',
3530
+ 'keyboardType',
3531
+ 'secureTextEntry',
3532
+ 'maxLength',
3533
+ 'multiline',
3534
+ 'autoCapitalize',
3535
+ 'autoCorrect',
3536
+ 'returnKeyType',
3537
+ 'onSubmitEditing',
3538
+ 'onFocus',
3539
+ 'onBlur',
3540
+ ].includes(name)
3541
+ ) {
2213
3542
  /* accepted but not yet lowered (no on-screen keyboard / submit wiring in the AOT path) */
2214
- } else throw aotError(`AOT: <TextInput> prop "${name}" is not supported`, 'supported props: value, onChangeText, placeholder, placeholderTextColor, cursorColor, editable, style.');
3543
+ } else
3544
+ throw aotError(
3545
+ `AOT: <TextInput> prop "${name}" is not supported`,
3546
+ 'supported props: value, onChangeText, placeholder, placeholderTextColor, cursorColor, editable, style.',
3547
+ );
2215
3548
  }
2216
3549
 
2217
3550
  // value → the input's text buffer (er_node_set_props → er_text_input_set_text): static literal, or a
@@ -2220,10 +3553,14 @@ function emitTextInput(el, scope, out, env, state) {
2220
3553
  if (valueNode) {
2221
3554
  try {
2222
3555
  const cv = evalStatic(valueNode, scope);
2223
- text = { dynamic: false, format: (cv == null ? '' : String(cv)).replace(/%/g, '%%'), args: [] };
3556
+ text = {
3557
+ dynamic: false,
3558
+ format: (cv == null ? '' : String(cv)).replace(/%/g, '%%'),
3559
+ args: [],
3560
+ };
2224
3561
  } catch {
2225
3562
  const e = emitExpr(valueNode, env);
2226
- text = { dynamic: true, format: printfSpec(e.cType), args: [e.code] };
3563
+ text = {dynamic: true, format: printfSpec(e.cType), args: [e.code]};
2227
3564
  }
2228
3565
  }
2229
3566
 
@@ -2232,20 +3569,49 @@ function emitTextInput(el, scope, out, env, state) {
2232
3569
  if (isDynamic) {
2233
3570
  out.build.push(` s_${v} = ${v};`);
2234
3571
  out.handles.push(v);
2235
- out.updates.push({ v, styleAssigns: staticAssigns, text, dynAssigns, placeholder });
3572
+ out.updates.push({
3573
+ v,
3574
+ styleAssigns: staticAssigns,
3575
+ text,
3576
+ dynAssigns,
3577
+ placeholder,
3578
+ });
2236
3579
  } else {
2237
3580
  out.build.push(` er_props_default(&p);`);
2238
- for (const a of staticAssigns) out.build.push(` p.${a.field} = ${a.expr};`);
2239
- if (placeholder != null) out.build.push(` snprintf(p.placeholder, sizeof(p.placeholder), "%s", ${cstr(placeholder)});`);
2240
- if (text) out.build.push(` snprintf(p.text, sizeof(p.text), "%s", ${cstr(text.format.replace(/%%/g, '%'))});`);
3581
+ for (const a of staticAssigns)
3582
+ out.build.push(` p.${a.field} = ${a.expr};`);
3583
+ if (placeholder != null)
3584
+ out.build.push(
3585
+ ` snprintf(p.placeholder, sizeof(p.placeholder), "%s", ${cstr(placeholder)});`,
3586
+ );
3587
+ if (text)
3588
+ out.build.push(
3589
+ ` snprintf(p.text, sizeof(p.text), "%s", ${cstr(text.format.replace(/%%/g, '%'))});`,
3590
+ );
2241
3591
  out.build.push(` er_node_set_props(${v}, &p);`);
2242
3592
  }
2243
3593
 
2244
3594
  if (onChangeFn) {
2245
- if (!isFn(onChangeFn)) throw aotError('AOT: onChangeText must be an inline function', 'onChangeText={(t) => setText(t)}');
3595
+ if (!isFn(onChangeFn))
3596
+ throw aotError(
3597
+ 'AOT: onChangeText must be an inline function',
3598
+ 'onChangeText={(t) => setText(t)}',
3599
+ );
2246
3600
  const handlerName = `er_handler_${out.handlers.length}`;
2247
- out.handlers.push({ name: handlerName, body: compileValueHandler(onChangeFn, 'data->changed_text', env, state, out, 'string') });
2248
- out.build.push(` er_event_set(${v}, ER_EVENT_CHANGE_TEXT, ${handlerName}, NULL);`);
3601
+ out.handlers.push({
3602
+ name: handlerName,
3603
+ body: compileValueHandler(
3604
+ onChangeFn,
3605
+ 'data->changed_text',
3606
+ env,
3607
+ state,
3608
+ out,
3609
+ 'string',
3610
+ ),
3611
+ });
3612
+ out.build.push(
3613
+ ` er_event_set(${v}, ER_EVENT_CHANGE_TEXT, ${handlerName}, NULL);`,
3614
+ );
2249
3615
  }
2250
3616
  emitRefBind(v, el.openingElement, out, env);
2251
3617
  return v;
@@ -2258,43 +3624,71 @@ function emitTextInput(el, scope, out, env, state) {
2258
3624
  */
2259
3625
  function emitActivityIndicator(el, scope, out, env) {
2260
3626
  const v = `n${out.n++}`;
2261
- const { staticAssigns, dynAssigns } = collectStyleAssigns(el.openingElement, scope, env);
2262
- const hasField = (f) => staticAssigns.some((a) => a.field === f) || dynAssigns.some((a) => a.field === f);
3627
+ const {staticAssigns, dynAssigns} = collectStyleAssigns(
3628
+ el.openingElement,
3629
+ scope,
3630
+ env,
3631
+ );
3632
+ const hasField = f =>
3633
+ staticAssigns.some(a => a.field === f) ||
3634
+ dynAssigns.some(a => a.field === f);
2263
3635
  let size = 36;
2264
3636
  for (const attr of el.openingElement.attributes) {
2265
- if (attr.type !== 'JSXAttribute') throw aotError('AOT: spread props on <ActivityIndicator> are not supported');
3637
+ if (attr.type !== 'JSXAttribute')
3638
+ throw aotError(
3639
+ 'AOT: spread props on <ActivityIndicator> are not supported',
3640
+ );
2266
3641
  const name = attr.name.name;
2267
3642
  if (name === 'style' || name === 'ref' || name === 'key') continue;
2268
3643
  const node = attrExpr(attr);
2269
3644
  if (name === 'color') {
2270
3645
  try {
2271
- staticAssigns.push({ field: 'indicator_color', expr: colorLiteral(String(evalStatic(node, scope))) });
3646
+ staticAssigns.push({
3647
+ field: 'indicator_color',
3648
+ expr: colorLiteral(String(evalStatic(node, scope))),
3649
+ });
2272
3650
  } catch {
2273
- dynAssigns.push({ field: 'indicator_color', code: emitColorExpr(node, env) });
3651
+ dynAssigns.push({
3652
+ field: 'indicator_color',
3653
+ code: emitColorExpr(node, env),
3654
+ });
2274
3655
  }
2275
3656
  } else if (name === 'size') {
2276
3657
  const sv = evalStatic(node, scope);
2277
3658
  size = sv === 'small' ? 20 : sv === 'large' ? 36 : Number(sv) || 36;
2278
3659
  } else if (name === 'animating') {
2279
3660
  try {
2280
- staticAssigns.push({ field: 'animating', expr: evalStatic(node, scope) ? '1' : '0' });
3661
+ staticAssigns.push({
3662
+ field: 'animating',
3663
+ expr: evalStatic(node, scope) ? '1' : '0',
3664
+ });
2281
3665
  } catch {
2282
- dynAssigns.push({ field: 'animating', code: `(uint8_t)((${emitExpr(node, env).code}) ? 1 : 0)` });
3666
+ dynAssigns.push({
3667
+ field: 'animating',
3668
+ code: `(uint8_t)((${emitExpr(node, env).code}) ? 1 : 0)`,
3669
+ });
2283
3670
  }
2284
- } else throw aotError(`AOT: <ActivityIndicator> prop "${name}" is not supported`, 'supported props: color, size, animating, style.');
3671
+ } else
3672
+ throw aotError(
3673
+ `AOT: <ActivityIndicator> prop "${name}" is not supported`,
3674
+ 'supported props: color, size, animating, style.',
3675
+ );
2285
3676
  }
2286
- if (!hasField('width')) staticAssigns.push({ field: 'width', expr: String(size) });
2287
- if (!hasField('height')) staticAssigns.push({ field: 'height', expr: String(size) });
3677
+ if (!hasField('width'))
3678
+ staticAssigns.push({field: 'width', expr: String(size)});
3679
+ if (!hasField('height'))
3680
+ staticAssigns.push({field: 'height', expr: String(size)});
2288
3681
 
2289
3682
  const isDynamic = dynAssigns.length > 0;
2290
3683
  out.build.push(` ${v} = er_node_create(ER_NODE_ACTIVITY_INDICATOR);`);
2291
3684
  if (isDynamic) {
2292
3685
  out.build.push(` s_${v} = ${v};`);
2293
3686
  out.handles.push(v);
2294
- out.updates.push({ v, styleAssigns: staticAssigns, text: null, dynAssigns });
3687
+ out.updates.push({v, styleAssigns: staticAssigns, text: null, dynAssigns});
2295
3688
  } else {
2296
3689
  out.build.push(` er_props_default(&p);`);
2297
- for (const a of staticAssigns) out.build.push(` p.${a.field} = ${a.expr};`);
3690
+ for (const a of staticAssigns)
3691
+ out.build.push(` p.${a.field} = ${a.expr};`);
2298
3692
  out.build.push(` er_node_set_props(${v}, &p);`);
2299
3693
  }
2300
3694
  emitRefBind(v, el.openingElement, out, env);
@@ -2309,8 +3703,14 @@ function emitActivityIndicator(el, scope, out, env) {
2309
3703
  */
2310
3704
  function emitModal(el, scope, out, env, state) {
2311
3705
  const v = `n${out.n++}`;
2312
- const { staticAssigns, dynAssigns } = collectStyleAssigns(el.openingElement, scope, env);
2313
- const hasField = (f) => staticAssigns.some((a) => a.field === f) || dynAssigns.some((a) => a.field === f);
3706
+ const {staticAssigns, dynAssigns} = collectStyleAssigns(
3707
+ el.openingElement,
3708
+ scope,
3709
+ env,
3710
+ );
3711
+ const hasField = f =>
3712
+ staticAssigns.some(a => a.field === f) ||
3713
+ dynAssigns.some(a => a.field === f);
2314
3714
  // Overlay defaults: absolute, fill the parent via four 0 insets (the robust "stretch" for an absolute
2315
3715
  // node), centre the content. The user's style overrides any of these.
2316
3716
  const DEFAULTS = [
@@ -2322,25 +3722,50 @@ function emitModal(el, scope, out, env, state) {
2322
3722
  ['align_items', 'ER_ALIGN_CENTER'],
2323
3723
  ['justify_content', 'ER_JUSTIFY_CENTER'],
2324
3724
  ];
2325
- for (const [f, expr] of DEFAULTS) if (!hasField(f)) staticAssigns.push({ field: f, expr });
3725
+ for (const [f, expr] of DEFAULTS)
3726
+ if (!hasField(f)) staticAssigns.push({field: f, expr});
2326
3727
 
2327
3728
  let visibleNode = null;
2328
3729
  for (const attr of el.openingElement.attributes) {
2329
- if (attr.type !== 'JSXAttribute') throw aotError('AOT: spread props on <Modal> are not supported');
3730
+ if (attr.type !== 'JSXAttribute')
3731
+ throw aotError('AOT: spread props on <Modal> are not supported');
2330
3732
  const name = attr.name.name;
2331
3733
  if (name === 'style' || name === 'ref' || name === 'key') continue;
2332
3734
  const node = attrExpr(attr);
2333
3735
  if (name === 'visible') visibleNode = node;
2334
- else if (name === 'backdropColor') staticAssigns.push({ field: 'backdrop_color', expr: colorLiteral(String(evalStatic(node, scope))) });
2335
- else if (name === 'transparent' || name === 'animationType' || name === 'onRequestClose' || name === 'statusBarTranslucent') {
3736
+ else if (name === 'backdropColor')
3737
+ staticAssigns.push({
3738
+ field: 'backdrop_color',
3739
+ expr: colorLiteral(String(evalStatic(node, scope))),
3740
+ });
3741
+ else if (
3742
+ name === 'transparent' ||
3743
+ name === 'animationType' ||
3744
+ name === 'onRequestClose' ||
3745
+ name === 'statusBarTranslucent'
3746
+ ) {
2336
3747
  /* accepted for RN compatibility; no-op in the AOT today */
2337
- } else throw aotError(`AOT: <Modal> prop "${name}" is not supported`, 'supported: visible, backdropColor, style, children (transparent / animationType / onRequestClose are accepted but no-ops).');
3748
+ } else
3749
+ throw aotError(
3750
+ `AOT: <Modal> prop "${name}" is not supported`,
3751
+ 'supported: visible, backdropColor, style, children (transparent / animationType / onRequestClose are accepted but no-ops).',
3752
+ );
2338
3753
  }
2339
- if (!visibleNode) throw aotError('AOT: a <Modal> needs a visible prop', '<Modal visible={show}>…</Modal>');
3754
+ if (!visibleNode)
3755
+ throw aotError(
3756
+ 'AOT: a <Modal> needs a visible prop',
3757
+ '<Modal visible={show}>…</Modal>',
3758
+ );
2340
3759
  try {
2341
- staticAssigns.push({ field: 'modal_visible', expr: evalStatic(visibleNode, scope) ? '1' : '0' });
3760
+ staticAssigns.push({
3761
+ field: 'modal_visible',
3762
+ expr: evalStatic(visibleNode, scope) ? '1' : '0',
3763
+ });
2342
3764
  } catch {
2343
- dynAssigns.push({ field: 'modal_visible', code: `(uint8_t)((${emitExpr(visibleNode, env).code}) ? 1 : 0)` });
3765
+ dynAssigns.push({
3766
+ field: 'modal_visible',
3767
+ code: `(uint8_t)((${emitExpr(visibleNode, env).code}) ? 1 : 0)`,
3768
+ });
2344
3769
  }
2345
3770
 
2346
3771
  const isDynamic = dynAssigns.length > 0;
@@ -2348,10 +3773,11 @@ function emitModal(el, scope, out, env, state) {
2348
3773
  if (isDynamic) {
2349
3774
  out.build.push(` s_${v} = ${v};`);
2350
3775
  out.handles.push(v);
2351
- out.updates.push({ v, styleAssigns: staticAssigns, text: null, dynAssigns });
3776
+ out.updates.push({v, styleAssigns: staticAssigns, text: null, dynAssigns});
2352
3777
  } else {
2353
3778
  out.build.push(` er_props_default(&p);`);
2354
- for (const a of staticAssigns) out.build.push(` p.${a.field} = ${a.expr};`);
3779
+ for (const a of staticAssigns)
3780
+ out.build.push(` p.${a.field} = ${a.expr};`);
2355
3781
  out.build.push(` er_node_set_props(${v}, &p);`);
2356
3782
  }
2357
3783
  emitRefBind(v, el.openingElement, out, env);
@@ -2370,39 +3796,89 @@ function emitFlatList(el, scope, out, env, state, opts) {
2370
3796
  let renderItem = null;
2371
3797
  let styleAttr = null;
2372
3798
  for (const attr of el.openingElement.attributes) {
2373
- if (attr.type !== 'JSXAttribute') throw aotError('AOT: spread props on <FlatList> are not supported');
3799
+ if (attr.type !== 'JSXAttribute')
3800
+ throw aotError('AOT: spread props on <FlatList> are not supported');
2374
3801
  const name = attr.name.name;
2375
3802
  if (name === 'data') dataNode = attrExpr(attr);
2376
3803
  else if (name === 'renderItem') renderItem = attrExpr(attr);
2377
3804
  else if (name === 'style') styleAttr = attr;
2378
3805
  else if (name === 'keyExtractor' || name === 'ref' || name === 'key') {
2379
3806
  /* ignored — the AOT unrolls at compile time, so React keys are irrelevant */
2380
- } else throw aotError(`AOT: <FlatList> prop "${name}" is not supported`, 'supported: data, renderItem, keyExtractor, style. For headers/footers/horizontal/onEndReached etc., use <ScrollView> + .map directly.');
3807
+ } else
3808
+ throw aotError(
3809
+ `AOT: <FlatList> prop "${name}" is not supported`,
3810
+ 'supported: data, renderItem, keyExtractor, style. For headers/footers/horizontal/onEndReached etc., use <ScrollView> + .map directly.',
3811
+ );
2381
3812
  }
2382
- if (!dataNode) throw aotError('AOT: <FlatList> needs a data prop', '<FlatList data={items} renderItem={({ item }) => <Row item={item} />} />');
2383
- if (!renderItem || !isFn(renderItem)) throw aotError('AOT: <FlatList> needs a renderItem function', 'renderItem={({ item, index }) => <Row item={item} />}');
3813
+ if (!dataNode)
3814
+ throw aotError(
3815
+ 'AOT: <FlatList> needs a data prop',
3816
+ '<FlatList data={items} renderItem={({ item }) => <Row item={item} />} />',
3817
+ );
3818
+ if (!renderItem || !isFn(renderItem))
3819
+ throw aotError(
3820
+ 'AOT: <FlatList> needs a renderItem function',
3821
+ 'renderItem={({ item, index }) => <Row item={item} />}',
3822
+ );
2384
3823
  const param = renderItem.params[0];
2385
- if (!param || param.type !== 'ObjectPattern') throw aotError('AOT: FlatList renderItem must destructure ({ item, index })', 'renderItem={({ item }) => <Row item={item} />}');
3824
+ if (!param || param.type !== 'ObjectPattern')
3825
+ throw aotError(
3826
+ 'AOT: FlatList renderItem must destructure ({ item, index })',
3827
+ 'renderItem={({ item }) => <Row item={item} />}',
3828
+ );
2386
3829
  let itemName = null;
2387
3830
  let indexName = null;
2388
3831
  for (const prop of param.properties) {
2389
- if (prop.type !== 'ObjectProperty' || prop.value.type !== 'Identifier') throw aotError('AOT: FlatList renderItem may destructure only item / index (to plain names)');
3832
+ if (prop.type !== 'ObjectProperty' || prop.value.type !== 'Identifier')
3833
+ throw aotError(
3834
+ 'AOT: FlatList renderItem may destructure only item / index (to plain names)',
3835
+ );
2390
3836
  if (prop.key.name === 'item') itemName = prop.value.name;
2391
3837
  else if (prop.key.name === 'index') indexName = prop.value.name;
2392
- else throw aotError(`AOT: FlatList renderItem cannot destructure "${prop.key.name}" (only item / index)`);
3838
+ else
3839
+ throw aotError(
3840
+ `AOT: FlatList renderItem cannot destructure "${prop.key.name}" (only item / index)`,
3841
+ );
2393
3842
  }
2394
- if (!itemName) throw aotError('AOT: FlatList renderItem must destructure item', 'renderItem={({ item }) => …}');
3843
+ if (!itemName)
3844
+ throw aotError(
3845
+ 'AOT: FlatList renderItem must destructure item',
3846
+ 'renderItem={({ item }) => …}',
3847
+ );
2395
3848
 
2396
3849
  // Rewrite renderItem `({ item, index }) => BODY` → a positional `.map` callback `(item, index) => BODY`.
2397
- const cbParams = [{ type: 'Identifier', name: itemName }];
2398
- if (indexName) cbParams.push({ type: 'Identifier', name: indexName });
2399
- const cb = { type: 'ArrowFunctionExpression', params: cbParams, body: renderItem.body, async: false, expression: renderItem.body.type !== 'BlockStatement' };
2400
- const mapCall = { type: 'CallExpression', callee: { type: 'MemberExpression', object: dataNode, property: { type: 'Identifier', name: 'map' }, computed: false }, arguments: [cb] };
3850
+ const cbParams = [{type: 'Identifier', name: itemName}];
3851
+ if (indexName) cbParams.push({type: 'Identifier', name: indexName});
3852
+ const cb = {
3853
+ type: 'ArrowFunctionExpression',
3854
+ params: cbParams,
3855
+ body: renderItem.body,
3856
+ async: false,
3857
+ expression: renderItem.body.type !== 'BlockStatement',
3858
+ };
3859
+ const mapCall = {
3860
+ type: 'CallExpression',
3861
+ callee: {
3862
+ type: 'MemberExpression',
3863
+ object: dataNode,
3864
+ property: {type: 'Identifier', name: 'map'},
3865
+ computed: false,
3866
+ },
3867
+ arguments: [cb],
3868
+ };
2401
3869
  const scrollView = {
2402
3870
  type: 'JSXElement',
2403
- openingElement: { type: 'JSXOpeningElement', name: { type: 'JSXIdentifier', name: 'ScrollView' }, attributes: styleAttr ? [styleAttr] : [], selfClosing: false },
2404
- closingElement: { type: 'JSXClosingElement', name: { type: 'JSXIdentifier', name: 'ScrollView' } },
2405
- children: [{ type: 'JSXExpressionContainer', expression: mapCall }],
3871
+ openingElement: {
3872
+ type: 'JSXOpeningElement',
3873
+ name: {type: 'JSXIdentifier', name: 'ScrollView'},
3874
+ attributes: styleAttr ? [styleAttr] : [],
3875
+ selfClosing: false,
3876
+ },
3877
+ closingElement: {
3878
+ type: 'JSXClosingElement',
3879
+ name: {type: 'JSXIdentifier', name: 'ScrollView'},
3880
+ },
3881
+ children: [{type: 'JSXExpressionContainer', expression: mapCall}],
2406
3882
  };
2407
3883
  return emitNode(scrollView, scope, out, env, state, opts);
2408
3884
  }
@@ -2429,7 +3905,11 @@ function imageNameFromSource(expr, env) {
2429
3905
  }
2430
3906
  if (expr.type === 'ObjectExpression') {
2431
3907
  // source={{ uri: 'wx_sun' }} — RN's remote-image shape; here the uri IS the baked asset name.
2432
- const uri = expr.properties.find((p) => p.type === 'ObjectProperty' && (p.key.name === 'uri' || p.key.value === 'uri'));
3908
+ const uri = expr.properties.find(
3909
+ p =>
3910
+ p.type === 'ObjectProperty' &&
3911
+ (p.key.name === 'uri' || p.key.value === 'uri'),
3912
+ );
2433
3913
  if (uri?.value?.type === 'StringLiteral') return uri.value.value;
2434
3914
  }
2435
3915
  return null;
@@ -2441,10 +3921,15 @@ function imageNameFromSource(expr, env) {
2441
3921
  * only REACHED images are baked — an import used solely in a folded-away branch costs no flash. */
2442
3922
  function resolveImageAttrs(el, env, out) {
2443
3923
  const attrs = el.openingElement.attributes;
2444
- const find = (n) => attrs.find((a) => a.type === 'JSXAttribute' && a.name.name === n);
3924
+ const find = n =>
3925
+ attrs.find(a => a.type === 'JSXAttribute' && a.name.name === n);
2445
3926
  const imAttr = find('imageName');
2446
3927
  const srcAttr = find('source');
2447
- const srcExpr = imAttr ? attrExpr(imAttr) : srcAttr ? attrExpr(srcAttr) : null;
3928
+ const srcExpr = imAttr
3929
+ ? attrExpr(imAttr)
3930
+ : srcAttr
3931
+ ? attrExpr(srcAttr)
3932
+ : null;
2448
3933
  let imageName = null; // static asset name (a literal), OR …
2449
3934
  let imageNameDyn = null; // … a runtime C string expr (a list-item field / state) set in app_update.
2450
3935
  if (srcExpr) {
@@ -2475,7 +3960,10 @@ function resolveImageAttrs(el, env, out) {
2475
3960
  const rm = evalStaticOr(attrExpr(rmAttr), env, null);
2476
3961
  resizeMode = RESIZE_MODES[rm];
2477
3962
  if (!resizeMode) {
2478
- const e = aotError(`AOT: unsupported <Image resizeMode> "${rm}"`, `resizeMode must be one of: ${Object.keys(RESIZE_MODES).join(' / ')}.`);
3963
+ const e = aotError(
3964
+ `AOT: unsupported <Image resizeMode> "${rm}"`,
3965
+ `resizeMode must be one of: ${Object.keys(RESIZE_MODES).join(' / ')}.`,
3966
+ );
2479
3967
  if (rmAttr.loc) e.aotLoc = rmAttr.loc.start;
2480
3968
  throw e;
2481
3969
  }
@@ -2484,9 +3972,10 @@ function resolveImageAttrs(el, env, out) {
2484
3972
  const tcAttr = find('tintColor');
2485
3973
  if (tcAttr) {
2486
3974
  const tc = evalStaticOr(attrExpr(tcAttr), env, null);
2487
- if (typeof tc === 'string' || typeof tc === 'number') tintColor = argbLiteral(tc);
3975
+ if (typeof tc === 'string' || typeof tc === 'number')
3976
+ tintColor = argbLiteral(tc);
2488
3977
  }
2489
- return { imageName, imageNameDyn, resizeMode, tintColor };
3978
+ return {imageName, imageNameDyn, resizeMode, tintColor};
2490
3979
  }
2491
3980
 
2492
3981
  // ---------------------------------------------------------------------------------------------------
@@ -2501,20 +3990,27 @@ function emitNodeImpl(el, scope, out, env, state, opts = {}) {
2501
3990
  if (tag === 'Svg') return emitSvg(el, scope, out, env, state, opts);
2502
3991
  if (tag === 'Switch') return emitSwitch(el, scope, out, env, state);
2503
3992
  if (tag === 'TextInput') return emitTextInput(el, scope, out, env, state);
2504
- if (tag === 'ActivityIndicator') return emitActivityIndicator(el, scope, out, env);
3993
+ if (tag === 'ActivityIndicator')
3994
+ return emitActivityIndicator(el, scope, out, env);
2505
3995
  if (tag === 'Modal') return emitModal(el, scope, out, env, state);
2506
3996
  if (tag === 'FlatList') return emitFlatList(el, scope, out, env, state, opts);
2507
3997
  const nodeType = NODE_TYPES[tag];
2508
3998
  if (!nodeType) {
2509
- if (out.components.has(tag)) return emitComponent(el, scope, out, env, state, opts);
2510
- throw aotError(`AOT: unknown element <${tag}> (not a built-in or a component in this file)`, `<${tag}> must be a built-in (View / Text / Pressable / Image / ScrollView / Svg + shapes / Animated.*) or a function component defined in THIS file. Check the import/spelling, or define the component here.`);
3999
+ if (out.components.has(tag))
4000
+ return emitComponent(el, scope, out, env, state, opts);
4001
+ throw aotError(
4002
+ `AOT: unknown element <${tag}> (not a built-in or a component in this file)`,
4003
+ `<${tag}> must be a built-in (View / Text / Pressable / Image / ScrollView / Svg + shapes / Animated.*) or a function component defined in THIS file. Check the import/spelling, or define the component here.`,
4004
+ );
2511
4005
  }
2512
4006
 
2513
4007
  // Spread attributes on a host element (`<View {...props} />`) aren't lowered — the style/event loops
2514
4008
  // below only read named JSXAttributes, so a spread would be SILENTLY dropped. Reject it explicitly
2515
4009
  // (the typed components — Switch/TextInput/Modal/… — already throw on spreads). Pin the location to the
2516
4010
  // spread itself, not the whole element, for a precise code-frame.
2517
- const spread = el.openingElement.attributes.find((a) => a.type === 'JSXSpreadAttribute');
4011
+ const spread = el.openingElement.attributes.find(
4012
+ a => a.type === 'JSXSpreadAttribute',
4013
+ );
2518
4014
  if (spread) {
2519
4015
  const e = aotError(
2520
4016
  `AOT: a spread {...} on <${tag}> is not supported`,
@@ -2525,22 +4021,35 @@ function emitNodeImpl(el, scope, out, env, state, opts = {}) {
2525
4021
  }
2526
4022
 
2527
4023
  const v = `n${out.n++}`;
2528
- const { staticAssigns, dynAssigns, binds } = collectStyleAssigns(el.openingElement, scope, env);
4024
+ const {staticAssigns, dynAssigns, binds} = collectStyleAssigns(
4025
+ el.openingElement,
4026
+ scope,
4027
+ env,
4028
+ );
2529
4029
  // A <Text> with a nested <Text> becomes inline SPANS; otherwise a single (possibly dynamic) string.
2530
- const spans = tag === 'Text' ? collectTextSpans(el.children, scope, env) : null;
2531
- const text = tag === 'Text' && !spans ? buildText(el.children, scope, env) : null;
4030
+ const spans =
4031
+ tag === 'Text' ? collectTextSpans(el.children, scope, env) : null;
4032
+ const text =
4033
+ tag === 'Text' && !spans ? buildText(el.children, scope, env) : null;
2532
4034
  // An <Image>'s baked-asset name + resize/tint. resize/tint are static → fold into staticAssigns so both
2533
4035
  // the static and (deferred) dynamic paths apply them; the asset name is a char buffer (set via snprintf),
2534
4036
  // either a compile-time literal (image.imageName) or a runtime string expr (image.imageNameDyn).
2535
4037
  const image = tag === 'Image' ? resolveImageAttrs(el, env, out) : null;
2536
- if (image?.resizeMode) staticAssigns.push({ field: 'resize_mode', expr: image.resizeMode });
2537
- if (image?.tintColor) staticAssigns.push({ field: 'tint_color', expr: image.tintColor });
4038
+ if (image?.resizeMode)
4039
+ staticAssigns.push({field: 'resize_mode', expr: image.resizeMode});
4040
+ if (image?.tintColor)
4041
+ staticAssigns.push({field: 'tint_color', expr: image.tintColor});
2538
4042
 
2539
4043
  // `displayCode` toggles show/hide for a state-driven conditional: the node is always built, its
2540
4044
  // `display` flips between flex and none in app_update (joining any state-driven style assigns).
2541
- if (opts.displayCode) dynAssigns.push({ field: 'display', code: `((${opts.displayCode}) ? ER_DISPLAY_FLEX : ER_DISPLAY_NONE)` });
4045
+ if (opts.displayCode)
4046
+ dynAssigns.push({
4047
+ field: 'display',
4048
+ code: `((${opts.displayCode}) ? ER_DISPLAY_FLEX : ER_DISPLAY_NONE)`,
4049
+ });
2542
4050
 
2543
- const isDynamic = !!text?.dynamic || dynAssigns.length > 0 || !!image?.imageNameDyn;
4051
+ const isDynamic =
4052
+ !!text?.dynamic || dynAssigns.length > 0 || !!image?.imageNameDyn;
2544
4053
 
2545
4054
  out.build.push(` ${v} = er_node_create(${nodeType});`);
2546
4055
  if (isDynamic) {
@@ -2548,12 +4057,25 @@ function emitNodeImpl(el, scope, out, env, state, opts = {}) {
2548
4057
  // <Image> carries its runtime asset name (imageName) so app_update re-snprintf's p.image_name each pass.
2549
4058
  out.build.push(` s_${v} = ${v};`);
2550
4059
  out.handles.push(v);
2551
- out.updates.push({ v, styleAssigns: staticAssigns, text, dynAssigns, imageName: image?.imageNameDyn });
4060
+ out.updates.push({
4061
+ v,
4062
+ styleAssigns: staticAssigns,
4063
+ text,
4064
+ dynAssigns,
4065
+ imageName: image?.imageNameDyn,
4066
+ });
2552
4067
  } else {
2553
4068
  out.build.push(` er_props_default(&p);`);
2554
- for (const a of staticAssigns) out.build.push(` p.${a.field} = ${a.expr};`);
2555
- if (text) out.build.push(` snprintf(p.text, sizeof(p.text), "%s", ${cstr(text.format.replace(/%%/g, '%'))});`);
2556
- if (image?.imageName != null) out.build.push(` snprintf(p.image_name, sizeof(p.image_name), "%s", ${cstr(image.imageName)});`);
4069
+ for (const a of staticAssigns)
4070
+ out.build.push(` p.${a.field} = ${a.expr};`);
4071
+ if (text)
4072
+ out.build.push(
4073
+ ` snprintf(p.text, sizeof(p.text), "%s", ${cstr(text.format.replace(/%%/g, '%'))});`,
4074
+ );
4075
+ if (image?.imageName != null)
4076
+ out.build.push(
4077
+ ` snprintf(p.image_name, sizeof(p.image_name), "%s", ${cstr(image.imageName)});`,
4078
+ );
2557
4079
  out.build.push(` er_node_set_props(${v}, &p);`);
2558
4080
  }
2559
4081
 
@@ -2561,8 +4083,15 @@ function emitNodeImpl(el, scope, out, env, state, opts = {}) {
2561
4083
  // unless it overrides (the local array is copied by er_node_set_text_spans).
2562
4084
  if (spans) {
2563
4085
  out.build.push(` {`, ` static const ERTextSpan spans_${v}[] = {`);
2564
- for (const s of spans) out.build.push(` { ${s.text}, ${s.color}, ${s.font_size}, ${s.font_weight}, ${s.font_style}, ${s.text_decoration}, ${s.letter_spacing} },`);
2565
- out.build.push(` };`, ` er_node_set_text_spans(${v}, spans_${v}, ${spans.length});`, ` }`);
4086
+ for (const s of spans)
4087
+ out.build.push(
4088
+ ` { ${s.text}, ${s.color}, ${s.font_size}, ${s.font_weight}, ${s.font_style}, ${s.text_decoration}, ${s.letter_spacing} },`,
4089
+ );
4090
+ out.build.push(
4091
+ ` };`,
4092
+ ` er_node_set_text_spans(${v}, spans_${v}, ${spans.length});`,
4093
+ ` }`,
4094
+ );
2566
4095
  }
2567
4096
 
2568
4097
  // Animated style props (opacity / transform / color) → bind the node to its animated value. The
@@ -2599,19 +4128,31 @@ function emitNodeImpl(el, scope, out, env, state, opts = {}) {
2599
4128
  if (!handlerName) {
2600
4129
  handlerName = `er_cb_${key}`;
2601
4130
  out.cbEmitted.set(key, handlerName);
2602
- out.handlers.push({ name: handlerName, body: compileHandler(env.callbacks.get(fn.name), env, state, out) });
4131
+ out.handlers.push({
4132
+ name: handlerName,
4133
+ body: compileHandler(env.callbacks.get(fn.name), env, state, out),
4134
+ });
2603
4135
  }
2604
4136
  } else if (fn.type === 'Identifier' && env.fnProps?.has(fn.name)) {
2605
4137
  // Callback prop: <Child onTap={() => …}/> where Child does onPress={onTap}. Inline the CALLER's
2606
4138
  // function as this handler, compiled in the caller's env/state so its setters/locals resolve there.
2607
4139
  const fp = env.fnProps.get(fn.name);
2608
4140
  handlerName = `er_handler_${out.handlers.length}`;
2609
- out.handlers.push({ name: handlerName, body: compileHandler(fp.node, fp.env, fp.state, out) });
4141
+ out.handlers.push({
4142
+ name: handlerName,
4143
+ body: compileHandler(fp.node, fp.env, fp.state, out),
4144
+ });
2610
4145
  } else if (isFn(fn)) {
2611
4146
  handlerName = `er_handler_${out.handlers.length}`;
2612
- out.handlers.push({ name: handlerName, body: compileHandler(fn, env, state, out) });
4147
+ out.handlers.push({
4148
+ name: handlerName,
4149
+ body: compileHandler(fn, env, state, out),
4150
+ });
2613
4151
  } else {
2614
- throw aotError(`AOT: ${attr.name.name} must be an inline function, a useCallback, or a callback prop`, `pass an inline arrow (onPress={() => setX(…)}), a useCallback identifier, or a function prop received by this component.`);
4152
+ throw aotError(
4153
+ `AOT: ${attr.name.name} must be an inline function, a useCallback, or a callback prop`,
4154
+ `pass an inline arrow (onPress={() => setX(…)}), a useCallback identifier, or a function prop received by this component.`,
4155
+ );
2615
4156
  }
2616
4157
  out.build.push(` er_event_set(${v}, ${evt}, ${handlerName}, NULL);`);
2617
4158
  }
@@ -2635,7 +4176,11 @@ function kbdColor(v) {
2635
4176
  * Shapes: { char } (types it), { char:' ', span } (space), { label, layer } (switch), { label, backspace },
2636
4177
  * { label, done }; optional span / highlight. */
2637
4178
  function kbdKeyToC(k, li) {
2638
- if (k == null || typeof k !== 'object') throw aotError('AOT: each setKeyboardConfig key must be an object', 'e.g. { char: "q" } or { label: "shift", layer: 1, highlight: true }');
4179
+ if (k == null || typeof k !== 'object')
4180
+ throw aotError(
4181
+ 'AOT: each setKeyboardConfig key must be an object',
4182
+ 'e.g. { char: "q" } or { label: "shift", layer: 1, highlight: true }',
4183
+ );
2639
4184
  let type;
2640
4185
  let label;
2641
4186
  let text = 'NULL';
@@ -2655,7 +4200,9 @@ function kbdKeyToC(k, li) {
2655
4200
  text = cstr(String(k.char));
2656
4201
  label = k.label ?? (String(k.char) === ' ' ? '' : String(k.char)); // a space bar shows no label
2657
4202
  } else {
2658
- throw aotError('AOT: a setKeyboardConfig key needs one of char / layer / backspace / done');
4203
+ throw aotError(
4204
+ 'AOT: a setKeyboardConfig key needs one of char / layer / backspace / done',
4205
+ );
2659
4206
  }
2660
4207
  const span = k.span != null ? Math.round(Number(k.span)) : 1;
2661
4208
  const hl = k.highlight ? li : 255;
@@ -2668,7 +4215,12 @@ function kbdKeyToC(k, li) {
2668
4215
  function compileKeyboardConfig(program, out) {
2669
4216
  let arg = null;
2670
4217
  for (const stmt of program.body) {
2671
- if (stmt.type === 'ExpressionStatement' && stmt.expression.type === 'CallExpression' && stmt.expression.callee.type === 'Identifier' && stmt.expression.callee.name === 'setKeyboardConfig') {
4218
+ if (
4219
+ stmt.type === 'ExpressionStatement' &&
4220
+ stmt.expression.type === 'CallExpression' &&
4221
+ stmt.expression.callee.type === 'Identifier' &&
4222
+ stmt.expression.callee.name === 'setKeyboardConfig'
4223
+ ) {
2672
4224
  arg = stmt.expression.arguments[0];
2673
4225
  break;
2674
4226
  }
@@ -2678,9 +4230,13 @@ function compileKeyboardConfig(program, out) {
2678
4230
  try {
2679
4231
  cfg = evalStatic(arg, {});
2680
4232
  } catch {
2681
- throw aotError('AOT: setKeyboardConfig(...) needs a statically-foldable config object', 'pass an object literal of colours/sizes (+ an optional `layers` array) — no state or runtime values.');
4233
+ throw aotError(
4234
+ 'AOT: setKeyboardConfig(...) needs a statically-foldable config object',
4235
+ 'pass an object literal of colours/sizes (+ an optional `layers` array) — no state or runtime values.',
4236
+ );
2682
4237
  }
2683
- if (cfg == null || typeof cfg !== 'object') throw aotError('AOT: setKeyboardConfig(...) needs a config object');
4238
+ if (cfg == null || typeof cfg !== 'object')
4239
+ throw aotError('AOT: setKeyboardConfig(...) needs a config object');
2684
4240
 
2685
4241
  const data = [];
2686
4242
  let layersExpr = 'NULL';
@@ -2688,21 +4244,33 @@ function compileKeyboardConfig(program, out) {
2688
4244
  if (Array.isArray(cfg.layers)) {
2689
4245
  const layerVars = [];
2690
4246
  cfg.layers.forEach((layer, li) => {
2691
- if (!Array.isArray(layer)) throw aotError('AOT: setKeyboardConfig `layers[i]` must be an array of rows');
4247
+ if (!Array.isArray(layer))
4248
+ throw aotError(
4249
+ 'AOT: setKeyboardConfig `layers[i]` must be an array of rows',
4250
+ );
2692
4251
  const rowVars = [];
2693
4252
  layer.forEach((row, ri) => {
2694
- if (!Array.isArray(row) || !row.length) throw aotError('AOT: each keyboard row must be a non-empty array of keys');
2695
- data.push(`static const ERKeyboardKey kbd_l${li}r${ri}[] = { ${row.map((k) => kbdKeyToC(k, li)).join(', ')} };`);
4253
+ if (!Array.isArray(row) || !row.length)
4254
+ throw aotError(
4255
+ 'AOT: each keyboard row must be a non-empty array of keys',
4256
+ );
4257
+ data.push(
4258
+ `static const ERKeyboardKey kbd_l${li}r${ri}[] = { ${row.map(k => kbdKeyToC(k, li)).join(', ')} };`,
4259
+ );
2696
4260
  rowVars.push(`{ kbd_l${li}r${ri}, ${row.length} }`);
2697
4261
  });
2698
- data.push(`static const ERKeyboardRow kbd_l${li}rows[] = { ${rowVars.join(', ')} };`);
4262
+ data.push(
4263
+ `static const ERKeyboardRow kbd_l${li}rows[] = { ${rowVars.join(', ')} };`,
4264
+ );
2699
4265
  layerVars.push(`{ kbd_l${li}rows, ${layer.length} }`);
2700
4266
  });
2701
- data.push(`static const ERKeyboardLayer kbd_layers[] = { ${layerVars.join(', ')} };`);
4267
+ data.push(
4268
+ `static const ERKeyboardLayer kbd_layers[] = { ${layerVars.join(', ')} };`,
4269
+ );
2702
4270
  layersExpr = 'kbd_layers';
2703
4271
  layerCount = cfg.layers.length;
2704
4272
  }
2705
- const num = (v) => (v == null ? 0 : Math.round(Number(v)));
4273
+ const num = v => (v == null ? 0 : Math.round(Number(v)));
2706
4274
  data.push(
2707
4275
  `static const ERKeyboardConfig kbd_cfg = { ${layersExpr}, ${layerCount}, ${num(cfg.gridCols)}, ${num(cfg.rowHeight)}, ` +
2708
4276
  `${num(cfg.keyGap)}, ${num(cfg.keyRadius)}, ${num(cfg.fontSize)}, ${kbdColor(cfg.panelColor)}, ${kbdColor(cfg.keyColor)}, ` +
@@ -2724,146 +4292,234 @@ function compileKeyboardConfig(program, out) {
2724
4292
  * @returns {{c: string, h: string, nodes: number, state: number, handlers: number, updates: number}}
2725
4293
  */
2726
4294
  function compileSourceImpl(src, demo = 'app', opts = {}) {
2727
- const ast = parse(src, { sourceType: 'module', plugins: ['jsx'] });
2728
-
2729
- const screen = opts.screen ?? { width: SCREEN_W, height: SCREEN_H };
2730
- // Image imports first, so their asset-name strings seed the module scope BEFORE its consts fold (a const
2731
- // array of `{ icon: wxSun }` needs wxSun resolvable). An image import is just its baked-name string.
2732
- const imageImports = collectImageImports(ast.program);
2733
- const imageSeed = Object.fromEntries([...imageImports].map(([local, imp]) => [local, imp.name]));
2734
- const scope = moduleScope(ast.program, screen, imageSeed);
2735
- const component = findComponent(ast.program);
2736
- // Fold statically-derived component-local consts (e.g. `const compact = screen.width < 400`) into the
2737
- // const scope, so responsive `if` branches and styles can switch on them at compile time. Dynamic consts
2738
- // (state-derived, useMemo, etc.) throw here and are skipped — they're handled later by memos/emitExpr.
2739
- for (const stmt of component.body.body) {
2740
- if (stmt.type !== 'VariableDeclaration' || stmt.kind !== 'const') continue;
2741
- for (const decl of stmt.declarations) {
2742
- if (decl.id.type !== 'Identifier' || !decl.init || decl.id.name in scope) continue;
4295
+ const ast = parse(src, {sourceType: 'module', plugins: ['jsx']});
4296
+
4297
+ const screen = opts.screen ?? {width: SCREEN_W, height: SCREEN_H};
4298
+ // Image imports first, so their asset-name strings seed the module scope BEFORE its consts fold (a const
4299
+ // array of `{ icon: wxSun }` needs wxSun resolvable). An image import is just its baked-name string.
4300
+ const imageImports = collectImageImports(ast.program);
4301
+ const svgImports = collectSvgImports(ast.program); // <Svg source> imports → artifacts baked by the CLI (opts.svgArtifacts)
4302
+ const imageSeed = Object.fromEntries(
4303
+ [...imageImports].map(([local, imp]) => [local, imp.name]),
4304
+ );
4305
+ const scope = moduleScope(ast.program, screen, imageSeed);
4306
+ const component = findComponent(ast.program);
4307
+ // Fold statically-derived component-local consts (e.g. `const compact = screen.width < 400`) into the
4308
+ // const scope, so responsive `if` branches and styles can switch on them at compile time. Dynamic consts
4309
+ // (state-derived, useMemo, etc.) throw here and are skipped — they're handled later by memos/emitExpr.
4310
+ for (const stmt of component.body.body) {
4311
+ if (stmt.type !== 'VariableDeclaration' || stmt.kind !== 'const') continue;
4312
+ for (const decl of stmt.declarations) {
4313
+ if (decl.id.type !== 'Identifier' || !decl.init || decl.id.name in scope)
4314
+ continue;
4315
+ try {
4316
+ scope[decl.id.name] = evalStatic(decl.init, scope);
4317
+ } catch {
4318
+ /* dynamic const — resolved later */
4319
+ }
4320
+ }
4321
+ }
4322
+ const state = collectState(component.body, scope);
4323
+ const rootJSX = findReturnJSX(component.body, scope);
4324
+
4325
+ const anims = collectAnims(component.body, scope);
4326
+ const refs = collectRefs(component.body, scope);
4327
+ const callbacks = collectCallbacks(component.body);
4328
+ const memos = collectMemos(component.body);
4329
+ const helpers = collectHelpers(component.body, ast.program);
4330
+ const imageNames = new Map(
4331
+ [...imageImports].map(([, imp]) => [imp.name, imp.importPath]),
4332
+ ); // asset name → path
4333
+ const env = {
4334
+ state: state.byName,
4335
+ locals: new Map(),
4336
+ consts: scope,
4337
+ anims,
4338
+ refs,
4339
+ callbacks,
4340
+ helpers,
4341
+ imageNames,
4342
+ svgImports,
4343
+ svgArtifacts: opts.svgArtifacts || {},
4344
+ };
4345
+ // Resolve memos in declaration order: constant-fold into the const scope when possible, else register a
4346
+ // derived C expression in locals so each reference inlines it (the AOT has no per-render cache — the dep
4347
+ // tracking re-applies dependent nodes anyway). Done before emit so references resolve.
4348
+ for (const [name, expr] of memos) {
2743
4349
  try {
2744
- scope[decl.id.name] = evalStatic(decl.init, scope);
4350
+ scope[name] = evalStatic(expr, scope);
2745
4351
  } catch {
2746
- /* dynamic const resolved later */
4352
+ const e = emitExpr(expr, env);
4353
+ env.locals.set(name, {code: `(${e.code})`, cType: e.cType});
2747
4354
  }
2748
4355
  }
2749
- }
2750
- const state = collectState(component.body, scope);
2751
- const rootJSX = findReturnJSX(component.body, scope);
4356
+ const out = {
4357
+ n: 0,
4358
+ build: [],
4359
+ handlers: [],
4360
+ updates: [],
4361
+ handles: [],
4362
+ components: collectComponents(ast.program),
4363
+ cbEmitted: new Map(),
4364
+ vectorData: [],
4365
+ vectorBuilders: [],
4366
+ svgUpdates: [],
4367
+ svgN: 0,
4368
+ needsMath: false,
4369
+ timerFns: [],
4370
+ usesTimers: false,
4371
+ mountEffects: [],
4372
+ animCbs: [],
4373
+ seqN: 0,
4374
+ kbdData: '',
4375
+ kbdSetup: '',
4376
+ images: new Map(),
4377
+ bakeAllImages: false,
4378
+ instN: 0,
4379
+ childStateRecords: [],
4380
+ childRefs: [],
4381
+ childAnims: [],
4382
+ program: ast.program,
4383
+ effN: 0,
4384
+ effectFns: [],
4385
+ effectDecls: [],
4386
+ depEffects: [],
4387
+ };
4388
+ compileKeyboardConfig(ast.program, out); // module-level setKeyboardConfig({...}) → static ERKeyboardConfig
4389
+ const appTop = emitNode(rootJSX, scope, out, env, state);
4390
+
4391
+ // Image baking: a REACHED static source registered its import in out.images during emit. If any reached
4392
+ // source is DYNAMIC (resolved by name at runtime), we can't enumerate it — fall back to baking every import.
4393
+ // Either way, an image used only in a folded-away branch (never emitted) costs no flash.
4394
+ if (out.bakeAllImages)
4395
+ for (const [, imp] of imageImports)
4396
+ out.images.set(imp.name, imp.importPath);
4397
+
4398
+ // Effects: `useEffect(fn, [])` runs once at mount; `useEffect(fn, [dep…])` re-runs from app_update when a
4399
+ // dep changes (see compileEffect). Compiled after emit so out.timerFns/usesTimers reflect handler timers too.
4400
+ for (const eff of collectEffects(component.body)) {
4401
+ compileEffect(eff, env, state, out);
4402
+ }
2752
4403
 
2753
- const anims = collectAnims(component.body, scope);
2754
- const refs = collectRefs(component.body, scope);
2755
- const callbacks = collectCallbacks(component.body);
2756
- const memos = collectMemos(component.body);
2757
- const helpers = collectHelpers(component.body, ast.program);
2758
- const imageNames = new Map([...imageImports].map(([, imp]) => [imp.name, imp.importPath])); // asset name → path
2759
- const env = { state: state.byName, locals: new Map(), consts: scope, anims, refs, callbacks, helpers, imageNames };
2760
- // Resolve memos in declaration order: constant-fold into the const scope when possible, else register a
2761
- // derived C expression in locals so each reference inlines it (the AOT has no per-render cache — the dep
2762
- // tracking re-applies dependent nodes anyway). Done before emit so references resolve.
2763
- for (const [name, expr] of memos) {
2764
- try {
2765
- scope[name] = evalStatic(expr, scope);
2766
- } catch {
2767
- const e = emitExpr(expr, env);
2768
- env.locals.set(name, { code: `(${e.code})`, cType: e.cType });
2769
- }
2770
- }
2771
- const out = { n: 0, build: [], handlers: [], updates: [], handles: [], components: collectComponents(ast.program), cbEmitted: new Map(), vectorData: [], vectorBuilders: [], svgUpdates: [], svgN: 0, needsMath: false, timerFns: [], usesTimers: false, mountEffects: [], animCbs: [], seqN: 0, kbdData: '', kbdSetup: '', images: new Map(), bakeAllImages: false, instN: 0, childStateRecords: [], childRefs: [], childAnims: [], program: ast.program, effN: 0, effectFns: [], effectDecls: [], depEffects: [] };
2772
- compileKeyboardConfig(ast.program, out); // module-level setKeyboardConfig({...}) → static ERKeyboardConfig
2773
- const appTop = emitNode(rootJSX, scope, out, env, state);
2774
-
2775
- // Image baking: a REACHED static source registered its import in out.images during emit. If any reached
2776
- // source is DYNAMIC (resolved by name at runtime), we can't enumerate it — fall back to baking every import.
2777
- // Either way, an image used only in a folded-away branch (never emitted) costs no flash.
2778
- if (out.bakeAllImages) for (const [, imp] of imageImports) out.images.set(imp.name, imp.importPath);
2779
-
2780
- // Effects: `useEffect(fn, [])` runs once at mount; `useEffect(fn, [dep…])` re-runs from app_update when a
2781
- // dep changes (see compileEffect). Compiled after emit so out.timerFns/usesTimers reflect handler timers too.
2782
- for (const eff of collectEffects(component.body)) {
2783
- compileEffect(eff, env, state, out);
2784
- }
2785
-
2786
- const nodeDecls = Array.from({ length: out.n }, (_, i) => `n${i}`);
2787
- // App state + every inlined child instance's per-instance state (each already namespaced via cField).
2788
- const stateRecords = [...state.byName.values(), ...out.childStateRecords];
2789
- const scalarRecords = stateRecords.filter((s) => s.kind === 'scalar');
2790
- const listRecords = stateRecords.filter((s) => s.kind === 'list');
2791
-
2792
- // Scalar state one ErAppState struct. List state a fixed-capacity struct array + a count each.
2793
- const fieldCDecl = (f) => (f.kind === 'string' ? ` char ${f.key}[${LIST_STR_CAP}];` : ` ${f.kind} ${f.key};`);
2794
- const itemInit = (item, struct) => `{ ${struct.fields.map((f) => (f.kind === 'string' ? cstr(String(item[f.key] ?? '')) : f.kind === 'float' ? `${Number(item[f.key]) || 0}f` : String(Math.round(Number(item[f.key]) || 0)))).join(', ')} }`;
2795
- const listBlocks = listRecords
2796
- .map(
2797
- (s) =>
2798
- `typedef struct\n{\n${s.struct.fields.map(fieldCDecl).join('\n')}\n} ${s.cTypeName};\n\n` +
2799
- `static ${s.cTypeName} ${s.arrayName}[${s.cap}] = {${s.items.map((it) => '\n ' + itemInit(it, s.struct)).join(',')}\n};\n` +
2800
- `static int ${s.countMember} = ${s.items.length};\n`,
2801
- )
2802
- .join('\n');
2803
- const scalarFieldDecl = (s) => (s.cType === 'string' ? ` char ${s.cField}[${LIST_STR_CAP}];` : ` ${s.cType === 'float' ? 'float' : 'int'} ${s.cField};`);
2804
- const scalarBlock = scalarRecords.length
2805
- ? `typedef struct\n{\n${scalarRecords.map(scalarFieldDecl).join('\n')}\n} ErAppState;\n\nstatic ErAppState s_state = {${scalarRecords.map((s) => ` .${s.cField} = ${s.initCode}`).join(',')} };\n`
2806
- : '';
2807
- const stateBlock = [scalarBlock, listBlocks].filter(Boolean).join('\n');
2808
-
2809
- const handleDecls = out.handles.map((v) => `static ERNode* s_${v};`).join('\n');
2810
-
2811
- // Value refs — a plain mutable static each (escape-hatch state that does not trigger a re-render).
2812
- const refDecls = [...refs.values(), ...out.childRefs].map((r) => `static ${r.cType} ${r.cVar} = ${r.initCode};`).join('\n');
2813
-
2814
- // Baked vector op-tapes + paint tables (static <Svg> geometry), emitted at file scope.
2815
- const vectorBlock = out.vectorData.join('\n\n');
2816
- // build_svgN() recompute functions (state-driven Svgs) declared before app_update, which calls them.
2817
- const vectorBuilderBlock = out.vectorBuilders.join('\n\n');
2818
-
2819
- // Animated values — one engine-side handle each, created at the top of er_app_build (binds reference them).
2820
- const animList = [...anims.values(), ...out.childAnims];
2821
- const animDecls = animList.map((a) => `static ERAnimValueHandle ${a.cVar};`).join('\n');
2822
- const animCreate = animList.map((a) => ` ${a.cVar} = er_anim_value_create(${a.initCode});`).join('\n');
2823
-
2824
- const hasUpdate = out.updates.length > 0 || out.svgUpdates.length > 0 || out.depEffects.length > 0;
2825
- const updateBlock = (() => {
2826
- if (!hasUpdate) return '';
2827
- const lines = ['static void app_update(void)', '{'];
2828
- if (out.updates.length) lines.push(' ERProps p;');
2829
- for (const u of out.updates) {
2830
- lines.push(` er_props_default(&p);`);
2831
- for (const a of u.styleAssigns) lines.push(` p.${a.field} = ${a.expr};`);
2832
- for (const a of u.dynAssigns) lines.push(` p.${a.field} = ${a.code};`);
2833
- if (u.placeholder != null) lines.push(` snprintf(p.placeholder, sizeof(p.placeholder), "%s", ${cstr(u.placeholder)});`);
2834
- if (u.imageName != null) lines.push(` snprintf(p.image_name, sizeof(p.image_name), "%s", ${u.imageName});`);
2835
- if (u.text) {
2836
- if (u.text.args.length) lines.push(` snprintf(p.text, sizeof(p.text), ${cstr(u.text.format)}, ${u.text.args.join(', ')});`);
2837
- else lines.push(` snprintf(p.text, sizeof(p.text), "%s", ${cstr(u.text.format.replace(/%%/g, '%'))});`);
2838
- }
2839
- lines.push(` er_node_set_props(s_${u.v}, &p);`);
2840
- }
2841
- // State-driven Svgs: recompute the op-tape from state and re-upload.
2842
- for (const s of out.svgUpdates) {
2843
- lines.push(` build_svg${s.id}();`);
2844
- lines.push(` er_node_set_vector_ops(${s.nodeVar}, s_svg${s.id}_ops, ${s.len}, s_svg${s.id}_paints, ${s.nPaints});`);
2845
- }
2846
- // Dep-driven useEffect: run each effect whose dependency value changed since the last app_update.
2847
- for (const block of out.depEffects) lines.push(block);
2848
- lines.push('}');
2849
- return lines.join('\n');
2850
- })();
2851
-
2852
- const handlerDefs = out.handlers
2853
- .map((h) => `static void ${h.name}(ERNode* node, const EREventData* data, void* user_data)\n{\n (void)node;\n (void)data;\n (void)user_data;\n${h.body.join('\n')}\n}`)
2854
- .join('\n\n');
2855
-
2856
- // Dep-driven useEffect: a static "previous value" per dependency, a forward decl (app_update calls each
2857
- // er_effect_N before it's defined), and the effect body as a parameterless C function.
2858
- const effectDeclsBlock = out.effectDecls.join('\n');
2859
- const effectFwdDecls = out.effectFns.map((f) => `static void ${f.name}(void);`).join('\n');
2860
- const effectFnDefs = out.effectFns.map((f) => `static void ${f.name}(void)\n{\n${f.body.join('\n')}\n}`).join('\n\n');
2861
-
2862
- // setInterval/setTimeout → a small fixed timer table advanced by er_app_tick(dt) (the host calls it each
2863
- // frame). The table + helpers are emitted only when timers are used; er_app_tick is always defined (a no-op
2864
- // otherwise) so a host can call it unconditionally. Timer callbacks become parameterless C functions.
2865
- const timerTableBlock = out.usesTimers
2866
- ? `#include <stdbool.h>
4404
+ const nodeDecls = Array.from({length: out.n}, (_, i) => `n${i}`);
4405
+ // App state + every inlined child instance's per-instance state (each already namespaced via cField).
4406
+ const stateRecords = [...state.byName.values(), ...out.childStateRecords];
4407
+ const scalarRecords = stateRecords.filter(s => s.kind === 'scalar');
4408
+ const listRecords = stateRecords.filter(s => s.kind === 'list');
4409
+
4410
+ // Scalar state one ErAppState struct. List state a fixed-capacity struct array + a count each.
4411
+ const fieldCDecl = f =>
4412
+ f.kind === 'string'
4413
+ ? ` char ${f.key}[${LIST_STR_CAP}];`
4414
+ : ` ${f.kind} ${f.key};`;
4415
+ const itemInit = (item, struct) =>
4416
+ `{ ${struct.fields.map(f => (f.kind === 'string' ? cstr(String(item[f.key] ?? '')) : f.kind === 'float' ? `${Number(item[f.key]) || 0}f` : String(Math.round(Number(item[f.key]) || 0)))).join(', ')} }`;
4417
+ const listBlocks = listRecords
4418
+ .map(
4419
+ s =>
4420
+ `typedef struct\n{\n${s.struct.fields.map(fieldCDecl).join('\n')}\n} ${s.cTypeName};\n\n` +
4421
+ `static ${s.cTypeName} ${s.arrayName}[${s.cap}] = {${s.items.map(it => '\n ' + itemInit(it, s.struct)).join(',')}\n};\n` +
4422
+ `static int ${s.countMember} = ${s.items.length};\n`,
4423
+ )
4424
+ .join('\n');
4425
+ const scalarFieldDecl = s =>
4426
+ s.cType === 'string'
4427
+ ? ` char ${s.cField}[${LIST_STR_CAP}];`
4428
+ : ` ${s.cType === 'float' ? 'float' : 'int'} ${s.cField};`;
4429
+ const scalarBlock = scalarRecords.length
4430
+ ? `typedef struct\n{\n${scalarRecords.map(scalarFieldDecl).join('\n')}\n} ErAppState;\n\nstatic ErAppState s_state = {${scalarRecords.map(s => ` .${s.cField} = ${s.initCode}`).join(',')} };\n`
4431
+ : '';
4432
+ const stateBlock = [scalarBlock, listBlocks].filter(Boolean).join('\n');
4433
+
4434
+ const handleDecls = out.handles.map(v => `static ERNode* s_${v};`).join('\n');
4435
+
4436
+ // Value refs — a plain mutable static each (escape-hatch state that does not trigger a re-render).
4437
+ const refDecls = [...refs.values(), ...out.childRefs]
4438
+ .map(r => `static ${r.cType} ${r.cVar} = ${r.initCode};`)
4439
+ .join('\n');
4440
+
4441
+ // Baked vector op-tapes + paint tables (static <Svg> geometry), emitted at file scope.
4442
+ const vectorBlock = out.vectorData.join('\n\n');
4443
+ // build_svgN() recompute functions (state-driven Svgs) declared before app_update, which calls them.
4444
+ const vectorBuilderBlock = out.vectorBuilders.join('\n\n');
4445
+
4446
+ // Animated values — one engine-side handle each, created at the top of er_app_build (binds reference them).
4447
+ const animList = [...anims.values(), ...out.childAnims];
4448
+ const animDecls = animList
4449
+ .map(a => `static ERAnimValueHandle ${a.cVar};`)
4450
+ .join('\n');
4451
+ const animCreate = animList
4452
+ .map(a => ` ${a.cVar} = er_anim_value_create(${a.initCode});`)
4453
+ .join('\n');
4454
+
4455
+ const hasUpdate =
4456
+ out.updates.length > 0 ||
4457
+ out.svgUpdates.length > 0 ||
4458
+ out.depEffects.length > 0;
4459
+ const updateBlock = (() => {
4460
+ if (!hasUpdate) return '';
4461
+ const lines = ['static void app_update(void)', '{'];
4462
+ if (out.updates.length) lines.push(' ERProps p;');
4463
+ for (const u of out.updates) {
4464
+ lines.push(` er_props_default(&p);`);
4465
+ for (const a of u.styleAssigns)
4466
+ lines.push(` p.${a.field} = ${a.expr};`);
4467
+ for (const a of u.dynAssigns) lines.push(` p.${a.field} = ${a.code};`);
4468
+ if (u.placeholder != null)
4469
+ lines.push(
4470
+ ` snprintf(p.placeholder, sizeof(p.placeholder), "%s", ${cstr(u.placeholder)});`,
4471
+ );
4472
+ if (u.imageName != null)
4473
+ lines.push(
4474
+ ` snprintf(p.image_name, sizeof(p.image_name), "%s", ${u.imageName});`,
4475
+ );
4476
+ if (u.text) {
4477
+ if (u.text.args.length)
4478
+ lines.push(
4479
+ ` snprintf(p.text, sizeof(p.text), ${cstr(u.text.format)}, ${u.text.args.join(', ')});`,
4480
+ );
4481
+ else
4482
+ lines.push(
4483
+ ` snprintf(p.text, sizeof(p.text), "%s", ${cstr(u.text.format.replace(/%%/g, '%'))});`,
4484
+ );
4485
+ }
4486
+ lines.push(` er_node_set_props(s_${u.v}, &p);`);
4487
+ }
4488
+ // State-driven Svgs: recompute the op-tape from state and re-upload.
4489
+ for (const s of out.svgUpdates) {
4490
+ lines.push(` build_svg${s.id}();`);
4491
+ lines.push(
4492
+ ` er_node_set_vector_ops(${s.nodeVar}, s_svg${s.id}_ops, ${s.len}, s_svg${s.id}_paints, ${s.nPaints}, NULL, 0);`,
4493
+ );
4494
+ }
4495
+ // Dep-driven useEffect: run each effect whose dependency value changed since the last app_update.
4496
+ for (const block of out.depEffects) lines.push(block);
4497
+ lines.push('}');
4498
+ return lines.join('\n');
4499
+ })();
4500
+
4501
+ const handlerDefs = out.handlers
4502
+ .map(
4503
+ h =>
4504
+ `static void ${h.name}(ERNode* node, const EREventData* data, void* user_data)\n{\n (void)node;\n (void)data;\n (void)user_data;\n${h.body.join('\n')}\n}`,
4505
+ )
4506
+ .join('\n\n');
4507
+
4508
+ // Dep-driven useEffect: a static "previous value" per dependency, a forward decl (app_update calls each
4509
+ // er_effect_N before it's defined), and the effect body as a parameterless C function.
4510
+ const effectDeclsBlock = out.effectDecls.join('\n');
4511
+ const effectFwdDecls = out.effectFns
4512
+ .map(f => `static void ${f.name}(void);`)
4513
+ .join('\n');
4514
+ const effectFnDefs = out.effectFns
4515
+ .map(f => `static void ${f.name}(void)\n{\n${f.body.join('\n')}\n}`)
4516
+ .join('\n\n');
4517
+
4518
+ // setInterval/setTimeout → a small fixed timer table advanced by er_app_tick(dt) (the host calls it each
4519
+ // frame). The table + helpers are emitted only when timers are used; er_app_tick is always defined (a no-op
4520
+ // otherwise) so a host can call it unconditionally. Timer callbacks become parameterless C functions.
4521
+ const timerTableBlock = out.usesTimers
4522
+ ? `#include <stdbool.h>
2867
4523
 
2868
4524
  #ifndef ER_AOT_MAX_TIMERS
2869
4525
  #define ER_AOT_MAX_TIMERS 8
@@ -2903,19 +4559,26 @@ static void er_timer_clear(int id)
2903
4559
  s_timers[id].active = false;
2904
4560
  }
2905
4561
  }`
2906
- : '';
2907
-
2908
- const timerFnDefs = out.timerFns.map((t) => `static void ${t.name}(void)\n{\n${t.body.join('\n')}\n}`).join('\n\n');
2909
-
2910
- // Animated.sequence on_complete callbacks: each starts the next step when the previous finishes. Forward-
2911
- // declared (the handler that starts step 0 references the first callback, and each callback the next).
2912
- const animCbDecls = out.animCbs.map((cb) => `static void ${cb.name}(bool finished, void* user_data);`).join('\n');
2913
- const animCbDefs = out.animCbs
2914
- .map((cb) => `static void ${cb.name}(bool finished, void* user_data)\n{\n (void)finished;\n (void)user_data;\n${cb.body.join('\n')}\n}`)
2915
- .join('\n\n');
2916
-
2917
- const appTickFn = out.usesTimers
2918
- ? `void er_app_tick(int dt_ms)
4562
+ : '';
4563
+
4564
+ const timerFnDefs = out.timerFns
4565
+ .map(t => `static void ${t.name}(void)\n{\n${t.body.join('\n')}\n}`)
4566
+ .join('\n\n');
4567
+
4568
+ // Animated.sequence on_complete callbacks: each starts the next step when the previous finishes. Forward-
4569
+ // declared (the handler that starts step 0 references the first callback, and each callback the next).
4570
+ const animCbDecls = out.animCbs
4571
+ .map(cb => `static void ${cb.name}(bool finished, void* user_data);`)
4572
+ .join('\n');
4573
+ const animCbDefs = out.animCbs
4574
+ .map(
4575
+ cb =>
4576
+ `static void ${cb.name}(bool finished, void* user_data)\n{\n (void)finished;\n (void)user_data;\n${cb.body.join('\n')}\n}`,
4577
+ )
4578
+ .join('\n\n');
4579
+
4580
+ const appTickFn = out.usesTimers
4581
+ ? `void er_app_tick(int dt_ms)
2919
4582
  {
2920
4583
  for (int i = 0; i < ER_AOT_MAX_TIMERS; i++)
2921
4584
  {
@@ -2946,13 +4609,31 @@ const appTickFn = out.usesTimers
2946
4609
  }
2947
4610
  }
2948
4611
  }`
2949
- : `void er_app_tick(int dt_ms)\n{\n (void)dt_ms;\n}`;
2950
-
2951
- const mountEffectsBlock = out.mountEffects.length ? '\n /* useEffect(fn, []) — run once on mount. */\n' + out.mountEffects.join('\n') + '\n' : '';
2952
-
2953
- // <math.h> when any libm symbol appears (Svg arc trig, or Math.* in expressions/handlers/timer callbacks).
2954
- const usesMath = out.needsMath || /\b(sinf|cosf|tanf|sqrtf|fabsf|roundf|floorf|ceilf|fminf|fmaxf|atan2f|powf|M_PI)\b/.test([stateBlock, refDecls, vectorBuilderBlock, updateBlock, handlerDefs, animCbDefs, timerFnDefs, out.mountEffects.join('\n'), out.build.join('\n')].join('\n'));
2955
- const body = `/*
4612
+ : `void er_app_tick(int dt_ms)\n{\n (void)dt_ms;\n}`;
4613
+
4614
+ const mountEffectsBlock = out.mountEffects.length
4615
+ ? '\n /* useEffect(fn, []) — run once on mount. */\n' +
4616
+ out.mountEffects.join('\n') +
4617
+ '\n'
4618
+ : '';
4619
+
4620
+ // <math.h> when any libm symbol appears (Svg arc trig, or Math.* in expressions/handlers/timer callbacks).
4621
+ const usesMath =
4622
+ out.needsMath ||
4623
+ /\b(sinf|cosf|tanf|sqrtf|fabsf|roundf|floorf|ceilf|fminf|fmaxf|atan2f|powf|M_PI)\b/.test(
4624
+ [
4625
+ stateBlock,
4626
+ refDecls,
4627
+ vectorBuilderBlock,
4628
+ updateBlock,
4629
+ handlerDefs,
4630
+ animCbDefs,
4631
+ timerFnDefs,
4632
+ out.mountEffects.join('\n'),
4633
+ out.build.join('\n'),
4634
+ ].join('\n'),
4635
+ );
4636
+ const body = `/*
2956
4637
  * Generated by the embedded-react Flow B AOT compiler (npm run aot -- ${demo}). DO NOT EDIT.
2957
4638
  * Builds the app's scene graph + state machine directly against er_scene.h — no QuickJS, no JS runtime.
2958
4639
  */
@@ -2990,7 +4671,7 @@ ${out.build.join('\n')}
2990
4671
  ${out.kbdSetup ? out.kbdSetup + ' /* app-supplied on-screen keyboard layout/appearance */\n' : ''}${hasUpdate ? '\n app_update(); /* apply initial state-dependent props */\n' : ''}${mountEffectsBlock}}
2991
4672
  `;
2992
4673
 
2993
- const header = `/* Generated by the embedded-react Flow B AOT compiler. DO NOT EDIT. */
4674
+ const header = `/* Generated by the embedded-react Flow B AOT compiler. DO NOT EDIT. */
2994
4675
  #ifndef ER_APP_GEN_H
2995
4676
  #define ER_APP_GEN_H
2996
4677
 
@@ -3006,8 +4687,19 @@ void er_app_tick(int dt_ms);
3006
4687
 
3007
4688
  // images: the baked-image imports the app actually references (name + source-relative path) — the CLI
3008
4689
  // resolves each path against the demo dir and bakes them into assets.generated.c (er_register_assets).
3009
- const images = [...out.images.entries()].map(([name, importPath]) => ({ name, importPath }));
3010
- return { c: body, h: header, nodes: out.n, state: stateRecords.length, handlers: out.handlers.length, updates: out.updates.length, images };
4690
+ const images = [...out.images.entries()].map(([name, importPath]) => ({
4691
+ name,
4692
+ importPath,
4693
+ }));
4694
+ return {
4695
+ c: body,
4696
+ h: header,
4697
+ nodes: out.n,
4698
+ state: stateRecords.length,
4699
+ handlers: out.handlers.length,
4700
+ updates: out.updates.length,
4701
+ images,
4702
+ };
3011
4703
  }
3012
4704
 
3013
4705
  /** Public entry: compile JSX source → { c, h, ... }. On an AOT error, annotate it with file:line:col + a
@@ -3027,38 +4719,56 @@ export function compileSource(src, demo = 'app', opts = {}) {
3027
4719
  // CLI entry — `node aot/compile.mjs [demo]`: read a demo's App.jsx, write dist/app.gen.{c,h}. Runs only
3028
4720
  // when this file is invoked directly (not when imported by the test harness).
3029
4721
  // ---------------------------------------------------------------------------------------------------
3030
- if (process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))) {
4722
+ if (
4723
+ process.argv[1] &&
4724
+ resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))
4725
+ ) {
3031
4726
  const demo = process.argv[2] || process.env.DEMO || 'thermostat';
3032
4727
  const appPath = resolve(demosDir, demo, 'App.jsx');
3033
4728
  if (!existsSync(appPath)) {
3034
- const avail = existsSync(demosDir) ? readdirSync(demosDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name) : [];
3035
- console.error(`AOT: demo "${demo}" not found (expected ${appPath}). Available: ${avail.join(', ') || '(none)'}`);
4729
+ const avail = existsSync(demosDir)
4730
+ ? readdirSync(demosDir, {withFileTypes: true})
4731
+ .filter(d => d.isDirectory())
4732
+ .map(d => d.name)
4733
+ : [];
4734
+ console.error(
4735
+ `AOT: demo "${demo}" not found (expected ${appPath}). Available: ${avail.join(', ') || '(none)'}`,
4736
+ );
3036
4737
  process.exit(1);
3037
4738
  }
4739
+ const src = readFileSync(appPath, 'utf8');
3038
4740
  let result;
3039
4741
  try {
3040
- result = compileSource(readFileSync(appPath, 'utf8'), demo, { filename: resolve(demosDir, demo, 'App.jsx') });
4742
+ // Bake any <Svg source> .svg imports to vector artifacts first (I/O), then compile (pure) with them in hand.
4743
+ const svgArtifacts = await bakeSvgArtifacts(src, resolve(demosDir, demo));
4744
+ result = compileSource(src, demo, {
4745
+ filename: resolve(demosDir, demo, 'App.jsx'),
4746
+ svgArtifacts,
4747
+ });
3041
4748
  } catch (e) {
3042
4749
  // A located AOT error already reads as "<reason>\n at file:line:col\n\n<frame>\n\nhint: ..."; print it
3043
4750
  // cleanly (no JS stack) so the developer sees exactly the unsupported construct.
3044
4751
  console.error(e && e.aotLoc ? e.message : e?.message || String(e));
3045
4752
  process.exit(1);
3046
4753
  }
3047
- mkdirSync(distDir, { recursive: true });
4754
+ mkdirSync(distDir, {recursive: true});
3048
4755
  writeFileSync(resolve(distDir, 'app.gen.c'), result.c);
3049
4756
  writeFileSync(resolve(distDir, 'app.gen.h'), result.h);
3050
4757
 
3051
4758
  // Bake the images the app imports into dist/assets.generated.{c,h} (er_register_assets) — the SAME baker
3052
4759
  // Flow A uses. Always written (even with 0 images → a no-op register fn) so the AOT host can always
3053
4760
  // compile + call it. Each importPath is source-relative to the demo's App.jsx.
3054
- const imageJobs = result.images.map((im) => ({ name: im.name, path: resolve(demosDir, demo, im.importPath) }));
4761
+ const imageJobs = result.images.map(im => ({
4762
+ name: im.name,
4763
+ path: resolve(demosDir, demo, im.importPath),
4764
+ }));
3055
4765
  for (const j of imageJobs) {
3056
4766
  if (!existsSync(j.path)) {
3057
4767
  console.error(`AOT: <Image> asset "${j.name}" not found at ${j.path}`);
3058
4768
  process.exit(1);
3059
4769
  }
3060
4770
  }
3061
- const baked = bakeAssets({ images: imageJobs, fonts: [], outDir: distDir });
4771
+ const baked = bakeAssets({images: imageJobs, fonts: [], outDir: distDir});
3062
4772
  console.log(
3063
4773
  `AOT: compiled demo "${demo}" -> dist/app.gen.c (${result.nodes} nodes, ${result.state} state, ` +
3064
4774
  `${result.handlers} handler(s), ${result.updates} dynamic) + ${baked.images} image(s) -> dist/assets.generated.c`,