2 Commits

Author SHA1 Message Date
3ef27bc28a feat: fmt.Stringer 2026-01-17 15:37:31 -05:00
dbb988b633 refactor: replace visitor pattern with type-switch recursion
Closes #36
2026-01-17 15:02:08 -05:00
31 changed files with 348 additions and 286 deletions

View File

@@ -1,6 +1,6 @@
--- ---
name: "Bug Report" name: "Bug Report"
about: "Report a bug or unexpected behavior in the lambda runtime." about: "Report a bug or unexpected behavior in the lambda interpreter."
title: "fix: " title: "fix: "
ref: "main" ref: "main"
assignees: [] assignees: []

View File

@@ -1,6 +1,6 @@
--- ---
name: "Feature Request" name: "Feature Request"
about: "Suggest a new feature or enhancement for the lambda runtime." about: "Suggest a new feature or enhancement for the lambda interpreter."
title: "feat: " title: "feat: "
ref: "main" ref: "main"
assignees: [] assignees: []

View File

@@ -48,7 +48,7 @@ The "source code" for a work means the preferred form of the work for making mod
A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code runtime used to run it. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those
subprograms and other parts of the work. subprograms and other parts of the work.

View File

@@ -8,7 +8,7 @@ TEST=simple
help: help:
echo "Available targets:" echo "Available targets:"
echo " build - Build the lambda executable" echo " build - Build the lambda executable"
echo " run - Build and run the lambda runtime (use TEST=<name> to specify sample)" echo " run - Build and run the lambda interpreter (use TEST=<name> to specify sample)"
echo " profile - Build and run with CPU profiling enabled" echo " profile - Build and run with CPU profiling enabled"
echo " explain - Build and run with explanation mode and profiling" echo " explain - Build and run with explanation mode and profiling"
echo " graph - Generate and open CPU profile visualization" echo " graph - Generate and open CPU profile visualization"

View File

@@ -1,6 +1,6 @@
# lambda # lambda
Making a lambda calculus runtime in Go. Making a lambda calculus interpreter in Go.
## Things to talk about ## Things to talk about

View File

@@ -7,7 +7,7 @@ import (
"git.maximhutz.com/max/lambda/internal/config" "git.maximhutz.com/max/lambda/internal/config"
"git.maximhutz.com/max/lambda/internal/plugins" "git.maximhutz.com/max/lambda/internal/plugins"
"git.maximhutz.com/max/lambda/pkg/convert" "git.maximhutz.com/max/lambda/pkg/convert"
"git.maximhutz.com/max/lambda/pkg/normalorder" "git.maximhutz.com/max/lambda/pkg/lambda"
"git.maximhutz.com/max/lambda/pkg/saccharine" "git.maximhutz.com/max/lambda/pkg/saccharine"
) )
@@ -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.
runtime := normalorder.NewRuntime(compiled) reducer := lambda.NewNormalOrderReducer(&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, runtime) plugins.NewPerformance(options.Profile, reducer)
} }
// 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(runtime) plugins.NewExplanation(reducer)
} }
// 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(runtime) plugins.NewStatistics(reducer)
} }
// 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, runtime) plugins.NewLogs(logger, reducer)
} }
// Run reduction. // Run reduction.
runtime.Run() reducer.Reduce()
// Return the final reduced result. // Return the final reduced result.
result := runtime.Expression().String() result := reducer.Expression().String()
err = options.Destination.Write(result) err = options.Destination.Write(result)
cli.HandleError(err) cli.HandleError(err)
} }

View File

@@ -7,12 +7,12 @@ import (
"testing" "testing"
"git.maximhutz.com/max/lambda/pkg/convert" "git.maximhutz.com/max/lambda/pkg/convert"
"git.maximhutz.com/max/lambda/pkg/normalorder" "git.maximhutz.com/max/lambda/pkg/lambda"
"git.maximhutz.com/max/lambda/pkg/saccharine" "git.maximhutz.com/max/lambda/pkg/saccharine"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
// Helper function to run a single sample through the lambda runtime. // Helper function to run a single sample through the lambda interpreter.
func runSample(samplePath string) (string, error) { func runSample(samplePath string) (string, error) {
// Read the sample file. // Read the sample file.
input, err := os.ReadFile(samplePath) input, err := os.ReadFile(samplePath)
@@ -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 := normalorder.NewRuntime(compiled) reducer := lambda.NewNormalOrderReducer(&compiled)
reducer.Run() reducer.Reduce()
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/runtime" "git.maximhutz.com/max/lambda/pkg/reducer"
) )
type Logs struct { type Logs struct {
logger *slog.Logger logger *slog.Logger
reducer runtime.Runtime reducer reducer.Reducer
} }
func NewLogs(logger *slog.Logger, r runtime.Runtime) *Logs { func NewLogs(logger *slog.Logger, r reducer.Reducer) *Logs {
plugin := &Logs{logger, r} plugin := &Logs{logger, r}
r.On(runtime.StepEvent, plugin.Step) r.On(reducer.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/runtime" "git.maximhutz.com/max/lambda/pkg/reducer"
) )
// Track the reductions made by a reduction process. // Track the reductions made by a reduction process.
type Explanation struct { type Explanation struct {
reducer runtime.Runtime reducer reducer.Reducer
} }
// Attaches a new explanation tracker to a reducer. // Attaches a new explanation tracker to a reducer.
func NewExplanation(r runtime.Runtime) *Explanation { func NewExplanation(r reducer.Reducer) *Explanation {
plugin := &Explanation{reducer: r} plugin := &Explanation{reducer: r}
r.On(runtime.StartEvent, plugin.Start) r.On(reducer.StartEvent, plugin.Start)
r.On(runtime.StepEvent, plugin.Step) r.On(reducer.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/runtime" "git.maximhutz.com/max/lambda/pkg/reducer"
) )
// 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 runtime.Runtime) *Performance { func NewPerformance(file string, process reducer.Reducer) *Performance {
plugin := &Performance{File: file} plugin := &Performance{File: file}
process.On(runtime.StartEvent, plugin.Start) process.On(reducer.StartEvent, plugin.Start)
process.On(runtime.StopEvent, plugin.Stop) process.On(reducer.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/runtime" "git.maximhutz.com/max/lambda/pkg/reducer"
) )
// 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 runtime.Runtime) *Statistics { func NewStatistics(r reducer.Reducer) *Statistics {
plugin := &Statistics{} plugin := &Statistics{}
r.On(runtime.StartEvent, plugin.Start) r.On(reducer.StartEvent, plugin.Start)
r.On(runtime.StepEvent, plugin.Step) r.On(reducer.StepEvent, plugin.Step)
r.On(runtime.StopEvent, plugin.Stop) r.On(reducer.StopEvent, plugin.Stop)
return plugin return plugin
} }

View File

@@ -19,7 +19,7 @@ func convertAbstraction(n *saccharine.Abstraction) lambda.Expression {
// If the function has no parameters, it is a thunk. Lambda calculus still // If the function has no parameters, it is a thunk. Lambda calculus still
// requires _some_ parameter exists, so generate one. // requires _some_ parameter exists, so generate one.
if len(parameters) == 0 { if len(parameters) == 0 {
freeVars := result.GetFree() freeVars := lambda.GetFreeVariables(result)
freshName := lambda.GenerateFreshName(freeVars) freshName := lambda.GenerateFreshName(freeVars)
parameters = append(parameters, freshName) parameters = append(parameters, freshName)
} }
@@ -63,7 +63,7 @@ func reduceLet(s *saccharine.LetStatement, e lambda.Expression) lambda.Expressio
} }
func reduceDeclare(s *saccharine.DeclareStatement, e lambda.Expression) lambda.Expression { func reduceDeclare(s *saccharine.DeclareStatement, e lambda.Expression) lambda.Expression {
freshVar := lambda.GenerateFreshName(e.GetFree()) freshVar := lambda.GenerateFreshName(lambda.GetFreeVariables(e))
return lambda.NewApplication( return lambda.NewApplication(
lambda.NewAbstraction(freshVar, e), lambda.NewAbstraction(freshVar, e),

View File

@@ -9,7 +9,7 @@ type Emitter[E comparable] interface {
} }
type BaseEmitter[E comparable] struct { type BaseEmitter[E comparable] struct {
listeners map[E]set.Set[Listener[E]] listeners map[E]*set.Set[Listener[E]]
} }
func (e *BaseEmitter[E]) On(kind E, fn func()) Listener[E] { func (e *BaseEmitter[E]) On(kind E, fn func()) Listener[E] {
@@ -41,6 +41,6 @@ func (e *BaseEmitter[E]) Emit(event E) {
func New[E comparable]() *BaseEmitter[E] { func New[E comparable]() *BaseEmitter[E] {
return &BaseEmitter[E]{ return &BaseEmitter[E]{
listeners: map[E]set.Set[Listener[E]]{}, listeners: map[E]*set.Set[Listener[E]]{},
} }
} }

View File

@@ -1,5 +1,5 @@
// Package expr provides the abstract Expression interface for all evaluatable // Package expr provides the abstract Expression interface for all evaluatable
// expression types in the lambda runtime. // expression types in the lambda interpreter.
package expr package expr
import ( import (

View File

@@ -2,30 +2,12 @@ package lambda
import ( import (
"git.maximhutz.com/max/lambda/pkg/expr" "git.maximhutz.com/max/lambda/pkg/expr"
"git.maximhutz.com/max/lambda/pkg/set"
) )
// Expression is the interface for all lambda calculus expression types. // Expression is the interface for all lambda calculus expression types.
// It embeds the general expr.Expression interface for cross-mode compatibility. // It embeds the general expr.Expression interface for cross-mode compatibility.
type Expression interface { type Expression interface {
expr.Expression expr.Expression
// Substitute replaces all free occurrences of the target variable with the
// replacement expression. Alpha-renaming is performed automatically to
// avoid variable capture.
Substitute(target string, replacement Expression) Expression
// GetFree 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.
GetFree() set.Set[string]
// Rename replaces all occurrences of the target variable name with the new name.
Rename(target string, newName string) Expression
// IsFree returns true if the variable name n occurs free in the expression.
// This function does not mutate the input expression.
IsFree(n string) bool
} }
/** ------------------------------------------------------------------------- */ /** ------------------------------------------------------------------------- */
@@ -35,22 +17,22 @@ type Abstraction struct {
body Expression body Expression
} }
var _ Expression = Abstraction{} var _ Expression = (*Abstraction)(nil)
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}
} }
/** ------------------------------------------------------------------------- */ /** ------------------------------------------------------------------------- */
@@ -60,40 +42,40 @@ type Application struct {
argument Expression argument Expression
} }
var _ Expression = Application{} var _ Expression = (*Application)(nil)
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 {
name string value string
} }
var _ Expression = Variable{} var _ Expression = (*Variable)(nil)
func (v Variable) Name() string { func (v *Variable) Value() string {
return v.name return v.value
} }
func (v Variable) String() string { func (v *Variable) String() string {
return v.name return v.value
} }
func NewVariable(name string) Variable { func NewVariable(name string) *Variable {
return Variable{name} return &Variable{name}
} }

View File

@@ -6,9 +6,7 @@ import (
"git.maximhutz.com/max/lambda/pkg/set" "git.maximhutz.com/max/lambda/pkg/set"
) )
// GenerateFreshName generates a variable name that is not in the used set. func GenerateFreshName(used *set.Set[string]) string {
// This function does not mutate the used set.
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, i, 10))

View File

@@ -2,18 +2,19 @@ package lambda
import "git.maximhutz.com/max/lambda/pkg/set" import "git.maximhutz.com/max/lambda/pkg/set"
func (e Variable) GetFree() set.Set[string] { func GetFreeVariables(e Expression) *set.Set[string] {
return set.New(e.Name()) switch e := e.(type) {
} case *Variable:
return set.New(e.value)
func (e Abstraction) GetFree() set.Set[string] { case *Abstraction:
vars := e.Body().GetFree() vars := GetFreeVariables(e.body)
vars.Remove(e.Parameter()) vars.Remove(e.parameter)
return vars return vars
} case *Application:
vars := GetFreeVariables(e.abstraction)
func (e Application) GetFree() set.Set[string] { vars.Merge(GetFreeVariables(e.argument))
vars := e.Abstraction().GetFree() return vars
vars.Merge(e.Argument().GetFree()) default:
return vars return nil
}
} }

View File

@@ -1,12 +1,14 @@
package lambda package lambda
func (e Variable) IsFree(n string) bool { func IsFreeVariable(n string, e Expression) bool {
return e.Name() == n switch e := e.(type) {
} case *Variable:
return e.value == n
func (e Abstraction) IsFree(n string) bool { case *Abstraction:
return e.Parameter() != n && e.Body().IsFree(n) return e.parameter != n && IsFreeVariable(n, e.body)
} case *Application:
func (e Application) IsFree(n string) bool { return IsFreeVariable(n, e.abstraction) || IsFreeVariable(n, e.argument)
return e.Abstraction().IsFree(n) || e.Argument().IsFree(n) default:
return false
}
} }

68
pkg/lambda/iterator.go Normal file
View File

@@ -0,0 +1,68 @@
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{}
}
}

61
pkg/lambda/reducer.go Normal file
View File

@@ -0,0 +1,61 @@
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,28 +1,38 @@
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 (e Variable) Rename(target string, newName string) Expression { switch e := expr.(type) {
if e.Name() == target { case *Variable:
return NewVariable(newName) if e.value == target {
return NewVariable(newName)
}
return e
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
}
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
}
return NewApplication(newAbs, newArg)
default:
return expr
} }
return e
}
func (e Abstraction) Rename(target string, newName string) Expression {
newParam := e.Parameter()
if e.Parameter() == target {
newParam = newName
}
newBody := e.Body().Rename(target, newName)
return NewAbstraction(newParam, newBody)
}
func (e Application) Rename(target string, newName string) Expression {
newAbs := e.Abstraction().Rename(target, newName)
newArg := e.Argument().Rename(target, newName)
return NewApplication(newAbs, newArg)
} }

View File

@@ -1,35 +1,46 @@
package lambda package lambda
func (e Variable) Substitute(target string, replacement Expression) Expression { func Substitute(expr Expression, target string, replacement Expression) Expression {
if e.Name() == target { switch e := expr.(type) {
return replacement case *Variable:
} if e.value == target {
return replacement
return e }
}
func (e Abstraction) Substitute(target string, replacement Expression) Expression {
if e.Parameter() == target {
return e return e
case *Abstraction:
if e.parameter == target {
return e
}
body := e.body
param := e.parameter
if IsFreeVariable(param, replacement) {
freeVars := GetFreeVariables(replacement)
freeVars.Merge(GetFreeVariables(body))
freshVar := GenerateFreshName(freeVars)
body = Rename(body, param, freshVar)
param = freshVar
}
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
}
return NewApplication(newAbs, newArg)
default:
return expr
} }
body := e.Body()
param := e.Parameter()
if replacement.IsFree(param) {
freeVars := replacement.GetFree()
freeVars.Merge(body.GetFree())
freshVar := GenerateFreshName(freeVars)
body = body.Rename(param, freshVar)
param = freshVar
}
newBody := body.Substitute(target, replacement)
return NewAbstraction(param, newBody)
}
func (e Application) Substitute(target string, replacement Expression) Expression {
abs := e.Abstraction().Substitute(target, replacement)
arg := e.Argument().Substitute(target, replacement)
return NewApplication(abs, arg)
} }

View File

@@ -1,34 +0,0 @@
package normalorder
import "git.maximhutz.com/max/lambda/pkg/lambda"
func ReduceOnce(e lambda.Expression) (lambda.Expression, bool) {
switch e := e.(type) {
case lambda.Abstraction:
body, reduced := ReduceOnce(e.Body())
if reduced {
return lambda.NewAbstraction(e.Parameter(), body), true
}
return e, false
case lambda.Application:
if fn, fnOk := e.Abstraction().(lambda.Abstraction); fnOk {
return fn.Body().Substitute(fn.Parameter(), e.Argument()), true
}
abs, reduced := ReduceOnce(e.Abstraction())
if reduced {
return lambda.NewApplication(abs, e.Argument()), true
}
arg, reduced := ReduceOnce(e.Argument())
if reduced {
return lambda.NewApplication(e.Abstraction(), arg), true
}
return e, false
default:
return e, false
}
}

View File

@@ -1,46 +0,0 @@
package normalorder
import (
"git.maximhutz.com/max/lambda/pkg/emitter"
"git.maximhutz.com/max/lambda/pkg/expr"
"git.maximhutz.com/max/lambda/pkg/lambda"
"git.maximhutz.com/max/lambda/pkg/runtime"
)
// NormalOrderReducer implements normal order (leftmost-outermost) reduction
// for lambda calculus expressions.
type Runtime struct {
emitter.BaseEmitter[runtime.Event]
expression lambda.Expression
}
// NewNormalOrderReducer creates a new normal order reducer.
func NewRuntime(expression lambda.Expression) *Runtime {
return &Runtime{
BaseEmitter: *emitter.New[runtime.Event](),
expression: expression,
}
}
// Expression returns the current expression state.
func (r *Runtime) Expression() expr.Expression {
return r.expression
}
func (r *Runtime) 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 *Runtime) Run() {
r.Emit(runtime.StartEvent)
for !r.Step() {
r.Emit(runtime.StepEvent)
}
r.Emit(runtime.StopEvent)
}

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

@@ -0,0 +1,13 @@
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
)

27
pkg/reducer/reducer.go Normal file
View File

@@ -0,0 +1,27 @@
// Package reducer provides the abstract Reducer interface for all expression
// reduction strategies.
package reducer
import (
"git.maximhutz.com/max/lambda/pkg/emitter"
"git.maximhutz.com/max/lambda/pkg/expr"
)
// Reducer defines the interface for expression reduction strategies.
// Different evaluation modes (normal order, applicative order, SKI combinators,
// etc.) implement this interface with their own reduction logic.
//
// Reducers also implement the Emitter interface to allow plugins to observe
// reduction lifecycle events (Start, Step, Stop).
type Reducer 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()
// Expression returns the current expression state.
Expression() expr.Expression
}

View File

@@ -1,13 +0,0 @@
package runtime
// 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,27 +0,0 @@
// Package runtime provides the abstract Reducer interface for all expression
// reduction strategies.
package runtime
import (
"git.maximhutz.com/max/lambda/pkg/emitter"
"git.maximhutz.com/max/lambda/pkg/expr"
)
// Runtime defines the interface for expression reduction strategies.
// Different evaluation modes (normal order, applicative order, SKI combinators,
// etc.) implement this interface with their own reduction logic.
//
// Runtimes also implement the Emitter interface to allow plugins to observe
// reduction lifecycle events (Start, Step, Stop).
type Runtime interface {
emitter.Emitter[Event]
// Run a single step. Returns whether the runtime is complete or not.
Step() bool
// Run until completion.
Run()
// Copy the state of the runtime.
Expression() expr.Expression
}

View File

@@ -4,9 +4,9 @@ import "iter"
type Set[T comparable] map[T]bool type Set[T comparable] map[T]bool
func (s Set[T]) Add(items ...T) { func (s *Set[T]) Add(items ...T) {
for _, item := range items { for _, item := range items {
s[item] = true (*s)[item] = true
} }
} }
@@ -14,14 +14,14 @@ func (s Set[T]) Has(item T) bool {
return s[item] return s[item]
} }
func (s Set[T]) Remove(items ...T) { func (s *Set[T]) Remove(items ...T) {
for _, item := range items { for _, item := range items {
delete(s, item) delete(*s, item)
} }
} }
func (s Set[T]) Merge(o Set[T]) { func (s *Set[T]) Merge(o *Set[T]) {
for item := range o { for item := range *o {
s.Add(item) s.Add(item)
} }
} }
@@ -46,8 +46,8 @@ func (s Set[T]) Items() iter.Seq[T] {
} }
} }
func New[T comparable](items ...T) Set[T] { func New[T comparable](items ...T) *Set[T] {
result := Set[T]{} result := &Set[T]{}
for _, item := range items { for _, item := range items {
result.Add(item) result.Add(item)

File diff suppressed because one or more lines are too long

8
tests/church_6^6.test Normal file
View File

@@ -0,0 +1,8 @@
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))