tom-weatherhead/thaw-grammar

View on GitHub
src/languages/clu/domain-object-model/operator-usage.ts

Summary

Maintainability
D
2 days
Test Coverage
// clu/domain-object-model/operator-usage.ts

// import { ArgumentException } from 'thaw-interpreter-core';

import {
    EnvironmentFrame,
    IEnvironmentFrame
} from '../../../common/domain-object-model/environment-frame';

import { IGlobalInfo } from '../../../common/domain-object-model/iglobal-info';

import {
    ICLUExpression,
    ICLUFunctionName,
    ICluster,
    ICLUValue,
    ICLUVariable
} from './interfaces/ivalue';

// import { isCLUPrimitiveValue } from './data-types/primitive-value';

import { CLUUserValue, isCLUUserValue } from './data-types/user-value';

import { isCluEvaluateOptions } from '../utilities/type-guards';

import { isCLUConstructorDefinition } from './constructor-definition';

import { CLUFunctionDefinitionBase } from './function-definition-base';

import { isCLUGlobalInfo } from './global-info';

import {
    CLUNormalFunctionDefinition,
    isCLUNormalFunctionDefinition
} from './normal-function-definition';

import { isOnePartFunctionName } from './one-part-function-name';

import { isCLUSelectorDefinition } from './selector-definition';

import { isCLUSettorDefinition } from './settor-definition';

import { isTwoPartFunctionName } from './two-part-function-name';

export class FunctionNotExportedException extends Error {
    constructor(clusterName: string, functionName: string) {
        super(`The cluster '${clusterName}' does not export a function named '${functionName}'.`);
    }
}

const builtInOperatorNames = ['+', '-', '*', '/', '=', '<', '>', 'print'];

const twoArgumentIntegerOperators = new Map<string, (x: number, y: number) => number>();
const twoArgumentIntegerPredicates = new Map<string, (x: number, y: number) => boolean>();

twoArgumentIntegerOperators.set('+', (x: number, y: number) => x + y);
twoArgumentIntegerOperators.set('-', (x: number, y: number) => x - y);
twoArgumentIntegerOperators.set('*', (x: number, y: number) => x * y);
twoArgumentIntegerOperators.set('/', (x: number, y: number) => Math.floor(x / y));

twoArgumentIntegerPredicates.set('=', (x: number, y: number) => x === y);
twoArgumentIntegerPredicates.set('<', (x: number, y: number) => x < y);
twoArgumentIntegerPredicates.set('>', (x: number, y: number) => x > y);

export class CLUOperatorUsage implements ICLUExpression {
    private readonly clusterName: string;
    private readonly functionName: string;

    constructor(operatorName: ICLUFunctionName, public readonly expressionList: ICLUExpression[]) {
        if (isTwoPartFunctionName(operatorName)) {
            this.clusterName = operatorName.clusterPart;
            this.functionName = operatorName.functionPart;
        } else if (isOnePartFunctionName(operatorName)) {
            this.clusterName = '';
            this.functionName = operatorName.functionPart;
        } else {
            throw new Error('CLUOperatorUsage constructor');
        }
    }

    /*
    public override string ToString()
    {
        return string.Format("({0} {1})", OperatorName, ExpressionList);
    }
     */

    protected tryGetExpectedNumArgs(
        funDef: CLUFunctionDefinitionBase | undefined,
        cluster: ICluster | undefined,
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        globalInfo: IGlobalInfo<ICLUValue>
    ): number | undefined {
        if (typeof funDef === 'undefined') {
            switch (this.functionName) {
                case 'print':
                    return 1;

                case '+':
                case '-':
                case '*':
                case '/':
                case '=':
                case '<':
                case '>':
                    return 2;

                default:
                    throw new Error(
                        `TryGetExpectedNumArgs() : Unknown built-in operator '${this.functionName}'.`
                    );
            }
        }

        if (isCLUNormalFunctionDefinition(funDef)) {
            return funDef.argList.length;
        } else if (isCLUConstructorDefinition(funDef)) {
            if (typeof cluster === 'undefined') {
                throw new Error('tryGetExpectedNumArgs() : cluster is undefined');
            }

            return cluster.clRep.length;
        } else if (isCLUSelectorDefinition(funDef)) {
            return 1;
        } else if (isCLUSettorDefinition(funDef)) {
            return 2;
        } else {
            throw new Error('tryGetExpectedNumArgs() : Unknown operator type.');
        }
    }

    protected checkArgTypes(
        funDef: CLUFunctionDefinitionBase | undefined,
        cluster: ICluster | undefined,
        evaluatedArguments: ICLUValue[]
    ): void {
        if (typeof funDef !== 'undefined') {
            let associatedVariable: ICLUVariable | undefined;

            if (isCLUSelectorDefinition(funDef)) {
                associatedVariable = funDef.associatedVariable;
            } else if (isCLUSettorDefinition(funDef)) {
                associatedVariable = funDef.associatedVariable;
            }

            if (typeof associatedVariable !== 'undefined') {
                const userValue = evaluatedArguments[0]; // as ICLUUserValue;

                if (!isCLUUserValue(userValue)) {
                    throw new Error(
                        'Selector/settor arg type check: First arg is not a CLUUserValue.'
                    );
                } else if (!userValue.value.has(associatedVariable)) {
                    throw new Error(
                        `Selector/settor arg type check: First arg does not contain the member var ${associatedVariable}.`
                    );
                }
            }

            return;
        }

        // if (IntegerOperatorKeeper.TwoArgumentOperators.ContainsKey(this.functionName) ||
        //     IntegerOperatorKeeper.TwoArgumentPredicates.ContainsKey(this.functionName)) {
        //
        //     if (!isCLUPrimitiveValue(evaluatedArguments[0])) {
        //         throw new ArgumentException(`Operator ${this.functionName} : First argument is not a number.`, 'evaluatedArguments[0]');
        //     }
        //
        //     if (!isCLUPrimitiveValue(evaluatedArguments[1])) {
        //         throw new ArgumentException(`Operator ${this.functionName} : Second argument is not a number.`, 'evaluatedArguments[1]');
        //     }
        // }
    }

    protected evaluateNormal(
        funDef: CLUNormalFunctionDefinition | undefined,
        evaluatedArguments: ICLUValue[],
        cluster: ICluster | undefined,
        globalInfo: IGlobalInfo<ICLUValue>
    ): ICLUValue {
        const firstArgAsInt =
            evaluatedArguments.length > 0 && globalInfo.valueIsInteger(evaluatedArguments[0])
                ? globalInfo.valueAsInteger(evaluatedArguments[0])
                : 0;
        const secondArgAsInt =
            evaluatedArguments.length > 1 && globalInfo.valueIsInteger(evaluatedArguments[1])
                ? globalInfo.valueAsInteger(evaluatedArguments[1])
                : 0;

        if (typeof cluster === 'undefined') {
            const operator = twoArgumentIntegerOperators.get(this.functionName);
            const predicate = twoArgumentIntegerPredicates.get(this.functionName);

            if (typeof operator !== 'undefined') {
                return globalInfo.integerAsValue(operator(firstArgAsInt, secondArgAsInt));
            } else if (typeof predicate !== 'undefined') {
                return predicate(firstArgAsInt, secondArgAsInt)
                    ? globalInfo.trueValue
                    : globalInfo.falseValue;
            }

            switch (this.functionName) {
                case '=':
                    return evaluatedArguments[0].equals(evaluatedArguments[1])
                        ? globalInfo.trueValue
                        : globalInfo.falseValue;

                case 'print':
                    console.log(evaluatedArguments[0]);

                    return evaluatedArguments[0];

                default:
                    break;
            }
        }

        if (typeof funDef === 'undefined') {
            throw new Error('evaluateNormal() : funDef is undefined');
        }

        // Evaluate a user-defined function.
        const newEnvironment = new EnvironmentFrame<ICLUValue>(globalInfo.globalEnvironment);

        newEnvironment.compose(funDef.argList, evaluatedArguments);

        return funDef.body.evaluate(globalInfo, newEnvironment, { cluster });
    }

    // public evaluate(
    //     localEnvironment: ICLUEnvironmentFrame,
    //     cluster: ICluster | undefined,
    //     globalInfo: ICLUGlobalInfo
    // ): ICLUValue {
    public evaluate(
        globalInfo: IGlobalInfo<ICLUValue>,
        localEnvironment?: IEnvironmentFrame<ICLUValue>,
        options?: unknown
    ): ICLUValue {
        let cluster: ICluster | undefined;

        if (typeof options !== 'undefined') {
            if (!isCluEvaluateOptions(options)) {
                throw new Error('CLUOperatorUsage.evaluate() : options is not CluEvaluateOptions');
            }

            cluster = options.cluster;
        }

        // let { cluster } = options;
        const originalCluster = cluster;
        let funDef: CLUFunctionDefinitionBase | undefined;

        if (this.clusterName !== '') {
            if (!isCLUGlobalInfo(globalInfo)) {
                throw new Error('Cluster.evaluate() : globalInfo is not isCLUGlobalInfo.');
            }

            cluster = globalInfo.clusterDict.get(this.clusterName);

            if (typeof cluster === 'undefined') {
                throw new Error(
                    `CLUOperatorUsage.evaluate() : Unknown cluster '${this.clusterName}'.`
                );
            }

            funDef = cluster.exportedDict.get(this.functionName);

            if (typeof funDef === 'undefined') {
                //throw new Exception(string.Format("CLUOperatorUsage.Evaluate() : Cluster '{0}' does not contain an exported function named '{1}'.", ClusterName, FunctionName));
                throw new FunctionNotExportedException(this.clusterName, this.functionName);
            }
        } else if (typeof cluster === 'undefined') {
            if (builtInOperatorNames.indexOf(this.functionName) >= 0) {
                funDef = undefined;
            } else {
                funDef = globalInfo.functionDefinitions.get(this.functionName);

                if (typeof funDef === 'undefined') {
                    throw new Error(
                        `CLUOperatorUsage.evaluate() : Unknown global function '${this.functionName}'.`
                    );
                }
            }
        } else if (cluster.exportedDict.has(this.functionName)) {
            funDef = cluster.exportedDict.get(this.functionName);
        } else if (cluster.nonExportedDict.has(this.functionName)) {
            funDef = cluster.nonExportedDict.get(this.functionName);
        } else {
            cluster = undefined;

            if (builtInOperatorNames.indexOf(this.functionName) >= 0) {
                funDef = undefined;
            } else {
                if (!globalInfo.functionDefinitions.has(this.functionName)) {
                    throw new Error(
                        `CLUOperatorUsage.evaluate() : Unknown global function '${this.functionName}'.`
                    );
                }

                funDef = globalInfo.functionDefinitions.get(this.functionName);
            }
        }

        // At this point, funDef == null means that it's a built-in operator.

        const actualNumArgs = this.expressionList.length;
        const expectedNumArgs = this.tryGetExpectedNumArgs(funDef, cluster, globalInfo);

        if (typeof expectedNumArgs === 'undefined') {
            throw new Error(
                `CLUOperatorUsage.evaluate() : Unknown operator name '${this.functionName}'.`
            );
        } else if (actualNumArgs != expectedNumArgs) {
            throw new Error(
                `CLUOperatorUsage : Expected ${expectedNumArgs} arguments for operator '${this.functionName}', instead of the actual ${actualNumArgs} arguments.`
            );
        }

        // Evaluate using originalCluster, not cluster:
        const evaluatedArguments = this.expressionList.map((expr) =>
            expr.evaluate(globalInfo, localEnvironment, { cluster: originalCluster })
        );

        this.checkArgTypes(funDef, cluster, evaluatedArguments);

        if (typeof funDef === 'undefined' || isCLUNormalFunctionDefinition(funDef)) {
            return this.evaluateNormal(funDef, evaluatedArguments, cluster, globalInfo);
        } else if (isCLUConstructorDefinition(funDef)) {
            if (typeof cluster === 'undefined') {
                throw new Error('CLUOperatorUsage.evaluate() : cluster is undefined');
            }

            const newEnvironment = new EnvironmentFrame<ICLUValue>(globalInfo.globalEnvironment);

            newEnvironment.compose(cluster.clRep, evaluatedArguments);
            return new CLUUserValue(cluster, newEnvironment);
        } else if (isCLUSelectorDefinition(funDef) && isCLUUserValue(evaluatedArguments[0])) {
            const instance = evaluatedArguments[0];

            return funDef.evaluate(globalInfo, instance.value, { cluster });
        } else if (isCLUSettorDefinition(funDef) && isCLUUserValue(evaluatedArguments[0])) {
            const settor = funDef;
            const instance = evaluatedArguments[0];

            settor.setValue = evaluatedArguments[1];
            return funDef.evaluate(globalInfo, instance.value, { cluster });
        } else {
            throw new Error('CLUOperatorUsage : Failed to evaluate; unrecognized type of funDef.');
        }
    }
}