Skip to content

Commit

Permalink
Include location in the AST for syntax highlighting
Browse files Browse the repository at this point in the history
  • Loading branch information
frostburn committed Mar 4, 2024
1 parent 18a94a3 commit 41fb4c9
Show file tree
Hide file tree
Showing 4 changed files with 194 additions and 48 deletions.
73 changes: 73 additions & 0 deletions src/__tests__/parser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
enumerateChord,
parseChord,
parseLine as parseLine_,
parsePartialAst,
parseScale,
reverseParseScale,
} from '../parser';
Expand Down Expand Up @@ -462,3 +463,75 @@ describe('Reverse parser', () => {
expect(arraysEqual(lines, ['13/12', '2/1'])).toBeTruthy();
});
});

describe('Scale Workshop 2 partial AST parser', () => {
it('has enough information to highlight parts of a binary operation', () => {
const partialAST = parsePartialAst('3\\5 +1\\7');
expect(partialAST).toEqual({
type: 'BinaryExpression',
operator: '+',
left: {
type: 'EdjiFraction',
numerator: 3,
denominator: 5,
equave: null,
location: {
source: undefined,
start: {offset: 0, line: 1, column: 1},
end: {offset: 3, line: 1, column: 4},
},
},
right: {
type: 'EdjiFraction',
numerator: 1,
denominator: 7,
equave: null,
location: {
source: undefined,
start: {offset: 7, line: 1, column: 8},
end: {offset: 10, line: 1, column: 11},
},
},
location: {
source: undefined,
start: {offset: 0, line: 1, column: 1},
end: {offset: 10, line: 1, column: 11},
},
});
});

it('can recover from errors', () => {
const partialAST = parsePartialAst('3\\5 + 1\\7 * asdf');
expect(partialAST).toEqual({
type: 'BinaryExpression',
operator: '+',
left: {
type: 'EdjiFraction',
numerator: 3,
denominator: 5,
equave: null,
location: {
source: undefined,
start: {offset: 0, line: 1, column: 1},
end: {offset: 3, line: 1, column: 4},
},
},
right: {
type: 'EdjiFraction',
numerator: 1,
denominator: 7,
equave: null,
location: {
source: undefined,
start: {offset: 6, line: 1, column: 7},
end: {offset: 9, line: 1, column: 10},
},
},
location: {
source: undefined,
start: {offset: 0, line: 1, column: 1},
end: {offset: 10, line: 1, column: 11},
},
});
});
});
33 changes: 33 additions & 0 deletions src/__tests__/sw2-ast.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ describe('Scale Workshop 2 Abstract Syntax Tree Parser', () => {
expect(ast.type).toBe('CentsLiteral');
expect(ast.whole).toBe(81);
expect(ast.fractional).toBe('80');
expect(ast.location).toEqual({
source: undefined,
start: {offset: 0, line: 1, column: 1},
end: {offset: 5, line: 1, column: 6},
});
});

it('parses comma-separated numbers as numeric literals', () => {
Expand Down Expand Up @@ -86,6 +91,34 @@ describe('Scale Workshop 2 Abstract Syntax Tree Parser', () => {
const ast = parse('2 + 1.23');
expect(ast.type).toBe('BinaryExpression');
expect(ast.operator).toBe('+');
expect(ast).toEqual({
type: 'BinaryExpression',
operator: '+',
left: {
type: 'PlainLiteral',
value: 2,
location: {
source: undefined,
start: {offset: 0, line: 1, column: 1},
end: {offset: 1, line: 1, column: 2},
},
},
right: {
type: 'CentsLiteral',
whole: 1,
fractional: '23',
location: {
source: undefined,
start: {offset: 4, line: 1, column: 5},
end: {offset: 8, line: 1, column: 9},
},
},
location: {
source: undefined,
start: {offset: 0, line: 1, column: 1},
end: {offset: 8, line: 1, column: 9},
},
});
});

it('parses binary subtracted numbers and cents', () => {
Expand Down
61 changes: 45 additions & 16 deletions src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,27 @@ import {Fraction, PRIMES, PRIME_CENTS} from 'xen-dev-utils';
import {Scale} from './scale';
import {parse} from './sw2-ast';

/** Provides information pointing to a location within a source. */
export interface Location {
/** Line in the parsed source (1-based). */
line: number;
/** Column in the parsed source (1-based). */
column: number;
/** Offset in the parsed source (0-based). */
offset: number;
}

/** The `start` and `end` position's of an object within the source. */
export interface LocationRange {
/** Any object that was supplied to the `parse()` call as the `grammarSource` option. */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
source: any;
/** Position at the beginning of the expression. */
start: Location;
/** Position after the end of the expression. */
end: Location;
}

/**
* The types of intervals strings can represent.
*/
Expand All @@ -19,54 +40,58 @@ export enum LINE_TYPE {
INVALID = 'invalid',
}

type Node = {
location: LocationRange;
};

// Abstract Syntax Tree hierarchy
type PlainLiteral = {
interface PlainLiteral extends Node {
type: 'PlainLiteral';
value: number;
};
}

type CentsLiteral = {
interface CentsLiteral extends Node {
type: 'CentsLiteral';
whole: number | null;
fractional: string | null;
};
}

type NumericLiteral = {
interface NumericLiteral extends Node {
type: 'NumericLiteral';
whole: number | null;
fractional: string | null;
};
}

type FractionLiteral = {
interface FractionLiteral extends Node {
type: 'FractionLiteral';
numerator: number;
denominator: number;
};
}

type EdjiFraction = {
interface EdjiFraction extends Node {
type: 'EdjiFraction';
numerator?: number;
denominator: number;
equave: null | PlainLiteral | FractionLiteral;
};
}

type Monzo = {
interface Monzo extends Node {
type: 'Monzo';
components: string[];
};
}

type UnaryExpression = {
interface UnaryExpression extends Node {
type: 'UnaryExpression';
operator: '-';
operand: Expression;
};
}

type BinaryExpression = {
interface BinaryExpression extends Node {
type: 'BinaryExpression';
operator: '+' | '-';
left: Expression;
right: Expression;
};
}

type Expression =
| PlainLiteral
Expand All @@ -82,6 +107,10 @@ function parseAst(input: string): Expression {
return parse(input);
}

export function parsePartialAst(input: string): Expression {
return parse(input, {peg$library: true}).peg$result;
}

/**
* Determine the type of interval a string represents.
* @param input String to analyze.
Expand Down
75 changes: 43 additions & 32 deletions src/sw2.pegjs
Original file line number Diff line number Diff line change
@@ -1,73 +1,81 @@
{{
function PlainLiteral(value) {
function PlainLiteral(value, location) {
return {
type: 'PlainLiteral',
value
}
value,
location,
};
}

function CentsLiteral(whole, fractional) {
function CentsLiteral(whole, fractional, location) {
return {
type: 'CentsLiteral',
whole,
fractional
}
fractional,
location,
};
}

function NumericLiteral(whole, fractional) {
function NumericLiteral(whole, fractional, location) {
return {
type: 'NumericLiteral',
whole,
fractional
}
fractional,
location,
};
}

function FractionLiteral(numerator, denominator) {
function FractionLiteral(numerator, denominator, location) {
return {
type: 'FractionLiteral',
numerator,
denominator
}
denominator,
location,
};
}

function EdjiFraction(numerator, denominator, equave) {
function EdjiFraction(numerator, denominator, equave, location) {
return {
type: 'EdjiFraction',
numerator,
denominator,
equave
}
equave,
location,
};
}

function Monzo(components) {
function Monzo(components, location) {
return {
type: 'Monzo',
components
components,
location,
}
}

function BinaryExpression(operator, left, right) {
function BinaryExpression(operator, left, right, location) {
return {
type: 'BinaryExpression',
operator,
left,
right
}
right,
location,
};
}

function UnaryExpression(operator, operand) {
function UnaryExpression(operator, operand, location) {
return {
type: 'UnaryExpression',
operator,
operand
operand,
location,
}
}

function operatorReducer (result, element) {
function operatorReducer (result, element, location) {
const left = result;
const [op, right] = element;

return BinaryExpression(op, left, right);
return BinaryExpression(op, left, right, location);
}
}}

Expand Down Expand Up @@ -97,7 +105,8 @@ _ = Whitespace*

Expression
= head:Term tail:(_ @('+' / '-') _ @Term)* {
return tail.reduce(operatorReducer, head);
const loc = location();
return tail.reduce((result, element) => operatorReducer(result, element, loc), head);
}

Term
Expand All @@ -117,28 +126,30 @@ SignedInteger
= sign:'-'? value:Integer { return sign ? -value : value }

DotDecimal
= whole:Integer? '.' fractional:$[0-9]* { return CentsLiteral(whole, fractional) }
= whole:Integer? '.' fractional:$[0-9]* { return CentsLiteral(whole, fractional, location()) }

CommaDecimal
= whole:Integer? ',' fractional:$[0-9]* { return NumericLiteral(whole, fractional) }
= whole:Integer? ',' fractional:$[0-9]* { return NumericLiteral(whole, fractional, location()) }

SlashFraction
= numerator:Integer '/' denominator:Integer { return FractionLiteral(numerator, denominator) }
= numerator:Integer '/' denominator:Integer { return FractionLiteral(numerator, denominator, location()) }

PlainNumber
= value:Integer { return PlainLiteral(value) }
= value:Integer { return PlainLiteral(value, location()) }

EquaveExpression
= '<' _ @(SlashFraction / PlainNumber) _ '>'

BackslashFraction
= numerator:Integer? '\\' denominator:SignedInteger equave:EquaveExpression? { return EdjiFraction(numerator, denominator, equave) }
= numerator:Integer? '\\' denominator:SignedInteger equave:EquaveExpression? {
return EdjiFraction(numerator, denominator, equave, location());
}

Component
= $([+-]? (SlashFraction / PlainNumber))

Monzo
= '[' components:Component|.., _ ','? _| '>' { return Monzo(components) }
= '[' components:Component|.., _ ','? _| '>' { return Monzo(components, location()) }

UnaryExpression
= operator:'-' operand:Primary { return UnaryExpression(operator, operand) }
= operator:'-' operand:Primary { return UnaryExpression(operator, operand, location()) }

0 comments on commit 41fb4c9

Please sign in to comment.