feat: progress
This commit is contained in:
29
Makefile
29
Makefile
@@ -1,49 +1,30 @@
|
|||||||
BINARY_NAME=lambda
|
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
|
.DEFAULT_GOAL := help
|
||||||
.SILENT:
|
.SILENT:
|
||||||
|
|
||||||
help:
|
help:
|
||||||
echo "Available targets:"
|
echo "Available targets:"
|
||||||
echo " build - Build the lambda executable"
|
echo " build - Build the lambda executable"
|
||||||
echo " run - Build and run the lambda runtime (use TEST=<name> 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 " docs - Start local godoc server on port 6060"
|
||||||
echo " test - Run tests for all samples"
|
echo " test - Run tests"
|
||||||
echo " bench - Run benchmarks for all samples"
|
echo " bench - Run benchmarks"
|
||||||
echo " clean - Remove all build artifacts"
|
echo " clean - Remove all build artifacts"
|
||||||
|
|
||||||
build:
|
build:
|
||||||
go build -o ${BINARY_NAME} ./cmd/lambda
|
go build -o ${BINARY_NAME} ./cmd/lambda
|
||||||
chmod +x ${BINARY_NAME}
|
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:
|
docs:
|
||||||
echo ">>> View at 'http://localhost:6060/pkg/git.maximhutz.com/max/lambda/'"
|
echo ">>> View at 'http://localhost:6060/pkg/git.maximhutz.com/max/lambda/'"
|
||||||
go run golang.org/x/tools/cmd/godoc@latest -http=:6060
|
go run golang.org/x/tools/cmd/godoc@latest -http=:6060
|
||||||
|
|
||||||
test:
|
test:
|
||||||
go test -v ./cmd/lambda
|
go test -v ./...
|
||||||
|
|
||||||
bench:
|
bench:
|
||||||
go test -bench=. -benchtime=10x -cpu=4 ./cmd/lambda
|
go test -bench=. -benchtime=10x -cpu=4 ./...
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -f ${BINARY_NAME}
|
rm -f ${BINARY_NAME}
|
||||||
|
|||||||
30
cmd/lambda/convert.go
Normal file
30
cmd/lambda/convert.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
21
cmd/lambda/engine.go
Normal file
21
cmd/lambda/engine.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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.")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
27
cmd/lambda/repl.go
Normal file
27
cmd/lambda/repl.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
21
cmd/lambda/repr.go
Normal file
21
cmd/lambda/repr.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
19
cmd/lambda/root.go
Normal file
19
cmd/lambda/root.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
31
cmd/lambda/run.go
Normal file
31
cmd/lambda/run.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
28
cmd/lambda/trace.go
Normal file
28
cmd/lambda/trace.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
3
go.mod
3
go.mod
@@ -6,6 +6,9 @@ require github.com/stretchr/testify v1.11.1
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
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/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
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
9
go.sum
9
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
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/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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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.
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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())
|
|
||||||
}
|
|
||||||
@@ -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())
|
|
||||||
}
|
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
@@ -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())
|
|
||||||
}
|
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
@@ -1,108 +1 @@
|
|||||||
package convert
|
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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]]{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
23
pkg/engine/engine.go
Normal file
23
pkg/engine/engine.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
10
pkg/repr/repr.go
Normal file
10
pkg/repr/repr.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package repr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Repr interface {
|
||||||
|
fmt.Stringer
|
||||||
|
fmt.Scanner
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
)
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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}
|
|
||||||
}
|
|
||||||
@@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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...)
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user