Implemented error messages
This commit is contained in:
parent
2d990b0bee
commit
9458253f31
@ -7,34 +7,20 @@ import (
|
||||
|
||||
// Build describes a compiler build.
|
||||
type Build struct {
|
||||
Files []string
|
||||
WriteExecutable bool
|
||||
Files []string
|
||||
}
|
||||
|
||||
// New creates a new build.
|
||||
func New(files ...string) *Build {
|
||||
return &Build{
|
||||
Files: files,
|
||||
WriteExecutable: true,
|
||||
Files: files,
|
||||
}
|
||||
}
|
||||
|
||||
// Run parses the input files and generates an executable file.
|
||||
func (build *Build) Run() error {
|
||||
func (build *Build) Run() (map[string]*Function, error) {
|
||||
functions, errors := Scan(build.Files)
|
||||
allFunctions, err := Compile(functions, errors)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !build.WriteExecutable {
|
||||
return nil
|
||||
}
|
||||
|
||||
path := build.Executable()
|
||||
code, data := Finalize(allFunctions)
|
||||
return Write(path, code, data)
|
||||
return Compile(functions, errors)
|
||||
}
|
||||
|
||||
// Executable returns the path to the executable.
|
||||
|
@ -1,6 +1,7 @@
|
||||
package build_test
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"git.akyoto.dev/cli/q/src/build"
|
||||
@ -9,16 +10,17 @@ import (
|
||||
|
||||
func TestBuild(t *testing.T) {
|
||||
b := build.New("../../examples/hello")
|
||||
assert.Nil(t, b.Run())
|
||||
_, err := b.Run()
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestSkipExecutable(t *testing.T) {
|
||||
func TestExecutable(t *testing.T) {
|
||||
b := build.New("../../examples/hello")
|
||||
b.WriteExecutable = false
|
||||
assert.Nil(t, b.Run())
|
||||
assert.Equal(t, filepath.Base(b.Executable()), "hello")
|
||||
}
|
||||
|
||||
func TestNonExisting(t *testing.T) {
|
||||
b := build.New("does-not-exist")
|
||||
assert.NotNil(t, b.Run())
|
||||
_, err := b.Run()
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
|
||||
"git.akyoto.dev/cli/q/src/build/directory"
|
||||
"git.akyoto.dev/cli/q/src/build/token"
|
||||
"git.akyoto.dev/cli/q/src/errors"
|
||||
)
|
||||
|
||||
// Scan scans the directory.
|
||||
@ -86,46 +87,141 @@ func scanFile(path string, functions chan<- *Function) error {
|
||||
tokens := token.Tokenize(contents)
|
||||
|
||||
var (
|
||||
i = 0
|
||||
groupLevel = 0
|
||||
blockLevel = 0
|
||||
headerStart = -1
|
||||
nameStart = -1
|
||||
paramsStart = -1
|
||||
bodyStart = -1
|
||||
)
|
||||
|
||||
for i, t := range tokens {
|
||||
switch t.Kind {
|
||||
case token.Identifier:
|
||||
if blockLevel == 0 && groupLevel == 0 {
|
||||
headerStart = i
|
||||
for {
|
||||
// Function name
|
||||
for i < len(tokens) {
|
||||
if tokens[i].Kind == token.Identifier {
|
||||
nameStart = i
|
||||
i++
|
||||
break
|
||||
}
|
||||
|
||||
case token.GroupStart:
|
||||
groupLevel++
|
||||
|
||||
case token.GroupEnd:
|
||||
groupLevel--
|
||||
|
||||
case token.BlockStart:
|
||||
blockLevel++
|
||||
|
||||
if blockLevel == 1 {
|
||||
bodyStart = i
|
||||
if tokens[i].Kind == token.NewLine {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
case token.BlockEnd:
|
||||
blockLevel--
|
||||
if tokens[i].Kind == token.EOF {
|
||||
return nil
|
||||
}
|
||||
|
||||
if blockLevel == 0 {
|
||||
function := &Function{
|
||||
Name: tokens[headerStart].Text(),
|
||||
Head: tokens[headerStart:bodyStart],
|
||||
Body: tokens[bodyStart : i+1],
|
||||
return errors.New(errors.ExpectedFunctionName, path, tokens, i)
|
||||
}
|
||||
|
||||
// Function parameters
|
||||
for i < len(tokens) {
|
||||
if tokens[i].Kind == token.GroupStart {
|
||||
groupLevel++
|
||||
|
||||
if groupLevel == 1 {
|
||||
paramsStart = i
|
||||
}
|
||||
|
||||
functions <- function
|
||||
i++
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
if tokens[i].Kind == token.GroupEnd {
|
||||
groupLevel--
|
||||
|
||||
if groupLevel < 0 {
|
||||
return errors.New(errors.MissingGroupStart, path, tokens, i)
|
||||
}
|
||||
|
||||
i++
|
||||
|
||||
if groupLevel == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if tokens[i].Kind == token.EOF {
|
||||
if groupLevel > 0 {
|
||||
return errors.New(errors.MissingGroupEnd, path, tokens, i)
|
||||
}
|
||||
|
||||
if paramsStart == -1 {
|
||||
return errors.New(errors.ExpectedFunctionParameters, path, tokens, i)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if groupLevel > 0 {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
return errors.New(errors.ExpectedFunctionParameters, path, tokens, i)
|
||||
}
|
||||
|
||||
// Function definition
|
||||
for i < len(tokens) {
|
||||
if tokens[i].Kind == token.BlockStart {
|
||||
blockLevel++
|
||||
|
||||
if blockLevel == 1 {
|
||||
bodyStart = i
|
||||
}
|
||||
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
if tokens[i].Kind == token.BlockEnd {
|
||||
blockLevel--
|
||||
|
||||
if blockLevel < 0 {
|
||||
return errors.New(errors.MissingBlockStart, path, tokens, i)
|
||||
}
|
||||
|
||||
i++
|
||||
|
||||
if blockLevel == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if tokens[i].Kind == token.EOF {
|
||||
if blockLevel > 0 {
|
||||
return errors.New(errors.MissingBlockEnd, path, tokens, i)
|
||||
}
|
||||
|
||||
if bodyStart == -1 {
|
||||
return errors.New(errors.ExpectedFunctionDefinition, path, tokens, i)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if blockLevel > 0 {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
return errors.New(errors.ExpectedFunctionDefinition, path, tokens, i)
|
||||
}
|
||||
|
||||
functions <- &Function{
|
||||
Name: tokens[nameStart].Text(),
|
||||
Head: tokens[paramsStart:bodyStart],
|
||||
Body: tokens[bodyStart : i+1],
|
||||
}
|
||||
|
||||
nameStart = -1
|
||||
paramsStart = -1
|
||||
bodyStart = -1
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,9 @@ const (
|
||||
// Invalid represents an invalid token.
|
||||
Invalid Kind = iota
|
||||
|
||||
// EOF represents the end of file.
|
||||
EOF
|
||||
|
||||
// NewLine represents the newline character.
|
||||
NewLine
|
||||
|
||||
@ -54,6 +57,7 @@ const (
|
||||
func (kind Kind) String() string {
|
||||
return [...]string{
|
||||
"Invalid",
|
||||
"EOF",
|
||||
"NewLine",
|
||||
"Identifier",
|
||||
"Keyword",
|
||||
|
@ -35,6 +35,11 @@ func TestFunction(t *testing.T) {
|
||||
Bytes: []byte("}"),
|
||||
Position: 7,
|
||||
},
|
||||
{
|
||||
Kind: token.EOF,
|
||||
Bytes: nil,
|
||||
Position: 8,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@ -51,6 +56,11 @@ func TestKeyword(t *testing.T) {
|
||||
Bytes: []byte("x"),
|
||||
Position: 7,
|
||||
},
|
||||
{
|
||||
Kind: token.EOF,
|
||||
Bytes: nil,
|
||||
Position: 8,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@ -77,6 +87,11 @@ func TestArray(t *testing.T) {
|
||||
Bytes: []byte("]"),
|
||||
Position: 7,
|
||||
},
|
||||
{
|
||||
Kind: token.EOF,
|
||||
Bytes: nil,
|
||||
Position: 8,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@ -93,6 +108,11 @@ func TestNewline(t *testing.T) {
|
||||
Bytes: []byte("\n"),
|
||||
Position: 1,
|
||||
},
|
||||
{
|
||||
Kind: token.EOF,
|
||||
Bytes: nil,
|
||||
Position: 2,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@ -109,6 +129,11 @@ func TestNumber(t *testing.T) {
|
||||
Bytes: []byte("-456"),
|
||||
Position: 4,
|
||||
},
|
||||
{
|
||||
Kind: token.EOF,
|
||||
Bytes: nil,
|
||||
Position: 8,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@ -140,6 +165,11 @@ func TestSeparator(t *testing.T) {
|
||||
Bytes: []byte("c"),
|
||||
Position: 4,
|
||||
},
|
||||
{
|
||||
Kind: token.EOF,
|
||||
Bytes: nil,
|
||||
Position: 5,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@ -156,6 +186,11 @@ func TestString(t *testing.T) {
|
||||
Bytes: []byte(`"World"`),
|
||||
Position: 8,
|
||||
},
|
||||
{
|
||||
Kind: token.EOF,
|
||||
Bytes: nil,
|
||||
Position: 15,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@ -167,6 +202,11 @@ func TestStringMultiline(t *testing.T) {
|
||||
Bytes: []byte("\"Hello\nWorld\""),
|
||||
Position: 0,
|
||||
},
|
||||
{
|
||||
Kind: token.EOF,
|
||||
Bytes: nil,
|
||||
Position: 13,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@ -178,6 +218,11 @@ func TestStringEOF(t *testing.T) {
|
||||
Bytes: []byte(`"EOF`),
|
||||
Position: 0,
|
||||
},
|
||||
{
|
||||
Kind: token.EOF,
|
||||
Bytes: nil,
|
||||
Position: 4,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@ -195,6 +240,7 @@ func TestTokenText(t *testing.T) {
|
||||
|
||||
func TestTokenKind(t *testing.T) {
|
||||
assert.Equal(t, token.Invalid.String(), "Invalid")
|
||||
assert.Equal(t, token.EOF.String(), "EOF")
|
||||
assert.Equal(t, token.NewLine.String(), "NewLine")
|
||||
assert.Equal(t, token.Identifier.String(), "Identifier")
|
||||
assert.Equal(t, token.Keyword.String(), "Keyword")
|
||||
|
@ -31,6 +31,7 @@ func Tokenize(buffer []byte) List {
|
||||
for i < len(buffer) {
|
||||
if buffer[i] == '"' {
|
||||
end = i + 1
|
||||
i++
|
||||
break
|
||||
}
|
||||
|
||||
@ -43,6 +44,8 @@ func Tokenize(buffer []byte) List {
|
||||
buffer[start:end],
|
||||
})
|
||||
|
||||
continue
|
||||
|
||||
// Parentheses start
|
||||
case '(':
|
||||
tokens = append(tokens, Token{GroupStart, i, groupStartBytes})
|
||||
@ -121,6 +124,7 @@ func Tokenize(buffer []byte) List {
|
||||
i++
|
||||
}
|
||||
|
||||
tokens = append(tokens, Token{EOF, i, nil})
|
||||
return tokens
|
||||
}
|
||||
|
||||
|
@ -11,11 +11,12 @@ import (
|
||||
// Build builds an executable.
|
||||
func Build(args []string) int {
|
||||
b := build.New()
|
||||
writeExecutable := true
|
||||
|
||||
for i := 0; i < len(args); i++ {
|
||||
switch args[i] {
|
||||
case "--dry":
|
||||
b.WriteExecutable = false
|
||||
writeExecutable = false
|
||||
|
||||
case "--verbose", "-v":
|
||||
config.Verbose = true
|
||||
@ -34,7 +35,20 @@ func Build(args []string) int {
|
||||
b.Files = append(b.Files, ".")
|
||||
}
|
||||
|
||||
err := b.Run()
|
||||
result, err := b.Run()
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return 1
|
||||
}
|
||||
|
||||
if !writeExecutable {
|
||||
return 0
|
||||
}
|
||||
|
||||
path := b.Executable()
|
||||
code, data := build.Finalize(result)
|
||||
err = build.Write(path, code, data)
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
|
@ -6,7 +6,7 @@ import "fmt"
|
||||
func Help(args []string) int {
|
||||
fmt.Println("Usage: q [command] [options]")
|
||||
fmt.Println("")
|
||||
fmt.Println(" build [directory]")
|
||||
fmt.Println(" build [directory] [file]")
|
||||
fmt.Println(" system")
|
||||
return 2
|
||||
}
|
||||
|
@ -1,8 +1,6 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"git.akyoto.dev/cli/q/src/cli"
|
||||
@ -19,17 +17,16 @@ func TestCLI(t *testing.T) {
|
||||
{[]string{}, 2},
|
||||
{[]string{"invalid"}, 2},
|
||||
{[]string{"system"}, 0},
|
||||
{[]string{"build", "non-existing-directory"}, 1},
|
||||
{[]string{"build", "../../examples/hello/hello.q"}, 1},
|
||||
{[]string{"build", "invalid-directory"}, 1},
|
||||
{[]string{"build", "--invalid-parameter"}, 2},
|
||||
{[]string{"build", "../../examples/hello", "--invalid"}, 2},
|
||||
{[]string{"build", "../../examples/hello", "--dry"}, 0},
|
||||
{[]string{"build", "../../examples/hello", "--dry", "--verbose"}, 0},
|
||||
{[]string{"build", "../../examples/hello/hello.q", "--dry"}, 0},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Log(test.arguments)
|
||||
directory, _ := os.Getwd()
|
||||
fmt.Println(directory)
|
||||
exitCode := cli.Main(test.arguments)
|
||||
assert.Equal(t, exitCode, test.expectedExitCode)
|
||||
}
|
||||
|
11
src/errors/Base.go
Normal file
11
src/errors/Base.go
Normal file
@ -0,0 +1,11 @@
|
||||
package errors
|
||||
|
||||
// Base is the base class for errors that have no parameters.
|
||||
type Base struct {
|
||||
Message string
|
||||
}
|
||||
|
||||
// Error generates the string representation.
|
||||
func (err *Base) Error() string {
|
||||
return err.Message
|
||||
}
|
67
src/errors/Error.go
Normal file
67
src/errors/Error.go
Normal file
@ -0,0 +1,67 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"git.akyoto.dev/cli/q/src/build/token"
|
||||
)
|
||||
|
||||
// Error is a compiler error at a given line and column.
|
||||
type Error struct {
|
||||
Path string
|
||||
Line int
|
||||
Column int
|
||||
Err error
|
||||
Stack string
|
||||
}
|
||||
|
||||
// New generates an error message at the current token position.
|
||||
// The error message is clickable in popular editors and leads you
|
||||
// directly to the faulty file at the given line and position.
|
||||
func New(err error, path string, tokens []token.Token, cursor int) *Error {
|
||||
var (
|
||||
lineCount = 1
|
||||
lineStart = -1
|
||||
)
|
||||
|
||||
for i := range cursor {
|
||||
if tokens[i].Kind == token.NewLine {
|
||||
lineCount++
|
||||
lineStart = int(tokens[i].Position)
|
||||
}
|
||||
}
|
||||
|
||||
var column int
|
||||
|
||||
if cursor < len(tokens) {
|
||||
column = tokens[cursor].Position - lineStart
|
||||
} else {
|
||||
lastToken := tokens[len(tokens)-1]
|
||||
column = lastToken.Position - lineStart + len(lastToken.Text())
|
||||
}
|
||||
|
||||
return &Error{path, lineCount, column, err, Stack()}
|
||||
}
|
||||
|
||||
// Error generates the string representation.
|
||||
func (e *Error) Error() string {
|
||||
path := e.Path
|
||||
cwd, err := os.Getwd()
|
||||
|
||||
if err == nil {
|
||||
relativePath, err := filepath.Rel(cwd, e.Path)
|
||||
|
||||
if err == nil {
|
||||
path = relativePath
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s:%d:%d: %s\n\n%s", path, e.Line, e.Column, e.Err, e.Stack)
|
||||
}
|
||||
|
||||
// Unwrap returns the wrapped error.
|
||||
func (e *Error) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
32
src/errors/Error_test.go
Normal file
32
src/errors/Error_test.go
Normal file
@ -0,0 +1,32 @@
|
||||
package errors_test
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.akyoto.dev/cli/q/src/build"
|
||||
"git.akyoto.dev/cli/q/src/errors"
|
||||
"git.akyoto.dev/go/assert"
|
||||
)
|
||||
|
||||
func TestErrors(t *testing.T) {
|
||||
tests := []struct {
|
||||
File string
|
||||
ExpectedError error
|
||||
}{
|
||||
{"ExpectedFunctionParameters.q", errors.ExpectedFunctionParameters},
|
||||
{"ExpectedFunctionDefinition.q", errors.ExpectedFunctionDefinition},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
name := strings.TrimSuffix(test.File, ".q")
|
||||
|
||||
t.Run(name, func(t *testing.T) {
|
||||
b := build.New(filepath.Join("testdata", test.File))
|
||||
_, err := b.Run()
|
||||
assert.NotNil(t, err)
|
||||
assert.Contains(t, err.Error(), test.ExpectedError.Error())
|
||||
})
|
||||
}
|
||||
}
|
11
src/errors/ScanErrors.go
Normal file
11
src/errors/ScanErrors.go
Normal file
@ -0,0 +1,11 @@
|
||||
package errors
|
||||
|
||||
var (
|
||||
MissingBlockStart = &Base{"Missing '{'"}
|
||||
MissingBlockEnd = &Base{"Missing '}'"}
|
||||
MissingGroupStart = &Base{"Missing '('"}
|
||||
MissingGroupEnd = &Base{"Missing ')'"}
|
||||
ExpectedFunctionName = &Base{"Expected function name"}
|
||||
ExpectedFunctionParameters = &Base{"Expected function parameters"}
|
||||
ExpectedFunctionDefinition = &Base{"Expected function definition"}
|
||||
)
|
30
src/errors/Stack.go
Normal file
30
src/errors/Stack.go
Normal file
@ -0,0 +1,30 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Stack generates a stack trace.
|
||||
func Stack() string {
|
||||
var (
|
||||
final []string
|
||||
buffer = make([]byte, 4096)
|
||||
n = runtime.Stack(buffer, false)
|
||||
stack = string(buffer[:n])
|
||||
lines = strings.Split(stack, "\n")
|
||||
)
|
||||
|
||||
for i := 6; i < len(lines); i += 2 {
|
||||
line := strings.TrimSpace(lines[i])
|
||||
space := strings.LastIndex(line, " ")
|
||||
|
||||
if space != -1 {
|
||||
line = line[:space]
|
||||
}
|
||||
|
||||
final = append(final, line)
|
||||
}
|
||||
|
||||
return strings.Join(final, "\n")
|
||||
}
|
1
src/errors/testdata/ExpectedFunctionDefinition.q
vendored
Normal file
1
src/errors/testdata/ExpectedFunctionDefinition.q
vendored
Normal file
@ -0,0 +1 @@
|
||||
main()
|
1
src/errors/testdata/ExpectedFunctionParameters.q
vendored
Normal file
1
src/errors/testdata/ExpectedFunctionParameters.q
vendored
Normal file
@ -0,0 +1 @@
|
||||
main
|
Loading…
Reference in New Issue
Block a user