@typecad/cuttlefish 0.1.0-alpha.1
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/LICENSE +21 -0
- package/README.md +79 -0
- package/dist/api/board-types.d.ts +1 -0
- package/dist/api/board-types.js +4 -0
- package/dist/api/capabilities.d.ts +12 -0
- package/dist/api/capabilities.js +1 -0
- package/dist/api/config.d.ts +120 -0
- package/dist/api/config.js +7 -0
- package/dist/api/index.d.ts +5 -0
- package/dist/api/index.js +2 -0
- package/dist/api/schema/builder.d.ts +14 -0
- package/dist/api/schema/builder.js +15 -0
- package/dist/api/schema/index.d.ts +2 -0
- package/dist/api/schema/index.js +1 -0
- package/dist/api/schema/types.d.ts +257 -0
- package/dist/api/schema/types.js +4 -0
- package/dist/api/shared/async-runtime-static.d.ts +8 -0
- package/dist/api/shared/async-runtime-static.js +201 -0
- package/dist/api/shared/async-types.d.ts +42 -0
- package/dist/api/shared/async-types.js +8 -0
- package/dist/api/shared/board-resolver.d.ts +12 -0
- package/dist/api/shared/board-resolver.js +7 -0
- package/dist/api/shared/cpp-type-ir.d.ts +229 -0
- package/dist/api/shared/cpp-type-ir.js +672 -0
- package/dist/api/shared/display-adapter.d.ts +12 -0
- package/dist/api/shared/display-adapter.js +145 -0
- package/dist/api/shared/display-adapters/eink-mono.d.ts +2 -0
- package/dist/api/shared/display-adapters/eink-mono.js +53 -0
- package/dist/api/shared/display-adapters/sdl.d.ts +2 -0
- package/dist/api/shared/display-adapters/sdl.js +452 -0
- package/dist/api/shared/display-adapters/ssd1309.d.ts +2 -0
- package/dist/api/shared/display-adapters/ssd1309.js +136 -0
- package/dist/api/shared/display-adapters/st7796.d.ts +2 -0
- package/dist/api/shared/display-adapters/st7796.js +132 -0
- package/dist/api/shared/display-capabilities.d.ts +45 -0
- package/dist/api/shared/display-capabilities.js +54 -0
- package/dist/api/shared/display-op-ir.d.ts +62 -0
- package/dist/api/shared/display-op-ir.js +19 -0
- package/dist/api/shared/display-profile.d.ts +265 -0
- package/dist/api/shared/display-profile.js +338 -0
- package/dist/api/shared/graphics-strategy.d.ts +29 -0
- package/dist/api/shared/graphics-strategy.js +11 -0
- package/dist/api/shared/hal-op-ir.d.ts +393 -0
- package/dist/api/shared/hal-op-ir.js +18 -0
- package/dist/api/shared/index.d.ts +26 -0
- package/dist/api/shared/index.js +21 -0
- package/dist/api/shared/ir-core.d.ts +363 -0
- package/dist/api/shared/ir-core.js +8 -0
- package/dist/api/shared/ir-declarations.d.ts +201 -0
- package/dist/api/shared/ir-declarations.js +26 -0
- package/dist/api/shared/ir.d.ts +85 -0
- package/dist/api/shared/ir.js +13 -0
- package/dist/api/shared/platform-strategy.d.ts +402 -0
- package/dist/api/shared/platform-strategy.js +14 -0
- package/dist/api/shared/polyfill-helper-registry.d.ts +11 -0
- package/dist/api/shared/polyfill-helper-registry.js +86 -0
- package/dist/api/shared/polyfill-types.d.ts +33 -0
- package/dist/api/shared/polyfill-types.js +76 -0
- package/dist/api/shared/promise-runtime.d.ts +10 -0
- package/dist/api/shared/promise-runtime.js +244 -0
- package/dist/api/shared/snprintf-types.d.ts +29 -0
- package/dist/api/shared/snprintf-types.js +8 -0
- package/dist/api/shared/string-method-registry.d.ts +53 -0
- package/dist/api/shared/string-method-registry.js +168 -0
- package/dist/api/shared/toolchain-types.d.ts +63 -0
- package/dist/api/shared/toolchain-types.js +71 -0
- package/dist/api/shared/types.d.ts +50 -0
- package/dist/api/shared/types.js +6 -0
- package/dist/ast/parse.d.ts +2 -0
- package/dist/ast/parse.js +4 -0
- package/dist/cache.d.ts +26 -0
- package/dist/cache.js +95 -0
- package/dist/cli-utils.d.ts +37 -0
- package/dist/cli-utils.js +250 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +635 -0
- package/dist/config-loader.d.ts +78 -0
- package/dist/config-loader.js +508 -0
- package/dist/config-schema.d.ts +233 -0
- package/dist/config-schema.js +83 -0
- package/dist/create/board-checklist.d.ts +2 -0
- package/dist/create/board-checklist.js +52 -0
- package/dist/create/board-codegen.d.ts +9 -0
- package/dist/create/board-codegen.js +238 -0
- package/dist/create/board-generators.d.ts +12 -0
- package/dist/create/board-generators.js +652 -0
- package/dist/create/board-spec.d.ts +640 -0
- package/dist/create/board-spec.js +215 -0
- package/dist/create/eslint-rules-template.d.ts +2 -0
- package/dist/create/eslint-rules-template.js +672 -0
- package/dist/create/index.d.ts +11 -0
- package/dist/create/index.js +8 -0
- package/dist/create/init-scaffold.d.ts +27 -0
- package/dist/create/init-scaffold.js +150 -0
- package/dist/create/init-templates.d.ts +24 -0
- package/dist/create/init-templates.js +379 -0
- package/dist/create/init-wizard.d.ts +8 -0
- package/dist/create/init-wizard.js +168 -0
- package/dist/debug/breakpoint-loader.d.ts +24 -0
- package/dist/debug/breakpoint-loader.js +137 -0
- package/dist/debug/index.d.ts +3 -0
- package/dist/debug/index.js +7 -0
- package/dist/debug/preprocessor.d.ts +9 -0
- package/dist/debug/preprocessor.js +214 -0
- package/dist/debug/types.d.ts +32 -0
- package/dist/debug/types.js +6 -0
- package/dist/diagnostics/diagnostics-report.d.ts +51 -0
- package/dist/diagnostics/diagnostics-report.js +333 -0
- package/dist/diagnostics/json-schema.d.ts +138 -0
- package/dist/diagnostics/json-schema.js +27 -0
- package/dist/diagnostics/md-writer.d.ts +10 -0
- package/dist/diagnostics/md-writer.js +336 -0
- package/dist/diagnostics/mermaid-builder.d.ts +65 -0
- package/dist/diagnostics/mermaid-builder.js +681 -0
- package/dist/emit/cpp-emitter.d.ts +6 -0
- package/dist/emit/cpp-emitter.js +48 -0
- package/dist/emit/emitters/class-emitter.d.ts +2 -0
- package/dist/emit/emitters/class-emitter.js +523 -0
- package/dist/emit/emitters/emitter-context.d.ts +174 -0
- package/dist/emit/emitters/emitter-context.js +1 -0
- package/dist/emit/emitters/entrypoint-synthesizer.d.ts +2 -0
- package/dist/emit/emitters/entrypoint-synthesizer.js +175 -0
- package/dist/emit/emitters/function-emitter-impl.d.ts +19 -0
- package/dist/emit/emitters/function-emitter-impl.js +428 -0
- package/dist/emit/emitters/line-appender.d.ts +15 -0
- package/dist/emit/emitters/line-appender.js +344 -0
- package/dist/emit/emitters/namespace-emitter.d.ts +2 -0
- package/dist/emit/emitters/namespace-emitter.js +324 -0
- package/dist/emit/emitters/output-finalizer.d.ts +4 -0
- package/dist/emit/emitters/output-finalizer.js +267 -0
- package/dist/emit/emitters/setup.d.ts +3 -0
- package/dist/emit/emitters/setup.js +851 -0
- package/dist/emit/emitters/top-level-prep.d.ts +2 -0
- package/dist/emit/emitters/top-level-prep.js +706 -0
- package/dist/emit/emitters/type-decl-emitter.d.ts +2 -0
- package/dist/emit/emitters/type-decl-emitter.js +251 -0
- package/dist/emit/emitters/ui-emitter.d.ts +21 -0
- package/dist/emit/emitters/ui-emitter.js +564 -0
- package/dist/emit/enum-emitter.d.ts +1 -0
- package/dist/emit/enum-emitter.js +1 -0
- package/dist/emit/expression-renderer.d.ts +283 -0
- package/dist/emit/expression-renderer.js +1477 -0
- package/dist/emit/native-helpers-emitter.d.ts +11 -0
- package/dist/emit/native-helpers-emitter.js +34 -0
- package/dist/emit/route-hal-op.d.ts +5 -0
- package/dist/emit/route-hal-op.js +13 -0
- package/dist/emit/snprintf-helpers.d.ts +17 -0
- package/dist/emit/snprintf-helpers.js +462 -0
- package/dist/emit/statement-renderer.d.ts +189 -0
- package/dist/emit/statement-renderer.js +948 -0
- package/dist/emit/utils/async-state-machine.d.ts +42 -0
- package/dist/emit/utils/async-state-machine.js +343 -0
- package/dist/emit/utils/comment-helpers.d.ts +20 -0
- package/dist/emit/utils/comment-helpers.js +31 -0
- package/dist/emit/utils/cpp-helpers.d.ts +47 -0
- package/dist/emit/utils/cpp-helpers.js +85 -0
- package/dist/emit/utils/include-resolver.d.ts +58 -0
- package/dist/emit/utils/include-resolver.js +155 -0
- package/dist/emit/utils/index.d.ts +8 -0
- package/dist/emit/utils/index.js +12 -0
- package/dist/emit/utils/type-inference.d.ts +159 -0
- package/dist/emit/utils/type-inference.js +559 -0
- package/dist/eslint-check.d.ts +10 -0
- package/dist/eslint-check.js +102 -0
- package/dist/framework-package.d.ts +10 -0
- package/dist/framework-package.js +58 -0
- package/dist/framework-registry.d.ts +39 -0
- package/dist/framework-registry.js +30 -0
- package/dist/incremental-cache.d.ts +116 -0
- package/dist/incremental-cache.js +335 -0
- package/dist/ir/adc-range-validation.d.ts +11 -0
- package/dist/ir/adc-range-validation.js +250 -0
- package/dist/ir/ast-node-utils.d.ts +8 -0
- package/dist/ir/ast-node-utils.js +32 -0
- package/dist/ir/board-pin-utils.d.ts +13 -0
- package/dist/ir/board-pin-utils.js +65 -0
- package/dist/ir/board-resolver.d.ts +36 -0
- package/dist/ir/board-resolver.js +437 -0
- package/dist/ir/build-ir-state.d.ts +167 -0
- package/dist/ir/build-ir-state.js +306 -0
- package/dist/ir/build-ir.d.ts +2 -0
- package/dist/ir/build-ir.js +808 -0
- package/dist/ir/call-graph.d.ts +35 -0
- package/dist/ir/call-graph.js +194 -0
- package/dist/ir/compile-time-only.d.ts +2 -0
- package/dist/ir/compile-time-only.js +16 -0
- package/dist/ir/declaration-builders.d.ts +9 -0
- package/dist/ir/declaration-builders.js +608 -0
- package/dist/ir/entry-points.d.ts +30 -0
- package/dist/ir/entry-points.js +191 -0
- package/dist/ir/expression-to-ir.d.ts +5 -0
- package/dist/ir/expression-to-ir.js +2249 -0
- package/dist/ir/extract-property-chain.d.ts +10 -0
- package/dist/ir/extract-property-chain.js +30 -0
- package/dist/ir/feature-prescan.d.ts +3 -0
- package/dist/ir/feature-prescan.js +43 -0
- package/dist/ir/feature-registry.d.ts +30 -0
- package/dist/ir/feature-registry.js +816 -0
- package/dist/ir/filter.d.ts +7 -0
- package/dist/ir/filter.js +85 -0
- package/dist/ir/function-builder.d.ts +19 -0
- package/dist/ir/function-builder.js +670 -0
- package/dist/ir/hal/hal-emitter.d.ts +62 -0
- package/dist/ir/hal/hal-emitter.js +735 -0
- package/dist/ir/hal/hal-parser.d.ts +61 -0
- package/dist/ir/hal/hal-parser.js +473 -0
- package/dist/ir/hal/hal-plugins.d.ts +34 -0
- package/dist/ir/hal/hal-plugins.js +706 -0
- package/dist/ir/hal-resolver.d.ts +5 -0
- package/dist/ir/hal-resolver.js +5 -0
- package/dist/ir/heap-analysis.d.ts +8 -0
- package/dist/ir/heap-analysis.js +350 -0
- package/dist/ir/heap-array-validation.d.ts +24 -0
- package/dist/ir/heap-array-validation.js +29 -0
- package/dist/ir/identifier-collector.d.ts +18 -0
- package/dist/ir/identifier-collector.js +439 -0
- package/dist/ir/interrupt-analysis.d.ts +39 -0
- package/dist/ir/interrupt-analysis.js +467 -0
- package/dist/ir/memory-budget-validation.d.ts +10 -0
- package/dist/ir/memory-budget-validation.js +86 -0
- package/dist/ir/namespace-builder.d.ts +7 -0
- package/dist/ir/namespace-builder.js +152 -0
- package/dist/ir/ownership-analysis.d.ts +9 -0
- package/dist/ir/ownership-analysis.js +1207 -0
- package/dist/ir/peripheral-ownership.d.ts +3 -0
- package/dist/ir/peripheral-ownership.js +152 -0
- package/dist/ir/peripheral-symbols.d.ts +7 -0
- package/dist/ir/peripheral-symbols.js +57 -0
- package/dist/ir/peripheral-usage.d.ts +54 -0
- package/dist/ir/peripheral-usage.js +691 -0
- package/dist/ir/peripheral-validation.d.ts +8 -0
- package/dist/ir/peripheral-validation.js +77 -0
- package/dist/ir/pin-alias-conflict.d.ts +4 -0
- package/dist/ir/pin-alias-conflict.js +34 -0
- package/dist/ir/pin-capability-validation.d.ts +6 -0
- package/dist/ir/pin-capability-validation.js +413 -0
- package/dist/ir/pin-mode-validation.d.ts +10 -0
- package/dist/ir/pin-mode-validation.js +301 -0
- package/dist/ir/pin-safety.d.ts +11 -0
- package/dist/ir/pin-safety.js +58 -0
- package/dist/ir/program-analysis.d.ts +44 -0
- package/dist/ir/program-analysis.js +589 -0
- package/dist/ir/pulldown-validation.d.ts +7 -0
- package/dist/ir/pulldown-validation.js +49 -0
- package/dist/ir/pwm-timer-sharing.d.ts +4 -0
- package/dist/ir/pwm-timer-sharing.js +93 -0
- package/dist/ir/reachability.d.ts +68 -0
- package/dist/ir/reachability.js +224 -0
- package/dist/ir/register-decorators.d.ts +6 -0
- package/dist/ir/register-decorators.js +49 -0
- package/dist/ir/render-expr.d.ts +4 -0
- package/dist/ir/render-expr.js +127 -0
- package/dist/ir/resource-analysis.d.ts +19 -0
- package/dist/ir/resource-analysis.js +153 -0
- package/dist/ir/statement-to-ir.d.ts +23 -0
- package/dist/ir/statement-to-ir.js +349 -0
- package/dist/ir/symbol-table.d.ts +68 -0
- package/dist/ir/symbol-table.js +179 -0
- package/dist/ir/symbol-types.d.ts +56 -0
- package/dist/ir/symbol-types.js +107 -0
- package/dist/ir/timer0-pwm-timing-conflict.d.ts +4 -0
- package/dist/ir/timer0-pwm-timing-conflict.js +71 -0
- package/dist/ir/timing-validation.d.ts +7 -0
- package/dist/ir/timing-validation.js +95 -0
- package/dist/ir/transformers/array-methods.d.ts +8 -0
- package/dist/ir/transformers/array-methods.js +493 -0
- package/dist/ir/transformers/call-statement.d.ts +5 -0
- package/dist/ir/transformers/call-statement.js +758 -0
- package/dist/ir/transformers/callback-context-registry.d.ts +20 -0
- package/dist/ir/transformers/callback-context-registry.js +55 -0
- package/dist/ir/transformers/canvas-lowering.d.ts +42 -0
- package/dist/ir/transformers/canvas-lowering.js +249 -0
- package/dist/ir/transformers/control-flow.d.ts +8 -0
- package/dist/ir/transformers/control-flow.js +508 -0
- package/dist/ir/transformers/expressions.d.ts +7 -0
- package/dist/ir/transformers/expressions.js +406 -0
- package/dist/ir/transformers/hal-call-resolver.d.ts +29 -0
- package/dist/ir/transformers/hal-call-resolver.js +274 -0
- package/dist/ir/transformers/hal-emit-helpers.d.ts +14 -0
- package/dist/ir/transformers/hal-emit-helpers.js +63 -0
- package/dist/ir/transformers/namespace-methods.d.ts +20 -0
- package/dist/ir/transformers/namespace-methods.js +110 -0
- package/dist/ir/transformers/register-assignment.d.ts +5 -0
- package/dist/ir/transformers/register-assignment.js +56 -0
- package/dist/ir/transformers/ui-call-resolver.d.ts +149 -0
- package/dist/ir/transformers/ui-call-resolver.js +1136 -0
- package/dist/ir/transformers/ui-callback-lowering.d.ts +62 -0
- package/dist/ir/transformers/ui-callback-lowering.js +623 -0
- package/dist/ir/transformers/ui-mount.d.ts +20 -0
- package/dist/ir/transformers/ui-mount.js +35 -0
- package/dist/ir/transformers/ui-reactive.d.ts +68 -0
- package/dist/ir/transformers/ui-reactive.js +129 -0
- package/dist/ir/transformers/variables.d.ts +14 -0
- package/dist/ir/transformers/variables.js +1045 -0
- package/dist/ir/try-catch-validation.d.ts +26 -0
- package/dist/ir/try-catch-validation.js +128 -0
- package/dist/ir/type-resolution.d.ts +31 -0
- package/dist/ir/type-resolution.js +977 -0
- package/dist/ir/ui-element-auto-wire.d.ts +50 -0
- package/dist/ir/ui-element-auto-wire.js +218 -0
- package/dist/ir/unit-suspicion-validation.d.ts +12 -0
- package/dist/ir/unit-suspicion-validation.js +296 -0
- package/dist/ir/utils/walk-ir.d.ts +6 -0
- package/dist/ir/utils/walk-ir.js +162 -0
- package/dist/ir/validation-orchestrator.d.ts +4 -0
- package/dist/ir/validation-orchestrator.js +45 -0
- package/dist/libdef/base-class-resolver.d.ts +23 -0
- package/dist/libdef/base-class-resolver.js +50 -0
- package/dist/libdef/cpp-to-decl.d.ts +34 -0
- package/dist/libdef/cpp-to-decl.js +584 -0
- package/dist/libdef/header-parser.d.ts +53 -0
- package/dist/libdef/header-parser.js +269 -0
- package/dist/libdef/registry.d.ts +10 -0
- package/dist/libdef/registry.js +142 -0
- package/dist/mapping/peripheral-names.d.ts +23 -0
- package/dist/mapping/peripheral-names.js +63 -0
- package/dist/mapping/source-map.d.ts +46 -0
- package/dist/mapping/source-map.js +207 -0
- package/dist/orchestrator/dts-generator.d.ts +17 -0
- package/dist/orchestrator/dts-generator.js +83 -0
- package/dist/orchestrator/graph-builder.d.ts +18 -0
- package/dist/orchestrator/graph-builder.js +218 -0
- package/dist/orchestrator/semantic-facts-verifier.d.ts +38 -0
- package/dist/orchestrator/semantic-facts-verifier.js +121 -0
- package/dist/orchestrator/semantic-facts.d.ts +141 -0
- package/dist/orchestrator/semantic-facts.js +652 -0
- package/dist/orchestrator/type-checker.d.ts +39 -0
- package/dist/orchestrator/type-checker.js +760 -0
- package/dist/platform/async-runtime.d.ts +8 -0
- package/dist/platform/async-runtime.js +26 -0
- package/dist/platform/generic-strategy.d.ts +101 -0
- package/dist/platform/generic-strategy.js +289 -0
- package/dist/platform/index.d.ts +4 -0
- package/dist/platform/index.js +9 -0
- package/dist/platform/registry.d.ts +21 -0
- package/dist/platform/registry.js +42 -0
- package/dist/platform/toolchain.d.ts +4 -0
- package/dist/platform/toolchain.js +43 -0
- package/dist/preview/client.d.ts +1 -0
- package/dist/preview/client.js +137 -0
- package/dist/preview/server.d.ts +5 -0
- package/dist/preview/server.js +199 -0
- package/dist/profiler/index.d.ts +28 -0
- package/dist/profiler/index.js +28 -0
- package/dist/profiler/profiler.d.ts +95 -0
- package/dist/profiler/profiler.js +338 -0
- package/dist/profiler/types.d.ts +87 -0
- package/dist/profiler/types.js +12 -0
- package/dist/stores/display-profile-store.d.ts +16 -0
- package/dist/stores/display-profile-store.js +48 -0
- package/dist/stores/theme-store.d.ts +13 -0
- package/dist/stores/theme-store.js +30 -0
- package/dist/testing.d.ts +45 -0
- package/dist/testing.js +47 -0
- package/dist/transpile/resolution.d.ts +29 -0
- package/dist/transpile/resolution.js +282 -0
- package/dist/transpile.d.ts +3 -0
- package/dist/transpile.js +722 -0
- package/dist/types.d.ts +217 -0
- package/dist/types.js +1 -0
- package/dist/ui/ui-bridge.d.ts +4 -0
- package/dist/ui/ui-bridge.js +34 -0
- package/dist/ui-hook.d.ts +84 -0
- package/dist/ui-hook.js +45 -0
- package/dist/utils/cli.d.ts +3 -0
- package/dist/utils/cli.js +463 -0
- package/dist/utils/fs.d.ts +4 -0
- package/dist/utils/fs.js +23 -0
- package/dist/utils/logger.d.ts +2 -0
- package/dist/utils/logger.js +8 -0
- package/dist/utils/strings.d.ts +35 -0
- package/dist/utils/strings.js +142 -0
- package/dist/utils/ui.d.ts +66 -0
- package/dist/utils/ui.js +149 -0
- package/dist/watch.d.ts +35 -0
- package/dist/watch.js +111 -0
- package/package.json +127 -0
|
@@ -0,0 +1,1207 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Ownership & Borrowing Safety Analysis
|
|
3
|
+
//
|
|
4
|
+
// Validates Rust-inspired ownership rules at transpile time:
|
|
5
|
+
// 1. Use-after-move: error when reading a moved Owned variable
|
|
6
|
+
// 2. Assign-to-shared: error when assigning to a Shared (immutable borrow)
|
|
7
|
+
// 3. Mutable exclusivity: warning when multiple Mutable borrows exist
|
|
8
|
+
// 4. Borrow mismatch: error passing Shared where Mutable is expected
|
|
9
|
+
// 5. Const suggestion: warning for let variables never reassigned
|
|
10
|
+
//
|
|
11
|
+
// Opt-in: if no ownership types (Shared, Mutable, Owned) are used anywhere
|
|
12
|
+
// in the program, no diagnostics are generated.
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
import { parseCppType, parsedIsPointer } from '../api/shared/cpp-type-ir.js';
|
|
15
|
+
/** True for any `std::`-prefixed type (vector/map/set/tuple/variant/function/string).
|
|
16
|
+
* Replaces the historical `cppType.startsWith('std::')` check. */
|
|
17
|
+
function isStdContainerType(cppType) {
|
|
18
|
+
const ir = parseCppType(cppType);
|
|
19
|
+
switch (ir.kind) {
|
|
20
|
+
case "vector":
|
|
21
|
+
case "map":
|
|
22
|
+
case "set":
|
|
23
|
+
case "tuple":
|
|
24
|
+
case "variant":
|
|
25
|
+
case "function":
|
|
26
|
+
case "string":
|
|
27
|
+
case "smartPointer":
|
|
28
|
+
return true;
|
|
29
|
+
default:
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** Compile-time exhaustiveness check for switch statements on IR kinds. */
|
|
34
|
+
function assertNever(x) {
|
|
35
|
+
throw new Error(`Unhandled IR kind: ${JSON.stringify(x)}`);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Returns true for C++ scalar/primitive types passed cheaply by value.
|
|
39
|
+
* Non-primitives (std::vector, String, structs) should be passed by C++ reference
|
|
40
|
+
* when borrowed via Shared<T> or Mutable<T>.
|
|
41
|
+
*/
|
|
42
|
+
function isPrimitiveCppType(cppType) {
|
|
43
|
+
const t = cppType.trim();
|
|
44
|
+
const primitives = new Set([
|
|
45
|
+
'int', 'float', 'double', 'bool', 'char', 'long', 'void',
|
|
46
|
+
'uint8_t', 'uint16_t', 'uint32_t', 'uint64_t',
|
|
47
|
+
'int8_t', 'int16_t', 'int32_t', 'int64_t',
|
|
48
|
+
'size_t', 'byte', 'word',
|
|
49
|
+
'unsigned int', 'unsigned long', 'unsigned char',
|
|
50
|
+
'signed int', 'signed long', 'signed char',
|
|
51
|
+
]);
|
|
52
|
+
return primitives.has(t);
|
|
53
|
+
}
|
|
54
|
+
/** Scope tracker for ownership analysis. */
|
|
55
|
+
class OwnershipScope {
|
|
56
|
+
constructor(parent) {
|
|
57
|
+
this.parent = parent;
|
|
58
|
+
/** Variables declared in this scope, keyed by name. */
|
|
59
|
+
this.vars = new Map();
|
|
60
|
+
/** Set of variable names that have been moved. */
|
|
61
|
+
this.movedVars = new Set();
|
|
62
|
+
/** Track Mutable variables and their sources for exclusivity checking. */
|
|
63
|
+
this.mutRefSources = new Map(); // source -> [mutRefVarName, ...]
|
|
64
|
+
/** Track parameter ownership kinds for the current function. */
|
|
65
|
+
this.paramKinds = new Map();
|
|
66
|
+
/** By-value struct parameters — mutations on their fields are invisible to callers. */
|
|
67
|
+
this.byValueStructParams = new Set();
|
|
68
|
+
/** Whether any ownership type is used in this scope. */
|
|
69
|
+
this.usesOwnershipTypes = false;
|
|
70
|
+
}
|
|
71
|
+
/** Declare a variable in this scope. */
|
|
72
|
+
declare(name, ownershipKind, isLet, borrowSource, cppType) {
|
|
73
|
+
if (ownershipKind !== 'owned' || borrowSource) {
|
|
74
|
+
this.usesOwnershipTypes = true;
|
|
75
|
+
}
|
|
76
|
+
this.vars.set(name, {
|
|
77
|
+
name,
|
|
78
|
+
ownershipKind,
|
|
79
|
+
moved: false,
|
|
80
|
+
isLet,
|
|
81
|
+
everAssigned: false,
|
|
82
|
+
borrowSource,
|
|
83
|
+
cppType,
|
|
84
|
+
});
|
|
85
|
+
// Track Mutable sources for exclusivity checking
|
|
86
|
+
if (ownershipKind === 'mutable' && borrowSource) {
|
|
87
|
+
const existing = this.mutRefSources.get(borrowSource) ?? [];
|
|
88
|
+
existing.push(name);
|
|
89
|
+
this.mutRefSources.set(borrowSource, existing);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** Mark a variable as moved. */
|
|
93
|
+
move(name) {
|
|
94
|
+
this.movedVars.add(name);
|
|
95
|
+
const v = this.resolve(name);
|
|
96
|
+
if (v) {
|
|
97
|
+
v.moved = true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** Mark a variable as having been assigned to. */
|
|
101
|
+
markAssigned(name) {
|
|
102
|
+
const v = this.resolve(name);
|
|
103
|
+
if (v) {
|
|
104
|
+
v.everAssigned = true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/** Resolve a variable in this scope or parent scopes. */
|
|
108
|
+
resolve(name) {
|
|
109
|
+
const v = this.vars.get(name);
|
|
110
|
+
if (v)
|
|
111
|
+
return v;
|
|
112
|
+
if (this.parent)
|
|
113
|
+
return this.parent.resolve(name);
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
/** Check if a variable has been moved. */
|
|
117
|
+
isMoved(name) {
|
|
118
|
+
if (this.movedVars.has(name))
|
|
119
|
+
return true;
|
|
120
|
+
if (this.parent)
|
|
121
|
+
return this.parent.isMoved(name);
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
/** Get the ownership kind for a variable. */
|
|
125
|
+
getOwnershipKind(name) {
|
|
126
|
+
const v = this.resolve(name);
|
|
127
|
+
return v?.ownershipKind;
|
|
128
|
+
}
|
|
129
|
+
/** Collect all let variables in this scope (for const suggestion). */
|
|
130
|
+
collectLetVariables() {
|
|
131
|
+
const result = [];
|
|
132
|
+
for (const v of this.vars.values()) {
|
|
133
|
+
if (v.isLet && v.ownershipKind === 'owned') {
|
|
134
|
+
result.push(v);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Checks for mutations on by-value struct parameters (no Mutable<T> annotation).
|
|
142
|
+
* Emits ownership-mutate-copy warning — changes are local copies invisible to callers.
|
|
143
|
+
*/
|
|
144
|
+
function checkByValueParamMutations(program, diagnostics) {
|
|
145
|
+
for (const fn of program.functions) {
|
|
146
|
+
const byValueParams = new Set();
|
|
147
|
+
for (const param of fn.parameters) {
|
|
148
|
+
if (!param.ownershipKind &&
|
|
149
|
+
param.cppType &&
|
|
150
|
+
!isPrimitiveCppType(param.cppType) &&
|
|
151
|
+
!parsedIsPointer(param.cppType) &&
|
|
152
|
+
!isStdContainerType(param.cppType)) {
|
|
153
|
+
byValueParams.add(param.name);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (byValueParams.size === 0)
|
|
157
|
+
continue;
|
|
158
|
+
scanStatementsForByValueMutation(fn.statements, fn.originalName, byValueParams, diagnostics);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Recursively scans statement lists for assign/update to fields of by-value params.
|
|
163
|
+
*/
|
|
164
|
+
function scanStatementsForByValueMutation(stmts, fnName, byValueParams, diagnostics) {
|
|
165
|
+
for (const stmt of stmts) {
|
|
166
|
+
if (stmt.kind === 'assign' || stmt.kind === 'update') {
|
|
167
|
+
const target = stmt.target;
|
|
168
|
+
if (target) {
|
|
169
|
+
const dotIdx = target.indexOf('.');
|
|
170
|
+
if (dotIdx > 0) {
|
|
171
|
+
const baseName = target.substring(0, dotIdx);
|
|
172
|
+
if (byValueParams.has(baseName)) {
|
|
173
|
+
diagnostics.push({
|
|
174
|
+
severity: 'warning',
|
|
175
|
+
message: `Mutating '${target}' on by-value parameter '${baseName}' in '${fnName}()' — changes are invisible to the caller.`,
|
|
176
|
+
hint: `Use '${baseName}: Mutable<T>' to pass by mutable reference (T&) instead of a copy.`,
|
|
177
|
+
line: stmt.sourceSpan.startLine,
|
|
178
|
+
column: stmt.sourceSpan.startColumn,
|
|
179
|
+
code: 'ownership-mutate-copy',
|
|
180
|
+
source: 'ownership-analysis',
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// Recurse into nested statement blocks
|
|
187
|
+
if ('body' in stmt && Array.isArray(stmt.body))
|
|
188
|
+
scanStatementsForByValueMutation(stmt.body, fnName, byValueParams, diagnostics);
|
|
189
|
+
if ('thenBranch' in stmt && Array.isArray(stmt.thenBranch))
|
|
190
|
+
scanStatementsForByValueMutation(stmt.thenBranch, fnName, byValueParams, diagnostics);
|
|
191
|
+
if ('elseBranch' in stmt && Array.isArray(stmt.elseBranch))
|
|
192
|
+
scanStatementsForByValueMutation(stmt.elseBranch, fnName, byValueParams, diagnostics);
|
|
193
|
+
if ('cases' in stmt && Array.isArray(stmt.cases)) {
|
|
194
|
+
for (const c of stmt.cases) {
|
|
195
|
+
if (c.body)
|
|
196
|
+
scanStatementsForByValueMutation(c.body, fnName, byValueParams, diagnostics);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if ('tryBlock' in stmt && Array.isArray(stmt.tryBlock))
|
|
200
|
+
scanStatementsForByValueMutation(stmt.tryBlock, fnName, byValueParams, diagnostics);
|
|
201
|
+
if ('catchBlock' in stmt && Array.isArray(stmt.catchBlock))
|
|
202
|
+
scanStatementsForByValueMutation(stmt.catchBlock, fnName, byValueParams, diagnostics);
|
|
203
|
+
if ('finallyBlock' in stmt && Array.isArray(stmt.finallyBlock))
|
|
204
|
+
scanStatementsForByValueMutation(stmt.finallyBlock, fnName, byValueParams, diagnostics);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Validates ownership rules across the entire program.
|
|
209
|
+
*
|
|
210
|
+
* @param program - The program IR to analyze
|
|
211
|
+
* @returns Array of diagnostics for ownership violations
|
|
212
|
+
*/
|
|
213
|
+
export function validateOwnership(program) {
|
|
214
|
+
const diagnostics = [];
|
|
215
|
+
// First pass: check if any ownership types are used anywhere
|
|
216
|
+
let usesOwnershipTypes = false;
|
|
217
|
+
const checkTypeForOwnership = (cppType) => {
|
|
218
|
+
return /\b(Shared|Mutable|Owned)</.test(cppType);
|
|
219
|
+
};
|
|
220
|
+
// Check top-level variable declarations
|
|
221
|
+
for (const stmt of program.topLevelStatements) {
|
|
222
|
+
if (stmt.kind === 'var_decl') {
|
|
223
|
+
if (stmt.ownershipKind) {
|
|
224
|
+
usesOwnershipTypes = true;
|
|
225
|
+
}
|
|
226
|
+
if (checkTypeForOwnership(stmt.cppType)) {
|
|
227
|
+
usesOwnershipTypes = true;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// Check function parameters and bodies
|
|
232
|
+
for (const fn of program.functions) {
|
|
233
|
+
for (const param of fn.parameters) {
|
|
234
|
+
if (param.ownershipKind) {
|
|
235
|
+
usesOwnershipTypes = true;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
for (const stmt of fn.statements) {
|
|
239
|
+
if (stmt.kind === 'var_decl') {
|
|
240
|
+
if (stmt.ownershipKind) {
|
|
241
|
+
usesOwnershipTypes = true;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// If no ownership types are used, skip full ownership validation (opt-in).
|
|
247
|
+
// Still check for by-value struct param mutations — that diagnostic is
|
|
248
|
+
// useful whether or not the user has adopted ownership annotations.
|
|
249
|
+
checkByValueParamMutations(program, diagnostics);
|
|
250
|
+
if (!usesOwnershipTypes) {
|
|
251
|
+
// Still do const suggestion for all let variables
|
|
252
|
+
validateConstSuggestions(program, diagnostics);
|
|
253
|
+
return diagnostics;
|
|
254
|
+
}
|
|
255
|
+
// Second pass: full ownership validation
|
|
256
|
+
// Validate top-level statements
|
|
257
|
+
const globalScope = new OwnershipScope();
|
|
258
|
+
analyzeStatements(program.topLevelStatements, globalScope, diagnostics);
|
|
259
|
+
// Validate each function
|
|
260
|
+
for (const fn of program.functions) {
|
|
261
|
+
const fnScope = new OwnershipScope(globalScope);
|
|
262
|
+
// Register parameters — those with explicit ownership annotations get full
|
|
263
|
+
// borrow tracking; by-value non-primitive params are recorded so we can warn
|
|
264
|
+
// when their fields are mutated (changes won't be visible to the caller).
|
|
265
|
+
for (const param of fn.parameters) {
|
|
266
|
+
const kind = param.ownershipKind;
|
|
267
|
+
if (kind) {
|
|
268
|
+
fnScope.declare(param.name, kind, false, undefined, param.cppType);
|
|
269
|
+
fnScope.usesOwnershipTypes = true;
|
|
270
|
+
}
|
|
271
|
+
else if (param.cppType &&
|
|
272
|
+
!isPrimitiveCppType(param.cppType) &&
|
|
273
|
+
!parsedIsPointer(param.cppType) &&
|
|
274
|
+
!isStdContainerType(param.cppType)) {
|
|
275
|
+
fnScope.byValueStructParams.add(param.name);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
analyzeStatements(fn.statements, fnScope, diagnostics);
|
|
279
|
+
}
|
|
280
|
+
// Const suggestions for ownership-aware code
|
|
281
|
+
validateConstSuggestions(program, diagnostics);
|
|
282
|
+
// Borrow mismatch: Shared<T> arg passed to Mutable<T> param
|
|
283
|
+
checkBorrowMismatch(program, diagnostics);
|
|
284
|
+
return diagnostics;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Analyze a list of statements for ownership violations.
|
|
288
|
+
*/
|
|
289
|
+
function analyzeStatements(stmts, scope, diagnostics) {
|
|
290
|
+
for (const stmt of stmts) {
|
|
291
|
+
analyzeStatement(stmt, scope, diagnostics);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Analyze a single statement for ownership violations.
|
|
296
|
+
*/
|
|
297
|
+
function analyzeStatement(stmt, scope, diagnostics) {
|
|
298
|
+
const span = stmt.sourceSpan;
|
|
299
|
+
switch (stmt.kind) {
|
|
300
|
+
case 'var_decl': {
|
|
301
|
+
const ownershipKind = stmt.ownershipKind;
|
|
302
|
+
// Detect borrow source from initializer
|
|
303
|
+
let borrowSource;
|
|
304
|
+
if (stmt.initializer && ownershipKind && ownershipKind !== 'owned') {
|
|
305
|
+
borrowSource = extractBorrowSource(stmt.initializer);
|
|
306
|
+
}
|
|
307
|
+
// Check initializer for use-after-move and detect moves from Owned variables.
|
|
308
|
+
// Order matters: analyzeExpression runs FIRST to detect pre-existing moves,
|
|
309
|
+
// then we register the new move. This avoids a false positive where we move
|
|
310
|
+
// the variable and then immediately check it again in analyzeExpression.
|
|
311
|
+
if (stmt.initializer) {
|
|
312
|
+
analyzeExpression(stmt.initializer, scope, diagnostics, span);
|
|
313
|
+
// If the initializer is a plain identifier referencing an explicitly Owned variable
|
|
314
|
+
// that hasn't already been moved, check whether this is a move or a borrow.
|
|
315
|
+
// Shared<T> and Mutable<T> destinations are borrows — the source stays alive.
|
|
316
|
+
// Only untyped or Owned<T> destinations trigger a move (ownership transfer).
|
|
317
|
+
const initName = extractIdentifier(stmt.initializer);
|
|
318
|
+
if (initName) {
|
|
319
|
+
const sourceVar = scope.resolve(initName);
|
|
320
|
+
if (sourceVar && sourceVar.ownershipKind === 'owned' && !scope.isMoved(initName)) {
|
|
321
|
+
const isBorrow = ownershipKind === 'shared' || ownershipKind === 'mutable';
|
|
322
|
+
if (!isBorrow) {
|
|
323
|
+
// Move ownership from source to this new variable
|
|
324
|
+
scope.move(initName);
|
|
325
|
+
// ownership-owned-copy: moving a non-primitive is a C++ copy, not a true move
|
|
326
|
+
if (!isPrimitiveCppType(stmt.cppType)) {
|
|
327
|
+
diagnostics.push({
|
|
328
|
+
severity: 'info',
|
|
329
|
+
message: `Moving '${initName}' into '${stmt.name}' creates a C++ copy.`,
|
|
330
|
+
hint: `const ${stmt.name}: Shared = ${initName}; // borrow by reference instead of copying`,
|
|
331
|
+
line: span.startLine,
|
|
332
|
+
column: span.startColumn,
|
|
333
|
+
code: 'ownership-owned-copy',
|
|
334
|
+
source: 'ownership-analysis',
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
// ownership-implicit-copy: unannotated non-primitive copied from a non-Owned variable
|
|
340
|
+
else if (!ownershipKind && !isPrimitiveCppType(stmt.cppType)) {
|
|
341
|
+
const srcVar = scope.resolve(initName);
|
|
342
|
+
if (srcVar && srcVar.ownershipKind !== 'owned') {
|
|
343
|
+
diagnostics.push({
|
|
344
|
+
severity: 'info',
|
|
345
|
+
message: `'${stmt.name}' silently copies '${initName}' — no borrow annotation.`,
|
|
346
|
+
hint: `const ${stmt.name}: Shared = ${initName}; // borrow by const reference, zero copy`,
|
|
347
|
+
line: span.startLine,
|
|
348
|
+
column: span.startColumn,
|
|
349
|
+
code: 'ownership-implicit-copy',
|
|
350
|
+
source: 'ownership-analysis',
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
// ownership-temp-ref-warn: Shared/Mutable assigned from a non-identifier non-primitive
|
|
356
|
+
if (ownershipKind && ownershipKind !== 'owned' && !isPrimitiveCppType(stmt.cppType)) {
|
|
357
|
+
if (stmt.initializer.kind !== 'identifier') {
|
|
358
|
+
const annotLabel = ownershipKind === 'shared' ? 'Shared' : 'Mutable';
|
|
359
|
+
const storageKw = ownershipKind === 'shared' ? 'const' : 'let';
|
|
360
|
+
diagnostics.push({
|
|
361
|
+
severity: 'warning',
|
|
362
|
+
message: `'${stmt.name}: ${annotLabel}' borrows a temporary — C++ cannot bind a reference to an rvalue. The emitter will fall back to a copy.`,
|
|
363
|
+
hint: `${storageKw} _tmp: Owned = ...;\nconst ${stmt.name}: ${annotLabel} = _tmp;`,
|
|
364
|
+
line: span.startLine,
|
|
365
|
+
column: span.startColumn,
|
|
366
|
+
code: 'ownership-temp-ref-warn',
|
|
367
|
+
source: 'ownership-analysis',
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (ownershipKind) {
|
|
373
|
+
scope.declare(stmt.name, ownershipKind, stmt.storage === 'let', borrowSource, stmt.cppType);
|
|
374
|
+
}
|
|
375
|
+
break;
|
|
376
|
+
}
|
|
377
|
+
case 'assign': {
|
|
378
|
+
// Check: assignment to Shared variable
|
|
379
|
+
const targetKind = scope.getOwnershipKind(stmt.target);
|
|
380
|
+
if (targetKind === 'shared') {
|
|
381
|
+
const targetVar = scope.resolve(stmt.target);
|
|
382
|
+
const typeAnnotation = (targetVar?.cppType && targetVar.cppType !== 'auto') ? `: Shared<${targetVar.cppType}>` : ': Shared';
|
|
383
|
+
diagnostics.push({
|
|
384
|
+
severity: 'error',
|
|
385
|
+
message: `Cannot assign to '${stmt.target}' — it is an immutable borrow.`,
|
|
386
|
+
hint: `change '${stmt.target}${typeAnnotation}' → '${stmt.target}: Mutable' // Mutable allows mutation`,
|
|
387
|
+
line: span.startLine,
|
|
388
|
+
column: span.startColumn,
|
|
389
|
+
code: 'ownership-assign-to-ref',
|
|
390
|
+
source: 'ownership-analysis',
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
// Check: assignment from Owned variable (move).
|
|
394
|
+
// Order matters: analyzeExpression runs FIRST to detect pre-existing moves,
|
|
395
|
+
// then we register the new move. This avoids a false positive.
|
|
396
|
+
if (stmt.value && stmt.operator === '=') {
|
|
397
|
+
analyzeExpression(stmt.value, scope, diagnostics, span);
|
|
398
|
+
// If the value is a plain identifier referencing an Owned variable that
|
|
399
|
+
// hasn't already been moved, register the move — but only if the target
|
|
400
|
+
// is not a borrow (Shared/Mutable). Borrows don't transfer ownership.
|
|
401
|
+
const sourceName = extractIdentifier(stmt.value);
|
|
402
|
+
if (sourceName) {
|
|
403
|
+
const sourceVar = scope.resolve(sourceName);
|
|
404
|
+
const targetOwnership = scope.getOwnershipKind(stmt.target);
|
|
405
|
+
const isTargetBorrow = targetOwnership === 'shared' || targetOwnership === 'mutable';
|
|
406
|
+
if (sourceVar && sourceVar.ownershipKind === 'owned' && !scope.isMoved(sourceName) && sourceVar.isLet && !isTargetBorrow) {
|
|
407
|
+
scope.move(sourceName);
|
|
408
|
+
}
|
|
409
|
+
if (isTargetBorrow && sourceVar && sourceVar.ownershipKind === 'owned') {
|
|
410
|
+
const targetVar = scope.resolve(stmt.target);
|
|
411
|
+
if (targetVar) {
|
|
412
|
+
targetVar.borrowSource = sourceName;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
else if (stmt.value) {
|
|
418
|
+
analyzeExpression(stmt.value, scope, diagnostics, span);
|
|
419
|
+
}
|
|
420
|
+
scope.markAssigned(stmt.target);
|
|
421
|
+
// ownership-mutate-copy: mutating a field on a by-value struct parameter.
|
|
422
|
+
// Changes are local to the function and invisible to the caller.
|
|
423
|
+
const dotIdx = stmt.target.indexOf('.');
|
|
424
|
+
if (dotIdx > 0) {
|
|
425
|
+
const baseName = stmt.target.substring(0, dotIdx);
|
|
426
|
+
if (scope.byValueStructParams.has(baseName)) {
|
|
427
|
+
diagnostics.push({
|
|
428
|
+
severity: 'warning',
|
|
429
|
+
message: `Mutating '${stmt.target}' on by-value parameter '${baseName}' — changes are invisible to the caller.`,
|
|
430
|
+
hint: `Use '${baseName}: Mutable<T>' to pass by mutable reference (T&) instead of a copy.`,
|
|
431
|
+
line: span.startLine,
|
|
432
|
+
column: span.startColumn,
|
|
433
|
+
code: 'ownership-mutate-copy',
|
|
434
|
+
source: 'ownership-analysis',
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
break;
|
|
439
|
+
}
|
|
440
|
+
case 'update': {
|
|
441
|
+
// Check: update to Shared variable
|
|
442
|
+
const updateTargetKind = scope.getOwnershipKind(stmt.target);
|
|
443
|
+
if (updateTargetKind === 'shared') {
|
|
444
|
+
const updateTargetVar = scope.resolve(stmt.target);
|
|
445
|
+
const typeAnnotation = (updateTargetVar?.cppType && updateTargetVar.cppType !== 'auto') ? `: Shared<${updateTargetVar.cppType}>` : ': Shared';
|
|
446
|
+
diagnostics.push({
|
|
447
|
+
severity: 'error',
|
|
448
|
+
message: `Cannot update '${stmt.target}' — it is an immutable borrow.`,
|
|
449
|
+
hint: `change '${stmt.target}${typeAnnotation}' → '${stmt.target}: Mutable' // Mutable allows mutation`,
|
|
450
|
+
line: span.startLine,
|
|
451
|
+
column: span.startColumn,
|
|
452
|
+
code: 'ownership-assign-to-ref',
|
|
453
|
+
source: 'ownership-analysis',
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
// Check: update to moved variable
|
|
457
|
+
if (scope.isMoved(stmt.target)) {
|
|
458
|
+
diagnostics.push({
|
|
459
|
+
severity: 'error',
|
|
460
|
+
message: `'${stmt.target}' was moved and cannot be used again.`,
|
|
461
|
+
hint: `const ${stmt.target}_ref: Shared = ${stmt.target}; // add this before the move`,
|
|
462
|
+
line: span.startLine,
|
|
463
|
+
column: span.startColumn,
|
|
464
|
+
code: 'ownership-use-after-move',
|
|
465
|
+
source: 'ownership-analysis',
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
scope.markAssigned(stmt.target);
|
|
469
|
+
// ownership-mutate-copy (update path): same check as assign above
|
|
470
|
+
const upDotIdx = stmt.target.indexOf('.');
|
|
471
|
+
if (upDotIdx > 0) {
|
|
472
|
+
const upBaseName = stmt.target.substring(0, upDotIdx);
|
|
473
|
+
if (scope.byValueStructParams.has(upBaseName)) {
|
|
474
|
+
diagnostics.push({
|
|
475
|
+
severity: 'warning',
|
|
476
|
+
message: `Mutating '${stmt.target}' on by-value parameter '${upBaseName}' — changes are invisible to the caller.`,
|
|
477
|
+
hint: `Use '${upBaseName}: Mutable<T>' to pass by mutable reference (T&) instead of a copy.`,
|
|
478
|
+
line: span.startLine,
|
|
479
|
+
column: span.startColumn,
|
|
480
|
+
code: 'ownership-mutate-copy',
|
|
481
|
+
source: 'ownership-analysis',
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
case 'call': {
|
|
488
|
+
// Check arguments for use-after-move
|
|
489
|
+
for (const arg of stmt.args) {
|
|
490
|
+
analyzeExpression(arg, scope, diagnostics, span);
|
|
491
|
+
}
|
|
492
|
+
break;
|
|
493
|
+
}
|
|
494
|
+
case 'return': {
|
|
495
|
+
if (stmt.value) {
|
|
496
|
+
analyzeExpression(stmt.value, scope, diagnostics, span);
|
|
497
|
+
// Check: returning a borrow whose source is an Owned variable — dangling reference
|
|
498
|
+
const retName = extractIdentifier(stmt.value);
|
|
499
|
+
if (retName) {
|
|
500
|
+
const retVar = scope.resolve(retName);
|
|
501
|
+
if (retVar && (retVar.ownershipKind === 'shared' || retVar.ownershipKind === 'mutable') && retVar.borrowSource) {
|
|
502
|
+
const source = scope.resolve(retVar.borrowSource);
|
|
503
|
+
if (source && source.ownershipKind === 'owned') {
|
|
504
|
+
diagnostics.push({
|
|
505
|
+
severity: 'error',
|
|
506
|
+
message: `Returning '${retName}' borrows '${retVar.borrowSource}' which will be destroyed when this function returns — dangling reference.`,
|
|
507
|
+
hint: `return ${retVar.borrowSource} directly as Owned, or change the function to accept '${retVar.borrowSource}: Shared' as a parameter`,
|
|
508
|
+
line: span.startLine,
|
|
509
|
+
column: span.startColumn,
|
|
510
|
+
code: 'ownership-return-local-ref',
|
|
511
|
+
source: 'ownership-analysis',
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
break;
|
|
518
|
+
}
|
|
519
|
+
case 'if': {
|
|
520
|
+
if (stmt.condition)
|
|
521
|
+
analyzeExpression(stmt.condition, scope, diagnostics, span);
|
|
522
|
+
const thenScope = new OwnershipScope(scope);
|
|
523
|
+
analyzeStatements(stmt.thenBranch, thenScope, diagnostics);
|
|
524
|
+
checkDanglingBorrowsOnScopeExit(thenScope, diagnostics, span);
|
|
525
|
+
if (stmt.elseBranch) {
|
|
526
|
+
const elseScope = new OwnershipScope(scope);
|
|
527
|
+
analyzeStatements(stmt.elseBranch, elseScope, diagnostics);
|
|
528
|
+
checkDanglingBorrowsOnScopeExit(elseScope, diagnostics, span);
|
|
529
|
+
}
|
|
530
|
+
break;
|
|
531
|
+
}
|
|
532
|
+
case 'while':
|
|
533
|
+
case 'do_while': {
|
|
534
|
+
if (stmt.condition)
|
|
535
|
+
analyzeExpression(stmt.condition, scope, diagnostics, span);
|
|
536
|
+
const loopScope = new OwnershipScope(scope);
|
|
537
|
+
analyzeStatements(stmt.body, loopScope, diagnostics);
|
|
538
|
+
checkDanglingBorrowsOnScopeExit(loopScope, diagnostics, span);
|
|
539
|
+
break;
|
|
540
|
+
}
|
|
541
|
+
case 'for': {
|
|
542
|
+
const forScope = new OwnershipScope(scope);
|
|
543
|
+
if (stmt.initializer)
|
|
544
|
+
analyzeStatement(stmt.initializer, forScope, diagnostics);
|
|
545
|
+
if (stmt.condition)
|
|
546
|
+
analyzeExpression(stmt.condition, forScope, diagnostics, span);
|
|
547
|
+
if (stmt.increment)
|
|
548
|
+
analyzeStatement(stmt.increment, forScope, diagnostics);
|
|
549
|
+
analyzeStatements(stmt.body, forScope, diagnostics);
|
|
550
|
+
checkDanglingBorrowsOnScopeExit(forScope, diagnostics, span);
|
|
551
|
+
break;
|
|
552
|
+
}
|
|
553
|
+
case 'for_of':
|
|
554
|
+
case 'for_in': {
|
|
555
|
+
const forScope = new OwnershipScope(scope);
|
|
556
|
+
if (stmt.variable)
|
|
557
|
+
analyzeStatement(stmt.variable, forScope, diagnostics);
|
|
558
|
+
if ('iterable' in stmt && stmt.iterable)
|
|
559
|
+
analyzeExpression(stmt.iterable, forScope, diagnostics, span);
|
|
560
|
+
if ('object' in stmt && stmt.object)
|
|
561
|
+
analyzeExpression(stmt.object, forScope, diagnostics, span);
|
|
562
|
+
analyzeStatements(stmt.body, forScope, diagnostics);
|
|
563
|
+
checkDanglingBorrowsOnScopeExit(forScope, diagnostics, span);
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
case 'switch': {
|
|
567
|
+
if (stmt.expression)
|
|
568
|
+
analyzeExpression(stmt.expression, scope, diagnostics, span);
|
|
569
|
+
for (const c of stmt.cases) {
|
|
570
|
+
const caseScope = new OwnershipScope(scope);
|
|
571
|
+
if (c.value)
|
|
572
|
+
analyzeExpression(c.value, caseScope, diagnostics, span);
|
|
573
|
+
analyzeStatements(c.body, caseScope, diagnostics);
|
|
574
|
+
checkDanglingBorrowsOnScopeExit(caseScope, diagnostics, span);
|
|
575
|
+
}
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
578
|
+
case 'block': {
|
|
579
|
+
const blockScope = new OwnershipScope(scope);
|
|
580
|
+
analyzeStatements(stmt.body, blockScope, diagnostics);
|
|
581
|
+
checkDanglingBorrowsOnScopeExit(blockScope, diagnostics, span);
|
|
582
|
+
break;
|
|
583
|
+
}
|
|
584
|
+
case 'labeled': {
|
|
585
|
+
const labelScope = new OwnershipScope(scope);
|
|
586
|
+
analyzeStatements(stmt.body, labelScope, diagnostics);
|
|
587
|
+
checkDanglingBorrowsOnScopeExit(labelScope, diagnostics, span);
|
|
588
|
+
break;
|
|
589
|
+
}
|
|
590
|
+
case 'try': {
|
|
591
|
+
const tryScope = new OwnershipScope(scope);
|
|
592
|
+
analyzeStatements(stmt.tryBlock, tryScope, diagnostics);
|
|
593
|
+
checkDanglingBorrowsOnScopeExit(tryScope, diagnostics, span);
|
|
594
|
+
if (stmt.catchBlock) {
|
|
595
|
+
const catchScope = new OwnershipScope(scope);
|
|
596
|
+
analyzeStatements(stmt.catchBlock, catchScope, diagnostics);
|
|
597
|
+
checkDanglingBorrowsOnScopeExit(catchScope, diagnostics, span);
|
|
598
|
+
}
|
|
599
|
+
if (stmt.finallyBlock) {
|
|
600
|
+
const finallyScope = new OwnershipScope(scope);
|
|
601
|
+
analyzeStatements(stmt.finallyBlock, finallyScope, diagnostics);
|
|
602
|
+
checkDanglingBorrowsOnScopeExit(finallyScope, diagnostics, span);
|
|
603
|
+
}
|
|
604
|
+
break;
|
|
605
|
+
}
|
|
606
|
+
case 'throw': {
|
|
607
|
+
if (stmt.value)
|
|
608
|
+
analyzeExpression(stmt.value, scope, diagnostics, span);
|
|
609
|
+
break;
|
|
610
|
+
}
|
|
611
|
+
case 'break':
|
|
612
|
+
case 'continue':
|
|
613
|
+
case 'hal-op':
|
|
614
|
+
case 'yield':
|
|
615
|
+
case 'super_call':
|
|
616
|
+
break;
|
|
617
|
+
default:
|
|
618
|
+
assertNever(stmt);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Analyze an expression for ownership violations (use-after-move).
|
|
623
|
+
*
|
|
624
|
+
* @param expr - The expression to analyze
|
|
625
|
+
* @param scope - The current ownership scope
|
|
626
|
+
* @param diagnostics - Accumulator for diagnostics
|
|
627
|
+
* @param fallbackSpan - SourceSpan from the enclosing statement, used when
|
|
628
|
+
* the expression itself doesn't carry location info
|
|
629
|
+
*/
|
|
630
|
+
function analyzeExpression(expr, scope, diagnostics, fallbackSpan) {
|
|
631
|
+
if (!expr || typeof expr !== 'object' || !expr.kind)
|
|
632
|
+
return;
|
|
633
|
+
const span = expr.kind === 'callback' ? expr.sourceSpan : fallbackSpan;
|
|
634
|
+
switch (expr.kind) {
|
|
635
|
+
case 'identifier': {
|
|
636
|
+
const name = expr.value;
|
|
637
|
+
if (scope.isMoved(name)) {
|
|
638
|
+
diagnostics.push({
|
|
639
|
+
severity: 'error',
|
|
640
|
+
message: `'${name}' was moved and cannot be used again.`,
|
|
641
|
+
hint: `const ${name}_ref: Shared = ${name}; // add this before the move`,
|
|
642
|
+
line: span.startLine,
|
|
643
|
+
column: span.startColumn,
|
|
644
|
+
code: 'ownership-use-after-move',
|
|
645
|
+
source: 'ownership-analysis',
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
break;
|
|
649
|
+
}
|
|
650
|
+
case 'binary': {
|
|
651
|
+
analyzeExpression(expr.left, scope, diagnostics, span);
|
|
652
|
+
analyzeExpression(expr.right, scope, diagnostics, span);
|
|
653
|
+
break;
|
|
654
|
+
}
|
|
655
|
+
case 'unary': {
|
|
656
|
+
analyzeExpression(expr.operand, scope, diagnostics, span);
|
|
657
|
+
break;
|
|
658
|
+
}
|
|
659
|
+
case 'ternary': {
|
|
660
|
+
analyzeExpression(expr.condition, scope, diagnostics, span);
|
|
661
|
+
analyzeExpression(expr.whenTrue, scope, diagnostics, span);
|
|
662
|
+
analyzeExpression(expr.whenFalse, scope, diagnostics, span);
|
|
663
|
+
break;
|
|
664
|
+
}
|
|
665
|
+
case 'await': {
|
|
666
|
+
analyzeExpression(expr.value, scope, diagnostics, span);
|
|
667
|
+
break;
|
|
668
|
+
}
|
|
669
|
+
case 'array': {
|
|
670
|
+
for (const el of expr.elements) {
|
|
671
|
+
analyzeExpression(el, scope, diagnostics, span);
|
|
672
|
+
}
|
|
673
|
+
break;
|
|
674
|
+
}
|
|
675
|
+
case 'object': {
|
|
676
|
+
for (const field of expr.fields) {
|
|
677
|
+
analyzeExpression(field.value, scope, diagnostics, span);
|
|
678
|
+
}
|
|
679
|
+
break;
|
|
680
|
+
}
|
|
681
|
+
case 'string_concat': {
|
|
682
|
+
for (const part of expr.parts) {
|
|
683
|
+
analyzeExpression(part, scope, diagnostics, span);
|
|
684
|
+
}
|
|
685
|
+
break;
|
|
686
|
+
}
|
|
687
|
+
case 'template_string': {
|
|
688
|
+
analyzeExpression(expr.expression, scope, diagnostics, span);
|
|
689
|
+
break;
|
|
690
|
+
}
|
|
691
|
+
case 'property-access': {
|
|
692
|
+
analyzeExpression(expr.object, scope, diagnostics, span);
|
|
693
|
+
break;
|
|
694
|
+
}
|
|
695
|
+
case 'instanceof': {
|
|
696
|
+
analyzeExpression(expr.object, scope, diagnostics, span);
|
|
697
|
+
break;
|
|
698
|
+
}
|
|
699
|
+
case 'spread_array': {
|
|
700
|
+
analyzeExpression(expr.spreadExpr, scope, diagnostics, span);
|
|
701
|
+
for (const el of expr.additionalElements) {
|
|
702
|
+
analyzeExpression(el, scope, diagnostics, span);
|
|
703
|
+
}
|
|
704
|
+
break;
|
|
705
|
+
}
|
|
706
|
+
case 'raw': {
|
|
707
|
+
const matches = expr.value.match(/[A-Za-z_][A-Za-z0-9_]*/g);
|
|
708
|
+
if (matches) {
|
|
709
|
+
for (const match of matches) {
|
|
710
|
+
if (scope.isMoved(match)) {
|
|
711
|
+
diagnostics.push({
|
|
712
|
+
severity: 'error',
|
|
713
|
+
message: `'${match}' was moved and cannot be used again.`,
|
|
714
|
+
hint: `const ${match}_ref: Shared = ${match}; // add this before the move`,
|
|
715
|
+
line: span.startLine,
|
|
716
|
+
column: span.startColumn,
|
|
717
|
+
code: 'ownership-use-after-move',
|
|
718
|
+
source: 'ownership-analysis',
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
break;
|
|
724
|
+
}
|
|
725
|
+
case 'callback': {
|
|
726
|
+
for (const s of expr.statements) {
|
|
727
|
+
analyzeStatement(s, scope, diagnostics);
|
|
728
|
+
}
|
|
729
|
+
break;
|
|
730
|
+
}
|
|
731
|
+
case 'lambda': {
|
|
732
|
+
for (const s of expr.body) {
|
|
733
|
+
analyzeStatement(s, scope, diagnostics);
|
|
734
|
+
}
|
|
735
|
+
break;
|
|
736
|
+
}
|
|
737
|
+
case 'method-call': {
|
|
738
|
+
for (const arg of expr.args) {
|
|
739
|
+
analyzeExpression(arg, scope, diagnostics, span);
|
|
740
|
+
}
|
|
741
|
+
break;
|
|
742
|
+
}
|
|
743
|
+
case 'element-access': {
|
|
744
|
+
analyzeExpression(expr.object, scope, diagnostics, span);
|
|
745
|
+
analyzeExpression(expr.index, scope, diagnostics, span);
|
|
746
|
+
break;
|
|
747
|
+
}
|
|
748
|
+
case 'tuple-access': {
|
|
749
|
+
analyzeExpression(expr.object, scope, diagnostics, span);
|
|
750
|
+
break;
|
|
751
|
+
}
|
|
752
|
+
case 'paren': {
|
|
753
|
+
analyzeExpression(expr.inner, scope, diagnostics, span);
|
|
754
|
+
break;
|
|
755
|
+
}
|
|
756
|
+
case 'number':
|
|
757
|
+
case 'string':
|
|
758
|
+
case 'boolean':
|
|
759
|
+
case 'hal-expr':
|
|
760
|
+
break;
|
|
761
|
+
default:
|
|
762
|
+
assertNever(expr);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Extract the identifier name from a simple expression.
|
|
767
|
+
* Returns undefined for complex expressions.
|
|
768
|
+
*/
|
|
769
|
+
function extractIdentifier(expr) {
|
|
770
|
+
if (expr.kind === 'identifier')
|
|
771
|
+
return expr.value;
|
|
772
|
+
return undefined;
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Extract the borrow source from an initializer expression.
|
|
776
|
+
* For `let ref: Shared<T> = source`, returns "source".
|
|
777
|
+
*/
|
|
778
|
+
function extractBorrowSource(expr) {
|
|
779
|
+
return extractIdentifier(expr);
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Validate const suggestions: warn about `let` variables that are never reassigned.
|
|
783
|
+
* This runs regardless of whether ownership types are used.
|
|
784
|
+
*/
|
|
785
|
+
function validateConstSuggestions(program, diagnostics) {
|
|
786
|
+
// Methods whose C++ lowering mutates the receiver (so a `const` binding
|
|
787
|
+
// mutated through them must be demoted to non-const).
|
|
788
|
+
const MUTATING_METHODS = new Set([
|
|
789
|
+
'push', 'push_back', 'pop', 'pop_back', 'shift', '__tc_shift',
|
|
790
|
+
'unshift', '__tc_unshift', 'splice', '__tc_splice1', '__tc_splice2',
|
|
791
|
+
'sort', '__tc_sort', '__tc_sort_fn', 'fill', '__tc_fill',
|
|
792
|
+
'__tc_fill3', 'reverse', '__tc_reverse', 'clear',
|
|
793
|
+
// Set.add() -> insert, Map/Set.delete() -> erase (demo #15 fix C)
|
|
794
|
+
'insert', 'erase',
|
|
795
|
+
]);
|
|
796
|
+
// Every lexical statement body in the program. Demotion (const-content-
|
|
797
|
+
// mutated) and the suggest-const global-assignment scan must reach class
|
|
798
|
+
// methods, getters, setters, constructors, and namespace-scoped functions —
|
|
799
|
+
// not just top-level statements and free `function`s. Without this, a
|
|
800
|
+
// `const`-bound loop variable (or collection) mutated inside a class method
|
|
801
|
+
// is never demoted, so the emitter keeps `const T&` and g++ rejects the
|
|
802
|
+
// mutation (demo #17). Each body is analyzed with its own scope-local maps
|
|
803
|
+
// (see analyzeScope below), so this only enumerates the bodies; it does not
|
|
804
|
+
// merge their scopes.
|
|
805
|
+
const allStatementBodies = [program.topLevelStatements];
|
|
806
|
+
for (const fn of program.functions)
|
|
807
|
+
allStatementBodies.push(fn.statements);
|
|
808
|
+
for (const cls of program.classes) {
|
|
809
|
+
for (const method of cls.methods)
|
|
810
|
+
allStatementBodies.push(method.statements);
|
|
811
|
+
for (const getter of cls.getters)
|
|
812
|
+
allStatementBodies.push(getter.statements);
|
|
813
|
+
for (const setter of cls.setters)
|
|
814
|
+
allStatementBodies.push(setter.statements);
|
|
815
|
+
if (cls.constructor)
|
|
816
|
+
allStatementBodies.push(cls.constructor.statements);
|
|
817
|
+
}
|
|
818
|
+
// Namespaces are recursive (a namespace can nest classes/namespaces).
|
|
819
|
+
const collectNamespaceBodies = (ns) => {
|
|
820
|
+
for (const fn of ns.functions)
|
|
821
|
+
allStatementBodies.push(fn.statements);
|
|
822
|
+
for (const cls of ns.classes) {
|
|
823
|
+
for (const method of cls.methods)
|
|
824
|
+
allStatementBodies.push(method.statements);
|
|
825
|
+
for (const getter of cls.getters)
|
|
826
|
+
allStatementBodies.push(getter.statements);
|
|
827
|
+
for (const setter of cls.setters)
|
|
828
|
+
allStatementBodies.push(setter.statements);
|
|
829
|
+
if (cls.constructor)
|
|
830
|
+
allStatementBodies.push(cls.constructor.statements);
|
|
831
|
+
}
|
|
832
|
+
for (const child of ns.children ?? [])
|
|
833
|
+
collectNamespaceBodies(child);
|
|
834
|
+
};
|
|
835
|
+
for (const ns of program.namespaces)
|
|
836
|
+
collectNamespaceBodies(ns);
|
|
837
|
+
// Collect every plain-identifier assignment / update target across the whole
|
|
838
|
+
// program. A `let` declared in one scope may be reassigned by a *different*
|
|
839
|
+
// function that closes over it (e.g. a top-level `let counter` reassigned in
|
|
840
|
+
// `main()`), so the suggest-const check must be suppressed for any name that
|
|
841
|
+
// is assigned anywhere. Demotion (const-content-mutated), by contrast, is
|
|
842
|
+
// resolved scope-locally below — a mutation in one function must not demote a
|
|
843
|
+
// same-named `const` in a sibling function (demo #16 gap #2).
|
|
844
|
+
const globallyAssignedNames = new Set();
|
|
845
|
+
const collectAssignedNames = (stmts) => {
|
|
846
|
+
for (const stmt of stmts) {
|
|
847
|
+
if (stmt.kind === 'assign' || stmt.kind === 'update') {
|
|
848
|
+
// Only a bare-identifier target counts as "this var is reassigned";
|
|
849
|
+
// dotted/bracket targets (out.a, arr[i]) mutate contents, not the binding.
|
|
850
|
+
if (/^[A-Za-z_$][\w$]*$/.test(stmt.target)) {
|
|
851
|
+
globallyAssignedNames.add(stmt.target);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
const nested = getNestedStatements(stmt);
|
|
855
|
+
if (nested)
|
|
856
|
+
collectAssignedNames(nested);
|
|
857
|
+
}
|
|
858
|
+
};
|
|
859
|
+
for (const body of allStatementBodies)
|
|
860
|
+
collectAssignedNames(body);
|
|
861
|
+
// Walk one lexical scope (a top-level or function body) in a single pass,
|
|
862
|
+
// registering declarations and detecting mutations against the SAME
|
|
863
|
+
// scope-local maps. This is critical: the previous implementation ran two
|
|
864
|
+
// separate passes over all functions with flat name-keyed maps, so a
|
|
865
|
+
// `const labels` in one function collided with a `let labels` in another
|
|
866
|
+
// (demo #16 gap #2 — a read-only `const` Map was wrongly demoted because a
|
|
867
|
+
// same-named binding in a sibling function was mutated). Scope-local maps
|
|
868
|
+
// make declaration and mutation resolve within their own function.
|
|
869
|
+
const analyzeScope = (stmts) => {
|
|
870
|
+
const letVars = new Map();
|
|
871
|
+
const constVars = new Map();
|
|
872
|
+
const walk = (statements) => {
|
|
873
|
+
for (const stmt of statements) {
|
|
874
|
+
if (stmt.kind === 'var_decl') {
|
|
875
|
+
if (stmt.storage === 'let') {
|
|
876
|
+
letVars.set(stmt.name, { name: stmt.name, everAssigned: false, stmt, span: stmt.sourceSpan });
|
|
877
|
+
}
|
|
878
|
+
else if (stmt.storage === 'const') {
|
|
879
|
+
// Track const decls whose contents could be mutated via a method
|
|
880
|
+
// or element/member assignment; the mutation checks below decide
|
|
881
|
+
// whether to demote.
|
|
882
|
+
constVars.set(stmt.name, { stmt, span: stmt.sourceSpan });
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
if (stmt.kind === 'assign') {
|
|
886
|
+
const entry = letVars.get(stmt.target);
|
|
887
|
+
if (entry)
|
|
888
|
+
entry.everAssigned = true;
|
|
889
|
+
// Element assignment on a `const` array/map (e.g. `arr[i] = x` where
|
|
890
|
+
// `arr` is `const`) compiles in TS but fails against the emitted
|
|
891
|
+
// `const std::vector<T>` / `const std::map<K,V>`. Demote the binding.
|
|
892
|
+
// The target string is the lowered C++ lvalue, e.g. `arr[0]`/`m[key]`.
|
|
893
|
+
const bracket = stmt.target.indexOf('[');
|
|
894
|
+
if (bracket > 0) {
|
|
895
|
+
const baseName = stmt.target.slice(0, bracket);
|
|
896
|
+
// A let array/map mutated via element assignment (arr[i] = x or
|
|
897
|
+
// m.set(k,v), which lowers to m[k]=v) is effectively reassigned —
|
|
898
|
+
// mark it so ownership-suggest-const does not wrongly recommend
|
|
899
|
+
// making it const (which would then fail g++). (demo #15 fix C)
|
|
900
|
+
const letEntry = letVars.get(baseName);
|
|
901
|
+
if (letEntry)
|
|
902
|
+
letEntry.everAssigned = true;
|
|
903
|
+
const constEntry = constVars.get(baseName);
|
|
904
|
+
if (constEntry && constEntry.stmt.storage === 'const') {
|
|
905
|
+
constEntry.stmt.storage = 'let';
|
|
906
|
+
diagnostics.push({
|
|
907
|
+
severity: 'info',
|
|
908
|
+
message: `'${baseName}' is declared 'const' but its contents are mutated via index assignment — demoted to non-const in C++ so the mutation compiles.`,
|
|
909
|
+
line: constEntry.span.startLine,
|
|
910
|
+
column: constEntry.span.startColumn,
|
|
911
|
+
code: 'ownership-const-content-mutated',
|
|
912
|
+
source: 'ownership-analysis',
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
// Member assignment on a `const` struct local (e.g. `out.a = 5` where
|
|
917
|
+
// `out` is `const Pair`) compiles in TS but fails against the emitted
|
|
918
|
+
// `const Pair out` (read-only aggregate). Demote the binding. The
|
|
919
|
+
// target is a dotted lvalue like `out.a` or `out->a`.
|
|
920
|
+
const dot = stmt.target.indexOf('.');
|
|
921
|
+
const arrow = stmt.target.indexOf('->');
|
|
922
|
+
const sepIdx = dot >= 0 ? dot : arrow;
|
|
923
|
+
if (sepIdx > 0) {
|
|
924
|
+
const baseName = stmt.target.slice(0, sepIdx);
|
|
925
|
+
const constEntry = constVars.get(baseName);
|
|
926
|
+
if (constEntry && constEntry.stmt.storage === 'const') {
|
|
927
|
+
constEntry.stmt.storage = 'let';
|
|
928
|
+
diagnostics.push({
|
|
929
|
+
severity: 'info',
|
|
930
|
+
message: `'${baseName}' is declared 'const' but a field is mutated via member assignment — demoted to non-const in C++ so the mutation compiles.`,
|
|
931
|
+
line: constEntry.span.startLine,
|
|
932
|
+
column: constEntry.span.startColumn,
|
|
933
|
+
code: 'ownership-const-content-mutated',
|
|
934
|
+
source: 'ownership-analysis',
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
if (stmt.kind === 'update') {
|
|
940
|
+
const entry = letVars.get(stmt.target);
|
|
941
|
+
if (entry)
|
|
942
|
+
entry.everAssigned = true;
|
|
943
|
+
// A `++`/`--` on a member of a `const` local (e.g. `t.hits++` where
|
|
944
|
+
// `t` is a const loop variable) is the UpdateExpression analogue of
|
|
945
|
+
// the member-assignment demotion above. The target string is the
|
|
946
|
+
// lowered lvalue, e.g. `t.hits` / `t->hits`. Demote the const base.
|
|
947
|
+
const dot = stmt.target.indexOf('.');
|
|
948
|
+
const arrow = stmt.target.indexOf('->');
|
|
949
|
+
const sepIdx = dot >= 0 ? dot : arrow;
|
|
950
|
+
if (sepIdx > 0) {
|
|
951
|
+
const baseName = stmt.target.slice(0, sepIdx);
|
|
952
|
+
const constEntry = constVars.get(baseName);
|
|
953
|
+
if (constEntry && constEntry.stmt.storage === 'const') {
|
|
954
|
+
constEntry.stmt.storage = 'let';
|
|
955
|
+
diagnostics.push({
|
|
956
|
+
severity: 'info',
|
|
957
|
+
message: `'${baseName}' is declared 'const' but a field is mutated via ++/-- — demoted to non-const in C++ so the mutation compiles.`,
|
|
958
|
+
line: constEntry.span.startLine,
|
|
959
|
+
column: constEntry.span.startColumn,
|
|
960
|
+
code: 'ownership-const-content-mutated',
|
|
961
|
+
source: 'ownership-analysis',
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
// A `let`/`const` collection mutated via a mutating method call
|
|
967
|
+
// (e.g. arr.push(x)) is effectively reassigned — the C++ emitter must
|
|
968
|
+
// keep the binding non-const so push_back/splice/etc. compile. The
|
|
969
|
+
// callee is the lowered C++ name, e.g. "arr.push_back".
|
|
970
|
+
if (stmt.kind === 'call' && typeof stmt.callee === 'string') {
|
|
971
|
+
const callee = stmt.callee;
|
|
972
|
+
const dot = callee.lastIndexOf('.');
|
|
973
|
+
if (dot > 0) {
|
|
974
|
+
const receiver = callee.slice(0, dot);
|
|
975
|
+
const method = callee.slice(dot + 1);
|
|
976
|
+
if (MUTATING_METHODS.has(method)) {
|
|
977
|
+
const entry = letVars.get(receiver);
|
|
978
|
+
if (entry)
|
|
979
|
+
entry.everAssigned = true;
|
|
980
|
+
// Demote a `const` binding whose contents are mutated via a
|
|
981
|
+
// method call: TS permits this, but the emitted
|
|
982
|
+
// `const std::vector<T>` (or std::map/std::string) rejects
|
|
983
|
+
// `.push_back`/etc. Flip the IR storage to `let` so the
|
|
984
|
+
// emitter drops the `const` qualifier, and surface an
|
|
985
|
+
// info-diagnostic so the rewrite isn't silent.
|
|
986
|
+
const constEntry = constVars.get(receiver);
|
|
987
|
+
if (constEntry && constEntry.stmt.storage === 'const') {
|
|
988
|
+
constEntry.stmt.storage = 'let';
|
|
989
|
+
diagnostics.push({
|
|
990
|
+
severity: 'info',
|
|
991
|
+
message: `'${receiver}' is declared 'const' but its contents are mutated via .${method}() — demoted to non-const in C++ so the mutation compiles.`,
|
|
992
|
+
line: constEntry.span.startLine,
|
|
993
|
+
column: constEntry.span.startColumn,
|
|
994
|
+
code: 'ownership-const-content-mutated',
|
|
995
|
+
source: 'ownership-analysis',
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
// Recurse into nested statements (shares this scope's maps — a nested
|
|
1002
|
+
// block can see and mutate the enclosing function's locals).
|
|
1003
|
+
const nested = getNestedStatements(stmt);
|
|
1004
|
+
if (nested) {
|
|
1005
|
+
walk(nested);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
};
|
|
1009
|
+
walk(stmts);
|
|
1010
|
+
// Promote `let` bindings that are never reassigned (in this scope or any
|
|
1011
|
+
// other) to `const` in the emitted C++. The transpiler's whole-program
|
|
1012
|
+
// reassignment analysis proves the binding is never written after init —
|
|
1013
|
+
// information avr-gcc cannot recover across translation units. Emitting
|
|
1014
|
+
// `const` lets the compiler place the value in flash/ROM and enables
|
|
1015
|
+
// constant folding. The inverse demotion (const→let when a member is
|
|
1016
|
+
// mutated) already mutates `.storage` the same way (see above), so the
|
|
1017
|
+
// emit path already handles both directions.
|
|
1018
|
+
for (const entry of letVars.values()) {
|
|
1019
|
+
if (!entry.everAssigned && !globallyAssignedNames.has(entry.name)) {
|
|
1020
|
+
entry.stmt.storage = 'const';
|
|
1021
|
+
diagnostics.push({
|
|
1022
|
+
severity: 'info',
|
|
1023
|
+
message: `'${entry.name}' is never reassigned — emitted as \`const\` so the C++ compiler can place it in ROM and fold it.`,
|
|
1024
|
+
line: entry.span.startLine,
|
|
1025
|
+
column: entry.span.startColumn,
|
|
1026
|
+
code: 'ownership-suggest-const',
|
|
1027
|
+
source: 'ownership-analysis',
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
};
|
|
1032
|
+
for (const body of allStatementBodies)
|
|
1033
|
+
analyzeScope(body);
|
|
1034
|
+
}
|
|
1035
|
+
/**
|
|
1036
|
+
* Check for borrow mismatch: a Shared<T>-annotated variable passed as a Mutable<T> parameter.
|
|
1037
|
+
* Runs as a separate top-level pass so it doesn't need to thread state through the main
|
|
1038
|
+
* recursive analysis.
|
|
1039
|
+
*/
|
|
1040
|
+
function checkBorrowMismatch(program, diagnostics) {
|
|
1041
|
+
// Build function parameter ownership map
|
|
1042
|
+
const fnParamKinds = new Map();
|
|
1043
|
+
for (const fn of program.functions) {
|
|
1044
|
+
fnParamKinds.set(fn.originalName, fn.parameters.map(p => p.ownershipKind));
|
|
1045
|
+
}
|
|
1046
|
+
// Build a flat varName → ownershipKind map from a list of statements (shallow, no nested)
|
|
1047
|
+
const buildVarKinds = (stmts, seed) => {
|
|
1048
|
+
const map = seed ?? new Map();
|
|
1049
|
+
for (const stmt of stmts) {
|
|
1050
|
+
if (stmt.kind === 'var_decl') {
|
|
1051
|
+
if (stmt.ownershipKind)
|
|
1052
|
+
map.set(stmt.name, stmt.ownershipKind);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
return map;
|
|
1056
|
+
};
|
|
1057
|
+
// Scan statements for call sites and check ownership mismatch on arguments
|
|
1058
|
+
const scanCalls = (stmts, varKinds) => {
|
|
1059
|
+
for (const stmt of stmts) {
|
|
1060
|
+
if (stmt.kind === 'call') {
|
|
1061
|
+
const paramKinds = fnParamKinds.get(stmt.callee);
|
|
1062
|
+
if (paramKinds) {
|
|
1063
|
+
for (let i = 0; i < stmt.args.length; i++) {
|
|
1064
|
+
const paramKind = paramKinds[i];
|
|
1065
|
+
const arg = stmt.args[i];
|
|
1066
|
+
if (paramKind === 'mutable' && arg?.kind === 'identifier') {
|
|
1067
|
+
const argKind = varKinds.get(arg.value);
|
|
1068
|
+
if (argKind === 'shared') {
|
|
1069
|
+
diagnostics.push({
|
|
1070
|
+
severity: 'error',
|
|
1071
|
+
message: `Cannot pass '${arg.value}' (immutable Shared) to '${stmt.callee}' which expects a mutable borrow.`,
|
|
1072
|
+
hint: `change '${arg.value}: Shared = ...' → '${arg.value}: Mutable = ...'`,
|
|
1073
|
+
line: stmt.sourceSpan.startLine,
|
|
1074
|
+
column: stmt.sourceSpan.startColumn,
|
|
1075
|
+
code: 'ownership-borrow-mismatch',
|
|
1076
|
+
source: 'ownership-analysis',
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
// Recurse into nested blocks
|
|
1084
|
+
const nested = getNestedStatements(stmt);
|
|
1085
|
+
if (nested)
|
|
1086
|
+
scanCalls(nested, varKinds);
|
|
1087
|
+
}
|
|
1088
|
+
};
|
|
1089
|
+
// Check top-level statements
|
|
1090
|
+
scanCalls(program.topLevelStatements, buildVarKinds(program.topLevelStatements));
|
|
1091
|
+
// Check each function (include parameters in the varKinds map)
|
|
1092
|
+
for (const fn of program.functions) {
|
|
1093
|
+
const varKinds = buildVarKinds(fn.statements);
|
|
1094
|
+
for (const param of fn.parameters) {
|
|
1095
|
+
const kind = param.ownershipKind;
|
|
1096
|
+
if (kind)
|
|
1097
|
+
varKinds.set(param.name, kind);
|
|
1098
|
+
}
|
|
1099
|
+
scanCalls(fn.statements, varKinds);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
/**
|
|
1103
|
+
* After a child scope exits, check whether any variable in an ancestor scope borrows
|
|
1104
|
+
* an owned variable that was declared only in the exiting child scope. Those borrows
|
|
1105
|
+
* are now dangling references in the generated C++.
|
|
1106
|
+
*/
|
|
1107
|
+
function checkDanglingBorrowsOnScopeExit(exitingScope, diagnostics, span) {
|
|
1108
|
+
// Collect the names of owned variables that are being destroyed
|
|
1109
|
+
const exitingOwnedNames = new Set();
|
|
1110
|
+
for (const v of exitingScope.vars.values()) {
|
|
1111
|
+
if (v.ownershipKind === 'owned') {
|
|
1112
|
+
exitingOwnedNames.add(v.name);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
if (exitingOwnedNames.size === 0)
|
|
1116
|
+
return;
|
|
1117
|
+
// Walk up ancestor scopes looking for borrows whose source is one of those variables
|
|
1118
|
+
let ancestor = exitingScope.parent;
|
|
1119
|
+
while (ancestor) {
|
|
1120
|
+
for (const v of ancestor.vars.values()) {
|
|
1121
|
+
if ((v.ownershipKind === 'shared' || v.ownershipKind === 'mutable') &&
|
|
1122
|
+
v.borrowSource &&
|
|
1123
|
+
exitingOwnedNames.has(v.borrowSource)) {
|
|
1124
|
+
diagnostics.push({
|
|
1125
|
+
severity: 'error',
|
|
1126
|
+
message: `'${v.name}' borrows '${v.borrowSource}' which goes out of scope here — potential dangling reference.`,
|
|
1127
|
+
hint: `move '${v.borrowSource}' to the outer scope, or ensure '${v.name}' does not outlive it`,
|
|
1128
|
+
line: span.startLine,
|
|
1129
|
+
column: span.startColumn,
|
|
1130
|
+
code: 'ownership-dangling-borrow',
|
|
1131
|
+
source: 'ownership-analysis',
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
ancestor = ancestor.parent;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
/**
|
|
1139
|
+
* Get nested statements from a compound statement.
|
|
1140
|
+
*/
|
|
1141
|
+
function getNestedStatements(stmt) {
|
|
1142
|
+
switch (stmt.kind) {
|
|
1143
|
+
case 'if': {
|
|
1144
|
+
return [...stmt.thenBranch, ...(stmt.elseBranch ?? [])];
|
|
1145
|
+
}
|
|
1146
|
+
case 'while':
|
|
1147
|
+
case 'do_while': {
|
|
1148
|
+
return stmt.body;
|
|
1149
|
+
}
|
|
1150
|
+
case 'for': {
|
|
1151
|
+
const result = [];
|
|
1152
|
+
if (stmt.initializer)
|
|
1153
|
+
result.push(stmt.initializer);
|
|
1154
|
+
if (stmt.increment)
|
|
1155
|
+
result.push(stmt.increment);
|
|
1156
|
+
result.push(...stmt.body);
|
|
1157
|
+
return result;
|
|
1158
|
+
}
|
|
1159
|
+
case 'for_of':
|
|
1160
|
+
case 'for_in': {
|
|
1161
|
+
const result = [];
|
|
1162
|
+
result.push(stmt.variable);
|
|
1163
|
+
result.push(...stmt.body);
|
|
1164
|
+
return result;
|
|
1165
|
+
}
|
|
1166
|
+
case 'switch': {
|
|
1167
|
+
const result = [];
|
|
1168
|
+
for (const c of stmt.cases) {
|
|
1169
|
+
result.push(...c.body);
|
|
1170
|
+
}
|
|
1171
|
+
return result;
|
|
1172
|
+
}
|
|
1173
|
+
case 'block':
|
|
1174
|
+
case 'labeled': {
|
|
1175
|
+
return stmt.body;
|
|
1176
|
+
}
|
|
1177
|
+
case 'try': {
|
|
1178
|
+
return [
|
|
1179
|
+
...stmt.tryBlock,
|
|
1180
|
+
...(stmt.catchBlock ?? []),
|
|
1181
|
+
...(stmt.finallyBlock ?? []),
|
|
1182
|
+
];
|
|
1183
|
+
}
|
|
1184
|
+
case 'call': {
|
|
1185
|
+
const result = [];
|
|
1186
|
+
for (const arg of stmt.args) {
|
|
1187
|
+
if (arg.kind === 'callback' && arg.statements) {
|
|
1188
|
+
result.push(...arg.statements);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
return result.length > 0 ? result : undefined;
|
|
1192
|
+
}
|
|
1193
|
+
case 'var_decl':
|
|
1194
|
+
case 'assign':
|
|
1195
|
+
case 'update':
|
|
1196
|
+
case 'return':
|
|
1197
|
+
case 'break':
|
|
1198
|
+
case 'continue':
|
|
1199
|
+
case 'throw':
|
|
1200
|
+
case 'hal-op':
|
|
1201
|
+
case 'yield':
|
|
1202
|
+
case 'super_call':
|
|
1203
|
+
return undefined;
|
|
1204
|
+
default:
|
|
1205
|
+
assertNever(stmt);
|
|
1206
|
+
}
|
|
1207
|
+
}
|