diff --git a/cmd/lambda/lambda.go b/cmd/lambda/lambda.go index 2089cb5..eade9ff 100644 --- a/cmd/lambda/lambda.go +++ b/cmd/lambda/lambda.go @@ -6,9 +6,7 @@ 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/explanation" - "git.maximhutz.com/max/lambda/internal/performance" - "git.maximhutz.com/max/lambda/internal/statistics" + "git.maximhutz.com/max/lambda/internal/plugins" "git.maximhutz.com/max/lambda/pkg/convert" "git.maximhutz.com/max/lambda/pkg/lambda" "git.maximhutz.com/max/lambda/pkg/saccharine" @@ -42,30 +40,23 @@ func main() { // If the user selected to track CPU performance, attach a profiler to the // process. if options.Profile != "" { - profiler := performance.Track(options.Profile) - process.On("start", profiler.Start) - process.On("end", profiler.End) + plugins.NewPerformance(options.Profile, process) } // If the user selected to produce a step-by-step explanation, attach an // observer here. if options.Explanation { - explanation.Track(process) + plugins.NewExplanation(process) } // If the user opted to track statistics, attach a tracker here, too. if options.Statistics { - statistics := statistics.Track() - process.On("start", statistics.Start) - process.On("step", statistics.Step) - process.On("end", statistics.End) + plugins.NewStatistics(process) } // If the user selected for verbose debug logs, attach a reduction tracker. if options.Verbose { - process.On("step", func() { - logger.Info("reduction", "tree", lambda.Stringify(compiled)) - }) + plugins.NewLogs(logger, process) } process.Run() diff --git a/internal/engine/engine.go b/internal/engine/engine.go index 0add767..49102ae 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -12,7 +12,7 @@ import ( type Engine struct { Config *config.Config Expression *lambda.Expression - emitter.Emitter + emitter.BaseEmitter[Event] } // Create a new engine, given an unreduced λ-expression. @@ -22,11 +22,11 @@ func New(config *config.Config, expression *lambda.Expression) *Engine { // Begin the reduction process. func (e Engine) Run() { - e.Emit("start") + e.Emit(StartEvent) lambda.ReduceAll(e.Expression, func() { - e.Emit("step") + e.Emit(StepEvent) }) - e.Emit("end") + e.Emit(StopEvent) } diff --git a/internal/engine/events.go b/internal/engine/events.go new file mode 100644 index 0000000..995ebea --- /dev/null +++ b/internal/engine/events.go @@ -0,0 +1,9 @@ +package engine + +type Event int + +const ( + StartEvent Event = iota + StepEvent + StopEvent +) diff --git a/internal/plugins/debug.go b/internal/plugins/debug.go new file mode 100644 index 0000000..77bd4c6 --- /dev/null +++ b/internal/plugins/debug.go @@ -0,0 +1,25 @@ +package plugins + +import ( + "log/slog" + + "git.maximhutz.com/max/lambda/internal/engine" + "git.maximhutz.com/max/lambda/pkg/lambda" +) + +type Logs struct { + logger *slog.Logger + process *engine.Engine +} + +func NewLogs(logger *slog.Logger, process *engine.Engine) *Logs { + plugin := &Logs{logger, process} + process.On(engine.StopEvent, plugin.Step) + + return plugin +} + +func (t *Logs) Step() { + stringified := lambda.Stringify(*t.process.Expression) + t.logger.Info("reduction", "tree", stringified) +} diff --git a/internal/explanation/tracker.go b/internal/plugins/explanation.go similarity index 62% rename from internal/explanation/tracker.go rename to internal/plugins/explanation.go index 4650bab..141e8fc 100644 --- a/internal/explanation/tracker.go +++ b/internal/plugins/explanation.go @@ -1,6 +1,6 @@ // Package "explanation" provides a observer to gather the reasoning during the // reduction, and present a thorough explanation to the user for each step. -package explanation +package plugins import ( "fmt" @@ -10,23 +10,23 @@ import ( ) // Track the reductions made by a reduction proess. -type Tracker struct { +type Explanation struct { process *engine.Engine } // Attaches a new explanation tracker to a process. -func Track(process *engine.Engine) *Tracker { - tracker := &Tracker{process: process} - process.On("start", tracker.Start) - process.On("step", tracker.Step) +func NewExplanation(process *engine.Engine) *Explanation { + plugin := &Explanation{process: process} + process.On(engine.StartEvent, plugin.Start) + process.On(engine.StepEvent, plugin.Step) - return tracker + return plugin } -func (t *Tracker) Start() { +func (t *Explanation) Start() { fmt.Println(lambda.Stringify(*t.process.Expression)) } -func (t *Tracker) Step() { +func (t *Explanation) Step() { fmt.Println(" =", lambda.Stringify(*t.process.Expression)) } diff --git a/internal/performance/tracker.go b/internal/plugins/performance.go similarity index 68% rename from internal/performance/tracker.go rename to internal/plugins/performance.go index a4d21ee..0dc69d4 100644 --- a/internal/performance/tracker.go +++ b/internal/plugins/performance.go @@ -1,28 +1,34 @@ // Package "performance" provides a tracker to observer CPU performance during // execution. -package performance +package plugins import ( "os" "path/filepath" "runtime/pprof" + + "git.maximhutz.com/max/lambda/internal/engine" ) // Observes a reduction process, and publishes a CPU performance profile on // completion. -type Tracker struct { +type Performance struct { File string filePointer *os.File Error error } // Create a performance tracker that outputs a profile to "file". -func Track(file string) *Tracker { - return &Tracker{File: file} +func NewPerformance(file string, process *engine.Engine) *Performance { + plugin := &Performance{File: file} + process.On(engine.StartEvent, plugin.Start) + process.On(engine.StopEvent, plugin.Stop) + + return plugin } // Begin profiling. -func (t *Tracker) Start() { +func (t *Performance) Start() { var absPath string absPath, t.Error = filepath.Abs(t.File) @@ -47,7 +53,7 @@ func (t *Tracker) Start() { } // Stop profiling. -func (t *Tracker) End() { +func (t *Performance) Stop() { pprof.StopCPUProfile() t.filePointer.Close() } diff --git a/internal/plugins/statistics.go b/internal/plugins/statistics.go new file mode 100644 index 0000000..00bf5c4 --- /dev/null +++ b/internal/plugins/statistics.go @@ -0,0 +1,44 @@ +package plugins + +import ( + "fmt" + "os" + "time" + + "git.maximhutz.com/max/lambda/internal/engine" + "git.maximhutz.com/max/lambda/internal/statistics" +) + +// An observer, to track reduction performance. +type Statistics struct { + start time.Time + steps uint64 +} + +// Create a new reduction performance Statistics. +func NewStatistics(process *engine.Engine) *Statistics { + plugin := &Statistics{} + process.On(engine.StartEvent, plugin.Start) + process.On(engine.StepEvent, plugin.Step) + process.On(engine.StopEvent, plugin.Step) + + return plugin +} + +func (t *Statistics) Start() { + t.start = time.Now() + t.steps = 0 +} + +func (t *Statistics) Step() { + t.steps++ +} + +func (t *Statistics) Stop() { + results := statistics.Results{ + StepsTaken: t.steps, + TimeElapsed: uint64(time.Since(t.start).Milliseconds()), + } + + fmt.Fprint(os.Stderr, results.String()) +} diff --git a/internal/statistics/tracker.go b/internal/statistics/tracker.go deleted file mode 100644 index 004b1f4..0000000 --- a/internal/statistics/tracker.go +++ /dev/null @@ -1,36 +0,0 @@ -package statistics - -import ( - "fmt" - "os" - "time" -) - -// An observer, to track reduction performance. -type Tracker struct { - start time.Time - steps uint64 -} - -// Create a new reduction performance tracker. -func Track() *Tracker { - return &Tracker{} -} - -func (t *Tracker) Start() { - t.start = time.Now() - t.steps = 0 -} - -func (t *Tracker) Step() { - t.steps++ -} - -func (t *Tracker) End() { - results := Results{ - StepsTaken: t.steps, - TimeElapsed: uint64(time.Since(t.start).Milliseconds()), - } - - fmt.Fprint(os.Stderr, results.String()) -} diff --git a/pkg/emitter/emitter.go b/pkg/emitter/emitter.go index 217da42..594a18a 100644 --- a/pkg/emitter/emitter.go +++ b/pkg/emitter/emitter.go @@ -2,53 +2,38 @@ package emitter import "git.maximhutz.com/max/lambda/pkg/set" -type Observer struct { - fn func() - message string - emitter *Emitter +type Emitter[E comparable] interface { + On(string, func()) Listener[E] + Off(Listener[E]) + Emit(E) } -type Emitter struct { - listeners map[string]*set.Set[*Observer] +type BaseEmitter[E comparable] struct { + listeners map[E]*set.Set[Listener[E]] } -func Ignore[T any](fn func()) func(T) { - return func(T) { fn() } +func (e *BaseEmitter[E]) On(kind E, fn func()) Listener[E] { + if e.listeners[kind] == nil { + e.listeners[kind] = set.New[Listener[E]]() + } + + listener := &BaseListener[E]{kind, fn} + e.listeners[kind].Add(listener) + return listener } -func (e *Emitter) On(message string, fn func()) *Observer { - observer := &Observer{ - fn: fn, - message: message, - emitter: e, +func (e *BaseEmitter[E]) Emit(event E) { + if e.listeners[event] == nil { + e.listeners[event] = set.New[Listener[E]]() } - if e.listeners == nil { - e.listeners = map[string]*set.Set[*Observer]{} - } - - if e.listeners[message] == nil { - e.listeners[message] = set.New[*Observer]() - } - - e.listeners[message].Add(observer) - return observer -} - -func (o *Observer) Off() { - if o.emitter.listeners[o.message] == nil { - return - } - - o.emitter.listeners[o.message].Remove(o) -} - -func (e *Emitter) Emit(message string) { - if e.listeners[message] == nil { - return - } - - for listener := range *e.listeners[message] { - listener.fn() + for listener := range e.listeners[event].Items() { + listener.Run() + } +} + +func New[E comparable]() *BaseEmitter[E] { + return &BaseEmitter[E]{ + listeners: map[E]*set.Set[Listener[E]]{}, } } diff --git a/pkg/emitter/listener.go b/pkg/emitter/listener.go new file mode 100644 index 0000000..43c95c6 --- /dev/null +++ b/pkg/emitter/listener.go @@ -0,0 +1,19 @@ +package emitter + +type Listener[E comparable] interface { + Kind() E + Run() +} + +type BaseListener[E comparable] struct { + kind E + fn func() +} + +func (l BaseListener[E]) Kind() E { + return l.kind +} + +func (l BaseListener[E]) Run() { + l.fn() +} diff --git a/pkg/set/set.go b/pkg/set/set.go index 27435d0..f5a70f0 100644 --- a/pkg/set/set.go +++ b/pkg/set/set.go @@ -1,5 +1,7 @@ package set +import "iter" + type Set[T comparable] map[T]bool func (s *Set[T]) Add(items ...T) { @@ -34,6 +36,16 @@ func (s Set[T]) ToList() []T { return list } +func (s Set[T]) Items() iter.Seq[T] { + return func(yield func(T) bool) { + for item := range s { + if !yield(item) { + return + } + } + } +} + func New[T comparable](items ...T) *Set[T] { result := &Set[T]{}