Fix compiler bug with nested optional arguments
This commit fixes compiler bug where it fails when optional values are compiled into absent values in nested calls. - Throw exception with more context for easier future debugging. - Add better validation of argument values for nested calls. - Refactor `FunctionCallCompiler` for better clarity and modularize it to make it more maintainable and testable. - Refactor related interface to not have `I` prefix, and function/variable names for better clarity. Context: Discovered this issue while attempting to call `RunInlineCodeAsTrustedInstaller` which in turn invokes `RunPowerShell` for issue #246. This led to the realization that despite parameters flagged as optional, the nested argument compilation didn't support them.
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
import { CompiledCode } from '../CompiledCode';
|
||||
|
||||
export interface CodeSegmentMerger {
|
||||
mergeCodeParts(codeSegments: readonly CompiledCode[]): CompiledCode;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { CompiledCode } from '../CompiledCode';
|
||||
import { CodeSegmentMerger } from './CodeSegmentMerger';
|
||||
|
||||
export class NewlineCodeSegmentMerger implements CodeSegmentMerger {
|
||||
public mergeCodeParts(codeSegments: readonly CompiledCode[]): CompiledCode {
|
||||
if (!codeSegments?.length) {
|
||||
throw new Error('missing segments');
|
||||
}
|
||||
return {
|
||||
code: joinCodeParts(codeSegments.map((f) => f.code)),
|
||||
revertCode: joinCodeParts(codeSegments.map((f) => f.revertCode)),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function joinCodeParts(codeSegments: readonly string[]): string {
|
||||
return codeSegments
|
||||
.filter((segment) => segment?.length > 0)
|
||||
.join('\n');
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
export interface ICompiledCode {
|
||||
export interface CompiledCode {
|
||||
readonly code: string;
|
||||
readonly revertCode?: string;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
|
||||
import { FunctionCall } from '../FunctionCall';
|
||||
import type { SingleCallCompiler } from './SingleCall/SingleCallCompiler';
|
||||
|
||||
export interface FunctionCallCompilationContext {
|
||||
readonly allFunctions: ISharedFunctionCollection;
|
||||
readonly rootCallSequence: readonly FunctionCall[];
|
||||
readonly singleCallCompiler: SingleCallCompiler;
|
||||
}
|
||||
@@ -1,149 +1,10 @@
|
||||
import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection';
|
||||
import { IFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/IFunctionCall';
|
||||
import { FunctionCallArgument } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument';
|
||||
import { ISharedFunctionCollection } from '../../ISharedFunctionCollection';
|
||||
import { IExpressionsCompiler } from '../../../Expressions/IExpressionsCompiler';
|
||||
import { ExpressionsCompiler } from '../../../Expressions/ExpressionsCompiler';
|
||||
import { ISharedFunction, IFunctionCode } from '../../ISharedFunction';
|
||||
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
|
||||
import { FunctionCall } from '../FunctionCall';
|
||||
import { FunctionCallArgumentCollection } from '../Argument/FunctionCallArgumentCollection';
|
||||
import { IFunctionCallCompiler } from './IFunctionCallCompiler';
|
||||
import { ICompiledCode } from './ICompiledCode';
|
||||
import { CompiledCode } from './CompiledCode';
|
||||
|
||||
export class FunctionCallCompiler implements IFunctionCallCompiler {
|
||||
public static readonly instance: IFunctionCallCompiler = new FunctionCallCompiler();
|
||||
|
||||
protected constructor(
|
||||
private readonly expressionsCompiler: IExpressionsCompiler = new ExpressionsCompiler(),
|
||||
) {
|
||||
}
|
||||
|
||||
public compileCall(
|
||||
calls: IFunctionCall[],
|
||||
export interface FunctionCallCompiler {
|
||||
compileFunctionCalls(
|
||||
calls: readonly FunctionCall[],
|
||||
functions: ISharedFunctionCollection,
|
||||
): ICompiledCode {
|
||||
if (!functions) { throw new Error('missing functions'); }
|
||||
if (!calls) { throw new Error('missing calls'); }
|
||||
if (calls.some((f) => !f)) { throw new Error('missing function call'); }
|
||||
const context: ICompilationContext = {
|
||||
allFunctions: functions,
|
||||
callSequence: calls,
|
||||
expressionsCompiler: this.expressionsCompiler,
|
||||
};
|
||||
const code = compileCallSequence(context);
|
||||
return code;
|
||||
}
|
||||
}
|
||||
|
||||
interface ICompilationContext {
|
||||
allFunctions: ISharedFunctionCollection;
|
||||
callSequence: readonly IFunctionCall[];
|
||||
expressionsCompiler: IExpressionsCompiler;
|
||||
}
|
||||
|
||||
interface ICompiledFunctionCall {
|
||||
readonly code: string;
|
||||
readonly revertCode: string;
|
||||
}
|
||||
|
||||
function compileCallSequence(context: ICompilationContext): ICompiledFunctionCall {
|
||||
const compiledFunctions = context.callSequence
|
||||
.flatMap((call) => compileSingleCall(call, context));
|
||||
return {
|
||||
code: merge(compiledFunctions.map((f) => f.code)),
|
||||
revertCode: merge(compiledFunctions.map((f) => f.revertCode)),
|
||||
};
|
||||
}
|
||||
|
||||
function compileSingleCall(
|
||||
call: IFunctionCall,
|
||||
context: ICompilationContext,
|
||||
): ICompiledFunctionCall[] {
|
||||
const func = context.allFunctions.getFunctionByName(call.functionName);
|
||||
ensureThatCallArgumentsExistInParameterDefinition(func, call.args);
|
||||
if (func.body.code) { // Function with inline code
|
||||
const compiledCode = compileCode(func.body.code, call.args, context.expressionsCompiler);
|
||||
return [compiledCode];
|
||||
}
|
||||
// Function with inner calls
|
||||
return func.body.calls
|
||||
.map((innerCall) => {
|
||||
const compiledArgs = compileArgs(innerCall.args, call.args, context.expressionsCompiler);
|
||||
const compiledCall = new FunctionCall(innerCall.functionName, compiledArgs);
|
||||
return compileSingleCall(compiledCall, context);
|
||||
})
|
||||
.flat();
|
||||
}
|
||||
|
||||
function compileCode(
|
||||
code: IFunctionCode,
|
||||
args: IReadOnlyFunctionCallArgumentCollection,
|
||||
compiler: IExpressionsCompiler,
|
||||
): ICompiledFunctionCall {
|
||||
return {
|
||||
code: compiler.compileExpressions(code.execute, args),
|
||||
revertCode: compiler.compileExpressions(code.revert, args),
|
||||
};
|
||||
}
|
||||
|
||||
function compileArgs(
|
||||
argsToCompile: IReadOnlyFunctionCallArgumentCollection,
|
||||
args: IReadOnlyFunctionCallArgumentCollection,
|
||||
compiler: IExpressionsCompiler,
|
||||
): IReadOnlyFunctionCallArgumentCollection {
|
||||
return argsToCompile
|
||||
.getAllParameterNames()
|
||||
.map((parameterName) => {
|
||||
const { argumentValue } = argsToCompile.getArgument(parameterName);
|
||||
const compiledValue = compiler.compileExpressions(argumentValue, args);
|
||||
return new FunctionCallArgument(parameterName, compiledValue);
|
||||
})
|
||||
.reduce((compiledArgs, arg) => {
|
||||
compiledArgs.addArgument(arg);
|
||||
return compiledArgs;
|
||||
}, new FunctionCallArgumentCollection());
|
||||
}
|
||||
|
||||
function merge(codeParts: readonly string[]): string {
|
||||
return codeParts
|
||||
.filter((part) => part?.length > 0)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
function ensureThatCallArgumentsExistInParameterDefinition(
|
||||
func: ISharedFunction,
|
||||
args: IReadOnlyFunctionCallArgumentCollection,
|
||||
): void {
|
||||
const callArgumentNames = args.getAllParameterNames();
|
||||
const functionParameterNames = func.parameters.all.map((param) => param.name) || [];
|
||||
const unexpectedParameters = findUnexpectedParameters(callArgumentNames, functionParameterNames);
|
||||
throwIfNotEmpty(func.name, unexpectedParameters, functionParameterNames);
|
||||
}
|
||||
|
||||
function findUnexpectedParameters(
|
||||
callArgumentNames: string[],
|
||||
functionParameterNames: string[],
|
||||
): string[] {
|
||||
if (!callArgumentNames.length && !functionParameterNames.length) {
|
||||
return [];
|
||||
}
|
||||
return callArgumentNames
|
||||
.filter((callParam) => !functionParameterNames.includes(callParam));
|
||||
}
|
||||
|
||||
function throwIfNotEmpty(
|
||||
functionName: string,
|
||||
unexpectedParameters: string[],
|
||||
expectedParameters: string[],
|
||||
) {
|
||||
if (!unexpectedParameters.length) {
|
||||
return;
|
||||
}
|
||||
throw new Error(
|
||||
// eslint-disable-next-line prefer-template
|
||||
`Function "${functionName}" has unexpected parameter(s) provided: `
|
||||
+ `"${unexpectedParameters.join('", "')}"`
|
||||
+ '. Expected parameter(s): '
|
||||
+ (expectedParameters.length ? `"${expectedParameters.join('", "')}"` : 'none'),
|
||||
);
|
||||
): CompiledCode;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
||||
import { ISharedFunctionCollection } from '../../ISharedFunctionCollection';
|
||||
import { FunctionCallCompiler } from './FunctionCallCompiler';
|
||||
import { CompiledCode } from './CompiledCode';
|
||||
import { FunctionCallCompilationContext } from './FunctionCallCompilationContext';
|
||||
import { SingleCallCompiler } from './SingleCall/SingleCallCompiler';
|
||||
import { AdaptiveFunctionCallCompiler } from './SingleCall/AdaptiveFunctionCallCompiler';
|
||||
import { CodeSegmentMerger } from './CodeSegmentJoin/CodeSegmentMerger';
|
||||
import { NewlineCodeSegmentMerger } from './CodeSegmentJoin/NewlineCodeSegmentMerger';
|
||||
|
||||
export class FunctionCallSequenceCompiler implements FunctionCallCompiler {
|
||||
public static readonly instance: FunctionCallCompiler = new FunctionCallSequenceCompiler();
|
||||
|
||||
/* The constructor is protected to enforce the singleton pattern. */
|
||||
protected constructor(
|
||||
private readonly singleCallCompiler: SingleCallCompiler = new AdaptiveFunctionCallCompiler(),
|
||||
private readonly codeSegmentMerger: CodeSegmentMerger = new NewlineCodeSegmentMerger(),
|
||||
) { }
|
||||
|
||||
public compileFunctionCalls(
|
||||
calls: readonly FunctionCall[],
|
||||
functions: ISharedFunctionCollection,
|
||||
): CompiledCode {
|
||||
if (!functions) { throw new Error('missing functions'); }
|
||||
if (!calls?.length) { throw new Error('missing calls'); }
|
||||
if (calls.some((f) => !f)) { throw new Error('missing function call'); }
|
||||
const context: FunctionCallCompilationContext = {
|
||||
allFunctions: functions,
|
||||
rootCallSequence: calls,
|
||||
singleCallCompiler: this.singleCallCompiler,
|
||||
};
|
||||
const codeSegments = context.rootCallSequence
|
||||
.flatMap((call) => this.singleCallCompiler.compileSingleCall(call, context));
|
||||
return this.codeSegmentMerger.mergeCodeParts(codeSegments);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
|
||||
import { IFunctionCall } from '../IFunctionCall';
|
||||
import { ICompiledCode } from './ICompiledCode';
|
||||
|
||||
export interface IFunctionCallCompiler {
|
||||
compileCall(
|
||||
calls: IFunctionCall[],
|
||||
functions: ISharedFunctionCollection): ICompiledCode;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { FunctionCall } from '../../FunctionCall';
|
||||
import { CompiledCode } from '../CompiledCode';
|
||||
import { FunctionCallCompilationContext } from '../FunctionCallCompilationContext';
|
||||
import { IReadOnlyFunctionCallArgumentCollection } from '../../Argument/IFunctionCallArgumentCollection';
|
||||
import { ISharedFunction } from '../../../ISharedFunction';
|
||||
import { SingleCallCompiler } from './SingleCallCompiler';
|
||||
import { SingleCallCompilerStrategy } from './SingleCallCompilerStrategy';
|
||||
import { InlineFunctionCallCompiler } from './Strategies/InlineFunctionCallCompiler';
|
||||
import { NestedFunctionCallCompiler } from './Strategies/NestedFunctionCallCompiler';
|
||||
|
||||
export class AdaptiveFunctionCallCompiler implements SingleCallCompiler {
|
||||
public constructor(
|
||||
private readonly strategies: SingleCallCompilerStrategy[] = [
|
||||
new InlineFunctionCallCompiler(),
|
||||
new NestedFunctionCallCompiler(),
|
||||
],
|
||||
) {
|
||||
}
|
||||
|
||||
public compileSingleCall(
|
||||
call: FunctionCall,
|
||||
context: FunctionCallCompilationContext,
|
||||
): CompiledCode[] {
|
||||
const func = context.allFunctions.getFunctionByName(call.functionName);
|
||||
ensureThatCallArgumentsExistInParameterDefinition(func, call.args);
|
||||
const strategy = this.findStrategy(func);
|
||||
return strategy.compileFunction(func, call, context);
|
||||
}
|
||||
|
||||
private findStrategy(func: ISharedFunction): SingleCallCompilerStrategy {
|
||||
const strategies = this.strategies.filter((strategy) => strategy.canCompile(func));
|
||||
if (strategies.length > 1) {
|
||||
throw new Error('Multiple strategies found to compile the function call.');
|
||||
}
|
||||
if (strategies.length === 0) {
|
||||
throw new Error('No strategies found to compile the function call.');
|
||||
}
|
||||
return strategies[0];
|
||||
}
|
||||
}
|
||||
|
||||
function ensureThatCallArgumentsExistInParameterDefinition(
|
||||
func: ISharedFunction,
|
||||
callArguments: IReadOnlyFunctionCallArgumentCollection,
|
||||
): void {
|
||||
const callArgumentNames = callArguments.getAllParameterNames();
|
||||
const functionParameterNames = func.parameters.all.map((param) => param.name) || [];
|
||||
const unexpectedParameters = findUnexpectedParameters(callArgumentNames, functionParameterNames);
|
||||
throwIfUnexpectedParametersExist(func.name, unexpectedParameters, functionParameterNames);
|
||||
}
|
||||
|
||||
function findUnexpectedParameters(
|
||||
callArgumentNames: string[],
|
||||
functionParameterNames: string[],
|
||||
): string[] {
|
||||
if (!callArgumentNames.length && !functionParameterNames.length) {
|
||||
return [];
|
||||
}
|
||||
return callArgumentNames
|
||||
.filter((callParam) => !functionParameterNames.includes(callParam));
|
||||
}
|
||||
|
||||
function throwIfUnexpectedParametersExist(
|
||||
functionName: string,
|
||||
unexpectedParameters: string[],
|
||||
expectedParameters: string[],
|
||||
) {
|
||||
if (!unexpectedParameters.length) {
|
||||
return;
|
||||
}
|
||||
throw new Error(
|
||||
// eslint-disable-next-line prefer-template
|
||||
`Function "${functionName}" has unexpected parameter(s) provided: `
|
||||
+ `"${unexpectedParameters.join('", "')}"`
|
||||
+ '. Expected parameter(s): '
|
||||
+ (expectedParameters.length ? `"${expectedParameters.join('", "')}"` : 'none'),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { FunctionCall } from '../../FunctionCall';
|
||||
import { CompiledCode } from '../CompiledCode';
|
||||
import { FunctionCallCompilationContext } from '../FunctionCallCompilationContext';
|
||||
|
||||
export interface SingleCallCompiler {
|
||||
compileSingleCall(
|
||||
call: FunctionCall,
|
||||
context: FunctionCallCompilationContext,
|
||||
): CompiledCode[];
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
||||
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
||||
import { CompiledCode } from '../CompiledCode';
|
||||
import { FunctionCallCompilationContext } from '../FunctionCallCompilationContext';
|
||||
|
||||
export interface SingleCallCompilerStrategy {
|
||||
canCompile(func: ISharedFunction): boolean;
|
||||
compileFunction(
|
||||
calledFunction: ISharedFunction,
|
||||
callToFunction: FunctionCall,
|
||||
context: FunctionCallCompilationContext,
|
||||
): CompiledCode[],
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
||||
import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
|
||||
|
||||
export interface ArgumentCompiler {
|
||||
createCompiledNestedCall(
|
||||
nestedFunctionCall: FunctionCall,
|
||||
parentFunctionCall: FunctionCall,
|
||||
context: FunctionCallCompilationContext,
|
||||
): FunctionCall;
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection';
|
||||
import { FunctionCallArgument } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument';
|
||||
import { FunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection';
|
||||
import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler';
|
||||
import { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler';
|
||||
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
||||
import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
|
||||
import { ParsedFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/ParsedFunctionCall';
|
||||
import { ArgumentCompiler } from './ArgumentCompiler';
|
||||
|
||||
export class NestedFunctionArgumentCompiler implements ArgumentCompiler {
|
||||
constructor(
|
||||
private readonly expressionsCompiler: IExpressionsCompiler = new ExpressionsCompiler(),
|
||||
) { }
|
||||
|
||||
public createCompiledNestedCall(
|
||||
nestedFunction: FunctionCall,
|
||||
parentFunction: FunctionCall,
|
||||
context: FunctionCallCompilationContext,
|
||||
): FunctionCall {
|
||||
const compiledArgs = compileNestedFunctionArguments(
|
||||
nestedFunction,
|
||||
parentFunction.args,
|
||||
context,
|
||||
this.expressionsCompiler,
|
||||
);
|
||||
const compiledCall = new ParsedFunctionCall(nestedFunction.functionName, compiledArgs);
|
||||
return compiledCall;
|
||||
}
|
||||
}
|
||||
|
||||
function compileNestedFunctionArguments(
|
||||
nestedFunction: FunctionCall,
|
||||
parentFunctionArgs: IReadOnlyFunctionCallArgumentCollection,
|
||||
context: FunctionCallCompilationContext,
|
||||
expressionsCompiler: IExpressionsCompiler,
|
||||
): IReadOnlyFunctionCallArgumentCollection {
|
||||
const requiredParameterNames = context
|
||||
.allFunctions
|
||||
.getRequiredParameterNames(nestedFunction.functionName);
|
||||
const compiledArguments = nestedFunction.args
|
||||
.getAllParameterNames()
|
||||
// Compile each argument value
|
||||
.map((paramName) => ({
|
||||
parameterName: paramName,
|
||||
compiledArgumentValue: compileArgument(
|
||||
paramName,
|
||||
nestedFunction,
|
||||
parentFunctionArgs,
|
||||
expressionsCompiler,
|
||||
),
|
||||
}))
|
||||
// Filter out arguments with absent values
|
||||
.filter(({
|
||||
parameterName,
|
||||
compiledArgumentValue,
|
||||
}) => isValidNonAbsentArgumentValue(
|
||||
parameterName,
|
||||
compiledArgumentValue,
|
||||
requiredParameterNames,
|
||||
))
|
||||
/*
|
||||
Create argument object with non-absent values.
|
||||
This is done after eliminating absent values because otherwise creating argument object
|
||||
with absent values throws error.
|
||||
*/
|
||||
.map(({
|
||||
parameterName,
|
||||
compiledArgumentValue,
|
||||
}) => new FunctionCallArgument(parameterName, compiledArgumentValue));
|
||||
return buildArgumentCollectionFromArguments(compiledArguments);
|
||||
}
|
||||
|
||||
function isValidNonAbsentArgumentValue(
|
||||
parameterName: string,
|
||||
argumentValue: string | undefined,
|
||||
requiredParameterNames: string[],
|
||||
): boolean {
|
||||
if (argumentValue) {
|
||||
return true;
|
||||
}
|
||||
if (!requiredParameterNames.includes(parameterName)) {
|
||||
return false;
|
||||
}
|
||||
throw new Error(`Compilation resulted in empty value for required parameter: "${parameterName}"`);
|
||||
}
|
||||
|
||||
function compileArgument(
|
||||
parameterName: string,
|
||||
nestedFunction: FunctionCall,
|
||||
parentFunctionArgs: IReadOnlyFunctionCallArgumentCollection,
|
||||
expressionsCompiler: IExpressionsCompiler,
|
||||
): string {
|
||||
try {
|
||||
const { argumentValue: codeInArgument } = nestedFunction.args.getArgument(parameterName);
|
||||
return expressionsCompiler.compileExpressions(codeInArgument, parentFunctionArgs);
|
||||
} catch (err) {
|
||||
throw new AggregateError([err], `Error when compiling argument for "${parameterName}"`);
|
||||
}
|
||||
}
|
||||
|
||||
function buildArgumentCollectionFromArguments(
|
||||
args: FunctionCallArgument[],
|
||||
): FunctionCallArgumentCollection {
|
||||
return args.reduce((compiledArgs, arg) => {
|
||||
compiledArgs.addArgument(arg);
|
||||
return compiledArgs;
|
||||
}, new FunctionCallArgumentCollection());
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler';
|
||||
import { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler';
|
||||
import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
||||
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
||||
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
|
||||
import { SingleCallCompilerStrategy } from '../SingleCallCompilerStrategy';
|
||||
|
||||
export class InlineFunctionCallCompiler implements SingleCallCompilerStrategy {
|
||||
public constructor(
|
||||
private readonly expressionsCompiler: IExpressionsCompiler = new ExpressionsCompiler(),
|
||||
) {
|
||||
}
|
||||
|
||||
public canCompile(func: ISharedFunction): boolean {
|
||||
return func.body.code !== undefined;
|
||||
}
|
||||
|
||||
public compileFunction(
|
||||
calledFunction: ISharedFunction,
|
||||
callToFunction: FunctionCall,
|
||||
): CompiledCode[] {
|
||||
const { code } = calledFunction.body;
|
||||
const { args } = callToFunction;
|
||||
return [
|
||||
{
|
||||
code: this.expressionsCompiler.compileExpressions(code.execute, args),
|
||||
revertCode: this.expressionsCompiler.compileExpressions(code.revert, args),
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
||||
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
||||
import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
|
||||
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
|
||||
import { SingleCallCompilerStrategy } from '../SingleCallCompilerStrategy';
|
||||
import { ArgumentCompiler } from './Argument/ArgumentCompiler';
|
||||
import { NestedFunctionArgumentCompiler } from './Argument/NestedFunctionArgumentCompiler';
|
||||
|
||||
export class NestedFunctionCallCompiler implements SingleCallCompilerStrategy {
|
||||
public constructor(
|
||||
private readonly argumentCompiler: ArgumentCompiler = new NestedFunctionArgumentCompiler(),
|
||||
) {
|
||||
}
|
||||
|
||||
public canCompile(func: ISharedFunction): boolean {
|
||||
return func.body.calls !== undefined;
|
||||
}
|
||||
|
||||
public compileFunction(
|
||||
calledFunction: ISharedFunction,
|
||||
callToFunction: FunctionCall,
|
||||
context: FunctionCallCompilationContext,
|
||||
): CompiledCode[] {
|
||||
const nestedCalls = calledFunction.body.calls;
|
||||
return nestedCalls.map((nestedCall) => {
|
||||
try {
|
||||
const compiledParentCall = this.argumentCompiler
|
||||
.createCompiledNestedCall(nestedCall, callToFunction, context);
|
||||
const compiledNestedCall = context.singleCallCompiler
|
||||
.compileSingleCall(compiledParentCall, context);
|
||||
return compiledNestedCall;
|
||||
} catch (err) {
|
||||
throw new AggregateError([err], `Error with call to "${nestedCall.functionName}" function from "${callToFunction.functionName}" function`);
|
||||
}
|
||||
}).flat();
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,6 @@
|
||||
import { IReadOnlyFunctionCallArgumentCollection } from './Argument/IFunctionCallArgumentCollection';
|
||||
import { IFunctionCall } from './IFunctionCall';
|
||||
|
||||
export class FunctionCall implements IFunctionCall {
|
||||
constructor(
|
||||
public readonly functionName: string,
|
||||
public readonly args: IReadOnlyFunctionCallArgumentCollection,
|
||||
) {
|
||||
if (!functionName) {
|
||||
throw new Error('missing function name in function call');
|
||||
}
|
||||
if (!args) {
|
||||
throw new Error('missing args');
|
||||
}
|
||||
}
|
||||
export interface FunctionCall {
|
||||
readonly functionName: string;
|
||||
readonly args: IReadOnlyFunctionCallArgumentCollection;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { FunctionCallData, FunctionCallsData, FunctionCallParametersData } from '@/application/collections/';
|
||||
import { IFunctionCall } from './IFunctionCall';
|
||||
import { FunctionCall } from './FunctionCall';
|
||||
import { FunctionCallArgumentCollection } from './Argument/FunctionCallArgumentCollection';
|
||||
import { FunctionCallArgument } from './Argument/FunctionCallArgument';
|
||||
import { FunctionCall } from './FunctionCall';
|
||||
import { ParsedFunctionCall } from './ParsedFunctionCall';
|
||||
|
||||
export function parseFunctionCalls(calls: FunctionCallsData): IFunctionCall[] {
|
||||
export function parseFunctionCalls(calls: FunctionCallsData): FunctionCall[] {
|
||||
if (calls === undefined) {
|
||||
throw new Error('missing call data');
|
||||
}
|
||||
@@ -22,12 +22,12 @@ function getCallSequence(calls: FunctionCallsData): FunctionCallData[] {
|
||||
return [calls as FunctionCallData];
|
||||
}
|
||||
|
||||
function parseFunctionCall(call: FunctionCallData): IFunctionCall {
|
||||
function parseFunctionCall(call: FunctionCallData): FunctionCall {
|
||||
if (!call) {
|
||||
throw new Error('missing call data');
|
||||
}
|
||||
const callArgs = parseArgs(call.parameters);
|
||||
return new FunctionCall(call.function, callArgs);
|
||||
return new ParsedFunctionCall(call.function, callArgs);
|
||||
}
|
||||
|
||||
function parseArgs(
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import { IReadOnlyFunctionCallArgumentCollection } from './Argument/IFunctionCallArgumentCollection';
|
||||
|
||||
export interface IFunctionCall {
|
||||
readonly functionName: string;
|
||||
readonly args: IReadOnlyFunctionCallArgumentCollection;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { IReadOnlyFunctionCallArgumentCollection } from './Argument/IFunctionCallArgumentCollection';
|
||||
import { FunctionCall } from './FunctionCall';
|
||||
|
||||
export class ParsedFunctionCall implements FunctionCall {
|
||||
constructor(
|
||||
public readonly functionName: string,
|
||||
public readonly args: IReadOnlyFunctionCallArgumentCollection,
|
||||
) {
|
||||
if (!functionName) {
|
||||
throw new Error('missing function name in function call');
|
||||
}
|
||||
if (!args) {
|
||||
throw new Error('missing args');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection';
|
||||
import { IFunctionCall } from './Call/IFunctionCall';
|
||||
import { FunctionCall } from './Call/FunctionCall';
|
||||
|
||||
export interface ISharedFunction {
|
||||
readonly name: string;
|
||||
@@ -9,8 +9,8 @@ export interface ISharedFunction {
|
||||
|
||||
export interface ISharedFunctionBody {
|
||||
readonly type: FunctionBodyType;
|
||||
readonly code: IFunctionCode;
|
||||
readonly calls: readonly IFunctionCall[];
|
||||
readonly code: IFunctionCode | undefined;
|
||||
readonly calls: readonly FunctionCall[] | undefined;
|
||||
}
|
||||
|
||||
export enum FunctionBodyType {
|
||||
|
||||
@@ -2,4 +2,5 @@ import { ISharedFunction } from './ISharedFunction';
|
||||
|
||||
export interface ISharedFunctionCollection {
|
||||
getFunctionByName(name: string): ISharedFunction;
|
||||
getRequiredParameterNames(functionName: string): string[];
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IFunctionCall } from './Call/IFunctionCall';
|
||||
import { FunctionCall } from './Call/FunctionCall';
|
||||
|
||||
import {
|
||||
FunctionBodyType, IFunctionCode, ISharedFunction, ISharedFunctionBody,
|
||||
@@ -8,7 +8,7 @@ import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParam
|
||||
export function createCallerFunction(
|
||||
name: string,
|
||||
parameters: IReadOnlyFunctionParameterCollection,
|
||||
callSequence: readonly IFunctionCall[],
|
||||
callSequence: readonly FunctionCall[],
|
||||
): ISharedFunction {
|
||||
if (!callSequence || !callSequence.length) {
|
||||
throw new Error(`missing call sequence in function "${name}"`);
|
||||
@@ -38,7 +38,7 @@ class SharedFunction implements ISharedFunction {
|
||||
constructor(
|
||||
public readonly name: string,
|
||||
public readonly parameters: IReadOnlyFunctionParameterCollection,
|
||||
content: IFunctionCode | readonly IFunctionCall[],
|
||||
content: IFunctionCode | readonly FunctionCall[],
|
||||
bodyType: FunctionBodyType,
|
||||
) {
|
||||
if (!name) { throw new Error('missing function name'); }
|
||||
@@ -46,7 +46,7 @@ class SharedFunction implements ISharedFunction {
|
||||
this.body = {
|
||||
type: bodyType,
|
||||
code: bodyType === FunctionBodyType.Code ? content as IFunctionCode : undefined,
|
||||
calls: bodyType === FunctionBodyType.Calls ? content as readonly IFunctionCall[] : undefined,
|
||||
calls: bodyType === FunctionBodyType.Calls ? content as readonly FunctionCall[] : undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,15 @@ export class SharedFunctionCollection implements ISharedFunctionCollection {
|
||||
return func;
|
||||
}
|
||||
|
||||
public getRequiredParameterNames(functionName: string): string[] {
|
||||
return this
|
||||
.getFunctionByName(functionName)
|
||||
.parameters
|
||||
.all
|
||||
.filter((parameter) => !parameter.isOptional)
|
||||
.map((parameter) => parameter.name);
|
||||
}
|
||||
|
||||
private has(functionName: string) {
|
||||
return this.functionsByName.has(functionName);
|
||||
}
|
||||
|
||||
@@ -7,12 +7,12 @@ import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmp
|
||||
import { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
|
||||
import { IScriptCompiler } from './IScriptCompiler';
|
||||
import { ISharedFunctionCollection } from './Function/ISharedFunctionCollection';
|
||||
import { IFunctionCallCompiler } from './Function/Call/Compiler/IFunctionCallCompiler';
|
||||
import { FunctionCallSequenceCompiler } from './Function/Call/Compiler/FunctionCallSequenceCompiler';
|
||||
import { FunctionCallCompiler } from './Function/Call/Compiler/FunctionCallCompiler';
|
||||
import { ISharedFunctionsParser } from './Function/ISharedFunctionsParser';
|
||||
import { SharedFunctionsParser } from './Function/SharedFunctionsParser';
|
||||
import { parseFunctionCalls } from './Function/Call/FunctionCallParser';
|
||||
import { ICompiledCode } from './Function/Call/Compiler/ICompiledCode';
|
||||
import { CompiledCode } from './Function/Call/Compiler/CompiledCode';
|
||||
|
||||
export class ScriptCompiler implements IScriptCompiler {
|
||||
private readonly functions: ISharedFunctionCollection;
|
||||
@@ -21,7 +21,7 @@ export class ScriptCompiler implements IScriptCompiler {
|
||||
functions: readonly FunctionData[] | undefined,
|
||||
syntax: ILanguageSyntax,
|
||||
sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance,
|
||||
private readonly callCompiler: IFunctionCallCompiler = FunctionCallCompiler.instance,
|
||||
private readonly callCompiler: FunctionCallCompiler = FunctionCallSequenceCompiler.instance,
|
||||
private readonly codeValidator: ICodeValidator = CodeValidator.instance,
|
||||
) {
|
||||
if (!syntax) { throw new Error('missing syntax'); }
|
||||
@@ -40,7 +40,7 @@ export class ScriptCompiler implements IScriptCompiler {
|
||||
if (!script) { throw new Error('missing script'); }
|
||||
try {
|
||||
const calls = parseFunctionCalls(script.call);
|
||||
const compiledCode = this.callCompiler.compileCall(calls, this.functions);
|
||||
const compiledCode = this.callCompiler.compileFunctionCalls(calls, this.functions);
|
||||
validateCompiledCode(compiledCode, this.codeValidator);
|
||||
return new ScriptCode(
|
||||
compiledCode.code,
|
||||
@@ -52,7 +52,7 @@ export class ScriptCompiler implements IScriptCompiler {
|
||||
}
|
||||
}
|
||||
|
||||
function validateCompiledCode(compiledCode: ICompiledCode, validator: ICodeValidator): void {
|
||||
function validateCompiledCode(compiledCode: CompiledCode, validator: ICodeValidator): void {
|
||||
[compiledCode.code, compiledCode.revertCode].forEach(
|
||||
(code) => validator.throwIfInvalid(code, [new NoEmptyLines()]),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user