Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make blocks legal expressions evaluating to arrays #352

Merged
merged 5 commits into from
Jun 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 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,29 @@ 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] *)
```

#### Block expression return value
Use a `return` statement inside a block expression to evaluate to the returned value.
```ocaml
const foo = {
const bar = 2
const baz = 3
return bar + baz
}
(* const foo = 5 *)
```

### 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
4 changes: 2 additions & 2 deletions 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 Expand Up @@ -194,7 +194,7 @@ The Basic Latin block is listed in full. Other blocks only where used.
| U+0020 | *SP* | Whitespace |
| U+0021 | ! | *Reserved for future use* |
| U+0022 | " | String literals |
| U+0023 | # | Sharp accidental |
| U+0023 | # | Sharp accidental, record literals |
| U+0024 | $ | Current scale |
| U+0025 | % | Unary inversion, binary division (loose binding) |
| U+0026 | & | MOS chroma up accidental |
Expand Down
12 changes: 12 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 @@ -420,6 +425,11 @@ export type ArrayLiteral = {
elements: Argument[];
};

export type SetLiteral = {
type: 'SetLiteral';
elements: Argument[];
};

export type RecordLiteral = {
type: 'RecordLiteral';
properties: [string | null, Expression][];
Expand All @@ -439,6 +449,7 @@ export type FalseLiteral = {
};

export type Expression =
| BlockExpression
| ConditionalExpression
| AccessExpression
| ArraySlice
Expand All @@ -460,6 +471,7 @@ export type Expression =
| Range
| ArrayComprehension
| ArrayLiteral
| SetLiteral
| RecordLiteral
| StringLiteral
| HarmonicSegment;
Expand Down
27 changes: 22 additions & 5 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 @@ -1036,6 +1044,7 @@ Primary
/ TemplateArgument
/ ArrayLiteral
/ RecordLiteral
/ SetLiteral
/ StringLiteral

UnitStepRange
Expand Down Expand Up @@ -1639,17 +1648,25 @@ ArrayLiteral
return {
type: 'ArrayLiteral',
elements,
}
};
}

SetLiteral
= '#[' _ elements: ArgumentList _ ']' {
return {
type: 'SetLiteral',
elements,
};
}

RecordLiteral
= '{' _ '}' {
= '#{' _ '}' {
return {
type: 'RecordLiteral',
properties: [],
};
}
/ '{' _ properties: PropertyNameAndValueList _ (',' _)? '}' {
/ '#{' _ properties: PropertyNameAndValueList _ (',' _)? '}' {
return {
type: 'RecordLiteral',
properties,
Expand Down
40 changes: 26 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 Expand Up @@ -2460,6 +2460,18 @@ describe('SonicWeave expression evaluator', () => {
expect(interval.value).toBeInstanceOf(TimeReal);
expect(interval.valueOf()).toBeCloseTo(1.000000745, 10);
});

it('allows return from block expressions', () => {
const foo = evaluate(`
const foo = ({
const ba = "ba"
return ba "r"
throw "not executed"
})
foo
`);
expect(foo).toBe('bar');
});
});

describe('Poor grammar / Fun with "<"', () => {
Expand Down
28 changes: 24 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 @@ -1205,6 +1205,28 @@ describe('SonicWeave Abstract Syntax Tree parser', () => {
expression: {type: 'ColorLiteral', value: 'rgba(255 50% 5 / .5)'},
});
});

it('parses set literals', () => {
const ast = parseSingle('#[1, 2]');
expect(ast).toEqual({
type: 'ExpressionStatement',
expression: {
type: 'SetLiteral',
elements: [
{
type: 'Argument',
spread: false,
expression: {type: 'IntegerLiteral', value: 1n},
},
{
type: 'Argument',
spread: false,
expression: {type: 'IntegerLiteral', value: 2n},
},
],
},
});
});
});

describe('Automatic semicolon insertion', () => {
Expand Down Expand Up @@ -1298,10 +1320,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
Loading
Loading