diff --git a/cmd/lambda/lambda.go b/cmd/lambda/lambda.go index 9900c5e..4ac4180 100644 --- a/cmd/lambda/lambda.go +++ b/cmd/lambda/lambda.go @@ -1,7 +1,6 @@ package main import ( - "errors" "fmt" "os" "time" @@ -26,10 +25,8 @@ func main() { cli.HandleError(err) // Parse tokens. - tokens, fails := tokenizer.GetTokens([]rune(input)) - if len(fails) > 0 { - cli.HandleError(errors.Join(fails...)) - } + tokens, err := tokenizer.GetTokens([]rune(input)) + cli.HandleError(err) logger.Info("Parsed tokens.", "tokens", tokens) // Turn tokens into syntax tree. diff --git a/pkg/iterator/iterator.go b/pkg/iterator/iterator.go index 503906b..afdfc0e 100644 --- a/pkg/iterator/iterator.go +++ b/pkg/iterator/iterator.go @@ -12,8 +12,8 @@ type Iterator[T any] struct { } // Create a new iterator, over a set of items. -func New[T any](items []T) Iterator[T] { - return Iterator[T]{data: items, index: 0} +func New[T any](items []T) *Iterator[T] { + return &Iterator[T]{data: items, index: 0} } // Returns the current position of the iterator. @@ -40,7 +40,7 @@ func (i Iterator[T]) Peek() (T, error) { // Moves the iterator pointer to the next item. Returns the current item. Fails // if there are no more items to iterate over. -func (i *Iterator[T]) Next() (T, error) { +func (i *Iterator[T]) Pop() (T, error) { val, err := i.Peek() if err != nil { return val, err @@ -49,3 +49,22 @@ func (i *Iterator[T]) Next() (T, error) { i.index++ return val, nil } + +// Pop until the clause returns false. +func (i *Iterator[T]) PopWhile(fn func(T) bool) []T { + result := []T{} + + for { + popped, err := i.Peek() + if err != nil || !fn(popped) { + break + } + + result = append(result, popped) + if _, err := i.Pop(); err != nil { + break + } + } + + return result +} diff --git a/pkg/lambda/generate_name.go b/pkg/lambda/generate_name.go index 3904035..ffafe76 100644 --- a/pkg/lambda/generate_name.go +++ b/pkg/lambda/generate_name.go @@ -7,12 +7,8 @@ import ( ) func GenerateFreshName(used set.Set[string]) string { - // Starts as '0', that is the base value of a 'uint64'. - var i uint64 - - for { + for i := uint64(0); ; i++ { attempt := "_" + string(strconv.AppendUint(nil, i, 10)) - i++ if !used.Has(attempt) { return attempt diff --git a/pkg/parser/parser.go b/pkg/parser/parser.go index bc7af89..81a6016 100644 --- a/pkg/parser/parser.go +++ b/pkg/parser/parser.go @@ -9,7 +9,7 @@ import ( ) func ParseExpression(i *iterator.Iterator[tokenizer.Token]) (lambda.Expression, error) { - token, err := i.Next() + token, err := i.Pop() if err != nil { return nil, fmt.Errorf("could not get next token: %w", err) } @@ -23,7 +23,7 @@ func ParseExpression(i *iterator.Iterator[tokenizer.Token]) (lambda.Expression, atoms := []string{} for { - atom, atomErr := i.Next() + atom, atomErr := i.Pop() if atomErr != nil { return nil, fmt.Errorf("could not find parameter or terminator of function: %w", atomErr) } else if atom.Type == tokenizer.TokenVariable { @@ -72,7 +72,7 @@ func ParseExpression(i *iterator.Iterator[tokenizer.Token]) (lambda.Expression, args = append(args, arg) } - closing, closingErr := i.Next() + closing, closingErr := i.Pop() if closingErr != nil { return nil, fmt.Errorf("could not parse call terminating parenthesis: %w", closingErr) } else if closing.Type != tokenizer.TokenCloseParen { @@ -94,6 +94,5 @@ func ParseExpression(i *iterator.Iterator[tokenizer.Token]) (lambda.Expression, } func GetTree(tokens []tokenizer.Token) (lambda.Expression, error) { - i := iterator.New(tokens) - return ParseExpression(&i) + return ParseExpression(iterator.New(tokens)) } diff --git a/pkg/tokenizer/token.go b/pkg/tokenizer/token.go index 0bc88f4..52a92d3 100644 --- a/pkg/tokenizer/token.go +++ b/pkg/tokenizer/token.go @@ -1,17 +1,27 @@ package tokenizer +// All tokens in the pseudo-lambda language. type TokenType int const ( + // Denotes the '(' token. TokenOpenParen TokenType = iota + // Denotes the ')' token. TokenCloseParen + // Denotes an alpha-numeric variable. TokenVariable + // Denotes the '/' token. TokenSlash + // Denotes the '.' token. TokenDot ) +// A representation of a token in source code. type Token struct { + // Where the token begins in the source text. Index int - Type TokenType + // What type the token is. + Type TokenType + // The value of the token. Value string } diff --git a/pkg/tokenizer/tokenizer.go b/pkg/tokenizer/tokenizer.go index bdd73f6..af2c946 100644 --- a/pkg/tokenizer/tokenizer.go +++ b/pkg/tokenizer/tokenizer.go @@ -1,95 +1,72 @@ package tokenizer import ( + "errors" "fmt" - "strings" "unicode" "git.maximhutz.com/max/lambda/pkg/iterator" ) +// isVariables determines whether a rune can be a valid variable. func isVariable(r rune) bool { return unicode.IsLetter(r) || unicode.IsNumber(r) } +// Pulls the next token from an iterator over runes. If it cannot, it will +// return nil. If an error occurs, it will return that. func getToken(i *iterator.Iterator[rune]) (*Token, error) { + index := i.Index() + if i.IsDone() { return nil, nil } - letter, err := i.Next() + letter, err := i.Pop() if err != nil { return nil, fmt.Errorf("cannot produce next token: %w", err) } - // If it is an operand. - switch letter { - case '(': - return &Token{ - Type: TokenOpenParen, - Index: i.Index(), - Value: string(letter), - }, nil - case ')': - return &Token{ - Type: TokenCloseParen, - Index: i.Index(), - Value: string(letter), - }, nil - case '.': - return &Token{ - Type: TokenDot, - Index: i.Index(), - Value: string(letter), - }, nil - case '\\': - return &Token{ - Type: TokenSlash, - Index: i.Index(), - Value: string(letter), - }, nil - } - - // If it is a space. - if unicode.IsSpace(letter) { + switch { + case letter == '(': + // The opening deliminator of an application. + return &Token{Type: TokenOpenParen, Index: index, Value: string(letter)}, nil + case letter == ')': + // The terminator of an application. + return &Token{Type: TokenCloseParen, Index: index, Value: string(letter)}, nil + case letter == '.': + // The terminator of the parameters in an abstraction. + return &Token{Type: TokenDot, Index: index, Value: string(letter)}, nil + case letter == '\\': + // The opening deliminator of an abstraction. + return &Token{Type: TokenSlash, Index: index, Value: string(letter)}, nil + case unicode.IsSpace(letter): + // If there is a space character, ignore it. return nil, nil + case isVariable(letter): + rest := i.PopWhile(isVariable) + atom := string(append([]rune{letter}, rest...)) + + return &Token{Index: index, Type: TokenVariable, Value: atom}, nil } - // Otherwise, it is an atom. - atom := strings.Builder{} - index := i.Index() - for !i.IsDone() { - pop, err := i.Peek() - if err != nil || !isVariable(pop) { - break - } - - atom.WriteRune(pop) - if _, err := i.Next(); err != nil { - break - } - } - - return &Token{ - Index: index, - Type: TokenVariable, - Value: atom.String(), - }, nil + return nil, fmt.Errorf("unknown character '%v'", letter) } -func GetTokens(input []rune) ([]Token, []error) { +// Parses a list of runes into tokens. All error encountered are returned, as well. +func GetTokens(input []rune) ([]Token, error) { i := iterator.New(input) tokens := []Token{} - errors := []error{} + errorList := []error{} for !i.IsDone() { - token, err := getToken(&i) + token, err := getToken(i) if err != nil { - errors = append(errors, err) + errorList = append(errorList, err) } else if token != nil { tokens = append(tokens, *token) } } - return tokens, errors + return tokens, errors.Join(errorList...) }