@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.
Files changed (65) hide show
  1. package/dist/add-preset.d.ts +4 -0
  2. package/dist/add-preset.js +74 -0
  3. package/dist/api/config.d.ts +5 -5
  4. package/dist/api/shared/display-adapters/sdl.js +1 -1
  5. package/dist/api/shared/display-profile.d.ts +11 -0
  6. package/dist/api/shared/display-profile.js +3 -0
  7. package/dist/api/shared/framework-manifest.d.ts +85 -78
  8. package/dist/api/shared/framework-manifest.js +1 -0
  9. package/dist/api/shared/hal-op-ir.d.ts +19 -0
  10. package/dist/api/shared/toolchain-types.d.ts +0 -1
  11. package/dist/cli.js +21 -4
  12. package/dist/config-loader.d.ts +8 -2
  13. package/dist/config-loader.js +202 -53
  14. package/dist/config-schema.d.ts +7 -7
  15. package/dist/config-schema.js +3 -3
  16. package/dist/create/board-spec.d.ts +122 -122
  17. package/dist/create/debug-artifacts.d.ts +20 -0
  18. package/dist/create/debug-artifacts.js +69 -0
  19. package/dist/create/eslint-rules-template.js +6 -3
  20. package/dist/create/index.d.ts +2 -0
  21. package/dist/create/index.js +1 -0
  22. package/dist/create/init-scaffold.d.ts +1 -0
  23. package/dist/create/init-scaffold.js +5 -0
  24. package/dist/create/init-templates.js +0 -2
  25. package/dist/emit/compliance/rules.js +52 -4
  26. package/dist/emit/emitters/function-emitter-impl.js +7 -1
  27. package/dist/emit/emitters/line-appender.js +6 -0
  28. package/dist/emit/emitters/setup.js +15 -2
  29. package/dist/emit/emitters/ui-emitter.js +40 -15
  30. package/dist/emit/route-hal-op.js +55 -1
  31. package/dist/emit/statement-renderer.js +5 -2
  32. package/dist/ir/build-ir.js +22 -1
  33. package/dist/ir/expression-to-ir.js +17 -0
  34. package/dist/ir/feature-registry.js +22 -6
  35. package/dist/ir/hal/hal-emitter.js +23 -5
  36. package/dist/ir/hal/hal-plugins.js +11 -0
  37. package/dist/ir/pin-mode-validation.js +32 -9
  38. package/dist/ir/pin-state-tracking.d.ts +58 -0
  39. package/dist/ir/pin-state-tracking.js +182 -0
  40. package/dist/ir/program-analysis.d.ts +10 -2
  41. package/dist/ir/program-analysis.js +40 -4
  42. package/dist/ir/statement-to-ir.js +14 -0
  43. package/dist/ir/transformers/control-flow.js +29 -0
  44. package/dist/ir/transformers/ui-call-resolver.js +105 -1
  45. package/dist/ir/ui-element-auto-wire.js +7 -4
  46. package/dist/orchestrator/graph-builder.d.ts +4 -1
  47. package/dist/orchestrator/graph-builder.js +7 -1
  48. package/dist/platform/async-runtime.d.ts +1 -1
  49. package/dist/platform/async-runtime.js +12 -3
  50. package/dist/platform/generic-strategy.js +1 -1
  51. package/dist/preview/api-shared-shim.d.ts +1 -0
  52. package/dist/preview/api-shared-shim.js +7 -0
  53. package/dist/preview/client.js +220 -1
  54. package/dist/preview/server.js +154 -62
  55. package/dist/theme-tokens.d.ts +22 -0
  56. package/dist/theme-tokens.js +172 -0
  57. package/dist/transpile.js +90 -12
  58. package/dist/types.d.ts +5 -0
  59. package/dist/ui-hook.d.ts +7 -0
  60. package/dist/utils/cli.js +9 -0
  61. package/dist/utils/fs.d.ts +2 -0
  62. package/dist/utils/fs.js +16 -0
  63. package/dist/utils/ui.d.ts +5 -0
  64. package/dist/utils/ui.js +7 -0
  65. 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
- usesChrono: false,
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 → signal.set(value).
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}.set(__ui_nodes[${nodeIndex}].value);`,
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 cycles value 0..count-1
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: `__ui_nodes[${nodeIndex}].value = (__ui_nodes[${nodeIndex}].value + 1) % ${count};`,
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;