diff --git a/src/dataflow/eval/eval.ts b/src/dataflow/eval/eval.ts new file mode 100644 index 00000000000..f4511ecfaf0 --- /dev/null +++ b/src/dataflow/eval/eval.ts @@ -0,0 +1,82 @@ +import type { RNode } from '../../r-bridge/lang-4.x/ast/model/model'; +import type { DataflowGraph } from '../graph/graph'; +import type { REnvironmentInformation } from '../environments/environment'; +import type { Value } from './values/r-value'; +import { Top } from './values/r-value'; +import { intervalFrom } from './values/intervals/interval-constants'; +import type { ParentInformation } from '../../r-bridge/lang-4.x/ast/model/processing/decorate'; +import { stringFrom } from './values/string/string-constants'; +import { liftLogical } from './values/logical/logical-constants'; +import { VertexType } from '../graph/vertex'; +import { resolveValueOfVariable } from '../environments/resolve-by-name'; +import { RType } from '../../r-bridge/lang-4.x/ast/model/type'; +import { DefaultValueFunctionEvaluator } from './values/functions/functions-value'; +import { log } from '../../util/log'; +import { EmptyArgument } from '../../r-bridge/lang-4.x/ast/model/nodes/r-function-call'; +import { Missing } from './values/missing/missing-constants'; +import { valuesFromTsValuesAsSet } from './values/general'; +import { ValueVectorTop } from './values/vectors/vector-constants'; + +export const ValueEvalLog = log.getSubLogger({ + name: 'value-eval' +}); + +/** + * Evaluates the given subtree using its dataflow graph and the current environment. + * + * TODO: this expects an expression, currently we do not handle in-expression side-effects + * TODO: a function with unknown side effects should make everything top + */ +export function evalRExpression(n: RNode | typeof EmptyArgument | undefined, dfg: DataflowGraph, env: REnvironmentInformation): Value { + if(n === undefined || n === EmptyArgument) { + return Missing; + } + ValueEvalLog.silly('Eval' + n.type + ' (' + n.lexeme + ')'); + // TODO: evaluation symbol tracker environment and only access the other environment if we do not know the value + switch(n.type) { + case RType.ExpressionList: + if(n.grouping) { + return callFn(n.grouping[0].lexeme, n.children, dfg, env) ?? Top; + } else { + return callFn('{', n.children, dfg, env) ?? Top; + } + case RType.Number: + return intervalFrom(n.content, n.content); + case RType.String: + return stringFrom(n.content); + case RType.Logical: + return liftLogical(n.content); + case RType.Symbol: { + const t = dfg.getVertex(n.info.id); + if(t?.tag === VertexType.Use) { + return valuesFromTsValuesAsSet(resolveValueOfVariable(n.content, env, dfg.idMap)); + } + return ValueVectorTop; + } + case RType.BinaryOp: + return callFn(n.operator, [n.lhs, n.rhs], dfg, env) ?? Top; + case RType.UnaryOp: + return callFn(n.operator, [n.operand], dfg, env) ?? Top; + case RType.FunctionCall: + // TODO: ap function arguments accordingly + if(n.named) { + return callFn(n.functionName.lexeme, n.arguments, dfg, env) ?? Top; + } else { + ValueEvalLog.silly('Anonymous function call'); + return Top; + } + case RType.Argument: + return evalRExpression(n.value, dfg, env); + } + ValueEvalLog.silly('No handler for ' + n.type); + return Top; +} + + +function callFn(name: string, args: readonly (RNode | typeof EmptyArgument)[], dfg: DataflowGraph, env: REnvironmentInformation): Value | undefined { + // TODO: check if not overriden etc. + return DefaultValueFunctionEvaluator.callFunction(name, args.map(a => + /* TODO: lazy? */ + [a === EmptyArgument ? undefined : a.name as string | undefined, evalRExpression(a, dfg, env)] + )); +} \ No newline at end of file diff --git a/src/dataflow/eval/values/functions/functions-value.ts b/src/dataflow/eval/values/functions/functions-value.ts new file mode 100644 index 00000000000..4ab1ceb03f2 --- /dev/null +++ b/src/dataflow/eval/values/functions/functions-value.ts @@ -0,0 +1,270 @@ +import type { ValueArgument, ValueFunctionDescription } from './value-function'; +import { getArgument , stringifyValueArgument , isOfEitherType } from './value-function'; +import type { Value, ValueSet } from '../r-value'; +import { isBottom, isTop , stringifyValue , Top } from '../r-value'; +import { unaryValue } from '../value-unary'; +import { binaryValue } from '../value-binary'; +import { ValueIntervalMinusOneToOne, ValueIntervalZeroToPositiveInfinity } from '../intervals/interval-constants'; +import { expensiveTrace, LogLevel } from '../../../../util/log'; +import { ValueEvalLog } from '../../eval'; +import { setFrom, ValueSetTop } from '../sets/set-constants'; +import { flattenVector, ValueVectorTop, vectorFrom } from '../vectors/vector-constants'; +import { ValueIntegerOne, ValueIntegerZero } from '../scalar/scalar-constants'; +import { stringFrom, ValueStringBot, ValueStringTop } from '../string/string-constants'; + +class FunctionEvaluator { + private functionProviders: Map = new Map(); + + /** + * Register an evaluator with the given description. + */ + public registerFunction(name: string, description: ValueFunctionDescription): void { + if(!this.functionProviders.has(name)) { + this.functionProviders.set(name, []); + } + (this.functionProviders.get(name) as ValueFunctionDescription[]).push(description); + } + + public registerFunctions(names: readonly string[], descriptions: ValueFunctionDescription): void { + names.forEach(name => this.registerFunction(name, descriptions)); + } + + + public callFunction(name: string, args: readonly ValueArgument[]): ValueSet { + expensiveTrace(ValueEvalLog, () => ` * callFunction(${name}, ${args.map(a => stringifyValueArgument(a)).join(', ')})`); + const providers = this.functionProviders.get(name); + if(providers === undefined) { + ValueEvalLog.trace(`No function providers for ${name}`); + return ValueSetTop; + } + const activeProviders = providers.filter(p => p.canApply(args, name)); + + if(ValueEvalLog.settings.minLevel <= LogLevel.Trace) { + ValueEvalLog.trace(`* Active providers for ${name}: ${activeProviders.length}`); + activeProviders.forEach(p => ValueEvalLog.trace(` - ${p.description}`)); + } + + const results = setFrom(...activeProviders.map(p => setFrom(p.apply(args, name)))); + expensiveTrace(ValueEvalLog, () => ` => callFunction(${name}, ${args.map(a => stringifyValueArgument(a)).join(', ')}) = ${stringifyValue(results)}`); + return results; + } +} + +export const DefaultValueFunctionEvaluator = new FunctionEvaluator(); + +const dvfe = DefaultValueFunctionEvaluator; + +function isNumericType(v: Value): boolean { + return isOfEitherType(v, 'number', 'interval', 'vector', 'set', Top.type); +} + +function isStringType(v: Value): boolean { + return isOfEitherType(v, 'string', 'set', Top.type); +} + +dvfe.registerFunctions(['id', 'force'], { + description: 'Simply return a single argument', + canApply: args => args.length === 1, + apply: args => args[0][1] +}); + +dvfe.registerFunction('-', { + description: '-a', + canApply: args => args.length === 1 && isNumericType(args[0][1]), + apply: ([arg]) => { + return unaryValue(arg[1], 'negate'); + } +}); + +// maybe use intersect for clamps +dvfe.registerFunction('abs', { + description: 'abs(a)', + canApply: args => args.length === 1 && isNumericType(getArgument(args, { position: 0, name: 'x' })), + apply: args => { + return binaryValue(unaryValue(getArgument(args, { position: 0, name: 'x' }), 'abs'), 'intersect', ValueIntervalZeroToPositiveInfinity); + } +}); + +dvfe.registerFunction('ceiling', { + description: 'ceiling(a)', + canApply: args => args.length === 1 && isNumericType(getArgument(args, { position: 0, name: 'x' })), + apply: (args) => { + return unaryValue(getArgument(args, { position: 0, name: 'x' }), 'ceil'); + } +}); + +dvfe.registerFunction('floor', { + description: 'floor(a)', + canApply: args => args.length === 1 && isNumericType(getArgument(args, { position: 0, name: 'x' })), + apply: (args) => { + return unaryValue(getArgument(args, { position: 0, name: 'x' }), 'floor'); + } +}); + +// TODO: support digits +dvfe.registerFunction('round', { + description: 'round(a)', + canApply: args => args.length === 1 && isNumericType(getArgument(args, { position: 0, name: 'x' })), + apply: (args) => { + return unaryValue(getArgument(args, { position: 0, name: 'x' }), 'round'); + } +}); + +dvfe.registerFunction('sign', { + description: 'sign(a)', + canApply: args => args.length === 1 && isNumericType(getArgument(args, { position: 0, name: 'x' })), + apply: (args) => { + return binaryValue(unaryValue(getArgument(args, { position: 0, name: 'x' }), 'sign'), 'intersect', ValueIntervalMinusOneToOne); + } +}); + +dvfe.registerFunction('add', { + description: 'a + b', + canApply: args => args.length === 2 && isNumericType(args[0][1]) && isNumericType(args[1][1]), + apply: ([a, b]) => { + return binaryValue(a[1], 'add', b[1]); + } +}); + +dvfe.registerFunction('sub', { + description: 'a - b', + canApply: args => args.length === 2 && isNumericType(args[0][1]) && isNumericType(args[1][1]), + apply: ([a, b]) => { + return binaryValue(a[1], 'sub', b[1]); + } +}); + +dvfe.registerFunction('mul', { + description: 'a * b', + canApply: args => args.length === 2 && isNumericType(args[0][1]) && isNumericType(args[1][1]), + apply: ([a, b]) => { + return binaryValue(a[1], 'mul', b[1]); + } +}); + +dvfe.registerFunction('div', { + description: 'a / b', + canApply: args => args.length === 2 && isNumericType(args[0][1]) && isNumericType(args[1][1]), + apply: ([a, b]) => { + return binaryValue(a[1], 'div', b[1]); + } +}); + +dvfe.registerFunction('pow', { + description: 'a ^ b', + canApply: args => args.length === 2 && isNumericType(args[0][1]) && isNumericType(args[1][1]), + apply: ([a, b]) => { + return binaryValue(a[1], 'pow', b[1]); + } +}); + +dvfe.registerFunction('mod', { + description: 'a % b', + canApply: args => args.length === 2 && isNumericType(args[0][1]) && isNumericType(args[1][1]), + apply: ([a, b]) => { + return binaryValue(a[1], 'mod', b[1]); + } +}); + +dvfe.registerFunction('max', { + description: 'max(a, b)', + canApply: args => args.every(a => isNumericType(a[1])), + apply: args => { + let result = args[0][1]; + for(let i = 1; i < args.length; i++) { + result = binaryValue(result, 'max', args[i][1]); + } + return result; + } +}); + +dvfe.registerFunction('min', { + description: 'min(a, b)', + canApply: args => args.every(a => isNumericType(a[1])), + apply: args => { + let result = args[0][1]; + for(let i = 1; i < args.length; i++) { + result = binaryValue(result, 'min', args[i][1]); + } + return result; + } +}); + +for(const [op, opname] of [ + ['+', 'add'], ['-', 'sub'], ['*', 'mul'], ['/', 'div'], ['^', 'pow'], ['%%', 'mod'], ['max', 'max'], ['min', 'min'], + ['<=', '<='], ['<', '<'], ['>=', '>='], ['>', '>'], ['==', '=='], ['!=', '!='] +]) { + dvfe.registerFunction(op, { + description: `a ${op} b`, + canApply: args => args.length === 2 && args.every(a => isNumericType(a[1])), + apply: ([larg, rarg]) => { + return binaryValue(larg[1], opname, rarg[1]); + } + }); +} + + +dvfe.registerFunction('{', { + description: 'R grouping, {a, b, c, ...}', + canApply: () => true, + apply: args => { + if(args.length === 0) { + return Top; + } + return args[args.length - 1][1]; + } +}); + +dvfe.registerFunction('c', { + description: 'c vector construction', + canApply: () => true, + apply: args => { + return flattenVector(...args.map(a => a[1])); + } +}); + +// TODO: vectorize, TODO: argument +dvfe.registerFunctions(['paste', 'paste0'], { + description: 'paste strings', + // TODO: support vectorization + canApply: args => args.every(a => isStringType(a[1])), + apply: (args, name) => { + const result = []; + for(const arg of args) { + if(isTop(arg[1])) { + return ValueStringTop; + } else if(isBottom(arg[1])) { + return ValueStringBot; + } else if(arg[1].type !== 'string') { + // TODO handle set + return ValueStringTop; + } else if(isTop(arg[1].value)) { + return ValueStringTop; + } else if(isBottom(arg[1].value)) { + return ValueStringBot; + } + result.push(arg[1].value.str); + } + return stringFrom(result.join(name === 'paste0' ? '' : ' ')); + } +}); + +dvfe.registerFunction('runif', { + description: 'R grouping, {a, b, c, ...}', + canApply: args => args.length > 0 && args.length <= 3 && args.every(a => isNumericType(a[1])), + apply: args => { + if(args.length === 0) { + return ValueVectorTop; + } + const min = args.length > 1 ? getArgument(args, { position: 1, name: 'min' }) : ValueIntegerZero; + const max = args.length > 2 ? getArgument(args, { position: 2, name: 'max' }) : ValueIntegerOne; + + const range = binaryValue(max, 'union', min); + + // TODO: support n + return vectorFrom({ + elements: Top, + domain: range + }); + } +}); \ No newline at end of file diff --git a/src/dataflow/eval/values/functions/value-function.ts b/src/dataflow/eval/values/functions/value-function.ts new file mode 100644 index 00000000000..126752db94f --- /dev/null +++ b/src/dataflow/eval/values/functions/value-function.ts @@ -0,0 +1,57 @@ +import type { Value, ValueTypes } from '../r-value'; +import { Top , stringifyValue } from '../r-value'; + + +export interface ValueFunctionDescription { + /** + * A human-readable description of what the function is about to do + */ + readonly description: string; + /** + * A check whether this function overload is applicable to the given arguments + * This must not have any side effects on the values. + * + * @see requiresSignature - a helper function to check the types of the arguments + */ + readonly canApply: (args: readonly ValueArgument[], fname: string) => boolean; + /** + * Apply the function to the given arguments. + * You may assume that `canApply` holds. + */ + readonly apply: (args: readonly ValueArgument[], fname: string) => Value; +} + +export function requiresSignature( + ...types: ValueTypes[] +): (args: readonly Value[]) => boolean { + return args => args.length === types.length && args.every((a, i) => a.type === types[i]); +} + +export function isOfEitherType( + v: Value, + ...orTypes: readonly Value['type'][] +): boolean { + return orTypes.some(t => v.type === t); +} + +export type ValueArgument = [Name, V]; + +export function stringifyValueArgument( + [a, v]: ValueArgument +): string { + return `${a ? a + '=' : ''}${stringifyValue(v)}`; +} + +export function getArgument( + args: readonly ValueArgument[], + from: { + position: number, + name?: string + } +): Value { + if(from.name) { + return args.find(([name]) => name === from.name)?.[1] ?? args[from.position]?.[1] ?? Top; + } else { + return args[from.position]?.[1] ?? Top; + } +} \ No newline at end of file diff --git a/src/dataflow/eval/values/general.ts b/src/dataflow/eval/values/general.ts new file mode 100644 index 00000000000..b4a1afb5bdc --- /dev/null +++ b/src/dataflow/eval/values/general.ts @@ -0,0 +1,42 @@ +import type { Lift, Value } from './r-value'; +import { Bottom, isBottom, isTop, Top } from './r-value'; +import { stringFrom } from './string/string-constants'; +import { intervalFrom } from './intervals/interval-constants'; +import { ValueLogicalFalse, ValueLogicalTrue } from './logical/logical-constants'; + +/** + * Takes n potentially lifted ops and returns `Top` or `Bottom` if any is `Top` or `Bottom`. + */ +export function bottomTopGuard(...a: Lift[]): typeof Top | typeof Bottom | undefined { + if(a.some(isBottom)) { + return Bottom; + } else if(a.some(isTop)) { + return Top; + } +} + +export function valueFromTsValue(a: unknown): Value { + if(a === undefined) { + return Bottom; + } else if(a === null) { + return Top; + } else if(typeof a === 'string') { + return stringFrom(a); + } else if(typeof a === 'number') { + return intervalFrom(a, a); + } else if(typeof a === 'boolean') { + return a ? ValueLogicalTrue : ValueLogicalFalse; + } + + return Top; +} + +export function valuesFromTsValuesAsSet(a: readonly unknown[] | undefined): Value { + if(a === undefined) { + return Top; + } + return { + type: 'set', + elements: a.length === 0 ? Top : a.map(valueFromTsValue), + }; +} \ No newline at end of file diff --git a/src/dataflow/eval/values/intervals/interval-binary.ts b/src/dataflow/eval/values/intervals/interval-binary.ts new file mode 100644 index 00000000000..5b4fe1e5d6d --- /dev/null +++ b/src/dataflow/eval/values/intervals/interval-binary.ts @@ -0,0 +1,307 @@ +import type { Lift, Value, ValueInterval, ValueNumber } from '../r-value'; +import { stringifyValue , Top , isTop , isBottom } from '../r-value'; +import { binaryScalar } from '../scalar/scalar-binary'; +import { + ValueIntervalBottom, + orderIntervalFrom, + ValueIntervalZero, + ValueIntervalTop, + getIntervalEnd, getIntervalStart +} from './interval-constants'; +import { iteLogical } from '../logical/logical-check'; +import { unaryInterval } from './interval-unary'; +import { + liftLogical, + ValueLogicalBot, + ValueLogicalFalse, + ValueLogicalMaybe, + ValueLogicalTrue +} from '../logical/logical-constants'; +import { bottomTopGuard } from '../general'; +import { unaryValue } from '../value-unary'; +import { binaryValue } from '../value-binary'; +import { expensiveTrace } from '../../../../util/log'; +import { ValueEvalLog } from '../../eval'; + +/** + * Take two potentially lifted intervals and combine them with the given op. + * This propagates `top` and `bottom` values. + */ +export function binaryInterval( + a: Lift, + op: string, + b: Lift +): Value { + let res: Value = Top; + if(op in Operations) { + res = Operations[op as keyof typeof Operations](a, b); + } + expensiveTrace(ValueEvalLog, () => ` * binaryInterval(${stringifyValue(a)}, ${op}, ${stringifyValue(b)}) = ${stringifyValue(res)}`); + return res; +} + +// TODO: improve handling of open intervals! +function closeBoth(a: ValueInterval, b: ValueInterval): [ValueInterval, ValueInterval] { + return [unaryInterval(a, 'toClosed') as ValueInterval, unaryInterval(b, 'toClosed') as ValueInterval]; +} + +const Operations = { + add: intervalAdd, + sub: intervalSub, + mul: intervalMul, + div: intervalDiv, + intersect: intervalIntersect, + union: intervalUnion, + setminus: intervalSetminus, + '<': (a, b) => intervalLower(a, b, '<'), + '>': (a, b) => intervalLower(b, a, '<'), + '<=': (a, b) => intervalLower(a, b, '<='), + '>=': (a, b) => intervalLower(b, a, '<='), + '!==': (a, b) => unaryValue(binaryValue(a, '===', b), 'not'), + /** checks if the bounds are identical (structurally) */ + '===': intervalIdentical, + /** checks if the values described by the intervals can be equal */ + '==': intervalEqual, + '!=': (a, b) => unaryValue(binaryValue(a, '==', b), 'not'), + /** structural subset eq comparison! **/ + '⊆': (a, b) => intervalSubset(a, b, '⊆'), + /** structural subset comparison! **/ + '⊂': (a, b) => intervalSubset(a, b, '⊂'), + /** structural superset comparison! **/ + '⊃': (a, b) => intervalSubset(b, a, '⊂'), + /** structural superset eq comparison! **/ + '⊇': (a, b) => intervalSubset(b, a, '⊆'), +} as const satisfies Record, b: Lift) => Value>; + +export type IntervalBinaryOperation = typeof Operations; + +function intervalAdd(a: Lift, b: Lift): Lift { + const bt = bottomTopGuard(a, b); + if(bt) { + return bt === Top ? ValueIntervalTop : ValueIntervalBottom; + } + [a, b] = closeBoth(a as ValueInterval, b as ValueInterval); + return orderIntervalFrom( + binaryScalar(a.start, 'add', b.start) as ValueNumber, + binaryScalar(a.end, 'add', b.end) as ValueNumber, + a.startInclusive && b.startInclusive, + a.endInclusive && b.endInclusive + ); +} + +function intervalSub(a: Lift, b: Lift): Lift { + const bt = bottomTopGuard(a, b); + if(bt) { + return bt === Top ? ValueIntervalTop : ValueIntervalBottom; + } + [a, b] = closeBoth(a as ValueInterval, b as ValueInterval); + return orderIntervalFrom( + binaryScalar(a.start, 'sub', b.end) as ValueNumber, + binaryScalar(a.end, 'sub', b.start) as ValueNumber, + a.startInclusive && b.endInclusive, + a.endInclusive && b.startInclusive + ); +} + +function intervalIntersect(a: Lift, b: Lift): Lift { + if(isBottom(a) || isBottom(b)) { + return ValueIntervalBottom; + } if(isTop(a) && isTop(b)) { + return ValueIntervalTop; + } + [a, b] = closeBoth(a, b); + // if they are top, clamp them by the other as we are either valid or empty + const aStart = isTop(a.start.value) ? b.start : a.start; + const aEnd = isTop(a.end.value) ? b.end : a.end; + const bStart = isTop(b.start.value) ? a.start : b.start; + const bEnd = isTop(b.end.value) ? a.end : b.end; + return orderIntervalFrom( + binaryScalar(aStart, 'max', bStart) as ValueNumber, + binaryScalar(aEnd, 'min', bEnd) as ValueNumber, + a.startInclusive && b.startInclusive, + a.endInclusive && b.endInclusive + ); +} + +function intervalUnion(a: Lift, b: Lift): Lift { + const bt = bottomTopGuard(a, b); + if(bt) { + return bt === Top ? ValueIntervalTop : ValueIntervalBottom; + } + [a, b] = closeBoth(a as ValueInterval, b as ValueInterval); + return orderIntervalFrom( + binaryScalar(a.start, 'min', b.start) as ValueNumber, + binaryScalar(a.end, 'max', b.end) as ValueNumber, + a.startInclusive && b.startInclusive, + a.endInclusive && b.endInclusive + ); +} + + +function intervalSetminus(a: Lift, b: Lift): Lift { + const bt = bottomTopGuard(a, b); + if(bt) { + return bt === Top ? ValueIntervalTop : ValueIntervalBottom; + } + [a, b] = closeBoth(a as ValueInterval, b as ValueInterval); + return orderIntervalFrom( + binaryScalar(a.start, 'max', b.end) as ValueNumber, + binaryScalar(a.end, 'min', b.start) as ValueNumber, + a.startInclusive && b.startInclusive, + a.endInclusive && b.endInclusive + ); +} + +function intervalMul(a: Lift, b: Lift): Lift { + const bt = bottomTopGuard(a, b); + if(bt) { + return bt === Top ? ValueIntervalTop : ValueIntervalBottom; + } + [a, b] = closeBoth(a as ValueInterval, b as ValueInterval); + if(isBottom(a.start.value) || isBottom(b.start.value) || isBottom(a.end.value) || isBottom(b.end.value)) { + return ValueIntervalBottom; + } + + const ll = binaryScalar(a.start, 'mul', b.start); + const lu = binaryScalar(a.start, 'mul', b.end); + const ul = binaryScalar(a.end, 'mul', b.start); + const uu = binaryScalar(a.end, 'mul', b.end); + + return orderIntervalFrom( + [ll, lu, ul, uu].reduce((acc, val) => binaryValue(acc, 'min', val)) as ValueNumber, + [ll, lu, ul, uu].reduce((acc, val) => binaryValue(acc, 'max', val)) as ValueNumber, + a.startInclusive && b.startInclusive, + a.endInclusive && b.endInclusive + ); +} + +// TODO: take support for div sin and other functions that i wrote + +function intervalDiv(a: Lift, b: Lift): Value { + const bt = bottomTopGuard(a, b); + if(bt) { + return bt === Top ? ValueIntervalTop : ValueIntervalBottom; + } + [a, b] = closeBoth(a as ValueInterval, b as ValueInterval); + // if both are zero switch to bot + const bothAreZero = binaryValue( + binaryInterval(a, '===', ValueIntervalZero), + 'and', + binaryInterval(b, '===', ValueIntervalZero)); + + const calcWithPotentialZero = () => + binaryValue(a, 'mul', unaryValue(b, 'flip')); + + return iteLogical( + bothAreZero, + { + onTrue: ValueIntervalBottom, + onMaybe: calcWithPotentialZero, + onFalse: calcWithPotentialZero, + onTop: ValueIntervalTop, + onBottom: ValueIntervalBottom + } + ); +} + +function intervalLower, B extends Lift>(a: A, b: B, op: '<' | '<='): Value { + // if intersect af a and b is non-empty, return maybe + // else return a.end <= b.start + const intersect = binaryInterval(a, 'intersect', b); + + // TODO: < case if one is inclusive and the other isn't + return iteLogical( + unaryValue(intersect, 'empty'), + { + onTrue: () => binaryScalar(getIntervalEnd(a), op, getIntervalStart(b)), + onFalse: ValueLogicalMaybe, + onMaybe: ValueLogicalMaybe, + onTop: ValueLogicalMaybe, + onBottom: ValueLogicalBot + } + ); +} + +function intervalSubset, B extends Lift>(a: A, b: B, op: '⊂' | '⊆'): Value { + if(isBottom(a) || isBottom(b)) { + return ValueLogicalBot; + } + // check if a.start >= b.start and a.end <= b.end (or a.start > b.start || a.end < b.end if not inclusive) + // hence we check for the interval not beign the same in the '⊂' case + + return iteLogical( + binaryValue(getIntervalStart(a), '>=', getIntervalStart(b)), + { + onTrue: () => + iteLogical(binaryValue(getIntervalEnd(a), '<=', getIntervalEnd(b)), { + onTrue: op === '⊂' ? binaryValue(a, '!==', b) : ValueLogicalTrue, + onFalse: ValueLogicalFalse, + onMaybe: ValueLogicalMaybe, + onTop: ValueLogicalMaybe, + onBottom: ValueLogicalBot + }), + onFalse: ValueLogicalFalse, + onMaybe: ValueLogicalMaybe, + onTop: ValueLogicalMaybe, + onBottom: ValueLogicalBot + } + ); +} + +function intervalEqual(a: Lift, b: Lift): Value { + // check if the interval describes a scalar value and if so, check whether the scalar values are equal + // if intersect af a and b is non-empty, return maybe + // else return false because they can never be equal + const intersect = binaryValue(a, 'intersect', b); + + const areBothScalar = () => + iteLogical( + binaryValue(unaryValue(a, 'scalar'), 'and', unaryValue(b, 'scalar')), + { + onTrue: ValueLogicalTrue, // they intersect and they are both scalar + onFalse: ValueLogicalFalse, + onMaybe: ValueLogicalMaybe, + onTop: ValueLogicalMaybe, + onBottom: ValueLogicalBot + } + ); + + return iteLogical( + unaryValue(intersect, 'empty'), + { + onTrue: ValueLogicalFalse, // if they don't intersect, they can never be equal + onFalse: areBothScalar, + onMaybe: ValueLogicalMaybe, + onTop: ValueLogicalMaybe, + onBottom: ValueLogicalBot + } + ); +} + +function intervalIdentical(a: Lift, b: Lift): Value { + // check if start, end, and inclusivity is identical + if( + isTop(a) || isTop(b) || isBottom(a) || isBottom(b) + ) { + return liftLogical( + a === b + ); + } else if( + isTop(a.start.value) || isTop(b.start.value) || isBottom(a.start.value) || isBottom(b.start.value) || + isTop(a.end.value) || isTop(b.end.value) || isBottom(a.end.value) || isBottom(b.end.value) + ) { + return liftLogical( + a.start.value === b.start.value && + a.end.value === b.end.value && + a.startInclusive === b.startInclusive && + a.endInclusive === b.endInclusive + ); + } else { + return (a.startInclusive === b.startInclusive && a.endInclusive === b.endInclusive) ? + binaryValue( + binaryValue(getIntervalStart(a), '===', getIntervalStart(b)), + 'and', + binaryValue(getIntervalEnd(a), '===', getIntervalEnd(b)) + ) : ValueLogicalFalse; + } +} \ No newline at end of file diff --git a/src/dataflow/eval/values/intervals/interval-constants.ts b/src/dataflow/eval/values/intervals/interval-constants.ts new file mode 100644 index 00000000000..f34602f2283 --- /dev/null +++ b/src/dataflow/eval/values/intervals/interval-constants.ts @@ -0,0 +1,70 @@ +import type { RNumberValue } from '../../../../r-bridge/lang-4.x/convert-values'; +import type { Lift, ValueInterval, ValueNumber } from '../r-value'; +import { isBottom, isTop } from '../r-value'; +import { + getScalarFromInteger, + liftScalar, + ValueIntegerBottom, ValueIntegerPositiveInfinity, + ValueIntegerTop, ValueIntegerZero +} from '../scalar/scalar-constants'; +import { iteLogical } from '../logical/logical-check'; +import { binaryScalar } from '../scalar/scalar-binary'; +import { bottomTopGuard } from '../general'; + +export function intervalFrom(start: RNumberValue | number, end = start, startInclusive = true, endInclusive = true): ValueInterval { + return intervalFromValues( + typeof start === 'number' ? getScalarFromInteger(start) : liftScalar(start), + typeof end === 'number' ? getScalarFromInteger(end) : liftScalar(end), + startInclusive, + endInclusive + ); +} + +function shiftNum(v: Lift): ValueNumber { + if(isBottom(v) || isTop(v)) { + return liftScalar(v); + } else { + return v; + } +} + +export function intervalFromValues(start: Lift, end = start, startInclusive = true, endInclusive = true): ValueInterval { + return { + type: 'interval', + start: shiftNum(start), + end: shiftNum(end), + startInclusive, + endInclusive, + }; +} + +export function orderIntervalFrom(start: Lift, end = start, startInclusive = true, endInclusive = true): ValueInterval { + const onTrue = () => intervalFromValues(start, end, startInclusive, endInclusive); + return iteLogical( + binaryScalar(start, '<=', end), + { + onTrue, + onMaybe: onTrue, + onFalse: () => intervalFromValues(end, start, endInclusive, startInclusive), + onTop: ValueIntervalTop, + onBottom: ValueIntervalBottom + } + ); +} + +export function getIntervalStart(interval: Lift): Lift { + return bottomTopGuard(interval) ?? (interval as ValueInterval).start; +} + +export function getIntervalEnd(interval: Lift): Lift { + return bottomTopGuard(interval) ?? (interval as ValueInterval).end; +} + +export const ValueIntervalZero = intervalFrom(0); +export const ValueIntervalOne = intervalFrom(1); +export const ValueIntervalNegativeOne = intervalFrom(-1); +export const ValueIntervalZeroToOne = intervalFrom(0, 1); +export const ValueIntervalZeroToPositiveInfinity = intervalFromValues(ValueIntegerZero, ValueIntegerPositiveInfinity, true, false); +export const ValueIntervalMinusOneToOne = intervalFrom(-1, 1); +export const ValueIntervalTop = intervalFromValues(ValueIntegerTop, ValueIntegerTop); +export const ValueIntervalBottom = intervalFromValues(ValueIntegerBottom, ValueIntegerBottom); diff --git a/src/dataflow/eval/values/intervals/interval-unary.ts b/src/dataflow/eval/values/intervals/interval-unary.ts new file mode 100644 index 00000000000..b49ec3a3c66 --- /dev/null +++ b/src/dataflow/eval/values/intervals/interval-unary.ts @@ -0,0 +1,287 @@ +import type { Lift, Value, ValueInterval, ValueLogical, ValueNumber } from '../r-value'; +import { stringifyValue , isBottom , Top , isTop , asValue } from '../r-value'; +import type { ScalarUnaryOperation } from '../scalar/scalar-unary'; +import { unaryScalar } from '../scalar/scalar-unary'; +import { + intervalFromValues, + ValueIntervalTop, + orderIntervalFrom, + ValueIntervalBottom, + ValueIntervalMinusOneToOne, + ValueIntervalZero, + ValueIntervalZeroToPositiveInfinity +} from './interval-constants'; +import { iteLogical } from '../logical/logical-check'; +import { binaryScalar } from '../scalar/scalar-binary'; +import { + ValueIntegerNegativeInfinity, + ValueIntegerOne, + ValueIntegerPositiveInfinity, + ValueIntegerZero, ValueNumberEpsilon +} from '../scalar/scalar-constants'; +import { liftLogical, ValueLogicalBot, ValueLogicalFalse, ValueLogicalTop } from '../logical/logical-constants'; +import { binaryInterval } from './interval-binary'; +import { bottomTopGuard } from '../general'; +import { binaryValue } from '../value-binary'; +import { expensiveTrace } from '../../../../util/log'; +import { ValueEvalLog } from '../../eval'; + +/** + * Take two potentially lifted intervals and combine them with the given op. + * This propagates `top` and `bottom` values. + */ +export function unaryInterval( + a: Lift, + op: string +): Value { + let res: Value = Top; + if(op in Operations) { + res = Operations[op as keyof typeof Operations](a); + } + expensiveTrace(ValueEvalLog, () => ` * unaryInterval(${stringifyValue(a)}, ${op}) = ${stringifyValue(res)}`); + return res; +} + +// TODO: sin, cos, tan, ... + +/** every operation returns the interval describing all possible results when applying the operation to any value in the input interval */ +const Operations = { + id: a => a, + negate: intervalNegate, + abs: intervalAbs, + ceil: a => intervalApplyBoth(a, 'ceil', true), + floor: a => intervalApplyBoth(a, 'floor', true), + round: a => intervalApplyBoth(a, 'round', true), + sign: intervalSign, + /** calculates 1/x */ + flip: intervalDivByOne, + /** returns the structural minimum of the interval, if it is exclusive, we use the closest eta-value */ + lowest: a => { + const bt = bottomTopGuard(a); + if(bt) { + return bt === Top ? ValueIntervalTop : ValueIntervalBottom; + } + a = asValue(a); + const min = a.startInclusive ? a.start : binaryScalar(a.start, 'add', ValueNumberEpsilon) as ValueNumber; + return intervalFromValues(min, min, true, true); + }, + /** returns the structural maximum of the interval, if it is exclusive, we use the closest eta-value */ + highest: a => { + const bt = bottomTopGuard(a); + if(bt) { + return bt === Top ? ValueIntervalTop : ValueIntervalBottom; + } + a = asValue(a); + const max = a.endInclusive ? a.end : binaryScalar(a.end, 'sub', ValueNumberEpsilon) as ValueNumber; + return intervalFromValues(max, max, true, true); + }, + /** essentially returns [lowest(v), highest(v)] */ + toClosed: a => { + const bt = bottomTopGuard(a); + if(bt) { + return bt === Top ? ValueIntervalTop : ValueIntervalBottom; + } + a = asValue(a); + const min = a.startInclusive ? a.start : binaryScalar(a.start, 'add', ValueNumberEpsilon) as ValueNumber; + const max = a.endInclusive ? a.end : binaryScalar(a.end, 'sub', ValueNumberEpsilon) as ValueNumber; + return intervalFromValues(min, max, true, true); + }, + /** check if the interval contains no values */ + empty: intervalEmpty, + /** check if the interval contains exactly one value */ + scalar: intervalScalar, + hasZero: a => binaryInterval(ValueIntervalZero, '⊆', a) +} as const satisfies Record) => Value>; + + +function intervalApplyBoth(a: Lift, op: ScalarUnaryOperation, toClosed: boolean, startInclusive?: boolean, endInclusive?: boolean): ValueInterval { + const bt = bottomTopGuard(a); + if(bt) { + return bt === Top ? ValueIntervalTop : ValueIntervalBottom; + } + a = asValue(a); + if(toClosed) { + a = asValue(unaryInterval(a, 'toClosed') as Lift); + startInclusive = true; + endInclusive = true; + } + + return orderIntervalFrom( + unaryScalar(a.start, op) as ValueNumber, + unaryScalar(a.end, op) as ValueNumber, + startInclusive ?? a.startInclusive, + endInclusive ?? a.endInclusive + ); +} + +function intervalSign(a: Lift): ValueInterval { + if(isBottom(a)) { + return ValueIntervalBottom; + } else if(isTop(a) || isTop(a.start.value) || isTop(a.end.value)) { + return ValueIntervalMinusOneToOne; + } + + return intervalApplyBoth(a, 'sign', true); +} + +function intervalNegate(a: Lift): ValueInterval { + const bt = bottomTopGuard(a); + if(bt) { + return bt === Top ? ValueIntervalTop : ValueIntervalBottom; + } + const x = asValue(a); + return intervalFromValues( + unaryScalar(x.end, 'negate') as ValueNumber, + unaryScalar(x.start, 'negate') as ValueNumber, + x.endInclusive, + x.startInclusive + ); +} + +function intervalAbs(a: Lift): ValueInterval { + if(isBottom(a)) { + return ValueIntervalBottom; + } else if(isTop(a)) { + return ValueIntervalZeroToPositiveInfinity; + } + // abs[a,b] = [0, max(abs(a), abs(b))] if a <= 0 <= b + // abs[a,b] = [a, b] if 0 <= a <= b + // abs[a,b] = [abs(b), abs(a)] if a <= b <= 0 + return iteLogical( + unaryScalar(a.start, 'isNegative'), + { + onTrue: () => iteLogical( + unaryScalar(a.end, 'isNonNegative'), + { + // a <= 0 <= b + onTrue: () => { + const startAbs = unaryScalar(a.start, 'abs') as ValueNumber; + const endAbs = unaryScalar(a.end, 'abs') as ValueNumber; + const max = binaryScalar( + startAbs, + 'max', + endAbs + ) as ValueNumber; + // we take the inclusivity of the max + const upperInclusive = max === startAbs ? a.startInclusive : a.endInclusive; + return intervalFromValues( + ValueIntegerZero, + max, + true, // TODO: check + upperInclusive // TODO: check + ); + }, + // a <= b <= 0 + onFalse: () => intervalFromValues( + unaryScalar(a.end, 'abs') as ValueNumber, + unaryScalar(a.start, 'abs') as ValueNumber, + a.endInclusive, + true // TODO: check + ), + onMaybe: ValueIntervalZeroToPositiveInfinity, + onTop: ValueIntervalZeroToPositiveInfinity, + onBottom: ValueIntervalBottom + } + ), + onMaybe: ValueIntervalTop, + onTop: ValueIntervalTop, + onBottom: ValueIntervalBottom, + onFalse: () => a + } + ); +} + +function intervalDivByOne(a: Lift): ValueInterval { + const bt = bottomTopGuard(a); + if(bt) { + return bt === Top ? ValueIntervalTop : ValueIntervalBottom; + } + a = asValue(a); + const ifStartIsZero = () => + iteLogical(unaryScalar(a.end, 'isZero'), + { + onTrue: ValueIntervalBottom, + onMaybe: ValueIntervalTop, + onFalse: () => intervalFromValues( + ValueIntegerNegativeInfinity, + ValueIntegerPositiveInfinity, + false, // TODO: check + true // TODO: check + ), + onTop: ValueIntervalTop, + onBottom: ValueIntervalBottom + }); + const neitherIsZero = () => iteLogical( + unaryInterval(a, 'hasZero'), + { + onTrue: ValueIntervalTop, + onTop: ValueIntervalTop, + onFalse: () => orderIntervalFrom( + binaryScalar(ValueIntegerOne, 'div', a.start) as ValueNumber, + binaryScalar(ValueIntegerOne, 'div', a.end) as ValueNumber, + a.startInclusive, // TODO: check + a.endInclusive // TODO: check + ), + onMaybe: ValueIntervalTop, + onBottom: ValueIntervalBottom + } + ); + const ifStartIsNotZero = () => iteLogical( + unaryScalar(a.end, 'isZero'), + { + // start is not zero, but end is zero + onTrue: () => intervalFromValues( + ValueIntegerNegativeInfinity, + binaryScalar(ValueIntegerOne, 'div', a.end) as ValueNumber, + false, // TODO: check + true // TODO: check + ), + onMaybe: ValueIntervalTop, + onFalse: neitherIsZero, + onTop: ValueIntervalTop, + onBottom: ValueIntervalBottom + } + ); + + return iteLogical( + unaryScalar(a.start, 'isZero'), + { + onTrue: ifStartIsZero, + onFalse: ifStartIsNotZero, + onMaybe: ValueIntervalTop, + onTop: ValueIntervalTop, + onBottom: ValueIntervalBottom + } + ); +} + +function intervalEmpty(a: Lift): ValueLogical { + const bt = bottomTopGuard(a); + if(bt) { + return bt === Top ? ValueLogicalTop : ValueLogicalBot; + } + a = asValue(a); + return binaryValue( + binaryScalar(a.start, '>', a.end), + 'or', + binaryValue( + binaryScalar(a.start, '===', a.end), + 'and', + liftLogical(!a.startInclusive || !a.endInclusive) + ) + ) as ValueLogical; +} + +function intervalScalar(a: Lift): ValueLogical { + const bt = bottomTopGuard(a); + if(bt) { + return bt === Top ? ValueLogicalTop : ValueLogicalBot; + } + a = asValue(a); + if(!a.startInclusive || !a.endInclusive) { + return ValueLogicalFalse; + } else { + return binaryScalar(a.start, '===', a.end) as ValueLogical; + } +} + diff --git a/src/dataflow/eval/values/logical/logical-binary.ts b/src/dataflow/eval/values/logical/logical-binary.ts new file mode 100644 index 00000000000..442aea82025 --- /dev/null +++ b/src/dataflow/eval/values/logical/logical-binary.ts @@ -0,0 +1,84 @@ +import type { Lift, TernaryLogical, Value, ValueLogical } from '../r-value'; +import { stringifyValue, Top } from '../r-value'; +import { bottomTopGuard } from '../general'; +import { guard } from '../../../../util/assert'; +import { expensiveTrace } from '../../../../util/log'; +import { ValueEvalLog } from '../../eval'; + +/** + * Take two potentially lifted logicals and combine them with the given op. + * This propagates `top` and `bottom` values. + */ +export function binaryLogical( + a: Lift, + op: string, + b: Lift +): Lift { + let res: Value = Top; + guard(op in Operations, `Unknown logical binary operation: ${op}`); + res = Operations[op as keyof typeof Operations](a, b); + expensiveTrace(ValueEvalLog, () => ` * binaryLogical(${stringifyValue(a)}, ${op}, ${stringifyValue(b)}) = ${stringifyValue(res)}`); + return res; +} + +const Operations = { + '===': (a, b) => logicalHelper(a, b, (a, b) => a === b), + '==': (a, b) => logicalHelper(a, b, (a, b) => { + if(a === 'maybe' || b === 'maybe') { + return 'maybe'; + } else { + return a === b; + } + }), + and: (a, b) => logicalHelper(a, b, (a, b) => { + if(a === false || b === false) { + return false; + } else if(a === true && b === true) { + return true; + } else { + return 'maybe'; + } + }), + or: (a, b) => logicalHelper(a, b, (a, b) => { + if(a === true || b === true) { + return true; + } else if(a === false && b === false) { + return false; + } else { + return 'maybe'; + } + }), + xor: (a, b) => logicalHelper(a, b, (a, b) => { + if(a === 'maybe' || b === 'maybe') { + return 'maybe'; + } else { + return a !== b; + } + }), + implies: (a, b) => logicalHelper(a, b, (a, b) => { + if(a === true) { + return b; + } else if(a === false) { + return true; + } else { + return 'maybe'; + } + }), + iff: (a, b) => logicalHelper(a, b, (a, b) => { + if(a === 'maybe' || b === 'maybe') { + return 'maybe'; + } else { + return a === b; + } + }) +} as const satisfies Record, b: Lift) => Lift>; + +export type LogicalBinaryOperation = typeof Operations; + +function logicalHelper, B extends Lift>(a: A, b: B, op: (a: TernaryLogical, b: TernaryLogical) => TernaryLogical): Lift { + const botTopGuard = bottomTopGuard(a, b) ?? bottomTopGuard((a as ValueLogical).value, (b as ValueLogical).value); + return { + type: 'logical', + value: botTopGuard ?? op((a as ValueLogical).value as TernaryLogical, (b as ValueLogical).value as TernaryLogical) + }; +} diff --git a/src/dataflow/eval/values/logical/logical-check.ts b/src/dataflow/eval/values/logical/logical-check.ts new file mode 100644 index 00000000000..451c112c3cd --- /dev/null +++ b/src/dataflow/eval/values/logical/logical-check.ts @@ -0,0 +1,67 @@ +import type { Value, ValueLogical } from '../r-value'; +import { stringifyValue , isBottom, isTop , Bottom , Top } from '../r-value'; +import type { CanBeLazy } from '../../../../util/lazy'; +import { force } from '../../../../util/lazy'; +import { liftLogical, ValueLogicalBot, ValueLogicalTop, ValueLogicalTrue } from './logical-constants'; +import { binaryScalar } from '../scalar/scalar-binary'; +import { ValueIntegerZero } from '../scalar/scalar-constants'; +import { binaryString } from '../string/string-binary'; +import { ValueEmptyString } from '../string/string-constants'; +import { unaryInterval } from '../intervals/interval-unary'; +import { binaryValue } from '../value-binary'; +import { expensiveTrace } from '../../../../util/log'; +import { ValueEvalLog } from '../../eval'; + +// TODO: truthy unary checks +export function toTruthy(a: Value): ValueLogical { + expensiveTrace(ValueEvalLog, () => ` * isTruthy(${stringifyValue(a)})`); + if(a === Top) { + return ValueLogicalTop; + } else if(a === Bottom) { + return ValueLogicalBot; + } else if(a.type === 'logical') { + return a; + } else if(a.type === 'number') { + return binaryScalar(a, '!==', ValueIntegerZero) as ValueLogical; + } else if(a.type === 'string') { + return binaryString(a, '!==', ValueEmptyString) as ValueLogical; + } else if(a.type === 'interval') { + return unaryInterval(a, 'hasZero') as ValueLogical; + } else if(a.type === 'vector') { + return isTop(a.elements) || isBottom(a.elements) ? liftLogical(a.elements) : + a.elements.length !== 0 ? ValueLogicalBot : + toTruthy(a.elements[0]); + } else if(a.type === 'set') { + return isTop(a.elements) || isBottom(a.elements) ? liftLogical(a.elements) : + a.elements.reduce((acc, el) => binaryValue(acc, 'or', toTruthy(el)), ValueLogicalTrue) as ValueLogical; + } + return ValueLogicalTop; +} + +export function isTruthy(a: Value): boolean { + return toTruthy(a).value === true; +} + +interface IteCases { + readonly onTrue: CanBeLazy; + readonly onFalse: CanBeLazy; + readonly onMaybe: CanBeLazy; + readonly onTop: CanBeLazy; + readonly onBottom: CanBeLazy; +} + +export function iteLogical( + cond: Value, + { onTrue, onFalse, onMaybe, onTop, onBottom }: IteCases +): Result { + const condVal = toTruthy(cond).value; + if(condVal === Top) { + return force(onTop); + } else if(condVal === Bottom) { + return force(onBottom); + } else if(condVal === 'maybe') { + return force(onMaybe); + } else { + return condVal ? force(onTrue) : force(onFalse); + } +} \ No newline at end of file diff --git a/src/dataflow/eval/values/logical/logical-constants.ts b/src/dataflow/eval/values/logical/logical-constants.ts new file mode 100644 index 00000000000..dceb3055675 --- /dev/null +++ b/src/dataflow/eval/values/logical/logical-constants.ts @@ -0,0 +1,27 @@ +import type { Lift, TernaryLogical, ValueLogical } from '../r-value'; +import { Bottom, Top } from '../r-value'; + +export function liftLogical(log: Lift): ValueLogical { + if(log === Top) { + return ValueLogicalTop; + } else if(log === Bottom) { + return ValueLogicalBot; + } else if(log === 'maybe') { + return ValueLogicalMaybe; + } else { + return log ? ValueLogicalTrue : ValueLogicalFalse; + } +} + +function makeLogical(log: Lift): ValueLogical { + return { + type: 'logical', + value: log + }; +} + +export const ValueLogicalTrue: ValueLogical = makeLogical(true); +export const ValueLogicalFalse: ValueLogical = makeLogical(false); +export const ValueLogicalMaybe: ValueLogical = makeLogical('maybe'); +export const ValueLogicalTop: ValueLogical = makeLogical(Top); +export const ValueLogicalBot: ValueLogical = makeLogical(Bottom); \ No newline at end of file diff --git a/src/dataflow/eval/values/logical/logical-unary.ts b/src/dataflow/eval/values/logical/logical-unary.ts new file mode 100644 index 00000000000..8f6ce1559bf --- /dev/null +++ b/src/dataflow/eval/values/logical/logical-unary.ts @@ -0,0 +1,69 @@ +import type { Lift, Value, ValueInterval, ValueLogical } from '../r-value'; +import { Top , stringifyValue } from '../r-value'; +import { bottomTopGuard } from '../general'; +import { iteLogical } from './logical-check'; +import { + ValueIntervalBottom, + ValueIntervalOne, + ValueIntervalTop, + ValueIntervalZero, + ValueIntervalZeroToOne +} from '../intervals/interval-constants'; +import { expensiveTrace } from '../../../../util/log'; +import { ValueEvalLog } from '../../eval'; +import { unaryInterval } from '../intervals/interval-unary'; + +/** + * Take one potentially lifted logical and apply the given unary op. + * This propagates `top` and `bottom` values. + */ +export function unaryLogical( + a: Lift, + // TODO: support common unary ops + op: string +): Value { + let res: Value | undefined = bottomTopGuard(a); + if(op in Operations) { + res = Operations[op as keyof typeof Operations](a as ValueLogical); + } else { + res = unaryInterval(logicalToInterval(a as ValueLogical), op); + } + expensiveTrace(ValueEvalLog, () => ` * unaryLogical(${stringifyValue(a)}, ${op}) = ${stringifyValue(res)}`); + return res ?? Top; +} + +/* + toNumber: (a: ValueLogical) => iteLogical(a, { + onTrue: ValueIntervalZero, + onMaybe: ValueNumberOneHalf, + onFalse: ValueIntegerOne, + }), + toInterval: (a: ValueLogical) => iteLogical(a, { + onTrue: intervalFromValues(0), + onMaybe: getScalarFromInteger(0.5, false), + onFalse: getScalarFromInteger(1), + }) + + */ + +export function logicalToInterval(a: A): ValueInterval { + return iteLogical(a, { + onTrue: ValueIntervalZero, + onMaybe: ValueIntervalZeroToOne, + onFalse: ValueIntervalOne, + onTop: ValueIntervalTop, + onBottom: ValueIntervalBottom + }); +} + +const Operations = { + not: logicalNot +} as const; + +function logicalNot(a: A): ValueLogical { + const val = bottomTopGuard(a.value); + return { + type: 'logical', + value: val ?? (a.value === 'maybe' ? 'maybe' : !a.value) + }; +} diff --git a/src/dataflow/eval/values/missing/missing-constants.ts b/src/dataflow/eval/values/missing/missing-constants.ts new file mode 100644 index 00000000000..a4a3f9fe8db --- /dev/null +++ b/src/dataflow/eval/values/missing/missing-constants.ts @@ -0,0 +1,5 @@ +import type { ValueMissing } from '../r-value'; + +export const Missing: ValueMissing = { + type: 'missing' +}; \ No newline at end of file diff --git a/src/dataflow/eval/values/r-value.ts b/src/dataflow/eval/values/r-value.ts new file mode 100644 index 00000000000..add60036660 --- /dev/null +++ b/src/dataflow/eval/values/r-value.ts @@ -0,0 +1,150 @@ +import type { RNumberValue, RStringValue } from '../../../r-bridge/lang-4.x/convert-values'; +import type { RLogicalValue } from '../../../r-bridge/lang-4.x/ast/model/nodes/r-logical'; +import { assertUnreachable, guard } from '../../../util/assert'; + +export const Top = { type: Symbol('⊤') }; +export const Bottom = { type: Symbol('⊥') }; + +export type Lift = N | typeof Top | typeof Bottom +export type Unlift = N extends typeof Top ? never : N extends typeof Bottom ? never : N + +export interface ValueInterval { + type: 'interval' + start: Limit + startInclusive: boolean + end: Limit + endInclusive: boolean +} + +/** + * An R vector with either a known set of elements or a known domain. + */ +export interface ValueVector = Lift, Domain extends Lift = Lift> { + type: 'vector' + elements: Elements + /** if we do not know the amount of elements, we can still know the domain */ + elementDomain: Domain +} +/** describes the static case of we do not know which value */ +export interface ValueSet = Lift> { + type: 'set' + elements: Elements +} +export interface ValueNumber = Lift> { + type: 'number' + value: Num +} +export interface ValueString = Lift> { + type: 'string' + value: Str +} +export interface ValueMissing { + type: 'missing' +} +export type TernaryLogical = RLogicalValue | 'maybe' +export interface ValueLogical { + type: 'logical' + value: Lift +} + +export type Value = Lift< + ValueInterval + | ValueVector + | ValueSet + | ValueNumber + | ValueString + | ValueLogical + | ValueMissing + > +export type ValueType = V extends { type: infer T } ? T : never +export type ValueTypes = ValueType + +export function typeOfValue(value: V): V['type'] { + return value.type; +} + +// @ts-expect-error -- this is a save cast +export function isTop>(value: V): value is typeof Top { + return value === Top; +} +// @ts-expect-error -- this is a save cast +export function isBottom>(value: V): value is typeof Bottom { + return value === Bottom; +} + +export function isValue>(value: V): value is Unlift { + return !isTop(value) && !isBottom(value); +} + +export function asValue>(value: V): Unlift { + guard(isValue(value), 'Expected a value, but got a top or bottom value'); + return value; +} + +function tryStringifyBoTop>( + value: V, + otherwise: (v: Unlift) => string, + onTop = () => '⊤', + onBottom = () => '⊥' +): string { + if(isTop(value)) { + return onTop(); + } else if(isBottom(value)) { + return onBottom(); + } else { + return otherwise(value as Unlift); + } +} + +function stringifyRNumberSuffix(value: RNumberValue): string { + let suffix = ''; + if(value.markedAsInt) { + suffix += 'L'; + } + if(value.complexNumber) { + suffix += 'i'; + } + // do something about iL even though it is impossible? + return suffix; +} + +function renderString(value: RStringValue): string { + const quote = value.quotes; + const raw = value.flag === 'raw'; + if(raw) { + return `r${quote}(${value.str})${quote}`; + } else { + return `${quote}${JSON.stringify(value.str).slice(1, -1)}${quote}`; + } +} + +export function stringifyValue(value: Lift): string { + return tryStringifyBoTop(value, v => { + const t = v.type; + switch(t) { + case 'interval': + return `${v.startInclusive ? '[' : '('}${stringifyValue(v.start)}, ${stringifyValue(v.end)}${v.endInclusive ? ']' : ')'}`; + case 'vector': + return tryStringifyBoTop(v.elements, e => { + return `<${stringifyValue(v.elementDomain)}> c(${e.map(stringifyValue).join(',')})`; + }, () => `⊤ (vector, ${stringifyValue(v.elementDomain)})`, () => `⊥ (vector, ${stringifyValue(v.elementDomain)})`); + case 'set': + return tryStringifyBoTop(v.elements, e => { + return e.length === 1 ? stringifyValue(e[0]) : `{ ${e.map(stringifyValue).join(',')} }`; + }, () => '⊤ (set)', () => '⊥ (set)'); + case 'number': + return tryStringifyBoTop(v.value, + n => `${n.num}${stringifyRNumberSuffix(n)}`, + () => '⊤ (number)', () => '⊥ (number)' + ); + case 'string': + return tryStringifyBoTop(v.value, renderString, () => '⊤ (string)', () => '⊥ (string)'); + case 'logical': + return tryStringifyBoTop(v.value, l => l === 'maybe' ? 'maybe' : l ? 'TRUE' : 'FALSE', () => '⊤ (logical)', () => '⊥ (logical)'); + case 'missing': + return '(missing)'; + default: + assertUnreachable(t); + } + }); +} \ No newline at end of file diff --git a/src/dataflow/eval/values/scalar/scalar-binary.ts b/src/dataflow/eval/values/scalar/scalar-binary.ts new file mode 100644 index 00000000000..bb94a85a1f3 --- /dev/null +++ b/src/dataflow/eval/values/scalar/scalar-binary.ts @@ -0,0 +1,113 @@ +import type { Lift, Value, ValueLogical, ValueNumber } from '../r-value'; +import { stringifyValue, Bottom } from '../r-value'; +import { bottomTopGuard } from '../general'; +import type { RNumberValue } from '../../../../r-bridge/lang-4.x/convert-values'; +import { liftScalar, ValueIntegerBottom, ValueIntegerTop, ValueNumberEpsilon } from './scalar-constants'; +import { liftLogical, ValueLogicalBot, ValueLogicalTop } from '../logical/logical-constants'; +import { expensiveTrace } from '../../../../util/log'; +import { ValueEvalLog } from '../../eval'; +import { binaryInterval } from '../intervals/interval-binary'; +import { intervalFromValues } from '../intervals/interval-constants'; + +/** + * Take two potentially lifted intervals and combine them with the given op. + * This propagates `top` and `bottom` values. + */ +export function binaryScalar( + a: Lift, + op: string, + b: Lift +): Value { + let res: Value | undefined = bottomTopGuard(a); + if(op in ScalarBinaryOperations) { + res ??= ScalarBinaryOperations[op as keyof typeof ScalarBinaryOperations](a, b); + } else { + res = binaryInterval( + intervalFromValues(a, a), + op, + intervalFromValues(b, b) + ); + } + res ??= ValueIntegerTop; + expensiveTrace(ValueEvalLog, () => ` * binaryScalar(${stringifyValue(a)}, ${op}, ${stringifyValue(b)}) = ${stringifyValue(res)}`); + return res; +} + + +const ScalarBinaryOperations = { + add: (a, b) => scalarHelper(a, b, (a, b) => a + b), + sub: (a, b) => scalarHelper(a, b, (a, b) => a - b), + mul: (a, b) => scalarHelper(a, b, (a, b) => a * b), + div: (a, b) => scalarHelper(a, b, (a, b) => a / b), + pow: (a, b) => scalarHelper(a, b, (a, b) => a ** b), + mod: (a, b) => scalarHelper(a, b, (a, b) => a % b), + max: (a, b) => scalarMaxMin(a, b, 'max'), + min: (a, b) => scalarMaxMin(a, b, 'min'), + '<=': (a, b) => scalarHelperLogical(a, b, (a, b) => a <= b), + '<': (a, b) => scalarHelperLogical(a, b, (a, b) => a < b), + '>=': (a, b) => scalarHelperLogical(a, b, (a, b) => a >= b), + '>': (a, b) => scalarHelperLogical(a, b, (a, b) => a > b), + '==': (a, b) => scalarHelperLogical(a, b, (a, b) => identicalNumbersThreshold(a, b)), + '!=': (a, b) => scalarHelperLogical(a, b, (a, b) => !identicalNumbersThreshold(a, b)), + '===': (a, b) => scalarHelperLogical(a, b, (a, b) => identicalNumbersThreshold(a, b)), + '!==': (a, b) => scalarHelperLogical(a, b, (a, b) => !identicalNumbersThreshold(a, b)), + /** subseteq is only fulfilled if they are the same */ + '⊆': (a, b) => scalarHelperLogical(a, b, (a, b) => identicalNumbersThreshold(a, b)), + /** subset is never fulfilled */ + '⊂': (a, b) => scalarHelperLogical(a, b, (_a, _b) => false), + '⊇': (a, b) => scalarHelperLogical(a, b, (a, b) => identicalNumbersThreshold(b, a)), + '⊃': (a, b) => scalarHelperLogical(a, b, (_a, _b) => false) +} as const satisfies Record, b: Lift) => Value>; + +export type ScalarBinaryOperation = typeof ScalarBinaryOperations; + +function identicalNumbersThreshold(a: number, b: number): boolean { + return a === b || Math.abs(a - b) < 2 * ValueNumberEpsilon.value.num; +} + + +function scalarHelperLogical( + a: Lift, + b: Lift, + c: (a: number, b: number) => boolean +): ValueLogical { + const val = bottomTopGuard(a, b, (a as ValueNumber).value, (b as ValueNumber).value); + if(val) { + return val === Bottom ? ValueLogicalBot : ValueLogicalTop; + } + const aval = (a as ValueNumber).value as RNumberValue; + const bval = (b as ValueNumber).value as RNumberValue; + return liftLogical(val ?? c(aval.num, bval.num)); +} + +function scalarHelper( + a: Lift, + b: Lift, + c: (a: number, b: number) => number +): ValueNumber { + const val = bottomTopGuard(a, b, (a as ValueNumber).value, (b as ValueNumber).value); + if(val) { + return val === Bottom ? ValueIntegerBottom : ValueIntegerTop; + } + const aval = (a as ValueNumber).value as RNumberValue; + const bval = (b as ValueNumber).value as RNumberValue; + /* do not calculate if top or bot */ + const result = c(aval.num, bval.num); + return liftScalar({ + markedAsInt: aval.markedAsInt && bval.markedAsInt && Number.isInteger(result), + complexNumber: aval.complexNumber || bval.complexNumber, + num: result + }); +} + +// max and min do not have to create knew objects +function scalarMaxMin(a: Lift, b: Lift, c: 'max' | 'min'): Lift { + const bt = bottomTopGuard(a, b, (a as ValueNumber).value, (b as ValueNumber).value); + if(bt) { + return ValueIntegerTop; + } + const aval = (a as ValueNumber).value as RNumberValue; + const bval = (b as ValueNumber).value as RNumberValue; + const takeA = c === 'max' ? aval.num > bval.num : aval.num < bval.num; + return takeA ? a : b; +} diff --git a/src/dataflow/eval/values/scalar/scalar-constants.ts b/src/dataflow/eval/values/scalar/scalar-constants.ts new file mode 100644 index 00000000000..299af46577a --- /dev/null +++ b/src/dataflow/eval/values/scalar/scalar-constants.ts @@ -0,0 +1,36 @@ +import type { Lift, ValueNumber } from '../r-value'; +import { Bottom , Top } from '../r-value'; +import type { RNumberValue } from '../../../../r-bridge/lang-4.x/convert-values'; + +export function getScalarFromInteger(num: number, markedAsInt = Number.isInteger(num), complexNumber = false): ValueNumber { + return { + type: 'number', + value: { + markedAsInt, + num, + complexNumber + } + }; +} + +export function liftScalar(value: Lift): ValueNumber { + return { + type: 'number', + value: value + }; +} + +const epsilon = 1e-7; + +export const ValueIntegerOne = getScalarFromInteger(1); +export const ValueNumberComplexOne = getScalarFromInteger(1, false, true); +export const ValueIntegerZero = getScalarFromInteger(0); +export const ValueIntegerNegativeOne = getScalarFromInteger(-1); +export const ValueIntegerPositiveInfinity = getScalarFromInteger(Number.POSITIVE_INFINITY); +export const ValueNumberPositiveInfinity = getScalarFromInteger(Number.POSITIVE_INFINITY, false); +export const ValueIntegerNegativeInfinity = getScalarFromInteger(Number.NEGATIVE_INFINITY); +export const ValueNumberEpsilon = getScalarFromInteger(epsilon, false); +export const ValueNumberOneHalf = getScalarFromInteger(0.5, false); + +export const ValueIntegerTop = liftScalar(Top); +export const ValueIntegerBottom = liftScalar(Bottom); \ No newline at end of file diff --git a/src/dataflow/eval/values/scalar/scalar-unary.ts b/src/dataflow/eval/values/scalar/scalar-unary.ts new file mode 100644 index 00000000000..5f1005245c1 --- /dev/null +++ b/src/dataflow/eval/values/scalar/scalar-unary.ts @@ -0,0 +1,125 @@ +import type { Lift, Value, ValueLogical, ValueNumber } from '../r-value'; +import { stringifyValue , asValue, Bottom , isBottom , isTop, Top } from '../r-value'; +import { bottomTopGuard } from '../general'; +import type { RNumberValue } from '../../../../r-bridge/lang-4.x/convert-values'; +import { liftScalar } from './scalar-constants'; +import { guard } from '../../../../util/assert'; +import { liftLogical, ValueLogicalBot, ValueLogicalTop } from '../logical/logical-constants'; +import { ValueIntervalMinusOneToOne, ValueIntervalZeroToPositiveInfinity } from '../intervals/interval-constants'; +import { expensiveTrace } from '../../../../util/log'; +import { ValueEvalLog } from '../../eval'; + +/** + * Take a potentially lifted scalar and apply the given op. + * This propagates `top` and `bottom` values. + */ +export function unaryScalar( + a: Lift, + op: string +): Value { + let res: Value = Top; + guard(op in ScalarUnaryOperations, `Unknown scalar unary operation: ${op}`); + res = bottomTopGuard(a) ?? ScalarUnaryOperations[op as keyof typeof ScalarUnaryOperations](a); + expensiveTrace(ValueEvalLog, () => ` * unaryScalar(${stringifyValue(a)}, ${op}) = ${stringifyValue(res)}`); + return res; +} + +const ScalarUnaryOperations = { + id: a => a, + negate: a => scalarHelper(a, (a) => -a), + abs: a => scalarHelper(a, Math.abs, ValueIntervalZeroToPositiveInfinity), + ceil: a => scalarHelper(a, Math.ceil), + floor: a => scalarHelper(a, Math.floor), + round: a => scalarHelper(a, Math.round), + exp: a => scalarHelper(a, Math.exp), + log: a => scalarHelper(a, Math.log), + log10: a => scalarHelper(a, Math.log10), + log2: a => scalarHelper(a, Math.log2), + sign: a => scalarHelper(a, Math.sign), + sqrt: a => scalarHelper(a, Math.sqrt), + sin: a => scalarHelper(a, Math.sin, ValueIntervalMinusOneToOne), + cos: a => scalarHelper(a, Math.cos, ValueIntervalMinusOneToOne), + tan: a => scalarHelper(a, Math.tan, ValueIntervalMinusOneToOne), + asin: a => scalarHelper(a, Math.asin), + acos: a => scalarHelper(a, Math.acos), + atan: a => scalarHelper(a, Math.atan), + sinh: a => scalarHelper(a, Math.sinh), + cosh: a => scalarHelper(a, Math.cosh), + tanh: a => scalarHelper(a, Math.tanh), + /** `=== 0` */ + 'isZero': s => scalarCheck(s, n => n === 0), + /** `> 0` */ + 'isNegative': s => scalarCheck(s, n => n < 0), + /** `< 0` */ + 'isPositive': s => scalarCheck(s, n => n > 0), + /** `>= 0` */ + 'isNonNegative': s => scalarCheck(s, n => n >= 0), + 'isMarkedAsInt': scalarMarkedAsInt, + 'isMarkedAsComplex': scalarMarkedAsComplex +} as const satisfies Record) => Value>; + +export type ScalarUnaryOperation = keyof typeof ScalarUnaryOperations; + +function scalarHelper(a: Lift, op: (a: number) => number, fallback: Value = Top): Value { + if(isTop(a)) { + return fallback; + } else if(isBottom(a)) { + return a; + } + const val = bottomTopGuard(a.value); + const aval = a.value as RNumberValue; + return liftScalar(val ?? { + markedAsInt: aval.markedAsInt, + complexNumber: aval.complexNumber, + num: op(aval.num) + }); +} + + +function scalarCheck(a: Lift, c: (n: number) => boolean): ValueLogical { + const val = bottomTopGuard(a); + if(val) { + return val === Bottom ? ValueLogicalBot : ValueLogicalTop; + } + a = asValue(a); + if(isTop(a.value)) { + return ValueLogicalTop; + } else if(isBottom(a.value)) { + return ValueLogicalBot; + } else { + return liftLogical(c(a.value.num)); + } +} + +function scalarMarkedAsInt(a: Lift): Lift { + if(isBottom(a)) { + return ValueLogicalBot; + } else if(isTop(a)) { + return ValueLogicalTop; + } + a = asValue(a); + if(isTop(a.value)) { + return ValueLogicalTop; + } else if(isBottom(a.value)) { + return ValueLogicalBot; + } else { + return liftLogical(a.value.markedAsInt); + } +} + +function scalarMarkedAsComplex(a: Lift): Lift { + if(isBottom(a)) { + return ValueLogicalBot; + } else if(isTop(a)) { + return ValueLogicalTop; + } + a = asValue(a); + if(isTop(a.value)) { + return ValueLogicalTop; + } else if(isBottom(a.value)) { + return ValueLogicalBot; + } else { + return liftLogical(a.value.complexNumber); + } +} + diff --git a/src/dataflow/eval/values/sets/set-binary.ts b/src/dataflow/eval/values/sets/set-binary.ts new file mode 100644 index 00000000000..df6464a6620 --- /dev/null +++ b/src/dataflow/eval/values/sets/set-binary.ts @@ -0,0 +1,72 @@ +import type { Value, ValueSet } from '../r-value'; +import { stringifyValue, Top , isBottom, isTop } from '../r-value'; +import { bottomTopGuard } from '../general'; +import { binaryValue } from '../value-binary'; +import { ValueLogicalBot, ValueLogicalTop, ValueLogicalTrue } from '../logical/logical-constants'; +import { setFrom } from './set-constants'; +import { expensiveTrace } from '../../../../util/log'; +import { ValueEvalLog } from '../../eval'; + +/** + * Take two potentially lifted sets and apply the given op. + * This propagates `top` and `bottom` values. + */ +export function binarySet( + a: ValueSet, + op: string, + b: ValueSet +): ValueSet { + let res: Value = Top; + if(op in SetBinaryOperations) { + res = SetBinaryOperations[op](a, b); + } else { + res = cartesianProduct(a, op, b); + } + expensiveTrace(ValueEvalLog, () => ` * binarySet(${stringifyValue(a)}, ${op}, ${stringifyValue(b)}) = ${stringifyValue(res)}`); + return res; +} + +function cartesianProduct( + a: ValueSet, + op: string, + b: ValueSet +): ValueSet { + const bt = bottomTopGuard(a.elements, b.elements); + if(bt) { + return setFrom(bt); + } else if((a as ValueSet).elements.length === 0 || (b as ValueSet).elements.length === 0) { + return setFrom(); + } + const elements: Value[] = []; + for(const aElement of (a.elements as Value[])) { + for(const bElement of (b.elements as Value[])) { + elements.push(binaryValue(aElement, op, bElement)); + } + } + return setFrom(...elements); +} + +const SetBinaryOperations = { + '===': (a, b) => { + const res = cartesianProduct(a, '===', b); + // TODO: what if multiple possbiilities, this should only check if one of them works + if(isTop(res.elements)) { + return ValueLogicalTop; + } else if(isBottom(res.elements)) { + return ValueLogicalBot; + } else { + if(res.elements.length === 0) { + return ((a as ValueSet).elements.length === 0 || (b as ValueSet).elements.length === 0) + ? ValueLogicalTrue : ValueLogicalBot; + } + return res.elements.reduce((acc, cur) => binaryValue(acc, 'and', cur), ValueLogicalTrue); + } + }, + // TODO %*% etc. + /* + first: a => vectorFrom(a.elements[0]), + last: a => vectorFrom(a.elements[a.elements.length - 1]), + */ +} as Record ValueSet>; + +// TODO: support sin clamp to [-1, 1] etc. diff --git a/src/dataflow/eval/values/sets/set-constants.ts b/src/dataflow/eval/values/sets/set-constants.ts new file mode 100644 index 00000000000..67ff44f3e74 --- /dev/null +++ b/src/dataflow/eval/values/sets/set-constants.ts @@ -0,0 +1,30 @@ +import type { Lift, Value, ValueSet } from '../r-value'; +import { Top } from '../r-value'; +import { bottomTopGuard } from '../general'; + +function flattenSetElements(s: Lift): Lift { + return bottomTopGuard(s) ?? (s as Value[]).flatMap(e => { + return e.type === 'set' ? flattenSetElements(e.elements) : e; + }); +} + +export function setFrom(...elements: V): ValueSet { + return { + type: 'set', + elements: elements.flatMap(e => { + return e.type === 'set' ? flattenSetElements(e.elements) : e; + }) + }; +} + +// TODO: flatten, union intervals etc. + +export const ValueEmptySet = setFrom(); +export const ValueSetTop: ValueSet = { + type: 'set', + elements: Top +}; +export const ValueSetBottom: ValueSet = { + type: 'set', + elements: Top +}; diff --git a/src/dataflow/eval/values/sets/set-unary.ts b/src/dataflow/eval/values/sets/set-unary.ts new file mode 100644 index 00000000000..0dcaa70e6ee --- /dev/null +++ b/src/dataflow/eval/values/sets/set-unary.ts @@ -0,0 +1,46 @@ +import type { Value, ValueSet } from '../r-value'; +import { stringifyValue, Top } from '../r-value'; +import { setFrom } from './set-constants'; +import { bottomTopGuard } from '../general'; +import { unaryValue } from '../value-unary'; +import { expensiveTrace } from '../../../../util/log'; +import { ValueEvalLog } from '../../eval'; + +/** + * Take a potentially lifted set and apply the given op. + * This propagates `top` and `bottom` values. + */ +export function unarySet( + a: A, + op: string +): ValueSet { + let res: Value = Top; + if(op in SetUnaryOperations) { + res = SetUnaryOperations[op](a); + } else { + res = applyComponentWise(a, op); + } + expensiveTrace(ValueEvalLog, () => ` * unarySet(${stringifyValue(a)}, ${op}) = ${stringifyValue(res)}`); + return res; +} + +function applyComponentWise( + a: ValueSet, + op: string +): ValueSet { + const bt = bottomTopGuard(a.elements); + return bt ? setFrom(bt) : setFrom( + ...(a as ValueSet).elements + .map((a) => unaryValue(a, op)) + ); +} + +const SetUnaryOperations: Record ValueSet> = { + id: a => a, + /* + first: a => setFrom(a.elements[0]), + last: a => setFrom(a.elements[a.elements.length - 1]), + */ +}; + +// TODO: support sin clamp to [-1, 1] etc. diff --git a/src/dataflow/eval/values/string/string-binary.ts b/src/dataflow/eval/values/string/string-binary.ts new file mode 100644 index 00000000000..9d10f67a3c3 --- /dev/null +++ b/src/dataflow/eval/values/string/string-binary.ts @@ -0,0 +1,80 @@ +import type { Lift, Value, ValueLogical, ValueString } from '../r-value'; +import { stringifyValue, Top , isBottom, isTop } from '../r-value'; +import { bottomTopGuard } from '../general'; +import type { RStringValue } from '../../../../r-bridge/lang-4.x/convert-values'; +import { liftLogical, ValueLogicalBot, ValueLogicalTop } from '../logical/logical-constants'; +import { guard } from '../../../../util/assert'; +import { expensiveTrace } from '../../../../util/log'; +import { ValueEvalLog } from '../../eval'; +import { liftString, ValueStringBot, ValueStringTop } from './string-constants'; + +/** + * Take two potentially lifted intervals and compare them with the given op. + * This propagates `top` and `bottom` values. + */ +export function binaryString( + a: Lift, + op: string, + b: Lift +): Value { + let res: Value = Top; + guard(op in Operations, `Unknown string binary operation: ${op}`); + res = bottomTopGuard(a) ?? Operations[op as keyof typeof Operations](a, b); + expensiveTrace(ValueEvalLog, () => ` * binaryString(${stringifyValue(a)}, ${op}, ${stringifyValue(b)}) = ${stringifyValue(res)}`); + return res; +} + +const Operations = { + 'add': (a, b) => stringHelper(a, b, (a, b) => a + b), + '<=': (a, b) => stringCheck(a, b, (a, b) => a <= b), + '<': (a, b) => stringCheck(a, b, (a, b) => a < b), + '>=': (a, b) => stringCheck(a, b, (a, b) => a >= b), + '>': (a, b) => stringCheck(a, b, (a, b) => a > b), + '==': (a, b) => stringCheck(a, b, (a, b) => a === b), + '!=': (a, b) => stringCheck(a, b, (a, b) => a !== b), + '===': (a, b) => stringCheck(a, b, (a, b) => a === b), + '!==': (a, b) => stringCheck(a, b, (a, b) => a !== b), + /* we do subsets as includes */ + '⊆': (a, b) => stringCheck(a, b, (a, b) => b.includes(a)), + '⊂': (a, b) => stringCheck(a, b, (a, b) => b.includes(a) && a !== b), + '⊇': (a, b) => stringCheck(a, b, (a, b) => a.includes(b)), + '⊃': (a, b) => stringCheck(a, b, (a, b) => a.includes(b) && a !== b) +} as const satisfies Record, b: Lift) => Value>; + +function stringHelper( + a: Lift, + b: Lift, + c: (a: string, b: string) => string +): ValueString { + if(isTop(a) || isTop(b)) { + return ValueStringTop; + } else if(isBottom(a) || isBottom(b)) { + return ValueStringBot; + } + const val = bottomTopGuard(a.value, b.value); + const aval = a.value as RStringValue; + const bval = b.value as RStringValue; + /** we ignore the string markers */ + return liftString(val ?? { + quotes: '"', + flag: undefined, + str: c(aval.str, bval.str) + }); +} + +function stringCheck( + a: Lift, + b: Lift, + c: (a: string, b: string) => boolean +): ValueLogical { + if(isTop(a) || isTop(b)) { + return ValueLogicalTop; + } else if(isBottom(a) || isBottom(b)) { + return ValueLogicalBot; + } + const val = bottomTopGuard(a.value, b.value); + const aval = a.value as RStringValue; + const bval = b.value as RStringValue; + /** we ignore the string markers */ + return liftLogical(val ?? c(aval.str, bval.str)); +} \ No newline at end of file diff --git a/src/dataflow/eval/values/string/string-constants.ts b/src/dataflow/eval/values/string/string-constants.ts new file mode 100644 index 00000000000..3082edddb8c --- /dev/null +++ b/src/dataflow/eval/values/string/string-constants.ts @@ -0,0 +1,26 @@ +import type { RStringValue } from '../../../../r-bridge/lang-4.x/convert-values'; +import type { Lift, ValueString } from '../r-value'; +import { Bottom, Top } from '../r-value'; + + +export function stringFrom(str: RStringValue | string): ValueString { + return { + type: 'string', + value: typeof str === 'string' ? { + quotes: '"', + str: str + } : str, + }; +} + +export function liftString(str: Lift): ValueString { + return { + type: 'string', + value: str + }; +} + + +export const ValueEmptyString = stringFrom(''); +export const ValueStringTop = liftString(Top); +export const ValueStringBot = liftString(Bottom); \ No newline at end of file diff --git a/src/dataflow/eval/values/string/string-unary.ts b/src/dataflow/eval/values/string/string-unary.ts new file mode 100644 index 00000000000..6a372243f1a --- /dev/null +++ b/src/dataflow/eval/values/string/string-unary.ts @@ -0,0 +1,68 @@ +import type { Lift, Value, ValueLogical, ValueString } from '../r-value'; +import { stringifyValue , asValue, Bottom , isBottom , isTop, Top } from '../r-value'; +import { bottomTopGuard } from '../general'; +import type { RStringValue } from '../../../../r-bridge/lang-4.x/convert-values'; +import { guard } from '../../../../util/assert'; +import { liftLogical, ValueLogicalBot, ValueLogicalTop } from '../logical/logical-constants'; +import { expensiveTrace } from '../../../../util/log'; +import { ValueEvalLog } from '../../eval'; +import { liftString } from './string-constants'; + +/** + * Take a potentially lifted strings and apply the given op. + * This propagates `top` and `bottom` values. + */ +export function unaryString( + a: Lift, + op: string +): Value { + let res: Value = Top; + guard(op in StringUnaryOperations, `Unknown string unary operation: ${op}`); + res = bottomTopGuard(a) ?? StringUnaryOperations[op as keyof typeof StringUnaryOperations](a); + expensiveTrace(ValueEvalLog, () => ` * unaryString(${stringifyValue(a)}, ${op}) = ${stringifyValue(res)}`); + return res; +} + +const StringUnaryOperations = { + id: a => a, + length: a => stringHelper(a, a => a.length.toString()), + isEmpty: a => stringCheck(a, a => a === ''), + isBlank: a => stringCheck(a, a => a.trim() === ''), + +} as const satisfies Record) => Value>; + +export type StringUnaryOperation = keyof typeof StringUnaryOperations; + +function stringHelper(a: Lift, op: (a: string) => string, fallback: Value = Top): Value { + if(isTop(a)) { + return fallback; + } else if(isBottom(a)) { + return a; + } + const val = bottomTopGuard(a.value); + const aval = a.value as RStringValue; + return liftString(val ?? { + flag: aval.flag, + quotes: aval.quotes, + str: op(aval.str) + }); +} + + +function stringCheck(a: Lift, c: (n: string) => boolean): ValueLogical { + const val = bottomTopGuard(a); + if(val) { + return val === Bottom ? ValueLogicalBot : ValueLogicalTop; + } + a = asValue(a); + if(isTop(a.value)) { + return ValueLogicalTop; + } else if(isBottom(a.value)) { + return ValueLogicalBot; + } else { + return liftLogical(c(a.value.str)); + } +} + + + diff --git a/src/dataflow/eval/values/value-binary.ts b/src/dataflow/eval/values/value-binary.ts new file mode 100644 index 00000000000..1778c6b157d --- /dev/null +++ b/src/dataflow/eval/values/value-binary.ts @@ -0,0 +1,89 @@ +import type { Lift, Value } from './r-value'; +import { stringifyValue , Bottom , isBottom, isTop, Top } from './r-value'; +import { intervalFromValues } from './intervals/interval-constants'; +import { binaryScalar } from './scalar/scalar-binary'; +import { binaryLogical } from './logical/logical-binary'; +import { binaryInterval } from './intervals/interval-binary'; +import { vectorFrom } from './vectors/vector-constants'; +import { binaryVector } from './vectors/vector-binary'; +import { binaryString } from './string/string-binary'; +import { guard } from '../../../util/assert'; +import { ValueLogicalFalse, ValueLogicalTrue } from './logical/logical-constants'; +import { expensiveTrace } from '../../../util/log'; +import { ValueEvalLog } from '../eval'; +import { setFrom } from './sets/set-constants'; +import { binarySet } from './sets/set-binary'; + +let binaryForType: Record Value> = undefined as unknown as Record Value>; + +function initialize() { + binaryForType ??= { + 'number': binaryScalar, + 'logical': binaryLogical, + 'interval': binaryInterval, + 'string': binaryString, + 'vector': binaryVector, + 'set': binarySet + } as Record Value>; +} + +export function binaryValue( + a: Lift, + op: string, + b: Lift +): Value { + expensiveTrace(ValueEvalLog, () => ` * binaryValue(${stringifyValue(a)}, ${op}, ${stringifyValue(b)})`); + if(isBottom(a) || isBottom(b)) { + if(op === '===') { + return a === b ? ValueLogicalTrue : ValueLogicalFalse; + } else if(op === '!==') { + return a !== b ? ValueLogicalTrue : ValueLogicalFalse; + } else { + return Bottom; + } + } + + if(a.type === 'set' && b.type !== 'set') { + return binaryValue(a, op, setFrom(b)); + } else if(b.type === 'set' && a.type !== 'set') { + return binaryValue(setFrom(a), op, b); + } else if(a.type === 'vector' && b.type !== 'vector') { + return binaryValue(a, op, vectorFrom({ elements: [b] })); + } else if(b.type === 'vector' && a.type !== 'vector') { + return binaryValue(vectorFrom({ elements: [a] }), op, b); + } + + if(isTop(a)) { + if(isTop(b)) { + if(op === '===') { + return ValueLogicalTrue; + } else if(op === '!==') { + return ValueLogicalFalse; + } else { + return Top; + } + } else { + return binaryEnsured(a, op, b, b.type); + } + } else if(isTop(b)) { + return binaryEnsured(a, op, b, a.type); + } + + if(a.type === b.type) { + return binaryEnsured(a, op, b, a.type); + } else if(a.type === 'interval' && b.type === 'number') { + return binaryEnsured(a, op, intervalFromValues(b, b), a.type); + } else if(a.type === 'number' && b.type === 'interval') { + return binaryEnsured(intervalFromValues(a, a), op, b, b.type); + } + return Top; +} + +function binaryEnsured( + a: A, op: string, b: B, + type: string +): Value { + initialize(); + guard(type in binaryForType, `Unknown binary operation for type: ${type}`); + return (binaryForType[type] as (a: Value, op: string, b: Value) => Value)(a, op, b); +} \ No newline at end of file diff --git a/src/dataflow/eval/values/value-unary.ts b/src/dataflow/eval/values/value-unary.ts new file mode 100644 index 00000000000..f5b534d3c0d --- /dev/null +++ b/src/dataflow/eval/values/value-unary.ts @@ -0,0 +1,48 @@ +import type { Lift, Value } from './r-value'; +import { stringifyValue , Bottom, isBottom, isTop, Top } from './r-value'; +import { unaryScalar } from './scalar/scalar-unary'; +import { unaryLogical } from './logical/logical-unary'; +import { unaryInterval } from './intervals/interval-unary'; +import { unaryVector } from './vectors/vector-unary'; +import { guard } from '../../../util/assert'; +import { expensiveTrace } from '../../../util/log'; +import { ValueEvalLog } from '../eval'; +import { unarySet } from './sets/set-unary'; +import { unaryString } from './string/string-unary'; + +let unaryForType: Record Value> = undefined as unknown as Record Value>; + +function initialize() { + unaryForType ??= { + 'number': unaryScalar, + 'logical': unaryLogical, + 'interval': unaryInterval, + 'string': unaryString, + 'vector': unaryVector, + 'set': unarySet + } as Record Value>; +} + + +export function unaryValue>( + a: A, + op: string +): Value { + expensiveTrace(ValueEvalLog, () => ` * unaryValue(${stringifyValue(a)}, ${op})`); + + if(isBottom(a)) { + return Bottom; + } else if(isTop(a)) { // if we do not even know the vector shape, there is not really anything to do + return Top; + } + return unaryEnsured(a, op, a.type); +} + +function unaryEnsured( + a: Value, op: string, + type: string +): Value { + initialize(); + guard(unaryForType[type], `No unary operation for type ${type}`); + return (unaryForType[type] as (a: Value, op: string) => Value)(a, op); +} \ No newline at end of file diff --git a/src/dataflow/eval/values/vectors/vector-binary.ts b/src/dataflow/eval/values/vectors/vector-binary.ts new file mode 100644 index 00000000000..84a0450dd40 --- /dev/null +++ b/src/dataflow/eval/values/vectors/vector-binary.ts @@ -0,0 +1,101 @@ +import type { Lift, Value, ValueVector } from '../r-value'; +import { stringifyValue, Top , isBottom, isTop } from '../r-value'; +import { vectorFrom } from './vector-constants'; +import { bottomTopGuard } from '../general'; +import { binaryValue } from '../value-binary'; +import { ValueLogicalBot, ValueLogicalFalse, ValueLogicalTrue } from '../logical/logical-constants'; +import { expensiveTrace } from '../../../../util/log'; +import { ValueEvalLog } from '../../eval'; +import { toTruthy } from '../logical/logical-check'; + +/** + * Take two potentially lifted vectors and apply the given op. + * This propagates `top` and `bottom` values. + */ +export function binaryVector( + a: ValueVector, + op: string, + b: ValueVector +): ValueVector { + let res: Value = Top; + if(op in VectorBinaryOperations) { + res = VectorBinaryOperations[op](a, b); + } else { + res = applyPairWise(a, op, b, true); + } + expensiveTrace(ValueEvalLog, () => ` * binaryVector(${stringifyValue(a)}, ${op}, ${stringifyValue(b)}) = ${stringifyValue(res)}`); + return res; +} + +function recycleUntilEqualLength( + a: ValueVector, + b: ValueVector +): [ValueVector, ValueVector] { + const aLen = a.elements.length; + const bLen = b.elements.length; + if(aLen === bLen) { + return [a, b]; + } + // make vectors same length reusing elements + if(aLen < bLen) { + const [x, y] = recycleUntilEqualLength(b, a); + return [y, x]; + } + const bElements = b.elements.slice(); + while(bElements.length < aLen) { + bElements.push(bElements[bElements.length % bLen]); + } + return [a, vectorFrom({ elements: bElements, domain: b.elementDomain })]; +} + +function applyPairWise( + a: ValueVector, + op: string, + b: ValueVector, + recycle: boolean +): ValueVector { + let elements: Lift | undefined = bottomTopGuard(a.elements, b.elements); + if((a as ValueVector).elements.length === 0 || (b as ValueVector).elements.length === 0) { + elements = []; + } + if(!elements) { + const [aV, bV] = recycle ? recycleUntilEqualLength(a as ValueVector, b as ValueVector) : [a as ValueVector, b as ValueVector]; + elements = aV.elements + .map((a, i) => bV.elements[i] ? binaryValue(a, op, bV.elements[i]) : Top); + } + const domain = bottomTopGuard(a.elementDomain) ?? binaryValue(a.elementDomain, op, b.elementDomain); + return vectorFrom({ + elements, + domain + }); +} + +const VectorBinaryOperations = { + '===': (a, b) => { + if(a === b) { + return ValueLogicalTrue; + } + const compareDomains = binaryValue(a.elementDomain, '===', b.elementDomain); + if(!toTruthy(compareDomains)) { + return ValueLogicalFalse; + } + const res = applyPairWise(a, '===', b, false); + if(isTop(res.elements)) { + return toTruthy(compareDomains); + } else if(isBottom(res.elements)) { + return ValueLogicalBot; + } else { + if(res.elements.length === 0) { + return ((a as ValueVector).elements.length === 0 || (b as ValueVector).elements.length === 0) + ? ValueLogicalTrue : ValueLogicalBot; + } + return res.elements.reduce((acc, cur) => binaryValue(acc, 'and', cur), ValueLogicalTrue); + } + }, + // TODO %*% etc. + /* + first: a => vectorFrom(a.elements[0]), + last: a => vectorFrom(a.elements[a.elements.length - 1]), + */ +} as Record ValueVector>; +// TODO: support sin clamp to [-1, 1] etc. diff --git a/src/dataflow/eval/values/vectors/vector-constants.ts b/src/dataflow/eval/values/vectors/vector-constants.ts new file mode 100644 index 00000000000..505a4c2dcf4 --- /dev/null +++ b/src/dataflow/eval/values/vectors/vector-constants.ts @@ -0,0 +1,58 @@ +import type { Lift, Value, ValueVector } from '../r-value'; +import { isValue , isBottom, isTop , Bottom , Top } from '../r-value'; +import { bottomTopGuard } from '../general'; +import { guard } from '../../../../util/assert'; +import { binaryValue } from '../value-binary'; + + +function inferVectorDomain(values: Lift): Value { + let domain: Value | undefined = bottomTopGuard(values); + if(domain) { + return domain; + } + const elements = values as Value[]; + if(elements.length === 0) { + return Top; + } + domain = elements[0]; + for(let i = 1; i < elements.length; i++) { + domain = binaryValue(domain, 'union', elements[i]); + } + return domain; +} + +export function vectorFrom>({ elements, domain = inferVectorDomain(elements) }: { domain?: Value, elements: V}): ValueVector { + guard(isTop(elements) || isBottom(elements) || Array.isArray(elements), 'Expected array of values'); + return { + type: 'vector', + elements, + elementDomain: domain + }; +} + +// TODO: pull up sets +function flattenVectorElements(s: Lift): Lift { + return bottomTopGuard(s) ?? (s as Value[]).flatMap(e => { + return e.type === 'vector' ? flattenVectorElements(e.elements): + e.type === 'set' && isValue(e.elements) && e.elements.length === 1 ? + e.elements[0].type === 'vector' ? flattenVectorElements(e.elements[0].elements) : e.elements : + e; + }); +} + +export function flattenVector(...elements: V): ValueVector { + return vectorFrom({ elements: flattenVectorElements(elements) }); +} + + +export const ValueEmptyVector = vectorFrom({ domain: Top, elements: [] }); +export const ValueVectorTop: ValueVector = { + type: 'vector', + elementDomain: Top, + elements: Top +}; +export const ValueVectorBottom: ValueVector = { + type: 'vector', + elementDomain: Bottom, + elements: Bottom +}; diff --git a/src/dataflow/eval/values/vectors/vector-unary.ts b/src/dataflow/eval/values/vectors/vector-unary.ts new file mode 100644 index 00000000000..47d9958bb37 --- /dev/null +++ b/src/dataflow/eval/values/vectors/vector-unary.ts @@ -0,0 +1,48 @@ +import type { Value, ValueVector } from '../r-value'; +import { stringifyValue, Top } from '../r-value'; +import { vectorFrom } from './vector-constants'; +import { bottomTopGuard } from '../general'; +import { unaryValue } from '../value-unary'; +import { expensiveTrace } from '../../../../util/log'; +import { ValueEvalLog } from '../../eval'; + +/** + * Take a potentially lifted vector and apply the given op. + * This propagates `top` and `bottom` values. + */ +export function unaryVector( + a: A, + op: string +): ValueVector { + let res: Value = Top; + if(op in VectorUnaryOperations) { + res = VectorUnaryOperations[op](a); + } else { + res = applyComponentWise(a, op); + } + expensiveTrace(ValueEvalLog, () => ` * unaryVector(${stringifyValue(a)}, ${op}) = ${stringifyValue(res)}`); + return res; +} + +function applyComponentWise( + a: ValueVector, + op: string +): ValueVector { + const elements = bottomTopGuard(a.elements) ?? (a as ValueVector).elements + .map((a) => unaryValue(a, op)); + const domain = bottomTopGuard(a.elementDomain) ?? unaryValue(a.elementDomain, op); + return vectorFrom({ + elements, + domain + }); +} + +const VectorUnaryOperations: Record ValueVector> = { + id: a => a, + /* + first: a => vectorFrom(a.elements[0]), + last: a => vectorFrom(a.elements[a.elements.length - 1]), + */ +}; + +// TODO: support sin clamp to [-1, 1] etc. diff --git a/src/util/lazy.ts b/src/util/lazy.ts new file mode 100644 index 00000000000..8a081a9ecd2 --- /dev/null +++ b/src/util/lazy.ts @@ -0,0 +1,38 @@ +/** + * A type that can be either a value or a function that returns a value (to be only calculated on a need-to-have basis). + * + * @see {@link Lazy} + * @see {@link isLazy} + * @see {@link force} + */ +export type CanBeLazy = V | Lazy; +/** + * A function that returns a value (to be only calculated on an need-to-have basis). + * + * @see {@link CanBeLazy} + * @see {@link isLazy} + * @see {@link force} + */ +export type Lazy = () => V; + +/** + * Check if a value is a {@link Lazy|lazy} value. + * + * @see {@link CanBeLazy} + * @see {@link Lazy} + * @see {@link force} + */ +export function isLazy(v: CanBeLazy): v is Lazy { + return typeof v === 'function'; +} + +/** + * Force a value to be calculated if it is lazy. + * + * @see {@link CanBeLazy} + * @see {@link Lazy} + * @see {@link isLazy} + */ +export function force(v: CanBeLazy): V { + return isLazy(v) ? v() : v; +} \ No newline at end of file diff --git a/src/util/logic.ts b/src/util/logic.ts index adc3dfc1559..5963ed476e3 100644 --- a/src/util/logic.ts +++ b/src/util/logic.ts @@ -1,4 +1,4 @@ -// diverging from boolean | maybe requires explicit handling +/** diverging from boolean, maybe requires explicit handling */ export enum Ternary { Always = 'always', Maybe = 'maybe', diff --git a/test/functionality/dataflow/eval/eval-simple.test.ts b/test/functionality/dataflow/eval/eval-simple.test.ts new file mode 100644 index 00000000000..e47425f13d0 --- /dev/null +++ b/test/functionality/dataflow/eval/eval-simple.test.ts @@ -0,0 +1,61 @@ +import { assert, describe, test } from 'vitest'; +import type { Value } from '../../../../src/dataflow/eval/values/r-value'; +import { Top , stringifyValue } from '../../../../src/dataflow/eval/values/r-value'; +import { withShell } from '../../_helper/shell'; +import { createDataflowPipeline } from '../../../../src/core/steps/pipeline/default-pipelines'; +import { requestFromInput } from '../../../../src/r-bridge/retriever'; +import { evalRExpression } from '../../../../src/dataflow/eval/eval'; +import { initializeCleanEnvironments } from '../../../../src/dataflow/environments/environment'; +import { + intervalFrom, + intervalFromValues, ValueIntervalMinusOneToOne +} from '../../../../src/dataflow/eval/values/intervals/interval-constants'; +import { + getScalarFromInteger, ValueNumberPositiveInfinity +} from '../../../../src/dataflow/eval/values/scalar/scalar-constants'; +import { stringFrom } from '../../../../src/dataflow/eval/values/string/string-constants'; +import { binaryValue } from '../../../../src/dataflow/eval/values/value-binary'; +import { toTruthy } from '../../../../src/dataflow/eval/values/logical/logical-check'; +import { ValueLogicalMaybe } from '../../../../src/dataflow/eval/values/logical/logical-constants'; +import { setFrom } from '../../../../src/dataflow/eval/values/sets/set-constants'; +import { vectorFrom } from '../../../../src/dataflow/eval/values/vectors/vector-constants'; + +describe.sequential('eval', withShell(shell => { + function assertEval(code: string, expect: Value) { + test(code + ' => ' + stringifyValue(expect), async() => { + const results = await createDataflowPipeline(shell, { + request: requestFromInput(code) + }).allRemainingSteps(); + const result = evalRExpression(results.normalize.ast, results.dataflow.graph, initializeCleanEnvironments()); + const isExpected = binaryValue(result, '===', expect); + assert.isTrue(toTruthy(isExpected).value === true, + `Expected ${stringifyValue(result)} to be ${stringifyValue(expect)} (${stringifyValue(isExpected)})` + ); + }); + } + + describe('constants and simple math', () => { + assertEval('1L', intervalFromValues(getScalarFromInteger(1, true))); + assertEval('1', intervalFromValues(getScalarFromInteger(1, false))); + assertEval('-1', intervalFromValues(getScalarFromInteger(-1, false))); + assertEval('1 + 2', intervalFromValues(getScalarFromInteger(3, false))); + assertEval('1 + 2 * 7 - 8', intervalFromValues(getScalarFromInteger(7, false))); + assertEval('c(1L, 2L)', vectorFrom({ elements: [intervalFrom(1, 1), intervalFrom(2,2)] })); + assertEval('c(1L, 2 * 7)', vectorFrom({ elements: [intervalFrom(1, 1), intervalFrom(14, 14)] })); + assertEval('c(1L, c(2L))', vectorFrom({ elements: [intervalFrom(1, 1), intervalFrom(2,2)] })); + assertEval('c(1L, c(2L,3L), 4L)', vectorFrom({ elements: [intervalFrom(1, 1), intervalFrom(2,2), intervalFrom(3,3), intervalFrom(4,4)] })); + }); + describe('Use variables', () => { + assertEval('u', Top); + assertEval('sign(u)', setFrom(ValueIntervalMinusOneToOne)); + assertEval('sign(u) > 2', ValueLogicalMaybe); + assertEval('sign(runif(2)) > 2', vectorFrom({ elements: Top, domain: ValueLogicalMaybe })); + assertEval('abs(u) + 1', intervalFromValues(getScalarFromInteger(1, false), ValueNumberPositiveInfinity)); + assertEval('abs(sign(u)) + 1', intervalFromValues(getScalarFromInteger(1, false), getScalarFromInteger(2, false))); + }); + describe('Strings', () => { + assertEval('"foo"', stringFrom('foo')); + assertEval('paste("foo", "bar")', stringFrom('foo bar')); + assertEval('paste0("foo", "bar")', stringFrom('foobar')); + }); +})); \ No newline at end of file diff --git a/test/functionality/dataflow/eval/interval/eval-interval-simple.test.ts b/test/functionality/dataflow/eval/interval/eval-interval-simple.test.ts new file mode 100644 index 00000000000..c4da912e124 --- /dev/null +++ b/test/functionality/dataflow/eval/interval/eval-interval-simple.test.ts @@ -0,0 +1,151 @@ +import { assert, describe, test } from 'vitest'; +import type { Value, ValueInterval } from '../../../../../src/dataflow/eval/values/r-value'; +import { stringifyValue } from '../../../../../src/dataflow/eval/values/r-value'; +import { binaryInterval } from '../../../../../src/dataflow/eval/values/intervals/interval-binary'; +import { + intervalFrom, ValueIntervalBottom, + ValueIntervalTop +} from '../../../../../src/dataflow/eval/values/intervals/interval-constants'; +import { + ValueIntegerOne, + ValueNumberEpsilon +} from '../../../../../src/dataflow/eval/values/scalar/scalar-constants'; +import { unaryInterval } from '../../../../../src/dataflow/eval/values/intervals/interval-unary'; +import { binaryValue } from '../../../../../src/dataflow/eval/values/value-binary'; +import { toTruthy } from '../../../../../src/dataflow/eval/values/logical/logical-check'; +import { unaryValue } from '../../../../../src/dataflow/eval/values/value-unary'; + +function i(l: '[' | '(', lv: number, rv: number, r: ']' | ')') { + return intervalFrom(lv, rv, l === '[', r === ']'); +} + +describe('interval', () => { + function shouldBeInterval(val: Value, expect: Value) { + const res = binaryValue(val, '===', expect); + const t = toTruthy(res); + assert.isTrue( + t.type === 'logical' && t.value, + `Expected ${stringifyValue(val)} to be ${stringifyValue(expect)}; but: ${stringifyValue(res)}` + ); + } + describe('unary', () => { + const tests = [ + { label: 'id', value: intervalFrom(1), expect: { type: 'interval', start: ValueIntegerOne, startInclusive: true, end: ValueIntegerOne, endInclusive: true } }, + { label: 'negate', value: ValueIntervalTop, expect: ValueIntervalTop }, + { label: 'negate', value: ValueIntervalBottom, expect: ValueIntervalBottom }, + { label: 'negate', value: intervalFrom(1), expect: intervalFrom(-1) }, + { label: 'negate', value: intervalFrom(2, 4), expect: i('[', -4, -2, ']') }, + { label: 'negate', value: intervalFrom(2, 4, false), expect: i('[', -4, -2, ')') }, + { label: 'negate', value: intervalFrom(2, 4, false, false), expect: i('(', -4, -2, ')') }, + { label: 'abs', value: ValueIntervalTop, expect: ValueIntervalTop }, + { label: 'abs', value: ValueIntervalBottom, expect: ValueIntervalBottom }, + { label: 'abs', value: intervalFrom(1), expect: i('[', 1, 1, ']') }, + { label: 'abs', value: intervalFrom(2, 4), expect: i('[', 2, 4, ']') }, + { label: 'abs', value: intervalFrom(-1, 1), expect: i('[', 0, 1, ']') }, + { label: 'abs', value: intervalFrom(-1, 1, false), expect: i('[', 0, 1, ']') }, + { label: 'abs', value: intervalFrom(-1, 1, false, false), expect: i('[', 0, 1, ')') }, + { label: 'abs', value: intervalFrom(-42, 12, false), expect: i('[', 0, 42, ')') }, + { label: 'ceil', value: ValueIntervalTop, expect: ValueIntervalTop }, + { label: 'ceil', value: ValueIntervalBottom, expect: ValueIntervalBottom }, + { label: 'ceil', value: intervalFrom(1), expect: i('[', 1, 1, ']') }, + { label: 'ceil', value: intervalFrom(2, 4), expect: i('[', 2, 4, ']') }, + { label: 'ceil', value: intervalFrom(2, 4, false), expect: i('[', 3, 4, ']') }, + { label: 'ceil', value: intervalFrom(2, 4, false, false), expect: i('[', 3, 4, ']') }, + { label: 'ceil', value: intervalFrom(-1, 1), expect: i('[', -1, 1, ']') }, + { label: 'ceil', value: intervalFrom(-1, 1, false), expect: i('[', 0, 1, ']') }, + { label: 'ceil', value: intervalFrom(-1.5, 12), expect: i('[', -1, 12, ']') }, + { label: 'ceil', value: intervalFrom(-1.5, 12, false), expect: i('[', -1, 12, ']') }, + { label: 'floor', value: ValueIntervalTop, expect: ValueIntervalTop }, + { label: 'floor', value: ValueIntervalBottom, expect: ValueIntervalBottom }, + { label: 'floor', value: intervalFrom(1), expect: i('[', 1, 1, ']') }, + { label: 'floor', value: intervalFrom(2, 4), expect: i('[', 2, 4, ']') }, + { label: 'floor', value: intervalFrom(2, 4, false), expect: i('[', 2, 4, ']') }, + { label: 'floor', value: intervalFrom(2, 4, false, false), expect: i('[', 2, 3, ']') }, + { label: 'floor', value: intervalFrom(-1, 1), expect: i('[', -1, 1, ']') }, + { label: 'floor', value: intervalFrom(-1, 1, true, false), expect: i('[', -1, 0, ']') }, + { label: 'floor', value: intervalFrom(-1.5, 1), expect: i('[', -2, 1, ']') }, + { label: 'floor', value: intervalFrom(-1.5, -1, true, false), expect: i('[', -2, -2, ']') }, + { label: 'round', value: ValueIntervalTop, expect: ValueIntervalTop }, + { label: 'round', value: ValueIntervalBottom, expect: ValueIntervalBottom }, + { label: 'round', value: intervalFrom(1), expect: i('[', 1, 1, ']') }, + { label: 'round', value: intervalFrom(2, 4), expect: i('[', 2, 4, ']') }, + { label: 'round', value: intervalFrom(2, 4, false), expect: i('[', 2, 4, ']') }, + { label: 'round', value: intervalFrom(2, 4, false, true), expect: i('[', 2, 4, ']') }, + { label: 'round', value: intervalFrom(-1, 1), expect: i('[', -1, 1, ']') }, + { label: 'round', value: intervalFrom(-1, 1.5), expect: i('[', -1, 2, ']') }, + { label: 'round', value: intervalFrom(-1, 1.5, false, true), expect: i('[', -1, 2, ']') }, + { label: 'round', value: intervalFrom(-1, 1.5, false, false), expect: i('[', -1, 1, ']') }, + { label: 'round', value: intervalFrom(-1.5, -1, false, false), expect: i('[', -1, -1, ']') }, + { label: 'sign', value: ValueIntervalTop, expect: i('[', -1, 1, ']') }, + { label: 'sign', value: ValueIntervalBottom, expect: ValueIntervalBottom }, + { label: 'sign', value: intervalFrom(1), expect: i('[', 1, 1, ']') }, + { label: 'sign', value: intervalFrom(2, 4), expect: i('[', 1, 1, ']') }, + { label: 'sign', value: intervalFrom(2, 4, false), expect: i('[', 1, 1, ']') }, + { label: 'sign', value: intervalFrom(-1, 1), expect: i('[', -1, 1, ']') }, + { label: 'sign', value: intervalFrom(-42, 0, false), expect: i('[', -1, 0, ']') }, + { label: 'sign', value: intervalFrom(-42, 0, false, false), expect: i('[', -1, -1, ']') }, + { label: 'sign', value: intervalFrom(-1.5, -1, false, false), expect: i('[', -1, -1, ']') }, + { label: 'flip', value: ValueIntervalTop, expect: ValueIntervalTop }, + { label: 'flip', value: ValueIntervalBottom, expect: ValueIntervalBottom }, + { label: 'flip', value: intervalFrom(1), expect: i('[', 1, 1, ']') }, + { label: 'flip', value: intervalFrom(1, 2, true, false), expect: i('(', 1 / 2, 1, ']') }, + { label: 'highest', value: ValueIntervalTop, expect: ValueIntervalTop }, + { label: 'highest', value: ValueIntervalBottom, expect: ValueIntervalBottom }, + { label: 'highest', value: intervalFrom(-42, 0), expect: i('[', 0, 0, ']') }, + { label: 'highest', value: intervalFrom(-1.5, -1, false, false), expect: i('[', -1 - ValueNumberEpsilon.value.num, -1 - ValueNumberEpsilon.value.num, ']') }, + { label: 'lowest', value: ValueIntervalTop, expect: ValueIntervalTop }, + { label: 'lowest', value: ValueIntervalBottom, expect: ValueIntervalBottom }, + { label: 'lowest', value: intervalFrom(-42, 0), expect: i('[', -42, -42, ']') }, + { label: 'lowest', value: intervalFrom(-1.5, -1, false, false), expect: i('[', -1.5 + ValueNumberEpsilon.value.num, -1.5 + ValueNumberEpsilon.value.num, ']') }, + { label: 'toClosed', value: ValueIntervalTop, expect: ValueIntervalTop }, + { label: 'toClosed', value: ValueIntervalBottom, expect: ValueIntervalBottom }, + { label: 'toClosed', value: intervalFrom(-1.5, 12, false), expect: i('[', -1.5 + ValueNumberEpsilon.value.num, 12, ']') }, + ] as const; + for(const t of tests) { + describe(t.label, () => { + test(t.label + ' ' + stringifyValue(t.value) + ' => ' + stringifyValue(t.expect), () => { + shouldBeInterval(unaryInterval(t.value, t.label), t.expect); + }); + }); + } + }); + describe('binary', () => { + const tests = [ + { label: 'add', left: intervalFrom(1), right: intervalFrom(1), expect: i('[', 2, 2, ']') }, + { label: 'add', left: intervalFrom(1), right: intervalFrom(0), expect: i('[', 1, 1, ']') }, + { label: 'add', left: intervalFrom(-1, 1), right: intervalFrom(-2, 4, true, false), expect: i('[', -3, 5, ')') }, + { label: 'add', left: intervalFrom(-3, -1, false, false), right: intervalFrom(-2, -1, false, false), expect: i('(', -5, -2, ')') }, + { label: 'add', left: ValueIntervalTop, right: intervalFrom(1), expect: ValueIntervalTop }, + { label: 'add', left: ValueIntervalBottom, right: intervalFrom(1), expect: ValueIntervalBottom }, + + { label: 'sub', left: intervalFrom(1), right: intervalFrom(1), expect: i('[', 0, 0, ']') }, + { label: 'sub', left: intervalFrom(2, 8), right: intervalFrom(2, 3), expect: i('(', -1, 6, ']') }, + { label: 'sub', left: intervalFrom(2, 8), right: intervalFrom(2, 3, false, false), expect: i('(', -1, 6, ']') }, + { label: 'sub', left: intervalFrom(2, 8), right: intervalFrom(2, 3, true, false), expect: i('[', -1, 6, ')') }, + { label: 'sub', left: intervalFrom(2, 8), right: intervalFrom(-2, -1, false, true), expect: i('[', 3, 10, ']') }, + + { label: 'sub', left: ValueIntervalTop, right: intervalFrom(1), expect: ValueIntervalTop }, + { label: 'sub', left: ValueIntervalBottom, right: intervalFrom(1), expect: ValueIntervalBottom }, + + { label: 'mul', left: ValueIntervalTop, right: intervalFrom(1), expect: ValueIntervalTop }, + { label: 'mul', left: ValueIntervalBottom, right: intervalFrom(1), expect: ValueIntervalBottom }, + { label: 'mul', left: intervalFrom(1), right: intervalFrom(0), expect: i('[', 0, 0, ']') }, + { label: 'mul', left: intervalFrom(1), right: intervalFrom(2), expect: i('[', 2, 2, ']') }, + ] as const; + + for(const t of tests) { + describe(t.label, () => { + const commutative = ['add', 'mul'].includes(t.label); + function f(l: ValueInterval, r: ValueInterval) { + return test(stringifyValue(l) + ' ' + t.label + ' ' + stringifyValue(r) + ' => ' + stringifyValue(t.expect), () => { + shouldBeInterval(unaryValue(binaryInterval(l, t.label, r), 'toClosed'), unaryValue(t.expect, 'toClosed')); + }); + } + f(t.left, t.right); + if(commutative && JSON.stringify(t.left) !== JSON.stringify(t.right)) { + f(t.right, t.left); + } + }); + } + }); +}); \ No newline at end of file diff --git a/test/functionality/dataflow/eval/logical/eval-logical-simple.test.ts b/test/functionality/dataflow/eval/logical/eval-logical-simple.test.ts new file mode 100644 index 00000000000..0a44242fbb1 --- /dev/null +++ b/test/functionality/dataflow/eval/logical/eval-logical-simple.test.ts @@ -0,0 +1,44 @@ +import { assert, describe, test } from 'vitest'; +import type { Value } from '../../../../../src/dataflow/eval/values/r-value'; +import { + liftLogical, + ValueLogicalFalse, + ValueLogicalMaybe, + ValueLogicalTrue +} from '../../../../../src/dataflow/eval/values/logical/logical-constants'; +import { unaryLogical } from '../../../../../src/dataflow/eval/values/logical/logical-unary'; +import { binaryLogical } from '../../../../../src/dataflow/eval/values/logical/logical-binary'; +import { isTruthy } from '../../../../../src/dataflow/eval/values/logical/logical-check'; +import { binaryValue } from '../../../../../src/dataflow/eval/values/value-binary'; + +describe('logical', () => { + function shouldBeBool(value: Value, expect: boolean | 'maybe') { + const truthy = isTruthy(binaryValue(value, '===', liftLogical(expect))); + assert.isTrue(truthy); + } + describe('unary', () => { + test.each([ + { label: '(sanity) should be one', value: ValueLogicalTrue, expect: true }, + { label: 'not should work', value: unaryLogical(ValueLogicalTrue, 'not'), expect: false }, + ])('$label', ({ value, expect }) => { + shouldBeBool(value, expect); + }); + }); + describe('binary', () => { + test.each([ + { label: '1 && 1', value: binaryLogical(ValueLogicalTrue, 'and', ValueLogicalTrue), expect: true }, + { label: '1 && 0', value: binaryLogical(ValueLogicalTrue, 'and', ValueLogicalFalse), expect: false }, + { label: '1 && ?', value: binaryLogical(ValueLogicalTrue, 'and', ValueLogicalMaybe), expect: 'maybe' as const }, + { label: '1 && ?', value: binaryLogical(ValueLogicalTrue, 'and', ValueLogicalMaybe), expect: 'maybe' as const }, + { label: '? && ?', value: binaryLogical(ValueLogicalMaybe, 'and', ValueLogicalMaybe), expect: 'maybe' as const }, + { label: '? implies 0', value: binaryLogical(ValueLogicalMaybe, 'implies', ValueLogicalFalse), expect: 'maybe' as const }, + { label: '0 implies ?', value: binaryLogical(ValueLogicalFalse, 'implies', ValueLogicalMaybe), expect: true }, + { label: '1 iff 1', value: binaryLogical(ValueLogicalTrue, 'iff', ValueLogicalTrue), expect: true }, + { label: '0 iff 1', value: binaryLogical(ValueLogicalFalse, 'iff', ValueLogicalTrue), expect: false }, + { label: '0 iff ?', value: binaryLogical(ValueLogicalFalse, 'iff', ValueLogicalMaybe), expect: 'maybe' as const }, + { label: '? iff ?', value: binaryLogical(ValueLogicalMaybe, 'iff', ValueLogicalMaybe), expect: 'maybe' as const }, + ])('$label', ({ value, expect }) => { + shouldBeBool(value, expect); + }); + }); +}); \ No newline at end of file diff --git a/test/functionality/dataflow/eval/scalar/eval-scalar-simple.test.ts b/test/functionality/dataflow/eval/scalar/eval-scalar-simple.test.ts new file mode 100644 index 00000000000..418d3bec30c --- /dev/null +++ b/test/functionality/dataflow/eval/scalar/eval-scalar-simple.test.ts @@ -0,0 +1,40 @@ +import { assert, describe, test } from 'vitest'; +import { + getScalarFromInteger, + ValueIntegerOne, + ValueIntegerZero +} from '../../../../../src/dataflow/eval/values/scalar/scalar-constants'; +import { guard } from '../../../../../src/util/assert'; +import { unaryScalar } from '../../../../../src/dataflow/eval/values/scalar/scalar-unary'; +import type { Value } from '../../../../../src/dataflow/eval/values/r-value'; +import { isBottom, isTop } from '../../../../../src/dataflow/eval/values/r-value'; +import { binaryScalar } from '../../../../../src/dataflow/eval/values/scalar/scalar-binary'; + +describe('scalar', () => { + function shouldBeNum(value: Value, expect: number) { + guard(!isTop(value) && !isBottom(value)); + guard(value.type === 'number' && 'num' in value.value); + assert.equal(value.value.num, expect); + } + describe('unary', () => { + test.each([ + { label: '(sanity) should be one', value: ValueIntegerOne, expect: 1 }, + { label: 'negate should work', value: unaryScalar(ValueIntegerOne, 'negate'), expect: -1 }, + ])('$label', ({ value, expect }) => { + shouldBeNum(value, expect); + }); + }); + describe('binary', () => { + test.each([ + { label: '1 + 1', value: binaryScalar(ValueIntegerOne, 'add', ValueIntegerOne), expect: 2 }, + { label: '1 + 0', value: binaryScalar(ValueIntegerOne, 'add', ValueIntegerZero), expect: 1 }, + { label: '1 - 1', value: binaryScalar(ValueIntegerOne, 'sub', ValueIntegerOne), expect: 0 }, + { label: '1 * 0', value: binaryScalar(ValueIntegerOne, 'mul', ValueIntegerZero), expect: 0 }, + { label: '1 * 2', value: binaryScalar(ValueIntegerOne, 'mul', getScalarFromInteger(2)), expect: 2 }, + { label: 'mod(5, 2)', value: binaryScalar(getScalarFromInteger(5), 'mod', getScalarFromInteger(2)), expect: 1 }, + { label: 'mod(5, 3)', value: binaryScalar(getScalarFromInteger(5), 'mod', getScalarFromInteger(3)), expect: 2 }, + ])('$label', ({ value, expect }) => { + shouldBeNum(value, expect); + }); + }); +}); \ No newline at end of file