From a872439ef7fdd655328171ed9ed03ec2243611da Mon Sep 17 00:00:00 2001 From: Florian Sihler Date: Fri, 14 Mar 2025 14:57:47 +0100 Subject: [PATCH 1/6] feat: fun stage 1 --- src/dataflow/eval/eval.ts | 10 +++ src/dataflow/eval/values/general.ts | 26 +++++++ .../eval/values/intervals/interval-binary.ts | 73 +++++++++++++++++++ .../eval/values/intervals/interval-check.ts | 17 +++++ .../eval/values/intervals/interval-compare.ts | 32 ++++++++ .../values/intervals/interval-constants.ts | 25 +++++++ .../eval/values/intervals/interval-unary.ts | 29 ++++++++ .../eval/values/logical/logical-binary.ts | 32 ++++++++ .../eval/values/logical/logical-check.ts | 25 +++++++ .../eval/values/logical/logical-constants.ts | 14 ++++ .../eval/values/logical/logical-unary.ts | 25 +++++++ src/dataflow/eval/values/r-value.ts | 61 ++++++++++++++++ .../eval/values/scalar/scalar-binary.ts | 45 ++++++++++++ .../eval/values/scalar/scalar-compare.ts | 38 ++++++++++ .../eval/values/scalar/scalar-constants.ts | 16 ++++ .../eval/values/scalar/scalar-unary.ts | 31 ++++++++ src/util/logic.ts | 2 +- .../eval/scalar/eval-scalar-unary.test.ts | 42 +++++++++++ 18 files changed, 542 insertions(+), 1 deletion(-) create mode 100644 src/dataflow/eval/eval.ts create mode 100644 src/dataflow/eval/values/general.ts create mode 100644 src/dataflow/eval/values/intervals/interval-binary.ts create mode 100644 src/dataflow/eval/values/intervals/interval-check.ts create mode 100644 src/dataflow/eval/values/intervals/interval-compare.ts create mode 100644 src/dataflow/eval/values/intervals/interval-constants.ts create mode 100644 src/dataflow/eval/values/intervals/interval-unary.ts create mode 100644 src/dataflow/eval/values/logical/logical-binary.ts create mode 100644 src/dataflow/eval/values/logical/logical-check.ts create mode 100644 src/dataflow/eval/values/logical/logical-constants.ts create mode 100644 src/dataflow/eval/values/logical/logical-unary.ts create mode 100644 src/dataflow/eval/values/r-value.ts create mode 100644 src/dataflow/eval/values/scalar/scalar-binary.ts create mode 100644 src/dataflow/eval/values/scalar/scalar-compare.ts create mode 100644 src/dataflow/eval/values/scalar/scalar-constants.ts create mode 100644 src/dataflow/eval/values/scalar/scalar-unary.ts create mode 100644 test/functionality/dataflow/eval/scalar/eval-scalar-unary.test.ts diff --git a/src/dataflow/eval/eval.ts b/src/dataflow/eval/eval.ts new file mode 100644 index 00000000000..66df2c962dc --- /dev/null +++ b/src/dataflow/eval/eval.ts @@ -0,0 +1,10 @@ +import type { RNode } from '../../r-bridge/lang-4.x/ast/model/model'; +import type { DataflowGraph } from '../graph/graph'; +import type { REnvironmentInformation } from '../environments/environment'; + +// x <- 5;eval(parse(text=paste("foo", x))) + +// 2 + 2 +export function eval(node: RNode, dfg: DataflowGraph, env: REnvironmentInformation): RValue { + +} \ 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..d51c526f9a0 --- /dev/null +++ b/src/dataflow/eval/values/general.ts @@ -0,0 +1,26 @@ +import type { Lift } from './r-value'; +import { Bottom, isBottom, isTop, Top } from './r-value'; + +/** + * Takes two potentially lifted ops and returns `Top` or `Bottom` if either is `Top` or `Bottom`. + */ +export function bottomTopGuard, B extends Lift>( + a: A, + b: B +): typeof Top | typeof Bottom | undefined { + if(isBottom(a) || isBottom(b)) { + return Bottom; + } else if(isTop(a) || isTop(b)) { + return Top; + } +} + +export function bottomTopGuardSingle>( + a: A +): typeof Top | typeof Bottom | undefined { + if(isBottom(a)) { + return Bottom; + } else if(isTop(a)) { + return Top; + } +} \ 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..b2724504ef4 --- /dev/null +++ b/src/dataflow/eval/values/intervals/interval-binary.ts @@ -0,0 +1,73 @@ +import type { Lift, ValueInterval } from '../r-value'; +import { bottomTopGuard } from '../general'; +import { binaryScalar } from '../scalar/scalar-binary'; + +/** + * Take two potentially lifted intervals and combine them with the given op. + * This propagates `top` and `bottom` values. + */ +export function binaryInterval, B extends Lift>( + a: A, + b: B, + op: keyof typeof Operations +): Lift { + return bottomTopGuard(a, b) ?? Operations[op](a as ValueInterval, b as ValueInterval); +} + +const Operations = { + add: intervalAdd, + sub: intervalSub, + intersect: intervalIntersect, + union: intervalUnion, + setminus: intervalSetminus +} as const; + +function intervalAdd(a: A, b: B): ValueInterval { + return { + type: 'interval', + startInclusive: a.startInclusive && b.startInclusive, + start: binaryScalar(a.start, b.start, 'add'), + endInclusive: a.endInclusive && b.endInclusive, + end: binaryScalar(a.end, b.end, 'add') + }; +} + +function intervalSub(a: A, b: B): ValueInterval { + return { + type: 'interval', + startInclusive: a.startInclusive && b.startInclusive, + start: binaryScalar(a.start, b.end, 'sub'), + endInclusive: a.endInclusive && b.endInclusive, + end: binaryScalar(a.end, b.start, 'sub') + }; +} + +function intervalIntersect(a: A, b: B): ValueInterval { + return { + type: 'interval', + startInclusive: a.startInclusive && b.startInclusive, + start: binaryScalar(a.start, b.start, 'max'), + endInclusive: a.endInclusive && b.endInclusive, + end: binaryScalar(a.end, b.end, 'min') + }; +} + +function intervalUnion(a: A, b: B): ValueInterval { + return { + type: 'interval', + startInclusive: a.startInclusive && b.startInclusive, + start: binaryScalar(a.start, b.start, 'min'), + endInclusive: a.endInclusive && b.endInclusive, + end: binaryScalar(a.end, b.end, 'max') + }; +} + +function intervalSetminus(a: A, b: B): ValueInterval { + return { + type: 'interval', + startInclusive: a.startInclusive && b.startInclusive, + start: binaryScalar(a.start, b.end, 'max'), + endInclusive: a.endInclusive && b.endInclusive, + end: binaryScalar(a.end, b.start, 'min') + }; +} \ No newline at end of file diff --git a/src/dataflow/eval/values/intervals/interval-check.ts b/src/dataflow/eval/values/intervals/interval-check.ts new file mode 100644 index 00000000000..76e6d3ef380 --- /dev/null +++ b/src/dataflow/eval/values/intervals/interval-check.ts @@ -0,0 +1,17 @@ +import type { Lift, ValueInterval, ValueLogical } from '../r-value'; +import { bottomTopGuardSingle } from '../general'; + +const CheckOperations = { + 'empty': intervalEmpty, +} as const; + +export function checkInterval>(a: A, op: keyof typeof CheckOperations): Lift { + return bottomTopGuardSingle(a) ?? CheckOperations[op](a as ValueInterval); +} + +function intervalEmpty(a: A): ValueLogical { + return { + type: 'logical', + value: a.start < a.end || (a.start === a.end && (!a.startInclusive || !a.endInclusive)) + }; +} \ No newline at end of file diff --git a/src/dataflow/eval/values/intervals/interval-compare.ts b/src/dataflow/eval/values/intervals/interval-compare.ts new file mode 100644 index 00000000000..6c47aa33e90 --- /dev/null +++ b/src/dataflow/eval/values/intervals/interval-compare.ts @@ -0,0 +1,32 @@ +import type { Lift, ValueInterval, ValueLogical } from '../r-value'; +import { bottomTopGuard } from '../general'; +import { checkInterval } from './interval-check'; +import { binaryInterval } from './interval-binary'; +import { iteLogical } from '../logical/logical-check'; +import { LogicalMaybe } from '../logical/logical-constants'; +import { compareScalar } from '../scalar/scalar-compare'; +import { getIntervalEnd, getIntervalStart } from './interval-constants'; + +const CompareOperations = { + '<=': intervalLeq, +} as const; + +export function compareInterval(a: A, b: B, op: keyof typeof CompareOperations): Lift { + return bottomTopGuard(a, b) ?? CompareOperations[op](a as ValueInterval, b as ValueInterval); +} + +function intervalLeq(a: A, b: B): Lift { + // if intersect af a and b is non-empty, return maybe + // else return a.end <= b.start + const intersect = binaryInterval(a, b, 'intersect'); + + return iteLogical( + checkInterval(intersect, 'empty'), + LogicalMaybe, + compareScalar( + getIntervalEnd(a), + getIntervalStart(b), + '<=' + ) + ); +} \ 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..88eb6ab0a99 --- /dev/null +++ b/src/dataflow/eval/values/intervals/interval-constants.ts @@ -0,0 +1,25 @@ +import type { RNumberValue } from '../../../../r-bridge/lang-4.x/convert-values'; +import type { Lift, ValueInterval, ValueNumber } from '../r-value'; +import { bottomTopGuardSingle } from '../general'; + +export function intervalFromScalar(scalar: RNumberValue) { + return { + type: 'interval', + start: scalar, + end: scalar, + startInclusive: true, + endInclusive: true, + }; +} + +export function getIntervalStart(interval: Lift): Lift { + return applyIntervalOp(interval, op => op.start); +} + +export function getIntervalEnd(interval: Lift): Lift { + return applyIntervalOp(interval, op => op.end); +} + +function applyIntervalOp(interval: Lift, op: (interval: ValueInterval) => Lift): Lift { + return bottomTopGuardSingle(interval) ?? op(interval as ValueInterval); +} \ No newline at end of file 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..a55880a97d4 --- /dev/null +++ b/src/dataflow/eval/values/intervals/interval-unary.ts @@ -0,0 +1,29 @@ +import type { Lift, ValueInterval } from '../r-value'; +import { bottomTopGuardSingle } from '../general'; +import { ValueIntegerNegativeOne } from '../scalar/scalar-constants'; +import { binaryScalar } from '../scalar/scalar-binary'; + +/** + * Take two potentially lifted intervals and combine them with the given op. + * This propagates `top` and `bottom` values. + */ +export function unaryInterval>( + a: A, + op: keyof typeof Operations +): Lift { + return bottomTopGuardSingle(a) ?? Operations[op](a as ValueInterval); +} + +const Operations = { + negate: intervalNegate +} as const; + +function intervalNegate(a: A): ValueInterval { + return { + type: 'interval', + startInclusive: a.endInclusive, + start: binaryScalar(a.end, ValueIntegerNegativeOne, 'mul'), + endInclusive: a.startInclusive, + end: binaryScalar(a.start, ValueIntegerNegativeOne, 'mul') + }; +} 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..d8757532c3d --- /dev/null +++ b/src/dataflow/eval/values/logical/logical-binary.ts @@ -0,0 +1,32 @@ +import type { Lift, TernaryLogical, ValueLogical } from '../r-value'; +import { bottomTopGuard } from '../general'; + +/** + * Take two potentially lifted logicals and combine them with the given op. + * This propagates `top` and `bottom` values. + */ +export function binaryLogical, B extends Lift>( + a: A, + b: B, + op: keyof typeof Operations +): Lift { + return bottomTopGuard(a, b) ?? Operations[op](a as ValueLogical, b as ValueLogical); +} + +const Operations = { + and: (a, b) => logicalHelper(a, b, (a, b) => a && b), + or: (a, b) => logicalHelper(a, b, (a, b) => a || b), + xor: (a, b) => logicalHelper(a, b, (a, b) => a !== b), + implies: (a, b) => logicalHelper(a, b, (a, b) => !a || b), + iff: (a, b) => logicalHelper(a, b, (a, b) => a === b), + nand: (a, b) => logicalHelper(a, b, (a, b) => !(a && b)), + nor: (a, b) => logicalHelper(a, b, (a, b) => !(a || b)), +} as const satisfies Record ValueLogical>; + +function logicalHelper(a: A, b: B, op: (a: TernaryLogical, b: TernaryLogical) => TernaryLogical): ValueLogical { + const botTopGuard = bottomTopGuard(a.value, b.value); + return { + type: 'logical', + value: botTopGuard ?? op(a.value as TernaryLogical, b.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..fd203439115 --- /dev/null +++ b/src/dataflow/eval/values/logical/logical-check.ts @@ -0,0 +1,25 @@ +import type { Lift, TernaryLogical, ValueLogical } from '../r-value'; +import { Bottom , Top } from '../r-value'; +import { bottomTopGuardSingle } from '../general'; + +export function unpackLogical(a: Lift): Lift { + return bottomTopGuardSingle(a) ?? (a as ValueLogical).value; +} + +export function iteLogical, Result>( + cond: A, + onTrue: Lift, + onFalse: Lift, + onMaybe: Lift = Top +): Lift { + const condVal = unpackLogical(cond); + if(condVal === Top) { + return Top; + } else if(condVal === Bottom) { + return Bottom; + } else if(condVal === 'maybe') { + return onMaybe; + } else { + return condVal ? onTrue : 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..f6f76380e7d --- /dev/null +++ b/src/dataflow/eval/values/logical/logical-constants.ts @@ -0,0 +1,14 @@ +import type { ValueLogical } from '../r-value'; + +export const LogicalTrue: ValueLogical = { + type: 'logical', + value: true +}; +export const LogicalFalse: ValueLogical = { + type: 'logical', + value: false +}; +export const LogicalMaybe: ValueLogical = { + type: 'logical', + value: 'maybe' +}; \ 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..7ca69ad0cc4 --- /dev/null +++ b/src/dataflow/eval/values/logical/logical-unary.ts @@ -0,0 +1,25 @@ +import type { Lift, ValueLogical } from '../r-value'; +import { bottomTopGuardSingle } from '../general'; + +/** + * Take one potentially lifted logical and apply the given unary op. + * This propagates `top` and `bottom` values. + */ +export function unaryLogical>( + a: A, + op: keyof typeof Operations +): Lift { + return bottomTopGuardSingle(a) ?? Operations[op](a as ValueLogical); +} + +const Operations = { + not: logicalNot +} as const; + +function logicalNot(a: A): ValueLogical { + const val = bottomTopGuardSingle(a.value); + return { + type: 'logical', + value: val ?? !a.value + }; +} diff --git a/src/dataflow/eval/values/r-value.ts b/src/dataflow/eval/values/r-value.ts new file mode 100644 index 00000000000..7aaf4dc07fb --- /dev/null +++ b/src/dataflow/eval/values/r-value.ts @@ -0,0 +1,61 @@ +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'; + +export const Top = { type: Symbol('⊤') }; +export const Bottom = { type: Symbol('⊥') }; + +export type Lift = N | typeof Top | typeof Bottom + +export interface ValueInterval { + type: 'interval' + start: Lift + startInclusive: boolean + end: Lift + endInclusive: boolean +} +export interface ValueSet { + type: 'set' + elements: Lift +} +export interface ValueVector { + type: 'vector' + elements: Lift +} +export interface ValueNumber { + type: 'number' + value: Lift +} +export interface ValueString { + type: 'string' + value: Lift +} +export type TernaryLogical = RLogicalValue | 'maybe' +export interface ValueLogical { + type: 'logical' + value: Lift +} + +export type Value = Lift< + ValueInterval + | ValueSet + | ValueVector + | ValueNumber + | ValueString + | ValueLogical + > +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 typeof value === 'object' && value !== null && 'type' in value && value.type === Top.type; +} +// @ts-expect-error -- this is a save cast +export function isBottom>(value: V): value is typeof Bottom { + return typeof value === 'object' && value !== null && 'type' in value && value.type === Bottom.type; +} + 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..204f295c98d --- /dev/null +++ b/src/dataflow/eval/values/scalar/scalar-binary.ts @@ -0,0 +1,45 @@ +import type { Lift, ValueNumber } from '../r-value'; +import { bottomTopGuard } from '../general'; +import type { RNumberValue } from '../../../../r-bridge/lang-4.x/convert-values'; + +/** + * Take two potentially lifted intervals and combine them with the given op. + * This propagates `top` and `bottom` values. + */ +export function binaryScalar, B extends Lift>( + a: A, + b: B, + op: keyof typeof Operations +): Lift { + return bottomTopGuard(a, b) ?? Operations[op](a as ValueNumber, b as ValueNumber); +} + +const Operations = { + 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) => scalarHelper(a, b, (a, b) => Math.max(a, b)), + min: (a, b) => scalarHelper(a, b, (a, b) => Math.min(a, b)), +} as const satisfies Record ValueNumber>; + +function scalarHelper( + a: A, + b: B, + c: (a: number, b: number) => number +): ValueNumber { + const val = bottomTopGuard(a.value, b.value); + const aval = a.value as RNumberValue; + const bval = b.value as RNumberValue; + const result = c(aval.num, bval.num); + return { + type: 'number', + value: val ?? { + markedAsInt: aval.markedAsInt && bval.markedAsInt && Number.isInteger(result), + complexNumber: aval.complexNumber || bval.complexNumber, + num: result + } + }; +} \ No newline at end of file diff --git a/src/dataflow/eval/values/scalar/scalar-compare.ts b/src/dataflow/eval/values/scalar/scalar-compare.ts new file mode 100644 index 00000000000..ce2823e6280 --- /dev/null +++ b/src/dataflow/eval/values/scalar/scalar-compare.ts @@ -0,0 +1,38 @@ +import type { Lift, ValueLogical, ValueNumber } from '../r-value'; +import { bottomTopGuard } from '../general'; +import type { RNumberValue } from '../../../../r-bridge/lang-4.x/convert-values'; + +/** + * Take two potentially lifted intervals and compare them with the given op. + * This propagates `top` and `bottom` values. + */ +export function compareScalar, B extends Lift>( + a: A, + b: B, + op: keyof typeof Operations +): Lift { + return bottomTopGuard(a, b) ?? Operations[op](a as ValueNumber, b as ValueNumber); +} + +const Operations = { + '<=': (a, b) => scalarHelper(a, b, (a, b) => a <= b), + '<': (a, b) => scalarHelper(a, b, (a, b) => a < b), + '>=': (a, b) => scalarHelper(a, b, (a, b) => a >= b), + '>': (a, b) => scalarHelper(a, b, (a, b) => a > b), + '==': (a, b) => scalarHelper(a, b, (a, b) => a === b), + '!=': (a, b) => scalarHelper(a, b, (a, b) => a !== b) +} as const satisfies Record ValueLogical>; + +function scalarHelper( + a: A, + b: B, + c: (a: number, b: number) => boolean +): ValueLogical { + const val = bottomTopGuard(a.value, b.value); + const aval = a.value as RNumberValue; + const bval = b.value as RNumberValue; + return { + type: 'logical', + value: val ?? c(aval.num, bval.num) + }; +} \ No newline at end of file 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..50d7d0c145a --- /dev/null +++ b/src/dataflow/eval/values/scalar/scalar-constants.ts @@ -0,0 +1,16 @@ +import type { ValueNumber } from '../r-value'; + +export function getScalarInteger(value: number): ValueNumber { + return { + type: 'number', + value: { + markedAsInt: true, + num: value, + complexNumber: false + } + }; +} + +export const ValueIntegerOne: ValueNumber = getScalarInteger(1); +export const ValueIntegerZero: ValueNumber = getScalarInteger(0); +export const ValueIntegerNegativeOne: ValueNumber = getScalarInteger(-1); 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..c004a8dc183 --- /dev/null +++ b/src/dataflow/eval/values/scalar/scalar-unary.ts @@ -0,0 +1,31 @@ +import type { Lift, ValueNumber } from '../r-value'; +import { bottomTopGuardSingle } from '../general'; +import type { RNumberValue } from '../../../../r-bridge/lang-4.x/convert-values'; + +/** + * Take a potentially lifted interval and apply the given op. + * This propagates `top` and `bottom` values. + */ +export function unaryScalar>( + a: A, + op: keyof typeof Operations +): Lift { + return bottomTopGuardSingle(a) ?? Operations[op](a as ValueNumber); +} + +const Operations = { + negate: scalarNegate +} as const satisfies Record ValueNumber>; + +function scalarNegate(a: A): ValueNumber { + const val = bottomTopGuardSingle(a.value); + const aval = a.value as RNumberValue; + return { + type: 'number', + value: val ?? { + markedAsInt: aval.markedAsInt, + complexNumber: aval.complexNumber, + num: -aval.num + } + }; +} \ 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/scalar/eval-scalar-unary.test.ts b/test/functionality/dataflow/eval/scalar/eval-scalar-unary.test.ts new file mode 100644 index 00000000000..b9117a1fc7b --- /dev/null +++ b/test/functionality/dataflow/eval/scalar/eval-scalar-unary.test.ts @@ -0,0 +1,42 @@ +import { assert, describe, test } from 'vitest'; +import { + getScalarInteger, + 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 { Lift, ValueNumber } 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: Lift, expect: number, shouldBeInt = false, shouldBeFloat = false) { + if(typeof expect === 'number') { + guard(!isTop(value) && !isBottom(value)); + guard('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, ValueIntegerOne, 'add'), expect: 2 }, + { label: '1 + 0', value: binaryScalar(ValueIntegerOne, ValueIntegerZero, 'add'), expect: 1 }, + { label: '1 - 1', value: binaryScalar(ValueIntegerOne, ValueIntegerOne, 'sub'), expect: 0 }, + { label: '1 * 0', value: binaryScalar(ValueIntegerOne, ValueIntegerZero, 'mul'), expect: 0 }, + { label: '1 * 2', value: binaryScalar(ValueIntegerOne, getScalarInteger(2), 'mul'), expect: 2 }, + { label: 'mod(5, 2)', value: binaryScalar(getScalarInteger(5), getScalarInteger(2), 'mod'), expect: 1 }, + { label: 'mod(5, 3)', value: binaryScalar(getScalarInteger(5), getScalarInteger(3), 'mod'), expect: 2 }, + ])('$label', ({ value, expect }) => { + shouldBeNum(value, expect); + }); + }); +}); \ No newline at end of file From 7a8b7ad9db830257d1b4afbb6f87a5e7af1f82c9 Mon Sep 17 00:00:00 2001 From: Florian Sihler Date: Fri, 14 Mar 2025 21:20:28 +0100 Subject: [PATCH 2/6] refactor: work on intervals --- src/dataflow/eval/values/general.ts | 21 +----- .../eval/values/intervals/interval-binary.ts | 75 +++++++++++-------- .../eval/values/intervals/interval-check.ts | 6 +- .../eval/values/intervals/interval-compare.ts | 4 +- .../values/intervals/interval-constants.ts | 36 +++++++-- .../eval/values/intervals/interval-unary.ts | 4 +- .../eval/values/logical/logical-binary.ts | 48 ++++++++++-- .../eval/values/logical/logical-check.ts | 4 +- .../eval/values/logical/logical-constants.ts | 6 +- .../eval/values/logical/logical-unary.ts | 8 +- .../eval/values/scalar/scalar-binary.ts | 1 + .../eval/values/scalar/scalar-constants.ts | 16 +++- .../eval/values/scalar/scalar-unary.ts | 32 ++++++-- .../interval/eval-interval-simple.test.ts | 41 ++++++++++ .../eval/logical/eval-logical-simple.test.ts | 47 ++++++++++++ ...ary.test.ts => eval-scalar-simple.test.ts} | 8 +- 16 files changed, 265 insertions(+), 92 deletions(-) create mode 100644 test/functionality/dataflow/eval/interval/eval-interval-simple.test.ts create mode 100644 test/functionality/dataflow/eval/logical/eval-logical-simple.test.ts rename test/functionality/dataflow/eval/scalar/{eval-scalar-unary.test.ts => eval-scalar-simple.test.ts} (85%) diff --git a/src/dataflow/eval/values/general.ts b/src/dataflow/eval/values/general.ts index d51c526f9a0..f2e869113b8 100644 --- a/src/dataflow/eval/values/general.ts +++ b/src/dataflow/eval/values/general.ts @@ -2,25 +2,12 @@ import type { Lift } from './r-value'; import { Bottom, isBottom, isTop, Top } from './r-value'; /** - * Takes two potentially lifted ops and returns `Top` or `Bottom` if either is `Top` or `Bottom`. + * Takes n potentially lifted ops and returns `Top` or `Bottom` if any is `Top` or `Bottom`. */ -export function bottomTopGuard, B extends Lift>( - a: A, - b: B -): typeof Top | typeof Bottom | undefined { - if(isBottom(a) || isBottom(b)) { +export function bottomTopGuard(...a: Lift[]): typeof Top | typeof Bottom | undefined { + if(a.some(isBottom)) { return Bottom; - } else if(isTop(a) || isTop(b)) { + } else if(a.some(isTop)) { return Top; } } - -export function bottomTopGuardSingle>( - a: A -): typeof Top | typeof Bottom | undefined { - if(isBottom(a)) { - return Bottom; - } else if(isTop(a)) { - return Top; - } -} \ 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 index b2724504ef4..740afdaba62 100644 --- a/src/dataflow/eval/values/intervals/interval-binary.ts +++ b/src/dataflow/eval/values/intervals/interval-binary.ts @@ -1,6 +1,7 @@ import type { Lift, ValueInterval } from '../r-value'; import { bottomTopGuard } from '../general'; import { binaryScalar } from '../scalar/scalar-binary'; +import { orderIntervalFrom } from './interval-constants'; /** * Take two potentially lifted intervals and combine them with the given op. @@ -17,57 +18,71 @@ export function binaryInterval, B extends Lift(a: A, b: B): ValueInterval { - return { - type: 'interval', - startInclusive: a.startInclusive && b.startInclusive, - start: binaryScalar(a.start, b.start, 'add'), - endInclusive: a.endInclusive && b.endInclusive, - end: binaryScalar(a.end, b.end, 'add') - }; +function intervalAdd(a: A, b: B): Lift { + return orderIntervalFrom( + binaryScalar(a.start, b.start, 'add'), + binaryScalar(a.end, b.end, 'add'), + a.startInclusive && b.startInclusive, + a.endInclusive && b.endInclusive + ); } -function intervalSub(a: A, b: B): ValueInterval { - return { - type: 'interval', - startInclusive: a.startInclusive && b.startInclusive, - start: binaryScalar(a.start, b.end, 'sub'), - endInclusive: a.endInclusive && b.endInclusive, - end: binaryScalar(a.end, b.start, 'sub') - }; +function intervalSub(a: A, b: B): Lift { + return orderIntervalFrom( + binaryScalar(a.start, b.end, 'sub'), + binaryScalar(a.end, b.start, 'sub'), + a.startInclusive && b.startInclusive, + a.endInclusive && b.endInclusive + ); } -function intervalIntersect(a: A, b: B): ValueInterval { - return { - type: 'interval', - startInclusive: a.startInclusive && b.startInclusive, - start: binaryScalar(a.start, b.start, 'max'), - endInclusive: a.endInclusive && b.endInclusive, - end: binaryScalar(a.end, b.end, 'min') - }; +function intervalIntersect(a: A, b: B): Lift { + return orderIntervalFrom( + binaryScalar(a.start, b.start, 'max'), + binaryScalar(a.end, b.end, 'min'), + a.startInclusive && b.startInclusive, + a.endInclusive && b.endInclusive + ); } -function intervalUnion(a: A, b: B): ValueInterval { +function intervalUnion(a: A, b: B): Lift { + return orderIntervalFrom( + binaryScalar(a.start, b.start, 'min'), + binaryScalar(a.end, b.end, 'max'), + a.startInclusive && b.startInclusive, + a.endInclusive && b.endInclusive + ); +} + +// TODO: take support for div sin and other functions that i wrote + +function intervalSetminus(a: A, b: B): Lift { return { type: 'interval', startInclusive: a.startInclusive && b.startInclusive, - start: binaryScalar(a.start, b.start, 'min'), + start: binaryScalar(a.start, b.end, 'max'), endInclusive: a.endInclusive && b.endInclusive, - end: binaryScalar(a.end, b.end, 'max') + end: binaryScalar(a.end, b.start, 'min') }; } -function intervalSetminus(a: A, b: B): ValueInterval { +function intervalMul(a: A, b: B): Lift { + const ll = binaryScalar(a.start, b.start, 'mul'); + const lu = binaryScalar(a.start, b.end, 'mul'); + const ul = binaryScalar(a.end, b.start, 'mul'); + const uu = binaryScalar(a.end, b.end, 'mul'); + return { type: 'interval', startInclusive: a.startInclusive && b.startInclusive, - start: binaryScalar(a.start, b.end, 'max'), + start: [ll, lu, ul, uu].reduce((acc, val) => binaryScalar(acc, val, 'min')), endInclusive: a.endInclusive && b.endInclusive, - end: binaryScalar(a.end, b.start, 'min') + end: [ll, lu, ul, uu].reduce((acc, val) => binaryScalar(acc, val, 'max')) }; } \ No newline at end of file diff --git a/src/dataflow/eval/values/intervals/interval-check.ts b/src/dataflow/eval/values/intervals/interval-check.ts index 76e6d3ef380..076a10aef2a 100644 --- a/src/dataflow/eval/values/intervals/interval-check.ts +++ b/src/dataflow/eval/values/intervals/interval-check.ts @@ -1,12 +1,12 @@ import type { Lift, ValueInterval, ValueLogical } from '../r-value'; -import { bottomTopGuardSingle } from '../general'; +import { bottomTopGuard } from '../general'; const CheckOperations = { - 'empty': intervalEmpty, + 'empty': intervalEmpty } as const; export function checkInterval>(a: A, op: keyof typeof CheckOperations): Lift { - return bottomTopGuardSingle(a) ?? CheckOperations[op](a as ValueInterval); + return bottomTopGuard(a) ?? CheckOperations[op](a as ValueInterval); } function intervalEmpty(a: A): ValueLogical { diff --git a/src/dataflow/eval/values/intervals/interval-compare.ts b/src/dataflow/eval/values/intervals/interval-compare.ts index 6c47aa33e90..8c66a97664c 100644 --- a/src/dataflow/eval/values/intervals/interval-compare.ts +++ b/src/dataflow/eval/values/intervals/interval-compare.ts @@ -3,7 +3,7 @@ import { bottomTopGuard } from '../general'; import { checkInterval } from './interval-check'; import { binaryInterval } from './interval-binary'; import { iteLogical } from '../logical/logical-check'; -import { LogicalMaybe } from '../logical/logical-constants'; +import { ValueLogicalMaybe } from '../logical/logical-constants'; import { compareScalar } from '../scalar/scalar-compare'; import { getIntervalEnd, getIntervalStart } from './interval-constants'; @@ -22,7 +22,7 @@ function intervalLeq(a: A, b: return iteLogical( checkInterval(intersect, 'empty'), - LogicalMaybe, + ValueLogicalMaybe, compareScalar( getIntervalEnd(a), getIntervalStart(b), diff --git a/src/dataflow/eval/values/intervals/interval-constants.ts b/src/dataflow/eval/values/intervals/interval-constants.ts index 88eb6ab0a99..05e5b207c39 100644 --- a/src/dataflow/eval/values/intervals/interval-constants.ts +++ b/src/dataflow/eval/values/intervals/interval-constants.ts @@ -1,17 +1,37 @@ import type { RNumberValue } from '../../../../r-bridge/lang-4.x/convert-values'; import type { Lift, ValueInterval, ValueNumber } from '../r-value'; -import { bottomTopGuardSingle } from '../general'; +import { bottomTopGuard } from '../general'; +import { getScalarFromInteger, liftScalar } from '../scalar/scalar-constants'; +import { iteLogical } from '../logical/logical-check'; +import { compareScalar } from '../scalar/scalar-compare'; -export function intervalFromScalar(scalar: RNumberValue) { +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 + ); +} + +export function intervalFromValues(start: Lift, end = start, startInclusive = true, endInclusive = true): ValueInterval { return { - type: 'interval', - start: scalar, - end: scalar, - startInclusive: true, - endInclusive: true, + type: 'interval', + start, + end, + startInclusive, + endInclusive, }; } +export function orderIntervalFrom(start: Lift, end = start, startInclusive = true, endInclusive = true): Lift { + return iteLogical( + compareScalar(start, end, '<='), + intervalFromValues(start, end, startInclusive, endInclusive), + intervalFromValues(end, start, startInclusive, endInclusive) + ); +} + export function getIntervalStart(interval: Lift): Lift { return applyIntervalOp(interval, op => op.start); } @@ -21,5 +41,5 @@ export function getIntervalEnd(interval: Lift): Lift } function applyIntervalOp(interval: Lift, op: (interval: ValueInterval) => Lift): Lift { - return bottomTopGuardSingle(interval) ?? op(interval as ValueInterval); + return bottomTopGuard(interval) ?? op(interval as ValueInterval); } \ No newline at end of file diff --git a/src/dataflow/eval/values/intervals/interval-unary.ts b/src/dataflow/eval/values/intervals/interval-unary.ts index a55880a97d4..c6b4ac1b37c 100644 --- a/src/dataflow/eval/values/intervals/interval-unary.ts +++ b/src/dataflow/eval/values/intervals/interval-unary.ts @@ -1,5 +1,5 @@ import type { Lift, ValueInterval } from '../r-value'; -import { bottomTopGuardSingle } from '../general'; +import { bottomTopGuard } from '../general'; import { ValueIntegerNegativeOne } from '../scalar/scalar-constants'; import { binaryScalar } from '../scalar/scalar-binary'; @@ -11,7 +11,7 @@ export function unaryInterval>( a: A, op: keyof typeof Operations ): Lift { - return bottomTopGuardSingle(a) ?? Operations[op](a as ValueInterval); + return bottomTopGuard(a) ?? Operations[op](a as ValueInterval); } const Operations = { diff --git a/src/dataflow/eval/values/logical/logical-binary.ts b/src/dataflow/eval/values/logical/logical-binary.ts index d8757532c3d..9046703870f 100644 --- a/src/dataflow/eval/values/logical/logical-binary.ts +++ b/src/dataflow/eval/values/logical/logical-binary.ts @@ -14,13 +14,47 @@ export function binaryLogical, B extends Lift logicalHelper(a, b, (a, b) => a && b), - or: (a, b) => logicalHelper(a, b, (a, b) => a || b), - xor: (a, b) => logicalHelper(a, b, (a, b) => a !== b), - implies: (a, b) => logicalHelper(a, b, (a, b) => !a || b), - iff: (a, b) => logicalHelper(a, b, (a, b) => a === b), - nand: (a, b) => logicalHelper(a, b, (a, b) => !(a && b)), - nor: (a, b) => logicalHelper(a, b, (a, b) => !(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 ValueLogical>; function logicalHelper(a: A, b: B, op: (a: TernaryLogical, b: TernaryLogical) => TernaryLogical): ValueLogical { diff --git a/src/dataflow/eval/values/logical/logical-check.ts b/src/dataflow/eval/values/logical/logical-check.ts index fd203439115..7b835df0e28 100644 --- a/src/dataflow/eval/values/logical/logical-check.ts +++ b/src/dataflow/eval/values/logical/logical-check.ts @@ -1,9 +1,9 @@ import type { Lift, TernaryLogical, ValueLogical } from '../r-value'; import { Bottom , Top } from '../r-value'; -import { bottomTopGuardSingle } from '../general'; +import { bottomTopGuard } from '../general'; export function unpackLogical(a: Lift): Lift { - return bottomTopGuardSingle(a) ?? (a as ValueLogical).value; + return bottomTopGuard(a) ?? (a as ValueLogical).value; } export function iteLogical, Result>( diff --git a/src/dataflow/eval/values/logical/logical-constants.ts b/src/dataflow/eval/values/logical/logical-constants.ts index f6f76380e7d..f4ca2f08572 100644 --- a/src/dataflow/eval/values/logical/logical-constants.ts +++ b/src/dataflow/eval/values/logical/logical-constants.ts @@ -1,14 +1,14 @@ import type { ValueLogical } from '../r-value'; -export const LogicalTrue: ValueLogical = { +export const ValueLogicalTrue: ValueLogical = { type: 'logical', value: true }; -export const LogicalFalse: ValueLogical = { +export const ValueLogicalFalse: ValueLogical = { type: 'logical', value: false }; -export const LogicalMaybe: ValueLogical = { +export const ValueLogicalMaybe: ValueLogical = { type: 'logical', value: 'maybe' }; \ 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 index 7ca69ad0cc4..01794d1db4a 100644 --- a/src/dataflow/eval/values/logical/logical-unary.ts +++ b/src/dataflow/eval/values/logical/logical-unary.ts @@ -1,5 +1,5 @@ import type { Lift, ValueLogical } from '../r-value'; -import { bottomTopGuardSingle } from '../general'; +import { bottomTopGuard } from '../general'; /** * Take one potentially lifted logical and apply the given unary op. @@ -9,7 +9,7 @@ export function unaryLogical>( a: A, op: keyof typeof Operations ): Lift { - return bottomTopGuardSingle(a) ?? Operations[op](a as ValueLogical); + return bottomTopGuard(a) ?? Operations[op](a as ValueLogical); } const Operations = { @@ -17,9 +17,9 @@ const Operations = { } as const; function logicalNot(a: A): ValueLogical { - const val = bottomTopGuardSingle(a.value); + const val = bottomTopGuard(a.value); return { type: 'logical', - value: val ?? !a.value + value: val ?? (a.value === 'maybe' ? 'maybe' : !a.value) }; } diff --git a/src/dataflow/eval/values/scalar/scalar-binary.ts b/src/dataflow/eval/values/scalar/scalar-binary.ts index 204f295c98d..2e105f7ad3c 100644 --- a/src/dataflow/eval/values/scalar/scalar-binary.ts +++ b/src/dataflow/eval/values/scalar/scalar-binary.ts @@ -14,6 +14,7 @@ export function binaryScalar, B extends Lift scalarHelper(a, b, (a, b) => a + b), sub: (a, b) => scalarHelper(a, b, (a, b) => a - b), diff --git a/src/dataflow/eval/values/scalar/scalar-constants.ts b/src/dataflow/eval/values/scalar/scalar-constants.ts index 50d7d0c145a..8b2bb6da9ff 100644 --- a/src/dataflow/eval/values/scalar/scalar-constants.ts +++ b/src/dataflow/eval/values/scalar/scalar-constants.ts @@ -1,6 +1,7 @@ import type { ValueNumber } from '../r-value'; +import type { RNumberValue } from '../../../../r-bridge/lang-4.x/convert-values'; -export function getScalarInteger(value: number): ValueNumber { +export function getScalarFromInteger(value: number): ValueNumber { return { type: 'number', value: { @@ -11,6 +12,13 @@ export function getScalarInteger(value: number): ValueNumber { }; } -export const ValueIntegerOne: ValueNumber = getScalarInteger(1); -export const ValueIntegerZero: ValueNumber = getScalarInteger(0); -export const ValueIntegerNegativeOne: ValueNumber = getScalarInteger(-1); +export function liftScalar(value: RNumberValue): ValueNumber { + return { + type: 'number', + value: value + }; +} + +export const ValueIntegerOne: ValueNumber = getScalarFromInteger(1); +export const ValueIntegerZero: ValueNumber = getScalarFromInteger(0); +export const ValueIntegerNegativeOne: ValueNumber = getScalarFromInteger(-1); diff --git a/src/dataflow/eval/values/scalar/scalar-unary.ts b/src/dataflow/eval/values/scalar/scalar-unary.ts index c004a8dc183..acaea9ca372 100644 --- a/src/dataflow/eval/values/scalar/scalar-unary.ts +++ b/src/dataflow/eval/values/scalar/scalar-unary.ts @@ -1,5 +1,5 @@ import type { Lift, ValueNumber } from '../r-value'; -import { bottomTopGuardSingle } from '../general'; +import { bottomTopGuard } from '../general'; import type { RNumberValue } from '../../../../r-bridge/lang-4.x/convert-values'; /** @@ -10,22 +10,42 @@ export function unaryScalar>( a: A, op: keyof typeof Operations ): Lift { - return bottomTopGuardSingle(a) ?? Operations[op](a as ValueNumber); + return bottomTopGuard(a) ?? Operations[op](a as ValueNumber); } const Operations = { - negate: scalarNegate + negate: (a: ValueNumber) => scalarHelper(a, (a) => -a), + abs: (a: ValueNumber) => scalarHelper(a, Math.abs), + ceil: (a: ValueNumber) => scalarHelper(a, Math.ceil), + floor: (a: ValueNumber) => scalarHelper(a, Math.floor), + round: (a: ValueNumber) => scalarHelper(a, Math.round), + exp: (a: ValueNumber) => scalarHelper(a, Math.exp), + log: (a: ValueNumber) => scalarHelper(a, Math.log), + log10: (a: ValueNumber) => scalarHelper(a, Math.log10), + log2: (a: ValueNumber) => scalarHelper(a, Math.log2), + sign: (a: ValueNumber) => scalarHelper(a, Math.sign), + sqrt: (a: ValueNumber) => scalarHelper(a, Math.sqrt), + sin: (a: ValueNumber) => scalarHelper(a, Math.sin), + cos: (a: ValueNumber) => scalarHelper(a, Math.cos), + tan: (a: ValueNumber) => scalarHelper(a, Math.tan), + asin: (a: ValueNumber) => scalarHelper(a, Math.asin), + acos: (a: ValueNumber) => scalarHelper(a, Math.acos), + atan: (a: ValueNumber) => scalarHelper(a, Math.atan), + sinh: (a: ValueNumber) => scalarHelper(a, Math.sinh), + cosh: (a: ValueNumber) => scalarHelper(a, Math.cosh), + tanh: (a: ValueNumber) => scalarHelper(a, Math.tanh), + } as const satisfies Record ValueNumber>; -function scalarNegate(a: A): ValueNumber { - const val = bottomTopGuardSingle(a.value); +function scalarHelper(a: A, op: (a: number) => number): ValueNumber { + const val = bottomTopGuard(a.value); const aval = a.value as RNumberValue; return { type: 'number', value: val ?? { markedAsInt: aval.markedAsInt, complexNumber: aval.complexNumber, - num: -aval.num + num: op(aval.num) } }; } \ 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..8e0de515507 --- /dev/null +++ b/test/functionality/dataflow/eval/interval/eval-interval-simple.test.ts @@ -0,0 +1,41 @@ +import { assert, describe, test } from 'vitest'; + + +import { guard } from '../../../../../src/util/assert'; +import type { Lift, ValueInterval } from '../../../../../src/dataflow/eval/values/r-value'; +import { isBottom, isTop } from '../../../../../src/dataflow/eval/values/r-value'; +import { binaryInterval } from '../../../../../src/dataflow/eval/values/intervals/interval-binary'; +import { unaryInterval } from '../../../../../src/dataflow/eval/values/intervals/interval-unary'; +import { intervalFrom } from '../../../../../src/dataflow/eval/values/intervals/interval-constants'; + +describe('interval', () => { + function shouldBeInterval(val: Lift, expect: readonly [startInclusive: boolean, start: number, end: number, endInclusive: boolean]) { + guard( + !isTop(val) && !isBottom(val) && + !isTop(val.start) && !isBottom(val.start) && + !isTop(val.end) && !isBottom(val.end) + ); + guard('num' in val.start.value && 'num' in val.end.value); + assert.strictEqual(val.startInclusive, expect[0], 'startInclusive'); + assert.strictEqual(val.start.value.num, expect[1], 'start'); + assert.strictEqual(val.end.value.num, expect[2], 'end'); + assert.strictEqual(val.endInclusive, expect[3], 'endInclusive'); + } + describe('unary', () => { + test.each([ + { label: '(sanity) should be one', value: intervalFrom(1), expect: [true, 1, 1, true] as const }, + { label: 'negate should work', value: unaryInterval(intervalFrom(1), 'negate'), expect: [true, -1, -1, true] as const }, + { label: 'negate should work for [2, 4]', value: unaryInterval(intervalFrom(2, 4), 'negate'), expect: [true, -4, -2, true] as const }, + { label: 'negate should work for (2, 4]', value: unaryInterval(intervalFrom(2, 4, false), 'negate'), expect: [true, -4, -2, false] as const }, + ])('$label', ({ value, expect }) => { + shouldBeInterval(value, expect); + }); + }); + describe('binary', () => { + test.each([ + { label: '1 + 1', value: binaryInterval(intervalFrom(1), intervalFrom(1), 'add'), expect: [true, 2, 2, true] as const }, + ])('$label', ({ value, expect }) => { + shouldBeInterval(value, expect); + }); + }); +}); \ 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..28c79722253 --- /dev/null +++ b/test/functionality/dataflow/eval/logical/eval-logical-simple.test.ts @@ -0,0 +1,47 @@ +import { assert, describe, test } from 'vitest'; + + +import { guard } from '../../../../../src/util/assert'; +import type { Lift, ValueLogical } from '../../../../../src/dataflow/eval/values/r-value'; +import { isBottom, isTop } from '../../../../../src/dataflow/eval/values/r-value'; +import { + 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'; + +describe('logical', () => { + function shouldBeBool(value: Lift, expect: boolean | 'maybe') { + if(typeof expect === 'boolean' || expect === 'maybe') { + guard(!isTop(value) && !isBottom(value)); + assert.equal(value.value, expect); + } + } + 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, ValueLogicalTrue, 'and'), expect: true }, + { label: '1 && 0', value: binaryLogical(ValueLogicalTrue, ValueLogicalFalse, 'and'), expect: false }, + { label: '1 && ?', value: binaryLogical(ValueLogicalTrue, ValueLogicalMaybe, 'and'), expect: 'maybe' as const }, + { label: '1 && ?', value: binaryLogical(ValueLogicalTrue, ValueLogicalMaybe, 'and'), expect: 'maybe' as const }, + { label: '? && ?', value: binaryLogical(ValueLogicalMaybe, ValueLogicalMaybe, 'and'), expect: 'maybe' as const }, + { label: '? implies 0', value: binaryLogical(ValueLogicalMaybe, ValueLogicalFalse, 'implies'), expect: 'maybe' as const }, + { label: '0 implies ?', value: binaryLogical(ValueLogicalFalse, ValueLogicalMaybe, 'implies'), expect: true }, + { label: '1 iff 1', value: binaryLogical(ValueLogicalTrue, ValueLogicalTrue, 'iff'), expect: true }, + { label: '0 iff 1', value: binaryLogical(ValueLogicalFalse, ValueLogicalTrue, 'iff'), expect: false }, + { label: '0 iff ?', value: binaryLogical(ValueLogicalFalse, ValueLogicalMaybe, 'iff'), expect: 'maybe' as const }, + { label: '? iff ?', value: binaryLogical(ValueLogicalMaybe, ValueLogicalMaybe, 'iff'), 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-unary.test.ts b/test/functionality/dataflow/eval/scalar/eval-scalar-simple.test.ts similarity index 85% rename from test/functionality/dataflow/eval/scalar/eval-scalar-unary.test.ts rename to test/functionality/dataflow/eval/scalar/eval-scalar-simple.test.ts index b9117a1fc7b..de705e94147 100644 --- a/test/functionality/dataflow/eval/scalar/eval-scalar-unary.test.ts +++ b/test/functionality/dataflow/eval/scalar/eval-scalar-simple.test.ts @@ -1,6 +1,6 @@ import { assert, describe, test } from 'vitest'; import { - getScalarInteger, + getScalarFromInteger, ValueIntegerOne, ValueIntegerZero } from '../../../../../src/dataflow/eval/values/scalar/scalar-constants'; @@ -32,9 +32,9 @@ describe('scalar', () => { { label: '1 + 0', value: binaryScalar(ValueIntegerOne, ValueIntegerZero, 'add'), expect: 1 }, { label: '1 - 1', value: binaryScalar(ValueIntegerOne, ValueIntegerOne, 'sub'), expect: 0 }, { label: '1 * 0', value: binaryScalar(ValueIntegerOne, ValueIntegerZero, 'mul'), expect: 0 }, - { label: '1 * 2', value: binaryScalar(ValueIntegerOne, getScalarInteger(2), 'mul'), expect: 2 }, - { label: 'mod(5, 2)', value: binaryScalar(getScalarInteger(5), getScalarInteger(2), 'mod'), expect: 1 }, - { label: 'mod(5, 3)', value: binaryScalar(getScalarInteger(5), getScalarInteger(3), 'mod'), expect: 2 }, + { label: '1 * 2', value: binaryScalar(ValueIntegerOne, getScalarFromInteger(2), 'mul'), expect: 2 }, + { label: 'mod(5, 2)', value: binaryScalar(getScalarFromInteger(5), getScalarFromInteger(2), 'mod'), expect: 1 }, + { label: 'mod(5, 3)', value: binaryScalar(getScalarFromInteger(5), getScalarFromInteger(3), 'mod'), expect: 2 }, ])('$label', ({ value, expect }) => { shouldBeNum(value, expect); }); From 469506d1a107b0170cc23603d19c73d7fd80aa7e Mon Sep 17 00:00:00 2001 From: Florian Sihler Date: Sat, 15 Mar 2025 17:50:03 +0100 Subject: [PATCH 3/6] feat: had a little bit of time again --- .../eval/values/intervals/interval-binary.ts | 118 +++++++---- .../eval/values/intervals/interval-check.ts | 41 +++- .../eval/values/intervals/interval-compare.ts | 142 +++++++++++-- .../values/intervals/interval-constants.ts | 44 ++-- .../eval/values/intervals/interval-unary.ts | 195 ++++++++++++++++-- .../eval/values/logical/logical-binary.ts | 12 +- .../eval/values/logical/logical-check.ts | 24 ++- .../eval/values/logical/logical-compare.ts | 17 ++ .../eval/values/logical/logical-constants.ts | 39 ++-- .../eval/values/logical/logical-unary.ts | 35 +++- src/dataflow/eval/values/r-value.ts | 99 +++++++-- .../eval/values/scalar/scalar-binary.ts | 53 +++-- .../eval/values/scalar/scalar-check.ts | 52 +++++ .../eval/values/scalar/scalar-compare.ts | 46 +++-- .../eval/values/scalar/scalar-constants.ts | 29 ++- .../eval/values/scalar/scalar-unary.ts | 57 ++--- .../eval/values/string/string-compare.ts | 45 ++++ src/dataflow/eval/values/value-compare.ts | 80 +++++++ src/util/lazy.ts | 38 ++++ .../interval/eval-interval-simple.test.ts | 163 ++++++++++++--- .../eval/logical/eval-logical-simple.test.ts | 22 +- .../eval/scalar/eval-scalar-simple.test.ts | 14 +- 22 files changed, 1112 insertions(+), 253 deletions(-) create mode 100644 src/dataflow/eval/values/logical/logical-compare.ts create mode 100644 src/dataflow/eval/values/scalar/scalar-check.ts create mode 100644 src/dataflow/eval/values/string/string-compare.ts create mode 100644 src/dataflow/eval/values/value-compare.ts create mode 100644 src/util/lazy.ts diff --git a/src/dataflow/eval/values/intervals/interval-binary.ts b/src/dataflow/eval/values/intervals/interval-binary.ts index 740afdaba62..3b6942f3f43 100644 --- a/src/dataflow/eval/values/intervals/interval-binary.ts +++ b/src/dataflow/eval/values/intervals/interval-binary.ts @@ -1,88 +1,122 @@ -import type { Lift, ValueInterval } from '../r-value'; -import { bottomTopGuard } from '../general'; +import type { ValueInterval } from '../r-value'; +import { isBottom } from '../r-value'; import { binaryScalar } from '../scalar/scalar-binary'; -import { orderIntervalFrom } from './interval-constants'; +import { ValueIntervalBottom, orderIntervalFrom, ValueIntervalZero, ValueIntervalTop } from './interval-constants'; +import { binaryLogical } from '../logical/logical-binary'; +import { compareInterval } from './interval-compare'; +import { iteLogical } from '../logical/logical-check'; +import { unaryInterval } from './interval-unary'; /** * Take two potentially lifted intervals and combine them with the given op. * This propagates `top` and `bottom` values. */ -export function binaryInterval, B extends Lift>( +export function binaryInterval( a: A, - b: B, - op: keyof typeof Operations -): Lift { - return bottomTopGuard(a, b) ?? Operations[op](a as ValueInterval, b as ValueInterval); + op: keyof typeof Operations, + b: B +): ValueInterval { + // TODO: improve handling of open intervals + a = unaryInterval(a, 'toClosed') as A; + b = unaryInterval(b, 'toClosed') as B; + return Operations[op](a as ValueInterval, b as ValueInterval); } const Operations = { add: intervalAdd, sub: intervalSub, mul: intervalMul, + div: intervalDiv, intersect: intervalIntersect, union: intervalUnion, setminus: intervalSetminus } as const; -function intervalAdd(a: A, b: B): Lift { +function intervalAdd(a: A, b: B): ValueInterval { return orderIntervalFrom( - binaryScalar(a.start, b.start, 'add'), - binaryScalar(a.end, b.end, 'add'), + binaryScalar(a.start, 'add', b.start), + binaryScalar(a.end, 'add', b.end), a.startInclusive && b.startInclusive, a.endInclusive && b.endInclusive ); } -function intervalSub(a: A, b: B): Lift { +function intervalSub(a: A, b: B): ValueInterval { return orderIntervalFrom( - binaryScalar(a.start, b.end, 'sub'), - binaryScalar(a.end, b.start, 'sub'), + binaryScalar(a.start, 'sub', b.end), + binaryScalar(a.end, 'sub', b.start), + a.startInclusive && b.endInclusive, + a.endInclusive && b.startInclusive + ); +} + +function intervalIntersect(a: A, b: B): ValueInterval { + return orderIntervalFrom( + binaryScalar(a.start, 'max', b.start), + binaryScalar(a.end, 'min', b.end), a.startInclusive && b.startInclusive, a.endInclusive && b.endInclusive ); } -function intervalIntersect(a: A, b: B): Lift { +function intervalUnion(a: A, b: B): ValueInterval { return orderIntervalFrom( - binaryScalar(a.start, b.start, 'max'), - binaryScalar(a.end, b.end, 'min'), + binaryScalar(a.start, 'min', b.start), + binaryScalar(a.end, 'max', b.end), a.startInclusive && b.startInclusive, a.endInclusive && b.endInclusive ); } -function intervalUnion(a: A, b: B): Lift { + +function intervalSetminus(a: A, b: B): ValueInterval { return orderIntervalFrom( - binaryScalar(a.start, b.start, 'min'), - binaryScalar(a.end, b.end, 'max'), + binaryScalar(a.start, 'max', b.end), + binaryScalar(a.end, 'min', b.start), a.startInclusive && b.startInclusive, a.endInclusive && b.endInclusive ); } -// TODO: take support for div sin and other functions that i wrote +function intervalMul(a: A, b: B): 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); -function intervalSetminus(a: A, b: B): Lift { - return { - type: 'interval', - startInclusive: a.startInclusive && b.startInclusive, - start: binaryScalar(a.start, b.end, 'max'), - endInclusive: a.endInclusive && b.endInclusive, - end: binaryScalar(a.end, b.start, 'min') - }; + return orderIntervalFrom( + [ll, lu, ul, uu].reduce((acc, val) => binaryScalar(acc, 'min', val)), + [ll, lu, ul, uu].reduce((acc, val) => binaryScalar(acc, 'max', val)), + a.startInclusive && b.startInclusive, + a.endInclusive && b.endInclusive + ); } -function intervalMul(a: A, b: B): Lift { - const ll = binaryScalar(a.start, b.start, 'mul'); - const lu = binaryScalar(a.start, b.end, 'mul'); - const ul = binaryScalar(a.end, b.start, 'mul'); - const uu = binaryScalar(a.end, b.end, 'mul'); - - return { - type: 'interval', - startInclusive: a.startInclusive && b.startInclusive, - start: [ll, lu, ul, uu].reduce((acc, val) => binaryScalar(acc, val, 'min')), - endInclusive: a.endInclusive && b.endInclusive, - end: [ll, lu, ul, uu].reduce((acc, val) => binaryScalar(acc, val, 'max')) - }; +// TODO: take support for div sin and other functions that i wrote + +function intervalDiv(a: A, b: B): ValueInterval { + // if both are zero switch to bot + const bothAreZero = binaryLogical( + compareInterval(a, '===', ValueIntervalZero), + 'and', + compareInterval(b, '===', ValueIntervalZero)); + + const calcWithPotentialZero = () => + binaryInterval(a, 'mul', unaryInterval(b, 'flip')); + + return iteLogical( + bothAreZero, + { + onTrue: ValueIntervalBottom, + onMaybe: calcWithPotentialZero, + onFalse: calcWithPotentialZero, + onTop: ValueIntervalTop, + onBottom: ValueIntervalBottom + } + ); + } \ No newline at end of file diff --git a/src/dataflow/eval/values/intervals/interval-check.ts b/src/dataflow/eval/values/intervals/interval-check.ts index 076a10aef2a..4eff06c8df9 100644 --- a/src/dataflow/eval/values/intervals/interval-check.ts +++ b/src/dataflow/eval/values/intervals/interval-check.ts @@ -1,17 +1,38 @@ import type { Lift, ValueInterval, ValueLogical } from '../r-value'; -import { bottomTopGuard } from '../general'; +import { liftLogical, ValueLogicalFalse } from '../logical/logical-constants'; +import { compareInterval } from './interval-compare'; +import { ValueIntervalZero } from './interval-constants'; +import { compareScalar } from '../scalar/scalar-compare'; +import { binaryLogical } from '../logical/logical-binary'; const CheckOperations = { - 'empty': intervalEmpty -} as const; + /** check if the interval contains no values */ + empty: intervalEmpty, + /** check if the interval contains exactly one value */ + scalar: intervalScalar, + hasZero: a => compareInterval(ValueIntervalZero, '⊆', a) +} as const as Record ValueLogical>; -export function checkInterval>(a: A, op: keyof typeof CheckOperations): Lift { - return bottomTopGuard(a) ?? CheckOperations[op](a as ValueInterval); +export function checkInterval>(a: A, op: keyof typeof CheckOperations): ValueLogical { + return CheckOperations[op](a as ValueInterval); } function intervalEmpty(a: A): ValueLogical { - return { - type: 'logical', - value: a.start < a.end || (a.start === a.end && (!a.startInclusive || !a.endInclusive)) - }; -} \ No newline at end of file + return binaryLogical( + compareScalar(a.start, '>', a.end), + 'or', + binaryLogical( + compareScalar(a.start, '===', a.end), + 'and', + liftLogical(!a.startInclusive || !a.endInclusive) + ) + ); +} + +function intervalScalar(a: A): ValueLogical { + if(!a.startInclusive || !a.endInclusive) { + return ValueLogicalFalse; + } else { + return compareScalar(a.start, '===', a.end); + } +} diff --git a/src/dataflow/eval/values/intervals/interval-compare.ts b/src/dataflow/eval/values/intervals/interval-compare.ts index 8c66a97664c..bff3508e235 100644 --- a/src/dataflow/eval/values/intervals/interval-compare.ts +++ b/src/dataflow/eval/values/intervals/interval-compare.ts @@ -1,32 +1,144 @@ -import type { Lift, ValueInterval, ValueLogical } from '../r-value'; -import { bottomTopGuard } from '../general'; +import type { ValueInterval, ValueLogical } from '../r-value'; +import { isBottom, isTop } from '../r-value'; import { checkInterval } from './interval-check'; import { binaryInterval } from './interval-binary'; import { iteLogical } from '../logical/logical-check'; -import { ValueLogicalMaybe } from '../logical/logical-constants'; +import { + liftLogical, + ValueLogicalBot, + ValueLogicalFalse, + ValueLogicalMaybe, + ValueLogicalTrue +} from '../logical/logical-constants'; import { compareScalar } from '../scalar/scalar-compare'; import { getIntervalEnd, getIntervalStart } from './interval-constants'; +import { binaryLogical } from '../logical/logical-binary'; +import { unaryLogical } from '../logical/logical-unary'; +import type { ValueCompareOperation } from '../value-compare'; const CompareOperations = { - '<=': intervalLeq, -} as const; + '<': (a, b) => intervalLower(a, b, '<'), + '>': (a, b) => intervalLower(b, a, '<'), + '<=': (a, b) => intervalLower(a, b, '<='), + '>=': (a, b) => intervalLower(b, a, '<='), + '!==': (a, b) => unaryLogical(compareInterval(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) => unaryLogical(compareInterval(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 as Record ValueLogical>; -export function compareInterval(a: A, b: B, op: keyof typeof CompareOperations): Lift { - return bottomTopGuard(a, b) ?? CompareOperations[op](a as ValueInterval, b as ValueInterval); +export function compareInterval(a: A, op: ValueCompareOperation, b: B): ValueLogical { + return CompareOperations[op](a, b); } -function intervalLeq(a: A, b: B): Lift { +function intervalLower(a: A, b: B, op: '<' | '<='): ValueLogical { // if intersect af a and b is non-empty, return maybe // else return a.end <= b.start - const intersect = binaryInterval(a, b, 'intersect'); + const intersect = binaryInterval(a, 'intersect', b); + // TODO: < case if one is inclusive and the other isn't return iteLogical( checkInterval(intersect, 'empty'), - ValueLogicalMaybe, - compareScalar( - getIntervalEnd(a), - getIntervalStart(b), - '<=' - ) + { + onTrue: () => compareScalar(getIntervalEnd(a), op, getIntervalStart(b)), + onFalse: ValueLogicalMaybe, + onMaybe: ValueLogicalMaybe, + onTop: ValueLogicalMaybe, + onBottom: ValueLogicalBot + } ); +} + +function intervalSubset(a: A, b: B, op: '⊂' | '⊆'): ValueLogical { + // 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( + compareScalar(getIntervalStart(a), '>=', getIntervalStart(b)), + { + onTrue: () => + iteLogical(compareScalar(getIntervalEnd(a), '<=', getIntervalEnd(b)), { + onTrue: op === '⊂' ? compareInterval(a, '!==', b) : ValueLogicalTrue, + onFalse: ValueLogicalFalse, + onMaybe: ValueLogicalMaybe, + onTop: ValueLogicalMaybe, + onBottom: ValueLogicalBot + }), + onFalse: ValueLogicalFalse, + onMaybe: ValueLogicalMaybe, + onTop: ValueLogicalMaybe, + onBottom: ValueLogicalBot + } + ); +} + +function intervalEqual(a: A, b: B): ValueLogical { + // 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 = binaryInterval(a, 'intersect', b); + + const areBothScalar = () => + iteLogical( + binaryLogical(checkInterval(a, 'scalar'), 'and', checkInterval(b, 'scalar')), + { + onTrue: ValueLogicalTrue, // they intersect and they are both scalar + onFalse: ValueLogicalFalse, + onMaybe: ValueLogicalMaybe, + onTop: ValueLogicalMaybe, + onBottom: ValueLogicalBot + } + ); + + return iteLogical( + checkInterval(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: A, b: B): ValueLogical { + // check if start, end, and inclusivity is identical + if( + isTop(a) || isTop(b) || isBottom(a) || isBottom(b) + ) { + return liftLogical( + a.start === b.start && + a.end === b.end && + a.startInclusive === b.startInclusive && + a.endInclusive === b.endInclusive + ); + } 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) ? + binaryLogical( + compareScalar(getIntervalStart(a), '===', getIntervalStart(b)), + 'and', + compareScalar(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 index 05e5b207c39..d67803f1170 100644 --- a/src/dataflow/eval/values/intervals/interval-constants.ts +++ b/src/dataflow/eval/values/intervals/interval-constants.ts @@ -1,7 +1,11 @@ import type { RNumberValue } from '../../../../r-bridge/lang-4.x/convert-values'; -import type { Lift, ValueInterval, ValueNumber } from '../r-value'; -import { bottomTopGuard } from '../general'; -import { getScalarFromInteger, liftScalar } from '../scalar/scalar-constants'; +import type { ValueInterval, ValueNumber } from '../r-value'; +import { + getScalarFromInteger, + liftScalar, + ValueIntegerBottom, ValueIntegerPositiveInfinity, + ValueIntegerTop, ValueIntegerZero +} from '../scalar/scalar-constants'; import { iteLogical } from '../logical/logical-check'; import { compareScalar } from '../scalar/scalar-compare'; @@ -14,7 +18,7 @@ export function intervalFrom(start: RNumberValue | number, end = start, startInc ); } -export function intervalFromValues(start: Lift, end = start, startInclusive = true, endInclusive = true): ValueInterval { +export function intervalFromValues(start: ValueNumber, end = start, startInclusive = true, endInclusive = true): ValueInterval { return { type: 'interval', start, @@ -24,22 +28,32 @@ export function intervalFromValues(start: Lift, end = start, startI }; } -export function orderIntervalFrom(start: Lift, end = start, startInclusive = true, endInclusive = true): Lift { +export function orderIntervalFrom(start: ValueNumber, end = start, startInclusive = true, endInclusive = true): ValueInterval { + const onTrue = () => intervalFromValues(start, end, startInclusive, endInclusive); return iteLogical( - compareScalar(start, end, '<='), - intervalFromValues(start, end, startInclusive, endInclusive), - intervalFromValues(end, start, startInclusive, endInclusive) + compareScalar(start, '<=', end), + { + onTrue, + onMaybe: onTrue, + onFalse: () => intervalFromValues(end, start, endInclusive, startInclusive), + onTop: ValueIntervalTop, + onBottom: ValueIntervalBottom + } ); } -export function getIntervalStart(interval: Lift): Lift { - return applyIntervalOp(interval, op => op.start); +export function getIntervalStart(interval: ValueInterval): ValueNumber { + return interval.start; } -export function getIntervalEnd(interval: Lift): Lift { - return applyIntervalOp(interval, op => op.end); +export function getIntervalEnd(interval: ValueInterval): ValueNumber { + return interval.end; } -function applyIntervalOp(interval: Lift, op: (interval: ValueInterval) => Lift): Lift { - return bottomTopGuard(interval) ?? op(interval as ValueInterval); -} \ No newline at end of file +export const ValueIntervalZero = intervalFrom(0); +export const ValueIntervalOne = intervalFrom(1); +export const ValueIntervalNegativeOne = intervalFrom(-1); +export const ValueIntervalZeroToOne = intervalFrom(0, 1); +export const ValueIntervalTop = intervalFromValues(ValueIntegerTop, ValueIntegerTop); +export const ValueIntervalBottom = intervalFromValues(ValueIntegerBottom, ValueIntegerBottom); +export const ValuePositiveInfinite = intervalFromValues(ValueIntegerZero, ValueIntegerPositiveInfinity, false); \ No newline at end of file diff --git a/src/dataflow/eval/values/intervals/interval-unary.ts b/src/dataflow/eval/values/intervals/interval-unary.ts index c6b4ac1b37c..89f52bc14d6 100644 --- a/src/dataflow/eval/values/intervals/interval-unary.ts +++ b/src/dataflow/eval/values/intervals/interval-unary.ts @@ -1,29 +1,196 @@ import type { Lift, ValueInterval } from '../r-value'; -import { bottomTopGuard } from '../general'; -import { ValueIntegerNegativeOne } from '../scalar/scalar-constants'; +import { asValue } from '../r-value'; +import type { ScalarUnaryOperation } from '../scalar/scalar-unary'; +import { unaryScalar } from '../scalar/scalar-unary'; +import { + intervalFromValues, + ValueIntervalTop, + orderIntervalFrom, + ValueIntervalBottom, ValuePositiveInfinite +} from './interval-constants'; +import { iteLogical } from '../logical/logical-check'; +import { checkScalar } from '../scalar/scalar-check'; import { binaryScalar } from '../scalar/scalar-binary'; +import { + ValueIntegerNegativeInfinity, + ValueIntegerOne, + ValueIntegerPositiveInfinity, + ValueIntegerZero, ValueNumberEpsilon +} from '../scalar/scalar-constants'; +import { checkInterval } from './interval-check'; + /** * Take two potentially lifted intervals and combine them with the given op. * This propagates `top` and `bottom` values. */ -export function unaryInterval>( +export function unaryInterval( a: A, op: keyof typeof Operations -): Lift { - return bottomTopGuard(a) ?? Operations[op](a as ValueInterval); +): ValueInterval { + return Operations[op](a as ValueInterval); } +// 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 = { - negate: intervalNegate -} as const; + 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), + // TODO: sign does not propagate top but returns [-1, 1]! + sign: a => intervalApplyBoth(a, 'sign', true), + /** 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 min = a.startInclusive ? a.start : binaryScalar(a.start, 'add', ValueNumberEpsilon); + 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 max = a.endInclusive ? a.end : binaryScalar(a.end, 'sub', ValueNumberEpsilon); + return intervalFromValues(max, max, true, true); + }, + /** essentially returns [lowest(v), highest(v)] */ + toClosed: a => { + const min = a.startInclusive ? a.start : binaryScalar(a.start, 'add', ValueNumberEpsilon); + const max = a.endInclusive ? a.end : binaryScalar(a.end, 'sub', ValueNumberEpsilon); + return intervalFromValues(min, max, true, true); + }, +} as const satisfies Record Lift>; + + +function intervalApplyBoth(a: A, op: ScalarUnaryOperation, toClosed: boolean, startInclusive = a.startInclusive, endInclusive = a.endInclusive): ValueInterval { + if(toClosed) { + a = asValue(unaryInterval(a, 'toClosed')) as A; + startInclusive = true; + endInclusive = true; + } + + return orderIntervalFrom( + unaryScalar(a.start, op), + unaryScalar(a.end, op), + startInclusive, + endInclusive + ); +} function intervalNegate(a: A): ValueInterval { - return { - type: 'interval', - startInclusive: a.endInclusive, - start: binaryScalar(a.end, ValueIntegerNegativeOne, 'mul'), - endInclusive: a.startInclusive, - end: binaryScalar(a.start, ValueIntegerNegativeOne, 'mul') - }; + return intervalFromValues( + unaryScalar(a.end, 'negate'), + unaryScalar(a.start, 'negate'), + a.endInclusive, + a.startInclusive + ); +} + +function intervalAbs(a: A): ValueInterval { + // 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( + checkScalar(a.start, 'isNegative'), + { + onTrue: () => iteLogical( + checkScalar(a.end, 'isNonNegative'), + { + // a <= 0 <= b + onTrue: () => { + const startAbs = unaryScalar(a.start, 'abs'); + const endAbs = unaryScalar(a.end, 'abs'); + const max = binaryScalar( + startAbs, + 'max', + endAbs + ); + // 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'), + unaryScalar(a.start, 'abs'), + a.endInclusive, + true // TODO: check + ), + onMaybe: ValuePositiveInfinite, + onTop: ValuePositiveInfinite, + onBottom: ValueIntervalBottom + } + ), + onMaybe: ValueIntervalTop, + onTop: ValueIntervalTop, + onBottom: ValueIntervalBottom, + onFalse: () => a + } + ); +} + +function intervalDivByOne(a: A): ValueInterval { + const ifStartIsZero = () => + iteLogical(checkScalar(a.end, 'isZero'), + { + onTrue: ValueIntervalBottom, + onMaybe: ValueIntervalTop, + onFalse: () => intervalFromValues( + ValueIntegerNegativeInfinity, + ValueIntegerPositiveInfinity, + false, // TODO: check + true // TODO: check + ), + onTop: ValueIntervalTop, + onBottom: ValueIntervalBottom + }); + const neitherIsZero = () => iteLogical( + checkInterval(a, 'hasZero'), + { + onTrue: ValueIntervalTop, + onTop: ValueIntervalTop, + onFalse: () => orderIntervalFrom( + binaryScalar(ValueIntegerOne, 'div', a.start), + binaryScalar(ValueIntegerOne, 'div', a.end), + a.startInclusive, // TODO: check + a.endInclusive // TODO: check + ), + onMaybe: ValueIntervalTop, + onBottom: ValueIntervalBottom + } + ); + const ifStartIsNotZero = () => iteLogical( + checkScalar(a.end, 'isZero'), + { + // start is not zero, but end is zero + onTrue: () => intervalFromValues( + ValueIntegerNegativeInfinity, + binaryScalar(ValueIntegerOne, 'div', a.end), + false, // TODO: check + true // TODO: check + ), + onMaybe: ValueIntervalTop, + onFalse: neitherIsZero, + onTop: ValueIntervalTop, + onBottom: ValueIntervalBottom + } + ); + + return iteLogical( + checkScalar(a.start, 'isZero'), + { + onTrue: ifStartIsZero, + onFalse: ifStartIsNotZero, + onMaybe: ValueIntervalTop, + onTop: ValueIntervalTop, + onBottom: ValueIntervalBottom + } + ); } diff --git a/src/dataflow/eval/values/logical/logical-binary.ts b/src/dataflow/eval/values/logical/logical-binary.ts index 9046703870f..738910fde68 100644 --- a/src/dataflow/eval/values/logical/logical-binary.ts +++ b/src/dataflow/eval/values/logical/logical-binary.ts @@ -1,16 +1,16 @@ -import type { Lift, TernaryLogical, ValueLogical } from '../r-value'; +import type { TernaryLogical, ValueLogical } from '../r-value'; import { bottomTopGuard } from '../general'; /** * Take two potentially lifted logicals and combine them with the given op. * This propagates `top` and `bottom` values. */ -export function binaryLogical, B extends Lift>( +export function binaryLogical( a: A, - b: B, - op: keyof typeof Operations -): Lift { - return bottomTopGuard(a, b) ?? Operations[op](a as ValueLogical, b as ValueLogical); + op: keyof typeof Operations, + b: B +): ValueLogical { + return Operations[op](a as ValueLogical, b as ValueLogical); } const Operations = { diff --git a/src/dataflow/eval/values/logical/logical-check.ts b/src/dataflow/eval/values/logical/logical-check.ts index 7b835df0e28..a46ce25fd00 100644 --- a/src/dataflow/eval/values/logical/logical-check.ts +++ b/src/dataflow/eval/values/logical/logical-check.ts @@ -1,25 +1,33 @@ import type { Lift, TernaryLogical, ValueLogical } from '../r-value'; import { Bottom , Top } from '../r-value'; import { bottomTopGuard } from '../general'; +import type { CanBeLazy } from '../../../../util/lazy'; +import { force } from '../../../../util/lazy'; export function unpackLogical(a: Lift): Lift { return bottomTopGuard(a) ?? (a as ValueLogical).value; } +interface IteCases { + readonly onTrue: CanBeLazy; + readonly onFalse: CanBeLazy; + readonly onMaybe: CanBeLazy; + readonly onTop: CanBeLazy; + readonly onBottom: CanBeLazy; +} + export function iteLogical, Result>( cond: A, - onTrue: Lift, - onFalse: Lift, - onMaybe: Lift = Top -): Lift { + { onTrue, onFalse, onMaybe, onTop, onBottom }: IteCases +): Result { const condVal = unpackLogical(cond); if(condVal === Top) { - return Top; + return force(onTop); } else if(condVal === Bottom) { - return Bottom; + return force(onBottom); } else if(condVal === 'maybe') { - return onMaybe; + return force(onMaybe); } else { - return condVal ? onTrue : onFalse; + return condVal ? force(onTrue) : force(onFalse); } } \ No newline at end of file diff --git a/src/dataflow/eval/values/logical/logical-compare.ts b/src/dataflow/eval/values/logical/logical-compare.ts new file mode 100644 index 00000000000..b0263a0a7b5 --- /dev/null +++ b/src/dataflow/eval/values/logical/logical-compare.ts @@ -0,0 +1,17 @@ +import type { ValueLogical } from '../r-value'; +import { logicalToInterval } from './logical-unary'; +import type { ValueCompareOperation } from '../value-compare'; +import { compareInterval } from '../intervals/interval-compare'; + + +export function compareLogical( + a: A, + op: ValueCompareOperation, + b: B +): ValueLogical { + return compareInterval( + logicalToInterval(a), + op, + logicalToInterval(b) + ); +} diff --git a/src/dataflow/eval/values/logical/logical-constants.ts b/src/dataflow/eval/values/logical/logical-constants.ts index f4ca2f08572..dceb3055675 100644 --- a/src/dataflow/eval/values/logical/logical-constants.ts +++ b/src/dataflow/eval/values/logical/logical-constants.ts @@ -1,14 +1,27 @@ -import type { ValueLogical } from '../r-value'; +import type { Lift, TernaryLogical, ValueLogical } from '../r-value'; +import { Bottom, Top } from '../r-value'; -export const ValueLogicalTrue: ValueLogical = { - type: 'logical', - value: true -}; -export const ValueLogicalFalse: ValueLogical = { - type: 'logical', - value: false -}; -export const ValueLogicalMaybe: ValueLogical = { - type: 'logical', - value: 'maybe' -}; \ No newline at end of file +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 index 01794d1db4a..c7f618a4404 100644 --- a/src/dataflow/eval/values/logical/logical-unary.ts +++ b/src/dataflow/eval/values/logical/logical-unary.ts @@ -1,5 +1,13 @@ -import type { Lift, ValueLogical } from '../r-value'; +import type { Lift, ValueInterval, ValueLogical } from '../r-value'; import { bottomTopGuard } from '../general'; +import { iteLogical } from './logical-check'; +import { + ValueIntervalBottom, + ValueIntervalOne, + ValueIntervalTop, + ValueIntervalZero, + ValueIntervalZeroToOne +} from '../intervals/interval-constants'; /** * Take one potentially lifted logical and apply the given unary op. @@ -7,11 +15,36 @@ import { bottomTopGuard } from '../general'; */ export function unaryLogical>( a: A, + // TODO: support common unary ops op: keyof typeof Operations ): Lift { return bottomTopGuard(a) ?? Operations[op](a as ValueLogical); } +/* + 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; diff --git a/src/dataflow/eval/values/r-value.ts b/src/dataflow/eval/values/r-value.ts index 7aaf4dc07fb..599b9fd73c0 100644 --- a/src/dataflow/eval/values/r-value.ts +++ b/src/dataflow/eval/values/r-value.ts @@ -1,34 +1,37 @@ 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 { 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: Lift + start: Limit startInclusive: boolean - end: Lift + end: Limit endInclusive: boolean } -export interface ValueSet { +export interface ValueSet> { type: 'set' - elements: Lift + elements: Elements } -export interface ValueVector { +export interface ValueVector> { type: 'vector' - elements: Lift + elements: Elements } -export interface ValueNumber { +export interface ValueNumber = Lift> { type: 'number' - value: Lift + value: Num } -export interface ValueString { +export interface ValueString = Lift> { type: 'string' - value: Lift + value: Str } +// TODO: drop maybe and treat it as top export type TernaryLogical = RLogicalValue | 'maybe' export interface ValueLogical { type: 'logical' @@ -52,10 +55,82 @@ export function typeOfValue(value: V): V['type'] { // @ts-expect-error -- this is a save cast export function isTop>(value: V): value is typeof Top { - return typeof value === 'object' && value !== null && 'type' in value && value.type === Top.type; + return value === Top; } // @ts-expect-error -- this is a save cast export function isBottom>(value: V): value is typeof Bottom { - return typeof value === 'object' && value !== null && 'type' in value && value.type === Bottom.type; + 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 => { + switch(v.type) { + case 'interval': + return `${v.startInclusive ? '[' : '('}${stringifyValue(v.start)}, ${stringifyValue(v.end)}${v.endInclusive ? ']' : ')'}`; + case 'set': + return tryStringifyBoTop(v.elements, e => { + return `{${e.map(stringifyValue).join(',')}}`; + }, () => '⊤ (set)', () => '⊥ (set)'); + case 'vector': + return tryStringifyBoTop(v.elements, e => { + return `c(${e.map(stringifyValue).join(',')})`; + }, () => '⊤ (vector)', () => '⊥ (vector)'); + 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)'); + } + }); +} \ 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 index 2e105f7ad3c..d71acaedec8 100644 --- a/src/dataflow/eval/values/scalar/scalar-binary.ts +++ b/src/dataflow/eval/values/scalar/scalar-binary.ts @@ -1,46 +1,63 @@ -import type { Lift, ValueNumber } from '../r-value'; +import type { ValueNumber } from '../r-value'; +import { Bottom } from '../r-value'; import { bottomTopGuard } from '../general'; import type { RNumberValue } from '../../../../r-bridge/lang-4.x/convert-values'; +import { liftScalar, ValueIntegerBottom, ValueIntegerTop } from './scalar-constants'; /** * Take two potentially lifted intervals and combine them with the given op. * This propagates `top` and `bottom` values. */ -export function binaryScalar, B extends Lift>( +export function binaryScalar( a: A, - b: B, - op: keyof typeof Operations -): Lift { - return bottomTopGuard(a, b) ?? Operations[op](a as ValueNumber, b as ValueNumber); + op: keyof typeof ScalarBinaryOperations, + b: B +): ValueNumber { + return ScalarBinaryOperations[op](a as ValueNumber, b as ValueNumber); } -const Operations = { +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) => scalarHelper(a, b, (a, b) => Math.max(a, b)), - min: (a, b) => scalarHelper(a, b, (a, b) => Math.min(a, b)), + max: (a, b) => scalarMaxMin(a, b, 'max'), + min: (a, b) => scalarMaxMin(a, b, 'min'), } as const satisfies Record ValueNumber>; +export type ScalarBinaryOperation = keyof typeof ScalarBinaryOperations; + function scalarHelper( a: A, b: B, c: (a: number, b: number) => number ): ValueNumber { const val = bottomTopGuard(a.value, b.value); + if(val) { + return val === Bottom ? ValueIntegerBottom : ValueIntegerTop; + } const aval = a.value as RNumberValue; const bval = b.value as RNumberValue; + /* do not calculate if top or bot */ const result = c(aval.num, bval.num); - return { - type: 'number', - value: val ?? { - markedAsInt: aval.markedAsInt && bval.markedAsInt && Number.isInteger(result), - complexNumber: aval.complexNumber || bval.complexNumber, - num: result - } - }; -} \ No newline at end of file + 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: A, b: B, c: 'max' | 'min'): ValueNumber { + const bt = bottomTopGuard(a.value, b.value); + if(bt) { + return ValueIntegerTop; + } + const aval = a.value as RNumberValue; + const bval = b.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-check.ts b/src/dataflow/eval/values/scalar/scalar-check.ts new file mode 100644 index 00000000000..0a120762671 --- /dev/null +++ b/src/dataflow/eval/values/scalar/scalar-check.ts @@ -0,0 +1,52 @@ +import type { Lift, ValueLogical, ValueNumber } from '../r-value'; +import { isBottom, isTop } from '../r-value'; +import { liftLogical, ValueLogicalBot, ValueLogicalTop } from '../logical/logical-constants'; +import { bottomTopGuard } from '../general'; + +const ScalarCheckOperations = { + /** `=== 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 Lift>; + +export function checkScalar>(a: A, op: keyof typeof ScalarCheckOperations): Lift { + return bottomTopGuard(a) ?? ScalarCheckOperations[op](a as ValueNumber); +} + +function scalarCheck(a: A, c: (n: number) => boolean): ValueLogical { + if(isTop(a.value)) { + return ValueLogicalTop; + } else if(isBottom(a.value)) { + return ValueLogicalBot; + } else { + return liftLogical(c(a.value.num)); + } +} + +function scalarMarkedAsInt(a: A): ValueLogical { + if(isTop(a.value)) { + return ValueLogicalTop; + } else if(isBottom(a.value)) { + return ValueLogicalBot; + } else { + return liftLogical(a.value.markedAsInt); + } +} + +function scalarMarkedAsComplex(a: A): ValueLogical { + 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/scalar/scalar-compare.ts b/src/dataflow/eval/values/scalar/scalar-compare.ts index ce2823e6280..aaa84364b7f 100644 --- a/src/dataflow/eval/values/scalar/scalar-compare.ts +++ b/src/dataflow/eval/values/scalar/scalar-compare.ts @@ -1,27 +1,42 @@ -import type { Lift, ValueLogical, ValueNumber } from '../r-value'; +import type { ValueLogical, ValueNumber } from '../r-value'; import { bottomTopGuard } from '../general'; import type { RNumberValue } from '../../../../r-bridge/lang-4.x/convert-values'; +import type { ValueCompareOperation } from '../value-compare'; +import { ValueNumberEpsilon } from './scalar-constants'; +import { liftLogical } from '../logical/logical-constants'; /** * Take two potentially lifted intervals and compare them with the given op. * This propagates `top` and `bottom` values. */ -export function compareScalar, B extends Lift>( +export function compareScalar( a: A, - b: B, - op: keyof typeof Operations -): Lift { - return bottomTopGuard(a, b) ?? Operations[op](a as ValueNumber, b as ValueNumber); + op: keyof typeof Operations, + b: B +): ValueLogical { + return Operations[op](a as ValueNumber, b as ValueNumber); +} + +function identicalNumbersThreshold(a: number, b: number): boolean { + return Math.abs(a - b) < 2 * ValueNumberEpsilon.value.num; } const Operations = { - '<=': (a, b) => scalarHelper(a, b, (a, b) => a <= b), - '<': (a, b) => scalarHelper(a, b, (a, b) => a < b), - '>=': (a, b) => scalarHelper(a, b, (a, b) => a >= b), - '>': (a, b) => scalarHelper(a, b, (a, b) => a > b), - '==': (a, b) => scalarHelper(a, b, (a, b) => a === b), - '!=': (a, b) => scalarHelper(a, b, (a, b) => a !== b) -} as const satisfies Record ValueLogical>; + '<=': (a, b) => scalarHelper(a, b, (a, b) => a <= b), + '<': (a, b) => scalarHelper(a, b, (a, b) => a < b), + '>=': (a, b) => scalarHelper(a, b, (a, b) => a >= b), + '>': (a, b) => scalarHelper(a, b, (a, b) => a > b), + '==': (a, b) => scalarHelper(a, b, (a, b) => identicalNumbersThreshold(a, b)), + '!=': (a, b) => scalarHelper(a, b, (a, b) => !identicalNumbersThreshold(a, b)), + '===': (a, b) => scalarHelper(a, b, (a, b) => identicalNumbersThreshold(a, b)), + '!==': (a, b) => scalarHelper(a, b, (a, b) => !identicalNumbersThreshold(a, b)), + /** subseteq is only fulfilled if they are the same */ + '⊆': (a, b) => scalarHelper(a, b, (a, b) => identicalNumbersThreshold(a, b)), + /** subset is never fulfilled */ + '⊂': (a, b) => scalarHelper(a, b, (_a, _b) => false), + '⊇': (a, b) => scalarHelper(a, b, (a, b) => identicalNumbersThreshold(b, a)), + '⊃': (a, b) => scalarHelper(a, b, (_a, _b) => false) +} as const satisfies Record ValueLogical>; function scalarHelper( a: A, @@ -31,8 +46,5 @@ function scalarHelper( const val = bottomTopGuard(a.value, b.value); const aval = a.value as RNumberValue; const bval = b.value as RNumberValue; - return { - type: 'logical', - value: val ?? c(aval.num, bval.num) - }; + return liftLogical(val ?? c(aval.num, bval.num)); } \ No newline at end of file diff --git a/src/dataflow/eval/values/scalar/scalar-constants.ts b/src/dataflow/eval/values/scalar/scalar-constants.ts index 8b2bb6da9ff..8250e793091 100644 --- a/src/dataflow/eval/values/scalar/scalar-constants.ts +++ b/src/dataflow/eval/values/scalar/scalar-constants.ts @@ -1,24 +1,35 @@ -import type { ValueNumber } from '../r-value'; +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(value: number): ValueNumber { +export function getScalarFromInteger(num: number, markedAsInt = Number.isInteger(num), complexNumber = false): ValueNumber { return { type: 'number', value: { - markedAsInt: true, - num: value, - complexNumber: false + markedAsInt, + num, + complexNumber } }; } -export function liftScalar(value: RNumberValue): ValueNumber { +export function liftScalar(value: Lift): ValueNumber { return { type: 'number', value: value }; } -export const ValueIntegerOne: ValueNumber = getScalarFromInteger(1); -export const ValueIntegerZero: ValueNumber = getScalarFromInteger(0); -export const ValueIntegerNegativeOne: ValueNumber = getScalarFromInteger(-1); +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 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 index acaea9ca372..2b918ef3807 100644 --- a/src/dataflow/eval/values/scalar/scalar-unary.ts +++ b/src/dataflow/eval/values/scalar/scalar-unary.ts @@ -1,4 +1,4 @@ -import type { Lift, ValueNumber } from '../r-value'; +import type { ValueNumber } from '../r-value'; import { bottomTopGuard } from '../general'; import type { RNumberValue } from '../../../../r-bridge/lang-4.x/convert-values'; @@ -6,37 +6,40 @@ import type { RNumberValue } from '../../../../r-bridge/lang-4.x/convert-values' * Take a potentially lifted interval and apply the given op. * This propagates `top` and `bottom` values. */ -export function unaryScalar>( +export function unaryScalar( a: A, - op: keyof typeof Operations -): Lift { - return bottomTopGuard(a) ?? Operations[op](a as ValueNumber); + op: keyof typeof ScalarUnaryOperations +): ValueNumber { + return ScalarUnaryOperations[op](a as ValueNumber); } -const Operations = { - negate: (a: ValueNumber) => scalarHelper(a, (a) => -a), - abs: (a: ValueNumber) => scalarHelper(a, Math.abs), - ceil: (a: ValueNumber) => scalarHelper(a, Math.ceil), - floor: (a: ValueNumber) => scalarHelper(a, Math.floor), - round: (a: ValueNumber) => scalarHelper(a, Math.round), - exp: (a: ValueNumber) => scalarHelper(a, Math.exp), - log: (a: ValueNumber) => scalarHelper(a, Math.log), - log10: (a: ValueNumber) => scalarHelper(a, Math.log10), - log2: (a: ValueNumber) => scalarHelper(a, Math.log2), - sign: (a: ValueNumber) => scalarHelper(a, Math.sign), - sqrt: (a: ValueNumber) => scalarHelper(a, Math.sqrt), - sin: (a: ValueNumber) => scalarHelper(a, Math.sin), - cos: (a: ValueNumber) => scalarHelper(a, Math.cos), - tan: (a: ValueNumber) => scalarHelper(a, Math.tan), - asin: (a: ValueNumber) => scalarHelper(a, Math.asin), - acos: (a: ValueNumber) => scalarHelper(a, Math.acos), - atan: (a: ValueNumber) => scalarHelper(a, Math.atan), - sinh: (a: ValueNumber) => scalarHelper(a, Math.sinh), - cosh: (a: ValueNumber) => scalarHelper(a, Math.cosh), - tanh: (a: ValueNumber) => scalarHelper(a, Math.tanh), - +const ScalarUnaryOperations = { + id: a => a, + negate: a => scalarHelper(a, (a) => -a), + abs: a => scalarHelper(a, Math.abs), + 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), + cos: a => scalarHelper(a, Math.cos), + tan: a => scalarHelper(a, Math.tan), + 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) } as const satisfies Record ValueNumber>; +export type ScalarUnaryOperation = keyof typeof ScalarUnaryOperations; + +// TODO: support sin clamp to [-1, 1] etc. function scalarHelper(a: A, op: (a: number) => number): ValueNumber { const val = bottomTopGuard(a.value); const aval = a.value as RNumberValue; diff --git a/src/dataflow/eval/values/string/string-compare.ts b/src/dataflow/eval/values/string/string-compare.ts new file mode 100644 index 00000000000..d1810db904e --- /dev/null +++ b/src/dataflow/eval/values/string/string-compare.ts @@ -0,0 +1,45 @@ +import type { ValueLogical, ValueString } from '../r-value'; +import { bottomTopGuard } from '../general'; +import type { RStringValue } from '../../../../r-bridge/lang-4.x/convert-values'; +import type { ValueCompareOperation } from '../value-compare'; +import { liftLogical } from '../logical/logical-constants'; + +/** + * Take two potentially lifted intervals and compare them with the given op. + * This propagates `top` and `bottom` values. + */ +export function compareString( + a: A, + op: keyof typeof Operations, + b: B +): ValueLogical { + return Operations[op](a as ValueString, b as ValueString); +} + +const Operations = { + '<=': (a, b) => stringHelper(a, b, (a, b) => a <= b), + '<': (a, b) => stringHelper(a, b, (a, b) => a < b), + '>=': (a, b) => stringHelper(a, b, (a, b) => a >= b), + '>': (a, b) => stringHelper(a, b, (a, b) => a > b), + '==': (a, b) => stringHelper(a, b, (a, b) => a === b), + '!=': (a, b) => stringHelper(a, b, (a, b) => a !== b), + '===': (a, b) => stringHelper(a, b, (a, b) => a === b), + '!==': (a, b) => stringHelper(a, b, (a, b) => a !== b), + /* we do subsets as includes */ + '⊆': (a, b) => stringHelper(a, b, (a, b) => b.includes(a)), + '⊂': (a, b) => stringHelper(a, b, (a, b) => b.includes(a) && a !== b), + '⊇': (a, b) => stringHelper(a, b, (a, b) => a.includes(b)), + '⊃': (a, b) => stringHelper(a, b, (a, b) => a.includes(b) && a !== b) +} as const satisfies Record ValueLogical>; + +function stringHelper( + a: A, + b: B, + c: (a: string, b: string) => boolean +): ValueLogical { + 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/value-compare.ts b/src/dataflow/eval/values/value-compare.ts new file mode 100644 index 00000000000..4cca6b4130e --- /dev/null +++ b/src/dataflow/eval/values/value-compare.ts @@ -0,0 +1,80 @@ +import type { Lift, Value, ValueLogical, ValueTypes } from './r-value'; +import { isBottom, isTop } from './r-value'; +import { liftLogical, ValueLogicalBot, ValueLogicalTop } from './logical/logical-constants'; +import { compareScalar } from './scalar/scalar-compare'; +import { compareLogical } from './logical/logical-compare'; +import { compareInterval } from './intervals/interval-compare'; +import { guard } from '../../../util/assert'; +import { intervalFromValues } from './intervals/interval-constants'; +import { compareString } from './string/string-compare'; + +export type ValueCompareOperation = '<' | '>' | '<=' | '>=' | '===' | '!==' + | '==' | '!=' | '⊆' | '⊂' | '⊃' | '⊇'; + +// besides identical and top/bot +const comparableTypes = new Set([ + ['number', 'interval'] +].flatMap(([a, b]) => [`${a}<>${b}`, `${b}<>${a}`])); + + +export const GeneralCompareOperations = { + 'meta:identical-objects': (a, b) => liftLogical(a === b), + 'meta:comparable': (a, b) => liftLogical( + isTop(a) || isTop(b) || isBottom(a) || isBottom(b) || a.type === b.type || comparableTypes.has(`${a.type}<>${b.type}`) + ) +} as const satisfies Record ValueLogical>; + + +const compareForType = { + 'number': compareScalar, + 'logical': compareLogical, + 'interval': compareInterval, + 'string': compareString, + 'set': compareScalar, // TODO + 'vector': compareScalar // TODO +} as const satisfies Record; + +export function compareValues, B extends Lift>( + a: A, + op: ValueCompareOperation | keyof typeof GeneralCompareOperations, + b: B +): Lift { + const general: undefined | ((a: Value, b: Value) => ValueLogical) = GeneralCompareOperations[op as keyof typeof GeneralCompareOperations]; + if(general !== undefined) { + return general(a, b); + } + if(isBottom(a) || isBottom(b)) { + return ValueLogicalBot; + } + + if(isTop(a)) { + if(isTop(b)) { + return ValueLogicalTop; + } else { + return compareEnsured(a, op, b, b.type); + } + } else if(isTop(b)) { + return compareEnsured(a, op, b, a.type); + } + + if(a.type === b.type) { + return compareEnsured(a, op, b, a.type); + } + + guard(comparableTypes.has(`${a.type}<>${b.type}`), `Cannot compare ${a.type} with ${b.type}`); + + if(a.type === 'interval' && b.type === 'number') { + return compareEnsured(a, op, intervalFromValues(b, b), a.type); + } else if(a.type === 'number' && b.type === 'interval') { + return compareEnsured(intervalFromValues(a, a), op, b, b.type); + } + + return ValueLogicalTop; +} + +function compareEnsured, B extends Lift>( + a: A, op: string, b: B, + type: ValueTypes +): Lift { + return (compareForType[type as keyof typeof compareForType] as (a: Value, op: string, b: Value) => Lift)(a, op, b); +} \ No newline at end of file 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/test/functionality/dataflow/eval/interval/eval-interval-simple.test.ts b/test/functionality/dataflow/eval/interval/eval-interval-simple.test.ts index 8e0de515507..d4126d345a3 100644 --- a/test/functionality/dataflow/eval/interval/eval-interval-simple.test.ts +++ b/test/functionality/dataflow/eval/interval/eval-interval-simple.test.ts @@ -1,41 +1,148 @@ import { assert, describe, test } from 'vitest'; - - -import { guard } from '../../../../../src/util/assert'; import type { Lift, ValueInterval } from '../../../../../src/dataflow/eval/values/r-value'; -import { isBottom, isTop } 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 { intervalFrom } from '../../../../../src/dataflow/eval/values/intervals/interval-constants'; +import { compareValues } from '../../../../../src/dataflow/eval/values/value-compare'; + +function i(l: '[' | '(', lv: number, rv: number, r: ']' | ')') { + return intervalFrom(lv, rv, l === '[', r === ']'); +} describe('interval', () => { - function shouldBeInterval(val: Lift, expect: readonly [startInclusive: boolean, start: number, end: number, endInclusive: boolean]) { - guard( - !isTop(val) && !isBottom(val) && - !isTop(val.start) && !isBottom(val.start) && - !isTop(val.end) && !isBottom(val.end) + function shouldBeInterval(val: Lift, expect: Lift) { + const res = compareValues(val, '===', expect); + assert.isTrue( + res.type === 'logical' && res.value === true, + `Expected ${stringifyValue(val)} to be ${stringifyValue(expect)}` ); - guard('num' in val.start.value && 'num' in val.end.value); - assert.strictEqual(val.startInclusive, expect[0], 'startInclusive'); - assert.strictEqual(val.start.value.num, expect[1], 'start'); - assert.strictEqual(val.end.value.num, expect[2], 'end'); - assert.strictEqual(val.endInclusive, expect[3], 'endInclusive'); } describe('unary', () => { - test.each([ - { label: '(sanity) should be one', value: intervalFrom(1), expect: [true, 1, 1, true] as const }, - { label: 'negate should work', value: unaryInterval(intervalFrom(1), 'negate'), expect: [true, -1, -1, true] as const }, - { label: 'negate should work for [2, 4]', value: unaryInterval(intervalFrom(2, 4), 'negate'), expect: [true, -4, -2, true] as const }, - { label: 'negate should work for (2, 4]', value: unaryInterval(intervalFrom(2, 4, false), 'negate'), expect: [true, -4, -2, false] as const }, - ])('$label', ({ value, expect }) => { - shouldBeInterval(value, expect); - }); + 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: ValueIntervalTop }, + { 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', () => { - test.each([ - { label: '1 + 1', value: binaryInterval(intervalFrom(1), intervalFrom(1), 'add'), expect: [true, 2, 2, true] as const }, - ])('$label', ({ value, expect }) => { - shouldBeInterval(value, expect); - }); + 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(unaryInterval(binaryInterval(l, t.label, r), 'toClosed'), unaryInterval(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 index 28c79722253..8f04df4cbd6 100644 --- a/test/functionality/dataflow/eval/logical/eval-logical-simple.test.ts +++ b/test/functionality/dataflow/eval/logical/eval-logical-simple.test.ts @@ -29,17 +29,17 @@ describe('logical', () => { }); describe('binary', () => { test.each([ - { label: '1 && 1', value: binaryLogical(ValueLogicalTrue, ValueLogicalTrue, 'and'), expect: true }, - { label: '1 && 0', value: binaryLogical(ValueLogicalTrue, ValueLogicalFalse, 'and'), expect: false }, - { label: '1 && ?', value: binaryLogical(ValueLogicalTrue, ValueLogicalMaybe, 'and'), expect: 'maybe' as const }, - { label: '1 && ?', value: binaryLogical(ValueLogicalTrue, ValueLogicalMaybe, 'and'), expect: 'maybe' as const }, - { label: '? && ?', value: binaryLogical(ValueLogicalMaybe, ValueLogicalMaybe, 'and'), expect: 'maybe' as const }, - { label: '? implies 0', value: binaryLogical(ValueLogicalMaybe, ValueLogicalFalse, 'implies'), expect: 'maybe' as const }, - { label: '0 implies ?', value: binaryLogical(ValueLogicalFalse, ValueLogicalMaybe, 'implies'), expect: true }, - { label: '1 iff 1', value: binaryLogical(ValueLogicalTrue, ValueLogicalTrue, 'iff'), expect: true }, - { label: '0 iff 1', value: binaryLogical(ValueLogicalFalse, ValueLogicalTrue, 'iff'), expect: false }, - { label: '0 iff ?', value: binaryLogical(ValueLogicalFalse, ValueLogicalMaybe, 'iff'), expect: 'maybe' as const }, - { label: '? iff ?', value: binaryLogical(ValueLogicalMaybe, ValueLogicalMaybe, 'iff'), expect: 'maybe' as const }, + { 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); }); diff --git a/test/functionality/dataflow/eval/scalar/eval-scalar-simple.test.ts b/test/functionality/dataflow/eval/scalar/eval-scalar-simple.test.ts index de705e94147..67483310d6e 100644 --- a/test/functionality/dataflow/eval/scalar/eval-scalar-simple.test.ts +++ b/test/functionality/dataflow/eval/scalar/eval-scalar-simple.test.ts @@ -28,13 +28,13 @@ describe('scalar', () => { }); describe('binary', () => { test.each([ - { label: '1 + 1', value: binaryScalar(ValueIntegerOne, ValueIntegerOne, 'add'), expect: 2 }, - { label: '1 + 0', value: binaryScalar(ValueIntegerOne, ValueIntegerZero, 'add'), expect: 1 }, - { label: '1 - 1', value: binaryScalar(ValueIntegerOne, ValueIntegerOne, 'sub'), expect: 0 }, - { label: '1 * 0', value: binaryScalar(ValueIntegerOne, ValueIntegerZero, 'mul'), expect: 0 }, - { label: '1 * 2', value: binaryScalar(ValueIntegerOne, getScalarFromInteger(2), 'mul'), expect: 2 }, - { label: 'mod(5, 2)', value: binaryScalar(getScalarFromInteger(5), getScalarFromInteger(2), 'mod'), expect: 1 }, - { label: 'mod(5, 3)', value: binaryScalar(getScalarFromInteger(5), getScalarFromInteger(3), 'mod'), expect: 2 }, + { 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); }); From 475349aecfa06edf176bb07b4a6e3aa4be2989c4 Mon Sep 17 00:00:00 2001 From: Florian Sihler Date: Sat, 15 Mar 2025 19:58:25 +0100 Subject: [PATCH 4/6] refactor: working on simple evals --- src/dataflow/eval/eval.ts | 67 ++++++++++++++++-- src/dataflow/eval/functions/eval-fns.ts | 68 +++++++++++++++++++ .../values/intervals/interval-constants.ts | 1 + .../eval/values/intervals/interval-unary.ts | 15 ++-- .../eval/values/string/string-constants.ts | 16 +++++ src/dataflow/eval/values/value-binary.ts | 57 ++++++++++++++++ src/dataflow/eval/values/value-unary.ts | 36 ++++++++++ .../dataflow/eval/eval-simple.test.ts | 45 ++++++++++++ 8 files changed, 295 insertions(+), 10 deletions(-) create mode 100644 src/dataflow/eval/functions/eval-fns.ts create mode 100644 src/dataflow/eval/values/string/string-constants.ts create mode 100644 src/dataflow/eval/values/value-binary.ts create mode 100644 src/dataflow/eval/values/value-unary.ts create mode 100644 test/functionality/dataflow/eval/eval-simple.test.ts diff --git a/src/dataflow/eval/eval.ts b/src/dataflow/eval/eval.ts index 66df2c962dc..5d4ed68bb03 100644 --- a/src/dataflow/eval/eval.ts +++ b/src/dataflow/eval/eval.ts @@ -1,10 +1,65 @@ 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 { callEvalFunction } from './functions/eval-fns'; -// x <- 5;eval(parse(text=paste("foo", x))) - -// 2 + 2 -export function eval(node: RNode, dfg: DataflowGraph, env: REnvironmentInformation): RValue { - -} \ No newline at end of file +/** + * 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 + */ +export function evalRExpression(n: RNode, dfg: DataflowGraph, env: REnvironmentInformation): Value { + // TODO: evaluation symbol tracker environment and only access the other environment if we do not know the value + switch(n.type) { + case RType.ExpressionList: { + // TODO: handle break return etc. + let result: Value = Top; + // TODO: '{' for grouping, side-effecs + for(const child of n.children) { + result = evalRExpression(child, dfg, env); + } + return result; + } + 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) { + const values = resolveValueOfVariable(n.content, env, dfg.idMap); + if(values === undefined || values.length === 0) { + return Top; + } + // TODO: map this to r value + const allNumber = values.every(v => typeof v === 'number'); + // TODO: sets + if(allNumber) { + return intervalFrom(Math.min(...values), Math.max(...values)); + } + const allString = values.every(v => typeof v === 'string'); + if(allString) { + // TODO: this handling is not correct + return stringFrom(values.join('')); + } + } + return Top; + } + case RType.BinaryOp: + return callEvalFunction(n.operator, n, [n.lhs, n.rhs], dfg, env) ?? Top; + case RType.UnaryOp: + return callEvalFunction(n.operator, n, [n.operand], dfg, env) ?? Top; + } + return Top; +} diff --git a/src/dataflow/eval/functions/eval-fns.ts b/src/dataflow/eval/functions/eval-fns.ts new file mode 100644 index 00000000000..a8fb16995e7 --- /dev/null +++ b/src/dataflow/eval/functions/eval-fns.ts @@ -0,0 +1,68 @@ +import type { RNode } from '../../../r-bridge/lang-4.x/ast/model/model'; +import type { ParentInformation } from '../../../r-bridge/lang-4.x/ast/model/processing/decorate'; +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 { binaryValues } from '../values/value-binary'; +import { evalRExpression } from '../eval'; +import { unaryValues } from '../values/value-unary'; + +const KnownBinaryFunctions = new Map, args: readonly RNode[], dfg: DataflowGraph, env: REnvironmentInformation) => Value +>(); + +const KnownUnaryFunctions = new Map, args: readonly RNode[], dfg: DataflowGraph, env: REnvironmentInformation) => Value +>(); + +export function callEvalFunction(name: string, node: RNode, args: readonly RNode[], dfg: DataflowGraph, env: REnvironmentInformation): Value { + if(args.length === 1) { + return KnownUnaryFunctions.get(name)?.(node, args, dfg, env) ?? Top; + } else if(args.length === 2) { + return KnownBinaryFunctions.get(name)?.(node, args, dfg, env) ?? Top; + } + return Top; +} + + +function registerBinaryEvalFunction(name: string, fn: (a: Value, b: Value) => Value) { + KnownBinaryFunctions.set(name, (node, args, dfg, env) => { + if(args.length !== 2) { + return Top; + } + return fn(evalRExpression(args[0], dfg, env), evalRExpression(args[1], dfg, env)); + }); +} + +function registerUnaryEvalFunction(name: string, fn: (a: Value) => Value) { + KnownUnaryFunctions.set(name, (node, args, dfg, env) => { + if(args.length !== 1) { + return Top; + } + return fn(evalRExpression(args[0], dfg, env)); + }); +} + +registerBinaryEvalFunction('+', (a, b) => binaryValues(a, 'add', b)); +registerBinaryEvalFunction('-', (a, b) => binaryValues(a, 'sub', b)); +registerBinaryEvalFunction('*', (a, b) => binaryValues(a, 'mul', b)); +registerBinaryEvalFunction('/', (a, b) => binaryValues(a, 'div', b)); +registerBinaryEvalFunction('^', (a, b) => binaryValues(a, 'pow', b)); +registerBinaryEvalFunction('%%', (a, b) => binaryValues(a, 'mod', b)); +registerBinaryEvalFunction('max', (a, b) => binaryValues(a, 'max', b)); +registerBinaryEvalFunction('min', (a, b) => binaryValues(a, 'min', b)); +registerBinaryEvalFunction('==', (a, b) => binaryValues(a, '==', b)); +registerBinaryEvalFunction('!=', (a, b) => binaryValues(a, '!=', b)); +registerBinaryEvalFunction('>', (a, b) => binaryValues(a, '>', b)); +registerBinaryEvalFunction('>=', (a, b) => binaryValues(a, '>=', b)); +registerBinaryEvalFunction('<', (a, b) => binaryValues(a, '<', b)); +registerBinaryEvalFunction('<=', (a, b) => binaryValues(a, '<=', b)); +registerUnaryEvalFunction('-', a => unaryValues(a, 'negate')); +registerUnaryEvalFunction('!', a => unaryValues(a, 'not')); +registerUnaryEvalFunction('sin', a => unaryValues(a, 'sin')); +registerUnaryEvalFunction('cos', a => unaryValues(a, 'cos')); +registerUnaryEvalFunction('tan', a => unaryValues(a, 'tan')); +registerUnaryEvalFunction('asin', a => unaryValues(a, 'asin')); +registerUnaryEvalFunction('sign', a => unaryValues(a, 'sign')); +registerUnaryEvalFunction('abs', a => unaryValues(a, 'abs')); diff --git a/src/dataflow/eval/values/intervals/interval-constants.ts b/src/dataflow/eval/values/intervals/interval-constants.ts index d67803f1170..01f10b44dc3 100644 --- a/src/dataflow/eval/values/intervals/interval-constants.ts +++ b/src/dataflow/eval/values/intervals/interval-constants.ts @@ -54,6 +54,7 @@ export const ValueIntervalZero = intervalFrom(0); export const ValueIntervalOne = intervalFrom(1); export const ValueIntervalNegativeOne = intervalFrom(-1); export const ValueIntervalZeroToOne = intervalFrom(0, 1); +export const ValueIntervalMinusOneToOne = intervalFrom(-1, 1); export const ValueIntervalTop = intervalFromValues(ValueIntegerTop, ValueIntegerTop); export const ValueIntervalBottom = intervalFromValues(ValueIntegerBottom, ValueIntegerBottom); export const ValuePositiveInfinite = intervalFromValues(ValueIntegerZero, ValueIntegerPositiveInfinity, false); \ No newline at end of file diff --git a/src/dataflow/eval/values/intervals/interval-unary.ts b/src/dataflow/eval/values/intervals/interval-unary.ts index 89f52bc14d6..f82f4909d15 100644 --- a/src/dataflow/eval/values/intervals/interval-unary.ts +++ b/src/dataflow/eval/values/intervals/interval-unary.ts @@ -1,12 +1,12 @@ import type { Lift, ValueInterval } from '../r-value'; -import { asValue } from '../r-value'; +import { isTop , asValue } from '../r-value'; import type { ScalarUnaryOperation } from '../scalar/scalar-unary'; import { unaryScalar } from '../scalar/scalar-unary'; import { intervalFromValues, ValueIntervalTop, orderIntervalFrom, - ValueIntervalBottom, ValuePositiveInfinite + ValueIntervalBottom, ValuePositiveInfinite, ValueIntervalMinusOneToOne } from './interval-constants'; import { iteLogical } from '../logical/logical-check'; import { checkScalar } from '../scalar/scalar-check'; @@ -41,8 +41,7 @@ const Operations = { ceil: a => intervalApplyBoth(a, 'ceil', true), floor: a => intervalApplyBoth(a, 'floor', true), round: a => intervalApplyBoth(a, 'round', true), - // TODO: sign does not propagate top but returns [-1, 1]! - sign: a => intervalApplyBoth(a, 'sign', 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 */ @@ -79,6 +78,14 @@ function intervalApplyBoth(a: A, op: ScalarUnaryOperati ); } +function intervalSign(a: A): ValueInterval { + if(isTop(a.start.value) || isTop(a.end.value)) { + return ValueIntervalMinusOneToOne; + } + + return intervalApplyBoth(a, 'sign', true); +} + function intervalNegate(a: A): ValueInterval { return intervalFromValues( unaryScalar(a.end, 'negate'), 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..7677ab2fcfb --- /dev/null +++ b/src/dataflow/eval/values/string/string-constants.ts @@ -0,0 +1,16 @@ +import type { RStringValue } from '../../../../r-bridge/lang-4.x/convert-values'; +import type { ValueString } from '../r-value'; + + +export function stringFrom(str: RStringValue | string): ValueString { + return { + type: 'string', + value: typeof str === 'string' ? { + quotes: '"', + str: str + } : str, + }; +} + + +export const ValueEmptyString = stringFrom(''); \ No newline at end of file diff --git a/src/dataflow/eval/values/value-binary.ts b/src/dataflow/eval/values/value-binary.ts new file mode 100644 index 00000000000..b1ed6078632 --- /dev/null +++ b/src/dataflow/eval/values/value-binary.ts @@ -0,0 +1,57 @@ +import type { Lift, Value, ValueTypes } from './r-value'; +import { Bottom, isBottom, isTop, Top } from './r-value'; +import { ValueLogicalTop } from './logical/logical-constants'; +import { intervalFromValues } from './intervals/interval-constants'; +import { binaryScalar } from './scalar/scalar-binary'; +import { binaryLogical } from './logical/logical-binary'; +import { binaryInterval } from './intervals/interval-binary'; + +const binaryForType = { + 'number': binaryScalar, + 'logical': binaryLogical, + 'interval': binaryInterval, + 'string': binaryScalar, // TODO + 'set': binaryScalar, // TODO + 'vector': binaryScalar // TODO +} as const satisfies Record; + + +export function binaryValues, B extends Lift>( + a: A, + op: string, + b: B +): Value { + if(isBottom(a) || isBottom(b)) { + return Bottom; + } + + if(isTop(a)) { + if(isTop(b)) { + 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); + } + + 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 ValueLogicalTop; +} + +function binaryEnsured( + a: A, op: string, b: B, + type: ValueTypes +): Value { + // TODO: top if not existing + return (binaryForType[type as keyof typeof binaryForType] 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..cdaaeed5ddc --- /dev/null +++ b/src/dataflow/eval/values/value-unary.ts @@ -0,0 +1,36 @@ +import type { Lift, Value, ValueTypes } from './r-value'; +import { Bottom, isBottom, isTop, Top } from './r-value'; +import { binaryScalar } from './scalar/scalar-binary'; +import { unaryScalar } from './scalar/scalar-unary'; +import { unaryLogical } from './logical/logical-unary'; +import { unaryInterval } from './intervals/interval-unary'; + +const unaryForType = { + 'number': unaryScalar, + 'logical': unaryLogical, + 'interval': unaryInterval, + 'string': binaryScalar, // TODO + 'set': binaryScalar, // TODO + 'vector': binaryScalar // TODO +} as const satisfies Record; + + +export function unaryValues>( + a: A, + op: string +): Value { + if(isBottom(a)) { + return Bottom; + } else if(isTop(a)) { + return Top; + } + return unaryEnsured(a, op, a, a.type); +} + +function unaryEnsured( + a: A, op: string, b: B, + type: ValueTypes +): Value { + // TODO: top if not existing + return (unaryForType[type as keyof typeof unaryForType] as (a: Value, op: string) => Value)(a, op); +} \ No newline at end of file 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..f51f58845e5 --- /dev/null +++ b/test/functionality/dataflow/eval/eval-simple.test.ts @@ -0,0 +1,45 @@ +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 { compareValues } from '../../../../src/dataflow/eval/values/value-compare'; +import { + intervalFrom, + intervalFromValues, + ValueIntervalMinusOneToOne +} from '../../../../src/dataflow/eval/values/intervals/interval-constants'; +import { getScalarFromInteger } from '../../../../src/dataflow/eval/values/scalar/scalar-constants'; +import { stringFrom } from '../../../../src/dataflow/eval/values/string/string-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 = compareValues(result, '===', expect); + assert.isTrue(isExpected.type === 'logical' && isExpected.value === true, + `Expected ${stringifyValue(result)} to be ${stringifyValue(expect)}` + ); + }); + } + + 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('"foo"', stringFrom('foo')); + }); + describe('Use variables', () => { + assertEval('u', Top); + assertEval('sign(u)', ValueIntervalMinusOneToOne); + assertEval('abs(sign(u)) + 1', intervalFrom(1, 2, true, true)); + }); +})); \ No newline at end of file From ad7540b31be1fa3333120568d488269539147688 Mon Sep 17 00:00:00 2001 From: Florian Sihler Date: Sat, 15 Mar 2025 22:50:09 +0100 Subject: [PATCH 5/6] feat: well well well --- src/dataflow/eval/eval.ts | 14 +- src/dataflow/eval/functions/eval-fns.ts | 68 ----- .../eval/values/functions/value-function.ts | 33 +++ .../eval/values/functions/value-functions.ts | 165 ++++++++++++ .../eval/values/intervals/interval-binary.ts | 251 +++++++++++++++--- .../eval/values/intervals/interval-check.ts | 38 --- .../eval/values/intervals/interval-compare.ts | 144 ---------- .../values/intervals/interval-constants.ts | 26 +- .../eval/values/intervals/interval-unary.ts | 170 +++++++++--- .../eval/values/logical/logical-binary.ts | 24 +- .../eval/values/logical/logical-check.ts | 39 ++- .../eval/values/logical/logical-compare.ts | 17 -- src/dataflow/eval/values/r-value.ts | 12 +- .../eval/values/scalar/scalar-binary.ts | 95 ++++--- .../eval/values/scalar/scalar-check.ts | 52 ---- .../eval/values/scalar/scalar-compare.ts | 50 ---- .../eval/values/scalar/scalar-unary.ts | 144 +++++++--- .../{string-compare.ts => string-binary.ts} | 33 ++- src/dataflow/eval/values/value-binary.ts | 69 +++-- src/dataflow/eval/values/value-compare.ts | 80 ------ src/dataflow/eval/values/value-unary.ts | 33 ++- .../eval/values/vectors/vector-binary.ts | 86 ++++++ .../eval/values/vectors/vector-constants.ts | 20 ++ .../eval/values/vectors/vector-operations.ts | 8 + .../eval/values/vectors/vector-unary.ts | 40 +++ .../dataflow/eval/eval-simple.test.ts | 9 +- .../interval/eval-interval-simple.test.ts | 14 +- .../eval/logical/eval-logical-simple.test.ts | 2 - .../eval/scalar/eval-scalar-simple.test.ts | 12 +- 29 files changed, 1035 insertions(+), 713 deletions(-) delete mode 100644 src/dataflow/eval/functions/eval-fns.ts create mode 100644 src/dataflow/eval/values/functions/value-function.ts create mode 100644 src/dataflow/eval/values/functions/value-functions.ts delete mode 100644 src/dataflow/eval/values/intervals/interval-check.ts delete mode 100644 src/dataflow/eval/values/intervals/interval-compare.ts delete mode 100644 src/dataflow/eval/values/logical/logical-compare.ts delete mode 100644 src/dataflow/eval/values/scalar/scalar-check.ts delete mode 100644 src/dataflow/eval/values/scalar/scalar-compare.ts rename src/dataflow/eval/values/string/{string-compare.ts => string-binary.ts} (63%) delete mode 100644 src/dataflow/eval/values/value-compare.ts create mode 100644 src/dataflow/eval/values/vectors/vector-binary.ts create mode 100644 src/dataflow/eval/values/vectors/vector-constants.ts create mode 100644 src/dataflow/eval/values/vectors/vector-operations.ts create mode 100644 src/dataflow/eval/values/vectors/vector-unary.ts diff --git a/src/dataflow/eval/eval.ts b/src/dataflow/eval/eval.ts index 5d4ed68bb03..db670098d96 100644 --- a/src/dataflow/eval/eval.ts +++ b/src/dataflow/eval/eval.ts @@ -10,7 +10,7 @@ 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 { callEvalFunction } from './functions/eval-fns'; +import { DefaultValueFunctionEvaluator } from './values/functions/value-functions'; /** * Evaluates the given subtree using its dataflow graph and the current environment. @@ -57,9 +57,17 @@ export function evalRExpression(n: RNode, dfg: DataflowGraph, return Top; } case RType.BinaryOp: - return callEvalFunction(n.operator, n, [n.lhs, n.rhs], dfg, env) ?? Top; + return callFn(n.operator, [n.lhs, n.rhs], dfg, env) ?? Top; case RType.UnaryOp: - return callEvalFunction(n.operator, n, [n.operand], dfg, env) ?? Top; + return callFn(n.operator, [n.operand], dfg, env) ?? Top; } return Top; } + + +function callFn(name: string, args: RNode[], dfg: DataflowGraph, env: REnvironmentInformation): Value | undefined { + return DefaultValueFunctionEvaluator.callFunction(name, args.map(a => + /* TODO: lazy? */ + evalRExpression(a, dfg, env) + )); +} \ No newline at end of file diff --git a/src/dataflow/eval/functions/eval-fns.ts b/src/dataflow/eval/functions/eval-fns.ts deleted file mode 100644 index a8fb16995e7..00000000000 --- a/src/dataflow/eval/functions/eval-fns.ts +++ /dev/null @@ -1,68 +0,0 @@ -import type { RNode } from '../../../r-bridge/lang-4.x/ast/model/model'; -import type { ParentInformation } from '../../../r-bridge/lang-4.x/ast/model/processing/decorate'; -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 { binaryValues } from '../values/value-binary'; -import { evalRExpression } from '../eval'; -import { unaryValues } from '../values/value-unary'; - -const KnownBinaryFunctions = new Map, args: readonly RNode[], dfg: DataflowGraph, env: REnvironmentInformation) => Value ->(); - -const KnownUnaryFunctions = new Map, args: readonly RNode[], dfg: DataflowGraph, env: REnvironmentInformation) => Value ->(); - -export function callEvalFunction(name: string, node: RNode, args: readonly RNode[], dfg: DataflowGraph, env: REnvironmentInformation): Value { - if(args.length === 1) { - return KnownUnaryFunctions.get(name)?.(node, args, dfg, env) ?? Top; - } else if(args.length === 2) { - return KnownBinaryFunctions.get(name)?.(node, args, dfg, env) ?? Top; - } - return Top; -} - - -function registerBinaryEvalFunction(name: string, fn: (a: Value, b: Value) => Value) { - KnownBinaryFunctions.set(name, (node, args, dfg, env) => { - if(args.length !== 2) { - return Top; - } - return fn(evalRExpression(args[0], dfg, env), evalRExpression(args[1], dfg, env)); - }); -} - -function registerUnaryEvalFunction(name: string, fn: (a: Value) => Value) { - KnownUnaryFunctions.set(name, (node, args, dfg, env) => { - if(args.length !== 1) { - return Top; - } - return fn(evalRExpression(args[0], dfg, env)); - }); -} - -registerBinaryEvalFunction('+', (a, b) => binaryValues(a, 'add', b)); -registerBinaryEvalFunction('-', (a, b) => binaryValues(a, 'sub', b)); -registerBinaryEvalFunction('*', (a, b) => binaryValues(a, 'mul', b)); -registerBinaryEvalFunction('/', (a, b) => binaryValues(a, 'div', b)); -registerBinaryEvalFunction('^', (a, b) => binaryValues(a, 'pow', b)); -registerBinaryEvalFunction('%%', (a, b) => binaryValues(a, 'mod', b)); -registerBinaryEvalFunction('max', (a, b) => binaryValues(a, 'max', b)); -registerBinaryEvalFunction('min', (a, b) => binaryValues(a, 'min', b)); -registerBinaryEvalFunction('==', (a, b) => binaryValues(a, '==', b)); -registerBinaryEvalFunction('!=', (a, b) => binaryValues(a, '!=', b)); -registerBinaryEvalFunction('>', (a, b) => binaryValues(a, '>', b)); -registerBinaryEvalFunction('>=', (a, b) => binaryValues(a, '>=', b)); -registerBinaryEvalFunction('<', (a, b) => binaryValues(a, '<', b)); -registerBinaryEvalFunction('<=', (a, b) => binaryValues(a, '<=', b)); -registerUnaryEvalFunction('-', a => unaryValues(a, 'negate')); -registerUnaryEvalFunction('!', a => unaryValues(a, 'not')); -registerUnaryEvalFunction('sin', a => unaryValues(a, 'sin')); -registerUnaryEvalFunction('cos', a => unaryValues(a, 'cos')); -registerUnaryEvalFunction('tan', a => unaryValues(a, 'tan')); -registerUnaryEvalFunction('asin', a => unaryValues(a, 'asin')); -registerUnaryEvalFunction('sign', a => unaryValues(a, 'sign')); -registerUnaryEvalFunction('abs', a => unaryValues(a, 'abs')); 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..a0acf65d5d4 --- /dev/null +++ b/src/dataflow/eval/values/functions/value-function.ts @@ -0,0 +1,33 @@ +import type { Value, ValueTypes } 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 Value[]) => boolean; + /** + * Apply the function to the given arguments. + * You may assume that `canApply` holds. + */ + readonly apply: (args: readonly Value[]) => 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); +} \ No newline at end of file diff --git a/src/dataflow/eval/values/functions/value-functions.ts b/src/dataflow/eval/values/functions/value-functions.ts new file mode 100644 index 00000000000..596dcffe30c --- /dev/null +++ b/src/dataflow/eval/values/functions/value-functions.ts @@ -0,0 +1,165 @@ +import type { ValueFunctionDescription } from './value-function'; +import { isOfEitherType } from './value-function'; +import type { Value, ValueVector } from '../r-value'; +import { Top } from '../r-value'; +import { vectorFrom } from '../vectors/vector-constants'; +import { unionVector } from '../vectors/vector-operations'; +import { unaryValue } from '../value-unary'; +import { binaryValue } from '../value-binary'; +import { ValueIntervalMinusOneToOne } from '../intervals/interval-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 Value[]): ValueVector { + const providers = this.functionProviders.get(name); + if(providers === undefined) { + return vectorFrom(); + } + const activeProviders = providers.filter(p => p.canApply(args)); + + // TODO: allow to deeply union these results + return unionVector(...activeProviders.map(p => vectorFrom(p.apply(args)))); + } +} + +export const DefaultValueFunctionEvaluator = new FunctionEvaluator(); + +const dvfe = DefaultValueFunctionEvaluator; + +dvfe.registerFunctions(['id', 'force'], { + description: 'Simply return a single argument', + canApply: args => args.length === 1, + apply: args => args[0] +}); + +dvfe.registerFunction('-', { + description: '-a', + canApply: args => args.length === 1 && isOfEitherType(args[0], 'number', 'interval', 'vector', Top.type), + apply: ([arg]) => { + return unaryValue(arg, 'negate'); + } +}); + +// maybe use intersect for clamps +dvfe.registerFunction('abs', { + description: 'abs(a)', + canApply: args => args.length === 1 && isOfEitherType(args[0], 'number', 'interval', 'vector', Top.type), + apply: ([arg]) => { + return binaryValue(unaryValue(arg, 'abs'), 'intersect', ValueIntervalMinusOneToOne); + } +}); + +dvfe.registerFunction('ceil', { + description: 'ceil(a)', + canApply: args => args.length === 1 && isOfEitherType(args[0], 'number', 'interval', 'vector', Top.type), + apply: ([arg]) => { + return unaryValue(arg, 'ceil'); + } +}); + +dvfe.registerFunction('floor', { + description: 'floor(a)', + canApply: args => args.length === 1 && isOfEitherType(args[0], 'number', 'interval', 'vector', Top.type), + apply: ([arg]) => { + return unaryValue(arg, 'floor'); + } +}); + +dvfe.registerFunction('round', { + description: 'round(a)', + canApply: args => args.length === 1 && isOfEitherType(args[0], 'number', 'interval', 'vector', Top.type), + apply: ([arg]) => { + return unaryValue(arg, 'round'); + } +}); + +dvfe.registerFunction('sign', { + description: 'sign(a)', + canApply: args => args.length === 1 && isOfEitherType(args[0], 'number', 'interval', 'vector', Top.type), + apply: ([arg]) => { + return unaryValue(arg, 'sign'); + } +}); + +dvfe.registerFunction('add', { + description: 'a + b', + canApply: args => args.length === 2 && isOfEitherType(args[0], 'number', 'interval', 'vector', Top.type) && isOfEitherType(args[1], 'number', 'interval', 'vector', Top.type), + apply: ([a, b]) => { + return binaryValue(a, 'add', b); + } +}); + +dvfe.registerFunction('sub', { + description: 'a - b', + canApply: args => args.length === 2 && isOfEitherType(args[0], 'number', 'interval', 'vector', Top.type) && isOfEitherType(args[1], 'number', 'interval', 'vector', Top.type), + apply: ([a, b]) => { + return binaryValue(a, 'sub', b); + } +}); + +dvfe.registerFunction('mul', { + description: 'a * b', + canApply: args => args.length === 2 && isOfEitherType(args[0], 'number', 'interval', 'vector', Top.type) && isOfEitherType(args[1], 'number', 'interval', 'vector', Top.type), + apply: ([a, b]) => { + return binaryValue(a, 'mul', b); + } +}); + +dvfe.registerFunction('div', { + description: 'a / b', + canApply: args => args.length === 2 && isOfEitherType(args[0], 'number', 'interval', 'vector', Top.type) && isOfEitherType(args[1], 'number', 'interval', 'vector', Top.type), + apply: ([a, b]) => { + return binaryValue(a, 'div', b); + } +}); + +dvfe.registerFunction('pow', { + description: 'a ^ b', + canApply: args => args.length === 2 && isOfEitherType(args[0], 'number', 'interval', 'vector', Top.type) && isOfEitherType(args[1], 'number', 'interval', 'vector', Top.type), + apply: ([a, b]) => { + return binaryValue(a, 'pow', b); + } +}); + +dvfe.registerFunction('mod', { + description: 'a % b', + canApply: args => args.length === 2 && isOfEitherType(args[0], 'number', 'interval', 'vector', Top.type) && isOfEitherType(args[1], 'number', 'interval', 'vector', Top.type), + apply: ([a, b]) => { + return binaryValue(a, 'mod', b); + } +}); + +dvfe.registerFunction('max', { + description: 'max(a, b)', + canApply: args => args.length === 2 && isOfEitherType(args[0], 'number', 'interval', 'vector', Top.type) && isOfEitherType(args[1], 'number', 'interval', 'vector', Top.type), + apply: ([a, b]) => { + return binaryValue(a, 'max', b); + } +}); + +dvfe.registerFunction('min', { + description: 'min(a, b)', + canApply: args => args.length === 2 && isOfEitherType(args[0], 'number', 'interval', 'vector', Top.type) && isOfEitherType(args[1], 'number', 'interval', 'vector', Top.type), + apply: ([a, b]) => { + return binaryValue(a, 'min', b); + } +}); + + + diff --git a/src/dataflow/eval/values/intervals/interval-binary.ts b/src/dataflow/eval/values/intervals/interval-binary.ts index 3b6942f3f43..28d268409bb 100644 --- a/src/dataflow/eval/values/intervals/interval-binary.ts +++ b/src/dataflow/eval/values/intervals/interval-binary.ts @@ -1,25 +1,44 @@ -import type { ValueInterval } from '../r-value'; -import { isBottom } from '../r-value'; +import type { Lift, Value, ValueInterval, ValueNumber } from '../r-value'; +import { Top , isTop , isBottom } from '../r-value'; import { binaryScalar } from '../scalar/scalar-binary'; -import { ValueIntervalBottom, orderIntervalFrom, ValueIntervalZero, ValueIntervalTop } from './interval-constants'; -import { binaryLogical } from '../logical/logical-binary'; -import { compareInterval } from './interval-compare'; +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'; /** * Take two potentially lifted intervals and combine them with the given op. * This propagates `top` and `bottom` values. */ -export function binaryInterval( - a: A, - op: keyof typeof Operations, - b: B -): ValueInterval { - // TODO: improve handling of open intervals - a = unaryInterval(a, 'toClosed') as A; - b = unaryInterval(b, 'toClosed') as B; - return Operations[op](a as ValueInterval, b as ValueInterval); +export function binaryInterval( + a: Lift, + op: string, + b: Lift +): Value { + if(op in Operations) { + return Operations[op as keyof typeof Operations](a, b); + } + return Top; +} + +// 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 = { @@ -29,56 +48,104 @@ const Operations = { div: intervalDiv, intersect: intervalIntersect, union: intervalUnion, - setminus: intervalSetminus -} as const; + 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>; -function intervalAdd(a: A, b: B): ValueInterval { +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), - binaryScalar(a.end, 'add', b.end), + 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: A, b: B): ValueInterval { +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), - binaryScalar(a.end, 'sub', b.start), + 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: A, b: B): ValueInterval { +function intervalIntersect(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.start), - binaryScalar(a.end, 'min', b.end), + binaryScalar(a.start, 'max', b.start) as ValueNumber, + binaryScalar(a.end, 'min', b.end) as ValueNumber, a.startInclusive && b.startInclusive, a.endInclusive && b.endInclusive ); } -function intervalUnion(a: A, b: B): ValueInterval { +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), - binaryScalar(a.end, 'max', b.end), + 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: A, b: B): ValueInterval { +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), - binaryScalar(a.end, 'min', b.start), + 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: A, b: B): ValueInterval { +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; } @@ -89,8 +156,8 @@ function intervalMul(a: A, b: const uu = binaryScalar(a.end, 'mul', b.end); return orderIntervalFrom( - [ll, lu, ul, uu].reduce((acc, val) => binaryScalar(acc, 'min', val)), - [ll, lu, ul, uu].reduce((acc, val) => binaryScalar(acc, 'max', val)), + [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 ); @@ -98,15 +165,20 @@ function intervalMul(a: A, b: // TODO: take support for div sin and other functions that i wrote -function intervalDiv(a: A, b: B): ValueInterval { +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 = binaryLogical( - compareInterval(a, '===', ValueIntervalZero), + const bothAreZero = binaryValue( + binaryInterval(a, '===', ValueIntervalZero), 'and', - compareInterval(b, '===', ValueIntervalZero)); + binaryInterval(b, '===', ValueIntervalZero)); const calcWithPotentialZero = () => - binaryInterval(a, 'mul', unaryInterval(b, 'flip')); + binaryValue(a, 'mul', unaryValue(b, 'flip')); return iteLogical( bothAreZero, @@ -118,5 +190,106 @@ function intervalDiv(a: A, b: 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 === '⊂' ? binaryScalar(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-check.ts b/src/dataflow/eval/values/intervals/interval-check.ts deleted file mode 100644 index 4eff06c8df9..00000000000 --- a/src/dataflow/eval/values/intervals/interval-check.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { Lift, ValueInterval, ValueLogical } from '../r-value'; -import { liftLogical, ValueLogicalFalse } from '../logical/logical-constants'; -import { compareInterval } from './interval-compare'; -import { ValueIntervalZero } from './interval-constants'; -import { compareScalar } from '../scalar/scalar-compare'; -import { binaryLogical } from '../logical/logical-binary'; - -const CheckOperations = { - /** check if the interval contains no values */ - empty: intervalEmpty, - /** check if the interval contains exactly one value */ - scalar: intervalScalar, - hasZero: a => compareInterval(ValueIntervalZero, '⊆', a) -} as const as Record ValueLogical>; - -export function checkInterval>(a: A, op: keyof typeof CheckOperations): ValueLogical { - return CheckOperations[op](a as ValueInterval); -} - -function intervalEmpty(a: A): ValueLogical { - return binaryLogical( - compareScalar(a.start, '>', a.end), - 'or', - binaryLogical( - compareScalar(a.start, '===', a.end), - 'and', - liftLogical(!a.startInclusive || !a.endInclusive) - ) - ); -} - -function intervalScalar(a: A): ValueLogical { - if(!a.startInclusive || !a.endInclusive) { - return ValueLogicalFalse; - } else { - return compareScalar(a.start, '===', a.end); - } -} diff --git a/src/dataflow/eval/values/intervals/interval-compare.ts b/src/dataflow/eval/values/intervals/interval-compare.ts deleted file mode 100644 index bff3508e235..00000000000 --- a/src/dataflow/eval/values/intervals/interval-compare.ts +++ /dev/null @@ -1,144 +0,0 @@ -import type { ValueInterval, ValueLogical } from '../r-value'; -import { isBottom, isTop } from '../r-value'; -import { checkInterval } from './interval-check'; -import { binaryInterval } from './interval-binary'; -import { iteLogical } from '../logical/logical-check'; -import { - liftLogical, - ValueLogicalBot, - ValueLogicalFalse, - ValueLogicalMaybe, - ValueLogicalTrue -} from '../logical/logical-constants'; -import { compareScalar } from '../scalar/scalar-compare'; -import { getIntervalEnd, getIntervalStart } from './interval-constants'; -import { binaryLogical } from '../logical/logical-binary'; -import { unaryLogical } from '../logical/logical-unary'; -import type { ValueCompareOperation } from '../value-compare'; - -const CompareOperations = { - '<': (a, b) => intervalLower(a, b, '<'), - '>': (a, b) => intervalLower(b, a, '<'), - '<=': (a, b) => intervalLower(a, b, '<='), - '>=': (a, b) => intervalLower(b, a, '<='), - '!==': (a, b) => unaryLogical(compareInterval(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) => unaryLogical(compareInterval(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 as Record ValueLogical>; - -export function compareInterval(a: A, op: ValueCompareOperation, b: B): ValueLogical { - return CompareOperations[op](a, b); -} - -function intervalLower(a: A, b: B, op: '<' | '<='): ValueLogical { - // 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( - checkInterval(intersect, 'empty'), - { - onTrue: () => compareScalar(getIntervalEnd(a), op, getIntervalStart(b)), - onFalse: ValueLogicalMaybe, - onMaybe: ValueLogicalMaybe, - onTop: ValueLogicalMaybe, - onBottom: ValueLogicalBot - } - ); -} - -function intervalSubset(a: A, b: B, op: '⊂' | '⊆'): ValueLogical { - // 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( - compareScalar(getIntervalStart(a), '>=', getIntervalStart(b)), - { - onTrue: () => - iteLogical(compareScalar(getIntervalEnd(a), '<=', getIntervalEnd(b)), { - onTrue: op === '⊂' ? compareInterval(a, '!==', b) : ValueLogicalTrue, - onFalse: ValueLogicalFalse, - onMaybe: ValueLogicalMaybe, - onTop: ValueLogicalMaybe, - onBottom: ValueLogicalBot - }), - onFalse: ValueLogicalFalse, - onMaybe: ValueLogicalMaybe, - onTop: ValueLogicalMaybe, - onBottom: ValueLogicalBot - } - ); -} - -function intervalEqual(a: A, b: B): ValueLogical { - // 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 = binaryInterval(a, 'intersect', b); - - const areBothScalar = () => - iteLogical( - binaryLogical(checkInterval(a, 'scalar'), 'and', checkInterval(b, 'scalar')), - { - onTrue: ValueLogicalTrue, // they intersect and they are both scalar - onFalse: ValueLogicalFalse, - onMaybe: ValueLogicalMaybe, - onTop: ValueLogicalMaybe, - onBottom: ValueLogicalBot - } - ); - - return iteLogical( - checkInterval(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: A, b: B): ValueLogical { - // check if start, end, and inclusivity is identical - if( - isTop(a) || isTop(b) || isBottom(a) || isBottom(b) - ) { - return liftLogical( - a.start === b.start && - a.end === b.end && - a.startInclusive === b.startInclusive && - a.endInclusive === b.endInclusive - ); - } 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) ? - binaryLogical( - compareScalar(getIntervalStart(a), '===', getIntervalStart(b)), - 'and', - compareScalar(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 index 01f10b44dc3..ff644186f7e 100644 --- a/src/dataflow/eval/values/intervals/interval-constants.ts +++ b/src/dataflow/eval/values/intervals/interval-constants.ts @@ -1,5 +1,6 @@ import type { RNumberValue } from '../../../../r-bridge/lang-4.x/convert-values'; -import type { ValueInterval, ValueNumber } from '../r-value'; +import type { Lift, ValueInterval, ValueNumber } from '../r-value'; +import { isBottom, isTop } from '../r-value'; import { getScalarFromInteger, liftScalar, @@ -7,7 +8,7 @@ import { ValueIntegerTop, ValueIntegerZero } from '../scalar/scalar-constants'; import { iteLogical } from '../logical/logical-check'; -import { compareScalar } from '../scalar/scalar-compare'; +import { binaryScalar } from '../scalar/scalar-binary'; export function intervalFrom(start: RNumberValue | number, end = start, startInclusive = true, endInclusive = true): ValueInterval { return intervalFromValues( @@ -18,20 +19,28 @@ export function intervalFrom(start: RNumberValue | number, end = start, startInc ); } -export function intervalFromValues(start: ValueNumber, end = start, startInclusive = true, endInclusive = true): ValueInterval { +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, - end, + type: 'interval', + start: shiftNum(start), + end: shiftNum(end), startInclusive, endInclusive, }; } -export function orderIntervalFrom(start: ValueNumber, end = start, startInclusive = true, endInclusive = true): ValueInterval { +export function orderIntervalFrom(start: Lift, end = start, startInclusive = true, endInclusive = true): ValueInterval { const onTrue = () => intervalFromValues(start, end, startInclusive, endInclusive); return iteLogical( - compareScalar(start, '<=', end), + binaryScalar(start, '<=', end), { onTrue, onMaybe: onTrue, @@ -54,6 +63,7 @@ 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 index f82f4909d15..6bc9096c666 100644 --- a/src/dataflow/eval/values/intervals/interval-unary.ts +++ b/src/dataflow/eval/values/intervals/interval-unary.ts @@ -1,15 +1,18 @@ -import type { Lift, ValueInterval } from '../r-value'; -import { isTop , asValue } from '../r-value'; +import type { Lift, Value, ValueInterval, ValueLogical, ValueNumber } from '../r-value'; +import { 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, ValuePositiveInfinite, ValueIntervalMinusOneToOne + ValueIntervalBottom, + ValuePositiveInfinite, + ValueIntervalMinusOneToOne, + ValueIntervalZero, + ValueIntervalZeroToPositiveInfinity } from './interval-constants'; import { iteLogical } from '../logical/logical-check'; -import { checkScalar } from '../scalar/scalar-check'; import { binaryScalar } from '../scalar/scalar-binary'; import { ValueIntegerNegativeInfinity, @@ -17,18 +20,24 @@ import { ValueIntegerPositiveInfinity, ValueIntegerZero, ValueNumberEpsilon } from '../scalar/scalar-constants'; -import { checkInterval } from './interval-check'; - +import { liftLogical, ValueLogicalBot, ValueLogicalFalse, ValueLogicalTop } from '../logical/logical-constants'; +import { binaryInterval } from './interval-binary'; +import { bottomTopGuard } from '../general'; +import { binaryValue } from '../value-binary'; /** * Take two potentially lifted intervals and combine them with the given op. * This propagates `top` and `bottom` values. */ -export function unaryInterval( - a: A, +export function unaryInterval( + a: Lift, op: keyof typeof Operations -): ValueInterval { - return Operations[op](a as ValueInterval); +): Value { + if(op in Operations) { + return Operations[op](a); + } else { + return Top; + } } // TODO: sin, cos, tan, ... @@ -46,74 +55,111 @@ const Operations = { flip: intervalDivByOne, /** returns the structural minimum of the interval, if it is exclusive, we use the closest eta-value */ lowest: a => { - const min = a.startInclusive ? a.start : binaryScalar(a.start, 'add', ValueNumberEpsilon); + 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 max = a.endInclusive ? a.end : binaryScalar(a.end, 'sub', ValueNumberEpsilon); + 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 min = a.startInclusive ? a.start : binaryScalar(a.start, 'add', ValueNumberEpsilon); - const max = a.endInclusive ? a.end : binaryScalar(a.end, 'sub', ValueNumberEpsilon); + 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); }, -} as const satisfies Record Lift>; + /** 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: A, op: ScalarUnaryOperation, toClosed: boolean, startInclusive = a.startInclusive, endInclusive = a.endInclusive): ValueInterval { +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 A; + a = asValue(unaryInterval(a, 'toClosed') as Lift); startInclusive = true; endInclusive = true; } return orderIntervalFrom( - unaryScalar(a.start, op), - unaryScalar(a.end, op), - startInclusive, - endInclusive + unaryScalar(a.start, op) as ValueNumber, + unaryScalar(a.end, op) as ValueNumber, + startInclusive ?? a.startInclusive, + endInclusive ?? a.endInclusive ); } -function intervalSign(a: A): ValueInterval { - if(isTop(a.start.value) || isTop(a.end.value)) { +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: A): ValueInterval { +function intervalNegate(a: Lift): ValueInterval { + const bt = bottomTopGuard(a); + if(bt) { + return bt === Top ? ValueIntervalTop : ValueIntervalBottom; + } + const x = asValue(a); return intervalFromValues( - unaryScalar(a.end, 'negate'), - unaryScalar(a.start, 'negate'), - a.endInclusive, - a.startInclusive + unaryScalar(x.end, 'negate') as ValueNumber, + unaryScalar(x.start, 'negate') as ValueNumber, + x.endInclusive, + x.startInclusive ); } -function intervalAbs(a: A): ValueInterval { +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( - checkScalar(a.start, 'isNegative'), + unaryScalar(a.start, 'isNegative'), { onTrue: () => iteLogical( - checkScalar(a.end, 'isNonNegative'), + unaryScalar(a.end, 'isNonNegative'), { // a <= 0 <= b onTrue: () => { - const startAbs = unaryScalar(a.start, 'abs'); - const endAbs = unaryScalar(a.end, 'abs'); + 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( @@ -125,8 +171,8 @@ function intervalAbs(a: A): ValueInterval { }, // a <= b <= 0 onFalse: () => intervalFromValues( - unaryScalar(a.end, 'abs'), - unaryScalar(a.start, 'abs'), + unaryScalar(a.end, 'abs') as ValueNumber, + unaryScalar(a.start, 'abs') as ValueNumber, a.endInclusive, true // TODO: check ), @@ -143,9 +189,14 @@ function intervalAbs(a: A): ValueInterval { ); } -function intervalDivByOne(a: A): ValueInterval { +function intervalDivByOne(a: Lift): ValueInterval { + const bt = bottomTopGuard(a); + if(bt) { + return bt === Top ? ValueIntervalTop : ValueIntervalBottom; + } + a = asValue(a); const ifStartIsZero = () => - iteLogical(checkScalar(a.end, 'isZero'), + iteLogical(unaryScalar(a.end, 'isZero'), { onTrue: ValueIntervalBottom, onMaybe: ValueIntervalTop, @@ -159,13 +210,13 @@ function intervalDivByOne(a: A): ValueInterval { onBottom: ValueIntervalBottom }); const neitherIsZero = () => iteLogical( - checkInterval(a, 'hasZero'), + unaryInterval(a, 'hasZero'), { onTrue: ValueIntervalTop, onTop: ValueIntervalTop, onFalse: () => orderIntervalFrom( - binaryScalar(ValueIntegerOne, 'div', a.start), - binaryScalar(ValueIntegerOne, 'div', a.end), + binaryScalar(ValueIntegerOne, 'div', a.start) as ValueNumber, + binaryScalar(ValueIntegerOne, 'div', a.end) as ValueNumber, a.startInclusive, // TODO: check a.endInclusive // TODO: check ), @@ -174,12 +225,12 @@ function intervalDivByOne(a: A): ValueInterval { } ); const ifStartIsNotZero = () => iteLogical( - checkScalar(a.end, 'isZero'), + unaryScalar(a.end, 'isZero'), { // start is not zero, but end is zero onTrue: () => intervalFromValues( ValueIntegerNegativeInfinity, - binaryScalar(ValueIntegerOne, 'div', a.end), + binaryScalar(ValueIntegerOne, 'div', a.end) as ValueNumber, false, // TODO: check true // TODO: check ), @@ -191,7 +242,7 @@ function intervalDivByOne(a: A): ValueInterval { ); return iteLogical( - checkScalar(a.start, 'isZero'), + unaryScalar(a.start, 'isZero'), { onTrue: ifStartIsZero, onFalse: ifStartIsNotZero, @@ -201,3 +252,34 @@ function intervalDivByOne(a: A): ValueInterval { } ); } + +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 index 738910fde68..9929d252065 100644 --- a/src/dataflow/eval/values/logical/logical-binary.ts +++ b/src/dataflow/eval/values/logical/logical-binary.ts @@ -1,16 +1,18 @@ -import type { TernaryLogical, ValueLogical } from '../r-value'; +import type { Lift, TernaryLogical, ValueLogical } from '../r-value'; import { bottomTopGuard } from '../general'; +import { guard } from '../../../../util/assert'; /** * Take two potentially lifted logicals and combine them with the given op. * This propagates `top` and `bottom` values. */ -export function binaryLogical( - a: A, - op: keyof typeof Operations, - b: B -): ValueLogical { - return Operations[op](a as ValueLogical, b as ValueLogical); +export function binaryLogical( + a: Lift, + op: string, + b: Lift +): Lift { + guard(op in Operations, `Unknown logical binary operation: ${op}`); + return Operations[op as keyof typeof Operations](a, b); } const Operations = { @@ -55,12 +57,12 @@ const Operations = { return a === b; } }) -} as const satisfies Record ValueLogical>; +} as const satisfies Record, b: Lift) => Lift>; -function logicalHelper(a: A, b: B, op: (a: TernaryLogical, b: TernaryLogical) => TernaryLogical): ValueLogical { - const botTopGuard = bottomTopGuard(a.value, b.value); +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.value as TernaryLogical, b.value as TernaryLogical) + 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 index a46ce25fd00..4d8eafeef64 100644 --- a/src/dataflow/eval/values/logical/logical-check.ts +++ b/src/dataflow/eval/values/logical/logical-check.ts @@ -1,11 +1,34 @@ -import type { Lift, TernaryLogical, ValueLogical } from '../r-value'; -import { Bottom , Top } from '../r-value'; -import { bottomTopGuard } from '../general'; +import type { Value, ValueLogical } from '../r-value'; +import { isBottom, isTop , Bottom , Top } from '../r-value'; import type { CanBeLazy } from '../../../../util/lazy'; import { force } from '../../../../util/lazy'; +import { liftLogical, ValueLogicalBot, ValueLogicalTop } 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'; -export function unpackLogical(a: Lift): Lift { - return bottomTopGuard(a) ?? (a as ValueLogical).value; +// TODO: truthy unary checks +export function isTruthy(a: Value): ValueLogical { + 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 : + isTruthy(a.elements[0]); + } + return ValueLogicalTop; } interface IteCases { @@ -16,11 +39,11 @@ interface IteCases { readonly onBottom: CanBeLazy; } -export function iteLogical, Result>( - cond: A, +export function iteLogical( + cond: Value, { onTrue, onFalse, onMaybe, onTop, onBottom }: IteCases ): Result { - const condVal = unpackLogical(cond); + const condVal = isTruthy(cond).value; if(condVal === Top) { return force(onTop); } else if(condVal === Bottom) { diff --git a/src/dataflow/eval/values/logical/logical-compare.ts b/src/dataflow/eval/values/logical/logical-compare.ts deleted file mode 100644 index b0263a0a7b5..00000000000 --- a/src/dataflow/eval/values/logical/logical-compare.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { ValueLogical } from '../r-value'; -import { logicalToInterval } from './logical-unary'; -import type { ValueCompareOperation } from '../value-compare'; -import { compareInterval } from '../intervals/interval-compare'; - - -export function compareLogical( - a: A, - op: ValueCompareOperation, - b: B -): ValueLogical { - return compareInterval( - logicalToInterval(a), - op, - logicalToInterval(b) - ); -} diff --git a/src/dataflow/eval/values/r-value.ts b/src/dataflow/eval/values/r-value.ts index 599b9fd73c0..a2096ab546d 100644 --- a/src/dataflow/eval/values/r-value.ts +++ b/src/dataflow/eval/values/r-value.ts @@ -15,11 +15,7 @@ export interface ValueInterval { end: Limit endInclusive: boolean } -export interface ValueSet> { - type: 'set' - elements: Elements -} -export interface ValueVector> { +export interface ValueVector = Lift> { type: 'vector' elements: Elements } @@ -31,7 +27,6 @@ export interface ValueString = Lift type: 'string' value: Str } -// TODO: drop maybe and treat it as top export type TernaryLogical = RLogicalValue | 'maybe' export interface ValueLogical { type: 'logical' @@ -40,7 +35,6 @@ export interface ValueLogical { export type Value = Lift< ValueInterval - | ValueSet | ValueVector | ValueNumber | ValueString @@ -114,10 +108,6 @@ export function stringifyValue(value: Lift): string { switch(v.type) { case 'interval': return `${v.startInclusive ? '[' : '('}${stringifyValue(v.start)}, ${stringifyValue(v.end)}${v.endInclusive ? ']' : ')'}`; - case 'set': - return tryStringifyBoTop(v.elements, e => { - return `{${e.map(stringifyValue).join(',')}}`; - }, () => '⊤ (set)', () => '⊥ (set)'); case 'vector': return tryStringifyBoTop(v.elements, e => { return `c(${e.map(stringifyValue).join(',')})`; diff --git a/src/dataflow/eval/values/scalar/scalar-binary.ts b/src/dataflow/eval/values/scalar/scalar-binary.ts index d71acaedec8..e1e1a73dc09 100644 --- a/src/dataflow/eval/values/scalar/scalar-binary.ts +++ b/src/dataflow/eval/values/scalar/scalar-binary.ts @@ -1,46 +1,81 @@ -import type { ValueNumber } from '../r-value'; +import type { Lift, Value, ValueLogical, ValueNumber } from '../r-value'; import { Bottom } from '../r-value'; import { bottomTopGuard } from '../general'; import type { RNumberValue } from '../../../../r-bridge/lang-4.x/convert-values'; -import { liftScalar, ValueIntegerBottom, ValueIntegerTop } from './scalar-constants'; +import { liftScalar, ValueIntegerBottom, ValueIntegerTop, ValueNumberEpsilon } from './scalar-constants'; +import { guard } from '../../../../util/assert'; +import { liftLogical, ValueLogicalBot, ValueLogicalTop } from '../logical/logical-constants'; /** * Take two potentially lifted intervals and combine them with the given op. * This propagates `top` and `bottom` values. */ -export function binaryScalar( - a: A, - op: keyof typeof ScalarBinaryOperations, - b: B -): ValueNumber { - return ScalarBinaryOperations[op](a as ValueNumber, b as ValueNumber); +export function binaryScalar( + a: Lift, + op: string, + b: Lift +): Value { + guard(op in ScalarBinaryOperations, `Unknown scalar binary operation: ${op}`); + return ScalarBinaryOperations[op as keyof typeof ScalarBinaryOperations](a, b); } 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'), -} as const satisfies Record ValueNumber>; - -export type ScalarBinaryOperation = keyof typeof ScalarBinaryOperations; - -function scalarHelper( - a: A, - b: B, + 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>; + + +function identicalNumbersThreshold(a: number, b: number): boolean { + return 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.value, b.value); + const val = bottomTopGuard(a, b, (a as ValueNumber).value, (b as ValueNumber).value); if(val) { return val === Bottom ? ValueIntegerBottom : ValueIntegerTop; } - const aval = a.value as RNumberValue; - const bval = b.value as RNumberValue; + 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({ @@ -51,13 +86,13 @@ function scalarHelper( } // max and min do not have to create knew objects -function scalarMaxMin(a: A, b: B, c: 'max' | 'min'): ValueNumber { - const bt = bottomTopGuard(a.value, b.value); +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.value as RNumberValue; - const bval = b.value as RNumberValue; + 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-check.ts b/src/dataflow/eval/values/scalar/scalar-check.ts deleted file mode 100644 index 0a120762671..00000000000 --- a/src/dataflow/eval/values/scalar/scalar-check.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { Lift, ValueLogical, ValueNumber } from '../r-value'; -import { isBottom, isTop } from '../r-value'; -import { liftLogical, ValueLogicalBot, ValueLogicalTop } from '../logical/logical-constants'; -import { bottomTopGuard } from '../general'; - -const ScalarCheckOperations = { - /** `=== 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 Lift>; - -export function checkScalar>(a: A, op: keyof typeof ScalarCheckOperations): Lift { - return bottomTopGuard(a) ?? ScalarCheckOperations[op](a as ValueNumber); -} - -function scalarCheck(a: A, c: (n: number) => boolean): ValueLogical { - if(isTop(a.value)) { - return ValueLogicalTop; - } else if(isBottom(a.value)) { - return ValueLogicalBot; - } else { - return liftLogical(c(a.value.num)); - } -} - -function scalarMarkedAsInt(a: A): ValueLogical { - if(isTop(a.value)) { - return ValueLogicalTop; - } else if(isBottom(a.value)) { - return ValueLogicalBot; - } else { - return liftLogical(a.value.markedAsInt); - } -} - -function scalarMarkedAsComplex(a: A): ValueLogical { - 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/scalar/scalar-compare.ts b/src/dataflow/eval/values/scalar/scalar-compare.ts deleted file mode 100644 index aaa84364b7f..00000000000 --- a/src/dataflow/eval/values/scalar/scalar-compare.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { ValueLogical, ValueNumber } from '../r-value'; -import { bottomTopGuard } from '../general'; -import type { RNumberValue } from '../../../../r-bridge/lang-4.x/convert-values'; -import type { ValueCompareOperation } from '../value-compare'; -import { ValueNumberEpsilon } from './scalar-constants'; -import { liftLogical } from '../logical/logical-constants'; - -/** - * Take two potentially lifted intervals and compare them with the given op. - * This propagates `top` and `bottom` values. - */ -export function compareScalar( - a: A, - op: keyof typeof Operations, - b: B -): ValueLogical { - return Operations[op](a as ValueNumber, b as ValueNumber); -} - -function identicalNumbersThreshold(a: number, b: number): boolean { - return Math.abs(a - b) < 2 * ValueNumberEpsilon.value.num; -} - -const Operations = { - '<=': (a, b) => scalarHelper(a, b, (a, b) => a <= b), - '<': (a, b) => scalarHelper(a, b, (a, b) => a < b), - '>=': (a, b) => scalarHelper(a, b, (a, b) => a >= b), - '>': (a, b) => scalarHelper(a, b, (a, b) => a > b), - '==': (a, b) => scalarHelper(a, b, (a, b) => identicalNumbersThreshold(a, b)), - '!=': (a, b) => scalarHelper(a, b, (a, b) => !identicalNumbersThreshold(a, b)), - '===': (a, b) => scalarHelper(a, b, (a, b) => identicalNumbersThreshold(a, b)), - '!==': (a, b) => scalarHelper(a, b, (a, b) => !identicalNumbersThreshold(a, b)), - /** subseteq is only fulfilled if they are the same */ - '⊆': (a, b) => scalarHelper(a, b, (a, b) => identicalNumbersThreshold(a, b)), - /** subset is never fulfilled */ - '⊂': (a, b) => scalarHelper(a, b, (_a, _b) => false), - '⊇': (a, b) => scalarHelper(a, b, (a, b) => identicalNumbersThreshold(b, a)), - '⊃': (a, b) => scalarHelper(a, b, (_a, _b) => false) -} as const satisfies Record ValueLogical>; - -function scalarHelper( - a: A, - b: B, - c: (a: number, b: number) => boolean -): ValueLogical { - const val = bottomTopGuard(a.value, b.value); - const aval = a.value as RNumberValue; - const bval = b.value as RNumberValue; - return liftLogical(val ?? c(aval.num, bval.num)); -} \ 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 index 2b918ef3807..bb29898ecf8 100644 --- a/src/dataflow/eval/values/scalar/scalar-unary.ts +++ b/src/dataflow/eval/values/scalar/scalar-unary.ts @@ -1,54 +1,120 @@ -import type { ValueNumber } from '../r-value'; +import type { Lift, Value, ValueLogical, ValueNumber } from '../r-value'; +import { 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'; /** * Take a potentially lifted interval and apply the given op. * This propagates `top` and `bottom` values. */ -export function unaryScalar( - a: A, - op: keyof typeof ScalarUnaryOperations -): ValueNumber { - return ScalarUnaryOperations[op](a as ValueNumber); +export function unaryScalar( + a: Lift, + op: string +): Value { + guard(op in ScalarUnaryOperations, `Unknown scalar unary operation: ${op}`); + return ScalarUnaryOperations[op as keyof typeof ScalarUnaryOperations](a); } const ScalarUnaryOperations = { - id: a => a, - negate: a => scalarHelper(a, (a) => -a), - abs: a => scalarHelper(a, Math.abs), - 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), - cos: a => scalarHelper(a, Math.cos), - tan: a => scalarHelper(a, Math.tan), - 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) -} as const satisfies Record ValueNumber>; + 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; -// TODO: support sin clamp to [-1, 1] etc. -function scalarHelper(a: A, op: (a: number) => number): ValueNumber { +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 { - type: 'number', - value: val ?? { - markedAsInt: aval.markedAsInt, - complexNumber: aval.complexNumber, - num: op(aval.num) - } - }; -} \ No newline at end of file + 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/string/string-compare.ts b/src/dataflow/eval/values/string/string-binary.ts similarity index 63% rename from src/dataflow/eval/values/string/string-compare.ts rename to src/dataflow/eval/values/string/string-binary.ts index d1810db904e..d085b8ed45e 100644 --- a/src/dataflow/eval/values/string/string-compare.ts +++ b/src/dataflow/eval/values/string/string-binary.ts @@ -1,19 +1,21 @@ -import type { ValueLogical, ValueString } from '../r-value'; +import type { Lift, Value, ValueLogical, ValueString } from '../r-value'; +import { isBottom, isTop } from '../r-value'; import { bottomTopGuard } from '../general'; import type { RStringValue } from '../../../../r-bridge/lang-4.x/convert-values'; -import type { ValueCompareOperation } from '../value-compare'; -import { liftLogical } from '../logical/logical-constants'; +import { liftLogical, ValueLogicalBot, ValueLogicalTop } from '../logical/logical-constants'; +import { guard } from '../../../../util/assert'; /** * Take two potentially lifted intervals and compare them with the given op. * This propagates `top` and `bottom` values. */ -export function compareString( - a: A, - op: keyof typeof Operations, - b: B -): ValueLogical { - return Operations[op](a as ValueString, b as ValueString); +export function binaryString( + a: Lift, + op: string, + b: Lift +): Value { + guard(op in Operations, `Unknown string binary operation: ${op}`); + return Operations[op as keyof typeof Operations](a, b); } const Operations = { @@ -30,13 +32,18 @@ const Operations = { '⊂': (a, b) => stringHelper(a, b, (a, b) => b.includes(a) && a !== b), '⊇': (a, b) => stringHelper(a, b, (a, b) => a.includes(b)), '⊃': (a, b) => stringHelper(a, b, (a, b) => a.includes(b) && a !== b) -} as const satisfies Record ValueLogical>; +} as const satisfies Record, b: Lift) => Value>; -function stringHelper( - a: A, - b: B, +function stringHelper( + 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; diff --git a/src/dataflow/eval/values/value-binary.ts b/src/dataflow/eval/values/value-binary.ts index b1ed6078632..03785678c12 100644 --- a/src/dataflow/eval/values/value-binary.ts +++ b/src/dataflow/eval/values/value-binary.ts @@ -1,33 +1,49 @@ -import type { Lift, Value, ValueTypes } from './r-value'; -import { Bottom, isBottom, isTop, Top } from './r-value'; -import { ValueLogicalTop } from './logical/logical-constants'; +import type { Lift, Value } from './r-value'; +import { 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'; -const binaryForType = { - 'number': binaryScalar, - 'logical': binaryLogical, - 'interval': binaryInterval, - 'string': binaryScalar, // TODO - 'set': binaryScalar, // TODO - 'vector': binaryScalar // TODO -} as const satisfies Record; +let binaryForType: Record Value> = undefined as unknown as Record Value>; +function initialize() { + binaryForType ??= { + 'number': binaryScalar, + 'logical': binaryLogical, + 'interval': binaryInterval, + 'string': binaryString, + 'vector': binaryVector + } as Record Value>; +} -export function binaryValues, B extends Lift>( - a: A, +export function binaryValue( + a: Lift, op: string, - b: B + b: Lift ): Value { if(isBottom(a) || isBottom(b)) { - return Bottom; - } - - if(isTop(a)) { + if(op === '===') { + return a === b ? ValueLogicalTrue : ValueLogicalFalse; + } else if(op === '!==') { + return a !== b ? ValueLogicalTrue : ValueLogicalFalse; + } else { + return Bottom; + } + } else if(isTop(a)) { if(isTop(b)) { - return Top; + if(op === '===') { + return ValueLogicalTrue; + } else if(op === '!==') { + return ValueLogicalFalse; + } else { + return Top; + } } else { return binaryEnsured(a, op, b, b.type); } @@ -37,21 +53,24 @@ export function binaryValues, B extends Lift>( if(a.type === b.type) { return binaryEnsured(a, op, b, a.type); + } else if(a.type === 'vector') { + return binaryValue(a, op, vectorFrom(b)); + } else if(b.type === 'vector') { + return binaryValue(vectorFrom(a), op, b); } - 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 ValueLogicalTop; + return Top; } function binaryEnsured( a: A, op: string, b: B, - type: ValueTypes + type: string ): Value { - // TODO: top if not existing - return (binaryForType[type as keyof typeof binaryForType] as (a: Value, op: string, b: Value) => Value)(a, op, b); + 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-compare.ts b/src/dataflow/eval/values/value-compare.ts deleted file mode 100644 index 4cca6b4130e..00000000000 --- a/src/dataflow/eval/values/value-compare.ts +++ /dev/null @@ -1,80 +0,0 @@ -import type { Lift, Value, ValueLogical, ValueTypes } from './r-value'; -import { isBottom, isTop } from './r-value'; -import { liftLogical, ValueLogicalBot, ValueLogicalTop } from './logical/logical-constants'; -import { compareScalar } from './scalar/scalar-compare'; -import { compareLogical } from './logical/logical-compare'; -import { compareInterval } from './intervals/interval-compare'; -import { guard } from '../../../util/assert'; -import { intervalFromValues } from './intervals/interval-constants'; -import { compareString } from './string/string-compare'; - -export type ValueCompareOperation = '<' | '>' | '<=' | '>=' | '===' | '!==' - | '==' | '!=' | '⊆' | '⊂' | '⊃' | '⊇'; - -// besides identical and top/bot -const comparableTypes = new Set([ - ['number', 'interval'] -].flatMap(([a, b]) => [`${a}<>${b}`, `${b}<>${a}`])); - - -export const GeneralCompareOperations = { - 'meta:identical-objects': (a, b) => liftLogical(a === b), - 'meta:comparable': (a, b) => liftLogical( - isTop(a) || isTop(b) || isBottom(a) || isBottom(b) || a.type === b.type || comparableTypes.has(`${a.type}<>${b.type}`) - ) -} as const satisfies Record ValueLogical>; - - -const compareForType = { - 'number': compareScalar, - 'logical': compareLogical, - 'interval': compareInterval, - 'string': compareString, - 'set': compareScalar, // TODO - 'vector': compareScalar // TODO -} as const satisfies Record; - -export function compareValues, B extends Lift>( - a: A, - op: ValueCompareOperation | keyof typeof GeneralCompareOperations, - b: B -): Lift { - const general: undefined | ((a: Value, b: Value) => ValueLogical) = GeneralCompareOperations[op as keyof typeof GeneralCompareOperations]; - if(general !== undefined) { - return general(a, b); - } - if(isBottom(a) || isBottom(b)) { - return ValueLogicalBot; - } - - if(isTop(a)) { - if(isTop(b)) { - return ValueLogicalTop; - } else { - return compareEnsured(a, op, b, b.type); - } - } else if(isTop(b)) { - return compareEnsured(a, op, b, a.type); - } - - if(a.type === b.type) { - return compareEnsured(a, op, b, a.type); - } - - guard(comparableTypes.has(`${a.type}<>${b.type}`), `Cannot compare ${a.type} with ${b.type}`); - - if(a.type === 'interval' && b.type === 'number') { - return compareEnsured(a, op, intervalFromValues(b, b), a.type); - } else if(a.type === 'number' && b.type === 'interval') { - return compareEnsured(intervalFromValues(a, a), op, b, b.type); - } - - return ValueLogicalTop; -} - -function compareEnsured, B extends Lift>( - a: A, op: string, b: B, - type: ValueTypes -): Lift { - return (compareForType[type as keyof typeof compareForType] as (a: Value, op: string, b: Value) => Lift)(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 index cdaaeed5ddc..85adbd3046f 100644 --- a/src/dataflow/eval/values/value-unary.ts +++ b/src/dataflow/eval/values/value-unary.ts @@ -1,21 +1,25 @@ -import type { Lift, Value, ValueTypes } from './r-value'; +import type { Lift, Value } from './r-value'; import { Bottom, isBottom, isTop, Top } from './r-value'; -import { binaryScalar } from './scalar/scalar-binary'; 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'; -const unaryForType = { - 'number': unaryScalar, - 'logical': unaryLogical, - 'interval': unaryInterval, - 'string': binaryScalar, // TODO - 'set': binaryScalar, // TODO - 'vector': binaryScalar // TODO -} as const satisfies Record; +let unaryForType: Record Value> = undefined as unknown as Record Value>; + +function initialize() { + unaryForType ??= { + 'number': unaryScalar, + 'logical': unaryLogical, + 'interval': unaryInterval, + 'string': unaryScalar, // TODO + 'vector': unaryVector + } as Record Value>; +} -export function unaryValues>( +export function unaryValue>( a: A, op: string ): Value { @@ -29,8 +33,9 @@ export function unaryValues>( function unaryEnsured( a: A, op: string, b: B, - type: ValueTypes + type: string ): Value { - // TODO: top if not existing - return (unaryForType[type as keyof typeof unaryForType] as (a: Value, op: string) => Value)(a, op); + 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..1622ad48641 --- /dev/null +++ b/src/dataflow/eval/values/vectors/vector-binary.ts @@ -0,0 +1,86 @@ +import type { Value, ValueVector } from '../r-value'; +import { isBottom, isTop } from '../r-value'; +import { vectorFrom } from './vector-constants'; +import { bottomTopGuard } from '../general'; +import { binaryValue } from '../value-binary'; +import { ValueLogicalBot, ValueLogicalTop, ValueLogicalTrue } from '../logical/logical-constants'; + +/** + * Take two potentially lifted vectors and apply the given op. + * This propagates `top` and `bottom` values. + */ +export function binaryVector( + a: A, + op: string, + b: B +): ValueVector { + if(op in VectorBinaryOperations) { + return VectorBinaryOperations[op](a as ValueVector, b as ValueVector); + } else { + return applyPairWiseRecycle(a, op, b); + } +} + +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(...bElements)]; +} + +function applyPairWiseRecycle( + a: ValueVector, + op: string, + b: ValueVector +): ValueVector { + const bt = bottomTopGuard(a.elements, b.elements); + if(bt) { + return vectorFrom(bt); + } else if((a as ValueVector).elements.length === 0 || (b as ValueVector).elements.length === 0) { + return vectorFrom(); + } + const [aV, bV] = recycleUntilEqualLength(a as ValueVector, b as ValueVector); + console.log(aV, bV); + return vectorFrom( + ...aV.elements + .map((a, i) => binaryValue(a, op, bV.elements[i])) + ); +} + +const VectorBinaryOperations = { + '===': (a, b) => { + const res = applyPairWiseRecycle(a, '===', b); + if(isTop(res.elements)) { + return ValueLogicalTop; + } 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..9fb1f6852b3 --- /dev/null +++ b/src/dataflow/eval/values/vectors/vector-constants.ts @@ -0,0 +1,20 @@ +import type { Value, ValueVector } from '../r-value'; +import { Bottom , Top } from '../r-value'; + + +export function vectorFrom(...elements: V): ValueVector { + return { + type: 'vector', + elements + }; +} + +export const ValueEmptyVector = vectorFrom(); +export const ValueVectorTop: ValueVector = { + type: 'vector', + elements: Top +}; +export const ValueVectorBottom: ValueVector = { + type: 'vector', + elements: Bottom +}; \ No newline at end of file diff --git a/src/dataflow/eval/values/vectors/vector-operations.ts b/src/dataflow/eval/values/vectors/vector-operations.ts new file mode 100644 index 00000000000..881e5182174 --- /dev/null +++ b/src/dataflow/eval/values/vectors/vector-operations.ts @@ -0,0 +1,8 @@ +import type { Value, ValueVector } from '../r-value'; + +export function unionVector(...vs: ValueVector[]): ValueVector { + return { + type: 'vector', + elements: vs.flatMap(s => s.elements) as V + }; +} \ No newline at end of file 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..ec174bbab09 --- /dev/null +++ b/src/dataflow/eval/values/vectors/vector-unary.ts @@ -0,0 +1,40 @@ +import type { Value, ValueVector } from '../r-value'; +import { vectorFrom } from './vector-constants'; +import { bottomTopGuard } from '../general'; +import { unaryValue } from '../value-unary'; + +/** + * Take a potentially lifted vector and apply the given op. + * This propagates `top` and `bottom` values. + */ +export function unaryVector( + a: A, + op: string +): ValueVector { + if(op in VectorUnaryOperations) { + return VectorUnaryOperations[op](a as ValueVector); + } else { + return applyComponentWise(a, op); + } +} + +function applyComponentWise( + a: ValueVector, + op: string +): ValueVector { + const bt = bottomTopGuard(a.elements); + return bt ? vectorFrom(bt) : vectorFrom( + ...(a as ValueVector).elements + .map((a) => unaryValue(a, op)) + ); +} + +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/test/functionality/dataflow/eval/eval-simple.test.ts b/test/functionality/dataflow/eval/eval-simple.test.ts index f51f58845e5..3ae219aa95c 100644 --- a/test/functionality/dataflow/eval/eval-simple.test.ts +++ b/test/functionality/dataflow/eval/eval-simple.test.ts @@ -6,7 +6,6 @@ import { createDataflowPipeline } from '../../../../src/core/steps/pipeline/defa import { requestFromInput } from '../../../../src/r-bridge/retriever'; import { evalRExpression } from '../../../../src/dataflow/eval/eval'; import { initializeCleanEnvironments } from '../../../../src/dataflow/environments/environment'; -import { compareValues } from '../../../../src/dataflow/eval/values/value-compare'; import { intervalFrom, intervalFromValues, @@ -14,6 +13,8 @@ import { } from '../../../../src/dataflow/eval/values/intervals/interval-constants'; import { getScalarFromInteger } 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 { isTruthy } from '../../../../src/dataflow/eval/values/logical/logical-check'; describe.sequential('eval', withShell(shell => { function assertEval(code: string, expect: Value) { @@ -22,9 +23,9 @@ describe.sequential('eval', withShell(shell => { request: requestFromInput(code) }).allRemainingSteps(); const result = evalRExpression(results.normalize.ast, results.dataflow.graph, initializeCleanEnvironments()); - const isExpected = compareValues(result, '===', expect); - assert.isTrue(isExpected.type === 'logical' && isExpected.value === true, - `Expected ${stringifyValue(result)} to be ${stringifyValue(expect)}` + const isExpected = binaryValue(result, '===', expect); + assert.isTrue(isTruthy(isExpected).value === true, + `Expected ${stringifyValue(result)} to be ${stringifyValue(expect)} (${stringifyValue(isExpected)})` ); }); } diff --git a/test/functionality/dataflow/eval/interval/eval-interval-simple.test.ts b/test/functionality/dataflow/eval/interval/eval-interval-simple.test.ts index d4126d345a3..31d9c6f6e45 100644 --- a/test/functionality/dataflow/eval/interval/eval-interval-simple.test.ts +++ b/test/functionality/dataflow/eval/interval/eval-interval-simple.test.ts @@ -1,5 +1,5 @@ import { assert, describe, test } from 'vitest'; -import type { Lift, ValueInterval } from '../../../../../src/dataflow/eval/values/r-value'; +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 { @@ -11,17 +11,19 @@ import { ValueNumberEpsilon } from '../../../../../src/dataflow/eval/values/scalar/scalar-constants'; import { unaryInterval } from '../../../../../src/dataflow/eval/values/intervals/interval-unary'; -import { compareValues } from '../../../../../src/dataflow/eval/values/value-compare'; +import { binaryValue } from '../../../../../src/dataflow/eval/values/value-binary'; +import { isTruthy } 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: Lift, expect: Lift) { - const res = compareValues(val, '===', expect); + function shouldBeInterval(val: Value, expect: Value) { + const res = binaryValue(val, '===', expect); assert.isTrue( - res.type === 'logical' && res.value === true, + isTruthy(res), `Expected ${stringifyValue(val)} to be ${stringifyValue(expect)}` ); } @@ -135,7 +137,7 @@ describe('interval', () => { 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(unaryInterval(binaryInterval(l, t.label, r), 'toClosed'), unaryInterval(t.expect, 'toClosed')); + shouldBeInterval(unaryValue(binaryInterval(l, t.label, r), 'toClosed'), unaryValue(t.expect, 'toClosed')); }); } f(t.left, t.right); diff --git a/test/functionality/dataflow/eval/logical/eval-logical-simple.test.ts b/test/functionality/dataflow/eval/logical/eval-logical-simple.test.ts index 8f04df4cbd6..27013058baa 100644 --- a/test/functionality/dataflow/eval/logical/eval-logical-simple.test.ts +++ b/test/functionality/dataflow/eval/logical/eval-logical-simple.test.ts @@ -1,6 +1,4 @@ import { assert, describe, test } from 'vitest'; - - import { guard } from '../../../../../src/util/assert'; import type { Lift, ValueLogical } from '../../../../../src/dataflow/eval/values/r-value'; import { isBottom, isTop } from '../../../../../src/dataflow/eval/values/r-value'; diff --git a/test/functionality/dataflow/eval/scalar/eval-scalar-simple.test.ts b/test/functionality/dataflow/eval/scalar/eval-scalar-simple.test.ts index 67483310d6e..418d3bec30c 100644 --- a/test/functionality/dataflow/eval/scalar/eval-scalar-simple.test.ts +++ b/test/functionality/dataflow/eval/scalar/eval-scalar-simple.test.ts @@ -6,17 +6,15 @@ import { } 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 { Lift, ValueNumber } from '../../../../../src/dataflow/eval/values/r-value'; +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: Lift, expect: number, shouldBeInt = false, shouldBeFloat = false) { - if(typeof expect === 'number') { - guard(!isTop(value) && !isBottom(value)); - guard('num' in value.value); - assert.equal(value.value.num, expect); - } + 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([ From 76880144e642313621c6e9a05bf268f6f5403d60 Mon Sep 17 00:00:00 2001 From: Florian Sihler Date: Sun, 16 Mar 2025 13:26:33 +0100 Subject: [PATCH 6/6] feat: up to strings, sets and intervals --- src/dataflow/eval/eval.ts | 65 +++-- .../eval/values/functions/functions-value.ts | 270 ++++++++++++++++++ .../eval/values/functions/value-function.ts | 30 +- .../eval/values/functions/value-functions.ts | 165 ----------- src/dataflow/eval/values/general.ts | 31 +- .../eval/values/intervals/interval-binary.ts | 32 ++- .../values/intervals/interval-constants.ts | 10 +- .../eval/values/intervals/interval-unary.ts | 18 +- .../eval/values/logical/logical-binary.ts | 20 +- .../eval/values/logical/logical-check.ts | 21 +- .../eval/values/logical/logical-unary.ts | 23 +- .../eval/values/missing/missing-constants.ts | 5 + src/dataflow/eval/values/r-value.ts | 42 ++- .../eval/values/scalar/scalar-binary.ts | 25 +- .../eval/values/scalar/scalar-constants.ts | 1 + .../eval/values/scalar/scalar-unary.ts | 11 +- src/dataflow/eval/values/sets/set-binary.ts | 72 +++++ .../eval/values/sets/set-constants.ts | 30 ++ src/dataflow/eval/values/sets/set-unary.ts | 46 +++ .../eval/values/string/string-binary.ts | 56 +++- .../eval/values/string/string-constants.ts | 14 +- .../eval/values/string/string-unary.ts | 68 +++++ src/dataflow/eval/values/value-binary.ts | 31 +- src/dataflow/eval/values/value-unary.ts | 21 +- .../eval/values/vectors/vector-binary.ts | 65 +++-- .../eval/values/vectors/vector-constants.ts | 60 +++- .../eval/values/vectors/vector-operations.ts | 8 - .../eval/values/vectors/vector-unary.ts | 22 +- .../dataflow/eval/eval-simple.test.ts | 31 +- .../interval/eval-interval-simple.test.ts | 9 +- .../eval/logical/eval-logical-simple.test.ts | 15 +- 31 files changed, 964 insertions(+), 353 deletions(-) create mode 100644 src/dataflow/eval/values/functions/functions-value.ts delete mode 100644 src/dataflow/eval/values/functions/value-functions.ts create mode 100644 src/dataflow/eval/values/missing/missing-constants.ts create mode 100644 src/dataflow/eval/values/sets/set-binary.ts create mode 100644 src/dataflow/eval/values/sets/set-constants.ts create mode 100644 src/dataflow/eval/values/sets/set-unary.ts create mode 100644 src/dataflow/eval/values/string/string-unary.ts delete mode 100644 src/dataflow/eval/values/vectors/vector-operations.ts diff --git a/src/dataflow/eval/eval.ts b/src/dataflow/eval/eval.ts index db670098d96..f4511ecfaf0 100644 --- a/src/dataflow/eval/eval.ts +++ b/src/dataflow/eval/eval.ts @@ -10,25 +10,36 @@ 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/value-functions'; +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, dfg: DataflowGraph, env: REnvironmentInformation): Value { +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: { - // TODO: handle break return etc. - let result: Value = Top; - // TODO: '{' for grouping, side-effecs - for(const child of n.children) { - result = evalRExpression(child, dfg, env); + 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; } - return result; - } case RType.Number: return intervalFrom(n.content, n.content); case RType.String: @@ -38,36 +49,34 @@ export function evalRExpression(n: RNode, dfg: DataflowGraph, case RType.Symbol: { const t = dfg.getVertex(n.info.id); if(t?.tag === VertexType.Use) { - const values = resolveValueOfVariable(n.content, env, dfg.idMap); - if(values === undefined || values.length === 0) { - return Top; - } - // TODO: map this to r value - const allNumber = values.every(v => typeof v === 'number'); - // TODO: sets - if(allNumber) { - return intervalFrom(Math.min(...values), Math.max(...values)); - } - const allString = values.every(v => typeof v === 'string'); - if(allString) { - // TODO: this handling is not correct - return stringFrom(values.join('')); - } + return valuesFromTsValuesAsSet(resolveValueOfVariable(n.content, env, dfg.idMap)); } - return Top; + 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: RNode[], dfg: DataflowGraph, env: REnvironmentInformation): Value | undefined { +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? */ - evalRExpression(a, dfg, env) + [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 index a0acf65d5d4..126752db94f 100644 --- a/src/dataflow/eval/values/functions/value-function.ts +++ b/src/dataflow/eval/values/functions/value-function.ts @@ -1,4 +1,6 @@ import type { Value, ValueTypes } from '../r-value'; +import { Top , stringifyValue } from '../r-value'; + export interface ValueFunctionDescription { /** @@ -11,12 +13,12 @@ export interface ValueFunctionDescription { * * @see requiresSignature - a helper function to check the types of the arguments */ - readonly canApply: (args: readonly Value[]) => boolean; + 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 Value[]) => Value; + readonly apply: (args: readonly ValueArgument[], fname: string) => Value; } export function requiresSignature( @@ -25,9 +27,31 @@ export function requiresSignature( return args => args.length === types.length && args.every((a, i) => a.type === types[i]); } -export function isOfEitherType( +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/functions/value-functions.ts b/src/dataflow/eval/values/functions/value-functions.ts deleted file mode 100644 index 596dcffe30c..00000000000 --- a/src/dataflow/eval/values/functions/value-functions.ts +++ /dev/null @@ -1,165 +0,0 @@ -import type { ValueFunctionDescription } from './value-function'; -import { isOfEitherType } from './value-function'; -import type { Value, ValueVector } from '../r-value'; -import { Top } from '../r-value'; -import { vectorFrom } from '../vectors/vector-constants'; -import { unionVector } from '../vectors/vector-operations'; -import { unaryValue } from '../value-unary'; -import { binaryValue } from '../value-binary'; -import { ValueIntervalMinusOneToOne } from '../intervals/interval-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 Value[]): ValueVector { - const providers = this.functionProviders.get(name); - if(providers === undefined) { - return vectorFrom(); - } - const activeProviders = providers.filter(p => p.canApply(args)); - - // TODO: allow to deeply union these results - return unionVector(...activeProviders.map(p => vectorFrom(p.apply(args)))); - } -} - -export const DefaultValueFunctionEvaluator = new FunctionEvaluator(); - -const dvfe = DefaultValueFunctionEvaluator; - -dvfe.registerFunctions(['id', 'force'], { - description: 'Simply return a single argument', - canApply: args => args.length === 1, - apply: args => args[0] -}); - -dvfe.registerFunction('-', { - description: '-a', - canApply: args => args.length === 1 && isOfEitherType(args[0], 'number', 'interval', 'vector', Top.type), - apply: ([arg]) => { - return unaryValue(arg, 'negate'); - } -}); - -// maybe use intersect for clamps -dvfe.registerFunction('abs', { - description: 'abs(a)', - canApply: args => args.length === 1 && isOfEitherType(args[0], 'number', 'interval', 'vector', Top.type), - apply: ([arg]) => { - return binaryValue(unaryValue(arg, 'abs'), 'intersect', ValueIntervalMinusOneToOne); - } -}); - -dvfe.registerFunction('ceil', { - description: 'ceil(a)', - canApply: args => args.length === 1 && isOfEitherType(args[0], 'number', 'interval', 'vector', Top.type), - apply: ([arg]) => { - return unaryValue(arg, 'ceil'); - } -}); - -dvfe.registerFunction('floor', { - description: 'floor(a)', - canApply: args => args.length === 1 && isOfEitherType(args[0], 'number', 'interval', 'vector', Top.type), - apply: ([arg]) => { - return unaryValue(arg, 'floor'); - } -}); - -dvfe.registerFunction('round', { - description: 'round(a)', - canApply: args => args.length === 1 && isOfEitherType(args[0], 'number', 'interval', 'vector', Top.type), - apply: ([arg]) => { - return unaryValue(arg, 'round'); - } -}); - -dvfe.registerFunction('sign', { - description: 'sign(a)', - canApply: args => args.length === 1 && isOfEitherType(args[0], 'number', 'interval', 'vector', Top.type), - apply: ([arg]) => { - return unaryValue(arg, 'sign'); - } -}); - -dvfe.registerFunction('add', { - description: 'a + b', - canApply: args => args.length === 2 && isOfEitherType(args[0], 'number', 'interval', 'vector', Top.type) && isOfEitherType(args[1], 'number', 'interval', 'vector', Top.type), - apply: ([a, b]) => { - return binaryValue(a, 'add', b); - } -}); - -dvfe.registerFunction('sub', { - description: 'a - b', - canApply: args => args.length === 2 && isOfEitherType(args[0], 'number', 'interval', 'vector', Top.type) && isOfEitherType(args[1], 'number', 'interval', 'vector', Top.type), - apply: ([a, b]) => { - return binaryValue(a, 'sub', b); - } -}); - -dvfe.registerFunction('mul', { - description: 'a * b', - canApply: args => args.length === 2 && isOfEitherType(args[0], 'number', 'interval', 'vector', Top.type) && isOfEitherType(args[1], 'number', 'interval', 'vector', Top.type), - apply: ([a, b]) => { - return binaryValue(a, 'mul', b); - } -}); - -dvfe.registerFunction('div', { - description: 'a / b', - canApply: args => args.length === 2 && isOfEitherType(args[0], 'number', 'interval', 'vector', Top.type) && isOfEitherType(args[1], 'number', 'interval', 'vector', Top.type), - apply: ([a, b]) => { - return binaryValue(a, 'div', b); - } -}); - -dvfe.registerFunction('pow', { - description: 'a ^ b', - canApply: args => args.length === 2 && isOfEitherType(args[0], 'number', 'interval', 'vector', Top.type) && isOfEitherType(args[1], 'number', 'interval', 'vector', Top.type), - apply: ([a, b]) => { - return binaryValue(a, 'pow', b); - } -}); - -dvfe.registerFunction('mod', { - description: 'a % b', - canApply: args => args.length === 2 && isOfEitherType(args[0], 'number', 'interval', 'vector', Top.type) && isOfEitherType(args[1], 'number', 'interval', 'vector', Top.type), - apply: ([a, b]) => { - return binaryValue(a, 'mod', b); - } -}); - -dvfe.registerFunction('max', { - description: 'max(a, b)', - canApply: args => args.length === 2 && isOfEitherType(args[0], 'number', 'interval', 'vector', Top.type) && isOfEitherType(args[1], 'number', 'interval', 'vector', Top.type), - apply: ([a, b]) => { - return binaryValue(a, 'max', b); - } -}); - -dvfe.registerFunction('min', { - description: 'min(a, b)', - canApply: args => args.length === 2 && isOfEitherType(args[0], 'number', 'interval', 'vector', Top.type) && isOfEitherType(args[1], 'number', 'interval', 'vector', Top.type), - apply: ([a, b]) => { - return binaryValue(a, 'min', b); - } -}); - - - diff --git a/src/dataflow/eval/values/general.ts b/src/dataflow/eval/values/general.ts index f2e869113b8..b4a1afb5bdc 100644 --- a/src/dataflow/eval/values/general.ts +++ b/src/dataflow/eval/values/general.ts @@ -1,5 +1,8 @@ -import type { Lift } from './r-value'; +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`. @@ -11,3 +14,29 @@ export function bottomTopGuard(...a: Lift[]): typeof Top | typeof Botto 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 index 28d268409bb..5b4fe1e5d6d 100644 --- a/src/dataflow/eval/values/intervals/interval-binary.ts +++ b/src/dataflow/eval/values/intervals/interval-binary.ts @@ -1,5 +1,5 @@ import type { Lift, Value, ValueInterval, ValueNumber } from '../r-value'; -import { Top , isTop , isBottom } from '../r-value'; +import { stringifyValue , Top , isTop , isBottom } from '../r-value'; import { binaryScalar } from '../scalar/scalar-binary'; import { ValueIntervalBottom, @@ -20,6 +20,8 @@ import { 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. @@ -30,10 +32,12 @@ export function binaryInterval( op: string, b: Lift ): Value { + let res: Value = Top; if(op in Operations) { - return Operations[op as keyof typeof Operations](a, b); + res = Operations[op as keyof typeof Operations](a, b); } - return Top; + expensiveTrace(ValueEvalLog, () => ` * binaryInterval(${stringifyValue(a)}, ${op}, ${stringifyValue(b)}) = ${stringifyValue(res)}`); + return res; } // TODO: improve handling of open intervals! @@ -69,6 +73,8 @@ const Operations = { '⊇': (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) { @@ -98,14 +104,20 @@ function intervalSub(a: Lift, b: Lift): Lift, b: Lift): Lift { - const bt = bottomTopGuard(a, b); - if(bt) { - return bt === Top ? ValueIntervalTop : ValueIntervalBottom; + if(isBottom(a) || isBottom(b)) { + return ValueIntervalBottom; + } if(isTop(a) && isTop(b)) { + return ValueIntervalTop; } - [a, b] = closeBoth(a as ValueInterval, b as ValueInterval); + [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(a.start, 'max', b.start) as ValueNumber, - binaryScalar(a.end, 'min', b.end) as ValueNumber, + binaryScalar(aStart, 'max', bStart) as ValueNumber, + binaryScalar(aEnd, 'min', bEnd) as ValueNumber, a.startInclusive && b.startInclusive, a.endInclusive && b.endInclusive ); @@ -222,7 +234,7 @@ function intervalSubset, B extends Lift iteLogical(binaryValue(getIntervalEnd(a), '<=', getIntervalEnd(b)), { - onTrue: op === '⊂' ? binaryScalar(a, '!==', b) : ValueLogicalTrue, + onTrue: op === '⊂' ? binaryValue(a, '!==', b) : ValueLogicalTrue, onFalse: ValueLogicalFalse, onMaybe: ValueLogicalMaybe, onTop: ValueLogicalMaybe, diff --git a/src/dataflow/eval/values/intervals/interval-constants.ts b/src/dataflow/eval/values/intervals/interval-constants.ts index ff644186f7e..f34602f2283 100644 --- a/src/dataflow/eval/values/intervals/interval-constants.ts +++ b/src/dataflow/eval/values/intervals/interval-constants.ts @@ -9,6 +9,7 @@ import { } 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( @@ -51,12 +52,12 @@ export function orderIntervalFrom(start: Lift, end = start, startIn ); } -export function getIntervalStart(interval: ValueInterval): ValueNumber { - return interval.start; +export function getIntervalStart(interval: Lift): Lift { + return bottomTopGuard(interval) ?? (interval as ValueInterval).start; } -export function getIntervalEnd(interval: ValueInterval): ValueNumber { - return interval.end; +export function getIntervalEnd(interval: Lift): Lift { + return bottomTopGuard(interval) ?? (interval as ValueInterval).end; } export const ValueIntervalZero = intervalFrom(0); @@ -67,4 +68,3 @@ export const ValueIntervalZeroToPositiveInfinity = intervalFromValues(ValueInteg export const ValueIntervalMinusOneToOne = intervalFrom(-1, 1); export const ValueIntervalTop = intervalFromValues(ValueIntegerTop, ValueIntegerTop); export const ValueIntervalBottom = intervalFromValues(ValueIntegerBottom, ValueIntegerBottom); -export const ValuePositiveInfinite = intervalFromValues(ValueIntegerZero, ValueIntegerPositiveInfinity, false); \ No newline at end of file diff --git a/src/dataflow/eval/values/intervals/interval-unary.ts b/src/dataflow/eval/values/intervals/interval-unary.ts index 6bc9096c666..b49ec3a3c66 100644 --- a/src/dataflow/eval/values/intervals/interval-unary.ts +++ b/src/dataflow/eval/values/intervals/interval-unary.ts @@ -1,5 +1,5 @@ import type { Lift, Value, ValueInterval, ValueLogical, ValueNumber } from '../r-value'; -import { isBottom , Top , isTop , asValue } 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 { @@ -7,7 +7,6 @@ import { ValueIntervalTop, orderIntervalFrom, ValueIntervalBottom, - ValuePositiveInfinite, ValueIntervalMinusOneToOne, ValueIntervalZero, ValueIntervalZeroToPositiveInfinity @@ -24,6 +23,8 @@ import { liftLogical, ValueLogicalBot, ValueLogicalFalse, ValueLogicalTop } from 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. @@ -31,13 +32,14 @@ import { binaryValue } from '../value-binary'; */ export function unaryInterval( a: Lift, - op: keyof typeof Operations + op: string ): Value { + let res: Value = Top; if(op in Operations) { - return Operations[op](a); - } else { - return Top; + res = Operations[op as keyof typeof Operations](a); } + expensiveTrace(ValueEvalLog, () => ` * unaryInterval(${stringifyValue(a)}, ${op}) = ${stringifyValue(res)}`); + return res; } // TODO: sin, cos, tan, ... @@ -176,8 +178,8 @@ function intervalAbs(a: Lift): ValueInterval { a.endInclusive, true // TODO: check ), - onMaybe: ValuePositiveInfinite, - onTop: ValuePositiveInfinite, + onMaybe: ValueIntervalZeroToPositiveInfinity, + onTop: ValueIntervalZeroToPositiveInfinity, onBottom: ValueIntervalBottom } ), diff --git a/src/dataflow/eval/values/logical/logical-binary.ts b/src/dataflow/eval/values/logical/logical-binary.ts index 9929d252065..442aea82025 100644 --- a/src/dataflow/eval/values/logical/logical-binary.ts +++ b/src/dataflow/eval/values/logical/logical-binary.ts @@ -1,6 +1,9 @@ -import type { Lift, TernaryLogical, ValueLogical } from '../r-value'; +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. @@ -11,11 +14,22 @@ export function binaryLogical( op: string, b: Lift ): Lift { + let res: Value = Top; guard(op in Operations, `Unknown logical binary operation: ${op}`); - return Operations[op as keyof typeof Operations](a, b); + 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; @@ -59,6 +73,8 @@ const Operations = { }) } 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 { diff --git a/src/dataflow/eval/values/logical/logical-check.ts b/src/dataflow/eval/values/logical/logical-check.ts index 4d8eafeef64..451c112c3cd 100644 --- a/src/dataflow/eval/values/logical/logical-check.ts +++ b/src/dataflow/eval/values/logical/logical-check.ts @@ -1,16 +1,20 @@ import type { Value, ValueLogical } from '../r-value'; -import { isBottom, isTop , Bottom , Top } 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 } from './logical-constants'; +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 isTruthy(a: Value): ValueLogical { +export function toTruthy(a: Value): ValueLogical { + expensiveTrace(ValueEvalLog, () => ` * isTruthy(${stringifyValue(a)})`); if(a === Top) { return ValueLogicalTop; } else if(a === Bottom) { @@ -26,11 +30,18 @@ export function isTruthy(a: Value): ValueLogical { } else if(a.type === 'vector') { return isTop(a.elements) || isBottom(a.elements) ? liftLogical(a.elements) : a.elements.length !== 0 ? ValueLogicalBot : - isTruthy(a.elements[0]); + 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; @@ -43,7 +54,7 @@ export function iteLogical( cond: Value, { onTrue, onFalse, onMaybe, onTop, onBottom }: IteCases ): Result { - const condVal = isTruthy(cond).value; + const condVal = toTruthy(cond).value; if(condVal === Top) { return force(onTop); } else if(condVal === Bottom) { diff --git a/src/dataflow/eval/values/logical/logical-unary.ts b/src/dataflow/eval/values/logical/logical-unary.ts index c7f618a4404..8f6ce1559bf 100644 --- a/src/dataflow/eval/values/logical/logical-unary.ts +++ b/src/dataflow/eval/values/logical/logical-unary.ts @@ -1,4 +1,5 @@ -import type { Lift, ValueInterval, ValueLogical } from '../r-value'; +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 { @@ -8,17 +9,27 @@ import { 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: A, +export function unaryLogical( + a: Lift, // TODO: support common unary ops - op: keyof typeof Operations -): Lift { - return bottomTopGuard(a) ?? Operations[op](a as ValueLogical); + 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; } /* 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 index a2096ab546d..add60036660 100644 --- a/src/dataflow/eval/values/r-value.ts +++ b/src/dataflow/eval/values/r-value.ts @@ -1,6 +1,6 @@ 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 { guard } from '../../../util/assert'; +import { assertUnreachable, guard } from '../../../util/assert'; export const Top = { type: Symbol('⊤') }; export const Bottom = { type: Symbol('⊥') }; @@ -15,9 +15,20 @@ export interface ValueInterval { end: Limit endInclusive: boolean } -export interface ValueVector = Lift> { - type: 'vector' - elements: Elements + +/** + * 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' @@ -27,6 +38,9 @@ export interface ValueString = Lift type: 'string' value: Str } +export interface ValueMissing { + type: 'missing' +} export type TernaryLogical = RLogicalValue | 'maybe' export interface ValueLogical { type: 'logical' @@ -35,10 +49,12 @@ export interface ValueLogical { export type Value = Lift< ValueInterval - | ValueVector + | ValueVector + | ValueSet | ValueNumber | ValueString | ValueLogical + | ValueMissing > export type ValueType = V extends { type: infer T } ? T : never export type ValueTypes = ValueType @@ -80,7 +96,6 @@ function tryStringifyBoTop>( } } - function stringifyRNumberSuffix(value: RNumberValue): string { let suffix = ''; if(value.markedAsInt) { @@ -105,13 +120,18 @@ function renderString(value: RStringValue): string { export function stringifyValue(value: Lift): string { return tryStringifyBoTop(value, v => { - switch(v.type) { + 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 `c(${e.map(stringifyValue).join(',')})`; - }, () => '⊤ (vector)', () => '⊥ (vector)'); + 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)}`, @@ -121,6 +141,10 @@ export function stringifyValue(value: Lift): 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 index e1e1a73dc09..bb94a85a1f3 100644 --- a/src/dataflow/eval/values/scalar/scalar-binary.ts +++ b/src/dataflow/eval/values/scalar/scalar-binary.ts @@ -1,10 +1,13 @@ import type { Lift, Value, ValueLogical, ValueNumber } from '../r-value'; -import { Bottom } 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 { guard } from '../../../../util/assert'; 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. @@ -15,8 +18,19 @@ export function binaryScalar( op: string, b: Lift ): Value { - guard(op in ScalarBinaryOperations, `Unknown scalar binary operation: ${op}`); - return ScalarBinaryOperations[op as keyof typeof ScalarBinaryOperations](a, b); + 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; } @@ -45,9 +59,10 @@ const ScalarBinaryOperations = { '⊃': (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 Math.abs(a - b) < 2 * ValueNumberEpsilon.value.num; + return a === b || Math.abs(a - b) < 2 * ValueNumberEpsilon.value.num; } diff --git a/src/dataflow/eval/values/scalar/scalar-constants.ts b/src/dataflow/eval/values/scalar/scalar-constants.ts index 8250e793091..299af46577a 100644 --- a/src/dataflow/eval/values/scalar/scalar-constants.ts +++ b/src/dataflow/eval/values/scalar/scalar-constants.ts @@ -27,6 +27,7 @@ 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); diff --git a/src/dataflow/eval/values/scalar/scalar-unary.ts b/src/dataflow/eval/values/scalar/scalar-unary.ts index bb29898ecf8..5f1005245c1 100644 --- a/src/dataflow/eval/values/scalar/scalar-unary.ts +++ b/src/dataflow/eval/values/scalar/scalar-unary.ts @@ -1,22 +1,27 @@ import type { Lift, Value, ValueLogical, ValueNumber } from '../r-value'; -import { asValue, Bottom , isBottom , isTop, Top } 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 interval and apply the given op. + * 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}`); - return ScalarUnaryOperations[op as keyof typeof ScalarUnaryOperations](a); + res = bottomTopGuard(a) ?? ScalarUnaryOperations[op as keyof typeof ScalarUnaryOperations](a); + expensiveTrace(ValueEvalLog, () => ` * unaryScalar(${stringifyValue(a)}, ${op}) = ${stringifyValue(res)}`); + return res; } const ScalarUnaryOperations = { 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 index d085b8ed45e..9d10f67a3c3 100644 --- a/src/dataflow/eval/values/string/string-binary.ts +++ b/src/dataflow/eval/values/string/string-binary.ts @@ -1,9 +1,12 @@ import type { Lift, Value, ValueLogical, ValueString } from '../r-value'; -import { isBottom, isTop } 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. @@ -14,27 +17,52 @@ export function binaryString( op: string, b: Lift ): Value { + let res: Value = Top; guard(op in Operations, `Unknown string binary operation: ${op}`); - return Operations[op as keyof typeof Operations](a, b); + 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 = { - '<=': (a, b) => stringHelper(a, b, (a, b) => a <= b), - '<': (a, b) => stringHelper(a, b, (a, b) => a < b), - '>=': (a, b) => stringHelper(a, b, (a, b) => a >= b), - '>': (a, b) => stringHelper(a, b, (a, b) => a > b), - '==': (a, b) => stringHelper(a, b, (a, b) => a === b), - '!=': (a, b) => stringHelper(a, b, (a, b) => a !== b), - '===': (a, b) => stringHelper(a, b, (a, b) => a === b), - '!==': (a, b) => stringHelper(a, b, (a, b) => a !== b), + '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) => stringHelper(a, b, (a, b) => b.includes(a)), - '⊂': (a, b) => stringHelper(a, b, (a, b) => b.includes(a) && a !== b), - '⊇': (a, b) => stringHelper(a, b, (a, b) => a.includes(b)), - '⊃': (a, b) => stringHelper(a, b, (a, b) => a.includes(b) && a !== b) + '⊆': (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 diff --git a/src/dataflow/eval/values/string/string-constants.ts b/src/dataflow/eval/values/string/string-constants.ts index 7677ab2fcfb..3082edddb8c 100644 --- a/src/dataflow/eval/values/string/string-constants.ts +++ b/src/dataflow/eval/values/string/string-constants.ts @@ -1,5 +1,6 @@ import type { RStringValue } from '../../../../r-bridge/lang-4.x/convert-values'; -import type { ValueString } from '../r-value'; +import type { Lift, ValueString } from '../r-value'; +import { Bottom, Top } from '../r-value'; export function stringFrom(str: RStringValue | string): ValueString { @@ -12,5 +13,14 @@ export function stringFrom(str: RStringValue | string): ValueString { }; } +export function liftString(str: Lift): ValueString { + return { + type: 'string', + value: str + }; +} + -export const ValueEmptyString = stringFrom(''); \ No newline at end of file +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 index 03785678c12..1778c6b157d 100644 --- a/src/dataflow/eval/values/value-binary.ts +++ b/src/dataflow/eval/values/value-binary.ts @@ -1,5 +1,5 @@ import type { Lift, Value } from './r-value'; -import { Bottom , isBottom, isTop, Top } 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'; @@ -9,6 +9,10 @@ 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>; @@ -18,7 +22,8 @@ function initialize() { 'logical': binaryLogical, 'interval': binaryInterval, 'string': binaryString, - 'vector': binaryVector + 'vector': binaryVector, + 'set': binarySet } as Record Value>; } @@ -27,6 +32,7 @@ export function binaryValue( 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; @@ -35,7 +41,19 @@ export function binaryValue( } else { return Bottom; } - } else if(isTop(a)) { + } + + 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; @@ -53,12 +71,7 @@ export function binaryValue( if(a.type === b.type) { return binaryEnsured(a, op, b, a.type); - } else if(a.type === 'vector') { - return binaryValue(a, op, vectorFrom(b)); - } else if(b.type === 'vector') { - return binaryValue(vectorFrom(a), op, b); - } - if(a.type === 'interval' && b.type === 'number') { + } 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); diff --git a/src/dataflow/eval/values/value-unary.ts b/src/dataflow/eval/values/value-unary.ts index 85adbd3046f..f5b534d3c0d 100644 --- a/src/dataflow/eval/values/value-unary.ts +++ b/src/dataflow/eval/values/value-unary.ts @@ -1,10 +1,14 @@ import type { Lift, Value } from './r-value'; -import { Bottom, isBottom, isTop, Top } 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>; @@ -13,8 +17,9 @@ function initialize() { 'number': unaryScalar, 'logical': unaryLogical, 'interval': unaryInterval, - 'string': unaryScalar, // TODO - 'vector': unaryVector + 'string': unaryString, + 'vector': unaryVector, + 'set': unarySet } as Record Value>; } @@ -23,16 +28,18 @@ export function unaryValue>( a: A, op: string ): Value { + expensiveTrace(ValueEvalLog, () => ` * unaryValue(${stringifyValue(a)}, ${op})`); + if(isBottom(a)) { return Bottom; - } else if(isTop(a)) { + } 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, a.type); + return unaryEnsured(a, op, a.type); } -function unaryEnsured( - a: A, op: string, b: B, +function unaryEnsured( + a: Value, op: string, type: string ): Value { initialize(); diff --git a/src/dataflow/eval/values/vectors/vector-binary.ts b/src/dataflow/eval/values/vectors/vector-binary.ts index 1622ad48641..84a0450dd40 100644 --- a/src/dataflow/eval/values/vectors/vector-binary.ts +++ b/src/dataflow/eval/values/vectors/vector-binary.ts @@ -1,24 +1,30 @@ -import type { Value, ValueVector } from '../r-value'; -import { isBottom, isTop } from '../r-value'; +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, ValueLogicalTop, ValueLogicalTrue } from '../logical/logical-constants'; +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: A, +export function binaryVector( + a: ValueVector, op: string, - b: B + b: ValueVector ): ValueVector { + let res: Value = Top; if(op in VectorBinaryOperations) { - return VectorBinaryOperations[op](a as ValueVector, b as ValueVector); + res = VectorBinaryOperations[op](a, b); } else { - return applyPairWiseRecycle(a, op, b); + res = applyPairWise(a, op, b, true); } + expensiveTrace(ValueEvalLog, () => ` * binaryVector(${stringifyValue(a)}, ${op}, ${stringifyValue(b)}) = ${stringifyValue(res)}`); + return res; } function recycleUntilEqualLength( @@ -39,33 +45,43 @@ function recycleUntilEqualLength( while(bElements.length < aLen) { bElements.push(bElements[bElements.length % bLen]); } - return [a, vectorFrom(...bElements)]; + return [a, vectorFrom({ elements: bElements, domain: b.elementDomain })]; } -function applyPairWiseRecycle( +function applyPairWise( a: ValueVector, op: string, - b: ValueVector + b: ValueVector, + recycle: boolean ): ValueVector { - const bt = bottomTopGuard(a.elements, b.elements); - if(bt) { - return vectorFrom(bt); - } else if((a as ValueVector).elements.length === 0 || (b as ValueVector).elements.length === 0) { - return vectorFrom(); + 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 [aV, bV] = recycleUntilEqualLength(a as ValueVector, b as ValueVector); - console.log(aV, bV); - return vectorFrom( - ...aV.elements - .map((a, i) => binaryValue(a, op, bV.elements[i])) - ); + const domain = bottomTopGuard(a.elementDomain) ?? binaryValue(a.elementDomain, op, b.elementDomain); + return vectorFrom({ + elements, + domain + }); } const VectorBinaryOperations = { '===': (a, b) => { - const res = applyPairWiseRecycle(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 ValueLogicalTop; + return toTruthy(compareDomains); } else if(isBottom(res.elements)) { return ValueLogicalBot; } else { @@ -82,5 +98,4 @@ const VectorBinaryOperations = { 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 index 9fb1f6852b3..505a4c2dcf4 100644 --- a/src/dataflow/eval/values/vectors/vector-constants.ts +++ b/src/dataflow/eval/values/vectors/vector-constants.ts @@ -1,20 +1,58 @@ -import type { Value, ValueVector } from '../r-value'; -import { Bottom , Top } from '../r-value'; +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'; -export function vectorFrom(...elements: V): ValueVector { +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 + type: 'vector', + elements, + elementDomain: domain }; } -export const ValueEmptyVector = vectorFrom(); +// 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', - elements: Top + type: 'vector', + elementDomain: Top, + elements: Top }; export const ValueVectorBottom: ValueVector = { - type: 'vector', - elements: Bottom -}; \ No newline at end of file + type: 'vector', + elementDomain: Bottom, + elements: Bottom +}; diff --git a/src/dataflow/eval/values/vectors/vector-operations.ts b/src/dataflow/eval/values/vectors/vector-operations.ts deleted file mode 100644 index 881e5182174..00000000000 --- a/src/dataflow/eval/values/vectors/vector-operations.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { Value, ValueVector } from '../r-value'; - -export function unionVector(...vs: ValueVector[]): ValueVector { - return { - type: 'vector', - elements: vs.flatMap(s => s.elements) as V - }; -} \ No newline at end of file diff --git a/src/dataflow/eval/values/vectors/vector-unary.ts b/src/dataflow/eval/values/vectors/vector-unary.ts index ec174bbab09..47d9958bb37 100644 --- a/src/dataflow/eval/values/vectors/vector-unary.ts +++ b/src/dataflow/eval/values/vectors/vector-unary.ts @@ -1,7 +1,10 @@ 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. @@ -11,22 +14,27 @@ export function unaryVector( a: A, op: string ): ValueVector { + let res: Value = Top; if(op in VectorUnaryOperations) { - return VectorUnaryOperations[op](a as ValueVector); + res = VectorUnaryOperations[op](a); } else { - return applyComponentWise(a, op); + res = applyComponentWise(a, op); } + expensiveTrace(ValueEvalLog, () => ` * unaryVector(${stringifyValue(a)}, ${op}) = ${stringifyValue(res)}`); + return res; } function applyComponentWise( a: ValueVector, op: string ): ValueVector { - const bt = bottomTopGuard(a.elements); - return bt ? vectorFrom(bt) : vectorFrom( - ...(a as ValueVector).elements - .map((a) => unaryValue(a, op)) - ); + 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> = { diff --git a/test/functionality/dataflow/eval/eval-simple.test.ts b/test/functionality/dataflow/eval/eval-simple.test.ts index 3ae219aa95c..e47425f13d0 100644 --- a/test/functionality/dataflow/eval/eval-simple.test.ts +++ b/test/functionality/dataflow/eval/eval-simple.test.ts @@ -8,13 +8,17 @@ import { evalRExpression } from '../../../../src/dataflow/eval/eval'; import { initializeCleanEnvironments } from '../../../../src/dataflow/environments/environment'; import { intervalFrom, - intervalFromValues, - ValueIntervalMinusOneToOne + intervalFromValues, ValueIntervalMinusOneToOne } from '../../../../src/dataflow/eval/values/intervals/interval-constants'; -import { getScalarFromInteger } from '../../../../src/dataflow/eval/values/scalar/scalar-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 { isTruthy } from '../../../../src/dataflow/eval/values/logical/logical-check'; +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) { @@ -24,7 +28,7 @@ describe.sequential('eval', withShell(shell => { }).allRemainingSteps(); const result = evalRExpression(results.normalize.ast, results.dataflow.graph, initializeCleanEnvironments()); const isExpected = binaryValue(result, '===', expect); - assert.isTrue(isTruthy(isExpected).value === true, + assert.isTrue(toTruthy(isExpected).value === true, `Expected ${stringifyValue(result)} to be ${stringifyValue(expect)} (${stringifyValue(isExpected)})` ); }); @@ -36,11 +40,22 @@ describe.sequential('eval', withShell(shell => { assertEval('-1', intervalFromValues(getScalarFromInteger(-1, false))); assertEval('1 + 2', intervalFromValues(getScalarFromInteger(3, false))); assertEval('1 + 2 * 7 - 8', intervalFromValues(getScalarFromInteger(7, false))); - assertEval('"foo"', stringFrom('foo')); + 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)', ValueIntervalMinusOneToOne); - assertEval('abs(sign(u)) + 1', intervalFrom(1, 2, true, true)); + 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 index 31d9c6f6e45..c4da912e124 100644 --- a/test/functionality/dataflow/eval/interval/eval-interval-simple.test.ts +++ b/test/functionality/dataflow/eval/interval/eval-interval-simple.test.ts @@ -12,7 +12,7 @@ import { } 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 { isTruthy } from '../../../../../src/dataflow/eval/values/logical/logical-check'; +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: ']' | ')') { @@ -22,9 +22,10 @@ function i(l: '[' | '(', lv: number, rv: number, r: ']' | ')') { describe('interval', () => { function shouldBeInterval(val: Value, expect: Value) { const res = binaryValue(val, '===', expect); + const t = toTruthy(res); assert.isTrue( - isTruthy(res), - `Expected ${stringifyValue(val)} to be ${stringifyValue(expect)}` + t.type === 'logical' && t.value, + `Expected ${stringifyValue(val)} to be ${stringifyValue(expect)}; but: ${stringifyValue(res)}` ); } describe('unary', () => { @@ -75,7 +76,7 @@ describe('interval', () => { { 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: ValueIntervalTop }, + { 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, ']') }, diff --git a/test/functionality/dataflow/eval/logical/eval-logical-simple.test.ts b/test/functionality/dataflow/eval/logical/eval-logical-simple.test.ts index 27013058baa..0a44242fbb1 100644 --- a/test/functionality/dataflow/eval/logical/eval-logical-simple.test.ts +++ b/test/functionality/dataflow/eval/logical/eval-logical-simple.test.ts @@ -1,21 +1,20 @@ import { assert, describe, test } from 'vitest'; -import { guard } from '../../../../../src/util/assert'; -import type { Lift, ValueLogical } from '../../../../../src/dataflow/eval/values/r-value'; -import { isBottom, isTop } from '../../../../../src/dataflow/eval/values/r-value'; +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: Lift, expect: boolean | 'maybe') { - if(typeof expect === 'boolean' || expect === 'maybe') { - guard(!isTop(value) && !isBottom(value)); - assert.equal(value.value, expect); - } + function shouldBeBool(value: Value, expect: boolean | 'maybe') { + const truthy = isTruthy(binaryValue(value, '===', liftLogical(expect))); + assert.isTrue(truthy); } describe('unary', () => { test.each([