From 8e3b2b087d74af561b8154a1692e91a9cfd79aad Mon Sep 17 00:00:00 2001 From: Lumi Pakkanen Date: Tue, 18 Jun 2024 10:56:17 +0300 Subject: [PATCH] Make blocks legal expressions evaluating to arrays (#352) Make blocks legal expressions evaluating to arrays Make return statements legal in block expressions to evaluate to something else. Reserve set literal syntax. ref #351 Co-authored-by: inthar-raven --- documentation/advanced-dsl.md | 29 +++++++- documentation/intermediate-dsl.md | 10 +-- documentation/technical.md | 4 +- src/ast.ts | 12 ++++ src/grammars/sonic-weave.pegjs | 27 ++++++-- src/parser/__tests__/expression.spec.ts | 40 +++++++---- src/parser/__tests__/sonic-weave-ast.spec.ts | 28 ++++++-- src/parser/__tests__/source.spec.ts | 68 ++++++++++++++++++- src/parser/__tests__/stdlib.spec.ts | 18 ++--- .../__tests__/vector-broadcasting.spec.ts | 12 ++-- src/parser/expression.ts | 19 ++++++ src/parser/statement.ts | 14 ++-- src/stdlib/prelude.ts | 2 +- src/stdlib/public.ts | 2 +- 14 files changed, 230 insertions(+), 55 deletions(-) diff --git a/documentation/advanced-dsl.md b/documentation/advanced-dsl.md index 15f59ed6..74744676 100644 --- a/documentation/advanced-dsl.md +++ b/documentation/advanced-dsl.md @@ -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.) @@ -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 `$$`. diff --git a/documentation/intermediate-dsl.md b/documentation/intermediate-dsl.md index 1a1eb470..8ceb08be 100644 --- a/documentation/intermediate-dsl.md +++ b/documentation/intermediate-dsl.md @@ -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, @@ -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 @@ -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. diff --git a/documentation/technical.md b/documentation/technical.md index f65737df..d9f87e2e 100644 --- a/documentation/technical.md +++ b/documentation/technical.md @@ -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. @@ -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 | diff --git a/src/ast.ts b/src/ast.ts index fc3ba499..bb83ad82 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -287,6 +287,11 @@ export type Statement = | ContinueStatement | ReturnStatement; +export type BlockExpression = { + type: 'BlockExpression'; + body: Statement[]; +}; + export type ConditionalExpression = { type: 'ConditionalExpression'; kind: ConditionalKind; @@ -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][]; @@ -439,6 +449,7 @@ export type FalseLiteral = { }; export type Expression = + | BlockExpression | ConditionalExpression | AccessExpression | ArraySlice @@ -460,6 +471,7 @@ export type Expression = | Range | ArrayComprehension | ArrayLiteral + | SetLiteral | RecordLiteral | StringLiteral | HarmonicSegment; diff --git a/src/grammars/sonic-weave.pegjs b/src/grammars/sonic-weave.pegjs index 60e4b260..d25042ee 100644 --- a/src/grammars/sonic-weave.pegjs +++ b/src/grammars/sonic-weave.pegjs @@ -216,12 +216,12 @@ Statements Statement = VariableManipulationStatement / PitchDeclaration + / BlockStatement / ExpressionStatement / VariableDeclaration / FunctionDeclaration / UpDeclaration / LiftDeclaration - / BlockStatement / ThrowStatement / ReturnStatement / BreakStatement @@ -671,8 +671,16 @@ ExpressionStatement }; } +BlockExpression + = '{' _ body: Statements? _ '}' { + return { + type: 'BlockExpression', + body: body ?? [], + }; + } + Expression - = LestExpression + = LestExpression / BlockExpression AssigningOperator = CoalescingOperator @@ -1036,6 +1044,7 @@ Primary / TemplateArgument / ArrayLiteral / RecordLiteral + / SetLiteral / StringLiteral UnitStepRange @@ -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, diff --git a/src/parser/__tests__/expression.spec.ts b/src/parser/__tests__/expression.spec.ts index 3079fcc9..79349cba 100644 --- a/src/parser/__tests__/expression.spec.ts +++ b/src/parser/__tests__/expression.spec.ts @@ -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; 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, ][]; @@ -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}); }); @@ -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 >; @@ -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); @@ -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 "<"', () => { diff --git a/src/parser/__tests__/sonic-weave-ast.spec.ts b/src/parser/__tests__/sonic-weave-ast.spec.ts index 8bc2c5f5..7a2c15f9 100644 --- a/src/parser/__tests__/sonic-weave-ast.spec.ts +++ b/src/parser/__tests__/sonic-weave-ast.spec.ts @@ -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: { @@ -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', () => { @@ -1298,10 +1320,8 @@ describe('Negative tests', () => { expect(() => parse('a\\b')).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', () => { diff --git a/src/parser/__tests__/source.spec.ts b/src/parser/__tests__/source.spec.ts index c860ce09..658fdbd9 100644 --- a/src/parser/__tests__/source.spec.ts +++ b/src/parser/__tests__/source.spec.ts @@ -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"', @@ -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 }`); @@ -2021,4 +2021,68 @@ 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']); + }); + + it('supports block expressions as function arguments', () => { + const truth = expand(` + hasConstantStructure({ + 9/8 + 81/64 + 4/3 + 3/2 + 27/16 + 243/128 + 2/1 + }) + `); + expect(truth).toEqual(expand('true')); + }); + + it('captures outside variables in block expressions', () => { + const pentic = expand(`{ + const octave = 2; + const fifth = 3/2; + const pentic = { + (fifth ^ 2) / octave + (fifth ^ 4) / (octave ^ 2) + fifth + (fifth ^ 3) / octave + octave + }; + pentic; + }`); + expect(pentic).toEqual( + expand(`{ + 9/8 + 81/64 + 3/2 + 27/16 + 2 + }`) + ); + }); }); diff --git a/src/parser/__tests__/stdlib.spec.ts b/src/parser/__tests__/stdlib.spec.ts index da44ba7a..71ebb92a 100644 --- a/src/parser/__tests__/stdlib.spec.ts +++ b/src/parser/__tests__/stdlib.spec.ts @@ -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', @@ -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', @@ -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', @@ -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']); }); diff --git a/src/parser/__tests__/vector-broadcasting.spec.ts b/src/parser/__tests__/vector-broadcasting.spec.ts index 479fa065..b6a5dff1 100644 --- a/src/parser/__tests__/vector-broadcasting.spec.ts +++ b/src/parser/__tests__/vector-broadcasting.spec.ts @@ -61,7 +61,7 @@ function swRec( describe('SonicWeave vector broadcasting', () => { it('refuses to mix arrays with records', () => { - expect(() => sw`[1, 2] {a: 3, b: 4}`).toThrow( + expect(() => sw`[1, 2] #{a: 3, b: 4}`).toThrow( 'Unable to broadcast an array and record together.' ); }); @@ -73,7 +73,7 @@ describe('SonicWeave vector broadcasting', () => { }); it('refuses to mix disparate records together', () => { - expect(() => sw`{a: 1} {b: 2}`).toThrow( + expect(() => sw`#{a: 1} #{b: 'label for b'}`).toThrow( 'Unable broadcast records together on key b.' ); }); @@ -139,7 +139,7 @@ describe('SonicWeave vector broadcasting', () => { }); it('multiplies records', () => { - const rec = swRec`{a: 3, b: 5} * {a: 7, b: 11}`; + const rec = swRec`#{a: 3, b: 5} * #{a: 7, b: 11}`; expect(rec).toEqual({a: 21, b: 55}); }); @@ -191,7 +191,7 @@ describe('SonicWeave vector broadcasting', () => { } const rec = evaluateExpression( - `${op}{four: 4, "negative half": -1/2}` + `${op}#{four: 4, "negative half": -1/2}` ) as Record; expect(Object.keys(rec)).toHaveLength(2); if (op === 'vnot ') { @@ -251,7 +251,7 @@ describe('SonicWeave vector broadcasting', () => { expect(mat[0]).toHaveLength(2); const rec = evaluateExpression( - `{a: 5, b: -1/2} ${op} {a: 2, b: PI}` + `#{a: 5, b: -1/2} ${op} #{a: 2, b: PI}` ) as Record; expect(Object.keys(rec)).toHaveLength(2); @@ -362,7 +362,7 @@ describe('SonicWeave vector broadcasting', () => { expect(mat[0]).toHaveLength(2); const rec = evaluateExpression( - `1=440z;${fn} {a: 3, "negative third": -1/3}` + `1=440z;${fn} #{a: 3, "negative third": -1/3}` ) as Record; expect(Object.keys(rec)).toHaveLength(2); }); diff --git a/src/parser/expression.ts b/src/parser/expression.ts index 3646b759..04184c45 100644 --- a/src/parser/expression.ts +++ b/src/parser/expression.ts @@ -92,6 +92,7 @@ import { TemplateArgument, UpdateExpression, UpdateOperator, + BlockExpression, } from '../ast'; import {type StatementVisitor} from './statement'; import {AbsoluteMosPitch, absoluteMosMonzo, mosMonzo} from '../diamond-mos'; @@ -313,6 +314,8 @@ export class ExpressionVisitor { visit(node: Expression): SonicWeaveValue { this.spendGas(); switch (node.type) { + case 'BlockExpression': + return this.visitBlockExpression(node); case 'ConditionalExpression': return this.visitConditionalExpression(node); case 'AccessExpression': @@ -414,10 +417,26 @@ export class ExpressionVisitor { return this.visitSquareSuperparticular(node); case 'TemplateArgument': return this.visitTemplateArgument(node); + case 'SetLiteral': + // This requires hashable Intervals and support in xen-dev-utils. + throw new Error('Set literals not implemented yet.'); } node satisfies never; } + protected visitBlockExpression(node: BlockExpression) { + const subVisitor = this.parent._createStatementVisitor(this); + const scale = this.currentScale; + subVisitor.mutables.set('$$', scale); + const interrupt = subVisitor.executeStatements(node.body); + if (interrupt?.type === 'ReturnStatement') { + return interrupt.value; + } else if (interrupt) { + throw new Error('Illegal interupt.'); + } + return subVisitor.currentScale; + } + protected visitTemplateArgument(node: TemplateArgument) { if (!this.rootContext) { throw new Error('Root context required to access template arguments.'); diff --git a/src/parser/statement.ts b/src/parser/statement.ts index d1052ad0..4abf34b1 100644 --- a/src/parser/statement.ts +++ b/src/parser/statement.ts @@ -102,7 +102,7 @@ export class StatementVisitor { /** * Parent context of the surrounding code block. */ - parent?: StatementVisitor; + parent?: StatementVisitor | ExpressionVisitor; /** * Local context for mutable (let) variables. */ @@ -138,7 +138,7 @@ export class StatementVisitor { * Construct a new visitor for a block of code inside the AST. * @param parent Parent context of the surrounding code block. */ - constructor(parent?: StatementVisitor) { + constructor(parent?: StatementVisitor | ExpressionVisitor) { this.parent = parent; this.mutables = new Map(); this.mutables.set('$', []); @@ -201,6 +201,12 @@ export class StatementVisitor { return new ExpressionVisitor(this); } + // Chicken-and-egg method to prevent circular dependency with ExpressionVisitor + /** @hidden */ + _createStatementVisitor(parent: ExpressionVisitor) { + return new StatementVisitor(parent); + } + /** * Convert the state of this statement visitor into a block of text in the SonicWeave DSL. Only intended for the user scope just above the global scope. * @param defaultRootContext Root context for determining if root pitch declaration must be included. @@ -570,7 +576,7 @@ export class StatementVisitor { if (this.modules.has(name)) { return this.modules.get(name)!; } - if (this.parent) { + if (this.parent && this.parent instanceof StatementVisitor) { return this.parent.getModule(name); } throw new Error(`Module ${name} not found.`); @@ -1369,7 +1375,7 @@ export class StatementVisitor { * @param value Value for the variable. * @throws An error if there is no variable declared under the given name or the given variable is declared constant. */ - set(name: string, value: SonicWeaveValue): undefined { + set(name: string, value: SonicWeaveValue): void { if (this.immutables.has(name)) { throw new Error('Assignment to a constant variable.'); } diff --git a/src/stdlib/prelude.ts b/src/stdlib/prelude.ts index 43536fcb..a8c1ba2f 100644 --- a/src/stdlib/prelude.ts +++ b/src/stdlib/prelude.ts @@ -685,7 +685,7 @@ riff realizeWord(word, sizes, equave = niente) { continue; total = total *~ sizes[letter] ~^ count; } - sizes = {...sizes}; + sizes = #{...sizes}; sizes[missingLetter] = (equave %~ total) ~/^ signature[missingLetter]; } else if (equave <> niente) { let total = 1; diff --git a/src/stdlib/public.ts b/src/stdlib/public.ts index a2174edb..5438aea9 100644 --- a/src/stdlib/public.ts +++ b/src/stdlib/public.ts @@ -321,7 +321,7 @@ function repr_( if (typeof value === 'object') { const s = repr_.bind(this); return ( - '{' + + '#{' + Object.entries(value) .map(([k, v]) => `${s(k)}: ${s(v)}`) .join(', ') +