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)) }