diff --git a/main.go b/main.go index fa79a8d..150ab56 100644 --- a/main.go +++ b/main.go @@ -3,16 +3,16 @@ package main import ( "flag" "fmt" - "github.com/mbStavola/slydes/pkg/lang" - "github.com/mbStavola/slydes/render/html" - "github.com/mbStavola/slydes/render/native" "os" "strings" + + "github.com/mbStavola/slydes/pkg/lang" + "github.com/mbStavola/slydes/render/html" ) func main() { filename := flag.String("file", "", "slide to open") - output := flag.String("out", "html", "method of display (html, native)") + output := flag.String("out", "noop", "method of display (noop, html)") debug := flag.Bool("debug", false, "print debug info") flag.Parse() @@ -23,8 +23,8 @@ func main() { } else if !strings.HasSuffix(*filename, ".sly") { fmt.Print("Only .sly files are supported") return - } else if *output != "native" && *output != "html" { - fmt.Print("Output must be either native or html") + } else if *output != "native" && *output != "html" && *output != "noop" { + fmt.Print("Output must be either noop or html") return } @@ -47,11 +47,7 @@ func main() { } switch *output { - case "native": - if err := native.Render(show); err != nil { - fmt.Print(err) - } - + case "noop": break case "html": if err := html.Render(show); err != nil { diff --git a/pkg/lang/compile.go b/pkg/lang/compile.go index 85286a5..d55ea51 100644 --- a/pkg/lang/compile.go +++ b/pkg/lang/compile.go @@ -1,9 +1,11 @@ package lang import ( + "errors" "fmt" - "github.com/mbStavola/slydes/pkg/types" "image/color" + + "github.com/mbStavola/slydes/pkg/types" ) type Compiler interface { @@ -18,13 +20,20 @@ func NewDefaultCompiler() DefaultCompiler { func (comp DefaultCompiler) Compile(statements []Statement) (types.Show, error) { state := newCompilationState() + errBundle := newErrorInfoBundle() for _, statement := range statements { - if err := state.processStatement(statement); err != nil { + if err := state.processStatement(statement); err != nil && errors.As(err, &ErrorInfo{}) { + errBundle.Add(err.(ErrorInfo)) + } else if err != nil { return types.Show{}, err } } + if errBundle.HasErrors() { + return types.Show{}, errBundle + } + return state.finalizeCompilation(), nil } diff --git a/pkg/lang/errors.go b/pkg/lang/errors.go index 1c19144..76b3b12 100644 --- a/pkg/lang/errors.go +++ b/pkg/lang/errors.go @@ -1,6 +1,35 @@ package lang -import "fmt" +import ( + "fmt" + "strings" +) + +type ErrorInfoBundle struct { + errors []ErrorInfo +} + +func newErrorInfoBundle() ErrorInfoBundle { + return ErrorInfoBundle{errors: make([]ErrorInfo, 0)} +} + +func (b *ErrorInfoBundle) Add(err ErrorInfo) { + b.errors = append(b.errors, err) +} + +func (b ErrorInfoBundle) HasErrors() bool { + return len(b.errors) > 0 +} + +func (b ErrorInfoBundle) Error() string { + builder := strings.Builder{} + for _, err := range b.errors { + builder.WriteString(err.Error()) + builder.WriteByte('\n') + } + + return builder.String() +} type ErrorInfo struct { line uint diff --git a/pkg/lang/lex.go b/pkg/lang/lex.go index c557f01..9064329 100644 --- a/pkg/lang/lex.go +++ b/pkg/lang/lex.go @@ -3,6 +3,7 @@ package lang import ( "bufio" "bytes" + "errors" "io" "strconv" "strings" @@ -14,6 +15,7 @@ type TokenType int const ( InvalidToken TokenType = iota EOF + Skip // Single Character LeftParen @@ -42,6 +44,7 @@ func (t TokenType) String() string { return []string{ "InvalidToken", "EOF", + "Skip", "LeftParen", "RightParen", @@ -82,284 +85,336 @@ func NewDefaultLexer() DefaultLexer { } func (lex DefaultLexer) Lex(reader io.Reader) ([]Token, error) { - bufReader := bufio.NewReader(reader) + muncher := newRuneMuncher(reader) + errBundle := newErrorInfoBundle() + tokens := make([]Token, 0, 1024) - var line uint = 1 - for char, _, err := bufReader.ReadRune(); err != io.EOF; char, _, err = bufReader.ReadRune() { - if err != nil { + for !muncher.atEnd() { + token, err := processRune(muncher) + if err != nil && errors.As(err, &ErrorInfo{}) { + errBundle.Add(err.(ErrorInfo)) + } else if err != nil { return tokens, err } - switch char { - case '#': - _, _, _ = bufReader.ReadLine() - line += 1 - - break - case '[': - ty := SlideScope - if shouldEat, err := eatIf(bufReader, '['); err == io.EOF { - return tokens, lexemeErrorInfo(line, char, "Unexpected end of file") - } else if err != nil { - return tokens, err - } else if shouldEat { - ty = SubScope - } - - title, err := bufReader.ReadString(']') - if err == io.EOF { - return tokens, lexemeErrorInfo(line, char, "Unterminated scope") - } else if err != nil { - return tokens, err - } - - if shouldEat, err := eatIf(bufReader, ']'); err == io.EOF { - return tokens, lexemeErrorInfo(line, char, "Unexpected end of file") - } else if err != nil { - return tokens, err - } else if ty == SubScope && !shouldEat { - return tokens, lexemeErrorInfo(line, ']', "Subscope expected closing ']'") - } else if ty == SlideScope && shouldEat { - return tokens, lexemeErrorInfo(line, ']', "Dangling scope end") - } - - tokens = append(tokens, Token{ - Type: ty, - line: line, - lexeme: char, - // Cut off the dangling ] in the scope title - data: title[:len(title)-1], - }) + if token.Type == Skip { + continue + } - break - case '(': - tokens = append(tokens, Token{ - Type: LeftParen, - line: line, - lexeme: char, - }) + tokens = append(tokens, token) + } - break - case ')': - tokens = append(tokens, Token{ - Type: RightParen, - line: line, - lexeme: char, - }) + if errBundle.HasErrors() { + return tokens, errBundle + } - break - case '{': - tokens = append(tokens, Token{ - Type: LeftBrace, - line: line, - lexeme: char, - }) + return tokens, nil +} - break - case '}': - tokens = append(tokens, Token{ - Type: RightBrace, - line: line, - lexeme: char, - }) +func processRune(muncher *runeMuncher) (Token, error) { + char, _, err := muncher.ReadRune() + if err != nil { + return Token{}, err + } - break - case '@': - tokens = append(tokens, Token{ - Type: AtSign, - line: line, - lexeme: char, - }) + switch char { + case '#': + _, _, _ = muncher.ReadLine() + muncher.newLine() + return Token{Type: Skip}, nil + + case '[': + ty := SlideScope + if shouldEat, err := muncher.eatIf('['); err == io.EOF { + return Token{}, lexemeErrorInfo(muncher.line, char, "Unexpected end of file") + } else if err != nil { + return Token{}, err + } else if shouldEat { + ty = SubScope + } - break - case '$': - tokens = append(tokens, Token{ - Type: DollarSign, - line: line, - lexeme: char, - }) + title, err := muncher.ReadString(']') + if err == io.EOF { + return Token{}, lexemeErrorInfo(muncher.line, char, "Unterminated scope") + } else if err != nil { + return Token{}, err + } - break - case '=': - tokens = append(tokens, Token{ - Type: EqualSign, - line: line, - lexeme: char, - }) + if shouldEat, err := muncher.eatIf(']'); err == io.EOF { + return Token{}, lexemeErrorInfo(muncher.line, char, "Unexpected end of file") + } else if err != nil { + return Token{}, err + } else if ty == SubScope && !shouldEat { + return Token{}, lexemeErrorInfo(muncher.line, ']', "Subscope expected closing ']'") + } else if ty == SlideScope && shouldEat { + return Token{}, lexemeErrorInfo(muncher.line, ']', "Dangling scope end") + } - break - case ';': - tokens = append(tokens, Token{ - Type: Semicolon, - line: line, - lexeme: char, - }) + return Token{ + Type: ty, + line: muncher.line, + lexeme: char, + // Cut off the dangling ] in the scope title + data: title[:len(title)-1], + }, nil + + case '(': + return Token{ + Type: LeftParen, + line: muncher.line, + lexeme: char, + }, nil + + case ')': + return Token{ + Type: RightParen, + line: muncher.line, + lexeme: char, + }, nil + + case '{': + return Token{ + Type: LeftBrace, + line: muncher.line, + lexeme: char, + }, nil + + case '}': + return Token{ + Type: RightBrace, + line: muncher.line, + lexeme: char, + }, nil + + case '@': + return Token{ + Type: AtSign, + line: muncher.line, + lexeme: char, + }, nil + + case '$': + return Token{ + Type: DollarSign, + line: muncher.line, + lexeme: char, + }, nil + + case '=': + return Token{ + Type: EqualSign, + line: muncher.line, + lexeme: char, + }, nil + + case ';': + return Token{ + Type: Semicolon, + line: muncher.line, + lexeme: char, + }, nil + + case ',': + return Token{ + Type: Comma, + line: muncher.line, + lexeme: char, + }, nil + + case '-': + if chars, err := muncher.Peek(2); err == io.EOF { + return Token{}, lexemeErrorInfo(muncher.line, char, "Unexpected end of file") + } else if err != nil { + return Token{}, err + } else if string(chars[:]) != "--" { + return Token{}, lexemeErrorInfo(muncher.line, char, "Malformed text block") + } - case ',': - tokens = append(tokens, Token{ - Type: Comma, - line: line, - lexeme: char, - }) + // Eat the starting dashes + if err = muncher.eatN(2); err != nil { + return Token{}, err + } - break - case '-': - if chars, err := bufReader.Peek(2); err == io.EOF { - return tokens, lexemeErrorInfo(line, char, "Unexpected end of file") - } else if err != nil { - return tokens, err - } else if string(chars[:]) != "--" { - return tokens, lexemeErrorInfo(line, char, "Malformed text block") + text := strings.Builder{} + dashCounter := 0 + + // Read runes until we encounter three dashes in a row + err := muncher.eatWhile(func(char rune) bool { + if char == '-' && dashCounter == 2 { + return false + } else if char == '-' { + dashCounter++ + return true + } else if dashCounter > 0 { + // If we've seen some number of dashes that is less than + // three, write them to the text buffer and reset the count + dashes := bytes.Repeat([]byte("-"), dashCounter) + text.Write(dashes) + dashCounter = 0 } - // Eat the starting dashes - if err = eatN(bufReader, 2); err != nil { - return tokens, err - } + text.WriteRune(char) - text := strings.Builder{} - dashCounter := 0 - - // Read runes until we encounter three dashes in a row - for char, _, err := bufReader.ReadRune(); ; char, _, err = bufReader.ReadRune() { - if char == '-' && dashCounter == 2 { - break - } else if char == '-' { - dashCounter += 1 - continue - } else if dashCounter > 0 { - // If we've seen some number of dashes that is less than - // three, write them to the text buffer and reset the count - dashes := bytes.Repeat([]byte("-"), dashCounter) - text.Write(dashes) - dashCounter = 0 - } + return true + }) - if err != nil { - return tokens, err - } + if err != nil { + return Token{}, err + } - text.WriteRune(char) + return Token{ + Type: Text, + line: muncher.line, + lexeme: char, + data: text.String(), + }, nil + + case '"': + str, err := muncher.ReadString('"') + if err == io.EOF { + return Token{}, simpleErrorInfo(muncher.line, "Unterminated String") + } else if err != nil { + return Token{}, err + } + + return Token{ + Type: String, + line: muncher.line, + lexeme: char, + // Cut off the dangling " in the string + data: str[:len(str)-1], + }, nil + + case '0': + case '1', '2', '3', '4', '5', '6', '7', '8', '9': + num := strings.Builder{} + num.WriteRune(char) + + err := muncher.eatWhile(func(char rune) bool { + if !unicode.IsNumber(char) { + muncher.UnreadRune() + return false } - tokens = append(tokens, Token{ - Type: Text, - line: line, - lexeme: char, - data: text.String(), - }) + num.WriteRune(char) - break - case '"': - str, err := bufReader.ReadString('"') - if err == io.EOF { - return tokens, simpleErrorInfo(line, "Unterminated String") - } else if err != nil { - return tokens, err - } + return true + }) - tokens = append(tokens, Token{ - Type: String, - line: line, - lexeme: char, - // Cut off the dangling " in the string - data: str[:len(str)-1], - }) + if err != nil { + return Token{}, err + } - break - case '0': - case '1', '2', '3', '4', '5', '6', '7', '8', '9': - num := strings.Builder{} - num.WriteRune(char) + data, err := strconv.ParseUint(num.String(), 10, 8) + if err != nil { + return Token{}, err + } - for char, _, err := bufReader.ReadRune(); ; char, _, err = bufReader.ReadRune() { - if err != nil { - return tokens, err + return Token{ + Type: Integer, + line: muncher.line, + lexeme: char, + data: uint8(data), + }, nil + + case ' ', '\t', '\r': + return Token{Type: Skip}, nil + + case '\n': + muncher.newLine() + return Token{Type: Skip}, nil + + default: + if unicode.IsLetter(char) { + ident := strings.Builder{} + ident.WriteRune(char) + + err := muncher.eatWhile(func(char rune) bool { + if !unicode.IsLetter(char) && !unicode.IsNumber(char) { + muncher.UnreadRune() + return false } - if !unicode.IsNumber(char) { - bufReader.UnreadRune() - break - } + ident.WriteRune(char) - num.WriteRune(char) - } + return true + }) - data, err := strconv.ParseUint(num.String(), 10, 8) if err != nil { - return tokens, err + return Token{}, err } - tokens = append(tokens, Token{ - Type: Integer, - line: line, + return Token{ + Type: Identifier, + line: muncher.line, lexeme: char, - data: uint8(data), - }) - - break - case ' ', '\t', '\r': - - break - case '\n': - line += 1 + data: ident.String(), + }, nil + } - break - default: - if unicode.IsLetter(char) { - ident := strings.Builder{} - ident.WriteRune(char) + return Token{}, lexemeErrorInfo(muncher.line, char, "Unexpected character") + } - for char, _, err := bufReader.ReadRune(); ; char, _, err = bufReader.ReadRune() { - if err != nil { - return tokens, err - } + panic("unreachable") +} - if !unicode.IsLetter(char) && !unicode.IsNumber(char) { - bufReader.UnreadRune() - break - } +type runeMuncher struct { + line uint + *bufio.Reader +} - ident.WriteRune(char) - } +func newRuneMuncher(reader io.Reader) *runeMuncher { + r := new(runeMuncher) - tokens = append(tokens, Token{ - Type: Identifier, - line: line, - lexeme: char, - data: ident.String(), - }) + r.line = 1 + r.Reader = bufio.NewReader(reader) - break - } + return r +} - return tokens, lexemeErrorInfo(line, char, "Unexpected character") - } - } +func (r *runeMuncher) atEnd() bool { + _, err := r.Peek(1) + return err == io.EOF +} - return tokens, nil +func (r *runeMuncher) newLine() { + r.line++ } // Helper function to conditionally eat a lexeme if it matches // the expected rune -func eatIf(reader *bufio.Reader, expected rune) (bool, error) { - if actual, _, err := reader.ReadRune(); err != nil { +func (r *runeMuncher) eatIf(expected rune) (bool, error) { + if actual, _, err := r.ReadRune(); err != nil { // TODO(Matt): Would it be more correct to unread the rune here? return false, err } else if actual == expected { return true, nil } - return false, reader.UnreadRune() + return false, r.UnreadRune() } // Helper function to discard a specified number of runes -func eatN(reader *bufio.Reader, n int) error { +func (r *runeMuncher) eatN(n int) error { for i := 0; i < n; i++ { - if _, _, err := reader.ReadRune(); err != nil { + if _, _, err := r.ReadRune(); err != nil { + return err + } + } + + return nil +} + +func (r *runeMuncher) eatWhile(callback func(rune) bool) error { + for char, _, err := r.ReadRune(); ; char, _, err = r.ReadRune() { + if err != nil { return err } + + if shouldContinue := callback(char); !shouldContinue { + break + } } return nil diff --git a/pkg/lang/parse.go b/pkg/lang/parse.go index a23cc78..9d16b88 100644 --- a/pkg/lang/parse.go +++ b/pkg/lang/parse.go @@ -1,6 +1,9 @@ package lang -import "fmt" +import ( + "errors" + "fmt" +) type StatementType int @@ -84,16 +87,24 @@ func NewDefaultParser() DefaultParser { func (pars DefaultParser) Parse(tokens []Token) ([]Statement, error) { statements := make([]Statement, 0, 1024) muncher := tokenMuncher{tokens: tokens} + errBundle := newErrorInfoBundle() for !muncher.atEnd() { statement, err := declaration(&muncher) - if err != nil { + if err != nil && errors.As(err, &ErrorInfo{}) { + errBundle.Add(err.(ErrorInfo)) + synchronizeFromErrorState(&muncher) + } else if err != nil { return statements, err } statements = append(statements, statement) } + if errBundle.HasErrors() { + return statements, errBundle + } + return statements, nil } @@ -234,11 +245,12 @@ func colorLiteral(muncher *tokenMuncher) (interface{}, error) { } // TODO(Matt): We should be able to use variables here - if value, err := muncher.tryEat(Integer); err != nil { + value, err := muncher.tryEat(Integer) + if err != nil { return nil, err - } else { - values[i] = value.data.(uint8) } + + values[i] = value.data.(uint8) } // Allow trailing comma @@ -284,6 +296,18 @@ func value(muncher *tokenMuncher) (interface{}, error) { return nil, tokenErrorInfo(token, "Expected value") } +func synchronizeFromErrorState(muncher *tokenMuncher) { + muncher.eat() + + for !muncher.atEnd() { + if muncher.previous().Type == Semicolon { + return + } + + muncher.eat() + } +} + type tokenMuncher struct { tokens []Token current int @@ -344,7 +368,7 @@ func (tm *tokenMuncher) check(expected TokenType) bool { func (tm *tokenMuncher) eat() Token { if !tm.atEnd() { - tm.current += 1 + tm.current++ } return tm.previous() diff --git a/pkg/lang/sly.go b/pkg/lang/sly.go index c1ee31f..be557f2 100644 --- a/pkg/lang/sly.go +++ b/pkg/lang/sly.go @@ -1,9 +1,10 @@ package lang import ( - "github.com/mbStavola/slydes/pkg/types" "io" "strings" + + "github.com/mbStavola/slydes/pkg/types" ) // This type represents a three phase "compiler" for diff --git a/render/native/render.go b/render/native/render.go deleted file mode 100644 index dc14ad4..0000000 --- a/render/native/render.go +++ /dev/null @@ -1,9 +0,0 @@ -package native - -import ( - "github.com/mbStavola/slydes/pkg/types" -) - -func Render(show types.Show) error { - return nil -}