From 09537991eb4524d7c198f5b2031b0d29e5ee85a7 Mon Sep 17 00:00:00 2001 From: Lumi Pakkanen Date: Thu, 13 Jun 2024 10:43:17 +0300 Subject: [PATCH] Disable intrinsic multiplication between intervals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace `2 edosteps` stdlib syntax with built-in `2 deg` syntax alongside `2°`. ref #346 --- documentation/advanced-dsl.md | 10 ++--- documentation/dsl.md | 2 +- documentation/intermediate-dsl.md | 2 +- documentation/technical.md | 2 +- src/expression.ts | 2 +- src/grammars/base.pegjs | 13 +++--- src/grammars/sonic-weave.pegjs | 2 +- src/parser/__tests__/expression.spec.ts | 41 ++++++++++--------- src/parser/__tests__/stdlib.spec.ts | 12 ------ .../__tests__/vector-broadcasting.spec.ts | 40 +++++++++--------- src/parser/expression.ts | 9 ++-- src/stdlib/prelude.ts | 4 -- src/warts.ts | 2 +- 13 files changed, 63 insertions(+), 78 deletions(-) diff --git a/documentation/advanced-dsl.md b/documentation/advanced-dsl.md index ae3226f2..15f59ed6 100644 --- a/documentation/advanced-dsl.md +++ b/documentation/advanced-dsl.md @@ -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.. @@ -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 diff --git a/documentation/dsl.md b/documentation/dsl.md index 44220901..1582ac99 100644 --- a/documentation/dsl.md +++ b/documentation/dsl.md @@ -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: diff --git a/documentation/intermediate-dsl.md b/documentation/intermediate-dsl.md index c37f05a3..1a1eb470 100644 --- a/documentation/intermediate-dsl.md +++ b/documentation/intermediate-dsl.md @@ -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

` 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) | diff --git a/documentation/technical.md b/documentation/technical.md index 47554c18..f65737df 100644 --- a/documentation/technical.md +++ b/documentation/technical.md @@ -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

` 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) | diff --git a/src/expression.ts b/src/expression.ts index 25bfae86..e27f4a49 100644 --- a/src/expression.ts +++ b/src/expression.ts @@ -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'; diff --git a/src/grammars/base.pegjs b/src/grammars/base.pegjs index 3340eb2f..6a1fcbe9 100644 --- a/src/grammars/base.pegjs +++ b/src/grammars/base.pegjs @@ -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 / '')|.., '.'| diff --git a/src/grammars/sonic-weave.pegjs b/src/grammars/sonic-weave.pegjs index ae46335b..60e4b260 100644 --- a/src/grammars/sonic-weave.pegjs +++ b/src/grammars/sonic-weave.pegjs @@ -1090,7 +1090,7 @@ DownExpression } StepLiteral - = count: BasicInteger '°' { + = count: BasicInteger __ ('°' / DegreeToken) { return { type: 'StepLiteral', count, diff --git a/src/parser/__tests__/expression.spec.ts b/src/parser/__tests__/expression.spec.ts index ca1aa93e..838a8b84 100644 --- a/src/parser/__tests__/expression.spec.ts +++ b/src/parser/__tests__/expression.spec.ts @@ -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'); @@ -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', () => { @@ -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", () => { @@ -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'); }); @@ -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)', () => { @@ -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)', () => { @@ -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.'); }); }); diff --git a/src/parser/__tests__/stdlib.spec.ts b/src/parser/__tests__/stdlib.spec.ts index a03349dd..da44ba7a 100644 --- a/src/parser/__tests__/stdlib.spec.ts +++ b/src/parser/__tests__/stdlib.spec.ts @@ -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] diff --git a/src/parser/__tests__/vector-broadcasting.spec.ts b/src/parser/__tests__/vector-broadcasting.spec.ts index 80ae41c6..479fa065 100644 --- a/src/parser/__tests__/vector-broadcasting.spec.ts +++ b/src/parser/__tests__/vector-broadcasting.spec.ts @@ -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}); }); diff --git a/src/parser/expression.ts b/src/parser/expression.ts index 3ea5227e..9a7c633f 100644 --- a/src/parser/expression.ts +++ b/src/parser/expression.ts @@ -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; @@ -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)); diff --git a/src/stdlib/prelude.ts b/src/stdlib/prelude.ts index ba37664d..43536fcb 100644 --- a/src/stdlib/prelude.ts +++ b/src/stdlib/prelude.ts @@ -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."; diff --git a/src/warts.ts b/src/warts.ts index 14c7f10c..bada6f45 100644 --- a/src/warts.ts +++ b/src/warts.ts @@ -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 === '') {