meld
Version:
Meld: A template language for LLM prompts
490 lines (430 loc) • 16.5 kB
text/typescript
import type { MeldNode, SourceLocation, DirectiveNode } from 'meld-spec';
import { interpreterLogger as logger } from '@core/utils/logger.js';
import { IInterpreterService, type InterpreterOptions } from './IInterpreterService.js';
import type { IDirectiveService } from '@services/pipeline/DirectiveService/IDirectiveService.js';
import type { IStateService } from '@services/state/StateService/IStateService.js';
import { MeldInterpreterError, type InterpreterLocation } from '@core/errors/MeldInterpreterError.js';
import { MeldError, ErrorSeverity } from '@core/errors/MeldError.js';
import { StateVariableCopier } from '@services/state/utilities/StateVariableCopier.js';
const DEFAULT_OPTIONS: Required<Omit<InterpreterOptions, 'initialState' | 'errorHandler'>> = {
filePath: '',
mergeState: true,
importFilter: [],
strict: true
};
function convertLocation(loc?: SourceLocation): InterpreterLocation | undefined {
if (!loc) return undefined;
return {
line: loc.start.line,
column: loc.start.column,
};
}
function getErrorMessage(error: unknown): string {
if (error instanceof Error) return error.message;
if (typeof error === 'string') return error;
return 'Unknown error';
}
export class InterpreterService implements IInterpreterService {
private directiveService?: IDirectiveService;
private stateService?: IStateService;
private initialized = false;
private stateVariableCopier = new StateVariableCopier();
public canHandleTransformations(): boolean {
return true;
}
initialize(
directiveService: IDirectiveService,
stateService: IStateService
): void {
this.directiveService = directiveService;
this.stateService = stateService;
this.initialized = true;
logger.debug('InterpreterService initialized');
}
/**
* Handle errors based on severity and options
* In strict mode, all errors throw
* In permissive mode, recoverable errors become warnings
*/
private handleError(error: Error, options: Required<Omit<InterpreterOptions, 'initialState' | 'errorHandler'>> & Pick<InterpreterOptions, 'errorHandler'>): void {
// If it's not a MeldError, wrap it
const meldError = error instanceof MeldError
? error
: MeldError.wrap(error);
// In strict mode, or if it's a fatal error, throw it
if (options.strict || !meldError.canBeWarning()) {
throw meldError;
}
// In permissive mode with recoverable errors, use the error handler or log a warning
if (options.errorHandler) {
options.errorHandler(meldError);
} else {
logger.warn(`Warning: ${meldError.message}`, {
code: meldError.code,
filePath: meldError.filePath,
severity: meldError.severity
});
}
}
async interpret(
nodes: MeldNode[],
options?: InterpreterOptions
): Promise<IStateService> {
this.ensureInitialized();
if (!nodes) {
throw new MeldInterpreterError(
'No nodes provided for interpretation',
'interpretation',
undefined,
{ severity: ErrorSeverity.Fatal }
);
}
if (!Array.isArray(nodes)) {
throw new MeldInterpreterError(
'Invalid nodes provided for interpretation: expected array',
'interpretation',
undefined,
{ severity: ErrorSeverity.Fatal }
);
}
const opts = { ...DEFAULT_OPTIONS, ...options };
let currentState: IStateService;
try {
// Initialize state
if (opts.initialState) {
if (opts.mergeState) {
// When mergeState is true, create child state from initial state
currentState = opts.initialState.createChildState();
} else {
// When mergeState is false, create completely isolated state
currentState = this.stateService!.createChildState();
}
} else {
// No initial state, create fresh state
currentState = this.stateService!.createChildState();
}
if (!currentState) {
throw new MeldInterpreterError(
'Failed to initialize state for interpretation',
'initialization',
undefined,
{ severity: ErrorSeverity.Fatal }
);
}
if (opts.filePath) {
currentState.setCurrentFilePath(opts.filePath);
}
// Take a snapshot of initial state for rollback
const initialSnapshot = currentState.clone();
let lastGoodState = initialSnapshot;
logger.debug('Starting interpretation', {
nodeCount: nodes?.length ?? 0,
filePath: opts.filePath,
mergeState: opts.mergeState
});
for (const node of nodes) {
try {
currentState = await this.interpretNode(node, currentState, opts);
// Update last good state after successful interpretation
lastGoodState = currentState.clone();
} catch (error) {
// Handle errors based on severity and options
try {
this.handleError(error instanceof Error ? error : new Error(String(error)), opts);
// If we get here, the error was handled as a warning
// Continue with the last good state
currentState = lastGoodState.clone();
} catch (fatalError) {
// If we get here, the error was fatal and should be propagated
// Restore to initial state before rethrowing
if (opts.initialState && opts.mergeState) {
// Only attempt to merge back if we have a parent and mergeState is true
opts.initialState.mergeChildState(initialSnapshot);
}
throw fatalError;
}
}
}
// Merge state back to parent if requested
if (opts.initialState && opts.mergeState) {
opts.initialState.mergeChildState(currentState);
}
logger.debug('Interpretation completed successfully', {
nodeCount: nodes?.length ?? 0,
filePath: currentState.getCurrentFilePath(),
finalStateNodes: currentState.getNodes()?.length ?? 0,
mergedToParent: opts.mergeState && opts.initialState
});
return currentState;
} catch (error) {
// Wrap any unexpected errors
const wrappedError = error instanceof Error
? error
: new MeldInterpreterError(
`Unexpected error during interpretation: ${String(error)}`,
'interpretation',
undefined,
{ severity: ErrorSeverity.Fatal, cause: error instanceof Error ? error : undefined }
);
throw wrappedError;
}
}
async interpretNode(
node: MeldNode,
state: IStateService,
options?: InterpreterOptions
): Promise<IStateService> {
if (!node) {
throw new MeldInterpreterError(
'No node provided for interpretation',
'interpretation'
);
}
if (!state) {
throw new MeldInterpreterError(
'No state provided for node interpretation',
'interpretation'
);
}
if (!node.type) {
throw new MeldInterpreterError(
'Unknown node type',
'interpretation',
convertLocation(node.location)
);
}
logger.debug('Interpreting node', {
type: node.type,
location: node.location,
filePath: state.getCurrentFilePath()
});
const opts = { ...DEFAULT_OPTIONS, ...options };
try {
// Take a snapshot before processing
const preNodeState = state.clone();
let currentState = preNodeState;
// Process based on node type
switch (node.type) {
case 'Text':
// Create new state for text node
const textState = currentState.clone();
textState.addNode(node);
currentState = textState;
break;
case 'CodeFence':
// Handle CodeFence nodes similar to Text nodes - preserve them exactly
const codeFenceState = currentState.clone();
codeFenceState.addNode(node);
currentState = codeFenceState;
break;
case 'TextVar':
// Handle TextVar nodes similar to Text nodes
const textVarState = currentState.clone();
textVarState.addNode(node);
currentState = textVarState;
break;
case 'DataVar':
// Handle DataVar nodes similar to Text/TextVar nodes
const dataVarState = currentState.clone();
dataVarState.addNode(node);
currentState = dataVarState;
break;
case 'Comment':
// Comments are ignored during interpretation
break;
case 'Directive':
if (!this.directiveService) {
throw new MeldInterpreterError(
'Directive service not initialized',
'directive_service'
);
}
// Process directive with cloned state to maintain immutability
const directiveState = currentState.clone();
// Add the node first to maintain order
directiveState.addNode(node);
if (node.type !== 'Directive' || !('directive' in node) || !node.directive) {
throw new MeldInterpreterError(
'Invalid directive node',
'invalid_directive',
convertLocation(node.location)
);
}
const directiveNode = node as DirectiveNode;
// Capture the original state for importing directives in transformation mode
const originalState = state;
const isImportDirective = directiveNode.directive.kind === 'import';
// Store the directive result to check for replacement nodes
const directiveResult = await this.directiveService.processDirective(directiveNode, {
state: directiveState,
parentState: currentState,
currentFilePath: state.getCurrentFilePath() ?? undefined
});
// Update current state with the result
currentState = directiveResult;
// Check if the directive handler returned a replacement node
// This happens when the handler implements the DirectiveResult interface
// with a replacement property
if (directiveResult && 'replacement' in directiveResult && 'state' in directiveResult) {
// We need to extract the replacement node and state from the result
const result = directiveResult as unknown as {
replacement: MeldNode;
state: IStateService;
};
const replacement = result.replacement;
const resultState = result.state;
// Update current state with the result state
currentState = resultState;
// Special handling for imports in transformation mode:
// Copy all variables from the imported file to the original state
if (isImportDirective &&
currentState.isTransformationEnabled &&
currentState.isTransformationEnabled()) {
try {
logger.debug('Import directive in transformation mode, copying variables to original state');
// Use the state variable copier utility to copy all variables
this.stateVariableCopier.copyAllVariables(currentState, originalState, {
skipExisting: false,
trackContextBoundary: false, // No tracking service in the interpreter
trackVariableCrossing: false
});
} catch (e) {
logger.debug('Error copying variables from import to original state', { error: e });
}
}
// If transformation is enabled and we have a replacement node,
// we need to apply it to the transformed nodes
if (currentState.isTransformationEnabled && currentState.isTransformationEnabled()) {
logger.debug('Applying replacement node from directive handler', {
originalType: node.type,
replacementType: replacement.type,
directiveKind: directiveNode.directive.kind
});
// Apply the transformation by replacing the directive node with the replacement
try {
// Ensure we have the transformed nodes array initialized
if (!currentState.getTransformedNodes || !currentState.getTransformedNodes()) {
// Initialize transformed nodes if needed
const originalNodes = currentState.getNodes();
if (originalNodes && currentState.setTransformedNodes) {
currentState.setTransformedNodes([...originalNodes]);
logger.debug('Initialized transformed nodes array', {
nodesCount: originalNodes.length
});
}
}
// Apply the transformation
currentState.transformNode(node, replacement as MeldNode);
} catch (transformError) {
logger.error('Error applying transformation', {
error: transformError,
directiveKind: directiveNode.directive.kind
});
// Continue execution despite transformation error
}
}
}
break;
default:
throw new MeldInterpreterError(
`Unknown node type: ${node.type}`,
'unknown_node',
convertLocation(node.location)
);
}
return currentState;
} catch (error) {
// Preserve MeldInterpreterError or wrap other errors
if (error instanceof MeldInterpreterError) {
throw error;
}
throw new MeldInterpreterError(
getErrorMessage(error),
node.type,
convertLocation(node.location),
{
cause: error instanceof Error ? error : undefined,
context: {
nodeType: node.type,
location: convertLocation(node.location),
state: {
filePath: state.getCurrentFilePath() ?? undefined
}
}
}
);
}
}
async createChildContext(
parentState: IStateService,
filePath?: string,
options?: InterpreterOptions
): Promise<IStateService> {
this.ensureInitialized();
if (!parentState) {
throw new MeldInterpreterError(
'No parent state provided for child context creation',
'context_creation'
);
}
try {
// Create child state from parent
const childState = parentState.createChildState();
if (!childState) {
throw new MeldInterpreterError(
'Failed to create child state',
'context_creation',
undefined,
{
context: {
parentFilePath: parentState.getCurrentFilePath() ?? undefined
}
}
);
}
// Set file path if provided
if (filePath) {
childState.setCurrentFilePath(filePath);
}
logger.debug('Created child context', {
parentFilePath: parentState.getCurrentFilePath(),
childFilePath: filePath,
hasParent: true
});
return childState;
} catch (error) {
logger.error('Failed to create child context', {
parentFilePath: parentState.getCurrentFilePath(),
childFilePath: filePath,
error
});
// Preserve MeldInterpreterError or wrap other errors
if (error instanceof MeldInterpreterError) {
throw error;
}
throw new MeldInterpreterError(
getErrorMessage(error),
'context_creation',
undefined,
{
cause: error instanceof Error ? error : undefined,
context: {
parentFilePath: parentState.getCurrentFilePath() ?? undefined,
childFilePath: filePath,
state: {
filePath: parentState.getCurrentFilePath() ?? undefined
}
}
}
);
}
}
private ensureInitialized(): void {
if (!this.initialized || !this.directiveService || !this.stateService) {
throw new MeldInterpreterError(
'InterpreterService must be initialized before use',
'initialization'
);
}
}
}