refactor: make lambda expression types immutable

- Change Abstraction, Application, and Variable to use private fields with getter methods
- Return value types instead of pointers from constructors
- Update all type switches to match value types instead of pointer types
- Remove pointer equality optimizations (not applicable with immutable values)
- Return empty set instead of nil from GetFreeVariables default case
This commit is contained in:
2026-01-17 16:29:24 -05:00
parent c2aa77cb92
commit 7318c0dac4
21 changed files with 191 additions and 255 deletions

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

View File

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

View File

@@ -6,9 +6,13 @@ import (
"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 {
for i := uint64(0); ; i++ {
attempt := "_" + string(strconv.AppendUint(nil, i, 10))
attempt := "_" + string(strconv.AppendUint(nil, ticker, 10))
if !used.Has(attempt) {
return attempt

View File

@@ -2,19 +2,22 @@ package lambda
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] {
switch e := e.(type) {
case *Variable:
return set.New(e.value)
case *Abstraction:
vars := GetFreeVariables(e.body)
vars.Remove(e.parameter)
case Variable:
return set.New(e.Name())
case Abstraction:
vars := GetFreeVariables(e.Body())
vars.Remove(e.Parameter())
return vars
case *Application:
vars := GetFreeVariables(e.abstraction)
vars.Merge(GetFreeVariables(e.argument))
case Application:
vars := GetFreeVariables(e.Abstraction())
vars.Merge(GetFreeVariables(e.Argument()))
return vars
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
// 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 {
switch e := e.(type) {
case *Variable:
return e.value == n
case *Abstraction:
return e.parameter != n && IsFreeVariable(n, e.body)
case *Application:
return IsFreeVariable(n, e.abstraction) || IsFreeVariable(n, e.argument)
case Variable:
return e.Name() == n
case Abstraction:
return e.Parameter() != n && IsFreeVariable(n, e.Body())
case Application:
return IsFreeVariable(n, e.Abstraction()) || IsFreeVariable(n, e.Argument())
default:
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
// Rename replaces all occurrences of the target variable name with the new name.
func Rename(expr Expression, target string, newName string) Expression {
switch e := expr.(type) {
case *Variable:
if e.value == target {
case Variable:
if e.Name() == target {
return NewVariable(newName)
}
return e
case *Abstraction:
newParam := e.parameter
if e.parameter == target {
case Abstraction:
newParam := e.Parameter()
if e.Parameter() == target {
newParam = newName
}
newBody := Rename(e.body, target, newName)
if newParam == e.parameter && newBody == e.body {
return e
}
newBody := Rename(e.Body(), target, newName)
return NewAbstraction(newParam, newBody)
case *Application:
newAbs := Rename(e.abstraction, target, newName)
newArg := Rename(e.argument, target, newName)
if newAbs == e.abstraction && newArg == e.argument {
return e
}
case Application:
newAbs := Rename(e.Abstraction(), target, newName)
newArg := Rename(e.Argument(), target, newName)
return NewApplication(newAbs, newArg)

View File

@@ -1,20 +1,22 @@
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 {
switch e := expr.(type) {
case *Variable:
if e.value == target {
case Variable:
if e.Name() == target {
return replacement
}
return e
case *Abstraction:
if e.parameter == target {
case Abstraction:
if e.Parameter() == target {
return e
}
body := e.body
param := e.parameter
body := e.Body()
param := e.Parameter()
if IsFreeVariable(param, replacement) {
freeVars := GetFreeVariables(replacement)
freeVars.Merge(GetFreeVariables(body))
@@ -24,19 +26,12 @@ func Substitute(expr Expression, target string, replacement Expression) Expressi
}
newBody := Substitute(body, target, replacement)
if newBody == body && param == e.parameter {
return e
}
return NewAbstraction(param, newBody)
case *Application:
newAbs := Substitute(e.abstraction, target, replacement)
newArg := Substitute(e.argument, target, replacement)
if newAbs == e.abstraction && newArg == e.argument {
return e
}
case Application:
newAbs := Substitute(e.Abstraction(), target, replacement)
newArg := Substitute(e.Argument(), target, replacement)
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
)