@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,1136 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// UI call resolver — intercepts ui.mount / ui.signal / ui.bind calls and
|
|
3
|
+
// lowers them to IR. This is the seam between author UI calls and the
|
|
4
|
+
// lowered tables / HAL ops.
|
|
5
|
+
//
|
|
6
|
+
// Pure-helper state (import map, signals, bindings) is module-scoped and reset
|
|
7
|
+
// per transpile run via resetUICallState(). The full tryResolveUICall() reads
|
|
8
|
+
// the active strategy from the compilation context (getContext().activeStrategy)
|
|
9
|
+
// and is invoked from call-statement.ts after tryResolveHALMethod.
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
import ts from "typescript";
|
|
12
|
+
import { makeSourceSpan } from "../ast-node-utils.js";
|
|
13
|
+
import { halOpsToIR } from "./hal-emit-helpers.js";
|
|
14
|
+
import { resolveMount } from "./ui-mount.js";
|
|
15
|
+
import { emitSignalDecl, recordListBinding, getListBindingsCount, recordInputBinding, getInputBindingsCount, resetInputBindings } from "./ui-reactive.js";
|
|
16
|
+
import { requireUIHook } from "../../ui-hook.js";
|
|
17
|
+
import { expressionToIR } from "../expression-to-ir.js";
|
|
18
|
+
import { renderExprAsText } from "../render-expr.js";
|
|
19
|
+
import { getDisplayProfile } from "../../stores/display-profile-store.js";
|
|
20
|
+
import { effectiveDisplaySize } from "../../api/shared/display-profile.js";
|
|
21
|
+
import { lowerCallbackStatements, lowerNamedOrInlineCallback, renameIdentifiersInStatements, renderStatementsCompact, resolveCallbackArg, resetCallbackLoweringState, resolveColorLiterals, resolveColorIR, } from "./ui-callback-lowering.js";
|
|
22
|
+
import { resolveDrawCanvasCall, resetCanvasBindings } from "./canvas-lowering.js";
|
|
23
|
+
import { getContext } from "../build-ir-state.js";
|
|
24
|
+
import { getCurrentIrTypeScope } from "../symbol-types.js";
|
|
25
|
+
import { inferExprCppType } from "../type-resolution.js";
|
|
26
|
+
import { escapeCppStringLiteral, escapeSnprintfFormatFragment } from "../../utils/strings.js";
|
|
27
|
+
// ── Pure-helper state ───────────────────────────────────────────────────────
|
|
28
|
+
/** Maps an imported UI tree name (e.g. "screen") → its resolved .ui.html path. */
|
|
29
|
+
const uiModuleImports = new Map();
|
|
30
|
+
/** Maps "treeName.elemId" (e.g. "screen.led") → node index in the UI tree. */
|
|
31
|
+
const elementValueMap = new Map();
|
|
32
|
+
/** Register an element's node index for .value access. Called during
|
|
33
|
+
* build-ir.ts's import processing (alongside registerUIModuleImport).
|
|
34
|
+
*
|
|
35
|
+
* Element ids here come from a tree walk over the SAME styled tree that
|
|
36
|
+
* resolveNodeIndex walks, so not-found is structurally unreachable in
|
|
37
|
+
* practice. We use resolveNodeIndexOrZero (which falls back to 0 on -1)
|
|
38
|
+
* rather than threading diagnostics through build-ir's tree-walk path. */
|
|
39
|
+
export function registerElementValue(treeName, elemId, htmlPath) {
|
|
40
|
+
const nodeIndex = resolveNodeIndexOrZero(htmlPath, elemId);
|
|
41
|
+
elementValueMap.set(`${treeName}.${elemId}`, nodeIndex);
|
|
42
|
+
}
|
|
43
|
+
/** Internal helper: resolveNodeIndex that falls back to 0 on not-found.
|
|
44
|
+
* Used only by registerElementValue (an unreachable-not-found site). Every
|
|
45
|
+
* author-facing call site must use resolveNodeIndex directly and check for
|
|
46
|
+
* the -1 sentinel, emitting a `ui-unknown-element` diagnostic. */
|
|
47
|
+
function resolveNodeIndexOrZero(htmlPath, id, screenId) {
|
|
48
|
+
const idx = resolveNodeIndex(htmlPath, id, screenId);
|
|
49
|
+
return idx < 0 ? 0 : idx;
|
|
50
|
+
}
|
|
51
|
+
/** Resolve "screen.led" → node index, or undefined if not registered. */
|
|
52
|
+
export function resolveElementValue(treeName, elemId) {
|
|
53
|
+
return elementValueMap.get(`${treeName}.${elemId}`);
|
|
54
|
+
}
|
|
55
|
+
/** Recorded signals: name → { cppType, initialValue, decl, emitted }. */
|
|
56
|
+
const signals = new Map();
|
|
57
|
+
/** Recorded binding specs, accumulated for emit-time table generation. */
|
|
58
|
+
const bindings = [];
|
|
59
|
+
const pressBindings = [];
|
|
60
|
+
export function recordPressBinding(binding) {
|
|
61
|
+
pressBindings.push(binding);
|
|
62
|
+
}
|
|
63
|
+
export function uiPressBindings() {
|
|
64
|
+
return pressBindings;
|
|
65
|
+
}
|
|
66
|
+
const _clickHandlers = [];
|
|
67
|
+
export function recordClickHandler(spec) {
|
|
68
|
+
_clickHandlers.push(spec);
|
|
69
|
+
}
|
|
70
|
+
export function clickHandlers() {
|
|
71
|
+
return _clickHandlers;
|
|
72
|
+
}
|
|
73
|
+
/** Max node index that has a click handler (for sizing the handler array). */
|
|
74
|
+
export function maxClickNodeIndex() {
|
|
75
|
+
return _clickHandlers.reduce((max, h) => Math.max(max, h.nodeIndex), -1);
|
|
76
|
+
}
|
|
77
|
+
const _watchPinSpecs = [];
|
|
78
|
+
export function recordWatchPin(spec) {
|
|
79
|
+
_watchPinSpecs.push(spec);
|
|
80
|
+
}
|
|
81
|
+
export function watchPinSpecs() {
|
|
82
|
+
return _watchPinSpecs;
|
|
83
|
+
}
|
|
84
|
+
export function registerUIModuleImport(name, htmlPath) {
|
|
85
|
+
uiModuleImports.set(name, htmlPath);
|
|
86
|
+
}
|
|
87
|
+
export function resolveUIModuleImport(name) {
|
|
88
|
+
return uiModuleImports.get(name);
|
|
89
|
+
}
|
|
90
|
+
export function recordSignal(name, cppType, initialValue) {
|
|
91
|
+
const decl = emitSignalDecl(name, cppType, initialValue);
|
|
92
|
+
signals.set(name, { cppType, initialValue, decl, emitted: false });
|
|
93
|
+
}
|
|
94
|
+
/** Mark a signal as already emitted via its own var_decl (const X = ui.signal).
|
|
95
|
+
* Prevents uiSignalDecls() from double-declaring it at file scope. */
|
|
96
|
+
export function markSignalEmitted(name) {
|
|
97
|
+
const s = signals.get(name);
|
|
98
|
+
if (s)
|
|
99
|
+
s.emitted = true;
|
|
100
|
+
}
|
|
101
|
+
export function uiSignalNames() {
|
|
102
|
+
return [...signals.keys()];
|
|
103
|
+
}
|
|
104
|
+
/** Is `name` a recorded UI signal? Used to lower `temp()` reads → `temp`. */
|
|
105
|
+
export function isSignalName(name) {
|
|
106
|
+
return signals.has(name);
|
|
107
|
+
}
|
|
108
|
+
/** The recorded C++ type of a signal, or undefined if not a signal.
|
|
109
|
+
* Used by the text-binding lowerer to pick %d vs %g (spec §5.3). */
|
|
110
|
+
export function signalCppType(name) {
|
|
111
|
+
return signals.get(name)?.cppType;
|
|
112
|
+
}
|
|
113
|
+
export function uiSignalDecls() {
|
|
114
|
+
// Skip signals already emitted via their own const X = ui.signal(...) var_decl
|
|
115
|
+
// — those are declared in the function body, not at file scope.
|
|
116
|
+
return [...signals.values()].filter((s) => !s.emitted).map((s) => s.decl);
|
|
117
|
+
}
|
|
118
|
+
export function recordBinding(spec) {
|
|
119
|
+
// Runs are static-content only: a node with rich-text runs cannot also have
|
|
120
|
+
// a PROP_TEXT binding (the binding model replaces the whole text buffer,
|
|
121
|
+
// which is incompatible with the node's precomputed run geometry). Reject the
|
|
122
|
+
// binding with a diagnostic instead of silently producing an unrenderable node.
|
|
123
|
+
if (spec.property === "text" && isRunNode(spec.nodeIndex)) {
|
|
124
|
+
_diagnostics.push({
|
|
125
|
+
severity: "warning",
|
|
126
|
+
message: `text binding on node ${spec.nodeIndex} ignored — node has rich-text runs (runs are static-only).`,
|
|
127
|
+
code: "run-text-binding-conflict",
|
|
128
|
+
});
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
bindings.push(spec);
|
|
132
|
+
}
|
|
133
|
+
export function uiBindings() {
|
|
134
|
+
return [...bindings];
|
|
135
|
+
}
|
|
136
|
+
// Node indices that carry rich-text runs (populated by auto-wire's tree walk,
|
|
137
|
+
// which assigns the same document-order indices the binding resolver uses).
|
|
138
|
+
const _runNodeIndices = new Set();
|
|
139
|
+
export function markRunNode(nodeIndex) { _runNodeIndices.add(nodeIndex); }
|
|
140
|
+
export function isRunNode(nodeIndex) { return _runNodeIndices.has(nodeIndex); }
|
|
141
|
+
const _diagnostics = [];
|
|
142
|
+
export function getDiagnostics() { return [..._diagnostics]; }
|
|
143
|
+
export function resetUICallState() {
|
|
144
|
+
uiModuleImports.clear();
|
|
145
|
+
elementValueMap.clear();
|
|
146
|
+
signals.clear();
|
|
147
|
+
bindings.length = 0;
|
|
148
|
+
pressBindings.length = 0;
|
|
149
|
+
_watchPinSpecs.length = 0;
|
|
150
|
+
_clickHandlers.length = 0;
|
|
151
|
+
_runNodeIndices.clear();
|
|
152
|
+
_diagnostics.length = 0;
|
|
153
|
+
resetInputBindings();
|
|
154
|
+
resetCallbackLoweringState();
|
|
155
|
+
resetCanvasBindings();
|
|
156
|
+
}
|
|
157
|
+
// ── Signal name synthesis ───────────────────────────────────────────────────
|
|
158
|
+
let signalCounter = 0;
|
|
159
|
+
function nextSyntheticSignalName() {
|
|
160
|
+
return `__ui_sig_${signalCounter++}`;
|
|
161
|
+
}
|
|
162
|
+
// ── Call detection ──────────────────────────────────────────────────────────
|
|
163
|
+
/** Is this call expression a `ui.<method>(...)` call? */
|
|
164
|
+
export function isUICall(call) {
|
|
165
|
+
return (ts.isPropertyAccessExpression(call.expression) &&
|
|
166
|
+
ts.isIdentifier(call.expression.expression) &&
|
|
167
|
+
call.expression.expression.text === "ui");
|
|
168
|
+
}
|
|
169
|
+
// ── ui.window.* (native desktop window controls) ────────────────────────────
|
|
170
|
+
/** Detect `ui.window.<method>(...)` — a two-level property access chain.
|
|
171
|
+
* Returns the method name ("setTitle" / "setIcon") or undefined. */
|
|
172
|
+
function matchUIWindowCall(call) {
|
|
173
|
+
// Shape: CallExpression { expression: PropertyAccessExpression {
|
|
174
|
+
// name: <method>, expression: PropertyAccessExpression {
|
|
175
|
+
// name: "window", expression: Identifier { text: "ui" } } } }
|
|
176
|
+
if (!ts.isPropertyAccessExpression(call.expression))
|
|
177
|
+
return undefined;
|
|
178
|
+
const inner = call.expression.expression;
|
|
179
|
+
if (!ts.isPropertyAccessExpression(inner))
|
|
180
|
+
return undefined;
|
|
181
|
+
if (!ts.isIdentifier(inner.expression) || inner.expression.text !== "ui")
|
|
182
|
+
return undefined;
|
|
183
|
+
if (!ts.isIdentifier(inner.name) || inner.name.text !== "window")
|
|
184
|
+
return undefined;
|
|
185
|
+
return call.expression.name.text;
|
|
186
|
+
}
|
|
187
|
+
/** Resolve ui.window.setTitle / ui.window.setIcon. Native SDL only; on hardware
|
|
188
|
+
* these are silent no-ops (a desktop-only convenience, not an error). */
|
|
189
|
+
function resolveWindowCall(method, call, sourceText, diagnostics) {
|
|
190
|
+
const fileName = call.getSourceFile()?.fileName ?? "";
|
|
191
|
+
const isSDL = (() => { try {
|
|
192
|
+
return getDisplayProfile().driver === "sdl";
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
return false;
|
|
196
|
+
} })();
|
|
197
|
+
const sourceSpan = makeSourceSpan(call, fileName, sourceText);
|
|
198
|
+
if (method === "setTitle") {
|
|
199
|
+
if (!isSDL)
|
|
200
|
+
return { kind: "block", sourceSpan, body: [] };
|
|
201
|
+
const arg = call.arguments[0];
|
|
202
|
+
if (!arg)
|
|
203
|
+
return { kind: "block", sourceSpan, body: [] };
|
|
204
|
+
// String literal: pass directly as a const char*.
|
|
205
|
+
if (ts.isStringLiteral(arg)) {
|
|
206
|
+
const escaped = escapeCppStringLiteral(arg.text);
|
|
207
|
+
return {
|
|
208
|
+
kind: "call",
|
|
209
|
+
callee: "__EMIT__",
|
|
210
|
+
args: [{ kind: "string", value: `ui_window_set_title("${escaped}");` }],
|
|
211
|
+
sourceSpan,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
// Template literal with interpolations: build a snprintf into a local buffer
|
|
215
|
+
// (same format-building logic as lowerTextBindingBody Shape 3), then pass
|
|
216
|
+
// the buffer to ui_window_set_title. Handles `taps: ${count}` correctly.
|
|
217
|
+
if (ts.isTemplateExpression(arg)) {
|
|
218
|
+
const fmtBuf = [escapeSnprintfFormatFragment(arg.head.text)];
|
|
219
|
+
const snprintfArgs = [];
|
|
220
|
+
for (const span of arg.templateSpans) {
|
|
221
|
+
fmtBuf.push(numericFormat(span.expression));
|
|
222
|
+
snprintfArgs.push(lowerInterpolationArg(span.expression, sourceText, diagnostics));
|
|
223
|
+
fmtBuf.push(escapeSnprintfFormatFragment(span.literal.text));
|
|
224
|
+
}
|
|
225
|
+
const fmt = fmtBuf.join("");
|
|
226
|
+
const argList = snprintfArgs.length ? ", " + snprintfArgs.join(", ") : "";
|
|
227
|
+
return {
|
|
228
|
+
kind: "call",
|
|
229
|
+
callee: "__EMIT__",
|
|
230
|
+
args: [{ kind: "string", value: `{ char __title[128]; snprintf(__title, sizeof(__title), "${fmt}"${argList}); ui_window_set_title(__title); }` }],
|
|
231
|
+
sourceSpan,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
// Any other expression (signal read, concatenation, etc): render it and
|
|
235
|
+
// pass as a const char* if it's string-compatible. Signal reads like
|
|
236
|
+
// count() lower to the variable name; string signals work directly.
|
|
237
|
+
const argText = renderExprAsText(expressionToIR(arg, sourceText, diagnostics));
|
|
238
|
+
return {
|
|
239
|
+
kind: "call",
|
|
240
|
+
callee: "__EMIT__",
|
|
241
|
+
args: [{ kind: "string", value: `ui_window_set_title(${argText});` }],
|
|
242
|
+
sourceSpan,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
if (method === "setIcon") {
|
|
246
|
+
// Runtime icon changes need SDL_image. For v1, recommend the config option.
|
|
247
|
+
if (isSDL) {
|
|
248
|
+
diagnostics.push({
|
|
249
|
+
severity: "warning",
|
|
250
|
+
code: "ui-window-seticon-runtime",
|
|
251
|
+
message: `ui.window.setIcon at runtime requires SDL_image, which isn't bundled. Use the display.icon config option for the launch icon (loaded once via core SDL2 SDL_LoadBMP).`,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
return { kind: "block", sourceSpan, body: [] };
|
|
255
|
+
}
|
|
256
|
+
// Unknown ui.window.* method — silent no-op (forward-compatible).
|
|
257
|
+
return { kind: "block", sourceSpan, body: [] };
|
|
258
|
+
}
|
|
259
|
+
// ── Argument extraction ─────────────────────────────────────────────────────
|
|
260
|
+
/** Read display wiring overrides from an optional ui.mount options object literal. */
|
|
261
|
+
function extractMountOptions(optsExpr, diagnostics) {
|
|
262
|
+
if (!optsExpr) {
|
|
263
|
+
return {};
|
|
264
|
+
}
|
|
265
|
+
if (!ts.isObjectLiteralExpression(optsExpr)) {
|
|
266
|
+
diagnostics.push({
|
|
267
|
+
severity: "error",
|
|
268
|
+
code: "ui-mount-opts",
|
|
269
|
+
message: "ui.mount expects an optional options object, e.g. ui.mount(screen) or ui.mount(screen, { display, bus, cs, dc, rst, rotation, backlight, spiFrequency, address, reset })",
|
|
270
|
+
});
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
const opts = {};
|
|
274
|
+
for (const prop of optsExpr.properties) {
|
|
275
|
+
if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name))
|
|
276
|
+
continue;
|
|
277
|
+
const key = prop.name.text;
|
|
278
|
+
if (ts.isStringLiteral(prop.initializer))
|
|
279
|
+
opts[key] = prop.initializer.text;
|
|
280
|
+
else if (ts.isNumericLiteral(prop.initializer))
|
|
281
|
+
opts[key] = Number(prop.initializer.text);
|
|
282
|
+
}
|
|
283
|
+
return opts;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Resolve a ui.* call to IR. Returns null if the call is not a ui.* call
|
|
287
|
+
* (so the caller falls through to other handling).
|
|
288
|
+
*
|
|
289
|
+
* Reads the active PlatformGraphicsStrategy from the compilation context to
|
|
290
|
+
* validate the driver and resolve colors/storage.
|
|
291
|
+
*/
|
|
292
|
+
/** Push a `ui-unknown-element` error diagnostic. Used by every author-facing
|
|
293
|
+
* binding/method call site when resolveNodeIndex returns -1, so a typo'd
|
|
294
|
+
* element id fails the build instead of silently re-targeting node 0.
|
|
295
|
+
* Exported so call-statement.ts and canvas-lowering.ts can share the exact
|
|
296
|
+
* same diagnostic shape across all element-binding entry points. */
|
|
297
|
+
export function pushUnknownElementDiagnostic(diagnostics, callLabel, id, treeName) {
|
|
298
|
+
diagnostics.push({
|
|
299
|
+
severity: "error",
|
|
300
|
+
code: "ui-unknown-element",
|
|
301
|
+
message: `${callLabel}: element "${id}" not found in screen "${treeName}"`,
|
|
302
|
+
hint: `Check the id attribute in your .ui.html file. Screen "${treeName}" does not contain an element with id "${id}".`,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
export function tryResolveUICall(call, fileName, sourceText, diagnostics, options = {}) {
|
|
306
|
+
// ui.window.<method>(...) — two-level property access (ui.window.setTitle).
|
|
307
|
+
// isUICall only matches ui.<method> (one level), so detect this shape first.
|
|
308
|
+
const windowCall = matchUIWindowCall(call);
|
|
309
|
+
if (windowCall)
|
|
310
|
+
return resolveWindowCall(windowCall, call, sourceText, diagnostics);
|
|
311
|
+
if (!isUICall(call))
|
|
312
|
+
return null;
|
|
313
|
+
const method = call.expression.name.text;
|
|
314
|
+
if (method === "mount") {
|
|
315
|
+
return resolveMountCall(call, fileName, sourceText, diagnostics);
|
|
316
|
+
}
|
|
317
|
+
if (method === "signal") {
|
|
318
|
+
return resolveSignalCall(call, fileName, sourceText, diagnostics, options);
|
|
319
|
+
}
|
|
320
|
+
if (method === "bind") {
|
|
321
|
+
return resolveBindCall(call, fileName, sourceText, diagnostics);
|
|
322
|
+
}
|
|
323
|
+
if (method === "bindList") {
|
|
324
|
+
return resolveBindListCall(call, fileName, sourceText, diagnostics);
|
|
325
|
+
}
|
|
326
|
+
if (method === "bindInput") {
|
|
327
|
+
return resolveBindInputCall(call, fileName, sourceText, diagnostics);
|
|
328
|
+
}
|
|
329
|
+
if (method === "watchPin") {
|
|
330
|
+
return resolveWatchPinCall(call, fileName, sourceText, diagnostics);
|
|
331
|
+
}
|
|
332
|
+
if (method === "onTap") {
|
|
333
|
+
return resolveOnTapCall(call, fileName, sourceText, diagnostics);
|
|
334
|
+
}
|
|
335
|
+
if (method === "drawCanvas") {
|
|
336
|
+
return resolveDrawCanvasCall(call, fileName, sourceText, diagnostics);
|
|
337
|
+
}
|
|
338
|
+
// Unknown ui.* method — let it fall through
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
function resolveMountCall(call, fileName, sourceText, diagnostics) {
|
|
342
|
+
const treeArg = call.arguments[0];
|
|
343
|
+
const optsArg = call.arguments[1];
|
|
344
|
+
if (!treeArg || !ts.isIdentifier(treeArg)) {
|
|
345
|
+
diagnostics.push({
|
|
346
|
+
severity: "error", code: "ui-mount-tree",
|
|
347
|
+
message: "ui.mount's first argument must be an imported UI tree (e.g. `screen`)",
|
|
348
|
+
});
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
const htmlPath = resolveUIModuleImport(treeArg.text);
|
|
352
|
+
if (!htmlPath) {
|
|
353
|
+
diagnostics.push({
|
|
354
|
+
severity: "error", code: "ui-mount-tree",
|
|
355
|
+
message: `ui.mount: "${treeArg.text}" is not a recognized UI tree import. Did you import it from a .ui.html file?`,
|
|
356
|
+
});
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
const opts = extractMountOptions(optsArg, diagnostics);
|
|
360
|
+
if (!opts) {
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
const strategy = getContext().activeStrategy;
|
|
364
|
+
if (!strategy) {
|
|
365
|
+
diagnostics.push({
|
|
366
|
+
severity: "error", code: "ui-no-strategy",
|
|
367
|
+
message: "ui.mount: no active platform strategy (cannot resolve display driver)",
|
|
368
|
+
});
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
// Viewport from the display profile (data-driven, not hardcoded).
|
|
372
|
+
const profile = getDisplayProfile();
|
|
373
|
+
const viewport = effectiveDisplaySize(profile);
|
|
374
|
+
const req = {
|
|
375
|
+
display: opts.display !== undefined ? String(opts.display) : profile.driver,
|
|
376
|
+
bus: opts.bus !== undefined ? String(opts.bus) : profile._mountBus,
|
|
377
|
+
cs: opts.cs !== undefined ? Number(opts.cs) : profile._mountCs,
|
|
378
|
+
dc: opts.dc !== undefined ? Number(opts.dc) : profile._mountDc,
|
|
379
|
+
rst: opts.rst !== undefined ? Number(opts.rst) : profile._mountRst,
|
|
380
|
+
rotation: opts.rotation !== undefined ? Number(opts.rotation) : profile.rotation,
|
|
381
|
+
backlight: opts.backlight !== undefined ? Number(opts.backlight) : profile.backlight,
|
|
382
|
+
spiFrequency: opts.spiFrequency !== undefined ? Number(opts.spiFrequency) : profile.spiFrequency,
|
|
383
|
+
address: opts.address !== undefined ? Number(opts.address) : profile._mountAddress,
|
|
384
|
+
reset: opts.reset !== undefined ? Number(opts.reset) : profile._mountReset,
|
|
385
|
+
};
|
|
386
|
+
// SPI-bus displays (ILI9341, ST7796, etc.) need bus/cs/dc/rst wiring. Host
|
|
387
|
+
// render targets (sdl, native-preview) have no SPI pins. I2C displays
|
|
388
|
+
// (ssd1309) need address/reset instead of SPI pins.
|
|
389
|
+
const isI2C = req.bus === "I2C" || req.bus === "i2c";
|
|
390
|
+
const spiDisplay = !isI2C && req.display !== "sdl" && req.display !== "native-preview";
|
|
391
|
+
if (spiDisplay && (req.bus === undefined || req.cs === undefined ||
|
|
392
|
+
req.dc === undefined || req.rst === undefined)) {
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
// Final layout + lower using the mount's viewport. Source the colorFormat
|
|
396
|
+
// from the resolved display profile (authoritative — set from config), not
|
|
397
|
+
// strategy.colorFormat() (capability-level, may default to rgb565 before the
|
|
398
|
+
// profile is wired into the strategy). This ensures node colors lower at the
|
|
399
|
+
// target's true depth (rgb888 for SDL → full 888, no 565 quantization).
|
|
400
|
+
const ui = requireUIHook();
|
|
401
|
+
const lowered = ui.lowerOnMount(htmlPath, {
|
|
402
|
+
colorFormat: profile.colorFormat,
|
|
403
|
+
storage: strategy.graphicsCapacity().nodeStorage,
|
|
404
|
+
viewport,
|
|
405
|
+
});
|
|
406
|
+
const displayInitOp = resolveMount(req, strategy, viewport);
|
|
407
|
+
// Mark the entry file as having a UI → gates runtime header + table injection.
|
|
408
|
+
ui.markEntryHasUI();
|
|
409
|
+
return halOpsToIR([displayInitOp], call, fileName, sourceText);
|
|
410
|
+
}
|
|
411
|
+
function resolveSignalCall(call, fileName, sourceText, diagnostics, options) {
|
|
412
|
+
const valueArg = call.arguments[0];
|
|
413
|
+
if (!valueArg)
|
|
414
|
+
return null;
|
|
415
|
+
// Infer initial value + cpp type from the literal.
|
|
416
|
+
let initialValue = 0;
|
|
417
|
+
let cppType = "int";
|
|
418
|
+
if (ts.isNumericLiteral(valueArg)) {
|
|
419
|
+
initialValue = Number(valueArg.text);
|
|
420
|
+
cppType = Number.isInteger(initialValue) ? "int" : "double";
|
|
421
|
+
}
|
|
422
|
+
else if (ts.isStringLiteral(valueArg)) {
|
|
423
|
+
initialValue = valueArg.text;
|
|
424
|
+
cppType = "const char*";
|
|
425
|
+
}
|
|
426
|
+
else if (valueArg.kind === ts.SyntaxKind.TrueKeyword || valueArg.kind === ts.SyntaxKind.FalseKeyword) {
|
|
427
|
+
initialValue = valueArg.kind === ts.SyntaxKind.TrueKeyword;
|
|
428
|
+
cppType = "bool";
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
// Unsupported initializer (object, array, null, identifier, template
|
|
432
|
+
// literal, ...). The @typecad/ui package's Signal<T extends SignalValue>
|
|
433
|
+
// constraint should catch most of these at editor time; this diagnostic
|
|
434
|
+
// is the build-time defense for anything that slips past the type system.
|
|
435
|
+
// Default to int=0 and continue (signals are often transient state).
|
|
436
|
+
diagnostics.push({
|
|
437
|
+
severity: "warning",
|
|
438
|
+
code: "ui-signal-initializer",
|
|
439
|
+
message: `ui.signal: unsupported initial value "${valueArg.getText()}". Only number, string, and boolean literals are supported; defaulting to 0 (int).`,
|
|
440
|
+
hint: 'Use a literal: ui.signal(0), ui.signal("label"), or ui.signal(true).',
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
const name = options.constName ?? nextSyntheticSignalName();
|
|
444
|
+
recordSignal(name, cppType, initialValue);
|
|
445
|
+
// The signal lowers to a device variable declaration, emitted as a raw line.
|
|
446
|
+
// The call itself produces no statement (the var decl is emitted at file scope
|
|
447
|
+
// via uiSignalDecls() at emit time), so return an empty block.
|
|
448
|
+
return {
|
|
449
|
+
kind: "block",
|
|
450
|
+
sourceSpan: makeSourceSpan(call, fileName, sourceText),
|
|
451
|
+
body: [],
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
/** Format specifier for a single signal/numeric interpolation per spec §5.3.
|
|
455
|
+
* Prefer signal types; otherwise consult IrTypeScope globals + activeFunctionReturnTypes
|
|
456
|
+
* via inferExprCppType for named free-function results (e.g. Math.floor → %g). */
|
|
457
|
+
function bindingTypeMaps() {
|
|
458
|
+
const scope = getCurrentIrTypeScope();
|
|
459
|
+
const localVariableTypes = new Map();
|
|
460
|
+
if (scope) {
|
|
461
|
+
for (const [k, v] of scope.globals)
|
|
462
|
+
localVariableTypes.set(k, v);
|
|
463
|
+
}
|
|
464
|
+
const functionReturnTypes = new Map([...(getContext().activeFunctionReturnTypes ?? new Map())]);
|
|
465
|
+
return { functionReturnTypes, localVariableTypes };
|
|
466
|
+
}
|
|
467
|
+
function numericFormat(expr) {
|
|
468
|
+
if (ts.isCallExpression(expr) && ts.isIdentifier(expr.expression) &&
|
|
469
|
+
expr.arguments.length === 0 && isSignalName(expr.expression.text)) {
|
|
470
|
+
const t = signalCppType(expr.expression.text);
|
|
471
|
+
if (t === "float" || t === "double")
|
|
472
|
+
return "%g";
|
|
473
|
+
if (t === "const char*" || t === "String" || t === "char*")
|
|
474
|
+
return "%s";
|
|
475
|
+
return "%d";
|
|
476
|
+
}
|
|
477
|
+
const { functionReturnTypes, localVariableTypes } = bindingTypeMaps();
|
|
478
|
+
const inferred = inferExprCppType(expr, functionReturnTypes, localVariableTypes, "");
|
|
479
|
+
if (inferred === "float" || inferred === "double")
|
|
480
|
+
return "%g";
|
|
481
|
+
if (inferred === "const char*" || inferred === "char*" || inferred === "std::string" || inferred === "__tc_str_ptr")
|
|
482
|
+
return "%s";
|
|
483
|
+
return "%d";
|
|
484
|
+
}
|
|
485
|
+
/** Lower a single expression as a snprintf argument.
|
|
486
|
+
* - signal read `name()` → "name"
|
|
487
|
+
* - numeric literal / other → rendered via the shared expression renderer */
|
|
488
|
+
function lowerInterpolationArg(expr, sourceText, diagnostics) {
|
|
489
|
+
if (ts.isCallExpression(expr) && ts.isIdentifier(expr.expression) &&
|
|
490
|
+
expr.arguments.length === 0 && isSignalName(expr.expression.text)) {
|
|
491
|
+
return expr.expression.text;
|
|
492
|
+
}
|
|
493
|
+
return renderExprAsText(expressionToIR(expr, sourceText, diagnostics));
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Lower a text-binding arrow body to an imperative C++ statement that writes
|
|
497
|
+
* into `buf` (the textFn's first parameter, size `size`). Recognizes three
|
|
498
|
+
* shapes (spec §5.2): String(<numeric>), a bare string literal, and a template
|
|
499
|
+
* literal with numeric interpolations. Anything else produces a safe no-op
|
|
500
|
+
* (`buf[0] = 0;`) plus a `ui-bind-text-unlowered` warning so the author sees it.
|
|
501
|
+
*
|
|
502
|
+
* Pure function of the AST + the recorded signal table; no side effects beyond
|
|
503
|
+
* pushing diagnostics.
|
|
504
|
+
*/
|
|
505
|
+
export function lowerTextBindingBody(body, _fileName, sourceText, diagnostics) {
|
|
506
|
+
const warn = () => {
|
|
507
|
+
diagnostics.push({
|
|
508
|
+
severity: "warning",
|
|
509
|
+
code: "ui-bind-text-unlowered",
|
|
510
|
+
message: `ui.bind text: this arrow-body shape is not supported in v1. Supported: String(<signal>), a string literal, or a template literal with numeric interpolations. The node will display an empty string.`,
|
|
511
|
+
});
|
|
512
|
+
return { cppBody: "buf[0] = 0;" };
|
|
513
|
+
};
|
|
514
|
+
// Shape 1: String(<signal or numeric expr>) — the format specifier comes
|
|
515
|
+
// from the signal's recorded C++ type (%d for int/bool, %g for float/double,
|
|
516
|
+
// %s for const char*/String); anything else defaults to %d.
|
|
517
|
+
if (ts.isCallExpression(body) && ts.isIdentifier(body.expression) &&
|
|
518
|
+
body.expression.text === "String" && body.arguments.length === 1) {
|
|
519
|
+
const arg = body.arguments[0];
|
|
520
|
+
const fmt = numericFormat(arg);
|
|
521
|
+
const argText = lowerInterpolationArg(arg, sourceText, diagnostics);
|
|
522
|
+
return { cppBody: `snprintf(buf, size, "${fmt}", ${argText});` };
|
|
523
|
+
}
|
|
524
|
+
// Shape 2: bare string literal
|
|
525
|
+
if (ts.isStringLiteral(body)) {
|
|
526
|
+
const escaped = escapeCppStringLiteral(body.text);
|
|
527
|
+
return { cppBody: `snprintf(buf, size, "%s", "${escaped}");` };
|
|
528
|
+
}
|
|
529
|
+
// Shape 3: template literal with signal/numeric interpolations.
|
|
530
|
+
// Build the format string by alternating literal fragments and %specifiers,
|
|
531
|
+
// and collect the matching argument expressions in order. Each interpolation's
|
|
532
|
+
// specifier comes from the signal's C++ type (%d/%g/%s); compound expressions
|
|
533
|
+
// default to %d. The literal fragments become part of the snprintf *format*
|
|
534
|
+
// string, so any embedded `%` must be doubled (escapeSnprintfFormatFragment)
|
|
535
|
+
// — otherwise a body like `meter: ${v}%` produces a malformed format string.
|
|
536
|
+
if (ts.isTemplateExpression(body)) {
|
|
537
|
+
const fmtBuf = [escapeSnprintfFormatFragment(body.head.text)];
|
|
538
|
+
const args = [];
|
|
539
|
+
for (const span of body.templateSpans) {
|
|
540
|
+
fmtBuf.push(numericFormat(span.expression));
|
|
541
|
+
args.push(lowerInterpolationArg(span.expression, sourceText, diagnostics));
|
|
542
|
+
fmtBuf.push(escapeSnprintfFormatFragment(span.literal.text));
|
|
543
|
+
}
|
|
544
|
+
const fmt = fmtBuf.join("");
|
|
545
|
+
const argList = args.length ? ", " + args.join(", ") : "";
|
|
546
|
+
return { cppBody: `snprintf(buf, size, "${fmt}"${argList});` };
|
|
547
|
+
}
|
|
548
|
+
// Shape 4: ternary with string literals (e.g. val === 0 ? 'Auto' : val === 1 ? 'Manual' : 'Off')
|
|
549
|
+
// Lowers to a chain of if/else with snprintf.
|
|
550
|
+
// Unwrap parentheses first: () => (cond ? 'a' : 'b')
|
|
551
|
+
const unwrapped = ts.isParenthesizedExpression(body) ? body.expression : body;
|
|
552
|
+
if (ts.isConditionalExpression(unwrapped)) {
|
|
553
|
+
return lowerTernaryTextChain(unwrapped, sourceText, diagnostics) ?? warn();
|
|
554
|
+
}
|
|
555
|
+
// Shape 5 fallback: snprintf a scalar/string expression via the main IR
|
|
556
|
+
// path (covers `x + "°C"`, helper calls, `.toFixed()`, bare identifiers,
|
|
557
|
+
// etc.). Reject containers/objects — those can't snprintf meaningfully.
|
|
558
|
+
if (ts.isArrayLiteralExpression(unwrapped) ||
|
|
559
|
+
ts.isObjectLiteralExpression(unwrapped) ||
|
|
560
|
+
ts.isNewExpression(unwrapped) ||
|
|
561
|
+
ts.isClassExpression(unwrapped)) {
|
|
562
|
+
return warn();
|
|
563
|
+
}
|
|
564
|
+
const fmt = numericFormat(body);
|
|
565
|
+
const argText = lowerInterpolationArg(body, sourceText, diagnostics);
|
|
566
|
+
if (argText && argText !== "0 /* unsupported_expr */" && !argText.trimStart().startsWith("{")) {
|
|
567
|
+
return { cppBody: `snprintf(buf, size, "${fmt}", ${argText});` };
|
|
568
|
+
}
|
|
569
|
+
return warn();
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Lower a `{expr}` interpolation text string to an imperative snprintf body
|
|
573
|
+
* that writes into `buf`/`size`. Mirrors `lowerTextBindingBody` Shape 3 but
|
|
574
|
+
* operates on a raw string (from HTML markup) rather than a TS AST node.
|
|
575
|
+
*
|
|
576
|
+
* Each `{...}` span becomes a `%d` argument (v1: numeric default — the dominant
|
|
577
|
+
* use case is counters/progress; string-signal interpolation should use the
|
|
578
|
+
* `ui.bind` path which has TS type information). Literal `%` in surrounding text
|
|
579
|
+
* is escaped to `%%` so snprintf doesn't misread it.
|
|
580
|
+
*
|
|
581
|
+
* "taps: {count}" → 'snprintf(buf, size, "taps: %d", count);'
|
|
582
|
+
* "a:{x} b:{y}" → 'snprintf(buf, size, "a:%d b:%d", x, y);'
|
|
583
|
+
* "{n}% done" → 'snprintf(buf, size, "%d%% done", n);'
|
|
584
|
+
*
|
|
585
|
+
* Empty braces `{}` are treated as literal text (not interpolation). Returns
|
|
586
|
+
* null if no interpolation is present (caller keeps the static text).
|
|
587
|
+
*/
|
|
588
|
+
export function lowerInterpolationText(raw) {
|
|
589
|
+
// Scan for non-empty {expr} spans. Track brace depth so `{a + {b}}` (malformed)
|
|
590
|
+
// is handled conservatively: only top-level `{...}` with no nested braces is
|
|
591
|
+
// treated as an interpolation.
|
|
592
|
+
const fmtBuf = [];
|
|
593
|
+
const args = [];
|
|
594
|
+
let i = 0;
|
|
595
|
+
let hasInterp = false;
|
|
596
|
+
while (i < raw.length) {
|
|
597
|
+
const ch = raw[i];
|
|
598
|
+
if (ch === "{") {
|
|
599
|
+
// Find the matching close brace at the same depth.
|
|
600
|
+
let depth = 1;
|
|
601
|
+
let j = i + 1;
|
|
602
|
+
while (j < raw.length && depth > 0) {
|
|
603
|
+
if (raw[j] === "{")
|
|
604
|
+
depth++;
|
|
605
|
+
else if (raw[j] === "}")
|
|
606
|
+
depth--;
|
|
607
|
+
if (depth === 0)
|
|
608
|
+
break;
|
|
609
|
+
j++;
|
|
610
|
+
}
|
|
611
|
+
const content = raw.slice(i + 1, j);
|
|
612
|
+
// Only non-empty, non-nested braces count as an interpolation.
|
|
613
|
+
if (depth === 0 && content.length > 0 && !content.includes("{")) {
|
|
614
|
+
hasInterp = true;
|
|
615
|
+
args.push(content.trim());
|
|
616
|
+
fmtBuf.push("%d");
|
|
617
|
+
i = j + 1;
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
// Else: literal '{' (empty {} or nested). Emit it verbatim.
|
|
621
|
+
}
|
|
622
|
+
// Literal character — escape % for snprintf format-string safety.
|
|
623
|
+
fmtBuf.push(ch === "%" ? "%%" : escapeCppStringLiteral(ch));
|
|
624
|
+
i++;
|
|
625
|
+
}
|
|
626
|
+
if (!hasInterp)
|
|
627
|
+
return null;
|
|
628
|
+
const fmt = fmtBuf.join("");
|
|
629
|
+
const argList = args.length ? ", " + args.join(", ") : "";
|
|
630
|
+
return `snprintf(buf, size, "${fmt}"${argList});`;
|
|
631
|
+
}
|
|
632
|
+
/** Lower a chain of ternary expressions with string branches to C++ if/else. */
|
|
633
|
+
function lowerTernaryTextChain(expr, sourceText, diagnostics) {
|
|
634
|
+
const cases = [];
|
|
635
|
+
let current = expr;
|
|
636
|
+
while (ts.isConditionalExpression(current)) {
|
|
637
|
+
const condText = renderExprAsText(expressionToIR(current.condition, sourceText, diagnostics));
|
|
638
|
+
if (!ts.isStringLiteral(current.whenTrue))
|
|
639
|
+
return null;
|
|
640
|
+
cases.push({ cond: condText, value: escapeCppStringLiteral(current.whenTrue.text) });
|
|
641
|
+
current = current.whenFalse;
|
|
642
|
+
}
|
|
643
|
+
// The final else value
|
|
644
|
+
if (ts.isStringLiteral(current)) {
|
|
645
|
+
cases.push({ cond: null, value: escapeCppStringLiteral(current.text) });
|
|
646
|
+
}
|
|
647
|
+
else {
|
|
648
|
+
return null;
|
|
649
|
+
}
|
|
650
|
+
const lines = cases.map((c, i) => {
|
|
651
|
+
if (c.cond !== null) {
|
|
652
|
+
const prefix = i === 0 ? "if" : "else if";
|
|
653
|
+
return `${prefix} (${c.cond}) { snprintf(buf, size, "%s", "${c.value}"); }`;
|
|
654
|
+
}
|
|
655
|
+
else {
|
|
656
|
+
return `else { snprintf(buf, size, "%s", "${c.value}"); }`;
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
return { cppBody: lines.join(" ") };
|
|
660
|
+
}
|
|
661
|
+
/** Extract the compute expression from a bind count/bindFn argument:
|
|
662
|
+
* arrow/fn-expr with expression body → that expr
|
|
663
|
+
* arrow/fn-expr with block → last return's expression
|
|
664
|
+
* identifier → top-level FunctionDeclaration last return or const-arrow body
|
|
665
|
+
* else diagnose and return undefined. */
|
|
666
|
+
function extractBindComputeExpr(arg, diagnostics, label) {
|
|
667
|
+
const lastReturnExpr = (body) => {
|
|
668
|
+
for (let i = body.statements.length - 1; i >= 0; i--) {
|
|
669
|
+
const s = body.statements[i];
|
|
670
|
+
if (ts.isReturnStatement(s) && s.expression)
|
|
671
|
+
return s.expression;
|
|
672
|
+
}
|
|
673
|
+
return undefined;
|
|
674
|
+
};
|
|
675
|
+
if (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg)) {
|
|
676
|
+
if (ts.isBlock(arg.body)) {
|
|
677
|
+
const expr = lastReturnExpr(arg.body);
|
|
678
|
+
if (!expr) {
|
|
679
|
+
diagnostics.push({
|
|
680
|
+
severity: "error",
|
|
681
|
+
code: "ui-bind-unlowered",
|
|
682
|
+
message: `${label}: block callback must end with a return expression.`,
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
return expr;
|
|
686
|
+
}
|
|
687
|
+
return arg.body;
|
|
688
|
+
}
|
|
689
|
+
if (ts.isIdentifier(arg)) {
|
|
690
|
+
const name = arg.text;
|
|
691
|
+
const sf = arg.getSourceFile();
|
|
692
|
+
for (const stmt of sf.statements) {
|
|
693
|
+
if (ts.isFunctionDeclaration(stmt) && stmt.name?.text === name && stmt.body) {
|
|
694
|
+
const expr = lastReturnExpr(stmt.body);
|
|
695
|
+
if (!expr) {
|
|
696
|
+
diagnostics.push({
|
|
697
|
+
severity: "error",
|
|
698
|
+
code: "ui-bind-unlowered",
|
|
699
|
+
message: `${label}: function '${name}' must end with a return expression.`,
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
return expr;
|
|
703
|
+
}
|
|
704
|
+
if (ts.isVariableStatement(stmt)) {
|
|
705
|
+
for (const d of stmt.declarationList.declarations) {
|
|
706
|
+
if (ts.isIdentifier(d.name) &&
|
|
707
|
+
d.name.text === name &&
|
|
708
|
+
d.initializer &&
|
|
709
|
+
(ts.isArrowFunction(d.initializer) || ts.isFunctionExpression(d.initializer))) {
|
|
710
|
+
const fn = d.initializer;
|
|
711
|
+
if (ts.isBlock(fn.body)) {
|
|
712
|
+
const expr = lastReturnExpr(fn.body);
|
|
713
|
+
if (!expr) {
|
|
714
|
+
diagnostics.push({
|
|
715
|
+
severity: "error",
|
|
716
|
+
code: "ui-bind-unlowered",
|
|
717
|
+
message: `${label}: '${name}' must end with a return expression.`,
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
return expr;
|
|
721
|
+
}
|
|
722
|
+
return fn.body;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
diagnostics.push({
|
|
728
|
+
severity: "error",
|
|
729
|
+
code: "ui-callback-unresolved-name",
|
|
730
|
+
message: `${label}: cannot resolve '${name}' to a top-level function or arrow.`,
|
|
731
|
+
hint: `Pass an inline arrow, or declare '${name}' at module scope.`,
|
|
732
|
+
});
|
|
733
|
+
return undefined;
|
|
734
|
+
}
|
|
735
|
+
diagnostics.push({
|
|
736
|
+
severity: "error",
|
|
737
|
+
code: "ui-bind-unlowered",
|
|
738
|
+
message: `${label}: expected an inline arrow/function or a top-level function name.`,
|
|
739
|
+
});
|
|
740
|
+
return undefined;
|
|
741
|
+
}
|
|
742
|
+
/** Rename a param only inside snprintf args AFTER the format string's closing `")`
|
|
743
|
+
* so format fragments like `"%d"` are never corrupted. */
|
|
744
|
+
function renameParamInSnprintfArgs(cppBody, from, to) {
|
|
745
|
+
if (from === to)
|
|
746
|
+
return cppBody;
|
|
747
|
+
const closeFmt = cppBody.indexOf('")');
|
|
748
|
+
if (closeFmt < 0) {
|
|
749
|
+
return cppBody.replace(new RegExp(`\\b${from}\\b`, "g"), to);
|
|
750
|
+
}
|
|
751
|
+
const head = cppBody.slice(0, closeFmt + 2);
|
|
752
|
+
const tail = cppBody.slice(closeFmt + 2).replace(new RegExp(`\\b${from}\\b`, "g"), to);
|
|
753
|
+
return head + tail;
|
|
754
|
+
}
|
|
755
|
+
function resolveBindCall(call, fileName, sourceText, diagnostics) {
|
|
756
|
+
const nodeArg = call.arguments[0];
|
|
757
|
+
const propArg = call.arguments[1];
|
|
758
|
+
const fnArg = call.arguments[2];
|
|
759
|
+
if (!nodeArg || !propArg || !fnArg)
|
|
760
|
+
return null;
|
|
761
|
+
// nodeArg is expected to be `screen.<id>` — a property access on an imported
|
|
762
|
+
// UI tree. Resolve the node index from the lowered tree's id ordering.
|
|
763
|
+
let nodeIndex = 0;
|
|
764
|
+
if (ts.isPropertyAccessExpression(nodeArg) && ts.isIdentifier(nodeArg.expression)) {
|
|
765
|
+
const treeName = nodeArg.expression.text;
|
|
766
|
+
const htmlPath = resolveUIModuleImport(treeName);
|
|
767
|
+
if (htmlPath) {
|
|
768
|
+
const resolved = resolveNodeIndex(htmlPath, nodeArg.name.text);
|
|
769
|
+
if (resolved < 0) {
|
|
770
|
+
pushUnknownElementDiagnostic(diagnostics, "ui.bind", nodeArg.name.text, treeName);
|
|
771
|
+
return null;
|
|
772
|
+
}
|
|
773
|
+
nodeIndex = resolved;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
let property = "text";
|
|
777
|
+
if (ts.isStringLiteral(propArg))
|
|
778
|
+
property = propArg.text;
|
|
779
|
+
const fnName = `__ui_bind_${property}_${bindings.length}`;
|
|
780
|
+
// Text bindings lower through a dedicated path (spec §5) that emits an
|
|
781
|
+
// imperative snprintf statement into the node's buffer. Color/numeric
|
|
782
|
+
// bindings keep the generic expression path with color-literal resolution.
|
|
783
|
+
let cppExpr = "";
|
|
784
|
+
let cppBody;
|
|
785
|
+
let bodyIR;
|
|
786
|
+
const body = extractBindComputeExpr(fnArg, diagnostics, "ui.bind");
|
|
787
|
+
if (!body) {
|
|
788
|
+
// diagnose already emitted; still record a stub so the table stays well-formed
|
|
789
|
+
if (property === "text")
|
|
790
|
+
cppBody = "buf[0] = 0;";
|
|
791
|
+
}
|
|
792
|
+
else if (property === "text") {
|
|
793
|
+
cppBody = lowerTextBindingBody(body, fileName, sourceText, diagnostics).cppBody;
|
|
794
|
+
}
|
|
795
|
+
else {
|
|
796
|
+
const ir = expressionToIR(body, sourceText, diagnostics);
|
|
797
|
+
bodyIR = resolveColorIR(ir);
|
|
798
|
+
cppExpr = resolveColorLiterals(renderExprAsText(ir));
|
|
799
|
+
}
|
|
800
|
+
recordBinding({ nodeIndex, property, fnName, cppExpr, cppBody, bodyIR });
|
|
801
|
+
return {
|
|
802
|
+
kind: "block",
|
|
803
|
+
sourceSpan: makeSourceSpan(call, fileName, sourceText),
|
|
804
|
+
body: [],
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
/** Resolve ui.bindList(node, countFn, itemFn) — records a list binding spec.
|
|
808
|
+
* countFn: () => number (total item count)
|
|
809
|
+
* itemFn: (index) => string (text for item at index)
|
|
810
|
+
* Both arrows are lowered to C++ functions. */
|
|
811
|
+
function resolveBindListCall(call, fileName, sourceText, diagnostics) {
|
|
812
|
+
if (call.arguments.length < 3)
|
|
813
|
+
return null;
|
|
814
|
+
const nodeArg = call.arguments[0];
|
|
815
|
+
const countArg = call.arguments[1];
|
|
816
|
+
const itemArg = call.arguments[2];
|
|
817
|
+
// Resolve the <list> node index.
|
|
818
|
+
let nodeIndex = 0;
|
|
819
|
+
if (ts.isPropertyAccessExpression(nodeArg) && ts.isIdentifier(nodeArg.expression)) {
|
|
820
|
+
const treeName = nodeArg.expression.text;
|
|
821
|
+
const htmlPath = resolveUIModuleImport(treeName);
|
|
822
|
+
if (htmlPath) {
|
|
823
|
+
const resolved = resolveNodeIndex(htmlPath, nodeArg.name.text);
|
|
824
|
+
if (resolved < 0) {
|
|
825
|
+
pushUnknownElementDiagnostic(diagnostics, "ui.bindList", nodeArg.name.text, treeName);
|
|
826
|
+
return null;
|
|
827
|
+
}
|
|
828
|
+
nodeIndex = resolved;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
// Lower the count function via extractBindComputeExpr → return StatementIR.
|
|
832
|
+
let countBody = "return 0;";
|
|
833
|
+
let countStatements;
|
|
834
|
+
let countSourceSpan;
|
|
835
|
+
if (countArg) {
|
|
836
|
+
countSourceSpan = makeSourceSpan(countArg, fileName, sourceText);
|
|
837
|
+
const countExpr = extractBindComputeExpr(countArg, diagnostics, "ui.bindList countFn");
|
|
838
|
+
if (countExpr) {
|
|
839
|
+
const exprIR = expressionToIR(countExpr, sourceText, diagnostics);
|
|
840
|
+
countStatements = [{
|
|
841
|
+
kind: "return",
|
|
842
|
+
sourceSpan: makeSourceSpan(countExpr, fileName, sourceText),
|
|
843
|
+
value: exprIR,
|
|
844
|
+
}];
|
|
845
|
+
countBody = `return ${renderExprAsText(exprIR)};`;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
// Lower the item function.
|
|
849
|
+
let itemBody = "buf[0] = 0;";
|
|
850
|
+
let itemStatements;
|
|
851
|
+
let itemSourceSpan;
|
|
852
|
+
if (itemArg) {
|
|
853
|
+
itemSourceSpan = makeSourceSpan(itemArg, fileName, sourceText);
|
|
854
|
+
if (ts.isArrowFunction(itemArg) || ts.isFunctionExpression(itemArg)) {
|
|
855
|
+
const paramName = itemArg.parameters.length > 0 && ts.isIdentifier(itemArg.parameters[0].name)
|
|
856
|
+
? itemArg.parameters[0].name.text
|
|
857
|
+
: "";
|
|
858
|
+
const body = itemArg.body;
|
|
859
|
+
if (ts.isExpression(body)) {
|
|
860
|
+
const { cppBody } = lowerTextBindingBody(body, fileName, sourceText, diagnostics);
|
|
861
|
+
itemBody = renameParamInSnprintfArgs(cppBody || "buf[0] = 0;", paramName, "idx");
|
|
862
|
+
itemStatements = [{
|
|
863
|
+
kind: "call",
|
|
864
|
+
sourceSpan: makeSourceSpan(body, fileName, sourceText),
|
|
865
|
+
callee: "__EMIT__",
|
|
866
|
+
args: [{ kind: "string", value: itemBody.replace(/;$/, "") }],
|
|
867
|
+
}];
|
|
868
|
+
}
|
|
869
|
+
else {
|
|
870
|
+
let stmts = lowerCallbackStatements(itemArg, fileName, sourceText, diagnostics, "ui-event-callback");
|
|
871
|
+
if (paramName && paramName !== "idx") {
|
|
872
|
+
stmts = renameIdentifiersInStatements(stmts, paramName, "idx");
|
|
873
|
+
}
|
|
874
|
+
itemStatements = stmts;
|
|
875
|
+
itemBody = renderStatementsCompact(stmts) || "buf[0] = 0;";
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
else {
|
|
879
|
+
// Named ref / other — treat as text-binding expression extractor.
|
|
880
|
+
const body = extractBindComputeExpr(itemArg, diagnostics, "ui.bindList itemFn");
|
|
881
|
+
if (body) {
|
|
882
|
+
const { cppBody } = lowerTextBindingBody(body, fileName, sourceText, diagnostics);
|
|
883
|
+
itemBody = cppBody || "buf[0] = 0;";
|
|
884
|
+
itemStatements = [{
|
|
885
|
+
kind: "call",
|
|
886
|
+
sourceSpan: makeSourceSpan(body, fileName, sourceText),
|
|
887
|
+
callee: "__EMIT__",
|
|
888
|
+
args: [{ kind: "string", value: itemBody.replace(/;$/, "") }],
|
|
889
|
+
}];
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
const countFnName = `__ui_list_count_${getListBindingsCount()}`;
|
|
894
|
+
const itemFnName = `__ui_list_item_${getListBindingsCount()}`;
|
|
895
|
+
const tapFnName = `__ui_list_tap_${getListBindingsCount()}`;
|
|
896
|
+
// Optional 4th arg: onTap callback (index) => { ... }
|
|
897
|
+
let tapFnBody = null;
|
|
898
|
+
let tapStatements;
|
|
899
|
+
let tapSourceSpan;
|
|
900
|
+
if (call.arguments.length >= 4) {
|
|
901
|
+
const tapArg = call.arguments[3];
|
|
902
|
+
tapSourceSpan = makeSourceSpan(tapArg, fileName, sourceText);
|
|
903
|
+
const lowered = lowerNamedOrInlineCallback(tapArg, fileName, sourceText, diagnostics, "ui.bindList tap", "ui-event-callback");
|
|
904
|
+
if (lowered) {
|
|
905
|
+
let stmts = lowered.statements;
|
|
906
|
+
const paramName = lowered.paramNames[0];
|
|
907
|
+
if (paramName && paramName !== "idx") {
|
|
908
|
+
stmts = renameIdentifiersInStatements(stmts, paramName, "idx");
|
|
909
|
+
}
|
|
910
|
+
tapStatements = stmts;
|
|
911
|
+
tapFnBody = renderStatementsCompact(stmts) || null;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
recordListBinding({
|
|
915
|
+
nodeIndex,
|
|
916
|
+
countFnName,
|
|
917
|
+
itemFnName,
|
|
918
|
+
tapFnName: tapStatements || tapFnBody ? tapFnName : null,
|
|
919
|
+
countFnBody: countBody,
|
|
920
|
+
itemFnBody: itemBody,
|
|
921
|
+
tapFnBody,
|
|
922
|
+
countStatements,
|
|
923
|
+
itemStatements,
|
|
924
|
+
tapStatements,
|
|
925
|
+
countSourceSpan,
|
|
926
|
+
itemSourceSpan,
|
|
927
|
+
tapSourceSpan,
|
|
928
|
+
});
|
|
929
|
+
return {
|
|
930
|
+
kind: "block",
|
|
931
|
+
sourceSpan: makeSourceSpan(call, fileName, sourceText),
|
|
932
|
+
body: [],
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
/** Resolve ui.bindInput(node, callback) — records a two-way input binding.
|
|
936
|
+
* The callback fires (with the current text) whenever the bound <input>
|
|
937
|
+
* node's textBuffer changes at runtime (e.g. the user typed via the
|
|
938
|
+
* on-screen keyboard). Mirrors the bindList tap-callback lowering. */
|
|
939
|
+
function resolveBindInputCall(call, fileName, sourceText, diagnostics) {
|
|
940
|
+
const nodeArg = call.arguments[0];
|
|
941
|
+
const cbArg = call.arguments[1];
|
|
942
|
+
if (!nodeArg || !cbArg)
|
|
943
|
+
return null;
|
|
944
|
+
// Resolve the <input> node index via the standard screen.<id> path.
|
|
945
|
+
let nodeIndex = 0;
|
|
946
|
+
if (ts.isPropertyAccessExpression(nodeArg) && ts.isIdentifier(nodeArg.expression)) {
|
|
947
|
+
const treeName = nodeArg.expression.text;
|
|
948
|
+
const htmlPath = resolveUIModuleImport(treeName);
|
|
949
|
+
if (htmlPath) {
|
|
950
|
+
const resolved = resolveNodeIndex(htmlPath, nodeArg.name.text);
|
|
951
|
+
if (resolved < 0) {
|
|
952
|
+
pushUnknownElementDiagnostic(diagnostics, "ui.bindInput", nodeArg.name.text, treeName);
|
|
953
|
+
return null;
|
|
954
|
+
}
|
|
955
|
+
nodeIndex = resolved;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
const cbFnName = `__ui_input_cb_${getInputBindingsCount()}`;
|
|
959
|
+
const lowered = lowerNamedOrInlineCallback(cbArg, fileName, sourceText, diagnostics, "ui.bindInput", "ui-event-callback");
|
|
960
|
+
let bodyStatements;
|
|
961
|
+
let cbFnBody = "";
|
|
962
|
+
if (lowered) {
|
|
963
|
+
bodyStatements = lowered.statements;
|
|
964
|
+
const paramName = lowered.paramNames[0];
|
|
965
|
+
if (paramName && paramName !== "text") {
|
|
966
|
+
bodyStatements = renameIdentifiersInStatements(bodyStatements, paramName, "text");
|
|
967
|
+
}
|
|
968
|
+
cbFnBody = renderStatementsCompact(bodyStatements);
|
|
969
|
+
}
|
|
970
|
+
recordInputBinding({
|
|
971
|
+
nodeIndex,
|
|
972
|
+
cbFnName,
|
|
973
|
+
cbFnBody,
|
|
974
|
+
bodyStatements,
|
|
975
|
+
sourceSpan: makeSourceSpan(cbArg, fileName, sourceText),
|
|
976
|
+
});
|
|
977
|
+
return {
|
|
978
|
+
kind: "block",
|
|
979
|
+
sourceSpan: makeSourceSpan(call, fileName, sourceText),
|
|
980
|
+
body: [],
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
/** Resolve ui.watchPin(pin, callback) — records a pin-watching spec.
|
|
984
|
+
* The callback body is lowered to C++ for the generated async watcher. */
|
|
985
|
+
function resolveWatchPinCall(call, fileName, sourceText, diagnostics) {
|
|
986
|
+
const pinArg = call.arguments[0];
|
|
987
|
+
const cbArg = call.arguments[1];
|
|
988
|
+
if (!pinArg)
|
|
989
|
+
return null;
|
|
990
|
+
const pin = ts.isIdentifier(pinArg) ? pinArg.text
|
|
991
|
+
: ts.isNumericLiteral(pinArg) ? pinArg.text
|
|
992
|
+
: pinArg.getText();
|
|
993
|
+
let bodyStatements;
|
|
994
|
+
const resolved = resolveCallbackArg(cbArg, cbArg?.getSourceFile(), diagnostics, "ui.watchPin");
|
|
995
|
+
if (resolved?.kind === "inline") {
|
|
996
|
+
bodyStatements = lowerCallbackStatements(resolved.fn, fileName, sourceText, diagnostics, "ui-event-callback");
|
|
997
|
+
}
|
|
998
|
+
else if (resolved?.kind === "named") {
|
|
999
|
+
bodyStatements = [{
|
|
1000
|
+
kind: "call",
|
|
1001
|
+
sourceSpan: makeSourceSpan(cbArg, fileName, sourceText),
|
|
1002
|
+
callee: resolved.name,
|
|
1003
|
+
args: [],
|
|
1004
|
+
}];
|
|
1005
|
+
}
|
|
1006
|
+
const fnName = `__ui_watchpin_${_watchPinSpecs.length}`;
|
|
1007
|
+
recordWatchPin({
|
|
1008
|
+
pin: String(pin),
|
|
1009
|
+
fnName,
|
|
1010
|
+
callbackBody: "",
|
|
1011
|
+
bodyStatements,
|
|
1012
|
+
sourceSpan: cbArg ? makeSourceSpan(cbArg, fileName, sourceText) : undefined,
|
|
1013
|
+
});
|
|
1014
|
+
return {
|
|
1015
|
+
kind: "block",
|
|
1016
|
+
sourceSpan: makeSourceSpan(call, fileName, sourceText),
|
|
1017
|
+
body: [],
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
/** Resolve ui.onTap([node]) — an awaitable tap notification.
|
|
1021
|
+
* Lowers to a marker call IR whose callee ("__UI_TAP__") the async state
|
|
1022
|
+
* machine recognizes and turns into a tap-counter poll. Must be awaited
|
|
1023
|
+
* (statement-level). args[0] is the node filter: -1 = any tap, >=0 = node.
|
|
1024
|
+
*
|
|
1025
|
+
* - await ui.onTap() → args=[-1] (resume on next tap anywhere)
|
|
1026
|
+
* - await ui.onTap(screen.x) → args=[idx] (resume only when x is tapped) */
|
|
1027
|
+
function resolveOnTapCall(call, fileName, sourceText, diagnostics) {
|
|
1028
|
+
const nodeArg = call.arguments[0];
|
|
1029
|
+
// Per-element form: await ui.onTap(screen.btn) → resolve the node index,
|
|
1030
|
+
// the same way onClick/onToggle do (screen.id property-access shape).
|
|
1031
|
+
if (nodeArg && ts.isPropertyAccessExpression(nodeArg) &&
|
|
1032
|
+
ts.isIdentifier(nodeArg.expression)) {
|
|
1033
|
+
const treeName = nodeArg.expression.text; // "screen"
|
|
1034
|
+
const id = nodeArg.name.text; // "btn"
|
|
1035
|
+
const htmlPath = resolveUIModuleImport(treeName);
|
|
1036
|
+
if (htmlPath) {
|
|
1037
|
+
const nodeIndex = resolveNodeIndex(htmlPath, id);
|
|
1038
|
+
if (nodeIndex < 0) {
|
|
1039
|
+
// Tree resolved but the element id wasn't found in it. Hard error —
|
|
1040
|
+
// silently falling back to "any tap" would hide a typo and change
|
|
1041
|
+
// the program's behavior (a per-element awaiter would resume on the
|
|
1042
|
+
// wrong tap).
|
|
1043
|
+
pushUnknownElementDiagnostic(diagnostics, "ui.onTap", id, treeName);
|
|
1044
|
+
return null;
|
|
1045
|
+
}
|
|
1046
|
+
return {
|
|
1047
|
+
kind: "call",
|
|
1048
|
+
sourceSpan: makeSourceSpan(call, fileName, sourceText),
|
|
1049
|
+
callee: "__UI_TAP__",
|
|
1050
|
+
args: [{ kind: "number", value: nodeIndex }],
|
|
1051
|
+
isAwaited: true,
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
// Unknown tree (not a recognized UI import) — fall through to the global
|
|
1055
|
+
// form, with a warning. This is a different failure mode than a typo'd
|
|
1056
|
+
// element id: the receiver isn't a UI tree at all.
|
|
1057
|
+
diagnostics.push({
|
|
1058
|
+
severity: "warning", code: "ui-ontap-arg",
|
|
1059
|
+
message: `ui.onTap(${nodeArg.getText()}) could not be resolved; awaiting any tap instead`,
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
// Global form: await ui.onTap() — resume on the next tap anywhere.
|
|
1063
|
+
return {
|
|
1064
|
+
kind: "call",
|
|
1065
|
+
sourceSpan: makeSourceSpan(call, fileName, sourceText),
|
|
1066
|
+
callee: "__UI_TAP__",
|
|
1067
|
+
args: [{ kind: "number", value: -1 }],
|
|
1068
|
+
isAwaited: true,
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
/** Look up a node's index in its tree by element id (pre-order DFS order).
|
|
1072
|
+
* Returns the node index, or `-1` if the id is not found (or the module
|
|
1073
|
+
* isn't loaded). Callers must check for `-1` and emit a `ui-unknown-element`
|
|
1074
|
+
* diagnostic — silently treating `-1` as a valid index would target node 0
|
|
1075
|
+
* (the screen root), which is the silent-miscompilation failure mode this
|
|
1076
|
+
* guard exists to prevent. */
|
|
1077
|
+
export function resolveNodeIndex(htmlPath, id, screenId) {
|
|
1078
|
+
// Node indices follow pre-order DFS of the styled tree. The lowered tables
|
|
1079
|
+
// share this order, so we walk the registry's styled tree to find the id.
|
|
1080
|
+
// When screenId is given (grouped handle screen.groups.<screenId>.<id>),
|
|
1081
|
+
// search only within that screen root.
|
|
1082
|
+
const mod = requireUIHook().getUIModule(htmlPath);
|
|
1083
|
+
if (!mod)
|
|
1084
|
+
return -1;
|
|
1085
|
+
let idx = 0;
|
|
1086
|
+
let found = -1;
|
|
1087
|
+
const walk = (n) => {
|
|
1088
|
+
// TS handle name: prefer `ref`, fall back to `id` (backward-compatible).
|
|
1089
|
+
if ((n.ref ?? n.id) === id) {
|
|
1090
|
+
found = idx;
|
|
1091
|
+
return true;
|
|
1092
|
+
}
|
|
1093
|
+
idx++;
|
|
1094
|
+
for (const c of n.children) {
|
|
1095
|
+
if (walk(c))
|
|
1096
|
+
return true;
|
|
1097
|
+
}
|
|
1098
|
+
return false;
|
|
1099
|
+
};
|
|
1100
|
+
const allRoots = mod.allStyledScreens.length > 0 ? mod.allStyledScreens : [mod.styled];
|
|
1101
|
+
const roots = screenId ? allRoots.filter(r => r.id === screenId) : allRoots;
|
|
1102
|
+
for (const root of roots) {
|
|
1103
|
+
if (walk(root))
|
|
1104
|
+
break;
|
|
1105
|
+
}
|
|
1106
|
+
return found;
|
|
1107
|
+
}
|
|
1108
|
+
/** Look up a node's HTML tag by element id (pre-order DFS order).
|
|
1109
|
+
* Used to route generic callbacks (e.g. onChange) to the right lowering path
|
|
1110
|
+
* based on element kind (range vs input). Returns "" if not found. */
|
|
1111
|
+
export function resolveNodeTag(htmlPath, id, screenId) {
|
|
1112
|
+
const mod = requireUIHook().getUIModule(htmlPath);
|
|
1113
|
+
if (!mod)
|
|
1114
|
+
return "";
|
|
1115
|
+
let idx = 0;
|
|
1116
|
+
let foundTag = "";
|
|
1117
|
+
const walk = (n) => {
|
|
1118
|
+
if ((n.ref ?? n.id) === id) {
|
|
1119
|
+
foundTag = n.tag;
|
|
1120
|
+
return true;
|
|
1121
|
+
}
|
|
1122
|
+
idx++;
|
|
1123
|
+
for (const c of n.children) {
|
|
1124
|
+
if (walk(c))
|
|
1125
|
+
return true;
|
|
1126
|
+
}
|
|
1127
|
+
return false;
|
|
1128
|
+
};
|
|
1129
|
+
const allRoots = mod.allStyledScreens.length > 0 ? mod.allStyledScreens : [mod.styled];
|
|
1130
|
+
const roots = screenId ? allRoots.filter(r => r.id === screenId) : allRoots;
|
|
1131
|
+
for (const root of roots) {
|
|
1132
|
+
if (walk(root))
|
|
1133
|
+
break;
|
|
1134
|
+
}
|
|
1135
|
+
return foundTag;
|
|
1136
|
+
}
|