embedded-react 0.2.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/aot/compile.mjs +2407 -697
- package/aot/screenshot-smoke.mjs +34 -17
- package/aot/style-map.mjs +156 -80
- package/assets/bake-font.mjs +45 -21
- package/assets/bake-image.mjs +7 -5
- package/assets/bake-svg.mjs +563 -0
- package/assets/build-builtin-font.mjs +25 -12
- package/assets/emit-c.mjs +52 -20
- package/assets/emit-container.mjs +5 -3
- package/assets/emit-pack.mjs +8 -2
- package/assets/index.mjs +25 -16
- package/assets/rasterize.mjs +45 -11
- package/assets/svg-loader.mjs +81 -0
- package/build.mjs +43 -20
- package/cli.mjs +258 -20
- package/pack-container.mjs +84 -35
- package/package.json +9 -3
- package/persist-transform.mjs +23 -9
- package/qjsc-wasm.mjs +83 -0
- package/sim/embedded-react.cjs +2 -0
- package/sim/embedded-react.js +1 -1
- package/sim/embedded-react.wasm +0 -0
- package/sim-server.mjs +160 -48
- package/src/embedded-react/Animated.js +51 -36
- package/src/embedded-react/AppRegistry.js +4 -4
- package/src/embedded-react/Easing.js +1 -1
- package/src/embedded-react/LayoutAnimation.js +13 -6
- package/src/embedded-react/StyleSheet.js +1 -1
- package/src/embedded-react/imperative.js +19 -7
- package/src/embedded-react/index.js +8 -8
- package/src/embedded-react/layout-anim-config.js +13 -9
- package/src/embedded-react/split-style.js +6 -6
- package/src/embedded-react/svg-ops.js +369 -41
- package/src/embedded-react/usePersistentState.js +3 -3
- package/src/host-config.js +137 -18
- package/src/native-ui.js +3 -1
- package/src/props.js +22 -10
- package/src/renderer.js +3 -3
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 {
|
|
37
|
-
import {
|
|
38
|
-
import {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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(
|
|
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 (
|
|
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 {
|
|
108
|
-
const loc = {
|
|
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, {
|
|
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(
|
|
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 '+':
|
|
161
|
-
|
|
162
|
-
case '
|
|
163
|
-
|
|
164
|
-
case '
|
|
165
|
-
|
|
166
|
-
case '
|
|
167
|
-
|
|
168
|
-
case '
|
|
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 '===':
|
|
207
|
+
case '===':
|
|
208
|
+
return l === r;
|
|
171
209
|
case '!=':
|
|
172
|
-
case '!==':
|
|
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)
|
|
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(
|
|
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
|
|
190
|
-
|
|
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')
|
|
197
|
-
|
|
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(
|
|
254
|
+
return node.elements.map(e => (e ? evalStatic(e, scope) : null));
|
|
204
255
|
case 'CallExpression': {
|
|
205
256
|
const c = node.callee;
|
|
206
|
-
if (
|
|
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(
|
|
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)
|
|
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 {
|
|
287
|
+
return {code: cstr(node.value), cType: 'string'};
|
|
229
288
|
case 'BooleanLiteral':
|
|
230
|
-
return {
|
|
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')
|
|
236
|
-
|
|
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')
|
|
241
|
-
|
|
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(
|
|
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 (
|
|
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 =
|
|
256
|
-
|
|
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 =
|
|
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'))
|
|
262
|
-
|
|
263
|
-
|
|
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 =
|
|
269
|
-
|
|
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 {
|
|
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 =
|
|
279
|
-
|
|
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')
|
|
286
|
-
|
|
287
|
-
|
|
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 (
|
|
295
|
-
|
|
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 (
|
|
299
|
-
|
|
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 {
|
|
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 (
|
|
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 {
|
|
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 (
|
|
310
|
-
|
|
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 (
|
|
314
|
-
|
|
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)
|
|
317
|
-
|
|
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')
|
|
320
|
-
|
|
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(
|
|
328
|
-
const UNARY = {
|
|
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'
|
|
475
|
+
return fn === 'round' || fn === 'floor' || fn === 'ceil'
|
|
476
|
+
? {code: `((int)${inner})`, cType: 'int'}
|
|
477
|
+
: {code: inner, cType: 'float'};
|
|
333
478
|
}
|
|
334
|
-
const BINARY = {
|
|
335
|
-
|
|
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(
|
|
492
|
+
throw new Error(
|
|
493
|
+
'AOT: unsupported call expression in a dynamic expression',
|
|
494
|
+
);
|
|
339
495
|
}
|
|
340
496
|
}
|
|
341
|
-
throw new Error(
|
|
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 =
|
|
346
|
-
|
|
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 =
|
|
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')
|
|
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(
|
|
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 = {
|
|
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 (
|
|
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(
|
|
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(
|
|
410
|
-
|
|
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 {
|
|
413
|
-
if (typeof v === 'number')
|
|
414
|
-
|
|
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 {
|
|
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 (
|
|
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 = {
|
|
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 (
|
|
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 =
|
|
474
|
-
|
|
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 {
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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 (
|
|
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 (
|
|
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))
|
|
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')
|
|
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)
|
|
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 (
|
|
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')
|
|
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 (
|
|
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 (
|
|
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')
|
|
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 (
|
|
637
|
-
|
|
638
|
-
|
|
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(
|
|
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 (
|
|
667
|
-
|
|
668
|
-
|
|
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 (
|
|
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 ||
|
|
693
|
-
refs.set(decl.id.name, {
|
|
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')
|
|
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, {
|
|
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')
|
|
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 = {
|
|
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 = {
|
|
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 = {
|
|
750
|
-
|
|
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 (
|
|
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
|
|
757
|
-
|
|
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 {
|
|
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 ? {
|
|
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 {
|
|
1176
|
+
const {ease, bezier} = easingInfo(easingNode, env);
|
|
772
1177
|
lines.push(` ${c}.easing = ${ease};`);
|
|
773
1178
|
if (bezier) {
|
|
774
|
-
lines.push(
|
|
775
|
-
|
|
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')
|
|
783
|
-
|
|
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')
|
|
786
|
-
|
|
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)
|
|
791
|
-
|
|
792
|
-
|
|
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'
|
|
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 {
|
|
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 {
|
|
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
|
|
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(
|
|
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)
|
|
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(
|
|
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)
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
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 =
|
|
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 =
|
|
876
|
-
if (
|
|
877
|
-
|
|
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 =
|
|
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')
|
|
889
|
-
|
|
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])
|
|
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])
|
|
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])
|
|
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])
|
|
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({
|
|
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))
|
|
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)))
|
|
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)
|
|
944
|
-
|
|
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 =
|
|
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)
|
|
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(
|
|
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(
|
|
1479
|
+
throw new Error(
|
|
1480
|
+
'AOT: nested <Text> / element children inside <Text> not yet supported (spans)',
|
|
1481
|
+
);
|
|
985
1482
|
}
|
|
986
1483
|
}
|
|
987
|
-
return {
|
|
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 (
|
|
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')
|
|
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 (
|
|
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 =
|
|
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(
|
|
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)
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
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
|
|
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(
|
|
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 {
|
|
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)
|
|
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')
|
|
1079
|
-
|
|
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')
|
|
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 (
|
|
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 (
|
|
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(
|
|
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
|
|
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 {
|
|
1672
|
+
const {cVar, kind, get, delayMs, loop} = entry;
|
|
1112
1673
|
const c = `cfg${idx}`;
|
|
1113
|
-
const lines = [
|
|
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(
|
|
1117
|
-
|
|
1118
|
-
|
|
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(
|
|
1122
|
-
|
|
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(
|
|
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')
|
|
1134
|
-
|
|
1135
|
-
|
|
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 (
|
|
1146
|
-
|
|
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))
|
|
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 =
|
|
1157
|
-
|
|
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 {
|
|
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 =
|
|
1165
|
-
|
|
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;
|
|
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)
|
|
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 =
|
|
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(
|
|
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 {
|
|
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)
|
|
1198
|
-
|
|
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')
|
|
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 (
|
|
1216
|
-
|
|
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(
|
|
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(
|
|
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))
|
|
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({
|
|
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({
|
|
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)
|
|
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 =
|
|
1263
|
-
|
|
1264
|
-
|
|
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({
|
|
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])
|
|
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 (
|
|
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 {
|
|
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) =>
|
|
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')
|
|
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 (
|
|
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')
|
|
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')
|
|
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')
|
|
1329
|
-
|
|
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)
|
|
1337
|
-
|
|
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 = [
|
|
1347
|
-
|
|
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')
|
|
1357
|
-
|
|
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 {
|
|
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(
|
|
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(
|
|
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)
|
|
1374
|
-
|
|
1375
|
-
|
|
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))
|
|
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({
|
|
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, {
|
|
2120
|
+
locals.set(p.name, {code: e.code, cType: e.cType});
|
|
1414
2121
|
}
|
|
1415
2122
|
});
|
|
1416
2123
|
const body = fn.body;
|
|
1417
|
-
const list =
|
|
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, {
|
|
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 (
|
|
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 (
|
|
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 (
|
|
1435
|
-
|
|
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)
|
|
1441
|
-
|
|
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)
|
|
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 (
|
|
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')
|
|
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 =
|
|
1457
|
-
|
|
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 (
|
|
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, {
|
|
1472
|
-
if (arg.body.type === 'BlockStatement')
|
|
1473
|
-
|
|
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')
|
|
1490
|
-
|
|
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 (
|
|
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(
|
|
1496
|
-
|
|
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(
|
|
1501
|
-
|
|
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(
|
|
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(
|
|
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 =
|
|
1543
|
-
|
|
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 = {
|
|
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')
|
|
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(
|
|
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(
|
|
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 = {
|
|
1562
|
-
out.effectFns.push({
|
|
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) =>
|
|
1565
|
-
|
|
1566
|
-
|
|
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')
|
|
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(
|
|
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 =
|
|
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 =
|
|
1584
|
-
|
|
1585
|
-
const
|
|
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(
|
|
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')
|
|
1612
|
-
|
|
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] = {
|
|
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] = {
|
|
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] = {
|
|
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] = {
|
|
2474
|
+
props[attr.name.name] = {static: true, value: evalStatic(node, scope)};
|
|
1633
2475
|
} catch {
|
|
1634
|
-
props[attr.name.name] = {
|
|
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)
|
|
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, {
|
|
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')
|
|
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 =
|
|
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')
|
|
1659
|
-
|
|
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')
|
|
1672
|
-
|
|
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(
|
|
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)
|
|
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 = {
|
|
2556
|
+
childrenRef = {kind: 'props', name: param.name};
|
|
1689
2557
|
}
|
|
1690
2558
|
|
|
1691
|
-
const childScope = {
|
|
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(
|
|
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)
|
|
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
|
|
2576
|
+
else
|
|
2577
|
+
childLocals.set(name, {code: d.code, cType: d.cType, struct: d.struct});
|
|
1701
2578
|
}
|
|
1702
|
-
const children = childNodes.length
|
|
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, {
|
|
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(
|
|
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')
|
|
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(
|
|
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))
|
|
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))
|
|
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 = {
|
|
2666
|
+
const iterScope = {...scope};
|
|
1776
2667
|
if (itemName) iterScope[itemName] = item;
|
|
1777
2668
|
if (idxName) iterScope[idxName] = i;
|
|
1778
|
-
emitElementInto(
|
|
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))
|
|
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 = {
|
|
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)
|
|
1798
|
-
|
|
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(
|
|
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)
|
|
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, {
|
|
2745
|
+
emitElementInto(expr.right, parentVar, scope, out, env, state, {
|
|
2746
|
+
displayCode: code,
|
|
2747
|
+
});
|
|
1825
2748
|
}
|
|
1826
|
-
} else if (
|
|
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(
|
|
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')
|
|
1835
|
-
|
|
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 (
|
|
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')
|
|
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
|
-
|
|
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(
|
|
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
|
|
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')
|
|
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')
|
|
1882
|
-
|
|
1883
|
-
else
|
|
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 (
|
|
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 {
|
|
2859
|
+
return {type, props};
|
|
1892
2860
|
}
|
|
1893
2861
|
|
|
1894
|
-
/** Emits one ERVectorPaint initializer from a
|
|
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 =
|
|
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) =>
|
|
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')
|
|
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] = {
|
|
2916
|
+
out[name] = {
|
|
2917
|
+
dyn: emitExpr(vn.expression, env).code,
|
|
2918
|
+
node: vn.expression,
|
|
2919
|
+
};
|
|
1926
2920
|
}
|
|
1927
|
-
} else
|
|
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 = {
|
|
1933
|
-
const JOIN_MAP = {
|
|
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 = [
|
|
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 [
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
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 [
|
|
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) => [
|
|
1979
|
-
|
|
1980
|
-
|
|
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 =
|
|
1984
|
-
|
|
1985
|
-
const
|
|
1986
|
-
const
|
|
1987
|
-
|
|
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))
|
|
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 = {
|
|
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 =
|
|
3071
|
+
const walk = node => {
|
|
1998
3072
|
if (node.type !== 'JSXElement') return;
|
|
1999
3073
|
for (const attr of node.openingElement.attributes) {
|
|
2000
|
-
if (
|
|
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 {
|
|
2017
|
-
out.build.push(
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
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)
|
|
2029
|
-
|
|
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 {
|
|
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 /
|
|
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
|
-
|
|
2041
|
-
out.
|
|
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)
|
|
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)
|
|
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(
|
|
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)
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
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(
|
|
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({
|
|
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 =
|
|
2099
|
-
|
|
2100
|
-
|
|
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(
|
|
2106
|
-
|
|
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, {
|
|
2109
|
-
const ctx = {
|
|
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 =
|
|
2112
|
-
|
|
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 {
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
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')
|
|
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')
|
|
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)
|
|
2143
|
-
|
|
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
|
|
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({
|
|
3427
|
+
staticAssigns.push({
|
|
3428
|
+
field: 'switch_value',
|
|
3429
|
+
expr: evalStatic(valueNode, scope) ? '1' : '0',
|
|
3430
|
+
});
|
|
2153
3431
|
} catch {
|
|
2154
|
-
dynAssigns.push({
|
|
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({
|
|
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)
|
|
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))
|
|
2172
|
-
|
|
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({
|
|
2176
|
-
|
|
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 {
|
|
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')
|
|
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')
|
|
2204
|
-
|
|
2205
|
-
else if (name === '
|
|
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({
|
|
3517
|
+
staticAssigns.push({
|
|
3518
|
+
field: 'editable',
|
|
3519
|
+
expr: evalStatic(node, scope) ? '1' : '0',
|
|
3520
|
+
});
|
|
2209
3521
|
} catch {
|
|
2210
|
-
dynAssigns.push({
|
|
3522
|
+
dynAssigns.push({
|
|
3523
|
+
field: 'editable',
|
|
3524
|
+
code: `(uint8_t)((${emitExpr(node, env).code}) ? 1 : 0)`,
|
|
3525
|
+
});
|
|
2211
3526
|
}
|
|
2212
|
-
} else if (
|
|
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
|
|
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 = {
|
|
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 = {
|
|
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({
|
|
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)
|
|
2239
|
-
|
|
2240
|
-
if (
|
|
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))
|
|
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({
|
|
2248
|
-
|
|
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 {
|
|
2262
|
-
|
|
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')
|
|
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({
|
|
3646
|
+
staticAssigns.push({
|
|
3647
|
+
field: 'indicator_color',
|
|
3648
|
+
expr: colorLiteral(String(evalStatic(node, scope))),
|
|
3649
|
+
});
|
|
2272
3650
|
} catch {
|
|
2273
|
-
dynAssigns.push({
|
|
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({
|
|
3661
|
+
staticAssigns.push({
|
|
3662
|
+
field: 'animating',
|
|
3663
|
+
expr: evalStatic(node, scope) ? '1' : '0',
|
|
3664
|
+
});
|
|
2281
3665
|
} catch {
|
|
2282
|
-
dynAssigns.push({
|
|
3666
|
+
dynAssigns.push({
|
|
3667
|
+
field: 'animating',
|
|
3668
|
+
code: `(uint8_t)((${emitExpr(node, env).code}) ? 1 : 0)`,
|
|
3669
|
+
});
|
|
2283
3670
|
}
|
|
2284
|
-
} else
|
|
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'))
|
|
2287
|
-
|
|
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({
|
|
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)
|
|
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 {
|
|
2313
|
-
|
|
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)
|
|
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')
|
|
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')
|
|
2335
|
-
|
|
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
|
|
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)
|
|
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({
|
|
3760
|
+
staticAssigns.push({
|
|
3761
|
+
field: 'modal_visible',
|
|
3762
|
+
expr: evalStatic(visibleNode, scope) ? '1' : '0',
|
|
3763
|
+
});
|
|
2342
3764
|
} catch {
|
|
2343
|
-
dynAssigns.push({
|
|
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({
|
|
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)
|
|
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')
|
|
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
|
|
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)
|
|
2383
|
-
|
|
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')
|
|
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')
|
|
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
|
|
3838
|
+
else
|
|
3839
|
+
throw aotError(
|
|
3840
|
+
`AOT: FlatList renderItem cannot destructure "${prop.key.name}" (only item / index)`,
|
|
3841
|
+
);
|
|
2393
3842
|
}
|
|
2394
|
-
if (!itemName)
|
|
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 = [{
|
|
2398
|
-
if (indexName) cbParams.push({
|
|
2399
|
-
const cb = {
|
|
2400
|
-
|
|
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: {
|
|
2404
|
-
|
|
2405
|
-
|
|
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(
|
|
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 =
|
|
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
|
|
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(
|
|
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')
|
|
3975
|
+
if (typeof tc === 'string' || typeof tc === 'number')
|
|
3976
|
+
tintColor = argbLiteral(tc);
|
|
2488
3977
|
}
|
|
2489
|
-
return {
|
|
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')
|
|
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))
|
|
2510
|
-
|
|
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(
|
|
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 {
|
|
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 =
|
|
2531
|
-
|
|
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)
|
|
2537
|
-
|
|
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)
|
|
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 =
|
|
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({
|
|
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)
|
|
2555
|
-
|
|
2556
|
-
if (
|
|
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)
|
|
2565
|
-
|
|
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({
|
|
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({
|
|
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({
|
|
4147
|
+
out.handlers.push({
|
|
4148
|
+
name: handlerName,
|
|
4149
|
+
body: compileHandler(fn, env, state, out),
|
|
4150
|
+
});
|
|
2613
4151
|
} else {
|
|
2614
|
-
throw aotError(
|
|
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')
|
|
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(
|
|
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 (
|
|
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(
|
|
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')
|
|
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))
|
|
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)
|
|
2695
|
-
|
|
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(
|
|
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(
|
|
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 =
|
|
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, {
|
|
2728
|
-
|
|
2729
|
-
const screen = opts.screen ?? {
|
|
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
|
|
2734
|
-
const
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
if
|
|
2741
|
-
|
|
2742
|
-
|
|
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[
|
|
4350
|
+
scope[name] = evalStatic(expr, scope);
|
|
2745
4351
|
} catch {
|
|
2746
|
-
|
|
4352
|
+
const e = emitExpr(expr, env);
|
|
4353
|
+
env.locals.set(name, {code: `(${e.code})`, cType: e.cType});
|
|
2747
4354
|
}
|
|
2748
4355
|
}
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
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
|
|
2754
|
-
|
|
2755
|
-
const
|
|
2756
|
-
const
|
|
2757
|
-
const
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
}
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
const
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
//
|
|
2793
|
-
const
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
.
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
const
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
const
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
//
|
|
2858
|
-
|
|
2859
|
-
const
|
|
2860
|
-
const
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
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
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
const
|
|
2918
|
-
|
|
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
|
-
|
|
2950
|
-
|
|
2951
|
-
const mountEffectsBlock = out.mountEffects.length
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
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]) => ({
|
|
3010
|
-
|
|
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 (
|
|
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)
|
|
3035
|
-
|
|
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
|
-
|
|
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, {
|
|
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(
|
|
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({
|
|
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`,
|