mirror of
https://github.com/alda-lang/alda.git
synced 2026-03-03 18:23:36 +01:00
io/ioutil has been deprecated since Go 1.16 https://pkg.go.dev/io/ioutil Signed-off-by: ginglis13 <ginglis05@gmail.com>
1145 lines
27 KiB
Go
1145 lines
27 KiB
Go
package parser
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
|
|
"alda.io/client/color"
|
|
"alda.io/client/help"
|
|
log "alda.io/client/logging"
|
|
"alda.io/client/model"
|
|
)
|
|
|
|
type parser struct {
|
|
filename string
|
|
input []Token
|
|
current int
|
|
// When true, source context is _not_ included in parsed tokens. This is
|
|
// useful for testing, e.g. for checking the equality of a list of expected
|
|
// tokens, agnostic of source context like line and column numbers.
|
|
suppressSourceContext bool
|
|
}
|
|
|
|
// A parseOption is a function that customizes a parser instance.
|
|
type parseOption func(*parser)
|
|
|
|
// SuppressSourceContext customizes a parser to ignore source context
|
|
func SuppressSourceContext(parser *parser) {
|
|
parser.suppressSourceContext = true
|
|
}
|
|
|
|
func (p *parser) sourceContext(token Token) model.AldaSourceContext {
|
|
if p.suppressSourceContext {
|
|
return model.AldaSourceContext{}
|
|
}
|
|
|
|
return token.sourceContext
|
|
}
|
|
|
|
func newParser(filename string, tokens []Token, opts ...parseOption) *parser {
|
|
parser := &parser{
|
|
filename: filename,
|
|
input: tokens,
|
|
current: 0,
|
|
}
|
|
|
|
for _, opt := range opts {
|
|
opt(parser)
|
|
}
|
|
|
|
return parser
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
func (p *parser) peek() Token {
|
|
return p.input[p.current]
|
|
}
|
|
|
|
func (p *parser) previous() Token {
|
|
return p.input[p.current-1]
|
|
}
|
|
|
|
// CAUTION: This will panic if the _current_ token is EOF, because that means
|
|
// there is no next token, so the index will be out of bounds.
|
|
func (p *parser) next() Token {
|
|
return p.input[p.current+1]
|
|
}
|
|
|
|
func (p *parser) check(types ...TokenType) bool {
|
|
for _, tokenType := range types {
|
|
if p.peek().tokenType == tokenType {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (p *parser) advance() Token {
|
|
if !p.check(EOF) {
|
|
p.current++
|
|
}
|
|
|
|
return p.previous()
|
|
}
|
|
|
|
func (p *parser) match(types ...TokenType) (Token, bool) {
|
|
for _, tokenType := range types {
|
|
if p.check(tokenType) {
|
|
return p.advance(), true
|
|
}
|
|
}
|
|
|
|
return Token{}, false
|
|
}
|
|
|
|
func (p *parser) errorAtToken(token Token, msg string) *model.AldaSourceError {
|
|
return &model.AldaSourceError{
|
|
Context: token.sourceContext,
|
|
Err: fmt.Errorf("%s", msg),
|
|
}
|
|
}
|
|
|
|
func (p *parser) unexpectedTokenError(
|
|
token Token, context string,
|
|
) *model.AldaSourceError {
|
|
if context != "" {
|
|
context = " " + context
|
|
}
|
|
|
|
tokenText := ""
|
|
if len(token.text) > 0 {
|
|
tokenText = fmt.Sprintf(" `%s`", token.text)
|
|
}
|
|
|
|
msg := fmt.Sprintf(
|
|
"Unexpected %s%s%s", token.tokenType.String(), tokenText, context,
|
|
)
|
|
|
|
return p.errorAtToken(token, msg)
|
|
}
|
|
|
|
func (p *parser) consume(tokenType TokenType, context string) (Token, error) {
|
|
if p.check(tokenType) {
|
|
return p.advance(), nil
|
|
}
|
|
|
|
return Token{}, p.unexpectedTokenError(p.peek(), context)
|
|
}
|
|
|
|
func (p *parser) lispForm(context string) (ASTNode, error) {
|
|
if token, matched := p.match(Symbol); matched {
|
|
return ASTNode{
|
|
Type: LispSymbolNode,
|
|
SourceContext: p.sourceContext(token),
|
|
Literal: token.text,
|
|
}, nil
|
|
}
|
|
|
|
if token, matched := p.match(Number); matched {
|
|
return ASTNode{
|
|
Type: LispNumberNode,
|
|
SourceContext: p.sourceContext(token),
|
|
Literal: token.literal,
|
|
}, nil
|
|
}
|
|
|
|
if token, matched := p.match(String); matched {
|
|
return ASTNode{
|
|
Type: LispStringNode,
|
|
SourceContext: p.sourceContext(token),
|
|
Literal: token.literal,
|
|
}, nil
|
|
}
|
|
|
|
if _, matched := p.match(LeftParen); matched {
|
|
return p.lispList()
|
|
}
|
|
|
|
return ASTNode{}, p.unexpectedTokenError(p.peek(), context)
|
|
}
|
|
|
|
func (p *parser) lispList() (ASTNode, error) {
|
|
// NB: This assumes the initial LeftParen token was already consumed.
|
|
list := ASTNode{
|
|
SourceContext: p.sourceContext(p.previous()),
|
|
Type: LispListNode,
|
|
}
|
|
|
|
for token := p.peek(); token.tokenType != RightParen; token = p.peek() {
|
|
if _, matched := p.match(EOF); matched {
|
|
return ASTNode{}, p.errorAtToken(token, "unterminated S-expression")
|
|
}
|
|
|
|
quoteToken, quoted := p.match(SingleQuote)
|
|
|
|
form, err := p.lispForm("in S-expression")
|
|
if err != nil {
|
|
return ASTNode{}, err
|
|
}
|
|
|
|
if quoted {
|
|
form = ASTNode{
|
|
Type: LispQuotedFormNode,
|
|
SourceContext: p.sourceContext(quoteToken),
|
|
Children: []ASTNode{form},
|
|
}
|
|
}
|
|
|
|
list.Children = append(list.Children, form)
|
|
}
|
|
|
|
if _, err := p.consume(RightParen, "in S-expression"); err != nil {
|
|
return ASTNode{}, err
|
|
}
|
|
|
|
return list, nil
|
|
}
|
|
|
|
func (p *parser) sexp() (ASTNode, error) {
|
|
// NB: This assumes the initial LeftParen token was already consumed.
|
|
list, err := p.lispList()
|
|
if err != nil {
|
|
return ASTNode{}, err
|
|
}
|
|
|
|
return p.singleOrRepeated(list), nil
|
|
}
|
|
|
|
func (p *parser) partDeclaration() (ASTNode, error) {
|
|
nameNode := func(nameToken Token) ASTNode {
|
|
return ASTNode{
|
|
Type: PartNameNode,
|
|
SourceContext: p.sourceContext(nameToken),
|
|
Literal: nameToken.text,
|
|
}
|
|
}
|
|
|
|
// NB: This assumes the initial Name token was already consumed.
|
|
nameToken := p.previous()
|
|
|
|
namesNode := ASTNode{
|
|
Type: PartNamesNode,
|
|
SourceContext: p.sourceContext(nameToken),
|
|
Children: []ASTNode{nameNode(nameToken)},
|
|
}
|
|
|
|
for {
|
|
if _, matched := p.match(Separator); !matched {
|
|
break
|
|
}
|
|
|
|
name, err := p.consume(Name, "in part declaration")
|
|
if err != nil {
|
|
return ASTNode{}, err
|
|
}
|
|
|
|
namesNode.Children = append(namesNode.Children, nameNode(name))
|
|
}
|
|
|
|
partDecl := ASTNode{
|
|
Type: PartDeclarationNode,
|
|
SourceContext: p.sourceContext(nameToken),
|
|
Children: []ASTNode{namesNode},
|
|
}
|
|
|
|
if alias, matched := p.match(Alias); matched {
|
|
partDecl.Children = append(
|
|
partDecl.Children,
|
|
ASTNode{
|
|
Type: PartAliasNode,
|
|
SourceContext: p.sourceContext(alias),
|
|
Literal: alias.literal,
|
|
},
|
|
)
|
|
}
|
|
|
|
if _, err := p.consume(Colon, "in part declaration"); err != nil {
|
|
return ASTNode{}, err
|
|
}
|
|
|
|
return partDecl, nil
|
|
}
|
|
|
|
func (p *parser) looksLikePartDeclaration() bool {
|
|
current := p.peek().tokenType
|
|
|
|
if current == EOF {
|
|
return false
|
|
}
|
|
|
|
next := p.next().tokenType
|
|
|
|
return current == Name &&
|
|
(next == Alias || next == Separator || next == Colon)
|
|
}
|
|
|
|
func (p *parser) partEvents() (ASTNode, error) {
|
|
partEvents := ASTNode{
|
|
Type: EventSequenceNode,
|
|
SourceContext: p.sourceContext(p.peek()),
|
|
Children: []ASTNode{},
|
|
}
|
|
|
|
// Keep consuming events until we reach either a part declaration or EOF.
|
|
for !p.check(EOF) && !p.looksLikePartDeclaration() {
|
|
event, err := p.innerEvent()
|
|
if err != nil {
|
|
return ASTNode{}, err
|
|
}
|
|
|
|
partEvents.Children = append(partEvents.Children, event)
|
|
}
|
|
|
|
return partEvents, nil
|
|
}
|
|
|
|
func (p *parser) part() (ASTNode, error) {
|
|
// NB: This assumes the initial Name token was already consumed.
|
|
nameToken := p.previous()
|
|
|
|
partDecl, err := p.partDeclaration()
|
|
if err != nil {
|
|
return ASTNode{}, err
|
|
}
|
|
|
|
partEvents, err := p.partEvents()
|
|
if err != nil {
|
|
return ASTNode{}, err
|
|
}
|
|
|
|
return ASTNode{
|
|
Type: PartNode,
|
|
SourceContext: p.sourceContext(nameToken),
|
|
Children: []ASTNode{partDecl, partEvents},
|
|
}, nil
|
|
}
|
|
|
|
// An "implicit part" is an AST node that can contain:
|
|
//
|
|
// - Initial variable definitions at the top of the file
|
|
// - Initial S-expressions at the top of the file, like global attributes
|
|
// - Events (see `innerEvent`) without a part definition, in the context of
|
|
// continuing a previous score, e.g. in REPL input.
|
|
func (p *parser) implicitPart() (ASTNode, error) {
|
|
partEvents, err := p.partEvents()
|
|
if err != nil {
|
|
return ASTNode{}, err
|
|
}
|
|
|
|
return ASTNode{
|
|
Type: ImplicitPartNode,
|
|
SourceContext: partEvents.SourceContext,
|
|
Children: []ASTNode{partEvents},
|
|
}, nil
|
|
}
|
|
|
|
func (p *parser) variableDefinition() (ASTNode, error) {
|
|
// NB: This assumes the initial Name token was already consumed.
|
|
nameToken := p.previous()
|
|
|
|
nameNode := ASTNode{
|
|
Type: VariableNameNode,
|
|
SourceContext: p.sourceContext(nameToken),
|
|
Literal: nameToken.text,
|
|
}
|
|
|
|
definitionLine := nameToken.sourceContext.Line
|
|
|
|
equalsToken, err := p.consume(Equals, "in variable definition")
|
|
if err != nil {
|
|
return ASTNode{}, err
|
|
}
|
|
|
|
if p.check(EOF) || p.peek().sourceContext.Line > definitionLine {
|
|
return ASTNode{}, &model.AldaSourceError{
|
|
Context: nameToken.sourceContext,
|
|
Err: fmt.Errorf(
|
|
"there must be at least one event on the same line as the '='",
|
|
),
|
|
}
|
|
}
|
|
|
|
eventsNode := ASTNode{
|
|
Type: EventSequenceNode,
|
|
SourceContext: p.sourceContext(p.peek()),
|
|
Children: []ASTNode{},
|
|
}
|
|
|
|
for t := p.peek(); t.sourceContext.Line == definitionLine; t = p.peek() {
|
|
if t.tokenType == EOF {
|
|
break
|
|
}
|
|
|
|
node, err := p.innerEvent()
|
|
if err != nil {
|
|
return ASTNode{}, err
|
|
}
|
|
|
|
eventsNode.Children = append(eventsNode.Children, node)
|
|
}
|
|
|
|
definitionNode := ASTNode{
|
|
Type: VariableDefinitionNode,
|
|
SourceContext: p.sourceContext(equalsToken),
|
|
Children: []ASTNode{nameNode, eventsNode},
|
|
}
|
|
|
|
return definitionNode, nil
|
|
}
|
|
|
|
func (p *parser) singleOrRepeated(node ASTNode) ASTNode {
|
|
if token, matched := p.match(Repeat); matched {
|
|
return ASTNode{
|
|
SourceContext: p.sourceContext(token),
|
|
Type: RepeatNode,
|
|
Children: []ASTNode{
|
|
node,
|
|
{
|
|
Type: TimesNode,
|
|
Literal: token.literal,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
return node
|
|
}
|
|
|
|
func (p *parser) variableReference() (ASTNode, error) {
|
|
// NB: This assumes the initial Name token was already consumed.
|
|
nameToken := p.previous()
|
|
|
|
reference := ASTNode{
|
|
Type: VariableReferenceNode,
|
|
SourceContext: p.sourceContext(nameToken),
|
|
Literal: nameToken.text,
|
|
}
|
|
|
|
return p.singleOrRepeated(reference), nil
|
|
}
|
|
|
|
func (p *parser) variableDefinitionOrReference() (ASTNode, error) {
|
|
// NB: This assumes the initial Name token was already consumed.
|
|
if p.check(Equals) {
|
|
return p.variableDefinition()
|
|
}
|
|
|
|
return p.variableReference()
|
|
}
|
|
|
|
func (p *parser) octaveSet() (ASTNode, error) {
|
|
// NB: This assumes the OctaveSet token was already consumed.
|
|
token := p.previous()
|
|
|
|
return ASTNode{
|
|
Type: OctaveSetNode,
|
|
SourceContext: p.sourceContext(token),
|
|
Literal: token.literal,
|
|
}, nil
|
|
}
|
|
|
|
func (p *parser) matchDurationComponent() (Token, bool) {
|
|
return p.match(NoteLength, NoteLengthMs, NoteLengthSeconds)
|
|
}
|
|
|
|
func (p *parser) durationComponent() ASTNode {
|
|
// NB: This assumes the duration component token was already consumed.
|
|
token := p.previous()
|
|
|
|
switch token.tokenType {
|
|
case NoteLength:
|
|
noteLength := token.literal.(noteLength)
|
|
|
|
nlNode := ASTNode{
|
|
Type: NoteLengthNode,
|
|
SourceContext: p.sourceContext(token),
|
|
Children: []ASTNode{
|
|
{
|
|
Type: DenominatorNode,
|
|
Literal: noteLength.denominator,
|
|
},
|
|
},
|
|
}
|
|
|
|
if noteLength.dots > 0 {
|
|
nlNode.Children = append(nlNode.Children, ASTNode{
|
|
Type: DotsNode,
|
|
Literal: noteLength.dots,
|
|
})
|
|
}
|
|
|
|
return nlNode
|
|
case NoteLengthMs:
|
|
return ASTNode{
|
|
Type: NoteLengthMsNode,
|
|
SourceContext: p.sourceContext(token),
|
|
Literal: token.literal,
|
|
}
|
|
case NoteLengthSeconds:
|
|
return ASTNode{
|
|
Type: NoteLengthSecondsNode,
|
|
SourceContext: p.sourceContext(token),
|
|
Literal: token.literal,
|
|
}
|
|
}
|
|
|
|
// We shouldn't get here.
|
|
return ASTNode{}
|
|
}
|
|
|
|
func (p *parser) duration() ASTNode {
|
|
durationNode := func(componentNodes []ASTNode) ASTNode {
|
|
return ASTNode{
|
|
Type: DurationNode,
|
|
SourceContext: componentNodes[0].SourceContext,
|
|
Children: componentNodes,
|
|
}
|
|
}
|
|
|
|
barlineNode := func(token Token) ASTNode {
|
|
return ASTNode{
|
|
Type: BarlineNode,
|
|
SourceContext: p.sourceContext(token),
|
|
}
|
|
}
|
|
|
|
// NB: This assumes the initial duration component token was already consumed.
|
|
componentNodes := []ASTNode{p.durationComponent()}
|
|
|
|
// Repeatedly parse duration components.
|
|
for {
|
|
// Repeatedly parse barlines amongst the duration components.
|
|
for {
|
|
if token, matched := p.match(Barline); matched {
|
|
componentNodes = append(componentNodes, barlineNode(token))
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
|
|
// Take note of the current position. If we encounter a tie that ends up
|
|
// actually being a slur (i.e. it isn't followed by a note length), we can
|
|
// backtrack to this position, return the duration, and let the parser
|
|
// consume the slur as part of e.g. a note.
|
|
beforeTies := p.current
|
|
|
|
if _, matched := p.match(Tie); !matched {
|
|
return durationNode(componentNodes)
|
|
}
|
|
|
|
// We'll stash any barlines that we encounter here temporarily. We'll add
|
|
// them to the duration components iff we aren't going to backtrack and
|
|
// consume them outside of the duration.
|
|
barlines := []ASTNode{}
|
|
|
|
for {
|
|
if token, matched := p.match(Barline); matched {
|
|
barlines = append(barlines, barlineNode(token))
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
|
|
// In some cases, it makes sense to have extraneous ties, e.g. when you're
|
|
// tying a duration across a barline and it feels right to have a tie on
|
|
// either side of the barline. So we'll consume any additional ties here.
|
|
for {
|
|
if _, matched := p.match(Tie); !matched {
|
|
break
|
|
}
|
|
}
|
|
|
|
if _, matched := p.matchDurationComponent(); !matched {
|
|
p.current = beforeTies
|
|
return durationNode(componentNodes)
|
|
}
|
|
|
|
componentNodes = append(componentNodes, barlines...)
|
|
componentNodes = append(componentNodes, p.durationComponent())
|
|
}
|
|
}
|
|
|
|
func (p *parser) note() (ASTNode, error) {
|
|
// NB: This assumes the initial NoteLetter token was already consumed.
|
|
noteLetterToken := p.previous()
|
|
|
|
laaNode := ASTNode{
|
|
Type: NoteLetterAndAccidentalsNode,
|
|
SourceContext: p.sourceContext(noteLetterToken),
|
|
Children: []ASTNode{
|
|
{
|
|
Type: NoteLetterNode,
|
|
SourceContext: p.sourceContext(noteLetterToken),
|
|
Literal: noteLetterToken.literal,
|
|
},
|
|
},
|
|
}
|
|
|
|
accidentalNodes := []ASTNode{}
|
|
|
|
AccidentalsLoop:
|
|
for {
|
|
if token, matched := p.match(Flat); matched {
|
|
accidentalNodes = append(accidentalNodes, ASTNode{
|
|
Type: FlatNode,
|
|
SourceContext: p.sourceContext(token),
|
|
})
|
|
} else if token, matched := p.match(Natural); matched {
|
|
accidentalNodes = append(accidentalNodes, ASTNode{
|
|
Type: NaturalNode,
|
|
SourceContext: p.sourceContext(token),
|
|
})
|
|
} else if token, matched := p.match(Sharp); matched {
|
|
accidentalNodes = append(accidentalNodes, ASTNode{
|
|
Type: SharpNode,
|
|
SourceContext: p.sourceContext(token),
|
|
})
|
|
} else {
|
|
break AccidentalsLoop
|
|
}
|
|
}
|
|
|
|
if len(accidentalNodes) > 0 {
|
|
laaNode.Children = append(laaNode.Children, ASTNode{
|
|
Type: NoteAccidentalsNode,
|
|
SourceContext: accidentalNodes[0].SourceContext,
|
|
Children: accidentalNodes,
|
|
})
|
|
}
|
|
|
|
noteNode := ASTNode{
|
|
Type: NoteNode,
|
|
SourceContext: p.sourceContext(noteLetterToken),
|
|
Children: []ASTNode{laaNode},
|
|
}
|
|
|
|
if _, matched := p.matchDurationComponent(); matched {
|
|
noteNode.Children = append(noteNode.Children, p.duration())
|
|
}
|
|
|
|
if tie, matched := p.match(Tie); matched {
|
|
noteNode.Children = append(noteNode.Children, ASTNode{
|
|
Type: TieNode,
|
|
SourceContext: p.sourceContext(tie),
|
|
})
|
|
}
|
|
|
|
return noteNode, nil
|
|
}
|
|
|
|
func (p *parser) rest() ASTNode {
|
|
// NB: This assumes the initial RestLetter token was already consumed.
|
|
token := p.previous()
|
|
|
|
rest := ASTNode{
|
|
Type: RestNode,
|
|
SourceContext: p.sourceContext(token),
|
|
}
|
|
|
|
if _, matched := p.matchDurationComponent(); matched {
|
|
rest.Children = []ASTNode{p.duration()}
|
|
}
|
|
|
|
return rest
|
|
}
|
|
|
|
func (p *parser) noteOrRest() (ASTNode, error) {
|
|
// NB: This assumes the initial NoteLetter/RestLetter was already consumed.
|
|
switch letter := p.previous(); letter.tokenType {
|
|
case NoteLetter:
|
|
return p.note()
|
|
case RestLetter:
|
|
return p.rest(), nil
|
|
default:
|
|
return ASTNode{}, p.unexpectedTokenError(letter, "in note/rest")
|
|
}
|
|
}
|
|
|
|
func (p *parser) nodesBetweenNotesInChord() ([]ASTNode, error) {
|
|
updates := []ASTNode{}
|
|
|
|
for {
|
|
if token, matched := p.match(OctaveUp); matched {
|
|
updates = append(updates, ASTNode{
|
|
Type: OctaveUpNode,
|
|
SourceContext: p.sourceContext(token),
|
|
})
|
|
} else if token, matched := p.match(OctaveDown); matched {
|
|
updates = append(updates, ASTNode{
|
|
Type: OctaveDownNode,
|
|
SourceContext: p.sourceContext(token),
|
|
})
|
|
} else if _, matched := p.match(OctaveSet); matched {
|
|
octaveSetNode, err := p.octaveSet()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
updates = append(updates, octaveSetNode)
|
|
} else if _, matched := p.match(LeftParen); matched {
|
|
sexp, err := p.lispList()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
updates = append(updates, sexp)
|
|
} else {
|
|
return updates, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// Parses a note or chord. A chord contains multiple chords and rests, not to
|
|
// mention attribute changes, so any of those will be parsed too in the process.
|
|
func (p *parser) noteRestOrChord() (ASTNode, error) {
|
|
// NB: This assumes the initial NoteLetter/RestLetter was already consumed.
|
|
|
|
// The cumulative list of nodes. Depending on whether this is a chord, the
|
|
// nodes will either be emitted as part of the chord, or emitted individually.
|
|
allNodes := []ASTNode{}
|
|
|
|
type maybeRepeat struct {
|
|
sourceContext model.AldaSourceContext
|
|
times int32
|
|
}
|
|
|
|
// We're essentially using this as a nil value. Below, we check whether
|
|
// `repeat.times` > 0, which will be true if we don't reassign `repeat`.
|
|
repeat := maybeRepeat{}
|
|
|
|
for {
|
|
noteOrRest, err := p.noteOrRest()
|
|
if err != nil {
|
|
return ASTNode{}, err
|
|
}
|
|
|
|
if token, matched := p.match(Repeat); matched {
|
|
allNodes = append(allNodes, noteOrRest)
|
|
repeat = maybeRepeat{
|
|
sourceContext: p.sourceContext(token),
|
|
times: token.literal.(int32),
|
|
}
|
|
break
|
|
}
|
|
|
|
// The nodes for just this iteration of the loop
|
|
nodes := []ASTNode{noteOrRest}
|
|
|
|
// HACK to work around the complexity that comes with allowing chords to
|
|
// include attribute changes in between the notes. This is all easier if we
|
|
// can make this function return a single node and not a list of nodes (see
|
|
// how we are using it in `innerEvent`.) That's easy enough if the result of
|
|
// the parsing that this function does is a single node, like a note, a
|
|
// rest, or a chord. However, if we start to parse a chord, and we parse
|
|
// something like:
|
|
//
|
|
// * note, (e.g. `a4`)
|
|
// * octave up (`>`)
|
|
// * something besides `/`
|
|
//
|
|
// Then we just want this function to return the note, and we want to
|
|
// backtrack so that the next invocation of `innerEvent` can parse the
|
|
// octave up and all subsequent events separately.
|
|
//
|
|
// Here, we capture the current parser position after we parse each note or
|
|
// rest in the chord, so that we can backtrack to that position if needed.
|
|
backtrackPosition := p.current
|
|
|
|
nodesBeforeSeparator, err := p.nodesBetweenNotesInChord()
|
|
if err != nil {
|
|
return ASTNode{}, err
|
|
}
|
|
|
|
if _, matched := p.match(Separator); !matched {
|
|
// See HACK comment above.
|
|
p.current = backtrackPosition
|
|
|
|
allNodes = append(allNodes, nodes...)
|
|
|
|
break
|
|
}
|
|
|
|
nodes = append(nodes, nodesBeforeSeparator...)
|
|
|
|
nodesAfterSeparator, err := p.nodesBetweenNotesInChord()
|
|
if err != nil {
|
|
return ASTNode{}, err
|
|
}
|
|
nodes = append(nodes, nodesAfterSeparator...)
|
|
|
|
allNodes = append(allNodes, nodes...)
|
|
|
|
if _, matched := p.match(NoteLetter, RestLetter); !matched {
|
|
return ASTNode{}, p.unexpectedTokenError(p.peek(), "in chord")
|
|
}
|
|
}
|
|
|
|
notesCount := 0
|
|
for _, node := range allNodes {
|
|
switch node.Type {
|
|
case NoteNode, RestNode:
|
|
notesCount++
|
|
}
|
|
}
|
|
|
|
if notesCount > 1 {
|
|
allNodes = []ASTNode{
|
|
{
|
|
Type: ChordNode,
|
|
SourceContext: allNodes[0].SourceContext,
|
|
Children: allNodes,
|
|
},
|
|
}
|
|
}
|
|
|
|
if repeat.times > 0 {
|
|
if len(allNodes) != 1 {
|
|
panic(fmt.Sprintf("Expected a single node in %#v", allNodes))
|
|
}
|
|
|
|
return ASTNode{
|
|
SourceContext: repeat.sourceContext,
|
|
Type: RepeatNode,
|
|
Children: []ASTNode{
|
|
allNodes[0],
|
|
{
|
|
Type: TimesNode,
|
|
Literal: repeat.times,
|
|
},
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
return allNodes[0], nil
|
|
}
|
|
|
|
func (p *parser) eventSeq() (ASTNode, error) {
|
|
// NB: This assumes the initial EventSeqOpen token was already consumed.
|
|
eventSeqOpenToken := p.previous()
|
|
|
|
eventNodes := []ASTNode{}
|
|
|
|
for token := p.peek(); token.tokenType != EventSeqClose; token = p.peek() {
|
|
if _, matched := p.match(EOF); matched {
|
|
return ASTNode{}, p.errorAtToken(token, "unterminated event sequence")
|
|
}
|
|
|
|
eventNode, err := p.innerEvent()
|
|
if err != nil {
|
|
return ASTNode{}, err
|
|
}
|
|
|
|
if token, matched := p.match(Repetitions); matched {
|
|
repetitionRanges := token.literal.([]model.RepetitionRange)
|
|
|
|
repetitionsNode := ASTNode{
|
|
Type: RepetitionsNode,
|
|
SourceContext: p.sourceContext(token),
|
|
}
|
|
|
|
for _, repetitionRange := range repetitionRanges {
|
|
repetitionsNode.Children = append(repetitionsNode.Children, ASTNode{
|
|
Type: RepetitionRangeNode,
|
|
Children: []ASTNode{
|
|
{
|
|
Type: FirstRepetitionNode,
|
|
Literal: repetitionRange.First,
|
|
},
|
|
{
|
|
Type: LastRepetitionNode,
|
|
Literal: repetitionRange.Last,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
eventNode = ASTNode{
|
|
Type: OnRepetitionsNode,
|
|
SourceContext: p.sourceContext(token),
|
|
Children: []ASTNode{eventNode, repetitionsNode},
|
|
}
|
|
}
|
|
|
|
eventNodes = append(eventNodes, eventNode)
|
|
}
|
|
|
|
if _, err := p.consume(EventSeqClose, "in event sequence"); err != nil {
|
|
return ASTNode{}, err
|
|
}
|
|
|
|
eventSeq := ASTNode{
|
|
Type: EventSequenceNode,
|
|
SourceContext: p.sourceContext(eventSeqOpenToken),
|
|
Children: eventNodes,
|
|
}
|
|
|
|
return p.singleOrRepeated(eventSeq), nil
|
|
}
|
|
|
|
func (p *parser) cram() (ASTNode, error) {
|
|
// NB: This assumes the initial CramOpen token was already consumed.
|
|
cramOpenToken := p.previous()
|
|
|
|
allEvents := []ASTNode{}
|
|
|
|
for token := p.peek(); token.tokenType != CramClose; token = p.peek() {
|
|
if _, matched := p.match(EOF); matched {
|
|
return ASTNode{}, p.errorAtToken(token, "unterminated cram expression")
|
|
}
|
|
|
|
event, err := p.innerEvent()
|
|
if err != nil {
|
|
return ASTNode{}, err
|
|
}
|
|
allEvents = append(allEvents, event)
|
|
}
|
|
|
|
if _, err := p.consume(CramClose, "in cram expression"); err != nil {
|
|
return ASTNode{}, err
|
|
}
|
|
|
|
eventsNode := ASTNode{
|
|
Type: EventSequenceNode,
|
|
SourceContext: allEvents[0].SourceContext,
|
|
Children: allEvents,
|
|
}
|
|
|
|
cram := ASTNode{
|
|
Type: CramNode,
|
|
SourceContext: p.sourceContext(cramOpenToken),
|
|
Children: []ASTNode{eventsNode},
|
|
}
|
|
|
|
if _, matched := p.matchDurationComponent(); matched {
|
|
cram.Children = append(cram.Children, p.duration())
|
|
}
|
|
|
|
return p.singleOrRepeated(cram), nil
|
|
}
|
|
|
|
func (p *parser) voiceNumber() (ASTNode, error) {
|
|
// NB: This assumes the VoiceMarker token was already consumed.
|
|
token := p.previous()
|
|
|
|
return ASTNode{
|
|
Type: VoiceNumberNode,
|
|
SourceContext: p.sourceContext(token),
|
|
Literal: token.literal.(int32),
|
|
}, nil
|
|
}
|
|
|
|
func (p *parser) voice() (ASTNode, error) {
|
|
// NB: This assumes the VoiceMarker token was already consumed.
|
|
|
|
voiceNumber, err := p.voiceNumber()
|
|
if err != nil {
|
|
return ASTNode{}, err
|
|
}
|
|
|
|
voiceEvents := ASTNode{
|
|
Type: EventSequenceNode,
|
|
SourceContext: p.sourceContext(p.peek()),
|
|
Children: []ASTNode{},
|
|
}
|
|
|
|
// Keep consuming events until we reach another voice marker (including a
|
|
// voice group end marker), a new part, EOF, or a closing ] or }.
|
|
for !p.check(EOF, VoiceMarker, EventSeqClose, CramClose) &&
|
|
!p.looksLikePartDeclaration() {
|
|
event, err := p.innerEvent()
|
|
if err != nil {
|
|
return ASTNode{}, err
|
|
}
|
|
|
|
voiceEvents.Children = append(voiceEvents.Children, event)
|
|
}
|
|
|
|
return ASTNode{
|
|
Type: VoiceNode,
|
|
SourceContext: voiceNumber.SourceContext,
|
|
Children: []ASTNode{voiceNumber, voiceEvents},
|
|
}, nil
|
|
}
|
|
|
|
func (p *parser) voiceGroup() (ASTNode, error) {
|
|
// NB: This assumes the first VoiceMarker token was already consumed.
|
|
firstVoiceMarkerToken := p.previous()
|
|
|
|
voiceGroupNode := ASTNode{
|
|
Type: VoiceGroupNode,
|
|
SourceContext: p.sourceContext(firstVoiceMarkerToken),
|
|
Children: []ASTNode{},
|
|
}
|
|
|
|
for {
|
|
voice, err := p.voice()
|
|
if err != nil {
|
|
return ASTNode{}, err
|
|
}
|
|
|
|
voiceGroupNode.Children = append(voiceGroupNode.Children, voice)
|
|
|
|
voiceMarker, matched := p.match(VoiceMarker)
|
|
if !matched {
|
|
break
|
|
}
|
|
|
|
if voiceMarker.literal.(int32) == 0 {
|
|
voiceGroupNode.Children = append(voiceGroupNode.Children, ASTNode{
|
|
Type: VoiceGroupEndMarkerNode,
|
|
SourceContext: p.sourceContext(voiceMarker),
|
|
})
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
return voiceGroupNode, nil
|
|
}
|
|
|
|
func (p *parser) innerEvent() (ASTNode, error) {
|
|
if _, matched := p.match(LeftParen); matched {
|
|
return p.sexp()
|
|
}
|
|
|
|
if _, matched := p.match(Name); matched {
|
|
return p.variableDefinitionOrReference()
|
|
}
|
|
|
|
if _, matched := p.match(OctaveSet); matched {
|
|
return p.octaveSet()
|
|
}
|
|
|
|
if token, matched := p.match(OctaveUp); matched {
|
|
return ASTNode{
|
|
Type: OctaveUpNode,
|
|
SourceContext: p.sourceContext(token),
|
|
}, nil
|
|
}
|
|
|
|
if token, matched := p.match(OctaveDown); matched {
|
|
return ASTNode{
|
|
Type: OctaveDownNode,
|
|
SourceContext: p.sourceContext(token),
|
|
}, nil
|
|
}
|
|
|
|
if _, matched := p.match(NoteLetter, RestLetter); matched {
|
|
return p.noteRestOrChord()
|
|
}
|
|
|
|
if token, matched := p.match(Barline); matched {
|
|
return ASTNode{
|
|
Type: BarlineNode,
|
|
SourceContext: p.sourceContext(token),
|
|
}, nil
|
|
}
|
|
|
|
if _, matched := p.match(EventSeqOpen); matched {
|
|
return p.eventSeq()
|
|
}
|
|
|
|
if _, matched := p.match(CramOpen); matched {
|
|
return p.cram()
|
|
}
|
|
|
|
if _, matched := p.match(VoiceMarker); matched {
|
|
return p.voiceGroup()
|
|
}
|
|
|
|
if token, matched := p.match(Marker); matched {
|
|
return ASTNode{
|
|
Type: MarkerNode,
|
|
SourceContext: p.sourceContext(token),
|
|
Literal: token.literal,
|
|
}, nil
|
|
}
|
|
|
|
if token, matched := p.match(AtMarker); matched {
|
|
return ASTNode{
|
|
Type: AtMarkerNode,
|
|
SourceContext: p.sourceContext(token),
|
|
Literal: token.literal,
|
|
}, nil
|
|
}
|
|
|
|
return ASTNode{}, p.unexpectedTokenError(p.peek(), "in inner events")
|
|
}
|
|
|
|
func (p *parser) topLevel() (ASTNode, error) {
|
|
if p.looksLikePartDeclaration() {
|
|
p.consume(Name, "in part declaration")
|
|
return p.part()
|
|
}
|
|
|
|
return p.implicitPart()
|
|
}
|
|
|
|
func (p *parser) parseAST() (ASTNode, error) {
|
|
rootNode := ASTNode{Type: RootNode}
|
|
|
|
for t := p.peek(); t.tokenType != EOF; t = p.peek() {
|
|
// fmt.Printf("t: %s\n", t.String())
|
|
node, err := p.topLevel()
|
|
if err != nil {
|
|
return ASTNode{}, err
|
|
}
|
|
|
|
rootNode.Children = append(rootNode.Children, node)
|
|
}
|
|
|
|
return rootNode, nil
|
|
}
|
|
|
|
// Parse a string of input into a root ASTNode.
|
|
func Parse(
|
|
filepath string, input string, opts ...parseOption,
|
|
) (ASTNode, error) {
|
|
defer func(start time.Time) {
|
|
if r := recover(); r != nil {
|
|
panic(fmt.Sprintf("Critical error while parsing %s", filepath))
|
|
}
|
|
|
|
log.Info().
|
|
Str("filepath", filepath).
|
|
Str("took", time.Since(start).String()).
|
|
Msg("Parsed input.")
|
|
}(time.Now())
|
|
|
|
tokens, err := Scan(filepath, input)
|
|
if err != nil {
|
|
return ASTNode{}, err
|
|
}
|
|
|
|
p := newParser(filepath, tokens, opts...)
|
|
|
|
return p.parseAST()
|
|
}
|
|
|
|
// ParseString reads and parses a string of input.
|
|
func ParseString(input string) (ASTNode, error) {
|
|
return Parse("", input)
|
|
}
|
|
|
|
// ParseFile reads a file and parses the input.
|
|
func ParseFile(filepath string) (ASTNode, error) {
|
|
contents, err := os.ReadFile(filepath)
|
|
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return ASTNode{}, help.UserFacingErrorf(
|
|
`Failed to open %s. The file does not seem to exist.
|
|
|
|
Please check that you haven't misspelled the file name, etc.`,
|
|
color.Aurora.BrightYellow(filepath),
|
|
)
|
|
}
|
|
|
|
if err != nil {
|
|
return ASTNode{}, err
|
|
}
|
|
|
|
return Parse(filepath, string(contents))
|
|
}
|