@typecad/cuttlefish 1.0.0-alpha.3 → 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/README.md +4 -4
- 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 +427 -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/types.d.ts +8 -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-utils.d.ts +1 -0
- package/dist/cli-utils.js +3 -1
- package/dist/cli.js +175 -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/index.d.ts +1 -1
- package/dist/create/index.js +1 -1
- package/dist/create/init-scaffold.d.ts +3 -0
- package/dist/create/init-scaffold.js +74 -2
- package/dist/create/init-templates.d.ts +4 -0
- package/dist/create/init-templates.js +217 -17
- 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 +155 -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/adc-range-validation.js +26 -25
- package/dist/ir/build-ir.js +5 -1
- package/dist/ir/expression-to-ir.js +57 -6
- 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 +571 -0
- package/dist/ir/identifier-collector.js +18 -0
- package/dist/ir/interrupt-analysis.js +8 -3
- package/dist/ir/memory-budget-validation.js +1 -0
- package/dist/ir/network-validation.d.ts +4 -0
- package/dist/ir/network-validation.js +184 -0
- package/dist/ir/ownership-analysis.js +33 -1
- package/dist/ir/peripheral-ownership.js +5 -0
- package/dist/ir/peripheral-validation.d.ts +1 -1
- package/dist/ir/peripheral-validation.js +6 -3
- package/dist/ir/pin-alias-conflict.d.ts +1 -1
- package/dist/ir/pin-alias-conflict.js +2 -1
- package/dist/ir/pin-capability-validation.js +34 -32
- package/dist/ir/pin-mode-validation.js +5 -0
- package/dist/ir/pin-safety.d.ts +1 -1
- package/dist/ir/pin-safety.js +2 -1
- package/dist/ir/program-analysis.d.ts +39 -0
- package/dist/ir/program-analysis.js +192 -0
- package/dist/ir/pulldown-validation.d.ts +1 -1
- package/dist/ir/pulldown-validation.js +2 -1
- package/dist/ir/pwm-timer-sharing.d.ts +1 -1
- package/dist/ir/pwm-timer-sharing.js +2 -1
- package/dist/ir/resource-analysis.js +2 -0
- package/dist/ir/timer0-pwm-timing-conflict.d.ts +1 -1
- package/dist/ir/timer0-pwm-timing-conflict.js +2 -1
- package/dist/ir/timing-validation.d.ts +6 -1
- package/dist/ir/timing-validation.js +51 -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 +86 -19
- package/dist/ir/try-catch-validation.js +2 -0
- package/dist/ir/type-resolution.js +2 -2
- package/dist/ir/unit-suspicion-validation.js +9 -7
- package/dist/ir/validation-orchestrator.js +9 -7
- 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/licenses.d.ts +185 -0
- package/dist/licenses.js +963 -0
- 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 +4 -2
- package/dist/testing.js +4 -2
- package/dist/transpile.d.ts +3 -0
- package/dist/transpile.js +78 -32
- package/dist/types.d.ts +5 -1
- package/dist/ui-hook.d.ts +17 -1
- package/dist/utils/cli.js +71 -1
- package/dist/utils/fs.d.ts +13 -0
- package/dist/utils/fs.js +50 -0
- package/package.json +9 -4
- package/dist/ir/heap-array-validation.d.ts +0 -24
- package/dist/ir/heap-array-validation.js +0 -29
|
@@ -93,9 +93,10 @@ function isInterruptHandlerCallback(callback, parentExpr) {
|
|
|
93
93
|
function scanStatementForUnsafeOps(stmt, diagnostics, unsafeOps) {
|
|
94
94
|
if (!stmt || typeof stmt !== 'object')
|
|
95
95
|
return;
|
|
96
|
+
const filePath = stmt.sourceSpan?.filePath;
|
|
96
97
|
if (stmt.kind === 'call') {
|
|
97
98
|
const call = stmt;
|
|
98
|
-
checkCalleeForUnsafeOp(call.callee, diagnostics, unsafeOps);
|
|
99
|
+
checkCalleeForUnsafeOp(call.callee, diagnostics, unsafeOps, filePath);
|
|
99
100
|
}
|
|
100
101
|
// HAL-op statements: after HAL resolution, bare Arduino calls like delay()
|
|
101
102
|
// and delayMicroseconds() become structured hal-op statements (timing.delay,
|
|
@@ -110,7 +111,7 @@ function scanStatementForUnsafeOps(stmt, diagnostics, unsafeOps) {
|
|
|
110
111
|
};
|
|
111
112
|
const key = HAL_OP_TO_UNSAFE[op.operation];
|
|
112
113
|
if (key)
|
|
113
|
-
checkCalleeForUnsafeOp(key, diagnostics, unsafeOps);
|
|
114
|
+
checkCalleeForUnsafeOp(key, diagnostics, unsafeOps, filePath);
|
|
114
115
|
}
|
|
115
116
|
}
|
|
116
117
|
walkNestedStatements(stmt, (s) => scanStatementForUnsafeOps(s, diagnostics, unsafeOps));
|
|
@@ -118,7 +119,7 @@ function scanStatementForUnsafeOps(stmt, diagnostics, unsafeOps) {
|
|
|
118
119
|
/**
|
|
119
120
|
* Check if a callee is an unsafe operation and generate diagnostic.
|
|
120
121
|
*/
|
|
121
|
-
function checkCalleeForUnsafeOp(callee, diagnostics, unsafeOps) {
|
|
122
|
+
function checkCalleeForUnsafeOp(callee, diagnostics, unsafeOps, filePath) {
|
|
122
123
|
if (!callee)
|
|
123
124
|
return;
|
|
124
125
|
// Check direct matches
|
|
@@ -128,6 +129,7 @@ function checkCalleeForUnsafeOp(callee, diagnostics, unsafeOps) {
|
|
|
128
129
|
severity: unsafe.severity,
|
|
129
130
|
message: `${callee}() ${unsafe.reason}`,
|
|
130
131
|
code: 'interrupt-unsafe-operation',
|
|
132
|
+
filePath,
|
|
131
133
|
source: 'interrupt-analysis',
|
|
132
134
|
});
|
|
133
135
|
return;
|
|
@@ -139,6 +141,7 @@ function checkCalleeForUnsafeOp(callee, diagnostics, unsafeOps) {
|
|
|
139
141
|
severity: info.severity,
|
|
140
142
|
message: `${callee} - ${info.reason}`,
|
|
141
143
|
code: 'interrupt-unsafe-operation',
|
|
144
|
+
filePath,
|
|
142
145
|
source: 'interrupt-analysis',
|
|
143
146
|
});
|
|
144
147
|
return;
|
|
@@ -354,6 +357,7 @@ export function inferVolatileForIsrSharedVars(program, diagnostics) {
|
|
|
354
357
|
diagnostics.push({
|
|
355
358
|
severity: 'info',
|
|
356
359
|
message: `'${name}' is written in an interrupt handler and read in main code — emitted as \`volatile\` to prevent the compiler from caching it in a register (the classic ISR/loop race).`,
|
|
360
|
+
filePath: stmt.sourceSpan?.filePath,
|
|
357
361
|
line: stmt.sourceSpan?.startLine,
|
|
358
362
|
column: stmt.sourceSpan?.startColumn,
|
|
359
363
|
code: 'volatile-isr-shared',
|
|
@@ -460,6 +464,7 @@ export function detectReentrancyRisk(program, diagnostics) {
|
|
|
460
464
|
severity: 'warning',
|
|
461
465
|
message: `'${name}' is called from both an interrupt handler and main-thread code. An interrupt firing mid-execution can corrupt the function's local state. Wrap the main-thread call in noInterrupts()/interrupts(), or refactor to avoid sharing the function.`,
|
|
462
466
|
code: 'reentrancy-risk',
|
|
467
|
+
filePath: program.fileName,
|
|
463
468
|
source: 'interrupt-analysis',
|
|
464
469
|
});
|
|
465
470
|
}
|
|
@@ -78,6 +78,7 @@ export function validateMemoryBudget(program, boardConstants) {
|
|
|
78
78
|
: `Stack and heap share the remaining space; deep call chains or heap allocation risk collision.`),
|
|
79
79
|
hint: `Reduce global/static data, move large buffers to PROGMEM (flash), shorten deep call chains, ` +
|
|
80
80
|
`or use a board with more SRAM. See the heap estimate in the diagnostics report for a breakdown.`,
|
|
81
|
+
filePath: program.fileName,
|
|
81
82
|
code: 'memory-budget',
|
|
82
83
|
source: 'memory-budget-validation',
|
|
83
84
|
});
|
|
@@ -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
|
+
}
|
|
@@ -176,6 +176,7 @@ function scanStatementsForByValueMutation(stmts, fnName, byValueParams, diagnost
|
|
|
176
176
|
hint: `Use '${baseName}: Mutable<T>' to pass by mutable reference (T&) instead of a copy.`,
|
|
177
177
|
line: stmt.sourceSpan.startLine,
|
|
178
178
|
column: stmt.sourceSpan.startColumn,
|
|
179
|
+
filePath: stmt.sourceSpan.filePath,
|
|
179
180
|
code: 'ownership-mutate-copy',
|
|
180
181
|
source: 'ownership-analysis',
|
|
181
182
|
});
|
|
@@ -330,6 +331,7 @@ function analyzeStatement(stmt, scope, diagnostics) {
|
|
|
330
331
|
hint: `const ${stmt.name}: Shared = ${initName}; // borrow by reference instead of copying`,
|
|
331
332
|
line: span.startLine,
|
|
332
333
|
column: span.startColumn,
|
|
334
|
+
filePath: span.filePath,
|
|
333
335
|
code: 'ownership-owned-copy',
|
|
334
336
|
source: 'ownership-analysis',
|
|
335
337
|
});
|
|
@@ -346,6 +348,7 @@ function analyzeStatement(stmt, scope, diagnostics) {
|
|
|
346
348
|
hint: `const ${stmt.name}: Shared = ${initName}; // borrow by const reference, zero copy`,
|
|
347
349
|
line: span.startLine,
|
|
348
350
|
column: span.startColumn,
|
|
351
|
+
filePath: span.filePath,
|
|
349
352
|
code: 'ownership-implicit-copy',
|
|
350
353
|
source: 'ownership-analysis',
|
|
351
354
|
});
|
|
@@ -363,6 +366,7 @@ function analyzeStatement(stmt, scope, diagnostics) {
|
|
|
363
366
|
hint: `${storageKw} _tmp: Owned = ...;\nconst ${stmt.name}: ${annotLabel} = _tmp;`,
|
|
364
367
|
line: span.startLine,
|
|
365
368
|
column: span.startColumn,
|
|
369
|
+
filePath: span.filePath,
|
|
366
370
|
code: 'ownership-temp-ref-warn',
|
|
367
371
|
source: 'ownership-analysis',
|
|
368
372
|
});
|
|
@@ -386,6 +390,7 @@ function analyzeStatement(stmt, scope, diagnostics) {
|
|
|
386
390
|
hint: `change '${stmt.target}${typeAnnotation}' → '${stmt.target}: Mutable' // Mutable allows mutation`,
|
|
387
391
|
line: span.startLine,
|
|
388
392
|
column: span.startColumn,
|
|
393
|
+
filePath: span.filePath,
|
|
389
394
|
code: 'ownership-assign-to-ref',
|
|
390
395
|
source: 'ownership-analysis',
|
|
391
396
|
});
|
|
@@ -430,6 +435,7 @@ function analyzeStatement(stmt, scope, diagnostics) {
|
|
|
430
435
|
hint: `Use '${baseName}: Mutable<T>' to pass by mutable reference (T&) instead of a copy.`,
|
|
431
436
|
line: span.startLine,
|
|
432
437
|
column: span.startColumn,
|
|
438
|
+
filePath: span.filePath,
|
|
433
439
|
code: 'ownership-mutate-copy',
|
|
434
440
|
source: 'ownership-analysis',
|
|
435
441
|
});
|
|
@@ -449,6 +455,7 @@ function analyzeStatement(stmt, scope, diagnostics) {
|
|
|
449
455
|
hint: `change '${stmt.target}${typeAnnotation}' → '${stmt.target}: Mutable' // Mutable allows mutation`,
|
|
450
456
|
line: span.startLine,
|
|
451
457
|
column: span.startColumn,
|
|
458
|
+
filePath: span.filePath,
|
|
452
459
|
code: 'ownership-assign-to-ref',
|
|
453
460
|
source: 'ownership-analysis',
|
|
454
461
|
});
|
|
@@ -461,6 +468,7 @@ function analyzeStatement(stmt, scope, diagnostics) {
|
|
|
461
468
|
hint: `const ${stmt.target}_ref: Shared = ${stmt.target}; // add this before the move`,
|
|
462
469
|
line: span.startLine,
|
|
463
470
|
column: span.startColumn,
|
|
471
|
+
filePath: span.filePath,
|
|
464
472
|
code: 'ownership-use-after-move',
|
|
465
473
|
source: 'ownership-analysis',
|
|
466
474
|
});
|
|
@@ -477,6 +485,7 @@ function analyzeStatement(stmt, scope, diagnostics) {
|
|
|
477
485
|
hint: `Use '${upBaseName}: Mutable<T>' to pass by mutable reference (T&) instead of a copy.`,
|
|
478
486
|
line: span.startLine,
|
|
479
487
|
column: span.startColumn,
|
|
488
|
+
filePath: span.filePath,
|
|
480
489
|
code: 'ownership-mutate-copy',
|
|
481
490
|
source: 'ownership-analysis',
|
|
482
491
|
});
|
|
@@ -507,6 +516,7 @@ function analyzeStatement(stmt, scope, diagnostics) {
|
|
|
507
516
|
hint: `return ${retVar.borrowSource} directly as Owned, or change the function to accept '${retVar.borrowSource}: Shared' as a parameter`,
|
|
508
517
|
line: span.startLine,
|
|
509
518
|
column: span.startColumn,
|
|
519
|
+
filePath: span.filePath,
|
|
510
520
|
code: 'ownership-return-local-ref',
|
|
511
521
|
source: 'ownership-analysis',
|
|
512
522
|
});
|
|
@@ -641,6 +651,7 @@ function analyzeExpression(expr, scope, diagnostics, fallbackSpan) {
|
|
|
641
651
|
hint: `const ${name}_ref: Shared = ${name}; // add this before the move`,
|
|
642
652
|
line: span.startLine,
|
|
643
653
|
column: span.startColumn,
|
|
654
|
+
filePath: span.filePath,
|
|
644
655
|
code: 'ownership-use-after-move',
|
|
645
656
|
source: 'ownership-analysis',
|
|
646
657
|
});
|
|
@@ -714,6 +725,7 @@ function analyzeExpression(expr, scope, diagnostics, fallbackSpan) {
|
|
|
714
725
|
hint: `const ${match}_ref: Shared = ${match}; // add this before the move`,
|
|
715
726
|
line: span.startLine,
|
|
716
727
|
column: span.startColumn,
|
|
728
|
+
filePath: span.filePath,
|
|
717
729
|
code: 'ownership-use-after-move',
|
|
718
730
|
source: 'ownership-analysis',
|
|
719
731
|
});
|
|
@@ -908,6 +920,7 @@ function validateConstSuggestions(program, diagnostics) {
|
|
|
908
920
|
message: `'${baseName}' is declared 'const' but its contents are mutated via index assignment — demoted to non-const in C++ so the mutation compiles.`,
|
|
909
921
|
line: constEntry.span.startLine,
|
|
910
922
|
column: constEntry.span.startColumn,
|
|
923
|
+
filePath: constEntry.span.filePath,
|
|
911
924
|
code: 'ownership-const-content-mutated',
|
|
912
925
|
source: 'ownership-analysis',
|
|
913
926
|
});
|
|
@@ -930,6 +943,7 @@ function validateConstSuggestions(program, diagnostics) {
|
|
|
930
943
|
message: `'${baseName}' is declared 'const' but a field is mutated via member assignment — demoted to non-const in C++ so the mutation compiles.`,
|
|
931
944
|
line: constEntry.span.startLine,
|
|
932
945
|
column: constEntry.span.startColumn,
|
|
946
|
+
filePath: constEntry.span.filePath,
|
|
933
947
|
code: 'ownership-const-content-mutated',
|
|
934
948
|
source: 'ownership-analysis',
|
|
935
949
|
});
|
|
@@ -957,6 +971,7 @@ function validateConstSuggestions(program, diagnostics) {
|
|
|
957
971
|
message: `'${baseName}' is declared 'const' but a field is mutated via ++/-- — demoted to non-const in C++ so the mutation compiles.`,
|
|
958
972
|
line: constEntry.span.startLine,
|
|
959
973
|
column: constEntry.span.startColumn,
|
|
974
|
+
filePath: constEntry.span.filePath,
|
|
960
975
|
code: 'ownership-const-content-mutated',
|
|
961
976
|
source: 'ownership-analysis',
|
|
962
977
|
});
|
|
@@ -968,7 +983,20 @@ function validateConstSuggestions(program, diagnostics) {
|
|
|
968
983
|
// keep the binding non-const so push_back/splice/etc. compile. The
|
|
969
984
|
// callee is the lowered C++ name, e.g. "arr.push_back".
|
|
970
985
|
if (stmt.kind === 'call' && typeof stmt.callee === 'string') {
|
|
971
|
-
|
|
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;
|
|
972
1000
|
const dot = callee.lastIndexOf('.');
|
|
973
1001
|
if (dot > 0) {
|
|
974
1002
|
const receiver = callee.slice(0, dot);
|
|
@@ -991,6 +1019,7 @@ function validateConstSuggestions(program, diagnostics) {
|
|
|
991
1019
|
message: `'${receiver}' is declared 'const' but its contents are mutated via .${method}() — demoted to non-const in C++ so the mutation compiles.`,
|
|
992
1020
|
line: constEntry.span.startLine,
|
|
993
1021
|
column: constEntry.span.startColumn,
|
|
1022
|
+
filePath: constEntry.span.filePath,
|
|
994
1023
|
code: 'ownership-const-content-mutated',
|
|
995
1024
|
source: 'ownership-analysis',
|
|
996
1025
|
});
|
|
@@ -1023,6 +1052,7 @@ function validateConstSuggestions(program, diagnostics) {
|
|
|
1023
1052
|
message: `'${entry.name}' is never reassigned — emitted as \`const\` so the C++ compiler can place it in ROM and fold it.`,
|
|
1024
1053
|
line: entry.span.startLine,
|
|
1025
1054
|
column: entry.span.startColumn,
|
|
1055
|
+
filePath: entry.span.filePath,
|
|
1026
1056
|
code: 'ownership-suggest-const',
|
|
1027
1057
|
source: 'ownership-analysis',
|
|
1028
1058
|
});
|
|
@@ -1072,6 +1102,7 @@ function checkBorrowMismatch(program, diagnostics) {
|
|
|
1072
1102
|
hint: `change '${arg.value}: Shared = ...' → '${arg.value}: Mutable = ...'`,
|
|
1073
1103
|
line: stmt.sourceSpan.startLine,
|
|
1074
1104
|
column: stmt.sourceSpan.startColumn,
|
|
1105
|
+
filePath: stmt.sourceSpan.filePath,
|
|
1075
1106
|
code: 'ownership-borrow-mismatch',
|
|
1076
1107
|
source: 'ownership-analysis',
|
|
1077
1108
|
});
|
|
@@ -1127,6 +1158,7 @@ function checkDanglingBorrowsOnScopeExit(exitingScope, diagnostics, span) {
|
|
|
1127
1158
|
hint: `move '${v.borrowSource}' to the outer scope, or ensure '${v.name}' does not outlive it`,
|
|
1128
1159
|
line: span.startLine,
|
|
1129
1160
|
column: span.startColumn,
|
|
1161
|
+
filePath: span.filePath,
|
|
1130
1162
|
code: 'ownership-dangling-borrow',
|
|
1131
1163
|
source: 'ownership-analysis',
|
|
1132
1164
|
});
|
|
@@ -32,6 +32,7 @@ export function validatePeripheralOwnership(program) {
|
|
|
32
32
|
severity: 'error',
|
|
33
33
|
message: `Double-take: '${bus}' is already owned.`,
|
|
34
34
|
hint: `Call '${bus}.release()' before taking it again.`,
|
|
35
|
+
filePath: span.filePath,
|
|
35
36
|
line: span.startLine,
|
|
36
37
|
column: span.startColumn,
|
|
37
38
|
code: 'hal-ownership-double-take',
|
|
@@ -79,6 +80,7 @@ export function validatePeripheralOwnership(program) {
|
|
|
79
80
|
severity: 'error',
|
|
80
81
|
message: `Double-take: '${bus}' is already owned.`,
|
|
81
82
|
hint: `Call '${bus}.release()' before taking it again.`,
|
|
83
|
+
filePath: span.filePath,
|
|
82
84
|
line: span.startLine,
|
|
83
85
|
column: span.startColumn,
|
|
84
86
|
code: 'hal-ownership-double-take',
|
|
@@ -94,6 +96,7 @@ export function validatePeripheralOwnership(program) {
|
|
|
94
96
|
severity: 'warning',
|
|
95
97
|
message: `'${bus}.release()' called but bus was not taken.`,
|
|
96
98
|
hint: `Ensure you call '${bus}.take()' before releasing.`,
|
|
99
|
+
filePath: span.filePath,
|
|
97
100
|
line: span.startLine,
|
|
98
101
|
column: span.startColumn,
|
|
99
102
|
code: 'hal-ownership-unowned-release',
|
|
@@ -108,6 +111,7 @@ export function validatePeripheralOwnership(program) {
|
|
|
108
111
|
severity: 'error',
|
|
109
112
|
message: `Unowned access: '${bus}' is used without being taken.`,
|
|
110
113
|
hint: `Call 'const bus = ${bus}.take();' and use the returned 'bus' object, or ensure '${bus}' is taken in the current scope.`,
|
|
114
|
+
filePath: span.filePath,
|
|
111
115
|
line: span.startLine,
|
|
112
116
|
column: span.startColumn,
|
|
113
117
|
code: 'hal-ownership-unowned-access',
|
|
@@ -142,6 +146,7 @@ export function validatePeripheralOwnership(program) {
|
|
|
142
146
|
severity: 'warning',
|
|
143
147
|
message: `'${bus}' was taken but never released.`,
|
|
144
148
|
hint: `Call '${bus}.release()' when finished with the bus.`,
|
|
149
|
+
filePath: busSpan.filePath,
|
|
145
150
|
line: busSpan.startLine,
|
|
146
151
|
column: busSpan.startColumn,
|
|
147
152
|
code: 'hal-ownership-leak',
|
|
@@ -5,4 +5,4 @@ import { Diagnostic } from '../types.js';
|
|
|
5
5
|
* Validate peripheral usage and return diagnostics.
|
|
6
6
|
* This is the main entry point for peripheral validation.
|
|
7
7
|
*/
|
|
8
|
-
export declare function validatePeripherals(usage: PeripheralUsage, boardConstants: BoardConstants | undefined): Diagnostic[];
|
|
8
|
+
export declare function validatePeripherals(usage: PeripheralUsage, boardConstants: BoardConstants | undefined, filePath: string): Diagnostic[];
|
|
@@ -26,7 +26,7 @@ function getPeripheralCapacity(boardConstants) {
|
|
|
26
26
|
* Validate peripheral usage against board capacity.
|
|
27
27
|
* Returns an array of diagnostics for any invalid peripheral usage.
|
|
28
28
|
*/
|
|
29
|
-
function validatePeripheralUsage(usage, capacity, boardName
|
|
29
|
+
function validatePeripheralUsage(usage, capacity, boardName, filePath) {
|
|
30
30
|
const diagnostics = [];
|
|
31
31
|
// Validate I2C instances
|
|
32
32
|
for (const instance of usage.i2cInstancesUsed) {
|
|
@@ -36,6 +36,7 @@ function validatePeripheralUsage(usage, capacity, boardName = 'this board') {
|
|
|
36
36
|
severity: 'error',
|
|
37
37
|
message: `I2C${instance} is not available on ${boardName}. Available: ${availableName}`,
|
|
38
38
|
code: 'peripheral-not-available',
|
|
39
|
+
filePath,
|
|
39
40
|
source: 'peripheral-validation',
|
|
40
41
|
});
|
|
41
42
|
}
|
|
@@ -48,6 +49,7 @@ function validatePeripheralUsage(usage, capacity, boardName = 'this board') {
|
|
|
48
49
|
severity: 'error',
|
|
49
50
|
message: `SPI${instance} is not available on ${boardName}. Available: ${availableName}`,
|
|
50
51
|
code: 'peripheral-not-available',
|
|
52
|
+
filePath,
|
|
51
53
|
source: 'peripheral-validation',
|
|
52
54
|
});
|
|
53
55
|
}
|
|
@@ -60,6 +62,7 @@ function validatePeripheralUsage(usage, capacity, boardName = 'this board') {
|
|
|
60
62
|
severity: 'error',
|
|
61
63
|
message: `UART${instance} is not available on ${boardName}. Available: ${availableName}`,
|
|
62
64
|
code: 'peripheral-not-available',
|
|
65
|
+
filePath,
|
|
63
66
|
source: 'peripheral-validation',
|
|
64
67
|
});
|
|
65
68
|
}
|
|
@@ -70,8 +73,8 @@ function validatePeripheralUsage(usage, capacity, boardName = 'this board') {
|
|
|
70
73
|
* Validate peripheral usage and return diagnostics.
|
|
71
74
|
* This is the main entry point for peripheral validation.
|
|
72
75
|
*/
|
|
73
|
-
export function validatePeripherals(usage, boardConstants) {
|
|
76
|
+
export function validatePeripherals(usage, boardConstants, filePath) {
|
|
74
77
|
const capacity = getPeripheralCapacity(boardConstants);
|
|
75
78
|
const boardName = boardConstants?.get('name') ?? 'this board';
|
|
76
|
-
return validatePeripheralUsage(usage, capacity, boardName);
|
|
79
|
+
return validatePeripheralUsage(usage, capacity, boardName, filePath);
|
|
77
80
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { Diagnostic } from '../types.js';
|
|
2
2
|
import type { BoardConstants } from './board-resolver.js';
|
|
3
3
|
import type { PeripheralUsage } from './peripheral-usage.js';
|
|
4
|
-
export declare function validatePinAliasConflicts(usage: PeripheralUsage, boardConstants: BoardConstants | undefined): Diagnostic[];
|
|
4
|
+
export declare function validatePinAliasConflicts(usage: PeripheralUsage, boardConstants: BoardConstants | undefined, filePath: string): Diagnostic[];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { findBoardPinByName } from './board-pin-utils.js';
|
|
2
|
-
export function validatePinAliasConflicts(usage, boardConstants) {
|
|
2
|
+
export function validatePinAliasConflicts(usage, boardConstants, filePath) {
|
|
3
3
|
if (!boardConstants || usage.pinsUsed.size === 0) {
|
|
4
4
|
return [];
|
|
5
5
|
}
|
|
@@ -26,6 +26,7 @@ export function validatePinAliasConflicts(usage, boardConstants) {
|
|
|
26
26
|
diagnostics.push({
|
|
27
27
|
severity: 'warning',
|
|
28
28
|
code: 'pin-alias-conflict',
|
|
29
|
+
filePath,
|
|
29
30
|
source: 'pin-alias-conflict',
|
|
30
31
|
message: `Pin '${canonicalPinName}' is referenced through multiple names${boardLabel}: ${orderedNames.join(', ')}. These names refer to the same physical pin. Pick one name to keep pin usage and diagnostics unambiguous.`,
|
|
31
32
|
});
|