import {toString, ValueType, BooleanType, CollatorType} from '../types'; import Assertion from './assertion'; import {typeOf} from '../values'; import RuntimeError from '../runtime_error'; import type {Expression} from '../expression'; import type EvaluationContext from '../evaluation_context'; import type ParsingContext from '../parsing_context'; import type {Type} from '../types'; type ComparisonOperator = '==' | '!=' | '<' | '>' | '<=' | '>='; function isComparableType(op: ComparisonOperator, type: Type) { if (op === '==' || op === '!=') { // equality operator return type.kind === 'boolean' || type.kind === 'string' || type.kind === 'number' || type.kind === 'null' || type.kind === 'value'; } else { // ordering operator return type.kind === 'string' || type.kind === 'number' || type.kind === 'value'; } } function eq(ctx, a, b) { return a === b; } function neq(ctx, a, b) { return a !== b; } function lt(ctx, a, b) { return a < b; } function gt(ctx, a, b) { return a > b; } function lteq(ctx, a, b) { return a <= b; } function gteq(ctx, a, b) { return a >= b; } function eqCollate(ctx, a, b, c) { return c.compare(a, b) === 0; } function neqCollate(ctx, a, b, c) { return !eqCollate(ctx, a, b, c); } function ltCollate(ctx, a, b, c) { return c.compare(a, b) < 0; } function gtCollate(ctx, a, b, c) { return c.compare(a, b) > 0; } function lteqCollate(ctx, a, b, c) { return c.compare(a, b) <= 0; } function gteqCollate(ctx, a, b, c) { return c.compare(a, b) >= 0; } /** * Special form for comparison operators, implementing the signatures: * - (T, T, ?Collator) => boolean * - (T, value, ?Collator) => boolean * - (value, T, ?Collator) => boolean * * For inequalities, T must be either value, string, or number. For ==/!=, it * can also be boolean or null. * * Equality semantics are equivalent to Javascript's strict equality (===/!==) * -- i.e., when the arguments' types don't match, == evaluates to false, != to * true. * * When types don't match in an ordering comparison, a runtime error is thrown. * * @private */ function makeComparison(op: ComparisonOperator, compareBasic, compareWithCollator) { const isOrderComparison = op !== '==' && op !== '!='; return class Comparison implements Expression { type: Type; lhs: Expression; rhs: Expression; collator: Expression; hasUntypedArgument: boolean; constructor(lhs: Expression, rhs: Expression, collator?: Expression | null) { this.type = BooleanType; this.lhs = lhs; this.rhs = rhs; this.collator = collator; this.hasUntypedArgument = lhs.type.kind === 'value' || rhs.type.kind === 'value'; } static parse(args: ReadonlyArray, context: ParsingContext): Expression { if (args.length !== 3 && args.length !== 4) return context.error('Expected two or three arguments.') as null; const op: ComparisonOperator = (args[0] as any); let lhs = context.parse(args[1], 1, ValueType); if (!lhs) return null; if (!isComparableType(op, lhs.type)) { return context.concat(1).error(`"${op}" comparisons are not supported for type '${toString(lhs.type)}'.`) as null; } let rhs = context.parse(args[2], 2, ValueType); if (!rhs) return null; if (!isComparableType(op, rhs.type)) { return context.concat(2).error(`"${op}" comparisons are not supported for type '${toString(rhs.type)}'.`) as null; } if ( lhs.type.kind !== rhs.type.kind && lhs.type.kind !== 'value' && rhs.type.kind !== 'value' ) { return context.error(`Cannot compare types '${toString(lhs.type)}' and '${toString(rhs.type)}'.`) as null; } if (isOrderComparison) { // typing rules specific to less/greater than operators if (lhs.type.kind === 'value' && rhs.type.kind !== 'value') { // (value, T) lhs = new Assertion(rhs.type, [lhs]); } else if (lhs.type.kind !== 'value' && rhs.type.kind === 'value') { // (T, value) rhs = new Assertion(lhs.type, [rhs]); } } let collator = null; if (args.length === 4) { if ( lhs.type.kind !== 'string' && rhs.type.kind !== 'string' && lhs.type.kind !== 'value' && rhs.type.kind !== 'value' ) { return context.error('Cannot use collator to compare non-string types.') as null; } collator = context.parse(args[3], 3, CollatorType); if (!collator) return null; } return new Comparison(lhs, rhs, collator); } evaluate(ctx: EvaluationContext) { const lhs = this.lhs.evaluate(ctx); const rhs = this.rhs.evaluate(ctx); if (isOrderComparison && this.hasUntypedArgument) { const lt = typeOf(lhs); const rt = typeOf(rhs); // check that type is string or number, and equal if (lt.kind !== rt.kind || !(lt.kind === 'string' || lt.kind === 'number')) { throw new RuntimeError(`Expected arguments for "${op}" to be (string, string) or (number, number), but found (${lt.kind}, ${rt.kind}) instead.`); } } if (this.collator && !isOrderComparison && this.hasUntypedArgument) { const lt = typeOf(lhs); const rt = typeOf(rhs); if (lt.kind !== 'string' || rt.kind !== 'string') { return compareBasic(ctx, lhs, rhs); } } return this.collator ? compareWithCollator(ctx, lhs, rhs, this.collator.evaluate(ctx)) : compareBasic(ctx, lhs, rhs); } eachChild(fn: (_: Expression) => void) { fn(this.lhs); fn(this.rhs); if (this.collator) { fn(this.collator); } } outputDefined(): boolean { return true; } }; } export const Equals = makeComparison('==', eq, eqCollate); export const NotEquals = makeComparison('!=', neq, neqCollate); export const LessThan = makeComparison('<', lt, ltCollate); export const GreaterThan = makeComparison('>', gt, gtCollate); export const LessThanOrEqual = makeComparison('<=', lteq, lteqCollate); export const GreaterThanOrEqual = makeComparison('>=', gteq, gteqCollate);