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

View File

@@ -6,8 +6,6 @@ import (
"strings"
"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/lambda"
"git.maximhutz.com/max/lambda/pkg/saccharine"
@@ -31,22 +29,11 @@ func runSample(samplePath string) (string, error) {
// Compile expression to lambda calculus.
compiled := convert.SaccharineToLambda(ast)
// Create minimal config for benchmarking.
cfg := &config.Config{
Source: config.StringSource{Data: ""},
Destination: config.StdoutDestination{},
Profile: "",
Explanation: false,
Statistics: false,
Verbose: false,
}
// Create and run the reducer.
reducer := lambda.NewNormalOrderReducer(&compiled)
reducer.Reduce()
// Create and run the engine with normal order reducer.
reducer := lambda.NewNormalOrderReducer()
process := engine.New(cfg, compiled, reducer)
process.Run()
return process.Expression.String() + "\n", nil
return reducer.Expression().String() + "\n", nil
}
// 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 (
"log/slog"
"git.maximhutz.com/max/lambda/internal/engine"
"git.maximhutz.com/max/lambda/pkg/reducer"
)
type Logs struct {
logger *slog.Logger
process *engine.Engine
reducer reducer.Reducer
}
func NewLogs(logger *slog.Logger, process *engine.Engine) *Logs {
plugin := &Logs{logger, process}
process.On(engine.StepEvent, plugin.Step)
func NewLogs(logger *slog.Logger, r reducer.Reducer) *Logs {
plugin := &Logs{logger, r}
r.On(reducer.StepEvent, plugin.Step)
return plugin
}
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 (
"fmt"
"git.maximhutz.com/max/lambda/internal/engine"
"git.maximhutz.com/max/lambda/pkg/reducer"
)
// Track the reductions made by a reduction process.
type Explanation struct {
process *engine.Engine
reducer reducer.Reducer
}
// Attaches a new explanation tracker to a process.
func NewExplanation(process *engine.Engine) *Explanation {
plugin := &Explanation{process: process}
process.On(engine.StartEvent, plugin.Start)
process.On(engine.StepEvent, plugin.Step)
// Attaches a new explanation tracker to a reducer.
func NewExplanation(r reducer.Reducer) *Explanation {
plugin := &Explanation{reducer: r}
r.On(reducer.StartEvent, plugin.Start)
r.On(reducer.StepEvent, plugin.Step)
return plugin
}
func (t *Explanation) Start() {
fmt.Println(t.process.Expression.String())
fmt.Println(t.reducer.Expression().String())
}
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"
"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
@@ -19,10 +19,10 @@ type Performance struct {
}
// 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}
process.On(engine.StartEvent, plugin.Start)
process.On(engine.StopEvent, plugin.Stop)
process.On(reducer.StartEvent, plugin.Start)
process.On(reducer.StopEvent, plugin.Stop)
return plugin
}

View File

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

View File

@@ -3,7 +3,7 @@ package emitter
import "git.maximhutz.com/max/lambda/pkg/set"
type Emitter[E comparable] interface {
On(string, func()) Listener[E]
On(E, func()) Listener[E]
Off(Listener[E])
Emit(E)
}
@@ -22,6 +22,13 @@ func (e *BaseEmitter[E]) On(kind E, fn func()) Listener[E] {
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) {
if e.listeners[event] == nil {
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
import (
"git.maximhutz.com/max/lambda/pkg/emitter"
"git.maximhutz.com/max/lambda/pkg/expr"
"git.maximhutz.com/max/lambda/pkg/reducer"
)
// Ensure NormalOrderReducer implements reducer.Reducer.
var _ reducer.Reducer = (*NormalOrderReducer)(nil)
// NormalOrderReducer implements normal order (leftmost-outermost) reduction
// for lambda calculus expressions.
type NormalOrderReducer struct{}
type NormalOrderReducer struct {
emitter.BaseEmitter[reducer.Event]
expression *Expression
}
// NewNormalOrderReducer creates a new normal order reducer.
func NewNormalOrderReducer() *NormalOrderReducer {
return &NormalOrderReducer{}
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(e expr.Expression, onStep func(expr.Expression)) expr.Expression {
lambdaExpr, ok := e.(Expression)
if !ok {
return e
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()
}
}
}
ReduceAll(&lambdaExpr, func() {
onStep(lambdaExpr)
})
return lambdaExpr
r.Emit(reducer.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
)

View File

@@ -2,14 +2,26 @@
// reduction strategies.
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.
// 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 {
// Reduce performs all reduction steps on the expression, calling onStep
// after each reduction.
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(e expr.Expression, onStep func(expr.Expression)) expr.Expression
Reduce()
// Expression returns the current expression state.
Expression() expr.Expression
}