Skip to content

Commit

Permalink
Make tagged template argument handling less janky
Browse files Browse the repository at this point in the history
Add workarounds to JavaScript's poor handling of backslashes inside tagged template literals.
  • Loading branch information
frostburn committed Apr 11, 2024
1 parent 2086c56 commit 5694547
Show file tree
Hide file tree
Showing 7 changed files with 155 additions and 31 deletions.
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ You can read more about domains and echelons [below](#interval-type-system).
| Integer | `2`, `5` | Linear | Relative | Same as `2/1` or `5/1`. |
| Decimal | `1,2`, `1.4e0` | Linear | Relative | Decimal commas only work in isolation. |
| Fraction | `4/3`, `10/7` | Linear | Relative | |
| 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-EDO | `1\5`, `7\12`, `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<p/q>` means `n` steps of `m` equal divisions of the ratio `p/q`. |
| Step | `7\`, `13\` | Logarithmic | Relative | Correspond to edo-steps after tempering is applied. |
| Step | `7\`, `13\`, `13°` | Logarithmic | Relative | Correspond to edo-steps after 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) |
Expand All @@ -37,6 +37,8 @@ You can read more about domains and echelons [below](#interval-type-system).
| Val | `<12, 19, 28]` | Cologarithmic | Relative | Used to temper scales. |
| Warts | `12@`, `[email protected]/5` | Cologarithmic | Relative | [Shorthand](https://en.xen.wiki/w/Val#Shorthand_notation) for vals. |

The `n°m` (n degrees of m edo) alternative exists to avoid using the backslash inside tagged template literals.

#### Numeric separators
It is possible to separate numbers into groups using underscores for readability e.g. `1_000_000` is one million as an integer and `123_201/123_200` is the [chalmerisia](https://en.xen.wiki/w/Chalmersia) as a fraction.

Expand Down Expand Up @@ -115,6 +117,8 @@ SonicWeave comes with some operators.

Down-shimmer sometimes requires curly brackets due to `v` colliding with the Latin alphabet.

Drop `\` can be spelled `drop` to avoid using the backslash inside template literals. Lift `/` may be spelled `lift` for minor grammatical reasons.

Increment/decrement assumes that you've declared `let i = 2` originally.
### Coalescing
| Name | Example | Result |
Expand Down Expand Up @@ -186,6 +190,7 @@ Outer product a.k.a. tensoring expands all possible products in two arrays into
| Logarithm (in base of) | `9 /_ 3` | `2` | `M23 % P12` | `2` |
| Round (to power of) | `5 by 2` | `4` | `M17^5 to P8` | `P15` |
| N of EDO | `(5+2)\12` | `7\12` | _N/A_ | |
| N of EDO | `(5+2)°12` | `7\12` | _N/A_ | |
| NEDJI Projection | `sqrt(2) ed 3` | `1\2<3>` | `2\3 ed S3` | `2\3<9/8>` |
| Val product | `12@ · 3/2` | `7` | `<12 19] dot P5` | `7` |

Expand Down
7 changes: 7 additions & 0 deletions src/__tests__/parser/expression.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1757,4 +1757,11 @@ describe('SonicWeave expression evaluator', () => {
expect(tooLong.isAbsolute()).toBe(true);
expect(tooLong.valueOf()).toBeCloseTo(3 * 1024 ** 8);
});

it('has a "lift" operator because the template tag requires a "drop" operator, but I guess it is useful for enumerated chords where mirroring would take precedence...', () => {
const rootLift = evaluateExpression('lift 4:5:6', false) as Interval[];
expect(rootLift).toHaveLength(2);
expect(rootLift[0].valueOf()).toBe(1.2463950666682366);
expect(rootLift[1].valueOf()).toBe(1.4956740800018837);
});
});
63 changes: 61 additions & 2 deletions src/__tests__/parser/tag.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {describe, it, expect} from 'vitest';
import {sw} from '../../parser';
import {sw, swr} from '../../parser';
import {Interval} from '../../interval';
import {Fraction} from 'xen-dev-utils';

describe('SonicWeave template tag', () => {
it('evaluates a single number', () => {
Expand All @@ -15,7 +16,7 @@ describe('SonicWeave template tag', () => {
});

it('evaluates a newline', () => {
const value = sw`"\n"` as string;
const value = sw`"\\n"` as string;
expect(value).toBe('\n');
});

Expand All @@ -32,4 +33,62 @@ describe('SonicWeave template tag', () => {
const value = sw`${Math.PI}` as Interval;
expect(value.valueOf()).toBeCloseTo(Math.PI);
});

it('evaluates the backslash of two number passed in', () => {
const value = sw`${12}\\${12}` as Interval;
expect(value.valueOf()).toBe(2);
});

it('evaluates drop of a fraction passed in', () => {
const fraction = new Fraction('3/2');
const value = sw`\\${fraction}` as Interval;
expect(value.valueOf()).toBe(1.4956740800018837);
});
});

describe('SonicWeave raw template tag', () => {
it('evaluates a single number', () => {
const value = swr`3` as Interval;
expect(value.value.toBigInteger()).toBe(3n);
});

it('evaluates a single number passed in', () => {
const n = Math.floor(Math.random() * 100);
const value = swr`${n}` as Interval;
expect(value.toInteger()).toBe(n);
});

it('evaluates a newline', () => {
const value = swr`"\n"` as string;
expect(value).toBe('\n');
});

it('evaluates an array of numbers passed in', () => {
const nums: number[] = [];
for (let i = 0; i < 10 * Math.random(); ++i) {
nums.push(Math.floor(Math.random() * 100));
}
const value = swr`${nums}` as Interval[];
expect(value.map(i => i.toInteger())).toEqual(nums);
});

it('evaluates PI passed in', () => {
const value = swr`${Math.PI}` as Interval;
expect(value.valueOf()).toBeCloseTo(Math.PI);
});

it('evaluates the backslash of two number passed in', () => {
// JS template grammar is broken:
// swr`${12}\${12}` escapes the dollar sign (Not even String.raw survives this corner case.)
// swr`${12}\\${12}` is equivalent to 12 \ (\12)
const value = swr`${12}°${12}` as Interval;
expect(value.valueOf()).toBe(2);
});

it('evaluates drop of a fraction passed in', () => {
const fraction = new Fraction('3/2');
// See above why swr`\${fraction}` just won't do...
const value = swr`drop${fraction}` as Interval;
expect(value.valueOf()).toBe(1.4956740800018837);
});
});
25 changes: 24 additions & 1 deletion src/ast.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
import {IntervalLiteral, literalToString} from './expression';

export type UnaryOperator =
| '+'
| '-'
| '%'
| '÷'
| 'not'
| '^'
| '/'
| 'lift'
| '\\'
| 'drop'
| '++'
| '--';

export type BinaryOperator =
| '??'
| 'or'
Expand Down Expand Up @@ -36,6 +50,7 @@ export type BinaryOperator =
| '%'
| '÷'
| '\\'
| '°'
| 'mod'
| 'modc'
| 'rd'
Expand Down Expand Up @@ -232,7 +247,7 @@ export type ArraySlice = {

export type UnaryExpression = {
type: 'UnaryExpression';
operator: '+' | '-' | '%' | '÷' | 'not' | '^' | '/' | '\\' | '++' | '--';
operator: UnaryOperator;
operand: Expression;
prefix: boolean;
uniform: boolean;
Expand Down Expand Up @@ -273,6 +288,11 @@ export type Identifier = {
id: string;
};

export type TemplateArgument = {
type: 'TemplateArgument';
index: number;
};

export type Argument = {
type: 'Argument';
expression: Expression;
Expand Down Expand Up @@ -365,6 +385,7 @@ export type Expression =
| FalseLiteral
| ColorLiteral
| Identifier
| TemplateArgument
| EnumeratedChord
| Range
| ArrayComprehension
Expand Down Expand Up @@ -409,6 +430,8 @@ export function expressionToString(node: Expression) {
return 'niente';
case 'Identifier':
return node.id;
case 'TemplateArgument':
return ${node.index}`;
case 'StringLiteral':
return JSON.stringify(node.value);
}
Expand Down
3 changes: 3 additions & 0 deletions src/context.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {SonicWeaveValue} from './builtin';
import {Val, type Interval} from './interval';
import {TimeMonzo} from './monzo';
import {ZERO} from './utils';
Expand All @@ -11,6 +12,7 @@ export class RootContext {
gas: number;
fragiles: (Interval | Val)[];
trackingIndex: number;
templateArguments: SonicWeaveValue[];

constructor(gas?: number) {
this.title = '';
Expand All @@ -20,6 +22,7 @@ export class RootContext {
this.gas = gas ?? Infinity;
this.fragiles = [];
this.trackingIndex = 0;
this.templateArguments = [];
}

get C4() {
Expand Down
52 changes: 33 additions & 19 deletions src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1177,6 +1177,8 @@ export class ExpressionVisitor {
return this.visitArrayComprehension(node);
case 'SquareSuperparticular':
return this.visitSquareSuperparticular(node);
case 'TemplateArgument':
return this.rootContext.templateArguments[node.index];
}
node satisfies never;
}
Expand Down Expand Up @@ -1650,15 +1652,16 @@ export class ExpressionVisitor {
`${node.operator} can only operate on intervals and vals.`
);
}
const operator = node.operator;
if (node.uniform) {
let value: TimeMonzo;
let newNode = operand.node;
switch (node.operator) {
switch (operator) {
case '-':
value = operand.value.neg();
break;
case '%':
case '\u00F7':
case '÷':
value = operand.value.inverse();
newNode = uniformInvertNode(newNode);
break;
Expand All @@ -1671,33 +1674,38 @@ export class ExpressionVisitor {
}
return new Interval(value, operand.domain, newNode, operand);
}
switch (node.operator) {
switch (operator) {
case '+':
return operand;
case '-':
return operand.neg();
case '%':
case '\u00F7':
case '÷':
return operand.inverse();
case '^':
return operand.up(this.rootContext);
case '/':
case 'lift':
return operand.lift(this.rootContext);
case '\\':
case 'drop':
return operand.drop(this.rootContext);
}
if (!(operand instanceof Interval)) {
throw new Error('Unsupported unary operation.');
}
let newValue: Interval | undefined;
if (node.operator === '++') {
newValue = operand.add(linearOne());
} else if (node.operator === '--') {
newValue = operand.sub(linearOne());
} else {
if (operator === 'not') {
// The runtime shouldn't let you get here.
throw new Error('Unexpected unary operation.');
}
operator satisfies '++' | '--';

let newValue: Interval;
if (operator === '++') {
newValue = operand.add(linearOne());
} else {
newValue = operand.sub(linearOne());
}
if (node.operand.type !== 'Identifier') {
throw new Error('Cannot increment/decrement a value.');
}
Expand Down Expand Up @@ -1838,6 +1846,7 @@ export class ExpressionVisitor {
value = left.value.project(right.value);
break;
case '\\':
case '°':
throw new Error('Preference not supported with backslahes');
default:
throw new Error(
Expand Down Expand Up @@ -1896,6 +1905,7 @@ export class ExpressionVisitor {
case '/_':
return left.log(right);
case '\\':
case '°':
return left.backslash(right);
case 'mod':
return left.mmod(right);
Expand Down Expand Up @@ -2569,22 +2579,20 @@ function convert(value: any): SonicWeaveValue {

export function createTag(
includePrelude = true,
extraBuiltins?: Record<string, SonicWeaveValue>
extraBuiltins?: Record<string, SonicWeaveValue>,
escapeStrings = false
) {
function tag(strings: TemplateStringsArray, ...args: any[]) {
const fragments = escapeStrings ? strings : strings.raw;
const globalVisitor = getSourceVisitor(includePrelude, extraBuiltins);
const visitor = new StatementVisitor(
globalVisitor.rootContext,
globalVisitor
);
let source = strings.raw[0];
let source = fragments[0];
for (let i = 0; i < args.length; ++i) {
const arg = args[i];
const name = `__tagArg${i}_${Math.floor(46656 * Math.random()).toString(
36
)}`;
visitor.immutables.set(name, convert(arg));
source += name + strings.raw[i + 1];
visitor.rootContext.templateArguments[i] = convert(args[i]);
source += ${i}` + fragments[i + 1];
}
const program = parseAST(source);
for (const statement of program.body.slice(0, -1)) {
Expand All @@ -2603,7 +2611,13 @@ export function createTag(
return tag;
}

export const sw = createTag();
export const swr = createTag();
Object.defineProperty(swr, 'name', {
value: 'swr',
enumerable: false,
});

export const sw = createTag(true, undefined, true);
Object.defineProperty(sw, 'name', {
value: 'sw',
enumerable: false,
Expand Down
Loading

0 comments on commit 5694547

Please sign in to comment.