From e6d791961210806702e2d03f2daa11fa509e513c Mon Sep 17 00:00:00 2001 From: Lumi Pakkanen Date: Sat, 15 Jun 2024 18:29:45 +0300 Subject: [PATCH] Fix complexity issues by coercing to reals Fix potential this alias issues in TimeMonzo.log(). refs: #244, #348 --- package-lock.json | 18 ++-- package.json | 4 +- src/monzo.ts | 32 +++++-- src/parser/__tests__/expression.spec.ts | 51 +++++++++++ src/parser/__tests__/source.spec.ts | 30 ++++++ src/parser/expression.ts | 116 +++++++++++++++++++----- 6 files changed, 207 insertions(+), 44 deletions(-) diff --git a/package-lock.json b/package-lock.json index fbd0207e..18438516 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,8 @@ "version": "0.5.0", "license": "MIT", "dependencies": { - "moment-of-symmetry": "^0.8.1", - "xen-dev-utils": "^0.9.0" + "moment-of-symmetry": "^0.8.2", + "xen-dev-utils": "^0.9.2" }, "bin": { "sonic-weave": "bin/sonic-weave.js" @@ -2938,11 +2938,11 @@ } }, "node_modules/moment-of-symmetry": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/moment-of-symmetry/-/moment-of-symmetry-0.8.1.tgz", - "integrity": "sha512-t8nR6DL4dpjv247WI7dIDbwmFrUhJZZHOguRNab1lw1TGWht0gEqNi1ux/uDxxCLAZRTivDUfM4MvXXmJUMb3A==", + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/moment-of-symmetry/-/moment-of-symmetry-0.8.2.tgz", + "integrity": "sha512-Ez+CsTACcJHgUIg0Dl+TMKSAfx68KlJloMWqz8KCGU880xGGZXkG62OYLglpGHt3ajtwFePpyW4N7aOFku5ZmA==", "dependencies": { - "xen-dev-utils": "^0.9.0" + "xen-dev-utils": "^0.9.2" }, "funding": { "type": "github", @@ -4492,9 +4492,9 @@ } }, "node_modules/xen-dev-utils": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/xen-dev-utils/-/xen-dev-utils-0.9.0.tgz", - "integrity": "sha512-JsbXSg1zXaBoiKI19p2jC8Ka22YADQsTBD7fc2FkVxLWSdCO5BCS5KcRquDP5vP6J9v8t3B14G/7GZ5DC73rzg==", + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/xen-dev-utils/-/xen-dev-utils-0.9.2.tgz", + "integrity": "sha512-1yC1TuCQT/pLOT4zwiRnqty7nvG6PU+ssDTjkGEEBwjOQlL3ZmoloBioAMC1Cj2GZed3kvZm9btSdh2DPbl2SQ==", "engines": { "node": ">=10.6.0" }, diff --git a/package.json b/package.json index 40a670dd..568cee65 100644 --- a/package.json +++ b/package.json @@ -67,8 +67,8 @@ "vitest": "^1.6.0" }, "dependencies": { - "moment-of-symmetry": "^0.8.1", - "xen-dev-utils": "^0.9.0" + "moment-of-symmetry": "^0.8.2", + "xen-dev-utils": "^0.9.2" }, "engines": { "node": ">=12.0.0" diff --git a/src/monzo.ts b/src/monzo.ts index d650ba05..247bd701 100644 --- a/src/monzo.ts +++ b/src/monzo.ts @@ -1827,7 +1827,14 @@ export class TimeMonzo { } const vector = []; for (let i = 0; i < other.primeExponents.length; ++i) { - vector.push(this.primeExponents[i].add(other.primeExponents[i])); + try { + vector.push(this.primeExponents[i].add(other.primeExponents[i])); + } catch { + return new TimeReal( + this.timeExponent.valueOf() + other.timeExponent.valueOf(), + this.valueOf() * other.valueOf() + ); + } } try { const residual = this.residual.mul(other.residual); @@ -1929,7 +1936,14 @@ export class TimeMonzo { } const vector = []; for (let i = 0; i < other.primeExponents.length; ++i) { - vector.push(self.primeExponents[i].sub(other.primeExponents[i])); + try { + vector.push(self.primeExponents[i].sub(other.primeExponents[i])); + } catch { + return new TimeReal( + self.timeExponent.valueOf() - other.timeExponent.valueOf(), + self.valueOf() / other.valueOf() + ); + } } try { const residual = self.residual.div(other.residual); @@ -2063,8 +2077,8 @@ export class TimeMonzo { if (solution === undefined) { if (other.primeExponents[i].n) { solution = self.primeExponents[i].div(other.primeExponents[i]); - } else if (this.primeExponents[i].n) { - return this.totalCents() / other.totalCents(); + } else if (self.primeExponents[i].n) { + return self.totalCents() / other.totalCents(); } } else if (solution !== undefined) { if ( @@ -2072,20 +2086,20 @@ export class TimeMonzo { other.primeExponents[i].mul(solution) ) ) { - return this.totalCents() / other.totalCents(); + return self.totalCents() / other.totalCents(); } } } if (solution === undefined) { - const residualLog = this.residual.log(other.residual); + const residualLog = self.residual.log(other.residual); if (residualLog === null) { - return this.totalCents() / other.totalCents(); + return self.totalCents() / other.totalCents(); } return residualLog; } const residualPow = other.residual.pow(solution); - if (residualPow === null || !residualPow.equals(this.residual)) { - return this.totalCents() / other.totalCents(); + if (residualPow === null || !residualPow.equals(self.residual)) { + return self.totalCents() / other.totalCents(); } return solution; } diff --git a/src/parser/__tests__/expression.spec.ts b/src/parser/__tests__/expression.spec.ts index 838a8b84..3079fcc9 100644 --- a/src/parser/__tests__/expression.spec.ts +++ b/src/parser/__tests__/expression.spec.ts @@ -2409,6 +2409,57 @@ describe('SonicWeave expression evaluator', () => { const {interval} = parseSingle(String.raw`\\\P1 tmpr 12@`); expect(interval.totalCents()).toBe(-1500); }); + + it('coerces too large integers to real', () => { + const interval = evaluate('9007199254740997') as Interval; + expect(interval.value).toBeInstanceOf(TimeReal); + expect(interval.valueOf()).toBeCloseTo(9007199254740996); + }); + + it('coerces too accurate fractions to real', () => { + const interval = evaluate('9007199254740997/9000000000000000') as Interval; + expect(interval.value).toBeInstanceOf(TimeReal); + expect(interval.valueOf()).toBeCloseTo(1.0008, 6); + }); + + it('coerces too accurate decimals to real', () => { + const interval = evaluate('1.23456789012345678901e') as Interval; + expect(interval.value).toBeInstanceOf(TimeReal); + expect(interval.valueOf()).toBeCloseTo(1.23456); + }); + + it('coerces too accurate hertz to real', () => { + const interval = evaluate('1.23456789012345678901Hz') as Interval; + expect(interval.value).toBeInstanceOf(TimeReal); + expect(interval.valueOf()).toBeCloseTo(1.23456); + expect(interval.isAbsolute()).toBe(true); + }); + + it('coarces too accurate nedji to real', () => { + const interval = evaluate( + '1000000000000000\\9007199254740997<3>' + ) as Interval; + expect(interval.value).toBeInstanceOf(TimeReal); + expect(interval.valueOf()).toBeCloseTo(1.12972); + }); + + it('coerces too accurate cents to real', () => { + const interval = evaluate('1234.56789012345678901') as Interval; + expect(interval.value).toBeInstanceOf(TimeReal); + expect(interval.valueOf()).toBeCloseTo(2.04); + }); + + it('coerces too accurate monzos to real', () => { + const ronzo = evaluate('[1.234567890123456780901>') as Interval; + expect(ronzo.value).toBeInstanceOf(TimeReal); + expect(ronzo.valueOf()).toBeCloseTo(2.3531); + }); + + it('coerces too complex nedo subtraction', () => { + const interval = evaluate('103\\94906266 - 1\\94906267') as Interval; + expect(interval.value).toBeInstanceOf(TimeReal); + expect(interval.valueOf()).toBeCloseTo(1.000000745, 10); + }); }); describe('Poor grammar / Fun with "<"', () => { diff --git a/src/parser/__tests__/source.spec.ts b/src/parser/__tests__/source.spec.ts index 12e8fb7f..c860ce09 100644 --- a/src/parser/__tests__/source.spec.ts +++ b/src/parser/__tests__/source.spec.ts @@ -1991,4 +1991,34 @@ describe('SonicWeave parser', () => { 'Index out of range.' ); }); + + it('can handle too complex multiplication resulting from too accurate prime mappings', () => { + const scale = expand(` + 14/13 + 8/7 + 44/35 + 4/3 + 99/70 + 99/65 + 396/245 + 2178/1225 + 66/35 + 9801/4900 + (* Commas = 1716/1715, 2080/2079 *) + PrimeMapping(1200., 1902.0236738027506, 2786.2942222449124, 3369.11433503606, 4151.361209675464, 4440.252343874884) + cents(£, 3) + `); + expect(scale).toEqual([ + '128.862', + '230.886', + '395.953', + '497.976', + '600.', + '728.862', + '830.886', + '995.953', + '1097.976', + '1200.', + ]); + }); }); diff --git a/src/parser/expression.ts b/src/parser/expression.ts index 9a7c633f..3646b759 100644 --- a/src/parser/expression.ts +++ b/src/parser/expression.ts @@ -1,4 +1,4 @@ -import {Fraction, gcd} from 'xen-dev-utils'; +import {Fraction, PRIMES, gcd} from 'xen-dev-utils'; import { NedjiLiteral, IntegerLiteral, @@ -662,7 +662,18 @@ export class ExpressionVisitor { protected visitComponent(component: VectorComponent) { // XXX: This is so backwards... - return new Fraction(formatComponent(component)); + const str = formatComponent(component); + try { + return new Fraction(str); + } catch { + if (component.separator === '/') { + return ( + (component.left / parseInt(component.right)) * + 10 ** (component.exponent ?? 0) + ); + } + return parseFloat(str); + } } protected upLift( @@ -703,7 +714,21 @@ export class ExpressionVisitor { } } } else { - value = new TimeMonzo(ZERO, exponents); + let valid = true; + for (const exponent of exponents) { + if (typeof exponent === 'number') { + valid = false; + } + } + if (valid) { + value = new TimeMonzo(ZERO, exponents as Fraction[]); + } else { + let num = 1; + for (let i = 0; i < exponents.length; ++i) { + num *= PRIMES[i] ** exponents[i].valueOf(); + } + value = new TimeReal(0, num); + } } const result = this.upLift(value, node); if (steps.d !== 1) { @@ -719,6 +744,11 @@ export class ExpressionVisitor { protected visitValLiteral(node: ValLiteral) { const val = node.components.map(this.visitComponent); + for (const component of val) { + if (typeof component === 'number') { + throw new Error('Invalid val literal.'); + } + } let value: TimeMonzo; let equave = TWO_MONZO; if (node.basis.length) { @@ -729,7 +759,7 @@ export class ExpressionVisitor { value = valToTimeMonzo(val, subgroup); equave = subgroup[0]; } else { - value = new TimeMonzo(ZERO, val); + value = new TimeMonzo(ZERO, val as Fraction[]); } return new Val(value, equave, node); } @@ -1874,8 +1904,13 @@ export class ExpressionVisitor { } protected visitIntegerLiteral(node: IntegerLiteral): Interval { - const value = TimeMonzo.fromBigInt(node.value); - return new Interval(value, 'linear', 0, node); + try { + const value = TimeMonzo.fromBigInt(node.value); + return new Interval(value, 'linear', 0, node); + } catch { + const value = TimeReal.fromValue(Number(node.value)); + return new Interval(value, 'linear'); + } } protected visitDecimalLiteral(node: DecimalLiteral): Interval { @@ -1899,11 +1934,26 @@ export class ExpressionVisitor { numerator = 10n * numerator + BigInt(c); denominator *= 10n; } - const value = TimeMonzo.fromBigNumeratorDenominator(numerator, denominator); - if (node.flavor === 'z') { - value.timeExponent = NEGATIVE_ONE; + try { + const value = TimeMonzo.fromBigNumeratorDenominator( + numerator, + denominator + ); + if (node.flavor === 'z') { + value.timeExponent = NEGATIVE_ONE; + } + return new Interval(value, 'linear', 0, node); + } catch { + const value = TimeReal.fromValue( + parseFloat( + `${node.sign}${node.whole}.${node.fractional}e${node.exponent ?? '0'}` + ) + ); + if (node.flavor === 'z') { + value.timeExponent = -1; + } + return new Interval(value, 'linear'); } - return new Interval(value, 'linear', 0, node); } protected visitCentsLiteral(node: CentsLiteral): Interval { @@ -1937,25 +1987,43 @@ export class ExpressionVisitor { } protected visitFractionLiteral(node: FractionLiteral): Interval { - const value = TimeMonzo.fromBigNumeratorDenominator( - node.numerator, - node.denominator - ); - return new Interval(value, 'linear', 0, node); + try { + const value = TimeMonzo.fromBigNumeratorDenominator( + node.numerator, + node.denominator + ); + return new Interval(value, 'linear', 0, node); + } catch { + const value = TimeReal.fromValue( + Number(node.numerator) / Number(node.denominator) + ); + return new Interval(value, 'linear'); + } } protected visitNedjiLiteral(node: NedjiLiteral): Interval { - let value: TimeMonzo; - const fractionOfEquave = new Fraction(node.numerator, node.denominator); - if (node.equaveNumerator !== null) { - value = TimeMonzo.fromEqualTemperament( - fractionOfEquave, - new Fraction(node.equaveNumerator, node.equaveDenominator ?? undefined) + try { + let value: TimeMonzo; + const fractionOfEquave = new Fraction(node.numerator, node.denominator); + if (node.equaveNumerator !== null) { + value = TimeMonzo.fromEqualTemperament( + fractionOfEquave, + new Fraction( + node.equaveNumerator, + node.equaveDenominator ?? undefined + ) + ); + } else { + value = TimeMonzo.fromEqualTemperament(fractionOfEquave); + } + return new Interval(value, 'logarithmic', 0, node); + } catch { + const base = (node.equaveNumerator ?? 2) / (node.equaveDenominator ?? 1); + const value = TimeReal.fromValue( + base ** (node.numerator / node.denominator) ); - } else { - value = TimeMonzo.fromEqualTemperament(fractionOfEquave); + return new Interval(value, 'logarithmic'); } - return new Interval(value, 'logarithmic', 0, node); } protected visitHertzLiteral(node: HertzLiteral): Interval {