Skip to content

Commit

Permalink
Implement vectorized versions of boolean 'and', 'not' and 'or'
Browse files Browse the repository at this point in the history
ref #264
  • Loading branch information
frostburn committed Apr 28, 2024
1 parent 37567fc commit d8ee281
Show file tree
Hide file tree
Showing 7 changed files with 84 additions and 34 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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\` |
Expand All @@ -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 |
Expand Down
6 changes: 3 additions & 3 deletions documentation/technical.md
Original file line number Diff line number Diff line change
Expand Up @@ -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* |

Expand Down
3 changes: 3 additions & 0 deletions src/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export type UnaryOperator =
| '%'
| '÷'
| 'not'
| 'vnot'
| '^'
| '∧'
| '\u2228' // ∨
Expand All @@ -20,7 +21,9 @@ export type BinaryOperator =
| 'lest'
| '??'
| 'or'
| 'vor'
| 'and'
| 'vand'
| '==='
| '!=='
| '=='
Expand Down
20 changes: 10 additions & 10 deletions src/grammars/sonic-weave.pegjs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@
'to',
'try',
'true',
'vand',
'vnot',
'vor',
'while',
]);

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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'
Expand Down
8 changes: 8 additions & 0 deletions src/parser/__tests__/source.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
});
});
61 changes: 43 additions & 18 deletions src/parser/__tests__/vector-broadcasting.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, Interval>;
expect(Object.keys(rec)).toHaveLength(2);
const rec = evaluateExpression(
`${op}{four: 4, "negative half": -1/2}`
) as Record<string, Interval>;
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);
}
);
});
});
17 changes: 14 additions & 3 deletions src/parser/expression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 '-':
Expand Down Expand Up @@ -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}'.`);
}
Expand Down Expand Up @@ -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)) {
Expand All @@ -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) {
Expand Down

0 comments on commit d8ee281

Please sign in to comment.