@kudzujs/core 0.8.35 → 0.8.36

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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  This document is the source of truth for Kudzu's product direction, architecture invariants, and future development order. Read it before extending React-shaped syntax or browser capabilities.
4
4
 
5
- The executable post-`0.8.35` compiler and large-application sequence is maintained in [`docs/next-architecture/large-application-ai-native-roadmap.md`](./docs/next-architecture/large-application-ai-native-roadmap.md). Follow its PR dependencies for implementation work; this document remains authoritative when selecting or accepting a migration capability.
5
+ The executable post-`0.8.36` compiler and large-application sequence is maintained in [`docs/next-architecture/large-application-ai-native-roadmap.md`](./docs/next-architecture/large-application-ai-native-roadmap.md). Follow its PR dependencies for implementation work; this document remains authoritative when selecting or accepting a migration capability.
6
6
 
7
7
  [`GOAL_A.md`](./GOAL_A.md) and [`GOAL_B.md`](./GOAL_B.md) are completed capability-validation records. Their commerce and realtime dashboard fixtures prove general lifecycle, navigation, async-workflow, and Worker capabilities; they are not separate product verticals or future priority lists.
8
8
 
package/PERFORMANCE.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  Reproducibility classes: `npm run benchmark`, `npm run benchmark:keyed`, `npm run benchmark:native`, and `npm run benchmark:module-cache` are maintained in this repository; `npm run benchmark:commerce` is a maintained paired runner over the public external storefront; older excluded-workspace sections are historical provenance only and are not current framework rankings.
4
4
 
5
+ ## P0.9 Semantic State Operations
6
+
7
+ Measured UTC 2026-08-11 on the Intel Core i5-9500 Linux x64 host with Node 24.14.0. The baseline was clean tag `v0.8.35` at `f25700d9d2b247c01db19f0e8c95f16cb1fa81a5`. The compiler and focused-check patch had SHA-256 `4c3c8a3de18b1e792ea84cf7608a89971850195036a58f37dd76e359bfc8a58d`, produced by:
8
+
9
+ ```bash
10
+ git diff --binary v0.8.35 -- framework/compiler/optimize/command-specialization.mjs framework/compiler/descriptor-session.mjs test/compiler-passes.test.mjs test/fixtures/effect-isolation/src/pages/command.tsx test/framework.test.mjs | sha256sum
11
+ ```
12
+
13
+ The maintained 100-importer fixture used three warm-ups and 21 alternating fresh-process samples. P0.9 keeps the existing direct command fast path first and invokes whole-handler semantic analysis only after direct specialization fails. Baseline and candidate produced the same normalized 382,603-byte graph, SHA-256 `8c35b3f6d2c571306bd97c4d51d4af76ca244badd36c57363bf579ef961f41aa`, 395,346-byte source result, and 103 / 103 / 100 parse, summary, and clone counts.
14
+
15
+ | Target | Compiler median | Range | Peak RSS median | Source-result bytes |
16
+ |---|---:|---:|---:|---:|
17
+ | `v0.8.35` | 947.100 ms | 839.270-1,064.444 ms | 258.0 MiB | 395,346 B |
18
+ | P0.9 candidate | 942.596 ms | 849.656-1,215.997 ms | 258.1 MiB | 395,346 B |
19
+
20
+ The candidate's unpaired median is 0.48% lower. Round-paired candidate-minus-baseline differences had a +1.754 ms median with the candidate faster in 10/21 pairs and the baseline faster in 11/21. Timing and peak-RSS ranges overlap, so no material improvement or regression is claimed. An initial implementation that routed every direct handler through whole-handler analysis measured a +12.584 ms paired median and 10.6 MiB higher RSS median; restoring the direct fast path and narrowing the analyzer removed that regression before this final record.
21
+
22
+ ```text
23
+ v0.8.35: [955.382,927.389,935.312,852.622,839.270,918.424,943.013,1026.033,936.514,1019.725,921.591,1064.444,949.671,975.729,933.380,896.773,949.751,951.418,947.100,998.950,1041.266]
24
+ candidate: [942.596,1215.997,965.312,849.656,877.468,928.334,971.812,966.482,938.268,1016.268,932.327,939.217,928.284,1082.782,1086.319,976.534,881.665,915.266,962.642,905.596,1000.939]
25
+ paired candidate-baseline: [-12.786,288.608,30.000,-2.966,38.198,9.910,28.799,-59.551,1.754,-3.457,10.736,-125.227,-21.387,107.053,152.939,79.761,-68.086,-36.152,15.542,-93.354,-40.327]
26
+ ```
27
+
28
+ The four required source forms lower to the same existing command HandlerIR and command-only browser path. Alias/helper forms therefore avoid the native handler module and native runtime they previously required; no command ABI, state batching, ownership, runtime source, or unaffected route artifact changes.
29
+
5
30
  ## P0.8 Stable ModuleSymbol And SiteId
6
31
 
7
32
  Measured UTC 2026-08-11 on the same Intel Core i5-9500 Linux x64 host with Node 24.14.0. The baseline was clean tag `v0.8.34` at `007fcb6e23c7d5bc742fa37c28388d070da9f598`. The compiler and maintained-check patch had SHA-256 `10ed6beb448b9e86961afab0b798f010932b8a29f98475d41f7bd8cfad04a872`, produced by:
package/README.md CHANGED
@@ -14,7 +14,7 @@ Kudzu compiles ordinary React-shaped TypeScript and TSX into complete static HTM
14
14
 
15
15
  > Experimental `0.8.x`: the compiler API and supported TSX surface may change.
16
16
 
17
- **Latest release: 0.8.35 - Stable module identity.** Cross-module declarations now resolve through stable ModuleSymbol and source-local SiteId records across aliases, barrel chains, `export *`, and cloned transformer trees. Read the [release notes](./RELEASES.md#0835---stable-module-identity), open the [release page](https://github.com/kudzujs/kudzu/releases/tag/v0.8.35), or follow the [architecture packet](./docs/next-architecture/README.md).
17
+ **Latest release: 0.8.36 - Semantic state operations.** Equivalent direct, aliased, and local-helper state updates now lower to the same command IR without adding handler JavaScript. Read the [release notes](./RELEASES.md#0836---semantic-state-operations), open the [release page](https://github.com/kudzujs/kudzu/releases/tag/v0.8.36), or follow the [architecture packet](./docs/next-architecture/README.md).
18
18
 
19
19
  - [Documentation](https://kudzujs.cloud/docs)
20
20
  - [Installation guide](https://kudzujs.cloud/docs#install)
@@ -32,6 +32,7 @@ Kudzu requires Node.js 22 or newer.
32
32
  ```bash
33
33
  npm create kudzu@latest my-app
34
34
  cd my-app
35
+ npm install
35
36
  npm run dev
36
37
  ```
37
38
 
package/RELEASES.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # Kudzu Releases
2
2
 
3
+ ## 0.8.36 - Semantic state operations
4
+
5
+ Kudzu 0.8.36 completes P0.9 by proving equivalent direct, aliased, and local-helper state updates and lowering them through the existing command HandlerIR path.
6
+
7
+ ### Changed in 0.8.36
8
+
9
+ - Direct `setCount(count + 1)`, an immutable result alias, a zero-argument arrow helper, and a one-parameter function helper emit the same structured `add` command.
10
+ - Binding identity proves state, setter, helper, and parameter ownership before specialization.
11
+ - Recursion, helper or alias escape, mutation, optional calls, and object-based dynamic dispatch fail at authored source locations.
12
+ - The existing direct command fast path remains first; whole-handler analysis runs only when direct specialization fails.
13
+ - Quick Start instructions now include an explicit `npm install`, and `create-kudzu@0.1.102` reports install-aware next steps and includes the install command in generated READMEs.
14
+
15
+ ### Performance
16
+
17
+ - The maintained 100-importer fixture preserves the same normalized graph, source-result bytes, digest, and 103 / 103 / 100 parse, summary, and clone counts.
18
+ - Twenty-one alternating `v0.8.35`/candidate samples measured compiler medians of 947.100 ms and 942.596 ms. The +1.754 ms paired candidate-minus-baseline median and overlapping RSS ranges establish no material improvement or regression.
19
+ - Alias and helper forms avoid native handler ESM and reuse the existing command runtime; direct command artifacts remain unchanged.
20
+
21
+ ### Validation
22
+
23
+ - `npm run check`, `npm test`, and `npm run test:package` pass with all 197 tests and 151 generated pages.
24
+ - Focused coverage proves identical JSON-safe command IR, signal ownership, build-module behavior calls, RouteIR commands, and zero native-handler artifacts for all four source forms.
25
+ - Existing command ABI, synchronous batching, ownership, browser runtime, and unrelated native handlers remain unchanged.
26
+ - P0.10 ModuleIR reference unification is next.
27
+
28
+ ### Upgrade
29
+
30
+ ```bash
31
+ npm install @kudzujs/core@^0.8.36
32
+ ```
33
+
3
34
  ## 0.8.35 - Stable module identity
4
35
 
5
36
  Kudzu 0.8.35 completes P0.8 by giving cross-module declarations and authored compiler sites stable identities that do not depend on transformed TypeScript AST objects or readable names.
@@ -11,7 +11,7 @@ The top-level [`GOAL_A.md`](../../GOAL_A.md) and [`GOAL_B.md`](../../GOAL_B.md)
11
11
  | C: state/resource model | Research only | Reduced fixtures expose a limitation |
12
12
  | D: routing compatibility | Current behavior preserved | Revisit only with migration evidence and invariant review |
13
13
 
14
- The active post-`0.8.35` implementation sequence is [`large-application-ai-native-roadmap.md`](./large-application-ai-native-roadmap.md). P0.8 stable ModuleSymbol and SiteId is complete; P0.9 semantic state operations is next. The plan orders compiler semantic generalization, large-application foundations, compatibility boundaries, AI tooling, and production validation without changing the invariants below.
14
+ The active post-`0.8.36` implementation sequence is [`large-application-ai-native-roadmap.md`](./large-application-ai-native-roadmap.md). P0.9 semantic state operations is complete; P0.10 ModuleIR reference unification is next. The plan orders compiler semantic generalization, large-application foundations, compatibility boundaries, AI tooling, and production validation without changing the invariants below.
15
15
 
16
16
  ## Required Invariants
17
17
 
@@ -1,6 +1,6 @@
1
1
  # Current Compiler Architecture
2
2
 
3
- This maps the current `0.8.35` architecture, built on the completed `0.8.23` Goal A compiler foundation. File and function names are the stable references; line numbers are intentionally omitted because later work may still move code.
3
+ This maps the current `0.8.36` architecture, built on the completed `0.8.23` Goal A compiler foundation. File and function names are the stable references; line numbers are intentionally omitted because later work may still move code.
4
4
 
5
5
  ## Responsibility Map
6
6
 
@@ -19,7 +19,7 @@ This maps the current `0.8.35` architecture, built on the completed `0.8.23` Goa
19
19
  | Main semantic analysis | [`framework/compiler/source-compiler.mjs`](../../framework/compiler/source-compiler.mjs), `createKudzuTransformer()` | Produces transformed source plus explicit component, handler, binding, derived, keyed, and effect ownership results. |
20
20
  | Component ownership analysis | [`framework/compiler/analysis/component-analysis.mjs`](../../framework/compiler/analysis/component-analysis.mjs) | Retains ordered JSON-safe owner and specialization records for state, setters, props, refs, IDs, direct signal links, source-local SiteIds, and source provenance; AST identity remains private to its source-local session. |
21
21
  | Per-source descriptor registration | [`framework/compiler/descriptor-session.mjs`](../../framework/compiler/descriptor-session.mjs), `createSemanticArtifact()`, `createDescriptorSession()` | Keeps AST descriptors private during analysis, then finalizes deterministic JSON-safe HandlerIR, BindingIR, DerivedIR, KeyedBlockIR, EffectIR, imports, and client roots into ModuleIR and validates every local slot reference. |
22
- | Command IR and codegen | [`framework/compiler/optimize/command-specialization.mjs`](../../framework/compiler/optimize/command-specialization.mjs), [`framework/compiler/ir/module-ir.mjs`](../../framework/compiler/ir/module-ir.mjs), [`framework/compiler/codegen/command-codegen.mjs`](../../framework/compiler/codegen/command-codegen.mjs) | Supported command handlers specialize to JSON-safe ModuleIR, then emit the existing `__kBehavior` AST without changing route plans. |
22
+ | Command IR and codegen | [`framework/compiler/optimize/command-specialization.mjs`](../../framework/compiler/optimize/command-specialization.mjs), [`framework/compiler/ir/module-ir.mjs`](../../framework/compiler/ir/module-ir.mjs), [`framework/compiler/codegen/command-codegen.mjs`](../../framework/compiler/codegen/command-codegen.mjs) | Direct commands use the existing fast path; proven immutable state aliases and one-call local helpers specialize to the same JSON-safe command ModuleIR. Recursion, escape, mutation, and dynamic helper dispatch fail explicitly, while unrelated handlers retain native ESM. Codegen emits the existing `__kBehavior` AST and command ABI. |
23
23
  | Source compilation | [`framework/compiler/source-compiler.mjs`](../../framework/compiler/source-compiler.mjs), `compileSource()` | Runs TypeScript with the Kudzu transformer, rejects surviving React/Router references, and returns a JSON-safe project-relative build module, component analysis, ModuleIR, optional handler module, and imported assets without filesystem writes. |
24
24
  | Handler/evaluator lowering | [`framework/compiler/handler-lowering.mjs`](../../framework/compiler/handler-lowering.mjs) | Completes source-local callback/binding/list AST rewriting and diagnostics before the JSON-safe IR boundary. |
25
25
  | Handler module codegen | [`framework/compiler/handler-codegen.mjs`](../../framework/compiler/handler-codegen.mjs) | Renders finalized ordered imports and concatenates generated module-export source without TypeScript AST or semantic discovery. |
@@ -137,6 +137,7 @@ This is an incremental evolution of the current repository:
137
137
  - [x] P0.6 Explicit ProjectSession is complete in `0.8.33`. Each build owns an absolute root, project paths, source records, bound graph operations, and Worker compiler. Explicit-root build/dev entry points preserve omitted-root CLI CWD behavior. One imported build function compiles two same-shaped roots with isolated config, HTML, `.kudzu`, source results, and Worker bundles; all 191 tests and package checks pass without browser or source-syntax changes.
138
138
  - [x] P0.7 Parsed module and export summary caching is complete in `0.8.34`. Canonical read-only source trees and narrow export summaries are invalidated together by source text and remain ProjectSession-local; every normalization context receives a deep clone with independent parent links. A 100-importer fixture parses and summarizes 103 unique page/barrel/component/helper modules exactly once and creates 200 importer-local clones. The maintained paired benchmark preserves the complete source-result digest and establishes no material timing or peak-RSS conclusion; all 193 tests and package checks pass without broadening exports, source syntax, or browser output.
139
139
  - [x] P0.8 Stable ModuleSymbol and SiteId is complete in `0.8.35`. ProjectSession records source-local declaration, import, and re-export sites and resolves stable ModuleSymbol records through default/named exports, aliases, barrel chains, `export *`, ambiguity, and cycles. Cross-module compiler consumers locate resolved declarations by SiteId in their private normalized clones, while component calls, hooks, keyed blocks, effects, and ownership records expose deterministic source-local SiteIds. Repeated sessions and compilations preserve IDs, and symbol-only barrel traversal avoids cloning intermediate modules without changing browser artifacts or source syntax.
140
+ - [x] P0.9 Semantic State Operations is complete in `0.8.36`. Direct setters, one immutable state-value alias, one synchronous zero-argument arrow helper, and one synchronous one-parameter function helper lower to identical existing command HandlerIR. Binding identity proves state/setter/helper/parameter ownership; recursion, escape, mutation, and dynamic helper dispatch fail at authored source locations. Existing direct command specialization remains first, unrelated safe handlers retain native ESM, and no command ABI, runtime, JavaScript VM, or general expression IR is added.
140
141
 
141
142
  ### P0: Semantic Correctness And Compiler Foundation
142
143
 
@@ -339,6 +340,8 @@ function increment(value) { setCount(value + 1) }; increment(count)
339
340
 
340
341
  **Done condition:** the equivalent forms produce the same StateWrite semantics, browser result, ownership references, and zero-unused-runtime behavior; recursion, escape, mutation, and dynamic dispatch fail with explicit diagnostics.
341
342
 
343
+ **Completed in `0.8.36`:** The command specializer now performs narrow whole-handler analysis only when the existing direct fast path fails. The four required forms emit the same `add` command, signal slot, owner, build-module behavior call, RouteIR command, and browser command runtime; alias/helper forms emit no native handler module. Focused compiler and command-only fixture coverage proves JSON-safe IR equivalence, source diagnostics, no native behavior artifacts, and unchanged batching/runtime ABI. The maintained 100-importer benchmark retains identical output and cache counts with no material timing or peak-RSS conclusion.
344
+
342
345
  ### PR 10: ModuleIR Reference Unification
343
346
 
344
347
  **Objective:** replace mixed state names, export strings, formatted owner strings, and slots with validated source-local slots and SymbolRef records.
@@ -468,4 +471,4 @@ The first comparison is Kudzu versus React + Vite using the same agent, model, t
468
471
 
469
472
  ## Immediate Decision
470
473
 
471
- PR 1 through PR 8 are complete. The next PR is **PR 9: Semantic State Operations**. Do not skip directly to a store, resource, router, virtualization, or ecosystem package feature.
474
+ PR 1 through PR 9 are complete. The next PR is **PR 10: ModuleIR Reference Unification**. Do not skip directly to a store, resource, router, virtualization, or ecosystem package feature.
@@ -1,6 +1,6 @@
1
1
  # Planned Version Sequence
2
2
 
3
- This is an execution sequence, not release history. `0.8.16` through `0.8.35` are completed scopes represented by package/release records.
3
+ This is an execution sequence, not release history. `0.8.16` through `0.8.36` are completed scopes represented by package/release records.
4
4
 
5
5
  Keep each patch behavior-preserving and independently reviewable. If a boundary proves inseparable, revise this plan before combining releases; do not silently broaden a patch.
6
6
 
@@ -26,6 +26,7 @@ Keep each patch behavior-preserving and independently reviewable. If a boundary
26
26
  | `0.8.33` | Move root, graph, source records, compiler paths, and Worker ownership into an explicit build-scoped ProjectSession. | Two independent roots compile through one imported build entry without config, source, `.kudzu`, Worker, or output leakage; CLI CWD behavior remains unchanged. |
27
27
  | `0.8.34` | Cache canonical parsed modules and narrow export summaries within one ProjectSession while cloning transformer input. | A 100-importer fixture parses and summarizes 103 unique modules once, creates independent normalization trees, preserves the complete source-result digest, and leaks no cache state across projects. |
28
28
  | `0.8.35` | Resolve cross-module declarations through stable ModuleSymbol and source-local SiteId records. | Default/named exports, aliases, barrel chains, `export *`, cycles, ambiguity, repeated compilation, source invalidation, private clones, and measured build behavior remain deterministic. |
29
+ | `0.8.36` | Lower equivalent direct, aliased, and local-helper state updates through existing command HandlerIR. | The four required forms emit identical commands and no native handler module; recursion, escape, mutation, and dynamic dispatch fail at authored source locations. |
29
30
 
30
31
  ## Sequence Rules
31
32
 
@@ -38,7 +39,7 @@ Keep each patch behavior-preserving and independently reviewable. If a boundary
38
39
 
39
40
  ## Generator Versions
40
41
 
41
- Keep `create-kudzu@0.1.101` while its template remains unchanged. Its `@kudzujs/core` range `^0.8.15` already accepts every planned Goal A patch. Publish `0.1.102` only when the template or minimum core range changes; a future `0.9.0` template must use `^0.9.0`.
42
+ `create-kudzu@0.1.102` adds an explicit install step to the generated README and clearer completion output. Its `@kudzujs/core` range `^0.8.15` accepts every current `0.8.x` patch; a future `0.9.0` template must use `^0.9.0`.
42
43
 
43
44
  ## Release Boundary
44
45
 
@@ -149,8 +149,9 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
149
149
 
150
150
  function compileOptimizedEvent(expression, setters, stateOwners, owner, keyedBlock) {
151
151
  const statements = ts.isBlock(expression.body) ? expression.body.statements : [factory.createExpressionStatement(expression.body)]
152
- const commands = statements.map(statement => ts.isExpressionStatement(statement) && canSpecializeCommand(statement.expression, expression, setters) ? compileEventCommand(statement.expression, setters) : undefined)
153
- if (!commands.length || commands.some(command => !command)) return undefined
152
+ let commands = statements.map(statement => ts.isExpressionStatement(statement) && canSpecializeCommand(statement.expression, expression, setters) ? compileEventCommand(statement.expression, setters) : undefined)
153
+ if ((!commands.length || commands.some(command => !command)) && compileEventCommand.handler) commands = compileEventCommand.handler(expression, setters, bindingIndex)
154
+ if (!commands?.length || commands.some(command => !command)) return undefined
154
155
  const original = ts.getOriginalNode(expression)
155
156
  const source = original.pos >= 0 && original.end >= 0 ? { file: sourceName(original.getSourceFile()), start: original.getStart(), end: original.end } : undefined
156
157
  const handler = registerCommandHandler(moduleIR, commands.map(command => ({ ...command, owner: stateOwners.get(command.state) ?? owner })), source, owner)
@@ -1,32 +1,203 @@
1
1
  import ts from "typescript"
2
+ import { sourceNodeError } from "../ast-helpers.mjs"
2
3
 
3
4
  export function createCommandSpecializer({ isPrimitiveLiteral }) {
4
- return function specializeCommand(expression, setters) {
5
+ const specialize = (expression, setters) => {
5
6
  if (ts.isCallExpression(expression) && ts.isPropertyAccessExpression(expression.expression) && ts.isIdentifier(expression.expression.expression) && expression.expression.expression.text === "console" && expression.expression.name.text === "log" && expression.arguments.length === 2 && ts.isStringLiteral(expression.arguments[0]) && ts.isIdentifier(expression.arguments[1]) && [...setters.values()].includes(expression.arguments[1].text)) {
6
7
  return { operation: "log", state: expression.arguments[1].text, value: expression.arguments[0].text }
7
8
  }
8
-
9
9
  if (!ts.isCallExpression(expression) || !ts.isIdentifier(expression.expression) || expression.arguments.length !== 1) return undefined
10
10
  const state = setters.get(expression.expression.text)
11
11
  if (!state) return undefined
12
-
13
12
  const value = expression.arguments[0]
14
- if (ts.isBinaryExpression(value) && ts.isIdentifier(value.left) && value.left.text === state && ts.isNumericLiteral(value.right)) {
15
- if (value.operatorToken.kind !== ts.SyntaxKind.PlusToken && value.operatorToken.kind !== ts.SyntaxKind.MinusToken) return undefined
16
- const operand = signedNumber(value.right, value.operatorToken.kind === ts.SyntaxKind.MinusToken ? "negative" : undefined)
17
- return operand ? { operation: "add", state, ...operand } : undefined
18
- }
19
- if (ts.isArrowFunction(value) && value.parameters.length === 1 && ts.isIdentifier(value.parameters[0].name) && ts.isBinaryExpression(value.body) && ts.isIdentifier(value.body.left) && value.body.left.text === value.parameters[0].name.text && ts.isNumericLiteral(value.body.right)) {
20
- if (value.body.operatorToken.kind !== ts.SyntaxKind.PlusToken && value.body.operatorToken.kind !== ts.SyntaxKind.MinusToken) return undefined
21
- const operand = signedNumber(value.body.right, value.body.operatorToken.kind === ts.SyntaxKind.MinusToken ? "negative" : undefined)
22
- return operand ? { operation: "add", state, ...operand } : undefined
23
- }
13
+ if (ts.isBinaryExpression(value) && ts.isIdentifier(value.left) && value.left.text === state) return addCommand(state, value)
14
+ if (ts.isArrowFunction(value) && value.parameters.length === 1 && ts.isIdentifier(value.parameters[0].name) && ts.isBinaryExpression(value.body) && ts.isIdentifier(value.body.left) && value.body.left.text === value.parameters[0].name.text) return addCommand(state, value.body)
24
15
  if (isPrimitiveLiteral(value)) {
25
16
  const literal = primitiveValue(value)
26
17
  return literal ? { operation: "set", state, ...literal } : undefined
27
18
  }
28
- return undefined
29
19
  }
20
+ specialize.handler = (handler, setters, bindingIndex) => specializeHandler(handler, setters, bindingIndex, specialize)
21
+ return specialize
22
+ }
23
+
24
+ function specializeHandler(handler, setters, bindingIndex, specialize) {
25
+ const statements = ts.isBlock(handler.body) ? handler.body.statements.filter(statement => !ts.isEmptyStatement(statement)) : []
26
+ if (statements.length === 2) {
27
+ const command = aliasCommand(statements, handler, setters, bindingIndex) ?? helperCommand(statements, handler, setters, bindingIndex, specialize)
28
+ if (command) return [command]
29
+ }
30
+ rejectUnsafe(statements, handler, setters, bindingIndex)
31
+ }
32
+
33
+ function aliasCommand([first, second], boundary, setters, bindingIndex) {
34
+ const declaration = oneDeclaration(first)
35
+ if (!declaration || !(first.declarationList.flags & ts.NodeFlags.Const) || !declaration.initializer || !ts.isExpressionStatement(second)) return undefined
36
+ const value = stateAdd(declaration.initializer, boundary, setters, bindingIndex)
37
+ const call = second.expression
38
+ if (!value || !directCall(call) || call.arguments.length !== 1 || !ts.isIdentifier(call.arguments[0])) return undefined
39
+ const state = setters.get(call.expression.text)
40
+ if (state !== value.state || !captured(call.expression, boundary, call.expression.text, bindingIndex) || !declaredBy(call.arguments[0], declaration.name, boundary, bindingIndex) || refs(boundary, declaration.name, boundary, bindingIndex).length !== 1) return undefined
41
+ return { operation: "add", state, ...value.operand }
42
+ }
43
+
44
+ function helperCommand([first, second], boundary, setters, bindingIndex, specialize) {
45
+ if (!ts.isExpressionStatement(second) || !directCall(second.expression)) return undefined
46
+ const call = second.expression
47
+ let name
48
+ let command
49
+ const declaration = oneDeclaration(first)
50
+ if (declaration && first.declarationList.flags & ts.NodeFlags.Const && declaration.initializer && ts.isArrowFunction(declaration.initializer) && !declaration.initializer.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) && !declaration.initializer.parameters.length && !ts.isBlock(declaration.initializer.body) && !call.arguments.length) {
51
+ name = declaration.name
52
+ command = safeDirect(declaration.initializer.body, boundary, setters, bindingIndex, specialize)
53
+ } else if (ts.isFunctionDeclaration(first) && first.name && first.body && !first.asteriskToken && !first.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) && first.parameters.length === 1 && first.body.statements.length === 1 && ts.isExpressionStatement(first.body.statements[0]) && call.arguments.length === 1 && ts.isIdentifier(call.arguments[0])) {
54
+ const parameter = first.parameters[0]
55
+ if (!ts.isIdentifier(parameter.name) || parameter.initializer || parameter.dotDotDotToken) return undefined
56
+ name = first.name
57
+ command = parameterCommand(first.body.statements[0].expression, parameter.name, first, call.arguments[0], boundary, setters, bindingIndex)
58
+ }
59
+ if (!name || !command || call.expression.text !== name.text || !declaredBy(call.expression, name, boundary, bindingIndex) || refs(boundary, name, boundary, bindingIndex).length !== 1) return undefined
60
+ return command
61
+ }
62
+
63
+ function safeDirect(expression, boundary, setters, bindingIndex, specialize) {
64
+ const command = specialize(expression, setters)
65
+ if (!command || command.operation === "log") return undefined
66
+ if (!captured(expression.expression, boundary, expression.expression.text, bindingIndex)) return undefined
67
+ const value = expression.arguments[0]
68
+ return !ts.isBinaryExpression(value) || captured(value.left, boundary, command.state, bindingIndex) ? command : undefined
69
+ }
70
+
71
+ function parameterCommand(expression, parameter, helper, argument, boundary, setters, bindingIndex) {
72
+ if (!directCall(expression) || expression.arguments.length !== 1 || !ts.isBinaryExpression(expression.arguments[0])) return undefined
73
+ const state = setters.get(expression.expression.text)
74
+ const value = expression.arguments[0]
75
+ if (!state || !captured(expression.expression, boundary, expression.expression.text, bindingIndex) || !ts.isIdentifier(value.left) || !declaredBy(value.left, parameter, helper, bindingIndex) || !captured(argument, boundary, state, bindingIndex)) return undefined
76
+ const operand = addOperand(value)
77
+ return operand ? { operation: "add", state, ...operand } : undefined
78
+ }
79
+
80
+ function rejectUnsafe(statements, boundary, setters, bindingIndex) {
81
+ if (!bindingIndex) return
82
+ for (const statement of statements) {
83
+ const declaration = oneDeclaration(statement)
84
+ if (declaration?.initializer && ts.isObjectLiteralExpression(declaration.initializer) && containsSetter(declaration.initializer, boundary, setters, bindingIndex)) {
85
+ const use = refs(boundary, declaration.name, boundary, bindingIndex).find(dynamic)
86
+ if (use) fail(use, "Semantic state helpers do not support dynamic dispatch")
87
+ }
88
+ if (declaration?.initializer && stateAdd(declaration.initializer, boundary, setters, bindingIndex)) {
89
+ const use = setterAliasUse(boundary, declaration.name, setters, bindingIndex)
90
+ if (!use) continue
91
+ const uses = refs(boundary, declaration.name, boundary, bindingIndex)
92
+ if (!(statement.declarationList.flags & ts.NodeFlags.Const) || uses.some(mutated)) fail(declaration.name, "Semantic state aliases must remain immutable")
93
+ fail(uses.find(reference => reference !== use) ?? use, "Semantic state aliases must be passed directly to one setter and cannot escape")
94
+ }
95
+ const helper = helperDeclaration(statement, boundary, setters, bindingIndex)
96
+ if (!helper) continue
97
+ const uses = refs(boundary, helper.name, boundary, bindingIndex)
98
+ if (uses.some(reference => inside(reference, helper.body))) fail(helper.name, "Semantic state helpers cannot be recursive")
99
+ if (helper.mutable || uses.some(mutated)) fail(helper.name, "Semantic state helpers must remain immutable")
100
+ if (uses.some(dynamic)) fail(uses.find(dynamic), "Semantic state helpers do not support dynamic dispatch")
101
+ if (uses.length !== 1 || !directIdentifierCall(uses[0])) fail(uses.find(reference => !directIdentifierCall(reference)) ?? helper.name, "Semantic state helpers must be called exactly once and cannot escape")
102
+ fail(helper.body, "Semantic state helpers must contain one synchronous state operation")
103
+ }
104
+ }
105
+
106
+ function helperDeclaration(statement, boundary, setters, bindingIndex) {
107
+ const declaration = oneDeclaration(statement)
108
+ if (declaration?.initializer && (ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer)) && containsSetter(declaration.initializer, boundary, setters, bindingIndex)) return { name: declaration.name, body: declaration.initializer, mutable: !(statement.declarationList.flags & ts.NodeFlags.Const) }
109
+ if (ts.isFunctionDeclaration(statement) && statement.name && statement.body && containsSetter(statement.body, boundary, setters, bindingIndex)) return { name: statement.name, body: statement.body, mutable: false }
110
+ }
111
+
112
+ function stateAdd(expression, boundary, setters, bindingIndex) {
113
+ if (!ts.isBinaryExpression(expression) || !ts.isIdentifier(expression.left) || ![...setters.values()].includes(expression.left.text) || !captured(expression.left, boundary, expression.left.text, bindingIndex)) return undefined
114
+ const operand = addOperand(expression)
115
+ return operand ? { state: expression.left.text, operand } : undefined
116
+ }
117
+
118
+ function addCommand(state, expression) {
119
+ const operand = addOperand(expression)
120
+ return operand ? { operation: "add", state, ...operand } : undefined
121
+ }
122
+
123
+ function addOperand(expression) {
124
+ if (!ts.isBinaryExpression(expression) || !ts.isNumericLiteral(expression.right) || ![ts.SyntaxKind.PlusToken, ts.SyntaxKind.MinusToken].includes(expression.operatorToken.kind)) return undefined
125
+ return signedNumber(expression.right, expression.operatorToken.kind === ts.SyntaxKind.MinusToken ? "negative" : undefined)
126
+ }
127
+
128
+ function oneDeclaration(statement) {
129
+ if (!ts.isVariableStatement(statement) || statement.declarationList.declarations.length !== 1) return undefined
130
+ const declaration = statement.declarationList.declarations[0]
131
+ return ts.isIdentifier(declaration.name) ? declaration : undefined
132
+ }
133
+
134
+ function directCall(node) {
135
+ return ts.isCallExpression(node) && !node.questionDotToken && ts.isIdentifier(node.expression)
136
+ }
137
+
138
+ function captured(identifier, boundary, name, bindingIndex) {
139
+ if (!ts.isIdentifier(identifier)) return false
140
+ const resolution = bindingIndex?.resolveReference(identifier, boundary)
141
+ return !resolution ? identifier.text === name : resolution.debugName === name && ["capture", "unresolved"].includes(resolution.kind)
142
+ }
143
+
144
+ function declaredBy(identifier, declaration, boundary, bindingIndex) {
145
+ const resolution = bindingIndex?.resolveReference(identifier, boundary)
146
+ return bindingIndex ? resolution?.declaration === declaration : identifier.text === declaration.text
147
+ }
148
+
149
+ function refs(root, declaration, boundary, bindingIndex) {
150
+ const found = []
151
+ const visit = node => {
152
+ if (ts.isIdentifier(node) && declaredBy(node, declaration, boundary, bindingIndex)) found.push(node)
153
+ ts.forEachChild(node, visit)
154
+ }
155
+ visit(root)
156
+ return found
157
+ }
158
+
159
+ function containsSetter(root, boundary, setters, bindingIndex) {
160
+ let found = false
161
+ const visit = node => {
162
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && setters.has(node.expression.text) && captured(node.expression, boundary, node.expression.text, bindingIndex)) found = true
163
+ if (!found) ts.forEachChild(node, visit)
164
+ }
165
+ visit(root)
166
+ return found
167
+ }
168
+
169
+ function setterAliasUse(root, declaration, setters, bindingIndex) {
170
+ let found
171
+ const visit = node => {
172
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && setters.has(node.expression.text) && ts.isIdentifier(node.arguments[0]) && declaredBy(node.arguments[0], declaration, root, bindingIndex)) found = node.arguments[0]
173
+ if (!found) ts.forEachChild(node, visit)
174
+ }
175
+ visit(root)
176
+ return found
177
+ }
178
+
179
+ function directIdentifierCall(identifier) {
180
+ return ts.isCallExpression(identifier.parent) && identifier.parent.expression === identifier && !identifier.parent.questionDotToken
181
+ }
182
+
183
+ function dynamic(identifier) {
184
+ if (ts.isCallExpression(identifier.parent) && identifier.parent.expression === identifier) return Boolean(identifier.parent.questionDotToken)
185
+ for (let current = identifier.parent; current && !ts.isStatement(current); current = current.parent) if (ts.isCallExpression(current)) return true
186
+ return false
187
+ }
188
+
189
+ function mutated(identifier) {
190
+ const parent = identifier.parent
191
+ return ts.isPrefixUnaryExpression(parent) && [ts.SyntaxKind.PlusPlusToken, ts.SyntaxKind.MinusMinusToken].includes(parent.operator) || ts.isPostfixUnaryExpression(parent) || ts.isBinaryExpression(parent) && parent.left === identifier && parent.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && parent.operatorToken.kind <= ts.SyntaxKind.LastAssignment
192
+ }
193
+
194
+ function inside(node, root) {
195
+ for (let current = node; current; current = current.parent) if (current === root) return true
196
+ return false
197
+ }
198
+
199
+ function fail(node, message) {
200
+ throw sourceNodeError(node, node.getSourceFile(), message)
30
201
  }
31
202
 
32
203
  function primitiveValue(node) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.8.35",
3
+ "version": "0.8.36",
4
4
  "description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
5
5
  "type": "module",
6
6
  "license": "MIT",