import { typeToString, NumberType, StringType, BooleanType, ColorType, ObjectType, ValueType, ErrorType, CollatorType, array } from './types'; import {ParsingContext} from './parsing_context'; import {EvaluationContext} from './evaluation_context'; import {expressions} from './definitions'; import {CollatorExpression} from './definitions/collator'; import {Within} from './definitions/within'; import {Literal} from './definitions/literal'; import {Assertion} from './definitions/assertion'; import {Coercion} from './definitions/coercion'; import {Var} from './definitions/var'; import {Distance} from './definitions/distance'; import {GlobalState} from './definitions/global_state'; import type {Expression, ExpressionRegistry} from './expression'; import type {Value} from './values'; import type {Type} from './types'; import {typeOf, validateRGBA, valueToString} from './values'; import {RuntimeError} from './runtime_error'; import {Color} from './types/color'; export type Varargs = { type: Type; }; type Signature = Array | Varargs; type Evaluate = (b: EvaluationContext, a: Array) => Value; type Definition = | [Type, Signature, Evaluate] | { type: Type; overloads: Array<[Signature, Evaluate]>; }; export class CompoundExpression implements Expression { name: string; type: Type; _evaluate: Evaluate; args: Array; static definitions: {[_: string]: Definition}; constructor(name: string, type: Type, evaluate: Evaluate, args: Array) { this.name = name; this.type = type; this._evaluate = evaluate; this.args = args; } evaluate(ctx: EvaluationContext) { return this._evaluate(ctx, this.args); } eachChild(fn: (_: Expression) => void) { this.args.forEach(fn); } outputDefined() { return false; } static parse(args: ReadonlyArray, context: ParsingContext): Expression { const op: string = args[0] as any; const definition = CompoundExpression.definitions[op]; if (!definition) { return context.error( `Unknown expression "${op}". If you wanted a literal array, use ["literal", [...]].`, 0 ) as null; } // Now check argument types against each signature const type = Array.isArray(definition) ? definition[0] : definition.type; const availableOverloads = Array.isArray(definition) ? [[definition[1], definition[2]]] : definition.overloads; const overloads = availableOverloads.filter( ([signature]) => !Array.isArray(signature) || // varags signature.length === args.length - 1 // correct param count ); let signatureContext: ParsingContext = null; for (const [params, evaluate] of overloads) { // Use a fresh context for each attempted signature so that, if // we eventually succeed, we haven't polluted `context.errors`. signatureContext = new ParsingContext( context.registry, isExpressionConstant, context.path, null, context.scope ); // First parse all the args, potentially coercing to the // types expected by this overload. const parsedArgs: Array = []; let argParseFailed = false; for (let i = 1; i < args.length; i++) { const arg = args[i]; const expectedType = Array.isArray(params) ? params[i - 1] : (params as Varargs).type; const parsed = signatureContext.parse(arg, 1 + parsedArgs.length, expectedType); if (!parsed) { argParseFailed = true; break; } parsedArgs.push(parsed); } if (argParseFailed) { // Couldn't coerce args of this overload to expected type, move // on to next one. continue; } if (Array.isArray(params)) { if (params.length !== parsedArgs.length) { signatureContext.error( `Expected ${params.length} arguments, but found ${parsedArgs.length} instead.` ); continue; } } for (let i = 0; i < parsedArgs.length; i++) { const expected = Array.isArray(params) ? params[i] : (params as Varargs).type; const arg = parsedArgs[i]; signatureContext.concat(i + 1).checkSubtype(expected, arg.type); } if (signatureContext.errors.length === 0) { return new CompoundExpression(op, type, evaluate as Evaluate, parsedArgs); } } if (overloads.length === 1) { context.errors.push(...signatureContext.errors); } else { const expected = overloads.length ? overloads : availableOverloads; const signatures = expected .map(([params]) => stringifySignature(params as Signature)) .join(' | '); const actualTypes = []; // For error message, re-parse arguments without trying to // apply any coercions for (let i = 1; i < args.length; i++) { const parsed = context.parse(args[i], 1 + actualTypes.length); if (!parsed) return null; actualTypes.push(typeToString(parsed.type)); } context.error( `Expected arguments of type ${signatures}, but found (${actualTypes.join(', ')}) instead.` ); } return null; } static register(registry: ExpressionRegistry, definitions: {[_: string]: Definition}) { CompoundExpression.definitions = definitions; for (const name in definitions) { registry[name] = CompoundExpression; } } } function rgba(ctx, [r, g, b, a]) { r = r.evaluate(ctx); g = g.evaluate(ctx); b = b.evaluate(ctx); const alpha = a ? a.evaluate(ctx) : 1; const error = validateRGBA(r, g, b, alpha); if (error) throw new RuntimeError(error); return new Color(r / 255, g / 255, b / 255, alpha, false); } function has(key, obj) { return key in obj; } function get(key, obj) { const v = obj[key]; return typeof v === 'undefined' ? null : v; } function binarySearch(v, a, i, j) { while (i <= j) { const m = (i + j) >> 1; if (a[m] === v) return true; if (a[m] > v) j = m - 1; else i = m + 1; } return false; } function varargs(type: Type): Varargs { return {type}; } CompoundExpression.register(expressions, { error: [ ErrorType, [StringType], (ctx, [v]) => { throw new RuntimeError(v.evaluate(ctx)); } ], typeof: [StringType, [ValueType], (ctx, [v]) => typeToString(typeOf(v.evaluate(ctx)))], 'to-rgba': [ array(NumberType, 4), [ColorType], (ctx, [v]) => { const [r, g, b, a] = v.evaluate(ctx).rgb; return [r * 255, g * 255, b * 255, a]; } ], rgb: [ColorType, [NumberType, NumberType, NumberType], rgba], rgba: [ColorType, [NumberType, NumberType, NumberType, NumberType], rgba], has: { type: BooleanType, overloads: [ [[StringType], (ctx, [key]) => has(key.evaluate(ctx), ctx.properties())], [ [StringType, ObjectType], (ctx, [key, obj]) => has(key.evaluate(ctx), obj.evaluate(ctx)) ] ] }, get: { type: ValueType, overloads: [ [[StringType], (ctx, [key]) => get(key.evaluate(ctx), ctx.properties())], [ [StringType, ObjectType], (ctx, [key, obj]) => get(key.evaluate(ctx), obj.evaluate(ctx)) ] ] }, 'feature-state': [ ValueType, [StringType], (ctx, [key]) => get(key.evaluate(ctx), ctx.featureState || {}) ], properties: [ObjectType, [], (ctx) => ctx.properties()], 'geometry-type': [StringType, [], (ctx) => ctx.geometryType()], id: [ValueType, [], (ctx) => ctx.id()], zoom: [NumberType, [], (ctx) => ctx.globals.zoom], 'heatmap-density': [NumberType, [], (ctx) => ctx.globals.heatmapDensity || 0], elevation: [NumberType, [], (ctx) => ctx.globals.elevation || 0], 'line-progress': [NumberType, [], (ctx) => ctx.globals.lineProgress || 0], accumulated: [ ValueType, [], (ctx) => (ctx.globals.accumulated === undefined ? null : ctx.globals.accumulated) ], '+': [ NumberType, varargs(NumberType), (ctx, args) => { let result = 0; for (const arg of args) { result += arg.evaluate(ctx); } return result; } ], '*': [ NumberType, varargs(NumberType), (ctx, args) => { let result = 1; for (const arg of args) { result *= arg.evaluate(ctx); } return result; } ], '-': { type: NumberType, overloads: [ [[NumberType, NumberType], (ctx, [a, b]) => a.evaluate(ctx) - b.evaluate(ctx)], [[NumberType], (ctx, [a]) => -a.evaluate(ctx)] ] }, '/': [NumberType, [NumberType, NumberType], (ctx, [a, b]) => a.evaluate(ctx) / b.evaluate(ctx)], '%': [NumberType, [NumberType, NumberType], (ctx, [a, b]) => a.evaluate(ctx) % b.evaluate(ctx)], ln2: [NumberType, [], () => Math.LN2], pi: [NumberType, [], () => Math.PI], e: [NumberType, [], () => Math.E], '^': [ NumberType, [NumberType, NumberType], (ctx, [b, e]) => Math.pow(b.evaluate(ctx), e.evaluate(ctx)) ], sqrt: [NumberType, [NumberType], (ctx, [x]) => Math.sqrt(x.evaluate(ctx))], log10: [NumberType, [NumberType], (ctx, [n]) => Math.log(n.evaluate(ctx)) / Math.LN10], ln: [NumberType, [NumberType], (ctx, [n]) => Math.log(n.evaluate(ctx))], log2: [NumberType, [NumberType], (ctx, [n]) => Math.log(n.evaluate(ctx)) / Math.LN2], sin: [NumberType, [NumberType], (ctx, [n]) => Math.sin(n.evaluate(ctx))], cos: [NumberType, [NumberType], (ctx, [n]) => Math.cos(n.evaluate(ctx))], tan: [NumberType, [NumberType], (ctx, [n]) => Math.tan(n.evaluate(ctx))], asin: [NumberType, [NumberType], (ctx, [n]) => Math.asin(n.evaluate(ctx))], acos: [NumberType, [NumberType], (ctx, [n]) => Math.acos(n.evaluate(ctx))], atan: [NumberType, [NumberType], (ctx, [n]) => Math.atan(n.evaluate(ctx))], min: [ NumberType, varargs(NumberType), (ctx, args) => Math.min(...args.map((arg) => arg.evaluate(ctx))) ], max: [ NumberType, varargs(NumberType), (ctx, args) => Math.max(...args.map((arg) => arg.evaluate(ctx))) ], abs: [NumberType, [NumberType], (ctx, [n]) => Math.abs(n.evaluate(ctx))], round: [ NumberType, [NumberType], (ctx, [n]) => { const v = n.evaluate(ctx); // Javascript's Math.round() rounds towards +Infinity for halfway // values, even when they're negative. It's more common to round // away from 0 (e.g., this is what python and C++ do) return v < 0 ? -Math.round(-v) : Math.round(v); } ], floor: [NumberType, [NumberType], (ctx, [n]) => Math.floor(n.evaluate(ctx))], ceil: [NumberType, [NumberType], (ctx, [n]) => Math.ceil(n.evaluate(ctx))], 'filter-==': [ BooleanType, [StringType, ValueType], (ctx, [k, v]) => ctx.properties()[(k as any).value] === (v as any).value ], 'filter-id-==': [BooleanType, [ValueType], (ctx, [v]) => ctx.id() === (v as any).value], 'filter-type-==': [ BooleanType, [StringType], (ctx, [v]) => ctx.geometryType() === (v as any).value ], 'filter-<': [ BooleanType, [StringType, ValueType], (ctx, [k, v]) => { const a = ctx.properties()[(k as any).value]; const b = (v as any).value; return typeof a === typeof b && a < b; } ], 'filter-id-<': [ BooleanType, [ValueType], (ctx, [v]) => { const a = ctx.id(); const b = (v as any).value; return typeof a === typeof b && a < b; } ], 'filter->': [ BooleanType, [StringType, ValueType], (ctx, [k, v]) => { const a = ctx.properties()[(k as any).value]; const b = (v as any).value; return typeof a === typeof b && a > b; } ], 'filter-id->': [ BooleanType, [ValueType], (ctx, [v]) => { const a = ctx.id(); const b = (v as any).value; return typeof a === typeof b && a > b; } ], 'filter-<=': [ BooleanType, [StringType, ValueType], (ctx, [k, v]) => { const a = ctx.properties()[(k as any).value]; const b = (v as any).value; return typeof a === typeof b && a <= b; } ], 'filter-id-<=': [ BooleanType, [ValueType], (ctx, [v]) => { const a = ctx.id(); const b = (v as any).value; return typeof a === typeof b && a <= b; } ], 'filter->=': [ BooleanType, [StringType, ValueType], (ctx, [k, v]) => { const a = ctx.properties()[(k as any).value]; const b = (v as any).value; return typeof a === typeof b && a >= b; } ], 'filter-id->=': [ BooleanType, [ValueType], (ctx, [v]) => { const a = ctx.id(); const b = (v as any).value; return typeof a === typeof b && a >= b; } ], 'filter-has': [BooleanType, [ValueType], (ctx, [k]) => (k as any).value in ctx.properties()], 'filter-has-id': [BooleanType, [], (ctx) => ctx.id() !== null && ctx.id() !== undefined], 'filter-type-in': [ BooleanType, [array(StringType)], (ctx, [v]) => (v as any).value.indexOf(ctx.geometryType()) >= 0 ], 'filter-id-in': [ BooleanType, [array(ValueType)], (ctx, [v]) => (v as any).value.indexOf(ctx.id()) >= 0 ], 'filter-in-small': [ BooleanType, [StringType, array(ValueType)], // assumes v is an array literal (ctx, [k, v]) => (v as any).value.indexOf(ctx.properties()[(k as any).value]) >= 0 ], 'filter-in-large': [ BooleanType, [StringType, array(ValueType)], // assumes v is a array literal with values sorted in ascending order and of a single type (ctx, [k, v]) => binarySearch( ctx.properties()[(k as any).value], (v as any).value, 0, (v as any).value.length - 1 ) ], all: { type: BooleanType, overloads: [ [[BooleanType, BooleanType], (ctx, [a, b]) => a.evaluate(ctx) && b.evaluate(ctx)], [ varargs(BooleanType), (ctx, args) => { for (const arg of args) { if (!arg.evaluate(ctx)) return false; } return true; } ] ] }, any: { type: BooleanType, overloads: [ [[BooleanType, BooleanType], (ctx, [a, b]) => a.evaluate(ctx) || b.evaluate(ctx)], [ varargs(BooleanType), (ctx, args) => { for (const arg of args) { if (arg.evaluate(ctx)) return true; } return false; } ] ] }, '!': [BooleanType, [BooleanType], (ctx, [b]) => !b.evaluate(ctx)], 'is-supported-script': [ BooleanType, [StringType], // At parse time this will always return true, so we need to exclude this expression with isGlobalPropertyConstant (ctx, [s]) => { const isSupportedScript = ctx.globals && ctx.globals.isSupportedScript; if (isSupportedScript) { return isSupportedScript(s.evaluate(ctx)); } return true; } ], upcase: [StringType, [StringType], (ctx, [s]) => s.evaluate(ctx).toUpperCase()], downcase: [StringType, [StringType], (ctx, [s]) => s.evaluate(ctx).toLowerCase()], concat: [ StringType, varargs(ValueType), (ctx, args) => args.map((arg) => valueToString(arg.evaluate(ctx))).join('') ], split: [ array(StringType), [StringType, StringType], (ctx, [s, delim]) => s.evaluate(ctx).split(delim.evaluate(ctx)) ], join: [ StringType, [array(StringType), StringType], (ctx, [arr, delim]) => arr.evaluate(ctx).join(delim.evaluate(ctx)) ], 'resolved-locale': [ StringType, [CollatorType], (ctx, [collator]) => collator.evaluate(ctx).resolvedLocale() ] }); function stringifySignature(signature: Signature): string { if (Array.isArray(signature)) { return `(${signature.map(typeToString).join(', ')})`; } else { return `(${typeToString(signature.type)}...)`; } } function isExpressionConstant(expression: Expression) { if (expression instanceof Var) { return isExpressionConstant(expression.boundExpression); } else if (expression instanceof CompoundExpression && expression.name === 'error') { return false; } else if (expression instanceof CollatorExpression) { // Although the results of a Collator expression with fixed arguments // generally shouldn't change between executions, we can't serialize them // as constant expressions because results change based on environment. return false; } else if (expression instanceof Within) { return false; } else if (expression instanceof Distance) { return false; } else if (expression instanceof GlobalState) { return false; } const isTypeAnnotation = expression instanceof Coercion || expression instanceof Assertion; let childrenConstant = true; expression.eachChild((child) => { // We can _almost_ assume that if `expressions` children are constant, // they would already have been evaluated to Literal values when they // were parsed. Type annotations are the exception, because they might // have been inferred and added after a child was parsed. // So we recurse into isConstant() for the children of type annotations, // but otherwise simply check whether they are Literals. if (isTypeAnnotation) { childrenConstant = childrenConstant && isExpressionConstant(child); } else { childrenConstant = childrenConstant && child instanceof Literal; } }); if (!childrenConstant) { return false; } return ( isFeatureConstant(expression) && isGlobalPropertyConstant(expression, [ 'zoom', 'heatmap-density', 'elevation', 'line-progress', 'accumulated', 'is-supported-script' ]) ); } function isFeatureConstant(e: Expression) { if (e instanceof CompoundExpression) { if (e.name === 'get' && e.args.length === 1) { return false; } else if (e.name === 'feature-state') { return false; } else if (e.name === 'has' && e.args.length === 1) { return false; } else if (e.name === 'properties' || e.name === 'geometry-type' || e.name === 'id') { return false; } else if (/^filter-/.test(e.name)) { return false; } } if (e instanceof Within) { return false; } if (e instanceof Distance) { return false; } let result = true; e.eachChild((arg) => { if (result && !isFeatureConstant(arg)) { result = false; } }); return result; } function isStateConstant(e: Expression) { if (e instanceof CompoundExpression) { if (e.name === 'feature-state') { return false; } } let result = true; e.eachChild((arg) => { if (result && !isStateConstant(arg)) { result = false; } }); return result; } function isGlobalPropertyConstant(e: Expression, properties: Array) { if (e instanceof CompoundExpression && properties.indexOf(e.name) >= 0) { return false; } let result = true; e.eachChild((arg) => { if (result && !isGlobalPropertyConstant(arg, properties)) { result = false; } }); return result; } export {isFeatureConstant, isGlobalPropertyConstant, isStateConstant, isExpressionConstant};