Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Disable intrinsic multiplication between intervals #347

Merged
merged 1 commit into from
Jun 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 `1°`. A conversion like `monzo(-440Hz)` evaluates to
The monzo basis also supports the special symbols `s`, `Hz`, `-1`, `0`, `rc`, `1°` 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 `1°` or `1 edostep`.
We can change this using a *lift declaration* `/ = (newLiftAmount)`. The syntax for an edosteps is `1°` 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 | `7°`, `13 edosteps` | Logarithmic | Relative | Correspond to edo-steps when tempering is applied. |
| Step | `7°`, `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 | `7°` | Logarithmic | Relative | Correspond to edo-steps when tempering is applied. |
| Step | `7°`, `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
Loading