diff --git a/documentation/advanced-dsl.md b/documentation/advanced-dsl.md index 3ff22b8e..266881de 100644 --- a/documentation/advanced-dsl.md +++ b/documentation/advanced-dsl.md @@ -27,6 +27,9 @@ Blocks start with a curly bracket `{`, have their own instance of a current scal ### Parent scale The current scale of the parent block can be accessed using `$$`. +### Popped parent scale +A copy of the current scale of the parent block can be obtained using `££` (or the ASCII variant `pop$$`) while simultaneously clearing the original. + ## Defer Defer is used to execute a statement while exiting the current block. diff --git a/documentation/intermediate-dsl.md b/documentation/intermediate-dsl.md index 5b72a50e..8c4096cc 100644 --- a/documentation/intermediate-dsl.md +++ b/documentation/intermediate-dsl.md @@ -99,6 +99,17 @@ Inspired by [NumPy](https://numpy.org/), most functions that accept intervals ma The previous example is equivalent to `$ = simplify([10/8, 12/8, 16/8])`. +### Popped scale +Using the current scale `$` as a variable often leads to duplicated data. There's a magic variable `£` (or the ASCII variant `pop$`) that obtains a copy of the current scale and clears the existing scale when used. Very handy with [vector broadcasting](#vector-broadcasting). + +```ocaml +10/9 +4/3 +16/9 +£ * 9/8 +``` +Results in `$ = [5/4, 3/2, 2]`. + ### Implicit tempering In addition to musical intervals SonicWeave features something known as *vals* which are mainly used for converting scales in just intonation to equally tempered scales. diff --git a/src/ast.ts b/src/ast.ts index 23bbdcd8..f6c2e3f9 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -347,6 +347,11 @@ export type Identifier = { id: string; }; +export type PopScale = { + type: 'PopScale'; + parent: boolean; +}; + export type TemplateArgument = { type: 'TemplateArgument'; index: number; @@ -443,6 +448,7 @@ export type Expression = | FalseLiteral | ColorLiteral | Identifier + | PopScale | TemplateArgument | EnumeratedChord | Range @@ -555,6 +561,8 @@ export function expressionToString(node: Expression) { return 'niente'; case 'Identifier': return node.id; + case 'PopScale': + return node.parent ? '££' : '£'; case 'TemplateArgument': return `¥${node.index}`; case 'StringLiteral': diff --git a/src/grammars/sonic-weave.pegjs b/src/grammars/sonic-weave.pegjs index 51527d36..23367bca 100644 --- a/src/grammars/sonic-weave.pegjs +++ b/src/grammars/sonic-weave.pegjs @@ -48,6 +48,8 @@ 'not', 'of', 'or', + 'pop$', + 'pop$$', 'rd', 'rdc', 'return', @@ -183,6 +185,8 @@ NoneToken = @'niente' !IdentifierPart NotToken = @'not' !IdentifierPart OfToken = @'of' !IdentifierPart OrToken = @'or' !IdentifierPart +PopScaleToken = @'pop$' !IdentifierPart +PopParentToken = @'pop$$' !IdentifierPart ReduceToken = @'rd' !IdentifierPart ReduceCeilingToken = @'rdc' !IdentifierPart ReturnToken = @'return' !IdentifierPart @@ -993,6 +997,7 @@ Quantity / SparseOffsetVal / ReciprocalCentLiteral / ReciprocalLogarithmicHertzLiteral + / PopScale / MonzoLiteral / ValLiteral / DownExpression @@ -1251,6 +1256,20 @@ ReciprocalCentLiteral ReciprocalLogarithmicHertzLiteral = '¶' { return { type: 'ReciprocalLogarithmicHertzLiteral' }; } +PopScale + = ('££' / PopParentToken) { + return { + type: 'PopScale', + parent: true, + }; + } + / ('£' / PopScaleToken) { + return { + type: 'PopScale', + parent: false, + }; + } + NoneLiteral = NoneToken { return { type: 'NoneLiteral' }; } diff --git a/src/parser/__tests__/source.spec.ts b/src/parser/__tests__/source.spec.ts index 41ced8b7..6893efe5 100644 --- a/src/parser/__tests__/source.spec.ts +++ b/src/parser/__tests__/source.spec.ts @@ -1844,4 +1844,41 @@ describe('SonicWeave parser', () => { visitor.visit(ast.body[0]); expect(() => visitor.visit(ast.body[1])).toThrow(); }); + + it('can pop the current scale as a magic variable', () => { + const scale = expand(` + 5/4 + 3/2 + 2/1 + sorted(%£ rdc pop$[-1]) + `); + expect(scale).toEqual(['4/3', '8/5', '2']); + }); + + it('can pop the parent scale as a magic variable', () => { + const scale = expand(String.raw`{ + fn poppyStack(array = ££) { + "Cumulatively stack the current/given intervals on top of each other."; + array; + let i = 0r; + const len = real(length($)); + while (++i < len) + $[i] ~*= $[i-1r]; + } + 2 \ 12 + 2 \ 12 + 1 \ 12 + poppyStack(); + $[-1] + poppyStack([2, 2, 2, 1] \ 12); + }`); + expect(scale).toEqual([ + '2\\12', + '4\\12', + '5\\12', + '7\\12', + '9\\12', + '11\\12', + '12\\12', + ]); + }); }); diff --git a/src/parser/expression.ts b/src/parser/expression.ts index 3d03a828..30af9bc8 100644 --- a/src/parser/expression.ts +++ b/src/parser/expression.ts @@ -281,6 +281,26 @@ export class ExpressionVisitor { this.parent.currentScale = scale; } + popScale(parent: boolean): Interval[] { + if (parent) { + if (!this.mutables.has('££')) { + const scale = this.get('$$') as Interval[]; + if (!Array.isArray(scale)) { + throw new Error('Context corruption detected.'); + } + this.mutables.set('££', [...scale]); + scale.length = 0; + } + return this.mutables.get('££') as Interval[]; + } + if (!this.mutables.has('£')) { + const scale = this.currentScale; + this.mutables.set('£', [...scale]); + scale.length = 0; + } + return this.mutables.get('£') as Interval[]; + } + spendGas(amount?: number) { this.parent.spendGas(amount); } @@ -358,6 +378,8 @@ export class ExpressionVisitor { return new Color(node.value); case 'Identifier': return this.visitIdentifier(node); + case 'PopScale': + return this.popScale(node.parent); case 'EnumeratedChord': return this.visitEnumeratedChord(node); case 'Range': diff --git a/src/stdlib/prelude.ts b/src/stdlib/prelude.ts index 68e3cdea..386d3988 100644 --- a/src/stdlib/prelude.ts +++ b/src/stdlib/prelude.ts @@ -478,9 +478,9 @@ riff mos(numberOfLargeSteps, numberOfSmallSteps, sizeOfLargeStep = 2, sizeOfSmal mosSubset(numberOfLargeSteps, numberOfSmallSteps, sizeOfLargeStep, sizeOfSmallStep, up, down); const divisions = abs $[-1]; if (equave == 2) - step => step \\ divisions; + £ \\ divisions; else - step => step \\ divisions ed equave; + £ \\ divisions ed equave; } riff rank2(generator, up, down = 0, period = 2, numPeriods = 1, generatorSizeHint = niente, periodSizeHint = niente) { @@ -529,7 +529,7 @@ riff wellTemperament(commaFractions, comma = 81/80, down = 0, generator = 3/2, p 1; generator ~* comma ~^ commaFractions; stack(); - i => i ~/ $[down] rdc period; + £ ~/ £[down] rdc period; sort(); period vor pop(); } @@ -562,7 +562,7 @@ riff parallelotope(basis, ups = niente, downs = niente, equave = 2, basisSizeHin if (basisSizeHints == niente and equaveSizeHint == niente) { $ = $$; - i => i ~rdc equave; + £ ~rdc equave; sort(); return $; } @@ -710,7 +710,7 @@ riff oddLimit(limit, equave = 2) { } const odds = popAll(); [n % d for n of odds for d of odds if gcd(n, d) == 1]; - i => i rdc equave; + £ rdc equave; sort(); } @@ -775,7 +775,7 @@ riff revpose(scale = $$) { "Change the sounding direction. Converts a descending scale to an ascending one." $ = scale; const equave = pop(); - i => i ~% equave; + £ ~% equave; reverse(); %equave; return; @@ -791,7 +791,7 @@ riff retrovert(scale = $$) { "Retrovert the current/given scale (negative harmony i.e reflect and transpose)."; $ = scale; const equave = pop(); - i => equave %~ i; + equave %~ £; reverse(); equave; return; @@ -831,7 +831,7 @@ riff rotate(onto = 1, scale = $$) { const equave = $[-1]; while (--onto) equave *~ shift(); const root = shift(); - i => i ~% root; + £ ~% root; equave colorOf(root) labelOf(root); return; } @@ -893,7 +893,7 @@ riff ground(scale = $$) { "Use the first interval in the current/given scale as the implicit unison."; $ = scale; const root = shift(); - i => i ~% root; + £ ~% root; return; } @@ -908,7 +908,7 @@ riff elevate(scale = $$) { $ = scale; unshift(sanitize($[-1]~^0)); const root = sanitize(%~gcd()); - i => i ~* root; + £ ~* root; return; } @@ -940,7 +940,7 @@ riff subset(degrees, scale = $$) { riff toHarmonics(fundamental, scale = $$) { "Quantize the current/given scale to harmonics of the given fundamental."; $ = scale; - i => i to~ %~fundamental colorOf(i) labelOf(i); + £ to~ %~fundamental colorOf(£) labelOf(£); return; } @@ -953,7 +953,7 @@ riff harmonicsOf(fundamental, scale = $$) { riff toSubharmonics(overtone, scale = $$) { "Quantize the current/given scale to subharmonics of the given overtone."; $ = scale; - i => %~(%~i to~ %~overtone) colorOf(i) labelOf(i); + %~(%~£ to~ %~overtone) colorOf(£) labelOf(£); return; } @@ -969,7 +969,7 @@ riff equalize(divisions, scale = $$) { let step = 1 \\ divisions; if ($[-1] <> 2) step ed= $[-1]; - i => i by~ step colorOf(i) labelOf(i); + £ by~ step colorOf(£) labelOf(£); return; } @@ -1015,7 +1015,7 @@ riff withOffset(offsets, overflow = 'drop', scale = $$) { riff stretch(amount, scale = $$) { "Stretch the current/given scale by the given amount. A value of \`1\` corresponds to no change."; $ = scale; - i => i ~^ amount; + £ ~^ amount; return; }