@typecad/cuttlefish 1.0.0-alpha.3 → 1.0.0-alpha.6
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/hal-op-ir.d.ts +10 -1
- package/dist/api/shared/types.d.ts +8 -0
- package/dist/cli-utils.d.ts +1 -0
- package/dist/cli-utils.js +3 -1
- package/dist/cli.js +66 -0
- package/dist/create/index.d.ts +1 -1
- package/dist/create/index.js +1 -1
- package/dist/create/init-scaffold.js +26 -2
- package/dist/create/init-templates.d.ts +2 -0
- package/dist/create/init-templates.js +189 -9
- package/dist/emit/emitters/setup.js +115 -0
- package/dist/ir/adc-range-validation.js +26 -25
- package/dist/ir/expression-to-ir.js +43 -6
- package/dist/ir/hal/hal-plugins.js +10 -0
- package/dist/ir/interrupt-analysis.js +8 -3
- package/dist/ir/memory-budget-validation.js +1 -0
- package/dist/ir/ownership-analysis.js +19 -0
- 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 +16 -0
- package/dist/ir/program-analysis.js +113 -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.js +1 -0
- package/dist/ir/transformers/variables.js +46 -17
- 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 +6 -6
- package/dist/licenses.d.ts +185 -0
- package/dist/licenses.js +963 -0
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +1 -1
- package/dist/transpile.js +6 -5
- package/dist/types.d.ts +5 -1
- package/dist/utils/cli.js +44 -0
- package/package.json +5 -4
- package/dist/ir/heap-array-validation.d.ts +0 -24
- package/dist/ir/heap-array-validation.js +0 -29
|
@@ -59,7 +59,7 @@ function extractNumericValue(expr) {
|
|
|
59
59
|
/**
|
|
60
60
|
* Check binary comparison expressions for ADC range issues.
|
|
61
61
|
*/
|
|
62
|
-
function checkComparisonForADCRange(left, operator, right, adcConfig, diagnostics) {
|
|
62
|
+
function checkComparisonForADCRange(left, operator, right, adcConfig, filePath, diagnostics) {
|
|
63
63
|
// Check if one side is an analog read and the other is a literal
|
|
64
64
|
let analogReadSide = null;
|
|
65
65
|
let literalValue = null;
|
|
@@ -83,6 +83,7 @@ function checkComparisonForADCRange(left, operator, right, adcConfig, diagnostic
|
|
|
83
83
|
severity: 'info',
|
|
84
84
|
message: `Comparison ${comparisonDesc} may never be true. ADC resolution is ${adcConfig.resolution}-bit (max ${adcConfig.maxValue}) on this board.`,
|
|
85
85
|
code: 'adc-range-warning',
|
|
86
|
+
filePath,
|
|
86
87
|
source: 'adc-range-validation',
|
|
87
88
|
});
|
|
88
89
|
}
|
|
@@ -90,7 +91,7 @@ function checkComparisonForADCRange(left, operator, right, adcConfig, diagnostic
|
|
|
90
91
|
/**
|
|
91
92
|
* Scan an expression for ADC range issues.
|
|
92
93
|
*/
|
|
93
|
-
function scanExpressionForADCRange(expr, adcConfig, diagnostics) {
|
|
94
|
+
function scanExpressionForADCRange(expr, adcConfig, filePath, diagnostics) {
|
|
94
95
|
if (!expr || typeof expr !== 'object')
|
|
95
96
|
return;
|
|
96
97
|
// Check binary comparisons
|
|
@@ -98,59 +99,59 @@ function scanExpressionForADCRange(expr, adcConfig, diagnostics) {
|
|
|
98
99
|
const bin = expr;
|
|
99
100
|
const comparisonOps = ['>', '>=', '<', '<=', '===', '==', '!==', '!='];
|
|
100
101
|
if (comparisonOps.includes(bin.operator)) {
|
|
101
|
-
checkComparisonForADCRange(bin.left, bin.operator, bin.right, adcConfig, diagnostics);
|
|
102
|
+
checkComparisonForADCRange(bin.left, bin.operator, bin.right, adcConfig, filePath, diagnostics);
|
|
102
103
|
}
|
|
103
104
|
// Recursively scan both sides
|
|
104
|
-
scanExpressionForADCRange(bin.left, adcConfig, diagnostics);
|
|
105
|
-
scanExpressionForADCRange(bin.right, adcConfig, diagnostics);
|
|
105
|
+
scanExpressionForADCRange(bin.left, adcConfig, filePath, diagnostics);
|
|
106
|
+
scanExpressionForADCRange(bin.right, adcConfig, filePath, diagnostics);
|
|
106
107
|
}
|
|
107
108
|
// Check ternary conditions
|
|
108
109
|
if (expr.kind === 'ternary') {
|
|
109
110
|
const ternary = expr;
|
|
110
|
-
scanExpressionForADCRange(ternary.condition, adcConfig, diagnostics);
|
|
111
|
-
scanExpressionForADCRange(ternary.whenTrue, adcConfig, diagnostics);
|
|
112
|
-
scanExpressionForADCRange(ternary.whenFalse, adcConfig, diagnostics);
|
|
111
|
+
scanExpressionForADCRange(ternary.condition, adcConfig, filePath, diagnostics);
|
|
112
|
+
scanExpressionForADCRange(ternary.whenTrue, adcConfig, filePath, diagnostics);
|
|
113
|
+
scanExpressionForADCRange(ternary.whenFalse, adcConfig, filePath, diagnostics);
|
|
113
114
|
}
|
|
114
115
|
// Check property access
|
|
115
116
|
if (expr.kind === 'property-access') {
|
|
116
117
|
const pa = expr;
|
|
117
|
-
scanExpressionForADCRange(pa.object, adcConfig, diagnostics);
|
|
118
|
+
scanExpressionForADCRange(pa.object, adcConfig, filePath, diagnostics);
|
|
118
119
|
}
|
|
119
120
|
}
|
|
120
121
|
/**
|
|
121
122
|
* Scan a statement for ADC range issues.
|
|
122
123
|
*/
|
|
123
|
-
function scanStatementForADCRange(stmt, adcConfig, diagnostics) {
|
|
124
|
+
function scanStatementForADCRange(stmt, adcConfig, filePath, diagnostics) {
|
|
124
125
|
if (!stmt || typeof stmt !== 'object')
|
|
125
126
|
return;
|
|
126
127
|
switch (stmt.kind) {
|
|
127
128
|
case 'var_decl': {
|
|
128
129
|
const varDecl = stmt;
|
|
129
130
|
if (varDecl.initializer) {
|
|
130
|
-
scanExpressionForADCRange(varDecl.initializer, adcConfig, diagnostics);
|
|
131
|
+
scanExpressionForADCRange(varDecl.initializer, adcConfig, filePath, diagnostics);
|
|
131
132
|
}
|
|
132
133
|
break;
|
|
133
134
|
}
|
|
134
135
|
case 'assign': {
|
|
135
136
|
const assign = stmt;
|
|
136
137
|
if (assign.value) {
|
|
137
|
-
scanExpressionForADCRange(assign.value, adcConfig, diagnostics);
|
|
138
|
+
scanExpressionForADCRange(assign.value, adcConfig, filePath, diagnostics);
|
|
138
139
|
}
|
|
139
140
|
break;
|
|
140
141
|
}
|
|
141
142
|
case 'if': {
|
|
142
143
|
const ifStmt = stmt;
|
|
143
144
|
if (ifStmt.condition) {
|
|
144
|
-
scanExpressionForADCRange(ifStmt.condition, adcConfig, diagnostics);
|
|
145
|
+
scanExpressionForADCRange(ifStmt.condition, adcConfig, filePath, diagnostics);
|
|
145
146
|
}
|
|
146
147
|
if (ifStmt.thenBranch) {
|
|
147
148
|
for (const s of ifStmt.thenBranch) {
|
|
148
|
-
scanStatementForADCRange(s, adcConfig, diagnostics);
|
|
149
|
+
scanStatementForADCRange(s, adcConfig, filePath, diagnostics);
|
|
149
150
|
}
|
|
150
151
|
}
|
|
151
152
|
if (ifStmt.elseBranch) {
|
|
152
153
|
for (const s of ifStmt.elseBranch) {
|
|
153
|
-
scanStatementForADCRange(s, adcConfig, diagnostics);
|
|
154
|
+
scanStatementForADCRange(s, adcConfig, filePath, diagnostics);
|
|
154
155
|
}
|
|
155
156
|
}
|
|
156
157
|
break;
|
|
@@ -158,11 +159,11 @@ function scanStatementForADCRange(stmt, adcConfig, diagnostics) {
|
|
|
158
159
|
case 'while': {
|
|
159
160
|
const whileStmt = stmt;
|
|
160
161
|
if (whileStmt.condition) {
|
|
161
|
-
scanExpressionForADCRange(whileStmt.condition, adcConfig, diagnostics);
|
|
162
|
+
scanExpressionForADCRange(whileStmt.condition, adcConfig, filePath, diagnostics);
|
|
162
163
|
}
|
|
163
164
|
if (whileStmt.body) {
|
|
164
165
|
for (const s of whileStmt.body) {
|
|
165
|
-
scanStatementForADCRange(s, adcConfig, diagnostics);
|
|
166
|
+
scanStatementForADCRange(s, adcConfig, filePath, diagnostics);
|
|
166
167
|
}
|
|
167
168
|
}
|
|
168
169
|
break;
|
|
@@ -170,11 +171,11 @@ function scanStatementForADCRange(stmt, adcConfig, diagnostics) {
|
|
|
170
171
|
case 'for': {
|
|
171
172
|
const forStmt = stmt;
|
|
172
173
|
if (forStmt.condition) {
|
|
173
|
-
scanExpressionForADCRange(forStmt.condition, adcConfig, diagnostics);
|
|
174
|
+
scanExpressionForADCRange(forStmt.condition, adcConfig, filePath, diagnostics);
|
|
174
175
|
}
|
|
175
176
|
if (forStmt.body) {
|
|
176
177
|
for (const s of forStmt.body) {
|
|
177
|
-
scanStatementForADCRange(s, adcConfig, diagnostics);
|
|
178
|
+
scanStatementForADCRange(s, adcConfig, filePath, diagnostics);
|
|
178
179
|
}
|
|
179
180
|
}
|
|
180
181
|
break;
|
|
@@ -182,7 +183,7 @@ function scanStatementForADCRange(stmt, adcConfig, diagnostics) {
|
|
|
182
183
|
case 'return': {
|
|
183
184
|
const retStmt = stmt;
|
|
184
185
|
if (retStmt.value) {
|
|
185
|
-
scanExpressionForADCRange(retStmt.value, adcConfig, diagnostics);
|
|
186
|
+
scanExpressionForADCRange(retStmt.value, adcConfig, filePath, diagnostics);
|
|
186
187
|
}
|
|
187
188
|
break;
|
|
188
189
|
}
|
|
@@ -190,7 +191,7 @@ function scanStatementForADCRange(stmt, adcConfig, diagnostics) {
|
|
|
190
191
|
const call = stmt;
|
|
191
192
|
if (call.args) {
|
|
192
193
|
for (const arg of call.args) {
|
|
193
|
-
scanExpressionForADCRange(arg, adcConfig, diagnostics);
|
|
194
|
+
scanExpressionForADCRange(arg, adcConfig, filePath, diagnostics);
|
|
194
195
|
}
|
|
195
196
|
}
|
|
196
197
|
break;
|
|
@@ -214,7 +215,7 @@ export function validateADCRange(program, boardConstants) {
|
|
|
214
215
|
// Scan top-level statements
|
|
215
216
|
if (program.topLevelStatements) {
|
|
216
217
|
for (const stmt of program.topLevelStatements) {
|
|
217
|
-
scanStatementForADCRange(stmt, adcConfig, diagnostics);
|
|
218
|
+
scanStatementForADCRange(stmt, adcConfig, program.fileName, diagnostics);
|
|
218
219
|
}
|
|
219
220
|
}
|
|
220
221
|
// Scan function bodies
|
|
@@ -222,7 +223,7 @@ export function validateADCRange(program, boardConstants) {
|
|
|
222
223
|
for (const fn of program.functions) {
|
|
223
224
|
if (fn.statements) {
|
|
224
225
|
for (const stmt of fn.statements) {
|
|
225
|
-
scanStatementForADCRange(stmt, adcConfig, diagnostics);
|
|
226
|
+
scanStatementForADCRange(stmt, adcConfig, program.fileName, diagnostics);
|
|
226
227
|
}
|
|
227
228
|
}
|
|
228
229
|
}
|
|
@@ -234,14 +235,14 @@ export function validateADCRange(program, boardConstants) {
|
|
|
234
235
|
for (const method of cls.methods) {
|
|
235
236
|
if (method.statements) {
|
|
236
237
|
for (const stmt of method.statements) {
|
|
237
|
-
scanStatementForADCRange(stmt, adcConfig, diagnostics);
|
|
238
|
+
scanStatementForADCRange(stmt, adcConfig, program.fileName, diagnostics);
|
|
238
239
|
}
|
|
239
240
|
}
|
|
240
241
|
}
|
|
241
242
|
}
|
|
242
243
|
if (cls.constructor?.statements) {
|
|
243
244
|
for (const stmt of cls.constructor.statements) {
|
|
244
|
-
scanStatementForADCRange(stmt, adcConfig, diagnostics);
|
|
245
|
+
scanStatementForADCRange(stmt, adcConfig, program.fileName, diagnostics);
|
|
245
246
|
}
|
|
246
247
|
}
|
|
247
248
|
}
|
|
@@ -264,15 +264,52 @@ export function expressionToIR(expr, sourceText, diagnostics, pointerVars = new
|
|
|
264
264
|
return `(sizeof(${safeText}) / sizeof(${safeText}[0]))`;
|
|
265
265
|
}
|
|
266
266
|
if (ts.isCallExpression(receiverNode)) {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
267
|
+
// Case B: a HAL buffer-returning method (I2CDevice.readBytes /
|
|
268
|
+
// SPIDevice.readRegister) used INLINE as a sub-expression — e.g.
|
|
269
|
+
// `dev.readBytes(0,6).length`. These methods lower to STATEMENTS (a fill
|
|
270
|
+
// loop), not a single C++ expression, so by the time we get here the
|
|
271
|
+
// receiver text is already a leaked `for (...) __buf[__i] = Wire.read()`
|
|
272
|
+
// statement spliced where an expression is required. The buffer also has
|
|
273
|
+
// no caller-side name to sizeof. Only the var-init form is supported
|
|
274
|
+
// (`const data = dev.readBytes(...)`); emit a clear diagnostic so the
|
|
275
|
+
// user gets an actionable error instead of inscrutable broken C++.
|
|
276
|
+
if (/\bfor\s*\(/.test(safeText) || /__buf|__spi_buf/.test(safeText)) {
|
|
277
|
+
const calleeName = ts.isPropertyAccessExpression(receiverNode.expression)
|
|
278
|
+
? receiverNode.expression.name.text : "call";
|
|
279
|
+
diagnostics.push(makeDiagnostic(sourceText, receiverNode.getStart(), `\`${calleeName}(...)\` returns a buffer and cannot be queried inline. Capture it into a variable first (e.g. \`const data = ${calleeName}(...)\`), then use \`data.length\`.`, "error", "TC_BUFFER_INLINE_LENGTH"));
|
|
280
|
+
return `0 /* ${calleeName}() result must be captured into a variable to use .length */`;
|
|
281
|
+
}
|
|
282
|
+
// Resolve the call's RETURN type (not the receiver object's type, which
|
|
283
|
+
// the previous code incorrectly did). Method calls resolve via the class
|
|
284
|
+
// registry; free-function calls resolve by scanning the source AST for
|
|
285
|
+
// the declared return type.
|
|
286
|
+
const returnType = resolveExprCppType(receiverNode)
|
|
287
|
+
?? resolveCallReturnTypeForNullGuard(receiverNode, sourceText);
|
|
288
|
+
if (returnType) {
|
|
289
|
+
const parsed = parseCppType(returnType);
|
|
290
|
+
// std::string → .length(); any STL container (vector/map/set) → .size().
|
|
291
|
+
if (parsedIsStdString(returnType)) {
|
|
292
|
+
return `static_cast<long long>(${safeText}.length())`;
|
|
293
|
+
}
|
|
294
|
+
if (parsedIsVector(returnType) || parsedIsMap(returnType) || parsedIsSet(returnType) || isContainer(parsed)) {
|
|
295
|
+
return `static_cast<long long>(${safeText}.size())`;
|
|
296
|
+
}
|
|
297
|
+
// A raw pointer / decayed-array return (e.g. uint8_t*) carries no size
|
|
298
|
+
// at the call site — sizeof would yield sizeof(pointer). This is
|
|
299
|
+
// genuinely un-sizeable inline; surface it rather than emit wrong code.
|
|
300
|
+
if (parsedIsPointer(returnType) || /\]\s*$/.test(returnType)) {
|
|
301
|
+
const calleeName = ts.isPropertyAccessExpression(receiverNode.expression)
|
|
302
|
+
? receiverNode.expression.name.text
|
|
303
|
+
: (ts.isIdentifier(receiverNode.expression) ? receiverNode.expression.text : "call");
|
|
304
|
+
diagnostics.push(makeDiagnostic(sourceText, receiverNode.getStart(), `Cannot use \`.length\` on \`${calleeName}()\` which returns a pointer (${returnType}); the size is not available at the call site. Capture the buffer into a variable first.`, "error", "TC_LENGTH_ON_POINTER_RETURN"));
|
|
305
|
+
return `0 /* .length unavailable on ${returnType} return */`;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
272
308
|
// Cast .size() to long long to match the loop-counter type (TS number ->
|
|
273
309
|
// long long). Without this, `i < vec.size()` compares long long vs
|
|
274
310
|
// size_t (unsigned) and g++ -Wall warns -Wsign-compare on every
|
|
275
|
-
// indexed loop over an array.
|
|
311
|
+
// indexed loop over an array. (Fallback for unresolvable return types —
|
|
312
|
+
// historically every call-result .length landed here.)
|
|
276
313
|
return `static_cast<long long>(${safeText}.size())`;
|
|
277
314
|
}
|
|
278
315
|
// Resolve by concrete cppType first so std::string vars render member calls
|
|
@@ -580,6 +580,16 @@ export function tryResolveSemanticCall(fnName, args, instance, paramNames, callA
|
|
|
580
580
|
return null;
|
|
581
581
|
return { operation: "spi.set_bit_order", bus, order };
|
|
582
582
|
}
|
|
583
|
+
case "spiReadBuffer": {
|
|
584
|
+
const bus = resolveSemanticArg(args, 0, instance, paramNames, callArgTexts, paramDefaults);
|
|
585
|
+
const count = resolveNumericOrExpression(args, 1, instance, paramNames, callArgTexts, paramDefaults);
|
|
586
|
+
if (bus === null || count === null)
|
|
587
|
+
return null;
|
|
588
|
+
// The buffer arg (args[2]) is only a placeholder for type resolution;
|
|
589
|
+
// the real target is the caller's variable, rewritten from
|
|
590
|
+
// __HAL_READ_BUF__ by replaceHalReadBufferPlaceholder.
|
|
591
|
+
return { operation: "spi.read_buffer", bus, count, buffer: "__HAL_READ_BUF__" };
|
|
592
|
+
}
|
|
583
593
|
// ── UART ──
|
|
584
594
|
case "uartBegin": {
|
|
585
595
|
const port = resolveSemanticArg(args, 0, instance, paramNames, callArgTexts, paramDefaults);
|
|
@@ -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
|
});
|
|
@@ -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
|
});
|
|
@@ -991,6 +1006,7 @@ function validateConstSuggestions(program, diagnostics) {
|
|
|
991
1006
|
message: `'${receiver}' is declared 'const' but its contents are mutated via .${method}() — demoted to non-const in C++ so the mutation compiles.`,
|
|
992
1007
|
line: constEntry.span.startLine,
|
|
993
1008
|
column: constEntry.span.startColumn,
|
|
1009
|
+
filePath: constEntry.span.filePath,
|
|
994
1010
|
code: 'ownership-const-content-mutated',
|
|
995
1011
|
source: 'ownership-analysis',
|
|
996
1012
|
});
|
|
@@ -1023,6 +1039,7 @@ function validateConstSuggestions(program, diagnostics) {
|
|
|
1023
1039
|
message: `'${entry.name}' is never reassigned — emitted as \`const\` so the C++ compiler can place it in ROM and fold it.`,
|
|
1024
1040
|
line: entry.span.startLine,
|
|
1025
1041
|
column: entry.span.startColumn,
|
|
1042
|
+
filePath: entry.span.filePath,
|
|
1026
1043
|
code: 'ownership-suggest-const',
|
|
1027
1044
|
source: 'ownership-analysis',
|
|
1028
1045
|
});
|
|
@@ -1072,6 +1089,7 @@ function checkBorrowMismatch(program, diagnostics) {
|
|
|
1072
1089
|
hint: `change '${arg.value}: Shared = ...' → '${arg.value}: Mutable = ...'`,
|
|
1073
1090
|
line: stmt.sourceSpan.startLine,
|
|
1074
1091
|
column: stmt.sourceSpan.startColumn,
|
|
1092
|
+
filePath: stmt.sourceSpan.filePath,
|
|
1075
1093
|
code: 'ownership-borrow-mismatch',
|
|
1076
1094
|
source: 'ownership-analysis',
|
|
1077
1095
|
});
|
|
@@ -1127,6 +1145,7 @@ function checkDanglingBorrowsOnScopeExit(exitingScope, diagnostics, span) {
|
|
|
1127
1145
|
hint: `move '${v.borrowSource}' to the outer scope, or ensure '${v.name}' does not outlive it`,
|
|
1128
1146
|
line: span.startLine,
|
|
1129
1147
|
column: span.startColumn,
|
|
1148
|
+
filePath: span.filePath,
|
|
1130
1149
|
code: 'ownership-dangling-borrow',
|
|
1131
1150
|
source: 'ownership-analysis',
|
|
1132
1151
|
});
|
|
@@ -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
|
});
|