Skip to content

Commit

Permalink
Disable intrinsic multiplication between intervals
Browse files Browse the repository at this point in the history
Replace `2 edosteps` stdlib syntax with built-in `2 deg` syntax alongside `2°`.

ref #346
  • Loading branch information
frostburn committed Jun 14, 2024
1 parent b6fa94c commit 0953799
Show file tree
Hide file tree
Showing 13 changed files with 63 additions and 78 deletions.
10 changes: 5 additions & 5 deletions documentation/advanced-dsl.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ An explicit subgroup may be given with monzos as well e.g. `[0 1 -1>@2.3.13/5` f
You can even use square roots in the basis e.g. `[-1 1>@√2.√3` is monzo representation for the neutral third `n3`.

## Universal monzos
The monzo basis also supports the special symbols `s`, `Hz`, `-1`, `0`, `rc` and ``. A conversion like `monzo(-440Hz)` evaluates to
The monzo basis also supports the special symbols `s`, `Hz`, `-1`, `0`, `rc`, `` and `deg`. A conversion like `monzo(-440Hz)` evaluates to
```ocaml
(* Hz, -1, 2, 3, 5, 7, 11 *)
[ 1, 1, 3, 0, 1, 0, 1 >@Hz.-1.2..
Expand Down Expand Up @@ -473,21 +473,21 @@ E.g. `C4 = 10ms` has the same effect as `C4 = 100 Hz`.
## Implicit intrinsic calls
Associating two values like `3/2 "fif"` invokes intrinsic behavior between the interval `3/2` and the string `"fif"` resulting in an interval with the value 3/2 and the label "fif".

Semantics of the expression `left right` follow this matrix depending on the types of the operands. Booleans are converted to `0` or `1` and follow interval semantics. Question marks indicate undefined behavior.
Semantics of the expression `left right` follow this matrix depending on the types of the operands. Booleans are converted to `0` or `1` and follow interval semantics. Question marks indicate undefined behavior. Exclamation marks indicate that previous behavior has been depracated.
| left↓ right→ | Niente | String | Color | Interval | Val | Function |
| ------------ | ------------- | ------------- | ------------- | -------------- | -------------- | ------------- |
| **Niente** | ? | ? | ? | bleach | ? | `right(left)` |
| **String** | ? | concatenate | ? | label | ? | `right(left)` |
| **Color** | ? | ? | ? | paint | ? | `right(left)` |
| **Interval** | bleach | label | paint | `left × right` | `left × right` | `right(left)` |
| **Val** | ? | ? | ? | `left × right` | ? | `right(left)` |
| **Interval** | bleach | label | paint | ?! | ?! | `right(left)` |
| **Val** | ? | ? | ? | ?! | ? | `right(left)` |
| **Function** | `left(right)` | `left(right)` | `left(right)` | `left(right)` | `left(right)` | `left(right)` |

Intrinsic behavior vectorizes and broadcasts like other binary operations.

Intrinsic behavior may be evoked explicitly by simply calling a value e.g. `3/2("fif")` and `"fif"(3/2)` both work.

Some expressions like `440Hz` or `440 Hz` appear similar to intrinsic calls and would correspond to `440 × (1 Hz)` but `600.0 Hz` is actually `600.0e × (1 Hz)` instead of cents multiplied by Hertz. This exception only applies to units like `Hz`. `600.0 PI` is just `logarithmic(sqrt(2) * PI)`.
Some expressions like `440Hz` or `440 Hz` appear similar to intrinsic calls and would correspond to `440 × (1 Hz)` but `600.0 Hz` is actually `600.0e × (1 Hz)`.
It's legal to declare `let Hz = 'whatever'`, but the grammar prevents the `Hz` variable from invoking intrinsic behavior of integer literals from the right.

### Obscure types
Expand Down
2 changes: 1 addition & 1 deletion documentation/dsl.md
Original file line number Diff line number Diff line change
Expand Up @@ -534,7 +534,7 @@ Normally `E4` would temper to `8\22` but using the down inflection we made it a

Larger edos are more accurate while still being simpler to work with than pure just intonation. One downside is that a single edosteps becomes almost unnoticeably small so we need a new symbol for groups of them. By default the lift inflection (`/`) is worth 5 positive edosteps while the corresponding drop inflection (`\`) is worth 5 negative edosteps.

We can change this using a *lift declaration* `/ = (newLiftAmount)`. The syntax for an edosteps is `` or `1 edostep`.
We can change this using a *lift declaration* `/ = (newLiftAmount)`. The syntax for an edosteps is `` or `1 deg`.

Declaring a lift to be worth 6 degrees of 311edo we arrive at this version of our major scale:

Expand Down
2 changes: 1 addition & 1 deletion documentation/intermediate-dsl.md
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ The normalized frequency is now `cbrt(15000000) Hz` ≈ 246.62 Hz i.e. something
| Fraction | `4/3`, `10/7` | Linear | Relative | The fraction slash binds stronger than exponentiation |
| N-of-EDO | `1\5`, `7\12` | Logarithmic | Relative | `n\m` means `n` steps of `m` equal divisions of the octave `2/1`. |
| N-of-EDJI | `9\13<3>`, `2\5<3/2>` | Logarithmic | Relative | `n\m<p/q>` means `n` steps of `m` equal divisions of the ratio `p/q`. |
| Step | ``, `13 edosteps` | Logarithmic | Relative | Correspond to edo-steps when tempering is applied. |
| Step | ``, `13 deg` | Logarithmic | Relative | Correspond to edo-steps when tempering is applied. |
| Cents | `701.955`, `100c` | Logarithmic | Relative | One centisemitone `1.0` is equal to `1\1200`. |
| Monzo | `[-4 4 -1>`, `[1,-1/2>` | Logarithmic | Relative | Also known as prime count vectors. Each component is an exponent of a prime number factor. |
| FJS | `P5`, `M3^5` | Logarithmic | Relative | [Functional Just System](https://en.xen.wiki/w/Functional_Just_System) |
Expand Down
2 changes: 1 addition & 1 deletion documentation/technical.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ To defer execution to the end of the current block prefix the statement with `de
| Fraction | `4/3`, `10/7` | Linear | Relative | The fraction slash binds stronger than exponentiation |
| N-of-EDO | `1\5`, `7\12` | Logarithmic | Relative | `n\m` means `n` steps of `m` equal divisions of the octave `2/1`. |
| N-of-EDJI | `9\13<3>`, `2\5<3/2>` | Logarithmic | Relative | `n\m<p/q>` means `n` steps of `m` equal divisions of the ratio `p/q`. |
| Step | `` | Logarithmic | Relative | Correspond to edo-steps when tempering is applied. |
| Step | ``, `13 deg` | Logarithmic | Relative | Correspond to edo-steps when tempering is applied. |
| Cents | `701.955`, `100c` | Logarithmic | Relative | One centisemitone `1.0` is equal to `1\1200`. |
| Monzo | `[-4 4 -1>`, `[1,-1/2>` | Logarithmic | Relative | Also known as prime count vectors. Each component is an exponent of a prime number factor. |
| FJS | `P5`, `M3^5` | Logarithmic | Relative | [Functional Just System](https://en.xen.wiki/w/Functional_Just_System) |
Expand Down
2 changes: 1 addition & 1 deletion src/expression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export type WartBasisElement = BasisFraction | '';

export type ValBasisElement = WartBasisElement | 's' | 'Hz' | 'hz';

export type BasisElement = ValBasisElement | 'rc' | 'r¢' | 'inf' | '1°';
export type BasisElement = ValBasisElement | 'rc' | 'r¢' | 'inf' | '1°' | 'deg';

export type IntegerLiteral = {
type: 'IntegerLiteral';
Expand Down
13 changes: 7 additions & 6 deletions src/grammars/base.pegjs
Original file line number Diff line number Diff line change
Expand Up @@ -233,15 +233,16 @@ Fraction
}

// Tokens representing units can only appear along scalars so they're not reserved.
CentToken = @'c' !IdentifierPart
HertzToken = @'Hz' !IdentifierPart
LowHertzToken = @'hz' !IdentifierPart
RealCentToken = @'rc' !IdentifierPart
SecondToken = @'s' !IdentifierPart
CentToken = @'c' !IdentifierPart
DegreeToken = @'deg' !IdentifierPart
HertzToken = @'Hz' !IdentifierPart
LowHertzToken = @'hz' !IdentifierPart
RealCentToken = @'rc' !IdentifierPart
SecondToken = @'s' !IdentifierPart

ValBasisElement = Fraction / SecondToken / HertzToken / LowHertzToken

BasisElement = ValBasisElement / RealCentToken / 'r¢' / 'inf' / '1°' / ''
BasisElement = ValBasisElement / RealCentToken / 'r¢' / 'inf' / '1°' / DegreeToken / ''

ValBasis = (ValBasisElement / '')|.., '.'|

Expand Down
2 changes: 1 addition & 1 deletion src/grammars/sonic-weave.pegjs
Original file line number Diff line number Diff line change
Expand Up @@ -1090,7 +1090,7 @@ DownExpression
}
StepLiteral
= count: BasicInteger '°' {
= count: BasicInteger __ ('°' / DegreeToken) {
return {
type: 'StepLiteral',
count,
Expand Down
41 changes: 22 additions & 19 deletions src/parser/__tests__/expression.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,12 @@ describe('SonicWeave expression evaluator', () => {
expect(interval.totalCents()).toBe(0);
});

it('has steps (ASCII)', () => {
const {interval} = parseSingle('5 deg');
expect(interval.steps).toBe(5);
expect(interval.totalCents()).toBe(0);
});

it('has gcd', () => {
const {interval} = parseSingle('gcd(30, 84)');
expect(interval.toString()).toBe('6');
Expand Down Expand Up @@ -2104,9 +2110,10 @@ describe('SonicWeave expression evaluator', () => {
expect(interval.totalCents()).toBe(1901.9);
});

it('has implicit elementwise product', () => {
const numpyIndexingTears = evaluate('[2, 3] [5, 7]') as Interval[];
expect(numpyIndexingTears.map(i => i.toInteger())).toEqual([10, 21]);
it('no longer has implicit elementwise product', () => {
expect(() => evaluate('[2, 3] [5, 7]')).toThrow(
'Undefined intrinsic call.'
);
});

it('has implicit function calls', () => {
Expand All @@ -2119,14 +2126,12 @@ describe('SonicWeave expression evaluator', () => {
expect(interval.toString()).toBe('1\\1<3/2>');
});

it('has implicit multiplication', () => {
const {fraction} = parseSingle('(3) 5');
expect(fraction).toBe('15');
it('has deprecated implicit multiplication', () => {
expect(() => parseSingle('(3) 5')).toThrow('Undefined intrinsic call.');
});

it('has explicit intrinsic multiplication', () => {
const {fraction} = parseSingle('3(5)');
expect(fraction).toBe('15');
it('has explicit deprecated intrinsic multiplication', () => {
expect(() => parseSingle('3(5)')).toThrow('Undefined intrinsic call.');
});

it("doesn't have negative literals for a good reason", () => {
Expand All @@ -2140,7 +2145,7 @@ describe('SonicWeave expression evaluator', () => {
});

it('multiplies a monzo from the left', () => {
const {fraction} = parseSingle('2 [2 -1>');
const {fraction} = parseSingle('2 × [2 -1>');
expect(fraction).toBe('16/9');
});

Expand All @@ -2153,10 +2158,8 @@ describe('SonicWeave expression evaluator', () => {
expect(() => evaluate('2 <2 -1]')).toThrow();
});

it('accepts val multiplication from the left if you speak softly enough', () => {
const val = evaluate('2 ⟨2 -1]') as Val;
expect(val.value.primeExponents[0].toFraction()).toBe('4');
expect(val.value.primeExponents[1].toFraction()).toBe('-2');
it('accepts deprecated val multiplication from the left if you speak softly enough', () => {
expect(() => evaluate('2 ⟨2 -1]')).toThrow('Undefined intrinsic call.');
});

it('has pythonic string multiplication (right)', () => {
Expand Down Expand Up @@ -2338,8 +2341,9 @@ describe('SonicWeave expression evaluator', () => {
});

it('evokes intrinsic behavior between PI and E', () => {
const product = evaluateExpression('PI(E)', false) as Interval;
expect(product.valueOf()).toBeCloseTo(8.5397);
expect(() => evaluateExpression('PI(E)')).toThrow(
'Undefined intrinsic call.'
);
});

it('normalizes zero (frequency)', () => {
Expand Down Expand Up @@ -2437,8 +2441,7 @@ describe('Poor grammar / Fun with "<"', () => {
expect(no).toBe(false);
});

it('has quadruple semitwelfth', () => {
const {fraction} = parseSingle('1\\2<3> 4');
expect(fraction).toBe('9');
it('parses deprecated quadruple semitwelfth', () => {
expect(() => parseSingle('1\\2<3> 4')).toThrow('Undefined intrinsic call.');
});
});
12 changes: 0 additions & 12 deletions src/parser/__tests__/stdlib.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1369,18 +1369,6 @@ describe('SonicWeave standard library', () => {
expect(duckDuckGooseDuck).toEqual([false, false, true, false]);
});

it('has soft ASCII for step literals (singular)', () => {
const oneStep = parseSingle('1 edostep');
expect(oneStep.steps).toBe(1);
expect(oneStep.totalCents()).toBe(0);
});

it('has soft ASCII for step literals (plural)', () => {
const oneStep = parseSingle('-3 edosteps');
expect(oneStep.steps).toBe(-3);
expect(oneStep.totalCents()).toBe(0);
});

it('has a broadcasting domain extractor', () => {
const scale = expand(`{
const x = [2, P5]
Expand Down
40 changes: 20 additions & 20 deletions src/parser/__tests__/vector-broadcasting.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,68 +78,68 @@ describe('SonicWeave vector broadcasting', () => {
);
});

it('implicitly multiplies a scalar with a 1D array', () => {
const vec = sw1D`3 [5, 7]`;
it('multiplies a scalar with a 1D array', () => {
const vec = sw1D`3 * [5, 7]`;
expect(vec).toEqual([15, 21]);
});

it('implicitly multiplies a scalar with a 2D array', () => {
const mat = sw2D`3 [[5, 7], [11, 13]]`;
it('multiplies a scalar with a 2D array', () => {
const mat = sw2D`3 * [[5, 7], [11, 13]]`;
expect(mat).toEqual([
[15, 21],
[33, 39],
]);
});

it('implicitly multiplies a scalar with a scalar', () => {
const x = sw0D`3 5`;
it('multiplies a scalar with a scalar', () => {
const x = sw0D`3 * 5`;
expect(x).toEqual(15);
});

it('implicitly multiplies a 1D array with a scalar', () => {
const vec = sw1D`[3, 5] 7`;
it('multiplies a 1D array with a scalar', () => {
const vec = sw1D`[3, 5] * 7`;
expect(vec).toEqual([21, 35]);
});

it('implicitly multiplies a 1D array with a 1D array', () => {
const vec = sw1D`[3, 5] [7, 11]`;
it('multiplies a 1D array with a 1D array', () => {
const vec = sw1D`[3, 5] * [7, 11]`;
expect(vec).toEqual([21, 55]);
});

it('implicitly multiplies a 1D array with a 2D array', () => {
const mat = sw2D`[3, 5] [[7, 11], [13, 17]]`;
it('multiplies a 1D array with a 2D array', () => {
const mat = sw2D`[3, 5] * [[7, 11], [13, 17]]`;
expect(mat).toEqual([
[21, 33],
[65, 85],
]);
});

it('implicitly multiplies a 2D array with a scalar', () => {
const mat = sw2D`[[3, 5], [7, 11]] 13`;
it('multiplies a 2D array with a scalar', () => {
const mat = sw2D`[[3, 5], [7, 11]] * 13`;
expect(mat).toEqual([
[39, 65],
[91, 143],
]);
});

it('implicitly multiplies a 2D array with a 1D array', () => {
const mat = sw2D`[[3, 5], [7, 11]] [13, 17]`;
it('multiplies a 2D array with a 1D array', () => {
const mat = sw2D`[[3, 5], [7, 11]] * [13, 17]`;
expect(mat).toEqual([
[39, 65],
[119, 187],
]);
});

it('implicitly multiplies a 2D array with a 2D array', () => {
const mat = sw2D`[[3, 5], [7, 11]] [[13, 17], [19, 23]]`;
it('multiplies a 2D array with a 2D array', () => {
const mat = sw2D`[[3, 5], [7, 11]] * [[13, 17], [19, 23]]`;
expect(mat).toEqual([
[39, 85],
[133, 253],
]);
});

it('implicitly multiplies records', () => {
const rec = swRec`{a: 3, b: 5} {a: 7, b: 11}`;
it('multiplies records', () => {
const rec = swRec`{a: 3, b: 5} * {a: 7, b: 11}`;
expect(rec).toEqual({a: 21, b: 55});
});

Expand Down
9 changes: 3 additions & 6 deletions src/parser/expression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1633,17 +1633,14 @@ export class ExpressionVisitor {
callee.label = caller;
return callee;
case 'boolean':
if (caller) {
return callee.shallowClone();
}
return fromInteger(0);
throw new Error('Undefined intrinsic call.');
case 'undefined':
callee = callee.shallowClone();
callee.color = undefined;
return callee;
}
if (caller instanceof Interval || caller instanceof Val) {
return caller.mul(callee);
throw new Error('Undefined intrinsic call.');
} else if (caller instanceof Color) {
callee = callee.shallowClone();
callee.color = caller;
Expand All @@ -1658,7 +1655,7 @@ export class ExpressionVisitor {
caller: SonicWeaveValue
): SonicWeaveValue {
if (typeof caller === 'boolean' || caller instanceof Interval) {
return upcastBool(caller).mul(callee);
throw new Error('Undefined intrinsic call.');
}
const ic = this.intrinsicValCall.bind(this);
return unaryBroadcast.bind(this)(caller, c => ic(callee, c));
Expand Down
4 changes: 0 additions & 4 deletions src/stdlib/prelude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,6 @@ riff absoluteHEJI(interval) {
return absoluteFJS(interval, 'h');
}
(** Constants **)
const edostep = 1°;
const edosteps = edostep;
(** Functions **)
riff vbool(value) {
"Convert value to a boolean. Vectorizes over arrays.";
Expand Down
2 changes: 1 addition & 1 deletion src/warts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export function parseSubgroup(basis: BasisElement[], targetSize?: number) {
checkSpan();
} else if (element === 'inf') {
subgroup.push(INF_MONZO);
} else if (element === '1°') {
} else if (element === '1°' || element === 'deg') {
subgroup.push(STEP_ELEMENT);
checkSpan();
} else if (element === '') {
Expand Down

0 comments on commit 0953799

Please sign in to comment.