From 1896cd652dfc1ca45b6e7e5c170d270a9e2c345f Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 27 Dec 2025 02:39:56 -0500 Subject: [PATCH] feat: better error messages --- Makefile | 5 ++++- pkg/saccharine/parser.go | 42 ++++++++++++++++++++--------------- pkg/saccharine/token/token.go | 31 +++++++++++++------------- pkg/saccharine/tokenizer.go | 5 ++++- pkg/trace/trace.go | 23 +++++++++++++++++++ samples/simple.txt | 15 ------------- 6 files changed, 70 insertions(+), 51 deletions(-) create mode 100644 pkg/trace/trace.go diff --git a/Makefile b/Makefile index ea3bfed..622fb51 100644 --- a/Makefile +++ b/Makefile @@ -5,4 +5,7 @@ it: @ chmod +x ${BINARY_NAME} ex: it - @ ./lambda.exe - < ./samples/simple.txt \ No newline at end of file + @ ./lambda.exe - < ./samples/simple.txt + +v: it + @ ./lambda.exe -v - < ./samples/simple.txt \ No newline at end of file diff --git a/pkg/saccharine/parser.go b/pkg/saccharine/parser.go index a9a7507..bed529d 100644 --- a/pkg/saccharine/parser.go +++ b/pkg/saccharine/parser.go @@ -1,12 +1,12 @@ package saccharine import ( - "errors" "fmt" "git.maximhutz.com/max/lambda/pkg/iterator" "git.maximhutz.com/max/lambda/pkg/saccharine/ast" "git.maximhutz.com/max/lambda/pkg/saccharine/token" + "git.maximhutz.com/max/lambda/pkg/trace" ) type TokenIterator = iterator.Iterator[token.Token] @@ -25,15 +25,22 @@ func parseToken(i *TokenIterator, expected token.Type) (*token.Token, error) { } func parseExpression(i *TokenIterator) (ast.Expression, error) { - if abs, absErr := parseAbstraction(i); absErr == nil { - return abs, nil - } else if atm, atmErr := parseApplication(i); atmErr == nil { - return atm, nil - } else if app, appErr := parseAtom(i); appErr == nil { - return app, nil - } else { - return nil, errors.Join(absErr, appErr, atmErr) + var err error + var exp ast.Expression + peek := i.MustGet() + + switch peek.Type { + case token.OpenParen: + exp, err = parseApplication(i) + case token.Slash: + exp, err = parseAbstraction(i) + case token.Atom: + exp, err = parseAtom(i) + default: + return nil, fmt.Errorf("expected expression, got '%v' (col %d)", peek.Value, peek.Index) } + + return exp, err } func parseParameters(i *TokenIterator) ([]string, error) { @@ -56,11 +63,11 @@ func parseAbstraction(i *TokenIterator) (*ast.Abstraction, error) { i2 := i.Copy() if _, err := parseToken(i2, token.Slash); err != nil { - return nil, fmt.Errorf("no function slash (col %d): %w", i2.MustGet().Index, err) + return nil, trace.WrapError(fmt.Errorf("no function slash (col %d)", i2.MustGet().Index), err) } else if parameters, err := parseParameters(i2); err != nil { return nil, err } else if _, err = parseToken(i2, token.Dot); err != nil { - return nil, fmt.Errorf("no function dot (col %d): %w", i2.MustGet().Index, err) + return nil, trace.WrapError(fmt.Errorf("no function dot (col %d)", i2.MustGet().Index), err) } else if body, err := parseExpression(i2); err != nil { return nil, err } else { @@ -74,11 +81,14 @@ func parseApplication(i *TokenIterator) (*ast.Application, error) { expressions := []ast.Expression{} if _, err := parseToken(i2, token.OpenParen); err != nil { - return nil, fmt.Errorf("no openning brackets (col %d): %w", i2.MustGet().Index, err) + return nil, trace.WrapError(fmt.Errorf("no openning brackets (col %d)", i2.MustGet().Index), err) } for { if exp, err := parseExpression(i2); err != nil { + if len(expressions) == 0 { + return nil, trace.WrapError(fmt.Errorf("application has no arguments"), err) + } break } else { expressions = append(expressions, exp) @@ -86,11 +96,7 @@ func parseApplication(i *TokenIterator) (*ast.Application, error) { } if _, err := parseToken(i2, token.CloseParen); err != nil { - return nil, fmt.Errorf("no closing brackets (col %d): %w", i2.MustGet().Index, err) - } - - if len(expressions) == 0 { - return nil, fmt.Errorf("application has no arguments") + return nil, trace.WrapError(fmt.Errorf("no closing brackets (col %d)", i2.MustGet().Index), err) } i.Sync(i2) @@ -99,7 +105,7 @@ func parseApplication(i *TokenIterator) (*ast.Application, error) { func parseAtom(i *TokenIterator) (*ast.Atom, error) { if tok, err := parseToken(i, token.Atom); err != nil { - return nil, fmt.Errorf("no variable (col %d): %w", i.Index(), err) + return nil, trace.WrapError(fmt.Errorf("no variable (col %d)", i.Index()), err) } else { return ast.NewAtom(tok.Value), nil } diff --git a/pkg/saccharine/token/token.go b/pkg/saccharine/token/token.go index 25983df..f7b143b 100644 --- a/pkg/saccharine/token/token.go +++ b/pkg/saccharine/token/token.go @@ -4,26 +4,19 @@ package token type Type int const ( - // Denotes the '(' token. - OpenParen Type = iota - // Denotes the ')' token. - CloseParen - // Denotes an alpha-numeric variable. - Atom - // Denotes the '/' token. - Slash - // Denotes the '.' token. - Dot + OpenParen Type = iota // Denotes the '(' token. + CloseParen // Denotes the ')' token. + Atom // Denotes an alpha-numeric variable. + Slash // Denotes the '/' token. + Dot // Denotes the '.' token. + Newline // Denotes a new-line. ) // A representation of a token in source code. type Token struct { - // Where the token begins in the source text. - Index int - // What type the token is. - Type Type - // The value of the token. - Value string + Index int // Where the token begins in the source text. + Type Type // What type the token is. + Value string // The value of the token. } func NewOpenParen(index int) *Token { @@ -46,6 +39,10 @@ func NewAtom(name string, index int) *Token { return &Token{Type: Atom, Index: index, Value: name} } +func NewNewline(index int) *Token { + return &Token{Type: Newline, Index: index, Value: "\\n"} +} + func Name(typ Type) string { switch typ { case OpenParen: @@ -58,6 +55,8 @@ func Name(typ Type) string { return "." case Atom: return "ATOM" + case Newline: + return "\\n" default: return "?" } diff --git a/pkg/saccharine/tokenizer.go b/pkg/saccharine/tokenizer.go index cdf4cbc..f2a156f 100644 --- a/pkg/saccharine/tokenizer.go +++ b/pkg/saccharine/tokenizer.go @@ -7,6 +7,7 @@ import ( "git.maximhutz.com/max/lambda/pkg/iterator" "git.maximhutz.com/max/lambda/pkg/saccharine/token" + "git.maximhutz.com/max/lambda/pkg/trace" ) // isVariables determines whether a rune can be a valid variable. @@ -38,7 +39,7 @@ func getToken(i *iterator.Iterator[rune]) (*token.Token, error) { letter, err := i.Next() if err != nil { - return nil, fmt.Errorf("cannot produce next token: %w", err) + return nil, trace.WrapError(fmt.Errorf("cannot produce next token"), err) } switch { @@ -50,6 +51,8 @@ func getToken(i *iterator.Iterator[rune]) (*token.Token, error) { return token.NewDot(index), nil case letter == '\\': return token.NewSlash(index), nil + case letter == '\n': + return token.NewNewline(index), nil case unicode.IsSpace(letter): return nil, nil case isVariable(letter): diff --git a/pkg/trace/trace.go b/pkg/trace/trace.go new file mode 100644 index 0000000..a2d9af6 --- /dev/null +++ b/pkg/trace/trace.go @@ -0,0 +1,23 @@ +package trace + +import ( + "errors" + "strings" +) + +func Indent(s string, size int) string { + lines := strings.Lines(s) + indent := strings.Repeat(" ", size) + + indented := "" + for line := range lines { + indented += indent + line + } + + return indented +} + +func WrapError(parent error, child error) error { + childErrString := Indent(child.Error(), 4) + return errors.New(parent.Error() + "\n" + childErrString) +} diff --git a/samples/simple.txt b/samples/simple.txt index fe8eaed..ed91543 100644 --- a/samples/simple.txt +++ b/samples/simple.txt @@ -1,16 +1 @@ (\0. - (\inc. - (\add. - (\mult. - (\exp. - (exp (inc (inc (inc (inc 0)))) (inc (inc (inc (inc (inc 0)))))) - \n m.(m n) - ) - \m n f.(m (n f)) - )) - \n m.(m inc n) - ) - \n f x.(f (n f x)) - ) - \f x.((((((x)))))) -) \ No newline at end of file