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/cmd/lambda/lambda.go b/cmd/lambda/lambda.go index 7722471..60070fc 100644 --- a/cmd/lambda/lambda.go +++ b/cmd/lambda/lambda.go @@ -3,65 +3,29 @@ package main import ( "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" + "github.com/spf13/cobra" ) -func main() { - // Parse CLI arguments. - options, err := config.FromArgs() - cli.HandleError(err) - - logger := options.GetLogger() - logger.Info("using program arguments", "args", os.Args) - logger.Info("parsed CLI options", "options", options) - - // Get input. - input, err := options.Source.Extract() - cli.HandleError(err) - - // Parse code into syntax tree. - ast, err := saccharine.Parse(input) - cli.HandleError(err) - logger.Info("parsed syntax tree", "tree", ast) - - // Compile expression to lambda calculus. - compiled := convert.SaccharineToLambda(ast) - logger.Info("compiled λ expression", "tree", compiled.String()) - - // 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) +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, _ []string) error { + return cmd.Help() + }, } - // If the user selected to produce a step-by-step explanation, attach an - // observer. - if options.Explanation { - plugins.NewExplanation(runtime) - } + cmd.AddCommand(LambdaConvert()) + cmd.AddCommand(LambdaEngine()) + cmd.AddCommand(LambdaReduce()) - // 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) - } - - // Run reduction. - runtime.Run() - - // Return the final reduced result. - result := runtime.Expression().String() - err = options.Destination.Write(result) - cli.HandleError(err) + return cmd +} + +func main() { + lambda := Lambda() + if err := lambda.Execute(); err != nil { + os.Exit(1) + } } diff --git a/cmd/lambda/lambda_convert.go b/cmd/lambda/lambda_convert.go new file mode 100644 index 0000000..bd7ad9e --- /dev/null +++ b/cmd/lambda/lambda_convert.go @@ -0,0 +1,94 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/spf13/cobra" +) + +// inferReprFromPath returns the repr type based on file extension. +func inferReprFromPath(path string) (string, error) { + switch ext := strings.ToLower(filepath.Ext(path)); ext { + case ".lambda", ".lam", ".lc": + return "lambda", nil + case ".saccharine", ".sch": + return "saccharine", nil + default: + return "", fmt.Errorf("unknown file extension '%s'", ext) + } +} + +func LambdaConvert() *cobra.Command { + var inputReprFlag, outputReprFlag string + + cmd := &cobra.Command{ + Use: "convert ", + Short: "Convert between lambda calculus representations", + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) != 2 { + return cmd.Help() + } + + var err error + inputPath, outputPath := args[0], args[1] + + // 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 := outputReprFlag + if outputRepr == "" { + if outputRepr, err = inferReprFromPath(outputPath); 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) + } + + // Convert to output repr if different. + result, err := r.ConvertTo(repr, outputRepr) + if err != nil { + return fmt.Errorf("converting %s to %s: %w", inputRepr, outputRepr, err) + } + + // Marshal output. + output, err := r.Marshal(result) + if err != nil { + return fmt.Errorf("unmarshaling output: %w", err) + } + + // Write output file. + err = os.WriteFile(outputPath, []byte(output), 0644) + if err != nil { + return fmt.Errorf("writing output file: %w", err) + } + + return nil + }, + } + + 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_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/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 +} 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 new file mode 100644 index 0000000..5603971 --- /dev/null +++ b/cmd/lambda/registry.go @@ -0,0 +1,26 @@ +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" + "git.maximhutz.com/max/lambda/pkg/saccharine" +) + +func GetRegistry() *registry.Registry { + r := registry.New() + + // Codecs + r.MustAddConversions(cli.ConvertCodec(convert.Saccharine2Lambda{}, "saccharine", "lambda")...) + + // Engines + r.MustAddEngine(cli.ConvertEngine(normalorder.Engine{}, "normalorder", "lambda")) + + // Marshalers + r.MustAddMarshaler(cli.ConvertMarshaler(lambda.Marshaler{}, "lambda")) + r.MustAddMarshaler(cli.ConvertMarshaler(saccharine.Marshaler{}, "saccharine")) + + return r +} diff --git a/go.mod b/go.mod index 241b0b4..3633877 100644 --- a/go.mod +++ b/go.mod @@ -2,10 +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/pmezard/go-difflib v1.0.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect ) diff --git a/go.sum b/go.sum index cc8b3f4..ef5d78d 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,11 @@ -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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -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/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +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= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 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/conversion.go b/internal/cli/conversion.go new file mode 100644 index 0000000..d198f8e --- /dev/null +++ b/internal/cli/conversion.go @@ -0,0 +1,67 @@ +package cli + +import ( + "fmt" + + "git.maximhutz.com/max/lambda/pkg/codec" +) + +type Conversion interface { + InType() string + OutType() string + + Run(Repr) (Repr, error) +} + +type forwardCodec[T, U any] struct { + codec codec.Codec[T, U] + inType, outType string +} + +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.inType) + } + + u, err := c.codec.Encode(t) + if err != nil { + return nil, err + } + + return NewRepr(c.outType, u), nil +} + +func (c forwardCodec[T, U]) InType() string { return c.inType } + +func (c forwardCodec[T, U]) OutType() string { return c.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.outType) + } + + t, err := c.codec.Decode(u) + if err != nil { + return nil, err + } + + return NewRepr(c.inType, 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/cli/engine.go b/internal/cli/engine.go new file mode 100644 index 0000000..15f1618 --- /dev/null +++ b/internal/cli/engine.go @@ -0,0 +1,27 @@ +package cli + +import "git.maximhutz.com/max/lambda/pkg/engine" + +type Engine interface { + Load() Process + Name() string + InType() string +} + +type convertedEngine[T any] struct { + engine engine.Engine[T] + name string + inType string +} + +func (e convertedEngine[T]) InType() string { return e.inType } + +func (e convertedEngine[T]) Name() string { return e.name } + +func (e convertedEngine[T]) Load() Process { + return convertedProcess[T]{e.engine.Load(), e.inType} +} + +func ConvertEngine[T any](e engine.Engine[T], name, inType string) Engine { + return &convertedEngine[T]{e, name, inType} +} 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/cli/marshaler.go b/internal/cli/marshaler.go new file mode 100644 index 0000000..a212417 --- /dev/null +++ b/internal/cli/marshaler.go @@ -0,0 +1,45 @@ +package cli + +import ( + "fmt" + "reflect" + + "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 { + 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) +} + +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/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/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/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 new file mode 100644 index 0000000..705b405 --- /dev/null +++ b/internal/registry/registry.go @@ -0,0 +1,173 @@ +package registry + +import ( + "fmt" + "iter" + "maps" + + "git.maximhutz.com/max/lambda/internal/cli" +) + +type Registry struct { + marshalers map[string]cli.Marshaler + converter *Converter + engines map[string]cli.Engine +} + +func New() *Registry { + return &Registry{ + marshalers: map[string]cli.Marshaler{}, + converter: NewConverter(), + engines: map[string]cli.Engine{}, + } +} + +func (r *Registry) AddConversions(conversions ...cli.Conversion) error { + for _, conversion := range conversions { + r.converter.Add(conversion) + } + + 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 { + return fmt.Errorf("marshaler for '%s' already registered", c.InType()) + } + + r.marshalers[c.InType()] = c + 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()) + } + + r.engines[e.Name()] = e + 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 { + return nil, fmt.Errorf("engine '%s' not found", name) + } + + 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 { + return engine, nil + } + } + + return nil, fmt.Errorf("no engine for '%s'", id) +} + +func (r *Registry) ConvertTo(repr cli.Repr, outType string) (cli.Repr, error) { + 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) { + 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) +} + +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/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 new file mode 100644 index 0000000..3132409 --- /dev/null +++ b/pkg/codec/codec.go @@ -0,0 +1,8 @@ +package codec + +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/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/engine.go b/pkg/engine/engine.go new file mode 100644 index 0000000..0fcebf3 --- /dev/null +++ b/pkg/engine/engine.go @@ -0,0 +1,11 @@ +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 new file mode 100644 index 0000000..fcdd362 --- /dev/null +++ b/pkg/engine/normalorder/engine.go @@ -0,0 +1,42 @@ +package normalorder + +import ( + "git.maximhutz.com/max/lambda/pkg/engine" + "git.maximhutz.com/max/lambda/pkg/lambda" +) + +type Process struct { + expr lambda.Expression +} + +func (e Process) Get() (lambda.Expression, error) { + return e.expr, nil +} + +func (e *Process) Set(l lambda.Expression) error { + e.expr = l + return nil +} + +func (e *Process) Step(i int) bool { + for range i { + next, reduced := ReduceOnce(e.expr) + if !reduced { + return false + } + + e.expr = next + } + + 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) diff --git a/pkg/normalorder/reduce_once.go b/pkg/engine/normalorder/reduce_one.go similarity index 100% rename from pkg/normalorder/reduce_once.go rename to pkg/engine/normalorder/reduce_one.go 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..b979a09 100644 --- a/pkg/lambda/expression.go +++ b/pkg/lambda/expression.go @@ -1,14 +1,15 @@ package lambda import ( - "git.maximhutz.com/max/lambda/pkg/expr" + "fmt" + "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 + fmt.Stringer // Substitute replaces all free occurrences of the target variable with the // replacement expression. Alpha-renaming is performed automatically to 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) diff --git a/pkg/normalorder/runtime.go b/pkg/normalorder/runtime.go deleted file mode 100644 index 37d38e4..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/expr" - "git.maximhutz.com/max/lambda/pkg/lambda" - "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() expr.Expression { - 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/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 c2aafad..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/expr" -) - -// 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() expr.Expression -} 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) -}