From d8ee2811ccba855f9ef0ec66b3ec1b52cc33d93f Mon Sep 17 00:00:00 2001 From: Lumi Pakkanen Date: Sun, 28 Apr 2024 11:02:01 +0300 Subject: [PATCH] Implement vectorized versions of boolean 'and', 'not' and 'or' ref #264 --- README.md | 3 + documentation/technical.md | 6 +- src/ast.ts | 3 + src/grammars/sonic-weave.pegjs | 20 +++--- src/parser/__tests__/source.spec.ts | 8 +++ .../__tests__/vector-broadcasting.spec.ts | 61 +++++++++++++------ src/parser/expression.ts | 17 +++++- 7 files changed, 84 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 1775111c..0c3064e3 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,7 @@ SonicWeave comes with some operators. | Inversion | `÷3/2` | `2/3` | `-P5` | `P-5` | | Geom. inverse | _N/A_ | | `%P8` | `v<1]` | | Logical NOT | `not 2` | `false` | `not P8` | `false` | +| Vector NOT | `vnot [0, 2]` | `[true, false]` | ... | ... | | Up | `^2` | * | `^P8` | `P8 + 1\` | | Down | `v{2}` | * | `vP8` | `P8 - 1\` | | Lift | `/2` | * | `/P8` | `P8 + 5\` | @@ -151,6 +152,8 @@ Increment/decrement assumes that you've declared `let i = 2` originally. | Logical AND | `2 and 0` | `0` | | Logical OR | `0 or 2` | `2` | | Nullish coalescing | `niente ?? 2` | `2` | +| Vector AND | `[0, 1] vand [2, 3]` | `[0, 3]` | +| Vector OR | `[0, 1] vor [2, 3]` | `[2, 1]` | ### Boolean | Name | Operator | diff --git a/documentation/technical.md b/documentation/technical.md index b8496b0f..cce0df06 100644 --- a/documentation/technical.md +++ b/documentation/technical.md @@ -47,9 +47,9 @@ All operations are left-associative except exponentiation, recipropower, logdivi | `x:y::z`, `/x::y:x` | Chord enumeration, reflected chord enumeration | | `to`, `by` | Linear rounding, logarithmic rounding | | `===`, `!==`, `==`, `!=`, `<=`, `>=`, `<`, `>`, `of`, `not of`, `~of`, `not ~of`, `in`, `not in`, `~in`, `not ~in` | Strict equality, size equality, comparisons, strict/non-strict value inclusion, strict/non-strict index/key inclusion | -| `not x` | Boolean not | -| `and` | Boolean and | -| `or`, `??` | Boolean or, niente coalescing | +| `not x`, `vnot x` | Boolean not, vector not | +| `and`, `vand` | Boolean and, vector and | +| `or`, `vor`, `??` | Boolean or, vector or, niente coalescing | | `x if y else z` | Ternary conditional | | `lest` | Fallback* | diff --git a/src/ast.ts b/src/ast.ts index 32ac26b4..db0a5a0f 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -6,6 +6,7 @@ export type UnaryOperator = | '%' | '÷' | 'not' + | 'vnot' | '^' | '∧' | '\u2228' // ∨ @@ -20,7 +21,9 @@ export type BinaryOperator = | 'lest' | '??' | 'or' + | 'vor' | 'and' + | 'vand' | '===' | '!==' | '==' diff --git a/src/grammars/sonic-weave.pegjs b/src/grammars/sonic-weave.pegjs index 1b7c1074..23ed2dfc 100644 --- a/src/grammars/sonic-weave.pegjs +++ b/src/grammars/sonic-weave.pegjs @@ -45,6 +45,9 @@ 'to', 'try', 'true', + 'vand', + 'vnot', + 'vor', 'while', ]); @@ -165,6 +168,9 @@ ThrowToken = @'throw' !IdentifierPart ToToken = @'to' !IdentifierPart TryToken = @'try' !IdentifierPart TrueToken = @'true' !IdentifierPart +VectorAndToken = @'vand' !IdentifierPart +VectorNotToken = @'vnot' !IdentifierPart +VectorOrToken = @'vor' !IdentifierPart WhileToken = @'while' !IdentifierPart Statements @@ -576,14 +582,14 @@ ConditionalExpression ); } -CoalescingOperator = '??' / OrToken +CoalescingOperator = '??' / OrToken / VectorOrToken CoalescingExpression = head: ConjunctionExpression tail: (__ @CoalescingOperator _ @ConjunctionExpression)* { return tail.reduce(operatorReducerLite, head); } -ConjunctOperator = AndToken +ConjunctOperator = AndToken / VectorAndToken Conjunct = NotExpression / RelationalExpression @@ -593,14 +599,8 @@ ConjunctionExpression } NotExpression - = operators: NotToken|.., __| __ operand: RelationalExpression { - if (!operators.length) { - return operand; - } - if (operators.length & 1) { - return UnaryExpression('not', operand, false); - } - return UnaryExpression('not', UnaryExpression('not', operand, false), false); + = operators: (NotToken / VectorNotToken)|.., __| __ operand: RelationalExpression { + return operators.reduce((result, operator) => UnaryExpression(operator, result, false), operand); } RelationalOperator 'relational operator' diff --git a/src/parser/__tests__/source.spec.ts b/src/parser/__tests__/source.spec.ts index f0bd2af7..6452b948 100644 --- a/src/parser/__tests__/source.spec.ts +++ b/src/parser/__tests__/source.spec.ts @@ -1365,4 +1365,12 @@ describe('SonicWeave parser', () => { '2 white "G"', ]); }); + + it('can build complex indexing conditionals using vectorized boolean ops', () => { + const scale = expand(`{ + const xs = [0..9]; + xs[vnot xs vor xs > 3 vand xs <= 7]; + }`); + expect(scale).toEqual(['0', '4', '5', '6', '7']); + }); }); diff --git a/src/parser/__tests__/vector-broadcasting.spec.ts b/src/parser/__tests__/vector-broadcasting.spec.ts index 218f73ef..58ee8558 100644 --- a/src/parser/__tests__/vector-broadcasting.spec.ts +++ b/src/parser/__tests__/vector-broadcasting.spec.ts @@ -149,31 +149,56 @@ describe('SonicWeave vector broadcasting', () => { expect(evaluateExpression('not [0, 0]')).toBe(false); }); - it.each(['-', '+', '%', '÷', '^', '∧', '∨', '/', 'lift ', '\\', 'drop '])( - 'broadcasts unary operator "%s"', - op => { - const four = evaluateExpression(`${op}4`) as Interval; - const negHalf = evaluateExpression(`${op}(-1/2)`) as Interval; - - const vec = evaluateExpression(`${op}[4, -1/2]`) as Interval[]; - expect(vec).toHaveLength(2); + it.each([ + 'vnot ', + '-', + '+', + '%', + '÷', + '^', + '∧', + '∨', + '/', + 'lift ', + '\\', + 'drop ', + ])('broadcasts unary operator "%s"', op => { + const four = evaluateExpression(`${op}4`) as Interval; + const negHalf = evaluateExpression(`${op}(-1/2)`) as Interval; + + const vec = evaluateExpression(`${op}[4, -1/2]`) as Interval[]; + expect(vec).toHaveLength(2); + if (op === 'vnot ') { + expect(vec[0]).toBe(four); + expect(vec[1]).toBe(negHalf); + } else { expect(vec[0].strictEquals(four)).toBe(true); expect(vec[1].strictEquals(negHalf)).toBe(true); + } - const mat = evaluateExpression( - `${op}[[4, -1/2], [1, 1]]` - ) as unknown as Interval[][]; - expect(mat).toHaveLength(2); - expect(mat[0]).toHaveLength(2); + const mat = evaluateExpression( + `${op}[[4, -1/2], [1, 1]]` + ) as unknown as Interval[][]; + expect(mat).toHaveLength(2); + expect(mat[0]).toHaveLength(2); + if (op === 'vnot ') { + expect(mat[0][0]).toBe(four); + expect(mat[0][1]).toBe(negHalf); + } else { expect(mat[0][0].strictEquals(four)).toBe(true); expect(mat[0][1].strictEquals(negHalf)).toBe(true); + } - const rec = evaluateExpression( - `${op}{four: 4, "negative half": -1/2}` - ) as Record; - expect(Object.keys(rec)).toHaveLength(2); + const rec = evaluateExpression( + `${op}{four: 4, "negative half": -1/2}` + ) as Record; + expect(Object.keys(rec)).toHaveLength(2); + if (op === 'vnot ') { + expect(rec.four).toBe(four); + expect(rec['negative half']).toBe(negHalf); + } else { expect(rec.four.strictEquals(four)).toBe(true); expect(rec['negative half'].strictEquals(negHalf)).toBe(true); } - ); + }); }); diff --git a/src/parser/expression.ts b/src/parser/expression.ts index 3e44badb..34bc8145 100644 --- a/src/parser/expression.ts +++ b/src/parser/expression.ts @@ -968,11 +968,14 @@ export class ExpressionVisitor { operand: SonicWeaveValue, node: UnaryExpression ): SonicWeaveValue { + const operator = node.operator; if (typeof operand === 'boolean') { + if (operator === 'vnot') { + return !operand; + } operand = upcastBool(operand); } if (operand instanceof Interval || operand instanceof Val) { - const operator = node.operator; if (node.uniform) { let value: TimeMonzo | TimeReal; let newNode = operand.node; @@ -998,6 +1001,8 @@ export class ExpressionVisitor { return new Interval(value, operand.domain, 0, newNode, operand); } switch (operator) { + case 'vnot': + return !sonicTruth(operand); case '+': return operand; case '-': @@ -1025,6 +1030,7 @@ export class ExpressionVisitor { case 'drop': return operand.drop(this.rootContext); } + operator satisfies 'not'; // The runtime shouldn't let you get here. throw new Error(`Unexpected unary operation '${operator}'.`); } @@ -1136,7 +1142,7 @@ export class ExpressionVisitor { left: SonicWeaveValue, right: SonicWeaveValue, node: BinaryExpression - ): boolean | boolean[] | Interval | Interval[] | Val | Val[] { + ): SonicWeaveValue { if (Array.isArray(left)) { const b = this.binaryOperate.bind(this); if (Array.isArray(right)) { @@ -1150,13 +1156,18 @@ export class ExpressionVisitor { const b = this.binaryOperate.bind(this); return right.map(r => b(left, r, node)) as Interval[]; } + const operator = node.operator; + if (operator === 'vor') { + return sonicTruth(left) ? left : right; + } else if (operator === 'vand') { + return !sonicTruth(left) ? left : right; + } if (typeof left === 'boolean') { left = upcastBool(left); } if (typeof right === 'boolean') { right = upcastBool(right); } - const operator = node.operator; if (left instanceof Interval) { if (right instanceof Interval) { if (node.preferLeft || node.preferRight) {