From 41fb4c9b31c1fbf391597f6019788aa40b31a20b Mon Sep 17 00:00:00 2001 From: Lumi Pakkanen Date: Mon, 4 Mar 2024 21:34:31 +0200 Subject: [PATCH] Include location in the AST for syntax highlighting --- src/__tests__/parser.spec.ts | 73 ++++++++++++++++++++++++++++++++++ src/__tests__/sw2-ast.spec.ts | 33 +++++++++++++++ src/parser.ts | 61 ++++++++++++++++++++-------- src/sw2.pegjs | 75 ++++++++++++++++++++--------------- 4 files changed, 194 insertions(+), 48 deletions(-) diff --git a/src/__tests__/parser.spec.ts b/src/__tests__/parser.spec.ts index 3b519c2..ab23c59 100644 --- a/src/__tests__/parser.spec.ts +++ b/src/__tests__/parser.spec.ts @@ -4,6 +4,7 @@ import { enumerateChord, parseChord, parseLine as parseLine_, + parsePartialAst, parseScale, reverseParseScale, } from '../parser'; @@ -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}, + }, + }); + }); +}); diff --git a/src/__tests__/sw2-ast.spec.ts b/src/__tests__/sw2-ast.spec.ts index 2f77dbd..e1499e5 100644 --- a/src/__tests__/sw2-ast.spec.ts +++ b/src/__tests__/sw2-ast.spec.ts @@ -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', () => { @@ -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', () => { diff --git a/src/parser.ts b/src/parser.ts index e79eb67..ac1d529 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -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. */ @@ -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 @@ -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. diff --git a/src/sw2.pegjs b/src/sw2.pegjs index a382049..06e3d29 100644 --- a/src/sw2.pegjs +++ b/src/sw2.pegjs @@ -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); } }} @@ -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 @@ -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()) }