From 0ec52008bb477774a6ed19f13fcf152c38169cb9 Mon Sep 17 00:00:00 2001 From: "M.V. Hutz" Date: Fri, 30 Jan 2026 16:24:17 -0500 Subject: [PATCH 01/14] feat: repr, codec --- .golangci.yml | 5 ++++- Makefile | 6 +++++- pkg/codec/codec.go | 12 ++++++++++++ pkg/expr/expr.go | 15 --------------- pkg/lambda/expression.go | 13 +++++++++++-- pkg/normalorder/runtime.go | 4 ++-- pkg/repr/repr.go | 15 +++++++++++++++ pkg/runtime/runtime.go | 4 ++-- 8 files changed, 51 insertions(+), 23 deletions(-) create mode 100644 pkg/codec/codec.go delete mode 100644 pkg/expr/expr.go create mode 100644 pkg/repr/repr.go diff --git a/.golangci.yml b/.golangci.yml index a9a6f07..9c9494b 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -48,7 +48,7 @@ linters: # More information: https://golangci-lint.run/usage/false-positives/#comments # # Please uncomment the following line if your code is not using the godoc format - - comments + # - comments # Common false positives # feel free to remove this if you don't have any false positives @@ -126,6 +126,9 @@ linters: # Blank import should be only in a main or test package, or have a comment justifying it. - name: blank-imports + # Packages should have comments of the form "Package x ...". + - name: package-comments + # context.Context() should be the first parameter of a function when provided as argument. - name: context-as-argument arguments: diff --git a/Makefile b/Makefile index 2ce49e2..c4ee60d 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ BINARY_NAME=lambda TEST=simple -.PHONY: help build run profile explain graph docs test bench clean +.PHONY: help build run profile explain graph docs test bench lint clean .DEFAULT_GOAL := help .SILENT: @@ -15,6 +15,7 @@ help: echo " docs - Start local godoc server on port 6060" echo " test - Run tests for all samples" echo " bench - Run benchmarks for all samples" + echo " lint - Run golangci-lint on all packages" echo " clean - Remove all build artifacts" build: @@ -45,6 +46,9 @@ test: bench: go test -bench=. -benchtime=10x -cpu=4 ./cmd/lambda +lint: + go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest run ./... + clean: rm -f ${BINARY_NAME} rm -f program.out diff --git a/pkg/codec/codec.go b/pkg/codec/codec.go new file mode 100644 index 0000000..7fc8a7f --- /dev/null +++ b/pkg/codec/codec.go @@ -0,0 +1,12 @@ +package codec + +import "git.maximhutz.com/max/lambda/pkg/repr" + +type String string + +func (s String) Id() string { return "string" } + +type Codec[T, U repr.Repr] interface { + Encode(T) (U, error) + Decode(U) (T, error) +} diff --git a/pkg/expr/expr.go b/pkg/expr/expr.go deleted file mode 100644 index b45a515..0000000 --- a/pkg/expr/expr.go +++ /dev/null @@ -1,15 +0,0 @@ -// Package expr provides the abstract Expression interface for all evaluatable -// expression types in the lambda runtime. -package expr - -import ( - "fmt" -) - -// Expression is the base interface for all evaluatable expression types. -// Different evaluation modes (lambda calculus, SKI combinators, typed lambda -// calculus, etc.) implement this interface with their own concrete types. -type Expression interface { - // The expression should have a human-readable representation. - fmt.Stringer -} diff --git a/pkg/lambda/expression.go b/pkg/lambda/expression.go index dd28fe6..3b78f13 100644 --- a/pkg/lambda/expression.go +++ b/pkg/lambda/expression.go @@ -1,14 +1,17 @@ package lambda import ( - "git.maximhutz.com/max/lambda/pkg/expr" + "fmt" + + "git.maximhutz.com/max/lambda/pkg/repr" "git.maximhutz.com/max/lambda/pkg/set" ) // Expression is the interface for all lambda calculus expression types. // It embeds the general expr.Expression interface for cross-mode compatibility. type Expression interface { - expr.Expression + repr.Repr + fmt.Stringer // Substitute replaces all free occurrences of the target variable with the // replacement expression. Alpha-renaming is performed automatically to @@ -37,6 +40,8 @@ type Abstraction struct { var _ Expression = Abstraction{} +func (a Abstraction) Id() string { return "lambda" } + func (a Abstraction) Parameter() string { return a.parameter } @@ -62,6 +67,8 @@ type Application struct { var _ Expression = Application{} +func (a Application) Id() string { return "lambda" } + func (a Application) Abstraction() Expression { return a.abstraction } @@ -86,6 +93,8 @@ type Variable struct { var _ Expression = Variable{} +func (a Variable) Id() string { return "lambda" } + func (v Variable) Name() string { return v.name } diff --git a/pkg/normalorder/runtime.go b/pkg/normalorder/runtime.go index 37d38e4..c014e3d 100644 --- a/pkg/normalorder/runtime.go +++ b/pkg/normalorder/runtime.go @@ -2,8 +2,8 @@ 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/repr" "git.maximhutz.com/max/lambda/pkg/runtime" ) @@ -23,7 +23,7 @@ func NewRuntime(expression lambda.Expression) *Runtime { } // Expression returns the current expression state. -func (r *Runtime) Expression() expr.Expression { +func (r *Runtime) Expression() repr.Repr { return r.expression } diff --git a/pkg/repr/repr.go b/pkg/repr/repr.go new file mode 100644 index 0000000..a1248f0 --- /dev/null +++ b/pkg/repr/repr.go @@ -0,0 +1,15 @@ +// Package repr defines a general definition of a representation of lambda +// calculus. +package repr + +import "fmt" + +// Repr is a representation of lambda calculus. +type Repr interface { + fmt.Stringer + + // Id returns to name of the objects underlying representation. If is + // assumed that if two Repr objects have the same Id(), they share the same + // representation. + Id() string +} diff --git a/pkg/runtime/runtime.go b/pkg/runtime/runtime.go index c2aafad..5749045 100644 --- a/pkg/runtime/runtime.go +++ b/pkg/runtime/runtime.go @@ -4,7 +4,7 @@ package runtime import ( "git.maximhutz.com/max/lambda/pkg/emitter" - "git.maximhutz.com/max/lambda/pkg/expr" + "git.maximhutz.com/max/lambda/pkg/repr" ) // Runtime defines the interface for expression reduction strategies. @@ -23,5 +23,5 @@ type Runtime interface { Run() // Copy the state of the runtime. - Expression() expr.Expression + Expression() repr.Repr } -- 2.49.1 From 0cdce0e42c93aac58a8e55ef8936725652f71532 Mon Sep 17 00:00:00 2001 From: "M.V. Hutz" Date: Fri, 30 Jan 2026 17:54:47 -0500 Subject: [PATCH 02/14] feat: cli versions --- internal/cli/codec.go | 55 ++++++++++++++++++++++++++++ internal/cli/engine.go | 49 +++++++++++++++++++++++++ internal/cli/marshaler.go | 42 ++++++++++++++++++++++ internal/cli/repr.go | 21 +++++++++++ internal/plugins/debug.go | 23 ------------ internal/plugins/explanation.go | 31 ---------------- internal/plugins/performance.go | 59 ------------------------------- internal/plugins/statistics.go | 44 ----------------------- internal/statistics/statistics.go | 28 --------------- pkg/codec/codec.go | 10 ++---- pkg/engine/engine.go | 7 ++++ pkg/lambda/expression.go | 8 ----- pkg/normalorder/reduce_once.go | 34 ------------------ pkg/normalorder/runtime.go | 46 ------------------------ pkg/repr/repr.go | 15 -------- pkg/runtime/events.go | 13 ------- pkg/runtime/runtime.go | 27 -------------- 17 files changed, 177 insertions(+), 335 deletions(-) create mode 100644 internal/cli/codec.go create mode 100644 internal/cli/engine.go create mode 100644 internal/cli/marshaler.go create mode 100644 internal/cli/repr.go delete mode 100644 internal/plugins/debug.go delete mode 100644 internal/plugins/explanation.go delete mode 100644 internal/plugins/performance.go delete mode 100644 internal/plugins/statistics.go delete mode 100644 internal/statistics/statistics.go create mode 100644 pkg/engine/engine.go delete mode 100644 pkg/normalorder/reduce_once.go delete mode 100644 pkg/normalorder/runtime.go delete mode 100644 pkg/repr/repr.go delete mode 100644 pkg/runtime/events.go delete mode 100644 pkg/runtime/runtime.go diff --git a/internal/cli/codec.go b/internal/cli/codec.go new file mode 100644 index 0000000..6f06f29 --- /dev/null +++ b/internal/cli/codec.go @@ -0,0 +1,55 @@ +package cli + +import ( + "fmt" + + "git.maximhutz.com/max/lambda/pkg/codec" +) + +type Codec interface { + codec.Codec[Repr, Repr] + + InType() string + OutType() string +} + +type convertedCodec[T, U any] struct { + codec codec.Codec[T, U] + inType, outType string +} + +func (c convertedCodec[T, U]) Decode(r Repr) (Repr, error) { + u, ok := r.Data().(U) + if !ok { + return nil, fmt.Errorf("could not parse '%v' as '%s'", r, c.inType) + } + + t, err := c.codec.Decode(u) + if err != nil { + return nil, err + } + + return NewRepr(c.outType, t), nil +} + +func (c convertedCodec[T, U]) Encode(r Repr) (Repr, error) { + t, ok := r.Data().(T) + if !ok { + return nil, fmt.Errorf("could not parse '%v' as '%s'", t, c.outType) + } + + u, err := c.codec.Encode(t) + if err != nil { + return nil, err + } + + return NewRepr(c.inType, u), nil +} + +func (c convertedCodec[T, U]) InType() string { return c.inType } + +func (c convertedCodec[T, U]) OutType() string { return c.outType } + +func ConvertCodec[T, U any](e codec.Codec[T, U], inType, outType string) Codec { + return convertedCodec[T, U]{e, inType, outType} +} diff --git a/internal/cli/engine.go b/internal/cli/engine.go new file mode 100644 index 0000000..f0141cd --- /dev/null +++ b/internal/cli/engine.go @@ -0,0 +1,49 @@ +package cli + +import ( + "fmt" + + "git.maximhutz.com/max/lambda/pkg/engine" +) + +type Engine interface { + engine.Engine[Repr] + + Name() string + InType() string +} + +type convertedEngine[T any] struct { + engine engine.Engine[T] + name string + inType string +} + +func (b convertedEngine[T]) InType() string { return b.inType } + +func (b convertedEngine[T]) Name() string { return b.name } + +func (b convertedEngine[T]) Get() (Repr, error) { + s, err := b.engine.Get() + if err != nil { + return nil, err + } + + return NewRepr(b.inType, s), nil +} + +func (b convertedEngine[T]) Set(r Repr) error { + if t, ok := r.Data().(T); ok { + return b.engine.Set(t) + } + + return fmt.Errorf("Incorrent format '%s' for engine '%s'.", r.Id(), b.inType) +} + +func (b convertedEngine[T]) Step(i int) bool { + return b.engine.Step(i) +} + +func ConvertEngine[T any](e engine.Engine[T], name string, inType string) Engine { + return convertedEngine[T]{e, name, inType} +} diff --git a/internal/cli/marshaler.go b/internal/cli/marshaler.go new file mode 100644 index 0000000..e10f7ee --- /dev/null +++ b/internal/cli/marshaler.go @@ -0,0 +1,42 @@ +package cli + +import ( + "fmt" + + "git.maximhutz.com/max/lambda/pkg/codec" +) + +type Marshaler interface { + codec.Marshaler[Repr] + + InType() string +} + +type convertedMarshaler[T any] struct { + codec codec.Marshaler[T] + inType string +} + +func (c convertedMarshaler[T]) Decode(s string) (Repr, error) { + t, err := c.codec.Decode(s) + if err != nil { + return nil, err + } + + return NewRepr(c.inType, t), nil +} + +func (c convertedMarshaler[T]) Encode(r Repr) (string, error) { + t, ok := r.Data().(T) + if !ok { + return "", fmt.Errorf("could not parse '%v' as 'string'", t) + } + + return c.codec.Encode(t) +} + +func (c convertedMarshaler[T]) InType() string { return c.inType } + +func ConvertMarshaler[T any](e codec.Marshaler[T], inType string) Marshaler { + return convertedMarshaler[T]{e, inType} +} diff --git a/internal/cli/repr.go b/internal/cli/repr.go new file mode 100644 index 0000000..c684639 --- /dev/null +++ b/internal/cli/repr.go @@ -0,0 +1,21 @@ +package cli + +type Repr interface { + // Id returns to name of the objects underlying representation. If is + // assumed that if two Repr objects have the same Id(), they share the same + // representation. + Id() string + + Data() any +} + +type baseRepr struct { + id string + data any +} + +func (r baseRepr) Id() string { return r.id } + +func (r baseRepr) Data() any { return r.data } + +func NewRepr(id string, data any) Repr { return baseRepr{id, data} } diff --git a/internal/plugins/debug.go b/internal/plugins/debug.go deleted file mode 100644 index 82cd1b8..0000000 --- a/internal/plugins/debug.go +++ /dev/null @@ -1,23 +0,0 @@ -package plugins - -import ( - "log/slog" - - "git.maximhutz.com/max/lambda/pkg/runtime" -) - -type Logs struct { - logger *slog.Logger - reducer runtime.Runtime -} - -func NewLogs(logger *slog.Logger, r runtime.Runtime) *Logs { - plugin := &Logs{logger, r} - r.On(runtime.StepEvent, plugin.Step) - - return plugin -} - -func (t *Logs) Step() { - t.logger.Info("reduction", "tree", t.reducer.Expression().String()) -} diff --git a/internal/plugins/explanation.go b/internal/plugins/explanation.go deleted file mode 100644 index ba92a99..0000000 --- a/internal/plugins/explanation.go +++ /dev/null @@ -1,31 +0,0 @@ -// Package "explanation" provides an observer to gather the reasoning during the -// reduction, and present a thorough explanation to the user for each step. -package plugins - -import ( - "fmt" - - "git.maximhutz.com/max/lambda/pkg/runtime" -) - -// Track the reductions made by a reduction process. -type Explanation struct { - reducer runtime.Runtime -} - -// Attaches a new explanation tracker to a reducer. -func NewExplanation(r runtime.Runtime) *Explanation { - plugin := &Explanation{reducer: r} - r.On(runtime.StartEvent, plugin.Start) - r.On(runtime.StepEvent, plugin.Step) - - return plugin -} - -func (t *Explanation) Start() { - fmt.Println(t.reducer.Expression().String()) -} - -func (t *Explanation) Step() { - fmt.Println(" =", t.reducer.Expression().String()) -} diff --git a/internal/plugins/performance.go b/internal/plugins/performance.go deleted file mode 100644 index eac3f20..0000000 --- a/internal/plugins/performance.go +++ /dev/null @@ -1,59 +0,0 @@ -// Package "performance" provides a tracker to observer CPU performance during -// execution. -package plugins - -import ( - "os" - "path/filepath" - "runtime/pprof" - - "git.maximhutz.com/max/lambda/pkg/runtime" -) - -// Observes a reduction process, and publishes a CPU performance profile on -// completion. -type Performance struct { - File string - filePointer *os.File - Error error -} - -// Create a performance tracker that outputs a profile to "file". -func NewPerformance(file string, process runtime.Runtime) *Performance { - plugin := &Performance{File: file} - process.On(runtime.StartEvent, plugin.Start) - process.On(runtime.StopEvent, plugin.Stop) - - return plugin -} - -// Begin profiling. -func (t *Performance) Start() { - var absPath string - - absPath, t.Error = filepath.Abs(t.File) - if t.Error != nil { - return - } - - t.Error = os.MkdirAll(filepath.Dir(absPath), 0777) - if t.Error != nil { - return - } - - t.filePointer, t.Error = os.Create(absPath) - if t.Error != nil { - return - } - - t.Error = pprof.StartCPUProfile(t.filePointer) - if t.Error != nil { - return - } -} - -// Stop profiling. -func (t *Performance) Stop() { - pprof.StopCPUProfile() - t.filePointer.Close() -} diff --git a/internal/plugins/statistics.go b/internal/plugins/statistics.go deleted file mode 100644 index 2845738..0000000 --- a/internal/plugins/statistics.go +++ /dev/null @@ -1,44 +0,0 @@ -package plugins - -import ( - "fmt" - "os" - "time" - - "git.maximhutz.com/max/lambda/internal/statistics" - "git.maximhutz.com/max/lambda/pkg/runtime" -) - -// An observer, to track reduction performance. -type Statistics struct { - start time.Time - steps uint64 -} - -// Create a new reduction performance Statistics. -func NewStatistics(r runtime.Runtime) *Statistics { - plugin := &Statistics{} - r.On(runtime.StartEvent, plugin.Start) - r.On(runtime.StepEvent, plugin.Step) - r.On(runtime.StopEvent, plugin.Stop) - - 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/statistics.go b/internal/statistics/statistics.go deleted file mode 100644 index 87cf386..0000000 --- a/internal/statistics/statistics.go +++ /dev/null @@ -1,28 +0,0 @@ -// Package "statistics" provides a way to observer reduction speed during -// execution. -package statistics - -import ( - "fmt" - "strings" -) - -// Statistics for a specific reduction. -type Results struct { - StepsTaken uint64 // Number of steps taken during execution. - TimeElapsed uint64 // The time (ms) taken for execution to complete. -} - -// Returns the average number of operations per second of the execution. -func (r Results) OpsPerSecond() float32 { - return float32(r.StepsTaken) / (float32(r.TimeElapsed) / 1000) -} - -// Format the results as a string. -func (r Results) String() string { - builder := strings.Builder{} - fmt.Fprintln(&builder, "Time Spent:", r.TimeElapsed, "ms") - fmt.Fprintln(&builder, "Steps:", r.StepsTaken) - fmt.Fprintln(&builder, "Speed:", r.OpsPerSecond(), "ops") - return builder.String() -} diff --git a/pkg/codec/codec.go b/pkg/codec/codec.go index 7fc8a7f..3132409 100644 --- a/pkg/codec/codec.go +++ b/pkg/codec/codec.go @@ -1,12 +1,8 @@ package codec -import "git.maximhutz.com/max/lambda/pkg/repr" - -type String string - -func (s String) Id() string { return "string" } - -type Codec[T, U repr.Repr] interface { +type Codec[T, U any] interface { Encode(T) (U, error) Decode(U) (T, error) } + +type Marshaler[T any] = Codec[T, string] diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go new file mode 100644 index 0000000..22f716d --- /dev/null +++ b/pkg/engine/engine.go @@ -0,0 +1,7 @@ +package engine + +type Engine[T any] interface { + Get() (T, error) + Set(T) error + Step(int) bool +} diff --git a/pkg/lambda/expression.go b/pkg/lambda/expression.go index 3b78f13..b979a09 100644 --- a/pkg/lambda/expression.go +++ b/pkg/lambda/expression.go @@ -3,14 +3,12 @@ package lambda import ( "fmt" - "git.maximhutz.com/max/lambda/pkg/repr" "git.maximhutz.com/max/lambda/pkg/set" ) // Expression is the interface for all lambda calculus expression types. // It embeds the general expr.Expression interface for cross-mode compatibility. type Expression interface { - repr.Repr fmt.Stringer // Substitute replaces all free occurrences of the target variable with the @@ -40,8 +38,6 @@ type Abstraction struct { var _ Expression = Abstraction{} -func (a Abstraction) Id() string { return "lambda" } - func (a Abstraction) Parameter() string { return a.parameter } @@ -67,8 +63,6 @@ type Application struct { var _ Expression = Application{} -func (a Application) Id() string { return "lambda" } - func (a Application) Abstraction() Expression { return a.abstraction } @@ -93,8 +87,6 @@ type Variable struct { var _ Expression = Variable{} -func (a Variable) Id() string { return "lambda" } - func (v Variable) Name() string { return v.name } diff --git a/pkg/normalorder/reduce_once.go b/pkg/normalorder/reduce_once.go deleted file mode 100644 index e1a8aff..0000000 --- a/pkg/normalorder/reduce_once.go +++ /dev/null @@ -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 - } -} diff --git a/pkg/normalorder/runtime.go b/pkg/normalorder/runtime.go deleted file mode 100644 index c014e3d..0000000 --- a/pkg/normalorder/runtime.go +++ /dev/null @@ -1,46 +0,0 @@ -package normalorder - -import ( - "git.maximhutz.com/max/lambda/pkg/emitter" - "git.maximhutz.com/max/lambda/pkg/lambda" - "git.maximhutz.com/max/lambda/pkg/repr" - "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() repr.Repr { - 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) -} diff --git a/pkg/repr/repr.go b/pkg/repr/repr.go deleted file mode 100644 index a1248f0..0000000 --- a/pkg/repr/repr.go +++ /dev/null @@ -1,15 +0,0 @@ -// Package repr defines a general definition of a representation of lambda -// calculus. -package repr - -import "fmt" - -// Repr is a representation of lambda calculus. -type Repr interface { - fmt.Stringer - - // Id returns to name of the objects underlying representation. If is - // assumed that if two Repr objects have the same Id(), they share the same - // representation. - Id() string -} diff --git a/pkg/runtime/events.go b/pkg/runtime/events.go deleted file mode 100644 index 6e884c8..0000000 --- a/pkg/runtime/events.go +++ /dev/null @@ -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 -) diff --git a/pkg/runtime/runtime.go b/pkg/runtime/runtime.go deleted file mode 100644 index 5749045..0000000 --- a/pkg/runtime/runtime.go +++ /dev/null @@ -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/repr" -) - -// 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() repr.Repr -} -- 2.49.1 From 68cc1624c75e3aa9c1d77cc6f914973ff5cd7c35 Mon Sep 17 00:00:00 2001 From: "M.V. Hutz" Date: Fri, 30 Jan 2026 20:07:35 -0500 Subject: [PATCH 03/14] feat: registry, normalorder engine, lambda codec and marshaler --- cmd/lambda/registry.go | 24 +++++++++ internal/registry/registry.go | 75 ++++++++++++++++++++++++++++ pkg/convert/saccharine_to_lambda.go | 68 ++++++++++++++++++------- pkg/engine/normalorder/engine.go | 34 +++++++++++++ pkg/engine/normalorder/reduce_one.go | 34 +++++++++++++ pkg/lambda/marshaler.go | 19 +++++++ 6 files changed, 235 insertions(+), 19 deletions(-) create mode 100644 cmd/lambda/registry.go create mode 100644 internal/registry/registry.go create mode 100644 pkg/engine/normalorder/engine.go create mode 100644 pkg/engine/normalorder/reduce_one.go create mode 100644 pkg/lambda/marshaler.go diff --git a/cmd/lambda/registry.go b/cmd/lambda/registry.go new file mode 100644 index 0000000..b983bc1 --- /dev/null +++ b/cmd/lambda/registry.go @@ -0,0 +1,24 @@ +package main + +import ( + "git.maximhutz.com/max/lambda/internal/cli" + "git.maximhutz.com/max/lambda/internal/registry" + "git.maximhutz.com/max/lambda/pkg/convert" + "git.maximhutz.com/max/lambda/pkg/engine/normalorder" + "git.maximhutz.com/max/lambda/pkg/lambda" +) + +func MakeRegistry() *registry.Registry { + r := registry.New() + + // Codecs + r.AddCodec(cli.ConvertCodec(convert.Saccharine2Lambda{}, "saccharine", "lambda")) + + // Engines + r.AddEngine(cli.ConvertEngine(normalorder.Engine{}, "normalorder", "lambda")) + + // Marshalers + r.AddMarshaler(cli.ConvertMarshaler(lambda.Marshaler{}, "lambda")) + + return r +} diff --git a/internal/registry/registry.go b/internal/registry/registry.go new file mode 100644 index 0000000..de6256a --- /dev/null +++ b/internal/registry/registry.go @@ -0,0 +1,75 @@ +package registry + +import ( + "fmt" + + "git.maximhutz.com/max/lambda/internal/cli" +) + +type Registry struct { + marshalers map[string]cli.Marshaler + codecs []cli.Codec + engines map[string]cli.Engine +} + +func New() *Registry { + return &Registry{ + marshalers: map[string]cli.Marshaler{}, + codecs: []cli.Codec{}, + engines: map[string]cli.Engine{}, + } +} + +func (r *Registry) AddCodec(c cli.Codec) error { + r.codecs = append(r.codecs, c) + return nil +} + +func (r *Registry) AddMarshaler(c cli.Marshaler) error { + if _, ok := r.marshalers[c.InType()]; ok { + return fmt.Errorf("marshaler for '%s' already registered", c.InType()) + } + + r.marshalers[c.InType()] = c + return nil +} + +func (r *Registry) AddEngine(e cli.Engine) error { + if _, ok := r.engines[e.Name()]; ok { + return fmt.Errorf("engine '%s' already registered", e.Name()) + } + + r.engines[e.Name()] = e + return nil +} + +func (r *Registry) GetEngine(name string) (cli.Engine, error) { + e, ok := r.engines[name] + if !ok { + return nil, fmt.Errorf("engine '%s' not found", name) + } + + return e, nil +} + +func (r *Registry) ConvertTo(repr cli.Repr, outType string) (cli.Repr, error) { + panic("") +} + +func (r *Registry) Marshal(repr cli.Repr) (string, error) { + m, ok := r.marshalers[repr.Id()] + if !ok { + return "", fmt.Errorf("no marshaler for '%s'", repr.Id()) + } + + return m.Encode(repr) +} + +func (r *Registry) Unmarshal(s string, outType string) (cli.Repr, error) { + m, ok := r.marshalers[outType] + if !ok { + return nil, fmt.Errorf("no marshaler for '%s'", outType) + } + + return m.Decode(s) +} diff --git a/pkg/convert/saccharine_to_lambda.go b/pkg/convert/saccharine_to_lambda.go index 8bab6b8..e19e77c 100644 --- a/pkg/convert/saccharine_to_lambda.go +++ b/pkg/convert/saccharine_to_lambda.go @@ -3,16 +3,17 @@ package convert import ( "fmt" + "git.maximhutz.com/max/lambda/pkg/codec" "git.maximhutz.com/max/lambda/pkg/lambda" "git.maximhutz.com/max/lambda/pkg/saccharine" ) -func convertAtom(n *saccharine.Atom) lambda.Expression { +func encodeAtom(n *saccharine.Atom) lambda.Expression { return lambda.NewVariable(n.Name) } -func convertAbstraction(n *saccharine.Abstraction) lambda.Expression { - result := SaccharineToLambda(n.Body) +func encodeAbstraction(n *saccharine.Abstraction) lambda.Expression { + result := encodeExpression(n.Body) parameters := n.Parameters @@ -31,13 +32,13 @@ func convertAbstraction(n *saccharine.Abstraction) lambda.Expression { return result } -func convertApplication(n *saccharine.Application) lambda.Expression { - result := SaccharineToLambda(n.Abstraction) +func encodeApplication(n *saccharine.Application) lambda.Expression { + result := encodeExpression(n.Abstraction) arguments := []lambda.Expression{} for _, argument := range n.Arguments { - convertedArgument := SaccharineToLambda(argument) - arguments = append(arguments, convertedArgument) + encodeedArgument := encodeExpression(argument) + arguments = append(arguments, encodeedArgument) } for _, argument := range arguments { @@ -51,9 +52,9 @@ func reduceLet(s *saccharine.LetStatement, e lambda.Expression) lambda.Expressio var value lambda.Expression if len(s.Parameters) == 0 { - value = SaccharineToLambda(s.Body) + value = encodeExpression(s.Body) } else { - value = convertAbstraction(saccharine.NewAbstraction(s.Parameters, s.Body)) + value = encodeAbstraction(saccharine.NewAbstraction(s.Parameters, s.Body)) } return lambda.NewApplication( @@ -67,7 +68,7 @@ func reduceDeclare(s *saccharine.DeclareStatement, e lambda.Expression) lambda.E return lambda.NewApplication( lambda.NewAbstraction(freshVar, e), - SaccharineToLambda(s.Value), + encodeExpression(s.Value), ) } @@ -82,8 +83,8 @@ func reduceStatement(s saccharine.Statement, e lambda.Expression) lambda.Express } } -func convertClause(n *saccharine.Clause) lambda.Expression { - result := SaccharineToLambda(n.Returns) +func encodeClause(n *saccharine.Clause) lambda.Expression { + result := encodeExpression(n.Returns) for i := len(n.Statements) - 1; i >= 0; i-- { result = reduceStatement(n.Statements[i], result) @@ -92,17 +93,46 @@ func convertClause(n *saccharine.Clause) lambda.Expression { return result } -func SaccharineToLambda(n saccharine.Expression) lambda.Expression { - switch n := n.(type) { +func encodeExpression(s saccharine.Expression) lambda.Expression { + switch s := s.(type) { case *saccharine.Atom: - return convertAtom(n) + return encodeAtom(s) case *saccharine.Abstraction: - return convertAbstraction(n) + return encodeAbstraction(s) case *saccharine.Application: - return convertApplication(n) + return encodeApplication(s) case *saccharine.Clause: - return convertClause(n) + return encodeClause(s) default: - panic(fmt.Errorf("unknown expression type: %T", n)) + panic(fmt.Errorf("unknown expression type: %T", s)) } } + +func decodeExression(l lambda.Expression) saccharine.Expression { + switch l := l.(type) { + case lambda.Variable: + return saccharine.NewAtom(l.Name()) + case lambda.Abstraction: + return saccharine.NewAbstraction( + []string{l.Parameter()}, + decodeExression(l.Body())) + case lambda.Application: + return saccharine.NewApplication( + decodeExression(l.Abstraction()), + []saccharine.Expression{decodeExression(l.Argument())}) + default: + panic(fmt.Errorf("unknown expression type: %T", l)) + } +} + +type Saccharine2Lambda struct{} + +func (c Saccharine2Lambda) Decode(l lambda.Expression) (saccharine.Expression, error) { + return decodeExression(l), nil +} + +func (c Saccharine2Lambda) Encode(s saccharine.Expression) (lambda.Expression, error) { + return encodeExpression(s), nil +} + +var _ codec.Codec[saccharine.Expression, lambda.Expression] = (*Saccharine2Lambda)(nil) diff --git a/pkg/engine/normalorder/engine.go b/pkg/engine/normalorder/engine.go new file mode 100644 index 0000000..41eca85 --- /dev/null +++ b/pkg/engine/normalorder/engine.go @@ -0,0 +1,34 @@ +package normalorder + +import ( + "git.maximhutz.com/max/lambda/pkg/engine" + "git.maximhutz.com/max/lambda/pkg/lambda" +) + +type Engine struct { + expr lambda.Expression +} + +func (e Engine) Get() (lambda.Expression, error) { + return e.expr, nil +} + +func (e Engine) Set(l lambda.Expression) error { + e.expr = l + return nil +} + +func (e Engine) Step(i int) bool { + var reduced bool + + for range i { + e.expr, reduced = ReduceOnce(e.expr) + if !reduced { + return false + } + } + + return true +} + +var _ engine.Engine[lambda.Expression] = (*Engine)(nil) diff --git a/pkg/engine/normalorder/reduce_one.go b/pkg/engine/normalorder/reduce_one.go new file mode 100644 index 0000000..e1a8aff --- /dev/null +++ b/pkg/engine/normalorder/reduce_one.go @@ -0,0 +1,34 @@ +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 + } +} diff --git a/pkg/lambda/marshaler.go b/pkg/lambda/marshaler.go new file mode 100644 index 0000000..6ee2c79 --- /dev/null +++ b/pkg/lambda/marshaler.go @@ -0,0 +1,19 @@ +package lambda + +import ( + "fmt" + + "git.maximhutz.com/max/lambda/pkg/codec" +) + +type Marshaler struct{} + +func (m Marshaler) Decode(string) (Expression, error) { + return nil, fmt.Errorf("unimplemented") +} + +func (m Marshaler) Encode(e Expression) (string, error) { + return e.String(), nil +} + +var _ codec.Marshaler[Expression] = (*Marshaler)(nil) -- 2.49.1 From 31924237b26342d60856f20cd8575ed87997c1f4 Mon Sep 17 00:00:00 2001 From: "M.V. Hutz" Date: Fri, 30 Jan 2026 20:16:57 -0500 Subject: [PATCH 04/14] feat: saccharine marshaler --- cmd/lambda/registry.go | 2 + pkg/saccharine/marshaler.go | 24 ++++++ pkg/saccharine/parse.go | 49 ++++++------ pkg/saccharine/saccharine.go | 22 ------ pkg/saccharine/{token/parse.go => scan.go} | 38 ++++----- pkg/saccharine/token.go | 91 ++++++++++++++++++++++ pkg/saccharine/token/token.go | 91 ---------------------- 7 files changed, 160 insertions(+), 157 deletions(-) create mode 100644 pkg/saccharine/marshaler.go delete mode 100644 pkg/saccharine/saccharine.go rename pkg/saccharine/{token/parse.go => scan.go} (70%) create mode 100644 pkg/saccharine/token.go delete mode 100644 pkg/saccharine/token/token.go diff --git a/cmd/lambda/registry.go b/cmd/lambda/registry.go index b983bc1..773afc0 100644 --- a/cmd/lambda/registry.go +++ b/cmd/lambda/registry.go @@ -6,6 +6,7 @@ import ( "git.maximhutz.com/max/lambda/pkg/convert" "git.maximhutz.com/max/lambda/pkg/engine/normalorder" "git.maximhutz.com/max/lambda/pkg/lambda" + "git.maximhutz.com/max/lambda/pkg/saccharine" ) func MakeRegistry() *registry.Registry { @@ -19,6 +20,7 @@ func MakeRegistry() *registry.Registry { // Marshalers r.AddMarshaler(cli.ConvertMarshaler(lambda.Marshaler{}, "lambda")) + r.AddMarshaler(cli.ConvertMarshaler(saccharine.Marshaler{}, "saccharine")) return r } diff --git a/pkg/saccharine/marshaler.go b/pkg/saccharine/marshaler.go new file mode 100644 index 0000000..bc5c3dc --- /dev/null +++ b/pkg/saccharine/marshaler.go @@ -0,0 +1,24 @@ +// Package "saccharine" provides a simple language built on top of λ-calculus, +// to facilitate productive coding using it. +package saccharine + +import ( + "git.maximhutz.com/max/lambda/pkg/codec" +) + +type Marshaler struct{} + +func (m Marshaler) Decode(s string) (Expression, error) { + tokens, err := scan(s) + if err != nil { + return nil, err + } + + return parse(tokens) +} + +func (m Marshaler) Encode(e Expression) (string, error) { + return stringifyExpression(e), nil +} + +var _ codec.Marshaler[Expression] = (*Marshaler)(nil) diff --git a/pkg/saccharine/parse.go b/pkg/saccharine/parse.go index eb045d0..8b73394 100644 --- a/pkg/saccharine/parse.go +++ b/pkg/saccharine/parse.go @@ -5,18 +5,17 @@ import ( "fmt" "git.maximhutz.com/max/lambda/pkg/iterator" - "git.maximhutz.com/max/lambda/pkg/saccharine/token" "git.maximhutz.com/max/lambda/pkg/trace" ) -type TokenIterator = iterator.Iterator[token.Token] +type TokenIterator = iterator.Iterator[Token] -func parseRawToken(i *TokenIterator, expected token.Type) (*token.Token, error) { - return iterator.Do(i, func(i *TokenIterator) (*token.Token, error) { +func parseRawToken(i *TokenIterator, expected TokenType) (*Token, error) { + return iterator.Do(i, func(i *TokenIterator) (*Token, error) { if tok, err := i.Next(); err != nil { return nil, err } else if tok.Type != expected { - return nil, fmt.Errorf("expected token %v, got %v'", token.Name(expected), tok.Value) + return nil, fmt.Errorf("expected token %v, got %v'", expected.Name(), tok.Value) } else { return &tok, nil } @@ -25,14 +24,14 @@ func parseRawToken(i *TokenIterator, expected token.Type) (*token.Token, error) func passSoftBreaks(i *TokenIterator) { for { - if _, err := parseRawToken(i, token.SoftBreak); err != nil { + if _, err := parseRawToken(i, TokenSoftBreak); err != nil { return } } } -func parseToken(i *TokenIterator, expected token.Type, ignoreSoftBreaks bool) (*token.Token, error) { - return iterator.Do(i, func(i *TokenIterator) (*token.Token, error) { +func parseToken(i *TokenIterator, expected TokenType, ignoreSoftBreaks bool) (*Token, error) { + return iterator.Do(i, func(i *TokenIterator) (*Token, error) { if ignoreSoftBreaks { passSoftBreaks(i) } @@ -42,17 +41,17 @@ func parseToken(i *TokenIterator, expected token.Type, ignoreSoftBreaks bool) (* } func parseString(i *TokenIterator) (string, error) { - if tok, err := parseToken(i, token.Atom, true); err != nil { + if tok, err := parseToken(i, TokenAtom, true); err != nil { return "", trace.Wrap(err, "no variable (col %d)", i.Index()) } else { return tok.Value, nil } } -func parseBreak(i *TokenIterator) (*token.Token, error) { - if tok, softErr := parseRawToken(i, token.SoftBreak); softErr == nil { +func parseBreak(i *TokenIterator) (*Token, error) { + if tok, softErr := parseRawToken(i, TokenSoftBreak); softErr == nil { return tok, nil - } else if tok, hardErr := parseRawToken(i, token.HardBreak); hardErr == nil { + } else if tok, hardErr := parseRawToken(i, TokenHardBreak); hardErr == nil { return tok, nil } else { return nil, errors.Join(softErr, hardErr) @@ -76,11 +75,11 @@ func parseList[U any](i *TokenIterator, fn func(*TokenIterator) (U, error), mini func parseAbstraction(i *TokenIterator) (*Abstraction, error) { return iterator.Do(i, func(i *TokenIterator) (*Abstraction, error) { - if _, err := parseToken(i, token.Slash, true); err != nil { + if _, err := parseToken(i, TokenSlash, true); err != nil { return nil, trace.Wrap(err, "no function slash (col %d)", i.MustGet().Column) } else if parameters, err := parseList(i, parseString, 0); err != nil { return nil, err - } else if _, err = parseToken(i, token.Dot, true); err != nil { + } else if _, err = parseToken(i, TokenDot, true); err != nil { return nil, trace.Wrap(err, "no function dot (col %d)", i.MustGet().Column) } else if body, err := parseExpression(i); err != nil { return nil, err @@ -92,11 +91,11 @@ func parseAbstraction(i *TokenIterator) (*Abstraction, error) { func parseApplication(i *TokenIterator) (*Application, error) { return iterator.Do(i, func(i *TokenIterator) (*Application, error) { - if _, err := parseToken(i, token.OpenParen, true); err != nil { + if _, err := parseToken(i, TokenOpenParen, true); err != nil { return nil, trace.Wrap(err, "no openning brackets (col %d)", i.MustGet().Column) } else if expressions, err := parseList(i, parseExpression, 1); err != nil { return nil, err - } else if _, err := parseToken(i, token.CloseParen, true); err != nil { + } else if _, err := parseToken(i, TokenCloseParen, true); err != nil { return nil, trace.Wrap(err, "no closing brackets (col %d)", i.MustGet().Column) } else { return NewApplication(expressions[0], expressions[1:]), nil @@ -105,7 +104,7 @@ func parseApplication(i *TokenIterator) (*Application, error) { } func parseAtom(i *TokenIterator) (*Atom, error) { - if tok, err := parseToken(i, token.Atom, true); err != nil { + if tok, err := parseToken(i, TokenAtom, true); err != nil { return nil, trace.Wrap(err, "no variable (col %d)", i.Index()) } else { return NewAtom(tok.Value), nil @@ -133,7 +132,7 @@ func parseStatements(i *TokenIterator) ([]Statement, error) { func parseClause(i *TokenIterator, braces bool) (*Clause, error) { if braces { - if _, err := parseToken(i, token.OpenBrace, true); err != nil { + if _, err := parseToken(i, TokenOpenBrace, true); err != nil { return nil, err } } @@ -152,7 +151,7 @@ func parseClause(i *TokenIterator, braces bool) (*Clause, error) { } if braces { - if _, err := parseToken(i, token.CloseBrace, true); err != nil { + if _, err := parseToken(i, TokenCloseBrace, true); err != nil { return nil, err } } @@ -165,13 +164,13 @@ func parseExpression(i *TokenIterator) (Expression, error) { passSoftBreaks(i) switch peek := i.MustGet(); peek.Type { - case token.OpenParen: + case TokenOpenParen: return parseApplication(i) - case token.Slash: + case TokenSlash: return parseAbstraction(i) - case token.Atom: + case TokenAtom: return parseAtom(i) - case token.OpenBrace: + case TokenOpenBrace: return parseClause(i, true) default: return nil, fmt.Errorf("expected expression, got '%v' (col %d)", peek.Value, peek.Column) @@ -183,7 +182,7 @@ func parseLet(i *TokenIterator) (*LetStatement, error) { return iterator.Do(i, func(i *TokenIterator) (*LetStatement, error) { if parameters, err := parseList(i, parseString, 1); err != nil { return nil, err - } else if _, err := parseToken(i, token.Assign, true); err != nil { + } else if _, err := parseToken(i, TokenAssign, true); err != nil { return nil, err } else if body, err := parseExpression(i); err != nil { return nil, err @@ -212,7 +211,7 @@ func parseStatement(i *TokenIterator) (Statement, error) { } // Given a list of tokens, attempt to parse it into an syntax tree. -func parse(tokens []token.Token) (Expression, error) { +func parse(tokens []Token) (Expression, error) { i := iterator.Of(tokens) exp, err := parseClause(i, false) diff --git a/pkg/saccharine/saccharine.go b/pkg/saccharine/saccharine.go deleted file mode 100644 index 57b1f17..0000000 --- a/pkg/saccharine/saccharine.go +++ /dev/null @@ -1,22 +0,0 @@ -// Package "saccharine" provides a simple language built on top of λ-calculus, -// to facilitate productive coding using it. -package saccharine - -import ( - "git.maximhutz.com/max/lambda/pkg/saccharine/token" -) - -// Convert a piece of valid saccharine code into an expression. -func Parse(code string) (Expression, error) { - tokens, err := token.Parse(code) - if err != nil { - return nil, err - } - - return parse(tokens) -} - -// Convert a parsed saccharine expression back into source code. -func Stringify(expression Expression) string { - return stringifyExpression(expression) -} diff --git a/pkg/saccharine/token/parse.go b/pkg/saccharine/scan.go similarity index 70% rename from pkg/saccharine/token/parse.go rename to pkg/saccharine/scan.go index b7d5033..08e3459 100644 --- a/pkg/saccharine/token/parse.go +++ b/pkg/saccharine/scan.go @@ -1,4 +1,4 @@ -package token +package saccharine import ( "errors" @@ -14,7 +14,7 @@ func isVariable(r rune) bool { return unicode.IsLetter(r) || unicode.IsNumber(r) } -func parseRune(i *iterator.Iterator[rune], expected func(rune) bool) (rune, error) { +func scanRune(i *iterator.Iterator[rune], expected func(rune) bool) (rune, error) { i2 := i.Copy() if r, err := i2.Next(); err != nil { @@ -27,7 +27,7 @@ func parseRune(i *iterator.Iterator[rune], expected func(rune) bool) (rune, erro } } -func parseCharacter(i *iterator.Iterator[rune], expected rune) (rune, error) { +func scanCharacter(i *iterator.Iterator[rune], expected rune) (rune, error) { i2 := i.Copy() if r, err := i2.Next(); err != nil { @@ -42,7 +42,7 @@ func parseCharacter(i *iterator.Iterator[rune], expected rune) (rune, error) { // Pulls the next token from an iterator over runes. If it cannot, it will // return nil. If an error occurs, it will return that. -func getToken(i *iterator.Iterator[rune]) (*Token, error) { +func scanToken(i *iterator.Iterator[rune]) (*Token, error) { index := i.Index() if i.Done() { @@ -56,27 +56,27 @@ func getToken(i *iterator.Iterator[rune]) (*Token, error) { switch { case letter == '(': - return NewOpenParen(index), nil + return NewTokenOpenParen(index), nil case letter == ')': - return NewCloseParen(index), nil + return NewTokenCloseParen(index), nil case letter == '.': - return NewDot(index), nil + return NewTokenDot(index), nil case letter == '\\': - return NewSlash(index), nil + return NewTokenSlash(index), nil case letter == '\n': - return NewSoftBreak(index), nil + return NewTokenSoftBreak(index), nil case letter == '{': - return NewOpenBrace(index), nil + return NewTokenOpenBrace(index), nil case letter == '}': - return NewCloseBrace(index), nil + return NewTokenCloseBrace(index), nil case letter == ':': - if _, err := parseCharacter(i, '='); err != nil { + if _, err := scanCharacter(i, '='); err != nil { return nil, err } else { - return NewAssign(index), nil + return NewTokenAssign(index), nil } case letter == ';': - return NewHardBreak(index), nil + return NewTokenHardBreak(index), nil case letter == '#': // Skip everything until the next newline or EOF. for !i.Done() { @@ -98,27 +98,27 @@ func getToken(i *iterator.Iterator[rune]) (*Token, error) { atom := []rune{letter} for { - if r, err := parseRune(i, isVariable); err != nil { + if r, err := scanRune(i, isVariable); err != nil { break } else { atom = append(atom, r) } } - return NewAtom(string(atom), index), nil + return NewTokenAtom(string(atom), index), nil } return nil, fmt.Errorf("unknown character '%v'", string(letter)) } -// Parse a string into tokens. -func Parse(input string) ([]Token, error) { +// scan a string into tokens. +func scan(input string) ([]Token, error) { i := iterator.Of([]rune(input)) tokens := []Token{} errorList := []error{} for !i.Done() { - token, err := getToken(i) + token, err := scanToken(i) if err != nil { errorList = append(errorList, err) } else if token != nil { diff --git a/pkg/saccharine/token.go b/pkg/saccharine/token.go new file mode 100644 index 0000000..ee61dbf --- /dev/null +++ b/pkg/saccharine/token.go @@ -0,0 +1,91 @@ +package saccharine + +import "fmt" + +// All tokens in the pseudo-lambda language. +type TokenType int + +const ( + TokenOpenParen TokenType = iota // Denotes the '(' token. + TokenCloseParen // Denotes the ')' token. + TokenOpenBrace // Denotes the '{' token. + TokenCloseBrace // Denotes the '}' token. + TokenHardBreak // Denotes the ';' token. + TokenAssign // Denotes the ':=' token. + TokenAtom // Denotes an alpha-numeric variable. + TokenSlash // Denotes the '/' token. + TokenDot // Denotes the '.' token. + TokenSoftBreak // Denotes a new-line. +) + +// A representation of a token in source code. +type Token struct { + Column int // Where the token begins in the source text. + Type TokenType // What type the token is. + Value string // The value of the token. +} + +func NewTokenOpenParen(column int) *Token { + return &Token{Type: TokenOpenParen, Column: column, Value: "("} +} + +func NewTokenCloseParen(column int) *Token { + return &Token{Type: TokenCloseParen, Column: column, Value: ")"} +} + +func NewTokenOpenBrace(column int) *Token { + return &Token{Type: TokenOpenBrace, Column: column, Value: "{"} +} + +func NewTokenCloseBrace(column int) *Token { + return &Token{Type: TokenCloseBrace, Column: column, Value: "}"} +} + +func NewTokenDot(column int) *Token { + return &Token{Type: TokenDot, Column: column, Value: "."} +} + +func NewTokenHardBreak(column int) *Token { + return &Token{Type: TokenHardBreak, Column: column, Value: ";"} +} + +func NewTokenAssign(column int) *Token { + return &Token{Type: TokenAssign, Column: column, Value: ":="} +} + +func NewTokenSlash(column int) *Token { + return &Token{Type: TokenSlash, Column: column, Value: "\\"} +} + +func NewTokenAtom(name string, column int) *Token { + return &Token{Type: TokenAtom, Column: column, Value: name} +} + +func NewTokenSoftBreak(column int) *Token { + return &Token{Type: TokenSoftBreak, Column: column, Value: "\\n"} +} + +func (t TokenType) Name() string { + switch t { + case TokenOpenParen: + return "(" + case TokenCloseParen: + return ")" + case TokenSlash: + return "\\" + case TokenDot: + return "." + case TokenAtom: + return "ATOM" + case TokenSoftBreak: + return "\\n" + case TokenHardBreak: + return ";" + default: + panic(fmt.Errorf("unknown token type %v", t)) + } +} + +func (t Token) Name() string { + return t.Type.Name() +} diff --git a/pkg/saccharine/token/token.go b/pkg/saccharine/token/token.go deleted file mode 100644 index 57771a5..0000000 --- a/pkg/saccharine/token/token.go +++ /dev/null @@ -1,91 +0,0 @@ -package token - -import "fmt" - -// All tokens in the pseudo-lambda language. -type Type int - -const ( - OpenParen Type = iota // Denotes the '(' token. - CloseParen // Denotes the ')' token. - OpenBrace // Denotes the '{' token. - CloseBrace // Denotes the '}' token. - HardBreak // Denotes the ';' token. - Assign // Denotes the ':=' token. - Atom // Denotes an alpha-numeric variable. - Slash // Denotes the '/' token. - Dot // Denotes the '.' token. - SoftBreak // Denotes a new-line. -) - -// A representation of a token in source code. -type Token struct { - Column int // Where the token begins in the source text. - Type Type // What type the token is. - Value string // The value of the token. -} - -func NewOpenParen(column int) *Token { - return &Token{Type: OpenParen, Column: column, Value: "("} -} - -func NewCloseParen(column int) *Token { - return &Token{Type: CloseParen, Column: column, Value: ")"} -} - -func NewOpenBrace(column int) *Token { - return &Token{Type: OpenBrace, Column: column, Value: "{"} -} - -func NewCloseBrace(column int) *Token { - return &Token{Type: CloseBrace, Column: column, Value: "}"} -} - -func NewDot(column int) *Token { - return &Token{Type: Dot, Column: column, Value: "."} -} - -func NewHardBreak(column int) *Token { - return &Token{Type: HardBreak, Column: column, Value: ";"} -} - -func NewAssign(column int) *Token { - return &Token{Type: Assign, Column: column, Value: ":="} -} - -func NewSlash(column int) *Token { - return &Token{Type: Slash, Column: column, Value: "\\"} -} - -func NewAtom(name string, column int) *Token { - return &Token{Type: Atom, Column: column, Value: name} -} - -func NewSoftBreak(column int) *Token { - return &Token{Type: SoftBreak, Column: column, Value: "\\n"} -} - -func Name(typ Type) string { - switch typ { - case OpenParen: - return "(" - case CloseParen: - return ")" - case Slash: - return "\\" - case Dot: - return "." - case Atom: - return "ATOM" - case SoftBreak: - return "\\n" - case HardBreak: - return ";" - default: - panic(fmt.Errorf("unknown token type %v", typ)) - } -} - -func (t Token) Name() string { - return Name(t.Type) -} -- 2.49.1 From ca1bb2ffa8fdc9bddced2aa9de6bd01073d98b30 Mon Sep 17 00:00:00 2001 From: "M.V. Hutz" Date: Thu, 5 Feb 2026 13:37:57 -0500 Subject: [PATCH 05/14] feat: new system --- cmd/lambda/lambda.go | 61 ++++++++++------------- cmd/lambda/lambda_test.go | 85 -------------------------------- cmd/lambda/registry.go | 6 +-- internal/cli/codec.go | 58 +++++++++++++--------- internal/registry/converter.go | 27 ++++++++++ internal/registry/registry.go | 85 ++++++++++++++++++++++++++++++-- pkg/engine/normalorder/engine.go | 10 ++-- 7 files changed, 176 insertions(+), 156 deletions(-) delete mode 100644 cmd/lambda/lambda_test.go create mode 100644 internal/registry/converter.go diff --git a/cmd/lambda/lambda.go b/cmd/lambda/lambda.go index 7722471..a85cb28 100644 --- a/cmd/lambda/lambda.go +++ b/cmd/lambda/lambda.go @@ -1,14 +1,11 @@ package main import ( + "fmt" "os" "git.maximhutz.com/max/lambda/internal/cli" "git.maximhutz.com/max/lambda/internal/config" - "git.maximhutz.com/max/lambda/internal/plugins" - "git.maximhutz.com/max/lambda/pkg/convert" - "git.maximhutz.com/max/lambda/pkg/normalorder" - "git.maximhutz.com/max/lambda/pkg/saccharine" ) func main() { @@ -20,48 +17,42 @@ func main() { logger.Info("using program arguments", "args", os.Args) logger.Info("parsed CLI options", "options", options) + r := GetRegistry() + fmt.Println(1) // Get input. input, err := options.Source.Extract() cli.HandleError(err) + fmt.Println(2) // Parse code into syntax tree. - ast, err := saccharine.Parse(input) + repr, err := r.Unmarshal(input, "saccharine") cli.HandleError(err) - logger.Info("parsed syntax tree", "tree", ast) + logger.Info("parsed syntax tree", "tree", repr) + fmt.Println(3) // Compile expression to lambda calculus. - compiled := convert.SaccharineToLambda(ast) - logger.Info("compiled λ expression", "tree", compiled.String()) - + compiled, err := r.ConvertTo(repr, "lambda") + cli.HandleError(err) + logger.Info("compiled λ expression", "tree", compiled) + fmt.Println(4) // Create reducer with the compiled expression. - runtime := normalorder.NewRuntime(compiled) - - // If the user selected to track CPU performance, attach a profiler. - if options.Profile != "" { - plugins.NewPerformance(options.Profile, runtime) - } - - // If the user selected to produce a step-by-step explanation, attach an - // observer. - if options.Explanation { - plugins.NewExplanation(runtime) - } - - // If the user opted to track statistics, attach a tracker. - if options.Statistics { - plugins.NewStatistics(runtime) - } - - // If the user selected for verbose debug logs, attach a reduction tracker. - if options.Verbose { - plugins.NewLogs(logger, runtime) - } + engine, err := r.GetDefaultEngine("lambda") + cli.HandleError(err) + fmt.Println(5) + err = engine.Set(compiled) + cli.HandleError(err) // Run reduction. - runtime.Run() - + for engine.Step(1) { + } + fmt.Println(6) // Return the final reduced result. - result := runtime.Expression().String() - err = options.Destination.Write(result) + result, err := engine.Get() + cli.HandleError(err) + fmt.Println(7, result) + output, err := r.Marshal(result) + cli.HandleError(err) + fmt.Println(8) + err = options.Destination.Write(output) cli.HandleError(err) } diff --git a/cmd/lambda/lambda_test.go b/cmd/lambda/lambda_test.go deleted file mode 100644 index cbca46b..0000000 --- a/cmd/lambda/lambda_test.go +++ /dev/null @@ -1,85 +0,0 @@ -package main - -import ( - "os" - "path/filepath" - "strings" - "testing" - - "git.maximhutz.com/max/lambda/pkg/convert" - "git.maximhutz.com/max/lambda/pkg/normalorder" - "git.maximhutz.com/max/lambda/pkg/saccharine" - "github.com/stretchr/testify/assert" -) - -// Helper function to run a single sample through the lambda runtime. -func runSample(samplePath string) (string, error) { - // Read the sample file. - input, err := os.ReadFile(samplePath) - if err != nil { - return "", err - } - - // Parse code into syntax tree. - ast, err := saccharine.Parse(string(input)) - if err != nil { - return "", err - } - - // Compile expression to lambda calculus. - compiled := convert.SaccharineToLambda(ast) - - // Create and run the reducer. - reducer := normalorder.NewRuntime(compiled) - reducer.Run() - - return reducer.Expression().String() + "\n", nil -} - -// Test that all samples produce expected output. -func TestSamplesValidity(t *testing.T) { - // Discover all .test files in the tests directory. - testFiles, err := filepath.Glob("../../tests/*.test") - assert.NoError(t, err, "Failed to read tests directory.") - assert.NotEmpty(t, testFiles, "No '*.test' files found in directory.") - - for _, testPath := range testFiles { - // Build expected file path. - expectedPath := strings.TrimSuffix(testPath, filepath.Ext(testPath)) + ".expected" - - name := strings.TrimSuffix(filepath.Base(testPath), filepath.Ext(testPath)) - - t.Run(name, func(t *testing.T) { - // Run the sample and capture output. - actual, err := runSample(testPath) - assert.NoError(t, err, "Failed to run sample.") - - // Read expected output. - expectedBytes, err := os.ReadFile(expectedPath) - assert.NoError(t, err, "Failed to read expected output.") - expected := string(expectedBytes) - - // Compare outputs. - assert.Equal(t, expected, actual, "Output does not match expected.") - }) - } -} - -// Benchmark all samples using sub-benchmarks. -func BenchmarkSamples(b *testing.B) { - // Discover all .test files in the tests directory. - testFiles, err := filepath.Glob("../../tests/*.test") - assert.NoError(b, err, "Failed to read tests directory.") - assert.NotEmpty(b, testFiles, "No '*.test' files found in directory.") - - for _, path := range testFiles { - name := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)) - - b.Run(name, func(b *testing.B) { - for b.Loop() { - _, err := runSample(path) - assert.NoError(b, err, "Failed to run sample.") - } - }) - } -} diff --git a/cmd/lambda/registry.go b/cmd/lambda/registry.go index 773afc0..88fba6f 100644 --- a/cmd/lambda/registry.go +++ b/cmd/lambda/registry.go @@ -9,14 +9,14 @@ import ( "git.maximhutz.com/max/lambda/pkg/saccharine" ) -func MakeRegistry() *registry.Registry { +func GetRegistry() *registry.Registry { r := registry.New() // Codecs - r.AddCodec(cli.ConvertCodec(convert.Saccharine2Lambda{}, "saccharine", "lambda")) + r.AddConversions(cli.ConvertCodec(convert.Saccharine2Lambda{}, "saccharine", "lambda")...) // Engines - r.AddEngine(cli.ConvertEngine(normalorder.Engine{}, "normalorder", "lambda")) + r.AddEngine(cli.ConvertEngine(&normalorder.Engine{}, "normalorder", "lambda")) // Marshalers r.AddMarshaler(cli.ConvertMarshaler(lambda.Marshaler{}, "lambda")) diff --git a/internal/cli/codec.go b/internal/cli/codec.go index 6f06f29..5fb0db1 100644 --- a/internal/cli/codec.go +++ b/internal/cli/codec.go @@ -6,33 +6,19 @@ import ( "git.maximhutz.com/max/lambda/pkg/codec" ) -type Codec interface { - codec.Codec[Repr, Repr] - +type Conversion interface { InType() string OutType() string + + Run(Repr) (Repr, error) } -type convertedCodec[T, U any] struct { +type forwardCodec[T, U any] struct { codec codec.Codec[T, U] inType, outType string } -func (c convertedCodec[T, U]) Decode(r Repr) (Repr, error) { - u, ok := r.Data().(U) - if !ok { - return nil, fmt.Errorf("could not parse '%v' as '%s'", r, c.inType) - } - - t, err := c.codec.Decode(u) - if err != nil { - return nil, err - } - - return NewRepr(c.outType, t), nil -} - -func (c convertedCodec[T, U]) Encode(r Repr) (Repr, error) { +func (c forwardCodec[T, U]) Run(r Repr) (Repr, error) { t, ok := r.Data().(T) if !ok { return nil, fmt.Errorf("could not parse '%v' as '%s'", t, c.outType) @@ -46,10 +32,36 @@ func (c convertedCodec[T, U]) Encode(r Repr) (Repr, error) { return NewRepr(c.inType, u), nil } -func (c convertedCodec[T, U]) InType() string { return c.inType } +func (c forwardCodec[T, U]) InType() string { return c.inType } -func (c convertedCodec[T, U]) OutType() string { return c.outType } +func (c forwardCodec[T, U]) OutType() string { return c.outType } -func ConvertCodec[T, U any](e codec.Codec[T, U], inType, outType string) Codec { - return convertedCodec[T, U]{e, inType, outType} +type backwardCodec[T, U any] struct { + codec codec.Codec[T, U] + inType, outType string +} + +func (c backwardCodec[T, U]) Run(r Repr) (Repr, error) { + u, ok := r.Data().(U) + if !ok { + return nil, fmt.Errorf("could not parse '%v' as '%s'", r, c.inType) + } + + t, err := c.codec.Decode(u) + if err != nil { + return nil, err + } + + return NewRepr(c.outType, t), nil +} + +func (c backwardCodec[T, U]) InType() string { return c.outType } + +func (c backwardCodec[T, U]) OutType() string { return c.inType } + +func ConvertCodec[T, U any](e codec.Codec[T, U], inType, outType string) []Conversion { + return []Conversion{ + forwardCodec[T, U]{e, inType, outType}, + backwardCodec[T, U]{e, inType, outType}, + } } diff --git a/internal/registry/converter.go b/internal/registry/converter.go new file mode 100644 index 0000000..3c29199 --- /dev/null +++ b/internal/registry/converter.go @@ -0,0 +1,27 @@ +package registry + +import ( + "git.maximhutz.com/max/lambda/internal/cli" +) + +type Converter struct { + data map[string][]cli.Conversion +} + +func NewConverter() *Converter { + return &Converter{data: map[string][]cli.Conversion{}} +} + +func (g *Converter) Add(c cli.Conversion) { + conversionsFromIn, ok := g.data[c.InType()] + if !ok { + conversionsFromIn = []cli.Conversion{} + } + + conversionsFromIn = append(conversionsFromIn, c) + g.data[c.InType()] = conversionsFromIn +} + +func (g *Converter) ConversionsFrom(t string) []cli.Conversion { + return g.data[t] +} diff --git a/internal/registry/registry.go b/internal/registry/registry.go index de6256a..624023a 100644 --- a/internal/registry/registry.go +++ b/internal/registry/registry.go @@ -8,20 +8,23 @@ import ( type Registry struct { marshalers map[string]cli.Marshaler - codecs []cli.Codec + converter *Converter engines map[string]cli.Engine } func New() *Registry { return &Registry{ marshalers: map[string]cli.Marshaler{}, - codecs: []cli.Codec{}, + converter: NewConverter(), engines: map[string]cli.Engine{}, } } -func (r *Registry) AddCodec(c cli.Codec) error { - r.codecs = append(r.codecs, c) +func (r *Registry) AddConversions(conversions ...cli.Conversion) error { + for _, conversion := range conversions { + r.converter.Add(conversion) + } + return nil } @@ -52,8 +55,31 @@ func (r *Registry) GetEngine(name string) (cli.Engine, error) { return e, nil } +func (r *Registry) GetDefaultEngine(id string) (cli.Engine, error) { + for _, engine := range r.engines { + if engine.InType() == id { + return engine, nil + } + } + + return nil, fmt.Errorf("no engine for '%s'", id) +} + func (r *Registry) ConvertTo(repr cli.Repr, outType string) (cli.Repr, error) { - panic("") + path, err := r.ConversionPath(repr.Id(), outType) + if err != nil { + return nil, err + } + + result := repr + for _, conversion := range path { + result, err = conversion.Run(result) + if err != nil { + return nil, fmt.Errorf("converting '%s' to '%s': %w", conversion.InType(), conversion.OutType(), err) + } + } + + return result, err } func (r *Registry) Marshal(repr cli.Repr) (string, error) { @@ -73,3 +99,52 @@ func (r *Registry) Unmarshal(s string, outType string) (cli.Repr, error) { return m.Decode(s) } + +func reverse[T any](list []T) []T { + if list == nil { + return list + } + + reversed := []T{} + + for i := len(list) - 1; i >= 0; i-- { + reversed = append(reversed, list[i]) + } + + return reversed +} + +func (r *Registry) ConversionPath(from, to string) ([]cli.Conversion, error) { + backtrack := map[string]cli.Conversion{} + iteration := []string{from} + for len(iteration) > 0 { + nextIteration := []string{} + + for _, item := range iteration { + for _, conversion := range r.converter.ConversionsFrom(item) { + if _, ok := backtrack[conversion.OutType()]; ok { + continue + } + + nextIteration = append(nextIteration, conversion.OutType()) + backtrack[conversion.OutType()] = conversion + } + } + + iteration = nextIteration + } + + reversedPath := []cli.Conversion{} + current := to + for current != from { + conversion, ok := backtrack[current] + if !ok { + return nil, fmt.Errorf("no valid conversion from '%s' to '%s'", from, to) + } + + reversedPath = append(reversedPath, conversion) + current = conversion.InType() + } + + return reverse(reversedPath), nil +} diff --git a/pkg/engine/normalorder/engine.go b/pkg/engine/normalorder/engine.go index 41eca85..2a09e51 100644 --- a/pkg/engine/normalorder/engine.go +++ b/pkg/engine/normalorder/engine.go @@ -13,19 +13,19 @@ func (e Engine) Get() (lambda.Expression, error) { return e.expr, nil } -func (e Engine) Set(l lambda.Expression) error { +func (e *Engine) Set(l lambda.Expression) error { e.expr = l return nil } -func (e Engine) Step(i int) bool { - var reduced bool - +func (e *Engine) Step(i int) bool { for range i { - e.expr, reduced = ReduceOnce(e.expr) + next, reduced := ReduceOnce(e.expr) if !reduced { return false } + + e.expr = next } return true -- 2.49.1 From dc872d15aeda2692e69a0ba7411b5fe9322e2727 Mon Sep 17 00:00:00 2001 From: "M.V. Hutz" Date: Thu, 5 Feb 2026 13:38:26 -0500 Subject: [PATCH 06/14] style: no debug commands --- cmd/lambda/lambda.go | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/cmd/lambda/lambda.go b/cmd/lambda/lambda.go index a85cb28..2276c3f 100644 --- a/cmd/lambda/lambda.go +++ b/cmd/lambda/lambda.go @@ -1,7 +1,6 @@ package main import ( - "fmt" "os" "git.maximhutz.com/max/lambda/internal/cli" @@ -18,41 +17,38 @@ func main() { logger.Info("parsed CLI options", "options", options) r := GetRegistry() - fmt.Println(1) + // Get input. input, err := options.Source.Extract() cli.HandleError(err) - fmt.Println(2) // Parse code into syntax tree. repr, err := r.Unmarshal(input, "saccharine") cli.HandleError(err) logger.Info("parsed syntax tree", "tree", repr) - fmt.Println(3) // Compile expression to lambda calculus. compiled, err := r.ConvertTo(repr, "lambda") cli.HandleError(err) logger.Info("compiled λ expression", "tree", compiled) - fmt.Println(4) + // Create reducer with the compiled expression. engine, err := r.GetDefaultEngine("lambda") cli.HandleError(err) - fmt.Println(5) err = engine.Set(compiled) cli.HandleError(err) // Run reduction. for engine.Step(1) { } - fmt.Println(6) + // Return the final reduced result. result, err := engine.Get() cli.HandleError(err) - fmt.Println(7, result) + output, err := r.Marshal(result) cli.HandleError(err) - fmt.Println(8) + err = options.Destination.Write(output) cli.HandleError(err) } -- 2.49.1 From 5f6a9f9663583fa81fa89e9c74a844ceb67b6cf2 Mon Sep 17 00:00:00 2001 From: "M.V. Hutz" Date: Thu, 5 Feb 2026 13:40:09 -0500 Subject: [PATCH 07/14] style: codec -> conversion --- internal/cli/{codec.go => conversion.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename internal/cli/{codec.go => conversion.go} (100%) diff --git a/internal/cli/codec.go b/internal/cli/conversion.go similarity index 100% rename from internal/cli/codec.go rename to internal/cli/conversion.go -- 2.49.1 From 22e8a99362689033c73a8c26376bf343524f5ad9 Mon Sep 17 00:00:00 2001 From: "M.V. Hutz" Date: Thu, 5 Feb 2026 14:20:30 -0500 Subject: [PATCH 08/14] feat: engine -> process, engine is now a factory for a proces --- cmd/lambda/lambda.go | 7 +++--- cmd/lambda/registry.go | 8 +++---- internal/cli/engine.go | 38 +++++++---------------------- internal/cli/process.go | 41 ++++++++++++++++++++++++++++++++ internal/registry/registry.go | 17 +++++++++++++ pkg/engine/engine.go | 4 ++++ pkg/engine/normalorder/engine.go | 16 +++++++++---- 7 files changed, 90 insertions(+), 41 deletions(-) create mode 100644 internal/cli/process.go diff --git a/cmd/lambda/lambda.go b/cmd/lambda/lambda.go index 2276c3f..0652123 100644 --- a/cmd/lambda/lambda.go +++ b/cmd/lambda/lambda.go @@ -36,14 +36,15 @@ func main() { engine, err := r.GetDefaultEngine("lambda") cli.HandleError(err) - err = engine.Set(compiled) + process := engine.Load() + err = process.Set(compiled) cli.HandleError(err) // Run reduction. - for engine.Step(1) { + for process.Step(1) { } // Return the final reduced result. - result, err := engine.Get() + result, err := process.Get() cli.HandleError(err) output, err := r.Marshal(result) diff --git a/cmd/lambda/registry.go b/cmd/lambda/registry.go index 88fba6f..5603971 100644 --- a/cmd/lambda/registry.go +++ b/cmd/lambda/registry.go @@ -13,14 +13,14 @@ func GetRegistry() *registry.Registry { r := registry.New() // Codecs - r.AddConversions(cli.ConvertCodec(convert.Saccharine2Lambda{}, "saccharine", "lambda")...) + r.MustAddConversions(cli.ConvertCodec(convert.Saccharine2Lambda{}, "saccharine", "lambda")...) // Engines - r.AddEngine(cli.ConvertEngine(&normalorder.Engine{}, "normalorder", "lambda")) + r.MustAddEngine(cli.ConvertEngine(normalorder.Engine{}, "normalorder", "lambda")) // Marshalers - r.AddMarshaler(cli.ConvertMarshaler(lambda.Marshaler{}, "lambda")) - r.AddMarshaler(cli.ConvertMarshaler(saccharine.Marshaler{}, "saccharine")) + r.MustAddMarshaler(cli.ConvertMarshaler(lambda.Marshaler{}, "lambda")) + r.MustAddMarshaler(cli.ConvertMarshaler(saccharine.Marshaler{}, "saccharine")) return r } diff --git a/internal/cli/engine.go b/internal/cli/engine.go index f0141cd..15f1618 100644 --- a/internal/cli/engine.go +++ b/internal/cli/engine.go @@ -1,14 +1,9 @@ package cli -import ( - "fmt" - - "git.maximhutz.com/max/lambda/pkg/engine" -) +import "git.maximhutz.com/max/lambda/pkg/engine" type Engine interface { - engine.Engine[Repr] - + Load() Process Name() string InType() string } @@ -19,31 +14,14 @@ type convertedEngine[T any] struct { inType string } -func (b convertedEngine[T]) InType() string { return b.inType } +func (e convertedEngine[T]) InType() string { return e.inType } -func (b convertedEngine[T]) Name() string { return b.name } +func (e convertedEngine[T]) Name() string { return e.name } -func (b convertedEngine[T]) Get() (Repr, error) { - s, err := b.engine.Get() - if err != nil { - return nil, err - } - - return NewRepr(b.inType, s), nil +func (e convertedEngine[T]) Load() Process { + return convertedProcess[T]{e.engine.Load(), e.inType} } -func (b convertedEngine[T]) Set(r Repr) error { - if t, ok := r.Data().(T); ok { - return b.engine.Set(t) - } - - return fmt.Errorf("Incorrent format '%s' for engine '%s'.", r.Id(), b.inType) -} - -func (b convertedEngine[T]) Step(i int) bool { - return b.engine.Step(i) -} - -func ConvertEngine[T any](e engine.Engine[T], name string, inType string) Engine { - return convertedEngine[T]{e, name, inType} +func ConvertEngine[T any](e engine.Engine[T], name, inType string) Engine { + return &convertedEngine[T]{e, name, inType} } diff --git a/internal/cli/process.go b/internal/cli/process.go new file mode 100644 index 0000000..f3445f7 --- /dev/null +++ b/internal/cli/process.go @@ -0,0 +1,41 @@ +package cli + +import ( + "fmt" + + "git.maximhutz.com/max/lambda/pkg/engine" +) + +type Process interface { + engine.Process[Repr] + + InType() string +} + +type convertedProcess[T any] struct { + process engine.Process[T] + inType string +} + +func (e convertedProcess[T]) InType() string { return e.inType } + +func (b convertedProcess[T]) Get() (Repr, error) { + s, err := b.process.Get() + if err != nil { + return nil, err + } + + return NewRepr(b.inType, s), nil +} + +func (b convertedProcess[T]) Set(r Repr) error { + if t, ok := r.Data().(T); ok { + return b.process.Set(t) + } + + return fmt.Errorf("Incorrent format '%s' for engine '%s'.", r.Id(), b.inType) +} + +func (b convertedProcess[T]) Step(i int) bool { + return b.process.Step(i) +} diff --git a/internal/registry/registry.go b/internal/registry/registry.go index 624023a..e47d942 100644 --- a/internal/registry/registry.go +++ b/internal/registry/registry.go @@ -27,6 +27,11 @@ func (r *Registry) AddConversions(conversions ...cli.Conversion) error { return nil } +func (r *Registry) MustAddConversions(conversions ...cli.Conversion) { + if err := r.AddConversions(conversions...); err != nil { + panic(err) + } +} func (r *Registry) AddMarshaler(c cli.Marshaler) error { if _, ok := r.marshalers[c.InType()]; ok { @@ -37,6 +42,12 @@ func (r *Registry) AddMarshaler(c cli.Marshaler) error { return nil } +func (r *Registry) MustAddMarshaler(c cli.Marshaler) { + if err := r.AddMarshaler(c); err != nil { + panic(err) + } +} + func (r *Registry) AddEngine(e cli.Engine) error { if _, ok := r.engines[e.Name()]; ok { return fmt.Errorf("engine '%s' already registered", e.Name()) @@ -46,6 +57,12 @@ func (r *Registry) AddEngine(e cli.Engine) error { return nil } +func (r *Registry) MustAddEngine(e cli.Engine) { + if err := r.AddEngine(e); err != nil { + panic(err) + } +} + func (r *Registry) GetEngine(name string) (cli.Engine, error) { e, ok := r.engines[name] if !ok { diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index 22f716d..0fcebf3 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -1,6 +1,10 @@ package engine type Engine[T any] interface { + Load() Process[T] +} + +type Process[T any] interface { Get() (T, error) Set(T) error Step(int) bool diff --git a/pkg/engine/normalorder/engine.go b/pkg/engine/normalorder/engine.go index 2a09e51..fcdd362 100644 --- a/pkg/engine/normalorder/engine.go +++ b/pkg/engine/normalorder/engine.go @@ -5,20 +5,20 @@ import ( "git.maximhutz.com/max/lambda/pkg/lambda" ) -type Engine struct { +type Process struct { expr lambda.Expression } -func (e Engine) Get() (lambda.Expression, error) { +func (e Process) Get() (lambda.Expression, error) { return e.expr, nil } -func (e *Engine) Set(l lambda.Expression) error { +func (e *Process) Set(l lambda.Expression) error { e.expr = l return nil } -func (e *Engine) Step(i int) bool { +func (e *Process) Step(i int) bool { for range i { next, reduced := ReduceOnce(e.expr) if !reduced { @@ -31,4 +31,12 @@ func (e *Engine) Step(i int) bool { return true } +type Engine struct { +} + +func (e Engine) Load() engine.Process[lambda.Expression] { + return &Process{} +} + +var _ engine.Process[lambda.Expression] = (*Process)(nil) var _ engine.Engine[lambda.Expression] = (*Engine)(nil) -- 2.49.1 From 7750d8615f6d4f56667781908d657652529d4255 Mon Sep 17 00:00:00 2001 From: "M.V. Hutz" Date: Thu, 5 Feb 2026 20:41:35 -0500 Subject: [PATCH 09/14] feat: some basic commands --- cmd/lambda/lambda.go | 109 +++++++++++++++++++++---------- cmd/lambda/lambda_convert.go | 100 ++++++++++++++++++++++++++++ cmd/lambda/lambda_engine.go | 19 ++++++ cmd/lambda/lambda_engine_list.go | 26 ++++++++ go.mod | 15 +++++ go.sum | 33 ++++++++++ internal/cli/exit.go | 18 ----- internal/registry/registry.go | 8 ++- 8 files changed, 274 insertions(+), 54 deletions(-) create mode 100644 cmd/lambda/lambda_convert.go create mode 100644 cmd/lambda/lambda_engine.go create mode 100644 cmd/lambda/lambda_engine_list.go delete mode 100644 internal/cli/exit.go diff --git a/cmd/lambda/lambda.go b/cmd/lambda/lambda.go index 0652123..81fcb2b 100644 --- a/cmd/lambda/lambda.go +++ b/cmd/lambda/lambda.go @@ -1,55 +1,94 @@ package main import ( + "fmt" "os" - "git.maximhutz.com/max/lambda/internal/cli" "git.maximhutz.com/max/lambda/internal/config" + "github.com/spf13/cobra" ) -func main() { - // Parse CLI arguments. - options, err := config.FromArgs() - cli.HandleError(err) +func Lambda() *cobra.Command { + cmd := &cobra.Command{ + Use: "lambda", + Short: "Lambda calculus interpreter", + Long: "A lambda calculus interpreter supporting multiple representations.", + RunE: func(cmd *cobra.Command, args []string) error { + // Legacy behavior when no subcommand is given. + options, err := config.FromArgs() + if err != nil { + return err + } - logger := options.GetLogger() - logger.Info("using program arguments", "args", os.Args) - logger.Info("parsed CLI options", "options", options) + logger := options.GetLogger() + logger.Info("using program arguments", "args", os.Args) + logger.Info("parsed CLI options", "options", options) - r := GetRegistry() + r := GetRegistry() - // Get input. - input, err := options.Source.Extract() - cli.HandleError(err) + // Get input. + input, err := options.Source.Extract() + if err != nil { + return err + } - // Parse code into syntax tree. - repr, err := r.Unmarshal(input, "saccharine") - cli.HandleError(err) - logger.Info("parsed syntax tree", "tree", repr) + // Parse code into syntax tree. + repr, err := r.Unmarshal(input, "saccharine") + if err != nil { + return err + } + logger.Info("parsed syntax tree", "tree", repr) - // Compile expression to lambda calculus. - compiled, err := r.ConvertTo(repr, "lambda") - cli.HandleError(err) - logger.Info("compiled λ expression", "tree", compiled) + // Compile expression to lambda calculus. + compiled, err := r.ConvertTo(repr, "lambda") + if err != nil { + return err + } + logger.Info("compiled λ expression", "tree", compiled) - // Create reducer with the compiled expression. - engine, err := r.GetDefaultEngine("lambda") - cli.HandleError(err) + // Create reducer with the compiled expression. + engine, err := r.GetDefaultEngine("lambda") + if err != nil { + return err + } - process := engine.Load() - err = process.Set(compiled) - cli.HandleError(err) - // Run reduction. - for process.Step(1) { + process := engine.Load() + err = process.Set(compiled) + if err != nil { + return err + } + + // Run reduction. + for process.Step(1) { + } + + // Return the final reduced result. + result, err := process.Get() + if err != nil { + return err + } + + output, err := r.Marshal(result) + if err != nil { + return err + } + + return options.Destination.Write(output) + }, } - // Return the final reduced result. - result, err := process.Get() - cli.HandleError(err) + cmd.PersistentFlags().BoolP("verbose", "v", false, "Enable verbose output") - output, err := r.Marshal(result) - cli.HandleError(err) + cmd.AddCommand(LambdaConvert()) + cmd.AddCommand(LambdaEngine()) - err = options.Destination.Write(output) - cli.HandleError(err) + return cmd +} + +func main() { + lambda := Lambda() + if err := lambda.Execute(); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } } diff --git a/cmd/lambda/lambda_convert.go b/cmd/lambda/lambda_convert.go new file mode 100644 index 0000000..c0737c5 --- /dev/null +++ b/cmd/lambda/lambda_convert.go @@ -0,0 +1,100 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "git.maximhutz.com/max/lambda/internal/cli" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// inferReprFromPath returns the repr type based on file extension. +func inferReprFromPath(path string) (string, error) { + switch ext := strings.ToLower(filepath.Ext(path)); ext { + case ".lam", ".lambda": + return "lambda", nil + case ".sac", ".saccharine": + return "saccharine", nil + default: + return "", fmt.Errorf("unknown file extension '%s'", ext) + } +} + +func LambdaConvert() *cobra.Command { + cmd := &cobra.Command{ + Use: "convert ", + Short: "Convert between lambda calculus representations", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + inputPath := args[0] + outputPath := args[1] + + // Infer repr types from extensions. + inputRepr, err := inferReprFromPath(inputPath) + if err != nil { + return fmt.Errorf("input file: %w", err) + } + + outputRepr, err := inferReprFromPath(outputPath) + if err != nil { + return fmt.Errorf("output file: %w", err) + } + + // Read input file. + input, err := os.ReadFile(inputPath) + if err != nil { + return fmt.Errorf("reading input file: %w", err) + } + + r := GetRegistry() + + // Parse input into syntax tree. + repr, err := r.Unmarshal(string(input), inputRepr) + if err != nil { + return fmt.Errorf("parsing input: %w", err) + } + + if viper.GetBool("verbose") { + fmt.Fprintf(os.Stderr, "Parsed %s from %s\n", inputRepr, inputPath) + } + + // Convert to output repr if different. + var result cli.Repr + if inputRepr != outputRepr { + result, err = r.ConvertTo(repr, outputRepr) + if err != nil { + return fmt.Errorf("converting %s to %s: %w", inputRepr, outputRepr, err) + } + + if viper.GetBool("verbose") { + fmt.Fprintf(os.Stderr, "Converted to %s\n", outputRepr) + } + } else { + result = repr + } + + // Marshal output. + output, err := r.Marshal(result) + if err != nil { + return fmt.Errorf("marshaling output: %w", err) + } + + // Write output file. + err = os.WriteFile(outputPath, []byte(output), 0644) + if err != nil { + return fmt.Errorf("writing output file: %w", err) + } + + if viper.GetBool("verbose") { + fmt.Fprintf(os.Stderr, "Wrote %s to %s\n", outputRepr, outputPath) + } + + return nil + }, + } + + return cmd +} diff --git a/cmd/lambda/lambda_engine.go b/cmd/lambda/lambda_engine.go new file mode 100644 index 0000000..b8a2f35 --- /dev/null +++ b/cmd/lambda/lambda_engine.go @@ -0,0 +1,19 @@ +package main + +import ( + "github.com/spf13/cobra" +) + +func LambdaEngine() *cobra.Command { + cmd := &cobra.Command{ + Use: "engine", + Short: "Information about available engines", + RunE: func(cmd *cobra.Command, args []string) error { + return cmd.Help() + }, + } + + cmd.AddCommand(LambdaEngineList()) + + return cmd +} diff --git a/cmd/lambda/lambda_engine_list.go b/cmd/lambda/lambda_engine_list.go new file mode 100644 index 0000000..338098d --- /dev/null +++ b/cmd/lambda/lambda_engine_list.go @@ -0,0 +1,26 @@ +package main + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +func LambdaEngineList() *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Short: "List available engines", + RunE: func(cmd *cobra.Command, args []string) error { + r := GetRegistry() + + for engine := range r.ListEngines() { + fmt.Println(engine.Name()) + } + + return nil + }, + } + + return cmd +} diff --git a/go.mod b/go.mod index 241b0b4..786df60 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,21 @@ require github.com/stretchr/testify v1.11.1 require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/cobra v1.10.2 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/spf13/viper v1.21.0 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/sys v0.29.0 // indirect + golang.org/x/text v0.28.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index cc8b3f4..ee8f6cf 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,42 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= +github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/cli/exit.go b/internal/cli/exit.go deleted file mode 100644 index 371ecee..0000000 --- a/internal/cli/exit.go +++ /dev/null @@ -1,18 +0,0 @@ -// Package "cli" provides miscellaneous helper functions. -package cli - -import ( - "fmt" - "os" -) - -// A helper function to handle errors in the program. If it is given an error, -// the program will exist, and print the error. -func HandleError(err error) { - if err == nil { - return - } - - fmt.Fprintln(os.Stderr, "ERROR:", err) - os.Exit(1) -} diff --git a/internal/registry/registry.go b/internal/registry/registry.go index e47d942..705b405 100644 --- a/internal/registry/registry.go +++ b/internal/registry/registry.go @@ -2,6 +2,8 @@ package registry import ( "fmt" + "iter" + "maps" "git.maximhutz.com/max/lambda/internal/cli" ) @@ -63,7 +65,7 @@ func (r *Registry) MustAddEngine(e cli.Engine) { } } -func (r *Registry) GetEngine(name string) (cli.Engine, error) { +func (r Registry) GetEngine(name string) (cli.Engine, error) { e, ok := r.engines[name] if !ok { return nil, fmt.Errorf("engine '%s' not found", name) @@ -72,6 +74,10 @@ func (r *Registry) GetEngine(name string) (cli.Engine, error) { return e, nil } +func (r Registry) ListEngines() iter.Seq[cli.Engine] { + return maps.Values(r.engines) +} + func (r *Registry) GetDefaultEngine(id string) (cli.Engine, error) { for _, engine := range r.engines { if engine.InType() == id { -- 2.49.1 From 36c0eede06012def8c48c5a7ff6039f781f7720a Mon Sep 17 00:00:00 2001 From: "M.V. Hutz" Date: Thu, 5 Feb 2026 20:55:07 -0500 Subject: [PATCH 10/14] fix: convert style --- cmd/lambda/lambda.go | 5 ++-- cmd/lambda/lambda_convert.go | 58 +++++++++++++++--------------------- go.mod | 18 +---------- go.sum | 31 ------------------- 4 files changed, 28 insertions(+), 84 deletions(-) diff --git a/cmd/lambda/lambda.go b/cmd/lambda/lambda.go index 81fcb2b..49151fa 100644 --- a/cmd/lambda/lambda.go +++ b/cmd/lambda/lambda.go @@ -4,8 +4,9 @@ import ( "fmt" "os" - "git.maximhutz.com/max/lambda/internal/config" "github.com/spf13/cobra" + + "git.maximhutz.com/max/lambda/internal/config" ) func Lambda() *cobra.Command { @@ -77,7 +78,7 @@ func Lambda() *cobra.Command { }, } - cmd.PersistentFlags().BoolP("verbose", "v", false, "Enable verbose output") + cmd.Flags().BoolP("verbose", "v", false, "Enable verbose output") cmd.AddCommand(LambdaConvert()) cmd.AddCommand(LambdaEngine()) diff --git a/cmd/lambda/lambda_convert.go b/cmd/lambda/lambda_convert.go index c0737c5..996e30e 100644 --- a/cmd/lambda/lambda_convert.go +++ b/cmd/lambda/lambda_convert.go @@ -6,17 +6,15 @@ import ( "path/filepath" "strings" - "git.maximhutz.com/max/lambda/internal/cli" "github.com/spf13/cobra" - "github.com/spf13/viper" ) // inferReprFromPath returns the repr type based on file extension. func inferReprFromPath(path string) (string, error) { switch ext := strings.ToLower(filepath.Ext(path)); ext { - case ".lam", ".lambda": + case ".lambda", ".lam", ".lc": return "lambda", nil - case ".sac", ".saccharine": + case ".saccharine", ".sch": return "saccharine", nil default: return "", fmt.Errorf("unknown file extension '%s'", ext) @@ -24,23 +22,29 @@ func inferReprFromPath(path string) (string, error) { } func LambdaConvert() *cobra.Command { + var inputReprFlag, outputReprFlag string + cmd := &cobra.Command{ Use: "convert ", Short: "Convert between lambda calculus representations", Args: cobra.ExactArgs(2), - RunE: func(cmd *cobra.Command, args []string) error { - inputPath := args[0] - outputPath := args[1] + RunE: func(_ *cobra.Command, args []string) error { + var err error + inputPath, outputPath := args[0], args[1] - // Infer repr types from extensions. - inputRepr, err := inferReprFromPath(inputPath) - if err != nil { - return fmt.Errorf("input file: %w", err) + // Use flag if provided, otherwise infer from extension. + inputRepr := inputReprFlag + if inputRepr == "" { + if inputRepr, err = inferReprFromPath(inputPath); err != nil { + return fmt.Errorf("input file: %w", err) + } } - outputRepr, err := inferReprFromPath(outputPath) - if err != nil { - return fmt.Errorf("output file: %w", err) + outputRepr := outputReprFlag + if outputRepr == "" { + if outputRepr, err = inferReprFromPath(outputPath); err != nil { + return fmt.Errorf("output file: %w", err) + } } // Read input file. @@ -57,23 +61,10 @@ func LambdaConvert() *cobra.Command { return fmt.Errorf("parsing input: %w", err) } - if viper.GetBool("verbose") { - fmt.Fprintf(os.Stderr, "Parsed %s from %s\n", inputRepr, inputPath) - } - // Convert to output repr if different. - var result cli.Repr - if inputRepr != outputRepr { - result, err = r.ConvertTo(repr, outputRepr) - if err != nil { - return fmt.Errorf("converting %s to %s: %w", inputRepr, outputRepr, err) - } - - if viper.GetBool("verbose") { - fmt.Fprintf(os.Stderr, "Converted to %s\n", outputRepr) - } - } else { - result = repr + result, err := r.ConvertTo(repr, outputRepr) + if err != nil { + return fmt.Errorf("converting %s to %s: %w", inputRepr, outputRepr, err) } // Marshal output. @@ -88,13 +79,12 @@ func LambdaConvert() *cobra.Command { return fmt.Errorf("writing output file: %w", err) } - if viper.GetBool("verbose") { - fmt.Fprintf(os.Stderr, "Wrote %s to %s\n", outputRepr, outputPath) - } - return nil }, } + cmd.Flags().StringVarP(&inputReprFlag, "input", "i", "", "Input representation (inferred from extension if unset)") + cmd.Flags().StringVarP(&outputReprFlag, "output", "o", "", "Output representation (inferred from extension if unset)") + return cmd } diff --git a/go.mod b/go.mod index 786df60..3633877 100644 --- a/go.mod +++ b/go.mod @@ -2,25 +2,9 @@ module git.maximhutz.com/max/lambda go 1.25.5 -require github.com/stretchr/testify v1.11.1 +require github.com/spf13/cobra v1.10.2 require ( - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/pelletier/go-toml/v2 v2.2.4 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/sagikazarmark/locafero v0.11.0 // indirect - github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect - github.com/spf13/afero v1.15.0 // indirect - github.com/spf13/cast v1.10.0 // indirect - github.com/spf13/cobra v1.10.2 // indirect github.com/spf13/pflag v1.0.10 // indirect - github.com/spf13/viper v1.21.0 // indirect - github.com/subosito/gotenv v1.6.0 // indirect - go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/sys v0.29.0 // indirect - golang.org/x/text v0.28.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index ee8f6cf..ef5d78d 100644 --- a/go.sum +++ b/go.sum @@ -1,42 +1,11 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= -github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= -github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= -github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= -github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= -github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= -github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= -github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= -github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= -github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= -github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= -golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -- 2.49.1 From 53042ea26d0bfeb92d45c374510a4c7cc84992b7 Mon Sep 17 00:00:00 2001 From: "M.V. Hutz" Date: Thu, 5 Feb 2026 20:57:30 -0500 Subject: [PATCH 11/14] fix: silence usage, and no double errors --- cmd/lambda/lambda.go | 2 -- cmd/lambda/lambda_convert.go | 7 ++++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/cmd/lambda/lambda.go b/cmd/lambda/lambda.go index 49151fa..7d7982d 100644 --- a/cmd/lambda/lambda.go +++ b/cmd/lambda/lambda.go @@ -1,7 +1,6 @@ package main import ( - "fmt" "os" "github.com/spf13/cobra" @@ -89,7 +88,6 @@ func Lambda() *cobra.Command { func main() { lambda := Lambda() if err := lambda.Execute(); err != nil { - fmt.Fprintln(os.Stderr, err) os.Exit(1) } } diff --git a/cmd/lambda/lambda_convert.go b/cmd/lambda/lambda_convert.go index 996e30e..a8ff217 100644 --- a/cmd/lambda/lambda_convert.go +++ b/cmd/lambda/lambda_convert.go @@ -25,9 +25,10 @@ func LambdaConvert() *cobra.Command { var inputReprFlag, outputReprFlag string cmd := &cobra.Command{ - Use: "convert ", - Short: "Convert between lambda calculus representations", - Args: cobra.ExactArgs(2), + Use: "convert ", + Short: "Convert between lambda calculus representations", + Args: cobra.ExactArgs(2), + SilenceUsage: true, RunE: func(_ *cobra.Command, args []string) error { var err error inputPath, outputPath := args[0], args[1] -- 2.49.1 From b47a3a3acb279f6690607b7482eb72a62749ab1f Mon Sep 17 00:00:00 2001 From: "M.V. Hutz" Date: Thu, 5 Feb 2026 22:28:53 -0500 Subject: [PATCH 12/14] feat: better convert usage --- cmd/lambda/lambda_convert.go | 11 +++++++---- internal/cli/conversion.go | 8 ++++---- internal/cli/marshaler.go | 5 ++++- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/cmd/lambda/lambda_convert.go b/cmd/lambda/lambda_convert.go index a8ff217..b2fd20c 100644 --- a/cmd/lambda/lambda_convert.go +++ b/cmd/lambda/lambda_convert.go @@ -25,11 +25,14 @@ func LambdaConvert() *cobra.Command { var inputReprFlag, outputReprFlag string cmd := &cobra.Command{ - Use: "convert ", + Use: "convert ", Short: "Convert between lambda calculus representations", - Args: cobra.ExactArgs(2), SilenceUsage: true, - RunE: func(_ *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) != 2 { + return cmd.Help() + } + var err error inputPath, outputPath := args[0], args[1] @@ -71,7 +74,7 @@ func LambdaConvert() *cobra.Command { // Marshal output. output, err := r.Marshal(result) if err != nil { - return fmt.Errorf("marshaling output: %w", err) + return fmt.Errorf("unmarshaling output: %w", err) } // Write output file. diff --git a/internal/cli/conversion.go b/internal/cli/conversion.go index 5fb0db1..d198f8e 100644 --- a/internal/cli/conversion.go +++ b/internal/cli/conversion.go @@ -21,7 +21,7 @@ type forwardCodec[T, U any] struct { func (c forwardCodec[T, U]) Run(r Repr) (Repr, error) { t, ok := r.Data().(T) if !ok { - return nil, fmt.Errorf("could not parse '%v' as '%s'", t, c.outType) + return nil, fmt.Errorf("could not parse '%v' as '%s'", t, c.inType) } u, err := c.codec.Encode(t) @@ -29,7 +29,7 @@ func (c forwardCodec[T, U]) Run(r Repr) (Repr, error) { return nil, err } - return NewRepr(c.inType, u), nil + return NewRepr(c.outType, u), nil } func (c forwardCodec[T, U]) InType() string { return c.inType } @@ -44,7 +44,7 @@ type backwardCodec[T, U any] struct { func (c backwardCodec[T, U]) Run(r Repr) (Repr, error) { u, ok := r.Data().(U) if !ok { - return nil, fmt.Errorf("could not parse '%v' as '%s'", r, c.inType) + return nil, fmt.Errorf("could not parse '%v' as '%s'", r, c.outType) } t, err := c.codec.Decode(u) @@ -52,7 +52,7 @@ func (c backwardCodec[T, U]) Run(r Repr) (Repr, error) { return nil, err } - return NewRepr(c.outType, t), nil + return NewRepr(c.inType, t), nil } func (c backwardCodec[T, U]) InType() string { return c.outType } diff --git a/internal/cli/marshaler.go b/internal/cli/marshaler.go index e10f7ee..a212417 100644 --- a/internal/cli/marshaler.go +++ b/internal/cli/marshaler.go @@ -2,6 +2,7 @@ package cli import ( "fmt" + "reflect" "git.maximhutz.com/max/lambda/pkg/codec" ) @@ -29,7 +30,9 @@ func (c convertedMarshaler[T]) Decode(s string) (Repr, error) { func (c convertedMarshaler[T]) Encode(r Repr) (string, error) { t, ok := r.Data().(T) if !ok { - return "", fmt.Errorf("could not parse '%v' as 'string'", t) + dataType := reflect.TypeOf(r.Data()) + allowedType := reflect.TypeFor[T]() + return "", fmt.Errorf("marshaler for '%s' cannot parse '%s'", allowedType, dataType) } return c.codec.Encode(t) -- 2.49.1 From d21dde40e9a03d824891d04fc48dbcc59c097f6e Mon Sep 17 00:00:00 2001 From: "M.V. Hutz" Date: Thu, 5 Feb 2026 22:55:33 -0500 Subject: [PATCH 13/14] feat: move lambda reduction to "lambda reduce" --- cmd/lambda/lambda.go | 66 +-------------------- cmd/lambda/lambda_convert.go | 4 +- cmd/lambda/lambda_reduce.go | 109 +++++++++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+), 66 deletions(-) create mode 100644 cmd/lambda/lambda_reduce.go diff --git a/cmd/lambda/lambda.go b/cmd/lambda/lambda.go index 7d7982d..e8effcb 100644 --- a/cmd/lambda/lambda.go +++ b/cmd/lambda/lambda.go @@ -4,8 +4,6 @@ import ( "os" "github.com/spf13/cobra" - - "git.maximhutz.com/max/lambda/internal/config" ) func Lambda() *cobra.Command { @@ -14,73 +12,13 @@ func Lambda() *cobra.Command { Short: "Lambda calculus interpreter", Long: "A lambda calculus interpreter supporting multiple representations.", RunE: func(cmd *cobra.Command, args []string) error { - // Legacy behavior when no subcommand is given. - options, err := config.FromArgs() - if err != nil { - return err - } - - logger := options.GetLogger() - logger.Info("using program arguments", "args", os.Args) - logger.Info("parsed CLI options", "options", options) - - r := GetRegistry() - - // Get input. - input, err := options.Source.Extract() - if err != nil { - return err - } - - // Parse code into syntax tree. - repr, err := r.Unmarshal(input, "saccharine") - if err != nil { - return err - } - logger.Info("parsed syntax tree", "tree", repr) - - // Compile expression to lambda calculus. - compiled, err := r.ConvertTo(repr, "lambda") - if err != nil { - return err - } - logger.Info("compiled λ expression", "tree", compiled) - - // Create reducer with the compiled expression. - engine, err := r.GetDefaultEngine("lambda") - if err != nil { - return err - } - - process := engine.Load() - err = process.Set(compiled) - if err != nil { - return err - } - - // Run reduction. - for process.Step(1) { - } - - // Return the final reduced result. - result, err := process.Get() - if err != nil { - return err - } - - output, err := r.Marshal(result) - if err != nil { - return err - } - - return options.Destination.Write(output) + return cmd.Help() }, } - cmd.Flags().BoolP("verbose", "v", false, "Enable verbose output") - cmd.AddCommand(LambdaConvert()) cmd.AddCommand(LambdaEngine()) + cmd.AddCommand(LambdaReduce()) return cmd } diff --git a/cmd/lambda/lambda_convert.go b/cmd/lambda/lambda_convert.go index b2fd20c..bd7ad9e 100644 --- a/cmd/lambda/lambda_convert.go +++ b/cmd/lambda/lambda_convert.go @@ -87,8 +87,8 @@ func LambdaConvert() *cobra.Command { }, } - cmd.Flags().StringVarP(&inputReprFlag, "input", "i", "", "Input representation (inferred from extension if unset)") - cmd.Flags().StringVarP(&outputReprFlag, "output", "o", "", "Output representation (inferred from extension if unset)") + cmd.Flags().StringVar(&inputReprFlag, "from", "", "Input representation (inferred from extension if unset)") + cmd.Flags().StringVar(&outputReprFlag, "to", "", "Output representation (inferred from extension if unset)") return cmd } diff --git a/cmd/lambda/lambda_reduce.go b/cmd/lambda/lambda_reduce.go new file mode 100644 index 0000000..a6f1039 --- /dev/null +++ b/cmd/lambda/lambda_reduce.go @@ -0,0 +1,109 @@ +package main + +import ( + "fmt" + + "github.com/spf13/cobra" + + "git.maximhutz.com/max/lambda/internal/cli" + "git.maximhutz.com/max/lambda/internal/config" +) + +func LambdaReduce() *cobra.Command { + var inputReprFlag string + var engineFlag string + + cmd := &cobra.Command{ + Use: "reduce ", + Short: "Reduce a lambda calculus expression", + SilenceUsage: true, + Aliases: []string{"run"}, + RunE: func(cmd *cobra.Command, args []string) error { + var err error + if len(args) != 1 { + return cmd.Help() + } + + inputPath := args[0] + + // Get input source. + var source config.Source + if inputPath == "-" { + source = config.StdinSource{} + } else { + source = config.FileSource{Path: inputPath} + } + + destination := config.StdoutDestination{} + + r := GetRegistry() + + // Get input. + input, err := source.Extract() + if err != nil { + return err + } + + // Use flag if provided, otherwise infer from extension. + inputRepr := inputReprFlag + if inputRepr == "" { + if inputRepr, err = inferReprFromPath(inputPath); err != nil { + return fmt.Errorf("input file: %w", err) + } + } + + // Find engine. + var engine cli.Engine + if engineFlag == "" { + if engine, err = r.GetDefaultEngine(inputRepr); err != nil { + return err + } + } else { + if engine, err = r.GetEngine(engineFlag); err != nil { + return err + } + } + + // Parse code into syntax tree. + repr, err := r.Unmarshal(input, inputRepr) + if err != nil { + return err + } + + // Compile expression to lambda calculus. + compiled, err := r.ConvertTo(repr, "lambda") + if err != nil { + return err + } + + // Create process. + process := engine.Load() + err = process.Set(compiled) + if err != nil { + return err + } + + // Run reduction. + for process.Step(1) { + } + + // Return the final reduced result. + result, err := process.Get() + if err != nil { + return err + } + + output, err := r.Marshal(result) + if err != nil { + return err + } + + return destination.Write(output) + }, + } + + cmd.Flags().StringVar(&inputReprFlag, "from", "", "Input representation (inferred from extension if unset)") + cmd.Flags().StringVarP(&engineFlag, "engine", "e", "", "Reduction engine (inferred from '--input' if unset)") + + return cmd +} -- 2.49.1 From da10c46af537e29af81b3d5a35e634a7575777de Mon Sep 17 00:00:00 2001 From: "M.V. Hutz" Date: Thu, 5 Feb 2026 23:01:56 -0500 Subject: [PATCH 14/14] fix: no unneeded args --- cmd/lambda/lambda.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/lambda/lambda.go b/cmd/lambda/lambda.go index e8effcb..60070fc 100644 --- a/cmd/lambda/lambda.go +++ b/cmd/lambda/lambda.go @@ -11,7 +11,7 @@ func Lambda() *cobra.Command { Use: "lambda", Short: "Lambda calculus interpreter", Long: "A lambda calculus interpreter supporting multiple representations.", - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, _ []string) error { return cmd.Help() }, } -- 2.49.1