@kudzujs/core 0.8.35 → 0.8.37

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.
@@ -1,48 +1,159 @@
1
1
  export function createModuleIR(file) {
2
- return { version: 1, file, signals: [], handlers: [], bindings: [], derived: [], effects: [], keyedBlocks: [], imports: [], clientModules: [] }
2
+ return { version: 2, file, symbols: [], signals: [], handlers: [], bindings: [], derived: [], effects: [], keyedBlocks: [], imports: [], clientModules: [] }
3
3
  }
4
4
 
5
- export function assertModuleIRReferences(moduleIR) {
6
- const slot = (records, value, label) => {
7
- if (!Number.isInteger(value) || value < 0 || value >= records.length) throw new Error(`ModuleIR ${label} references missing slot ${JSON.stringify(value)}`)
5
+ export function assertModuleIRReferences(moduleIR, componentAnalysis) {
6
+ if (moduleIR?.version !== 2) throw new Error(`Unsupported ModuleIR version: ${JSON.stringify(moduleIR?.version)}`)
7
+ if (componentAnalysis && componentAnalysis.version !== 2) throw new Error(`Unsupported ComponentAnalysis version: ${JSON.stringify(componentAnalysis.version)}`)
8
+ const slot = (records, value, label, kind) => {
9
+ if (!Number.isInteger(value) || value < 0 || value >= records.length) throw new Error(`${label} references missing ${kind} slot ${JSON.stringify(value)}`)
10
+ return records[value]
8
11
  }
9
- for (const [name, records] of [["signal", moduleIR.signals], ["handler", moduleIR.handlers], ["binding", moduleIR.bindings], ["derived", moduleIR.derived], ["effect", moduleIR.effects], ["keyed block", moduleIR.keyedBlocks]]) {
12
+ const indexed = (name, records) => {
10
13
  records.forEach((record, index) => {
11
- if (record.slot !== index) throw new Error(`ModuleIR ${name} slot ${JSON.stringify(record.slot)} must equal its index ${index}`)
14
+ if (record.slot !== index) throw new Error(`${name} slot ${JSON.stringify(record.slot)} must equal its index ${index}`)
12
15
  })
13
16
  }
17
+ indexed("SymbolRef", moduleIR.symbols)
18
+ indexed("SignalIR", moduleIR.signals)
19
+ indexed("HandlerIR", moduleIR.handlers)
20
+ indexed("BindingIR", moduleIR.bindings)
21
+ indexed("DerivedIR", moduleIR.derived)
22
+ indexed("EffectIR", moduleIR.effects)
23
+ indexed("KeyedBlockIR", moduleIR.keyedBlocks)
24
+ indexed("ImportIR", moduleIR.imports)
25
+ if (componentAnalysis) {
26
+ indexed("Component owner", componentAnalysis.owners)
27
+ indexed("Component specialization", componentAnalysis.specializations)
28
+ for (const owner of componentAnalysis.owners) {
29
+ indexed(`Component owner ${owner.slot} state`, owner.states)
30
+ indexed(`Component owner ${owner.slot} ref`, owner.refs)
31
+ indexed(`Component owner ${owner.slot} ID`, owner.ids)
32
+ for (const setter of owner.setters) slot(owner.states, setter.signal, `Component owner ${owner.slot} setter ${JSON.stringify(setter.name)}`, "state")
33
+ for (const state of owner.states) if (state.owner !== undefined) ownerRef(state.owner, `Component owner ${owner.slot} state ${state.slot} external owner`)
34
+ }
35
+ for (const specialization of componentAnalysis.specializations) {
36
+ indexed(`Component specialization ${specialization.slot} state`, specialization.states)
37
+ indexed(`Component specialization ${specialization.slot} ref`, specialization.refs)
38
+ indexed(`Component specialization ${specialization.slot} ID`, specialization.ids)
39
+ if (specialization.owner !== undefined) ownerRef(specialization.owner, `Component specialization ${specialization.slot} owner`)
40
+ for (const prop of specialization.props ?? []) for (const signal of prop.signals ?? []) slot(moduleIR.signals, signal, `Component specialization ${specialization.slot} prop ${JSON.stringify(prop.name)}`, "SignalIR")
41
+ }
42
+ }
43
+ function moduleSymbol(symbol, label) {
44
+ if (!symbol || typeof symbol.id !== "string" || typeof symbol.module !== "string" || typeof symbol.site !== "string" || typeof symbol.name !== "string") throw new Error(`${label} must be a ModuleSymbol`)
45
+ }
46
+ function ownerRef(reference, label) {
47
+ if (!reference || typeof reference !== "object") throw new Error(`${label} must be an OwnerRef`)
48
+ if (reference.kind === "component") return slot(componentAnalysis?.owners ?? [], reference.slot, label, "component owner")
49
+ if (reference.kind === "specialization") return slot(componentAnalysis?.specializations ?? [], reference.slot, label, "component specialization")
50
+ if (reference.kind === "module-symbol") return moduleSymbol(reference.symbol, label)
51
+ if (reference.kind !== "module") throw new Error(`${label} has invalid kind ${JSON.stringify(reference.kind)}`)
52
+ }
53
+ function stateRef(reference, label) {
54
+ if (!reference || typeof reference !== "object") throw new Error(`${label} must be a StateRef`)
55
+ if (reference.kind === "module-symbol") return moduleSymbol(reference.symbol, label)
56
+ if (reference.kind === "symbol") return slot(moduleIR.symbols, reference.symbol, label, "SymbolRef")
57
+ if (reference.kind !== "state") throw new Error(`${label} has invalid kind ${JSON.stringify(reference.kind)}`)
58
+ const owner = ownerRef(reference.owner, `${label} owner`)
59
+ const states = reference.owner.kind === "component" || reference.owner.kind === "specialization" ? owner.states : []
60
+ slot(states, reference.slot, label, "owner state")
61
+ }
62
+ const exports = new Map()
63
+ const exported = (record, label) => {
64
+ if (record.kind !== "module-export") return
65
+ if (typeof record.exportName !== "string" || !record.exportName) throw new Error(`${label} requires an export name`)
66
+ const previous = exports.get(record.exportName)
67
+ if (previous) throw new Error(`ModuleIR export ${JSON.stringify(record.exportName)} is declared by both ${previous} and ${label}`)
68
+ exports.set(record.exportName, label)
69
+ }
70
+ for (const signal of moduleIR.signals) stateRef(signal.reference, `SignalIR ${signal.slot}`)
14
71
  for (const handler of moduleIR.handlers) {
15
- for (const command of handler.commands ?? []) slot(moduleIR.signals, command.signal, `handler ${handler.slot} command signal`)
16
- if (handler.keyedBlock !== undefined) slot(moduleIR.keyedBlocks, handler.keyedBlock, `handler ${handler.slot} keyed block`)
72
+ exported(handler, `HandlerIR ${handler.slot}`)
73
+ for (const [index, command] of (handler.commands ?? []).entries()) slot(moduleIR.signals, command.signal, `HandlerIR ${handler.slot} command ${index}`, "SignalIR")
74
+ for (const [index, signal] of (handler.signals ?? []).entries()) slot(moduleIR.signals, signal.signal, `HandlerIR ${handler.slot} signal ${index}`, "SignalIR")
75
+ for (const [index, imported] of (handler.imports ?? []).entries()) slot(moduleIR.imports, imported, `HandlerIR ${handler.slot} import ${index}`, "ImportIR")
76
+ for (const [index, capture] of (handler.captures ?? []).entries()) if (capture.symbol !== undefined) slot(moduleIR.symbols, capture.symbol, `HandlerIR ${handler.slot} capture ${index}`, "SymbolRef")
77
+ if (handler.keyedBlock !== undefined) slot(moduleIR.keyedBlocks, handler.keyedBlock, `HandlerIR ${handler.slot} keyed block`, "KeyedBlockIR")
17
78
  }
18
- for (const binding of moduleIR.bindings) if (binding.keyedBlock !== undefined) slot(moduleIR.keyedBlocks, binding.keyedBlock, `binding ${binding.slot} keyed block`)
79
+ for (const binding of moduleIR.bindings) {
80
+ exported(binding, `BindingIR ${binding.slot}`)
81
+ for (const [index, signal] of (binding.signals ?? []).entries()) slot(moduleIR.signals, signal, `BindingIR ${binding.slot} signal ${index}`, "SignalIR")
82
+ for (const [index, imported] of (binding.imports ?? []).entries()) slot(moduleIR.imports, imported, `BindingIR ${binding.slot} import ${index}`, "ImportIR")
83
+ for (const [index, capture] of (binding.captures ?? []).entries()) if (capture.symbol !== undefined) slot(moduleIR.symbols, capture.symbol, `BindingIR ${binding.slot} capture ${index}`, "SymbolRef")
84
+ if (binding.keyedBlock !== undefined) slot(moduleIR.keyedBlocks, binding.keyedBlock, `BindingIR ${binding.slot} keyed block`, "KeyedBlockIR")
85
+ }
86
+ for (const derived of moduleIR.derived) for (const [index, signal] of (derived.signals ?? []).entries()) slot(moduleIR.signals, signal, `DerivedIR ${derived.slot} signal ${index}`, "SignalIR")
19
87
  for (const effect of moduleIR.effects) {
20
- slot(moduleIR.handlers, effect.setup?.handler, `effect ${effect.slot} setup handler`)
21
- for (const dependency of effect.dependencies ?? []) if (dependency.kind === "derived") slot(moduleIR.derived, dependency.derived, `effect ${effect.slot} derived dependency`)
22
- if (effect.ownership?.keyedBlock !== undefined) slot(moduleIR.keyedBlocks, effect.ownership.keyedBlock, `effect ${effect.slot} keyed block`)
88
+ const handler = slot(moduleIR.handlers, effect.setup?.handler, `EffectIR ${effect.slot} setup`, "HandlerIR")
89
+ if (handler.kind !== "module-export" || handler.role !== "effect") throw new Error(`EffectIR ${effect.slot} setup HandlerIR ${handler.slot} must have role "effect"`)
90
+ for (const [index, dependency] of (effect.dependencies ?? []).entries()) {
91
+ if (dependency.kind === "signal") slot(moduleIR.signals, dependency.signal, `EffectIR ${effect.slot} dependency ${index}`, "SignalIR")
92
+ else if (dependency.kind === "derived") {
93
+ slot(moduleIR.derived, dependency.derived, `EffectIR ${effect.slot} dependency ${index}`, "DerivedIR")
94
+ for (const [sourceIndex, signal] of (dependency.sources ?? []).entries()) slot(moduleIR.signals, signal, `EffectIR ${effect.slot} dependency ${index} source ${sourceIndex}`, "SignalIR")
95
+ } else throw new Error(`EffectIR ${effect.slot} dependency ${index} has invalid kind ${JSON.stringify(dependency.kind)}`)
96
+ }
97
+ for (const [index, signal] of (effect.subscriptions ?? []).entries()) slot(moduleIR.signals, signal, `EffectIR ${effect.slot} subscription ${index}`, "SignalIR")
98
+ for (const [index, signal] of (effect.dependencySignals ?? []).entries()) slot(moduleIR.signals, signal, `EffectIR ${effect.slot} dependency signal ${index}`, "SignalIR")
99
+ if (effect.ownership?.owner) ownerRef(effect.ownership.owner, `EffectIR ${effect.slot} ownership`)
100
+ if (effect.ownership?.keyedBlock !== undefined) {
101
+ slot(moduleIR.keyedBlocks, effect.ownership.keyedBlock, `EffectIR ${effect.slot} ownership`, "KeyedBlockIR")
102
+ if (handler.keyedBlock !== effect.ownership.keyedBlock) throw new Error(`EffectIR ${effect.slot} and HandlerIR ${handler.slot} must reference the same KeyedBlockIR`)
103
+ }
23
104
  }
24
105
  for (const block of moduleIR.keyedBlocks) {
25
- if (block.parent !== undefined) slot(moduleIR.keyedBlocks, block.parent, `keyed block ${block.slot} parent`)
26
- for (const child of block.children ?? []) slot(moduleIR.keyedBlocks, child, `keyed block ${block.slot} child`)
27
- if (block.selector !== undefined) slot(moduleIR.derived, block.selector, `keyed block ${block.slot} selector`)
106
+ if (block.collection?.kind === "signal") slot(moduleIR.signals, block.collection.signal, `KeyedBlockIR ${block.slot} collection`, "SignalIR")
107
+ else if (block.collection?.kind === "binding") slot(moduleIR.bindings, block.collection.binding, `KeyedBlockIR ${block.slot} collection`, "BindingIR")
108
+ else if (block.collection?.kind === "symbol") slot(moduleIR.symbols, block.collection.symbol, `KeyedBlockIR ${block.slot} collection`, "SymbolRef")
109
+ else if (block.collection?.kind !== "static") throw new Error(`KeyedBlockIR ${block.slot} collection has invalid kind ${JSON.stringify(block.collection?.kind)}`)
110
+ if (block.parent !== undefined) {
111
+ const parent = slot(moduleIR.keyedBlocks, block.parent, `KeyedBlockIR ${block.slot} parent`, "KeyedBlockIR")
112
+ if (!(parent.children ?? []).includes(block.slot)) throw new Error(`KeyedBlockIR ${block.slot} parent ${parent.slot} does not reciprocally list child ${block.slot}`)
113
+ }
114
+ for (const childSlot of block.children ?? []) {
115
+ const child = slot(moduleIR.keyedBlocks, childSlot, `KeyedBlockIR ${block.slot} child`, "KeyedBlockIR")
116
+ if (child.parent !== block.slot) throw new Error(`KeyedBlockIR ${block.slot} child ${child.slot} does not reciprocally reference parent ${block.slot}`)
117
+ }
118
+ if (new Set(block.children ?? []).size !== (block.children ?? []).length) throw new Error(`KeyedBlockIR ${block.slot} has duplicate children`)
119
+ if (block.selector !== undefined) slot(moduleIR.derived, block.selector, `KeyedBlockIR ${block.slot} selector`, "DerivedIR")
120
+ for (const [index, signal] of (block.selectorSignals ?? []).entries()) slot(moduleIR.signals, signal, `KeyedBlockIR ${block.slot} selector signal ${index}`, "SignalIR")
121
+ for (const [index, specialization] of (block.specializations ?? []).entries()) slot(componentAnalysis?.specializations ?? [], specialization, `KeyedBlockIR ${block.slot} specialization ${index}`, "component specialization")
122
+ for (const [index, row] of (block.rowStates ?? []).entries()) slot(moduleIR.signals, row.signal, `KeyedBlockIR ${block.slot} row state ${index}`, "SignalIR")
123
+ for (const [index, row] of (block.rowRefs ?? []).entries()) {
124
+ const specialization = slot(componentAnalysis?.specializations ?? [], row.specialization, `KeyedBlockIR ${block.slot} row ref ${index}`, "component specialization")
125
+ slot(specialization.refs, row.ref, `KeyedBlockIR ${block.slot} row ref ${index}`, "specialization ref")
126
+ }
127
+ }
128
+ const visiting = new Set()
129
+ const visited = new Set()
130
+ const visit = (block, trail) => {
131
+ if (visiting.has(block.slot)) throw new Error(`KeyedBlockIR parent cycle: ${[...trail, block.slot].join(" -> ")}`)
132
+ if (visited.has(block.slot)) return
133
+ visiting.add(block.slot)
134
+ if (block.parent !== undefined) visit(moduleIR.keyedBlocks[block.parent], [...trail, block.slot])
135
+ visiting.delete(block.slot)
136
+ visited.add(block.slot)
28
137
  }
138
+ for (const block of moduleIR.keyedBlocks) visit(block, [])
29
139
  return moduleIR
30
140
  }
31
141
 
32
- export function registerCommandHandler(moduleIR, commands, source, scope = "module") {
33
- const slots = new Map(moduleIR.signals.map(signal => [signal.key, signal.slot]))
34
- for (const { state, owner = scope } of commands) {
35
- const key = `${owner}:${state}`
36
- if (slots.has(key)) continue
37
- const slot = moduleIR.signals.length
38
- slots.set(key, slot)
39
- moduleIR.signals.push({ slot, key, debugName: state })
142
+ export function registerSignal(moduleIR, reference, debugName) {
143
+ const key = JSON.stringify(reference)
144
+ let signal = moduleIR.signals.find(entry => JSON.stringify(entry.reference) === key)
145
+ if (!signal) {
146
+ signal = { slot: moduleIR.signals.length, reference, debugName }
147
+ moduleIR.signals.push(signal)
40
148
  }
41
- const signal = command => slots.get(`${command.owner ?? scope}:${command.state}`)
149
+ return signal
150
+ }
151
+
152
+ export function registerCommandHandler(moduleIR, commands, source) {
42
153
  const handler = {
43
154
  slot: moduleIR.handlers.length,
44
155
  kind: "commands",
45
- commands: commands.map(({ operation, state, owner, value, syntax }) => ({ operation, signal: signal({ state, owner }), value, ...(syntax ? { syntax } : {}) })),
156
+ commands: commands.map(({ operation, reference, state, value, syntax }) => ({ operation, signal: registerSignal(moduleIR, reference, state).slot, value, ...(syntax ? { syntax } : {}) })),
46
157
  ...(source ? { source } : {})
47
158
  }
48
159
  moduleIR.handlers.push(handler)
@@ -50,31 +161,31 @@ export function registerCommandHandler(moduleIR, commands, source, scope = "modu
50
161
  }
51
162
 
52
163
  export function registerModuleHandler(moduleIR, descriptor) {
53
- const handler = { slot: moduleIR.handlers.length, kind: "module-export", ...descriptor }
164
+ const handler = { ...descriptor, slot: moduleIR.handlers.length, kind: "module-export" }
54
165
  moduleIR.handlers.push(handler)
55
166
  return handler
56
167
  }
57
168
 
58
169
  export function registerBinding(moduleIR, descriptor) {
59
- const binding = { slot: moduleIR.bindings.length, kind: "module-export", ...descriptor }
170
+ const binding = { ...descriptor, slot: moduleIR.bindings.length, kind: "module-export" }
60
171
  moduleIR.bindings.push(binding)
61
172
  return binding
62
173
  }
63
174
 
64
175
  export function registerDerived(moduleIR, descriptor) {
65
- const derived = { slot: moduleIR.derived.length, ...descriptor }
176
+ const derived = { ...descriptor, slot: moduleIR.derived.length }
66
177
  moduleIR.derived.push(derived)
67
178
  return derived
68
179
  }
69
180
 
70
181
  export function registerEffect(moduleIR, descriptor) {
71
- const effect = { slot: moduleIR.effects.length, ...descriptor }
182
+ const effect = { ...descriptor, slot: moduleIR.effects.length }
72
183
  moduleIR.effects.push(effect)
73
184
  return effect
74
185
  }
75
186
 
76
187
  export function registerKeyedBlock(moduleIR, descriptor) {
77
- const block = { slot: moduleIR.keyedBlocks.length, ...descriptor }
188
+ const block = { ...descriptor, slot: moduleIR.keyedBlocks.length }
78
189
  moduleIR.keyedBlocks.push(block)
79
190
  return block
80
191
  }
@@ -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) {