Skip to content

Commit

Permalink
Make abs a unary operator
Browse files Browse the repository at this point in the history
Add labs as the logarithmic analogue.
Same domain semantics as everything else.

ref #309
  • Loading branch information
frostburn committed May 9, 2024
1 parent 7b6331b commit be1a280
Show file tree
Hide file tree
Showing 12 changed files with 118 additions and 58 deletions.
6 changes: 0 additions & 6 deletions documentation/BUILTIN.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@

## Built-in functions

### abs(*interval*)
Calculate the absolute value of the interval.

### absolute(*interval*)
Convert interval to absolute representation. Normalized to a frequency.

Expand Down Expand Up @@ -566,9 +563,6 @@ Apply labels (or colors) from the first array to a copy of the current/given sca
### labelsOf(*scale = $$*)
Obtain an array of labels of the current/given scale.

### labs(*x*)
Calculate the logarithmic absolute value. Inputs below unison are inverted.

### log(*x*, *y = E*)
Calculate the logarithm of x base y. Base defaults to E.

Expand Down
35 changes: 18 additions & 17 deletions documentation/intermediate-dsl.md
Original file line number Diff line number Diff line change
Expand Up @@ -364,22 +364,23 @@ Operations can be applied to intervals to create new intervals.

### Unary operators

| Name | Linear | Result | Logarithmic | Result |
| -------------- | --------- | ----------- | ----------- | ------------- |
| Identity | `+2` | `2` | `+P8` | `P8` |
| Negation | `-2` | `-2` | _N/A_ | |
| Inversion | `%2` | `1/2` | `-P8` | `P-8` |
| Inversion | `÷3/2` | `2/3` | `-P5` | `P-5` |
| Geom. inverse | _N/A_ | | `%P8` | `<1 0 0 ...]` |
| Square root | `√4` | `2` | `√P15` | `P8` |
| Logical NOT | `not 2` | `false` | `not P8` | `false` |
| Up | `^2` | * | `^P8` | `P8 + 1°` |
| Down | `v{2}` | * | `vP8` | `P8 - 1°` |
| Lift | `/2` | * | `/P8` | `P8 + 5°` |
| Drop | `\2` | * | `\P8` | `P8 - 5°` |
| Increment | `++i` | `3` | _N/A_ | |
| Decrement | `--i` | `1` | _N/A_ | |
| Absolute value | `abs(-2)` | `2` | `abs(-P8)` | `P8` |
| Name | Linear | Result | Logarithmic | Result |
| -------------- | ---------- | ----------- | ----------- | ------------- |
| Identity | `+2` | `2` | `+P8` | `P8` |
| Negation | `-2` | `-2` | _N/A_ | |
| Inversion | `%2` | `1/2` | `-P8` | `P-8` |
| Inversion | `÷3/2` | `2/3` | `-P5` | `P-5` |
| Geom. inverse | _N/A_ | | `%P8` | `<1 0 0 ...]` |
| Square root | `√4` | `2` | `√P15` | `P8` |
| Logical NOT | `not 2` | `false` | `not P8` | `false` |
| Up | `^2` | * | `^P8` | `P8 + 1°` |
| Down | `v{2}` | * | `vP8` | `P8 - 1°` |
| Lift | `/2` | * | `/P8` | `P8 + 5°` |
| Drop | `\2` | * | `\P8` | `P8 - 5°` |
| Increment | `++i` | `3` | _N/A_ | |
| Decrement | `--i` | `1` | _N/A_ | |
| Absolute value | `abs -2` | `2` | `abs(-P8)` | `P8` |
| Geometric abs | `labs 1/2` | `2` | _N/A_ | |

Square root uses the same operator in both domains because the square of a logarithmic quantity is undefined so there's no ambiguity.

Expand All @@ -389,7 +390,7 @@ The down operator sometimes requires curly brackets due to `v` colliding with th

Drop `\` can be spelled `drop` to avoid using the backslash inside template literals. Lift `/` may be spelled `lift` for minor grammatical reasons.

Note that the absolute value is technically a function call and that it behaves differently depending on the domain of the argument. In the logarithmic domain it inverts values below unity e.g. `linear(abs(logarithmic(1/2)))` evaluates to `2`.
Geometric (i.e. logarithmic) absolute value takes the normal absolute value and further inverts the result if it's below 1/1.

#### Vectorized unary operators

Expand Down
4 changes: 2 additions & 2 deletions documentation/technical.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ All operations are left-associative except exponentiation, recipropower, and log
| `++x`, `--x`, `+x`, `^x`, `∧x`, `∨x`, `/x`, `\x` | Increment, decrement, no-op, up, down, lift, drop |
| `/` | Fraction |
| `^`, `^/`, `/^`, `/_` | Exponentiation, recipropower, logdivision* |
| `-x`, `%x`, `÷x`, `√x` | Negation, inversion, square root |
| `-x`, `%x`, `÷x`, `abs x`, `labs x`, `√x` | Negation, inversion, absolute value, geometric absolute value, square root |
| `*`, `×`, `%`, `÷`, `\`, `dot`, `·`, `tns`, ``, `tmpr` | Multiplication, division, N-of-EDO, val-monzo product, array tensoring, tempering |
| `mod`, `modc`, `rd`, `rdc`, `ed` | Modulo, ceiling modulo, reduction, ceiling reduction, octave projection |
| `+`, `-`, `/+`, ``, `/-`, `` | Addition, subtraction, lens addition, lens subtraction |
Expand All @@ -51,7 +51,7 @@ All operations are left-associative except exponentiation, recipropower, and log
| `and`, `vand` | Boolean and, vector and |
| `or`, `vor`, `al` | Boolean or, vector or, niente coalescing |
| `x if y else z` | Ternary conditional |
| `lest` | Fallback[^1] |
| `lest` | Fallback[^1] |

Parenthesis, `^`, `×`, `÷`, `+`, `-` follow [PEMDAS](https://en.wikipedia.org/wiki/Order_of_operations). The fraction slash `/` represents vertically aligned fractions similar to `$\frac{3}{2}^\frac{1}{2}$` in LaTeX e.g. `3/2 ^ 1/2` evaluates to `sqrt(3 ÷ 2)`.

Expand Down
2 changes: 2 additions & 0 deletions src/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export type UnaryOperator =
| '-'
| '%'
| '÷'
| 'abs'
| 'labs'
| '√'
| 'not'
| 'vnot'
Expand Down
28 changes: 28 additions & 0 deletions src/expression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,34 @@ export function absNode(node?: IntervalLiteral): IntervalLiteral | undefined {
return undefined;
}

/** @hidden */
export function pitchAbsNode(
node?: IntervalLiteral
): IntervalLiteral | undefined {
if (!node) {
return undefined;
}
if (node.type === 'IntegerLiteral') {
if (node.value === 0n) {
return undefined;
}
return {type: 'IntegerLiteral', value: bigAbs(node.value)};
}
if (node.type === 'FractionLiteral') {
let numerator = bigAbs(node.numerator);
let denominator = bigAbs(node.denominator);
if (denominator > numerator) {
[numerator, denominator] = [denominator, numerator];
}
return {
type: 'FractionLiteral',
numerator,
denominator,
};
}
return undefined;
}

/** @hidden */
export function sqrtNode(node?: IntervalLiteral): IntervalLiteral | undefined {
if (!node) {
Expand Down
6 changes: 5 additions & 1 deletion src/grammars/sonic-weave.pegjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
};

const RESERVED_WORDS = new Set([
'abs',
'al',
'and',
'as',
Expand All @@ -30,6 +31,7 @@
'if',
'import',
'in',
'labs',
'lest',
'let',
'lift',
Expand Down Expand Up @@ -133,6 +135,7 @@ Program
};
}

AbsToken = @'abs' !IdentifierPart
AlToken = @'al' !IdentifierPart
AndToken = @'and' !IdentifierPart
AsToken = @'as' !IdentifierPart
Expand All @@ -154,6 +157,7 @@ FromToken = @'from' !IdentifierPart
IfToken = @'if' !IdentifierPart
ImportToken = @'import' !IdentifierPart
InToken = @'in' !IdentifierPart
LogAbsToken = @'labs' !IdentifierPart
LestToken = @'lest' !IdentifierPart
LetToken = @'let' !IdentifierPart
LiftToken = @'lift' !IdentifierPart
Expand Down Expand Up @@ -807,7 +811,7 @@ MultiplicativeExpression
// The radical is universal, but featured here for precedence.
UniformUnaryOperator
= '-' / '%' / '÷' / ''
= '-' / '%' / '÷' / (@AbsToken _) / (@LogAbsToken _) / ''
UniformUnaryExpression
= operator: '--' argument: ExponentiationExpression {
Expand Down
33 changes: 32 additions & 1 deletion src/interval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
literalToJSON,
literalFromJSON,
sqrtNode,
pitchAbsNode,
} from './expression';
import {TimeMonzo, TimeReal} from './monzo';
import {asAbsoluteFJS, asFJS} from './fjs';
Expand Down Expand Up @@ -380,7 +381,7 @@ export class Interval {
*/
abs() {
if (this.steps) {
throw new Error('Steps are ambiguous in abs().');
throw new Error('Steps are ambiguous in abs.');
}
const node = absNode(this.node);
if (this.domain === 'linear') {
Expand All @@ -389,6 +390,27 @@ export class Interval {
return new Interval(this.value.pitchAbs(), this.domain, 0, node, this);
}

/**
* Calculate the geometric absolute value of a linter interval.
* @returns Superunitary value.
*/
pitchAbs() {
if (this.domain === 'logarithmic') {
throw new Error(
'Logarithmic absolute value not implemented in the already-logarithmic domain.'
);
}
if (this.steps) {
throw new Error('Steps are ambiguous in labs.');
}
const node = pitchAbsNode(this.node);
return new Interval(this.value.pitchAbs(), this.domain, 0, node, this);
}

/**
* Calculate the square root of the underlying value regardless of domain.
* @returns The square root.
*/
sqrt() {
if (this.steps % 2) {
throw new Error('Cannot split steps using √.');
Expand Down Expand Up @@ -1329,6 +1351,15 @@ export class Val {
return new Val(this.value.pitchAbs(), this.equave);
}

/**
* Throws an error.
*/
pitchAbs(): Val {
throw new Error(
'Logarithmic extension of the already-cologarithmic domain not implemented.'
);
}

/**
* A meaningless operation.
* @returns A new Val obtained by pretending its value represents a linear quantity.
Expand Down
4 changes: 2 additions & 2 deletions src/parser/__tests__/expression.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -763,7 +763,7 @@ describe('SonicWeave expression evaluator', () => {
});

it('calculates geometric absolute value', () => {
const {interval} = parseSingle('abs(logarithmic(-1/6))');
const {interval} = parseSingle('abs logarithmic(-1/6)');
expect(interval.toString()).toBe('1\\1<6>');
});

Expand Down Expand Up @@ -2115,7 +2115,7 @@ describe('SonicWeave expression evaluator', () => {
});

it('has implicit fallback call semantics for chaining unary functions', () => {
const {interval} = parseSingle('(-6/4) simplify abs logarithmic');
const {interval} = parseSingle('(abs -6/4) simplify logarithmic');
expect(interval.toString()).toBe('1\\1<3/2>');
});

Expand Down
14 changes: 10 additions & 4 deletions src/parser/__tests__/source.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ describe('SonicWeave parser', () => {
});

it('can call functions from arrays', () => {
const scale = parseSource('[round, abs][0](3,14);');
const scale = parseSource('[round, trunc][0](3,14);');
expect(scale).toHaveLength(1);
expect(scale[0].toString()).toBe('3');
});
Expand Down Expand Up @@ -1454,14 +1454,20 @@ describe('SonicWeave parser', () => {
});

it('rejects unassigned absolute pitches (ASCII)', () => {
expect(() => evaluateSource('fsi#4')).toThrow('Nominal fsi is unassigned.');
expect(() => evaluateSource('fsi#4', false)).toThrow(
'Nominal fsi is unassigned.'
);
});

it('rejects unassigned absolute pitches (unicode)', () => {
expect(() => evaluateSource('ς♭2')).toThrow('Nominal ς is unassigned.');
expect(() => evaluateSource('ς♭2', false)).toThrow(
'Nominal ς is unassigned.'
);
});

it('rejects unassigned accidentals', () => {
expect(() => evaluateSource('Ce4')).toThrow('Accidental e is unassigned.');
expect(() => evaluateSource('Ce4', false)).toThrow(
'Accidental e is unassigned.'
);
});
});
14 changes: 13 additions & 1 deletion src/parser/expression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -973,6 +973,7 @@ export class ExpressionVisitor {
if (operand instanceof Interval || operand instanceof Val) {
if (node.uniform) {
let value: TimeMonzo | TimeReal;
let newSteps = 0;
let newNode = operand.node;
switch (operator) {
case '-':
Expand All @@ -982,6 +983,13 @@ export class ExpressionVisitor {
case '÷':
value = operand.value.inverse();
newNode = uniformInvertNode(newNode);
newSteps = operand instanceof Interval ? -operand.steps : 0;
break;
case 'abs':
value = operand.value.abs();
break;
case 'labs':
value = operand.value.pitchAbs();
break;
default:
// Runtime exception for √~
Expand All @@ -993,7 +1001,7 @@ export class ExpressionVisitor {
}
throw new Error('Val unary operation failed.');
}
return new Interval(value, operand.domain, 0, newNode, operand);
return new Interval(value, operand.domain, newSteps, newNode, operand);
}
switch (operator) {
case 'vnot':
Expand All @@ -1005,6 +1013,10 @@ export class ExpressionVisitor {
case '%':
case '÷':
return operand.inverse();
case 'abs':
return operand.abs();
case 'labs':
return operand.pitchAbs();
case '√':
return operand.sqrt();
}
Expand Down
13 changes: 0 additions & 13 deletions src/stdlib/builtin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1833,18 +1833,6 @@ function ceil(
ceil.__doc__ = 'Round value up to the nearest integer.';
ceil.__node__ = builtinNode(ceil);

function abs(
this: ExpressionVisitor,
interval: SonicWeaveValue
): SonicWeaveValue {
if (typeof interval === 'boolean' || interval instanceof Interval) {
return upcastBool(interval).abs();
}
return unaryBroadcast.bind(this)(interval, abs.bind(this));
}
abs.__doc__ = 'Calculate the absolute value of the interval.';
abs.__node__ = builtinNode(abs);

/**
* Obtain the argument with the minimum value.'
* @param this {@link ExpressionVisitor} instance providing context for comparing across echelons.
Expand Down Expand Up @@ -2555,7 +2543,6 @@ export const BUILTIN_CONTEXT: Record<string, Interval | SonicWeaveFunction> = {
slice,
zip,
zipLongest,
abs,
minimum,
maximum,
random,
Expand Down
Loading

0 comments on commit be1a280

Please sign in to comment.