refactor: make lambda expression types immutable #38

Merged
mvhutz merged 1 commits from refactor/immutable-expressions into main 2026-01-17 22:00:54 +00:00
21 changed files with 191 additions and 255 deletions

View File

@@ -34,34 +34,34 @@ func main() {
logger.Info("compiled λ expression", "tree", compiled.String()) logger.Info("compiled λ expression", "tree", compiled.String())
// Create reducer with the compiled expression. // Create reducer with the compiled expression.
reducer := lambda.NewNormalOrderReducer(&compiled) interpreter := lambda.NewInterpreter(compiled)
// If the user selected to track CPU performance, attach a profiler. // If the user selected to track CPU performance, attach a profiler.
if options.Profile != "" { if options.Profile != "" {
plugins.NewPerformance(options.Profile, reducer) plugins.NewPerformance(options.Profile, interpreter)
} }
// If the user selected to produce a step-by-step explanation, attach an // If the user selected to produce a step-by-step explanation, attach an
// observer. // observer.
if options.Explanation { if options.Explanation {
plugins.NewExplanation(reducer) plugins.NewExplanation(interpreter)
} }
// If the user opted to track statistics, attach a tracker. // If the user opted to track statistics, attach a tracker.
if options.Statistics { if options.Statistics {
plugins.NewStatistics(reducer) plugins.NewStatistics(interpreter)
} }
// If the user selected for verbose debug logs, attach a reduction tracker. // If the user selected for verbose debug logs, attach a reduction tracker.
if options.Verbose { if options.Verbose {
plugins.NewLogs(logger, reducer) plugins.NewLogs(logger, interpreter)
} }
// Run reduction. // Run reduction.
reducer.Reduce() interpreter.Run()
// Return the final reduced result. // Return the final reduced result.
result := reducer.Expression().String() result := interpreter.Expression().String()
err = options.Destination.Write(result) err = options.Destination.Write(result)
cli.HandleError(err) cli.HandleError(err)
} }

View File

@@ -30,8 +30,8 @@ func runSample(samplePath string) (string, error) {
compiled := convert.SaccharineToLambda(ast) compiled := convert.SaccharineToLambda(ast)
// Create and run the reducer. // Create and run the reducer.
reducer := lambda.NewNormalOrderReducer(&compiled) reducer := lambda.NewInterpreter(compiled)
reducer.Reduce() reducer.Run()
return reducer.Expression().String() + "\n", nil return reducer.Expression().String() + "\n", nil
} }

View File

@@ -3,17 +3,17 @@ package plugins
import ( import (
"log/slog" "log/slog"
"git.maximhutz.com/max/lambda/pkg/reducer" "git.maximhutz.com/max/lambda/pkg/interpreter"
) )
type Logs struct { type Logs struct {
logger *slog.Logger logger *slog.Logger
reducer reducer.Reducer reducer interpreter.Interpreter
} }
func NewLogs(logger *slog.Logger, r reducer.Reducer) *Logs { func NewLogs(logger *slog.Logger, r interpreter.Interpreter) *Logs {
plugin := &Logs{logger, r} plugin := &Logs{logger, r}
r.On(reducer.StepEvent, plugin.Step) r.On(interpreter.StepEvent, plugin.Step)
return plugin return plugin
} }

View File

@@ -5,19 +5,19 @@ package plugins
import ( import (
"fmt" "fmt"
"git.maximhutz.com/max/lambda/pkg/reducer" "git.maximhutz.com/max/lambda/pkg/interpreter"
) )
// Track the reductions made by a reduction process. // Track the reductions made by a reduction process.
type Explanation struct { type Explanation struct {
reducer reducer.Reducer reducer interpreter.Interpreter
} }
// Attaches a new explanation tracker to a reducer. // Attaches a new explanation tracker to a reducer.
func NewExplanation(r reducer.Reducer) *Explanation { func NewExplanation(r interpreter.Interpreter) *Explanation {
plugin := &Explanation{reducer: r} plugin := &Explanation{reducer: r}
r.On(reducer.StartEvent, plugin.Start) r.On(interpreter.StartEvent, plugin.Start)
r.On(reducer.StepEvent, plugin.Step) r.On(interpreter.StepEvent, plugin.Step)
return plugin return plugin
} }

View File

@@ -7,7 +7,7 @@ import (
"path/filepath" "path/filepath"
"runtime/pprof" "runtime/pprof"
"git.maximhutz.com/max/lambda/pkg/reducer" "git.maximhutz.com/max/lambda/pkg/interpreter"
) )
// Observes a reduction process, and publishes a CPU performance profile on // Observes a reduction process, and publishes a CPU performance profile on
@@ -19,10 +19,10 @@ type Performance struct {
} }
// Create a performance tracker that outputs a profile to "file". // Create a performance tracker that outputs a profile to "file".
func NewPerformance(file string, process reducer.Reducer) *Performance { func NewPerformance(file string, process interpreter.Interpreter) *Performance {
plugin := &Performance{File: file} plugin := &Performance{File: file}
process.On(reducer.StartEvent, plugin.Start) process.On(interpreter.StartEvent, plugin.Start)
process.On(reducer.StopEvent, plugin.Stop) process.On(interpreter.StopEvent, plugin.Stop)
return plugin return plugin
} }

View File

@@ -6,7 +6,7 @@ import (
"time" "time"
"git.maximhutz.com/max/lambda/internal/statistics" "git.maximhutz.com/max/lambda/internal/statistics"
"git.maximhutz.com/max/lambda/pkg/reducer" "git.maximhutz.com/max/lambda/pkg/interpreter"
) )
// An observer, to track reduction performance. // An observer, to track reduction performance.
@@ -16,11 +16,11 @@ type Statistics struct {
} }
// Create a new reduction performance Statistics. // Create a new reduction performance Statistics.
func NewStatistics(r reducer.Reducer) *Statistics { func NewStatistics(r interpreter.Interpreter) *Statistics {
plugin := &Statistics{} plugin := &Statistics{}
r.On(reducer.StartEvent, plugin.Start) r.On(interpreter.StartEvent, plugin.Start)
r.On(reducer.StepEvent, plugin.Step) r.On(interpreter.StepEvent, plugin.Step)
r.On(reducer.StopEvent, plugin.Stop) r.On(interpreter.StopEvent, plugin.Stop)
return plugin return plugin
} }

13
pkg/interpreter/events.go Normal file
View File

@@ -0,0 +1,13 @@
package interpreter
// Event represents lifecycle events during interpretation.
type Event int
const (
// StartEvent is emitted before interpretation begins.
StartEvent Event = iota
// StepEvent is emitted after each interpretation step.
StepEvent
// StopEvent is emitted after interpretation completes.
StopEvent
)

View File

@@ -1,6 +1,6 @@
// Package reducer provides the abstract Reducer interface for all expression // Package interpreter provides the abstract Reducer interface for all expression
// reduction strategies. // reduction strategies.
package reducer package interpreter
import ( import (
"git.maximhutz.com/max/lambda/pkg/emitter" "git.maximhutz.com/max/lambda/pkg/emitter"
@@ -13,14 +13,14 @@ import (
// //
// Reducers also implement the Emitter interface to allow plugins to observe // Reducers also implement the Emitter interface to allow plugins to observe
// reduction lifecycle events (Start, Step, Stop). // reduction lifecycle events (Start, Step, Stop).
type Reducer interface { type Interpreter interface {
emitter.Emitter[Event] emitter.Emitter[Event]
// Reduce performs all reduction steps on the expression. // Run a single step. Returns whether the interpreter is complete or not.
// Emits StartEvent before reduction, StepEvent after each step, and Step() bool
// StopEvent after completion.
// Returns the final reduced expression. // Run until completion.
Reduce() Run()
// Expression returns the current expression state. // Expression returns the current expression state.
Expression() expr.Expression Expression() expr.Expression

View File

@@ -17,22 +17,22 @@ type Abstraction struct {
body Expression body Expression
} }
var _ Expression = (*Abstraction)(nil) var _ Expression = Abstraction{}
func (a *Abstraction) Parameter() string { func (a Abstraction) Parameter() string {
return a.parameter return a.parameter
} }
func (a *Abstraction) Body() Expression { func (a Abstraction) Body() Expression {
return a.body return a.body
} }
func (a *Abstraction) String() string { func (a Abstraction) String() string {
return "\\" + a.parameter + "." + a.body.String() return "\\" + a.parameter + "." + a.body.String()
} }
func NewAbstraction(parameter string, body Expression) *Abstraction { func NewAbstraction(parameter string, body Expression) Abstraction {
return &Abstraction{parameter, body} return Abstraction{parameter, body}
} }
/** ------------------------------------------------------------------------- */ /** ------------------------------------------------------------------------- */
@@ -42,40 +42,40 @@ type Application struct {
argument Expression argument Expression
} }
var _ Expression = (*Application)(nil) var _ Expression = Application{}
func (a *Application) Abstraction() Expression { func (a Application) Abstraction() Expression {
return a.abstraction return a.abstraction
} }
func (a *Application) Argument() Expression { func (a Application) Argument() Expression {
return a.argument return a.argument
} }
func (a *Application) String() string { func (a Application) String() string {
return "(" + a.abstraction.String() + " " + a.argument.String() + ")" return "(" + a.abstraction.String() + " " + a.argument.String() + ")"
} }
func NewApplication(abstraction Expression, argument Expression) *Application { func NewApplication(abstraction Expression, argument Expression) Application {
return &Application{abstraction, argument} return Application{abstraction, argument}
} }
/** ------------------------------------------------------------------------- */ /** ------------------------------------------------------------------------- */
type Variable struct { type Variable struct {
value string name string
} }
var _ Expression = (*Variable)(nil) var _ Expression = Variable{}
func (v *Variable) Value() string { func (v Variable) Name() string {
return v.value return v.name
} }
func (v *Variable) String() string { func (v Variable) String() string {
return v.value return v.name
} }
func NewVariable(name string) *Variable { func NewVariable(name string) Variable {
return &Variable{name} return Variable{name}
} }

View File

@@ -6,9 +6,13 @@ import (
"git.maximhutz.com/max/lambda/pkg/set" "git.maximhutz.com/max/lambda/pkg/set"
) )
var ticker uint64 = 0
// GenerateFreshName generates a variable name that is not in the used set.
// This function does not mutate the used set.
func GenerateFreshName(used *set.Set[string]) string { func GenerateFreshName(used *set.Set[string]) string {
for i := uint64(0); ; i++ { for i := uint64(0); ; i++ {
attempt := "_" + string(strconv.AppendUint(nil, i, 10)) attempt := "_" + string(strconv.AppendUint(nil, ticker, 10))
if !used.Has(attempt) { if !used.Has(attempt) {
return attempt return attempt

View File

@@ -2,19 +2,22 @@ package lambda
import "git.maximhutz.com/max/lambda/pkg/set" import "git.maximhutz.com/max/lambda/pkg/set"
// GetFreeVariables returns the set of all free variable names in the expression.
// This function does not mutate the input expression.
// The returned set is newly allocated and can be modified by the caller.
func GetFreeVariables(e Expression) *set.Set[string] { func GetFreeVariables(e Expression) *set.Set[string] {
switch e := e.(type) { switch e := e.(type) {
case *Variable: case Variable:
return set.New(e.value) return set.New(e.Name())
case *Abstraction: case Abstraction:
vars := GetFreeVariables(e.body) vars := GetFreeVariables(e.Body())
vars.Remove(e.parameter) vars.Remove(e.Parameter())
return vars return vars
case *Application: case Application:
vars := GetFreeVariables(e.abstraction) vars := GetFreeVariables(e.Abstraction())
vars.Merge(GetFreeVariables(e.argument)) vars.Merge(GetFreeVariables(e.Argument()))
return vars return vars
default: default:
return nil return set.New[string]()
} }
} }

45
pkg/lambda/interpreter.go Normal file
View File

@@ -0,0 +1,45 @@
package lambda
import (
"git.maximhutz.com/max/lambda/pkg/emitter"
"git.maximhutz.com/max/lambda/pkg/expr"
"git.maximhutz.com/max/lambda/pkg/interpreter"
)
// NormalOrderReducer implements normal order (leftmost-outermost) reduction
// for lambda calculus expressions.
type Interpreter struct {
emitter.BaseEmitter[interpreter.Event]
expression Expression
}
// NewNormalOrderReducer creates a new normal order reducer.
func NewInterpreter(expression Expression) *Interpreter {
return &Interpreter{
BaseEmitter: *emitter.New[interpreter.Event](),
expression: expression,
}
}
// Expression returns the current expression state.
func (r *Interpreter) Expression() expr.Expression {
return r.expression
}
func (r *Interpreter) Step() bool {
result, done := ReduceOnce(r.expression)
r.expression = result
return !done
}
// Reduce performs normal order reduction on a lambda expression.
// The expression must be a lambda.Expression; other types are returned unchanged.
func (r *Interpreter) Run() {
r.Emit(interpreter.StartEvent)
for !r.Step() {
r.Emit(interpreter.StepEvent)
}
r.Emit(interpreter.StopEvent)
}

View File

@@ -1,13 +1,15 @@
package lambda package lambda
// IsFreeVariable returns true if the variable name n occurs free in the expression.
// This function does not mutate the input expression.
func IsFreeVariable(n string, e Expression) bool { func IsFreeVariable(n string, e Expression) bool {
switch e := e.(type) { switch e := e.(type) {
case *Variable: case Variable:
return e.value == n return e.Name() == n
case *Abstraction: case Abstraction:
return e.parameter != n && IsFreeVariable(n, e.body) return e.Parameter() != n && IsFreeVariable(n, e.Body())
case *Application: case Application:
return IsFreeVariable(n, e.abstraction) || IsFreeVariable(n, e.argument) return IsFreeVariable(n, e.Abstraction()) || IsFreeVariable(n, e.Argument())
default: default:
return false return false
} }

View File

@@ -1,68 +0,0 @@
package lambda
type Iterator struct {
trace []*Expression
}
func NewIterator(expr *Expression) *Iterator {
return &Iterator{[]*Expression{expr}}
}
func (i *Iterator) Done() bool {
return len(i.trace) == 0
}
func (i *Iterator) Current() *Expression {
if i.Done() {
return nil
}
return i.trace[len(i.trace)-1]
}
func (i *Iterator) Parent() *Expression {
if len(i.trace) < 2 {
return nil
}
return i.trace[len(i.trace)-2]
}
func (i *Iterator) Swap(with Expression) {
current := i.Current()
if current != nil {
*current = with
}
}
func (i *Iterator) Back() bool {
if i.Done() {
return false
}
i.trace = i.trace[:len(i.trace)-1]
return true
}
func (i *Iterator) Next() {
switch typed := (*i.Current()).(type) {
case *Abstraction:
i.trace = append(i.trace, &typed.body)
case *Application:
i.trace = append(i.trace, &typed.abstraction)
case *Variable:
for len(i.trace) > 1 {
if app, ok := (*i.Parent()).(*Application); ok {
if app.abstraction == *i.Current() {
i.Back()
i.trace = append(i.trace, &app.argument)
return
}
}
i.Back()
}
i.trace = []*Expression{}
}
}

32
pkg/lambda/reduce.go Normal file
View File

@@ -0,0 +1,32 @@
package lambda
func ReduceOnce(e Expression) (Expression, bool) {
switch e := e.(type) {
case Abstraction:
body, reduced := ReduceOnce(e.Body())
if reduced {
return NewAbstraction(e.Parameter(), body), true
}
return e, false
case Application:
if fn, fnOk := e.Abstraction().(Abstraction); fnOk {
return Substitute(fn.Body(), fn.Parameter(), e.Argument()), true
}
abs, reduced := ReduceOnce(e.Abstraction())
if reduced {
return NewApplication(abs, e.Argument()), true
}
arg, reduced := ReduceOnce(e.Argument())
if reduced {
return NewApplication(e.Abstraction(), arg), true
}
return e, false
default:
return e, false
}
}

View File

@@ -1,61 +0,0 @@
package lambda
import (
"git.maximhutz.com/max/lambda/pkg/emitter"
"git.maximhutz.com/max/lambda/pkg/expr"
"git.maximhutz.com/max/lambda/pkg/reducer"
)
// NormalOrderReducer implements normal order (leftmost-outermost) reduction
// for lambda calculus expressions.
type NormalOrderReducer struct {
emitter.BaseEmitter[reducer.Event]
expression *Expression
}
// NewNormalOrderReducer creates a new normal order reducer.
func NewNormalOrderReducer(expression *Expression) *NormalOrderReducer {
return &NormalOrderReducer{
BaseEmitter: *emitter.New[reducer.Event](),
expression: expression,
}
}
// Expression returns the current expression state.
func (r *NormalOrderReducer) Expression() expr.Expression {
return *r.expression
}
func isViable(e *Expression) (*Abstraction, Expression, bool) {
if e == nil {
return nil, nil, false
} else if app, appOk := (*e).(*Application); !appOk {
return nil, nil, false
} else if fn, fnOk := app.abstraction.(*Abstraction); !fnOk {
return nil, nil, false
} else {
return fn, app.argument, true
}
}
// Reduce performs normal order reduction on a lambda expression.
// The expression must be a lambda.Expression; other types are returned unchanged.
func (r *NormalOrderReducer) Reduce() {
r.Emit(reducer.StartEvent)
it := NewIterator(r.expression)
for !it.Done() {
if fn, arg, ok := isViable(it.Current()); !ok {
it.Next()
} else {
it.Swap(Substitute(fn.body, fn.parameter, arg))
r.Emit(reducer.StepEvent)
if _, _, ok := isViable(it.Parent()); ok {
it.Back()
}
}
}
r.Emit(reducer.StopEvent)
}

View File

@@ -1,34 +1,27 @@
package lambda package lambda
// Rename replaces all occurrences of the target variable name with the new name.
func Rename(expr Expression, target string, newName string) Expression { func Rename(expr Expression, target string, newName string) Expression {
switch e := expr.(type) { switch e := expr.(type) {
case *Variable: case Variable:
if e.value == target { if e.Name() == target {
return NewVariable(newName) return NewVariable(newName)
} }
return e return e
case *Abstraction: case Abstraction:
newParam := e.parameter newParam := e.Parameter()
if e.parameter == target { if e.Parameter() == target {
newParam = newName newParam = newName
} }
newBody := Rename(e.body, target, newName) newBody := Rename(e.Body(), target, newName)
if newParam == e.parameter && newBody == e.body {
return e
}
return NewAbstraction(newParam, newBody) return NewAbstraction(newParam, newBody)
case *Application: case Application:
newAbs := Rename(e.abstraction, target, newName) newAbs := Rename(e.Abstraction(), target, newName)
newArg := Rename(e.argument, target, newName) newArg := Rename(e.Argument(), target, newName)
if newAbs == e.abstraction && newArg == e.argument {
return e
}
return NewApplication(newAbs, newArg) return NewApplication(newAbs, newArg)

View File

@@ -1,20 +1,22 @@
package lambda package lambda
// Substitute replaces all free occurrences of the target variable with the replacement expression.
// Alpha-renaming is performed automatically to avoid variable capture.
func Substitute(expr Expression, target string, replacement Expression) Expression { func Substitute(expr Expression, target string, replacement Expression) Expression {
switch e := expr.(type) { switch e := expr.(type) {
case *Variable: case Variable:
if e.value == target { if e.Name() == target {
return replacement return replacement
} }
return e return e
case *Abstraction: case Abstraction:
if e.parameter == target { if e.Parameter() == target {
return e return e
} }
body := e.body body := e.Body()
param := e.parameter param := e.Parameter()
if IsFreeVariable(param, replacement) { if IsFreeVariable(param, replacement) {
freeVars := GetFreeVariables(replacement) freeVars := GetFreeVariables(replacement)
freeVars.Merge(GetFreeVariables(body)) freeVars.Merge(GetFreeVariables(body))
@@ -24,19 +26,12 @@ func Substitute(expr Expression, target string, replacement Expression) Expressi
} }
newBody := Substitute(body, target, replacement) newBody := Substitute(body, target, replacement)
if newBody == body && param == e.parameter {
return e
}
return NewAbstraction(param, newBody) return NewAbstraction(param, newBody)
case *Application: case Application:
newAbs := Substitute(e.abstraction, target, replacement) newAbs := Substitute(e.Abstraction(), target, replacement)
newArg := Substitute(e.argument, target, replacement) newArg := Substitute(e.Argument(), target, replacement)
if newAbs == e.abstraction && newArg == e.argument {
return e
}
return NewApplication(newAbs, newArg) return NewApplication(newAbs, newArg)

View File

@@ -1,13 +0,0 @@
package reducer
// Event represents lifecycle events during reduction.
type Event int
const (
// StartEvent is emitted before reduction begins.
StartEvent Event = iota
// StepEvent is emitted after each reduction step.
StepEvent
// StopEvent is emitted after reduction completes.
StopEvent
)

File diff suppressed because one or more lines are too long

View File

@@ -1,8 +0,0 @@
0 := \f.\x.x
inc n := \f x.(f (n f x))
exp n m := (m n)
print n := (n F X)
N := (inc (inc (inc (inc (inc (inc 0))))))
(print (exp N N))