Compiler & Code Generation
The Behave-Graph Compiler is a build-time code generation tool that separates authoring-time metadata from runtime execution logic. This separation is fundamental to achieving optimal performance while maintaining excellent developer experience.
The Problem: Mixed Concerns
Section titled “The Problem: Mixed Concerns”Traditional node-based systems often mix authoring metadata with runtime logic:
// Traditional approach - everything mixed togetherexport const addNode = { // Authoring metadata (needed in editor) category: 'Math', description: 'Adds two numbers together', label: 'Add', color: '#4CAF50', icon: 'plus', documentation: 'https://docs.example.com/math/add', tags: ['arithmetic', 'basic', 'math'],
// Runtime metadata (needed for execution) type: 'math/add', inputs: ['a', 'b'], outputs: ['result'],
// Execution logic exec: (context) => { context.write('result', context.read('a') + context.read('b')); }};Problems with this approach:
- Bundle bloat: Runtime includes unnecessary authoring metadata
- Performance: Extra properties to parse and skip at runtime
- Coupling: UI concerns leak into execution engine
- Maintenance: Changes to UI require changing runtime code
The Solution: Separation via Compilation
Section titled “The Solution: Separation via Compilation”The compiler transforms developer-friendly TypeScript functions into runtime-optimized node definitions:
Source (Authoring Time)
Section titled “Source (Authoring Time)”/** * Adds two numbers together * @category Math * @tags arithmetic, basic */export function add(a: number, b: number, result: Output<number>): void { result.value = a + b;}Generated (Runtime)
Section titled “Generated (Runtime)”import { makeInNOutFunctionDesc } from '@kiberon-labs/behave-graph';import { add as add_impl } from './mathNodes.js';
export const add = makeInNOutFunctionDesc({ name: 'add', in: { a: 'number', b: 'number' }, out: { result: 'number' }, exec: add_impl});How It Works
Section titled “How It Works”1. TypeScript Analysis
Section titled “1. TypeScript Analysis”The compiler uses TypeScript’s compiler API to analyze source files:
// Compiler collects:// - Exported functions- Parameter types// - Output<T> parameters (identified as outputs)// - JSDoc annotations (for metadata)// - Type imports and references2. Type Inference
Section titled “2. Type Inference”Parameter types are automatically inferred:
function process( count: number, // → input: 'number' name: string, // → input: 'string' enabled: boolean, // → input: 'boolean' result: Output<string>, // → output: 'string' valid: Output<boolean> // → output: 'boolean'): void { // ...}The compiler:
- Detects
Output<T>wrapper type - Extracts
Tas the output value type - Treats non-Output parameters as inputs
- Validates type compatibility
3. Code Generation
Section titled “3. Code Generation”Generated code imports the original function and wraps it:
// Original function is importedimport { process as process_impl } from './source.js';
// Wrapped in node descriptorexport const process = makeInNOutFunctionDesc({ name: 'process', in: { count: 'number', name: 'string', enabled: 'boolean' }, out: { result: 'string', valid: 'boolean' }, exec: process_impl // Reference to original});Benefits
Section titled “Benefits”1. Developer Experience
Section titled “1. Developer Experience”Write nodes as plain functions:
// No boilerplate, just logicexport function lerp(a: number, b: number, t: number, result: Output<number>) { result.value = a + (b - a) * t;}2. Type Safety
Section titled “2. Type Safety”TypeScript’s type checker validates everything:
export function convert( value: number, output: Output<string> // Type-checked!): void { output.value = value.toString();}
// Compiler error if types don't match3. Separation of Concerns
Section titled “3. Separation of Concerns”Authoring Environment (Source)
Section titled “Authoring Environment (Source)”- JSDoc comments for documentation
- Clear parameter names
- Type annotations for validation
- Comments explaining logic
Runtime Environment (Generated)
Section titled “Runtime Environment (Generated)”- Minimal metadata (just what’s needed)
- Optimized for execution
- No UI-specific data
- Tree-shakeable
4. Multiple Targets
Section titled “4. Multiple Targets”The same source can generate different outputs:
// Web runtime (minimal)export const add = makeInNOutFunctionDesc({ name: 'add', in: { a: 'number', b: 'number' }, out: { result: 'number' }, exec: add_impl});
// Editor metadata (rich)export const addMetadata = { description: 'Adds two numbers', category: 'Math', tags: ['arithmetic'], examples: [...]};5. Performance
Section titled “5. Performance”Runtime bundles are smaller:
// Before: ~200 lines per node (with metadata)// After: ~10 lines per node (just descriptors)Integration with Flow UI
Section titled “Integration with Flow UI”The compiler works seamlessly with the Flow editor:
Build Process
Section titled “Build Process”# 1. Compile nodes during buildnpx behave-graph-compile src/nodes/*.ts --out dist/nodes.ts
# 2. Bundle with your app# Generated nodes are imported like any other moduleLoading Compiled Nodes
Section titled “Loading Compiled Nodes”import { System } from '@kiberon-labs/behave-graph-flow';import * as compiledNodes from './dist/nodes';
const system = new System();
// Register all compiled nodesObject.values(compiledNodes).forEach(nodeDesc => { system.registry.getState().registerNode(nodeDesc);});Metadata for UI
Section titled “Metadata for UI”Authoring metadata can be kept separate:
// nodes/metadata.ts (not compiled, used only in editor)export const nodeMetadata = { 'math/add': { description: 'Adds two numbers', category: 'Math', icon: 'plus', color: '#4CAF50' }, // ...};
// In editorimport { nodeMetadata } from './metadata';
system.registry.getState().setMetadata(nodeMetadata);Advanced Patterns
Section titled “Advanced Patterns”1. Class-Based Nodes
Section titled “1. Class-Based Nodes”export class Vector3Math { static exec( a: Vector3, b: Vector3, operation: string, result: Output<Vector3> ): void { switch (operation) { case 'add': result.value = { x: a.x + b.x, y: a.y + b.y, z: a.z + b.z }; break; case 'subtract': result.value = { x: a.x - b.x, y: a.y - b.y, z: a.z - b.z }; break; } }}
// Generates: { exec: Vector3Math.exec }2. Multi-Output Nodes
Section titled “2. Multi-Output Nodes”export function divmod( dividend: number, divisor: number, quotient: Output<number>, remainder: Output<number>): void { quotient.value = Math.floor(dividend / divisor); remainder.value = dividend % divisor;}
// Generates two outputs: quotient, remainder3. Custom Value Types
Section titled “3. Custom Value Types”type Color = { r: number; g: number; b: number; a: number };
export function blendColors( color1: Color, color2: Color, factor: number, result: Output<Color>): void { result.value = { r: color1.r + (color2.r - color1.r) * factor, g: color1.g + (color2.g - color1.g) * factor, b: color1.b + (color2.b - color1.b) * factor, a: color1.a + (color2.a - color1.a) * factor };}4. Conditional Outputs
Section titled “4. Conditional Outputs”export function parseNumber( input: string, value: Output<number>, isValid: Output<boolean>): void { const parsed = Number(input); value.value = parsed; isValid.value = !isNaN(parsed);}Configuration
Section titled “Configuration”Compiler Options
Section titled “Compiler Options”import { generateNodesFromFile } from '@kiberon-labs/behave-graph-compiler';
const { code, nodes } = generateNodesFromFile('src/nodes.ts', { // Module names to search for behave-graph imports behaveGraphModuleNames: ['@kiberon-labs/behave-graph'],
// Additional files (ambient types, etc.) extraRootFiles: ['src/types/global.d.ts'],
// TypeScript compiler options compilerOptions: { strict: true, target: 'ESNext', module: 'ESNext' },
// Output file path for relative imports outputFilePath: 'dist/generated-nodes.ts'});Build Integration
Section titled “Build Integration”package.json
Section titled “package.json”{ "scripts": { "build:nodes": "behave-graph-compile src/nodes/**/*.ts --out dist/nodes.ts", "build": "npm run build:nodes && vite build", "dev": "npm run build:nodes && vite" }}Watch Mode
Section titled “Watch Mode”# Watch for changes and recompilenpx behave-graph-compile src/nodes/*.ts --out dist/nodes.ts --watchComparison: Before vs After
Section titled “Comparison: Before vs After”Before Compilation
Section titled “Before Compilation”Developer writes:
export const complexNode = { type: 'custom/complex', category: 'Advanced', description: 'Complex operation', in: { input1: { type: 'number', default: 0 }, input2: { type: 'string', default: '' } }, out: { result: 'boolean' }, exec: (inputs) => { // Logic here }};Bundle includes: All metadata + logic (~50-100 lines)
After Compilation
Section titled “After Compilation”Developer writes:
export function complex( input1: number, input2: string, result: Output<boolean>): void { // Logic here}Compiler generates:
export const complex = makeInNOutFunctionDesc({ name: 'complex', in: { input1: 'number', input2: 'string' }, out: { result: 'boolean' }, exec: complex_impl});Bundle includes: Minimal descriptor + logic (~10 lines)
Best Practices
Section titled “Best Practices”1. Explicit Types
Section titled “1. Explicit Types”Always annotate parameter types:
// ✅ Goodexport function process(value: number, out: Output<string>): void { out.value = value.toString();}
// ❌ Bad - compiler can't inferexport function process(value, out) { out.value = value.toString();}2. Descriptive Names
Section titled “2. Descriptive Names”Use clear, self-documenting names:
// ✅ Goodexport function calculateDistance( x1: number, y1: number, x2: number, y2: number, distance: Output<number>): void { // ...}
// ❌ Badexport function calc(a: number, b: number, c: number, d: number, o: Output<number>): void { // ...}3. JSDoc for Metadata
Section titled “3. JSDoc for Metadata”Use JSDoc for authoring metadata:
/** * Interpolates between two values * @category Math * @tags interpolation, animation */export function lerp(a: number, b: number, t: number, result: Output<number>): void { result.value = a + (b - a) * t;}Troubleshooting
Section titled “Troubleshooting”Type Inference Issues
Section titled “Type Inference Issues”Problem: Compiler can’t infer output type
Solution: Ensure Output<T> is properly imported:
import type { Output } from '@kiberon-labs/behave-graph';
export function myNode(out: Output<number>): void { // ^^^^^^ This must be the exact Output type out.value = 42;}Module Resolution
Section titled “Module Resolution”Problem: Generated imports don’t resolve
Solution: Set outputFilePath option:
generateNodesFromFile('src/nodes.ts', { outputFilePath: 'dist/nodes.ts' // Compiler will calculate relative path});Next Steps
Section titled “Next Steps”- Compiler CLI Reference - Command-line options
- Plugin System - Loading compiled nodes as plugins
- Custom Value Types - Defining custom types