import {Value, ValueType, IntValue, ListValue} from './Value'; import {StoryException} from './StoryException'; import {Void} from './Void'; import {Path} from './Path'; import {InkList, InkListItem} from './InkList'; import {InkObject} from './Object'; import {asOrNull, asOrThrows} from './TypeAssertion'; import {throwNullException} from './NullException'; type BinaryOp = (left: T, right: T) => any; type UnaryOp = (val: T) => any; export class NativeFunctionCall extends InkObject{ // tslint:disable:variable-name public static readonly Add: string = '+'; public static readonly Subtract: string = '-'; public static readonly Divide: string = '/'; public static readonly Multiply: string = '*'; public static readonly Mod: string = '%'; public static readonly Negate: string = '_'; public static readonly Equal: string = '=='; public static readonly Greater: string = '>'; public static readonly Less: string = '<'; public static readonly GreaterThanOrEquals: string = '>='; public static readonly LessThanOrEquals: string = '<='; public static readonly NotEquals: string = '!='; public static readonly Not: string = '!'; public static readonly And: string = '&&'; public static readonly Or: string = '||'; public static readonly Min: string = 'MIN'; public static readonly Max: string = 'MAX'; public static readonly Pow: string = 'POW'; public static readonly Floor: string = 'FLOOR'; public static readonly Ceiling: string = 'CEILING'; public static readonly Int: string = 'INT'; public static readonly Float: string = 'FLOAT'; public static readonly Has: string = '?'; public static readonly Hasnt: string = '!?'; public static readonly Intersect: string = '^'; public static readonly ListMin: string = 'LIST_MIN'; public static readonly ListMax: string = 'LIST_MAX'; public static readonly All: string = 'LIST_ALL'; public static readonly Count: string = 'LIST_COUNT'; public static readonly ValueOfList: string = 'LIST_VALUE'; public static readonly Invert: string = 'LIST_INVERT'; // tslint:enable:variable-name public static CallWithName(functionName: string){ return new NativeFunctionCall(functionName); } public static CallExistsWithName(functionName: string){ this.GenerateNativeFunctionsIfNecessary(); return this._nativeFunctions!.get(functionName); } get name(){ if (this._name === null) return throwNullException('NativeFunctionCall._name'); return this._name; } set name(value: string){ this._name = value; if( !this._isPrototype ) { if (NativeFunctionCall._nativeFunctions === null) throwNullException('NativeFunctionCall._nativeFunctions'); else this._prototype = NativeFunctionCall._nativeFunctions.get(this._name) || null; } } public _name: string | null = null; get numberOfParameters(){ if (this._prototype) { return this._prototype.numberOfParameters; } else { return this._numberOfParameters; } } set numberOfParameters(value: number){ this._numberOfParameters = value; } public _numberOfParameters: number = 0; public Call(parameters: InkObject[]): InkObject | null{ if (this._prototype) { return this._prototype.Call(parameters); } if (this.numberOfParameters != parameters.length) { throw new Error('Unexpected number of parameters'); } let hasList = false; for (let p of parameters) { if (p instanceof Void) throw new StoryException('Attempting to perform operation on a void value. Did you forget to "return" a value from a function you called here?'); if (p instanceof ListValue) hasList = true; } if (parameters.length == 2 && hasList){ return this.CallBinaryListOperation(parameters); } let coercedParams = this.CoerceValuesToSingleType(parameters); let coercedType = coercedParams[0].valueType; if (coercedType == ValueType.Int) { return this.CallType(coercedParams); } else if (coercedType == ValueType.Float) { return this.CallType(coercedParams); } else if (coercedType == ValueType.String) { return this.CallType(coercedParams); } else if (coercedType == ValueType.DivertTarget) { return this.CallType(coercedParams); } else if (coercedType == ValueType.List) { return this.CallType(coercedParams); } return null; } public CallType(parametersOfSingleType: Array>){ let param1 = asOrThrows(parametersOfSingleType[0], Value); let valType = param1.valueType; let val1 = param1 as Value; let paramCount = parametersOfSingleType.length; if (paramCount == 2 || paramCount == 1) { if (this._operationFuncs === null) return throwNullException('NativeFunctionCall._operationFuncs'); let opForTypeObj = this._operationFuncs.get(valType); if (!opForTypeObj) { throw new StoryException('Cannot perform operation '+this.name+' on '+valType); } if (paramCount == 2) { let param2 = asOrThrows(parametersOfSingleType[1], Value); let val2 = param2 as Value; let opForType = opForTypeObj as BinaryOp; if (val1.value === null || val2.value === null) return throwNullException('NativeFunctionCall.Call BinaryOp values'); let resultVal = opForType(val1.value, val2.value); return Value.Create(resultVal); } else { let opForType = opForTypeObj as UnaryOp; if (val1.value === null) return throwNullException('NativeFunctionCall.Call UnaryOp value'); let resultVal = opForType(val1.value); return Value.Create(resultVal); } } else { throw new Error('Unexpected number of parameters to NativeFunctionCall: ' + parametersOfSingleType.length); } } public CallBinaryListOperation(parameters: InkObject[]){ if ((this.name == '+' || this.name == '-') && parameters[0] instanceof ListValue && parameters[1] instanceof IntValue) return this.CallListIncrementOperation(parameters); let v1 = asOrThrows(parameters[0], Value); let v2 = asOrThrows(parameters[1], Value); if ((this.name == '&&' || this.name == '||') && (v1.valueType != ValueType.List || v2.valueType != ValueType.List)) { if (this._operationFuncs === null) return throwNullException('NativeFunctionCall._operationFuncs'); let op = this._operationFuncs.get(ValueType.Int) as BinaryOp; if (op === null) return throwNullException('NativeFunctionCall.CallBinaryListOperation op'); let result = op(v1.isTruthy ? 1 : 0, v2.isTruthy ? 1 : 0); return new IntValue(result); } if (v1.valueType == ValueType.List && v2.valueType == ValueType.List) return this.CallType([v1, v2]); throw new StoryException('Can not call use ' + this.name + ' operation on ' + v1.valueType + ' and ' + v2.valueType); } public CallListIncrementOperation(listIntParams: InkObject[]){ let listVal = asOrThrows(listIntParams[0], ListValue); let intVal = asOrThrows(listIntParams[1], IntValue); let resultInkList = new InkList(); if (listVal.value === null) return throwNullException('NativeFunctionCall.CallListIncrementOperation listVal.value'); for (let [listItemKey, listItemValue] of listVal.value) { let listItem = InkListItem.fromSerializedKey(listItemKey); if (this._operationFuncs === null) return throwNullException('NativeFunctionCall._operationFuncs'); let intOp = this._operationFuncs.get(ValueType.Int) as BinaryOp; if (intVal.value === null) return throwNullException('NativeFunctionCall.CallListIncrementOperation intVal.value'); let targetInt = intOp(listItemValue, intVal.value); let itemOrigin = null; if (listVal.value.origins === null) return throwNullException('NativeFunctionCall.CallListIncrementOperation listVal.value.origins'); for (let origin of listVal.value.origins) { if (origin.name == listItem.originName) { itemOrigin = origin; break; } } if (itemOrigin != null) { let incrementedItem = itemOrigin.TryGetItemWithValue(targetInt, InkListItem.Null); if (incrementedItem.exists) resultInkList.Add(incrementedItem.result, targetInt); } } return new ListValue(resultInkList); } public CoerceValuesToSingleType(parametersIn: InkObject[]){ let valType = ValueType.Int; let specialCaseList: null | ListValue = null; for (let obj of parametersIn) { let val = asOrThrows(obj, Value); if (val.valueType > valType) { valType = val.valueType; } if (val.valueType == ValueType.List) { specialCaseList = asOrNull(val, ListValue); } } let parametersOut = []; if (ValueType[valType] == ValueType[ValueType.List]) { for (let inkObjectVal of parametersIn){ let val = asOrThrows(inkObjectVal, Value); if (val.valueType == ValueType.List) { parametersOut.push(val); } else if (val.valueType == ValueType.Int) { let intVal = parseInt(val.valueObject); specialCaseList = asOrThrows(specialCaseList, ListValue); if (specialCaseList.value === null) return throwNullException('NativeFunctionCall.CoerceValuesToSingleType specialCaseList.value'); let list = specialCaseList.value.originOfMaxItem; if (list === null) return throwNullException('NativeFunctionCall.CoerceValuesToSingleType list'); let item = list.TryGetItemWithValue(intVal, InkListItem.Null); if (item.exists) { let castedValue = new ListValue(item.result, intVal); parametersOut.push(castedValue); } else throw new StoryException('Could not find List item with the value ' + intVal + ' in ' + list.name); } else throw new StoryException('Cannot mix Lists and ' + val.valueType + ' values in this operation'); } } else { for (let inkObjectVal of parametersIn){ let val = asOrThrows(inkObjectVal, Value); let castedValue = val.Cast(valType); parametersOut.push(castedValue); } } return parametersOut; } constructor(name: string); constructor(name: string, numberOfParameters: number); constructor(); constructor() { super(); if (arguments.length === 0) { NativeFunctionCall.GenerateNativeFunctionsIfNecessary(); } else if (arguments.length === 1) { let name = arguments[0]; NativeFunctionCall.GenerateNativeFunctionsIfNecessary(); this.name = name; } else if (arguments.length === 2) { let name = arguments[0]; let numberOfParameters = arguments[1]; this._isPrototype = true; this.name = name; this.numberOfParameters = numberOfParameters; } } public static Identity(t: T): any { return t; } public static GenerateNativeFunctionsIfNecessary(){ if (this._nativeFunctions == null) { this._nativeFunctions = new Map(); // Int operations this.AddIntBinaryOp(this.Add, (x, y) => x + y); this.AddIntBinaryOp(this.Subtract, (x, y) => x - y); this.AddIntBinaryOp(this.Multiply, (x, y) => x * y); this.AddIntBinaryOp(this.Divide, (x, y) => Math.round(x / y)); this.AddIntBinaryOp(this.Mod, (x, y) => x % y); this.AddIntUnaryOp(this.Negate, (x) => -x); this.AddIntBinaryOp(this.Equal, (x, y) => x == y ? 1 : 0); this.AddIntBinaryOp(this.Greater, (x, y) => x > y ? 1 : 0); this.AddIntBinaryOp(this.Less, (x, y) => x < y ? 1 : 0); this.AddIntBinaryOp(this.GreaterThanOrEquals, (x, y) => x >= y ? 1 : 0); this.AddIntBinaryOp(this.LessThanOrEquals, (x, y) => x <= y ? 1 : 0); this.AddIntBinaryOp(this.NotEquals, (x, y) => x != y ? 1 : 0); this.AddIntUnaryOp(this.Not, (x) => (x == 0) ? 1 : 0); this.AddIntBinaryOp(this.And, (x, y) => x != 0 && y != 0 ? 1 : 0); this.AddIntBinaryOp(this.Or, (x, y) => x != 0 || y != 0 ? 1 : 0); this.AddIntBinaryOp(this.Max, (x, y) => Math.max(x, y)); this.AddIntBinaryOp(this.Min, (x, y) => Math.min(x, y)); this.AddIntBinaryOp(this.Pow, (x, y) => Math.pow(x, y)); this.AddIntUnaryOp(this.Floor, NativeFunctionCall.Identity); this.AddIntUnaryOp(this.Ceiling, NativeFunctionCall.Identity); this.AddIntUnaryOp(this.Int, NativeFunctionCall.Identity); this.AddIntUnaryOp(this.Float, (x) => x); // Float operations this.AddFloatBinaryOp(this.Add, (x, y) => x + y); this.AddFloatBinaryOp(this.Subtract, (x, y) => x - y); this.AddFloatBinaryOp(this.Multiply, (x, y) => x * y); this.AddFloatBinaryOp(this.Divide, (x, y) => x / y); this.AddFloatBinaryOp(this.Mod, (x, y) => x % y); this.AddFloatUnaryOp(this.Negate, (x) => -x); this.AddFloatBinaryOp(this.Equal, (x, y) => x == y ? 1 : 0); this.AddFloatBinaryOp(this.Greater, (x, y) => x > y ? 1 : 0); this.AddFloatBinaryOp(this.Less, (x, y) => x < y ? 1 : 0); this.AddFloatBinaryOp(this.GreaterThanOrEquals, (x, y) => x >= y ? 1 : 0); this.AddFloatBinaryOp(this.LessThanOrEquals, (x, y) => x <= y ? 1 : 0); this.AddFloatBinaryOp(this.NotEquals, (x, y) => x != y ? 1 : 0); this.AddFloatUnaryOp(this.Not, (x) => (x == 0.0) ? 1 : 0); this.AddFloatBinaryOp(this.And, (x, y) => x != 0.0 && y != 0.0 ? 1 : 0); this.AddFloatBinaryOp(this.Or, (x, y) => x != 0.0 || y != 0.0 ? 1 : 0); this.AddFloatBinaryOp(this.Max, (x, y) => Math.max(x, y)); this.AddFloatBinaryOp(this.Min, (x, y) => Math.min(x, y)); this.AddFloatBinaryOp(this.Pow, (x, y) => Math.pow(x, y)); this.AddFloatUnaryOp(this.Floor, (x) => Math.floor(x)); this.AddFloatUnaryOp(this.Ceiling, (x) => Math.ceil(x)); this.AddFloatUnaryOp(this.Int, (x) => Math.floor(x)); this.AddFloatUnaryOp(this.Float, NativeFunctionCall.Identity); // String operations this.AddStringBinaryOp(this.Add, (x, y) => x + y); // concat this.AddStringBinaryOp(this.Equal, (x, y) => x === y ? 1 : 0); this.AddStringBinaryOp(this.NotEquals,(x, y) => !(x === y) ? 1 : 0); this.AddStringBinaryOp(this.Has, (x, y) => x.includes(y) ? 1 : 0); this.AddStringBinaryOp(this.Hasnt, (x, y) => x.includes(y) ? 0 : 1); this.AddListBinaryOp(this.Add, (x, y) => x.Union(y)); this.AddListBinaryOp(this.Subtract, (x, y) => x.Without(y)); this.AddListBinaryOp(this.Has, (x, y) => x.Contains(y) ? 1 : 0); this.AddListBinaryOp(this.Hasnt, (x, y) => x.Contains(y) ? 0 : 1); this.AddListBinaryOp(this.Intersect, (x, y) => x.Intersect(y)); this.AddListBinaryOp(this.Equal, (x, y) => x.Equals(y) ? 1 : 0); this.AddListBinaryOp(this.Greater, (x, y) => x.GreaterThan(y) ? 1 : 0); this.AddListBinaryOp(this.Less, (x, y) => x.LessThan(y) ? 1 : 0); this.AddListBinaryOp(this.GreaterThanOrEquals, (x, y) => x.GreaterThanOrEquals(y) ? 1 : 0); this.AddListBinaryOp(this.LessThanOrEquals, (x, y) => x.LessThanOrEquals(y) ? 1 : 0); this.AddListBinaryOp(this.NotEquals, (x, y) => !x.Equals(y) ? 1 : 0); this.AddListBinaryOp (this.And, (x, y) => x.Count > 0 && y.Count > 0 ? 1 : 0); this.AddListBinaryOp (this.Or, (x, y) => x.Count > 0 || y.Count > 0 ? 1 : 0); this.AddListUnaryOp(this.Not, (x) => x.Count == 0 ? 1 : 0); this.AddListUnaryOp(this.Invert, (x) => x.inverse); this.AddListUnaryOp(this.All, (x) => x.all); this.AddListUnaryOp(this.ListMin, (x) => x.MinAsList()); this.AddListUnaryOp(this.ListMax, (x) => x.MaxAsList()); this.AddListUnaryOp(this.Count, (x) => x.Count); this.AddListUnaryOp(this.ValueOfList, (x) => x.maxItem.Value); let divertTargetsEqual = (d1: Path, d2: Path) => { return d1.Equals(d2) ? 1 : 0; }; let divertTargetsNotEqual = (d1: Path, d2: Path) => { return d1.Equals (d2) ? 0 : 1; }; this.AddOpToNativeFunc(this.Equal, 2, ValueType.DivertTarget, divertTargetsEqual); this.AddOpToNativeFunc(this.NotEquals, 2, ValueType.DivertTarget, divertTargetsNotEqual); } } public AddOpFuncForType(valType: ValueType, op: UnaryOp | BinaryOp): void{ if (this._operationFuncs == null) { this._operationFuncs = new Map(); } this._operationFuncs.set(valType, op); } public static AddOpToNativeFunc(name: string, args: number, valType: ValueType, op: UnaryOp | BinaryOp): void{ if (this._nativeFunctions === null) return throwNullException('NativeFunctionCall._nativeFunctions'); let nativeFunc = this._nativeFunctions.get(name); if (!nativeFunc) { nativeFunc = new NativeFunctionCall(name, args); this._nativeFunctions.set(name, nativeFunc); } nativeFunc.AddOpFuncForType(valType, op); } public static AddIntBinaryOp(name: string, op: BinaryOp){ this.AddOpToNativeFunc(name, 2, ValueType.Int, op); } public static AddIntUnaryOp(name: string, op: UnaryOp){ this.AddOpToNativeFunc(name, 1, ValueType.Int, op); } public static AddFloatBinaryOp(name: string, op: BinaryOp){ this.AddOpToNativeFunc(name, 2, ValueType.Float, op); } public static AddFloatUnaryOp(name: string, op: UnaryOp){ this.AddOpToNativeFunc(name, 1, ValueType.Float, op); } public static AddStringBinaryOp(name: string, op: BinaryOp){ this.AddOpToNativeFunc(name, 2, ValueType.String, op); } public static AddListBinaryOp(name: string, op: BinaryOp){ this.AddOpToNativeFunc(name, 2, ValueType.List, op); } public static AddListUnaryOp(name: string, op: UnaryOp){ this.AddOpToNativeFunc(name, 1, ValueType.List, op); } public toString(){ return 'Native "' + this.name + '"'; } public _prototype: NativeFunctionCall | null = null; public _isPrototype: boolean = false; public _operationFuncs: Map | UnaryOp> | null = null; public static _nativeFunctions: Map | null = null; }