import { isVmFunction, isVmScript, wrapToVmValue, type VmScript, type VmValue } from '@mirascript/mirascript'; import { serialize } from '@mirascript/mirascript/subtle'; import { defaultOptions, type Logger, type Options } from './interface.js'; import { parse } from './parser.js'; import { analyze, type GlobalReferenceChain } from './analyze.js'; import { CompiledExpression, type Expression, type ExpressionFunction, type ExpressionOrValue, type ExpressionSource, ExpressionTag, isExpression, } from './expression.js'; import { evaluate } from './eval.js'; import { TypeInfo } from './type.js'; import { Scope } from './scope.js'; import { argumentConverter, choiceParameterType, fillArgumentMap, isChoiceValue, type ArgumentMap, type ArgumentValue, type Choice, type ChoiceArgumentValue, type ChoiceParameterType, type ConditionExpression, type Parameter, type ParameterGroup, type ParameterMap, } from './definitions.js'; const { isArray } = Array; /** 选项类型 */ export type EvaluatedChoices = { [K in T]: ParameterMap[K] extends { choices?: infer C } ? (C extends readonly unknown[] ? C : never) : never; }[T]; /** * 表达式求值 */ export class Evaluator { constructor(options?: Partial | null) { this.options = options ?? defaultOptions; } /** Logger */ get logger(): Logger { return this.options.logger ?? console; } /** 选项 */ readonly options: Readonly>; /** 默认执行环境 */ private readonly scope = new Scope(null, true, ''); private readonly __imported = new Map(); /** 执行环境 */ get imported(): ReadonlyMap { return this.__imported; } /** * 导入到执行环境 * * 使用 `undefined` 删除已导入的变量 */ import(values: Record): void { for (const [key, value] of Object.entries(values)) { if (value === undefined) { this.__imported.delete(key); } else { this.__imported.set(key, wrapToVmValue(value, null)); } } } /** 编译表达式 */ compile(expression: Expression | VmScript, throws = true): CompiledExpression { let value, tag; if (isVmScript(expression)) { value = parse(this, expression, throws, false); tag = '' as const; } else { value = parse(this, expression.source, throws, false); tag = expression[ExpressionTag]; } const type = tag ? TypeInfo.parse(tag) : ''; let compiled: ExpressionFunction; if (!type) { compiled = (scope, evaluator) => evaluate(evaluator, value, scope); } else { const to = TypeInfo.converter(type); compiled = (scope, evaluator) => { const ret = evaluate(evaluator, value, scope); if (ret == null) return null; return to(ret) as T; }; } return CompiledExpression(compiled, expression.source, type); } /** 执行 MiraScript 模板 */ template(template: string | null | undefined, scope?: Scope | null): string { if (!template) return ''; if (!template.includes('$')) return template; const throws = scope?.throws ?? true; const value = parse(this, template, throws, true); const result = evaluate(this, value, scope ?? this.scope); if (typeof result == 'string') return result; if (!throws) return ''; throw new TypeError(`Template evaluates to non-string: ${serialize(result)}`); } /** 求值 */ evaluate( expression: ExpressionOrValue | ExpressionFunction | null | undefined, scope?: Scope | null, ): T | null; /** 求值 */ evaluate>( expression: ExpressionOrValue | ExpressionFunction | null | undefined, scope: Scope | null | undefined, defaults: D, ): NonNullable | D; /** 求值 */ evaluate(expression: ExpressionFunction | T | null | undefined, scope?: Scope | null): T | null; /** 求值 */ evaluate>( expression: ExpressionFunction | T | null | undefined, scope: Scope | null | undefined, defaults: D, ): NonNullable | D; /** 求值 */ evaluate>( expression: ExpressionOrValue | ExpressionFunction | null | undefined, scope: Scope | null | undefined, defaults: D | null = null, ): T | D | null { if (expression == null) return defaults; if (typeof expression == 'function' && !isVmFunction(expression)) { try { const ret = expression(scope ?? this.scope, this); if (ret == null) return defaults; return ret; } catch (ex) { if (scope?.throws ?? true) { throw ex; } return defaults; } } if (!isExpression(expression)) { return expression; } const result = this.evaluateSource(expression.source, scope); if (result == null) return defaults; const tag = expression[ExpressionTag]; if (!tag) return result; return TypeInfo.to(result, tag) as T; } /** 求值 */ evaluateSource( expression: ExpressionSource | T | null | undefined, scope: Scope | null | undefined, ): T | null; /** 求值 */ evaluateSource>( expression: ExpressionSource | T | null | undefined, scope: Scope | null | undefined, defaults: D, ): NonNullable | D; /** 求值 */ evaluateSource>( expression: ExpressionSource | T | null | undefined, scope: Scope | null | undefined, defaults: D | null = null, ): NonNullable | D | null { if (expression == null) return defaults; if (typeof expression != 'string') return expression; scope ??= this.scope; const { throws } = scope; const value = parse(this, expression, throws, false); const result = evaluate(this, value, scope); if (result == null) return defaults; return result; } /** 求条件 */ evaluateCondition( condition: ConditionExpression | null | undefined, scope: Scope | null | undefined, defaults = true, ): boolean { if (condition == null || condition === '') return defaults; if (typeof condition == 'boolean') return condition; if (condition === 'true') return true; if (condition === 'false') return false; try { const result = this.evaluateSource(condition, scope, defaults); if (typeof result == 'boolean') return result; throw new Error(`Condition evaluates to \`${serialize(result)}\``); } catch (ex) { if (scope?.throws) { throw ex; } this.logger.warn(`Invalid condition`, ex); return defaults; } } /** 求选项列表 */ evaluateChoices( definition: Pick, scope: Scope | null | undefined, ): EvaluatedChoices { const ret: Choice[] = []; const { type, choices } = definition; const isLogical = type === 'logical'; const choiceType = choiceParameterType(definition); const fallbackKeys = !isLogical ? [] : choiceType === 'boolean' || !choiceType ? [false, true] : choiceType === 'number' ? [0, 1] : ['0', '1']; if (choices != null) { const evaluatedChoices = this.evaluate(choices, scope); if (evaluatedChoices == null) { // do nothing } else if (!isArray(evaluatedChoices as unknown[])) { if (scope?.throws ?? true) { throw new Error(`Choices is not an array`); } } else if (evaluatedChoices.length > 0) { const to = TypeInfo.converter(choiceType); let len = evaluatedChoices.length; if (isLogical && len > 2) len = 2; for (let i = 0; i < len; i++) { try { const item = evaluatedChoices[i] as Choice | ChoiceArgumentValue | null | undefined; let key, name, description, condition; if (item != null && typeof item == 'object') { key = item.key; name = item.name; description = item.description; condition = item.condition; } else if (isChoiceValue(item)) { key = item; name = undefined; description = undefined; condition = undefined; } key ??= fallbackKeys[i] ?? i; ret[i] = { key: to(key) as ChoiceArgumentValue, name: name ?? (isLogical ? '' : String(key)), description: description ?? '', condition: condition == null || condition === '' ? undefined : condition, }; } catch (ex) { if (scope?.throws ?? true) { throw new Error(`Invalid choice value: ${serialize(evaluatedChoices[i])}`, { cause: ex, }); } ret[i] = { key: to(fallbackKeys[i] ?? i, i) as ChoiceArgumentValue, name: '', description: '', condition: undefined, }; } } } } if (isLogical) { while (ret.length < 2) { ret.push({ key: fallbackKeys[ret.length]!, name: '', description: '', condition: undefined, }); } } return ret as EvaluatedChoices; } /** 求参数值 */ evaluateArg( expression: ExpressionOrValue | null | undefined, scope: Scope | null | undefined, definition: Parameter, ): V { // 默认值有可能为表达式,因此需要先填充再求值 expression ??= definition.value as ExpressionOrValue; // 求值失败时返回默认值 const result = this.evaluate(expression, scope, definition.value as V); const converter = argumentConverter(definition); // 转换失败时返回转换前的值 return converter(result, result) as V; } /** 求参数表的所有参数值 */ evaluateArgs>( args: ArgumentMap | null | undefined, scope: Scope | null | undefined, definition: ReadonlyArray>, ): T { const a = fillArgumentMap(definition, args); const ret: Record = {}; for (const group of definition) { for (const param of group.items) { const { key } = param; if (!key) continue; const arg = a[key]; ret[key] = this.evaluateArg(arg, scope, param); } } return ret as T; } /** 解析表达式 */ validate(expression: Expression): void { if (!isExpression(expression) || !expression.source) { throw new Error(`${expression.source || String(expression)} is not a valid expression`); } const tag = expression[ExpressionTag]; if (tag) TypeInfo.parse(tag); parse(this, expression.source, true, false); } /** 分析表达式 */ analyze(expression: Expression, scope?: Scope | null): readonly GlobalReferenceChain[] { if (!isExpression(expression) || !expression.source) { throw new Error(`${expression.source || String(expression)} is not a valid expression`); } scope ??= this.scope; const exp = parse(this, expression.source, scope.throws, false); // 如果 throws=true,则 parse 会抛出错误,此处有 error 一定是 throws=false 的情况 if (exp.error) return []; return analyze(exp, this, scope); } }