diff --git a/documentation/tag.md b/documentation/tag.md index f2dd402f..7efcfc5f 100644 --- a/documentation/tag.md +++ b/documentation/tag.md @@ -6,10 +6,53 @@ This document describes the `sw` and `swr` tags used for writing SonicWeave insi import {sw} from 'sonic-weave'; const myFifth = sw`3/2`; -console.log(myFifth.totalCents()) // 701.9550008653875 +console.log(myFifth.totalCents()); // 701.9550008653875 const myOctave = sw`${2}`; -console.log(myOctave.totalCents()) // 1200 +console.log(myOctave.totalCents()); // 1200 ``` -TODO: Actual description +## Raison d'être +Constructing intervals using the [JavaScript API](https://github.com/xenharmonic-devs/sonic-weave/blob/main/documentation/package.md) is somewhat tedious so you can use [SonicWeave DSL](https://github.com/xenharmonic-devs/sonic-weave/blob/main/documentation/dsl.md) instead when writing JS scripts for generating and analysing microtonal scales. + +## The sw tag +The `sw` tag evaluates escapes such as `\n` for a newline inside the tag: +```ts +import {sw} from 'sonic-weave'; + +const ratio = sw`const fif = 3/2\nconst third = 6/5\nthird * fif`; +ratio.toFraction(); // new Fraction(9, 5) +ratio.toFraction().toFraction(); // "9/5" +``` + +This means that backslashes must be entered doubled (`\\`). Luckily the binary operator `sof` and the unary operator `drop` exist to make the meaning of your code more clear at a glance. +```ts +import {sw} from 'sonic-weave'; + +const minorSeventh = sw` + const fif = 7 sof 12; + const third = 3 sof 12; + third + fif; +`; +minorSeventh.totalCents(); // 1000 +``` + +## The swr tag +The `swr` tag uses `String.raw` semantics which makes backslash fractions a.k.a. NEDO easier to enter: +```ts +import {swr} from 'sonic-weave'; + +const minorSeventh = swr` + const fif = 7\12; + const third = 3\12; + third + fif; +`; +minorSeventh.totalCents(); // 1000 +``` + +## The sw$ and sw$r tags +The `sw$` tag and it's raw `sw$r` counterpart produce arrays of intervals. +```ts +const tet5 = sw$`tet(5)`; +console.log(tet5.map(interval => interval.totalCents())); // [240, 480, 720, 960, 1200] +``` diff --git a/src/parser/__tests__/tag.spec.ts b/src/parser/__tests__/tag.spec.ts index 484563e4..8924bd9d 100644 --- a/src/parser/__tests__/tag.spec.ts +++ b/src/parser/__tests__/tag.spec.ts @@ -1,5 +1,5 @@ import {describe, it, expect} from 'vitest'; -import {sw, swr} from '../../parser'; +import {sw, sw$, sw$r, swr} from '../../parser'; import {Color, Interval, Val} from '../../interval'; import {Fraction} from 'xen-dev-utils'; @@ -164,3 +164,15 @@ describe('SonicWeave raw template tag', () => { expect(interval.totalCents()).toBe(700); }); }); + +describe('SonicWeave scale template tags', () => { + it('has an escaping variant', () => { + const scale = sw$`rank2(7\\12, 4)`; + expect(scale.map(i => i.totalCents())).toEqual([200, 400, 700, 900, 1200]); + }); + + it('has a raw variant', () => { + const scale = sw$r`rank2(7\12, 4)`; + expect(scale.map(i => i.totalCents())).toEqual([200, 400, 700, 900, 1200]); + }); +}); diff --git a/src/parser/parser.ts b/src/parser/parser.ts index bf707a84..ddb98dce 100644 --- a/src/parser/parser.ts +++ b/src/parser/parser.ts @@ -210,12 +210,14 @@ function convert(value: any): SonicWeaveValue { /** * Create a tag for [templates literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#tagged_templates) to evaluate SonicWeave programs inside JavaScript code. + * @param expression Whether or not this tag is intended for evaluation single expressions or full scales producing arrays of {@link Interval} instances. * @param includePrelude Whether or not to include the extended standard library. Passing in `false` results in a faster start-up time. * @param extraBuiltins Custom builtins callable inside the SonicWeave program. * @param escapeStrings If `true` all escape sequences are evaluated before interpreting the literal as a SonicWeave program. * @returns A tag that can be attached to template literals in order to evaluate them. */ export function createTag( + expression = true, includePrelude = true, extraBuiltins?: Record, escapeStrings = false @@ -232,29 +234,34 @@ export function createTag( source += `¥${i}` + fragments[i + 1]; } const program = parseAST(source); - for (const statement of program.body.slice(0, -1)) { - const interrupt = visitor.visit(statement); - if (interrupt) { - throw new Error(`Illegal ${interrupt.type}.`); + if (expression) { + for (const statement of program.body.slice(0, -1)) { + const interrupt = visitor.visit(statement); + if (interrupt) { + throw new Error(`Illegal ${interrupt.type}.`); + } } + if (visitor.deferred.length) { + throw new Error( + 'Deferred actions not allowed when evaluating tagged templates.' + ); + } + const finalStatement = program.body[program.body.length - 1]; + if (finalStatement.type !== 'ExpressionStatement') { + throw new Error(`Expected expression. Got ${finalStatement.type}.`); + } + const subVisitor = visitor.createExpressionVisitor(); + return subVisitor.visit(finalStatement.expression); + } else { + visitor.executeProgram(program); + return visitor.currentScale; } - if (visitor.deferred.length) { - throw new Error( - 'Deferred actions not allowed when evaluating tagged templates.' - ); - } - const finalStatement = program.body[program.body.length - 1]; - if (finalStatement.type !== 'ExpressionStatement') { - throw new Error(`Expected expression. Got ${finalStatement.type}.`); - } - const subVisitor = visitor.createExpressionVisitor(); - return subVisitor.visit(finalStatement.expression); } return tag; } /** - * Tag for evaluating [templates literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#tagged_templates) as SonicWeave programs. + * Tag for evaluating [templates literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#tagged_templates) as SonicWeave expressions. * Has raw (unescaped) semantics. * * Example: @@ -270,7 +277,26 @@ Object.defineProperty(swr, 'name', { }); /** - * Tag for evaluating [templates literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#tagged_templates) as SonicWeave programs. + * Tag for evaluating [templates literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#tagged_templates) as SonicWeave scales. + * Has raw (unescaped) semantics. + * + * Example: + * ```ts + * const pentatonic = sw$r`rank2(7\12, 4)`; + * console.log(pentatonic.map(interval => interval.totalCents())); // [200, 400, 700, 900, 1200] + * ``` + */ +export const sw$r = createTag(false) as ( + strings: TemplateStringsArray, + ...args: any[] +) => Interval[]; +Object.defineProperty(swr, 'name', { + value: 'sw$r', + enumerable: false, +}); + +/** + * Tag for evaluating [templates literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#tagged_templates) as SonicWeave expressions. * Evaluates escapes before interpreting the program so e.g. a double backslash means only a single backslash within the program. * * Example: @@ -279,8 +305,27 @@ Object.defineProperty(swr, 'name', { * console.log(interval.totalCents()); // 700 * ``` */ -export const sw = createTag(true, undefined, true); +export const sw = createTag(true, true, undefined, true); Object.defineProperty(sw, 'name', { value: 'sw', enumerable: false, }); + +/** + * Tag for evaluating [templates literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#tagged_templates) as SonicWeave scales. + * Evaluates escapes before interpreting the program so e.g. a double backslash means only a single backslash within the program. + * + * Example: + * ```ts + * const pentatonic = sw$`rank2(7\\12, 4)`; + * console.log(pentatonic.map(interval => interval.totalCents())); // [200, 400, 700, 900, 1200] + * ``` + */ +export const sw$ = createTag(false, true, undefined, true) as ( + strings: TemplateStringsArray, + ...args: any[] +) => Interval[]; +Object.defineProperty(swr, 'name', { + value: 'sw$', + enumerable: false, +});