From 5f2dcc939452375cedfaa3da3bfee3e8fb1e368c Mon Sep 17 00:00:00 2001 From: "M.V. Hutz" Date: Sun, 18 Jan 2026 18:27:48 -0500 Subject: [PATCH] feat: progress --- Makefile | 29 +--- cmd/lambda/convert.go | 30 ++++ cmd/lambda/engine.go | 21 +++ cmd/lambda/lambda.go | 67 -------- cmd/lambda/lambda_test.go | 85 ----------- cmd/lambda/repl.go | 27 ++++ cmd/lambda/repr.go | 21 +++ cmd/lambda/root.go | 19 +++ cmd/lambda/run.go | 31 ++++ cmd/lambda/trace.go | 28 ++++ go.mod | 3 + go.sum | 9 ++ internal/cli/exit.go | 18 --- internal/config/config.go | 12 -- internal/config/destination.go | 27 ---- internal/config/get_logger.go | 23 --- internal/config/parse_from_args.go | 56 ------- internal/config/source.go | 41 ----- 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/convert/saccharine_to_lambda.go | 107 ------------- pkg/emitter/emitter.go | 46 ------ pkg/emitter/listener.go | 19 --- pkg/engine/engine.go | 23 +++ pkg/expr/expr.go | 15 -- pkg/iterator/iterator.go | 86 ----------- pkg/lambda/expression.go | 99 ------------ pkg/lambda/generate_name.go | 19 --- pkg/lambda/get_free_variables.go | 19 --- pkg/lambda/is_free_variable.go | 12 -- pkg/lambda/rename.go | 28 ---- pkg/lambda/substitute.go | 35 ----- pkg/normalorder/reduce_once.go | 34 ----- pkg/normalorder/runtime.go | 46 ------ pkg/repr/repr.go | 10 ++ pkg/runtime/events.go | 13 -- pkg/runtime/runtime.go | 27 ---- pkg/saccharine/expression.go | 49 ------ pkg/saccharine/parse.go | 228 ---------------------------- pkg/saccharine/saccharine.go | 22 --- pkg/saccharine/statement.go | 30 ---- pkg/saccharine/stringify.go | 69 --------- pkg/saccharine/token/parse.go | 130 ---------------- pkg/saccharine/token/token.go | 91 ----------- pkg/set/set.go | 57 ------- pkg/trace/trace.go | 25 --- 49 files changed, 227 insertions(+), 1844 deletions(-) create mode 100644 cmd/lambda/convert.go create mode 100644 cmd/lambda/engine.go delete mode 100644 cmd/lambda/lambda.go delete mode 100644 cmd/lambda/lambda_test.go create mode 100644 cmd/lambda/repl.go create mode 100644 cmd/lambda/repr.go create mode 100644 cmd/lambda/root.go create mode 100644 cmd/lambda/run.go create mode 100644 cmd/lambda/trace.go delete mode 100644 internal/cli/exit.go delete mode 100644 internal/config/config.go delete mode 100644 internal/config/destination.go delete mode 100644 internal/config/get_logger.go delete mode 100644 internal/config/parse_from_args.go delete mode 100644 internal/config/source.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 delete mode 100644 pkg/emitter/emitter.go delete mode 100644 pkg/emitter/listener.go create mode 100644 pkg/engine/engine.go delete mode 100644 pkg/expr/expr.go delete mode 100644 pkg/iterator/iterator.go delete mode 100644 pkg/lambda/expression.go delete mode 100644 pkg/lambda/generate_name.go delete mode 100644 pkg/lambda/get_free_variables.go delete mode 100644 pkg/lambda/is_free_variable.go delete mode 100644 pkg/lambda/rename.go delete mode 100644 pkg/lambda/substitute.go delete mode 100644 pkg/normalorder/reduce_once.go delete mode 100644 pkg/normalorder/runtime.go create mode 100644 pkg/repr/repr.go delete mode 100644 pkg/runtime/events.go delete mode 100644 pkg/runtime/runtime.go delete mode 100644 pkg/saccharine/expression.go delete mode 100644 pkg/saccharine/parse.go delete mode 100644 pkg/saccharine/saccharine.go delete mode 100644 pkg/saccharine/statement.go delete mode 100644 pkg/saccharine/stringify.go delete mode 100644 pkg/saccharine/token/parse.go delete mode 100644 pkg/saccharine/token/token.go delete mode 100644 pkg/set/set.go delete mode 100644 pkg/trace/trace.go diff --git a/Makefile b/Makefile index 2ce49e2..2bda163 100644 --- a/Makefile +++ b/Makefile @@ -1,49 +1,30 @@ BINARY_NAME=lambda -TEST=simple -.PHONY: help build run profile explain graph docs test bench clean +.PHONY: help build docs test bench clean .DEFAULT_GOAL := help .SILENT: help: echo "Available targets:" echo " build - Build the lambda executable" - echo " run - Build and run the lambda runtime (use TEST= to specify sample)" - echo " profile - Build and run with CPU profiling enabled" - echo " explain - Build and run with explanation mode and profiling" - echo " graph - Generate and open CPU profile visualization" echo " docs - Start local godoc server on port 6060" - echo " test - Run tests for all samples" - echo " bench - Run benchmarks for all samples" + echo " test - Run tests" + echo " bench - Run benchmarks" echo " clean - Remove all build artifacts" build: go build -o ${BINARY_NAME} ./cmd/lambda chmod +x ${BINARY_NAME} -run: build - ./${BINARY_NAME} -s -f ./tests/$(TEST).test -o program.out - -profile: build - ./${BINARY_NAME} -p profile/cpu.prof -f ./tests/$(TEST).test -o program.out - -explain: build - ./${BINARY_NAME} -x -p profile/cpu.prof -f ./tests/$(TEST).test -o program.out > explain.out - -graph: - go tool pprof -raw -output=profile/cpu.raw profile/cpu.prof - go tool pprof -svg profile/cpu.prof > profile/cpu.svg - echo ">>> View at 'file://$(PWD)/profile/cpu.svg'" - docs: echo ">>> View at 'http://localhost:6060/pkg/git.maximhutz.com/max/lambda/'" go run golang.org/x/tools/cmd/godoc@latest -http=:6060 test: - go test -v ./cmd/lambda + go test -v ./... bench: - go test -bench=. -benchtime=10x -cpu=4 ./cmd/lambda + go test -bench=. -benchtime=10x -cpu=4 ./... clean: rm -f ${BINARY_NAME} diff --git a/cmd/lambda/convert.go b/cmd/lambda/convert.go new file mode 100644 index 0000000..1c5575c --- /dev/null +++ b/cmd/lambda/convert.go @@ -0,0 +1,30 @@ +package main + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" +) + +var convertCmd = &cobra.Command{ + Use: "convert [input] [output]", + Short: "Convert between lambda calculus representations", + Long: `Convert lambda calculus expressions between different representations. + +The format is inferred from file extensions when possible. +Use --from and --to flags to override format detection.`, + Example: ` lambda convert input.sch output.dbi + lambda convert input.txt output.txt --from saccharine --to lambda`, + Args: cobra.ExactArgs(2), + Run: func(cmd *cobra.Command, args []string) { + fmt.Fprintln(os.Stderr, "not implemented") + os.Exit(1) + }, +} + +func init() { + convertCmd.Flags().String("from", "", "source format") + convertCmd.Flags().String("to", "", "target format") + rootCmd.AddCommand(convertCmd) +} diff --git a/cmd/lambda/engine.go b/cmd/lambda/engine.go new file mode 100644 index 0000000..1f8413b --- /dev/null +++ b/cmd/lambda/engine.go @@ -0,0 +1,21 @@ +package main + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" +) + +var engineCmd = &cobra.Command{ + Use: "engine", + Short: "List available evaluation engines", + Run: func(cmd *cobra.Command, args []string) { + fmt.Fprintln(os.Stderr, "not implemented") + os.Exit(1) + }, +} + +func init() { + rootCmd.AddCommand(engineCmd) +} diff --git a/cmd/lambda/lambda.go b/cmd/lambda/lambda.go deleted file mode 100644 index 7722471..0000000 --- a/cmd/lambda/lambda.go +++ /dev/null @@ -1,67 +0,0 @@ -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" -) - -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) - } - - // 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) - } - - // Run reduction. - runtime.Run() - - // Return the final reduced result. - result := runtime.Expression().String() - err = options.Destination.Write(result) - 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/repl.go b/cmd/lambda/repl.go new file mode 100644 index 0000000..a7deb25 --- /dev/null +++ b/cmd/lambda/repl.go @@ -0,0 +1,27 @@ +package main + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" +) + +var replCmd = &cobra.Command{ + Use: "repl", + Short: "Start an interactive lambda calculus REPL", + Long: `Start an interactive Read-Eval-Print Loop for lambda calculus. + +Enter lambda expressions to evaluate them. +Type 'exit' or 'quit' to leave the REPL. +Press Ctrl+D to exit.`, + Example: ` lambda repl`, + Run: func(cmd *cobra.Command, args []string) { + fmt.Fprintln(os.Stderr, "not implemented") + os.Exit(1) + }, +} + +func init() { + rootCmd.AddCommand(replCmd) +} diff --git a/cmd/lambda/repr.go b/cmd/lambda/repr.go new file mode 100644 index 0000000..8dd8a81 --- /dev/null +++ b/cmd/lambda/repr.go @@ -0,0 +1,21 @@ +package main + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" +) + +var reprCmd = &cobra.Command{ + Use: "repr", + Short: "List available representations", + Run: func(cmd *cobra.Command, args []string) { + fmt.Fprintln(os.Stderr, "not implemented") + os.Exit(1) + }, +} + +func init() { + rootCmd.AddCommand(reprCmd) +} diff --git a/cmd/lambda/root.go b/cmd/lambda/root.go new file mode 100644 index 0000000..9a2be61 --- /dev/null +++ b/cmd/lambda/root.go @@ -0,0 +1,19 @@ +package main + +import ( + "os" + + "github.com/spf13/cobra" +) + +var rootCmd = &cobra.Command{ + Use: "lambda", + Short: "A lambda calculus interpreter and toolkit", + Long: `Lambda is a CLI tool for working with lambda calculus expressions.`, +} + +func main() { + if err := rootCmd.Execute(); err != nil { + os.Exit(1) + } +} diff --git a/cmd/lambda/run.go b/cmd/lambda/run.go new file mode 100644 index 0000000..9ea5cf6 --- /dev/null +++ b/cmd/lambda/run.go @@ -0,0 +1,31 @@ +package main + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" +) + +var runCmd = &cobra.Command{ + Use: "run [expression]", + Short: "Evaluate a lambda calculus expression", + Long: `Evaluate a lambda calculus expression and print the result. + +The expression can be provided as an argument, read from a file with --file, +or piped through stdin.`, + Example: ` lambda run "\x.x" + echo "\x.x" | lambda run + lambda run --file program.sch + lambda run --file program.sch --engine krivine`, + Run: func(cmd *cobra.Command, args []string) { + fmt.Fprintln(os.Stderr, "not implemented") + os.Exit(1) + }, +} + +func init() { + runCmd.Flags().StringP("file", "f", "", "read expression from file") + runCmd.Flags().StringP("engine", "e", "normal", "evaluation engine to use") + rootCmd.AddCommand(runCmd) +} diff --git a/cmd/lambda/trace.go b/cmd/lambda/trace.go new file mode 100644 index 0000000..da1e0e1 --- /dev/null +++ b/cmd/lambda/trace.go @@ -0,0 +1,28 @@ +package main + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" +) + +var traceCmd = &cobra.Command{ + Use: "trace [file]", + Short: "Trace the evaluation of a lambda calculus expression", + Long: `Trace the step-by-step evaluation of a lambda calculus expression. + +This command shows each reduction step during evaluation.`, + Example: ` lambda trace program.sch + lambda trace program.sch --engine krivine`, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + fmt.Fprintln(os.Stderr, "not implemented") + os.Exit(1) + }, +} + +func init() { + traceCmd.Flags().StringP("engine", "e", "normal", "evaluation engine to use") + rootCmd.AddCommand(traceCmd) +} diff --git a/go.mod b/go.mod index 241b0b4..0132f8a 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,9 @@ require github.com/stretchr/testify v1.11.1 require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/spf13/cobra v1.10.2 // indirect + github.com/spf13/pflag v1.0.9 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index cc8b3f4..ce2af4a 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,18 @@ +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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 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/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 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +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/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/config/config.go b/internal/config/config.go deleted file mode 100644 index 063ee2e..0000000 --- a/internal/config/config.go +++ /dev/null @@ -1,12 +0,0 @@ -// Package "config" parses ad handles the user settings given to the program. -package config - -// Configuration settings for the program. -type Config struct { - Source Source // The source code given to the program. - Destination Destination // The destination for the final result. - Verbose bool // Whether or not to print debug logs. - Explanation bool // Whether or not to print an explanation of the reduction. - Profile string // If not nil, print a CPU profile during execution. - Statistics bool // Whether or not to print statistics. -} diff --git a/internal/config/destination.go b/internal/config/destination.go deleted file mode 100644 index e9fd236..0000000 --- a/internal/config/destination.go +++ /dev/null @@ -1,27 +0,0 @@ -package config - -import ( - "fmt" - "os" -) - -// A method of writing output to the user. -type Destination interface { - // Write data to this destination. - Write(data string) error -} - -// A destination writing to stdout. -type StdoutDestination struct{} - -func (d StdoutDestination) Write(data string) error { - fmt.Println(data) - return nil -} - -// A destination writing to a file. -type FileDestination struct{ Path string } - -func (d FileDestination) Write(data string) error { - return os.WriteFile(d.Path, []byte(data+"\n"), 0644) -} diff --git a/internal/config/get_logger.go b/internal/config/get_logger.go deleted file mode 100644 index 8880878..0000000 --- a/internal/config/get_logger.go +++ /dev/null @@ -1,23 +0,0 @@ -package config - -import ( - "log/slog" - "os" -) - -// Returns a structured logger with the appropriate configurations. -func (c Config) GetLogger() *slog.Logger { - // By default, only print out errors. - level := slog.LevelError - - // If the user set the output to be "VERBOSE", return the debug logs. - if c.Verbose { - level = slog.LevelInfo - } - - return slog.New( - slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ - Level: level, - }), - ) -} diff --git a/internal/config/parse_from_args.go b/internal/config/parse_from_args.go deleted file mode 100644 index 908e7e1..0000000 --- a/internal/config/parse_from_args.go +++ /dev/null @@ -1,56 +0,0 @@ -package config - -import ( - "flag" - "fmt" -) - -// Extract the program configuration from the command-line arguments. -func FromArgs() (*Config, error) { - // Relevant flags. - verbose := flag.Bool("v", false, "Verbosity. If set, the program will print logs.") - explanation := flag.Bool("x", false, "Explanation. Whether or not to show all reduction steps.") - statistics := flag.Bool("s", false, "Statistics. If set, the process will print various statistics about the run.") - profile := flag.String("p", "", "CPU profiling. If an output file is defined, the program will profile its execution and dump its results into it.") - file := flag.String("f", "", "File. If set, read source from the specified file.") - output := flag.String("o", "", "Output. If set, write result to the specified file. Use '-' for stdout (default).") - flag.Parse() - - // Parse source type. - var source Source - if *file != "" { - // File flag takes precedence. - if flag.NArg() > 0 { - return nil, fmt.Errorf("cannot specify both -f flag and positional argument") - } - source = FileSource{Path: *file} - } else if flag.NArg() == 0 { - return nil, fmt.Errorf("no input given") - } else if flag.NArg() > 1 { - return nil, fmt.Errorf("more than 1 command-line argument") - } else { - // Positional argument. - if flag.Arg(0) == "-" { - source = StdinSource{} - } else { - source = StringSource{Data: flag.Arg(0)} - } - } - - // Parse destination type. - var destination Destination - if *output == "" || *output == "-" { - destination = StdoutDestination{} - } else { - destination = FileDestination{Path: *output} - } - - return &Config{ - Source: source, - Destination: destination, - Verbose: *verbose, - Explanation: *explanation, - Profile: *profile, - Statistics: *statistics, - }, nil -} diff --git a/internal/config/source.go b/internal/config/source.go deleted file mode 100644 index 758eaa0..0000000 --- a/internal/config/source.go +++ /dev/null @@ -1,41 +0,0 @@ -package config - -import ( - "io" - "os" -) - -// A method of extracting input from the user. -type Source interface { - // Fetch data from this source. - Extract() (string, error) -} - -// A source defined by a string. -type StringSource struct{ Data string } - -func (s StringSource) Extract() (string, error) { return s.Data, nil } - -// A source pulling from standard input. -type StdinSource struct{} - -func (s StdinSource) Extract() (string, error) { - data, err := io.ReadAll(os.Stdin) - if err != nil { - return "", err - } - - return string(data), nil -} - -// A source reading from a file. -type FileSource struct{ Path string } - -func (s FileSource) Extract() (string, error) { - data, err := os.ReadFile(s.Path) - if err != nil { - return "", err - } - - return string(data), nil -} 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/convert/saccharine_to_lambda.go b/pkg/convert/saccharine_to_lambda.go index 8bab6b8..233bcde 100644 --- a/pkg/convert/saccharine_to_lambda.go +++ b/pkg/convert/saccharine_to_lambda.go @@ -1,108 +1 @@ package convert - -import ( - "fmt" - - "git.maximhutz.com/max/lambda/pkg/lambda" - "git.maximhutz.com/max/lambda/pkg/saccharine" -) - -func convertAtom(n *saccharine.Atom) lambda.Expression { - return lambda.NewVariable(n.Name) -} - -func convertAbstraction(n *saccharine.Abstraction) lambda.Expression { - result := SaccharineToLambda(n.Body) - - parameters := n.Parameters - - // If the function has no parameters, it is a thunk. Lambda calculus still - // requires _some_ parameter exists, so generate one. - if len(parameters) == 0 { - freeVars := result.GetFree() - freshName := lambda.GenerateFreshName(freeVars) - parameters = append(parameters, freshName) - } - - for i := len(parameters) - 1; i >= 0; i-- { - result = lambda.NewAbstraction(parameters[i], result) - } - - return result -} - -func convertApplication(n *saccharine.Application) lambda.Expression { - result := SaccharineToLambda(n.Abstraction) - - arguments := []lambda.Expression{} - for _, argument := range n.Arguments { - convertedArgument := SaccharineToLambda(argument) - arguments = append(arguments, convertedArgument) - } - - for _, argument := range arguments { - result = lambda.NewApplication(result, argument) - } - - return result -} - -func reduceLet(s *saccharine.LetStatement, e lambda.Expression) lambda.Expression { - var value lambda.Expression - - if len(s.Parameters) == 0 { - value = SaccharineToLambda(s.Body) - } else { - value = convertAbstraction(saccharine.NewAbstraction(s.Parameters, s.Body)) - } - - return lambda.NewApplication( - lambda.NewAbstraction(s.Name, e), - value, - ) -} - -func reduceDeclare(s *saccharine.DeclareStatement, e lambda.Expression) lambda.Expression { - freshVar := lambda.GenerateFreshName(e.GetFree()) - - return lambda.NewApplication( - lambda.NewAbstraction(freshVar, e), - SaccharineToLambda(s.Value), - ) -} - -func reduceStatement(s saccharine.Statement, e lambda.Expression) lambda.Expression { - switch s := s.(type) { - case *saccharine.DeclareStatement: - return reduceDeclare(s, e) - case *saccharine.LetStatement: - return reduceLet(s, e) - default: - panic(fmt.Errorf("unknown statement type: %v", s)) - } -} - -func convertClause(n *saccharine.Clause) lambda.Expression { - result := SaccharineToLambda(n.Returns) - - for i := len(n.Statements) - 1; i >= 0; i-- { - result = reduceStatement(n.Statements[i], result) - } - - return result -} - -func SaccharineToLambda(n saccharine.Expression) lambda.Expression { - switch n := n.(type) { - case *saccharine.Atom: - return convertAtom(n) - case *saccharine.Abstraction: - return convertAbstraction(n) - case *saccharine.Application: - return convertApplication(n) - case *saccharine.Clause: - return convertClause(n) - default: - panic(fmt.Errorf("unknown expression type: %T", n)) - } -} diff --git a/pkg/emitter/emitter.go b/pkg/emitter/emitter.go deleted file mode 100644 index 2c34b55..0000000 --- a/pkg/emitter/emitter.go +++ /dev/null @@ -1,46 +0,0 @@ -package emitter - -import "git.maximhutz.com/max/lambda/pkg/set" - -type Emitter[E comparable] interface { - On(E, func()) Listener[E] - Off(Listener[E]) - Emit(E) -} - -type BaseEmitter[E comparable] struct { - listeners map[E]set.Set[Listener[E]] -} - -func (e *BaseEmitter[E]) On(kind E, fn func()) Listener[E] { - if e.listeners[kind] == nil { - e.listeners[kind] = set.New[Listener[E]]() - } - - listener := &BaseListener[E]{kind, fn} - e.listeners[kind].Add(listener) - return listener -} - -func (e *BaseEmitter[E]) Off(listener Listener[E]) { - kind := listener.Kind() - if e.listeners[kind] != nil { - e.listeners[kind].Remove(listener) - } -} - -func (e *BaseEmitter[E]) Emit(event E) { - if e.listeners[event] == nil { - e.listeners[event] = set.New[Listener[E]]() - } - - for listener := range e.listeners[event].Items() { - listener.Run() - } -} - -func New[E comparable]() *BaseEmitter[E] { - return &BaseEmitter[E]{ - listeners: map[E]set.Set[Listener[E]]{}, - } -} diff --git a/pkg/emitter/listener.go b/pkg/emitter/listener.go deleted file mode 100644 index 43c95c6..0000000 --- a/pkg/emitter/listener.go +++ /dev/null @@ -1,19 +0,0 @@ -package emitter - -type Listener[E comparable] interface { - Kind() E - Run() -} - -type BaseListener[E comparable] struct { - kind E - fn func() -} - -func (l BaseListener[E]) Kind() E { - return l.kind -} - -func (l BaseListener[E]) Run() { - l.fn() -} diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go new file mode 100644 index 0000000..583e1ac --- /dev/null +++ b/pkg/engine/engine.go @@ -0,0 +1,23 @@ +package engine + +import ( + "fmt" + + "git.maximhutz.com/max/lambda/pkg/repr" +) + +type Engine[R repr.Repr] interface { + Run(trace bool, count int) (Trace, bool) + Step() uint64 + State() (repr.Repr, error) + + fmt.Stringer +} + +type Trace interface { + Before() string + After() string + Step() uint64 + TransitionName() string + TransitionNodes() string +} 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/iterator/iterator.go b/pkg/iterator/iterator.go deleted file mode 100644 index 2db5cd4..0000000 --- a/pkg/iterator/iterator.go +++ /dev/null @@ -1,86 +0,0 @@ -/* -Package "iterator" -*/ -package iterator - -import "fmt" - -// An iterator over slices. -type Iterator[T any] struct { - items []T - index int -} - -// Create a new iterator, over a set of items. -func Of[T any](items []T) *Iterator[T] { - return &Iterator[T]{items: items, index: 0} -} - -// Returns the current position of the iterator. -func (i Iterator[T]) Index() int { - return i.index -} - -func (i Iterator[T]) Copy() *Iterator[T] { - return &Iterator[T]{items: i.items, index: i.index} -} - -func (i *Iterator[T]) Sync(o *Iterator[T]) { - i.index = o.index -} - -// Create a new iterator, over a set of items. -func (i Iterator[T]) Get() (T, error) { - var null T - if i.Done() { - return null, fmt.Errorf("iterator is exhausted") - } - - return i.items[i.index], nil -} - -func (i Iterator[T]) MustGet() T { - var null T - if i.Done() { - return null - } - - return i.items[i.index] -} - -func (i *Iterator[T]) Forward() { - if !i.Done() { - i.index++ - } -} - -// Create a new iterator, over a set of items. -func (i *Iterator[T]) Next() (T, error) { - item, err := i.Get() - if err == nil { - i.index++ - } - - return item, err -} - -// Create a new iterator, over a set of items. -func (i *Iterator[T]) Back() { - i.index = max(i.index-1, 0) -} - -// Returns the current position of the iterator. -func (i Iterator[T]) Done() bool { - return i.index == len(i.items) -} - -func Do[T any, U any](i *Iterator[T], fn func(i *Iterator[T]) (U, error)) (U, error) { - i2 := i.Copy() - - out, err := fn(i2) - if err == nil { - i.Sync(i2) - } - - return out, err -} diff --git a/pkg/lambda/expression.go b/pkg/lambda/expression.go deleted file mode 100644 index dd28fe6..0000000 --- a/pkg/lambda/expression.go +++ /dev/null @@ -1,99 +0,0 @@ -package lambda - -import ( - "git.maximhutz.com/max/lambda/pkg/expr" - "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 - - // Substitute replaces all free occurrences of the target variable with the - // replacement expression. Alpha-renaming is performed automatically to - // avoid variable capture. - Substitute(target string, replacement Expression) Expression - - // GetFree returns the set of all free variable names in the expression. - // This function does not mutate the input expression. - // The returned set is newly allocated and can be modified by the caller. - GetFree() set.Set[string] - - // Rename replaces all occurrences of the target variable name with the new name. - Rename(target string, newName string) Expression - - // IsFree returns true if the variable name n occurs free in the expression. - // This function does not mutate the input expression. - IsFree(n string) bool -} - -/** ------------------------------------------------------------------------- */ - -type Abstraction struct { - parameter string - body Expression -} - -var _ Expression = Abstraction{} - -func (a Abstraction) Parameter() string { - return a.parameter -} - -func (a Abstraction) Body() Expression { - return a.body -} - -func (a Abstraction) String() string { - return "\\" + a.parameter + "." + a.body.String() -} - -func NewAbstraction(parameter string, body Expression) Abstraction { - return Abstraction{parameter, body} -} - -/** ------------------------------------------------------------------------- */ - -type Application struct { - abstraction Expression - argument Expression -} - -var _ Expression = Application{} - -func (a Application) Abstraction() Expression { - return a.abstraction -} - -func (a Application) Argument() Expression { - return a.argument -} - -func (a Application) String() string { - return "(" + a.abstraction.String() + " " + a.argument.String() + ")" -} - -func NewApplication(abstraction Expression, argument Expression) Application { - return Application{abstraction, argument} -} - -/** ------------------------------------------------------------------------- */ - -type Variable struct { - name string -} - -var _ Expression = Variable{} - -func (v Variable) Name() string { - return v.name -} - -func (v Variable) String() string { - return v.name -} - -func NewVariable(name string) Variable { - return Variable{name} -} diff --git a/pkg/lambda/generate_name.go b/pkg/lambda/generate_name.go deleted file mode 100644 index 03f5018..0000000 --- a/pkg/lambda/generate_name.go +++ /dev/null @@ -1,19 +0,0 @@ -package lambda - -import ( - "strconv" - - "git.maximhutz.com/max/lambda/pkg/set" -) - -// GenerateFreshName generates a variable name that is not in the used set. -// This function does not mutate the used set. -func GenerateFreshName(used set.Set[string]) string { - for i := uint64(0); ; i++ { - attempt := "_" + string(strconv.AppendUint(nil, i, 10)) - - if !used.Has(attempt) { - return attempt - } - } -} diff --git a/pkg/lambda/get_free_variables.go b/pkg/lambda/get_free_variables.go deleted file mode 100644 index 1c2bf91..0000000 --- a/pkg/lambda/get_free_variables.go +++ /dev/null @@ -1,19 +0,0 @@ -package lambda - -import "git.maximhutz.com/max/lambda/pkg/set" - -func (e Variable) GetFree() set.Set[string] { - return set.New(e.Name()) -} - -func (e Abstraction) GetFree() set.Set[string] { - vars := e.Body().GetFree() - vars.Remove(e.Parameter()) - return vars -} - -func (e Application) GetFree() set.Set[string] { - vars := e.Abstraction().GetFree() - vars.Merge(e.Argument().GetFree()) - return vars -} diff --git a/pkg/lambda/is_free_variable.go b/pkg/lambda/is_free_variable.go deleted file mode 100644 index 09d77a6..0000000 --- a/pkg/lambda/is_free_variable.go +++ /dev/null @@ -1,12 +0,0 @@ -package lambda - -func (e Variable) IsFree(n string) bool { - return e.Name() == n -} - -func (e Abstraction) IsFree(n string) bool { - return e.Parameter() != n && e.Body().IsFree(n) -} -func (e Application) IsFree(n string) bool { - return e.Abstraction().IsFree(n) || e.Argument().IsFree(n) -} diff --git a/pkg/lambda/rename.go b/pkg/lambda/rename.go deleted file mode 100644 index 2c39237..0000000 --- a/pkg/lambda/rename.go +++ /dev/null @@ -1,28 +0,0 @@ -package lambda - -// Rename replaces all occurrences of the target variable name with the new name. -func (e Variable) Rename(target string, newName string) Expression { - if e.Name() == target { - return NewVariable(newName) - } - - return e -} - -func (e Abstraction) Rename(target string, newName string) Expression { - newParam := e.Parameter() - if e.Parameter() == target { - newParam = newName - } - - newBody := e.Body().Rename(target, newName) - - return NewAbstraction(newParam, newBody) -} - -func (e Application) Rename(target string, newName string) Expression { - newAbs := e.Abstraction().Rename(target, newName) - newArg := e.Argument().Rename(target, newName) - - return NewApplication(newAbs, newArg) -} diff --git a/pkg/lambda/substitute.go b/pkg/lambda/substitute.go deleted file mode 100644 index 1f6b38e..0000000 --- a/pkg/lambda/substitute.go +++ /dev/null @@ -1,35 +0,0 @@ -package lambda - -func (e Variable) Substitute(target string, replacement Expression) Expression { - if e.Name() == target { - return replacement - } - - return e -} - -func (e Abstraction) Substitute(target string, replacement Expression) Expression { - if e.Parameter() == target { - return e - } - - body := e.Body() - param := e.Parameter() - if replacement.IsFree(param) { - freeVars := replacement.GetFree() - freeVars.Merge(body.GetFree()) - freshVar := GenerateFreshName(freeVars) - body = body.Rename(param, freshVar) - param = freshVar - } - - newBody := body.Substitute(target, replacement) - return NewAbstraction(param, newBody) -} - -func (e Application) Substitute(target string, replacement Expression) Expression { - abs := e.Abstraction().Substitute(target, replacement) - arg := e.Argument().Substitute(target, replacement) - - return NewApplication(abs, arg) -} 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 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/repr/repr.go b/pkg/repr/repr.go new file mode 100644 index 0000000..87961b5 --- /dev/null +++ b/pkg/repr/repr.go @@ -0,0 +1,10 @@ +package repr + +import ( + "fmt" +) + +type Repr interface { + fmt.Stringer + fmt.Scanner +} 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/expression.go b/pkg/saccharine/expression.go deleted file mode 100644 index 80c8f1b..0000000 --- a/pkg/saccharine/expression.go +++ /dev/null @@ -1,49 +0,0 @@ -package saccharine - -type Expression interface { - IsExpression() -} - -/** ------------------------------------------------------------------------- */ - -type Abstraction struct { - Parameters []string - Body Expression -} - -type Application struct { - Abstraction Expression - Arguments []Expression -} - -type Atom struct { - Name string -} - -type Clause struct { - Statements []Statement - Returns Expression -} - -func (Abstraction) IsExpression() {} -func (Application) IsExpression() {} -func (Atom) IsExpression() {} -func (Clause) IsExpression() {} - -/** ------------------------------------------------------------------------- */ - -func NewAbstraction(parameter []string, body Expression) *Abstraction { - return &Abstraction{Parameters: parameter, Body: body} -} - -func NewApplication(abstraction Expression, arguments []Expression) *Application { - return &Application{Abstraction: abstraction, Arguments: arguments} -} - -func NewAtom(name string) *Atom { - return &Atom{Name: name} -} - -func NewClause(statements []Statement, returns Expression) *Clause { - return &Clause{Statements: statements, Returns: returns} -} diff --git a/pkg/saccharine/parse.go b/pkg/saccharine/parse.go deleted file mode 100644 index eb045d0..0000000 --- a/pkg/saccharine/parse.go +++ /dev/null @@ -1,228 +0,0 @@ -package saccharine - -import ( - "errors" - "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] - -func parseRawToken(i *TokenIterator, expected token.Type) (*token.Token, error) { - return iterator.Do(i, func(i *TokenIterator) (*token.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) - } else { - return &tok, nil - } - }) -} - -func passSoftBreaks(i *TokenIterator) { - for { - if _, err := parseRawToken(i, token.SoftBreak); 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) { - if ignoreSoftBreaks { - passSoftBreaks(i) - } - - return parseRawToken(i, expected) - }) -} - -func parseString(i *TokenIterator) (string, error) { - if tok, err := parseToken(i, token.Atom, 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 { - return tok, nil - } else if tok, hardErr := parseRawToken(i, token.HardBreak); hardErr == nil { - return tok, nil - } else { - return nil, errors.Join(softErr, hardErr) - } -} - -func parseList[U any](i *TokenIterator, fn func(*TokenIterator) (U, error), minimum int) ([]U, error) { - results := []U{} - - for { - if u, err := fn(i); err != nil { - if len(results) < minimum { - return nil, trace.Wrap(err, "expected at least '%v' items, got only '%v'", minimum, len(results)) - } - return results, nil - } else { - results = append(results, u) - } - } -} - -func parseAbstraction(i *TokenIterator) (*Abstraction, error) { - return iterator.Do(i, func(i *TokenIterator) (*Abstraction, error) { - if _, err := parseToken(i, token.Slash, 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 { - return nil, trace.Wrap(err, "no function dot (col %d)", i.MustGet().Column) - } else if body, err := parseExpression(i); err != nil { - return nil, err - } else { - return NewAbstraction(parameters, body), nil - } - }) -} - -func parseApplication(i *TokenIterator) (*Application, error) { - return iterator.Do(i, func(i *TokenIterator) (*Application, error) { - if _, err := parseToken(i, token.OpenParen, 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 { - return nil, trace.Wrap(err, "no closing brackets (col %d)", i.MustGet().Column) - } else { - return NewApplication(expressions[0], expressions[1:]), nil - } - }) -} - -func parseAtom(i *TokenIterator) (*Atom, error) { - if tok, err := parseToken(i, token.Atom, true); err != nil { - return nil, trace.Wrap(err, "no variable (col %d)", i.Index()) - } else { - return NewAtom(tok.Value), nil - } -} - -func parseStatements(i *TokenIterator) ([]Statement, error) { - statements := []Statement{} - - //nolint:errcheck - parseList(i, parseBreak, 0) - - for { - if statement, err := parseStatement(i); err != nil { - break - } else if _, err := parseList(i, parseBreak, 1); err != nil && !i.Done() { - break - } else { - statements = append(statements, statement) - } - } - - return statements, nil -} - -func parseClause(i *TokenIterator, braces bool) (*Clause, error) { - if braces { - if _, err := parseToken(i, token.OpenBrace, true); err != nil { - return nil, err - } - } - - var stmts []Statement - var last *DeclareStatement - var err error - var ok bool - - if stmts, err = parseStatements(i); err != nil { - return nil, err - } else if len(stmts) == 0 { - return nil, fmt.Errorf("no statements in clause") - } else if last, ok = stmts[len(stmts)-1].(*DeclareStatement); !ok { - return nil, fmt.Errorf("this clause contains no final return value (col %d)", i.MustGet().Column) - } - - if braces { - if _, err := parseToken(i, token.CloseBrace, true); err != nil { - return nil, err - } - } - - return NewClause(stmts[:len(stmts)-1], last.Value), nil -} - -func parseExpression(i *TokenIterator) (Expression, error) { - return iterator.Do(i, func(i *TokenIterator) (Expression, error) { - passSoftBreaks(i) - - switch peek := i.MustGet(); peek.Type { - case token.OpenParen: - return parseApplication(i) - case token.Slash: - return parseAbstraction(i) - case token.Atom: - return parseAtom(i) - case token.OpenBrace: - return parseClause(i, true) - default: - return nil, fmt.Errorf("expected expression, got '%v' (col %d)", peek.Value, peek.Column) - } - }) -} - -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 { - return nil, err - } else if body, err := parseExpression(i); err != nil { - return nil, err - } else { - return NewLet(parameters[0], parameters[1:], body), nil - } - }) -} - -func parseDeclare(i *TokenIterator) (*DeclareStatement, error) { - if value, err := parseExpression(i); err != nil { - return nil, err - } else { - return NewDeclare(value), nil - } -} - -func parseStatement(i *TokenIterator) (Statement, error) { - if let, letErr := parseLet(i); letErr == nil { - return let, nil - } else if declare, declErr := parseDeclare(i); declErr == nil { - return declare, nil - } else { - return nil, errors.Join(letErr, declErr) - } -} - -// Given a list of tokens, attempt to parse it into an syntax tree. -func parse(tokens []token.Token) (Expression, error) { - i := iterator.Of(tokens) - - exp, err := parseClause(i, false) - if err != nil { - return nil, err - } - - if !i.Done() { - return nil, fmt.Errorf("expected EOF, found more code (col %d)", i.MustGet().Column) - } - - return exp, nil -} 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/statement.go b/pkg/saccharine/statement.go deleted file mode 100644 index 1d0dd15..0000000 --- a/pkg/saccharine/statement.go +++ /dev/null @@ -1,30 +0,0 @@ -package saccharine - -type Statement interface { - IsStatement() -} - -/** ------------------------------------------------------------------------- */ - -type LetStatement struct { - Name string - Parameters []string - Body Expression -} - -type DeclareStatement struct { - Value Expression -} - -func (LetStatement) IsStatement() {} -func (DeclareStatement) IsStatement() {} - -/** ------------------------------------------------------------------------- */ - -func NewLet(name string, parameters []string, body Expression) *LetStatement { - return &LetStatement{Name: name, Parameters: parameters, Body: body} -} - -func NewDeclare(value Expression) *DeclareStatement { - return &DeclareStatement{Value: value} -} diff --git a/pkg/saccharine/stringify.go b/pkg/saccharine/stringify.go deleted file mode 100644 index 5c6f0ca..0000000 --- a/pkg/saccharine/stringify.go +++ /dev/null @@ -1,69 +0,0 @@ -package saccharine - -import ( - "fmt" - "strings" -) - -func stringifyAtom(n *Atom) string { - return n.Name -} - -func stringifyAbstraction(n *Abstraction) string { - return "\\" + strings.Join(n.Parameters, " ") + "." + stringifyExpression(n.Body) -} - -func stringifyApplication(n *Application) string { - arguments := []string{stringifyExpression(n.Abstraction)} - - for _, argument := range n.Arguments { - arguments = append(arguments, stringifyExpression(argument)) - } - - return "(" + strings.Join(arguments, " ") + ")" -} - -func stringifyLet(s *LetStatement) string { - return s.Name + " " + strings.Join(s.Parameters, " ") + " := " + stringifyExpression(s.Body) -} - -func stringifyDeclare(s *DeclareStatement) string { - return stringifyExpression(s.Value) -} - -func stringifyStatement(s Statement) string { - switch s := s.(type) { - case *DeclareStatement: - return stringifyDeclare(s) - case *LetStatement: - return stringifyLet(s) - default: - panic(fmt.Errorf("unknown statement type: %v", s)) - } -} - -func stringifyClause(n *Clause) string { - stmts := "" - - for _, statement := range n.Statements { - stmts += stringifyStatement(statement) + "; " - } - - return "{ " + stmts + stringifyExpression(n.Returns) + " }" -} - -// Convert an expression back into valid source code. -func stringifyExpression(n Expression) string { - switch n := n.(type) { - case *Atom: - return stringifyAtom(n) - case *Abstraction: - return stringifyAbstraction(n) - case *Application: - return stringifyApplication(n) - case *Clause: - return stringifyClause(n) - default: - panic(fmt.Errorf("unknown expression type: %T", n)) - } -} diff --git a/pkg/saccharine/token/parse.go b/pkg/saccharine/token/parse.go deleted file mode 100644 index b7d5033..0000000 --- a/pkg/saccharine/token/parse.go +++ /dev/null @@ -1,130 +0,0 @@ -package token - -import ( - "errors" - "fmt" - "unicode" - - "git.maximhutz.com/max/lambda/pkg/iterator" - "git.maximhutz.com/max/lambda/pkg/trace" -) - -// isVariables determines whether a rune can be a valid variable. -func isVariable(r rune) bool { - return unicode.IsLetter(r) || unicode.IsNumber(r) -} - -func parseRune(i *iterator.Iterator[rune], expected func(rune) bool) (rune, error) { - i2 := i.Copy() - - if r, err := i2.Next(); err != nil { - return r, err - } else if !expected(r) { - return r, fmt.Errorf("got unexpected rune %v'", r) - } else { - i.Sync(i2) - return r, nil - } -} - -func parseCharacter(i *iterator.Iterator[rune], expected rune) (rune, error) { - i2 := i.Copy() - - if r, err := i2.Next(); err != nil { - return r, err - } else if r != expected { - return r, fmt.Errorf("got unexpected rune %v'", r) - } else { - i.Sync(i2) - return r, nil - } -} - -// 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) { - index := i.Index() - - if i.Done() { - return nil, nil - } - - letter, err := i.Next() - if err != nil { - return nil, trace.Wrap(err, "cannot produce next token") - } - - switch { - case letter == '(': - return NewOpenParen(index), nil - case letter == ')': - return NewCloseParen(index), nil - case letter == '.': - return NewDot(index), nil - case letter == '\\': - return NewSlash(index), nil - case letter == '\n': - return NewSoftBreak(index), nil - case letter == '{': - return NewOpenBrace(index), nil - case letter == '}': - return NewCloseBrace(index), nil - case letter == ':': - if _, err := parseCharacter(i, '='); err != nil { - return nil, err - } else { - return NewAssign(index), nil - } - case letter == ';': - return NewHardBreak(index), nil - case letter == '#': - // Skip everything until the next newline or EOF. - for !i.Done() { - r, err := i.Next() - if err != nil { - return nil, trace.Wrap(err, "error while parsing comment") - } - - if r == '\n' { - // Put the newline back so it can be processed as a soft break. - i.Back() - break - } - } - return nil, nil - case unicode.IsSpace(letter): - return nil, nil - case isVariable(letter): - atom := []rune{letter} - - for { - if r, err := parseRune(i, isVariable); err != nil { - break - } else { - atom = append(atom, r) - } - } - - return NewAtom(string(atom), index), nil - } - - return nil, fmt.Errorf("unknown character '%v'", string(letter)) -} - -// Parse a string into tokens. -func Parse(input string) ([]Token, error) { - i := iterator.Of([]rune(input)) - tokens := []Token{} - errorList := []error{} - - for !i.Done() { - token, err := getToken(i) - if err != nil { - errorList = append(errorList, err) - } else if token != nil { - tokens = append(tokens, *token) - } - } - - return tokens, errors.Join(errorList...) -} 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) -} diff --git a/pkg/set/set.go b/pkg/set/set.go deleted file mode 100644 index c66cf71..0000000 --- a/pkg/set/set.go +++ /dev/null @@ -1,57 +0,0 @@ -package set - -import "iter" - -type Set[T comparable] map[T]bool - -func (s Set[T]) Add(items ...T) { - for _, item := range items { - s[item] = true - } -} - -func (s Set[T]) Has(item T) bool { - return s[item] -} - -func (s Set[T]) Remove(items ...T) { - for _, item := range items { - delete(s, item) - } -} - -func (s Set[T]) Merge(o Set[T]) { - for item := range o { - s.Add(item) - } -} - -func (s Set[T]) ToList() []T { - list := []T{} - - for item := range s { - list = append(list, item) - } - - return list -} - -func (s Set[T]) Items() iter.Seq[T] { - return func(yield func(T) bool) { - for item := range s { - if !yield(item) { - return - } - } - } -} - -func New[T comparable](items ...T) Set[T] { - result := Set[T]{} - - for _, item := range items { - result.Add(item) - } - - return result -} diff --git a/pkg/trace/trace.go b/pkg/trace/trace.go deleted file mode 100644 index 87a0337..0000000 --- a/pkg/trace/trace.go +++ /dev/null @@ -1,25 +0,0 @@ -package trace - -import ( - "errors" - "fmt" - "strings" -) - -func Indent(s string, size int) string { - lines := strings.Lines(s) - indent := strings.Repeat(" ", size) - - indented := "" - for line := range lines { - indented += indent + line - } - - return indented -} - -func Wrap(child error, format string, a ...any) error { - parent := fmt.Errorf(format, a...) - childErrString := Indent(child.Error(), 4) - return errors.New(parent.Error() + "\n" + childErrString) -}