@typecad/cuttlefish 0.1.0-alpha.2 → 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.
Files changed (54) hide show
  1. package/README.md +10 -10
  2. package/dist/api/board-types.d.ts +1 -1
  3. package/dist/api/shared/hal-op-ir.d.ts +10 -1
  4. package/dist/api/shared/platform-strategy.d.ts +6 -0
  5. package/dist/api/shared/types.d.ts +8 -0
  6. package/dist/cli-utils.d.ts +1 -0
  7. package/dist/cli-utils.js +3 -1
  8. package/dist/cli.js +75 -0
  9. package/dist/create/index.d.ts +1 -1
  10. package/dist/create/index.js +1 -1
  11. package/dist/create/init-scaffold.js +48 -2
  12. package/dist/create/init-templates.d.ts +2 -0
  13. package/dist/create/init-templates.js +189 -9
  14. package/dist/emit/emitters/setup.js +122 -1
  15. package/dist/ir/adc-range-validation.js +26 -25
  16. package/dist/ir/expression-to-ir.js +43 -6
  17. package/dist/ir/hal/hal-plugins.js +10 -0
  18. package/dist/ir/interrupt-analysis.js +8 -3
  19. package/dist/ir/memory-budget-validation.js +1 -0
  20. package/dist/ir/ownership-analysis.js +19 -0
  21. package/dist/ir/peripheral-ownership.js +5 -0
  22. package/dist/ir/peripheral-validation.d.ts +1 -1
  23. package/dist/ir/peripheral-validation.js +6 -3
  24. package/dist/ir/pin-alias-conflict.d.ts +1 -1
  25. package/dist/ir/pin-alias-conflict.js +2 -1
  26. package/dist/ir/pin-capability-validation.js +34 -32
  27. package/dist/ir/pin-mode-validation.js +5 -0
  28. package/dist/ir/pin-safety.d.ts +1 -1
  29. package/dist/ir/pin-safety.js +2 -1
  30. package/dist/ir/program-analysis.d.ts +16 -0
  31. package/dist/ir/program-analysis.js +113 -0
  32. package/dist/ir/pulldown-validation.d.ts +1 -1
  33. package/dist/ir/pulldown-validation.js +2 -1
  34. package/dist/ir/pwm-timer-sharing.d.ts +1 -1
  35. package/dist/ir/pwm-timer-sharing.js +2 -1
  36. package/dist/ir/resource-analysis.js +2 -0
  37. package/dist/ir/timer0-pwm-timing-conflict.d.ts +1 -1
  38. package/dist/ir/timer0-pwm-timing-conflict.js +2 -1
  39. package/dist/ir/timing-validation.js +1 -0
  40. package/dist/ir/transformers/variables.js +46 -17
  41. package/dist/ir/try-catch-validation.js +2 -0
  42. package/dist/ir/type-resolution.js +2 -2
  43. package/dist/ir/unit-suspicion-validation.js +9 -7
  44. package/dist/ir/validation-orchestrator.js +6 -6
  45. package/dist/licenses.d.ts +185 -0
  46. package/dist/licenses.js +963 -0
  47. package/dist/testing.d.ts +1 -1
  48. package/dist/testing.js +1 -1
  49. package/dist/transpile.js +17 -9
  50. package/dist/types.d.ts +7 -1
  51. package/dist/utils/cli.js +44 -0
  52. package/package.json +5 -4
  53. package/dist/ir/heap-array-validation.d.ts +0 -24
  54. package/dist/ir/heap-array-validation.js +0 -29
@@ -31,6 +31,80 @@ function filterShimBlock(lines, startMarker, endMarker) {
31
31
  }
32
32
  return filtered;
33
33
  }
34
+ /**
35
+ * Detect whether a program uses the watchdog timer. The HAL resolver lowers
36
+ * `WDT.enable/reset/disable` calls to structured `wdt.*` hal-ops, which the
37
+ * setup emitter then renders as bare `wdt_enable/wdt_reset/wdt_disable` calls
38
+ * (or constant-folded `wdt_enable(WDTO_*)` macros). All of those require
39
+ * `<avr/wdt.h>`, so the include is gated on this check instead of being forced
40
+ * into every AVR program.
41
+ *
42
+ * `programAnalysis.usesWDT` is NOT used here: it scans `WDT.`-prefixed
43
+ * callees and raw-code hal-ops, but the structured `wdt.*` ops carry a typed
44
+ * operation name with no raw code, so the analysis misses them.
45
+ *
46
+ * The recursion mirrors `collectStatementIdentifiers` (identifier-collector.ts)
47
+ * — the canonical IR walker — so every compound statement shape is covered.
48
+ */
49
+ function programUsesWdt(program) {
50
+ const visitStatement = (stmt) => {
51
+ if (stmt.kind === "hal-op") {
52
+ const opName = stmt.operation?.operation;
53
+ if (typeof opName === "string" && opName.startsWith("wdt."))
54
+ return true;
55
+ }
56
+ switch (stmt.kind) {
57
+ case "block":
58
+ case "labeled":
59
+ return visit(stmt.body);
60
+ case "if":
61
+ return visit(stmt.thenBranch) || visit(stmt.elseBranch ?? []);
62
+ case "for":
63
+ return visit(stmt.body)
64
+ || (stmt.initializer ? visitStatement(stmt.initializer) : false);
65
+ case "while":
66
+ case "do_while":
67
+ case "for_of":
68
+ case "for_in":
69
+ return visit(stmt.body);
70
+ case "switch":
71
+ return stmt.cases.some((c) => visit(c.body ?? []));
72
+ case "try":
73
+ return visit(stmt.tryBlock) || visit(stmt.catchBlock ?? []) || visit(stmt.finallyBlock ?? []);
74
+ default:
75
+ return false;
76
+ }
77
+ };
78
+ const visit = (statements) => {
79
+ if (!statements)
80
+ return false;
81
+ for (const stmt of statements) {
82
+ if (visitStatement(stmt))
83
+ return true;
84
+ }
85
+ return false;
86
+ };
87
+ if (visit(program.topLevelStatements))
88
+ return true;
89
+ for (const fn of program.functions) {
90
+ if (visit(fn.statements))
91
+ return true;
92
+ }
93
+ for (const cls of program.classes) {
94
+ for (const m of cls.methods)
95
+ if (visit(m.statements))
96
+ return true;
97
+ for (const g of cls.getters)
98
+ if (visit(g.statements))
99
+ return true;
100
+ for (const s of cls.setters)
101
+ if (visit(s.statements))
102
+ return true;
103
+ if (cls.constructor && visit(cls.constructor.statements))
104
+ return true;
105
+ }
106
+ return false;
107
+ }
34
108
  /**
35
109
  * Extract a `#ifndef MACRO ... #endif` include-guard block from a shim line
36
110
  * list. Used to emit just the macro definition (e.g. CUTTLEFISH_UNDEFINED) in
@@ -118,6 +192,14 @@ export function buildEmitterContext(program, options) {
118
192
  })();
119
193
  const programAnalysis = analyzeProgram(program, strategy);
120
194
  if (options.platformContext) {
195
+ // The UI runtime's per-frame tick calls millis() (injected by the emitter,
196
+ // not present in user source), so a mounted UI needs the native millis ISR
197
+ // even when usesNativeTiming is false. OR entryHasUI() into the flag the
198
+ // AVR strategy reads when gating native_millis, so framework-avr doesn't
199
+ // drop the Timer0 ISR from a UI program.
200
+ if (!programAnalysis.usesNativeTiming && entryHasUI()) {
201
+ programAnalysis.usesNativeTiming = true;
202
+ }
121
203
  options.platformContext.analysis = programAnalysis;
122
204
  if (!options.platformContext.architecture && program.boardConstants) {
123
205
  options.platformContext.architecture = program.boardConstants.get("architecture");
@@ -223,6 +305,28 @@ export function buildEmitterContext(program, options) {
223
305
  if (!programAnalysis.usesStrPtr) {
224
306
  shimLines = filterShimBlock(shimLines, '#ifndef CUTTLEFISH_STR_BUF_SIZE', 'inline size_t (strlen)(const __tc_str_ptr& s) { return ::strlen(s.buf); }');
225
307
  }
308
+ // framework-avr native peripheral driver shims. Each block carries a
309
+ // CUTTLEFISH_*_BEGIN/END marker pair; strip the ones the program doesn't
310
+ // use. framework-arduino's shimLines contain none of these markers, so
311
+ // these filters are no-ops there. The strategies also self-gate on the
312
+ // same flags; this is the defensive backstop (mirrors how usesWDT/etc.
313
+ // backstop the strategy-side gating above).
314
+ if (!programAnalysis.usesUart) {
315
+ shimLines = filterShimBlock(shimLines, '// CUTTLEFISH_UART_BEGIN', '// CUTTLEFISH_UART_END');
316
+ shimLines = filterShimBlock(shimLines, '// CUTTLEFISH_UART_EXT_BEGIN', '// CUTTLEFISH_UART_EXT_END');
317
+ }
318
+ if (!programAnalysis.usesSPI) {
319
+ shimLines = filterShimBlock(shimLines, '// CUTTLEFISH_SPI_BEGIN', '// CUTTLEFISH_SPI_END');
320
+ }
321
+ if (!programAnalysis.usesI2C) {
322
+ shimLines = filterShimBlock(shimLines, '// CUTTLEFISH_TWI_BEGIN', '// CUTTLEFISH_TWI_END');
323
+ }
324
+ if (!programAnalysis.usesEEPROM) {
325
+ shimLines = filterShimBlock(shimLines, '// CUTTLEFISH_EEPROM_BEGIN', '// CUTTLEFISH_EEPROM_END');
326
+ }
327
+ if (!programAnalysis.usesTone) {
328
+ shimLines = filterShimBlock(shimLines, '// CUTTLEFISH_TONE_BEGIN', '// CUTTLEFISH_TONE_END');
329
+ }
226
330
  profileDiagnostics = [...strategy.profileDiagnostics(program, options.platformContext)];
227
331
  }
228
332
  for (const imported of program.imports) {
@@ -715,7 +819,13 @@ export function buildEmitterContext(program, options) {
715
819
  includes.push(...emittedPolyfills.includes.map((include) => normalizeInclude(include)));
716
820
  }
717
821
  if (program.requiredIncludes) {
718
- includes.push(...program.requiredIncludes);
822
+ let reqIncludes = [...program.requiredIncludes];
823
+ // Let the strategy strip stale includes (e.g. framework-avr removes
824
+ // Arduino library headers it doesn't need — Wire.h, SPI.h, etc.).
825
+ if (strategy.filterRequiredIncludes) {
826
+ reqIncludes = strategy.filterRequiredIncludes(reqIncludes);
827
+ }
828
+ includes.push(...reqIncludes);
719
829
  }
720
830
  // UI text bindings lower to snprintf bodies that need <stdio.h>. Pushed here
721
831
  // (in buildEmitterContext) rather than in emitUIRuntime because emitPreamble
@@ -739,6 +849,17 @@ export function buildEmitterContext(program, options) {
739
849
  if (stringEnumNames.size > 0) {
740
850
  includes.push(strategy.cstringHeader());
741
851
  }
852
+ // `<avr/wdt.h>` is only needed when the program actually uses the watchdog.
853
+ // The HAL resolver lowers WDT.enable/reset/disable to bare wdt_*() calls
854
+ // (or constant-folded wdt_enable(WDTO_*) macros), all of which require the
855
+ // header. Detect the structured `wdt.*` hal-ops directly here rather than
856
+ // relying on programAnalysis.usesWDT (which misses these ops — they carry
857
+ // a typed operation name, not raw code the analysis scans) or on a forced
858
+ // include in the AVR profile (which leaked the header into every AVR
859
+ // program, even ones that never touch the watchdog like `led.toggle()`).
860
+ if (programUsesWdt(program)) {
861
+ includes.push("<avr/wdt.h>");
862
+ }
742
863
  // Placeholder defaults for fields that are computed later by other phases
743
864
  const noopFixPointer = (c) => c;
744
865
  // Demo #18 Finding B: collect the names of non-exported free functions that
@@ -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
- const returnType = ts.isPropertyAccessExpression(receiverNode.expression) && ts.isIdentifier(receiverNode.expression.expression)
268
- ? getCurrentIrTypeScope()?.locals.get(receiverNode.expression.expression.text)
269
- : undefined;
270
- if (returnType === "std::string")
271
- return `static_cast<long long>(${safeText}.length())`;
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
  });