@typecad/cuttlefish 1.0.0-alpha.11 → 1.0.0-alpha.13
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/dist/add-preset.d.ts +4 -0
- package/dist/add-preset.js +74 -0
- package/dist/api/config.d.ts +5 -5
- package/dist/api/shared/display-adapters/sdl.js +1 -1
- package/dist/api/shared/display-profile.d.ts +11 -0
- package/dist/api/shared/display-profile.js +3 -0
- package/dist/api/shared/framework-manifest.d.ts +85 -78
- package/dist/api/shared/framework-manifest.js +1 -0
- package/dist/api/shared/hal-op-ir.d.ts +19 -0
- package/dist/api/shared/toolchain-types.d.ts +0 -1
- package/dist/cli.js +21 -4
- package/dist/config-loader.d.ts +8 -2
- package/dist/config-loader.js +202 -53
- package/dist/config-schema.d.ts +7 -7
- package/dist/config-schema.js +3 -3
- package/dist/create/board-spec.d.ts +122 -122
- package/dist/create/debug-artifacts.d.ts +20 -0
- package/dist/create/debug-artifacts.js +69 -0
- package/dist/create/eslint-rules-template.js +6 -3
- package/dist/create/index.d.ts +2 -0
- package/dist/create/index.js +1 -0
- package/dist/create/init-scaffold.d.ts +1 -0
- package/dist/create/init-scaffold.js +5 -0
- package/dist/create/init-templates.js +0 -2
- package/dist/emit/compliance/rules.js +52 -4
- package/dist/emit/emitters/function-emitter-impl.js +7 -1
- package/dist/emit/emitters/line-appender.js +6 -0
- package/dist/emit/emitters/setup.js +15 -2
- package/dist/emit/emitters/ui-emitter.js +40 -15
- package/dist/emit/route-hal-op.js +55 -1
- package/dist/emit/statement-renderer.js +5 -2
- package/dist/ir/build-ir.js +22 -1
- package/dist/ir/expression-to-ir.js +17 -0
- package/dist/ir/feature-registry.js +22 -6
- package/dist/ir/hal/hal-emitter.js +23 -5
- package/dist/ir/hal/hal-plugins.js +11 -0
- package/dist/ir/pin-mode-validation.js +32 -9
- package/dist/ir/pin-state-tracking.d.ts +58 -0
- package/dist/ir/pin-state-tracking.js +182 -0
- package/dist/ir/program-analysis.d.ts +10 -2
- package/dist/ir/program-analysis.js +40 -4
- package/dist/ir/statement-to-ir.js +14 -0
- package/dist/ir/transformers/control-flow.js +29 -0
- package/dist/ir/transformers/ui-call-resolver.js +105 -1
- package/dist/ir/ui-element-auto-wire.js +7 -4
- package/dist/orchestrator/graph-builder.d.ts +4 -1
- package/dist/orchestrator/graph-builder.js +7 -1
- package/dist/platform/async-runtime.d.ts +1 -1
- package/dist/platform/async-runtime.js +12 -3
- package/dist/platform/generic-strategy.js +1 -1
- package/dist/preview/api-shared-shim.d.ts +1 -0
- package/dist/preview/api-shared-shim.js +7 -0
- package/dist/preview/client.js +220 -1
- package/dist/preview/server.js +154 -62
- package/dist/theme-tokens.d.ts +22 -0
- package/dist/theme-tokens.js +172 -0
- package/dist/transpile.js +90 -12
- package/dist/types.d.ts +5 -0
- package/dist/ui-hook.d.ts +7 -0
- package/dist/utils/cli.js +9 -0
- package/dist/utils/fs.d.ts +2 -0
- package/dist/utils/fs.js +16 -0
- package/dist/utils/ui.d.ts +5 -0
- package/dist/utils/ui.js +7 -0
- package/package.json +7 -5
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { ProgramIR } from "../api/index.js";
|
|
2
|
+
export type TrackedPinLevel = 'high' | 'low' | 'unknown';
|
|
3
|
+
/** C++ identifier for a pin's shadow state variable. */
|
|
4
|
+
export declare function pinShadowVarName(pin: number): string;
|
|
5
|
+
export declare function resetPinStateTracking(): void;
|
|
6
|
+
/** Record gpio.set_mode — OUTPUT* modes enable tracking, anything else ends it. */
|
|
7
|
+
export declare function notePinSetMode(pin: number, mode: string | null): void;
|
|
8
|
+
/** Record gpio.write — literal values set the level; expressions make it unknown. */
|
|
9
|
+
export declare function notePinWrite(pin: number, literalValue: number | boolean | null): void;
|
|
10
|
+
/** Record gpio.toggle — inverts a known level, keeps unknown as unknown. */
|
|
11
|
+
export declare function notePinToggle(pin: number): void;
|
|
12
|
+
/**
|
|
13
|
+
* Record PWM/tone output — the pin no longer has a digital level, so any
|
|
14
|
+
* tracked level goes unknown (reads must not fold to the pre-PWM literal).
|
|
15
|
+
* A later digital write/toggle restores digital tracking.
|
|
16
|
+
*/
|
|
17
|
+
export declare function notePinAnalogOutput(pin: number): void;
|
|
18
|
+
/**
|
|
19
|
+
* Resolve a read on a pin at the current program point.
|
|
20
|
+
* Returns 'high'/'low' when the level folds to a constant, 'shadow' when a
|
|
21
|
+
* shadow variable must carry it, or null when the pin is not a tracked
|
|
22
|
+
* output pin (unconfigured or input mode → normal hardware read lowering).
|
|
23
|
+
*/
|
|
24
|
+
export declare function resolveTrackedRead(pin: number): 'high' | 'low' | 'shadow' | null;
|
|
25
|
+
/** Whether write/toggle ops on this pin must also update its shadow variable. */
|
|
26
|
+
export declare function isShadowPin(pin: number): boolean;
|
|
27
|
+
/** Consume the shadow declarations owed by this build (pin + initializer).
|
|
28
|
+
* Clears the shadow set: emit reads the updatesShadow flags baked into the
|
|
29
|
+
* ops below, never this module's live state (each file's build resets the
|
|
30
|
+
* tracker, and all files finish building before any emit runs). */
|
|
31
|
+
export declare function takeShadowDeclarations(): {
|
|
32
|
+
pin: number;
|
|
33
|
+
initial: boolean;
|
|
34
|
+
}[];
|
|
35
|
+
/** Mark every gpio.write / gpio.toggle op in the program whose pin has a
|
|
36
|
+
* tracked shadow read, so the emitter appends the shadow-variable update.
|
|
37
|
+
* Runs once at the end of buildProgramIR, after the whole file is lowered —
|
|
38
|
+
* at that point the shadow set is final, so writes that were lowered BEFORE
|
|
39
|
+
* the first shadow read are marked too (their ops are already in the IR).
|
|
40
|
+
* This is what makes emit independent of tracker state across files. */
|
|
41
|
+
export declare function markShadowUpdatingOps(program: ProgramIR): void;
|
|
42
|
+
export type PinLevelSnapshot = Map<number, TrackedPinLevel>;
|
|
43
|
+
export declare function snapshotPinLevels(): PinLevelSnapshot;
|
|
44
|
+
export declare function restorePinLevels(snapshot: PinLevelSnapshot): void;
|
|
45
|
+
/**
|
|
46
|
+
* Merge two branch-end states into the live state: pins that agree keep
|
|
47
|
+
* their level; pins that differ (or exist in only one branch) become
|
|
48
|
+
* unknown. Pass the pre-branch state as one side when there is no else
|
|
49
|
+
* branch, so pins written in the taken branch invalidate.
|
|
50
|
+
*/
|
|
51
|
+
export declare function mergePinLevels(a: PinLevelSnapshot, b: PinLevelSnapshot): void;
|
|
52
|
+
/** Invalidate all known levels (after loop/switch bodies, which may run any
|
|
53
|
+
* number of times). Shadow tracking is unaffected — it is runtime truth. */
|
|
54
|
+
export declare function invalidatePinLevels(): void;
|
|
55
|
+
/** Disable/enable constant folding around function and callback bodies. */
|
|
56
|
+
export declare function setPinFoldingEnabled(enabled: boolean): void;
|
|
57
|
+
/** Whether pin-level constant folding is currently allowed. */
|
|
58
|
+
export declare function isPinFoldingEnabled(): boolean;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Output Pin State Tracking
|
|
3
|
+
//
|
|
4
|
+
// Tracks the driven level of pins configured as OUTPUT so that
|
|
5
|
+
// OutputPin.read()/isHigh()/isLow() can be answered without a hardware pin
|
|
6
|
+
// read. Reading back an OUTPUT-only pin is not portable (Zephyr's
|
|
7
|
+
// gpio_pin_get reads the input latch, which is undefined for direction-only
|
|
8
|
+
// outputs), so the transpiler instead maintains the state itself:
|
|
9
|
+
//
|
|
10
|
+
// - When the level is statically known at the read site (asOutput(false)
|
|
11
|
+
// followed by literal high/low/write/toggle calls), the read folds to a
|
|
12
|
+
// compile-time constant.
|
|
13
|
+
// - When it is not (runtime-valued writes, loops, branches, function
|
|
14
|
+
// bodies), the read lowers to a shadow variable (`__tc_pin_state_<n>`)
|
|
15
|
+
// that every generated write/toggle on that pin keeps updated.
|
|
16
|
+
//
|
|
17
|
+
// The tracker is updated in program order during IR lowering (the same pass
|
|
18
|
+
// that resolves HAL method bodies), so the state seen at each read site is
|
|
19
|
+
// the state at that point in the source. Control-flow joins and loop bodies
|
|
20
|
+
// conservatively invalidate levels (see control-flow.ts hooks); function and
|
|
21
|
+
// callback bodies disable folding entirely and force the shadow form.
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
import { walkProgramIR, walkExpressions } from "./utils/walk-ir.js";
|
|
24
|
+
/** Pin numbers explicitly configured as OUTPUT via gpio.set_mode. */
|
|
25
|
+
const outputPins = new Set();
|
|
26
|
+
/** Last driven level per output pin, at the current program point. */
|
|
27
|
+
const pinLevels = new Map();
|
|
28
|
+
/** Output pins whose reads lowered to the shadow variable form. */
|
|
29
|
+
const shadowPins = new Set();
|
|
30
|
+
/** Initializer for each shadow variable: the statically-known level at the
|
|
31
|
+
* time the first shadow read was resolved, or `false` when unknown. */
|
|
32
|
+
const shadowInitial = new Map();
|
|
33
|
+
/** Folding is disabled inside function/callback bodies, where the top-level
|
|
34
|
+
* program-point state does not apply at runtime. */
|
|
35
|
+
let foldingEnabled = true;
|
|
36
|
+
/** C++ identifier for a pin's shadow state variable. */
|
|
37
|
+
export function pinShadowVarName(pin) {
|
|
38
|
+
return `__tc_pin_state_${pin}`;
|
|
39
|
+
}
|
|
40
|
+
export function resetPinStateTracking() {
|
|
41
|
+
outputPins.clear();
|
|
42
|
+
pinLevels.clear();
|
|
43
|
+
shadowPins.clear();
|
|
44
|
+
shadowInitial.clear();
|
|
45
|
+
foldingEnabled = true;
|
|
46
|
+
}
|
|
47
|
+
/** Record gpio.set_mode — OUTPUT* modes enable tracking, anything else ends it. */
|
|
48
|
+
export function notePinSetMode(pin, mode) {
|
|
49
|
+
const isOutput = mode !== null && /output/i.test(mode);
|
|
50
|
+
if (isOutput) {
|
|
51
|
+
outputPins.add(pin);
|
|
52
|
+
pinLevels.set(pin, 'unknown');
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
outputPins.delete(pin);
|
|
56
|
+
pinLevels.delete(pin);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Record gpio.write — literal values set the level; expressions make it unknown. */
|
|
60
|
+
export function notePinWrite(pin, literalValue) {
|
|
61
|
+
if (!outputPins.has(pin))
|
|
62
|
+
return;
|
|
63
|
+
pinLevels.set(pin, literalValue === null ? 'unknown' : (literalValue ? 'high' : 'low'));
|
|
64
|
+
}
|
|
65
|
+
/** Record gpio.toggle — inverts a known level, keeps unknown as unknown. */
|
|
66
|
+
export function notePinToggle(pin) {
|
|
67
|
+
if (!outputPins.has(pin))
|
|
68
|
+
return;
|
|
69
|
+
const level = pinLevels.get(pin);
|
|
70
|
+
if (level === 'high')
|
|
71
|
+
pinLevels.set(pin, 'low');
|
|
72
|
+
else if (level === 'low')
|
|
73
|
+
pinLevels.set(pin, 'high');
|
|
74
|
+
else
|
|
75
|
+
pinLevels.set(pin, 'unknown');
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Record PWM/tone output — the pin no longer has a digital level, so any
|
|
79
|
+
* tracked level goes unknown (reads must not fold to the pre-PWM literal).
|
|
80
|
+
* A later digital write/toggle restores digital tracking.
|
|
81
|
+
*/
|
|
82
|
+
export function notePinAnalogOutput(pin) {
|
|
83
|
+
if (!outputPins.has(pin))
|
|
84
|
+
return;
|
|
85
|
+
pinLevels.set(pin, 'unknown');
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Resolve a read on a pin at the current program point.
|
|
89
|
+
* Returns 'high'/'low' when the level folds to a constant, 'shadow' when a
|
|
90
|
+
* shadow variable must carry it, or null when the pin is not a tracked
|
|
91
|
+
* output pin (unconfigured or input mode → normal hardware read lowering).
|
|
92
|
+
*/
|
|
93
|
+
export function resolveTrackedRead(pin) {
|
|
94
|
+
if (!outputPins.has(pin))
|
|
95
|
+
return null;
|
|
96
|
+
const level = pinLevels.get(pin);
|
|
97
|
+
if (foldingEnabled && (level === 'high' || level === 'low'))
|
|
98
|
+
return level;
|
|
99
|
+
shadowPins.add(pin);
|
|
100
|
+
if (!shadowInitial.has(pin))
|
|
101
|
+
shadowInitial.set(pin, level === 'high');
|
|
102
|
+
return 'shadow';
|
|
103
|
+
}
|
|
104
|
+
/** Whether write/toggle ops on this pin must also update its shadow variable. */
|
|
105
|
+
export function isShadowPin(pin) {
|
|
106
|
+
return shadowPins.has(pin);
|
|
107
|
+
}
|
|
108
|
+
/** Consume the shadow declarations owed by this build (pin + initializer).
|
|
109
|
+
* Clears the shadow set: emit reads the updatesShadow flags baked into the
|
|
110
|
+
* ops below, never this module's live state (each file's build resets the
|
|
111
|
+
* tracker, and all files finish building before any emit runs). */
|
|
112
|
+
export function takeShadowDeclarations() {
|
|
113
|
+
const decls = [...shadowPins].sort((a, b) => a - b).map(pin => ({
|
|
114
|
+
pin,
|
|
115
|
+
initial: shadowInitial.get(pin) ?? false,
|
|
116
|
+
}));
|
|
117
|
+
shadowPins.clear();
|
|
118
|
+
shadowInitial.clear();
|
|
119
|
+
return decls;
|
|
120
|
+
}
|
|
121
|
+
/** Mark every gpio.write / gpio.toggle op in the program whose pin has a
|
|
122
|
+
* tracked shadow read, so the emitter appends the shadow-variable update.
|
|
123
|
+
* Runs once at the end of buildProgramIR, after the whole file is lowered —
|
|
124
|
+
* at that point the shadow set is final, so writes that were lowered BEFORE
|
|
125
|
+
* the first shadow read are marked too (their ops are already in the IR).
|
|
126
|
+
* This is what makes emit independent of tracker state across files. */
|
|
127
|
+
export function markShadowUpdatingOps(program) {
|
|
128
|
+
if (shadowPins.size === 0)
|
|
129
|
+
return;
|
|
130
|
+
const markOp = (op) => {
|
|
131
|
+
const halOp = op;
|
|
132
|
+
if (!halOp)
|
|
133
|
+
return;
|
|
134
|
+
if ((halOp.operation === "gpio.write" || halOp.operation === "gpio.toggle") && isShadowPin(halOp.pin)) {
|
|
135
|
+
halOp.updatesShadow = true;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
walkProgramIR(program, (stmt) => {
|
|
139
|
+
if (stmt.kind === "hal-op")
|
|
140
|
+
markOp(stmt.operation);
|
|
141
|
+
walkExpressions([stmt], (expr) => {
|
|
142
|
+
if (expr.kind === "hal-expr")
|
|
143
|
+
markOp(expr.operation);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
export function snapshotPinLevels() {
|
|
148
|
+
return new Map(pinLevels);
|
|
149
|
+
}
|
|
150
|
+
export function restorePinLevels(snapshot) {
|
|
151
|
+
pinLevels.clear();
|
|
152
|
+
for (const [pin, level] of snapshot)
|
|
153
|
+
pinLevels.set(pin, level);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Merge two branch-end states into the live state: pins that agree keep
|
|
157
|
+
* their level; pins that differ (or exist in only one branch) become
|
|
158
|
+
* unknown. Pass the pre-branch state as one side when there is no else
|
|
159
|
+
* branch, so pins written in the taken branch invalidate.
|
|
160
|
+
*/
|
|
161
|
+
export function mergePinLevels(a, b) {
|
|
162
|
+
const pins = new Set([...a.keys(), ...b.keys()]);
|
|
163
|
+
for (const pin of pins) {
|
|
164
|
+
const la = a.get(pin) ?? 'unknown';
|
|
165
|
+
const lb = b.get(pin) ?? 'unknown';
|
|
166
|
+
pinLevels.set(pin, la === lb ? la : 'unknown');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/** Invalidate all known levels (after loop/switch bodies, which may run any
|
|
170
|
+
* number of times). Shadow tracking is unaffected — it is runtime truth. */
|
|
171
|
+
export function invalidatePinLevels() {
|
|
172
|
+
for (const pin of outputPins)
|
|
173
|
+
pinLevels.set(pin, 'unknown');
|
|
174
|
+
}
|
|
175
|
+
/** Disable/enable constant folding around function and callback bodies. */
|
|
176
|
+
export function setPinFoldingEnabled(enabled) {
|
|
177
|
+
foldingEnabled = enabled;
|
|
178
|
+
}
|
|
179
|
+
/** Whether pin-level constant folding is currently allowed. */
|
|
180
|
+
export function isPinFoldingEnabled() {
|
|
181
|
+
return foldingEnabled;
|
|
182
|
+
}
|
|
@@ -19,6 +19,12 @@ export interface ProgramAnalysisResult {
|
|
|
19
19
|
usesStringConversion: boolean;
|
|
20
20
|
usesDateNow: boolean;
|
|
21
21
|
usesMillis: boolean;
|
|
22
|
+
/** True when the program references millis()/micros() directly — WITHOUT the
|
|
23
|
+
* delay() conflation usesMillis carries (framework-avr needs delay to keep
|
|
24
|
+
* the native timing ISR alive, but frameworks whose delay() lowers straight
|
|
25
|
+
* to a native sleep — e.g. Zephyr's k_msleep — must not treat a delay-only
|
|
26
|
+
* program as a millis() consumer). */
|
|
27
|
+
usesWallClock: boolean;
|
|
22
28
|
usesNullish: boolean;
|
|
23
29
|
/** True when this file actually emits a cuttlefish_nullish/exists/is_nullish CALL
|
|
24
30
|
* (e.g. from a `??` lowering), as opposed to just referencing the
|
|
@@ -52,6 +58,10 @@ export interface ProgramAnalysisResult {
|
|
|
52
58
|
* __tc_TimerRuntime::MAX_TIMERS to the observed count (floor 1) rather than
|
|
53
59
|
* a blind constant, so a one-timer program links one slot, not eight. */
|
|
54
60
|
timerCallCount: number;
|
|
61
|
+
/** True when any function is declared `async` — the async runtime polls
|
|
62
|
+
* millis() every pump even when no timing call appears in user source
|
|
63
|
+
* (Async.sleep lowers to a raw hal-op the text scanners can't see). */
|
|
64
|
+
hasAsync: boolean;
|
|
55
65
|
/** ESP32 peripheral usage — framework-esp32 gates its IDF driver blocks and
|
|
56
66
|
* forced includes on these. Detected from HAL-op operation names, the same
|
|
57
67
|
* way usesUart/usesSPI/usesI2C are. Other frameworks have no CUTTLEFISH_*
|
|
@@ -103,8 +113,6 @@ export interface ProgramAnalysisResult {
|
|
|
103
113
|
/** Worker offload usage. Detected from worker.* ops. Frameworks gate the
|
|
104
114
|
* worker_runtime polyfill (and its per-framework backing) on this. */
|
|
105
115
|
usesWorker: boolean;
|
|
106
|
-
/** Native/desktop: std::chrono / steady_clock usage (gates <chrono>). */
|
|
107
|
-
usesChrono: boolean;
|
|
108
116
|
/** Native/desktop: std::set usage (gates <set>). */
|
|
109
117
|
usesSet: boolean;
|
|
110
118
|
/** Native/desktop: std::algorithm usage (std::sort/find/transform etc., gates <algorithm>). */
|
|
@@ -61,6 +61,18 @@ function analyzeExpression(expr, result, strategy) {
|
|
|
61
61
|
if (/\bmillis\s*\(/.test(expr.value)) {
|
|
62
62
|
result.usesMillis = true;
|
|
63
63
|
}
|
|
64
|
+
if (/\bmillis\s*\(/.test(expr.value) || /\bmicros\s*\(/.test(expr.value)) {
|
|
65
|
+
result.usesWallClock = true;
|
|
66
|
+
}
|
|
67
|
+
// Test-runner console helpers (@typecad/expect's preprocessor injects
|
|
68
|
+
// __tc_print/__tc_println calls into the source). Track them as polyfill
|
|
69
|
+
// helpers so frameworks can gate their definitions (and <cstdio>) on use.
|
|
70
|
+
if (expr.value.includes("__tc_println(")) {
|
|
71
|
+
result.usedPolyfillHelpers.add("__tc_println");
|
|
72
|
+
}
|
|
73
|
+
if (expr.value.includes("__tc_print(")) {
|
|
74
|
+
result.usedPolyfillHelpers.add("__tc_print");
|
|
75
|
+
}
|
|
64
76
|
if (expr.value.includes('cuttlefish_nullish(') || expr.value.includes('cuttlefish_exists(') || expr.value.includes('cuttlefish_is_nullish(')) {
|
|
65
77
|
result.usesNullish = true;
|
|
66
78
|
result.usesNullishHelper = true;
|
|
@@ -77,9 +89,6 @@ function analyzeExpression(expr, result, strategy) {
|
|
|
77
89
|
if (expr.value.includes('__tc_str_ptr')) {
|
|
78
90
|
result.usesStrPtr = true;
|
|
79
91
|
}
|
|
80
|
-
if (/std::chrono|steady_clock/.test(expr.value)) {
|
|
81
|
-
result.usesChrono = true;
|
|
82
|
-
}
|
|
83
92
|
if (/std::set\s*</.test(expr.value)) {
|
|
84
93
|
result.usesSet = true;
|
|
85
94
|
}
|
|
@@ -103,6 +112,12 @@ function analyzeExpression(expr, result, strategy) {
|
|
|
103
112
|
if (/\bmillis\b/.test(expr.callee) || /\bdelay\b/.test(expr.callee) || /\bmicros\b/.test(expr.callee)) {
|
|
104
113
|
result.usesMillis = true;
|
|
105
114
|
}
|
|
115
|
+
if (/\bmillis\b/.test(expr.callee) || /\bmicros\b/.test(expr.callee)) {
|
|
116
|
+
result.usesWallClock = true;
|
|
117
|
+
}
|
|
118
|
+
if (expr.callee === "__tc_print" || expr.callee === "__tc_println") {
|
|
119
|
+
result.usedPolyfillHelpers.add(expr.callee);
|
|
120
|
+
}
|
|
106
121
|
if (expr.callee === "Date.now" || expr.callee === "Date::now") {
|
|
107
122
|
result.usesDateNow = true;
|
|
108
123
|
}
|
|
@@ -376,10 +391,17 @@ function analyzeStatement(statement, result, strategy) {
|
|
|
376
391
|
if (statement.callee === "millis" || statement.callee === "delay") {
|
|
377
392
|
result.usesMillis = true;
|
|
378
393
|
}
|
|
394
|
+
if (statement.callee === "millis") {
|
|
395
|
+
result.usesWallClock = true;
|
|
396
|
+
}
|
|
379
397
|
// Awaited HAL wait markers become async state-machine poll states that
|
|
380
398
|
// arm deadlines via currentTimeMillis().
|
|
381
399
|
if (statement.callee === "__WIFI_WAIT__" || statement.callee === "__HTTP_WAIT__" || statement.callee === "__HAL_WAIT__") {
|
|
382
400
|
result.usesMillis = true;
|
|
401
|
+
result.usesWallClock = true;
|
|
402
|
+
}
|
|
403
|
+
if (statement.callee === "__tc_print" || statement.callee === "__tc_println") {
|
|
404
|
+
result.usedPolyfillHelpers.add(statement.callee);
|
|
383
405
|
}
|
|
384
406
|
// Namespace-qualified polyfill entry points used as bare call statements
|
|
385
407
|
// (e.g. `Timing.delay(5);`). The expression-level analyzer (case
|
|
@@ -697,6 +719,9 @@ function analyzeStatement(statement, result, strategy) {
|
|
|
697
719
|
result.usesTiming = true;
|
|
698
720
|
result.usesMillis = true;
|
|
699
721
|
}
|
|
722
|
+
if (opName === "timing.millis" || opName === "timing.micros") {
|
|
723
|
+
result.usesWallClock = true;
|
|
724
|
+
}
|
|
700
725
|
}
|
|
701
726
|
// Scan raw C++ code in HAL ops for polyfill helper usage
|
|
702
727
|
if (statement.operation && statement.operation.operation === "raw" && typeof statement.operation.code === "string") {
|
|
@@ -725,6 +750,15 @@ function analyzeStatement(statement, result, strategy) {
|
|
|
725
750
|
result.usesTiming = true;
|
|
726
751
|
result.usesMillis = true;
|
|
727
752
|
}
|
|
753
|
+
if (/\b(millis|micros)\s*\(/.test(code)) {
|
|
754
|
+
result.usesWallClock = true;
|
|
755
|
+
}
|
|
756
|
+
if (code.includes("__tc_println(")) {
|
|
757
|
+
result.usedPolyfillHelpers.add("__tc_println");
|
|
758
|
+
}
|
|
759
|
+
if (code.includes("__tc_print(")) {
|
|
760
|
+
result.usedPolyfillHelpers.add("__tc_print");
|
|
761
|
+
}
|
|
728
762
|
// Timer polyfill sizing — `Timing.setInterval(...)` / `Timing.setTimeout(...)`
|
|
729
763
|
// are resolved by the HAL method resolver, which reads TimingClass's
|
|
730
764
|
// rawCpp body and emits a raw hal-op whose code is e.g.
|
|
@@ -798,6 +832,7 @@ export function analyzeProgram(program, strategy) {
|
|
|
798
832
|
usesStringConversion: false,
|
|
799
833
|
usesDateNow: false,
|
|
800
834
|
usesMillis: false,
|
|
835
|
+
usesWallClock: false,
|
|
801
836
|
usesNullish: false,
|
|
802
837
|
usesNullishHelper: false,
|
|
803
838
|
usesNum: false,
|
|
@@ -844,7 +879,7 @@ export function analyzeProgram(program, strategy) {
|
|
|
844
879
|
usesHwtimer: false,
|
|
845
880
|
usesCapacitive: false,
|
|
846
881
|
usesWorker: false,
|
|
847
|
-
|
|
882
|
+
hasAsync: false,
|
|
848
883
|
usesSet: false,
|
|
849
884
|
usesAlgorithm: false,
|
|
850
885
|
usesCstdio: false,
|
|
@@ -1010,6 +1045,7 @@ export function analyzeProgram(program, strategy) {
|
|
|
1010
1045
|
// consume site. Without this gate, every AVR program pulled in the Timer0
|
|
1011
1046
|
// ISR even when it never uses timing.
|
|
1012
1047
|
const hasAsync = program.functions.some(fn => fn.isAsync);
|
|
1048
|
+
result.hasAsync = hasAsync;
|
|
1013
1049
|
result.usesNativeTiming = result.usesMillis
|
|
1014
1050
|
|| result.usesTiming
|
|
1015
1051
|
|| result.timerCallCount > 0
|
|
@@ -3,6 +3,7 @@ import { isStringEnum } from "../api/shared/index.js";
|
|
|
3
3
|
import { extractNodeComments, makeDiagnostic, makeSourceSpan } from "./ast-node-utils.js";
|
|
4
4
|
import { isCompileTimeOnlyCallName, isCompileTimeOnlyClassName } from "./compile-time-only.js";
|
|
5
5
|
import { TYPED_ARRAY_ELEMENT_MAP, hoistedNestedEnums, hoistedNestedInterfaces, hoistedNestedTypeAliases, nestedFunctionAliases, nestedClassAliases, mutableArrayVars, arrayLiteralSizes, activeEnumNames, activeStringEnumNames, resetFunctionScopeState } from "./build-ir-state.js";
|
|
6
|
+
import { isPinFoldingEnabled, setPinFoldingEnabled, invalidatePinLevels } from "./pin-state-tracking.js";
|
|
6
7
|
import { getCurrentIrTypeScope, bindIrTypeScopeLocals } from "./symbol-types.js";
|
|
7
8
|
import { renderExprAsText } from "./render-expr.js";
|
|
8
9
|
import { expressionToIR } from "./expression-to-ir.js";
|
|
@@ -177,6 +178,15 @@ export function lowerStatementList(statements, fileName, sourceText, diagnostics
|
|
|
177
178
|
const lowered = [];
|
|
178
179
|
const nestedNames = [];
|
|
179
180
|
const nestedClassNames = [];
|
|
181
|
+
// Function/method bodies run at an unmodeled time relative to top-level
|
|
182
|
+
// flow, so pin-state constant folding must be off inside them (reads fall
|
|
183
|
+
// back to the shadow variable), and any writes they contain invalidate the
|
|
184
|
+
// top-level levels afterwards. "" and "<top-level>" are the top-level
|
|
185
|
+
// markers; everything else names a function/method/lambda body.
|
|
186
|
+
const isFunctionBodyCtx = functionNameForDiagnostics !== "" && functionNameForDiagnostics !== "<top-level>";
|
|
187
|
+
const prevPinFolding = isPinFoldingEnabled();
|
|
188
|
+
if (isFunctionBodyCtx)
|
|
189
|
+
setPinFoldingEnabled(false);
|
|
180
190
|
// Phase 1: Pre-scan for nested function declarations — register aliases only.
|
|
181
191
|
// This ensures sibling functions can reference each other.
|
|
182
192
|
for (const statement of statements) {
|
|
@@ -345,5 +355,9 @@ export function lowerStatementList(statements, fileName, sourceText, diagnostics
|
|
|
345
355
|
for (const name of nestedClassNames) {
|
|
346
356
|
nestedClassAliases.delete(name);
|
|
347
357
|
}
|
|
358
|
+
if (isFunctionBodyCtx) {
|
|
359
|
+
setPinFoldingEnabled(prevPinFolding);
|
|
360
|
+
invalidatePinLevels();
|
|
361
|
+
}
|
|
348
362
|
return lowered;
|
|
349
363
|
}
|
|
@@ -6,6 +6,7 @@ import { parseCppType, renderCppType, isPointer, parsedIsVector, parsedElementSt
|
|
|
6
6
|
import { expressionToIR } from "../expression-to-ir.js";
|
|
7
7
|
import { lowerStatementList, expressionStatementToIR } from "../statement-to-ir.js";
|
|
8
8
|
import { assignmentOperatorToString, updateLocalTypeFromAssignment, extractForInKeys } from "./variables.js";
|
|
9
|
+
import { snapshotPinLevels, restorePinLevels, mergePinLevels, invalidatePinLevels } from "../pin-state-tracking.js";
|
|
9
10
|
// Monotonic counter for synthetic for...of destructure loop variables.
|
|
10
11
|
let forOfDestructureCounter = 0;
|
|
11
12
|
/**
|
|
@@ -184,6 +185,9 @@ export function lowerControlFlowStatement(statement, fileName, sourceText, diagn
|
|
|
184
185
|
}];
|
|
185
186
|
}
|
|
186
187
|
if (ts.isWhileStatement(statement)) {
|
|
188
|
+
// Loop conditions/bodies may evaluate any number of times, so tracked
|
|
189
|
+
// pin levels from before the loop are not valid inside it.
|
|
190
|
+
invalidatePinLevels();
|
|
187
191
|
const comments = extractNodeComments(statement, sourceText);
|
|
188
192
|
const bodyStatements = lowerStatementList(ts.isBlock(statement.statement) ? statement.statement.statements : [statement.statement], fileName, sourceText, diagnostics, functionReturnTypes, localVariableTypes, functionNameForDiagnostics, typeAliases, pointerVars);
|
|
189
193
|
return [{
|
|
@@ -196,12 +200,24 @@ export function lowerControlFlowStatement(statement, fileName, sourceText, diagn
|
|
|
196
200
|
}];
|
|
197
201
|
}
|
|
198
202
|
if (ts.isIfStatement(statement)) {
|
|
203
|
+
// Branch merge: the level after the if must be the join of both branch
|
|
204
|
+
// ends (pins that disagree become unknown). The condition is lowered
|
|
205
|
+
// after the branches in this function but runs before them at runtime,
|
|
206
|
+
// so the pre-branch state is restored before the condition is evaluated
|
|
207
|
+
// and the merge is applied last.
|
|
208
|
+
const preBranchPinState = snapshotPinLevels();
|
|
199
209
|
const comments = extractNodeComments(statement, sourceText);
|
|
200
210
|
const thenStatements = lowerStatementList(ts.isBlock(statement.thenStatement) ? statement.thenStatement.statements : [statement.thenStatement], fileName, sourceText, diagnostics, functionReturnTypes, localVariableTypes, functionNameForDiagnostics, typeAliases, pointerVars);
|
|
211
|
+
const thenBranchPinState = snapshotPinLevels();
|
|
212
|
+
// Restore the pre-branch state before lowering the else branch and the
|
|
213
|
+
// condition — both run before the branches at runtime... the condition
|
|
214
|
+
// before them, the else on the path where the then branch never ran.
|
|
215
|
+
restorePinLevels(preBranchPinState);
|
|
201
216
|
let elseBranch;
|
|
202
217
|
if (statement.elseStatement) {
|
|
203
218
|
elseBranch = lowerStatementList(ts.isBlock(statement.elseStatement) ? statement.elseStatement.statements : [statement.elseStatement], fileName, sourceText, diagnostics, functionReturnTypes, localVariableTypes, functionNameForDiagnostics, typeAliases, pointerVars);
|
|
204
219
|
}
|
|
220
|
+
const elseBranchPinState = snapshotPinLevels();
|
|
205
221
|
// Evaluate the condition. If it's a bare identifier that resolves to a
|
|
206
222
|
// HAL alias (e.g. `if (bus2)` where bus2 = I2C0.take()), the alias is a
|
|
207
223
|
// compile-time non-null reference — constant-fold to `true` so the emit
|
|
@@ -213,6 +229,10 @@ export function lowerControlFlowStatement(statement, fileName, sourceText, diagn
|
|
|
213
229
|
else {
|
|
214
230
|
condition = expressionToIR(statement.expression, sourceText, diagnostics, pointerVars);
|
|
215
231
|
}
|
|
232
|
+
// Join the branch-end states into the live state. With no else branch,
|
|
233
|
+
// elseBranchPinState equals the pre-branch state, so pins written by the
|
|
234
|
+
// then branch invalidate unless the write agrees with the prior level.
|
|
235
|
+
mergePinLevels(thenBranchPinState, elseBranchPinState);
|
|
216
236
|
return [{
|
|
217
237
|
kind: "if",
|
|
218
238
|
sourceSpan: makeSourceSpan(statement, fileName, sourceText),
|
|
@@ -235,6 +255,9 @@ export function lowerControlFlowStatement(statement, fileName, sourceText, diagn
|
|
|
235
255
|
initializer = loweredExpr;
|
|
236
256
|
}
|
|
237
257
|
}
|
|
258
|
+
// Loop conditions/bodies may evaluate any number of times, so tracked
|
|
259
|
+
// pin levels from before the loop are not valid inside it.
|
|
260
|
+
invalidatePinLevels();
|
|
238
261
|
const condition = statement.condition
|
|
239
262
|
? expressionToIR(statement.condition, sourceText, diagnostics, pointerVars)
|
|
240
263
|
: undefined;
|
|
@@ -362,6 +385,9 @@ export function lowerControlFlowStatement(statement, fileName, sourceText, diagn
|
|
|
362
385
|
}
|
|
363
386
|
// Handle do...while loops
|
|
364
387
|
if (ts.isDoStatement(statement)) {
|
|
388
|
+
// Loop bodies may run more than once; pre-loop pin levels are not valid
|
|
389
|
+
// inside them.
|
|
390
|
+
invalidatePinLevels();
|
|
365
391
|
const comments = extractNodeComments(statement, sourceText);
|
|
366
392
|
const bodyStatements = lowerStatementList(ts.isBlock(statement.statement) ? statement.statement.statements : [statement.statement], fileName, sourceText, diagnostics, functionReturnTypes, localVariableTypes, functionNameForDiagnostics, typeAliases, pointerVars);
|
|
367
393
|
return [{
|
|
@@ -375,6 +401,9 @@ export function lowerControlFlowStatement(statement, fileName, sourceText, diagn
|
|
|
375
401
|
}
|
|
376
402
|
// Handle switch statements
|
|
377
403
|
if (ts.isSwitchStatement(statement)) {
|
|
404
|
+
// Exactly one case runs at runtime, but all are lowered here; pin levels
|
|
405
|
+
// from case bodies must not leak past the switch.
|
|
406
|
+
invalidatePinLevels();
|
|
378
407
|
const comments = extractNodeComments(statement, sourceText);
|
|
379
408
|
const cases = [];
|
|
380
409
|
for (const clause of statement.caseBlock.clauses) {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// and is invoked from call-statement.ts after tryResolveHALMethod.
|
|
10
10
|
// ---------------------------------------------------------------------------
|
|
11
11
|
import ts from "typescript";
|
|
12
|
-
import { makeSourceSpan } from "../ast-node-utils.js";
|
|
12
|
+
import { makeDiagnostic, makeSourceSpan } from "../ast-node-utils.js";
|
|
13
13
|
import { halOpsToIR } from "./hal-emit-helpers.js";
|
|
14
14
|
import { resolveMount } from "./ui-mount.js";
|
|
15
15
|
import { emitSignalDecl, recordListBinding, getListBindingsCount, recordInputBinding, getInputBindingsCount, resetInputBindings } from "./ui-reactive.js";
|
|
@@ -184,6 +184,79 @@ function matchUIWindowCall(call) {
|
|
|
184
184
|
return undefined;
|
|
185
185
|
return call.expression.name.text;
|
|
186
186
|
}
|
|
187
|
+
/** Detect `ui.drawer.<method>(...)` — same shape as ui.window. Returns the
|
|
188
|
+
* method name ("open" / "close") or undefined. */
|
|
189
|
+
function matchUIDrawerCall(call) {
|
|
190
|
+
if (!ts.isPropertyAccessExpression(call.expression))
|
|
191
|
+
return undefined;
|
|
192
|
+
const inner = call.expression.expression;
|
|
193
|
+
if (!ts.isPropertyAccessExpression(inner))
|
|
194
|
+
return undefined;
|
|
195
|
+
if (!ts.isIdentifier(inner.expression) || inner.expression.text !== "ui")
|
|
196
|
+
return undefined;
|
|
197
|
+
if (!ts.isIdentifier(inner.name) || inner.name.text !== "drawer")
|
|
198
|
+
return undefined;
|
|
199
|
+
return call.expression.name.text;
|
|
200
|
+
}
|
|
201
|
+
/** Detect ui.dialog.<method>(...) — same shape, resolves <dialog> ids. */
|
|
202
|
+
function matchUIDialogCall(call) {
|
|
203
|
+
if (!ts.isPropertyAccessExpression(call.expression))
|
|
204
|
+
return undefined;
|
|
205
|
+
const inner = call.expression.expression;
|
|
206
|
+
if (!ts.isPropertyAccessExpression(inner))
|
|
207
|
+
return undefined;
|
|
208
|
+
if (!ts.isIdentifier(inner.expression) || inner.expression.text !== "ui")
|
|
209
|
+
return undefined;
|
|
210
|
+
if (!ts.isIdentifier(inner.name) || inner.name.text !== "dialog")
|
|
211
|
+
return undefined;
|
|
212
|
+
return call.expression.name.text;
|
|
213
|
+
}
|
|
214
|
+
/** Detect ui.toast('<id>') — show a <toast>; auto-closes via duration. */
|
|
215
|
+
function matchUIToastCall(call) {
|
|
216
|
+
if (!ts.isPropertyAccessExpression(call.expression))
|
|
217
|
+
return false;
|
|
218
|
+
const inner = call.expression.expression;
|
|
219
|
+
if (!ts.isIdentifier(inner) || inner.text !== "ui")
|
|
220
|
+
return false;
|
|
221
|
+
return ts.isIdentifier(call.expression.name) && call.expression.name.text === "toast";
|
|
222
|
+
}
|
|
223
|
+
/** Resolve ui.drawer.open('<id>') / ui.drawer.close('<id>' | ), and the
|
|
224
|
+
* dialog/toast aliases. The id resolves at BUILD time to the node index of
|
|
225
|
+
* the <drawer>/<dialog>/<toast> element, emitting ui_drawer_open(N) /
|
|
226
|
+
* ui_drawer_close(N) (or close_all with no argument) — the preview
|
|
227
|
+
* evaluates the same call dynamically. */
|
|
228
|
+
function resolveDrawerCall(method, call, sourceText, diagnostics, kind = "drawer") {
|
|
229
|
+
const sourceSpan = makeSourceSpan(call, call.getSourceFile()?.fileName ?? "", sourceText);
|
|
230
|
+
if (method !== "open" && method !== "close") {
|
|
231
|
+
diagnostics.push(makeDiagnostic(sourceText, call.pos, `ui.${kind}.${method} is not supported — use open(id) or close(id?).`, "error", "UI_DRAWER_METHOD"));
|
|
232
|
+
return { kind: "block", sourceSpan, body: [] };
|
|
233
|
+
}
|
|
234
|
+
const arg = call.arguments[0];
|
|
235
|
+
const emit = (stmt) => ({
|
|
236
|
+
kind: "call",
|
|
237
|
+
callee: "__EMIT__",
|
|
238
|
+
args: [{ kind: "string", value: stmt }],
|
|
239
|
+
sourceSpan,
|
|
240
|
+
});
|
|
241
|
+
// close() with no id closes every open drawer.
|
|
242
|
+
if (method === "close" && arg === undefined)
|
|
243
|
+
return emit("ui_drawer_close_all();");
|
|
244
|
+
let idText;
|
|
245
|
+
if (arg && ts.isStringLiteral(arg))
|
|
246
|
+
idText = arg.text;
|
|
247
|
+
if (idText === undefined) {
|
|
248
|
+
diagnostics.push(makeDiagnostic(sourceText, call.pos, `ui.${kind}.${method} needs an id string literal (e.g. ui.${kind}.open('settings')).`, "error", "UI_DRAWER_ID"));
|
|
249
|
+
return { kind: "block", sourceSpan, body: [] };
|
|
250
|
+
}
|
|
251
|
+
const nodeIdx = resolveElementValue("screen", idText);
|
|
252
|
+
if (nodeIdx === undefined) {
|
|
253
|
+
diagnostics.push(makeDiagnostic(sourceText, call.pos, `ui.${kind}.${method}('${idText}'): no <${kind} id="${idText}"> in the mounted UI tree.`, "error", "UI_DRAWER_UNKNOWN_ID"));
|
|
254
|
+
return { kind: "block", sourceSpan, body: [] };
|
|
255
|
+
}
|
|
256
|
+
return emit(method === "open"
|
|
257
|
+
? `ui_drawer_open(${nodeIdx});`
|
|
258
|
+
: `ui_drawer_close(${nodeIdx});`);
|
|
259
|
+
}
|
|
187
260
|
/** Resolve ui.window.setTitle / ui.window.setIcon. Native SDL only; on hardware
|
|
188
261
|
* these are silent no-ops (a desktop-only convenience, not an error). */
|
|
189
262
|
function resolveWindowCall(method, call, sourceText, diagnostics) {
|
|
@@ -308,6 +381,37 @@ export function tryResolveUICall(call, fileName, sourceText, diagnostics, option
|
|
|
308
381
|
const windowCall = matchUIWindowCall(call);
|
|
309
382
|
if (windowCall)
|
|
310
383
|
return resolveWindowCall(windowCall, call, sourceText, diagnostics);
|
|
384
|
+
// ui.drawer.open('id') / ui.drawer.close('id'|) — also a two-level chain.
|
|
385
|
+
const drawerCall = matchUIDrawerCall(call);
|
|
386
|
+
if (drawerCall)
|
|
387
|
+
return resolveDrawerCall(drawerCall, call, sourceText, diagnostics);
|
|
388
|
+
const dialogCall = matchUIDialogCall(call);
|
|
389
|
+
if (dialogCall)
|
|
390
|
+
return resolveDrawerCall(dialogCall, call, sourceText, diagnostics, "dialog");
|
|
391
|
+
if (matchUIToastCall(call)) {
|
|
392
|
+
// ui.toast('<id>') — show a <toast>; the duration attribute drives the
|
|
393
|
+
// auto-close. Same open lowering as a drawer.
|
|
394
|
+
const sourceSpan = makeSourceSpan(call, call.getSourceFile()?.fileName ?? "", sourceText);
|
|
395
|
+
const arg = call.arguments[0];
|
|
396
|
+
let idText;
|
|
397
|
+
if (arg && ts.isStringLiteral(arg))
|
|
398
|
+
idText = arg.text;
|
|
399
|
+
if (idText === undefined) {
|
|
400
|
+
diagnostics.push(makeDiagnostic(sourceText, call.pos, "ui.toast needs a toast id string literal (e.g. ui.toast('saved')).", "error", "UI_TOAST_ID"));
|
|
401
|
+
return { kind: "block", sourceSpan, body: [] };
|
|
402
|
+
}
|
|
403
|
+
const nodeIdx = resolveElementValue("screen", idText);
|
|
404
|
+
if (nodeIdx === undefined) {
|
|
405
|
+
diagnostics.push(makeDiagnostic(sourceText, call.pos, `ui.toast('${idText}'): no <toast id="${idText}"> in the mounted UI tree.`, "error", "UI_TOAST_UNKNOWN_ID"));
|
|
406
|
+
return { kind: "block", sourceSpan, body: [] };
|
|
407
|
+
}
|
|
408
|
+
return {
|
|
409
|
+
kind: "call",
|
|
410
|
+
sourceSpan,
|
|
411
|
+
callee: "__EMIT__",
|
|
412
|
+
args: [{ kind: "string", value: `ui_drawer_open(${nodeIdx});` }],
|
|
413
|
+
};
|
|
414
|
+
}
|
|
311
415
|
if (!isUICall(call))
|
|
312
416
|
return null;
|
|
313
417
|
const method = call.expression.name.text;
|
|
@@ -99,12 +99,13 @@ function autoWireNode(treeName, node, nodeIndex) {
|
|
|
99
99
|
fnName: `__ui_bindval_${nodeIndex}`,
|
|
100
100
|
cppExpr: sig,
|
|
101
101
|
});
|
|
102
|
-
// Write: range drag / check toggle →
|
|
102
|
+
// Write: range drag / check toggle → plain assignment (signals lower
|
|
103
|
+
// to plain device variables; .set() is author-facing syntax only).
|
|
103
104
|
recordClickHandler({
|
|
104
105
|
nodeIndex,
|
|
105
106
|
kind: node.tag === "check" ? "click" : "rangechange",
|
|
106
107
|
fnName: `__ui_bindval_cb_${nodeIndex}`,
|
|
107
|
-
callbackBody: `${sig}
|
|
108
|
+
callbackBody: `${sig} = __ui_nodes[${nodeIndex}].value;`,
|
|
108
109
|
});
|
|
109
110
|
}
|
|
110
111
|
}
|
|
@@ -149,12 +150,14 @@ function autoWireNode(treeName, node, nodeIndex) {
|
|
|
149
150
|
? node.options.map(o => o.text)
|
|
150
151
|
: (node.text || "").split(",").map(s => s.trim()).filter(Boolean);
|
|
151
152
|
const count = Math.max(options.length, 2);
|
|
152
|
-
// Auto-wire: onClick
|
|
153
|
+
// Auto-wire: onClick opens the modal option list (ui_select_menu_open);
|
|
154
|
+
// tapping a row in the modal sets the value. Preview parity: the preview
|
|
155
|
+
// runtime opens its own overlay on tap instead of cycling.
|
|
153
156
|
recordClickHandler({
|
|
154
157
|
nodeIndex,
|
|
155
158
|
kind: "click",
|
|
156
159
|
fnName: `__ui_${node.id}_autoclick`,
|
|
157
|
-
callbackBody: `
|
|
160
|
+
callbackBody: `ui_select_menu_open(${nodeIndex});`,
|
|
158
161
|
});
|
|
159
162
|
// Auto-bind text to show the current option via snprintf if/else chain.
|
|
160
163
|
// Cast size to size_t to satisfy -Wformat (snprintf's n param is size_t;
|