Skip to content

Commit

Permalink
Make blocks legal expressions evaluating to arrays
Browse files Browse the repository at this point in the history
ref #351
  • Loading branch information
frostburn committed Jun 17, 2024
1 parent f465fb6 commit 4efd04c
Show file tree
Hide file tree
Showing 14 changed files with 124 additions and 53 deletions.
18 changes: 15 additions & 3 deletions documentation/advanced-dsl.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
This document describes programming in the SonicWeave domain-specific language.

## Record broadcasting
Records behave like arrays in that operations are broadcast over their values e.g. `{a: 1, b: 2, c:3} * 5` evaluates to
Records behave like arrays in that operations are broadcast over their values e.g. `#{a: 1, b: 2, c:3} * 5` evaluates to
```ocaml
{
#{
a: 1*5,
b: 2*5,
c: 3*5,
}
```
or `{a: 5, b: 10, c: 15}`.
or `#{a: 5, b: 10, c: 15}`.

## Tiers
* natural (`1`, `-3`, `7`, `P8`, etc.)
Expand Down Expand Up @@ -38,6 +38,18 @@ Deleting a non-existent entry throws an error unless the access was nullish i.e.
## Blocks
Blocks start with a curly bracket `{`, have their own instance of a current scale `$` and end with `}`. The current scale is unrolled onto the parent scale at the end of the block.

### Block expressions
Blocks are valid expressions and evaluate to arrays. They have the lowest precedence and usually need to be wrapped in parenthesis.
```ocaml
10 * ({
defer sort()
2
1
3
})
(* $ = [10, 20, 30] *)
```

### Parent scale
The current scale of the parent block can be accessed using `$$`.

Expand Down
10 changes: 5 additions & 5 deletions documentation/intermediate-dsl.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ When a record is encountered its values are sorted by size and the keys are used

```ocaml
4/3
{
#{
fif: 3/2,
octave: 2,
"my third": 5/4,
Expand Down Expand Up @@ -232,7 +232,7 @@ Values in SonicWeave fall into these categories
| Interval | `7/5` | There are many kinds of intervals with their own operator semantics. |
| Val | `12@` | Used to convert scales in just intonation to equal temperaments. |
| Array | `[5/4, P5, 9\9]` | Musical scales are represented using arrays of intervals. |
| Record | `{fif: 3/2, "p/e": 2}` | Associative data indexed by strings. |
| Record | `#{fif: 3/2, "p/e": 2}` | Associative data indexed by strings. |
| Function | `riff f(x){ x+1 }` | _Riff_ is a music term for a short repeated phrase. |

## Interval domains
Expand Down Expand Up @@ -796,12 +796,12 @@ In slice syntax the end points are optional e.g. `[1, 2, 3][..]` evaluates to `[
Excluding the end point can be handy to get the first n elements. `"Greetings!"[..<5]` evaluates to `"Greet"`.

## Records
Record literals are constructed using `key: value` pairs inside curly brackets e.g. `{fif: 3/2, "my octave": 2/1}`.
Record literals are constructed using `key: value` pairs inside hash curly brackets e.g. `#{fif: 3/2, "my octave": 2/1}`.

### Record access
Records are accessed with the same syntax as arrays but using string indices e.g. `{fif: 3/2}["fif"]` evaluates to `3/2`.
Records are accessed with the same syntax as arrays but using string indices e.g. `#{fif: 3/2}["fif"]` evaluates to `3/2`.

Nullish access is supported e.g. `{}~["nothing here"]` evaluates to `niente`.
Nullish access is supported e.g. `#{}~["nothing here"]` evaluates to `niente`.

## Metric prefixes
Frequency literals support [metric prefixes](https://en.wikipedia.org/wiki/Metric_prefix) e.g. `1.2 kHz` is the same as `1200 Hz`. [Binary prefixes](https://en.wikipedia.org/wiki/Binary_prefix) are also supported for no particular reason.
Expand Down
2 changes: 1 addition & 1 deletion documentation/technical.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Values in SonicWeave fall into these categories
| Interval | `7/5` | There are many kinds of intervals with their own operator semantics. |
| Val | `12@` | Used to convert scales in just intonation to equal temperaments. |
| Array | `[5/4, P5, 9\9]` | Musical scales are represented using arrays of intervals. |
| Record | `{fif: 3/2, "p/e": 2}` | Associative data indexed by strings. |
| Record | `#{fif: 3/2, "p/e": 2}` | Associative data indexed by strings. |
| Function | `riff plusOne(x) {x+1}` | _Riff_ is a music term for a short repeated phrase. |

Array and record types are recursive i.e. arrays may contain other arrays or records and the values of records can be anything.
Expand Down
6 changes: 6 additions & 0 deletions src/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,11 @@ export type Statement =
| ContinueStatement
| ReturnStatement;

export type BlockExpression = {
type: 'BlockExpression';
body: Statement[];
};

export type ConditionalExpression = {
type: 'ConditionalExpression';
kind: ConditionalKind;
Expand Down Expand Up @@ -439,6 +444,7 @@ export type FalseLiteral = {
};

export type Expression =
| BlockExpression
| ConditionalExpression
| AccessExpression
| ArraySlice
Expand Down
16 changes: 12 additions & 4 deletions src/grammars/sonic-weave.pegjs
Original file line number Diff line number Diff line change
Expand Up @@ -216,12 +216,12 @@ Statements
Statement
= VariableManipulationStatement
/ PitchDeclaration
/ BlockStatement
/ ExpressionStatement
/ VariableDeclaration
/ FunctionDeclaration
/ UpDeclaration
/ LiftDeclaration
/ BlockStatement
/ ThrowStatement
/ ReturnStatement
/ BreakStatement
Expand Down Expand Up @@ -671,8 +671,16 @@ ExpressionStatement
};
}

BlockExpression
= '{' _ body: Statements? _ '}' {
return {
type: 'BlockExpression',
body: body ?? [],
};
}

Expression
= LestExpression
= LestExpression / BlockExpression

AssigningOperator
= CoalescingOperator
Expand Down Expand Up @@ -1643,13 +1651,13 @@ ArrayLiteral
}
RecordLiteral
= '{' _ '}' {
= '#{' _ '}' {
return {
type: 'RecordLiteral',
properties: [],
};
}
/ '{' _ properties: PropertyNameAndValueList _ (',' _)? '}' {
/ '#{' _ properties: PropertyNameAndValueList _ (',' _)? '}' {
return {
type: 'RecordLiteral',
properties,
Expand Down
28 changes: 14 additions & 14 deletions src/parser/__tests__/expression.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1649,49 +1649,49 @@ describe('SonicWeave expression evaluator', () => {
});

it('has record syntax', () => {
const record = evaluate('{foo: "a", "bar": "b", "here be spaces": "c"}');
const record = evaluate('#{foo: "a", "bar": "b", "here be spaces": "c"}');
expect(record).toEqual({bar: 'b', 'here be spaces': 'c', foo: 'a'});
});

it('has record access', () => {
const {interval} = parseSingle('{foo: 3/2}["foo"]');
const {interval} = parseSingle('#{foo: 3/2}["foo"]');
expect(interval.toString()).toBe('3/2');
});

it('has the empty record', () => {
const blank = evaluate('{}');
const blank = evaluate('#{}');
expect(blank).toEqual({});
});

it('has nullish record access', () => {
const nothing = evaluate('{}~["zero nothings"]');
const nothing = evaluate('#{}~["zero nothings"]');
expect(nothing).toBe(undefined);
});

it('is resistant to pathological JS record keys', () => {
expect(() => evaluate('{}["toString"]')).toThrow('Key error: "toString"');
expect(() => evaluate('#{}["toString"]')).toThrow('Key error: "toString"');
});

it('has string representation of records', () => {
const str = evaluate('str({foo: 1})');
expect(str).toBe('{"foo": 1}');
const str = evaluate('str(#{foo: 1})');
expect(str).toBe('#{"foo": 1}');
});

it('can assign record keys', () => {
const record = evaluate('const rec = {foo: "a"};rec["bar"] = "b";rec');
const record = evaluate('const rec = #{foo: "a"};rec["bar"] = "b";rec');
expect(record).toEqual({foo: 'a', bar: 'b'});
});

it('can re-assign record values', () => {
const record = evaluate(
'const rec = {foo: 1, bar: 2};rec["bar"] *= 3; rec'
'const rec = #{foo: 1, bar: 2};rec["bar"] *= 3; rec'
) as Record<string, Interval>;
expect(record['foo'].toString()).toBe('1');
expect(record['bar'].toString()).toBe('6');
});

it('can get the entries of a record', () => {
const entries = evaluate('entries({foo: "a", bar: "b"})') as unknown as [
const entries = evaluate('entries(#{foo: "a", bar: "b"})') as unknown as [
string,
string,
][];
Expand All @@ -1703,12 +1703,12 @@ describe('SonicWeave expression evaluator', () => {
});

it('can test for presence of keys in a record', () => {
const yes = evaluate('"foo" in {foo: 1}');
const yes = evaluate('"foo" in #{foo: 1}');
expect(yes).toBe(true);
});

it('has a record shorthand', () => {
const record = evaluate('const foo = "a";{foo}');
const record = evaluate('const foo = "a";#{foo}');
const foo = 'a';
expect(record).toEqual({foo});
});
Expand Down Expand Up @@ -2042,7 +2042,7 @@ describe('SonicWeave expression evaluator', () => {
});

it('increments a record value', () => {
const rec = evaluate('const rec = {a: 1, b: 2};++rec["a"];rec') as Record<
const rec = evaluate('const rec = #{a: 1, b: 2};++rec["a"];rec') as Record<
string,
Interval
>;
Expand Down Expand Up @@ -2233,7 +2233,7 @@ describe('SonicWeave expression evaluator', () => {
});

it('ignores ternary broadcasting of records', () => {
const oof = evaluate('1 where [true, false] else {a: 2}') as any;
const oof = evaluate('1 where [true, false] else #{a: 2}') as any;
expect(oof).toHaveLength(2);
expect(oof[0].valueOf()).toBe(1);
expect(oof[1].a.valueOf()).toBe(2);
Expand Down
6 changes: 2 additions & 4 deletions src/parser/__tests__/sonic-weave-ast.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -926,7 +926,7 @@ describe('SonicWeave Abstract Syntax Tree parser', () => {
});

it('parses record literals', () => {
const ast = parseSingle('{foo: 1, "bar": 2}');
const ast = parseSingle('#{foo: 1, "bar": 2}');
expect(ast).toEqual({
type: 'ExpressionStatement',
expression: {
Expand Down Expand Up @@ -1298,10 +1298,8 @@ describe('Negative tests', () => {
expect(() => parse('a\\b<c/d>')).toThrow();
});

// XXX: Without the parenthesis this is actually a valid block statement containing an enumeration.
// Might need to rethink record syntax if this causes more issues.
it('rejects dim5 as an identifier', () => {
expect(() => parse('({dim5: "no good"})')).toThrow();
expect(() => parse('(#{dim5: "no good"})')).toThrow();
});

it('rejects Pythonic matrix multiplication with a human readable error message', () => {
Expand Down
29 changes: 27 additions & 2 deletions src/parser/__tests__/source.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1193,7 +1193,7 @@ describe('SonicWeave parser', () => {

it('has inline labels for ordered scales using records', () => {
const scale = expand(
'3/1 "pre-existing";{third: 6/5, "The Octave": 2/1, fif: 3/2}'
'3/1 "pre-existing";#{third: 6/5, "The Octave": 2/1, fif: 3/2}'
);
expect(scale).toEqual([
'3/1 "pre-existing"',
Expand Down Expand Up @@ -1926,7 +1926,7 @@ describe('SonicWeave parser', () => {

it('can delete record entries', () => {
const scale = expand(`{
const foo = {bar: 1, baz: 2}
const foo = #{bar: 1, baz: 2}
del foo['bar']
foo
}`);
Expand Down Expand Up @@ -2021,4 +2021,29 @@ describe('SonicWeave parser', () => {
'1200.',
]);
});

it('supports block expressions as valid RHS in assignment', () => {
const scale = expand(`{
const arr = {
2
1
3
sort()
}
arr;
}`);
expect(scale).toEqual(['1', '2', '3']);
});

it('supports block expressions as operands', () => {
const scale = expand(`
10 * ({
defer sort()
2
1
3
})
`);
expect(scale).toEqual(['10', '20', '30']);
});
});
18 changes: 10 additions & 8 deletions src/parser/__tests__/stdlib.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1188,19 +1188,21 @@ describe('SonicWeave standard library', () => {
});

it('can get the keys of a record', () => {
const keys = evaluateExpression('keys({foo: 1, bar: 2})') as string[];
const keys = evaluateExpression('keys(#{foo: 1, bar: 2})') as string[];
keys.sort();
expect(keys).toEqual(['bar', 'foo']);
});

it('can get the values of a record', () => {
const keys = evaluateExpression('values({foo: "a", bar: "b"})') as string[];
const keys = evaluateExpression(
'values(#{foo: "a", bar: "b"})'
) as string[];
keys.sort();
expect(keys).toEqual(['a', 'b']);
});

it('realizes a scale word', () => {
const scale = expand('realizeWord("LLsLLLs", {L: 9/8, s: 256/243})');
const scale = expand('realizeWord("LLsLLLs", #{L: 9/8, s: 256/243})');
expect(scale).toEqual([
'9/8',
'81/64',
Expand All @@ -1213,7 +1215,7 @@ describe('SonicWeave standard library', () => {
});

it('realizes a scale word with a missing step', () => {
const scale = expand('realizeWord("sLsLsLs", {L: 2\\10})');
const scale = expand('realizeWord("sLsLsLs", #{L: 2\\10})');
expect(scale).toEqual([
'1\\10',
'3\\10',
Expand All @@ -1227,7 +1229,7 @@ describe('SonicWeave standard library', () => {

it('gracefully handles extra step sizes in the record', () => {
const scale = expand(
'realizeWord("LLsLLLs", {L: 9/8, m: 16/15, s: 256/243, c: 81/80})'
'realizeWord("LLsLLLs", #{L: 9/8, m: 16/15, s: 256/243, c: 81/80})'
);
expect(scale).toEqual([
'9/8',
Expand All @@ -1241,11 +1243,11 @@ describe('SonicWeave standard library', () => {
});

it('realizes edge cases of `realizeWord`', () => {
const emptiness = parseSource('realizeWord("", {L: 2})');
const emptiness = parseSource('realizeWord("", #{L: 2})');
expect(emptiness).toEqual([]);
const octave = expand('realizeWord("L", {})');
const octave = expand('realizeWord("L", #{})');
expect(octave).toEqual(['2']);
const threeWholeTones = expand('realizeWord("LLL", {L: 9/8})');
const threeWholeTones = expand('realizeWord("LLL", #{L: 9/8})');
expect(threeWholeTones).toEqual(['9/8', '81/64', '729/512']);
});

Expand Down
Loading

0 comments on commit 4efd04c

Please sign in to comment.