The AOT subset
The Flow B compiler turns JSX into a package. It can only do that for code it can fully resolve at build time, so it accepts a subset of what runs under Flow A and refuses the rest, at build time, with the file and line:
AOT: a state-driven <Path d=…> is not yet supported (use Arc/Circle/Rect/Line for dynamic shapes)
at App.jsx:41:12
The rules come in two kinds. Some are permanent, because a compiled app cannot do what only an
interpreter could: a component tree shaped by data that arrives at runtime, or a <Path d> string
built on the fly. Many more are simply not yet implemented, and the compiler says so in its
message (not yet supported). Those are the ones that shrink with each release; see
Where the subset is going. This page lists the rules by area as they
stand today, then the habits that keep an app inside them.
The browser simulator runs Flow A and accepts everything. If a board without external RAM is the
target, run npx embedded-react build --aot from the first day, so each rule turns up
while the code that breaks it is still one line.
Components and props
- The entry is
export function App() {...}and a component body must return a JSX element. A component that branches at the top level withifmay only test a compile-time constant, such as thescreenglobal; runtime layout branching is expressed inside JSX with&&or a ternary. The left of a state-driven&&must be a condition, not a number: React draws a0as text, so writecount > 0 && <El/>. - A state-driven
&&or ternary builds the branch it picks and destroys it when another is picked, so the package needs nodes for the largest set shown at once. To keep a small branch that flips often built, write<El visible={cond}/>instead. In a list row, every branch is built with the row and toggled. The state of a component in a branch is kept while the branch is gone.updateVectorcan't draw on a node in a branch. - Components live in the same file as
App: local.jsximports are not resolved. - A component that receives state-driven or function props must destructure them:
function Card({title}).function Card(props)works only when every prop is a constant. Rest props (...rest) are not supported. A spread onto a component ({...x}) must be a compile-time-constant object. - Every element takes a fixed set of props, style keys, and children; anything else is an
AOT:error at the prop. That covers a prop the element does not have, a style key the engine does not draw on it (backgroundColoron a<Text>,borderWidthon an<Image>; see Styles), text outside a<Text>, and children on an<Image>,<Switch>,<TextInput>or<ActivityIndicator>.testIDand the accessibility props are accepted and ignored everywhere but<FlatList>. <Modal>needs avisibleprop, and itsanimationTypecan only be'none';<Switch>withonValueChangeneeds avalueprop, and cannot bedisabled;disabledon a<Pressable>or<TouchableOpacity>andactiveOpacitymust be constants.<Image source>takes an imported asset, a string asset name,{uri: 'name'}or string state; a state-driven source bakes every image the app imports.<Text numberOfLines>andellipsizeModemust be constants, andellipsizeModeis'tail'or'clip'.visibleor an event on an<Svg>is not supported: wrap it in a<View>or<Pressable>.<TextInput>,<Switch>,<ActivityIndicator>,Animated.View/Text/Imageand a module-levelsetKeyboardConfig({...})also compile.<FlatList>honoursdata,renderItem,keyExtractorandstyleonly, andrenderItemmust destructure({item, index}). For anything more, use<ScrollView>with.map.
State, hooks, and lists
useState,setState(including the updater form),useEffectwith an array-literal dependency list and an optional cleanup as its last statement,useRef,useCallback,useMemo, anduseAnimatedValuewith a constant initial value (everyAnimatedcall drives one).useHostValue(initial)is Flow B's way to feed host data (a sensor, a clock) into the app: the compiler lists it as a package input, and the host sets it witherb_input_set_int. The RP2040 guide shows it driving a step counter..mapover a list compiles when the list is a compile-time-constant array, or state that the app appends object literals to; the row pool is sized byER_AOT_LIST_CAP(16 by default, lower it on a tight board). A list computed at runtime from anything else does not compile.Date.now()andperformance.now()work and are 64-bit whole milliseconds. Keep a timestamp in state, a ref, or a local; compare it; show it; divide it by a constant with%orMath.floor/ceil/round/trunc(a / b). A plain/, a divisor from state, or mixing it with a float or a boolean is a compile error. There are noDateobjects.- Whole-number arithmetic is defined at the edges:
+,-,*and negation saturate rather than overflow,x % 0is 0,x / 0saturates, and a float that is NaN or out of range becomes 0 or the nearest integer. Constant maths is folded at build time.
Handlers
An event handler body may contain const locals holding a number, a boolean, or a string; if and
else; state setter calls; ref.current writes, including += and ++; setInterval,
setTimeout and their clears; updateVector calls; and .start() or .stop() on any Animated
animation or composition. Locals cannot be reassigned or destructured, and loops, switch and try
are not lowered. A helper function called as a statement is inlined, but one used inside an
expression is refused, so express a calculation as a chain of consts and ternaries.
setInterval/setTimeout callbacks must be inline functions. updateText is Flow A only.
Styles and colours
- Colors are
#rgb,#rrggbb,#rrggbbaaor a named color.rgba()andhsl()are not parsed. A state-driven color must be a color literal or a ternary of literals. - A
View'sbackgroundColoralpha is ignored (the fill is opaque); borders and vector paints do blend. For a "10% tint" fill, pre-mix a solid color. - Static or state-driven is decided per style key, so constants and state can share one inline
object. A state-driven value works on colors,
opacity, sizes, margins, padding,gap,flexGrow/flexShrink, border radii,borderWidth,zIndex,fontSize,lineHeight,letterSpacingand the enumsflexDirection,alignItems,alignSelf,justifyContent,positionanddisplay.flex,flexBasis,left/top/right/bottomandfontWeightmust be constants. - A state-driven enum style (
display,flexDirectionand the like) must be a string literal or a ternary of literals.display: page === 'home' ? 'flex' : 'none'compiles;visible={bool}takes any boolean. fontFamily,textAlignandoverfloware not in the subset. Text uses the built-in font; center it with a wrappingViewandalignItems: 'center'.
Text
A <Text> compiles to one formatted string. Interpolation (count is {count}) and one level of nested
<Text> spans work; a span inside a span, a dynamic {...} segment inside a multi-span text, and a
state-driven style on a span do not. A ternary in text cannot mix a string branch with a numeric
one, or a boolean branch with a non-boolean one; split those into two {cond && <Text/>} nodes.
Comparing a string with a number is refused rather than coerced. A nested <Text> holds at most 4
spans by default; ER_AOT_MAX_TEXT_SPANS raises it, and must match the engine's ER_TEXT_MAX_SPANS.
Text can only show glyphs the built-in font bakes: printable ASCII plus a fixed symbol set. ×,
•, −, – and — are there; · and ✕ are not, and render as ?. The build warns about any
character it cannot find. See Assets.
Animation
Animated.timing,spring,decay,sequence,parallel,stagger,delayandloopall compile, with.start()andanim.stop().loopcan repeat a timing, spring, decay or sequence (not a parallel or stagger), anditerationsmust be a build-time constant. Asequencetakestiming,spring,decayanddelayentries, but not a nestedparallel,staggerorloop; a sequence inside aparallelorstaggerneeds a known duration, so no spring or decay there. A.start(callback)on a parallel or stagger of more than one animation is not yet supported; besides delays, one animation's callback runs when whichever ends last has finished.interpolatetakes an object literal withinputRangeandoutputRangeof equal length, up to 8 breakpoints.- The engine drives every animation natively once started, in both flows.
Vector shapes and dials
- A static
<Svg source={imported.svg}>supports the full baked feature set:viewBox, groups, gradients. A state-driven<Svg>(shapes whose props come from state) uses absolute pixels, with noviewBoxand no<G>, and its children must be literal shape elements. - An
<Svg>cannot sit directly under a state-driven&&or ternary, or in a row of a state list: put the condition on a wrapping<View>. - A state-driven
<Path d>is not supported. Use<Arc>,<Circle>,<Rect>and<Line>for geometry that moves; they takeMath.sin/Math.cosin their props. - By default one
<Svg>node holds at most 16 shapes, and the engine 8 vector nodes at once (ERUI_VECTOR_PAINTS_MAX,ERUI_MAX_VECTOR_NODES; see Memory). Combine many tick marks into one<Path>with several subpaths rather than many<Line>s. - For a dial, gauge or progress ring, use
<Dial>: one engine node, animated natively, with built-in drag-to-set. ItsindicatorGradientmust be a static object with 2 to 4 stops, and itsonChangean inline function or auseCallback.
Gestures
PanResponder.create({...}) compiles when kept in a useRef and given an object literal of
callbacks, spread onto a host element. Should-set predicates must be a single boolean expression.
onPanResponderStart/End are refused: they report extra fingers, and Flow B lowers one gesture
per responder. The gesture itself runs
in the engine's C responder negotiation, so the compiled version is not a transpilation.
Assets and screen size
Images and SVGs must be imports (import logo from './logo.png').
Values derived from screen.width and screen.height (with + - * /, comparisons, ternaries and
Math.min, max, floor, ceil, round, trunc and abs) are worked out at boot from the real
panel. A branch on screen whose two sides differ only in values (styles, props, {text}) is one
tree whose values are picked at boot.
A branch that changes the tree adds a layout. A package declares no sizes: the compiler finds the layouts from the app's breakpoints, proves every panel size up to 32767×32767 has one, and stores each with a check. The firmware passes its resolution and the loader picks the first layout whose check passes, so one package runs on every panel. Anything identical between layouts is stored once, and only the chosen layout uses RAM.
const WIDE = screen.width >= 480;
export function App() {
return WIDE ? <TwoColumns /> : <OneColumn />;
}
A top-level if (COMPACT) return ... or a returned ternary keeps a whole layout in one place.
- A state's initial value derived from
screenis worked out at boot, as long as it is a number or a boolean. A string adds a layout for each value. width={screen.width}on an<Svg viewBox>is worked out at boot, and the drawing scales with no layout per size. Shapes calculated fromscreenfail the build, naming the expression: draw in the viewBox's units instead.- Every layout has to compile. One that doesn't fails the build, and the error names a size that picks it. A package holds at most 16 layouts; past that the build fails, naming the expression that changes the most.
--screen 240x320,800x480only drops layouts. It keeps the ones those sizes pick, which saves the flash of the rest. A panel that picks a dropped layout shows the error screen.
Limits worth knowing:
- Fonts are baked at fixed sizes. Breakpoint sizes work, but
fontSize: screen.width / 10snaps to the nearest baked size, with a warning. - Every board gets every layout, including the images and fonts only one layout uses, unless
--screendrops them. - The build proves each layout compiles, not that it looks right. A very small screen gets the smallest layout and may overflow, as on the web.
Fixed limits
A compiled app keeps its state in buffers sized at build time. The compiler checks each limit where it can know the answer, and the app reports the rest at run time:
| Limit | Default | Raise it with | When it is past |
|---|---|---|---|
| Rows in a list state | 16 | ER_AOT_LIST_CAP | An initial list with more rows fails the build. An append to a full list is skipped and reported. |
| Bytes in a string state, a list's string field or a local | 47 (a 48-byte buffer) | ER_AOT_LIST_STR_CAP | A constant string that is too long, as an initial value or a write, fails the build. A string built at run time is cut short and reported. |
Bytes in a node's text, a <Text> span, a placeholder or an image name | 255, 63, 63, 63 | Fixed in er_scene.h (ER_TEXT_MAX and the like) | A constant string that is too long fails the build. A string built at run time is cut short and reported. |
| Engine nodes | 512 | ERUI_MAX_NODES | The app's node count is fixed when it compiles, and the runtime checks it against ERUI_MAX_NODES when it loads the package, refusing one a pool can't hold. A node the pool cannot give is reported. |
| Live timers | 8 | ER_AOT_MAX_TIMERS | A setTimeout or setInterval past it never fires, and is reported. |
| Animation completion waits | 8 | Fixed in the compiler | The run ends at once, as not finished, and is reported. |
ER_AOT_LIST_CAP, ER_AOT_LIST_STR_CAP and ER_AOT_MAX_TIMERS are read by the compiler (npm run aot, or
embedded-react build --aot). They size what the package holds, so raising one needs no firmware rebuild. A report is
one line through the runtime's log hook, once for each place in the app that can hit it, and names the limit to raise:
ERB: list full; the row was dropped (raise ER_AOT_LIST_CAP) (App.jsx:41)
Where the subset is going
The subset is a snapshot, not a boundary. Flow B started with the parts of the API a compiler can
resolve most directly, and every release since has widened it; Animated.loop around a sequence,
counted loops, anim.stop(), useHostValue, PanResponder, <Dial> and 64-bit time all arrived
after the first version. The direction is for anything that runs in the simulator to compile, and
for the remaining gap to be the permanent kind.
Queued next, from the roadmap:
Button,ImageBackgroundandSectionList. The three JavaScript-only wrappers that the compiler currently rejects by name, with the tree to write by hand. The first two are fixed rewrites of the kindFlatListalready gets; a section is a header plus a variable-length list, which the list unroller has no shape for yet.- Live
toValue. An animation whose target depends on current state animates toward the value captured when the animation was set up, not the live one. Tracked for the animation hardening pass. - Animation composition on hardware. Hardening
sequence,parallel,loopandinterpolate, and imperative refs, on the no-PSRAM boards.
If a rule on this page is in your way, say so in an issue: the order the gaps close in follows what real apps hit.
Keeping an app in the subset
- Build for Flow B from the start, and let the compiler's messages shape the code.
- Separate static from dynamic:
StyleSheet.createfor what never changes, a small inline overlay for what does. - Express handler logic as
constchains. If a calculation wants a helper, call the helper as a statement, or compute the value in render and pass it down. - Reach for
<Dial>and<Arc>before a<Path>that would need to change. - Use
visibleanddisplay: 'none'to switch pages; they cost no node churn in either flow.
The demos in the repository build both ways and are the best worked examples; the thermostat's compact layout in particular is a tour of these rules.