Skip to content

Commit

Permalink
Implement deferred actions
Browse files Browse the repository at this point in the history
ref #279
  • Loading branch information
frostburn committed May 3, 2024
1 parent 456207d commit 29052fa
Show file tree
Hide file tree
Showing 12 changed files with 218 additions and 21 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,12 @@ Scale title, colors and labels.
* Marc Sabat - Notation adviser

## Acknowledgments / inspiration
SonicWeave looks like Javascript with Python semantics, has Haskell ranges and operates similar to xen-calc.
SonicWeave looks like Javascript with Python semantics, has Haskell ranges and operates similar to xen-calc with some Zig sprinkled on top.

* ECMAScript - Brendan Eich et. al.
* Python - Guido van Rossum et. al.
* Haskell - Lennart Augustsson et. al.
* Zig - Andrew Kelley et. al.
* NumPy - Travis Oliphant et. al.
* Scala - Manuel Op de Coul
* Scale Workshop 1 - Sean Archibald et. al.
Expand Down
3 changes: 3 additions & 0 deletions documentation/BUILTIN.md
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,9 @@ Calculate the cumulative geometric sums of a periodic difference pattern. Undoes
### asinh(*x*)
Calculate the inverse hyperbolic sine of x.

### assert(*test*, *message = "Assertion failed."*)
Assert that the test expression is true or fail with the given message.

### atanh(*x*)
Calculate the inverse hyperbolic tangent of x.

Expand Down
25 changes: 25 additions & 0 deletions documentation/advanced-dsl.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,31 @@ 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 `$$`.

## Defer
Defer is used to execute a statement while exiting the current block.

```c
let x = 5;
{
defer x += 2;
assert(x === 5);
}
assert(x === 7);
```
When there are multiple defers in a single block, they are executed in reverse order.
```c
let x = 5;
{
defer x += 2;
defer x /= 2;
}
assert(x === 4.5e);
```

Defer is useful for pushing implicit tempering and general housekeeping to the top of the source instead of having to dangle everything at the end while editing the scale.

## While
"While" loops repeat a statement until the test becames *falsy* e.g.
```c
Expand Down
6 changes: 6 additions & 0 deletions src/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,11 @@ export type TryStatement = {
finalizer?: Statement;
};

export type DeferStatement = {
type: 'DeferStatement';
body: Statement;
};

export type ExpressionStatement = {
type: 'ExpressionStatement';
expression: Expression;
Expand All @@ -223,6 +228,7 @@ export type Statement =
| IfStatement
| IterationStatement
| TryStatement
| DeferStatement
| ThrowStatement
| BreakStatement
| ContinueStatement
Expand Down
11 changes: 11 additions & 0 deletions src/grammars/sonic-weave.pegjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
'catch',
'const',
'continue',
'defer',
'dot',
'drop',
'ed',
Expand Down Expand Up @@ -132,6 +133,7 @@ ByToken = @'by' !IdentifierPart
CatchToken = @'catch' !IdentifierPart
ConstToken = @'const' !IdentifierPart
ContinueToken = @'continue' !IdentifierPart
DeferToken = @'defer' !IdentifierPart
DotToken = @'dot' !IdentifierPart
DropToken = @'drop' !IdentifierPart
EdToken = @'ed' !IdentifierPart
Expand Down Expand Up @@ -194,6 +196,7 @@ Statement
/ IfStatement
/ IterationStatement
/ TryStatement
/ DeferStatement
/ EmptyStatement

ReassignmentTail
Expand Down Expand Up @@ -530,6 +533,14 @@ CatchClause

TryFinalizer = FinallyToken _ @Statement

DeferStatement
= DeferToken _ body: Statement {
return {
type: 'DeferStatement',
body,
};
}

EmptyStatement
= (_ ';' / __ SingleLineComment LineTerminatorSequence) {
return {
Expand Down
11 changes: 11 additions & 0 deletions src/parser/__tests__/expression.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2306,4 +2306,15 @@ describe('Poor grammar / Fun with "<"', () => {
const val = evaluate('valFromPrimeArray([12, 19, 28])') as Val;
expect(val.toString()).toBe('<12 19 28]');
});

it('rejects defer at root scope', () => {
expect(() => evaluate('defer 2;3')).toThrow(
'Deferred actions not allowed when evaluating expressions.'
);
});

it('accepts defer in sub-blocks', () => {
const {fraction} = parseSingle('{defer 2; 3};$[-1]');
expect(fraction).toBe('2');
});
});
49 changes: 49 additions & 0 deletions src/parser/__tests__/source.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1392,4 +1392,53 @@ describe('SonicWeave parser', () => {
);
expect(scale).toEqual(['8 Hz', '10 Hz', '12 Hz']);
});

it('can defer tempering', () => {
const scale = expand(`
defer 12@
M3
P5
P8
`);
expect(scale).toEqual(['4\\12', '7\\12', '12\\12']);
});

it('has defer similar to Zig (single)', () => {
evaluateSource(`
let x = 5;
{
defer x += 2;
if (x !== 5) {
throw 'Defer executed early!';
}
}
if (x !== 7) {
throw 'Defer did not execute!';
}
`);
});

it('has defer similar to Zig (multi)', () => {
evaluateSource(`
let x = 5;
{
defer x += 2;
defer x /= 2;
}
if (x !== 4.5e) {
throw 'Deferred statements executed in the wrong order!';
}
`);
});

it('rejects confusing defer', () => {
expect(() =>
evaluateSource(`
for (const i of [3, 5]) {
defer break;
i / (i - 1);
}
`)
).toThrow('Illegal BreakStatement inside a deferred block.');
});
});
32 changes: 32 additions & 0 deletions src/parser/__tests__/stdlib.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1442,4 +1442,36 @@ describe('SonicWeave standard library', () => {
'6/3',
]);
});

it('has assert (success)', () => {
evaluateSource(`
let x = 5;
{
defer x += 2;
assert(x === 5);
}
assert(x === 7);
`);
});

it('has assert (failure)', () => {
expect(() => evaluateSource('assert(1 === 2)')).toThrow(
'Assertion failed.'
);
});

it('executes deferred actions before interrupting', () => {
const result = evaluateExpression(`
let x = "Nothing happened...";
riff doStuff() {
defer x = "Deferred action triggered.";
return 311;
x = "Unreachable code executed!";
throw "Execution shouldn't reach here!";
}
assert(doStuff() === 311);
x;
`);
expect(result).toBe('Deferred action triggered.');
});
});
11 changes: 11 additions & 0 deletions src/parser/__tests__/tag.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,17 @@ describe('SonicWeave template tag', () => {
const first = sw`${'first'};${'second'};templateArg(0)`;
expect(first).toBe('first');
});

it('rejects defer at root scope', () => {
expect(() => sw`defer 2;3`).toThrow(
'Deferred actions not allowed when evaluating tagged templates.'
);
});

it('accepts defer in sub-blocks', () => {
const two = sw`{defer 2; 3};$[-1]` as Interval;
expect(two.toString()).toBe('2');
});
});

describe('SonicWeave raw template tag', () => {
Expand Down
18 changes: 13 additions & 5 deletions src/parser/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,11 +130,9 @@ export function evaluateSource(
const visitor = getSourceVisitor(includePrelude, extraBuiltins);

const program = parseAST(source);
for (const statement of program.body) {
const interrupt = visitor.visit(statement);
if (interrupt) {
throw new Error(`Illegal ${interrupt.type}.`);
}
const interrupt = visitor.executeProgram(program);
if (interrupt) {
throw new Error(`Illegal ${interrupt.type}.`);
}
return visitor;
}
Expand All @@ -160,6 +158,11 @@ export function evaluateExpression(
throw new Error(`Illegal ${interrupt.type}.`);
}
}
if (visitor.deferred.length) {
throw new Error(
'Deferred actions not allowed when evaluating expressions.'
);
}
const finalStatement = program.body[program.body.length - 1];
if (finalStatement.type !== 'ExpressionStatement') {
throw new Error(`Expected expression. Got ${finalStatement.type}`);
Expand Down Expand Up @@ -238,6 +241,11 @@ export function createTag(
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}.`);
Expand Down
64 changes: 49 additions & 15 deletions src/parser/statement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ import {
UpDeclaration,
VariableDeclaration,
WhileStatement,
DeferStatement,
Program,
} from '../ast';
import {
ExpressionVisitor,
Expand Down Expand Up @@ -96,6 +98,10 @@ export class StatementVisitor {
* Whether or not the state of the visitor can be represented as text and that state represents the runtime from a user's perspective. The global context doesn't have a representation because the builtins are not written in the SonicWeave DSL.
*/
isUserRoot: boolean;
/**
* Deferred statement to be executed at the end of the current block.
*/
deferred: Statement[];

private rootContext_?: RootContext;

Expand All @@ -109,6 +115,7 @@ export class StatementVisitor {
this.mutables.set('$', []);
this.immutables = new Map();
this.isUserRoot = false;
this.deferred = [];
}

/**
Expand Down Expand Up @@ -260,12 +267,19 @@ export class StatementVisitor {
return this.visitContinueStatement(node);
case 'ThrowStatement':
throw this.visitThrowStatement(node);
case 'DeferStatement':
return this.visitDeferStatement(node);
case 'EmptyStatement':
return;
}
node satisfies never;
}

protected visitDeferStatement(node: DeferStatement) {
this.deferred.push(node.body);
return undefined;
}

protected visitReturnStatement(node: ReturnStatement) {
let value: SonicWeaveValue;
if (node.argument) {
Expand Down Expand Up @@ -726,18 +740,40 @@ export class StatementVisitor {
scale.push(...result);
}

/**
* Execute the abstract syntax tree of a SonicWeave program or an array of statements.
* @param body Program containing the AST to be executed.
* @returns An interrupt or undefined if none encountered.
*/
executeProgram(body: Program | Statement[]): Interrupt | undefined {
if (!Array.isArray(body)) {
body = body.body;
}
let interrupt: Interrupt | undefined = undefined;
for (const statement of body) {
interrupt = this.visit(statement);
if (interrupt) {
break;
}
}
while (this.deferred.length) {
const badInterrupt = this.visit(this.deferred.pop()!);
if (badInterrupt) {
throw new Error(
`Illegal ${badInterrupt.type} inside a deferred block.`
);
}
}
return interrupt;
}

protected visitBlockStatement(node: BlockStatement) {
const subVisitor = new StatementVisitor(this);
const scale = this.currentScale;
subVisitor.mutables.set('$$', scale);
let interrupt: Interrupt | undefined = undefined;
for (const statement of node.body) {
interrupt = subVisitor.visit(statement);
if (interrupt?.type === 'ReturnStatement') {
return interrupt;
} else if (interrupt) {
break;
}
const interrupt = subVisitor.executeProgram(node.body);
if (interrupt?.type === 'ReturnStatement') {
return interrupt;
}
const subScale = subVisitor.currentScale;
scale.push(...subScale);
Expand Down Expand Up @@ -923,13 +959,11 @@ export class StatementVisitor {
const localSubvisitor = localVisitor.createExpressionVisitor(true);
localSubvisitor.localAssign(node.parameters, args as Interval[]);

for (const statement of node.body) {
const interrupt = localVisitor.visit(statement);
if (interrupt?.type === 'ReturnStatement') {
return interrupt.value;
} else if (interrupt) {
throw new Error(`Illegal ${interrupt.type}.`);
}
const interrupt = localVisitor.executeProgram(node.body);
if (interrupt?.type === 'ReturnStatement') {
return interrupt.value;
} else if (interrupt) {
throw new Error(`Illegal ${interrupt.type}.`);
}
return localVisitor.currentScale;
}
Expand Down
Loading

0 comments on commit 29052fa

Please sign in to comment.