Skip to content

Commit

Permalink
Implement range/slice syntax for excluding the end point
Browse files Browse the repository at this point in the history
ref #292
  • Loading branch information
frostburn committed May 5, 2024
1 parent beb6843 commit 7c9f19b
Show file tree
Hide file tree
Showing 10 changed files with 109 additions and 30 deletions.
9 changes: 4 additions & 5 deletions documentation/intermediate-dsl.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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]`.

Expand Down Expand Up @@ -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}`.

Expand Down
2 changes: 2 additions & 0 deletions src/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ export type ArraySlice = {
object: Expression;
start: Expression | null;
second: Expression | null;
penultimate: boolean;
end: Expression | null;
};

Expand Down Expand Up @@ -338,6 +339,7 @@ export type Range = {
type: 'Range';
start: Expression;
second?: Expression;
penultimate: boolean;
end: Expression;
};

Expand Down
28 changes: 26 additions & 2 deletions src/grammars/paren-counter.pegjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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; }
Expand All @@ -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;
Expand Down
20 changes: 13 additions & 7 deletions src/grammars/sonic-weave.pegjs
Original file line number Diff line number Diff line change
Expand Up @@ -799,6 +799,10 @@ LabeledCommaDecimal
return labels.reduce(operatorReducerLite, object);
}
RangeDotsPenultimate = '..' _ penultimate: '<'? {
return !!penultimate;
}
CallTail
= head: (__ '(' _ @ArgumentList _ ')')
tail: (
Expand All @@ -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});
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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,
};
}
Expand Down
17 changes: 16 additions & 1 deletion src/parser/__tests__/expression.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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);
Expand Down
25 changes: 25 additions & 0 deletions src/parser/__tests__/paren-counter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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});
});
});
4 changes: 4 additions & 0 deletions src/parser/__tests__/sonic-weave-ast.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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},
},
});
Expand Down Expand Up @@ -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},
Expand Down Expand Up @@ -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},
Expand Down
7 changes: 6 additions & 1 deletion src/parser/__tests__/stdlib.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
});
Expand All @@ -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']);
});
});
18 changes: 10 additions & 8 deletions src/parser/expression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -852,6 +852,7 @@ export class ExpressionVisitor {

let start = 0;
let step = 1;
const pu = node.penultimate;
let end = -1;

if (node.start) {
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
}
Expand Down Expand Up @@ -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.');
}
Expand All @@ -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);
}
Expand Down
9 changes: 3 additions & 6 deletions src/stdlib/prelude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
}
Expand Down

0 comments on commit 7c9f19b

Please sign in to comment.