diff --git a/documentation/intermediate-dsl.md b/documentation/intermediate-dsl.md index 84ca465f..d5d83786 100644 --- a/documentation/intermediate-dsl.md +++ b/documentation/intermediate-dsl.md @@ -133,11 +133,6 @@ Results in `$ = [4/3, 5/4 "my third", 3/2 "fif", 2 "octave"]`. Notice how 4/3 wa ### Boolean conversion `true` is converted to `1` and `false` to `0` before pushing them onto the current scale. -## Ranges -Ranges of integers are generated by giving the start and end points e.g. `[1..5]` evaluates to `[1, 2, 3, 4, 5]`. - -To skip over values you can specify the second element `[1,3..10]` evaluates to `[1, 3, 5, 7, 9]` (the iteration stops and rejects after reaching `11`). - ## Variables Variables in SonicWeave can hold any type of value. They must be declared before use. Variables declared `const` cannot be re-assigned while `let` variables may change what value they refer to. @@ -683,6 +678,8 @@ To skip over values you can specify the second element `[1,3..10]` evaluates to Reversed ranges require the second element `[5..1]` evaluates to the empty array `[]` but `[5,4..1]` equals `[5, 4, 3, 2, 1]` as expected. +Some times it's practical to exclude the end point e.g. `[0 .. < 3]` evaluates to `[0, 1, 2]`. + ### Harmonic segments Segments of the (musical) harmonic series can be obtained by specifying the root and the harmonic of equivalence e.g. `4::8` evaluates to `[5/4, 6/4, 7/4, 8/4]`. @@ -732,6 +729,8 @@ Range syntax inside array access gets a copy of a subset of the array e.g. `[1, In slice syntax the end points are optional e.g. `[1, 2, 3][..]` evaluates to `[1, 2, 3]` (a new copy). +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}`. diff --git a/src/ast.ts b/src/ast.ts index 48756b42..e946f331 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -253,6 +253,7 @@ export type ArraySlice = { object: Expression; start: Expression | null; second: Expression | null; + penultimate: boolean; end: Expression | null; }; @@ -338,6 +339,7 @@ export type Range = { type: 'Range'; start: Expression; second?: Expression; + penultimate: boolean; end: Expression; }; diff --git a/src/grammars/paren-counter.pegjs b/src/grammars/paren-counter.pegjs index 0b3a21d5..2530e1a1 100644 --- a/src/grammars/paren-counter.pegjs +++ b/src/grammars/paren-counter.pegjs @@ -6,7 +6,7 @@ const empty = {parens: 0, squares: 0, curlies: 0}; }} -Start = _ content: (StringLiteral / MonzoLiteral / ValLiteral / Other)|.., _| _ EOF { +Start = _ content: Expression|.., _| _ EOF { const result = {parens: 0, squares: 0, curlies: 0}; for (const counts of content) { result.parens += counts.parens; @@ -16,6 +16,30 @@ Start = _ content: (StringLiteral / MonzoLiteral / ValLiteral / Other)|.., _| _ return result; } +Expression + = RangeOrSlice + / StringLiteral + / MonzoLiteral + / Array + / ValLiteral + / Other + +RangeOrSlice + = '[' _ Expression _ '..' _ '<'? _ Expression _ closed: ']'? { + if (closed) { + return empty; + } + return {parens: 0, squares: 1, curlies: 0}; + } + +Array + = '[' _ Expression|.., _| _ closed: ']'? { + if (closed) { + return empty; + } + return {parens: 0, squares: 1, curlies: 0}; + } + StringLiteral = '"' chars: DoubleStringCharacter* '"' { return empty; } / "'" chars: SingleStringCharacter* "'" { return empty; } @@ -30,7 +54,7 @@ ValLiteral return empty; } -Other = (!WhiteSpace !LineTerminatorSequence !Comment SourceCharacter)+ { +Other = (!WhiteSpace !LineTerminatorSequence !Comment !']' SourceCharacter)+ { const t = text(); const parens = (t.match(/\(/g) ?? []).length - (t.match(/\)/g) ?? []).length; const squares = (t.match(/\[/g) ?? []).length - (t.match(/\]/g) ?? []).length; diff --git a/src/grammars/sonic-weave.pegjs b/src/grammars/sonic-weave.pegjs index 251caf9d..78515494 100644 --- a/src/grammars/sonic-weave.pegjs +++ b/src/grammars/sonic-weave.pegjs @@ -799,6 +799,10 @@ LabeledCommaDecimal return labels.reduce(operatorReducerLite, object); } +RangeDotsPenultimate = '..' _ penultimate: '<'? { + return !!penultimate; +} + CallTail = head: (__ '(' _ @ArgumentList _ ')') tail: ( @@ -808,8 +812,8 @@ CallTail / __ '[' _ key: Expression _ ']' { return { type: 'AccessExpression', key }; } - / __ '[' _ start: Expression? _ second: (',' _ @Expression)? _ '..' _ end: Expression? _ ']' { - return { type: 'ArraySlice', start, second, end }; + / __ '[' _ start: Expression? _ second: (',' _ @Expression)? _ penultimate: RangeDotsPenultimate _ end: Expression? _ ']' { + return { type: 'ArraySlice', start, second, penultimate, end }; } )* { tail.unshift({ type: 'CallExpression', args: head}); @@ -854,9 +858,9 @@ TrueAccessExpression } ArraySlice - = head: Primary tail: (_ '[' _ @Expression? @(_ ',' _ @Expression)? _ '..' _ @Expression? _ ']')* { - return tail.reduce( (object, [start, second, end]) => { - return { type: 'ArraySlice', object, start, second, end }; + = head: Primary tail: (_ '[' _ @Expression? @(_ ',' _ @Expression)? _ @RangeDotsPenultimate _ @Expression? _ ']')* { + return tail.reduce( (object, [start, second, penultimate, end]) => { + return { type: 'ArraySlice', object, start, second, penultimate, end }; }, head); } @@ -922,20 +926,22 @@ Primary / StringLiteral UnitStepRange - = '[' _ start: Expression _ '..' _ end: Expression _ ']' { + = '[' _ start: Expression _ penultimate: RangeDotsPenultimate _ end: Expression _ ']' { return { type: 'Range', start, + penultimate, end, }; } StepRange - = '[' _ start: Expression _ ',' _ second: Expression _ '..' _ end: Expression _ ']' { + = '[' _ start: Expression _ ',' _ second: Expression _ penultimate: RangeDotsPenultimate _ end: Expression _ ']' { return { type: 'Range', start, second, + penultimate, end, }; } diff --git a/src/parser/__tests__/expression.spec.ts b/src/parser/__tests__/expression.spec.ts index c0290523..46434458 100644 --- a/src/parser/__tests__/expression.spec.ts +++ b/src/parser/__tests__/expression.spec.ts @@ -540,6 +540,16 @@ describe('SonicWeave expression evaluator', () => { expect(ba).toBe('ba'); }); + it('supports penultimation tag in slices', () => { + const oba = evaluate('"foobar"[2..<5]'); + expect(oba).toBe('oba'); + }); + + it('supports penultimation tag in slices (first two)', () => { + const fo = evaluate('"foobar"[..<2]'); + expect(fo).toBe('fo'); + }); + it('supports indexing on str calls', () => { const C = evaluate('str(C4)[0]'); expect(C).toBe('C'); @@ -616,12 +626,17 @@ describe('SonicWeave expression evaluator', () => { expect(stuff).toEqual(['1', ...['2', '3'], '4']); }); - it('cannot produce empty ranges', () => { + it('cannot produce empty ranges without the penultimation flag', () => { const zero = evaluate('[0..0]') as Interval[]; expect(zero).toHaveLength(1); expect(zero[0].toInteger()).toBe(0); }); + it('can produce empty ranges with the penultimation flag', () => { + const zero = evaluate('[0..<0]') as Interval[]; + expect(zero).toHaveLength(0); + }); + it('can produce empty segments', () => { const nothing = evaluate('1::1') as Interval[]; expect(nothing).toHaveLength(0); diff --git a/src/parser/__tests__/paren-counter.spec.ts b/src/parser/__tests__/paren-counter.spec.ts index b6822bbb..46ffadfd 100644 --- a/src/parser/__tests__/paren-counter.spec.ts +++ b/src/parser/__tests__/paren-counter.spec.ts @@ -53,4 +53,29 @@ describe('Parenthesis counter', () => { const counts = parse('if(1){\n[-1 1>\n'); expect(counts).toEqual({parens: 0, squares: 0, curlies: 1}); }); + + it('works with penultimate ranges', () => { + const counts = parse('[1 .. < 5]'); + expect(counts).toEqual({parens: 0, squares: 0, curlies: 0}); + }); + + it('works with penultimate ranges (unclosed)', () => { + const counts = parse('[1 .. < 5'); + expect(counts).toEqual({parens: 0, squares: 1, curlies: 0}); + }); + + it('works with arrays', () => { + const counts = parse('[1, 2, "hello"]'); + expect(counts).toEqual({parens: 0, squares: 0, curlies: 0}); + }); + + it('works with arrays (unclosed)', () => { + const counts = parse('[1, 2, "hello"'); + expect(counts).toEqual({parens: 0, squares: 1, curlies: 0}); + }); + + it('works with subgroup tails', () => { + const counts = parse('[1 2>@2..'); + expect(counts).toEqual({parens: 0, squares: 0, curlies: 0}); + }); }); diff --git a/src/parser/__tests__/sonic-weave-ast.spec.ts b/src/parser/__tests__/sonic-weave-ast.spec.ts index c408bf63..6b8c94bc 100644 --- a/src/parser/__tests__/sonic-weave-ast.spec.ts +++ b/src/parser/__tests__/sonic-weave-ast.spec.ts @@ -221,6 +221,7 @@ describe('SonicWeave Abstract Syntax Tree parser', () => { expression: { type: 'Range', start: {type: 'IntegerLiteral', value: 1n}, + penultimate: false, end: {type: 'IntegerLiteral', value: 10n}, }, }); @@ -348,10 +349,12 @@ describe('SonicWeave Abstract Syntax Tree parser', () => { object: {type: 'Identifier', id: 'x'}, start: {type: 'IntegerLiteral', value: 0n}, second: {type: 'IntegerLiteral', value: 2n}, + penultimate: false, end: {type: 'IntegerLiteral', value: 10n}, }, start: {type: 'IntegerLiteral', value: 1n}, second: null, + penultimate: false, end: {type: 'IntegerLiteral', value: 2n}, }, key: {type: 'IntegerLiteral', value: 0n}, @@ -470,6 +473,7 @@ describe('SonicWeave Abstract Syntax Tree parser', () => { object: {type: 'Identifier', id: 'arr'}, start: null, second: null, + penultimate: false, end: null, }, key: {type: 'IntegerLiteral', value: 1n}, diff --git a/src/parser/__tests__/stdlib.spec.ts b/src/parser/__tests__/stdlib.spec.ts index 3e21ff83..701de5bf 100644 --- a/src/parser/__tests__/stdlib.spec.ts +++ b/src/parser/__tests__/stdlib.spec.ts @@ -1478,7 +1478,7 @@ describe('SonicWeave standard library', () => { expect(range).toEqual(['3', '5', '7']); }); - it('has Python 2 ranges (start + end)', () => { + it('has Python 2 ranges (start with end)', () => { const range = expand('range(-1, 3)'); expect(range).toEqual(['-1', '0', '1', '2']); }); @@ -1487,4 +1487,9 @@ describe('SonicWeave standard library', () => { const range = expand('range(3)'); expect(range).toEqual(['0', '1', '2']); }); + + it('has Python 2 ranges (negative step)', () => { + const range = expand('range(3, 0, -1)'); + expect(range).toEqual(['3', '2', '1']); + }); }); diff --git a/src/parser/expression.ts b/src/parser/expression.ts index a57b0735..fb2c49c5 100644 --- a/src/parser/expression.ts +++ b/src/parser/expression.ts @@ -852,6 +852,7 @@ export class ExpressionVisitor { let start = 0; let step = 1; + const pu = node.penultimate; let end = -1; if (node.start) { @@ -887,14 +888,14 @@ export class ExpressionVisitor { if (step > 0) { start = Math.max(0, start); - if (start > end || start >= object.length) { + if ((pu ? start >= end : start > end) || start >= object.length) { return empty; } end = Math.min(object.length - 1, end); const result = [object[start]]; let next = start + step; - while (next <= end) { + while (pu ? next < end : next <= end) { result.push(object[next]); next += step; } @@ -904,14 +905,14 @@ export class ExpressionVisitor { return result as Interval[]; } else if (step < 0) { start = Math.min(object.length - 1, start); - if (start < end || start < 0) { + if ((pu ? start <= end : start < end) || start < 0) { return empty; } end = Math.max(0, end); const result = [object[start]]; let next = start + step; - while (next >= end) { + while (pu ? next > end : next >= end) { result.push(object[next]); next += step; } @@ -1980,6 +1981,7 @@ export class ExpressionVisitor { protected visitRange(node: Range): Interval[] { const start = this.visit(node.start); const end = this.visit(node.end); + const pu = node.penultimate; if (!(start instanceof Interval && end instanceof Interval)) { throw new Error('Ranges must consist of intervals.'); } @@ -1997,23 +1999,23 @@ export class ExpressionVisitor { this.spendGas(Math.abs((end.valueOf() - start.valueOf()) / stepValue)); } if (stepValue > 0) { - if (start.compare(end) > 0) { + if (pu ? start.compare(end) >= 0 : start.compare(end) > 0) { return []; } const result = [start]; let next = start.add(step); - while (next.compare(end) <= 0) { + while (pu ? next.compare(end) < 0 : next.compare(end) <= 0) { result.push(next); next = next.add(step); } return result; } else if (stepValue < 0) { - if (start.compare(end) < 0) { + if (pu ? start.compare(end) <= 0 : start.compare(end) < 0) { return []; } const result = [start]; let next = start.add(step); - while (next.compare(end) >= 0) { + while (pu ? next.compare(end) > 0 : next.compare(end) >= 0) { result.push(next); next = next.add(step); } diff --git a/src/stdlib/prelude.ts b/src/stdlib/prelude.ts index 56d4b749..4c2607c1 100644 --- a/src/stdlib/prelude.ts +++ b/src/stdlib/prelude.ts @@ -85,10 +85,7 @@ riff range(start, stop = niente, step = 1) { stop = start; start = 0; } - const result = [start, start+step .. stop]; - if (result and result[-1] ~= stop) - void(pop(result)); - return result; + return [start, start+step .. < stop]; } riff sanitize(interval) { @@ -802,7 +799,7 @@ riff repeated(times = 2, scale = $$) { return []; scale; const equave = scale[-1]; - for (const level of equave ~^ [1..times-1]) + for (const level of equave ~^ [1.. < times]) scale ~* level; } @@ -821,7 +818,7 @@ riff repeatedLinear(times = 2, scale = $$) { return []; scale; const total = (scale[-1] ~- 1); - for (const level of total ~* [1..times-1]) + for (const level of total ~* [1.. < times]) (scale ~- 1) ~+ level ~+ 1; }