refactor: move event system to reducer, remove engine package #32

Merged
mvhutz merged 7 commits from refactor/move-events-to-reducer into main 2026-01-17 00:27:37 +00:00
14 changed files with 143 additions and 154 deletions

View File

@@ -80,3 +80,26 @@ Use the `tea` CLI (Gitea command-line tool) for PR operations instead of `gh`.
**Linking issues**: When a PR solves an issue, reference the issue in both the commit message and PR description using `Closes #<number>`. **Linking issues**: When a PR solves an issue, reference the issue in both the commit message and PR description using `Closes #<number>`.
This automatically links and closes the issue when the PR is merged. This automatically links and closes the issue when the PR is merged.
### Updating PRs
When pushing additional changes to an existing PR, add a comment summarizing the new commits.
This keeps reviewers informed of what changed since the initial PR description.
Use the `tea` CLI to add comments to pull requests:
```bash
tea comment <number> "Comment text"
```
#### Examples
```bash
# Add a comment to PR #42
tea comment 42 "Updated implementation based on feedback"
# Add a multi-line comment
tea comment 42 "Summary of changes:
- Fixed bug in reducer
- Added new tests"
```

View File

@@ -5,7 +5,6 @@ import (
"git.maximhutz.com/max/lambda/internal/cli" "git.maximhutz.com/max/lambda/internal/cli"
"git.maximhutz.com/max/lambda/internal/config" "git.maximhutz.com/max/lambda/internal/config"
"git.maximhutz.com/max/lambda/internal/engine"
"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/lambda" "git.maximhutz.com/max/lambda/pkg/lambda"
@@ -34,36 +33,35 @@ func main() {
compiled := convert.SaccharineToLambda(ast) compiled := convert.SaccharineToLambda(ast)
logger.Info("compiled λ expression", "tree", compiled.String()) logger.Info("compiled λ expression", "tree", compiled.String())
// Create reduction engine with normal order reducer. // Create reducer with the compiled expression.
reducer := lambda.NewNormalOrderReducer() reducer := lambda.NewNormalOrderReducer(&compiled)
process := engine.New(options, compiled, reducer)
// If the user selected to track CPU performance, attach a profiler to the // If the user selected to track CPU performance, attach a profiler.
// process.
if options.Profile != "" { if options.Profile != "" {
plugins.NewPerformance(options.Profile, process) 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 here. // observer.
if options.Explanation { if options.Explanation {
plugins.NewExplanation(process) plugins.NewExplanation(reducer)
} }
// If the user opted to track statistics, attach a tracker here, too. // If the user opted to track statistics, attach a tracker.
if options.Statistics { if options.Statistics {
plugins.NewStatistics(process) 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, process) plugins.NewLogs(logger, reducer)
} }
process.Run() // Run reduction.
reducer.Reduce()
// Return the final reduced result. // Return the final reduced result.
result := process.Expression.String() result := reducer.Expression().String()
err = options.Destination.Write(result) err = options.Destination.Write(result)
cli.HandleError(err) cli.HandleError(err)
} }

View File

@@ -6,8 +6,6 @@ import (
"strings" "strings"
"testing" "testing"
"git.maximhutz.com/max/lambda/internal/config"
"git.maximhutz.com/max/lambda/internal/engine"
"git.maximhutz.com/max/lambda/pkg/convert" "git.maximhutz.com/max/lambda/pkg/convert"
"git.maximhutz.com/max/lambda/pkg/lambda" "git.maximhutz.com/max/lambda/pkg/lambda"
"git.maximhutz.com/max/lambda/pkg/saccharine" "git.maximhutz.com/max/lambda/pkg/saccharine"
@@ -31,22 +29,11 @@ func runSample(samplePath string) (string, error) {
// Compile expression to lambda calculus. // Compile expression to lambda calculus.
compiled := convert.SaccharineToLambda(ast) compiled := convert.SaccharineToLambda(ast)
// Create minimal config for benchmarking. // Create and run the reducer.
cfg := &config.Config{ reducer := lambda.NewNormalOrderReducer(&compiled)
Source: config.StringSource{Data: ""}, reducer.Reduce()
Destination: config.StdoutDestination{},
Profile: "",
Explanation: false,
Statistics: false,
Verbose: false,
}
// Create and run the engine with normal order reducer. return reducer.Expression().String() + "\n", nil
reducer := lambda.NewNormalOrderReducer()
process := engine.New(cfg, compiled, reducer)
process.Run()
return process.Expression.String() + "\n", nil
} }
// Test that all samples produce expected output. // Test that all samples produce expected output.

View File

@@ -1,40 +0,0 @@
// Package "engine" provides an extensible interface for users to interact with
// λ-calculus and other expression evaluation modes.
package engine
import (
"git.maximhutz.com/max/lambda/internal/config"
"git.maximhutz.com/max/lambda/pkg/emitter"
"git.maximhutz.com/max/lambda/pkg/expr"
"git.maximhutz.com/max/lambda/pkg/reducer"
)
// Engine is a process for reducing one expression.
type Engine struct {
Config *config.Config
Expression expr.Expression
Reducer reducer.Reducer
emitter.BaseEmitter[Event]
}
// New creates a new engine with the given expression and reducer.
func New(config *config.Config, expression expr.Expression, reducer reducer.Reducer) *Engine {
return &Engine{
Config: config,
Expression: expression,
Reducer: reducer,
BaseEmitter: *emitter.New[Event](),
}
}
// Run begins the reduction process.
func (e *Engine) Run() {
e.Emit(StartEvent)
e.Expression = e.Reducer.Reduce(e.Expression, func(reduced expr.Expression) {
e.Expression = reduced
e.Emit(StepEvent)
})
e.Emit(StopEvent)
}

View File

@@ -1,9 +0,0 @@
package engine
type Event int
const (
StartEvent Event = iota
StepEvent
StopEvent
)

View File

@@ -3,21 +3,21 @@ package plugins
import ( import (
"log/slog" "log/slog"
"git.maximhutz.com/max/lambda/internal/engine" "git.maximhutz.com/max/lambda/pkg/reducer"
) )
type Logs struct { type Logs struct {
logger *slog.Logger logger *slog.Logger
process *engine.Engine reducer reducer.Reducer
} }
func NewLogs(logger *slog.Logger, process *engine.Engine) *Logs { func NewLogs(logger *slog.Logger, r reducer.Reducer) *Logs {
plugin := &Logs{logger, process} plugin := &Logs{logger, r}
process.On(engine.StepEvent, plugin.Step) r.On(reducer.StepEvent, plugin.Step)
return plugin return plugin
} }
func (t *Logs) Step() { func (t *Logs) Step() {
t.logger.Info("reduction", "tree", t.process.Expression.String()) t.logger.Info("reduction", "tree", t.reducer.Expression().String())
} }

View File

@@ -5,27 +5,27 @@ package plugins
import ( import (
"fmt" "fmt"
"git.maximhutz.com/max/lambda/internal/engine" "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 {
process *engine.Engine reducer reducer.Reducer
} }
// Attaches a new explanation tracker to a process. // Attaches a new explanation tracker to a reducer.
func NewExplanation(process *engine.Engine) *Explanation { func NewExplanation(r reducer.Reducer) *Explanation {
plugin := &Explanation{process: process} plugin := &Explanation{reducer: r}
process.On(engine.StartEvent, plugin.Start) r.On(reducer.StartEvent, plugin.Start)
process.On(engine.StepEvent, plugin.Step) r.On(reducer.StepEvent, plugin.Step)
return plugin return plugin
} }
func (t *Explanation) Start() { func (t *Explanation) Start() {
fmt.Println(t.process.Expression.String()) fmt.Println(t.reducer.Expression().String())
} }
func (t *Explanation) Step() { func (t *Explanation) Step() {
fmt.Println(" =", t.process.Expression.String()) fmt.Println(" =", t.reducer.Expression().String())
} }

View File

@@ -7,7 +7,7 @@ import (
"path/filepath" "path/filepath"
"runtime/pprof" "runtime/pprof"
"git.maximhutz.com/max/lambda/internal/engine" "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 *engine.Engine) *Performance { func NewPerformance(file string, process reducer.Reducer) *Performance {
plugin := &Performance{File: file} plugin := &Performance{File: file}
process.On(engine.StartEvent, plugin.Start) process.On(reducer.StartEvent, plugin.Start)
process.On(engine.StopEvent, plugin.Stop) process.On(reducer.StopEvent, plugin.Stop)
return plugin return plugin
} }

View File

@@ -5,8 +5,8 @@ import (
"os" "os"
"time" "time"
"git.maximhutz.com/max/lambda/internal/engine"
"git.maximhutz.com/max/lambda/internal/statistics" "git.maximhutz.com/max/lambda/internal/statistics"
"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(process *engine.Engine) *Statistics { func NewStatistics(r reducer.Reducer) *Statistics {
plugin := &Statistics{} plugin := &Statistics{}
process.On(engine.StartEvent, plugin.Start) r.On(reducer.StartEvent, plugin.Start)
process.On(engine.StepEvent, plugin.Step) r.On(reducer.StepEvent, plugin.Step)
process.On(engine.StopEvent, plugin.Stop) r.On(reducer.StopEvent, plugin.Stop)
return plugin return plugin
} }

View File

@@ -3,7 +3,7 @@ package emitter
import "git.maximhutz.com/max/lambda/pkg/set" import "git.maximhutz.com/max/lambda/pkg/set"
type Emitter[E comparable] interface { type Emitter[E comparable] interface {
On(string, func()) Listener[E] On(E, func()) Listener[E]
Off(Listener[E]) Off(Listener[E])
Emit(E) Emit(E)
} }
@@ -22,6 +22,13 @@ func (e *BaseEmitter[E]) On(kind E, fn func()) Listener[E] {
return listener return listener
} }
func (e *BaseEmitter[E]) Off(listener Listener[E]) {
kind := listener.Kind()
if e.listeners[kind] != nil {
e.listeners[kind].Remove(listener)
}
}
func (e *BaseEmitter[E]) Emit(event E) { func (e *BaseEmitter[E]) Emit(event E) {
if e.listeners[event] == nil { if e.listeners[event] == nil {
e.listeners[event] = set.New[Listener[E]]() e.listeners[event] = set.New[Listener[E]]()

View File

@@ -1,30 +0,0 @@
package lambda
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
}
}
func ReduceAll(e *Expression, step func()) {
it := NewIterator(e)
for !it.Done() {
if fn, arg, ok := IsViable(it.Current()); !ok {
it.Next()
} else {
it.Swap(Substitute(fn.body, fn.parameter, arg))
step()
if _, _, ok := IsViable(it.Parent()); ok {
it.Back()
}
}
}
}

View File

@@ -1,33 +1,61 @@
package lambda package lambda
import ( import (
"git.maximhutz.com/max/lambda/pkg/emitter"
"git.maximhutz.com/max/lambda/pkg/expr" "git.maximhutz.com/max/lambda/pkg/expr"
"git.maximhutz.com/max/lambda/pkg/reducer" "git.maximhutz.com/max/lambda/pkg/reducer"
) )
// Ensure NormalOrderReducer implements reducer.Reducer.
var _ reducer.Reducer = (*NormalOrderReducer)(nil)
// NormalOrderReducer implements normal order (leftmost-outermost) reduction // NormalOrderReducer implements normal order (leftmost-outermost) reduction
// for lambda calculus expressions. // for lambda calculus expressions.
type NormalOrderReducer struct{} type NormalOrderReducer struct {
emitter.BaseEmitter[reducer.Event]
expression *Expression
}
// NewNormalOrderReducer creates a new normal order reducer. // NewNormalOrderReducer creates a new normal order reducer.
func NewNormalOrderReducer() *NormalOrderReducer { func NewNormalOrderReducer(expression *Expression) *NormalOrderReducer {
return &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. // Reduce performs normal order reduction on a lambda expression.
// The expression must be a lambda.Expression; other types are returned unchanged. // The expression must be a lambda.Expression; other types are returned unchanged.
func (r *NormalOrderReducer) Reduce(e expr.Expression, onStep func(expr.Expression)) expr.Expression { func (r *NormalOrderReducer) Reduce() {
lambdaExpr, ok := e.(Expression) r.Emit(reducer.StartEvent)
if !ok { it := NewIterator(r.expression)
return e
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()
}
}
} }
ReduceAll(&lambdaExpr, func() { r.Emit(reducer.StopEvent)
onStep(lambdaExpr)
})
return lambdaExpr
} }

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
)

View File

@@ -2,14 +2,26 @@
// reduction strategies. // reduction strategies.
package reducer package reducer
import "git.maximhutz.com/max/lambda/pkg/expr" import (
"git.maximhutz.com/max/lambda/pkg/emitter"
"git.maximhutz.com/max/lambda/pkg/expr"
)
// Reducer defines the interface for expression reduction strategies. // Reducer defines the interface for expression reduction strategies.
// Different evaluation modes (normal order, applicative order, SKI combinators, // Different evaluation modes (normal order, applicative order, SKI combinators,
// etc.) implement this interface with their own reduction logic. // 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 { type Reducer interface {
// Reduce performs all reduction steps on the expression, calling onStep emitter.Emitter[Event]
// after each reduction.
// 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. // Returns the final reduced expression.
Reduce(e expr.Expression, onStep func(expr.Expression)) expr.Expression Reduce()
// Expression returns the current expression state.
Expression() expr.Expression
} }