From ca1bb2ffa8fdc9bddced2aa9de6bd01073d98b30 Mon Sep 17 00:00:00 2001 From: "M.V. Hutz" Date: Thu, 5 Feb 2026 13:37:57 -0500 Subject: [PATCH] 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