Skip to content

Commit 1e3a86d

Browse files
Compile machine execution kernel (#61)
1 parent fc9989b commit 1e3a86d

5 files changed

Lines changed: 661 additions & 280 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@typeonce/effect-machine": patch
3+
---
4+
5+
Compile reusable statechart execution metadata and run eligible flat machines through a synchronous specialized planner inside the compact Effect process kernel. This removes per-event Effect wrappers and repeated topology construction while preserving schema validation, raised-event stabilization, lifecycle ordering, observation, interruption, invoked children, and the public planning and machine APIs.

src/internal/machinePlanner.ts

Lines changed: 195 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,22 @@ interface Collected<Event> {
6767
readonly emittedEvents: Array<unknown>
6868
}
6969

70+
const targetBuilderCache = new WeakMap<object, Map<string, unknown>>()
71+
72+
const getTargetBuilder = (machine: Machine.Any, path: string): any => {
73+
let byPath = targetBuilderCache.get(machine)
74+
if (byPath === undefined) {
75+
byPath = new Map()
76+
targetBuilderCache.set(machine, byPath)
77+
}
78+
if (byPath.has(path)) {
79+
return byPath.get(path)
80+
}
81+
const builder = machine.makeTargetBuilder(path as any)
82+
byPath.set(path, builder)
83+
return builder
84+
}
85+
7086
const makeCollector = <Event>(machine: Machine.Any): Collected<Event> => {
7187
const commands: Array<RuntimeCommand> = []
7288
const raisedEvents: Array<Event> = []
@@ -389,7 +405,7 @@ const resolveHistoryTarget = (
389405
}
390406
const collected = collectTransition(machine, fallback, {
391407
event,
392-
target: machine.makeTargetBuilder(target.parent).full,
408+
target: getTargetBuilder(machine, target.parent).full,
393409
parent: target.parent
394410
})
395411
if (collected.state === undefined || isHistoryTarget(collected.state) || !isSnapshot(collected.state)) {
@@ -591,7 +607,7 @@ const makeTransitionContext = <
591607
parents: getParentValues(machine, configuration, path) as Machine.ParentStateValues<States, StateId>,
592608
event,
593609
snapshot,
594-
target: machine.makeTargetBuilder(path as StateId)
610+
target: getTargetBuilder(machine, path)
595611
})
596612

597613
const makeDoneContext = <
@@ -613,7 +629,7 @@ const makeDoneContext = <
613629
event,
614630
output: output as Machine.CompletionOutputByIdentifier<States, StateId>,
615631
snapshot,
616-
target: machine.makeTargetBuilder(path as StateId)
632+
target: getTargetBuilder(machine, path)
617633
})
618634

619635
const collectStateActions = <
@@ -715,7 +731,7 @@ const selectAlwaysTransitions = <
715731
>,
716732
event,
717733
snapshot: capturedSnapshot(),
718-
target: machine.makeTargetBuilder(path as Machine.StateIdentifier<States>)
734+
target: getTargetBuilder(machine, path)
719735
}
720736
})
721737
}
@@ -1081,7 +1097,7 @@ const resolveChoiceTarget = (
10811097
parent: getParentValue(machine, provisional, node.path),
10821098
parents: getParentValues(machine, provisional, node.path),
10831099
event,
1084-
target: machine.makeTargetBuilder(node.path as any)
1100+
target: getTargetBuilder(machine, node.path)
10851101
})
10861102
if (collected.state === undefined) {
10871103
throw new Error(`Machine choice resolver for "${node.path}" must return a target`)
@@ -1930,6 +1946,180 @@ const macrostepConfiguration = <
19301946
return settle(machine, step.next, decodedEvent, commands, raisedEvents, emittedEvents, microsteps)
19311947
}
19321948

1949+
interface FlatExecutionDescriptor {
1950+
readonly paths: ReadonlySet<string>
1951+
}
1952+
1953+
const compileFlatExecutionDescriptor = (machine: Machine.Any): FlatExecutionDescriptor | undefined => {
1954+
const paths = new Set<string>()
1955+
for (const node of machine.stateNodes.byPath.values() as Iterable<Machine.StateNode>) {
1956+
if (node.parent !== undefined || (node.type !== "atomic" && node.type !== "final")) {
1957+
return undefined
1958+
}
1959+
const config = machine.handlers[node.path] as Machine.AnyStateConfig | undefined
1960+
if (
1961+
config?.entry !== undefined || config?.exit !== undefined || config?.always !== undefined ||
1962+
config?.onDone !== undefined || config?.invoke !== undefined
1963+
) {
1964+
return undefined
1965+
}
1966+
paths.add(node.path)
1967+
}
1968+
return paths.size === 0 ? undefined : { paths }
1969+
}
1970+
1971+
const planFlatConfiguration = (
1972+
machine: Machine.Any,
1973+
descriptor: FlatExecutionDescriptor,
1974+
configuration: ActiveConfiguration,
1975+
input: unknown
1976+
): MacrostepPlan<ActiveConfiguration, any, any, any, any> => {
1977+
const decoded = decodeEventSync(machine, input) as { readonly _tag: PropertyKey }
1978+
if (isActiveFinalConfiguration(machine, configuration)) {
1979+
const completed = completeConfigurationSync(machine, configuration, decoded)
1980+
const root = getRootPath(machine, completed.configuration)
1981+
if (!completed.configuration.outputs.has(root)) {
1982+
throw new Error("Machine reached a terminal configuration without a completed root output")
1983+
}
1984+
return {
1985+
next: completed.configuration,
1986+
commands: [],
1987+
emittedEvents: [],
1988+
microsteps: [],
1989+
done: true,
1990+
output: completed.configuration.outputs.get(root)
1991+
}
1992+
}
1993+
1994+
let current = configuration
1995+
let event: any = decoded
1996+
const pending: Array<any> = []
1997+
let pendingIndex = 0
1998+
const commands: Array<RuntimeCommand> = []
1999+
const emittedEvents: Array<unknown> = []
2000+
const microsteps: Array<MicrostepPlan<ActiveConfiguration, any, any, any>> = []
2001+
let iterations = 0
2002+
2003+
while (true) {
2004+
iterations += 1
2005+
if (iterations > MaxMacrostepIterations) {
2006+
throw new InfiniteTransitionError({
2007+
machineId: machine.id,
2008+
state: String(getLeafPath(machine, current)),
2009+
maxIterations: MaxMacrostepIterations
2010+
})
2011+
}
2012+
2013+
const source = getRootPath(machine, current)
2014+
const transition = normalizeTransition(machine.handlers[source]?.on?.[event._tag])
2015+
if (transition !== undefined) {
2016+
const snapshot = snapshotFromConfiguration(machine, current)
2017+
const collected = collectTransition(machine, transition.transition, {
2018+
state: getActiveValue(current, source),
2019+
parent: undefined,
2020+
parents: {},
2021+
event,
2022+
snapshot,
2023+
target: getTargetBuilder(machine, source)
2024+
})
2025+
validateDeclaredTransitionTarget(
2026+
source,
2027+
{ type: "event", event: event._tag },
2028+
transition.targets,
2029+
collected.state
2030+
)
2031+
2032+
let next = current
2033+
let targetPath: string | undefined
2034+
if (collected.state !== undefined) {
2035+
if (!isTarget(collected.state) && !isSnapshot(collected.state)) {
2036+
throw new Error("Machine expected transition target to be a snapshot or target builder result")
2037+
}
2038+
targetPath = getTargetNodePath(collected.state)
2039+
if (!descriptor.paths.has(targetPath)) {
2040+
throw new Error(`Machine expected flat transition target "${targetPath}" to be a root state`)
2041+
}
2042+
next = normalizeTargetConfigurationSync(machine, current, collected.state as any)
2043+
}
2044+
const changed = transition.reenter || source !== targetPath && targetPath !== undefined
2045+
const exitPaths = changed ? [source] : []
2046+
const entryPaths = changed ? [getRootPath(machine, next)] : []
2047+
const step = {
2048+
next,
2049+
event,
2050+
transitions: [{
2051+
source,
2052+
trigger: { type: "event" as const, event: event._tag },
2053+
reenter: transition.reenter,
2054+
target: targetPath,
2055+
resolvedTarget: targetPath
2056+
}],
2057+
commands: collected.commands,
2058+
raisedEvents: collected.raisedEvents,
2059+
emittedEvents: collected.emittedEvents,
2060+
exitPaths,
2061+
entryPaths,
2062+
changed
2063+
}
2064+
current = next
2065+
commands.push(...collected.commands)
2066+
pending.push(...collected.raisedEvents)
2067+
emittedEvents.push(...collected.emittedEvents)
2068+
microsteps.push(step)
2069+
2070+
if (isActiveFinalConfiguration(machine, current)) {
2071+
const completed = completeConfigurationSync(machine, current, event)
2072+
const root = getRootPath(machine, completed.configuration)
2073+
if (!completed.configuration.outputs.has(root)) {
2074+
throw new Error("Machine reached a terminal configuration without a completed root output")
2075+
}
2076+
return {
2077+
next: completed.configuration,
2078+
commands,
2079+
emittedEvents,
2080+
microsteps,
2081+
done: true,
2082+
output: completed.configuration.outputs.get(root)
2083+
}
2084+
}
2085+
}
2086+
2087+
if (pendingIndex >= pending.length) {
2088+
return {
2089+
next: current,
2090+
commands,
2091+
emittedEvents,
2092+
microsteps,
2093+
done: false,
2094+
output: undefined
2095+
}
2096+
}
2097+
event = pending[pendingIndex++]
2098+
}
2099+
}
2100+
2101+
export interface CompiledExecutionPlan {
2102+
readonly plan: (
2103+
configuration: ActiveConfiguration,
2104+
event: unknown
2105+
) => MacrostepPlan<ActiveConfiguration, any, any, any, any>
2106+
}
2107+
2108+
const executionPlanCache = new WeakMap<Machine.Any, CompiledExecutionPlan>()
2109+
2110+
export const compileExecutionPlan = (machine: Machine.Any): CompiledExecutionPlan => {
2111+
const cached = executionPlanCache.get(machine)
2112+
if (cached !== undefined) {
2113+
return cached
2114+
}
2115+
const flat = compileFlatExecutionDescriptor(machine)
2116+
const compiled: CompiledExecutionPlan = flat === undefined
2117+
? { plan: (configuration, event) => planConfiguration(machine as any, configuration, event as any) }
2118+
: { plan: (configuration, event) => planFlatConfiguration(machine, flat, configuration, event) }
2119+
executionPlanCache.set(machine, compiled)
2120+
return compiled
2121+
}
2122+
19332123
const snapshotMacrostep = <
19342124
const States extends Machine.StateSchemas,
19352125
Event,

0 commit comments

Comments
 (0)