Codegen diagrams
Yantrix Runtime Event Flow
Figure 1: This diagram shows the runtime event flow in Yantrix: external events are dispatched to the EventBus, processed by the CoreLoop through the EventAdapter into actions, reduced by the generated automaton and its RootReducer, and finally emitted to destinations such as UI or external I/O
Yantrix Code Generation Pipeline
Figure 2: This diagram illustrates the code generation pipeline in Yantrix: the CLI reads a Mermaid state diagram and Yantrix notes, parses them with mermaid-parser and YantrixParser, runs the codegen module to build dictionaries, reducers and the automaton class, and finally writes the generated automaton code to a file
Reducer compilation details
This section explains in detail how reducer functions for each state are compiled from Yantrix notes into JavaScript/TypeScript code. All the logic described here corresponds to the implementation in packages/codegen/src/core/modules/JavaScript/JavaScriptCompiler/context/serializer.ts.
1. Overall goal
The contextSerializer is responsible for generating two main pieces of runtime code:
const reducer = { [stateId]: (prevContext, payload, functionDictionary, automata) => newContext }const getDefaultContext = (prevContext, payload) => { ... }
These are later embedded into the generated automata class and are used at runtime to compute the next context for each state transition.
The key exported helpers are:
getStateReducerCode– builds thereducerobject.getStateToContext– generates per-state reducer functions.getContextTransition– computes the context expression for a given state.getContextItem– converts a singlecontextDescriptionblock intokey: expressionpairs.mapReducerItems– converts reducer rows into right-hand-side expressions.getBoundValues– binds intermediate values to final context properties, with fallbacks.getDefaultContext– generates the default context constructor based onStartState.
2. From getStateReducerCode to getContextTransition
At the top level, getStateReducerCode produces the reducer definition:
ts
function getStateReducerCode(props) {
return `const reducer = {
${getStateToContext(props).join(',\n\t')}
}`;
}getStateToContext walks all states in the diagram and creates one reducer function per state:
ts
function getStateToContext(props) {
return props.diagram.states.map((state) => {
const stateValue = props.stateDictionary.getStateValues({ keys: [state.id] })[0];
if (!stateValue) {
throw new Error('Invalid state');
}
return `${stateValue}: (prevContext, payload, functionDictionary, automata) => {
return ${getContextTransition({
value: stateValue,
stateDictionary: props.stateDictionary,
diagram: props.diagram,
expressions: props.expressions,
})}
}`;
});
}For each logical state:
- it looks up the numeric
stateValuefromBasicStateDictionarybystate.id; - it then delegates to
getContextTransition(...), which returns a string expression representing the new context for that state:- either
"prevContext"(identity), - or an object literal string like
{ foo: ..., bar: ... }.
- either
The result is a reducer object of the form:
ts
const reducer = {
1: (prevContext, payload, functionDictionary, automata) => {
return { /* compiled context for state 1 */ };
},
2: (prevContext, payload, functionDictionary, automata) => {
return prevContext;
},
// ...
};getContextTransition is the entry point for computing the per-state context expression:
ts
function getContextTransition(props) {
const stateFromDict = props.stateDictionary.getStateKeys({ states: [props.value] })[0];
if (stateFromDict === null) {
throw new Error(`Invalid state - ${props.value}`);
}
const diagramState = props.diagram.states.find((diagramState) => {
return diagramState.id === stateFromDict;
});
if (!diagramState) {
throw new Error(`Invalid state - ${props.value}`);
}
const ctxRes: string[] = [];
diagramState.notes?.contextDescription.forEach((ctx) => {
const newContext = getContextItem({
ctx,
expressions: props.expressions,
});
ctxRes.push(...newContext);
});
if (ctxRes.length === 0) return 'prevContext';
return `{${ctxRes.join(',\n\t')}}`;
};Steps:
- Reverse-lookup
stateFromDictfrom the numericvalueusingstateDictionary.getStateKeys. - Find the corresponding
diagramStateindiagram.states. - For each
ctxindiagramState.notes?.contextDescription, callgetContextItemto obtain a list of"key: expression"strings and accumulate them inctxRes. - If there is no context description (
ctxRes.length === 0), return'prevContext'so the reducer becomes an identity function. - Otherwise, wrap all context entries into an object literal:
`{${ctxRes.join(',\n\t')}}`.
The final reducer function for a state then effectively looks like:
ts
stateValue: (prevContext, payload, functionDictionary, automata) => {
return {
foo: (function(){ ... }()),
bar: (function(){ ... }()),
};
}3. getContextItem: with and without reducer blocks
getContextItem is responsible for transforming a single TContextItem (one block from notes.contextDescription) into an array of "key: expression" strings:
ts
function getContextItem(props: { ctx: TContextItem; expressions: TExpressionRecord; }) {
if (isContextWithReducer(props.ctx)) {
const { context, reducer } = props.ctx;
return getBoundValues({
expressions: props.expressions,
arr: mapReducerItems({ reducer, expressions: props.expressions }),
context,
});
} else {
const { context } = props.ctx;
return context.map(({ keyItem }) => {
const { identifier } = keyItem;
if (isKeyItemWithExpression(keyItem)) {
const expressionValue = expressions.functions.getExpressionValue({
expression: keyItem.expression,
expressionRecord: props.expressions,
});
return `${identifier}: ${expressions.serializer.getDefaultPropertyContext('prevContext', identifier, expressionValue)}`;
} else {
return `${identifier}: ${expressions.serializer.getDefaultPropertyContext('prevContext', identifier)}`;
}
});
}
};There are two major branches:
isContextWithReducer(ctx)is true:This means the context block declares an explicit
reducersection in Yantrix notes.The flow is:
mapReducerItems({ reducer, expressions })– converts each reducer row into a right-hand-side expression string.getBoundValues({ arr, context, expressions })– zips these expressions with thecontextdefinitions to produce final"targetProperty: expression"pairs.
The output is an array of strings like:
ts[ "foo: (function(){ const boundValue = ...; return boundValue; }())", "bar: (function(){ const boundValue = ...; if(boundValue !== null) return boundValue; else return <fallback>; }())", ]
No reducer in
ctx:- This is the simpler form, where each
contextentry only defines a target identifier (and maybe an expression for default value). - For each
keyItemincontext:If it has an expression:
tsconst expressionValue = expressions.functions.getExpressionValue({ expression: keyItem.expression, expressionRecord: props.expressions, }); return `${identifier}: ${expressions.serializer.getDefaultPropertyContext('prevContext', identifier, expressionValue)}`;Here:
getExpressionValueturns the Yantrix expression into a JS snippet.getDefaultPropertyContext('prevContext', identifier, expressionValue)generates code that prefersprevContext[identifier], but falls back toexpressionValue.
If there is no expression:
tsreturn `${identifier}: ${expressions.serializer.getDefaultPropertyContext('prevContext', identifier)}`;In this case the value is fully taken from
prevContext[identifier](or some default insidegetDefaultPropertyContext).
- This is the simpler form, where each
In summary, getContextItem returns an array of "key: expression" strings, either driven by an explicit reducer or by simple context defaults.
4. mapReducerItems: compiling reducer rows
mapReducerItems takes a reducer: TKeyItems<'reducer'> and produces an array of raw expressions (arr: string[]), which are later bound to target context properties by getBoundValues:
ts
function mapReducerItems(props: {
reducer: TKeyItems<'reducer'>;
sourcePath?: string;
expressions: TExpressionRecord;
}) {
return props.reducer
.map(({ keyItem }) => {
if (isKeyItemReference(keyItem)) {
const { expressionType, identifier: boundIdentifier } = keyItem;
const path = props.sourcePath ?? pathRecord[expressionType];
if (keyItem.expressionType === ExpressionTypes.Constant) {
const expressionValueRight = expressions.functions.getExpressionValue({
expression: keyItem,
expressionRecord: props.expressions,
});
return `(function(){
return ${expressionValueRight}
}())`;
}
if (isKeyItemWithExpression(keyItem)) {
const { expression } = keyItem;
const expressionValueRight = expressions.functions.getExpressionValue({
expression,
expressionRecord: props.expressions,
});
return expressions.serializer.getDefaultPropertyContext(path, boundIdentifier, expressionValueRight);
}
return expressions.serializer.getDefaultPropertyContext(path, boundIdentifier);
} else {
const { expression } = keyItem;
const expressionValueRight = expressions.functions.getExpressionValue({
expression,
expressionRecord: props.expressions,
});
return `(function(){
return ${expressionValueRight}
}())`;
}
});
}Key branches:
isKeyItemReference(keyItem)is true:- Indicates that the row refers to some source path (context, payload, constants, etc.) and binds it to an identifier.
expressionType(fromExpressionTypes) determines which path to use viapathRecord[expressionType].
expressionType === Constant:tsconst expressionValueRight = expressions.functions.getExpressionValue({ expression: keyItem, expressionRecord: props.expressions, }); return `(function(){ return ${expressionValueRight} }())`;getExpressionValueresolves the constant reference (e.g.%%FOO) into a JS snippet, such asCONSTANTS.FOO.- The result is wrapped in an IIFE so it becomes an evaluated value at runtime.
isKeyItemWithExpression(keyItem)is true:tsconst expressionValueRight = expressions.functions.getExpressionValue({ expression, expressionRecord: props.expressions, }); return expressions.serializer.getDefaultPropertyContext(path, boundIdentifier, expressionValueRight);- Builds an expression that first tries
path[boundIdentifier], then falls back to the explicitexpressionValueRight.
- Builds an expression that first tries
No expression on the keyItem:
tsreturn expressions.serializer.getDefaultPropertyContext(path, boundIdentifier);- Just uses
pathandboundIdentifier(e.g.context.foo,payload.bar).
- Just uses
isKeyItemReference(keyItem)is false:- The row is a plain expression not tied to a specific source path.
tsconst expressionValueRight = expressions.functions.getExpressionValue({ expression, expressionRecord: props.expressions, }); return `(function(){ return ${expressionValueRight} }())`;
The result of mapReducerItems is arr: string[], where each element is a right-hand-side expression that computes some intermediate value. These expressions are not yet associated with final context properties; that is handled next.
5. getBoundValues: zipping values to context properties
getBoundValues takes:
arr– the array of expressions frommapReducerItems.context– the target context description (withkeyItem.identifierfor each property).
It produces final "targetProperty: expression" strings:
ts
function getBoundValues(props: {
expressions: TExpressionRecord;
arr: string[];
context: any;
}) {
return props.arr
.map((el, index) => {
const item = props.context[index];
if (!item) {
throw new Error('Unexpected index bound property');
}
const { keyItem } = item;
const { identifier: targetProperty } = keyItem;
if (isKeyItemWithExpression(keyItem)) {
const { expression } = keyItem;
const expressionValueRight = expressions.functions.getExpressionValue({
expression,
expressionRecord: props.expressions,
});
return `${targetProperty}: (function(){
const boundValue = ${el}
if(boundValue !== null){
return boundValue
}
else {
return ${expressionValueRight}
}
}())`;
} else {
return `${targetProperty}: (function(){
const boundValue = ${el}
return boundValue
}())`;
}
});
}Logic:
It iterates over
arrwith an index and finds the correspondingcontext[index]entry.- If there is no matching context item, it throws an error (defensive check).
Extracts the
targetPropertyname fromkeyItem.identifier.If
keyItemhas its own expression:It builds a fallback value with
getExpressionValue.It generates a final expression:
tstargetProperty: (function(){ const boundValue = <arr[index]>; if(boundValue !== null){ return boundValue } else { return <expressionValueRight> } }())This means: prefer the intermediate
boundValue(frommapReducerItems), but if it isnull, fall back to the local expression defined on the context key.
If
keyItemhas no expression:The final expression simply returns the intermediate
boundValue:tstargetProperty: (function(){ const boundValue = <arr[index]>; return boundValue; }())
This step is where the intermediate expressions produced by mapReducerItems are bound to their final context properties, using the positional pairing between reducer rows and context items.
6. getDefaultContext: initial context from StartState
The getDefaultContext function is generated based on the context description for StartState:
ts
function getDefaultContext(props) {
const state = props.stateDictionary.getStateValues({ keys: [StartState] })[0];
if (state) {
const ctx = getContextTransition({
diagram: props.diagram,
expressions: props.expressions,
stateDictionary: props.stateDictionary,
value: state,
});
return `const getDefaultContext = (prevContext, payload) => {
const ctx = ${ctx}
return Object.assign({}, prevContext, ctx);
}
`;
}
return `const getDefaultContext = (prevContext, payload) => {
return prevContext
}`;
}It resolves the numeric state id for
StartStatefromBasicStateDictionary.If present, it reuses
getContextTransitionto compute the context expression for the start state.It then generates:
tsconst getDefaultContext = (prevContext, payload) => { const ctx = <ctxExprForStartState>; return Object.assign({}, prevContext, ctx); };If no start state is found,
getDefaultContextis a simple identity function that returnsprevContextunchanged.
7. Role of TExpressionRecord and getExpressionValue
TExpressionRecord (provided by ../expressions) is a registry that knows how to:
- interpret different kinds of Yantrix expressions (context references, payload references, constants, function calls, etc.),
- serialize these expressions into JavaScript code snippets,
- provide helpers such as
getDefaultPropertyContext.
The most important API from this record, used in serializer.ts, is:
ts
expressions.functions.getExpressionValue({
expression,
expressionRecord: props.expressions,
});This function:
- takes a high-level Yantrix
expressionnode, - uses
expressionRecordto dispatch to the correct handler, - returns a string representing the JavaScript code that must be inserted into the generated output.
expressions.serializer.getDefaultPropertyContext(...) complements this by generating higher-level patterns that combine access to a source object with optional fallback expressions.
Together, getExpressionValue and the serializer helpers allow the reducer compiler to stay declarative: it does not hardcode the shape of every expression; instead it relies on TExpressionRecord to generate the exact JavaScript code for each case.
This is the complete flow of how textual Yantrix reducer declarations in diagram notes become executable JavaScript/TypeScript reducer functions in the generated automata class.