diff --git a/documentation/BUILTIN.md b/documentation/BUILTIN.md index d4de70bc..ec69992b 100644 --- a/documentation/BUILTIN.md +++ b/documentation/BUILTIN.md @@ -124,7 +124,7 @@ Obtain a copy of the given/current scale containing values that evaluate to `tru Convert interval to (relative) FJS. ### flatten(*array*, *depth*) -Returns a new array with all sub-array elements concatenated into it recursively up to the specified depth (default `Infinity`). +Returns a new array with all sub-array elements concatenated into it recursively up to the specified depth (default `inf`). ### floor(*interval*) Round value down to the nearest integer. diff --git a/documentation/advanced-dsl.md b/documentation/advanced-dsl.md index 297fa4a2..e1391ffa 100644 --- a/documentation/advanced-dsl.md +++ b/documentation/advanced-dsl.md @@ -463,6 +463,8 @@ It's legal to declare `let Hz = 'whatever'`, but the grammar prevents the `Hz` v ### Obscure types | Type | Literal | Meaning | | ----------------- | ---------- | ---------------------------------------------------------- | +| Infinity | `inf` | Linear relative infinity | +| Not-a-number | `nan` | Generic invalid real value | | Second | `1s` | Inverse of `1Hz` i.e. `1s * 1Hz` evaluates to `1` | | Jorp | `€` | Geometric inverse of `c` i.e. `€` is equal to `<1200]` | | Pilcrowspoob | `¶` | Geometric inverse of `logarithmic(1Hz)` | diff --git a/src/__tests__/cli.spec.ts b/src/__tests__/cli.spec.ts index 0b331867..cb211ef8 100644 --- a/src/__tests__/cli.spec.ts +++ b/src/__tests__/cli.spec.ts @@ -8,22 +8,22 @@ describe('Interchange format', () => { }); it('has representation for infinity', () => { - const result = toSonicWeaveInterchange('Infinity'); - expect(result).toContain('Infinity'); + const result = toSonicWeaveInterchange('inf'); + expect(result).toContain('inf'); }); - it('has representation for NaN', () => { - const result = toSonicWeaveInterchange('NaN'); - expect(result).toContain('NaN'); + it('has representation for nan', () => { + const result = toSonicWeaveInterchange('nan'); + expect(result).toContain('nan'); }); - it('has representation for Infinity Hz', () => { - const result = toSonicWeaveInterchange('Infinity * 1 Hz'); - expect(result).toContain('Infinity * 1Hz'); + it('has representation for infinity Hz', () => { + const result = toSonicWeaveInterchange('inf * 1 Hz'); + expect(result).toContain('inf * 1Hz'); }); - it('has representation for NaN Hz (normalizes)', () => { - const result = toSonicWeaveInterchange('NaN * 1 Hz'); - expect(result).toContain('NaN'); + it('has representation for nan Hz (normalizes)', () => { + const result = toSonicWeaveInterchange('nan * 1 Hz'); + expect(result).toContain('nan'); }); }); diff --git a/src/__tests__/monzo.spec.ts b/src/__tests__/monzo.spec.ts index 6f344346..a54286c8 100644 --- a/src/__tests__/monzo.spec.ts +++ b/src/__tests__/monzo.spec.ts @@ -13,13 +13,13 @@ describe('Real value with time', () => { it('can represent NaN', () => { const nan = TimeReal.fromValue(NaN); expect(nan.value).toBeNaN(); - expect(nan.toString()).toBe('NaN'); + expect(nan.toString()).toBe('nan'); }); it('can represent Infinity', () => { const inf = TimeReal.fromValue(Infinity); expect(inf.value).toBe(Infinity); - expect(inf.toString()).toBe('Infinity'); + expect(inf.toString()).toBe('inf'); }); }); @@ -466,13 +466,13 @@ describe('JSON serialization', () => { ]; const serialized = JSON.stringify(data); expect(serialized).toBe( - '["Hello, world!",{"n":10,"d":7},{"type":"TimeReal","timeExponent":-1,"value":777},3.5,{"type":"TimeMonzo","timeExponent":{"n":0,"d":1},"primeExponents":[{"n":-4,"d":1},{"n":4,"d":1},{"n":-1,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1}],"residual":{"n":1,"d":1}},null,{"type":"TimeReal","timeExponent":0,"value":"NaN"},{"type":"TimeReal","timeExponent":1,"value":"Infinity"}]' + '["Hello, world!",{"n":10,"d":7},{"type":"TimeReal","timeExponent":-1,"value":777},3.5,{"type":"TimeMonzo","timeExponent":{"n":0,"d":1},"primeExponents":[{"n":-4,"d":1},{"n":4,"d":1},{"n":-1,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1}],"residual":{"n":1,"d":1}},null,{"type":"TimeReal","timeExponent":0,"value":"nan"},{"type":"TimeReal","timeExponent":1,"value":"inf"}]' ); }); it('can deserialize an array of primitives, fractions and monzos', () => { const serialized = - '["Hello, world!",{"n":10,"d":7},{"type":"TimeReal","timeExponent":-1,"value":777},3.5,{"type":"TimeMonzo","timeExponent":{"n":0,"d":1},"primeExponents":[{"n":-4,"d":1},{"n":4,"d":1},{"n":-1,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1}],"residual":{"n":1,"d":1}},null,{"type":"TimeReal","timeExponent":0,"value":"NaN"},{"type":"TimeReal","timeExponent":1,"value":"Infinity"}]'; + '["Hello, world!",{"n":10,"d":7},{"type":"TimeReal","timeExponent":-1,"value":777},3.5,{"type":"TimeMonzo","timeExponent":{"n":0,"d":1},"primeExponents":[{"n":-4,"d":1},{"n":4,"d":1},{"n":-1,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1}],"residual":{"n":1,"d":1}},null,{"type":"TimeReal","timeExponent":0,"value":"nan"},{"type":"TimeReal","timeExponent":1,"value":"inf"}]'; function reviver(key: string, value: any) { return TimeMonzo.reviver( key, diff --git a/src/expression.ts b/src/expression.ts index e1a19343..02d90c9d 100644 --- a/src/expression.ts +++ b/src/expression.ts @@ -150,6 +150,14 @@ export type ReciprocalLogarithmicHertzLiteral = { type: 'ReciprocalLogarithmicHertzLiteral'; }; +export type NotANumberLiteral = { + type: 'NotANumberLiteral'; +}; + +export type InfinityLiteral = { + type: 'InfinityLiteral'; +}; + export type FJS = { type: 'FJS'; ups: number; @@ -257,6 +265,8 @@ export type IntervalLiteral = | CentsLiteral | CentLiteral | ReciprocalCentLiteral + | NotANumberLiteral + | InfinityLiteral | FJS | AspiringFJS | AbsoluteFJS @@ -1051,6 +1061,10 @@ export function literalToString(literal: IntervalLiteral) { return '€'; case 'ReciprocalLogarithmicHertzLiteral': return '¶'; + case 'NotANumberLiteral': + return 'nan'; + case 'InfinityLiteral': + return 'inf'; case 'FJS': return formatFJS(literal); case 'AbsoluteFJS': @@ -1076,6 +1090,7 @@ export function literalToString(literal: IntervalLiteral) { case 'MosStepLiteral': return formatMosStepLiteral(literal); default: + literal satisfies AspiringFJS | AspiringAbsoluteFJS; throw new Error(`Cannot format ${literal.type}`); } } @@ -1172,6 +1187,8 @@ export function literalToJSON(literal?: IntervalLiteral): any { case 'NedjiLiteral': case 'CentLiteral': case 'ReciprocalCentLiteral': + case 'NotANumberLiteral': + case 'InfinityLiteral': case 'FJS': case 'AspiringFJS': case 'AbsoluteFJS': @@ -1222,6 +1239,8 @@ export function literalFromJSON(object: any): IntervalLiteral | undefined { case 'NedjiLiteral': case 'CentLiteral': case 'ReciprocalCentLiteral': + case 'NotANumberLiteral': + case 'InfinityLiteral': case 'FJS': case 'AspiringFJS': case 'AbsoluteFJS': diff --git a/src/grammars/sonic-weave.pegjs b/src/grammars/sonic-weave.pegjs index da1ed098..aabc0192 100644 --- a/src/grammars/sonic-weave.pegjs +++ b/src/grammars/sonic-weave.pegjs @@ -31,6 +31,7 @@ 'if', 'import', 'in', + 'inf', 'labs', 'lest', 'let', @@ -42,6 +43,7 @@ 'modc', 'module', 'MOS', + 'nan', 'niente', 'not', 'of', @@ -158,6 +160,7 @@ FromToken = @'from' !IdentifierPart IfToken = @'if' !IdentifierPart ImportToken = @'import' !IdentifierPart InToken = @'in' !IdentifierPart +InfinityToken = @'inf' !IdentifierPart LogAbsToken = @'labs' !IdentifierPart LestToken = @'lest' !IdentifierPart LetToken = @'let' !IdentifierPart @@ -168,6 +171,7 @@ MinToken = @'min' !IdentifierPart ModToken = @'mod' !IdentifierPart ModCeilingToken = @'modc' !IdentifierPart ModuleToken = @'module' !IdentifierPart +NotANumberToken = @'nan' !IdentifierPart NoneToken = @'niente' !IdentifierPart NotToken = @'not' !IdentifierPart OfToken = @'of' !IdentifierPart @@ -995,6 +999,8 @@ Primary / NoneLiteral / TrueLiteral / FalseLiteral + / NotANumberLiteral + / InfinityLiteral / NedjiLiteral / StepLiteral / ScalarMultiple @@ -1247,6 +1253,12 @@ TrueLiteral FalseLiteral = FalseToken { return { type: 'FalseLiteral' }; } +NotANumberLiteral + = NotANumberToken { return { type: 'NotANumberLiteral' }; } + +InfinityLiteral + = InfinityToken { return { type: 'InfinityLiteral' }; } + ColorLiteral = value: (@RGB8 / @RGB4) { return { diff --git a/src/monzo.ts b/src/monzo.ts index cf071867..d9154aa7 100644 --- a/src/monzo.ts +++ b/src/monzo.ts @@ -238,11 +238,11 @@ export class TimeReal { value.type === 'TimeReal' ) { let v: number | string = value.value; - if (v === 'NaN') { + if (v === 'nan') { v = NaN; - } else if (v === 'Infinity') { + } else if (v === 'inf') { v = Infinity; - } else if (v === '-Infinity') { + } else if (v === '-inf') { v = -Infinity; } return new TimeReal(value.timeExponent, v as number); @@ -258,11 +258,11 @@ export class TimeReal { let value: number | string = this.value; // JSON sure is a standard if (isNaN(value)) { - value = 'NaN'; + value = 'nan'; } else if (value === Infinity) { - value = 'Infinity'; + value = 'inf'; } else if (value === -Infinity) { - value = '-Infinity'; + value = 'inf'; } return { @@ -851,13 +851,13 @@ export class TimeReal { if (isNaN(this.value)) { switch (domain) { case 'linear': - return 'NaN'; + return 'nan'; case 'logarithmic': - return 'logarithmic(NaN)'; + return 'logarithmic(nan)'; } } if (!isFinite(this.value)) { - let value = this.value < 0 ? '-Infinity' : 'Infinity'; + let value = this.value < 0 ? '-inf' : 'inf'; if (this.timeExponent) { value = `${value} * 1${this.formatTimeExponent()}`; } diff --git a/src/parser/__tests__/expression.spec.ts b/src/parser/__tests__/expression.spec.ts index cda0d7c5..11d21237 100644 --- a/src/parser/__tests__/expression.spec.ts +++ b/src/parser/__tests__/expression.spec.ts @@ -1218,15 +1218,15 @@ describe('SonicWeave expression evaluator', () => { }); it('has not-a-number', () => { - const nan = evaluate('NaN') as Interval; + const nan = evaluate('nan') as Interval; expect(nan.valueOf()).toBeNaN(); - expect(nan.toString()).toBe('NaN'); + expect(nan.toString()).toBe('nan'); }); it('has negative infinity', () => { - const inf = evaluate('-Infinity') as Interval; + const inf = evaluate('-inf') as Interval; expect(inf.valueOf()).toBe(-Infinity); - expect(inf.toString()).toBe('-Infinity'); + expect(inf.toString()).toBe('-inf'); }); it('has root half', () => { @@ -1250,7 +1250,7 @@ describe('SonicWeave expression evaluator', () => { it('produces nan from asin', () => { const wishIwasAcomplexNumber = evaluate('asin(2)') as Interval; - expect(wishIwasAcomplexNumber.toString()).toBe('NaN'); + expect(wishIwasAcomplexNumber.toString()).toBe('nan'); }); it('has domain-crossing acos', () => { diff --git a/src/parser/__tests__/vector-broadcasting.spec.ts b/src/parser/__tests__/vector-broadcasting.spec.ts index 9113de95..80ae41c6 100644 --- a/src/parser/__tests__/vector-broadcasting.spec.ts +++ b/src/parser/__tests__/vector-broadcasting.spec.ts @@ -390,7 +390,7 @@ describe('SonicWeave vector broadcasting', () => { const polarity = sw1D`[-1/2, PI] sign`; expect(polarity).toEqual([-1, 1]); - const circleOfNaN = sw2D`sign([[0, LN2], [-1, NaN]])`; + const circleOfNaN = sw2D`sign([[0, LN2], [-1, nan]])`; expect(circleOfNaN).toEqual([ [0, 1], [-1, NaN], diff --git a/src/parser/expression.ts b/src/parser/expression.ts index bb979279..3d03a828 100644 --- a/src/parser/expression.ts +++ b/src/parser/expression.ts @@ -372,6 +372,10 @@ export class ExpressionVisitor { return node.value; case 'NoneLiteral': return undefined; + case 'NotANumberLiteral': + return new Interval(new TimeReal(0, NaN), 'linear', 0, node); + case 'InfinityLiteral': + return new Interval(new TimeReal(0, Infinity), 'linear', 0, node); case 'DownExpression': return this.visitDownExpression(node); case 'StepLiteral': diff --git a/src/stdlib/builtin.ts b/src/stdlib/builtin.ts index 3d2d39f7..7480d9d8 100644 --- a/src/stdlib/builtin.ts +++ b/src/stdlib/builtin.ts @@ -87,8 +87,6 @@ const PI = new Interval(TimeReal.fromValue(Math.PI), 'linear'); const SQRT1_2 = new Interval(TimeMonzo.fromEqualTemperament('-1/2'), 'linear'); const SQRT2 = new Interval(TimeMonzo.fromEqualTemperament('1/2'), 'linear'); const TAU = new Interval(TimeReal.fromValue(2 * Math.PI), 'linear'); -const NAN = new Interval(TimeReal.fromValue(NaN), 'linear'); -const INFINITY = new Interval(TimeReal.fromValue(Infinity), 'linear'); // == Real-valued Math wrappers == const MATH_WRAPPERS: Record = {}; @@ -1221,7 +1219,7 @@ function flatten(array: SonicWeaveValue, depth?: Interval) { return [array]; } flatten.__doc__ = - 'Returns a new array with all sub-array elements concatenated into it recursively up to the specified depth (default `Infinity`).'; + 'Returns a new array with all sub-array elements concatenated into it recursively up to the specified depth (default `inf`).'; flatten.__node__ = builtinNode(flatten); function clear(this: ExpressionVisitor, scale?: Interval[]) { @@ -2487,8 +2485,6 @@ export const BUILTIN_CONTEXT: Record = { SQRT1_2, SQRT2, TAU, - NaN: NAN, - Infinity: INFINITY, VERSION, // First-party wrappers numComponents, diff --git a/src/stdlib/prelude.ts b/src/stdlib/prelude.ts index aff13e0f..0714d1c8 100644 --- a/src/stdlib/prelude.ts +++ b/src/stdlib/prelude.ts @@ -179,7 +179,7 @@ riff denominator(x) { } riff sign(x) { "Calculate the sign of x."; - return 1 where x > 0 else -1 where x < 0 else 0 where x == 0 else NaN; + return 1 where x > 0 else -1 where x < 0 else 0 where x == 0 else nan; } riff oddLimitOf(x, equave = 2) { "Calculate the odd limit of x. Here 'odd' means not divisible by the equave.";