embedded-react 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/NOTICE +58 -0
- package/README.md +224 -0
- package/aot/compile.mjs +3066 -0
- package/aot/screenshot-smoke.mjs +110 -0
- package/aot/style-map.mjs +248 -0
- package/assets/bake-font.mjs +190 -0
- package/assets/bake-image.mjs +50 -0
- package/assets/build-builtin-font.mjs +51 -0
- package/assets/emit-c.mjs +187 -0
- package/assets/emit-container.mjs +121 -0
- package/assets/emit-pack.mjs +128 -0
- package/assets/index.mjs +72 -0
- package/assets/rasterize.mjs +169 -0
- package/build.mjs +136 -0
- package/pack-container.mjs +161 -0
- package/package.json +79 -0
- package/persist-transform.mjs +106 -0
- package/src/embedded-react/Animated.js +352 -0
- package/src/embedded-react/AppRegistry.js +49 -0
- package/src/embedded-react/Easing.js +39 -0
- package/src/embedded-react/LayoutAnimation.js +45 -0
- package/src/embedded-react/Platform.js +26 -0
- package/src/embedded-react/StyleSheet.js +36 -0
- package/src/embedded-react/components.js +44 -0
- package/src/embedded-react/imperative.js +68 -0
- package/src/embedded-react/index.js +52 -0
- package/src/embedded-react/layout-anim-config.js +91 -0
- package/src/embedded-react/split-style.js +58 -0
- package/src/embedded-react/svg-ops.js +564 -0
- package/src/embedded-react/usePersistentState.js +69 -0
- package/src/host-config.js +196 -0
- package/src/native-ui.js +24 -0
- package/src/props.js +183 -0
- package/src/renderer.js +57 -0
package/aot/compile.mjs
ADDED
|
@@ -0,0 +1,3066 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Cory Lamming
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// `npm run aot [demo]` — the Flow B ahead-of-time compiler (vertical slice).
|
|
18
|
+
//
|
|
19
|
+
// Compiles a demo's JSX straight to C against er_scene.h: no QuickJS, no JS at runtime. The generated
|
|
20
|
+
// app.gen.c builds the engine node tree directly and wires state + events, so it fits an MCU with only
|
|
21
|
+
// internal RAM.
|
|
22
|
+
//
|
|
23
|
+
// Supported subset (grows demo by demo; unsupported syntax throws "AOT: ..."):
|
|
24
|
+
// - View / Text / Pressable / Image / ScrollView elements
|
|
25
|
+
// - StyleSheet + inline styles → ERProps (static values)
|
|
26
|
+
// - text with literal + {interpolation} segments (interpolations may reference state)
|
|
27
|
+
// - useState(initial) → C state; on* handlers (onPress/onPressIn/onPressOut/onLongPress) → C functions
|
|
28
|
+
// - setState(value) and setState(prev => expr); a small C expression subset (literals, identifiers,
|
|
29
|
+
// +-*/% , comparisons, ?:)
|
|
30
|
+
//
|
|
31
|
+
// The compiler tracks which nodes depend on which state, so a state change re-sets ONLY the dependent
|
|
32
|
+
// nodes (er_node_set_props) — no diffing, no reconciler. See /PLAN.md Flow B.
|
|
33
|
+
//
|
|
34
|
+
// npm run aot # default demo (thermostat) — but use a minimal demo for the slice
|
|
35
|
+
// npm run aot -- music-player # a specific demo by folder name
|
|
36
|
+
import { parse } from '@babel/parser';
|
|
37
|
+
import { codeFrameColumns } from '@babel/code-frame';
|
|
38
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from 'node:fs';
|
|
39
|
+
import { resolve, dirname } from 'node:path';
|
|
40
|
+
import { fileURLToPath } from 'node:url';
|
|
41
|
+
import { lowerStyle, NODE_TYPES, DYN_FIELDS, colorLiteral } from './style-map.mjs';
|
|
42
|
+
import { flattenSvg, parseColor, parsePath } from '../src/embedded-react/svg-ops.js';
|
|
43
|
+
import { bakeAssets } from '../assets/index.mjs';
|
|
44
|
+
|
|
45
|
+
const here = dirname(fileURLToPath(import.meta.url)); // bridges/quickjs/js/aot
|
|
46
|
+
const repoRoot = resolve(here, '../../../..');
|
|
47
|
+
const demosDir = resolve(repoRoot, 'demos');
|
|
48
|
+
const distDir = resolve(here, '..', 'dist');
|
|
49
|
+
|
|
50
|
+
// The compiler's own version (kept in lockstep with the engine via tools/sync-version.mjs). Stamped into the
|
|
51
|
+
// generated app + asserted against the engine's er_version.h so a version mismatch fails at COMPILE time.
|
|
52
|
+
const PKG_VERSION = JSON.parse(readFileSync(resolve(here, '..', 'package.json'), 'utf8')).version;
|
|
53
|
+
const [PKG_MAJOR, PKG_MINOR] = PKG_VERSION.split('.');
|
|
54
|
+
|
|
55
|
+
// The core compiler is exported as compileSource(src) so it can be unit-tested on inline JSX. The CLI
|
|
56
|
+
// (read a demo's App.jsx, write dist/app.gen.{c,h}) lives in the entry guard at the bottom of this file.
|
|
57
|
+
//
|
|
58
|
+
// ---------------------------------------------------------------------------------------------------
|
|
59
|
+
// SECTION MAP (top → bottom). The pipeline: parse the App.jsx → collect the module's component, state,
|
|
60
|
+
// hooks & refs → emit each piece to C (expressions, styles/text, handlers, nodes) → assemble app.gen.c.
|
|
61
|
+
//
|
|
62
|
+
// 1. Diagnostics aotError / withLoc / formatAotError — locate + hint unsupported syntax
|
|
63
|
+
// 2. Static evaluation evalStatic — fold the compile-time-constant subset (styles, initials)
|
|
64
|
+
// 3. C expression emission emitExpr — lower a JS expression (state/props/refs) to a C expression
|
|
65
|
+
// 4. Collection passes moduleScope + collect{State,Components,Callbacks,Memos,Effects}
|
|
66
|
+
// 5. Animations & refs collect{Anims,Refs}, useAnimatedValue, Easing, interpolate (native driver)
|
|
67
|
+
// 6. JSX → style/text/events attrExpr, collectStyleAssigns, buildText / text spans
|
|
68
|
+
// 7. Handler compilation on* arrow → C statements (setters, refs, updateVector, Animated.start)
|
|
69
|
+
// 8. Emit: control flow components / conditionals / .map — all UNROLL at compile time
|
|
70
|
+
// 9. Vector / Svg <Svg> subtree → flattenSvg ops/paints → er_node_set_vector_ops
|
|
71
|
+
// 10. Node emitters typed components (Switch/TextInput/Modal/…) + the generic host node
|
|
72
|
+
// 11. Keyboard config setKeyboardConfig({...}) → static ERKeyboardConfig tables
|
|
73
|
+
// 12. Compile orchestration compileSourceImpl — stitch the above into app.gen.{c,h}
|
|
74
|
+
// 13. CLI entry node aot/compile.mjs [demo] → read App.jsx, write dist/app.gen.*
|
|
75
|
+
// ---------------------------------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------------------------------
|
|
78
|
+
// Diagnostics — turn "AOT: <reason>" into "<reason> at file:line:col" + a source code-frame (+ a rewrite
|
|
79
|
+
// hint when one is attached). emitExpr / emitNode / compileHandlerExpr are wrapped (withLoc) so the
|
|
80
|
+
// DEEPEST node that failed pins the location; compileSource formats it at the top.
|
|
81
|
+
// ---------------------------------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
/** Throws an AOT error carrying an optional `hint` (a "rewrite it like this" suggestion shown to the user). */
|
|
84
|
+
function aotError(message, hint) {
|
|
85
|
+
const e = new Error(message.startsWith('AOT:') ? message : `AOT: ${message}`);
|
|
86
|
+
if (hint) e.aotHint = hint;
|
|
87
|
+
return e;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Wraps an emit fn so a thrown AOT error (without a location yet) is tagged with the current node's loc. */
|
|
91
|
+
function withLoc(fn) {
|
|
92
|
+
return function (node, ...rest) {
|
|
93
|
+
try {
|
|
94
|
+
return fn(node, ...rest);
|
|
95
|
+
} catch (e) {
|
|
96
|
+
if (e && typeof e.message === 'string' && e.message.startsWith('AOT:') && !e.aotLoc && node && node.loc) {
|
|
97
|
+
e.aotLoc = node.loc.start; // babel loc: { line (1-based), column (0-based) }
|
|
98
|
+
}
|
|
99
|
+
throw e;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Re-throws an AOT error with `file:line:col`, a code-frame, and any hint folded into the message. */
|
|
105
|
+
function formatAotError(e, src, filename) {
|
|
106
|
+
if (!e || !e.aotLoc) return e; // nothing to locate — leave the bare message
|
|
107
|
+
const { line, column } = e.aotLoc;
|
|
108
|
+
const loc = { start: { line, column: column + 1 } }; // code-frame columns are 1-based
|
|
109
|
+
let frame = '';
|
|
110
|
+
try {
|
|
111
|
+
frame = codeFrameColumns(src, loc, { highlightCode: false });
|
|
112
|
+
} catch {
|
|
113
|
+
/* code-frame is best-effort */
|
|
114
|
+
}
|
|
115
|
+
const hint = e.aotHint ? `\n\nhint: ${e.aotHint}` : '';
|
|
116
|
+
const out = new Error(`${e.message}\n at ${filename}:${line}:${column + 1}\n\n${frame}${hint}`);
|
|
117
|
+
out.aotLoc = e.aotLoc;
|
|
118
|
+
if (e.aotHint) out.aotHint = e.aotHint;
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* evalStatic at a boundary that REQUIRES a compile-time constant. On a fold failure it rethrows as a
|
|
124
|
+
* LOCATED aotError with a clear message (+ optional hint), instead of letting evalStatic's bare
|
|
125
|
+
* control-flow error ("cannot statically resolve identifier …") leak to the user without a location.
|
|
126
|
+
*/
|
|
127
|
+
function evalStaticOrThrow(node, scope, message, hint) {
|
|
128
|
+
try {
|
|
129
|
+
return evalStatic(node, scope);
|
|
130
|
+
} catch {
|
|
131
|
+
const e = aotError(message, hint);
|
|
132
|
+
if (node && node.loc) e.aotLoc = node.loc.start;
|
|
133
|
+
throw e;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ---------------------------------------------------------------------------------------------------
|
|
138
|
+
// Static expression evaluation — folds the constant subset (used for styles + state initials). Throws
|
|
139
|
+
// on anything dynamic (e.g., a state reference), which the caller catches to fall back to C emission.
|
|
140
|
+
// ---------------------------------------------------------------------------------------------------
|
|
141
|
+
function evalStatic(node, scope) {
|
|
142
|
+
switch (node.type) {
|
|
143
|
+
case 'NumericLiteral':
|
|
144
|
+
case 'StringLiteral':
|
|
145
|
+
case 'BooleanLiteral':
|
|
146
|
+
return node.value;
|
|
147
|
+
case 'NullLiteral':
|
|
148
|
+
return null;
|
|
149
|
+
case 'UnaryExpression': {
|
|
150
|
+
const a = evalStatic(node.argument, scope);
|
|
151
|
+
if (node.operator === '-') return -a;
|
|
152
|
+
if (node.operator === '+') return +a;
|
|
153
|
+
if (node.operator === '!') return !a;
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
case 'BinaryExpression': {
|
|
157
|
+
const l = evalStatic(node.left, scope);
|
|
158
|
+
const r = evalStatic(node.right, scope);
|
|
159
|
+
switch (node.operator) {
|
|
160
|
+
case '+': return l + r;
|
|
161
|
+
case '-': return l - r;
|
|
162
|
+
case '*': return l * r;
|
|
163
|
+
case '/': return l / r;
|
|
164
|
+
case '%': return l % r;
|
|
165
|
+
case '<': return l < r;
|
|
166
|
+
case '>': return l > r;
|
|
167
|
+
case '<=': return l <= r;
|
|
168
|
+
case '>=': return l >= r;
|
|
169
|
+
case '==':
|
|
170
|
+
case '===': return l === r;
|
|
171
|
+
case '!=':
|
|
172
|
+
case '!==': return l !== r;
|
|
173
|
+
}
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
case 'LogicalExpression': {
|
|
177
|
+
const l = evalStatic(node.left, scope);
|
|
178
|
+
if (node.operator === '&&') return l ? evalStatic(node.right, scope) : l;
|
|
179
|
+
if (node.operator === '||') return l ? l : evalStatic(node.right, scope);
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
case 'ConditionalExpression':
|
|
183
|
+
return evalStatic(node.test, scope) ? evalStatic(node.consequent, scope) : evalStatic(node.alternate, scope);
|
|
184
|
+
case 'Identifier':
|
|
185
|
+
if (node.name in scope) return scope[node.name];
|
|
186
|
+
throw new Error(`AOT: cannot statically resolve identifier "${node.name}"`);
|
|
187
|
+
case 'MemberExpression': {
|
|
188
|
+
const obj = evalStatic(node.object, scope);
|
|
189
|
+
const key = node.computed ? evalStatic(node.property, scope) : node.property.name;
|
|
190
|
+
if (obj == null) throw new Error(`AOT: member access on null/undefined ("${key}")`);
|
|
191
|
+
return obj[key];
|
|
192
|
+
}
|
|
193
|
+
case 'ObjectExpression': {
|
|
194
|
+
const o = {};
|
|
195
|
+
for (const prop of node.properties) {
|
|
196
|
+
if (prop.type !== 'ObjectProperty') throw new Error('AOT: object spreads/methods not supported in static objects');
|
|
197
|
+
const k = prop.computed ? evalStatic(prop.key, scope) : prop.key.name ?? prop.key.value;
|
|
198
|
+
o[k] = evalStatic(prop.value, scope);
|
|
199
|
+
}
|
|
200
|
+
return o;
|
|
201
|
+
}
|
|
202
|
+
case 'ArrayExpression':
|
|
203
|
+
return node.elements.map((e) => (e ? evalStatic(e, scope) : null));
|
|
204
|
+
case 'CallExpression': {
|
|
205
|
+
const c = node.callee;
|
|
206
|
+
if (c.type === 'MemberExpression' && c.object.name === 'StyleSheet' && c.property.name === 'create') {
|
|
207
|
+
return evalStatic(node.arguments[0], scope);
|
|
208
|
+
}
|
|
209
|
+
throw new Error(`AOT: cannot statically evaluate call expression`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
throw new Error(`AOT: unsupported expression "${node.type}" in static context`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ---------------------------------------------------------------------------------------------------
|
|
216
|
+
// C expression emission — lowers a JS expression to C, given the current state + local bindings. Each
|
|
217
|
+
// result carries a C type so callers pick the right printf specifier / assignment.
|
|
218
|
+
// env = { state: Map(name→record), locals: Map(name→{code,cType}), consts: scope object }
|
|
219
|
+
// ---------------------------------------------------------------------------------------------------
|
|
220
|
+
const ARITH = new Set(['+', '-', '*', '/', '%']);
|
|
221
|
+
const COMPARE = new Set(['<', '>', '<=', '>=', '==', '!=', '===', '!==']);
|
|
222
|
+
|
|
223
|
+
function emitExprImpl(node, env) {
|
|
224
|
+
switch (node.type) {
|
|
225
|
+
case 'NumericLiteral':
|
|
226
|
+
return Number.isInteger(node.value) ? { code: String(node.value), cType: 'int' } : { code: `${node.value}f`, cType: 'float' };
|
|
227
|
+
case 'StringLiteral':
|
|
228
|
+
return { code: cstr(node.value), cType: 'string' };
|
|
229
|
+
case 'BooleanLiteral':
|
|
230
|
+
return { code: node.value ? '1' : '0', cType: 'int' };
|
|
231
|
+
case 'Identifier': {
|
|
232
|
+
if (env.locals.has(node.name)) return env.locals.get(node.name);
|
|
233
|
+
if (env.state.has(node.name)) {
|
|
234
|
+
const s = env.state.get(node.name);
|
|
235
|
+
if (s.kind === 'list') throw new Error(`AOT: a list state ("${node.name}") can only be used via .length or .map`);
|
|
236
|
+
return { code: s.cMember, cType: s.cType };
|
|
237
|
+
}
|
|
238
|
+
if (node.name in env.consts) {
|
|
239
|
+
const v = env.consts[node.name];
|
|
240
|
+
if (typeof v === 'number') return Number.isInteger(v) ? { code: String(v), cType: 'int' } : { code: `${v}f`, cType: 'float' };
|
|
241
|
+
if (typeof v === 'string') return { code: cstr(v), cType: 'string' };
|
|
242
|
+
}
|
|
243
|
+
throw new Error(`AOT: cannot resolve identifier "${node.name}" in a dynamic expression`);
|
|
244
|
+
}
|
|
245
|
+
case 'UnaryExpression': {
|
|
246
|
+
const a = emitExpr(node.argument, env);
|
|
247
|
+
// Parenthesize the operand so `-` on a negative literal emits `(-(-135))`, not `(--135)` (a decrement).
|
|
248
|
+
if (node.operator === '-' || node.operator === '+' || node.operator === '!') return { code: `(${node.operator}(${a.code}))`, cType: node.operator === '!' ? 'int' : a.cType };
|
|
249
|
+
throw new Error(`AOT: unsupported unary operator "${node.operator}"`);
|
|
250
|
+
}
|
|
251
|
+
case 'BinaryExpression': {
|
|
252
|
+
const l = emitExpr(node.left, env);
|
|
253
|
+
const r = emitExpr(node.right, env);
|
|
254
|
+
if (ARITH.has(node.operator)) {
|
|
255
|
+
const cType = l.cType === 'float' || r.cType === 'float' ? 'float' : 'int';
|
|
256
|
+
return { code: `(${l.code} ${node.operator} ${r.code})`, cType };
|
|
257
|
+
}
|
|
258
|
+
if (COMPARE.has(node.operator)) {
|
|
259
|
+
const eqOp = node.operator === '===' || node.operator === '==' ? '==' : node.operator === '!==' || node.operator === '!=' ? '!=' : null;
|
|
260
|
+
// String (in)equality → strcmp; the generated C already includes <string.h>.
|
|
261
|
+
if (eqOp && (l.cType === 'string' || r.cType === 'string')) return { code: `(strcmp(${l.code}, ${r.code}) ${eqOp} 0)`, cType: 'int' };
|
|
262
|
+
const op = node.operator === '===' ? '==' : node.operator === '!==' ? '!=' : node.operator;
|
|
263
|
+
return { code: `(${l.code} ${op} ${r.code})`, cType: 'int' };
|
|
264
|
+
}
|
|
265
|
+
throw new Error(`AOT: unsupported binary operator "${node.operator}"`);
|
|
266
|
+
}
|
|
267
|
+
case 'LogicalExpression': {
|
|
268
|
+
const op = node.operator === '&&' || node.operator === '||' ? node.operator : null;
|
|
269
|
+
if (!op) throw new Error(`AOT: unsupported logical operator "${node.operator}"`);
|
|
270
|
+
const l = emitExpr(node.left, env);
|
|
271
|
+
const r = emitExpr(node.right, env);
|
|
272
|
+
return { code: `(${l.code} ${op} ${r.code})`, cType: 'int' };
|
|
273
|
+
}
|
|
274
|
+
case 'ConditionalExpression': {
|
|
275
|
+
const t = emitExpr(node.test, env);
|
|
276
|
+
const c = emitExpr(node.consequent, env);
|
|
277
|
+
const a = emitExpr(node.alternate, env);
|
|
278
|
+
const cType = c.cType === 'float' || a.cType === 'float' ? 'float' : c.cType === a.cType ? c.cType : 'int';
|
|
279
|
+
return { code: `(${t.code} ? ${c.code} : ${a.code})`, cType };
|
|
280
|
+
}
|
|
281
|
+
case 'MemberExpression': {
|
|
282
|
+
// Static fold: member access that resolves to a compile-time constant (e.g. a .map item's `.key`).
|
|
283
|
+
try {
|
|
284
|
+
const v = evalStatic(node, env.consts ?? {});
|
|
285
|
+
if (typeof v === 'number') return Number.isInteger(v) ? { code: String(v), cType: 'int' } : { code: `${v}f`, cType: 'float' };
|
|
286
|
+
if (typeof v === 'string') return { code: cstr(v), cType: 'string' };
|
|
287
|
+
if (typeof v === 'boolean') return { code: v ? '1' : '0', cType: 'int' };
|
|
288
|
+
} catch {
|
|
289
|
+
/* not static — fall through to the dynamic member forms below */
|
|
290
|
+
}
|
|
291
|
+
const obj = node.object;
|
|
292
|
+
const prop = node.computed ? null : node.property.name;
|
|
293
|
+
// `<list>.length` → the runtime count.
|
|
294
|
+
if (obj.type === 'Identifier' && env.state.get(obj.name)?.kind === 'list' && prop === 'length') {
|
|
295
|
+
return { code: env.state.get(obj.name).countMember, cType: 'int' };
|
|
296
|
+
}
|
|
297
|
+
// `<item>.field` where item is a struct local (a list row's bound element).
|
|
298
|
+
if (obj.type === 'Identifier' && env.locals.get(obj.name)?.struct && prop) {
|
|
299
|
+
const f = env.locals.get(obj.name).struct.fields.find((x) => x.key === prop);
|
|
300
|
+
if (!f) throw new Error(`AOT: unknown field "${prop}" on a list item`);
|
|
301
|
+
return { code: `${env.locals.get(obj.name).code}.${f.key}`, cType: f.kind === 'string' ? 'string' : f.kind };
|
|
302
|
+
}
|
|
303
|
+
// `<ref>.current` — a value ref's mutable C slot.
|
|
304
|
+
if (obj.type === 'Identifier' && env.refs?.has(obj.name) && prop === 'current') {
|
|
305
|
+
const r = env.refs.get(obj.name);
|
|
306
|
+
return { code: r.cVar, cType: r.cType };
|
|
307
|
+
}
|
|
308
|
+
// `<event>.x / .y / .dx / .dy` — touch fields of the handler's EREventData.
|
|
309
|
+
if (obj.type === 'Identifier' && env.event === obj.name && (prop === 'x' || prop === 'y' || prop === 'dx' || prop === 'dy')) {
|
|
310
|
+
return { code: `data->${prop}`, cType: 'int' };
|
|
311
|
+
}
|
|
312
|
+
// `<event>.layout.x / .y / .width / .height` — the onLayout rect (EREventData.layout_rect; ERRect uses w/h).
|
|
313
|
+
if (obj.type === 'MemberExpression' && !obj.computed && obj.object.type === 'Identifier' && env.event === obj.object.name && obj.property.name === 'layout') {
|
|
314
|
+
const RECT = { x: 'x', y: 'y', width: 'w', height: 'h' };
|
|
315
|
+
const f = RECT[prop];
|
|
316
|
+
if (!f) throw new Error(`AOT: unknown onLayout rect field "${prop}" (use x / y / width / height)`);
|
|
317
|
+
return { code: `data->layout_rect.${f}`, cType: 'int' };
|
|
318
|
+
}
|
|
319
|
+
if (obj.type === 'Identifier' && obj.name === 'Math' && prop === 'PI') return { code: '(float)M_PI', cType: 'float' };
|
|
320
|
+
throw aotError('AOT: unsupported member expression in a dynamic context', 'in a handler or dynamic expression you can read state, `ref.current`, a `.map` item field, event fields (e.x / e.y / e.dx / e.dy / e.layout.*), and Math.PI — other member access must be a compile-time constant.');
|
|
321
|
+
}
|
|
322
|
+
case 'CallExpression': {
|
|
323
|
+
// Math.* helpers → libm (the generated C includes <math.h> when these appear).
|
|
324
|
+
const c = node.callee;
|
|
325
|
+
if (c.type === 'MemberExpression' && c.object.name === 'Math') {
|
|
326
|
+
const fn = c.property.name;
|
|
327
|
+
const a = node.arguments.map((x) => emitExpr(x, env));
|
|
328
|
+
const UNARY = { sin: 'sinf', cos: 'cosf', tan: 'tanf', sqrt: 'sqrtf', abs: 'fabsf', round: 'roundf', floor: 'floorf', ceil: 'ceilf' };
|
|
329
|
+
if (UNARY[fn] && a.length === 1) {
|
|
330
|
+
const inner = `${UNARY[fn]}((float)(${a[0].code}))`;
|
|
331
|
+
// round/floor/ceil yield a whole number — cast to int so %d / int assignments are correct.
|
|
332
|
+
return fn === 'round' || fn === 'floor' || fn === 'ceil' ? { code: `((int)${inner})`, cType: 'int' } : { code: inner, cType: 'float' };
|
|
333
|
+
}
|
|
334
|
+
const BINARY = { min: 'fminf', max: 'fmaxf', atan2: 'atan2f', pow: 'powf' };
|
|
335
|
+
if (BINARY[fn] && a.length === 2) return { code: `${BINARY[fn]}((float)(${a[0].code}), (float)(${a[1].code}))`, cType: 'float' };
|
|
336
|
+
throw new Error(`AOT: unsupported Math.${fn}(...) (arity ${a.length})`);
|
|
337
|
+
}
|
|
338
|
+
throw new Error('AOT: unsupported call expression in a dynamic expression');
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
throw new Error(`AOT: unsupported expression "${node.type}" in a dynamic context`);
|
|
342
|
+
}
|
|
343
|
+
const emitExpr = withLoc(emitExprImpl);
|
|
344
|
+
|
|
345
|
+
const printfSpec = (cType) => (cType === 'string' ? '%s' : cType === 'float' ? '%g' : '%d');
|
|
346
|
+
const cTypeOfValue = (v) => (typeof v === 'string' ? 'string' : typeof v === 'number' && !Number.isInteger(v) ? 'float' : 'int');
|
|
347
|
+
|
|
348
|
+
// ---------------------------------------------------------------------------------------------------
|
|
349
|
+
// AST helpers + collection passes — small predicates (isFn, fnReturnsJSX, …) and the up-front scans
|
|
350
|
+
// that walk the component body ONCE to gather what later emission needs: the module scope, useState,
|
|
351
|
+
// child components, and the useCallback / useMemo / useEffect / useRef hooks.
|
|
352
|
+
// ---------------------------------------------------------------------------------------------------
|
|
353
|
+
const isFn = (n) => n && (n.type === 'FunctionDeclaration' || n.type === 'FunctionExpression' || n.type === 'ArrowFunctionExpression');
|
|
354
|
+
|
|
355
|
+
function findComponent(program) {
|
|
356
|
+
for (const stmt of program.body) {
|
|
357
|
+
const d = stmt.type === 'ExportNamedDeclaration' ? stmt.declaration : stmt;
|
|
358
|
+
if (!d) continue;
|
|
359
|
+
if (d.type === 'FunctionDeclaration' && d.id?.name === 'App') return d;
|
|
360
|
+
if (d.type === 'VariableDeclaration') for (const decl of d.declarations) if (decl.id?.name === 'App' && isFn(decl.init)) return decl.init;
|
|
361
|
+
}
|
|
362
|
+
throw new Error('AOT: no `App` component found (expected `export function App() { ... }`)');
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Target screen size, baked at compile time so the demo's responsive `screen.width`/`screen.height`
|
|
366
|
+
// branching folds to the layout for THIS build (each board compiles its own binary). Override per target,
|
|
367
|
+
// e.g. ER_AOT_SCREEN_W=240 ER_AOT_SCREEN_H=320 for the CYD; defaults to a wide 800×480.
|
|
368
|
+
const SCREEN_W = Number(process.env.ER_AOT_SCREEN_W) || 800;
|
|
369
|
+
const SCREEN_H = Number(process.env.ER_AOT_SCREEN_H) || 480;
|
|
370
|
+
|
|
371
|
+
function moduleScope(program, screen, seed = {}) {
|
|
372
|
+
// `seed` pre-populates the scope (e.g. image imports as their asset-name strings) BEFORE module consts are
|
|
373
|
+
// folded, so a const that references one — `const DAYS = [{ icon: wxSun }]` — folds correctly.
|
|
374
|
+
const scope = { screen, ...seed };
|
|
375
|
+
for (const stmt of program.body) {
|
|
376
|
+
const d = stmt.type === 'ExportNamedDeclaration' ? stmt.declaration : stmt;
|
|
377
|
+
if (d?.type !== 'VariableDeclaration') continue;
|
|
378
|
+
for (const decl of d.declarations) {
|
|
379
|
+
if (!decl.id || decl.id.type !== 'Identifier' || !decl.init || isFn(decl.init)) continue;
|
|
380
|
+
try {
|
|
381
|
+
scope[decl.id.name] = evalStatic(decl.init, scope);
|
|
382
|
+
} catch {
|
|
383
|
+
/* not a static const — skip */
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return scope;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** Collects useState declarations → state descriptors keyed by both state name and setter name. */
|
|
391
|
+
/** Max characters stored per string field of a list-state item (fixed buffer — embedded-friendly). */
|
|
392
|
+
const LIST_STR_CAP = Number(process.env.ER_AOT_LIST_STR_CAP) || 48;
|
|
393
|
+
/** Max rows a list-state can hold (pre-allocated pool; rows beyond the count are display:none).
|
|
394
|
+
* Override with ER_AOT_LIST_CAP — lower it on a tight-RAM MCU (each pooled row costs engine nodes). */
|
|
395
|
+
const LIST_CAP = Number(process.env.ER_AOT_LIST_CAP) || 16;
|
|
396
|
+
/** Max inline segments in a nested-<Text>. Must match the engine's ER_TEXT_MAX_SPANS (default 4); if a
|
|
397
|
+
* project raises that #define, set ER_AOT_MAX_TEXT_SPANS to the same value when generating. */
|
|
398
|
+
const AOT_MAX_TEXT_SPANS = Number(process.env.ER_AOT_MAX_TEXT_SPANS) || 4;
|
|
399
|
+
|
|
400
|
+
/** Infers a C struct shape from a list-state's initial elements (objects of strings/numbers). */
|
|
401
|
+
function inferItemStruct(items, name) {
|
|
402
|
+
const shapeHint =
|
|
403
|
+
'a list state is a fixed-shape struct array: each element must be an OBJECT with the same string/number fields, ' +
|
|
404
|
+
'e.g. useState([{ title: "A", n: 1 }, { title: "B", n: 2 }]). The first element defines the columns.';
|
|
405
|
+
if (!Array.isArray(items) || !items.length)
|
|
406
|
+
throw aotError(`AOT: list state "${name}" needs ≥1 initial element to infer its item shape`, shapeHint);
|
|
407
|
+
const first = items[0];
|
|
408
|
+
if (typeof first !== 'object' || first === null || Array.isArray(first))
|
|
409
|
+
throw aotError(`AOT: list state "${name}" elements must be objects`, shapeHint);
|
|
410
|
+
const fields = Object.keys(first).map((key) => {
|
|
411
|
+
const v = first[key];
|
|
412
|
+
if (typeof v === 'string') return { key, kind: 'string' };
|
|
413
|
+
if (typeof v === 'number') return { key, kind: Number.isInteger(v) ? 'int' : 'float' };
|
|
414
|
+
throw aotError(`AOT: list state "${name}" field "${key}" must be a string or number`, shapeHint);
|
|
415
|
+
});
|
|
416
|
+
return { fields };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Collects a component's useState declarations → state descriptors keyed by JS name (reads) and setter
|
|
421
|
+
* name (writes). `prefix` namespaces the C STORAGE so each inlined child instance gets its own slots: the
|
|
422
|
+
* lookup keys stay the bare JS names (`count`), but the C field / array / count derive from `cField`
|
|
423
|
+
* (`<prefix>count`). prefix='' (the App) leaves storage names exactly as the JS names — backward compatible.
|
|
424
|
+
*/
|
|
425
|
+
function collectState(fnBody, scope, prefix = '') {
|
|
426
|
+
const byName = new Map();
|
|
427
|
+
const bySetter = new Map();
|
|
428
|
+
for (const stmt of fnBody.body) {
|
|
429
|
+
if (stmt.type !== 'VariableDeclaration') continue;
|
|
430
|
+
for (const decl of stmt.declarations) {
|
|
431
|
+
const init = decl.init;
|
|
432
|
+
if (init?.type !== 'CallExpression' || init.callee.name !== 'useState' || decl.id.type !== 'ArrayPattern') continue;
|
|
433
|
+
const name = decl.id.elements[0]?.name;
|
|
434
|
+
const setter = decl.id.elements[1]?.name;
|
|
435
|
+
if (!name) continue;
|
|
436
|
+
const cField = prefix + name; // C storage name (== name for the App; instance-unique for a child)
|
|
437
|
+
const initArg = init.arguments[0];
|
|
438
|
+
|
|
439
|
+
let rec;
|
|
440
|
+
if (initArg?.type === 'ArrayExpression') {
|
|
441
|
+
// List state → a fixed-capacity C struct array + a count (s_<name>[CAP], s_<name>_count).
|
|
442
|
+
const items = evalStaticOrThrow(
|
|
443
|
+
initArg,
|
|
444
|
+
scope,
|
|
445
|
+
`AOT: the initial value of list state "${name}" must be a compile-time constant array`,
|
|
446
|
+
'useState([...]) initial must be a literal array of objects/numbers/strings — no runtime values or function calls in the initial.',
|
|
447
|
+
);
|
|
448
|
+
let struct;
|
|
449
|
+
try {
|
|
450
|
+
struct = inferItemStruct(items, name);
|
|
451
|
+
} catch (e) {
|
|
452
|
+
if (initArg.loc && !e.aotLoc) e.aotLoc = initArg.loc.start; // collectState isn't withLoc-wrapped
|
|
453
|
+
throw e;
|
|
454
|
+
}
|
|
455
|
+
rec = { name, cField, setter, kind: 'list', struct, items, cap: LIST_CAP, cTypeName: `ErItem_${cField}`, arrayName: `s_${cField}`, countMember: `s_${cField}_count` };
|
|
456
|
+
} else {
|
|
457
|
+
const initVal = initArg
|
|
458
|
+
? evalStaticOrThrow(
|
|
459
|
+
initArg,
|
|
460
|
+
scope,
|
|
461
|
+
`AOT: the initial value of state "${name}" must be a compile-time constant`,
|
|
462
|
+
'useState(x) initial must be a literal or a constant expression (number, string, bool, or arithmetic over consts) — not a runtime value or function call.',
|
|
463
|
+
)
|
|
464
|
+
: 0;
|
|
465
|
+
let cType = cTypeOfValue(initVal);
|
|
466
|
+
// A numeric literal written with a decimal point or exponent (e.g. useState(70.0)) forces a FLOAT
|
|
467
|
+
// slot even though the value is integral — lets the state hold sub-integer values (a smooth drag)
|
|
468
|
+
// while the UI shows Math.round(value). (70.0 === 70 in JS, so we read the raw source to tell them apart.)
|
|
469
|
+
if (cType === 'int' && initArg?.type === 'NumericLiteral' && typeof initArg.extra?.raw === 'string' && /[.eE]/.test(initArg.extra.raw)) {
|
|
470
|
+
cType = 'float';
|
|
471
|
+
}
|
|
472
|
+
// String scalar → a fixed char buffer in ErAppState; setters snprintf into it (see scalarAssign).
|
|
473
|
+
const initCode = cType === 'string' ? cstr(String(initVal)) : cType === 'float' ? floatLit(initVal) : String(Number(initVal));
|
|
474
|
+
rec = { name, cField, setter, kind: 'scalar', cType, cMember: `s_state.${cField}`, initCode };
|
|
475
|
+
}
|
|
476
|
+
byName.set(name, rec);
|
|
477
|
+
if (setter) bySetter.set(setter, rec);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return { byName, bySetter };
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function findReturnJSX(fnBody, scope = {}) {
|
|
484
|
+
// Fold top-level `if (staticCond) return …` at compile time — responsive layouts switch on `screen`.
|
|
485
|
+
const scan = (stmts) => {
|
|
486
|
+
for (const stmt of stmts) {
|
|
487
|
+
if (stmt.type === 'IfStatement') {
|
|
488
|
+
let test;
|
|
489
|
+
try {
|
|
490
|
+
test = evalStatic(stmt.test, scope);
|
|
491
|
+
} catch {
|
|
492
|
+
throw new Error('AOT: a top-level `if` in the component must have a compile-time-constant test (e.g. on the `screen` global) — runtime layout branching is not supported');
|
|
493
|
+
}
|
|
494
|
+
const branch = test ? stmt.consequent : stmt.alternate;
|
|
495
|
+
if (branch) {
|
|
496
|
+
const r = scan(branch.type === 'BlockStatement' ? branch.body : [branch]);
|
|
497
|
+
if (r) return r;
|
|
498
|
+
}
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
if (stmt.type === 'ReturnStatement' && stmt.argument) {
|
|
502
|
+
if (stmt.argument.type === 'JSXElement') return stmt.argument;
|
|
503
|
+
throw new Error(`AOT: the component must return a single JSX element (got ${stmt.argument.type})`);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return null;
|
|
507
|
+
};
|
|
508
|
+
const r = scan(fnBody.body);
|
|
509
|
+
if (!r) throw new Error('AOT: component has no return statement');
|
|
510
|
+
return r;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/** Returns the JSX a function component returns (arrow expression body or a block's return). */
|
|
514
|
+
function componentReturnJSX(fn, scope = {}) {
|
|
515
|
+
if (fn.body.type === 'JSXElement') return fn.body;
|
|
516
|
+
if (fn.body.type === 'BlockStatement') return findReturnJSX(fn.body, scope);
|
|
517
|
+
throw new Error('AOT: component body must return a JSX element');
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const fnReturnsJSX = (fn) => fn.body.type === 'JSXElement' || (fn.body.type === 'BlockStatement' && fn.body.body.some((s) => s.type === 'ReturnStatement' && s.argument?.type === 'JSXElement'));
|
|
521
|
+
|
|
522
|
+
/** Collects top-level function components (name → fn node), excluding the `App` entry component. */
|
|
523
|
+
/** Resolves a component definition expression to its function node, unwrapping memo(fn) / React.memo(fn). */
|
|
524
|
+
function asComponentFn(node) {
|
|
525
|
+
if (isFn(node)) return node;
|
|
526
|
+
if (node?.type === 'CallExpression' && isFn(node.arguments[0]) && (node.callee.name === 'memo' || (node.callee.type === 'MemberExpression' && node.callee.property?.name === 'memo'))) return node.arguments[0];
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function collectComponents(program) {
|
|
531
|
+
const comps = new Map();
|
|
532
|
+
for (const stmt of program.body) {
|
|
533
|
+
const d = stmt.type === 'ExportNamedDeclaration' ? stmt.declaration : stmt;
|
|
534
|
+
if (!d) continue;
|
|
535
|
+
if (d.type === 'FunctionDeclaration' && d.id && d.id.name !== 'App' && fnReturnsJSX(d)) comps.set(d.id.name, d);
|
|
536
|
+
if (d.type === 'VariableDeclaration')
|
|
537
|
+
for (const decl of d.declarations) {
|
|
538
|
+
if (decl.id?.type !== 'Identifier' || decl.id.name === 'App') continue;
|
|
539
|
+
const fn = asComponentFn(decl.init); // unwrap memo(...)
|
|
540
|
+
if (fn && fnReturnsJSX(fn)) comps.set(decl.id.name, fn);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return comps;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Collects callable HELPER functions (non-component, non-hook) a handler can inline: module-level
|
|
548
|
+
* `function f(){}` / `const f = () => {}` and the component's own local `const f = (args) => {…}` arrows.
|
|
549
|
+
* A helper is any function that does NOT return JSX (those are components) and is a plain function (not a
|
|
550
|
+
* useCallback/useMemo/useState call). Returns Map(name → fn node). Re-collected per component (cheap).
|
|
551
|
+
*/
|
|
552
|
+
function collectHelpers(componentBody, program) {
|
|
553
|
+
const helpers = new Map();
|
|
554
|
+
const add = (name, fn) => {
|
|
555
|
+
if (name && name !== 'App' && isFn(fn) && !fnReturnsJSX(fn)) helpers.set(name, fn);
|
|
556
|
+
};
|
|
557
|
+
for (const stmt of program.body) {
|
|
558
|
+
const d = stmt.type === 'ExportNamedDeclaration' ? stmt.declaration : stmt;
|
|
559
|
+
if (!d) continue;
|
|
560
|
+
if (d.type === 'FunctionDeclaration' && d.id) add(d.id.name, d);
|
|
561
|
+
if (d.type === 'VariableDeclaration') for (const decl of d.declarations) if (decl.id?.type === 'Identifier') add(decl.id.name, decl.init);
|
|
562
|
+
}
|
|
563
|
+
if (componentBody.type === 'BlockStatement') {
|
|
564
|
+
for (const stmt of componentBody.body) {
|
|
565
|
+
if (stmt.type !== 'VariableDeclaration') continue;
|
|
566
|
+
for (const decl of stmt.declarations) if (decl.id?.type === 'Identifier') add(decl.id.name, decl.init);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return helpers;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const IMAGE_EXT_RE = /\.(png|jpe?g|webp|gif|bmp)$/i;
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Collects image imports — `import wxSun from './assets/wx_sun.png'` → Map(local → { name, importPath }).
|
|
576
|
+
* `name` is the file's basename without extension (the asset key `<Image source>` resolves to and that the
|
|
577
|
+
* Flow A bundler also uses); `importPath` is the source-relative path the CLI bakes from. Mirrors the Flow A
|
|
578
|
+
* esbuild asset plugin so the same `import → basename` convention holds in both flows.
|
|
579
|
+
*/
|
|
580
|
+
function collectImageImports(program) {
|
|
581
|
+
const byLocal = new Map();
|
|
582
|
+
for (const stmt of program.body) {
|
|
583
|
+
if (stmt.type !== 'ImportDeclaration' || typeof stmt.source.value !== 'string') continue;
|
|
584
|
+
const importPath = stmt.source.value;
|
|
585
|
+
if (!IMAGE_EXT_RE.test(importPath)) continue;
|
|
586
|
+
const name = importPath.split(/[\\/]/).pop().replace(IMAGE_EXT_RE, '');
|
|
587
|
+
for (const spec of stmt.specifiers) {
|
|
588
|
+
if (spec.type === 'ImportDefaultSpecifier') byLocal.set(spec.local.name, { name, importPath });
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
return byLocal;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/** Collects `const fn = useCallback((...) => {...}, deps)` → Map(name → arrow fn node). Deps are ignored:
|
|
595
|
+
* the AOT re-renders via its own dependency tracking, so useCallback only names a shared C handler. */
|
|
596
|
+
function collectCallbacks(fnBody) {
|
|
597
|
+
const cbs = new Map();
|
|
598
|
+
if (fnBody.type !== 'BlockStatement') return cbs;
|
|
599
|
+
for (const stmt of fnBody.body) {
|
|
600
|
+
if (stmt.type !== 'VariableDeclaration') continue;
|
|
601
|
+
for (const decl of stmt.declarations) {
|
|
602
|
+
const init = decl.init;
|
|
603
|
+
if (init?.type === 'CallExpression' && init.callee.name === 'useCallback' && decl.id.type === 'Identifier' && isFn(init.arguments[0])) {
|
|
604
|
+
cbs.set(decl.id.name, init.arguments[0]);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return cbs;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/** Collects `const m = useMemo(() => expr, deps)` → Map(name → the memo's expression node). */
|
|
612
|
+
function collectMemos(fnBody) {
|
|
613
|
+
const memos = new Map();
|
|
614
|
+
if (fnBody.type !== 'BlockStatement') return memos;
|
|
615
|
+
for (const stmt of fnBody.body) {
|
|
616
|
+
if (stmt.type !== 'VariableDeclaration') continue;
|
|
617
|
+
for (const decl of stmt.declarations) {
|
|
618
|
+
const init = decl.init;
|
|
619
|
+
if (init?.type === 'CallExpression' && init.callee.name === 'useMemo' && decl.id.type === 'Identifier' && isFn(init.arguments[0])) {
|
|
620
|
+
const body = init.arguments[0].body;
|
|
621
|
+
if (body.type === 'BlockStatement') throw new Error(`AOT: useMemo for "${decl.id.name}" must be a single expression (for now)`);
|
|
622
|
+
memos.set(decl.id.name, body);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
return memos;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/** Collects `useEffect(() => {…}, deps)` calls → { fn, deps, node }. Deps validity is checked at compile. */
|
|
630
|
+
function collectEffects(fnBody) {
|
|
631
|
+
const effects = [];
|
|
632
|
+
if (fnBody.type !== 'BlockStatement') return effects;
|
|
633
|
+
for (const stmt of fnBody.body) {
|
|
634
|
+
if (stmt.type !== 'ExpressionStatement') continue;
|
|
635
|
+
const call = stmt.expression;
|
|
636
|
+
if (call?.type === 'CallExpression' && call.callee.type === 'Identifier' && call.callee.name === 'useEffect') {
|
|
637
|
+
if (!isFn(call.arguments[0])) throw aotError('AOT: useEffect must take an inline function', 'write useEffect(() => { … }, []).');
|
|
638
|
+
effects.push({ fn: call.arguments[0], deps: call.arguments[1], node: call });
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return effects;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/** True if a function component declares any useState (per-instance child state — not yet supported). */
|
|
645
|
+
function usesState(fn) {
|
|
646
|
+
if (fn.body.type !== 'BlockStatement') return false;
|
|
647
|
+
return fn.body.body.some((s) => s.type === 'VariableDeclaration' && s.declarations.some((d) => d.init?.type === 'CallExpression' && d.init.callee.name === 'useState'));
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// ---------------------------------------------------------------------------------------------------
|
|
651
|
+
// Animations — useAnimatedValue → an engine-side ERAnimValueHandle (native driver). The value binds to
|
|
652
|
+
// a node property (opacity / transform / color) via er_anim_value_bind, and Animated.timing/spring(...)
|
|
653
|
+
// .start() → er_anim_value_animate. The host's per-frame embedded_renderer_tick advances it in C — no
|
|
654
|
+
// per-frame JS, no app_update needed for the motion itself.
|
|
655
|
+
// ---------------------------------------------------------------------------------------------------
|
|
656
|
+
|
|
657
|
+
/** Collects `const x = useAnimatedValue(initial)` → Map(name → {cVar, initCode}). `prefix` namespaces the
|
|
658
|
+
* C var (`s_av_<prefix><name>`) so each inlined child instance gets its own engine value handle. */
|
|
659
|
+
function collectAnims(fnBody, scope, prefix = '') {
|
|
660
|
+
const anims = new Map();
|
|
661
|
+
if (fnBody.type !== 'BlockStatement') return anims;
|
|
662
|
+
for (const stmt of fnBody.body) {
|
|
663
|
+
if (stmt.type !== 'VariableDeclaration') continue;
|
|
664
|
+
for (const decl of stmt.declarations) {
|
|
665
|
+
const init = decl.init;
|
|
666
|
+
if (init?.type === 'CallExpression' && init.callee.name === 'useAnimatedValue' && decl.id.type === 'Identifier') {
|
|
667
|
+
const initVal = init.arguments[0] ? evalStatic(init.arguments[0], scope) : 0;
|
|
668
|
+
anims.set(decl.id.name, { cVar: `s_av_${prefix}${decl.id.name}`, initCode: floatLit(initVal) });
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return anims;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Collects `const r = useRef(initial)` refs → Map(name → {cVar, cType, initCode, kind}). Two kinds:
|
|
677
|
+
* - VALUE ref (numeric initial): a mutable C slot (escape-hatch state that does NOT re-render;
|
|
678
|
+
* `.current` reads/writes).
|
|
679
|
+
* - NODE ref (`useRef()` / `useRef(null)`): holds an `ERNode*`, captured by `ref={r}` on an element and
|
|
680
|
+
* used as the target of imperative calls like updateVector(r, …). kind === 'node'.
|
|
681
|
+
*/
|
|
682
|
+
function collectRefs(fnBody, scope, prefix = '') {
|
|
683
|
+
const refs = new Map();
|
|
684
|
+
if (fnBody.type !== 'BlockStatement') return refs;
|
|
685
|
+
for (const stmt of fnBody.body) {
|
|
686
|
+
if (stmt.type !== 'VariableDeclaration') continue;
|
|
687
|
+
for (const decl of stmt.declarations) {
|
|
688
|
+
const init = decl.init;
|
|
689
|
+
if (init?.type === 'CallExpression' && init.callee.name === 'useRef' && decl.id.type === 'Identifier') {
|
|
690
|
+
const cVar = `s_ref_${prefix}${decl.id.name}`;
|
|
691
|
+
const arg = init.arguments[0];
|
|
692
|
+
if (!arg || (arg.type === 'NullLiteral')) {
|
|
693
|
+
refs.set(decl.id.name, { cVar, cType: 'ERNode*', initCode: 'NULL', kind: 'node' });
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
const v = evalStatic(arg, scope);
|
|
697
|
+
if (typeof v !== 'number') throw new Error(`AOT: useRef initial for "${decl.id.name}" must be a number (value ref) or null/empty (node ref)`);
|
|
698
|
+
const cType = Number.isInteger(v) ? 'int' : 'float';
|
|
699
|
+
refs.set(decl.id.name, { cVar, cType, initCode: cType === 'float' ? `${v}f` : String(v), kind: 'value' });
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
return refs;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/** Resolves a JSX tag to a name, mapping `Animated.View/Text/Image` to their host element. */
|
|
707
|
+
function resolveTag(openingElement) {
|
|
708
|
+
const n = openingElement.name;
|
|
709
|
+
if (n.type === 'JSXIdentifier') return n.name;
|
|
710
|
+
if (n.type === 'JSXMemberExpression' && n.object.name === 'Animated') return n.property.name; // Animated.View → View
|
|
711
|
+
throw new Error('AOT: unsupported JSX tag expression');
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/** Style key → the ERAnimProp(s) an animated value binds to. */
|
|
715
|
+
const ANIM_STYLE_PROPS = { opacity: ['ER_PROP_OPACITY'], backgroundColor: ['ER_PROP_BACKGROUND_COLOR'], color: ['ER_PROP_COLOR'] };
|
|
716
|
+
const ANIM_TRANSFORM_PROPS = {
|
|
717
|
+
scale: ['ER_PROP_SCALE_X', 'ER_PROP_SCALE_Y'],
|
|
718
|
+
scaleX: ['ER_PROP_SCALE_X'],
|
|
719
|
+
scaleY: ['ER_PROP_SCALE_Y'],
|
|
720
|
+
translateX: ['ER_PROP_TRANSLATE_X'],
|
|
721
|
+
translateY: ['ER_PROP_TRANSLATE_Y'],
|
|
722
|
+
rotate: ['ER_PROP_ROTATE_Z'],
|
|
723
|
+
rotateZ: ['ER_PROP_ROTATE_Z'],
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
/** Formats a number as a valid C float literal (`1` → `1.0f`, not `1f` which doesn't compile). */
|
|
727
|
+
function floatLit(n) {
|
|
728
|
+
const v = Number(n);
|
|
729
|
+
return Number.isInteger(v) ? `${v}.0f` : `${v}f`;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/** The polynomial family of an `Easing.quad` / `Easing.cubic` node, for in/out/inOut composition. */
|
|
733
|
+
function easingFamily(node) {
|
|
734
|
+
if (node?.type === 'MemberExpression' && node.object?.name === 'Easing') {
|
|
735
|
+
if (node.property.name === 'quad') return 'QUAD';
|
|
736
|
+
if (node.property.name === 'cubic') return 'CUBIC';
|
|
737
|
+
}
|
|
738
|
+
return null;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/** Maps an `Easing.*` node → { ease: 'ER_EASE_*', bezier: [x1,y1,x2,y2] | null }. Handles the bare curves
|
|
742
|
+
* (linear/ease/quad/cubic/bounce/elastic), the in/out/inOut wrappers around quad/cubic, and
|
|
743
|
+
* Easing.bezier(x1,y1,x2,y2). No easing → ER_EASE_EASE_IN_OUT (RN's timing default); unknown → same. */
|
|
744
|
+
function easingInfo(node, env) {
|
|
745
|
+
const FALLBACK = { ease: 'ER_EASE_EASE_IN_OUT', bezier: null };
|
|
746
|
+
if (!node) return FALLBACK;
|
|
747
|
+
// Bare member: Easing.linear / Easing.ease / Easing.quad (== quad-in) / ...
|
|
748
|
+
if (node.type === 'MemberExpression' && node.object?.name === 'Easing') {
|
|
749
|
+
const m = { linear: 'ER_EASE_LINEAR', ease: 'ER_EASE_EASE', quad: 'ER_EASE_QUAD_IN', cubic: 'ER_EASE_CUBIC_IN', bounce: 'ER_EASE_BOUNCE_OUT', elastic: 'ER_EASE_ELASTIC_OUT' };
|
|
750
|
+
return { ease: m[node.property.name] || 'ER_EASE_EASE_IN_OUT', bezier: null };
|
|
751
|
+
}
|
|
752
|
+
// Call: Easing.bezier(...), Easing.elastic(n), Easing.in/out/inOut(inner)
|
|
753
|
+
if (node.type === 'CallExpression' && node.callee.type === 'MemberExpression' && node.callee.object?.name === 'Easing') {
|
|
754
|
+
const fn = node.callee.property.name;
|
|
755
|
+
if (fn === 'bezier') {
|
|
756
|
+
const cps = node.arguments.slice(0, 4).map((a) => Number(evalStaticOr(a, env, 0)));
|
|
757
|
+
return cps.length === 4 ? { ease: 'ER_EASE_BEZIER', bezier: cps } : FALLBACK;
|
|
758
|
+
}
|
|
759
|
+
if (fn === 'elastic') return { ease: 'ER_EASE_ELASTIC_OUT', bezier: null };
|
|
760
|
+
if (fn === 'in' || fn === 'out' || fn === 'inOut') {
|
|
761
|
+
const fam = easingFamily(node.arguments[0]);
|
|
762
|
+
const dir = fn === 'in' ? 'IN' : fn === 'out' ? 'OUT' : 'IN_OUT';
|
|
763
|
+
return fam ? { ease: `ER_EASE_${fam}_${dir}`, bezier: null } : FALLBACK;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
return FALLBACK;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/** Pushes `cfg.easing = …;` (and bezier control points for Easing.bezier) onto a timing config's C lines. */
|
|
770
|
+
function pushEasing(lines, c, easingNode, env) {
|
|
771
|
+
const { ease, bezier } = easingInfo(easingNode, env);
|
|
772
|
+
lines.push(` ${c}.easing = ${ease};`);
|
|
773
|
+
if (bezier) {
|
|
774
|
+
lines.push(` ${c}.bezier_x1 = ${floatLit(bezier[0])}; ${c}.bezier_y1 = ${floatLit(bezier[1])};`);
|
|
775
|
+
lines.push(` ${c}.bezier_x2 = ${floatLit(bezier[2])}; ${c}.bezier_y2 = ${floatLit(bezier[3])};`);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/** Parses a `.interpolate({ inputRange, outputRange, extrapolate })` config object → a static
|
|
780
|
+
* { input, output, exLeft, exRight } descriptor (ranges must be static, equal-length, 2..8 points). */
|
|
781
|
+
function parseInterp(cfgNode, env) {
|
|
782
|
+
if (cfgNode?.type !== 'ObjectExpression') throw aotError('AOT: .interpolate() needs a config object literal { inputRange, outputRange }');
|
|
783
|
+
const get = (k) => cfgNode.properties.find((p) => (p.key.name ?? p.key.value) === k)?.value;
|
|
784
|
+
const arr = (node, name) => {
|
|
785
|
+
if (node?.type !== 'ArrayExpression') throw aotError(`AOT: .interpolate() ${name} must be an array literal`);
|
|
786
|
+
return node.elements.map((e) => Number(evalStatic(e, env.consts ?? {})));
|
|
787
|
+
};
|
|
788
|
+
const input = arr(get('inputRange'), 'inputRange');
|
|
789
|
+
const output = arr(get('outputRange'), 'outputRange');
|
|
790
|
+
if (input.length < 2 || input.length !== output.length) throw aotError('AOT: .interpolate() inputRange and outputRange must be the same length (>= 2)');
|
|
791
|
+
if (input.length > 8) throw aotError('AOT: .interpolate() supports up to 8 breakpoints (ER_INTERPOLATE_MAX_POINTS)');
|
|
792
|
+
const ex = (node) => {
|
|
793
|
+
const v = node ? String(evalStaticOr(node, env, 'extend')) : 'extend';
|
|
794
|
+
return v === 'clamp' ? 'ER_EXTRAPOLATE_CLAMP' : v === 'identity' ? 'ER_EXTRAPOLATE_IDENTITY' : 'ER_EXTRAPOLATE_EXTEND';
|
|
795
|
+
};
|
|
796
|
+
const both = get('extrapolate'); // RN: `extrapolate` sets both ends; extrapolateLeft/Right override.
|
|
797
|
+
return { input, output, exLeft: ex(get('extrapolateLeft') ?? both), exRight: ex(get('extrapolateRight') ?? both) };
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// ---------------------------------------------------------------------------------------------------
|
|
801
|
+
// JSX → style / text / events
|
|
802
|
+
// ---------------------------------------------------------------------------------------------------
|
|
803
|
+
function attrExpr(attr) {
|
|
804
|
+
const v = attr.value;
|
|
805
|
+
if (!v) return { type: 'BooleanLiteral', value: true };
|
|
806
|
+
if (v.type === 'StringLiteral') return v;
|
|
807
|
+
if (v.type === 'JSXExpressionContainer') return v.expression;
|
|
808
|
+
return v;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/** A static ARGB8888 C literal (`0xAARRGGBBu`) from a CSS color string or number. */
|
|
812
|
+
function argbLiteral(value) {
|
|
813
|
+
return '0x' + (parseColor(String(value)) >>> 0).toString(16).padStart(8, '0').toUpperCase() + 'u';
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/** Lowers a dynamic (state-referencing) color expression to a C ARGB expression. */
|
|
817
|
+
function emitColorExpr(node, env) {
|
|
818
|
+
if (node.type === 'StringLiteral') return colorLiteral(node.value);
|
|
819
|
+
if (node.type === 'ConditionalExpression') {
|
|
820
|
+
const t = emitExpr(node.test, env).code;
|
|
821
|
+
return `((${t}) ? ${emitColorExpr(node.consequent, env)} : ${emitColorExpr(node.alternate, env)})`;
|
|
822
|
+
}
|
|
823
|
+
// A statically-resolvable color (a const string, or a theme token like `theme.card`) folds to a literal.
|
|
824
|
+
try {
|
|
825
|
+
const s = evalStatic(node, env.consts ?? {});
|
|
826
|
+
if (typeof s === 'string') return colorLiteral(s);
|
|
827
|
+
} catch {
|
|
828
|
+
/* not static — fall through to the error below */
|
|
829
|
+
}
|
|
830
|
+
throw new Error('AOT: a dynamic color must be a color string literal or a ternary of them');
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/** Lowers a dynamic enum-style expression (e.g. `flexDirection: row ? 'row' : 'column'`) to its ER_* constant
|
|
834
|
+
* (or a C ternary of them), looking values up in the style key's enum `table`. */
|
|
835
|
+
function emitEnumExpr(node, table, env) {
|
|
836
|
+
if (node.type === 'StringLiteral') {
|
|
837
|
+
const c = table[node.value];
|
|
838
|
+
if (!c) throw aotError(`AOT: unsupported enum value "${node.value}"`, `one of: ${Object.keys(table).join(', ')}`);
|
|
839
|
+
return c;
|
|
840
|
+
}
|
|
841
|
+
if (node.type === 'ConditionalExpression') {
|
|
842
|
+
const t = emitExpr(node.test, env).code;
|
|
843
|
+
return `((${t}) ? ${emitEnumExpr(node.consequent, table, env)} : ${emitEnumExpr(node.alternate, table, env)})`;
|
|
844
|
+
}
|
|
845
|
+
// A statically resolvable enum (a const string) folds to its constant.
|
|
846
|
+
try {
|
|
847
|
+
const s = evalStatic(node, env.consts ?? {});
|
|
848
|
+
if (typeof s === 'string' && table[s]) return table[s];
|
|
849
|
+
} catch {
|
|
850
|
+
/* not static — fall through */
|
|
851
|
+
}
|
|
852
|
+
throw aotError('AOT: a state-driven enum style must be a string literal or a ternary of them', "e.g. flexDirection: wide ? 'row' : 'column'");
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/** Lowers one dynamic inline-style value to ERProps field assignment(s) (C expressions). */
|
|
856
|
+
function lowerDynamicStyleValue(key, valueNode, env) {
|
|
857
|
+
const meta = DYN_FIELDS[key];
|
|
858
|
+
if (!meta) throw aotError(`AOT: a state-driven value for style "${key}" is not supported (static only)`, `state-driven styles supported: colors, opacity, sizes/margins/padding, and the layout enums (flexDirection, alignItems, alignSelf, justifyContent, position). Make "${key}" static, or drive the change another way.`);
|
|
859
|
+
if (meta.kind === 'color') return [{ field: meta.field, code: emitColorExpr(valueNode, env) }];
|
|
860
|
+
if (meta.kind === 'opacity') return [{ field: meta.field, code: `(uint8_t)((${emitExpr(valueNode, env).code}) * 255.0f)` }];
|
|
861
|
+
if (meta.kind === 'enum') return [{ field: meta.field, code: emitEnumExpr(valueNode, meta.table, env) }];
|
|
862
|
+
return [{ field: meta.field, code: `(int16_t)(${emitExpr(valueNode, env).code})` }]; /* num */
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* Collects an element's merged style into static field assigns and dynamic (state-driven) field assigns.
|
|
867
|
+
* Inline object values are tried statically first; a value that references state becomes a dynAssign.
|
|
868
|
+
* Later style sources override earlier ones per field (RN merge), kept in `fields` by ERProps field.
|
|
869
|
+
*/
|
|
870
|
+
function collectStyleAssigns(openingElement, scope, env) {
|
|
871
|
+
const fields = new Map(); // ERProps field -> { dynamic: bool, code: string }
|
|
872
|
+
const binds = []; // [{ cVar, prop, interp? }] — animated values bound to node properties (native driver)
|
|
873
|
+
const animRef = (node) => (node?.type === 'Identifier' && env.anims?.has(node.name) ? env.anims.get(node.name).cVar : null);
|
|
874
|
+
// `<animValue>.interpolate({ inputRange, outputRange, extrapolate })` → { cVar, interp } for a mapped bind.
|
|
875
|
+
const animInterpRef = (node) => {
|
|
876
|
+
if (node?.type === 'CallExpression' && node.callee.type === 'MemberExpression' && !node.callee.computed && node.callee.property.name === 'interpolate' && node.callee.object.type === 'Identifier' && env.anims?.has(node.callee.object.name)) {
|
|
877
|
+
return { cVar: env.anims.get(node.callee.object.name).cVar, interp: parseInterp(node.arguments[0], env) };
|
|
878
|
+
}
|
|
879
|
+
return null;
|
|
880
|
+
};
|
|
881
|
+
const apply = (expr) => {
|
|
882
|
+
if (expr.type === 'ArrayExpression') {
|
|
883
|
+
for (const e of expr.elements) if (e) apply(e);
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
if (expr.type === 'ObjectExpression') {
|
|
887
|
+
for (const prop of expr.properties) {
|
|
888
|
+
if (prop.type !== 'ObjectProperty') throw new Error('AOT: spread/method in an inline style object not supported');
|
|
889
|
+
const key = prop.computed ? evalStatic(prop.key, scope) : prop.key.name ?? prop.key.value;
|
|
890
|
+
|
|
891
|
+
// Animated value bound directly to a prop (opacity / backgroundColor / color), optionally through
|
|
892
|
+
// an .interpolate({ inputRange, outputRange }) mapping.
|
|
893
|
+
const av = animRef(prop.value);
|
|
894
|
+
if (av && ANIM_STYLE_PROPS[key]) {
|
|
895
|
+
for (const p of ANIM_STYLE_PROPS[key]) binds.push({ cVar: av, prop: p });
|
|
896
|
+
continue;
|
|
897
|
+
}
|
|
898
|
+
const ai = animInterpRef(prop.value);
|
|
899
|
+
if (ai && ANIM_STYLE_PROPS[key]) {
|
|
900
|
+
for (const p of ANIM_STYLE_PROPS[key]) binds.push({ cVar: ai.cVar, prop: p, interp: ai.interp });
|
|
901
|
+
continue;
|
|
902
|
+
}
|
|
903
|
+
// transform: [{ scale: <anim> }, { translateX: <anim>.interpolate(...) }, ...] — bind each entry.
|
|
904
|
+
if (key === 'transform' && prop.value.type === 'ArrayExpression') {
|
|
905
|
+
let handled = false;
|
|
906
|
+
for (const entry of prop.value.elements) {
|
|
907
|
+
if (entry?.type !== 'ObjectExpression') continue;
|
|
908
|
+
for (const tp of entry.properties) {
|
|
909
|
+
const tk = tp.key.name ?? tp.key.value;
|
|
910
|
+
const tav = animRef(tp.value);
|
|
911
|
+
if (tav && ANIM_TRANSFORM_PROPS[tk]) {
|
|
912
|
+
for (const p of ANIM_TRANSFORM_PROPS[tk]) binds.push({ cVar: tav, prop: p });
|
|
913
|
+
handled = true;
|
|
914
|
+
continue;
|
|
915
|
+
}
|
|
916
|
+
const tai = animInterpRef(tp.value);
|
|
917
|
+
if (tai && ANIM_TRANSFORM_PROPS[tk]) {
|
|
918
|
+
for (const p of ANIM_TRANSFORM_PROPS[tk]) binds.push({ cVar: tai.cVar, prop: p, interp: tai.interp });
|
|
919
|
+
handled = true;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
if (handled) continue;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
try {
|
|
927
|
+
for (const a of lowerStyle({ [key]: evalStatic(prop.value, scope) })) fields.set(a.field, { dynamic: false, code: a.expr });
|
|
928
|
+
} catch {
|
|
929
|
+
for (const a of lowerDynamicStyleValue(key, prop.value, env)) fields.set(a.field, { dynamic: true, code: a.code });
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
// A StyleSheet reference / identifier resolving to a static style object.
|
|
935
|
+
for (const a of lowerStyle(evalStatic(expr, scope))) fields.set(a.field, { dynamic: false, code: a.expr });
|
|
936
|
+
};
|
|
937
|
+
for (const attr of openingElement.attributes) {
|
|
938
|
+
if (attr.type !== 'JSXAttribute' || attr.name.name !== 'style') continue;
|
|
939
|
+
apply(attrExpr(attr));
|
|
940
|
+
}
|
|
941
|
+
const staticAssigns = [];
|
|
942
|
+
const dynAssigns = [];
|
|
943
|
+
for (const [field, v] of fields) (v.dynamic ? dynAssigns : staticAssigns).push(v.dynamic ? { field, code: v.code } : { field, expr: v.code });
|
|
944
|
+
return { staticAssigns, dynAssigns, binds };
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
const EVENT_TYPES = {
|
|
948
|
+
onPress: 'ER_EVENT_PRESS',
|
|
949
|
+
onLongPress: 'ER_EVENT_LONG_PRESS',
|
|
950
|
+
onPressIn: 'ER_EVENT_PRESS_IN',
|
|
951
|
+
onPressOut: 'ER_EVENT_PRESS_OUT',
|
|
952
|
+
onTouchStart: 'ER_EVENT_TOUCH_START',
|
|
953
|
+
onTouchMove: 'ER_EVENT_TOUCH_MOVE',
|
|
954
|
+
onTouchEnd: 'ER_EVENT_TOUCH_END',
|
|
955
|
+
onLayout: 'ER_EVENT_LAYOUT',
|
|
956
|
+
};
|
|
957
|
+
|
|
958
|
+
const cstr = (s) => `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n').replace(/\t/g, '\\t')}"`;
|
|
959
|
+
|
|
960
|
+
/**
|
|
961
|
+
* Builds a Text node's content. Static interpolations fold into the literal; any that reference state
|
|
962
|
+
* make it dynamic (a printf format + C arg expressions recomputed on update).
|
|
963
|
+
*/
|
|
964
|
+
function buildText(children, scope, env) {
|
|
965
|
+
let format = '';
|
|
966
|
+
const args = [];
|
|
967
|
+
let dynamic = false;
|
|
968
|
+
for (const child of children) {
|
|
969
|
+
if (child.type === 'JSXText') {
|
|
970
|
+
const t = /\n/.test(child.value) ? child.value.replace(/\s+/g, ' ').trim() : child.value;
|
|
971
|
+
format += t.replace(/%/g, '%%');
|
|
972
|
+
} else if (child.type === 'JSXExpressionContainer') {
|
|
973
|
+
if (child.expression.type === 'JSXEmptyExpression') continue;
|
|
974
|
+
try {
|
|
975
|
+
const v = evalStatic(child.expression, scope); // constant → fold in
|
|
976
|
+
format += (v === undefined || v === null ? '' : String(v)).replace(/%/g, '%%');
|
|
977
|
+
} catch {
|
|
978
|
+
const e = emitExpr(child.expression, env); // references state → dynamic
|
|
979
|
+
format += printfSpec(e.cType);
|
|
980
|
+
args.push(e.code);
|
|
981
|
+
dynamic = true;
|
|
982
|
+
}
|
|
983
|
+
} else if (child.type === 'JSXElement') {
|
|
984
|
+
throw new Error('AOT: nested <Text> / element children inside <Text> not yet supported (spans)');
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
return { dynamic, format, args };
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/** Normalises JSX text the way Babel does: trim per-line, drop blank lines, join with single spaces; a
|
|
991
|
+
* same-line leading/trailing space is preserved (so `Hello <b>x</b>` keeps the space before the span). */
|
|
992
|
+
function cleanJsxText(value) {
|
|
993
|
+
const lines = value.split(/\r\n|\n|\r/);
|
|
994
|
+
let last = 0;
|
|
995
|
+
for (let i = 0; i < lines.length; i++) if (/[^ \t]/.test(lines[i])) last = i;
|
|
996
|
+
let out = '';
|
|
997
|
+
for (let i = 0; i < lines.length; i++) {
|
|
998
|
+
let line = lines[i].replace(/\t/g, ' ');
|
|
999
|
+
if (i !== 0) line = line.replace(/^ +/, '');
|
|
1000
|
+
if (i !== lines.length - 1) line = line.replace(/ +$/, '');
|
|
1001
|
+
if (line) out += i !== last ? line + ' ' : line;
|
|
1002
|
+
}
|
|
1003
|
+
return out;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
/** Concatenates a (span) <Text>'s static text content (literal + folded {expr}); a nested element throws. */
|
|
1007
|
+
function staticTextContent(children, scope) {
|
|
1008
|
+
let s = '';
|
|
1009
|
+
for (const c of children) {
|
|
1010
|
+
if (c.type === 'JSXText') s += cleanJsxText(c.value);
|
|
1011
|
+
else if (c.type === 'JSXExpressionContainer' && c.expression.type !== 'JSXEmptyExpression') {
|
|
1012
|
+
const v = evalStatic(c.expression, scope); // throws if it references state
|
|
1013
|
+
if (v !== undefined && v !== null) s += String(v);
|
|
1014
|
+
} else if (c.type === 'JSXElement') throw aotError('AOT: a nested <Text> span may not itself contain another <Text> (one level of spans only)');
|
|
1015
|
+
}
|
|
1016
|
+
return s;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
/**
|
|
1020
|
+
* If a <Text>'s children include a nested <Text>, returns inline SPANS [{text, color, font_size,
|
|
1021
|
+
* font_weight, font_style, text_decoration, letter_spacing}] (C-expr fields; inherit sentinels for unset).
|
|
1022
|
+
* Returns null when there's no nested <Text> (caller uses the single-string buildText path). Static only —
|
|
1023
|
+
* a dynamic {…} segment or a state-driven span style throws.
|
|
1024
|
+
*/
|
|
1025
|
+
function collectTextSpans(children, scope, env) {
|
|
1026
|
+
if (!children.some((c) => c.type === 'JSXElement' && c.openingElement.name.name === 'Text')) return null;
|
|
1027
|
+
// Inherit sentinels (see ERTextSpan doc): color 0, font_size 0, weight/style/decoration 0xFF, spacing AUTO.
|
|
1028
|
+
const inheritSpan = (text) => ({ text, color: '0u', font_size: '0', font_weight: '0xFF', font_style: '0xFF', text_decoration: '0xFF', letter_spacing: 'ER_LAYOUT_AUTO' });
|
|
1029
|
+
const spans = [];
|
|
1030
|
+
for (const c of children) {
|
|
1031
|
+
if (c.type === 'JSXText') {
|
|
1032
|
+
const t = cleanJsxText(c.value);
|
|
1033
|
+
if (t) spans.push(inheritSpan(cstr(t)));
|
|
1034
|
+
} else if (c.type === 'JSXExpressionContainer') {
|
|
1035
|
+
if (c.expression.type === 'JSXEmptyExpression') continue;
|
|
1036
|
+
let v;
|
|
1037
|
+
try {
|
|
1038
|
+
v = evalStatic(c.expression, scope);
|
|
1039
|
+
} catch {
|
|
1040
|
+
throw aotError('AOT: a dynamic {…} segment inside a multi-span <Text> is not supported', 'spans must be static; keep dynamic text in its own single <Text> (no nested <Text> siblings).');
|
|
1041
|
+
}
|
|
1042
|
+
if (v !== undefined && v !== null) spans.push(inheritSpan(cstr(String(v))));
|
|
1043
|
+
} else if (c.type === 'JSXElement' && c.openingElement.name.name === 'Text') {
|
|
1044
|
+
const { staticAssigns, dynAssigns } = collectStyleAssigns(c.openingElement, scope, env);
|
|
1045
|
+
if (dynAssigns.length) throw aotError('AOT: a state-driven style on a nested <Text> span is not supported', 'give the span <Text> a static style.');
|
|
1046
|
+
const field = (f, dflt) => staticAssigns.find((a) => a.field === f)?.expr ?? dflt;
|
|
1047
|
+
spans.push({
|
|
1048
|
+
text: cstr(staticTextContent(c.children, scope)),
|
|
1049
|
+
color: field('color', '0u'),
|
|
1050
|
+
font_size: field('font_size', '0'),
|
|
1051
|
+
font_weight: field('font_weight', '0xFF'),
|
|
1052
|
+
font_style: field('font_style', '0xFF'),
|
|
1053
|
+
text_decoration: field('text_decoration', '0xFF'),
|
|
1054
|
+
letter_spacing: field('letter_spacing', 'ER_LAYOUT_AUTO'),
|
|
1055
|
+
});
|
|
1056
|
+
} else throw aotError('AOT: unsupported child inside a multi-span <Text>', 'a <Text> with a nested <Text> may contain text, {static expressions}, and nested <Text> only.');
|
|
1057
|
+
}
|
|
1058
|
+
// The engine renders at most ER_TEXT_MAX_SPANS segments; refuse to silently drop the rest.
|
|
1059
|
+
if (spans.length > AOT_MAX_TEXT_SPANS) {
|
|
1060
|
+
throw aotError(`AOT: a <Text> has ${spans.length} inline segments but the engine renders at most ${AOT_MAX_TEXT_SPANS}`, `combine adjacent plain-text segments, or end the sentence right after a styled <Text> (e.g. "A <b>B</b> C <b>D</b>" is 4). If your engine build raised ER_TEXT_MAX_SPANS, set ER_AOT_MAX_TEXT_SPANS to match when running the AOT.`);
|
|
1061
|
+
}
|
|
1062
|
+
return spans;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// ---------------------------------------------------------------------------------------------------
|
|
1066
|
+
// Handler compilation — an on* arrow/function → C statements that mutate state and re-render.
|
|
1067
|
+
// ---------------------------------------------------------------------------------------------------
|
|
1068
|
+
/** Compiles a list-state setter call (`setItems(...)`) to bounded C array mutations. */
|
|
1069
|
+
function compileListOp(rec, arg, env) {
|
|
1070
|
+
const { arrayName: arr, countMember: cnt, cap, struct } = rec;
|
|
1071
|
+
// setItems([...items, a, b]) — append; setItems([]) — clear.
|
|
1072
|
+
if (arg.type === 'ArrayExpression') {
|
|
1073
|
+
if (arg.elements.length === 0) return [` ${cnt} = 0;`];
|
|
1074
|
+
const [head, ...rest] = arg.elements;
|
|
1075
|
+
if (head?.type !== 'SpreadElement' || head.argument.name !== rec.name) throw new Error(`AOT: a list literal must spread the current list first: [...${rec.name}, item]`);
|
|
1076
|
+
const lines = [];
|
|
1077
|
+
for (const el of rest) {
|
|
1078
|
+
if (el.type !== 'ObjectExpression') throw new Error('AOT: appended list items must be object literals');
|
|
1079
|
+
const props = new Map(el.properties.map((p) => [p.key.name ?? p.key.value, p.value]));
|
|
1080
|
+
lines.push(` if (${cnt} < ${cap})`, ' {');
|
|
1081
|
+
for (const f of struct.fields) {
|
|
1082
|
+
const valNode = props.get(f.key);
|
|
1083
|
+
if (!valNode) continue;
|
|
1084
|
+
const e = emitExpr(valNode, env);
|
|
1085
|
+
if (f.kind === 'string') lines.push(` snprintf(${arr}[${cnt}].${f.key}, sizeof(${arr}[${cnt}].${f.key}), "${printfSpec(e.cType)}", ${e.code});`);
|
|
1086
|
+
else lines.push(` ${arr}[${cnt}].${f.key} = ${e.code};`);
|
|
1087
|
+
}
|
|
1088
|
+
lines.push(` ${cnt}++;`, ' }');
|
|
1089
|
+
}
|
|
1090
|
+
return lines;
|
|
1091
|
+
}
|
|
1092
|
+
// setItems(items.slice(0, X)) — slice(0,-1) pops the last; slice(0,n) truncates to n.
|
|
1093
|
+
if (arg.type === 'CallExpression' && arg.callee.type === 'MemberExpression' && arg.callee.object.name === rec.name && arg.callee.property.name === 'slice') {
|
|
1094
|
+
const end = arg.arguments[1];
|
|
1095
|
+
if (end?.type === 'UnaryExpression' && end.operator === '-' && end.argument.value === 1) return [` if (${cnt} > 0) ${cnt}--;`];
|
|
1096
|
+
const e = emitExpr(end, env);
|
|
1097
|
+
return [` ${cnt} = (${cnt} < (${e.code})) ? ${cnt} : (${e.code});`];
|
|
1098
|
+
}
|
|
1099
|
+
throw new Error(`AOT: unsupported list operation on "${rec.name}" (use [...${rec.name}, item], ${rec.name}.slice(0, -1), or [])`);
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
/** Builds a `get(key)` accessor over an `Animated.*(value, config)` call's config object literal. */
|
|
1103
|
+
function animConfigGetter(cfgObj) {
|
|
1104
|
+
return (k) => cfgObj?.properties?.find((p) => (p.key.name ?? p.key.value) === k)?.value;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
/** Emits one atomic animation (timing/spring/decay) → a scoped ERAnimConfig + er_anim_value_animate.
|
|
1108
|
+
* `delayMs` is the absolute start delay (how composition offsets are realised); `loop` repeats a timing;
|
|
1109
|
+
* `onCompleteCb` (optional) is a C function name set as cfg.on_complete — used to chain sequence steps. */
|
|
1110
|
+
function emitAnimEntry(entry, env, idx, onCompleteCb) {
|
|
1111
|
+
const { cVar, kind, get, delayMs, loop } = entry;
|
|
1112
|
+
const c = `cfg${idx}`;
|
|
1113
|
+
const lines = [' {', ` ERAnimConfig ${c};`, ` memset(&${c}, 0, sizeof(${c}));`];
|
|
1114
|
+
if (kind === 'spring') {
|
|
1115
|
+
lines.push(` ${c}.type = ER_ANIM_SPRING;`);
|
|
1116
|
+
lines.push(` ${c}.stiffness = ${floatLit(evalStaticOr(get('stiffness'), env, 200))};`);
|
|
1117
|
+
lines.push(` ${c}.damping = ${floatLit(evalStaticOr(get('damping'), env, 18))};`);
|
|
1118
|
+
lines.push(` ${c}.mass = ${floatLit(evalStaticOr(get('mass'), env, 1))};`);
|
|
1119
|
+
} else if (kind === 'decay') {
|
|
1120
|
+
lines.push(` ${c}.type = ER_ANIM_DECAY;`);
|
|
1121
|
+
lines.push(` ${c}.deceleration = ${floatLit(evalStaticOr(get('deceleration'), env, 0.998))};`);
|
|
1122
|
+
lines.push(` ${c}.velocity = ${floatLit(evalStaticOr(get('velocity'), env, 0))};`);
|
|
1123
|
+
} else {
|
|
1124
|
+
lines.push(` ${c}.type = ER_ANIM_TIMING;`);
|
|
1125
|
+
lines.push(` ${c}.duration_ms = ${Math.round(Number(evalStaticOr(get('duration'), env, 250)))};`);
|
|
1126
|
+
pushEasing(lines, c, get('easing'), env);
|
|
1127
|
+
}
|
|
1128
|
+
if (delayMs > 0) lines.push(` ${c}.delay_ms = ${delayMs};`);
|
|
1129
|
+
if (loop) lines.push(` ${c}.loop = true;`);
|
|
1130
|
+
if (onCompleteCb) lines.push(` ${c}.on_complete = ${onCompleteCb};`);
|
|
1131
|
+
// decay is velocity-driven and has no toValue target; every other type needs one.
|
|
1132
|
+
const toNode = get('toValue');
|
|
1133
|
+
if (!toNode && kind !== 'decay') throw aotError(`AOT: Animated.${kind}() config needs a toValue`);
|
|
1134
|
+
const toCode = toNode ? emitExpr(toNode, env).code : `er_anim_value_get(${cVar})`;
|
|
1135
|
+
lines.push(` er_anim_value_animate(${cVar}, (float)(${toCode}), &${c});`, ' }');
|
|
1136
|
+
return lines;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
/** Flattens an Animated composition (timing/spring/decay/sequence/parallel/stagger/delay/loop) into a flat
|
|
1140
|
+
* list of atomic entries, each with an ABSOLUTE start delay (ms) — composition is realised purely through
|
|
1141
|
+
* per-entry delay_ms (no engine grouping needed for standalone values). Returns { entries, duration } where
|
|
1142
|
+
* `duration` is this node's own run length in ms, used to offset later siblings in a sequence/stagger;
|
|
1143
|
+
* null = unknown (spring/decay/loop), which is illegal to sequence anything after. */
|
|
1144
|
+
function flattenAnim(node, env, baseDelay, loop) {
|
|
1145
|
+
if (node?.type !== 'CallExpression' || node.callee.type !== 'MemberExpression' || node.callee.object?.name !== 'Animated')
|
|
1146
|
+
throw aotError('AOT: an animation must be Animated.timing/spring/decay/sequence/parallel/stagger/delay/loop(...)');
|
|
1147
|
+
const kind = node.callee.property.name;
|
|
1148
|
+
const args = node.arguments;
|
|
1149
|
+
|
|
1150
|
+
if (kind === 'timing' || kind === 'spring' || kind === 'decay') {
|
|
1151
|
+
const valRef = args[0];
|
|
1152
|
+
if (valRef?.type !== 'Identifier' || !env.anims?.has(valRef.name)) throw aotError(`AOT: Animated.${kind}() first argument must be a useAnimatedValue`);
|
|
1153
|
+
const cVar = env.anims.get(valRef.name).cVar;
|
|
1154
|
+
const get = animConfigGetter(args[1]);
|
|
1155
|
+
const ownDelay = Math.round(Number(evalStaticOr(get('delay'), env, 0)));
|
|
1156
|
+
const duration = kind === 'timing' ? ownDelay + Math.round(Number(evalStaticOr(get('duration'), env, 250))) : null;
|
|
1157
|
+
return { entries: [{ cVar, kind, get, delayMs: baseDelay + ownDelay, loop }], duration };
|
|
1158
|
+
}
|
|
1159
|
+
if (kind === 'delay') {
|
|
1160
|
+
return { entries: [], duration: Math.round(Number(evalStaticOr(args[0], env, 0))) };
|
|
1161
|
+
}
|
|
1162
|
+
if (kind === 'sequence' || kind === 'parallel' || kind === 'stagger') {
|
|
1163
|
+
const list = kind === 'stagger' ? args[1] : args[0];
|
|
1164
|
+
const staggerMs = kind === 'stagger' ? Math.round(Number(evalStaticOr(args[0], env, 0))) : 0;
|
|
1165
|
+
if (list?.type !== 'ArrayExpression') throw aotError(`AOT: Animated.${kind}(...) needs an array of animations`);
|
|
1166
|
+
const entries = [];
|
|
1167
|
+
let off = baseDelay; // running offset (sequence)
|
|
1168
|
+
let groupDur = 0; // max end-time relative to baseDelay (parallel/stagger)
|
|
1169
|
+
let i = 0;
|
|
1170
|
+
for (const child of list.elements) {
|
|
1171
|
+
if (!child) continue;
|
|
1172
|
+
const start = kind === 'sequence' ? off : baseDelay + i * staggerMs;
|
|
1173
|
+
const r = flattenAnim(child, env, start, loop);
|
|
1174
|
+
entries.push(...r.entries);
|
|
1175
|
+
if (kind === 'sequence') {
|
|
1176
|
+
if (r.duration == null) throw aotError('AOT: an Animated.sequence entry needs a known duration — use Animated.timing / Animated.delay (a spring/decay/loop inside a sequence is not supported; it has no fixed length to offset the next entry by)');
|
|
1177
|
+
off += r.duration;
|
|
1178
|
+
} else {
|
|
1179
|
+
const end = (start - baseDelay) + (r.duration ?? 0);
|
|
1180
|
+
if (end > groupDur) groupDur = end;
|
|
1181
|
+
}
|
|
1182
|
+
i++;
|
|
1183
|
+
}
|
|
1184
|
+
// Same value can't appear twice in a flat (delay_ms) composition: er_anim_value_animate cancels the
|
|
1185
|
+
// running anim on a value, so concurrent/flat-sequenced same-value steps cancel each other. (A
|
|
1186
|
+
// top-level Animated.sequence is handled separately via on_complete chaining, which does support this.)
|
|
1187
|
+
const seen = new Set();
|
|
1188
|
+
for (const e of entries) {
|
|
1189
|
+
if (seen.has(e.cVar))
|
|
1190
|
+
throw aotError('AOT: the same animated value is driven more than once in this composition — concurrent/flat same-value steps cancel each other. Use a top-level Animated.sequence(...) for multi-step animation of one value.');
|
|
1191
|
+
seen.add(e.cVar);
|
|
1192
|
+
}
|
|
1193
|
+
return { entries, duration: kind === 'sequence' ? off - baseDelay : groupDur };
|
|
1194
|
+
}
|
|
1195
|
+
if (kind === 'loop') {
|
|
1196
|
+
const r = flattenAnim(args[0], env, baseDelay, true);
|
|
1197
|
+
if (r.entries.length !== 1) throw aotError('AOT: Animated.loop currently wraps a single Animated.timing/spring/decay (looping a sequence/parallel is not yet supported)');
|
|
1198
|
+
return { entries: r.entries, duration: null };
|
|
1199
|
+
}
|
|
1200
|
+
throw aotError(`AOT: Animated.${kind}(...) is not a supported animation`);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
/** Lowers `Animated.sequence([...]).start()` to an on_complete CHAIN: step 0 starts inline (in the handler),
|
|
1204
|
+
* and each step's completion callback starts the next. Unlike the flat delay_ms path this is correct when
|
|
1205
|
+
* steps share a value (out-and-back) — er_anim_value_animate cancels the running anim on a value, so
|
|
1206
|
+
* synchronous same-value animates would cancel each other — and needs no fixed duration (spring/decay OK).
|
|
1207
|
+
* delay() entries fold into the next step's delay_ms. Nested parallel/stagger/loop in a sequence throws. */
|
|
1208
|
+
function compileSequenceChain(seqNode, env, ctx, doneCb = null) {
|
|
1209
|
+
const list = seqNode.arguments[0];
|
|
1210
|
+
if (list?.type !== 'ArrayExpression') throw aotError('AOT: Animated.sequence(...) needs an array of animations');
|
|
1211
|
+
const steps = [];
|
|
1212
|
+
let pendingDelay = 0;
|
|
1213
|
+
for (const child of list.elements) {
|
|
1214
|
+
if (!child) continue;
|
|
1215
|
+
if (child.type !== 'CallExpression' || child.callee.type !== 'MemberExpression' || child.callee.object?.name !== 'Animated')
|
|
1216
|
+
throw aotError('AOT: Animated.sequence entries must be Animated.timing/spring/decay/delay(...)');
|
|
1217
|
+
const kind = child.callee.property.name;
|
|
1218
|
+
if (kind === 'delay') {
|
|
1219
|
+
pendingDelay += Math.round(Number(evalStaticOr(child.arguments[0], env, 0)));
|
|
1220
|
+
continue;
|
|
1221
|
+
}
|
|
1222
|
+
if (kind !== 'timing' && kind !== 'spring' && kind !== 'decay')
|
|
1223
|
+
throw aotError('AOT: Animated.sequence entries must be Animated.timing/spring/decay/delay (a nested parallel/stagger/loop inside a sequence is not yet supported — keep the sequence flat)');
|
|
1224
|
+
const valRef = child.arguments[0];
|
|
1225
|
+
if (valRef?.type !== 'Identifier' || !env.anims?.has(valRef.name)) throw aotError(`AOT: Animated.${kind}() first argument must be a useAnimatedValue`);
|
|
1226
|
+
const get = animConfigGetter(child.arguments[1]);
|
|
1227
|
+
const ownDelay = Math.round(Number(evalStaticOr(get('delay'), env, 0)));
|
|
1228
|
+
steps.push({ cVar: env.anims.get(valRef.name).cVar, kind, get, delayMs: pendingDelay + ownDelay, loop: false });
|
|
1229
|
+
pendingDelay = 0;
|
|
1230
|
+
}
|
|
1231
|
+
if (!steps.length) return [];
|
|
1232
|
+
const seqId = ctx.out.seqN++; // GLOBAL: callback names are file-scope, so must be unique across handlers
|
|
1233
|
+
let firstLines = [];
|
|
1234
|
+
// Build from the tail so each step knows its successor's callback name. Step 0 runs inline; the rest
|
|
1235
|
+
// become on_complete callbacks pushed to out.animCbs (emitted at file scope).
|
|
1236
|
+
for (let i = steps.length - 1; i >= 0; i--) {
|
|
1237
|
+
// The last step's on_complete is the .start(onComplete) callback (if any) — the sequence is "done".
|
|
1238
|
+
const nextCb = i < steps.length - 1 ? `er_seqcb_${seqId}_${i + 1}` : doneCb;
|
|
1239
|
+
const lines = emitAnimEntry(steps[i], env, `${seqId}_${i}`, nextCb);
|
|
1240
|
+
if (i === 0) firstLines = lines;
|
|
1241
|
+
else ctx.out.animCbs.push({ name: `er_seqcb_${seqId}_${i}`, body: lines });
|
|
1242
|
+
}
|
|
1243
|
+
return firstLines;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
/** Compiles `<animation>.start()` — a single Animated.timing/spring/decay or a composition
|
|
1247
|
+
* (sequence/parallel/stagger/delay/loop). A top-level sequence chains via on_complete (compileSequenceChain);
|
|
1248
|
+
* everything else flattens to one ERAnimConfig + er_anim_value_animate per atomic entry, composition
|
|
1249
|
+
* expressed through per-entry delay_ms. Native-driven; sets no React state. */
|
|
1250
|
+
/** Compiles a `.start(onComplete)` completion callback to a file-scope C fn (ERAnimCompleteFn) set as the
|
|
1251
|
+
* animation's on_complete; its body runs setters/refs/etc. and re-applies state via app_update if needed. */
|
|
1252
|
+
function emitCompletionCb(fnNode, env, state, ctx) {
|
|
1253
|
+
const id = ctx.out.seqN++; // GLOBAL — callback names are file-scope
|
|
1254
|
+
const name = `er_donecb_${id}`;
|
|
1255
|
+
// `start(({ finished }) => …)`: expose the engine's `finished` bool as that local; a bare param is ignored.
|
|
1256
|
+
const locals = new Map(env.locals);
|
|
1257
|
+
const param = fnNode.params[0];
|
|
1258
|
+
if (param?.type === 'ObjectPattern') {
|
|
1259
|
+
for (const p of param.properties) if ((p.key?.name ?? p.key?.value) === 'finished') locals.set(p.value?.name ?? 'finished', { code: 'finished', cType: 'int' });
|
|
1260
|
+
}
|
|
1261
|
+
const body = fnNode.body;
|
|
1262
|
+
const list = body.type === 'BlockStatement' ? body.body : [{ type: 'ExpressionStatement', expression: body }];
|
|
1263
|
+
const cctx = { stateChanged: false, animIdx: 0, out: ctx.out };
|
|
1264
|
+
const lines = compileStmts(list, { ...env, locals }, state, cctx, ' ');
|
|
1265
|
+
if (cctx.stateChanged) lines.push(' app_update();');
|
|
1266
|
+
ctx.out.animCbs.push({ name, body: lines });
|
|
1267
|
+
return name;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
function compileAnimateStart(expr, env, state, ctx) {
|
|
1271
|
+
// .start(onComplete?) — an optional completion callback fired when the animation finishes.
|
|
1272
|
+
const doneCb = isFn(expr.arguments[0]) ? emitCompletionCb(expr.arguments[0], env, state, ctx) : null;
|
|
1273
|
+
const receiver = expr.callee.object;
|
|
1274
|
+
if (receiver?.type === 'CallExpression' && receiver.callee.type === 'MemberExpression' && receiver.callee.object?.name === 'Animated' && receiver.callee.property.name === 'sequence') {
|
|
1275
|
+
return compileSequenceChain(receiver, env, ctx, doneCb);
|
|
1276
|
+
}
|
|
1277
|
+
const { entries } = flattenAnim(receiver, env, 0, false);
|
|
1278
|
+
if (doneCb && entries.length > 1)
|
|
1279
|
+
throw aotError(
|
|
1280
|
+
'AOT: a .start(onComplete) callback on a parallel/stagger animation is not yet supported',
|
|
1281
|
+
'attach the completion callback to a single animation or an Animated.sequence(...). For "after all parallel anims", restructure as a sequence.',
|
|
1282
|
+
);
|
|
1283
|
+
const lines = [];
|
|
1284
|
+
entries.forEach((e, i) => lines.push(...emitAnimEntry(e, env, ctx.animIdx++, i === entries.length - 1 ? doneCb : null)));
|
|
1285
|
+
return lines;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
/** evalStatic with a fallback default when the node is absent or not foldable. */
|
|
1289
|
+
function evalStaticOr(node, env, dflt) {
|
|
1290
|
+
if (!node) return dflt;
|
|
1291
|
+
try {
|
|
1292
|
+
return evalStatic(node, env.consts ?? {});
|
|
1293
|
+
} catch {
|
|
1294
|
+
return dflt;
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
/** Returns a statement node's body list: a BlockStatement's contents, or the lone statement wrapped. */
|
|
1299
|
+
function blockList(node) {
|
|
1300
|
+
return node.type === 'BlockStatement' ? node.body : [node];
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
/** Emits C to write a value into a scalar state slot: snprintf for a string buffer, plain assign else. */
|
|
1304
|
+
function scalarAssign(rec, e, indent) {
|
|
1305
|
+
if (rec.cType === 'string') return `${indent}snprintf(${rec.cMember}, sizeof(${rec.cMember}), "${printfSpec(e.cType)}", ${e.code});`;
|
|
1306
|
+
return `${indent}${rec.cMember} = ${e.code};`;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
/** True if `node` is a `<ref>.current` member access on a known value ref. */
|
|
1310
|
+
function refTarget(node, env) {
|
|
1311
|
+
if (node?.type === 'MemberExpression' && !node.computed && node.object.type === 'Identifier' && node.property.name === 'current' && env.refs?.has(node.object.name)) {
|
|
1312
|
+
return env.refs.get(node.object.name);
|
|
1313
|
+
}
|
|
1314
|
+
return null;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
/** An imperative updateVector shape `{ arc:[…]|circle:[…]|rect:[…]|line:[…]|path:'…', fill, stroke, … }`
|
|
1318
|
+
* → { entries (op-tape C exprs), paint (static 7-num record) }. Geometry coords may reference state/refs/
|
|
1319
|
+
* event fields (emitExpr); paint must be static. Shares the ...EntriesC geometry with the JSX path. */
|
|
1320
|
+
function imperativeShape(shapeNode, env) {
|
|
1321
|
+
if (shapeNode?.type !== 'ObjectExpression') throw new Error('AOT: each updateVector shape must be an object literal');
|
|
1322
|
+
const props = {};
|
|
1323
|
+
for (const p of shapeNode.properties) {
|
|
1324
|
+
if (p.type !== 'ObjectProperty') throw new Error('AOT: spread/method in an updateVector shape not supported');
|
|
1325
|
+
props[p.key.name ?? p.key.value] = p.value;
|
|
1326
|
+
}
|
|
1327
|
+
const arr = (key, n) => {
|
|
1328
|
+
if (props[key].type !== 'ArrayExpression') throw new Error(`AOT: updateVector "${key}" must be an array literal`);
|
|
1329
|
+
return props[key].elements.slice(0, n).map((el) => `(float)(${emitExpr(el, env).code})`);
|
|
1330
|
+
};
|
|
1331
|
+
let entries;
|
|
1332
|
+
if (props.arc) entries = arcEntriesC(...arr('arc', 5));
|
|
1333
|
+
else if (props.circle) entries = circleEntriesC(...arr('circle', 3));
|
|
1334
|
+
else if (props.rect) entries = rectEntriesC(...arr('rect', 4));
|
|
1335
|
+
else if (props.line) entries = lineEntriesC(...arr('line', 4));
|
|
1336
|
+
else if (props.path) entries = parsePath(String(evalStatic(props.path, env.consts ?? {}))).map(floatLit);
|
|
1337
|
+
else throw new Error('AOT: an updateVector shape needs one of arc / circle / rect / line / path');
|
|
1338
|
+
const stat = (key, dflt) => {
|
|
1339
|
+
if (props[key] == null) return dflt;
|
|
1340
|
+
try {
|
|
1341
|
+
return evalStatic(props[key], env.consts ?? {});
|
|
1342
|
+
} catch {
|
|
1343
|
+
throw new Error(`AOT: updateVector paint "${key}" must be static`);
|
|
1344
|
+
}
|
|
1345
|
+
};
|
|
1346
|
+
const paint = [parseColor(stat('fill', 'none')), parseColor(stat('stroke', 'none')), svgNum(stat('strokeWidth', 1), 1), svgNum(stat('miter', 4), 4), CAP_MAP[stat('cap', 'butt')] ?? 0, JOIN_MAP[stat('join', 'miter')] ?? 0, stat('fillRule', 'nonzero') === 'evenodd' ? 1 : 0];
|
|
1347
|
+
return { entries, paint };
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
/** Lowers `updateVector(nodeRef, shapes, [x,y,w,h]?)` to: fill a mutable op-tape, push it to the node, and
|
|
1351
|
+
* (optionally) hint the dirty sub-rect — the imperative fast path (drag) that bypasses app_update. */
|
|
1352
|
+
function compileUpdateVector(expr, env, ctx, indent) {
|
|
1353
|
+
const out = ctx.out;
|
|
1354
|
+
const [refArg, shapesArg, dirtyArg] = expr.arguments;
|
|
1355
|
+
const ref = refArg?.type === 'Identifier' ? env.refs?.get(refArg.name) : null;
|
|
1356
|
+
if (ref?.kind !== 'node') throw new Error('AOT: updateVector(ref, …) first arg must be a node ref (const r = useRef())');
|
|
1357
|
+
if (shapesArg?.type !== 'ArrayExpression') throw new Error('AOT: updateVector(ref, shapes, …) shapes must be an array literal');
|
|
1358
|
+
const entries = [];
|
|
1359
|
+
const paints = [];
|
|
1360
|
+
for (const s of shapesArg.elements) {
|
|
1361
|
+
const { entries: e, paint } = imperativeShape(s, env);
|
|
1362
|
+
entries.push('ER_VOP_SHAPE', floatLit(paints.length), ...e);
|
|
1363
|
+
paints.push(paint);
|
|
1364
|
+
}
|
|
1365
|
+
const id = out.svgN++;
|
|
1366
|
+
const len = entries.length;
|
|
1367
|
+
out.needsMath = true;
|
|
1368
|
+
out.vectorData.push(`static float s_uv${id}_ops[${len}];`);
|
|
1369
|
+
out.vectorData.push(`static const ERVectorPaint s_uv${id}_paints[] = {\n${paints.map((p) => ' ' + emitVectorPaint(p)).join(',\n')}\n};`);
|
|
1370
|
+
const lines = entries.map((e, i) => `${indent}s_uv${id}_ops[${i}] = ${e};`);
|
|
1371
|
+
lines.push(`${indent}er_node_set_vector_ops(${ref.cVar}, s_uv${id}_ops, ${len}, s_uv${id}_paints, ${paints.length});`);
|
|
1372
|
+
if (dirtyArg) {
|
|
1373
|
+
if (dirtyArg.type !== 'ArrayExpression' || dirtyArg.elements.length < 4) throw new Error('AOT: updateVector dirtyRect must be a [x, y, w, h] array literal');
|
|
1374
|
+
const [x, y, w, h] = dirtyArg.elements.map((el) => emitExpr(el, env).code);
|
|
1375
|
+
lines.push(`${indent}er_node_set_vector_dirty_rect(${ref.cVar}, ${x}, ${y}, ${w}, ${h});`);
|
|
1376
|
+
}
|
|
1377
|
+
return lines;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
/** Compiles one handler ExpressionStatement: a state setter, ref mutation, or Animated.*(...).start(). */
|
|
1381
|
+
/** setInterval/setTimeout(cb, ms) → a C `er_timer_add(ms, repeat, fn)` expr; registers cb as a timer fn. */
|
|
1382
|
+
function compileTimerAdd(expr, env, state, ctx) {
|
|
1383
|
+
const cb = expr.arguments[0];
|
|
1384
|
+
if (!isFn(cb)) throw aotError('AOT: a setInterval/setTimeout callback must be an inline function', 'pass an inline arrow, e.g. setInterval(() => setTick((t) => t + 1), 1000).');
|
|
1385
|
+
const ms = expr.arguments[1] ? emitExpr(expr.arguments[1], env).code : '0';
|
|
1386
|
+
const repeat = expr.callee.name === 'setInterval';
|
|
1387
|
+
const slot = ctx.out.timerFns.length;
|
|
1388
|
+
const name = `er_timer_fn_${slot}`;
|
|
1389
|
+
ctx.out.usesTimers = true;
|
|
1390
|
+
ctx.out.timerFns.push({ name, body: null }); // reserve the slot before compiling the body (it may add more)
|
|
1391
|
+
ctx.out.timerFns[slot].body = compileHandler(cb, env, state, ctx.out);
|
|
1392
|
+
return `er_timer_add((int)(${ms}), ${repeat ? 'true' : 'false'}, ${name})`;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
/** Inlines a handler-statement call to a helper / useCallback: binds the call's args to the helper's params
|
|
1396
|
+
* as C locals, then compiles the helper body here in the current env/state/ctx. Guards against recursion. */
|
|
1397
|
+
function inlineHelperCall(name, fn, args, env, state, ctx, indent) {
|
|
1398
|
+
ctx.inlining = ctx.inlining ?? new Set();
|
|
1399
|
+
if (ctx.inlining.has(name))
|
|
1400
|
+
throw aotError(
|
|
1401
|
+
`AOT: helper "${name}" is recursive — can't be inlined into a handler`,
|
|
1402
|
+
'handlers are flattened to straight-line C, so a helper that calls itself (directly or via another helper) has no base case to unroll. Move recursive logic out of the handler, or precompute the value.',
|
|
1403
|
+
);
|
|
1404
|
+
const locals = new Map(env.locals);
|
|
1405
|
+
fn.params.forEach((p, i) => {
|
|
1406
|
+
if (p.type !== 'Identifier')
|
|
1407
|
+
throw aotError(
|
|
1408
|
+
`AOT: helper "${name}" must take simple (identifier) params to be inlined`,
|
|
1409
|
+
'a helper called from a handler must use plain positional params (e.g. `(a, b) => …`) — destructuring or default params in the signature are not supported.',
|
|
1410
|
+
);
|
|
1411
|
+
if (args[i]) {
|
|
1412
|
+
const e = emitExpr(args[i], env);
|
|
1413
|
+
locals.set(p.name, { code: e.code, cType: e.cType });
|
|
1414
|
+
}
|
|
1415
|
+
});
|
|
1416
|
+
const body = fn.body;
|
|
1417
|
+
const list = body.type === 'BlockStatement' ? body.body : [{ type: 'ExpressionStatement', expression: body }];
|
|
1418
|
+
ctx.inlining.add(name);
|
|
1419
|
+
const lines = compileStmts(list, { ...env, locals }, state, ctx, indent);
|
|
1420
|
+
ctx.inlining.delete(name);
|
|
1421
|
+
return lines;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
function compileHandlerExprImpl(expr, env, state, ctx, indent) {
|
|
1425
|
+
// updateVector(ref, shapes, dirtyRect?) — imperative vector redraw (no app_update).
|
|
1426
|
+
if (expr.type === 'CallExpression' && expr.callee.type === 'Identifier' && expr.callee.name === 'updateVector') {
|
|
1427
|
+
return compileUpdateVector(expr, env, ctx, indent);
|
|
1428
|
+
}
|
|
1429
|
+
// setInterval / setTimeout(cb, ms) → register a host-tick timer (the returned id is discarded here).
|
|
1430
|
+
if (expr.type === 'CallExpression' && expr.callee.type === 'Identifier' && (expr.callee.name === 'setInterval' || expr.callee.name === 'setTimeout')) {
|
|
1431
|
+
return [`${indent}${compileTimerAdd(expr, env, state, ctx)};`];
|
|
1432
|
+
}
|
|
1433
|
+
// clearInterval / clearTimeout(id) → deactivate the timer slot.
|
|
1434
|
+
if (expr.type === 'CallExpression' && expr.callee.type === 'Identifier' && (expr.callee.name === 'clearInterval' || expr.callee.name === 'clearTimeout')) {
|
|
1435
|
+
return [`${indent}er_timer_clear(${emitExpr(expr.arguments[0], env).code});`];
|
|
1436
|
+
}
|
|
1437
|
+
// `ref.current = expr` / `ref.current += expr` — a value ref write; does NOT trigger a re-render.
|
|
1438
|
+
if (expr.type === 'AssignmentExpression') {
|
|
1439
|
+
const r = refTarget(expr.left, env);
|
|
1440
|
+
if (!r) throw new Error('AOT: the only assignment allowed in a handler is `ref.current = ...`');
|
|
1441
|
+
return [`${indent}${r.cVar} ${expr.operator} ${emitExpr(expr.right, env).code};`];
|
|
1442
|
+
}
|
|
1443
|
+
// `ref.current++` / `ref.current--`.
|
|
1444
|
+
if (expr.type === 'UpdateExpression') {
|
|
1445
|
+
const r = refTarget(expr.argument, env);
|
|
1446
|
+
if (!r) throw new Error('AOT: the only ++/-- allowed in a handler is on `ref.current`');
|
|
1447
|
+
return [`${indent}${r.cVar}${expr.operator};`];
|
|
1448
|
+
}
|
|
1449
|
+
// Animated.*(…).start() — single timing/spring/decay OR a sequence/parallel/stagger/delay/loop
|
|
1450
|
+
// composition; native-driven, sets no React state, needs no app_update.
|
|
1451
|
+
if (expr.type === 'CallExpression' && expr.callee.type === 'MemberExpression' && expr.callee.property.name === 'start') {
|
|
1452
|
+
return compileAnimateStart(expr, env, state, ctx);
|
|
1453
|
+
}
|
|
1454
|
+
if (expr.type !== 'CallExpression' || expr.callee.type !== 'Identifier') throw aotError('AOT: a handler statement must be a state setter, a ref write, or Animated.timing/spring(...).start()', 'each statement in a handler must be one of: setX(value) / setX(prev => …), a `ref.current = …` write, an `updateVector(…)` call, or `Animated.timing|spring(v, …).start()`. Wrap conditional logic in `if (…) { … }`.');
|
|
1455
|
+
// A call to a helper / useCallback (e.g. `reset();`) → inline its body here so handlers can compose logic.
|
|
1456
|
+
const helperFn = env.helpers?.get(expr.callee.name) ?? env.callbacks?.get(expr.callee.name);
|
|
1457
|
+
if (helperFn) return inlineHelperCall(expr.callee.name, helperFn, expr.arguments, env, state, ctx, indent);
|
|
1458
|
+
const rec = state.bySetter.get(expr.callee.name);
|
|
1459
|
+
if (!rec)
|
|
1460
|
+
throw aotError(
|
|
1461
|
+
`AOT: "${expr.callee.name}" is not a known state setter`,
|
|
1462
|
+
`a handler can only call a setter from this component's own useState (e.g. setCount), a ref write, updateVector(…), or Animated…start(). "${expr.callee.name}" isn't one of those — arbitrary functions (fetch, console.*, helpers) can't be lowered to C.`,
|
|
1463
|
+
);
|
|
1464
|
+
ctx.stateChanged = true;
|
|
1465
|
+
const arg = expr.arguments[0];
|
|
1466
|
+
if (rec.kind === 'list') return compileListOp(rec, arg, env);
|
|
1467
|
+
if (arg && (arg.type === 'ArrowFunctionExpression' || arg.type === 'FunctionExpression')) {
|
|
1468
|
+
// setState(prev => expr): bind the param to the current value, assign the result.
|
|
1469
|
+
const param = arg.params[0]?.name;
|
|
1470
|
+
const locals = new Map(env.locals);
|
|
1471
|
+
if (param) locals.set(param, { code: rec.cMember, cType: rec.cType });
|
|
1472
|
+
if (arg.body.type === 'BlockStatement') throw new Error('AOT: updater function must be a single expression (for now)');
|
|
1473
|
+
return [scalarAssign(rec, emitExpr(arg.body, { ...env, locals }), indent)];
|
|
1474
|
+
}
|
|
1475
|
+
return [scalarAssign(rec, emitExpr(arg, env), indent)];
|
|
1476
|
+
}
|
|
1477
|
+
const compileHandlerExpr = withLoc(compileHandlerExprImpl);
|
|
1478
|
+
|
|
1479
|
+
/**
|
|
1480
|
+
* Compiles a list of handler statements to C lines. Supports: `const x = expr` (a C local, visible to
|
|
1481
|
+
* later statements), `if (cond) {...} else {...}`, state setters, and Animated.*(...).start(). `ctx`
|
|
1482
|
+
* accumulates `stateChanged` (→ trailing app_update) and `animIdx` (unique ERAnimConfig locals).
|
|
1483
|
+
*/
|
|
1484
|
+
function compileStmts(list, env, state, ctx, indent) {
|
|
1485
|
+
const lines = [];
|
|
1486
|
+
for (const st of list) {
|
|
1487
|
+
if (st.type === 'VariableDeclaration') {
|
|
1488
|
+
for (const decl of st.declarations) {
|
|
1489
|
+
if (decl.id.type !== 'Identifier') throw new Error('AOT: destructuring a handler local is not supported');
|
|
1490
|
+
if (!decl.init) throw new Error('AOT: a handler local must have an initializer');
|
|
1491
|
+
const cName = `l_${decl.id.name}`;
|
|
1492
|
+
// `const id = setInterval/setTimeout(…)` → an int timer-id local (so a later clearInterval(id) resolves).
|
|
1493
|
+
if (decl.init.type === 'CallExpression' && decl.init.callee.type === 'Identifier' && (decl.init.callee.name === 'setInterval' || decl.init.callee.name === 'setTimeout')) {
|
|
1494
|
+
// The id is only needed for a later clear*(); a mount effect drops its cleanup, so mark it used.
|
|
1495
|
+
lines.push(`${indent}int ${cName} = ${compileTimerAdd(decl.init, env, state, ctx)};`, `${indent}(void)${cName};`);
|
|
1496
|
+
env = { ...env, locals: new Map(env.locals).set(decl.id.name, { code: cName, cType: 'int' }) };
|
|
1497
|
+
continue;
|
|
1498
|
+
}
|
|
1499
|
+
const e = emitExpr(decl.init, env);
|
|
1500
|
+
lines.push(`${indent}${e.cType === 'float' ? 'float' : 'int'} ${cName} = ${e.code};`);
|
|
1501
|
+
env = { ...env, locals: new Map(env.locals).set(decl.id.name, { code: cName, cType: e.cType }) };
|
|
1502
|
+
}
|
|
1503
|
+
continue;
|
|
1504
|
+
}
|
|
1505
|
+
// An effect's cleanup `return () => …` — not run on an MCU (the app never unmounts), so drop it. (A
|
|
1506
|
+
// dep-driven effect that needs cleanup on re-run isn't supported yet; mount-only effects use this.)
|
|
1507
|
+
if (st.type === 'ReturnStatement') {
|
|
1508
|
+
if (ctx.allowReturn) continue;
|
|
1509
|
+
throw new Error('AOT: `return` is only allowed as an effect cleanup');
|
|
1510
|
+
}
|
|
1511
|
+
if (st.type === 'IfStatement') {
|
|
1512
|
+
lines.push(`${indent}if (${emitExpr(st.test, env).code})`, `${indent}{`);
|
|
1513
|
+
lines.push(...compileStmts(blockList(st.consequent), env, state, ctx, indent + ' '));
|
|
1514
|
+
lines.push(`${indent}}`);
|
|
1515
|
+
if (st.alternate) {
|
|
1516
|
+
lines.push(`${indent}else`, `${indent}{`);
|
|
1517
|
+
lines.push(...compileStmts(blockList(st.alternate), env, state, ctx, indent + ' '));
|
|
1518
|
+
lines.push(`${indent}}`);
|
|
1519
|
+
}
|
|
1520
|
+
continue;
|
|
1521
|
+
}
|
|
1522
|
+
if (st.type !== 'ExpressionStatement')
|
|
1523
|
+
throw aotError(
|
|
1524
|
+
`AOT: unsupported statement "${st.type}" in event handler`,
|
|
1525
|
+
'a handler supports only `const x = …` locals, `if (…) { … } else { … }`, and expression statements (setters / ref writes / updateVector / Animated…start). Loops (for/while), switch, try/catch, and early return are not lowered — precompute values or flatten the logic into if/else.',
|
|
1526
|
+
);
|
|
1527
|
+
lines.push(...compileHandlerExpr(st.expression, env, state, ctx, indent));
|
|
1528
|
+
}
|
|
1529
|
+
return lines;
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
/**
|
|
1533
|
+
* Compiles a useEffect (App or child) into C. Two shapes:
|
|
1534
|
+
* - `useEffect(fn, [])` — MOUNT-ONCE: body runs after the initial app_update (in er_app_build).
|
|
1535
|
+
* - `useEffect(fn, [dep…])` — DEP-DRIVEN: body becomes a file-scope `er_effect_N()`; runs once at mount,
|
|
1536
|
+
* then again from app_update whenever a SCALAR dep changes (compared against a stored prev). The body is
|
|
1537
|
+
* compiled WITHOUT a trailing app_update — it runs INSIDE app_update / at mount, so re-applying state it
|
|
1538
|
+
* sets happens on the next app_update (one-frame), and it can never re-enter app_update (no infinite loop).
|
|
1539
|
+
*/
|
|
1540
|
+
function compileEffect(eff, env, state, out) {
|
|
1541
|
+
const body = eff.fn.body;
|
|
1542
|
+
const stmts = body.type === 'BlockStatement' ? body.body : [{ type: 'ExpressionStatement', expression: body }];
|
|
1543
|
+
const isMount = !eff.deps || (eff.deps.type === 'ArrayExpression' && eff.deps.elements.length === 0);
|
|
1544
|
+
if (isMount) {
|
|
1545
|
+
const ctx = { stateChanged: false, animIdx: 0, out, allowReturn: true };
|
|
1546
|
+
const lines = compileStmts(stmts, env, state, ctx, ' ');
|
|
1547
|
+
if (ctx.stateChanged) lines.push(' app_update();');
|
|
1548
|
+
out.mountEffects.push(...lines);
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
if (eff.deps.type !== 'ArrayExpression') throw aotError('AOT: a useEffect dependency list must be an array literal', 'pass `[]` (run once) or `[a, b]` (re-run when a/b change).');
|
|
1552
|
+
const id = out.effN++;
|
|
1553
|
+
const name = `er_effect_${id}`;
|
|
1554
|
+
const deps = eff.deps.elements.map((d) => {
|
|
1555
|
+
if (!d) throw aotError('AOT: a useEffect dependency must be an expression');
|
|
1556
|
+
const e = emitExpr(d, env);
|
|
1557
|
+
if (e.cType !== 'int' && e.cType !== 'float' && e.cType !== 'string')
|
|
1558
|
+
throw aotError('AOT: useEffect dependencies must be scalar (number / bool / string)', 'depend on scalar state values; object/array dependencies are not yet supported.');
|
|
1559
|
+
return e;
|
|
1560
|
+
});
|
|
1561
|
+
const ctx = { stateChanged: false, animIdx: 0, out, allowReturn: true };
|
|
1562
|
+
out.effectFns.push({ name, body: compileStmts(stmts, env, state, ctx, ' ') });
|
|
1563
|
+
// A static "previous value" per dep; snapshot at mount, then app_update detects changes against it.
|
|
1564
|
+
deps.forEach((d, j) => out.effectDecls.push(d.cType === 'string' ? `static char s_eff${id}_d${j}[${LIST_STR_CAP}];` : `static ${d.cType === 'float' ? 'float' : 'int'} s_eff${id}_d${j};`));
|
|
1565
|
+
const snap = (j, d) => (d.cType === 'string' ? `snprintf(s_eff${id}_d${j}, sizeof(s_eff${id}_d${j}), "%s", ${d.code})` : `s_eff${id}_d${j} = ${d.code}`);
|
|
1566
|
+
out.mountEffects.push(` ${name}();`, ...deps.map((d, j) => ` ${snap(j, d)};`));
|
|
1567
|
+
const check = [' {', ' int er_changed = 0;'];
|
|
1568
|
+
deps.forEach((d, j) => {
|
|
1569
|
+
if (d.cType === 'string') check.push(` if (strcmp(s_eff${id}_d${j}, ${d.code}) != 0) { ${snap(j, d)}; er_changed = 1; }`);
|
|
1570
|
+
else {
|
|
1571
|
+
const t = d.cType === 'float' ? 'float' : 'int';
|
|
1572
|
+
check.push(` ${t} er_d${j} = ${d.code}; if (er_d${j} != s_eff${id}_d${j}) { s_eff${id}_d${j} = er_d${j}; er_changed = 1; }`);
|
|
1573
|
+
}
|
|
1574
|
+
});
|
|
1575
|
+
check.push(` if (er_changed) ${name}();`, ' }');
|
|
1576
|
+
out.depEffects.push(check.join('\n'));
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
function compileHandler(fnNode, env, state, out) {
|
|
1580
|
+
const body = fnNode.body;
|
|
1581
|
+
const list = body.type === 'BlockStatement' ? body.body : [{ type: 'ExpressionStatement', expression: body }];
|
|
1582
|
+
// The handler's first parameter is the event; `<event>.x/.y/.dx/.dy` map to EREventData fields.
|
|
1583
|
+
const eventParam = fnNode.params[0]?.type === 'Identifier' ? fnNode.params[0].name : null;
|
|
1584
|
+
const henv = eventParam ? { ...env, event: eventParam } : env;
|
|
1585
|
+
const ctx = { stateChanged: false, animIdx: 0, out };
|
|
1586
|
+
const stmts = compileStmts(list, henv, state, ctx, ' ');
|
|
1587
|
+
if (ctx.stateChanged) stmts.push(' app_update();'); // re-apply state-dependent props once
|
|
1588
|
+
return stmts;
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
// ---------------------------------------------------------------------------------------------------
|
|
1592
|
+
// Emit — control flow (components / conditionals / lists) all unroll at COMPILE TIME into fixed nodes.
|
|
1593
|
+
// Runtime-dynamic conditionals/lists (where the node COUNT changes with state) are not yet supported
|
|
1594
|
+
// and throw a clear "AOT: ..." — see /PLAN.md Flow B.
|
|
1595
|
+
// ---------------------------------------------------------------------------------------------------
|
|
1596
|
+
|
|
1597
|
+
/** Reads a component instance's props (attributes) as static values; dynamic props throw (for now). */
|
|
1598
|
+
/** Reads a component's props as descriptors: `{static:true,value}` (folded) or `{static:false,code,cType,struct}`
|
|
1599
|
+
* (a runtime C expression — e.g. a list row's `item.field`). */
|
|
1600
|
+
function extractProps(openingElement, scope, env) {
|
|
1601
|
+
const props = {};
|
|
1602
|
+
for (const attr of openingElement.attributes) {
|
|
1603
|
+
if (attr.type === 'JSXSpreadAttribute') {
|
|
1604
|
+
// Static spread: {...obj} where obj folds to a compile-time object → merge its keys as props.
|
|
1605
|
+
let obj;
|
|
1606
|
+
try {
|
|
1607
|
+
obj = evalStatic(attr.argument, scope);
|
|
1608
|
+
} catch {
|
|
1609
|
+
throw new Error('AOT: only a compile-time-constant object can be spread to a component ({...obj})');
|
|
1610
|
+
}
|
|
1611
|
+
if (obj == null || typeof obj !== 'object') throw new Error('AOT: a component spread {...x} must resolve to an object');
|
|
1612
|
+
for (const [k, v] of Object.entries(obj)) props[k] = { static: true, value: v };
|
|
1613
|
+
continue;
|
|
1614
|
+
}
|
|
1615
|
+
if (attr.type !== 'JSXAttribute' || attr.name.name === 'key') continue;
|
|
1616
|
+
const node = attrExpr(attr);
|
|
1617
|
+
// Callback prop: a function passed to a child (inline arrow, or an identifier bound to a useCallback in
|
|
1618
|
+
// the caller). Captured as a `fn` descriptor and resolved where the child uses it as an event handler.
|
|
1619
|
+
if (isFn(node)) {
|
|
1620
|
+
props[attr.name.name] = { fn: true, node };
|
|
1621
|
+
continue;
|
|
1622
|
+
}
|
|
1623
|
+
if (node.type === 'Identifier' && env.callbacks?.has(node.name)) {
|
|
1624
|
+
props[attr.name.name] = { fn: true, node: env.callbacks.get(node.name) };
|
|
1625
|
+
continue;
|
|
1626
|
+
}
|
|
1627
|
+
if (node.type === 'Identifier' && env.fnProps?.has(node.name)) {
|
|
1628
|
+
props[attr.name.name] = { fn: true, ...env.fnProps.get(node.name) }; // forward original {node, env, state}
|
|
1629
|
+
continue;
|
|
1630
|
+
}
|
|
1631
|
+
try {
|
|
1632
|
+
props[attr.name.name] = { static: true, value: evalStatic(node, scope) };
|
|
1633
|
+
} catch {
|
|
1634
|
+
props[attr.name.name] = { static: false, ...emitExpr(node, env) };
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
return props;
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
/** Maps a component's parameter to its prop descriptors (handles destructure rename + defaults). */
|
|
1641
|
+
function bindParams(fn, props) {
|
|
1642
|
+
const out = new Map();
|
|
1643
|
+
const param = fn.params[0];
|
|
1644
|
+
if (!param) return out;
|
|
1645
|
+
if (param.type === 'Identifier') {
|
|
1646
|
+
const obj = {};
|
|
1647
|
+
for (const [k, d] of Object.entries(props)) {
|
|
1648
|
+
if (!d.static) throw new Error('AOT: dynamic props require a destructured component parameter (e.g. `function C({ x })`)');
|
|
1649
|
+
obj[k] = d.value;
|
|
1650
|
+
}
|
|
1651
|
+
out.set(param.name, { static: true, value: obj });
|
|
1652
|
+
} else if (param.type === 'ObjectPattern') {
|
|
1653
|
+
for (const p of param.properties) {
|
|
1654
|
+
if (p.type === 'RestElement') throw new Error('AOT: rest props (...rest) in a component param not supported');
|
|
1655
|
+
const propName = p.key.name ?? p.key.value;
|
|
1656
|
+
const bindName = p.value?.type === 'Identifier' ? p.value.name : p.value?.type === 'AssignmentPattern' ? p.value.left.name : propName;
|
|
1657
|
+
let d = props[propName];
|
|
1658
|
+
if (!d && p.value?.type === 'AssignmentPattern') d = { static: true, value: evalStatic(p.value.right, {}) };
|
|
1659
|
+
out.set(bindName, d ?? { static: true, value: undefined });
|
|
1660
|
+
}
|
|
1661
|
+
} else {
|
|
1662
|
+
throw new Error('AOT: unsupported component parameter pattern');
|
|
1663
|
+
}
|
|
1664
|
+
return out;
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
/** True if `expr` is how a component body refers to its children (destructured {children} or props.children). */
|
|
1668
|
+
function isChildrenRef(expr, env) {
|
|
1669
|
+
const cr = env.children?.ref;
|
|
1670
|
+
if (!cr) return false;
|
|
1671
|
+
if (cr.kind === 'local') return expr.type === 'Identifier' && expr.name === cr.name;
|
|
1672
|
+
return expr.type === 'MemberExpression' && !expr.computed && expr.object.type === 'Identifier' && expr.object.name === cr.name && expr.property.name === 'children';
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
/** Inlines a function component instance: bind props (static → scope, dynamic → locals), emit its JSX.
|
|
1676
|
+
* Children passed at the call site are captured and emitted (in the CALLER's scope) where the body uses them. */
|
|
1677
|
+
function emitComponent(el, scope, out, env, state, opts) {
|
|
1678
|
+
const tag = el.openingElement.name.name;
|
|
1679
|
+
const fn = out.components.get(tag);
|
|
1680
|
+
const childNodes = el.children.filter((c) => c.type === 'JSXElement' || (c.type === 'JSXExpressionContainer' && c.expression.type !== 'JSXEmptyExpression'));
|
|
1681
|
+
|
|
1682
|
+
// How the body refers to children: destructured `{ children }` (a local) or whole `props` → props.children.
|
|
1683
|
+
const param = fn.params[0];
|
|
1684
|
+
let childrenRef = null;
|
|
1685
|
+
if (param?.type === 'ObjectPattern') {
|
|
1686
|
+
for (const p of param.properties) if ((p.key?.name ?? p.key?.value) === 'children') childrenRef = { kind: 'local', name: p.value?.name ?? 'children' };
|
|
1687
|
+
} else if (param?.type === 'Identifier') {
|
|
1688
|
+
childrenRef = { kind: 'props', name: param.name };
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
const childScope = { ...scope };
|
|
1692
|
+
const childLocals = new Map(env.locals);
|
|
1693
|
+
// Callback props bound here resolve to the CALLER's function (node + caller env/state) so the child can
|
|
1694
|
+
// use them as event handlers (onPress={onTap}); inherit any the caller itself received (forwarding).
|
|
1695
|
+
const fnProps = new Map(env.fnProps);
|
|
1696
|
+
for (const [name, d] of bindParams(fn, extractProps(el.openingElement, scope, env))) {
|
|
1697
|
+
if (childrenRef?.kind === 'local' && name === childrenRef.name) continue; // children come from the slot, not a value prop
|
|
1698
|
+
if (d.fn) fnProps.set(name, { node: d.node, env: d.env ?? env, state: d.state ?? state });
|
|
1699
|
+
else if (d.static) childScope[name] = d.value;
|
|
1700
|
+
else childLocals.set(name, { code: d.code, cType: d.cType, struct: d.struct });
|
|
1701
|
+
}
|
|
1702
|
+
const children = childNodes.length ? { nodes: childNodes, scope, env, ref: childrenRef } : null;
|
|
1703
|
+
|
|
1704
|
+
// Per-instance hooks: a child component is inlined, so EACH instance gets its OWN state, refs, animated
|
|
1705
|
+
// values, callbacks, memos and mount-effects — namespaced by a unique prefix (`c<N>_`) so two instances
|
|
1706
|
+
// (e.g. two animated <Card/>s) stay fully independent. The child sees ITS OWN hooks (not the parent's),
|
|
1707
|
+
// exactly like a React component — it receives everything else through props. All initials/values fold
|
|
1708
|
+
// against the child's static-prop scope. prefix is per-instance; the App keeps the bare (unprefixed) names.
|
|
1709
|
+
const prefix = `c${out.instN++}_`;
|
|
1710
|
+
const childAnims = collectAnims(fn.body, childScope, prefix);
|
|
1711
|
+
const childRefs = collectRefs(fn.body, childScope, prefix);
|
|
1712
|
+
const childCallbacks = collectCallbacks(fn.body);
|
|
1713
|
+
const childMemos = collectMemos(fn.body);
|
|
1714
|
+
let childState = state;
|
|
1715
|
+
if (usesState(fn)) {
|
|
1716
|
+
childState = collectState(fn.body, childScope, prefix);
|
|
1717
|
+
out.childStateRecords.push(...childState.byName.values());
|
|
1718
|
+
}
|
|
1719
|
+
out.childRefs.push(...childRefs.values());
|
|
1720
|
+
out.childAnims.push(...childAnims.values());
|
|
1721
|
+
|
|
1722
|
+
const childEnv = {
|
|
1723
|
+
...env,
|
|
1724
|
+
consts: childScope,
|
|
1725
|
+
locals: childLocals,
|
|
1726
|
+
children,
|
|
1727
|
+
fnProps,
|
|
1728
|
+
state: childState.byName,
|
|
1729
|
+
anims: childAnims,
|
|
1730
|
+
refs: childRefs,
|
|
1731
|
+
callbacks: childCallbacks,
|
|
1732
|
+
helpers: collectHelpers(fn.body, out.program),
|
|
1733
|
+
cbPrefix: prefix,
|
|
1734
|
+
};
|
|
1735
|
+
|
|
1736
|
+
// Resolve the child's memos in declaration order (fold to a const, else inline as a local C expr), then
|
|
1737
|
+
// run its mount-once useEffect(fn, []) bodies — both compiled in the child's own env/state.
|
|
1738
|
+
for (const [name, expr] of childMemos) {
|
|
1739
|
+
try {
|
|
1740
|
+
childScope[name] = evalStatic(expr, childScope);
|
|
1741
|
+
} catch {
|
|
1742
|
+
const e = emitExpr(expr, childEnv);
|
|
1743
|
+
childLocals.set(name, { code: `(${e.code})`, cType: e.cType });
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
for (const eff of collectEffects(fn.body)) {
|
|
1747
|
+
compileEffect(eff, childEnv, childState, out);
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
return emitNode(componentReturnJSX(fn, childScope), childScope, out, childEnv, childState, opts);
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
/** Emits an element / component child and appends it to the parent. opts.displayCode toggles its show. */
|
|
1754
|
+
function emitElementInto(node, parentVar, scope, out, env, state, opts) {
|
|
1755
|
+
if (node.type !== 'JSXElement') throw new Error(`AOT: expected a JSX element here, got ${node.type}`);
|
|
1756
|
+
const cv = emitNode(node, scope, out, env, state, opts);
|
|
1757
|
+
out.build.push(` er_tree_append_child(${parentVar}, ${cv});`);
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
/** Unrolls `arr.map((item, i) => <JSX/>)` over a COMPILE-TIME-CONSTANT array. */
|
|
1761
|
+
function emitMap(call, parentVar, scope, out, env, state) {
|
|
1762
|
+
let arr;
|
|
1763
|
+
try {
|
|
1764
|
+
arr = evalStatic(call.callee.object, scope);
|
|
1765
|
+
} catch {
|
|
1766
|
+
throw new Error('AOT: .map target must be a compile-time-constant array (dynamic lists not yet supported)');
|
|
1767
|
+
}
|
|
1768
|
+
if (!Array.isArray(arr)) throw new Error('AOT: .map target did not resolve to an array');
|
|
1769
|
+
const cb = call.arguments[0];
|
|
1770
|
+
if (!isFn(cb)) throw new Error('AOT: .map argument must be an inline function');
|
|
1771
|
+
const itemName = cb.params[0]?.name;
|
|
1772
|
+
const idxName = cb.params[1]?.name;
|
|
1773
|
+
const retJSX = componentReturnJSX(cb);
|
|
1774
|
+
arr.forEach((item, i) => {
|
|
1775
|
+
const iterScope = { ...scope };
|
|
1776
|
+
if (itemName) iterScope[itemName] = item;
|
|
1777
|
+
if (idxName) iterScope[idxName] = i;
|
|
1778
|
+
emitElementInto(retJSX, parentVar, iterScope, out, { ...env, consts: iterScope }, state);
|
|
1779
|
+
});
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
/**
|
|
1783
|
+
* `{listState.map((item, i) => <Row/>)}` over a STATE array of variable length. Pre-allocates a fixed
|
|
1784
|
+
* pool of `cap` rows (no runtime malloc); each row k binds `item` to `s_<name>[k]` (a struct local) and
|
|
1785
|
+
* is shown only while `k < count` (display toggle). app_update then drives every row's content and show.
|
|
1786
|
+
*/
|
|
1787
|
+
function emitDynamicMap(call, rec, parentVar, scope, out, env, state) {
|
|
1788
|
+
const cb = call.arguments[0];
|
|
1789
|
+
if (!isFn(cb)) throw new Error('AOT: .map argument must be an inline function');
|
|
1790
|
+
const itemName = cb.params[0]?.name;
|
|
1791
|
+
const idxName = cb.params[1]?.name;
|
|
1792
|
+
const retJSX = componentReturnJSX(cb);
|
|
1793
|
+
for (let k = 0; k < rec.cap; k++) {
|
|
1794
|
+
const iterScope = { ...scope };
|
|
1795
|
+
if (idxName) iterScope[idxName] = k; // the index is a compile-time literal per pooled row
|
|
1796
|
+
const locals = new Map(env.locals);
|
|
1797
|
+
if (itemName) locals.set(itemName, { code: `${rec.arrayName}[${k}]`, struct: rec.struct });
|
|
1798
|
+
emitElementInto(retJSX, parentVar, iterScope, out, { ...env, consts: iterScope, locals }, state, { displayCode: `(${k} < ${rec.countMember})` });
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
/** Emits the children of a container node, handling element + {expression} children. */
|
|
1803
|
+
function emitChildren(children, parentVar, scope, out, env, state) {
|
|
1804
|
+
for (const child of children) {
|
|
1805
|
+
if (child.type === 'JSXElement') {
|
|
1806
|
+
emitElementInto(child, parentVar, scope, out, env, state);
|
|
1807
|
+
} else if (child.type === 'JSXExpressionContainer') {
|
|
1808
|
+
const expr = child.expression;
|
|
1809
|
+
if (expr.type === 'JSXEmptyExpression') continue;
|
|
1810
|
+
if (isChildrenRef(expr, env)) {
|
|
1811
|
+
// {children} / {props.children}: emit the captured call-site children, in the caller's scope/env.
|
|
1812
|
+
emitChildren(env.children.nodes, parentVar, env.children.scope, out, env.children.env, state);
|
|
1813
|
+
continue;
|
|
1814
|
+
}
|
|
1815
|
+
if (expr.type === 'LogicalExpression' && expr.operator === '&&') {
|
|
1816
|
+
// `{cond && <X/>}`. Static cond → include/omit at compile time. Dynamic (state) cond → always
|
|
1817
|
+
// build X but toggle its display (none/flex) in app_update — show/hide without node churn.
|
|
1818
|
+
let cond;
|
|
1819
|
+
try {
|
|
1820
|
+
cond = evalStatic(expr.left, scope);
|
|
1821
|
+
if (cond) emitElementInto(expr.right, parentVar, scope, out, env, state);
|
|
1822
|
+
} catch {
|
|
1823
|
+
const code = emitExpr(expr.left, env).code;
|
|
1824
|
+
emitElementInto(expr.right, parentVar, scope, out, env, state, { displayCode: code });
|
|
1825
|
+
}
|
|
1826
|
+
} else if (expr.type === 'ConditionalExpression' && (expr.consequent.type === 'JSXElement' || expr.alternate.type === 'JSXElement')) {
|
|
1827
|
+
// `{cond ? <A/> : <B/>}`. Static cond picks a branch; dynamic cond builds both and toggles each.
|
|
1828
|
+
let test;
|
|
1829
|
+
try {
|
|
1830
|
+
test = evalStatic(expr.test, scope);
|
|
1831
|
+
emitElementInto(test ? expr.consequent : expr.alternate, parentVar, scope, out, env, state);
|
|
1832
|
+
} catch {
|
|
1833
|
+
const code = emitExpr(expr.test, env).code;
|
|
1834
|
+
if (expr.consequent.type === 'JSXElement') emitElementInto(expr.consequent, parentVar, scope, out, env, state, { displayCode: code });
|
|
1835
|
+
if (expr.alternate.type === 'JSXElement') emitElementInto(expr.alternate, parentVar, scope, out, env, state, { displayCode: `!(${code})` });
|
|
1836
|
+
}
|
|
1837
|
+
} else if (expr.type === 'CallExpression' && expr.callee.type === 'MemberExpression' && expr.callee.property.name === 'map') {
|
|
1838
|
+
const obj = expr.callee.object;
|
|
1839
|
+
const rec = obj.type === 'Identifier' ? env.state.get(obj.name) : null;
|
|
1840
|
+
if (rec?.kind === 'list') emitDynamicMap(expr, rec, parentVar, scope, out, env, state);
|
|
1841
|
+
else emitMap(expr, parentVar, scope, out, env, state);
|
|
1842
|
+
} else {
|
|
1843
|
+
// A constant that renders nothing (false/null/'') is fine; anything else is unsupported.
|
|
1844
|
+
let v;
|
|
1845
|
+
try {
|
|
1846
|
+
v = evalStatic(expr, scope);
|
|
1847
|
+
} catch {
|
|
1848
|
+
const e = aotError(
|
|
1849
|
+
`AOT: unsupported expression child "${expr.type}" in a container`,
|
|
1850
|
+
'a child expression must be a JSX element, `cond && <El/>` / a ternary of elements, or `list.map(item => <El/>)`. A bare variable holding JSX isn\'t inlined — write the element directly. (If this is a responsive Flow-A-only branch, compile at the target board size via ER_AOT_SCREEN_W/H so the AOT folds the supported branch.)',
|
|
1851
|
+
);
|
|
1852
|
+
if (expr.loc) e.aotLoc = expr.loc.start;
|
|
1853
|
+
throw e;
|
|
1854
|
+
}
|
|
1855
|
+
if (v !== false && v != null && v !== '') {
|
|
1856
|
+
const e = aotError(`AOT: a non-element expression child (${JSON.stringify(v)}) cannot render here`, 'only JSX elements render as children; wrap text in a <Text>{…}</Text>.');
|
|
1857
|
+
if (expr.loc) e.aotLoc = expr.loc.start;
|
|
1858
|
+
throw e;
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
// ---------------------------------------------------------------------------------------------------
|
|
1866
|
+
// Vector / Svg — a static <Svg> subtree is converted to flattenSvg()'s element shape (the same converter
|
|
1867
|
+
// Flow A uses), giving a flat {ops, paints}. We bake those into C const arrays + er_node_set_vector_ops.
|
|
1868
|
+
// ---------------------------------------------------------------------------------------------------
|
|
1869
|
+
|
|
1870
|
+
/** Converts an SVG JSX element (Svg/Circle/Path/Rect/Line/Arc/G/…) to flattenSvg's `{type, props}` shape,
|
|
1871
|
+
* statically evaluating every attribute. Dynamic attrs/children throw (state-driven Svg is Phase 6b). */
|
|
1872
|
+
function jsxToSvgElement(node, scope) {
|
|
1873
|
+
if (node.type !== 'JSXElement') return null;
|
|
1874
|
+
const type = node.openingElement.name.name;
|
|
1875
|
+
const props = {};
|
|
1876
|
+
for (const attr of node.openingElement.attributes) {
|
|
1877
|
+
if (attr.type !== 'JSXAttribute') throw new Error('AOT: spread attributes on an <Svg> element not supported');
|
|
1878
|
+
const name = attr.name.name;
|
|
1879
|
+
if (name === 'ref' || name === 'key') continue; // not geometry/paint
|
|
1880
|
+
if (attr.value == null) props[name] = true;
|
|
1881
|
+
else if (attr.value.type === 'StringLiteral') props[name] = attr.value.value;
|
|
1882
|
+
else if (attr.value.type === 'JSXExpressionContainer') props[name] = evalStatic(attr.value.expression, scope);
|
|
1883
|
+
else throw new Error(`AOT: unsupported <${type}> attribute value for "${name}"`);
|
|
1884
|
+
}
|
|
1885
|
+
const children = [];
|
|
1886
|
+
for (const c of node.children) {
|
|
1887
|
+
if (c.type === 'JSXElement') children.push(jsxToSvgElement(c, scope));
|
|
1888
|
+
else if (c.type === 'JSXExpressionContainer' && c.expression.type !== 'JSXEmptyExpression') throw new Error('AOT: dynamic <Svg> children ({…}) not yet supported — use literal shape elements');
|
|
1889
|
+
}
|
|
1890
|
+
if (children.length) props.children = children;
|
|
1891
|
+
return { type, props };
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
/** Emits one ERVectorPaint initializer from a 7-number flattenSvg paint record [fill,stroke,w,miter,cap,join,rule]. */
|
|
1895
|
+
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} }`;
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
/** Static numeric coercion for an SVG attribute value (mirrors svg-ops `num`). */
|
|
1900
|
+
const svgNum = (v, d = 0) => {
|
|
1901
|
+
const n = typeof v === 'number' ? v : parseFloat(v);
|
|
1902
|
+
return Number.isNaN(n) ? d : n;
|
|
1903
|
+
};
|
|
1904
|
+
/** True if an attribute value is a state-driven C expression (vs a static number/string). */
|
|
1905
|
+
const isDyn = (v) => v != null && typeof v === 'object' && 'dyn' in v;
|
|
1906
|
+
/** Lowers an SVG coordinate attr to a C float expression (literal when static, cast expr when dynamic). */
|
|
1907
|
+
const cf = (v, d = 0) => (isDyn(v) ? `(float)(${v.dyn})` : floatLit(svgNum(v, d)));
|
|
1908
|
+
|
|
1909
|
+
/** Reads an SVG element's attributes → { name: number|string|true | {dyn: cExpr} } (state attrs → {dyn}). */
|
|
1910
|
+
function svgAttrs(openingElement, scope, env) {
|
|
1911
|
+
const out = {};
|
|
1912
|
+
for (const attr of openingElement.attributes) {
|
|
1913
|
+
if (attr.type !== 'JSXAttribute') throw new Error('AOT: spread attributes on an <Svg> element not supported');
|
|
1914
|
+
const name = attr.name.name;
|
|
1915
|
+
if (name === 'ref' || name === 'key') continue; // not geometry/paint
|
|
1916
|
+
const vn = attr.value;
|
|
1917
|
+
if (vn == null) out[name] = true;
|
|
1918
|
+
else if (vn.type === 'StringLiteral') out[name] = vn.value;
|
|
1919
|
+
else if (vn.type === 'JSXExpressionContainer') {
|
|
1920
|
+
try {
|
|
1921
|
+
out[name] = evalStatic(vn.expression, scope);
|
|
1922
|
+
} catch {
|
|
1923
|
+
// Keep the raw expression node too: color paint attrs (fill/stroke) lower via emitColorExpr (→ ARGB),
|
|
1924
|
+
// not the generic numeric `dyn` code, so a dynamic color resolves to a uint, not a char*.
|
|
1925
|
+
out[name] = { dyn: emitExpr(vn.expression, env).code, node: vn.expression };
|
|
1926
|
+
}
|
|
1927
|
+
} else throw new Error(`AOT: unsupported SVG attribute value for "${name}"`);
|
|
1928
|
+
}
|
|
1929
|
+
return out;
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
const CAP_MAP = { butt: 0, round: 1, square: 2 };
|
|
1933
|
+
const JOIN_MAP = { miter: 0, round: 1, bevel: 2 };
|
|
1934
|
+
|
|
1935
|
+
/** The ERVectorPaint members, in op-tape paint order [fill,stroke,stroke_w,miter,cap,join,fill_rule]. */
|
|
1936
|
+
const PAINT_FIELDS = ['fill', 'stroke', 'stroke_w', 'miter', 'cap', 'join', 'fill_rule'];
|
|
1937
|
+
|
|
1938
|
+
/**
|
|
1939
|
+
* A shape's paint as 7 C-expr fields (matching PAINT_FIELDS) + whether any is state-driven.
|
|
1940
|
+
* - fill / stroke may be DYNAMIC → lowered via emitColorExpr to an ARGB uint expr (a color string, a
|
|
1941
|
+
* ternary of them, or a folded theme token); static → a baked `0xAARRGGBBu` literal.
|
|
1942
|
+
* - strokeWidth may be DYNAMIC (numeric C expr); static → a float literal.
|
|
1943
|
+
* - cap / join / miterlimit / fillRule must be STATIC (a dynamic one throws clear).
|
|
1944
|
+
*/
|
|
1945
|
+
function paintSpec(a, env) {
|
|
1946
|
+
let anyDynamic = false;
|
|
1947
|
+
const color = (v, dflt) => {
|
|
1948
|
+
if (isDyn(v)) {
|
|
1949
|
+
anyDynamic = true;
|
|
1950
|
+
return emitColorExpr(v.node, env);
|
|
1951
|
+
}
|
|
1952
|
+
return `${parseColor(v ?? dflt) >>> 0}u`;
|
|
1953
|
+
};
|
|
1954
|
+
let strokeW;
|
|
1955
|
+
if (isDyn(a.strokeWidth)) {
|
|
1956
|
+
anyDynamic = true;
|
|
1957
|
+
strokeW = `(float)(${a.strokeWidth.dyn})`;
|
|
1958
|
+
} else strokeW = floatLit(svgNum(a.strokeWidth, 1));
|
|
1959
|
+
for (const k of ['strokeLinecap', 'strokeLinejoin', 'strokeMiterlimit', 'fillRule'])
|
|
1960
|
+
if (isDyn(a[k])) throw new Error(`AOT: a state-driven <Svg> "${k}" is not supported (only fill / stroke / strokeWidth can be state-driven)`);
|
|
1961
|
+
const fields = [color(a.fill, 'black'), color(a.stroke, 'none'), strokeW, floatLit(svgNum(a.strokeMiterlimit, 4)), String(CAP_MAP[a.strokeLinecap] ?? 0), String(JOIN_MAP[a.strokeLinejoin] ?? 0), String(a.fillRule === 'evenodd' ? 1 : 0)];
|
|
1962
|
+
return { fields, anyDynamic };
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
/** A `{ .fill = …, … }` ERVectorPaint initializer from a paintSpec's C-expr fields (used for static paints). */
|
|
1966
|
+
function paintInitFromSpec(ps) {
|
|
1967
|
+
return `{ ${PAINT_FIELDS.map((f, i) => `.${f} = ${ps.fields[i]}`).join(', ')} }`;
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
// Per-shape op-tape entries (C-expr strings; opcodes as ER_VOP_* macros). The ...C base fns take resolved
|
|
1971
|
+
// C-float expressions, so both the JSX path (6b, via cf) and the imperative updateVector path (6c, via
|
|
1972
|
+
// arrays) share the geometry. Mirror svg-ops circleOps / arcOpsCW / etc.
|
|
1973
|
+
const arcEntriesC = (cx, cy, r, a0deg, a1deg) => {
|
|
1974
|
+
const a0 = `((${a0deg} - 90.0f) * (float)M_PI / 180.0f)`;
|
|
1975
|
+
const a1 = `((${a1deg} - 90.0f) * (float)M_PI / 180.0f)`;
|
|
1976
|
+
return ['ER_VOP_MOVE', `(${cx} + ${r} * cosf(${a0}))`, `(${cy} + ${r} * sinf(${a0}))`, 'ER_VOP_ARC', cx, cy, r, a0, a1, '0.0f'];
|
|
1977
|
+
};
|
|
1978
|
+
const circleEntriesC = (cx, cy, r) => ['ER_VOP_MOVE', `(${cx} + ${r})`, cy, 'ER_VOP_ARC', cx, cy, r, '0.0f', '(2.0f * (float)M_PI)', '0.0f', 'ER_VOP_CLOSE'];
|
|
1979
|
+
const rectEntriesC = (x, y, w, h) => ['ER_VOP_MOVE', x, y, 'ER_VOP_LINE', `(${x} + ${w})`, y, 'ER_VOP_LINE', `(${x} + ${w})`, `(${y} + ${h})`, 'ER_VOP_LINE', x, `(${y} + ${h})`, 'ER_VOP_CLOSE'];
|
|
1980
|
+
const lineEntriesC = (x1, y1, x2, y2) => ['ER_VOP_MOVE', x1, y1, 'ER_VOP_LINE', x2, y2];
|
|
1981
|
+
|
|
1982
|
+
// JSX-attribute wrappers (6b): resolve each attr to a C float via cf().
|
|
1983
|
+
const arcEntries = (a) => arcEntriesC(cf(a.cx), cf(a.cy), cf(a.r), cf(a.startAngle), cf(a.endAngle));
|
|
1984
|
+
const circleEntries = (a) => circleEntriesC(cf(a.cx), cf(a.cy), cf(a.r));
|
|
1985
|
+
const rectEntries = (a) => rectEntriesC(cf(a.x), cf(a.y), cf(a.width), cf(a.height));
|
|
1986
|
+
const lineEntries = (a) => lineEntriesC(cf(a.x1), cf(a.y1), cf(a.x2), cf(a.y2));
|
|
1987
|
+
const pathEntries = (a) => {
|
|
1988
|
+
if (a.d == null) return [];
|
|
1989
|
+
if (isDyn(a.d)) throw new Error('AOT: a state-driven <Path d=…> is not yet supported (use Arc/Circle/Rect/Line for dynamic shapes)');
|
|
1990
|
+
return parsePath(String(a.d)).map(floatLit); // opcodes are encoded as float values 0..6, like coords
|
|
1991
|
+
};
|
|
1992
|
+
const SHAPE_ENTRIES = { Arc: arcEntries, Circle: circleEntries, Rect: rectEntries, Line: lineEntries, Path: pathEntries };
|
|
1993
|
+
|
|
1994
|
+
/** True if any attribute anywhere in the <Svg> subtree references state (→ the state-driven path). */
|
|
1995
|
+
function svgHasDynamic(el, scope) {
|
|
1996
|
+
let dyn = false;
|
|
1997
|
+
const walk = (node) => {
|
|
1998
|
+
if (node.type !== 'JSXElement') return;
|
|
1999
|
+
for (const attr of node.openingElement.attributes) {
|
|
2000
|
+
if (attr.type === 'JSXAttribute' && attr.name.name !== 'ref' && attr.name.name !== 'key' && attr.value?.type === 'JSXExpressionContainer') {
|
|
2001
|
+
try {
|
|
2002
|
+
evalStatic(attr.value.expression, scope);
|
|
2003
|
+
} catch {
|
|
2004
|
+
dyn = true;
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
for (const c of node.children) walk(c);
|
|
2009
|
+
};
|
|
2010
|
+
walk(el);
|
|
2011
|
+
return dyn;
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
/** Emits the vector node's box: create + props + width/height + optional style={}. */
|
|
2015
|
+
function emitSvgBox(v, width, height, openingElement, scope, out, env) {
|
|
2016
|
+
const { staticAssigns } = collectStyleAssigns(openingElement, scope, env);
|
|
2017
|
+
out.build.push(` ${v} = er_node_create(ER_NODE_VECTOR);`, ` er_props_default(&p);`);
|
|
2018
|
+
if (typeof width === 'number') out.build.push(` p.width = (int16_t)${Math.round(width)};`);
|
|
2019
|
+
if (typeof height === 'number') out.build.push(` p.height = (int16_t)${Math.round(height)};`);
|
|
2020
|
+
for (const a of staticAssigns) out.build.push(` p.${a.field} = ${a.expr};`);
|
|
2021
|
+
out.build.push(` er_node_set_props(${v}, &p);`);
|
|
2022
|
+
emitRefBind(v, openingElement, out, env);
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
/** <Svg> → ER_NODE_VECTOR. Static subtree → a baked const op-tape; any state-driven attr → a symbolic
|
|
2026
|
+
* op-tape rebuilt by a generated build_svgN() at build time and on every app_update. */
|
|
2027
|
+
function emitSvg(el, scope, out, env, state, opts) {
|
|
2028
|
+
if (opts.displayCode) throw new Error('AOT: an <Svg> inside a dynamic conditional is not yet supported');
|
|
2029
|
+
return svgHasDynamic(el, scope) ? emitSvgDynamic(el, scope, out, env, state) : emitSvgStatic(el, scope, out, env);
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
/** Static <Svg>: reuse flattenSvg (full feature set: viewBox, <G>, Path) and bake const arrays. */
|
|
2033
|
+
function emitSvgStatic(el, scope, out, env) {
|
|
2034
|
+
const svgEl = jsxToSvgElement(el, scope);
|
|
2035
|
+
const { ops, paints } = flattenSvg(svgEl.props);
|
|
2036
|
+
const v = `n${out.n++}`;
|
|
2037
|
+
const id = out.svgN++;
|
|
2038
|
+
const nPaints = paints.length / 7;
|
|
2039
|
+
if (ops.length) {
|
|
2040
|
+
out.vectorData.push(`static const float s_svg${id}_ops[] = {\n ${Array.from(ops, floatLit).join(', ')}\n};`);
|
|
2041
|
+
out.vectorData.push(`static const ERVectorPaint s_svg${id}_paints[] = {\n${Array.from({ length: nPaints }, (_, i) => ' ' + emitVectorPaint(paints.slice(i * 7, i * 7 + 7))).join(',\n')}\n};`);
|
|
2042
|
+
}
|
|
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
|
+
return v;
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
/** State-driven <Svg> (flat Arc/Circle/Rect/Line/static-Path; no viewBox/<G>): emit a mutable op-tape
|
|
2049
|
+
* + build_svgN() that recomputes it from state, called at build and re-called on each app_update. */
|
|
2050
|
+
function emitSvgDynamic(el, scope, out, env, state) {
|
|
2051
|
+
const svgA = svgAttrs(el.openingElement, scope, env);
|
|
2052
|
+
if (svgA.viewBox != null) throw new Error('AOT: a viewBox on a state-driven <Svg> is not yet supported — size shapes in the width/height space');
|
|
2053
|
+
const entries = [];
|
|
2054
|
+
const specs = [];
|
|
2055
|
+
for (const c of el.children) {
|
|
2056
|
+
if (c.type !== 'JSXElement') continue;
|
|
2057
|
+
const type = c.openingElement.name.name;
|
|
2058
|
+
const fn = SHAPE_ENTRIES[type];
|
|
2059
|
+
if (!fn) throw new Error(`AOT: <${type}> is not a supported shape in a state-driven <Svg> (no <G>/viewBox yet)`);
|
|
2060
|
+
const a = svgAttrs(c.openingElement, scope, env);
|
|
2061
|
+
const shape = fn(a);
|
|
2062
|
+
if (!shape.length) continue;
|
|
2063
|
+
entries.push('ER_VOP_SHAPE', floatLit(specs.length), ...shape);
|
|
2064
|
+
specs.push(paintSpec(a, env));
|
|
2065
|
+
}
|
|
2066
|
+
const v = `n${out.n++}`;
|
|
2067
|
+
const id = out.svgN++;
|
|
2068
|
+
const len = entries.length;
|
|
2069
|
+
const nPaints = specs.length;
|
|
2070
|
+
const dynPaint = specs.some((p) => p.anyDynamic);
|
|
2071
|
+
out.needsMath = true; // build_svg uses cosf/sinf/M_PI for arcs
|
|
2072
|
+
out.vectorData.push(`static float s_svg${id}_ops[${len}];`);
|
|
2073
|
+
// Dynamic paint → a MUTABLE paint table (re)filled by build_svg from state each update; else a const table.
|
|
2074
|
+
if (dynPaint) out.vectorData.push(`static ERVectorPaint s_svg${id}_paints[${nPaints}];`);
|
|
2075
|
+
else out.vectorData.push(`static const ERVectorPaint s_svg${id}_paints[] = {\n${specs.map((p) => ' ' + paintInitFromSpec(p)).join(',\n')}\n};`);
|
|
2076
|
+
const builderLines = entries.map((e, i) => ` s_svg${id}_ops[${i}] = ${e};`);
|
|
2077
|
+
if (dynPaint) specs.forEach((ps, pi) => ps.fields.forEach((f, fi) => builderLines.push(` s_svg${id}_paints[${pi}].${PAINT_FIELDS[fi]} = ${f};`)));
|
|
2078
|
+
out.vectorBuilders.push(`static void build_svg${id}(void)\n{\n${builderLines.join('\n')}\n}`);
|
|
2079
|
+
|
|
2080
|
+
emitSvgBox(v, svgA.width, svgA.height, el.openingElement, scope, out, env);
|
|
2081
|
+
out.build.push(` build_svg${id}();`, ` er_node_set_vector_ops(${v}, s_svg${id}_ops, ${len}, s_svg${id}_paints, ${nPaints});`, ` s_${v} = ${v};`);
|
|
2082
|
+
out.handles.push(v);
|
|
2083
|
+
out.svgUpdates.push({ id, len, nPaints, nodeVar: `s_${v}` });
|
|
2084
|
+
return v;
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
// ---------------------------------------------------------------------------------------------------
|
|
2088
|
+
// Node emitters — typed components. Each maps one JSX element to its engine node + props: the shared
|
|
2089
|
+
// helpers (emitRefBind, compileValueHandler) then Switch / TextInput / ActivityIndicator / Modal /
|
|
2090
|
+
// FlatList. The generic host node (View/Text/Pressable/Image/ScrollView) + the element dispatcher live
|
|
2091
|
+
// in the next section (emitNodeImpl).
|
|
2092
|
+
// ---------------------------------------------------------------------------------------------------
|
|
2093
|
+
|
|
2094
|
+
/** Captures `ref={r}` (r a node ref) by storing the freshly-created node handle into the ref's slot. */
|
|
2095
|
+
function emitRefBind(v, openingElement, out, env) {
|
|
2096
|
+
for (const attr of openingElement.attributes) {
|
|
2097
|
+
if (attr.type !== 'JSXAttribute' || attr.name.name !== 'ref') continue;
|
|
2098
|
+
const e = attr.value?.type === 'JSXExpressionContainer' ? attr.value.expression : null;
|
|
2099
|
+
if (e?.type === 'Identifier' && env.refs?.get(e.name)?.kind === 'node') out.build.push(` ${env.refs.get(e.name).cVar} = ${v};`);
|
|
2100
|
+
else throw new Error('AOT: ref={…} must reference a node ref declared with useRef()');
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
/** Compiles a value-callback (e.g. Switch onValueChange) — binds its first param to `valueCode`, not an event. */
|
|
2105
|
+
function compileValueHandler(fnNode, valueCode, env, state, out, cType = 'int') {
|
|
2106
|
+
const param = fnNode.params[0]?.type === 'Identifier' ? fnNode.params[0].name : null;
|
|
2107
|
+
const locals = new Map(env.locals);
|
|
2108
|
+
if (param) locals.set(param, { code: valueCode, cType });
|
|
2109
|
+
const ctx = { stateChanged: false, animIdx: 0, out };
|
|
2110
|
+
const body = fnNode.body;
|
|
2111
|
+
const list = body.type === 'BlockStatement' ? body.body : [{ type: 'ExpressionStatement', expression: body }];
|
|
2112
|
+
const stmts = compileStmts(list, { ...env, locals }, state, ctx, ' ');
|
|
2113
|
+
if (ctx.stateChanged) stmts.push(' app_update();');
|
|
2114
|
+
return stmts;
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
/**
|
|
2118
|
+
* <Switch value={on} onValueChange={(v) => setOn(v)} trackColor={{false,true}} thumbColor=… style=… />
|
|
2119
|
+
* → ER_NODE_SWITCH. The engine flips its own value on press (+ animates the thumb) then fires ER_EVENT_PRESS,
|
|
2120
|
+
* so onValueChange maps to PRESS and its `v` param is the TOGGLED value (!value). `value` drives switch_value
|
|
2121
|
+
* (state → dynamic). Default RN 51×31 box (the renderer scales the track/thumb to it); style can override.
|
|
2122
|
+
*/
|
|
2123
|
+
function emitSwitch(el, scope, out, env, state) {
|
|
2124
|
+
const v = `n${out.n++}`;
|
|
2125
|
+
const { staticAssigns, dynAssigns } = collectStyleAssigns(el.openingElement, scope, env);
|
|
2126
|
+
const hasField = (f) => staticAssigns.some((a) => a.field === f) || dynAssigns.some((a) => a.field === f);
|
|
2127
|
+
if (!hasField('width')) staticAssigns.push({ field: 'width', expr: '51' });
|
|
2128
|
+
if (!hasField('height')) staticAssigns.push({ field: 'height', expr: '31' });
|
|
2129
|
+
|
|
2130
|
+
let valueNode = null;
|
|
2131
|
+
let onChangeFn = null;
|
|
2132
|
+
for (const attr of el.openingElement.attributes) {
|
|
2133
|
+
if (attr.type !== 'JSXAttribute') throw aotError('AOT: spread props on <Switch> are not supported');
|
|
2134
|
+
const name = attr.name.name;
|
|
2135
|
+
if (name === 'style' || name === 'ref' || name === 'key') continue;
|
|
2136
|
+
const node = attrExpr(attr);
|
|
2137
|
+
if (name === 'value') valueNode = node;
|
|
2138
|
+
else if (name === 'onValueChange') onChangeFn = node;
|
|
2139
|
+
else if (name === 'thumbColor') staticAssigns.push({ field: 'thumb_color', expr: colorLiteral(String(evalStatic(node, scope))) });
|
|
2140
|
+
else if (name === 'trackColor') {
|
|
2141
|
+
const tc = evalStatic(node, scope);
|
|
2142
|
+
if (tc?.false != null) staticAssigns.push({ field: 'track_color_false', expr: colorLiteral(String(tc.false)) });
|
|
2143
|
+
if (tc?.true != null) staticAssigns.push({ field: 'track_color_true', expr: colorLiteral(String(tc.true)) });
|
|
2144
|
+
} else if (name === 'disabled') {
|
|
2145
|
+
/* accepted; the AOT has no disabled-visual yet, so it is a no-op */
|
|
2146
|
+
} else throw aotError(`AOT: <Switch> prop "${name}" is not supported`, 'supported props: value, onValueChange, trackColor, thumbColor, style.');
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
// value → switch_value (static or, when state-driven, recomputed in app_update).
|
|
2150
|
+
if (valueNode) {
|
|
2151
|
+
try {
|
|
2152
|
+
staticAssigns.push({ field: 'switch_value', expr: evalStatic(valueNode, scope) ? '1' : '0' });
|
|
2153
|
+
} catch {
|
|
2154
|
+
dynAssigns.push({ field: 'switch_value', code: `(uint8_t)((${emitExpr(valueNode, env).code}) ? 1 : 0)` });
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
const isDynamic = dynAssigns.length > 0;
|
|
2159
|
+
out.build.push(` ${v} = er_node_create(ER_NODE_SWITCH);`);
|
|
2160
|
+
if (isDynamic) {
|
|
2161
|
+
out.build.push(` s_${v} = ${v};`);
|
|
2162
|
+
out.handles.push(v);
|
|
2163
|
+
out.updates.push({ v, styleAssigns: staticAssigns, text: null, dynAssigns });
|
|
2164
|
+
} else {
|
|
2165
|
+
out.build.push(` er_props_default(&p);`);
|
|
2166
|
+
for (const a of staticAssigns) out.build.push(` p.${a.field} = ${a.expr};`);
|
|
2167
|
+
out.build.push(` er_node_set_props(${v}, &p);`);
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
if (onChangeFn) {
|
|
2171
|
+
if (!isFn(onChangeFn)) throw aotError('AOT: onValueChange must be an inline function', 'onValueChange={(v) => setX(v)}');
|
|
2172
|
+
if (!valueNode) throw aotError('AOT: a <Switch> with onValueChange needs a value prop', 'controlled switch: <Switch value={on} onValueChange={(v) => setOn(v)} />');
|
|
2173
|
+
const handlerName = `er_handler_${out.handlers.length}`;
|
|
2174
|
+
const toggled = `(!(${emitExpr(valueNode, env).code}))`; // the engine toggles on press → param is !value
|
|
2175
|
+
out.handlers.push({ name: handlerName, body: compileValueHandler(onChangeFn, toggled, env, state, out) });
|
|
2176
|
+
out.build.push(` er_event_set(${v}, ER_EVENT_PRESS, ${handlerName}, NULL);`);
|
|
2177
|
+
}
|
|
2178
|
+
emitRefBind(v, el.openingElement, out, env);
|
|
2179
|
+
return v;
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
/**
|
|
2183
|
+
* <TextInput value={text} onChangeText={(t) => setText(t)} placeholder="…" placeholderTextColor=… style=… />
|
|
2184
|
+
* → ER_NODE_TEXT_INPUT. The engine auto-focuses on tap (hit_test) and edits its own buffer, firing
|
|
2185
|
+
* ER_EVENT_CHANGE_TEXT with the new text — bound to the handler's param via `data->changed_text` (a string).
|
|
2186
|
+
* `value` drives the text buffer (er_node_set_props → er_text_input_set_text; state → dynamic, re-synced in
|
|
2187
|
+
* app_update; set_text is a no-op when unchanged, so a controlled input is safe). Desktop types via the
|
|
2188
|
+
* keyboard; the touch-only CYD needs an on-screen keyboard to enter text (deferred follow-on).
|
|
2189
|
+
*/
|
|
2190
|
+
function emitTextInput(el, scope, out, env, state) {
|
|
2191
|
+
const v = `n${out.n++}`;
|
|
2192
|
+
const { staticAssigns, dynAssigns } = collectStyleAssigns(el.openingElement, scope, env);
|
|
2193
|
+
let valueNode = null;
|
|
2194
|
+
let onChangeFn = null;
|
|
2195
|
+
let placeholder = null;
|
|
2196
|
+
for (const attr of el.openingElement.attributes) {
|
|
2197
|
+
if (attr.type !== 'JSXAttribute') throw aotError('AOT: spread props on <TextInput> are not supported');
|
|
2198
|
+
const name = attr.name.name;
|
|
2199
|
+
if (name === 'style' || name === 'ref' || name === 'key') continue;
|
|
2200
|
+
const node = attrExpr(attr);
|
|
2201
|
+
if (name === 'value' || name === 'defaultValue') valueNode = node;
|
|
2202
|
+
else if (name === 'onChangeText') onChangeFn = node;
|
|
2203
|
+
else if (name === 'placeholder') placeholder = String(evalStatic(node, scope));
|
|
2204
|
+
else if (name === 'placeholderTextColor') staticAssigns.push({ field: 'placeholder_color', expr: colorLiteral(String(evalStatic(node, scope))) });
|
|
2205
|
+
else if (name === 'cursorColor') staticAssigns.push({ field: 'cursor_color', expr: colorLiteral(String(evalStatic(node, scope))) });
|
|
2206
|
+
else if (name === 'editable') {
|
|
2207
|
+
try {
|
|
2208
|
+
staticAssigns.push({ field: 'editable', expr: evalStatic(node, scope) ? '1' : '0' });
|
|
2209
|
+
} catch {
|
|
2210
|
+
dynAssigns.push({ field: 'editable', code: `(uint8_t)((${emitExpr(node, env).code}) ? 1 : 0)` });
|
|
2211
|
+
}
|
|
2212
|
+
} else if (['autoFocus', 'keyboardType', 'secureTextEntry', 'maxLength', 'multiline', 'autoCapitalize', 'autoCorrect', 'returnKeyType', 'onSubmitEditing', 'onFocus', 'onBlur'].includes(name)) {
|
|
2213
|
+
/* accepted but not yet lowered (no on-screen keyboard / submit wiring in the AOT path) */
|
|
2214
|
+
} else throw aotError(`AOT: <TextInput> prop "${name}" is not supported`, 'supported props: value, onChangeText, placeholder, placeholderTextColor, cursorColor, editable, style.');
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
// value → the input's text buffer (er_node_set_props → er_text_input_set_text): static literal, or a
|
|
2218
|
+
// state-driven value re-synced each app_update.
|
|
2219
|
+
let text = null;
|
|
2220
|
+
if (valueNode) {
|
|
2221
|
+
try {
|
|
2222
|
+
const cv = evalStatic(valueNode, scope);
|
|
2223
|
+
text = { dynamic: false, format: (cv == null ? '' : String(cv)).replace(/%/g, '%%'), args: [] };
|
|
2224
|
+
} catch {
|
|
2225
|
+
const e = emitExpr(valueNode, env);
|
|
2226
|
+
text = { dynamic: true, format: printfSpec(e.cType), args: [e.code] };
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
const isDynamic = dynAssigns.length > 0 || (text && text.dynamic);
|
|
2231
|
+
out.build.push(` ${v} = er_node_create(ER_NODE_TEXT_INPUT);`);
|
|
2232
|
+
if (isDynamic) {
|
|
2233
|
+
out.build.push(` s_${v} = ${v};`);
|
|
2234
|
+
out.handles.push(v);
|
|
2235
|
+
out.updates.push({ v, styleAssigns: staticAssigns, text, dynAssigns, placeholder });
|
|
2236
|
+
} else {
|
|
2237
|
+
out.build.push(` er_props_default(&p);`);
|
|
2238
|
+
for (const a of staticAssigns) out.build.push(` p.${a.field} = ${a.expr};`);
|
|
2239
|
+
if (placeholder != null) out.build.push(` snprintf(p.placeholder, sizeof(p.placeholder), "%s", ${cstr(placeholder)});`);
|
|
2240
|
+
if (text) out.build.push(` snprintf(p.text, sizeof(p.text), "%s", ${cstr(text.format.replace(/%%/g, '%'))});`);
|
|
2241
|
+
out.build.push(` er_node_set_props(${v}, &p);`);
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
if (onChangeFn) {
|
|
2245
|
+
if (!isFn(onChangeFn)) throw aotError('AOT: onChangeText must be an inline function', 'onChangeText={(t) => setText(t)}');
|
|
2246
|
+
const handlerName = `er_handler_${out.handlers.length}`;
|
|
2247
|
+
out.handlers.push({ name: handlerName, body: compileValueHandler(onChangeFn, 'data->changed_text', env, state, out, 'string') });
|
|
2248
|
+
out.build.push(` er_event_set(${v}, ER_EVENT_CHANGE_TEXT, ${handlerName}, NULL);`);
|
|
2249
|
+
}
|
|
2250
|
+
emitRefBind(v, el.openingElement, out, env);
|
|
2251
|
+
return v;
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
/**
|
|
2255
|
+
* <ActivityIndicator color={…} size="small"|"large"|N animating={…} style={…} /> → ER_NODE_ACTIVITY_INDICATOR.
|
|
2256
|
+
* The engine spins it on its own (a looping rotate; render is a ring of 8 fading dots). No intrinsic size, so
|
|
2257
|
+
* a default box is set from `size` (small=20, large=36) unless style sets width/height.
|
|
2258
|
+
*/
|
|
2259
|
+
function emitActivityIndicator(el, scope, out, env) {
|
|
2260
|
+
const v = `n${out.n++}`;
|
|
2261
|
+
const { staticAssigns, dynAssigns } = collectStyleAssigns(el.openingElement, scope, env);
|
|
2262
|
+
const hasField = (f) => staticAssigns.some((a) => a.field === f) || dynAssigns.some((a) => a.field === f);
|
|
2263
|
+
let size = 36;
|
|
2264
|
+
for (const attr of el.openingElement.attributes) {
|
|
2265
|
+
if (attr.type !== 'JSXAttribute') throw aotError('AOT: spread props on <ActivityIndicator> are not supported');
|
|
2266
|
+
const name = attr.name.name;
|
|
2267
|
+
if (name === 'style' || name === 'ref' || name === 'key') continue;
|
|
2268
|
+
const node = attrExpr(attr);
|
|
2269
|
+
if (name === 'color') {
|
|
2270
|
+
try {
|
|
2271
|
+
staticAssigns.push({ field: 'indicator_color', expr: colorLiteral(String(evalStatic(node, scope))) });
|
|
2272
|
+
} catch {
|
|
2273
|
+
dynAssigns.push({ field: 'indicator_color', code: emitColorExpr(node, env) });
|
|
2274
|
+
}
|
|
2275
|
+
} else if (name === 'size') {
|
|
2276
|
+
const sv = evalStatic(node, scope);
|
|
2277
|
+
size = sv === 'small' ? 20 : sv === 'large' ? 36 : Number(sv) || 36;
|
|
2278
|
+
} else if (name === 'animating') {
|
|
2279
|
+
try {
|
|
2280
|
+
staticAssigns.push({ field: 'animating', expr: evalStatic(node, scope) ? '1' : '0' });
|
|
2281
|
+
} catch {
|
|
2282
|
+
dynAssigns.push({ field: 'animating', code: `(uint8_t)((${emitExpr(node, env).code}) ? 1 : 0)` });
|
|
2283
|
+
}
|
|
2284
|
+
} else throw aotError(`AOT: <ActivityIndicator> prop "${name}" is not supported`, 'supported props: color, size, animating, style.');
|
|
2285
|
+
}
|
|
2286
|
+
if (!hasField('width')) staticAssigns.push({ field: 'width', expr: String(size) });
|
|
2287
|
+
if (!hasField('height')) staticAssigns.push({ field: 'height', expr: String(size) });
|
|
2288
|
+
|
|
2289
|
+
const isDynamic = dynAssigns.length > 0;
|
|
2290
|
+
out.build.push(` ${v} = er_node_create(ER_NODE_ACTIVITY_INDICATOR);`);
|
|
2291
|
+
if (isDynamic) {
|
|
2292
|
+
out.build.push(` s_${v} = ${v};`);
|
|
2293
|
+
out.handles.push(v);
|
|
2294
|
+
out.updates.push({ v, styleAssigns: staticAssigns, text: null, dynAssigns });
|
|
2295
|
+
} else {
|
|
2296
|
+
out.build.push(` er_props_default(&p);`);
|
|
2297
|
+
for (const a of staticAssigns) out.build.push(` p.${a.field} = ${a.expr};`);
|
|
2298
|
+
out.build.push(` er_node_set_props(${v}, &p);`);
|
|
2299
|
+
}
|
|
2300
|
+
emitRefBind(v, el.openingElement, out, env);
|
|
2301
|
+
return v;
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
/**
|
|
2305
|
+
* <Modal visible={show} backdropColor=… style=…>{content}</Modal> → ER_NODE_MODAL. The engine draws a
|
|
2306
|
+
* full-screen backdrop then the modal + its children when visible, and toggles the node's layout display
|
|
2307
|
+
* from `visible`. Defaults to an absolute full-screen overlay centring its content (style can override).
|
|
2308
|
+
* transparent / animationType / onRequestClose are accepted but currently no-ops.
|
|
2309
|
+
*/
|
|
2310
|
+
function emitModal(el, scope, out, env, state) {
|
|
2311
|
+
const v = `n${out.n++}`;
|
|
2312
|
+
const { staticAssigns, dynAssigns } = collectStyleAssigns(el.openingElement, scope, env);
|
|
2313
|
+
const hasField = (f) => staticAssigns.some((a) => a.field === f) || dynAssigns.some((a) => a.field === f);
|
|
2314
|
+
// Overlay defaults: absolute, fill the parent via four 0 insets (the robust "stretch" for an absolute
|
|
2315
|
+
// node), centre the content. The user's style overrides any of these.
|
|
2316
|
+
const DEFAULTS = [
|
|
2317
|
+
['position', 'ER_POS_ABSOLUTE'],
|
|
2318
|
+
['left', '0'],
|
|
2319
|
+
['top', '0'],
|
|
2320
|
+
['right', '0'],
|
|
2321
|
+
['bottom', '0'],
|
|
2322
|
+
['align_items', 'ER_ALIGN_CENTER'],
|
|
2323
|
+
['justify_content', 'ER_JUSTIFY_CENTER'],
|
|
2324
|
+
];
|
|
2325
|
+
for (const [f, expr] of DEFAULTS) if (!hasField(f)) staticAssigns.push({ field: f, expr });
|
|
2326
|
+
|
|
2327
|
+
let visibleNode = null;
|
|
2328
|
+
for (const attr of el.openingElement.attributes) {
|
|
2329
|
+
if (attr.type !== 'JSXAttribute') throw aotError('AOT: spread props on <Modal> are not supported');
|
|
2330
|
+
const name = attr.name.name;
|
|
2331
|
+
if (name === 'style' || name === 'ref' || name === 'key') continue;
|
|
2332
|
+
const node = attrExpr(attr);
|
|
2333
|
+
if (name === 'visible') visibleNode = node;
|
|
2334
|
+
else if (name === 'backdropColor') staticAssigns.push({ field: 'backdrop_color', expr: colorLiteral(String(evalStatic(node, scope))) });
|
|
2335
|
+
else if (name === 'transparent' || name === 'animationType' || name === 'onRequestClose' || name === 'statusBarTranslucent') {
|
|
2336
|
+
/* accepted for RN compatibility; no-op in the AOT today */
|
|
2337
|
+
} else throw aotError(`AOT: <Modal> prop "${name}" is not supported`, 'supported: visible, backdropColor, style, children (transparent / animationType / onRequestClose are accepted but no-ops).');
|
|
2338
|
+
}
|
|
2339
|
+
if (!visibleNode) throw aotError('AOT: a <Modal> needs a visible prop', '<Modal visible={show}>…</Modal>');
|
|
2340
|
+
try {
|
|
2341
|
+
staticAssigns.push({ field: 'modal_visible', expr: evalStatic(visibleNode, scope) ? '1' : '0' });
|
|
2342
|
+
} catch {
|
|
2343
|
+
dynAssigns.push({ field: 'modal_visible', code: `(uint8_t)((${emitExpr(visibleNode, env).code}) ? 1 : 0)` });
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
const isDynamic = dynAssigns.length > 0;
|
|
2347
|
+
out.build.push(` ${v} = er_node_create(ER_NODE_MODAL);`);
|
|
2348
|
+
if (isDynamic) {
|
|
2349
|
+
out.build.push(` s_${v} = ${v};`);
|
|
2350
|
+
out.handles.push(v);
|
|
2351
|
+
out.updates.push({ v, styleAssigns: staticAssigns, text: null, dynAssigns });
|
|
2352
|
+
} else {
|
|
2353
|
+
out.build.push(` er_props_default(&p);`);
|
|
2354
|
+
for (const a of staticAssigns) out.build.push(` p.${a.field} = ${a.expr};`);
|
|
2355
|
+
out.build.push(` er_node_set_props(${v}, &p);`);
|
|
2356
|
+
}
|
|
2357
|
+
emitRefBind(v, el.openingElement, out, env);
|
|
2358
|
+
emitChildren(el.children, v, scope, out, env, state); // the modal's content (shown/hidden with the modal)
|
|
2359
|
+
return v;
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
/**
|
|
2363
|
+
* <FlatList data={items} renderItem={({ item, index }) => <Row …/>} keyExtractor=… style=… /> → the SAME as
|
|
2364
|
+
* <ScrollView style=…>{items.map((item, index) => <Row …/>)}</ScrollView>. The engine's FlatList IS a
|
|
2365
|
+
* ScrollView (no virtualization), and the AOT already unrolls a .map (static or state-list), so this is a thin
|
|
2366
|
+
* API-compat rewrite: synthesize that ScrollView+map AST and emit it. keyExtractor is ignored (no reconciler).
|
|
2367
|
+
*/
|
|
2368
|
+
function emitFlatList(el, scope, out, env, state, opts) {
|
|
2369
|
+
let dataNode = null;
|
|
2370
|
+
let renderItem = null;
|
|
2371
|
+
let styleAttr = null;
|
|
2372
|
+
for (const attr of el.openingElement.attributes) {
|
|
2373
|
+
if (attr.type !== 'JSXAttribute') throw aotError('AOT: spread props on <FlatList> are not supported');
|
|
2374
|
+
const name = attr.name.name;
|
|
2375
|
+
if (name === 'data') dataNode = attrExpr(attr);
|
|
2376
|
+
else if (name === 'renderItem') renderItem = attrExpr(attr);
|
|
2377
|
+
else if (name === 'style') styleAttr = attr;
|
|
2378
|
+
else if (name === 'keyExtractor' || name === 'ref' || name === 'key') {
|
|
2379
|
+
/* ignored — the AOT unrolls at compile time, so React keys are irrelevant */
|
|
2380
|
+
} else throw aotError(`AOT: <FlatList> prop "${name}" is not supported`, 'supported: data, renderItem, keyExtractor, style. For headers/footers/horizontal/onEndReached etc., use <ScrollView> + .map directly.');
|
|
2381
|
+
}
|
|
2382
|
+
if (!dataNode) throw aotError('AOT: <FlatList> needs a data prop', '<FlatList data={items} renderItem={({ item }) => <Row item={item} />} />');
|
|
2383
|
+
if (!renderItem || !isFn(renderItem)) throw aotError('AOT: <FlatList> needs a renderItem function', 'renderItem={({ item, index }) => <Row item={item} />}');
|
|
2384
|
+
const param = renderItem.params[0];
|
|
2385
|
+
if (!param || param.type !== 'ObjectPattern') throw aotError('AOT: FlatList renderItem must destructure ({ item, index })', 'renderItem={({ item }) => <Row item={item} />}');
|
|
2386
|
+
let itemName = null;
|
|
2387
|
+
let indexName = null;
|
|
2388
|
+
for (const prop of param.properties) {
|
|
2389
|
+
if (prop.type !== 'ObjectProperty' || prop.value.type !== 'Identifier') throw aotError('AOT: FlatList renderItem may destructure only item / index (to plain names)');
|
|
2390
|
+
if (prop.key.name === 'item') itemName = prop.value.name;
|
|
2391
|
+
else if (prop.key.name === 'index') indexName = prop.value.name;
|
|
2392
|
+
else throw aotError(`AOT: FlatList renderItem cannot destructure "${prop.key.name}" (only item / index)`);
|
|
2393
|
+
}
|
|
2394
|
+
if (!itemName) throw aotError('AOT: FlatList renderItem must destructure item', 'renderItem={({ item }) => …}');
|
|
2395
|
+
|
|
2396
|
+
// Rewrite renderItem `({ item, index }) => BODY` → a positional `.map` callback `(item, index) => BODY`.
|
|
2397
|
+
const cbParams = [{ type: 'Identifier', name: itemName }];
|
|
2398
|
+
if (indexName) cbParams.push({ type: 'Identifier', name: indexName });
|
|
2399
|
+
const cb = { type: 'ArrowFunctionExpression', params: cbParams, body: renderItem.body, async: false, expression: renderItem.body.type !== 'BlockStatement' };
|
|
2400
|
+
const mapCall = { type: 'CallExpression', callee: { type: 'MemberExpression', object: dataNode, property: { type: 'Identifier', name: 'map' }, computed: false }, arguments: [cb] };
|
|
2401
|
+
const scrollView = {
|
|
2402
|
+
type: 'JSXElement',
|
|
2403
|
+
openingElement: { type: 'JSXOpeningElement', name: { type: 'JSXIdentifier', name: 'ScrollView' }, attributes: styleAttr ? [styleAttr] : [], selfClosing: false },
|
|
2404
|
+
closingElement: { type: 'JSXClosingElement', name: { type: 'JSXIdentifier', name: 'ScrollView' } },
|
|
2405
|
+
children: [{ type: 'JSXExpressionContainer', expression: mapCall }],
|
|
2406
|
+
};
|
|
2407
|
+
return emitNode(scrollView, scope, out, env, state, opts);
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2410
|
+
const RESIZE_MODES = {
|
|
2411
|
+
cover: 'ER_RESIZE_COVER',
|
|
2412
|
+
contain: 'ER_RESIZE_CONTAIN',
|
|
2413
|
+
stretch: 'ER_RESIZE_STRETCH',
|
|
2414
|
+
repeat: 'ER_RESIZE_REPEAT',
|
|
2415
|
+
center: 'ER_RESIZE_CENTER',
|
|
2416
|
+
};
|
|
2417
|
+
|
|
2418
|
+
/** Resolves an <Image source>/imageName expression to its baked asset NAME (a string) if it folds at compile
|
|
2419
|
+
* time, else null (a runtime/dynamic source — the caller emits a dynamic image_name). Image imports live in
|
|
2420
|
+
* the const scope as their asset-name string, so this folds `wxSun`, `item.icon` (unrolled map), and static
|
|
2421
|
+
* ternaries; `{ uri }` is the explicit remote-shape escape. */
|
|
2422
|
+
function imageNameFromSource(expr, env) {
|
|
2423
|
+
if (!expr) return null;
|
|
2424
|
+
try {
|
|
2425
|
+
const v = evalStatic(expr, env.consts ?? {});
|
|
2426
|
+
if (typeof v === 'string') return v;
|
|
2427
|
+
} catch {
|
|
2428
|
+
/* not a compile-time constant — fall through to {uri}, else dynamic */
|
|
2429
|
+
}
|
|
2430
|
+
if (expr.type === 'ObjectExpression') {
|
|
2431
|
+
// source={{ uri: 'wx_sun' }} — RN's remote-image shape; here the uri IS the baked asset name.
|
|
2432
|
+
const uri = expr.properties.find((p) => p.type === 'ObjectProperty' && (p.key.name === 'uri' || p.key.value === 'uri'));
|
|
2433
|
+
if (uri?.value?.type === 'StringLiteral') return uri.value.value;
|
|
2434
|
+
}
|
|
2435
|
+
return null;
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
/** Resolves an <Image>'s source/imageName/resizeMode/tintColor for the node props (static name, a dynamic
|
|
2439
|
+
* name expr, resize mode, tint). Records baking intent in `out`: a static name that matches an import is
|
|
2440
|
+
* marked used (out.images); a dynamic source flips out.bakeAllImages (its asset can't be enumerated). So
|
|
2441
|
+
* only REACHED images are baked — an import used solely in a folded-away branch costs no flash. */
|
|
2442
|
+
function resolveImageAttrs(el, env, out) {
|
|
2443
|
+
const attrs = el.openingElement.attributes;
|
|
2444
|
+
const find = (n) => attrs.find((a) => a.type === 'JSXAttribute' && a.name.name === n);
|
|
2445
|
+
const imAttr = find('imageName');
|
|
2446
|
+
const srcAttr = find('source');
|
|
2447
|
+
const srcExpr = imAttr ? attrExpr(imAttr) : srcAttr ? attrExpr(srcAttr) : null;
|
|
2448
|
+
let imageName = null; // static asset name (a literal), OR …
|
|
2449
|
+
let imageNameDyn = null; // … a runtime C string expr (a list-item field / state) set in app_update.
|
|
2450
|
+
if (srcExpr) {
|
|
2451
|
+
imageName = imageNameFromSource(srcExpr, env);
|
|
2452
|
+
if (imageName != null) {
|
|
2453
|
+
const path = env.imageNames?.get(imageName); // a baked import (vs a bare {uri} name the app supplies)
|
|
2454
|
+
if (path) out.images.set(imageName, path);
|
|
2455
|
+
} else {
|
|
2456
|
+
// Dynamic source: emit it as a runtime string. The engine resolves it against the image registry by
|
|
2457
|
+
// name each frame, so the candidate assets must be baked — and they can't be enumerated, so bake them
|
|
2458
|
+
// ALL (out.bakeAllImages). Only triggered when a dynamic source is actually REACHED.
|
|
2459
|
+
out.bakeAllImages = true;
|
|
2460
|
+
const e = emitExpr(srcExpr, env);
|
|
2461
|
+
if (e.cType !== 'string') {
|
|
2462
|
+
const err = aotError(
|
|
2463
|
+
'AOT: an <Image source> must resolve to an asset NAME (a string)',
|
|
2464
|
+
"use an imported image (`import logo from './logo.png'` → source={logo}), a string asset name, source={{ uri: 'name' }}, or a string-valued state / list-item field for a dynamic source.",
|
|
2465
|
+
);
|
|
2466
|
+
if (srcExpr.loc) err.aotLoc = srcExpr.loc.start;
|
|
2467
|
+
throw err;
|
|
2468
|
+
}
|
|
2469
|
+
imageNameDyn = e.code;
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
let resizeMode = null;
|
|
2473
|
+
const rmAttr = find('resizeMode');
|
|
2474
|
+
if (rmAttr) {
|
|
2475
|
+
const rm = evalStaticOr(attrExpr(rmAttr), env, null);
|
|
2476
|
+
resizeMode = RESIZE_MODES[rm];
|
|
2477
|
+
if (!resizeMode) {
|
|
2478
|
+
const e = aotError(`AOT: unsupported <Image resizeMode> "${rm}"`, `resizeMode must be one of: ${Object.keys(RESIZE_MODES).join(' / ')}.`);
|
|
2479
|
+
if (rmAttr.loc) e.aotLoc = rmAttr.loc.start;
|
|
2480
|
+
throw e;
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
let tintColor = null;
|
|
2484
|
+
const tcAttr = find('tintColor');
|
|
2485
|
+
if (tcAttr) {
|
|
2486
|
+
const tc = evalStaticOr(attrExpr(tcAttr), env, null);
|
|
2487
|
+
if (typeof tc === 'string' || typeof tc === 'number') tintColor = argbLiteral(tc);
|
|
2488
|
+
}
|
|
2489
|
+
return { imageName, imageNameDyn, resizeMode, tintColor };
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
// ---------------------------------------------------------------------------------------------------
|
|
2493
|
+
// Node emitter — the element dispatcher + the generic host node. emitNodeImpl is the entry point for
|
|
2494
|
+
// every JSX element: it routes Svg / typed components / FlatList / components to their emitters above,
|
|
2495
|
+
// and handles the generic host nodes (View / Text / Pressable / Image / ScrollView) itself — style,
|
|
2496
|
+
// text, events, refs, children. emitNode wraps it with withLoc so a thrown AOT error gets a location.
|
|
2497
|
+
// ---------------------------------------------------------------------------------------------------
|
|
2498
|
+
|
|
2499
|
+
function emitNodeImpl(el, scope, out, env, state, opts = {}) {
|
|
2500
|
+
const tag = resolveTag(el.openingElement);
|
|
2501
|
+
if (tag === 'Svg') return emitSvg(el, scope, out, env, state, opts);
|
|
2502
|
+
if (tag === 'Switch') return emitSwitch(el, scope, out, env, state);
|
|
2503
|
+
if (tag === 'TextInput') return emitTextInput(el, scope, out, env, state);
|
|
2504
|
+
if (tag === 'ActivityIndicator') return emitActivityIndicator(el, scope, out, env);
|
|
2505
|
+
if (tag === 'Modal') return emitModal(el, scope, out, env, state);
|
|
2506
|
+
if (tag === 'FlatList') return emitFlatList(el, scope, out, env, state, opts);
|
|
2507
|
+
const nodeType = NODE_TYPES[tag];
|
|
2508
|
+
if (!nodeType) {
|
|
2509
|
+
if (out.components.has(tag)) return emitComponent(el, scope, out, env, state, opts);
|
|
2510
|
+
throw aotError(`AOT: unknown element <${tag}> (not a built-in or a component in this file)`, `<${tag}> must be a built-in (View / Text / Pressable / Image / ScrollView / Svg + shapes / Animated.*) or a function component defined in THIS file. Check the import/spelling, or define the component here.`);
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
// Spread attributes on a host element (`<View {...props} />`) aren't lowered — the style/event loops
|
|
2514
|
+
// below only read named JSXAttributes, so a spread would be SILENTLY dropped. Reject it explicitly
|
|
2515
|
+
// (the typed components — Switch/TextInput/Modal/… — already throw on spreads). Pin the location to the
|
|
2516
|
+
// spread itself, not the whole element, for a precise code-frame.
|
|
2517
|
+
const spread = el.openingElement.attributes.find((a) => a.type === 'JSXSpreadAttribute');
|
|
2518
|
+
if (spread) {
|
|
2519
|
+
const e = aotError(
|
|
2520
|
+
`AOT: a spread {...} on <${tag}> is not supported`,
|
|
2521
|
+
`list each prop explicitly (e.g. style={…} onPress={…}). Spread props are only supported on a function component instance whose spread object folds to a compile-time constant.`,
|
|
2522
|
+
);
|
|
2523
|
+
if (spread.loc) e.aotLoc = spread.loc.start;
|
|
2524
|
+
throw e;
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2527
|
+
const v = `n${out.n++}`;
|
|
2528
|
+
const { staticAssigns, dynAssigns, binds } = collectStyleAssigns(el.openingElement, scope, env);
|
|
2529
|
+
// A <Text> with a nested <Text> becomes inline SPANS; otherwise a single (possibly dynamic) string.
|
|
2530
|
+
const spans = tag === 'Text' ? collectTextSpans(el.children, scope, env) : null;
|
|
2531
|
+
const text = tag === 'Text' && !spans ? buildText(el.children, scope, env) : null;
|
|
2532
|
+
// An <Image>'s baked-asset name + resize/tint. resize/tint are static → fold into staticAssigns so both
|
|
2533
|
+
// the static and (deferred) dynamic paths apply them; the asset name is a char buffer (set via snprintf),
|
|
2534
|
+
// either a compile-time literal (image.imageName) or a runtime string expr (image.imageNameDyn).
|
|
2535
|
+
const image = tag === 'Image' ? resolveImageAttrs(el, env, out) : null;
|
|
2536
|
+
if (image?.resizeMode) staticAssigns.push({ field: 'resize_mode', expr: image.resizeMode });
|
|
2537
|
+
if (image?.tintColor) staticAssigns.push({ field: 'tint_color', expr: image.tintColor });
|
|
2538
|
+
|
|
2539
|
+
// `displayCode` toggles show/hide for a state-driven conditional: the node is always built, its
|
|
2540
|
+
// `display` flips between flex and none in app_update (joining any state-driven style assigns).
|
|
2541
|
+
if (opts.displayCode) dynAssigns.push({ field: 'display', code: `((${opts.displayCode}) ? ER_DISPLAY_FLEX : ER_DISPLAY_NONE)` });
|
|
2542
|
+
|
|
2543
|
+
const isDynamic = !!text?.dynamic || dynAssigns.length > 0 || !!image?.imageNameDyn;
|
|
2544
|
+
|
|
2545
|
+
out.build.push(` ${v} = er_node_create(${nodeType});`);
|
|
2546
|
+
if (isDynamic) {
|
|
2547
|
+
// Props are (re)applied in app_update(); here just create the node and remember its handle. A dynamic
|
|
2548
|
+
// <Image> carries its runtime asset name (imageName) so app_update re-snprintf's p.image_name each pass.
|
|
2549
|
+
out.build.push(` s_${v} = ${v};`);
|
|
2550
|
+
out.handles.push(v);
|
|
2551
|
+
out.updates.push({ v, styleAssigns: staticAssigns, text, dynAssigns, imageName: image?.imageNameDyn });
|
|
2552
|
+
} else {
|
|
2553
|
+
out.build.push(` er_props_default(&p);`);
|
|
2554
|
+
for (const a of staticAssigns) out.build.push(` p.${a.field} = ${a.expr};`);
|
|
2555
|
+
if (text) out.build.push(` snprintf(p.text, sizeof(p.text), "%s", ${cstr(text.format.replace(/%%/g, '%'))});`);
|
|
2556
|
+
if (image?.imageName != null) out.build.push(` snprintf(p.image_name, sizeof(p.image_name), "%s", ${cstr(image.imageName)});`);
|
|
2557
|
+
out.build.push(` er_node_set_props(${v}, &p);`);
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
// Inline text spans (a <Text> with nested <Text>): set once; each span inherits the node's base style
|
|
2561
|
+
// unless it overrides (the local array is copied by er_node_set_text_spans).
|
|
2562
|
+
if (spans) {
|
|
2563
|
+
out.build.push(` {`, ` static const ERTextSpan spans_${v}[] = {`);
|
|
2564
|
+
for (const s of spans) out.build.push(` { ${s.text}, ${s.color}, ${s.font_size}, ${s.font_weight}, ${s.font_style}, ${s.text_decoration}, ${s.letter_spacing} },`);
|
|
2565
|
+
out.build.push(` };`, ` er_node_set_text_spans(${v}, spans_${v}, ${spans.length});`, ` }`);
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
// Animated style props (opacity / transform / color) → bind the node to its animated value. The
|
|
2569
|
+
// engine's native driver advances it each tick (no per-frame JS, no app_update for the motion). A bind
|
|
2570
|
+
// carrying an `interp` maps the raw value through a piecewise-linear range first (value.interpolate(...)).
|
|
2571
|
+
binds.forEach((b, i) => {
|
|
2572
|
+
if (b.interp) {
|
|
2573
|
+
const it = b.interp;
|
|
2574
|
+
out.build.push(
|
|
2575
|
+
` {`,
|
|
2576
|
+
` static const ERInterpolation interp_${v}_${i} = { { ${it.input.map(floatLit).join(', ')} }, { ${it.output.map(floatLit).join(', ')} }, ${it.input.length}, ${it.exLeft}, ${it.exRight} };`,
|
|
2577
|
+
` er_anim_value_bind_interpolated(${b.cVar}, ${v}, ${b.prop}, &interp_${v}_${i});`,
|
|
2578
|
+
` }`,
|
|
2579
|
+
);
|
|
2580
|
+
} else {
|
|
2581
|
+
out.build.push(` er_anim_value_bind(${b.cVar}, ${v}, ${b.prop});`);
|
|
2582
|
+
}
|
|
2583
|
+
});
|
|
2584
|
+
|
|
2585
|
+
emitRefBind(v, el.openingElement, out, env);
|
|
2586
|
+
|
|
2587
|
+
for (const attr of el.openingElement.attributes) {
|
|
2588
|
+
if (attr.type !== 'JSXAttribute') continue;
|
|
2589
|
+
const evt = EVENT_TYPES[attr.name.name];
|
|
2590
|
+
if (!evt) continue;
|
|
2591
|
+
const fn = attrExpr(attr);
|
|
2592
|
+
let handlerName;
|
|
2593
|
+
if (fn.type === 'Identifier' && env.callbacks?.has(fn.name)) {
|
|
2594
|
+
// onPress={fn} where fn is a useCallback → emit one shared handler, reused across elements. The
|
|
2595
|
+
// cbPrefix namespaces it per child instance (so two instances' same-named callbacks stay distinct,
|
|
2596
|
+
// each compiled in its own env/state); '' for the App — unchanged.
|
|
2597
|
+
const key = `${env.cbPrefix ?? ''}${fn.name}`;
|
|
2598
|
+
handlerName = out.cbEmitted.get(key);
|
|
2599
|
+
if (!handlerName) {
|
|
2600
|
+
handlerName = `er_cb_${key}`;
|
|
2601
|
+
out.cbEmitted.set(key, handlerName);
|
|
2602
|
+
out.handlers.push({ name: handlerName, body: compileHandler(env.callbacks.get(fn.name), env, state, out) });
|
|
2603
|
+
}
|
|
2604
|
+
} else if (fn.type === 'Identifier' && env.fnProps?.has(fn.name)) {
|
|
2605
|
+
// Callback prop: <Child onTap={() => …}/> where Child does onPress={onTap}. Inline the CALLER's
|
|
2606
|
+
// function as this handler, compiled in the caller's env/state so its setters/locals resolve there.
|
|
2607
|
+
const fp = env.fnProps.get(fn.name);
|
|
2608
|
+
handlerName = `er_handler_${out.handlers.length}`;
|
|
2609
|
+
out.handlers.push({ name: handlerName, body: compileHandler(fp.node, fp.env, fp.state, out) });
|
|
2610
|
+
} else if (isFn(fn)) {
|
|
2611
|
+
handlerName = `er_handler_${out.handlers.length}`;
|
|
2612
|
+
out.handlers.push({ name: handlerName, body: compileHandler(fn, env, state, out) });
|
|
2613
|
+
} else {
|
|
2614
|
+
throw aotError(`AOT: ${attr.name.name} must be an inline function, a useCallback, or a callback prop`, `pass an inline arrow (onPress={() => setX(…)}), a useCallback identifier, or a function prop received by this component.`);
|
|
2615
|
+
}
|
|
2616
|
+
out.build.push(` er_event_set(${v}, ${evt}, ${handlerName}, NULL);`);
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2619
|
+
if (tag !== 'Text') emitChildren(el.children, v, scope, out, env, state);
|
|
2620
|
+
return v;
|
|
2621
|
+
}
|
|
2622
|
+
const emitNode = withLoc(emitNodeImpl);
|
|
2623
|
+
|
|
2624
|
+
// ---------------------------------------------------------------------------------------------------
|
|
2625
|
+
// On-screen keyboard config — lower a module-level setKeyboardConfig({...}) call to static C tables
|
|
2626
|
+
// (ERKeyboardKey/Row/Layer + ERKeyboardConfig) that er_app_build hands to er_keyboard_set_config.
|
|
2627
|
+
// ---------------------------------------------------------------------------------------------------
|
|
2628
|
+
|
|
2629
|
+
/** A keyboard-config color → C ARGB literal; null/undefined → "0" (the engine's "use default" sentinel). */
|
|
2630
|
+
function kbdColor(v) {
|
|
2631
|
+
return v == null ? '0' : argbLiteral(v);
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2634
|
+
/** One JS keyboard key object → a C ERKeyboardKey initializer. `li` is its layer index (for shift highlight).
|
|
2635
|
+
* Shapes: { char } (types it), { char:' ', span } (space), { label, layer } (switch), { label, backspace },
|
|
2636
|
+
* { label, done }; optional span / highlight. */
|
|
2637
|
+
function kbdKeyToC(k, li) {
|
|
2638
|
+
if (k == null || typeof k !== 'object') throw aotError('AOT: each setKeyboardConfig key must be an object', 'e.g. { char: "q" } or { label: "shift", layer: 1, highlight: true }');
|
|
2639
|
+
let type;
|
|
2640
|
+
let label;
|
|
2641
|
+
let text = 'NULL';
|
|
2642
|
+
let layer = 0;
|
|
2643
|
+
if (k.backspace) {
|
|
2644
|
+
type = 'ER_KBD_KEY_BACKSPACE';
|
|
2645
|
+
label = k.label ?? '<';
|
|
2646
|
+
} else if (k.done) {
|
|
2647
|
+
type = 'ER_KBD_KEY_DONE';
|
|
2648
|
+
label = k.label ?? 'OK';
|
|
2649
|
+
} else if (k.layer != null) {
|
|
2650
|
+
type = 'ER_KBD_KEY_LAYER';
|
|
2651
|
+
label = k.label ?? '';
|
|
2652
|
+
layer = Math.round(Number(k.layer));
|
|
2653
|
+
} else if (k.char != null) {
|
|
2654
|
+
type = 'ER_KBD_KEY_CHAR';
|
|
2655
|
+
text = cstr(String(k.char));
|
|
2656
|
+
label = k.label ?? (String(k.char) === ' ' ? '' : String(k.char)); // a space bar shows no label
|
|
2657
|
+
} else {
|
|
2658
|
+
throw aotError('AOT: a setKeyboardConfig key needs one of char / layer / backspace / done');
|
|
2659
|
+
}
|
|
2660
|
+
const span = k.span != null ? Math.round(Number(k.span)) : 1;
|
|
2661
|
+
const hl = k.highlight ? li : 255;
|
|
2662
|
+
return `{ ${label === '' ? 'NULL' : cstr(String(label))}, ${text}, ${type}, ${layer}, ${span}, ${hl} }`;
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2665
|
+
/** Lowers a module-level `setKeyboardConfig({...})` to a static ERKeyboardConfig + an er_keyboard_set_config()
|
|
2666
|
+
* call in er_app_build — customising the on-screen keyboard (colours/sizes, and optionally a full layout) from
|
|
2667
|
+
* the app, no engine edit. The config must be statically foldable; omit `layers` to keep the built-in QWERTY. */
|
|
2668
|
+
function compileKeyboardConfig(program, out) {
|
|
2669
|
+
let arg = null;
|
|
2670
|
+
for (const stmt of program.body) {
|
|
2671
|
+
if (stmt.type === 'ExpressionStatement' && stmt.expression.type === 'CallExpression' && stmt.expression.callee.type === 'Identifier' && stmt.expression.callee.name === 'setKeyboardConfig') {
|
|
2672
|
+
arg = stmt.expression.arguments[0];
|
|
2673
|
+
break;
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
if (!arg) return;
|
|
2677
|
+
let cfg;
|
|
2678
|
+
try {
|
|
2679
|
+
cfg = evalStatic(arg, {});
|
|
2680
|
+
} catch {
|
|
2681
|
+
throw aotError('AOT: setKeyboardConfig(...) needs a statically-foldable config object', 'pass an object literal of colours/sizes (+ an optional `layers` array) — no state or runtime values.');
|
|
2682
|
+
}
|
|
2683
|
+
if (cfg == null || typeof cfg !== 'object') throw aotError('AOT: setKeyboardConfig(...) needs a config object');
|
|
2684
|
+
|
|
2685
|
+
const data = [];
|
|
2686
|
+
let layersExpr = 'NULL';
|
|
2687
|
+
let layerCount = 0;
|
|
2688
|
+
if (Array.isArray(cfg.layers)) {
|
|
2689
|
+
const layerVars = [];
|
|
2690
|
+
cfg.layers.forEach((layer, li) => {
|
|
2691
|
+
if (!Array.isArray(layer)) throw aotError('AOT: setKeyboardConfig `layers[i]` must be an array of rows');
|
|
2692
|
+
const rowVars = [];
|
|
2693
|
+
layer.forEach((row, ri) => {
|
|
2694
|
+
if (!Array.isArray(row) || !row.length) throw aotError('AOT: each keyboard row must be a non-empty array of keys');
|
|
2695
|
+
data.push(`static const ERKeyboardKey kbd_l${li}r${ri}[] = { ${row.map((k) => kbdKeyToC(k, li)).join(', ')} };`);
|
|
2696
|
+
rowVars.push(`{ kbd_l${li}r${ri}, ${row.length} }`);
|
|
2697
|
+
});
|
|
2698
|
+
data.push(`static const ERKeyboardRow kbd_l${li}rows[] = { ${rowVars.join(', ')} };`);
|
|
2699
|
+
layerVars.push(`{ kbd_l${li}rows, ${layer.length} }`);
|
|
2700
|
+
});
|
|
2701
|
+
data.push(`static const ERKeyboardLayer kbd_layers[] = { ${layerVars.join(', ')} };`);
|
|
2702
|
+
layersExpr = 'kbd_layers';
|
|
2703
|
+
layerCount = cfg.layers.length;
|
|
2704
|
+
}
|
|
2705
|
+
const num = (v) => (v == null ? 0 : Math.round(Number(v)));
|
|
2706
|
+
data.push(
|
|
2707
|
+
`static const ERKeyboardConfig kbd_cfg = { ${layersExpr}, ${layerCount}, ${num(cfg.gridCols)}, ${num(cfg.rowHeight)}, ` +
|
|
2708
|
+
`${num(cfg.keyGap)}, ${num(cfg.keyRadius)}, ${num(cfg.fontSize)}, ${kbdColor(cfg.panelColor)}, ${kbdColor(cfg.keyColor)}, ` +
|
|
2709
|
+
`${kbdColor(cfg.keyActiveColor)}, ${kbdColor(cfg.labelColor)} };`,
|
|
2710
|
+
);
|
|
2711
|
+
out.kbdData = data.join('\n');
|
|
2712
|
+
out.kbdSetup = ' er_keyboard_set_config(&kbd_cfg);';
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
// ---------------------------------------------------------------------------------------------------
|
|
2716
|
+
// Compile orchestration — JSX source string → generated C. Pure (no I/O) so it can be unit-tested
|
|
2717
|
+
// directly; the CLI entry at the bottom of the file wraps it with the file read/write.
|
|
2718
|
+
// ---------------------------------------------------------------------------------------------------
|
|
2719
|
+
|
|
2720
|
+
/**
|
|
2721
|
+
* Compiles a Flow B app's JSX source to C.
|
|
2722
|
+
* @param {string} src The App.jsx source text.
|
|
2723
|
+
* @param {string} demo Demo name (only used in the generated-by header comment).
|
|
2724
|
+
* @returns {{c: string, h: string, nodes: number, state: number, handlers: number, updates: number}}
|
|
2725
|
+
*/
|
|
2726
|
+
function compileSourceImpl(src, demo = 'app', opts = {}) {
|
|
2727
|
+
const ast = parse(src, { sourceType: 'module', plugins: ['jsx'] });
|
|
2728
|
+
|
|
2729
|
+
const screen = opts.screen ?? { width: SCREEN_W, height: SCREEN_H };
|
|
2730
|
+
// Image imports first, so their asset-name strings seed the module scope BEFORE its consts fold (a const
|
|
2731
|
+
// array of `{ icon: wxSun }` needs wxSun resolvable). An image import is just its baked-name string.
|
|
2732
|
+
const imageImports = collectImageImports(ast.program);
|
|
2733
|
+
const imageSeed = Object.fromEntries([...imageImports].map(([local, imp]) => [local, imp.name]));
|
|
2734
|
+
const scope = moduleScope(ast.program, screen, imageSeed);
|
|
2735
|
+
const component = findComponent(ast.program);
|
|
2736
|
+
// Fold statically-derived component-local consts (e.g. `const compact = screen.width < 400`) into the
|
|
2737
|
+
// const scope, so responsive `if` branches and styles can switch on them at compile time. Dynamic consts
|
|
2738
|
+
// (state-derived, useMemo, etc.) throw here and are skipped — they're handled later by memos/emitExpr.
|
|
2739
|
+
for (const stmt of component.body.body) {
|
|
2740
|
+
if (stmt.type !== 'VariableDeclaration' || stmt.kind !== 'const') continue;
|
|
2741
|
+
for (const decl of stmt.declarations) {
|
|
2742
|
+
if (decl.id.type !== 'Identifier' || !decl.init || decl.id.name in scope) continue;
|
|
2743
|
+
try {
|
|
2744
|
+
scope[decl.id.name] = evalStatic(decl.init, scope);
|
|
2745
|
+
} catch {
|
|
2746
|
+
/* dynamic const — resolved later */
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
const state = collectState(component.body, scope);
|
|
2751
|
+
const rootJSX = findReturnJSX(component.body, scope);
|
|
2752
|
+
|
|
2753
|
+
const anims = collectAnims(component.body, scope);
|
|
2754
|
+
const refs = collectRefs(component.body, scope);
|
|
2755
|
+
const callbacks = collectCallbacks(component.body);
|
|
2756
|
+
const memos = collectMemos(component.body);
|
|
2757
|
+
const helpers = collectHelpers(component.body, ast.program);
|
|
2758
|
+
const imageNames = new Map([...imageImports].map(([, imp]) => [imp.name, imp.importPath])); // asset name → path
|
|
2759
|
+
const env = { state: state.byName, locals: new Map(), consts: scope, anims, refs, callbacks, helpers, imageNames };
|
|
2760
|
+
// Resolve memos in declaration order: constant-fold into the const scope when possible, else register a
|
|
2761
|
+
// derived C expression in locals so each reference inlines it (the AOT has no per-render cache — the dep
|
|
2762
|
+
// tracking re-applies dependent nodes anyway). Done before emit so references resolve.
|
|
2763
|
+
for (const [name, expr] of memos) {
|
|
2764
|
+
try {
|
|
2765
|
+
scope[name] = evalStatic(expr, scope);
|
|
2766
|
+
} catch {
|
|
2767
|
+
const e = emitExpr(expr, env);
|
|
2768
|
+
env.locals.set(name, { code: `(${e.code})`, cType: e.cType });
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
const out = { n: 0, build: [], handlers: [], updates: [], handles: [], components: collectComponents(ast.program), cbEmitted: new Map(), vectorData: [], vectorBuilders: [], svgUpdates: [], svgN: 0, needsMath: false, timerFns: [], usesTimers: false, mountEffects: [], animCbs: [], seqN: 0, kbdData: '', kbdSetup: '', images: new Map(), bakeAllImages: false, instN: 0, childStateRecords: [], childRefs: [], childAnims: [], program: ast.program, effN: 0, effectFns: [], effectDecls: [], depEffects: [] };
|
|
2772
|
+
compileKeyboardConfig(ast.program, out); // module-level setKeyboardConfig({...}) → static ERKeyboardConfig
|
|
2773
|
+
const appTop = emitNode(rootJSX, scope, out, env, state);
|
|
2774
|
+
|
|
2775
|
+
// Image baking: a REACHED static source registered its import in out.images during emit. If any reached
|
|
2776
|
+
// source is DYNAMIC (resolved by name at runtime), we can't enumerate it — fall back to baking every import.
|
|
2777
|
+
// Either way, an image used only in a folded-away branch (never emitted) costs no flash.
|
|
2778
|
+
if (out.bakeAllImages) for (const [, imp] of imageImports) out.images.set(imp.name, imp.importPath);
|
|
2779
|
+
|
|
2780
|
+
// Effects: `useEffect(fn, [])` runs once at mount; `useEffect(fn, [dep…])` re-runs from app_update when a
|
|
2781
|
+
// dep changes (see compileEffect). Compiled after emit so out.timerFns/usesTimers reflect handler timers too.
|
|
2782
|
+
for (const eff of collectEffects(component.body)) {
|
|
2783
|
+
compileEffect(eff, env, state, out);
|
|
2784
|
+
}
|
|
2785
|
+
|
|
2786
|
+
const nodeDecls = Array.from({ length: out.n }, (_, i) => `n${i}`);
|
|
2787
|
+
// App state + every inlined child instance's per-instance state (each already namespaced via cField).
|
|
2788
|
+
const stateRecords = [...state.byName.values(), ...out.childStateRecords];
|
|
2789
|
+
const scalarRecords = stateRecords.filter((s) => s.kind === 'scalar');
|
|
2790
|
+
const listRecords = stateRecords.filter((s) => s.kind === 'list');
|
|
2791
|
+
|
|
2792
|
+
// Scalar state → one ErAppState struct. List state → a fixed-capacity struct array + a count each.
|
|
2793
|
+
const fieldCDecl = (f) => (f.kind === 'string' ? ` char ${f.key}[${LIST_STR_CAP}];` : ` ${f.kind} ${f.key};`);
|
|
2794
|
+
const itemInit = (item, struct) => `{ ${struct.fields.map((f) => (f.kind === 'string' ? cstr(String(item[f.key] ?? '')) : f.kind === 'float' ? `${Number(item[f.key]) || 0}f` : String(Math.round(Number(item[f.key]) || 0)))).join(', ')} }`;
|
|
2795
|
+
const listBlocks = listRecords
|
|
2796
|
+
.map(
|
|
2797
|
+
(s) =>
|
|
2798
|
+
`typedef struct\n{\n${s.struct.fields.map(fieldCDecl).join('\n')}\n} ${s.cTypeName};\n\n` +
|
|
2799
|
+
`static ${s.cTypeName} ${s.arrayName}[${s.cap}] = {${s.items.map((it) => '\n ' + itemInit(it, s.struct)).join(',')}\n};\n` +
|
|
2800
|
+
`static int ${s.countMember} = ${s.items.length};\n`,
|
|
2801
|
+
)
|
|
2802
|
+
.join('\n');
|
|
2803
|
+
const scalarFieldDecl = (s) => (s.cType === 'string' ? ` char ${s.cField}[${LIST_STR_CAP}];` : ` ${s.cType === 'float' ? 'float' : 'int'} ${s.cField};`);
|
|
2804
|
+
const scalarBlock = scalarRecords.length
|
|
2805
|
+
? `typedef struct\n{\n${scalarRecords.map(scalarFieldDecl).join('\n')}\n} ErAppState;\n\nstatic ErAppState s_state = {${scalarRecords.map((s) => ` .${s.cField} = ${s.initCode}`).join(',')} };\n`
|
|
2806
|
+
: '';
|
|
2807
|
+
const stateBlock = [scalarBlock, listBlocks].filter(Boolean).join('\n');
|
|
2808
|
+
|
|
2809
|
+
const handleDecls = out.handles.map((v) => `static ERNode* s_${v};`).join('\n');
|
|
2810
|
+
|
|
2811
|
+
// Value refs — a plain mutable static each (escape-hatch state that does not trigger a re-render).
|
|
2812
|
+
const refDecls = [...refs.values(), ...out.childRefs].map((r) => `static ${r.cType} ${r.cVar} = ${r.initCode};`).join('\n');
|
|
2813
|
+
|
|
2814
|
+
// Baked vector op-tapes + paint tables (static <Svg> geometry), emitted at file scope.
|
|
2815
|
+
const vectorBlock = out.vectorData.join('\n\n');
|
|
2816
|
+
// build_svgN() recompute functions (state-driven Svgs) — declared before app_update, which calls them.
|
|
2817
|
+
const vectorBuilderBlock = out.vectorBuilders.join('\n\n');
|
|
2818
|
+
|
|
2819
|
+
// Animated values — one engine-side handle each, created at the top of er_app_build (binds reference them).
|
|
2820
|
+
const animList = [...anims.values(), ...out.childAnims];
|
|
2821
|
+
const animDecls = animList.map((a) => `static ERAnimValueHandle ${a.cVar};`).join('\n');
|
|
2822
|
+
const animCreate = animList.map((a) => ` ${a.cVar} = er_anim_value_create(${a.initCode});`).join('\n');
|
|
2823
|
+
|
|
2824
|
+
const hasUpdate = out.updates.length > 0 || out.svgUpdates.length > 0 || out.depEffects.length > 0;
|
|
2825
|
+
const updateBlock = (() => {
|
|
2826
|
+
if (!hasUpdate) return '';
|
|
2827
|
+
const lines = ['static void app_update(void)', '{'];
|
|
2828
|
+
if (out.updates.length) lines.push(' ERProps p;');
|
|
2829
|
+
for (const u of out.updates) {
|
|
2830
|
+
lines.push(` er_props_default(&p);`);
|
|
2831
|
+
for (const a of u.styleAssigns) lines.push(` p.${a.field} = ${a.expr};`);
|
|
2832
|
+
for (const a of u.dynAssigns) lines.push(` p.${a.field} = ${a.code};`);
|
|
2833
|
+
if (u.placeholder != null) lines.push(` snprintf(p.placeholder, sizeof(p.placeholder), "%s", ${cstr(u.placeholder)});`);
|
|
2834
|
+
if (u.imageName != null) lines.push(` snprintf(p.image_name, sizeof(p.image_name), "%s", ${u.imageName});`);
|
|
2835
|
+
if (u.text) {
|
|
2836
|
+
if (u.text.args.length) lines.push(` snprintf(p.text, sizeof(p.text), ${cstr(u.text.format)}, ${u.text.args.join(', ')});`);
|
|
2837
|
+
else lines.push(` snprintf(p.text, sizeof(p.text), "%s", ${cstr(u.text.format.replace(/%%/g, '%'))});`);
|
|
2838
|
+
}
|
|
2839
|
+
lines.push(` er_node_set_props(s_${u.v}, &p);`);
|
|
2840
|
+
}
|
|
2841
|
+
// State-driven Svgs: recompute the op-tape from state and re-upload.
|
|
2842
|
+
for (const s of out.svgUpdates) {
|
|
2843
|
+
lines.push(` build_svg${s.id}();`);
|
|
2844
|
+
lines.push(` er_node_set_vector_ops(${s.nodeVar}, s_svg${s.id}_ops, ${s.len}, s_svg${s.id}_paints, ${s.nPaints});`);
|
|
2845
|
+
}
|
|
2846
|
+
// Dep-driven useEffect: run each effect whose dependency value changed since the last app_update.
|
|
2847
|
+
for (const block of out.depEffects) lines.push(block);
|
|
2848
|
+
lines.push('}');
|
|
2849
|
+
return lines.join('\n');
|
|
2850
|
+
})();
|
|
2851
|
+
|
|
2852
|
+
const handlerDefs = out.handlers
|
|
2853
|
+
.map((h) => `static void ${h.name}(ERNode* node, const EREventData* data, void* user_data)\n{\n (void)node;\n (void)data;\n (void)user_data;\n${h.body.join('\n')}\n}`)
|
|
2854
|
+
.join('\n\n');
|
|
2855
|
+
|
|
2856
|
+
// Dep-driven useEffect: a static "previous value" per dependency, a forward decl (app_update calls each
|
|
2857
|
+
// er_effect_N before it's defined), and the effect body as a parameterless C function.
|
|
2858
|
+
const effectDeclsBlock = out.effectDecls.join('\n');
|
|
2859
|
+
const effectFwdDecls = out.effectFns.map((f) => `static void ${f.name}(void);`).join('\n');
|
|
2860
|
+
const effectFnDefs = out.effectFns.map((f) => `static void ${f.name}(void)\n{\n${f.body.join('\n')}\n}`).join('\n\n');
|
|
2861
|
+
|
|
2862
|
+
// setInterval/setTimeout → a small fixed timer table advanced by er_app_tick(dt) (the host calls it each
|
|
2863
|
+
// frame). The table + helpers are emitted only when timers are used; er_app_tick is always defined (a no-op
|
|
2864
|
+
// otherwise) so a host can call it unconditionally. Timer callbacks become parameterless C functions.
|
|
2865
|
+
const timerTableBlock = out.usesTimers
|
|
2866
|
+
? `#include <stdbool.h>
|
|
2867
|
+
|
|
2868
|
+
#ifndef ER_AOT_MAX_TIMERS
|
|
2869
|
+
#define ER_AOT_MAX_TIMERS 8
|
|
2870
|
+
#endif
|
|
2871
|
+
|
|
2872
|
+
typedef struct
|
|
2873
|
+
{
|
|
2874
|
+
int interval_ms;
|
|
2875
|
+
int remaining_ms;
|
|
2876
|
+
bool repeat;
|
|
2877
|
+
bool active;
|
|
2878
|
+
void (*fn)(void);
|
|
2879
|
+
} ErTimer;
|
|
2880
|
+
static ErTimer s_timers[ER_AOT_MAX_TIMERS];
|
|
2881
|
+
|
|
2882
|
+
static int er_timer_add(int ms, bool repeat, void (*fn)(void))
|
|
2883
|
+
{
|
|
2884
|
+
for (int i = 0; i < ER_AOT_MAX_TIMERS; i++)
|
|
2885
|
+
{
|
|
2886
|
+
if (!s_timers[i].active)
|
|
2887
|
+
{
|
|
2888
|
+
s_timers[i].interval_ms = ms < 1 ? 1 : ms;
|
|
2889
|
+
s_timers[i].remaining_ms = s_timers[i].interval_ms;
|
|
2890
|
+
s_timers[i].repeat = repeat;
|
|
2891
|
+
s_timers[i].active = true;
|
|
2892
|
+
s_timers[i].fn = fn;
|
|
2893
|
+
return i;
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
return -1; /* table full (raise ER_AOT_MAX_TIMERS) */
|
|
2897
|
+
}
|
|
2898
|
+
|
|
2899
|
+
static void er_timer_clear(int id)
|
|
2900
|
+
{
|
|
2901
|
+
if (id >= 0 && id < ER_AOT_MAX_TIMERS)
|
|
2902
|
+
{
|
|
2903
|
+
s_timers[id].active = false;
|
|
2904
|
+
}
|
|
2905
|
+
}`
|
|
2906
|
+
: '';
|
|
2907
|
+
|
|
2908
|
+
const timerFnDefs = out.timerFns.map((t) => `static void ${t.name}(void)\n{\n${t.body.join('\n')}\n}`).join('\n\n');
|
|
2909
|
+
|
|
2910
|
+
// Animated.sequence on_complete callbacks: each starts the next step when the previous finishes. Forward-
|
|
2911
|
+
// declared (the handler that starts step 0 references the first callback, and each callback the next).
|
|
2912
|
+
const animCbDecls = out.animCbs.map((cb) => `static void ${cb.name}(bool finished, void* user_data);`).join('\n');
|
|
2913
|
+
const animCbDefs = out.animCbs
|
|
2914
|
+
.map((cb) => `static void ${cb.name}(bool finished, void* user_data)\n{\n (void)finished;\n (void)user_data;\n${cb.body.join('\n')}\n}`)
|
|
2915
|
+
.join('\n\n');
|
|
2916
|
+
|
|
2917
|
+
const appTickFn = out.usesTimers
|
|
2918
|
+
? `void er_app_tick(int dt_ms)
|
|
2919
|
+
{
|
|
2920
|
+
for (int i = 0; i < ER_AOT_MAX_TIMERS; i++)
|
|
2921
|
+
{
|
|
2922
|
+
if (!s_timers[i].active)
|
|
2923
|
+
{
|
|
2924
|
+
continue;
|
|
2925
|
+
}
|
|
2926
|
+
s_timers[i].remaining_ms -= dt_ms;
|
|
2927
|
+
if (s_timers[i].remaining_ms <= 0)
|
|
2928
|
+
{
|
|
2929
|
+
void (*fn)(void) = s_timers[i].fn;
|
|
2930
|
+
if (s_timers[i].repeat)
|
|
2931
|
+
{
|
|
2932
|
+
s_timers[i].remaining_ms += s_timers[i].interval_ms;
|
|
2933
|
+
if (s_timers[i].remaining_ms <= 0)
|
|
2934
|
+
{
|
|
2935
|
+
s_timers[i].remaining_ms = s_timers[i].interval_ms; /* dt ran long; don't spiral */
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
else
|
|
2939
|
+
{
|
|
2940
|
+
s_timers[i].active = false;
|
|
2941
|
+
}
|
|
2942
|
+
if (fn)
|
|
2943
|
+
{
|
|
2944
|
+
fn();
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
}`
|
|
2949
|
+
: `void er_app_tick(int dt_ms)\n{\n (void)dt_ms;\n}`;
|
|
2950
|
+
|
|
2951
|
+
const mountEffectsBlock = out.mountEffects.length ? '\n /* useEffect(fn, []) — run once on mount. */\n' + out.mountEffects.join('\n') + '\n' : '';
|
|
2952
|
+
|
|
2953
|
+
// <math.h> when any libm symbol appears (Svg arc trig, or Math.* in expressions/handlers/timer callbacks).
|
|
2954
|
+
const usesMath = out.needsMath || /\b(sinf|cosf|tanf|sqrtf|fabsf|roundf|floorf|ceilf|fminf|fmaxf|atan2f|powf|M_PI)\b/.test([stateBlock, refDecls, vectorBuilderBlock, updateBlock, handlerDefs, animCbDefs, timerFnDefs, out.mountEffects.join('\n'), out.build.join('\n')].join('\n'));
|
|
2955
|
+
const body = `/*
|
|
2956
|
+
* Generated by the embedded-react Flow B AOT compiler (npm run aot -- ${demo}). DO NOT EDIT.
|
|
2957
|
+
* Builds the app's scene graph + state machine directly against er_scene.h — no QuickJS, no JS runtime.
|
|
2958
|
+
*/
|
|
2959
|
+
#include "app.gen.h"
|
|
2960
|
+
|
|
2961
|
+
#include "er_scene.h"
|
|
2962
|
+
#include "er_version.h"
|
|
2963
|
+
|
|
2964
|
+
#include <stdio.h>
|
|
2965
|
+
#include <string.h>
|
|
2966
|
+
|
|
2967
|
+
/* Version-pin: this file was generated by embedded-react ${PKG_VERSION}. The engine ships LOCKSTEP, so its
|
|
2968
|
+
headers must be the same major.minor — otherwise these generated er_scene.h calls may not match the ABI.
|
|
2969
|
+
A mismatch fails HERE at compile time (not on-device). Regenerate the app (npm run aot) or align versions. */
|
|
2970
|
+
_Static_assert(ER_VERSION_MAJOR == ${PKG_MAJOR} && ER_VERSION_MINOR == ${PKG_MINOR},
|
|
2971
|
+
"embedded-react version mismatch: app.gen.c was generated by ${PKG_VERSION} but the engine header (er_version.h) is a different major.minor. Regenerate the app with 'npm run aot', or align the engine and npm versions.");
|
|
2972
|
+
${usesMath ? '#include <math.h>\n' : ''}${stateBlock ? '\n' + stateBlock : ''}${refDecls ? '\n' + refDecls + '\n' : ''}${effectDeclsBlock ? '\n' + effectDeclsBlock + '\n' : ''}${vectorBlock ? '\n' + vectorBlock + '\n' : ''}${vectorBuilderBlock ? '\n' + vectorBuilderBlock + '\n' : ''}${animDecls ? '\n' + animDecls + '\n' : ''}${handleDecls ? '\n' + handleDecls + '\n' : ''}${timerTableBlock ? '\n' + timerTableBlock + '\n' : ''}${effectFwdDecls ? '\n' + effectFwdDecls + '\n' : ''}${updateBlock ? '\n' + updateBlock + '\n' : ''}${animCbDecls ? '\n' + animCbDecls + '\n' : ''}${handlerDefs ? '\n' + handlerDefs + '\n' : ''}${effectFnDefs ? '\n' + effectFnDefs + '\n' : ''}${animCbDefs ? '\n' + animCbDefs + '\n' : ''}${timerFnDefs ? '\n' + timerFnDefs + '\n' : ''}${out.kbdData ? '\n' + out.kbdData + '\n' : ''}
|
|
2973
|
+
${appTickFn}
|
|
2974
|
+
|
|
2975
|
+
void er_app_build(int screen_w, int screen_h)
|
|
2976
|
+
{
|
|
2977
|
+
ERProps p;
|
|
2978
|
+
ERNode* ${nodeDecls.join(';\n ERNode* ')};
|
|
2979
|
+
|
|
2980
|
+
/* A screen-sized root the app tree fills (mirrors AppRegistry mounting into a screen-sized host). */
|
|
2981
|
+
ERNode* root = er_node_create(ER_NODE_VIEW);
|
|
2982
|
+
er_props_default(&p);
|
|
2983
|
+
p.width = (int16_t)screen_w;
|
|
2984
|
+
p.height = (int16_t)screen_h;
|
|
2985
|
+
er_node_set_props(root, &p);
|
|
2986
|
+
${animCreate ? '\n' + animCreate + '\n' : ''}
|
|
2987
|
+
${out.build.join('\n')}
|
|
2988
|
+
er_tree_append_child(root, ${appTop});
|
|
2989
|
+
er_tree_set_root(root);
|
|
2990
|
+
${out.kbdSetup ? out.kbdSetup + ' /* app-supplied on-screen keyboard layout/appearance */\n' : ''}${hasUpdate ? '\n app_update(); /* apply initial state-dependent props */\n' : ''}${mountEffectsBlock}}
|
|
2991
|
+
`;
|
|
2992
|
+
|
|
2993
|
+
const header = `/* Generated by the embedded-react Flow B AOT compiler. DO NOT EDIT. */
|
|
2994
|
+
#ifndef ER_APP_GEN_H
|
|
2995
|
+
#define ER_APP_GEN_H
|
|
2996
|
+
|
|
2997
|
+
/** @brief Builds the AOT-compiled app's scene graph + state machine (call once after backend init). */
|
|
2998
|
+
void er_app_build(int screen_w, int screen_h);
|
|
2999
|
+
|
|
3000
|
+
/** @brief Advances app timers (setInterval/setTimeout). Call once per frame with the elapsed ms; a no-op
|
|
3001
|
+
* for apps that use no timers, so it is always safe to call. */
|
|
3002
|
+
void er_app_tick(int dt_ms);
|
|
3003
|
+
|
|
3004
|
+
#endif
|
|
3005
|
+
`;
|
|
3006
|
+
|
|
3007
|
+
// images: the baked-image imports the app actually references (name + source-relative path) — the CLI
|
|
3008
|
+
// resolves each path against the demo dir and bakes them into assets.generated.c (er_register_assets).
|
|
3009
|
+
const images = [...out.images.entries()].map(([name, importPath]) => ({ name, importPath }));
|
|
3010
|
+
return { c: body, h: header, nodes: out.n, state: stateRecords.length, handlers: out.handlers.length, updates: out.updates.length, images };
|
|
3011
|
+
}
|
|
3012
|
+
|
|
3013
|
+
/** Public entry: compile JSX source → { c, h, ... }. On an AOT error, annotate it with file:line:col + a
|
|
3014
|
+
* source code-frame (+ hint) so the failure points at the exact unsupported construct. */
|
|
3015
|
+
export function compileSource(src, demo = 'app', opts = {}) {
|
|
3016
|
+
try {
|
|
3017
|
+
return compileSourceImpl(src, demo, opts);
|
|
3018
|
+
} catch (e) {
|
|
3019
|
+
if (e && typeof e.message === 'string' && e.message.startsWith('AOT:')) {
|
|
3020
|
+
throw formatAotError(e, src, opts.filename || `demos/${demo}/App.jsx`);
|
|
3021
|
+
}
|
|
3022
|
+
throw e;
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
|
|
3026
|
+
// ---------------------------------------------------------------------------------------------------
|
|
3027
|
+
// CLI entry — `node aot/compile.mjs [demo]`: read a demo's App.jsx, write dist/app.gen.{c,h}. Runs only
|
|
3028
|
+
// when this file is invoked directly (not when imported by the test harness).
|
|
3029
|
+
// ---------------------------------------------------------------------------------------------------
|
|
3030
|
+
if (process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))) {
|
|
3031
|
+
const demo = process.argv[2] || process.env.DEMO || 'thermostat';
|
|
3032
|
+
const appPath = resolve(demosDir, demo, 'App.jsx');
|
|
3033
|
+
if (!existsSync(appPath)) {
|
|
3034
|
+
const avail = existsSync(demosDir) ? readdirSync(demosDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name) : [];
|
|
3035
|
+
console.error(`AOT: demo "${demo}" not found (expected ${appPath}). Available: ${avail.join(', ') || '(none)'}`);
|
|
3036
|
+
process.exit(1);
|
|
3037
|
+
}
|
|
3038
|
+
let result;
|
|
3039
|
+
try {
|
|
3040
|
+
result = compileSource(readFileSync(appPath, 'utf8'), demo, { filename: resolve(demosDir, demo, 'App.jsx') });
|
|
3041
|
+
} catch (e) {
|
|
3042
|
+
// A located AOT error already reads as "<reason>\n at file:line:col\n\n<frame>\n\nhint: ..."; print it
|
|
3043
|
+
// cleanly (no JS stack) so the developer sees exactly the unsupported construct.
|
|
3044
|
+
console.error(e && e.aotLoc ? e.message : e?.message || String(e));
|
|
3045
|
+
process.exit(1);
|
|
3046
|
+
}
|
|
3047
|
+
mkdirSync(distDir, { recursive: true });
|
|
3048
|
+
writeFileSync(resolve(distDir, 'app.gen.c'), result.c);
|
|
3049
|
+
writeFileSync(resolve(distDir, 'app.gen.h'), result.h);
|
|
3050
|
+
|
|
3051
|
+
// Bake the images the app imports into dist/assets.generated.{c,h} (er_register_assets) — the SAME baker
|
|
3052
|
+
// Flow A uses. Always written (even with 0 images → a no-op register fn) so the AOT host can always
|
|
3053
|
+
// compile + call it. Each importPath is source-relative to the demo's App.jsx.
|
|
3054
|
+
const imageJobs = result.images.map((im) => ({ name: im.name, path: resolve(demosDir, demo, im.importPath) }));
|
|
3055
|
+
for (const j of imageJobs) {
|
|
3056
|
+
if (!existsSync(j.path)) {
|
|
3057
|
+
console.error(`AOT: <Image> asset "${j.name}" not found at ${j.path}`);
|
|
3058
|
+
process.exit(1);
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3061
|
+
const baked = bakeAssets({ images: imageJobs, fonts: [], outDir: distDir });
|
|
3062
|
+
console.log(
|
|
3063
|
+
`AOT: compiled demo "${demo}" -> dist/app.gen.c (${result.nodes} nodes, ${result.state} state, ` +
|
|
3064
|
+
`${result.handlers} handler(s), ${result.updates} dynamic) + ${baked.images} image(s) -> dist/assets.generated.c`,
|
|
3065
|
+
);
|
|
3066
|
+
}
|