@typecad/cuttlefish 1.0.0-alpha.6 → 1.0.0-alpha.7
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/api/shared/display-adapter.d.ts +2 -1
- package/dist/api/shared/display-adapter.js +8 -1
- package/dist/api/shared/display-adapters/sdl.js +9 -2
- package/dist/api/shared/display-profile.d.ts +20 -3
- package/dist/api/shared/display-profile.js +21 -6
- package/dist/api/shared/framework-manifest-registry.d.ts +9 -0
- package/dist/api/shared/framework-manifest-registry.js +25 -0
- package/dist/api/shared/framework-manifest.d.ts +462 -0
- package/dist/api/shared/framework-manifest.js +149 -0
- package/dist/api/shared/glcdfont.d.ts +12 -0
- package/dist/api/shared/glcdfont.js +124 -0
- package/dist/api/shared/graphics-strategy.d.ts +28 -0
- package/dist/api/shared/hal-op-ir.d.ts +418 -1
- package/dist/api/shared/hal-op-ir.js +95 -1
- package/dist/api/shared/index.d.ts +11 -1
- package/dist/api/shared/index.js +14 -0
- package/dist/api/shared/native-display-op-resolver.d.ts +10 -0
- package/dist/api/shared/native-display-op-resolver.js +64 -0
- package/dist/api/shared/platform-strategy.d.ts +9 -0
- package/dist/api/shared/promise-runtime.js +78 -0
- package/dist/api/shared/validate-framework-manifest.d.ts +28 -0
- package/dist/api/shared/validate-framework-manifest.js +417 -0
- package/dist/cli.js +109 -4
- package/dist/config-loader.js +20 -1
- package/dist/config-schema.d.ts +36 -36
- package/dist/create/board-spec.d.ts +4 -4
- package/dist/create/init-scaffold.d.ts +3 -0
- package/dist/create/init-scaffold.js +48 -0
- package/dist/create/init-templates.d.ts +2 -0
- package/dist/create/init-templates.js +28 -8
- package/dist/create/init-wizard.js +20 -1
- package/dist/emit/cpp-emitter.js +4 -3
- package/dist/emit/emitters/emitter-context.d.ts +5 -0
- package/dist/emit/emitters/function-emitter-impl.js +69 -59
- package/dist/emit/emitters/output-finalizer.d.ts +6 -0
- package/dist/emit/emitters/output-finalizer.js +21 -11
- package/dist/emit/emitters/setup.js +40 -0
- package/dist/emit/emitters/top-level-prep.js +2 -0
- package/dist/emit/emitters/ui-emitter.js +23 -8
- package/dist/emit/expression-renderer.js +10 -1
- package/dist/emit/snprintf-helpers.js +8 -0
- package/dist/emit/statement-renderer.js +13 -0
- package/dist/emit/utils/async-state-machine.js +185 -116
- package/dist/emit/utils/hal-op-cpp-type.d.ts +6 -0
- package/dist/emit/utils/hal-op-cpp-type.js +40 -0
- package/dist/ir/build-ir.js +5 -1
- package/dist/ir/expression-to-ir.js +14 -0
- package/dist/ir/feature-registry.js +7 -25
- package/dist/ir/hal/hal-emitter.d.ts +5 -2
- package/dist/ir/hal/hal-emitter.js +40 -12
- package/dist/ir/hal/hal-parser.d.ts +6 -0
- package/dist/ir/hal/hal-parser.js +74 -0
- package/dist/ir/hal/hal-plugins.js +561 -0
- package/dist/ir/identifier-collector.js +18 -0
- package/dist/ir/network-validation.d.ts +4 -0
- package/dist/ir/network-validation.js +184 -0
- package/dist/ir/ownership-analysis.js +14 -1
- package/dist/ir/program-analysis.d.ts +23 -0
- package/dist/ir/program-analysis.js +81 -2
- package/dist/ir/timing-validation.d.ts +6 -1
- package/dist/ir/timing-validation.js +50 -12
- package/dist/ir/transformers/expressions.js +58 -0
- package/dist/ir/transformers/hal-emit-helpers.js +1 -1
- package/dist/ir/transformers/variables.js +41 -3
- package/dist/ir/validation-orchestrator.js +3 -1
- package/dist/libdef/c-to-decl.d.ts +27 -0
- package/dist/libdef/c-to-decl.js +397 -0
- package/dist/libdef/component-decls.d.ts +2 -0
- package/dist/libdef/component-decls.js +6 -0
- package/dist/libdef/component-discovery.d.ts +43 -0
- package/dist/libdef/component-discovery.js +83 -0
- package/dist/libdef/cpp-to-decl.d.ts +9 -0
- package/dist/libdef/cpp-to-decl.js +72 -0
- package/dist/libdef/idf-discovery.d.ts +7 -0
- package/dist/libdef/idf-discovery.js +59 -0
- package/dist/libdef/registry.js +5 -2
- package/dist/lint-cache.d.ts +59 -0
- package/dist/lint-cache.js +257 -0
- package/dist/orchestrator/graph-builder.js +6 -2
- package/dist/stores/display-profile-store.d.ts +1 -0
- package/dist/stores/display-profile-store.js +1 -0
- package/dist/testing.d.ts +3 -1
- package/dist/testing.js +3 -1
- package/dist/transpile.d.ts +3 -0
- package/dist/transpile.js +72 -27
- package/dist/ui-hook.d.ts +17 -1
- package/dist/utils/cli.js +27 -1
- package/dist/utils/fs.d.ts +13 -0
- package/dist/utils/fs.js +50 -0
- package/package.json +8 -4
|
@@ -435,5 +435,23 @@ function collectHALOpIdentifiers(op) {
|
|
|
435
435
|
}
|
|
436
436
|
// Other HAL ops have only numeric/literal fields — no identifier references
|
|
437
437
|
}
|
|
438
|
+
// wifi.* / http.* ops carry resolved C++ expression texts in their string
|
|
439
|
+
// fields (e.g. wifi.connect ssid: `WIFI_SSID` — a top-level const, or a
|
|
440
|
+
// quoted literal). Scan every string field for identifiers so referenced
|
|
441
|
+
// globals survive tree-shaking; quoted literals are skipped.
|
|
442
|
+
if (op.operation.startsWith("wifi.") || op.operation.startsWith("http.")) {
|
|
443
|
+
for (const value of Object.values(op)) {
|
|
444
|
+
if (typeof value !== "string" || value === op.operation)
|
|
445
|
+
continue;
|
|
446
|
+
if (/^".*"$/.test(value.trim()))
|
|
447
|
+
continue;
|
|
448
|
+
const matches = value.match(/[A-Za-z_][A-Za-z0-9_]*/g);
|
|
449
|
+
if (matches) {
|
|
450
|
+
for (const match of matches) {
|
|
451
|
+
identifiers.add(match);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
438
456
|
return identifiers;
|
|
439
457
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ProgramIR } from "../api/index.js";
|
|
2
|
+
import type { Diagnostic } from "../types.js";
|
|
3
|
+
import type { BoardConstants } from "./board-resolver.js";
|
|
4
|
+
export declare function validateNetworkUsage(program: ProgramIR, boardConstants?: BoardConstants): Diagnostic[];
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Network (WiFi / HTTP) Validation
|
|
3
|
+
//
|
|
4
|
+
// Board-aware, compile-time checks for the WiFi/HTTP HAL:
|
|
5
|
+
//
|
|
6
|
+
// wifi-no-radio (error) wifi.*/http.* used on a board whose
|
|
7
|
+
// architecture has no WiFi radio (avr).
|
|
8
|
+
// http-without-wifi (warning) http.* used but the program never
|
|
9
|
+
// brings a WiFi link up (connect /
|
|
10
|
+
// connectSaved / connectAsync / AP).
|
|
11
|
+
// wifi-blocking-in-loop (warning) blocking WiFi.connect()/scan() or
|
|
12
|
+
// Http send inside loop() — stalls every
|
|
13
|
+
// iteration; use async/await instead.
|
|
14
|
+
// wifi-ap-password-short (error) WiFi.startAP() with a literal WPA2
|
|
15
|
+
// password shorter than 8 characters —
|
|
16
|
+
// esp_wifi rejects it at runtime.
|
|
17
|
+
// http-max-body-large (warning) Http maxBody() above 64 KB — the
|
|
18
|
+
// response buffer is heap-allocated.
|
|
19
|
+
//
|
|
20
|
+
// Follows the ADC-validation convention: checks that need board data return
|
|
21
|
+
// nothing when the data is missing (no MCU-name fallbacks).
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
import { walkProgramIR, walkNestedStatements } from "./utils/walk-ir.js";
|
|
24
|
+
/** Architectures with no WiFi radio. Driven by the `architecture` board
|
|
25
|
+
* constant (from the MCU package); absent data emits nothing. */
|
|
26
|
+
const NO_RADIO_ARCHITECTURES = new Set(["avr"]);
|
|
27
|
+
/** Ops that bring the WiFi link (STA or AP) up. */
|
|
28
|
+
const LINK_UP_OPS = new Set([
|
|
29
|
+
"wifi.connect",
|
|
30
|
+
"wifi.connect_start",
|
|
31
|
+
"wifi.connect_saved",
|
|
32
|
+
"wifi.ap_start",
|
|
33
|
+
]);
|
|
34
|
+
/** Blocking waits that stall loop() for their full duration. */
|
|
35
|
+
const BLOCKING_LOOP_OPS = new Set([
|
|
36
|
+
"wifi.connect",
|
|
37
|
+
"wifi.connect_saved",
|
|
38
|
+
"wifi.wait_connected",
|
|
39
|
+
"wifi.wait_disconnected",
|
|
40
|
+
"wifi.scan",
|
|
41
|
+
"http.send",
|
|
42
|
+
]);
|
|
43
|
+
/** Strip surrounding quotes from a resolved HAL string field; returns null
|
|
44
|
+
* when the field is not a literal (identifier/expression — unknowable). */
|
|
45
|
+
function literalString(value) {
|
|
46
|
+
if (typeof value !== "string")
|
|
47
|
+
return null;
|
|
48
|
+
const trimmed = value.trim();
|
|
49
|
+
const match = trimmed.match(/^"((?:[^"\\]|\\.)*)"$/);
|
|
50
|
+
return match ? match[1] : null;
|
|
51
|
+
}
|
|
52
|
+
function diagAt(stmt, partial) {
|
|
53
|
+
const s = stmt;
|
|
54
|
+
return {
|
|
55
|
+
...partial,
|
|
56
|
+
line: s.sourceSpan?.startLine,
|
|
57
|
+
column: s.sourceSpan?.startColumn,
|
|
58
|
+
filePath: s.sourceSpan?.filePath,
|
|
59
|
+
source: "network-validation",
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** Pull every wifi./http. op out of a statement: hal-op statements, awaited
|
|
63
|
+
* __WIFI_WAIT__/__HTTP_WAIT__ marker calls (hal-expr arg), and hal-expr
|
|
64
|
+
* expressions in call args / initializers / conditions. */
|
|
65
|
+
function collectNetworkOps(stmt, out) {
|
|
66
|
+
const s = stmt;
|
|
67
|
+
const record = (op) => {
|
|
68
|
+
const operation = op?.operation;
|
|
69
|
+
if (typeof operation === "string" && (operation.startsWith("wifi.") || operation.startsWith("http."))) {
|
|
70
|
+
out.push({ op: op, stmt });
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
if (stmt.kind === "hal-op")
|
|
74
|
+
record(s.operation);
|
|
75
|
+
const visitExpr = (expr) => {
|
|
76
|
+
if (!expr || typeof expr !== "object")
|
|
77
|
+
return;
|
|
78
|
+
const e = expr;
|
|
79
|
+
if (e.kind === "hal-expr")
|
|
80
|
+
record(e.operation);
|
|
81
|
+
for (const key of ["args", "elements", "parts"]) {
|
|
82
|
+
if (Array.isArray(e[key]))
|
|
83
|
+
for (const child of e[key])
|
|
84
|
+
visitExpr(child);
|
|
85
|
+
}
|
|
86
|
+
for (const key of ["left", "right", "operand", "condition", "whenTrue", "whenFalse", "inner", "value", "expression"]) {
|
|
87
|
+
if (e[key] && typeof e[key] === "object")
|
|
88
|
+
visitExpr(e[key]);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
for (const key of ["args", "value", "condition", "initializer", "expression"]) {
|
|
92
|
+
if (s[key]) {
|
|
93
|
+
if (Array.isArray(s[key]))
|
|
94
|
+
for (const child of s[key])
|
|
95
|
+
visitExpr(child);
|
|
96
|
+
else
|
|
97
|
+
visitExpr(s[key]);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
export function validateNetworkUsage(program, boardConstants) {
|
|
102
|
+
const diagnostics = [];
|
|
103
|
+
const constants = boardConstants ?? program.boardConstants;
|
|
104
|
+
// Collect every wifi./http. op in the program.
|
|
105
|
+
const allOps = [];
|
|
106
|
+
walkProgramIR(program, (stmt) => collectNetworkOps(stmt, allOps));
|
|
107
|
+
if (allOps.length === 0)
|
|
108
|
+
return diagnostics;
|
|
109
|
+
const wifiOps = allOps.filter(({ op }) => op.operation.startsWith("wifi."));
|
|
110
|
+
const httpOps = allOps.filter(({ op }) => op.operation.startsWith("http."));
|
|
111
|
+
// ── wifi-no-radio ────────────────────────────────────────────────────────
|
|
112
|
+
const architecture = constants?.get("architecture");
|
|
113
|
+
const archName = typeof architecture === "string" ? architecture.replace(/^["']|["']$/g, "").toLowerCase() : null;
|
|
114
|
+
if (archName && NO_RADIO_ARCHITECTURES.has(archName)) {
|
|
115
|
+
const first = allOps[0];
|
|
116
|
+
diagnostics.push(diagAt(first.stmt, {
|
|
117
|
+
severity: "error",
|
|
118
|
+
code: "wifi-no-radio",
|
|
119
|
+
message: `WiFi/HTTP APIs are used but the target board's architecture ('${archName}') has no WiFi radio.`,
|
|
120
|
+
hint: "Target a WiFi-capable board (e.g. @typecad/board-esp32-devkit) or remove the WiFi/HTTP calls.",
|
|
121
|
+
}));
|
|
122
|
+
// No point piling on the remaining checks for a board that can't radio.
|
|
123
|
+
return diagnostics;
|
|
124
|
+
}
|
|
125
|
+
// ── http-without-wifi ────────────────────────────────────────────────────
|
|
126
|
+
if (httpOps.length > 0 && !wifiOps.some(({ op }) => LINK_UP_OPS.has(op.operation))) {
|
|
127
|
+
diagnostics.push(diagAt(httpOps[0].stmt, {
|
|
128
|
+
severity: "warning",
|
|
129
|
+
code: "http-without-wifi",
|
|
130
|
+
message: "HTTP requests are made but the program never brings the WiFi link up — every request will fail at runtime.",
|
|
131
|
+
hint: "Call WiFi.connect(ssid, password) (or WiFi.connectSaved() / WiFi.startAP()) before sending HTTP requests.",
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
// ── wifi-blocking-in-loop ────────────────────────────────────────────────
|
|
135
|
+
// Blocking hal-op statements inside loop()'s body stall every iteration.
|
|
136
|
+
// Awaited forms are rewritten to __WIFI_WAIT__/__HTTP_WAIT__ marker calls
|
|
137
|
+
// (kind "call"), so only genuine blocking hal-ops are flagged here.
|
|
138
|
+
const loopFn = program.functions.find((fn) => fn.originalName === "loop");
|
|
139
|
+
if (loopFn?.statements) {
|
|
140
|
+
const visit = (stmt) => {
|
|
141
|
+
if (stmt.kind === "hal-op" && BLOCKING_LOOP_OPS.has(stmt.operation?.operation)) {
|
|
142
|
+
const opName = stmt.operation.operation;
|
|
143
|
+
diagnostics.push(diagAt(stmt, {
|
|
144
|
+
severity: "warning",
|
|
145
|
+
code: "wifi-blocking-in-loop",
|
|
146
|
+
message: `Blocking network operation '${opName}' inside loop() stalls every iteration until it completes (seconds for a connect/scan/request).`,
|
|
147
|
+
hint: "Move the call into an async function and `await` it — the transpiler lowers awaited WiFi/HTTP calls to non-blocking start + poll states.",
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
walkNestedStatements(stmt, visit);
|
|
151
|
+
};
|
|
152
|
+
for (const stmt of loopFn.statements)
|
|
153
|
+
visit(stmt);
|
|
154
|
+
}
|
|
155
|
+
// ── wifi-ap-password-short ───────────────────────────────────────────────
|
|
156
|
+
for (const { op, stmt } of wifiOps) {
|
|
157
|
+
if (op.operation !== "wifi.ap_start")
|
|
158
|
+
continue;
|
|
159
|
+
const password = literalString(op.password);
|
|
160
|
+
if (password !== null && password.length > 0 && password.length < 8) {
|
|
161
|
+
diagnostics.push(diagAt(stmt, {
|
|
162
|
+
severity: "error",
|
|
163
|
+
code: "wifi-ap-password-short",
|
|
164
|
+
message: `WiFi.startAP() password is ${password.length} characters — WPA2 requires at least 8, and esp_wifi rejects shorter ones at runtime.`,
|
|
165
|
+
hint: "Use a password of 8+ characters, or omit the password entirely for an open access point.",
|
|
166
|
+
}));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// ── http-max-body-large ──────────────────────────────────────────────────
|
|
170
|
+
for (const { op, stmt } of httpOps) {
|
|
171
|
+
if (op.operation !== "http.set_max_body")
|
|
172
|
+
continue;
|
|
173
|
+
const bytes = Number(String(op.bytes).trim());
|
|
174
|
+
if (Number.isFinite(bytes) && bytes > 65536) {
|
|
175
|
+
diagnostics.push(diagAt(stmt, {
|
|
176
|
+
severity: "warning",
|
|
177
|
+
code: "http-max-body-large",
|
|
178
|
+
message: `Http maxBody(${bytes}) heap-allocates a ${Math.round(bytes / 1024)} KB response buffer — large buffers risk allocation failure under fragmentation.`,
|
|
179
|
+
hint: "Keep the response cap at or below 64 KB, or process large payloads in chunks server-side.",
|
|
180
|
+
}));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return diagnostics;
|
|
184
|
+
}
|
|
@@ -983,7 +983,20 @@ function validateConstSuggestions(program, diagnostics) {
|
|
|
983
983
|
// keep the binding non-const so push_back/splice/etc. compile. The
|
|
984
984
|
// callee is the lowered C++ name, e.g. "arr.push_back".
|
|
985
985
|
if (stmt.kind === 'call' && typeof stmt.callee === 'string') {
|
|
986
|
-
|
|
986
|
+
// Value-arg array methods (.push/.pop/.fill/...) are lowered in
|
|
987
|
+
// call-statement.ts to a fully-formed raw C++ expression wrapped as
|
|
988
|
+
// `__RAW_STMT__<receiver>.<method>(<args>)` (e.g. `__RAW_STMT__arr.push_back(1)`).
|
|
989
|
+
// Strip the wrapper — and the trailing `(args)` when present — before
|
|
990
|
+
// splitting, otherwise the dot parse yields a receiver of
|
|
991
|
+
// `__RAW_STMT__arr` and a method of `push_back(1)` and the demotion
|
|
992
|
+
// below never fires, leaving a non-compiling `const std::vector` +
|
|
993
|
+
// push_back. Mirrors the __RAW_STMT__ handling in program-analysis.ts.
|
|
994
|
+
const rawCallee = stmt.callee;
|
|
995
|
+
const unwrapped = rawCallee.startsWith('__RAW_STMT__')
|
|
996
|
+
? rawCallee.slice('__RAW_STMT__'.length)
|
|
997
|
+
: rawCallee;
|
|
998
|
+
const parenIdx = unwrapped.indexOf('(');
|
|
999
|
+
const callee = parenIdx > 0 ? unwrapped.slice(0, parenIdx) : unwrapped;
|
|
987
1000
|
const dot = callee.lastIndexOf('.');
|
|
988
1001
|
if (dot > 0) {
|
|
989
1002
|
const receiver = callee.slice(0, dot);
|
|
@@ -52,6 +52,29 @@ export interface ProgramAnalysisResult {
|
|
|
52
52
|
* __tc_TimerRuntime::MAX_TIMERS to the observed count (floor 1) rather than
|
|
53
53
|
* a blind constant, so a one-timer program links one slot, not eight. */
|
|
54
54
|
timerCallCount: number;
|
|
55
|
+
/** ESP32 peripheral usage — framework-esp32 gates its IDF driver blocks and
|
|
56
|
+
* forced includes on these. Detected from HAL-op operation names, the same
|
|
57
|
+
* way usesUart/usesSPI/usesI2C are. Other frameworks have no CUTTLEFISH_*
|
|
58
|
+
* blocks with these marker names so the setup.ts filters are no-ops there. */
|
|
59
|
+
usesGPIO: boolean;
|
|
60
|
+
usesPWM: boolean;
|
|
61
|
+
usesRmt: boolean;
|
|
62
|
+
usesADC: boolean;
|
|
63
|
+
usesDAC: boolean;
|
|
64
|
+
usesPower: boolean;
|
|
65
|
+
usesWdt: boolean;
|
|
66
|
+
usesInterrupts: boolean;
|
|
67
|
+
usesPulse: boolean;
|
|
68
|
+
usesShift: boolean;
|
|
69
|
+
/** WiFi / HTTP / BLE usage — framework-esp32 gates the __tc_wifi/__tc_http/
|
|
70
|
+
* __tc_ble runtime shims and their esp_wifi/esp_http_client/nimble includes
|
|
71
|
+
* on these. Detected from wifi.* / http.* / ble.* HAL-op operation names. */
|
|
72
|
+
usesWifi: boolean;
|
|
73
|
+
usesHttp: boolean;
|
|
74
|
+
usesBle: boolean;
|
|
75
|
+
/** Preferences (NVS) usage — framework-esp32 gates the __tc_prefs runtime shim
|
|
76
|
+
* and its nvs_flash/nvs includes on this. Detected from preferences.* ops. */
|
|
77
|
+
usesPreferences: boolean;
|
|
55
78
|
}
|
|
56
79
|
/**
|
|
57
80
|
* Analyze a program IR in a single pass to detect all features.
|
|
@@ -216,6 +216,23 @@ function analyzeExpression(expr, result, strategy) {
|
|
|
216
216
|
analyzeExpression(expr.expression, result, strategy);
|
|
217
217
|
}
|
|
218
218
|
break;
|
|
219
|
+
case "hal-expr":
|
|
220
|
+
// Value-position HAL ops (e.g. `const ip = WiFi.localIP()` lowers to a
|
|
221
|
+
// hal-expr initializer) never pass through the statement-level hal-op
|
|
222
|
+
// detection above, so mirror the wifi./http. flag detection here. These
|
|
223
|
+
// gate the framework-esp32 __tc_wifi/__tc_http runtime shims.
|
|
224
|
+
if (expr.operation && typeof expr.operation.operation === "string") {
|
|
225
|
+
const opName = expr.operation.operation;
|
|
226
|
+
if (opName.startsWith("wifi."))
|
|
227
|
+
result.usesWifi = true;
|
|
228
|
+
if (opName.startsWith("http."))
|
|
229
|
+
result.usesHttp = true;
|
|
230
|
+
if (opName.startsWith("ble."))
|
|
231
|
+
result.usesBle = true;
|
|
232
|
+
if (opName.startsWith("preferences."))
|
|
233
|
+
result.usesPreferences = true;
|
|
234
|
+
}
|
|
235
|
+
break;
|
|
219
236
|
}
|
|
220
237
|
}
|
|
221
238
|
/**
|
|
@@ -242,6 +259,11 @@ function analyzeStatement(statement, result, strategy) {
|
|
|
242
259
|
if (statement.callee === "millis" || statement.callee === "delay") {
|
|
243
260
|
result.usesMillis = true;
|
|
244
261
|
}
|
|
262
|
+
// Awaited HAL wait markers become async state-machine poll states that
|
|
263
|
+
// arm deadlines via currentTimeMillis().
|
|
264
|
+
if (statement.callee === "__WIFI_WAIT__" || statement.callee === "__HTTP_WAIT__" || statement.callee === "__HAL_WAIT__") {
|
|
265
|
+
result.usesMillis = true;
|
|
266
|
+
}
|
|
245
267
|
// Namespace-qualified polyfill entry points used as bare call statements
|
|
246
268
|
// (e.g. `Timing.delay(5);`). The expression-level analyzer (case
|
|
247
269
|
// "method-call") already checks these prefixes, but a statement-form call
|
|
@@ -264,7 +286,10 @@ function analyzeStatement(statement, result, strategy) {
|
|
|
264
286
|
if (statement.callee === "tone" || statement.callee === "noTone") {
|
|
265
287
|
result.usesTone = true;
|
|
266
288
|
}
|
|
267
|
-
|
|
289
|
+
// console.* is NOT UART: on ESP-IDF it lowers to printf, on Arduino to
|
|
290
|
+
// Serial via hasConsoleCalls. Only real UART HAL / Serial peripheral
|
|
291
|
+
// usage should gate the uart shim (avoids unused __tc_uart*_init).
|
|
292
|
+
if (statement.callee.startsWith("_uart_")) {
|
|
268
293
|
result.usesUart = true;
|
|
269
294
|
}
|
|
270
295
|
if (statement.callee.startsWith("Serial.") || statement.callee === "Serial") {
|
|
@@ -424,6 +449,46 @@ function analyzeStatement(statement, result, strategy) {
|
|
|
424
449
|
result.usesTone = true;
|
|
425
450
|
if (opName.startsWith("uart."))
|
|
426
451
|
result.usesUart = true;
|
|
452
|
+
// Display HAL ops (display.init from ui.mount, display.flush per frame)
|
|
453
|
+
// always imply GPIO usage (CS/DC/RST pins). Native SPI/I2C display
|
|
454
|
+
// adapters emit their own transport #includes (driver/spi_master.h,
|
|
455
|
+
// driver/i2c_master.h) — we don't force usesSPI/usesI2C here because
|
|
456
|
+
// that would pull headers even for non-display SPI/I2C code, and the
|
|
457
|
+
// display's transport type can't be determined from the op alone.
|
|
458
|
+
if (opName.startsWith("display.")) {
|
|
459
|
+
result.usesGPIO = true;
|
|
460
|
+
}
|
|
461
|
+
// ESP32 peripheral usage — framework-esp32 gates IDF driver blocks
|
|
462
|
+
// and forced includes on these. No-op for other frameworks (their
|
|
463
|
+
// shimLines emit no CUTTLEFISH_* blocks with these marker names).
|
|
464
|
+
if (opName.startsWith("gpio."))
|
|
465
|
+
result.usesGPIO = true;
|
|
466
|
+
if (opName.startsWith("pwm."))
|
|
467
|
+
result.usesPWM = true;
|
|
468
|
+
if (opName.startsWith("rmt."))
|
|
469
|
+
result.usesRmt = true;
|
|
470
|
+
if (opName.startsWith("adc."))
|
|
471
|
+
result.usesADC = true;
|
|
472
|
+
if (opName.startsWith("dac."))
|
|
473
|
+
result.usesDAC = true;
|
|
474
|
+
if (opName.startsWith("power."))
|
|
475
|
+
result.usesPower = true;
|
|
476
|
+
if (opName.startsWith("wdt."))
|
|
477
|
+
result.usesWdt = true;
|
|
478
|
+
if (opName.startsWith("interrupt."))
|
|
479
|
+
result.usesInterrupts = true;
|
|
480
|
+
if (opName.startsWith("pulse."))
|
|
481
|
+
result.usesPulse = true;
|
|
482
|
+
if (opName.startsWith("shift."))
|
|
483
|
+
result.usesShift = true;
|
|
484
|
+
if (opName.startsWith("wifi."))
|
|
485
|
+
result.usesWifi = true;
|
|
486
|
+
if (opName.startsWith("http."))
|
|
487
|
+
result.usesHttp = true;
|
|
488
|
+
if (opName.startsWith("ble."))
|
|
489
|
+
result.usesBle = true;
|
|
490
|
+
if (opName.startsWith("preferences."))
|
|
491
|
+
result.usesPreferences = true;
|
|
427
492
|
// Timing HAL ops (timing.delay/millis/micros) carry a typed operation
|
|
428
493
|
// name, not raw code, so the regex scans below miss them. Mirror the
|
|
429
494
|
// raw-code timing detection here so usesMillis/usesTiming (and thus
|
|
@@ -467,7 +532,7 @@ function analyzeStatement(statement, result, strategy) {
|
|
|
467
532
|
if (/\bEEPROM\b/.test(code) || /\beeprom_(read|write|update)_byte\b/.test(code)) {
|
|
468
533
|
result.usesEEPROM = true;
|
|
469
534
|
}
|
|
470
|
-
if (/\
|
|
535
|
+
if (/\bSerial\b/.test(code)) {
|
|
471
536
|
result.usesUart = true;
|
|
472
537
|
}
|
|
473
538
|
if (/\bSPI\b/.test(code)) {
|
|
@@ -535,6 +600,20 @@ export function analyzeProgram(program, strategy) {
|
|
|
535
600
|
hasGenerators: false,
|
|
536
601
|
usesStdMap: false,
|
|
537
602
|
timerCallCount: 0,
|
|
603
|
+
usesGPIO: false,
|
|
604
|
+
usesPWM: false,
|
|
605
|
+
usesRmt: false,
|
|
606
|
+
usesADC: false,
|
|
607
|
+
usesDAC: false,
|
|
608
|
+
usesPower: false,
|
|
609
|
+
usesWdt: false,
|
|
610
|
+
usesInterrupts: false,
|
|
611
|
+
usesPulse: false,
|
|
612
|
+
usesShift: false,
|
|
613
|
+
usesWifi: false,
|
|
614
|
+
usesHttp: false,
|
|
615
|
+
usesBle: false,
|
|
616
|
+
usesPreferences: false,
|
|
538
617
|
};
|
|
539
618
|
// Analyze type aliases
|
|
540
619
|
for (const typeAlias of program.typeAliases) {
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import type { ProgramIR } from '../api/index.js';
|
|
2
2
|
import type { Diagnostic } from '../types.js';
|
|
3
|
+
import type { PlatformStrategy } from '../api/shared/index.js';
|
|
3
4
|
/**
|
|
4
5
|
* Detect blocking delay() calls inside loop()'s body. Emits a warning for
|
|
5
6
|
* each, explaining the cooperative-async alternative.
|
|
7
|
+
*
|
|
8
|
+
* On RTOS targets (ESP-IDF), timing.delay lowers to vTaskDelay which yields
|
|
9
|
+
* the CPU — it does NOT freeze the async queue or UI. Only delayMicroseconds
|
|
10
|
+
* (busy-wait) triggers the warning.
|
|
6
11
|
*/
|
|
7
|
-
export declare function validateBlockingDelayInLoop(program: ProgramIR): Diagnostic[];
|
|
12
|
+
export declare function validateBlockingDelayInLoop(program: ProgramIR, strategy?: PlatformStrategy): Diagnostic[];
|
|
@@ -12,20 +12,43 @@
|
|
|
12
12
|
// g++ sees only an ordinary function. This validator flags blocking delays in
|
|
13
13
|
// loop()'s direct body so the user can replace them with the cooperative
|
|
14
14
|
// Async.sleep() / millis()-comparison pattern.
|
|
15
|
+
//
|
|
16
|
+
// IMPORTANT: on RTOS targets (ESP-IDF / FreeRTOS), delay() lowers to
|
|
17
|
+
// vTaskDelay which YIELDS the CPU — it does not freeze other tasks, UI
|
|
18
|
+
// rendering, or the async queue. Only delayMicroseconds (esp_rom_delay_us)
|
|
19
|
+
// is a true busy-wait. So for RTOS targets, only delay_microseconds triggers
|
|
20
|
+
// the warning; delay is safe.
|
|
15
21
|
// ---------------------------------------------------------------------------
|
|
16
|
-
|
|
17
|
-
|
|
22
|
+
import { hasLoadedFramework, getLoadedFramework } from '../framework-registry.js';
|
|
23
|
+
/** Check a single statement for a blocking delay call or hal-op.
|
|
24
|
+
* On RTOS targets, timing.delay (vTaskDelay) is NOT blocking — only
|
|
25
|
+
* timing.delay_microseconds (esp_rom_delay_us) is. */
|
|
26
|
+
function isBlockingDelay(stmt, isRtos) {
|
|
18
27
|
const s = stmt;
|
|
19
28
|
// Direct call: delay(...) or delayMicroseconds(...)
|
|
20
29
|
if (stmt.kind === 'call' && typeof s.callee === 'string') {
|
|
21
|
-
if (
|
|
22
|
-
|
|
30
|
+
if (isRtos) {
|
|
31
|
+
// On RTOS targets, only delayMicroseconds is a busy-wait.
|
|
32
|
+
if (s.callee === 'delayMicroseconds')
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
if (s.callee === 'delay' || s.callee === 'delayMicroseconds')
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
23
39
|
}
|
|
24
40
|
// Hal-op: after HAL resolution, delay() becomes timing.delay /
|
|
25
41
|
// timing.delay_microseconds.
|
|
26
42
|
if (stmt.kind === 'hal-op' && s.operation?.operation) {
|
|
27
|
-
if (
|
|
28
|
-
|
|
43
|
+
if (isRtos) {
|
|
44
|
+
// On RTOS targets, only timing.delay_microseconds is blocking.
|
|
45
|
+
if (s.operation.operation === 'timing.delay_microseconds')
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
if (s.operation.operation === 'timing.delay' || s.operation.operation === 'timing.delay_microseconds') {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
29
52
|
}
|
|
30
53
|
}
|
|
31
54
|
return false;
|
|
@@ -33,22 +56,37 @@ function isBlockingDelay(stmt) {
|
|
|
33
56
|
/**
|
|
34
57
|
* Detect blocking delay() calls inside loop()'s body. Emits a warning for
|
|
35
58
|
* each, explaining the cooperative-async alternative.
|
|
59
|
+
*
|
|
60
|
+
* On RTOS targets (ESP-IDF), timing.delay lowers to vTaskDelay which yields
|
|
61
|
+
* the CPU — it does NOT freeze the async queue or UI. Only delayMicroseconds
|
|
62
|
+
* (busy-wait) triggers the warning.
|
|
36
63
|
*/
|
|
37
|
-
export function validateBlockingDelayInLoop(program) {
|
|
64
|
+
export function validateBlockingDelayInLoop(program, strategy) {
|
|
38
65
|
const diagnostics = [];
|
|
66
|
+
// Detect RTOS targets where delay() yields rather than blocks.
|
|
67
|
+
// framework-esp32 uses FreeRTOS; vTaskDelay is a yielding delay.
|
|
68
|
+
const isRtos = strategy?.isRtosTarget?.() === true
|
|
69
|
+
|| (hasLoadedFramework() && getLoadedFramework().strategy.isRtosTarget?.() === true);
|
|
39
70
|
const loopFn = program.functions.find(fn => fn.originalName === 'loop');
|
|
40
71
|
if (!loopFn || !loopFn.statements)
|
|
41
72
|
return diagnostics;
|
|
42
73
|
const visit = (stmts) => {
|
|
43
74
|
for (const stmt of stmts) {
|
|
44
|
-
if (isBlockingDelay(stmt)) {
|
|
75
|
+
if (isBlockingDelay(stmt, isRtos)) {
|
|
45
76
|
const s = stmt;
|
|
77
|
+
const message = isRtos
|
|
78
|
+
? `Blocking delayMicroseconds() inside loop() is a busy-wait that wastes CPU cycles. ` +
|
|
79
|
+
`On RTOS targets (ESP-IDF), prefer timing.delay() (vTaskDelay) which yields the CPU to other tasks.`
|
|
80
|
+
: `Blocking delay() inside loop() freezes the async microtask queue and UI rendering ` +
|
|
81
|
+
`for the delay duration. This causes display tearing and makes the sketch unresponsive.`;
|
|
82
|
+
const hint = isRtos
|
|
83
|
+
? `Replace delayMicroseconds with delay() if the timing permits, or accept the brief busy-wait if sub-millisecond precision is required.`
|
|
84
|
+
: `Use the cooperative pattern instead: track elapsed time with millis() comparisons, ` +
|
|
85
|
+
`or use Async.sleep(ms) / Async.yield() to let other tasks run between checks.`;
|
|
46
86
|
diagnostics.push({
|
|
47
87
|
severity: 'warning',
|
|
48
|
-
message
|
|
49
|
-
|
|
50
|
-
hint: `Use the cooperative pattern instead: track elapsed time with millis() comparisons, ` +
|
|
51
|
-
`or use Async.sleep(ms) / Async.yield() to let other tasks run between checks.`,
|
|
88
|
+
message,
|
|
89
|
+
hint,
|
|
52
90
|
line: stmt.sourceSpan?.startLine,
|
|
53
91
|
column: stmt.sourceSpan?.startColumn,
|
|
54
92
|
filePath: stmt.sourceSpan?.filePath,
|
|
@@ -16,6 +16,17 @@ export function expressionStatementToIR(statement, fileName, sourceText, diagnos
|
|
|
16
16
|
if (callStmt && callStmt.kind === "call") {
|
|
17
17
|
return { ...callStmt, isAwaited: true };
|
|
18
18
|
}
|
|
19
|
+
// Awaited network HAL ops (WiFi.connect / WiFi.untilConnected / Http.send
|
|
20
|
+
// / ...) resolve to hal-op statements, which would otherwise lower to the
|
|
21
|
+
// BLOCKING shim call even inside an async state machine. Rewrite them to
|
|
22
|
+
// an awaited __WIFI_WAIT__/__HTTP_WAIT__ marker call carrying the original
|
|
23
|
+
// op; the async state-machine generator turns it into a start + poll state
|
|
24
|
+
// pair, and the statement renderer falls back to the blocking form when
|
|
25
|
+
// the marker is rendered outside a state machine (top-level await, awaits
|
|
26
|
+
// nested in unsupported positions).
|
|
27
|
+
const netMarker = awaitedNetMarker(callStmt);
|
|
28
|
+
if (netMarker)
|
|
29
|
+
return netMarker;
|
|
19
30
|
return callStmt;
|
|
20
31
|
}
|
|
21
32
|
// ── Register bit-field write ────────────────────────────────────────
|
|
@@ -404,3 +415,50 @@ export function expressionStatementToIR(statement, fileName, sourceText, diagnos
|
|
|
404
415
|
}
|
|
405
416
|
return undefined;
|
|
406
417
|
}
|
|
418
|
+
/** HAL ops with a start/poll split available in the async state machine
|
|
419
|
+
* (see async-state-machine.ts netWaitInfo — keep the two in sync).
|
|
420
|
+
* timing.delay is included because `delay()` from @typecad/hal resolves to a
|
|
421
|
+
* hal-op, dropping the isAwaited flag the state machine keys on. */
|
|
422
|
+
const AWAITABLE_HAL_OPS = new Set([
|
|
423
|
+
"timing.delay",
|
|
424
|
+
"wifi.connect",
|
|
425
|
+
"wifi.wait_connected",
|
|
426
|
+
"wifi.wait_disconnected",
|
|
427
|
+
"wifi.scan",
|
|
428
|
+
"http.send",
|
|
429
|
+
"ble.until_connected",
|
|
430
|
+
]);
|
|
431
|
+
/**
|
|
432
|
+
* Rewrite an awaited hal-op statement (or a block whose LAST statement is
|
|
433
|
+
* one — chained HAL calls resolve to blocks of hal-ops) into an awaited
|
|
434
|
+
* `__WIFI_WAIT__` / `__HTTP_WAIT__` / `__HAL_WAIT__` marker call carrying the
|
|
435
|
+
* original op as a hal-expr argument. Returns undefined when the statement is
|
|
436
|
+
* not an awaitable HAL op.
|
|
437
|
+
*/
|
|
438
|
+
function awaitedNetMarker(stmt) {
|
|
439
|
+
if (!stmt)
|
|
440
|
+
return undefined;
|
|
441
|
+
if (stmt.kind === "hal-op" && AWAITABLE_HAL_OPS.has(stmt.operation.operation)) {
|
|
442
|
+
const opName = stmt.operation.operation;
|
|
443
|
+
const callee = opName.startsWith("http.") ? "__HTTP_WAIT__"
|
|
444
|
+
: opName.startsWith("wifi.") ? "__WIFI_WAIT__"
|
|
445
|
+
: opName.startsWith("ble.") ? "__BLE_WAIT__"
|
|
446
|
+
: "__HAL_WAIT__";
|
|
447
|
+
return {
|
|
448
|
+
kind: "call",
|
|
449
|
+
sourceSpan: stmt.sourceSpan,
|
|
450
|
+
leadingComments: stmt.leadingComments,
|
|
451
|
+
trailingComments: stmt.trailingComments,
|
|
452
|
+
callee,
|
|
453
|
+
args: [{ kind: "hal-expr", operation: stmt.operation }],
|
|
454
|
+
isAwaited: true,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
if (stmt.kind === "block" && stmt.body.length > 0) {
|
|
458
|
+
const last = awaitedNetMarker(stmt.body[stmt.body.length - 1]);
|
|
459
|
+
if (last) {
|
|
460
|
+
return { ...stmt, body: [...stmt.body.slice(0, -1), last] };
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return undefined;
|
|
464
|
+
}
|
|
@@ -27,7 +27,7 @@ export function collectChainedHALEmits(expr, sourceText, diagnostics, pointerVar
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
// Continue recursion to collect deeper chain levels
|
|
30
|
-
if (ts.isCallExpression(innerReceiver) && ts.isPropertyAccessExpression(innerReceiver)) {
|
|
30
|
+
if (ts.isCallExpression(innerReceiver) && ts.isPropertyAccessExpression(innerReceiver.expression)) {
|
|
31
31
|
collectChainedHALEmits(innerReceiver, sourceText, diagnostics, pointerVars, emitLines, halOps);
|
|
32
32
|
}
|
|
33
33
|
}
|
|
@@ -7,7 +7,8 @@ import { getCurrentIrTypeScope, setScopeLocalType } from "../symbol-types.js";
|
|
|
7
7
|
import { renderExprAsText } from "../render-expr.js";
|
|
8
8
|
import { expressionToIR } from "../expression-to-ir.js";
|
|
9
9
|
import { buildInlineForLoop } from "./array-methods.js";
|
|
10
|
-
import { isKnownHALClass, getCtorIncludes, registerFloatVariable, resolveHALReceiver, isHALSingleton, } from "../hal-resolver.js";
|
|
10
|
+
import { isKnownHALClass, getCtorIncludes, getHALCtorFieldMap, registerFloatVariable, resolveHALReceiver, isHALSingleton, } from "../hal-resolver.js";
|
|
11
|
+
import { httpFactoryVerb, httpUrlArgText } from "../hal/hal-parser.js";
|
|
11
12
|
import { resolveHALCallForVarInit } from "./hal-call-resolver.js";
|
|
12
13
|
import { recordSignal } from "./ui-call-resolver.js";
|
|
13
14
|
function replaceHalReadBufferPlaceholder(op, varName) {
|
|
@@ -408,9 +409,26 @@ export function variableStatementToIR(statement, fileName, sourceText, diagnosti
|
|
|
408
409
|
}
|
|
409
410
|
}
|
|
410
411
|
}
|
|
412
|
+
// new HttpRequest(method, url) — positional ctor fields. The URL is
|
|
413
|
+
// stored as C++ expression text (quoted literal or identifier).
|
|
414
|
+
if (className === "HttpRequest" && ctorArgs && ctorArgs.length >= 2) {
|
|
415
|
+
const methodArg = ctorArgs[0];
|
|
416
|
+
if (ts.isStringLiteral(methodArg))
|
|
417
|
+
fieldValues.set("_method", methodArg.text.toUpperCase());
|
|
418
|
+
const urlText = httpUrlArgText(ctorArgs[1]);
|
|
419
|
+
if (urlText)
|
|
420
|
+
fieldValues.set("_url", urlText);
|
|
421
|
+
}
|
|
411
422
|
if (ctorArgs) {
|
|
423
|
+
// Resolve a Pin-identifier ctor arg to its _pin number, for any HAL
|
|
424
|
+
// class whose first constructor field is _pin (Pin itself, plus
|
|
425
|
+
// pin-bearing wrappers like RmtChannel). Mirrors the Pin branch
|
|
426
|
+
// above but keyed off the class's registered ctor field map so new
|
|
427
|
+
// pin-keyed classes work without a per-class branch here.
|
|
428
|
+
const ctorFieldMap = getHALCtorFieldMap(className);
|
|
429
|
+
const firstFieldIsPin = ctorFieldMap && Array.from(ctorFieldMap.keys())[0] === "_pin";
|
|
412
430
|
for (const arg of ctorArgs) {
|
|
413
|
-
if (ts.isIdentifier(arg) && className === "Pin") {
|
|
431
|
+
if (ts.isIdentifier(arg) && (className === "Pin" || firstFieldIsPin)) {
|
|
414
432
|
const existing = halInstances.get(arg.text);
|
|
415
433
|
if (existing && existing.fieldValues.has("_pin")) {
|
|
416
434
|
fieldValues.set("_pin", existing.fieldValues.get("_pin"));
|
|
@@ -516,6 +534,19 @@ export function variableStatementToIR(statement, fileName, sourceText, diagnosti
|
|
|
516
534
|
}
|
|
517
535
|
}
|
|
518
536
|
}
|
|
537
|
+
// For Http factory calls (const req = Http.get(url)), record the
|
|
538
|
+
// HTTP verb + URL so req.send() can resolve this._method/this._url.
|
|
539
|
+
if (result.returnClassName === "HttpRequest") {
|
|
540
|
+
const verb = httpFactoryVerb(init.expression.name.text);
|
|
541
|
+
if (verb)
|
|
542
|
+
fieldValues.set("_method", verb);
|
|
543
|
+
const urlArg = init.arguments?.[0];
|
|
544
|
+
if (urlArg) {
|
|
545
|
+
const urlText = httpUrlArgText(urlArg);
|
|
546
|
+
if (urlText)
|
|
547
|
+
fieldValues.set("_url", urlText);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
519
550
|
halInstances.set(varName, {
|
|
520
551
|
className: result.returnClassName,
|
|
521
552
|
fieldValues,
|
|
@@ -573,7 +604,14 @@ export function variableStatementToIR(statement, fileName, sourceText, diagnosti
|
|
|
573
604
|
// fill ops). Everything else is a scalar/value return captured
|
|
574
605
|
// after the side-effect ops.
|
|
575
606
|
const isTypedArray = result.returnValue.startsWith("__TYPED_ARRAY__:");
|
|
576
|
-
|
|
607
|
+
// A factory returning a compile-time HAL instance (e.g.
|
|
608
|
+
// `const req = Http.get(url)` → returnValue `new HttpRequest(...)`)
|
|
609
|
+
// is fully tracked via halInstances — every later method call on
|
|
610
|
+
// the variable resolves at compile time. Emitting the raw
|
|
611
|
+
// `new HttpRequest(...)` would reference a class that doesn't
|
|
612
|
+
// exist in the C++ output.
|
|
613
|
+
const isHalInstanceReturn = !!result.returnClassName && /^new\s/.test(result.returnValue.trim());
|
|
614
|
+
if (!isTypedArray && !isHalInstanceReturn) {
|
|
577
615
|
lowered.push({
|
|
578
616
|
kind: "var_decl",
|
|
579
617
|
sourceSpan: makeSourceSpan(declaration, fileName, sourceText),
|