From 1d8ecba1184561145ae3096a3bc0fc3e3b51b870 Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 23 Dec 2025 21:54:42 -0500 Subject: [PATCH] feat: parser --- cmd/lambda/lambda.go | 9 +++-- pkg/lambda/expression.go | 23 ++++++++++++ pkg/parser/parser.go | 72 ++++++++++++++++++++++++++++++++++++++ pkg/tokenizer/tokenizer.go | 23 ++++++------ 4 files changed, 112 insertions(+), 15 deletions(-) create mode 100644 pkg/lambda/expression.go create mode 100644 pkg/parser/parser.go diff --git a/cmd/lambda/lambda.go b/cmd/lambda/lambda.go index 46c5e82..f39cfde 100644 --- a/cmd/lambda/lambda.go +++ b/cmd/lambda/lambda.go @@ -6,19 +6,24 @@ import ( "os" "git.maximhutz.com/max/lambda/pkg/cli" + "git.maximhutz.com/max/lambda/pkg/parser" "git.maximhutz.com/max/lambda/pkg/tokenizer" ) func main() { slog.Info("Using program arguments.", "args", os.Args) + options, err := cli.ParseOptions(os.Args[1:]) cli.HandleError(err) - slog.Info("Parsed CLI options.", "options", options) + tokens, fails := tokenizer.GetTokens([]rune(options.Input)) if len(fails) > 0 { cli.HandleError(errors.Join(fails...)) } - slog.Info("Parsed tokens.", "tokens", tokens) + + ast, err := parser.GetTree(tokens) + cli.HandleError(err) + slog.Info("Parsed syntax tree.", "tree", ast) } diff --git a/pkg/lambda/expression.go b/pkg/lambda/expression.go new file mode 100644 index 0000000..91dfe51 --- /dev/null +++ b/pkg/lambda/expression.go @@ -0,0 +1,23 @@ +package lambda + +type Expression interface { + isExpression() +} + +type Function struct { + Parameter string + Body Expression +} + +type Call struct { + Function Expression + Argument Expression +} + +type Atom struct { + Value string +} + +func (_ Function) isExpression() {} +func (_ Call) isExpression() {} +func (_ Atom) isExpression() {} \ No newline at end of file diff --git a/pkg/parser/parser.go b/pkg/parser/parser.go new file mode 100644 index 0000000..0e5a7a1 --- /dev/null +++ b/pkg/parser/parser.go @@ -0,0 +1,72 @@ +package parser + +import ( + "fmt" + + "git.maximhutz.com/max/lambda/pkg/iterator" + "git.maximhutz.com/max/lambda/pkg/lambda" + "git.maximhutz.com/max/lambda/pkg/tokenizer" +) + +func ParseExpression(i *iterator.Iterator[tokenizer.Token]) (lambda.Expression, error) { + token, err := i.Next() + if err != nil { + return nil, fmt.Errorf("Could not get next token: %w", err) + } + + switch token.Type { + case tokenizer.TokenAtom: + return lambda.Atom{Value: token.Value}, nil + case tokenizer.TokenDot: + return nil, fmt.Errorf("Token '.' found without a corresponding slash (column %d).", token.Index) + case tokenizer.TokenSlash: + atom, atom_err := i.Next() + if atom_err != nil { + return nil, fmt.Errorf("Could not find parameter of function: %w", atom_err) + } else if atom.Type != tokenizer.TokenAtom { + return nil, fmt.Errorf("Expected function parameter, got '%v' (column %d).", atom.Value, atom.Index) + } + + dot, dot_err := i.Next() + if dot_err != nil { + return nil, fmt.Errorf("Could not find function parameter terminator: %w", dot_err) + } else if dot.Type != tokenizer.TokenDot { + return nil, fmt.Errorf("Expected function parameter terminator, got '%v' (column %v).", dot.Value, dot.Index) + } + + body, body_err := ParseExpression(i) + if body_err != nil { + return nil, fmt.Errorf("Could not parse function body: %w", body_err) + } + + return lambda.Function{Parameter: atom.Value, Body: body}, nil + case tokenizer.TokenOpenParen: + fn, fn_err := ParseExpression(i) + if fn_err != nil { + return nil, fmt.Errorf("Could not parse call function: %w", fn_err) + } + + arg, arg_err := ParseExpression(i) + if arg_err != nil { + return nil, fmt.Errorf("Could not parse call argument: %w", arg_err) + } + + close, close_err := i.Next() + if close_err != nil { + return nil, fmt.Errorf("Could not parse call terminating parenthesis: %w", close_err) + } else if close.Type != tokenizer.TokenCloseParen { + return nil, fmt.Errorf("Expected call terminating parenthesis, got '%v' (column %v).", close.Value, close.Index) + } + + return lambda.Call{Function: fn, Argument: arg}, nil + case tokenizer.TokenCloseParen: + return nil, fmt.Errorf("Token ')' found without a corresponding openning parenthesis (column %d).", token.Index) + default: + return nil, fmt.Errorf("Unknown token '%v' (column %d).", token.Value, token.Index) + } +} + +func GetTree(tokens []tokenizer.Token) (lambda.Expression, error) { + i := iterator.New(tokens) + return ParseExpression(&i) +} diff --git a/pkg/tokenizer/tokenizer.go b/pkg/tokenizer/tokenizer.go index d18e806..571f468 100644 --- a/pkg/tokenizer/tokenizer.go +++ b/pkg/tokenizer/tokenizer.go @@ -21,25 +21,25 @@ func getToken(i *iterator.Iterator[rune]) (*Token, error) { switch letter { case '(': return &Token{ - Type: TokenOpenParen, + Type: TokenOpenParen, Index: i.Index(), Value: string(letter), }, nil case ')': return &Token{ - Type: TokenCloseParen, + Type: TokenCloseParen, Index: i.Index(), Value: string(letter), }, nil case '.': return &Token{ - Type: TokenDot, + Type: TokenDot, Index: i.Index(), Value: string(letter), }, nil case '\\': return &Token{ - Type: TokenSlash, + Type: TokenSlash, Index: i.Index(), Value: string(letter), }, nil @@ -58,19 +58,16 @@ func getToken(i *iterator.Iterator[rune]) (*Token, error) { return nil, nil } - pop, err := i.Next() - if err != nil { - return nil, err - } - - if unicode.IsSpace(pop) { + pop, err := i.Peek() + if err != nil || unicode.IsSpace(pop) || unicode.IsPunct(pop) { return &Token{ Index: index, - Type: TokenAtom, + Type: TokenAtom, Value: atom, }, nil } - + + i.Next() atom += string(pop) } } @@ -90,4 +87,4 @@ func GetTokens(input []rune) ([]Token, []error) { } return tokens, errors -} \ No newline at end of file +}