@typecad/cuttlefish 1.0.0-alpha.11 → 1.0.0-alpha.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/add-preset.d.ts +4 -0
- package/dist/add-preset.js +74 -0
- package/dist/api/config.d.ts +5 -5
- package/dist/api/shared/display-adapters/sdl.js +1 -1
- package/dist/api/shared/display-profile.d.ts +11 -0
- package/dist/api/shared/display-profile.js +3 -0
- package/dist/api/shared/framework-manifest.d.ts +85 -78
- package/dist/api/shared/framework-manifest.js +1 -0
- package/dist/api/shared/hal-op-ir.d.ts +19 -0
- package/dist/api/shared/toolchain-types.d.ts +0 -1
- package/dist/cli.js +21 -4
- package/dist/config-loader.d.ts +8 -2
- package/dist/config-loader.js +202 -53
- package/dist/config-schema.d.ts +7 -7
- package/dist/config-schema.js +3 -3
- package/dist/create/board-spec.d.ts +122 -122
- package/dist/create/debug-artifacts.d.ts +20 -0
- package/dist/create/debug-artifacts.js +69 -0
- package/dist/create/eslint-rules-template.js +6 -3
- package/dist/create/index.d.ts +2 -0
- package/dist/create/index.js +1 -0
- package/dist/create/init-scaffold.d.ts +1 -0
- package/dist/create/init-scaffold.js +5 -0
- package/dist/create/init-templates.js +0 -2
- package/dist/emit/compliance/rules.js +52 -4
- package/dist/emit/emitters/function-emitter-impl.js +7 -1
- package/dist/emit/emitters/line-appender.js +6 -0
- package/dist/emit/emitters/setup.js +15 -2
- package/dist/emit/emitters/ui-emitter.js +40 -15
- package/dist/emit/route-hal-op.js +55 -1
- package/dist/emit/statement-renderer.js +5 -2
- package/dist/ir/build-ir.js +22 -1
- package/dist/ir/expression-to-ir.js +17 -0
- package/dist/ir/feature-registry.js +22 -6
- package/dist/ir/hal/hal-emitter.js +23 -5
- package/dist/ir/hal/hal-plugins.js +11 -0
- package/dist/ir/pin-mode-validation.js +32 -9
- package/dist/ir/pin-state-tracking.d.ts +58 -0
- package/dist/ir/pin-state-tracking.js +182 -0
- package/dist/ir/program-analysis.d.ts +10 -2
- package/dist/ir/program-analysis.js +40 -4
- package/dist/ir/statement-to-ir.js +14 -0
- package/dist/ir/transformers/control-flow.js +29 -0
- package/dist/ir/transformers/ui-call-resolver.js +105 -1
- package/dist/ir/ui-element-auto-wire.js +7 -4
- package/dist/orchestrator/graph-builder.d.ts +4 -1
- package/dist/orchestrator/graph-builder.js +7 -1
- package/dist/platform/async-runtime.d.ts +1 -1
- package/dist/platform/async-runtime.js +12 -3
- package/dist/platform/generic-strategy.js +1 -1
- package/dist/preview/api-shared-shim.d.ts +1 -0
- package/dist/preview/api-shared-shim.js +7 -0
- package/dist/preview/client.js +220 -1
- package/dist/preview/server.js +154 -62
- package/dist/theme-tokens.d.ts +22 -0
- package/dist/theme-tokens.js +172 -0
- package/dist/transpile.js +90 -12
- package/dist/types.d.ts +5 -0
- package/dist/ui-hook.d.ts +7 -0
- package/dist/utils/cli.js +9 -0
- package/dist/utils/fs.d.ts +2 -0
- package/dist/utils/fs.js +16 -0
- package/dist/utils/ui.d.ts +5 -0
- package/dist/utils/ui.js +7 -0
- package/package.json +7 -5
|
@@ -43,6 +43,40 @@ export const RULES = [
|
|
|
43
43
|
justification: "AVR freeHeap() measures the gap between the stack and heap via the avr-libc __heap_start/__brkval linker symbols; pointer-to-integer conversion is the only way to compute a byte distance between two addresses on a target with no numeric free-heap API.",
|
|
44
44
|
kind: "polyfill",
|
|
45
45
|
},
|
|
46
|
+
{
|
|
47
|
+
// framework-zephyr BLE lowering: characteristic read handlers are TS
|
|
48
|
+
// functions with heterogeneous inferred return types (const char*,
|
|
49
|
+
// double), but the GATT attribute table registers one C callback for
|
|
50
|
+
// every characteristic. Handlers are stored in a fixed-size void*
|
|
51
|
+
// table (__tc_ble.on_read[]) and the characteristic index rides in
|
|
52
|
+
// bt_gatt_attr::user_data; the dispatcher casts back to the concrete
|
|
53
|
+
// signature per the char's type field. C++14 has no type-safe way to
|
|
54
|
+
// store or invoke heterogeneous signatures through one type-erased
|
|
55
|
+
// slot. Marker-based: every site references the table or its index
|
|
56
|
+
// channel.
|
|
57
|
+
detect: /__tc_ble\.on_read|__tc_ble_attrs|attr->user_data/,
|
|
58
|
+
justification: "framework-zephyr BLE lowering stores heterogeneously-typed read handlers in a void* table (__tc_ble.on_read[]) keyed by bt_gatt_attr::user_data; C++14 offers no type-safe mechanism to store or invoke mixed signatures through one type-erased slot, so the store/dispatch sites must reinterpret_cast.",
|
|
59
|
+
kind: "polyfill",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
// Zephyr C APIs take byte buffers as uint8_t*/void* (bt_data.data,
|
|
63
|
+
// mqtt_utf8.utf8, MQTT payload.data, spi_buf.buf, i2c_read tx/rx)
|
|
64
|
+
// while cuttlefish lowers TS buffers and strings to char*/uint8_t
|
|
65
|
+
// arrays. The pointer reinterpretation happens exactly at the C API
|
|
66
|
+
// boundary in the framework shims (ble/mqtt/i2c/spi lowerings).
|
|
67
|
+
detect: /reinterpret_cast<(?:const )?uint8_t\*>|\.buf = reinterpret_cast<void\*>/,
|
|
68
|
+
justification: "Zephyr's C APIs (bt_data, mqtt_utf8, MQTT payload, spi_buf, i2c_read) accept byte buffers as uint8_t*/void* while the transpiler lowers TS buffers/strings to char*/uint8_t arrays; reinterpret_cast at the API boundary is the only way to pass the lowered buffer.",
|
|
69
|
+
kind: "polyfill",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
// Raw register declarations (framework-native and MCU register
|
|
73
|
+
// exports) map a literal address to a volatile uint32_t* MMIO
|
|
74
|
+
// pointer. C++14 has no standard integer→pointer conversion other
|
|
75
|
+
// than reinterpret_cast.
|
|
76
|
+
detect: /reinterpret_cast<volatile uint32_t\*>/,
|
|
77
|
+
justification: "MMIO register declarations convert a literal address to a volatile uint32_t* pointer; reinterpret_cast is the only standard C++14 integer-to-pointer conversion.",
|
|
78
|
+
kind: "other",
|
|
79
|
+
},
|
|
46
80
|
],
|
|
47
81
|
},
|
|
48
82
|
{ id: "M5-2-8", title: "No pointer arithmetic out of bounds", severity: "required", category: "C", enabled: true },
|
|
@@ -112,8 +146,8 @@ export const RULES = [
|
|
|
112
146
|
},
|
|
113
147
|
],
|
|
114
148
|
},
|
|
115
|
-
{ id: "A18-5-10", title: "No malloc/calloc/realloc", severity: "required", category: "C",
|
|
116
|
-
detect: /\b(malloc|calloc|realloc)\s*\(/, enabled: true,
|
|
149
|
+
{ id: "A18-5-10", title: "No malloc/calloc/realloc/free (C dynamic memory family)", severity: "required", category: "C",
|
|
150
|
+
detect: /\b(?:ps_)?(?:malloc|calloc|realloc|free)\s*\(/, enabled: true,
|
|
117
151
|
knownPatterns: [
|
|
118
152
|
{
|
|
119
153
|
// Offscreen canvas allocation (CuttlefishCanvas16/CuttlefishCanvasMono
|
|
@@ -129,10 +163,24 @@ export const RULES = [
|
|
|
129
163
|
// the alternative (operator new) is the very thing that crashes. The
|
|
130
164
|
// object is placement-constructed on the malloc'd memory and freed via
|
|
131
165
|
// an explicit dtor + free, so the vtable/lifetime are correct.
|
|
132
|
-
|
|
133
|
-
|
|
166
|
+
// ps_malloc is the ESP32 PSRAM variant of the same constraint.
|
|
167
|
+
detect: /(?:ps_)?malloc\s*\(/,
|
|
168
|
+
justification: "Canvas object/buffer allocation on full-libcpp-without-exceptions targets; malloc (or ESP32 ps_malloc) avoids the operator-new std::bad_alloc → std::terminate → abort path. OOM returns NULL and the runtime degrades gracefully.",
|
|
134
169
|
kind: "ts-literal",
|
|
135
170
|
},
|
|
171
|
+
{
|
|
172
|
+
// Offscreen canvas teardown, the release side of the allocations
|
|
173
|
+
// above (canvas objects and their malloc'd/ps_malloc'd pixel
|
|
174
|
+
// buffers). On Arduino cores operator new is malloc-backed and
|
|
175
|
+
// free() releases both SRAM and PSRAM objects via the ESP32 unified
|
|
176
|
+
// heap, so dtor + free() is the correct teardown for every canvas
|
|
177
|
+
// allocation path — `delete` would be UB on the placement-new PSRAM
|
|
178
|
+
// object. Only these named canvas/buffer releases are deviations;
|
|
179
|
+
// any other free() stays an unrecorded violation.
|
|
180
|
+
detect: /\bfree\s*\(\s*(?:canvas|buffer_|psramBuf)\s*\)/,
|
|
181
|
+
justification: "Canvas teardown on targets whose operator new is malloc-backed (Arduino cores, ESP32 unified heap): the object was placement-constructed or allocation-path-compatible, so dtor + free() is the only well-defined release; delete would be UB on placement-new PSRAM objects.",
|
|
182
|
+
kind: "raw-array",
|
|
183
|
+
},
|
|
136
184
|
],
|
|
137
185
|
},
|
|
138
186
|
{ id: "A27-0-4", title: "No function returning std::move of local", severity: "required", category: "C",
|
|
@@ -31,7 +31,13 @@ export function emitPostClassDeclarations(ctx) {
|
|
|
31
31
|
// and failed at g++ time. Demo #28 Finding B.
|
|
32
32
|
if (ctx.promotedVarDecls.size > 0) {
|
|
33
33
|
for (const [varName, info] of ctx.promotedVarDecls) {
|
|
34
|
-
|
|
34
|
+
// A3-9-1: promoted file-scope declarations bypass renderVarDecl, so the
|
|
35
|
+
// int -> fixed-width substitution has to be applied here as well.
|
|
36
|
+
const autosarOn = ctx.compliance.isEnabled() && ctx.compliance.isBanned("A3-9-1");
|
|
37
|
+
const fwdType = autosarOn && info.cppType === "int"
|
|
38
|
+
? strategy.defaultNumericType(ctx.compliance)
|
|
39
|
+
: normalizeCppTypeForTarget(info.cppType);
|
|
40
|
+
appendSourceLine(ctx, `${fwdType} ${escapeCppKeyword(varName, platformReservedNames)} = {};`);
|
|
35
41
|
// Seed the top-level scope's type map so subsequent assign rendering
|
|
36
42
|
// (e.g. the deferred `c = SafeInt(0)` initializer) can resolve the
|
|
37
43
|
// variable's type and inject template args / casts via
|
|
@@ -298,6 +298,12 @@ export function appendRenderedStatement(ctx, statement, indent, scopeState) {
|
|
|
298
298
|
return;
|
|
299
299
|
}
|
|
300
300
|
if (statement.kind === "block") {
|
|
301
|
+
if (statement.body.length === 0) {
|
|
302
|
+
// An empty block is a no-op statement — skip it entirely so lowered
|
|
303
|
+
// top-level statements don't litter setup() with bare { } pairs.
|
|
304
|
+
emitCommentLines(statement.trailingComments, indent, (line) => appendSourceLine(ctx, line));
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
301
307
|
appendSourceLine(ctx, `${indent}{`, { tsSpan: statement.sourceSpan, nodeKind: statement.kind });
|
|
302
308
|
const nestedScope = cloneEmissionScopeState(scopeState);
|
|
303
309
|
for (const nested of statement.body)
|
|
@@ -320,6 +320,15 @@ export function buildEmitterContext(program, options) {
|
|
|
320
320
|
if (!programAnalysis.usesNativeTiming && entryHasUI()) {
|
|
321
321
|
programAnalysis.usesNativeTiming = true;
|
|
322
322
|
}
|
|
323
|
+
// The UI runtime header calls constrain() in the progress/range node draw
|
|
324
|
+
// and touch-slider paths (injected by the emitter, not present in user
|
|
325
|
+
// source), so a mounted UI needs the constrain polyfill even when
|
|
326
|
+
// usesConstrain is false. OR entryHasUI() into the flag the native
|
|
327
|
+
// strategy reads when gating the constrain helper, mirroring the
|
|
328
|
+
// usesNativeTiming handling above.
|
|
329
|
+
if (!programAnalysis.usesConstrain && entryHasUI()) {
|
|
330
|
+
programAnalysis.usesConstrain = true;
|
|
331
|
+
}
|
|
323
332
|
options.platformContext.analysis = programAnalysis;
|
|
324
333
|
if (!options.platformContext.architecture && program.boardConstants) {
|
|
325
334
|
options.platformContext.architecture = program.boardConstants.get("architecture");
|
|
@@ -416,8 +425,12 @@ export function buildEmitterContext(program, options) {
|
|
|
416
425
|
}
|
|
417
426
|
// The UI runtime's per-frame tick uses millis() (injected by the emitter,
|
|
418
427
|
// not authored in user source), so keep the millis() shim when a UI is
|
|
419
|
-
// mounted even if the source-level analysis didn't flag usesMillis.
|
|
420
|
-
|
|
428
|
+
// mounted even if the source-level analysis didn't flag usesMillis. The
|
|
429
|
+
// async runtime and the setInterval/setTimeout scheduler also poll
|
|
430
|
+
// millis() without any user-source millis() call (Async.sleep lowers to a
|
|
431
|
+
// raw hal-op the timing scanners can't see) — mirror the usesNativeTiming
|
|
432
|
+
// derivation and keep the shim for those hidden consumers too.
|
|
433
|
+
if (!programAnalysis.usesMillis && !programAnalysis.hasAsync && programAnalysis.timerCallCount === 0 && !entryHasUI()) {
|
|
421
434
|
shimLines = shimLines.filter(l => !l.includes('millis()'));
|
|
422
435
|
}
|
|
423
436
|
// Strip the nullish helper FUNCTIONS (not the CUTTLEFISH_UNDEFINED macro)
|
|
@@ -46,8 +46,13 @@ export function generateTouchPollBody(input) {
|
|
|
46
46
|
const hasNativeSize = profile.nativeWidth !== undefined && profile.nativeHeight !== undefined;
|
|
47
47
|
const nativeWidth = profile.nativeWidth ?? profile.width;
|
|
48
48
|
const nativeHeight = profile.nativeHeight ?? profile.height;
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
// Inlined Arduino map() arithmetic, NOT a map() call: the emitted body must
|
|
50
|
+
// be self-contained. The map/constrain helpers are capability-gated
|
|
51
|
+
// (usesMap is set by SCRIPT-level map() calls only), so a bare map() here
|
|
52
|
+
// failed to link on frameworks without a core map() (Zephyr).
|
|
53
|
+
const mapCall = (raw, inMin, inMax, outMin, outMax) => `((${raw} - ${inMin}) * (${outMax} - ${outMin}) / (${inMax} - ${inMin}) + ${outMin})`;
|
|
54
|
+
const rawMapX = mapCall("__rawX", xMin, xMax, 0, nativeWidth);
|
|
55
|
+
const rawMapY = mapCall("__rawY", yMin, yMax, 0, nativeHeight);
|
|
51
56
|
let mapX, mapY;
|
|
52
57
|
let sdlClamp = false;
|
|
53
58
|
if (library === "sdl") {
|
|
@@ -69,15 +74,15 @@ export function generateTouchPollBody(input) {
|
|
|
69
74
|
const invertY = rotation === 1 || rotation === 2;
|
|
70
75
|
if (isLandscape) {
|
|
71
76
|
mapX = invertX
|
|
72
|
-
?
|
|
73
|
-
:
|
|
77
|
+
? mapCall("__rawX", xMin, xMax, profile.width, 0)
|
|
78
|
+
: mapCall("__rawX", xMin, xMax, 0, profile.width);
|
|
74
79
|
mapY = invertY
|
|
75
|
-
?
|
|
76
|
-
:
|
|
80
|
+
? mapCall("__rawY", yMin, yMax, profile.height, 0)
|
|
81
|
+
: mapCall("__rawY", yMin, yMax, 0, profile.height);
|
|
77
82
|
}
|
|
78
83
|
else {
|
|
79
|
-
mapX =
|
|
80
|
-
mapY =
|
|
84
|
+
mapX = mapCall("__rawY", yMin, yMax, profile.width, 0);
|
|
85
|
+
mapY = mapCall("__rawX", xMin, xMax, 0, profile.height);
|
|
81
86
|
}
|
|
82
87
|
}
|
|
83
88
|
else {
|
|
@@ -323,8 +328,12 @@ export function emitUIRuntime(ctx) {
|
|
|
323
328
|
ctx.sourceLines.push(lowered.keyboardDispatch);
|
|
324
329
|
}
|
|
325
330
|
// 3. Signal variables (one per ui.signal / const X = ui.signal).
|
|
331
|
+
// A3-9-1: signal decls bypass the statement renderer, so apply the
|
|
332
|
+
// int -> fixed-width substitution here under --autosar.
|
|
333
|
+
const signalAutosarOn = ctx.compliance.isEnabled() && ctx.compliance.isBanned("A3-9-1");
|
|
334
|
+
const fixedWidth = signalAutosarOn && ctx.strategy ? ctx.strategy.defaultNumericType(ctx.compliance) : "";
|
|
326
335
|
for (const decl of uiSignalDecls()) {
|
|
327
|
-
ctx.sourceLines.push(decl);
|
|
336
|
+
ctx.sourceLines.push(fixedWidth && decl.startsWith("int ") ? `${fixedWidth}${decl.slice(3)}` : decl);
|
|
328
337
|
}
|
|
329
338
|
// 4. Binding table (accumulated from ui.bind calls).
|
|
330
339
|
// Forward-declare the binding compute functions first: the table references
|
|
@@ -462,17 +471,33 @@ export function emitUIRuntime(ctx) {
|
|
|
462
471
|
}
|
|
463
472
|
return entries.join(", ");
|
|
464
473
|
};
|
|
474
|
+
// Per-table counts: the runtime indexes each table by node, so the safe
|
|
475
|
+
// bound for dispatch is each table's own highest populated index + 1 — not
|
|
476
|
+
// the shared click-table size (holds/releases can span fewer nodes).
|
|
477
|
+
const tableCount = (kind) => {
|
|
478
|
+
const max = touchHandlers
|
|
479
|
+
.filter(h => h.kind === kind)
|
|
480
|
+
.reduce((m, h) => Math.max(m, h.nodeIndex), -1);
|
|
481
|
+
return Math.max(max + 1, 1);
|
|
482
|
+
};
|
|
483
|
+
// The handler tables are link-time constants (function pointers only) and
|
|
484
|
+
// are never written at runtime — emit them const so they land in flash
|
|
485
|
+
// rodata instead of stealing DRAM (~4.5KB on a 380-node demo).
|
|
465
486
|
if (profile.touch) {
|
|
466
|
-
ctx.sourceLines.push(`void (*__ui_click_handlers[])() = { ${buildTable("click")} };`);
|
|
467
|
-
ctx.sourceLines.push(`void (*__ui_hold_handlers[])() = { ${buildTable("hold")} };`);
|
|
468
|
-
ctx.sourceLines.push(`void (*__ui_release_handlers[])() = { ${buildTable("release")} };`);
|
|
487
|
+
ctx.sourceLines.push(`void (*const __ui_click_handlers[])() = { ${buildTable("click")} };`);
|
|
488
|
+
ctx.sourceLines.push(`void (*const __ui_hold_handlers[])() = { ${buildTable("hold")} };`);
|
|
489
|
+
ctx.sourceLines.push(`void (*const __ui_release_handlers[])() = { ${buildTable("release")} };`);
|
|
469
490
|
ctx.sourceLines.push(`const uint16_t __ui_click_handler_count = ${tableSize};`);
|
|
491
|
+
ctx.sourceLines.push(`const uint16_t __ui_hold_handler_count = ${tableCount("hold")};`);
|
|
492
|
+
ctx.sourceLines.push(`const uint16_t __ui_release_handler_count = ${tableCount("release")};`);
|
|
470
493
|
}
|
|
471
494
|
else {
|
|
472
|
-
ctx.sourceLines.push(`void (*__ui_click_handlers[])() = {};`);
|
|
473
|
-
ctx.sourceLines.push(`void (*__ui_hold_handlers[])() = {};`);
|
|
474
|
-
ctx.sourceLines.push(`void (*__ui_release_handlers[])() = {};`);
|
|
495
|
+
ctx.sourceLines.push(`void (*const __ui_click_handlers[])() = {};`);
|
|
496
|
+
ctx.sourceLines.push(`void (*const __ui_hold_handlers[])() = {};`);
|
|
497
|
+
ctx.sourceLines.push(`void (*const __ui_release_handlers[])() = {};`);
|
|
475
498
|
ctx.sourceLines.push(`const uint16_t __ui_click_handler_count = 0;`);
|
|
499
|
+
ctx.sourceLines.push(`const uint16_t __ui_hold_handler_count = 0;`);
|
|
500
|
+
ctx.sourceLines.push(`const uint16_t __ui_release_handler_count = 0;`);
|
|
476
501
|
}
|
|
477
502
|
// 8b. Input onChange dispatch — assigns __ui_kb_onchange based on __ui_kb_target.
|
|
478
503
|
// Forward-declare __ui_kb_set_onchange unconditionally: the runtime header's
|
|
@@ -8,8 +8,20 @@
|
|
|
8
8
|
// everything else goes to resolveHALOperation (the existing generic seam).
|
|
9
9
|
// Keeps the consumer sites (expression-renderer, statement-renderer,
|
|
10
10
|
// render-expr) DRY.
|
|
11
|
+
//
|
|
12
|
+
// Output-pin state tracking is also handled here, centrally, so every
|
|
13
|
+
// strategy benefits without per-strategy changes:
|
|
14
|
+
// - gpio.read with `trackedValue` never reaches the strategy (a hardware
|
|
15
|
+
// read of a direction-only output is not portable, e.g. Zephyr); it folds
|
|
16
|
+
// to a constant or the tracked shadow variable.
|
|
17
|
+
// - gpio.write / gpio.toggle flagged `updatesShadow` get the shadow variable
|
|
18
|
+
// update appended to whatever the strategy produced. The flag is baked
|
|
19
|
+
// into the op at IR-build time (markShadowUpdatingOps) — emit must not
|
|
20
|
+
// consult live tracker state, because all files build (each resetting the
|
|
21
|
+
// tracker) before any file emits.
|
|
11
22
|
// ---------------------------------------------------------------------------
|
|
12
23
|
import { getSafetyHook } from "../safety-hook.js";
|
|
24
|
+
import { pinShadowVarName } from "../ir/pin-state-tracking.js";
|
|
13
25
|
export function routeHALOp(op, strategy) {
|
|
14
26
|
if (typeof op.operation === "string") {
|
|
15
27
|
if (op.operation.startsWith("display.")) {
|
|
@@ -22,5 +34,47 @@ export function routeHALOp(op, strategy) {
|
|
|
22
34
|
return getSafetyHook()?.resolveSafetyOp?.(op);
|
|
23
35
|
}
|
|
24
36
|
}
|
|
25
|
-
|
|
37
|
+
if (op.operation === "gpio.read" && op.trackedValue) {
|
|
38
|
+
// Tracked OUTPUT-pin read: software truth, never a hardware read.
|
|
39
|
+
if (op.trackedValue === "high")
|
|
40
|
+
return { expression: "true" };
|
|
41
|
+
if (op.trackedValue === "low")
|
|
42
|
+
return { expression: "false" };
|
|
43
|
+
return { expression: pinShadowVarName(op.pin) };
|
|
44
|
+
}
|
|
45
|
+
const resolved = strategy.resolveHALOperation?.(op);
|
|
46
|
+
// Toggle on a shadow-tracked pin must not use the strategy's default
|
|
47
|
+
// read-modify-write form (e.g. digitalWrite(p, digitalRead(p) ...) on
|
|
48
|
+
// Arduino) — reading the pin back is exactly what tracking avoids. Lower
|
|
49
|
+
// it as a write of the shadow's current value, then flip the shadow.
|
|
50
|
+
if (op.operation === "gpio.toggle" && op.updatesShadow) {
|
|
51
|
+
const varName = pinShadowVarName(op.pin);
|
|
52
|
+
const writeOp = {
|
|
53
|
+
operation: "gpio.write",
|
|
54
|
+
pin: op.pin,
|
|
55
|
+
value: varName,
|
|
56
|
+
...(op.port !== undefined ? { port: op.port } : {}),
|
|
57
|
+
};
|
|
58
|
+
const writeResolved = strategy.resolveHALOperation?.(writeOp);
|
|
59
|
+
const flip = `${varName} = (!${varName});`;
|
|
60
|
+
if (writeResolved?.code) {
|
|
61
|
+
return { ...writeResolved, code: `${writeResolved.code}\n${flip}` };
|
|
62
|
+
}
|
|
63
|
+
if (writeResolved?.expression) {
|
|
64
|
+
return { ...writeResolved, expression: `${writeResolved.expression}, ${flip}` };
|
|
65
|
+
}
|
|
66
|
+
return { code: flip };
|
|
67
|
+
}
|
|
68
|
+
if (op.operation === "gpio.write" && op.updatesShadow) {
|
|
69
|
+
const varName = pinShadowVarName(op.pin);
|
|
70
|
+
const update = `${varName} = ((${op.value}) != 0);`;
|
|
71
|
+
if (resolved?.code) {
|
|
72
|
+
return { ...resolved, code: `${resolved.code}\n${update}` };
|
|
73
|
+
}
|
|
74
|
+
if (resolved?.expression) {
|
|
75
|
+
return { ...resolved, expression: `${resolved.expression}, ${update}` };
|
|
76
|
+
}
|
|
77
|
+
return { code: update };
|
|
78
|
+
}
|
|
79
|
+
return resolved;
|
|
26
80
|
}
|
|
@@ -551,8 +551,11 @@ export class StatementRenderer {
|
|
|
551
551
|
}
|
|
552
552
|
const declaredType = this.normalizeCppType(statement.cppType);
|
|
553
553
|
const volatilePrefix = statement.isVolatile ? "volatile " : "";
|
|
554
|
-
// Transform type name for Arduino library classes (add namespace prefix)
|
|
555
|
-
|
|
554
|
+
// Transform type name for Arduino library classes (add namespace prefix).
|
|
555
|
+
// A3-9-1: substitute the fixed-width default for the IR's hardcoded "int"
|
|
556
|
+
// BEFORE the class-name transform so var declarations match the other
|
|
557
|
+
// declaration paths under --autosar.
|
|
558
|
+
const transformedType = transformTypeName(declaredType, this.classNameMap);
|
|
556
559
|
const ownershipKind = statement.ownershipKind;
|
|
557
560
|
// Emit const for Shared<T> ownership annotations (ownershipKind === 'shared')
|
|
558
561
|
const isConst = statement.storage === "const" || ownershipKind === 'shared';
|
package/dist/ir/build-ir.js
CHANGED
|
@@ -9,6 +9,7 @@ import { resolveBoardConstants, tryResolveBoardDefFile } from "./board-resolver.
|
|
|
9
9
|
import { analyzePeripheralUsage, createEmptyPeripheralUsage } from "./peripheral-usage.js";
|
|
10
10
|
import { runProgramValidations } from "./validation-orchestrator.js";
|
|
11
11
|
import { registerFieldMap, hoistedNestedFunctions, hoistedNestedClasses, hoistedNestedEnums, hoistedNestedInterfaces, hoistedNestedTypeAliases, activeNamespaceNames, activeEnumNames, activeStringEnumNames, peripheralAliasMap, pinAliasMap, mcuPinReverseMap, topLevelClassNames, topLevelInterfaceNames, classTypeNames, topLevelClasses, requiredIncludes, resetBuildState, getCurrentBoardConstants, setCurrentBoardConstants, contextStorage, CompilationContext, registeredCallbacks, getContext, discriminatedUnionVariantNames, restParamFunctions, topLevelAliasReceivers } from "./build-ir-state.js";
|
|
12
|
+
import { pinShadowVarName, takeShadowDeclarations, resetPinStateTracking, markShadowUpdatingOps } from "./pin-state-tracking.js";
|
|
12
13
|
import { collectPointerVars, expressionStatementToIR, lowerStatement, variableStatementToIR, prescanArrayUsage, lowerStatementList } from "./statement-to-ir.js";
|
|
13
14
|
import { registerUIModuleImport, registerElementValue } from "./transformers/ui-call-resolver.js";
|
|
14
15
|
import { requireUIHook } from "../ui-hook.js";
|
|
@@ -181,6 +182,7 @@ export function buildProgramIR(fileName, sourceText, boardPackage, prebuiltClass
|
|
|
181
182
|
// Reset module-level state for this file
|
|
182
183
|
resetBuildState();
|
|
183
184
|
resetHALResolver();
|
|
185
|
+
resetPinStateTracking();
|
|
184
186
|
registerFieldMap.clear();
|
|
185
187
|
// Phase 0: Pre-scan for top-level classes and register them so type inference can resolve them.
|
|
186
188
|
// Also register classes from other files in the transpile graph so that property accesses
|
|
@@ -786,7 +788,7 @@ export function buildProgramIR(fileName, sourceText, boardPackage, prebuiltClass
|
|
|
786
788
|
// If peripheral analysis fails, use empty usage
|
|
787
789
|
peripheralUsage = createEmptyPeripheralUsage();
|
|
788
790
|
}
|
|
789
|
-
|
|
791
|
+
const program = {
|
|
790
792
|
fileName,
|
|
791
793
|
imports,
|
|
792
794
|
reExports,
|
|
@@ -808,5 +810,24 @@ export function buildProgramIR(fileName, sourceText, boardPackage, prebuiltClass
|
|
|
808
810
|
restParamFunctions: new Map(restParamFunctions),
|
|
809
811
|
...(defaultExportName ? { defaultExportName } : {}),
|
|
810
812
|
};
|
|
813
|
+
// Output-pin state tracking. First bake the shadow-update flags into the
|
|
814
|
+
// write/toggle ops, while this file's tracker state is still live (the
|
|
815
|
+
// next file's build resets it, and emit runs after every file is built —
|
|
816
|
+
// multi-file programs would lose the updates otherwise). Then consume the
|
|
817
|
+
// shadow declarations — pins whose reads lowered to the shadow variable
|
|
818
|
+
// form need a file-scope declaration.
|
|
819
|
+
markShadowUpdatingOps(program);
|
|
820
|
+
const shadowDecls = takeShadowDeclarations();
|
|
821
|
+
for (const { pin, initial } of shadowDecls) {
|
|
822
|
+
program.topLevelStatements.unshift({
|
|
823
|
+
kind: "var_decl",
|
|
824
|
+
sourceSpan: { filePath: fileName, startOffset: 0, endOffset: 0, startLine: 0, startColumn: 0, endLine: 0, endColumn: 0 },
|
|
825
|
+
name: pinShadowVarName(pin),
|
|
826
|
+
storage: "var",
|
|
827
|
+
cppType: "bool",
|
|
828
|
+
initializer: { kind: "boolean", value: initial },
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
return program;
|
|
811
832
|
});
|
|
812
833
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
2
|
import { makeDiagnostic, makeSourceSpan } from "./ast-node-utils.js";
|
|
3
3
|
import { PIN_FACTORY_FUNCTIONS, CONSTANT_FOLD_FUNCTIONS, TYPED_ARRAY_ELEMENT_MAP, activeCArrayVars, activeArrayLiteralVars, activeStringVars, nestedFunctionAliases, nestedClassAliases, hoistedNestedClasses, mutableArrayVars, arrayLiteralSizes, filteredArrayLengthVars, activeNamespaceNames, activeEnumNames, activeStringEnumNames, topLevelClassNames, topLevelInterfaceNames, classTypeNames, topLevelClasses, getActiveExtendsClass, restParamFunctions, getContext, getCurrentBoardConstants } from "./build-ir-state.js";
|
|
4
|
+
import { isPinFoldingEnabled, setPinFoldingEnabled } from "./pin-state-tracking.js";
|
|
4
5
|
import { getCurrentIrTypeScope } from "./symbol-types.js";
|
|
5
6
|
import { renderExprAsText } from "./render-expr.js";
|
|
6
7
|
import { lowerStatement, tryResolveHALExpression } from "./statement-to-ir.js";
|
|
@@ -340,6 +341,17 @@ export function expressionToIR(expr, sourceText, diagnostics, pointerVars = new
|
|
|
340
341
|
* variable name prefixed with "__FILTERED_LEN__" so the caller can detect it.
|
|
341
342
|
*/
|
|
342
343
|
function resolveLengthProperty(receiverNode, objectText) {
|
|
344
|
+
// UI element text: screen.<id>.text is a raw char buffer on the node —
|
|
345
|
+
// `.length` must be strlen, not `.size()` (char[33] has no size member).
|
|
346
|
+
if (ts.isPropertyAccessExpression(receiverNode) &&
|
|
347
|
+
receiverNode.name.text === "text" &&
|
|
348
|
+
ts.isPropertyAccessExpression(receiverNode.expression) &&
|
|
349
|
+
ts.isIdentifier(receiverNode.expression.expression)) {
|
|
350
|
+
const nodeIdx = resolveElementValue(receiverNode.expression.expression.text, receiverNode.expression.name.text);
|
|
351
|
+
if (nodeIdx !== undefined) {
|
|
352
|
+
return `static_cast<long long>(strlen(__ui_nodes[${nodeIdx}].textBuffer))`;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
343
355
|
if (ts.isStringLiteral(receiverNode) || ts.isNoSubstitutionTemplateLiteral(receiverNode)) {
|
|
344
356
|
return `${receiverNode.text.length}`;
|
|
345
357
|
}
|
|
@@ -2185,6 +2197,10 @@ export function expressionToIR(expr, sourceText, diagnostics, pointerVars = new
|
|
|
2185
2197
|
}
|
|
2186
2198
|
}
|
|
2187
2199
|
const isBlock = ts.isBlock(body);
|
|
2200
|
+
// Lambda bodies run at an unmodeled time (callbacks), so pin-state
|
|
2201
|
+
// constant folding must be off inside them.
|
|
2202
|
+
const prevPinFolding = isPinFoldingEnabled();
|
|
2203
|
+
setPinFoldingEnabled(false);
|
|
2188
2204
|
const bodyStmts = isBlock
|
|
2189
2205
|
? body.statements.map(stmt => {
|
|
2190
2206
|
const lowered = lowerStatement(stmt, "", sourceText, diagnostics, new Map(), new Map(), "<lambda>", new Map(), pointerVars);
|
|
@@ -2196,6 +2212,7 @@ export function expressionToIR(expr, sourceText, diagnostics, pointerVars = new
|
|
|
2196
2212
|
sourceSpan: makeSourceSpan(body, "", sourceText),
|
|
2197
2213
|
value: expressionToIR(body, sourceText, diagnostics, pointerVars),
|
|
2198
2214
|
}];
|
|
2215
|
+
setPinFoldingEnabled(prevPinFolding);
|
|
2199
2216
|
// Infer return type: use explicit annotation, or infer from body. Thread
|
|
2200
2217
|
// the lambda's own param types into the inference so a body like
|
|
2201
2218
|
// `(n) => n.capacity` can resolve `n` and deduce the return type.
|
|
@@ -112,7 +112,10 @@ add(ts.SyntaxKind.BigIntLiteral, {
|
|
|
112
112
|
hint: "Use a number literal with an explicit fixed-width type annotation.",
|
|
113
113
|
code: "TS2CPP_NO_EQUIVALENT",
|
|
114
114
|
eslint: {
|
|
115
|
-
|
|
115
|
+
// The parser emits the attribute as lowercase `bigint` (a string); the
|
|
116
|
+
// previous [bigInt=true] form matched nothing, so the editor never
|
|
117
|
+
// flagged what the build-time prescan did.
|
|
118
|
+
selector: "Literal[bigint]",
|
|
116
119
|
message: "[transpiler] BigInt literals are not supported (no C++ equivalent for embedded targets). Use a number literal with an explicit fixed-width type.",
|
|
117
120
|
},
|
|
118
121
|
});
|
|
@@ -222,7 +225,10 @@ const CONTEXT_LINT_RULES = [
|
|
|
222
225
|
source: "context",
|
|
223
226
|
},
|
|
224
227
|
{
|
|
225
|
-
|
|
228
|
+
// The parser names the attribute `operator`, not `type` — the previous
|
|
229
|
+
// [type='keyof'] form matched nothing, so the editor never flagged what
|
|
230
|
+
// the build-time prescan did.
|
|
231
|
+
selector: "TSTypeOperator[operator='keyof']",
|
|
226
232
|
message: "[transpiler] the keyof operator is not supported (no C++ equivalent). Use a string union or a switch over field names.",
|
|
227
233
|
source: "context",
|
|
228
234
|
},
|
|
@@ -317,12 +323,22 @@ const CONTEXT_LINT_RULES = [
|
|
|
317
323
|
source: "context",
|
|
318
324
|
},
|
|
319
325
|
{
|
|
320
|
-
|
|
326
|
+
// ui.bind is a recognized UI authoring call (intercepted by the
|
|
327
|
+
// transpiler's call-lowering), not Function.prototype.bind — the :not()
|
|
328
|
+
// guard exempts the `ui` receiver so the editor selector matches the
|
|
329
|
+
// build-time prescan (checkContextSensitive exempts exactly ui.bind).
|
|
330
|
+
// The exemption lives in the selector because generateEslintConfig()
|
|
331
|
+
// drops non-selector fields when rendering no-restricted-syntax. Only
|
|
332
|
+
// `bind` is exempted — ui.call/ui.apply are not UI APIs and the prescan
|
|
333
|
+
// flags them, so the editor does too.
|
|
334
|
+
selector: "CallExpression > MemberExpression.callee[property.name='bind']:not([object.name='ui'])",
|
|
335
|
+
message: "[transpiler] .bind/.call/.apply rebind `this` at call time, which has no C++ lowering (this is a fixed pointer). Call the function/method directly.",
|
|
336
|
+
source: "context",
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
selector: "CallExpression > MemberExpression.callee[property.name=/^(call|apply)$/]",
|
|
321
340
|
message: "[transpiler] .bind/.call/.apply rebind `this` at call time, which has no C++ lowering (this is a fixed pointer). Call the function/method directly.",
|
|
322
341
|
source: "context",
|
|
323
|
-
// ui.bind is a recognized UI authoring call (intercepted by the
|
|
324
|
-
// transpiler's call-lowering), not Function.prototype.bind.
|
|
325
|
-
filter: "(node) => { const o = node.callee.object; return !(o && o.type === 'Identifier' && o.name === 'ui'); }",
|
|
326
342
|
},
|
|
327
343
|
{
|
|
328
344
|
selector: "NewExpression[callee.name='Function']",
|
|
@@ -5,6 +5,7 @@ import { renderExprAsText } from "../render-expr.js";
|
|
|
5
5
|
import { escapeCppKeyword } from "../../utils/strings.js";
|
|
6
6
|
import { halClassRegistry, halGlobalFunctions } from "./hal-parser.js";
|
|
7
7
|
import { tryResolveSemanticCall, tryResolveBoardResolveArg, tryResolveCompoundSemanticReturn, resolveConcatPath } from "./hal-plugins.js";
|
|
8
|
+
import { resolveTrackedRead, pinShadowVarName } from "../pin-state-tracking.js";
|
|
8
9
|
import { cppTypeForHalOp } from "../../emit/utils/hal-op-cpp-type.js";
|
|
9
10
|
/** Escape C++ keywords in resolved text, but only when the text looks like a
|
|
10
11
|
* variable reference (not a literal like "false", "true", "42", or a string). */
|
|
@@ -58,13 +59,30 @@ function prefixOperatorText(operator) {
|
|
|
58
59
|
function inlineThisGetterCall(methodName, pin, strategy) {
|
|
59
60
|
switch (methodName) {
|
|
60
61
|
case "read":
|
|
61
|
-
return strategy?.readDigitalPin?.(pin) ?? `digitalRead(${pin})`;
|
|
62
|
-
case "readAnalog":
|
|
63
|
-
return strategy?.readAnalogPin?.(pin) ?? `analogRead(${pin})`;
|
|
64
62
|
case "isHigh":
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
case "isLow": {
|
|
64
|
+
// Output-pin state tracking: when the receiver is a tracked OUTPUT pin,
|
|
65
|
+
// lower to the tracked level (constant when statically known, shadow
|
|
66
|
+
// variable otherwise) instead of a hardware read. Reading back a
|
|
67
|
+
// direction-only output is not portable (e.g. Zephyr).
|
|
68
|
+
const pinNum = Number.parseInt(pin, 10);
|
|
69
|
+
if (Number.isFinite(pinNum)) {
|
|
70
|
+
const tracked = resolveTrackedRead(pinNum);
|
|
71
|
+
if (tracked !== null) {
|
|
72
|
+
const levelText = tracked === "shadow"
|
|
73
|
+
? pinShadowVarName(pinNum)
|
|
74
|
+
: (tracked === "high" ? "true" : "false");
|
|
75
|
+
return methodName === "isLow" ? `(!${levelText})` : levelText;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (methodName === "read")
|
|
79
|
+
return strategy?.readDigitalPin?.(pin) ?? `digitalRead(${pin})`;
|
|
80
|
+
if (methodName === "isHigh")
|
|
81
|
+
return strategy?.readDigitalPin?.(pin) ?? `digitalRead(${pin})`;
|
|
67
82
|
return strategy?.readDigitalPin ? `(!${strategy.readDigitalPin(pin)})` : `(!digitalRead(${pin}))`;
|
|
83
|
+
}
|
|
84
|
+
case "readAnalog":
|
|
85
|
+
return strategy?.readAnalogPin?.(pin) ?? `analogRead(${pin})`;
|
|
68
86
|
default:
|
|
69
87
|
return null;
|
|
70
88
|
}
|
|
@@ -3,6 +3,7 @@ import { getCurrentBoardConstants, halInstances, getContext } from "../build-ir-
|
|
|
3
3
|
import { resolveExpressionText } from "./hal-emitter.js";
|
|
4
4
|
import { renderExprAsText } from "../render-expr.js";
|
|
5
5
|
import { hasSafetyHook, requireSafetyHook } from "../../safety-hook.js";
|
|
6
|
+
import { notePinSetMode, notePinToggle, notePinWrite, notePinAnalogOutput, resolveTrackedRead } from "../pin-state-tracking.js";
|
|
6
7
|
/**
|
|
7
8
|
* Split a comma-joined argument list back into individual arguments, respecting
|
|
8
9
|
* nesting (parens/brackets/braces) and string literals so a comma inside one of
|
|
@@ -326,11 +327,13 @@ export function tryResolveSemanticCall(fnName, args, instance, paramNames, callA
|
|
|
326
327
|
// Try literal resolution first (compile-time 0/1/true/false)
|
|
327
328
|
const numValue = resolveNumericArg(args, 1, instance, paramNames, callArgTexts, paramDefaults);
|
|
328
329
|
if (numValue !== null) {
|
|
330
|
+
notePinWrite(pin, numValue);
|
|
329
331
|
return { operation: "gpio.write", port, pin, value: (numValue ? 1 : 0) };
|
|
330
332
|
}
|
|
331
333
|
// Fall back to runtime expression (e.g. a variable, negated expression)
|
|
332
334
|
const exprValue = resolveSemanticArg(args, 1, instance, paramNames, callArgTexts, paramDefaults);
|
|
333
335
|
if (exprValue !== null) {
|
|
336
|
+
notePinWrite(pin, null);
|
|
334
337
|
return { operation: "gpio.write", port, pin, value: exprValue };
|
|
335
338
|
}
|
|
336
339
|
return null;
|
|
@@ -339,12 +342,17 @@ export function tryResolveSemanticCall(fnName, args, instance, paramNames, callA
|
|
|
339
342
|
const pin = resolveNumericArg(args, 0, instance, paramNames, callArgTexts, paramDefaults);
|
|
340
343
|
if (pin === null)
|
|
341
344
|
return null;
|
|
345
|
+
const tracked = resolveTrackedRead(pin);
|
|
346
|
+
if (tracked !== null) {
|
|
347
|
+
return { operation: "gpio.read", port, pin, trackedValue: tracked };
|
|
348
|
+
}
|
|
342
349
|
return { operation: "gpio.read", port, pin };
|
|
343
350
|
}
|
|
344
351
|
case "gpioToggle": {
|
|
345
352
|
const pin = resolveNumericArg(args, 0, instance, paramNames, callArgTexts, paramDefaults);
|
|
346
353
|
if (pin === null)
|
|
347
354
|
return null;
|
|
355
|
+
notePinToggle(pin);
|
|
348
356
|
return { operation: "gpio.toggle", port, pin };
|
|
349
357
|
}
|
|
350
358
|
case "gpioSetMode": {
|
|
@@ -352,6 +360,7 @@ export function tryResolveSemanticCall(fnName, args, instance, paramNames, callA
|
|
|
352
360
|
const mode = resolveSemanticArg(args, 1, instance, paramNames, callArgTexts, paramDefaults);
|
|
353
361
|
if (pin === null || mode === null)
|
|
354
362
|
return null;
|
|
363
|
+
notePinSetMode(pin, mode);
|
|
355
364
|
return { operation: "gpio.set_mode", port, pin, mode };
|
|
356
365
|
}
|
|
357
366
|
// ── PWM ──
|
|
@@ -360,6 +369,7 @@ export function tryResolveSemanticCall(fnName, args, instance, paramNames, callA
|
|
|
360
369
|
const duty = resolveNumericOrExpression(args, 1, instance, paramNames, callArgTexts, paramDefaults);
|
|
361
370
|
if (pin === null || duty === null)
|
|
362
371
|
return null;
|
|
372
|
+
notePinAnalogOutput(pin);
|
|
363
373
|
return { operation: "pwm.write", port, pin, duty };
|
|
364
374
|
}
|
|
365
375
|
// ── RMT ──
|
|
@@ -1127,6 +1137,7 @@ export function tryResolveSemanticCall(fnName, args, instance, paramNames, callA
|
|
|
1127
1137
|
const duration = resolveNumericOrExpression(args, 2, instance, paramNames, callArgTexts, paramDefaults);
|
|
1128
1138
|
if (pin === null || frequency === null)
|
|
1129
1139
|
return null;
|
|
1140
|
+
notePinAnalogOutput(pin);
|
|
1130
1141
|
return { operation: "tone.play", port, pin, frequency, ...(duration !== null ? { duration } : {}) };
|
|
1131
1142
|
}
|
|
1132
1143
|
case "toneStop": {
|
|
@@ -34,6 +34,9 @@ export function validatePinModeConfig(program) {
|
|
|
34
34
|
const diagnostics = [];
|
|
35
35
|
// Track which receivers have had their mode explicitly set
|
|
36
36
|
const pinModeSet = new Set();
|
|
37
|
+
// Pins currently driven by PWM/tone (since their last digital write or
|
|
38
|
+
// mode change). Reading such a pin has no defined digital level.
|
|
39
|
+
const analogDrivenPins = new Set();
|
|
37
40
|
const checkCuttlefishCall = (receiver, receiverKind, method) => {
|
|
38
41
|
if (!receiverKind || !PIN_RECEIVER_KINDS.has(receiverKind))
|
|
39
42
|
return;
|
|
@@ -98,16 +101,36 @@ export function validatePinModeConfig(program) {
|
|
|
98
101
|
const pinKey = `pin${op.pin}`;
|
|
99
102
|
if (op.operation === 'gpio.set_mode') {
|
|
100
103
|
pinModeSet.add(pinKey);
|
|
104
|
+
analogDrivenPins.delete(pinKey);
|
|
101
105
|
}
|
|
102
|
-
else if (op.operation === '
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
106
|
+
else if (op.operation === 'pwm.write' || op.operation === 'tone.play') {
|
|
107
|
+
analogDrivenPins.add(pinKey);
|
|
108
|
+
}
|
|
109
|
+
else if (op.operation === 'gpio.write' || op.operation === 'gpio.toggle') {
|
|
110
|
+
analogDrivenPins.delete(pinKey);
|
|
111
|
+
}
|
|
112
|
+
else if (op.operation === 'gpio.read') {
|
|
113
|
+
if (analogDrivenPins.has(pinKey)) {
|
|
114
|
+
diagnostics.push({
|
|
115
|
+
severity: 'warning',
|
|
116
|
+
message: `Pin ${op.pin} read while driven by PWM/tone. ` +
|
|
117
|
+
`The pin has no defined digital level while an analog output is active; ` +
|
|
118
|
+
`tracked reads return the last digital write, not the waveform.`,
|
|
119
|
+
filePath: program.fileName,
|
|
120
|
+
code: 'pin-read-while-pwm',
|
|
121
|
+
source: 'pin-mode-validation',
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
else if (!pinModeSet.has(pinKey)) {
|
|
125
|
+
diagnostics.push({
|
|
126
|
+
severity: 'warning',
|
|
127
|
+
message: `Pin ${op.pin} read without prior mode configuration. ` +
|
|
128
|
+
`Call asInput() or inputPullUp() first — reading a floating pin is undefined behavior.`,
|
|
129
|
+
filePath: program.fileName,
|
|
130
|
+
code: 'pin-mode-not-set',
|
|
131
|
+
source: 'pin-mode-validation',
|
|
132
|
+
});
|
|
133
|
+
}
|
|
111
134
|
}
|
|
112
135
|
else if ((op.operation === 'gpio.write' || op.operation === 'gpio.toggle') && !pinModeSet.has(pinKey)) {
|
|
113
136
|
diagnostics.push({
|