@typecad/cuttlefish 1.0.0-alpha.12 → 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 +0 -4
- 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/hal-op-ir.d.ts +19 -0
- package/dist/api/shared/toolchain-types.d.ts +0 -1
- package/dist/cli.js +15 -4
- package/dist/config-loader.d.ts +0 -2
- package/dist/config-loader.js +10 -5
- package/dist/config-schema.d.ts +87 -94
- package/dist/config-schema.js +0 -3
- package/dist/create/debug-artifacts.d.ts +20 -0
- package/dist/create/debug-artifacts.js +69 -0
- 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 +18 -4
- package/dist/emit/emitters/function-emitter-impl.js +7 -1
- package/dist/emit/emitters/line-appender.js +6 -0
- 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/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 +6 -0
- package/dist/ir/program-analysis.js +38 -0
- 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/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 +35 -5
- 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/ui.d.ts +5 -0
- package/dist/utils/ui.js +7 -0
- package/package.json +7 -6
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// debug-artifacts.ts — create-time framework debug profile generation
|
|
3
|
+
//
|
|
4
|
+
// `cuttlefish create` scaffolds a project that has never been built, so the
|
|
5
|
+
// framework toolchain's post-build debug-artifact writer (e.g. Zephyr's
|
|
6
|
+
// writeDebugConfig, which runs after a successful `west build --debug`) has
|
|
7
|
+
// never had a chance to run. Without a .vscode/launch.json, pressing F5 in VS
|
|
8
|
+
// Code just opens the "select a debugger" menu.
|
|
9
|
+
//
|
|
10
|
+
// Frameworks that support native debugging may export
|
|
11
|
+
// `writeProjectDebugArtifacts({ workspaceRoot, buildTarget })` from their
|
|
12
|
+
// package root (see @typecad/framework-zephyr). This helper loads the freshly
|
|
13
|
+
// installed framework package and invokes that export — the framework decides
|
|
14
|
+
// whether the target is debug-capable and what to write. Frameworks without
|
|
15
|
+
// the export (or targets without native debug support) are a silent no-op.
|
|
16
|
+
//
|
|
17
|
+
// Everything here is best-effort: a failed or skipped generation never fails
|
|
18
|
+
// `cuttlefish create`, because the first `--debug` build still writes the
|
|
19
|
+
// artifacts the hard way.
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
import path from "node:path";
|
|
22
|
+
import { fileURLToPath } from "node:url";
|
|
23
|
+
import { loadFrameworkPackage } from "../framework-package.js";
|
|
24
|
+
const cliModuleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
/**
|
|
26
|
+
* Generate the framework's starter debug artifacts for a new project.
|
|
27
|
+
* Returns the workspace-relative paths written (e.g. ['.vscode/launch.json']),
|
|
28
|
+
* or [] when the framework has no debug support, isn't resolvable yet, or the
|
|
29
|
+
* generator failed (warns, never throws).
|
|
30
|
+
*/
|
|
31
|
+
export function generateFrameworkDebugArtifacts(o, loadModule) {
|
|
32
|
+
if (!o.frameworkPackage || !o.workspaceRoot)
|
|
33
|
+
return [];
|
|
34
|
+
// Default loader: prefer the new project's node_modules (populated by the
|
|
35
|
+
// create install step), then the CLI's own module location (monorepo /
|
|
36
|
+
// --no-install flows where the framework is a workspace sibling of the
|
|
37
|
+
// running cuttlefish).
|
|
38
|
+
const loader = loadModule ?? ((packageName) => {
|
|
39
|
+
for (const dir of [o.workspaceRoot, cliModuleDir]) {
|
|
40
|
+
try {
|
|
41
|
+
return loadFrameworkPackage(packageName, dir);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// not resolvable from this location — try the next
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
});
|
|
49
|
+
let mod;
|
|
50
|
+
try {
|
|
51
|
+
mod = loader(o.frameworkPackage);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
if (!mod || typeof mod.writeProjectDebugArtifacts !== "function")
|
|
57
|
+
return [];
|
|
58
|
+
try {
|
|
59
|
+
const written = mod.writeProjectDebugArtifacts({
|
|
60
|
+
workspaceRoot: o.workspaceRoot,
|
|
61
|
+
buildTarget: o.buildTarget,
|
|
62
|
+
});
|
|
63
|
+
return Array.isArray(written) ? written.map(String) : [];
|
|
64
|
+
}
|
|
65
|
+
catch (e) {
|
|
66
|
+
console.warn(`! Debug profile generation failed: ${e.message}`);
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
}
|
package/dist/create/index.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export type { ProjectInstallResult } from './install-deps.js';
|
|
|
7
7
|
export { generateProjectPackageJson, generateProjectTsconfig, generateProjectConfig, generateProjectEnvDts, generateStarterSketch, generateStarterTest, generateStarterSim, generateGitignore, generateEslintConfig, } from './init-templates.js';
|
|
8
8
|
export type { InitProjectOptions } from './init-templates.js';
|
|
9
9
|
export { runInitWizard } from './init-wizard.js';
|
|
10
|
+
export { generateFrameworkDebugArtifacts } from './debug-artifacts.js';
|
|
11
|
+
export type { FrameworkDebugArtifactsOptions, FrameworkModuleLoader } from './debug-artifacts.js';
|
|
10
12
|
export { scaffoldBoardPackages } from './board-codegen.js';
|
|
11
13
|
export type { ScaffoldBoardResult } from './board-codegen.js';
|
|
12
14
|
export { generateFrameworkChecklist } from './board-checklist.js';
|
package/dist/create/index.js
CHANGED
|
@@ -3,6 +3,7 @@ export { FRAMEWORK_CATALOG, frameworksForTarget, frameworkCatalogEntry, detectPa
|
|
|
3
3
|
export { installProjectDependencies, __setProjectInstallRunnerForTest } from './install-deps.js';
|
|
4
4
|
export { generateProjectPackageJson, generateProjectTsconfig, generateProjectConfig, generateProjectEnvDts, generateStarterSketch, generateStarterTest, generateStarterSim, generateGitignore, generateEslintConfig, } from './init-templates.js';
|
|
5
5
|
export { runInitWizard } from './init-wizard.js';
|
|
6
|
+
export { generateFrameworkDebugArtifacts } from './debug-artifacts.js';
|
|
6
7
|
// Board codegen tool (`cuttlefish board add`)
|
|
7
8
|
export { scaffoldBoardPackages } from './board-codegen.js';
|
|
8
9
|
export { generateFrameworkChecklist } from './board-checklist.js';
|
|
@@ -33,4 +33,5 @@ export interface ScaffoldProjectResult {
|
|
|
33
33
|
export declare function scaffoldProject(options: InitProjectOptions, outDir?: string): ScaffoldProjectResult;
|
|
34
34
|
export declare function printInitNextSteps(options: InitProjectOptions, outDir: string, opts?: {
|
|
35
35
|
installed?: boolean;
|
|
36
|
+
debugProfile?: boolean;
|
|
36
37
|
}): void;
|
|
@@ -168,6 +168,11 @@ export function printInitNextSteps(options, outDir, opts = {}) {
|
|
|
168
168
|
console.log(chalk.dim(` npm install`));
|
|
169
169
|
}
|
|
170
170
|
console.log(` ${chalk.cyan("npm run compile")}`);
|
|
171
|
+
if (opts.debugProfile) {
|
|
172
|
+
console.log();
|
|
173
|
+
console.log(chalk.bold.white("To debug (VS Code):"));
|
|
174
|
+
console.log(` ${chalk.cyan("open the folder and press F5")} ${chalk.dim("(builds + flashes, then attaches GDB)")}`);
|
|
175
|
+
}
|
|
171
176
|
if (!options.isNative) {
|
|
172
177
|
const portHint = process.platform === 'win32' ? 'COM4' : '/dev/ttyACM0';
|
|
173
178
|
console.log();
|
|
@@ -149,7 +149,6 @@ const config = {
|
|
|
149
149
|
|
|
150
150
|
// Output options
|
|
151
151
|
output: {
|
|
152
|
-
optimize: 'speed',
|
|
153
152
|
outDir: './out',
|
|
154
153
|
},
|
|
155
154
|
};
|
|
@@ -198,7 +197,6 @@ const config: CuttlefishConfig = {
|
|
|
198
197
|
// Output / build options
|
|
199
198
|
output: {
|
|
200
199
|
framework: '${options.framework}',
|
|
201
|
-
optimize: 'size',
|
|
202
200
|
outDir: './out',
|
|
203
201
|
},
|
|
204
202
|
|
|
@@ -146,8 +146,8 @@ export const RULES = [
|
|
|
146
146
|
},
|
|
147
147
|
],
|
|
148
148
|
},
|
|
149
|
-
{ id: "A18-5-10", title: "No malloc/calloc/realloc", severity: "required", category: "C",
|
|
150
|
-
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,
|
|
151
151
|
knownPatterns: [
|
|
152
152
|
{
|
|
153
153
|
// Offscreen canvas allocation (CuttlefishCanvas16/CuttlefishCanvasMono
|
|
@@ -163,10 +163,24 @@ export const RULES = [
|
|
|
163
163
|
// the alternative (operator new) is the very thing that crashes. The
|
|
164
164
|
// object is placement-constructed on the malloc'd memory and freed via
|
|
165
165
|
// an explicit dtor + free, so the vtable/lifetime are correct.
|
|
166
|
-
|
|
167
|
-
|
|
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.",
|
|
168
169
|
kind: "ts-literal",
|
|
169
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
|
+
},
|
|
170
184
|
],
|
|
171
185
|
},
|
|
172
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)
|
|
@@ -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.
|
|
@@ -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({
|