diff --git a/Makefile b/Makefile index 0fac9f5..5874839 100644 --- a/Makefile +++ b/Makefile @@ -4,5 +4,5 @@ it: @ go build -o ${BINARY_NAME} ./cmd/lambda @ chmod +x ${BINARY_NAME} -ex1: it - @ ./lambda.exe "(\n.\f.\x.(f ((n f) x)) \f.\x.x)" \ No newline at end of file +ex: it + @ ./lambda.exe -v "(\add.(add (add \f.\x.x)) \n.\f.\x.(f ((n f) x)))" \ No newline at end of file diff --git a/cmd/lambda/lambda.go b/cmd/lambda/lambda.go index 72e0f34..1e232bf 100644 --- a/cmd/lambda/lambda.go +++ b/cmd/lambda/lambda.go @@ -28,11 +28,11 @@ func main() { expression, err := parser.GetTree(tokens) cli.HandleError(err) - logger.Info("Parsed syntax tree.", "tree", expression) + logger.Info("Parsed syntax tree.", "tree", lambda.Stringify(expression)) - evaluated := lambda.Evaluate(expression) + lambda.Normalize(&expression) cli.HandleError(err) - logger.Info("Evaluated expression.", "tree", evaluated) + logger.Info("Evaluated expression.", "tree", expression) - fmt.Println(lambda.ToString(evaluated)) + fmt.Println(lambda.Stringify(expression)) } diff --git a/pkg/lambda/evaluate.go b/pkg/lambda/evaluate.go deleted file mode 100644 index 97822f9..0000000 --- a/pkg/lambda/evaluate.go +++ /dev/null @@ -1,70 +0,0 @@ -package lambda - -// func replaceUnbound(name string, replacement Expression, e Expression) Expression { -// switch e := e.(type) { -// case Atom: -// if e.Value == name { -// return replacement -// } else { -// return e -// } -// case Function: -// if e.Parameter == name { -// return e -// } else { -// return Function{ -// Parameter: e.Parameter, -// Body: replaceUnbound(name, replacement, e.Body), -// } -// } -// case Call: -// return Call{ -// Function: replaceUnbound(name, replacement, e.Function), -// Argument: replaceUnbound(name, replacement, e.Argument), -// } -// } -// } - -// func evaluateAtom(a Atom) Expression { -// return a -// } - -// func evaluateFunction(f Function) Expression { -// return Function{ -// Parameter: f.Parameter, -// Body: Evaluate(f.Body), -// } -// } - -// func evaluateCall(c Call) Expression { -// fn := c.Function - -// as_fn, as_fn_ok := fn.(Function) -// if !as_fn_ok { -// fn = Evaluate(fn) - -// if as_fn, as_fn_ok = fn.(Function); !as_fn_ok { -// return Call{ -// Function: fn, -// Argument: Evaluate(c.Argument), -// } -// } -// } - -// return replaceUnboundAtoms(as_fn.Parameter, c.Argument, as_fn.Body) -// } - -func Evaluate(e Expression) Expression { - // switch e := e.(type) { - // case Atom: - // return e - // case Call: - - // case Function: - // return Function{ - // Parameter: e.Parameter, - // Body: Evaluate(e.Body), - // } - // } - return e -} diff --git a/pkg/lambda/expression.go b/pkg/lambda/expression.go index fc02016..64a8a33 100644 --- a/pkg/lambda/expression.go +++ b/pkg/lambda/expression.go @@ -1,65 +1,57 @@ package lambda type Expression interface { - Accept(ExpressionVisitor) + Accept(Visitor) } /** ------------------------------------------------------------------------- */ -type Function struct { +type Abstraction struct { Parameter string Body Expression } -func NewFunction(parameter string, body Expression) *Function { - return &Function{ - Parameter: parameter, - Body: body, - } +func NewAbstraction(parameter string, body Expression) *Abstraction { + return &Abstraction{Parameter: parameter, Body: body} } -func (f *Function) Accept(v ExpressionVisitor) { - v.VisitFunction(f) +func (this *Abstraction) Accept(v Visitor) { + v.VisitAbstraction(this) } /** ------------------------------------------------------------------------- */ -type Call struct { - Function Expression - Argument Expression +type Application struct { + Abstraction Expression + Argument Expression } -func NewCall(function Expression, argument Expression) *Call { - return &Call{ - Function: function, - Argument: argument, - } +func NewApplication(function Expression, argument Expression) *Application { + return &Application{Abstraction: function, Argument: argument} } -func (c *Call) Accept(v ExpressionVisitor) { - v.VisitCall(c) +func (this *Application) Accept(v Visitor) { + v.VisitApplication(this) } /** ------------------------------------------------------------------------- */ -type Atom struct { +type Variable struct { Value string } -func NewAtom(name string) *Atom { - return &Atom{ - Value: name, - } +func NewVariable(name string) *Variable { + return &Variable{Value: name} } -func (a *Atom) Accept(v ExpressionVisitor) { - v.VisitAtom(a) +func (this *Variable) Accept(v Visitor) { + v.VisitVariable(this) } /** ------------------------------------------------------------------------- */ -type ExpressionVisitor interface { - VisitFunction(*Function) - VisitCall(*Call) - VisitAtom(*Atom) +type Visitor interface { + VisitAbstraction(*Abstraction) + VisitApplication(*Application) + VisitVariable(*Variable) } diff --git a/pkg/lambda/generate_name.go b/pkg/lambda/generate_name.go new file mode 100644 index 0000000..a37ab67 --- /dev/null +++ b/pkg/lambda/generate_name.go @@ -0,0 +1,19 @@ +package lambda + +import ( + "strconv" + + "git.maximhutz.com/max/lambda/pkg/set" +) + +func GenerateFreshName(used set.Set[string]) string { + var i uint64 = 0 + for { + attempt := "_" + string(strconv.AppendUint(nil, i, 10)) + i++ + + if !used.Has(attempt) { + return attempt + } + } +} diff --git a/pkg/lambda/get_free_variables.go b/pkg/lambda/get_free_variables.go new file mode 100644 index 0000000..d729a84 --- /dev/null +++ b/pkg/lambda/get_free_variables.go @@ -0,0 +1,20 @@ +package lambda + +import "git.maximhutz.com/max/lambda/pkg/set" + +func GetFreeVariables(e Expression) set.Set[string] { + switch e := e.(type) { + case *Variable: + return set.New(e.Value) + case *Abstraction: + vars := GetFreeVariables(e.Body) + vars.Remove(e.Parameter) + return vars + case *Application: + vars := GetFreeVariables(e.Abstraction) + vars.Union(GetFreeVariables(e.Argument)) + return vars + default: + return nil + } +} diff --git a/pkg/lambda/print.go b/pkg/lambda/print.go deleted file mode 100644 index 0690bc1..0000000 --- a/pkg/lambda/print.go +++ /dev/null @@ -1,36 +0,0 @@ -package lambda - -import "strings" - -type StringifyVisitor struct { - builder strings.Builder -} - -func (v *StringifyVisitor) VisitAtom(a *Atom) { - v.builder.WriteString(a.Value) -} - -func (v *StringifyVisitor) VisitFunction(f *Function) { - v.builder.WriteRune('\\') - v.builder.WriteString(f.Parameter) - v.builder.WriteRune('.') - f.Body.Accept(v) -} - -func (v *StringifyVisitor) VisitCall(c *Call) { - v.builder.WriteRune('(') - c.Function.Accept(v) - v.builder.WriteRune(' ') - c.Argument.Accept(v) - v.builder.WriteRune(')') -} - -func ToString(e Expression) string { - b := StringifyVisitor{ - builder: strings.Builder{}, - } - - e.Accept(&b) - - return b.builder.String() -} diff --git a/pkg/lambda/reduce.go b/pkg/lambda/reduce.go new file mode 100644 index 0000000..4410551 --- /dev/null +++ b/pkg/lambda/reduce.go @@ -0,0 +1,28 @@ +package lambda + +func ReduceOnce(e *Expression) bool { + switch typed := (*e).(type) { + case *Abstraction: + return ReduceOnce(&typed.Body) + case *Application: + fn, fn_ok := typed.Abstraction.(*Abstraction) + if fn_ok { + Substitute(&fn.Body, fn.Parameter, typed.Argument) + *e = fn.Body + return true + } + good := ReduceOnce(&typed.Abstraction) + if good { + return true + } + + return ReduceOnce(&typed.Argument) + default: + return false + } +} + +func Normalize(e *Expression) { + for ReduceOnce(e) { + } +} diff --git a/pkg/lambda/rename.go b/pkg/lambda/rename.go new file mode 100644 index 0000000..1cacb9c --- /dev/null +++ b/pkg/lambda/rename.go @@ -0,0 +1,19 @@ +package lambda + +func Rename(e Expression, target string, substitute string) { + switch e := e.(type) { + case *Variable: + if e.Value == target { + e.Value = substitute + } + case *Abstraction: + if e.Parameter == target { + e.Parameter = substitute + } + + Rename(e.Body, target, substitute) + case *Application: + Rename(e.Abstraction, target, substitute) + Rename(e.Argument, target, substitute) + } +} diff --git a/pkg/lambda/stringify.go b/pkg/lambda/stringify.go new file mode 100644 index 0000000..88f9c3f --- /dev/null +++ b/pkg/lambda/stringify.go @@ -0,0 +1,32 @@ +package lambda + +import "strings" + +type stringifyVisitor struct { + builder strings.Builder +} + +func (this *stringifyVisitor) VisitVariable(a *Variable) { + this.builder.WriteString(a.Value) +} + +func (this *stringifyVisitor) VisitAbstraction(f *Abstraction) { + this.builder.WriteRune('\\') + this.builder.WriteString(f.Parameter) + this.builder.WriteRune('.') + f.Body.Accept(this) +} + +func (this *stringifyVisitor) VisitApplication(c *Application) { + this.builder.WriteRune('(') + c.Abstraction.Accept(this) + this.builder.WriteRune(' ') + c.Argument.Accept(this) + this.builder.WriteRune(')') +} + +func Stringify(e Expression) string { + b := &stringifyVisitor{} + e.Accept(b) + return b.builder.String() +} diff --git a/pkg/lambda/substitute.go b/pkg/lambda/substitute.go new file mode 100644 index 0000000..3b219c5 --- /dev/null +++ b/pkg/lambda/substitute.go @@ -0,0 +1,29 @@ +package lambda + +func Substitute(e *Expression, target string, replacement Expression) { + switch typed := (*e).(type) { + case *Variable: + if typed.Value == target { + *e = replacement + } + case *Abstraction: + if typed.Parameter == target { + return + } + + replacement_free_vars := GetFreeVariables(replacement) + if !replacement_free_vars.Has(typed.Parameter) { + Substitute(&typed.Body, target, replacement) + return + } + + used := GetFreeVariables(typed.Body) + used.Union(replacement_free_vars) + fresh_var := GenerateFreshName(used) + Rename(typed.Body, typed.Parameter, fresh_var) + Substitute(&typed.Body, target, replacement) + case *Application: + Substitute(&typed.Abstraction, target, replacement) + Substitute(&typed.Argument, target, replacement) + } +} diff --git a/pkg/parser/parser.go b/pkg/parser/parser.go index 87e20ca..7a3301d 100644 --- a/pkg/parser/parser.go +++ b/pkg/parser/parser.go @@ -15,15 +15,15 @@ func ParseExpression(i *iterator.Iterator[tokenizer.Token]) (lambda.Expression, } switch token.Type { - case tokenizer.TokenAtom: - return lambda.NewAtom(token.Value), nil + case tokenizer.TokenVariable: + return lambda.NewVariable(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 { + } else if atom.Type != tokenizer.TokenVariable { return nil, fmt.Errorf("Expected function parameter, got '%v' (column %d).", atom.Value, atom.Index) } @@ -39,7 +39,7 @@ func ParseExpression(i *iterator.Iterator[tokenizer.Token]) (lambda.Expression, return nil, fmt.Errorf("Could not parse function body: %w", body_err) } - return lambda.NewFunction(atom.Value, body), nil + return lambda.NewAbstraction(atom.Value, body), nil case tokenizer.TokenOpenParen: fn, fn_err := ParseExpression(i) if fn_err != nil { @@ -58,7 +58,7 @@ func ParseExpression(i *iterator.Iterator[tokenizer.Token]) (lambda.Expression, return nil, fmt.Errorf("Expected call terminating parenthesis, got '%v' (column %v).", close.Value, close.Index) } - return lambda.NewCall(fn, arg), nil + return lambda.NewApplication(fn, arg), nil case tokenizer.TokenCloseParen: return nil, fmt.Errorf("Token ')' found without a corresponding openning parenthesis (column %d).", token.Index) default: diff --git a/pkg/set/set.go b/pkg/set/set.go new file mode 100644 index 0000000..e8be639 --- /dev/null +++ b/pkg/set/set.go @@ -0,0 +1,45 @@ +package set + +type Set[T comparable] map[T]bool + +func (this *Set[T]) Add(items ...T) { + for _, item := range items { + (*this)[item] = true + } +} + +func (this Set[T]) Has(item T) bool { + return this[item] == true +} + +func (this *Set[T]) Remove(items ...T) { + for _, item := range items { + delete(*this, item) + } +} + +func (this *Set[T]) Union(o Set[T]) { + for item := range o { + this.Add(item) + } +} + +func (this Set[T]) ToList() []T { + list := []T{} + + for item := range this { + list = append(list, item) + } + + return list +} + +func New[T comparable](items ...T) Set[T] { + result := Set[T]{} + + for _, item := range items { + result.Add(item) + } + + return result +} diff --git a/pkg/tokenizer/token.go b/pkg/tokenizer/token.go index 7ab9aa4..0bc88f4 100644 --- a/pkg/tokenizer/token.go +++ b/pkg/tokenizer/token.go @@ -5,13 +5,13 @@ type TokenType int const ( TokenOpenParen TokenType = iota TokenCloseParen - TokenAtom + TokenVariable TokenSlash TokenDot ) type Token struct { Index int - Type TokenType + Type TokenType Value string } diff --git a/pkg/tokenizer/tokenizer.go b/pkg/tokenizer/tokenizer.go index 571f468..332713f 100644 --- a/pkg/tokenizer/tokenizer.go +++ b/pkg/tokenizer/tokenizer.go @@ -62,7 +62,7 @@ func getToken(i *iterator.Iterator[rune]) (*Token, error) { if err != nil || unicode.IsSpace(pop) || unicode.IsPunct(pop) { return &Token{ Index: index, - Type: TokenAtom, + Type: TokenVariable, Value: atom, }, nil }