Compare commits
2 Commits
feat/upgra
...
3ef27bc28a
| Author | SHA1 | Date | |
|---|---|---|---|
|
3ef27bc28a
|
|||
|
dbb988b633
|
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: "Bug Report"
|
||||
about: "Report a bug or unexpected behavior in the lambda runtime."
|
||||
about: "Report a bug or unexpected behavior in the lambda interpreter."
|
||||
title: "fix: "
|
||||
ref: "main"
|
||||
assignees: []
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: "Feature Request"
|
||||
about: "Suggest a new feature or enhancement for the lambda runtime."
|
||||
about: "Suggest a new feature or enhancement for the lambda interpreter."
|
||||
title: "feat: "
|
||||
ref: "main"
|
||||
assignees: []
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -48,7 +48,7 @@ The "source code" for a work means the preferred form of the work for making mod
|
||||
|
||||
A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code runtime used to run it.
|
||||
The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
29
Makefile
29
Makefile
@@ -1,30 +1,49 @@
|
||||
BINARY_NAME=lambda
|
||||
TEST=simple
|
||||
|
||||
.PHONY: help build docs test bench clean
|
||||
.PHONY: help build run profile explain graph 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 interpreter (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 " test - Run tests"
|
||||
echo " bench - Run benchmarks"
|
||||
echo " test - Run tests for all samples"
|
||||
echo " bench - Run benchmarks for all samples"
|
||||
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 ./...
|
||||
go test -v ./cmd/lambda
|
||||
|
||||
bench:
|
||||
go test -bench=. -benchtime=10x -cpu=4 ./...
|
||||
go test -bench=. -benchtime=10x -cpu=4 ./cmd/lambda
|
||||
|
||||
clean:
|
||||
rm -f ${BINARY_NAME}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# lambda
|
||||
|
||||
Making a lambda calculus runtime in Go.
|
||||
Making a lambda calculus interpreter in Go.
|
||||
|
||||
## Things to talk about
|
||||
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
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)
|
||||
}
|
||||
67
cmd/lambda/lambda.go
Normal file
67
cmd/lambda/lambda.go
Normal file
@@ -0,0 +1,67 @@
|
||||
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/lambda"
|
||||
"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.
|
||||
reducer := lambda.NewNormalOrderReducer(&compiled)
|
||||
|
||||
// If the user selected to track CPU performance, attach a profiler.
|
||||
if options.Profile != "" {
|
||||
plugins.NewPerformance(options.Profile, reducer)
|
||||
}
|
||||
|
||||
// If the user selected to produce a step-by-step explanation, attach an
|
||||
// observer.
|
||||
if options.Explanation {
|
||||
plugins.NewExplanation(reducer)
|
||||
}
|
||||
|
||||
// If the user opted to track statistics, attach a tracker.
|
||||
if options.Statistics {
|
||||
plugins.NewStatistics(reducer)
|
||||
}
|
||||
|
||||
// If the user selected for verbose debug logs, attach a reduction tracker.
|
||||
if options.Verbose {
|
||||
plugins.NewLogs(logger, reducer)
|
||||
}
|
||||
|
||||
// Run reduction.
|
||||
reducer.Reduce()
|
||||
|
||||
// Return the final reduced result.
|
||||
result := reducer.Expression().String()
|
||||
err = options.Destination.Write(result)
|
||||
cli.HandleError(err)
|
||||
}
|
||||
85
cmd/lambda/lambda_test.go
Normal file
85
cmd/lambda/lambda_test.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.maximhutz.com/max/lambda/pkg/convert"
|
||||
"git.maximhutz.com/max/lambda/pkg/lambda"
|
||||
"git.maximhutz.com/max/lambda/pkg/saccharine"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Helper function to run a single sample through the lambda interpreter.
|
||||
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 := lambda.NewNormalOrderReducer(&compiled)
|
||||
reducer.Reduce()
|
||||
|
||||
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.")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
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,9 +6,6 @@ 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
|
||||
)
|
||||
|
||||
9
go.sum
9
go.sum
@@ -1,18 +1,9 @@
|
||||
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=
|
||||
|
||||
18
internal/cli/exit.go
Normal file
18
internal/cli/exit.go
Normal file
@@ -0,0 +1,18 @@
|
||||
// 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)
|
||||
}
|
||||
12
internal/config/config.go
Normal file
12
internal/config/config.go
Normal file
@@ -0,0 +1,12 @@
|
||||
// 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.
|
||||
}
|
||||
27
internal/config/destination.go
Normal file
27
internal/config/destination.go
Normal file
@@ -0,0 +1,27 @@
|
||||
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)
|
||||
}
|
||||
23
internal/config/get_logger.go
Normal file
23
internal/config/get_logger.go
Normal file
@@ -0,0 +1,23 @@
|
||||
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,
|
||||
}),
|
||||
)
|
||||
}
|
||||
56
internal/config/parse_from_args.go
Normal file
56
internal/config/parse_from_args.go
Normal file
@@ -0,0 +1,56 @@
|
||||
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
|
||||
}
|
||||
41
internal/config/source.go
Normal file
41
internal/config/source.go
Normal file
@@ -0,0 +1,41 @@
|
||||
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
|
||||
}
|
||||
23
internal/plugins/debug.go
Normal file
23
internal/plugins/debug.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
|
||||
"git.maximhutz.com/max/lambda/pkg/reducer"
|
||||
)
|
||||
|
||||
type Logs struct {
|
||||
logger *slog.Logger
|
||||
reducer reducer.Reducer
|
||||
}
|
||||
|
||||
func NewLogs(logger *slog.Logger, r reducer.Reducer) *Logs {
|
||||
plugin := &Logs{logger, r}
|
||||
r.On(reducer.StepEvent, plugin.Step)
|
||||
|
||||
return plugin
|
||||
}
|
||||
|
||||
func (t *Logs) Step() {
|
||||
t.logger.Info("reduction", "tree", t.reducer.Expression().String())
|
||||
}
|
||||
31
internal/plugins/explanation.go
Normal file
31
internal/plugins/explanation.go
Normal file
@@ -0,0 +1,31 @@
|
||||
// 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/reducer"
|
||||
)
|
||||
|
||||
// Track the reductions made by a reduction process.
|
||||
type Explanation struct {
|
||||
reducer reducer.Reducer
|
||||
}
|
||||
|
||||
// Attaches a new explanation tracker to a reducer.
|
||||
func NewExplanation(r reducer.Reducer) *Explanation {
|
||||
plugin := &Explanation{reducer: r}
|
||||
r.On(reducer.StartEvent, plugin.Start)
|
||||
r.On(reducer.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())
|
||||
}
|
||||
59
internal/plugins/performance.go
Normal file
59
internal/plugins/performance.go
Normal file
@@ -0,0 +1,59 @@
|
||||
// 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/reducer"
|
||||
)
|
||||
|
||||
// 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 reducer.Reducer) *Performance {
|
||||
plugin := &Performance{File: file}
|
||||
process.On(reducer.StartEvent, plugin.Start)
|
||||
process.On(reducer.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()
|
||||
}
|
||||
44
internal/plugins/statistics.go
Normal file
44
internal/plugins/statistics.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"git.maximhutz.com/max/lambda/internal/statistics"
|
||||
"git.maximhutz.com/max/lambda/pkg/reducer"
|
||||
)
|
||||
|
||||
// An observer, to track reduction performance.
|
||||
type Statistics struct {
|
||||
start time.Time
|
||||
steps uint64
|
||||
}
|
||||
|
||||
// Create a new reduction performance Statistics.
|
||||
func NewStatistics(r reducer.Reducer) *Statistics {
|
||||
plugin := &Statistics{}
|
||||
r.On(reducer.StartEvent, plugin.Start)
|
||||
r.On(reducer.StepEvent, plugin.Step)
|
||||
r.On(reducer.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())
|
||||
}
|
||||
28
internal/statistics/statistics.go
Normal file
28
internal/statistics/statistics.go
Normal file
@@ -0,0 +1,28 @@
|
||||
// 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()
|
||||
}
|
||||
@@ -3,11 +3,11 @@ package convert
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.maximhutz.com/max/lambda/pkg/repr/lambda"
|
||||
"git.maximhutz.com/max/lambda/pkg/repr/saccharine"
|
||||
"git.maximhutz.com/max/lambda/pkg/lambda"
|
||||
"git.maximhutz.com/max/lambda/pkg/saccharine"
|
||||
)
|
||||
|
||||
func convertAtom(n *saccharine.Variable) lambda.Expression {
|
||||
func convertAtom(n *saccharine.Atom) lambda.Expression {
|
||||
return lambda.NewVariable(n.Name)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ func convertAbstraction(n *saccharine.Abstraction) lambda.Expression {
|
||||
// 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()
|
||||
freeVars := lambda.GetFreeVariables(result)
|
||||
freshName := lambda.GenerateFreshName(freeVars)
|
||||
parameters = append(parameters, freshName)
|
||||
}
|
||||
@@ -63,7 +63,7 @@ func reduceLet(s *saccharine.LetStatement, e lambda.Expression) lambda.Expressio
|
||||
}
|
||||
|
||||
func reduceDeclare(s *saccharine.DeclareStatement, e lambda.Expression) lambda.Expression {
|
||||
freshVar := lambda.GenerateFreshName(e.GetFree())
|
||||
freshVar := lambda.GenerateFreshName(lambda.GetFreeVariables(e))
|
||||
|
||||
return lambda.NewApplication(
|
||||
lambda.NewAbstraction(freshVar, e),
|
||||
@@ -94,7 +94,7 @@ func convertClause(n *saccharine.Clause) lambda.Expression {
|
||||
|
||||
func SaccharineToLambda(n saccharine.Expression) lambda.Expression {
|
||||
switch n := n.(type) {
|
||||
case *saccharine.Variable:
|
||||
case *saccharine.Atom:
|
||||
return convertAtom(n)
|
||||
case *saccharine.Abstraction:
|
||||
return convertAbstraction(n)
|
||||
|
||||
46
pkg/emitter/emitter.go
Normal file
46
pkg/emitter/emitter.go
Normal file
@@ -0,0 +1,46 @@
|
||||
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]]{},
|
||||
}
|
||||
}
|
||||
19
pkg/emitter/listener.go
Normal file
19
pkg/emitter/listener.go
Normal file
@@ -0,0 +1,19 @@
|
||||
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()
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
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
|
||||
}
|
||||
15
pkg/expr/expr.go
Normal file
15
pkg/expr/expr.go
Normal file
@@ -0,0 +1,15 @@
|
||||
// Package expr provides the abstract Expression interface for all evaluatable
|
||||
// expression types in the lambda interpreter.
|
||||
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
|
||||
}
|
||||
81
pkg/lambda/expression.go
Normal file
81
pkg/lambda/expression.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package lambda
|
||||
|
||||
import (
|
||||
"git.maximhutz.com/max/lambda/pkg/expr"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
/** ------------------------------------------------------------------------- */
|
||||
|
||||
type Abstraction struct {
|
||||
parameter string
|
||||
body Expression
|
||||
}
|
||||
|
||||
var _ Expression = (*Abstraction)(nil)
|
||||
|
||||
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)(nil)
|
||||
|
||||
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 {
|
||||
value string
|
||||
}
|
||||
|
||||
var _ Expression = (*Variable)(nil)
|
||||
|
||||
func (v *Variable) Value() string {
|
||||
return v.value
|
||||
}
|
||||
|
||||
func (v *Variable) String() string {
|
||||
return v.value
|
||||
}
|
||||
|
||||
func NewVariable(name string) *Variable {
|
||||
return &Variable{name}
|
||||
}
|
||||
@@ -6,9 +6,7 @@ import (
|
||||
"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 {
|
||||
func GenerateFreshName(used *set.Set[string]) string {
|
||||
for i := uint64(0); ; i++ {
|
||||
attempt := "_" + string(strconv.AppendUint(nil, i, 10))
|
||||
|
||||
20
pkg/lambda/get_free_variables.go
Normal file
20
pkg/lambda/get_free_variables.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package lambda
|
||||
|
||||
import "git.maximhutz.com/max/lambda/pkg/set"
|
||||
|
||||
func GetFreeVariables(e Expression) *set.Set[string] {
|
||||
switch e := e.(type) {
|
||||
case *Variable:
|
||||
return set.New(e.value)
|
||||
case *Abstraction:
|
||||
vars := GetFreeVariables(e.body)
|
||||
vars.Remove(e.parameter)
|
||||
return vars
|
||||
case *Application:
|
||||
vars := GetFreeVariables(e.abstraction)
|
||||
vars.Merge(GetFreeVariables(e.argument))
|
||||
return vars
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
14
pkg/lambda/is_free_variable.go
Normal file
14
pkg/lambda/is_free_variable.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package lambda
|
||||
|
||||
func IsFreeVariable(n string, e Expression) bool {
|
||||
switch e := e.(type) {
|
||||
case *Variable:
|
||||
return e.value == n
|
||||
case *Abstraction:
|
||||
return e.parameter != n && IsFreeVariable(n, e.body)
|
||||
case *Application:
|
||||
return IsFreeVariable(n, e.abstraction) || IsFreeVariable(n, e.argument)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
68
pkg/lambda/iterator.go
Normal file
68
pkg/lambda/iterator.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package lambda
|
||||
|
||||
type Iterator struct {
|
||||
trace []*Expression
|
||||
}
|
||||
|
||||
func NewIterator(expr *Expression) *Iterator {
|
||||
return &Iterator{[]*Expression{expr}}
|
||||
}
|
||||
|
||||
func (i *Iterator) Done() bool {
|
||||
return len(i.trace) == 0
|
||||
}
|
||||
|
||||
func (i *Iterator) Current() *Expression {
|
||||
if i.Done() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return i.trace[len(i.trace)-1]
|
||||
}
|
||||
|
||||
func (i *Iterator) Parent() *Expression {
|
||||
if len(i.trace) < 2 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return i.trace[len(i.trace)-2]
|
||||
}
|
||||
|
||||
func (i *Iterator) Swap(with Expression) {
|
||||
current := i.Current()
|
||||
if current != nil {
|
||||
*current = with
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Iterator) Back() bool {
|
||||
if i.Done() {
|
||||
return false
|
||||
}
|
||||
|
||||
i.trace = i.trace[:len(i.trace)-1]
|
||||
return true
|
||||
}
|
||||
|
||||
func (i *Iterator) Next() {
|
||||
switch typed := (*i.Current()).(type) {
|
||||
case *Abstraction:
|
||||
i.trace = append(i.trace, &typed.body)
|
||||
case *Application:
|
||||
i.trace = append(i.trace, &typed.abstraction)
|
||||
case *Variable:
|
||||
for len(i.trace) > 1 {
|
||||
if app, ok := (*i.Parent()).(*Application); ok {
|
||||
if app.abstraction == *i.Current() {
|
||||
i.Back()
|
||||
i.trace = append(i.trace, &app.argument)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
i.Back()
|
||||
}
|
||||
|
||||
i.trace = []*Expression{}
|
||||
}
|
||||
}
|
||||
61
pkg/lambda/reducer.go
Normal file
61
pkg/lambda/reducer.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package lambda
|
||||
|
||||
import (
|
||||
"git.maximhutz.com/max/lambda/pkg/emitter"
|
||||
"git.maximhutz.com/max/lambda/pkg/expr"
|
||||
"git.maximhutz.com/max/lambda/pkg/reducer"
|
||||
)
|
||||
|
||||
// NormalOrderReducer implements normal order (leftmost-outermost) reduction
|
||||
// for lambda calculus expressions.
|
||||
type NormalOrderReducer struct {
|
||||
emitter.BaseEmitter[reducer.Event]
|
||||
expression *Expression
|
||||
}
|
||||
|
||||
// NewNormalOrderReducer creates a new normal order reducer.
|
||||
func NewNormalOrderReducer(expression *Expression) *NormalOrderReducer {
|
||||
return &NormalOrderReducer{
|
||||
BaseEmitter: *emitter.New[reducer.Event](),
|
||||
expression: expression,
|
||||
}
|
||||
}
|
||||
|
||||
// Expression returns the current expression state.
|
||||
func (r *NormalOrderReducer) Expression() expr.Expression {
|
||||
return *r.expression
|
||||
}
|
||||
|
||||
func isViable(e *Expression) (*Abstraction, Expression, bool) {
|
||||
if e == nil {
|
||||
return nil, nil, false
|
||||
} else if app, appOk := (*e).(*Application); !appOk {
|
||||
return nil, nil, false
|
||||
} else if fn, fnOk := app.abstraction.(*Abstraction); !fnOk {
|
||||
return nil, nil, false
|
||||
} else {
|
||||
return fn, app.argument, true
|
||||
}
|
||||
}
|
||||
|
||||
// Reduce performs normal order reduction on a lambda expression.
|
||||
// The expression must be a lambda.Expression; other types are returned unchanged.
|
||||
func (r *NormalOrderReducer) Reduce() {
|
||||
r.Emit(reducer.StartEvent)
|
||||
it := NewIterator(r.expression)
|
||||
|
||||
for !it.Done() {
|
||||
if fn, arg, ok := isViable(it.Current()); !ok {
|
||||
it.Next()
|
||||
} else {
|
||||
it.Swap(Substitute(fn.body, fn.parameter, arg))
|
||||
r.Emit(reducer.StepEvent)
|
||||
|
||||
if _, _, ok := isViable(it.Parent()); ok {
|
||||
it.Back()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
r.Emit(reducer.StopEvent)
|
||||
}
|
||||
38
pkg/lambda/rename.go
Normal file
38
pkg/lambda/rename.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package lambda
|
||||
|
||||
func Rename(expr Expression, target string, newName string) Expression {
|
||||
switch e := expr.(type) {
|
||||
case *Variable:
|
||||
if e.value == target {
|
||||
return NewVariable(newName)
|
||||
}
|
||||
return e
|
||||
|
||||
case *Abstraction:
|
||||
newParam := e.parameter
|
||||
if e.parameter == target {
|
||||
newParam = newName
|
||||
}
|
||||
|
||||
newBody := Rename(e.body, target, newName)
|
||||
|
||||
if newParam == e.parameter && newBody == e.body {
|
||||
return e
|
||||
}
|
||||
|
||||
return NewAbstraction(newParam, newBody)
|
||||
|
||||
case *Application:
|
||||
newAbs := Rename(e.abstraction, target, newName)
|
||||
newArg := Rename(e.argument, target, newName)
|
||||
|
||||
if newAbs == e.abstraction && newArg == e.argument {
|
||||
return e
|
||||
}
|
||||
|
||||
return NewApplication(newAbs, newArg)
|
||||
|
||||
default:
|
||||
return expr
|
||||
}
|
||||
}
|
||||
46
pkg/lambda/substitute.go
Normal file
46
pkg/lambda/substitute.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package lambda
|
||||
|
||||
func Substitute(expr Expression, target string, replacement Expression) Expression {
|
||||
switch e := expr.(type) {
|
||||
case *Variable:
|
||||
if e.value == target {
|
||||
return replacement
|
||||
}
|
||||
return e
|
||||
|
||||
case *Abstraction:
|
||||
if e.parameter == target {
|
||||
return e
|
||||
}
|
||||
|
||||
body := e.body
|
||||
param := e.parameter
|
||||
if IsFreeVariable(param, replacement) {
|
||||
freeVars := GetFreeVariables(replacement)
|
||||
freeVars.Merge(GetFreeVariables(body))
|
||||
freshVar := GenerateFreshName(freeVars)
|
||||
body = Rename(body, param, freshVar)
|
||||
param = freshVar
|
||||
}
|
||||
|
||||
newBody := Substitute(body, target, replacement)
|
||||
if newBody == body && param == e.parameter {
|
||||
return e
|
||||
}
|
||||
|
||||
return NewAbstraction(param, newBody)
|
||||
|
||||
case *Application:
|
||||
newAbs := Substitute(e.abstraction, target, replacement)
|
||||
newArg := Substitute(e.argument, target, replacement)
|
||||
|
||||
if newAbs == e.abstraction && newArg == e.argument {
|
||||
return e
|
||||
}
|
||||
|
||||
return NewApplication(newAbs, newArg)
|
||||
|
||||
default:
|
||||
return expr
|
||||
}
|
||||
}
|
||||
13
pkg/reducer/events.go
Normal file
13
pkg/reducer/events.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package reducer
|
||||
|
||||
// Event represents lifecycle events during reduction.
|
||||
type Event int
|
||||
|
||||
const (
|
||||
// StartEvent is emitted before reduction begins.
|
||||
StartEvent Event = iota
|
||||
// StepEvent is emitted after each reduction step.
|
||||
StepEvent
|
||||
// StopEvent is emitted after reduction completes.
|
||||
StopEvent
|
||||
)
|
||||
27
pkg/reducer/reducer.go
Normal file
27
pkg/reducer/reducer.go
Normal file
@@ -0,0 +1,27 @@
|
||||
// Package reducer provides the abstract Reducer interface for all expression
|
||||
// reduction strategies.
|
||||
package reducer
|
||||
|
||||
import (
|
||||
"git.maximhutz.com/max/lambda/pkg/emitter"
|
||||
"git.maximhutz.com/max/lambda/pkg/expr"
|
||||
)
|
||||
|
||||
// Reducer 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.
|
||||
//
|
||||
// Reducers also implement the Emitter interface to allow plugins to observe
|
||||
// reduction lifecycle events (Start, Step, Stop).
|
||||
type Reducer interface {
|
||||
emitter.Emitter[Event]
|
||||
|
||||
// Reduce performs all reduction steps on the expression.
|
||||
// Emits StartEvent before reduction, StepEvent after each step, and
|
||||
// StopEvent after completion.
|
||||
// Returns the final reduced expression.
|
||||
Reduce()
|
||||
|
||||
// Expression returns the current expression state.
|
||||
Expression() expr.Expression
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
package lambda
|
||||
|
||||
import (
|
||||
"git.maximhutz.com/max/lambda/pkg/repr"
|
||||
"git.maximhutz.com/max/lambda/pkg/set"
|
||||
)
|
||||
|
||||
// Expression is the interface for all lambda calculus expression types.
|
||||
// It embeds the general expr.Expression interface for cross-mode compatibility.
|
||||
type Expression interface {
|
||||
repr.Repr
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
var (
|
||||
_ Expression = Abstraction{}
|
||||
_ Expression = Application{}
|
||||
_ Expression = Variable{}
|
||||
)
|
||||
|
||||
/** ------------------------------------------------------------------------- */
|
||||
|
||||
type Abstraction struct {
|
||||
parameter string
|
||||
body Expression
|
||||
}
|
||||
|
||||
func (a Abstraction) Parameter() string {
|
||||
return a.parameter
|
||||
}
|
||||
|
||||
func (a Abstraction) Body() Expression {
|
||||
return a.body
|
||||
}
|
||||
|
||||
func NewAbstraction(parameter string, body Expression) Abstraction {
|
||||
return Abstraction{parameter, body}
|
||||
}
|
||||
|
||||
/** ------------------------------------------------------------------------- */
|
||||
|
||||
type Application struct {
|
||||
abstraction Expression
|
||||
argument Expression
|
||||
}
|
||||
|
||||
func (a Application) Abstraction() Expression {
|
||||
return a.abstraction
|
||||
}
|
||||
|
||||
func (a Application) Argument() Expression {
|
||||
return a.argument
|
||||
}
|
||||
|
||||
func NewApplication(abstraction Expression, argument Expression) Application {
|
||||
return Application{abstraction, argument}
|
||||
}
|
||||
|
||||
/** ------------------------------------------------------------------------- */
|
||||
|
||||
type Variable struct {
|
||||
name string
|
||||
}
|
||||
|
||||
func (v Variable) Name() string {
|
||||
return v.name
|
||||
}
|
||||
|
||||
func NewVariable(name string) Variable {
|
||||
return Variable{name}
|
||||
}
|
||||
@@ -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,4 +0,0 @@
|
||||
package repr
|
||||
|
||||
type Repr interface {
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
package saccharine
|
||||
|
||||
import "fmt"
|
||||
|
||||
// All tokens in the pseudo-lambda language.
|
||||
type Type int
|
||||
|
||||
const (
|
||||
TokenOpenParen Type = iota // Denotes the '(' token.
|
||||
TokenCloseParen // Denotes the ')' token.
|
||||
TokenOpenBrace // Denotes the '{' token.
|
||||
TokenCloseBrace // Denotes the '}' token.
|
||||
TokenHardBreak // Denotes the ';' token.
|
||||
TokenAssign // Denotes the ':=' token.
|
||||
TokenAtom // Denotes an alpha-numeric variable.
|
||||
TokenSlash // Denotes the '/' token.
|
||||
TokenDot // Denotes the '.' token.
|
||||
TokenSoftBreak // Denotes a new-line.
|
||||
)
|
||||
|
||||
// A representation of a token in source code.
|
||||
type Token struct {
|
||||
Column int // Where the token begins in the source text.
|
||||
Type Type // What type the token is.
|
||||
Value string // The value of the token.
|
||||
}
|
||||
|
||||
func NewOpenParen(column int) *Token {
|
||||
return &Token{Type: TokenOpenParen, Column: column, Value: "("}
|
||||
}
|
||||
|
||||
func NewCloseParen(column int) *Token {
|
||||
return &Token{Type: TokenCloseParen, Column: column, Value: ")"}
|
||||
}
|
||||
|
||||
func NewOpenBrace(column int) *Token {
|
||||
return &Token{Type: TokenOpenBrace, Column: column, Value: "{"}
|
||||
}
|
||||
|
||||
func NewCloseBrace(column int) *Token {
|
||||
return &Token{Type: TokenCloseBrace, Column: column, Value: "}"}
|
||||
}
|
||||
|
||||
func NewDot(column int) *Token {
|
||||
return &Token{Type: TokenDot, Column: column, Value: "."}
|
||||
}
|
||||
|
||||
func NewHardBreak(column int) *Token {
|
||||
return &Token{Type: TokenHardBreak, Column: column, Value: ";"}
|
||||
}
|
||||
|
||||
func NewAssign(column int) *Token {
|
||||
return &Token{Type: TokenAssign, Column: column, Value: ":="}
|
||||
}
|
||||
|
||||
func NewSlash(column int) *Token {
|
||||
return &Token{Type: TokenSlash, Column: column, Value: "\\"}
|
||||
}
|
||||
|
||||
func NewAtom(name string, column int) *Token {
|
||||
return &Token{Type: TokenAtom, Column: column, Value: name}
|
||||
}
|
||||
|
||||
func NewSoftBreak(column int) *Token {
|
||||
return &Token{Type: TokenSoftBreak, Column: column, Value: "\\n"}
|
||||
}
|
||||
|
||||
func Name(typ Type) string {
|
||||
switch typ {
|
||||
case TokenOpenParen:
|
||||
return "("
|
||||
case TokenCloseParen:
|
||||
return ")"
|
||||
case TokenSlash:
|
||||
return "\\"
|
||||
case TokenDot:
|
||||
return "."
|
||||
case TokenAtom:
|
||||
return "ATOM"
|
||||
case TokenSoftBreak:
|
||||
return "\\n"
|
||||
case TokenHardBreak:
|
||||
return ";"
|
||||
default:
|
||||
panic(fmt.Errorf("unknown token type %v", typ))
|
||||
}
|
||||
}
|
||||
|
||||
func (t Token) Name() string {
|
||||
return Name(t.Type)
|
||||
}
|
||||
@@ -1,20 +1,9 @@
|
||||
package saccharine
|
||||
|
||||
import (
|
||||
"git.maximhutz.com/max/lambda/pkg/repr"
|
||||
)
|
||||
|
||||
type Expression interface {
|
||||
repr.Repr
|
||||
IsExpression()
|
||||
}
|
||||
|
||||
var (
|
||||
_ Expression = Abstraction{}
|
||||
_ Expression = Application{}
|
||||
_ Expression = Variable{}
|
||||
_ Expression = Clause{}
|
||||
)
|
||||
|
||||
/** ------------------------------------------------------------------------- */
|
||||
|
||||
type Abstraction struct {
|
||||
@@ -22,38 +11,39 @@ type Abstraction struct {
|
||||
Body Expression
|
||||
}
|
||||
|
||||
func NewAbstraction(parameter []string, body Expression) *Abstraction {
|
||||
return &Abstraction{Parameters: parameter, Body: body}
|
||||
}
|
||||
|
||||
/** ------------------------------------------------------------------------- */
|
||||
|
||||
type Application struct {
|
||||
Abstraction Expression
|
||||
Arguments []Expression
|
||||
}
|
||||
|
||||
func NewApplication(abstraction Expression, arguments []Expression) *Application {
|
||||
return &Application{Abstraction: abstraction, Arguments: arguments}
|
||||
}
|
||||
|
||||
/** ------------------------------------------------------------------------- */
|
||||
|
||||
type Variable struct {
|
||||
type Atom struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func NewVariable(name string) *Variable {
|
||||
return &Variable{Name: name}
|
||||
}
|
||||
|
||||
/** ------------------------------------------------------------------------- */
|
||||
|
||||
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}
|
||||
}
|
||||
@@ -5,17 +5,18 @@ import (
|
||||
"fmt"
|
||||
|
||||
"git.maximhutz.com/max/lambda/pkg/iterator"
|
||||
"git.maximhutz.com/max/lambda/pkg/saccharine/token"
|
||||
"git.maximhutz.com/max/lambda/pkg/trace"
|
||||
)
|
||||
|
||||
type TokenIterator = iterator.Iterator[Token]
|
||||
type TokenIterator = iterator.Iterator[token.Token]
|
||||
|
||||
func parseRawToken(i *TokenIterator, expected Type) (*Token, error) {
|
||||
return iterator.Do(i, func(i *TokenIterator) (*Token, error) {
|
||||
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'", Name(expected), tok.Value)
|
||||
return nil, fmt.Errorf("expected token %v, got %v'", token.Name(expected), tok.Value)
|
||||
} else {
|
||||
return &tok, nil
|
||||
}
|
||||
@@ -24,14 +25,14 @@ func parseRawToken(i *TokenIterator, expected Type) (*Token, error) {
|
||||
|
||||
func passSoftBreaks(i *TokenIterator) {
|
||||
for {
|
||||
if _, err := parseRawToken(i, TokenSoftBreak); err != nil {
|
||||
if _, err := parseRawToken(i, token.SoftBreak); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func parseToken(i *TokenIterator, expected Type, ignoreSoftBreaks bool) (*Token, error) {
|
||||
return iterator.Do(i, func(i *TokenIterator) (*Token, error) {
|
||||
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)
|
||||
}
|
||||
@@ -41,17 +42,17 @@ func parseToken(i *TokenIterator, expected Type, ignoreSoftBreaks bool) (*Token,
|
||||
}
|
||||
|
||||
func parseString(i *TokenIterator) (string, error) {
|
||||
if tok, err := parseToken(i, TokenAtom, true); err != nil {
|
||||
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, error) {
|
||||
if tok, softErr := parseRawToken(i, TokenSoftBreak); softErr == 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, TokenHardBreak); hardErr == nil {
|
||||
} else if tok, hardErr := parseRawToken(i, token.HardBreak); hardErr == nil {
|
||||
return tok, nil
|
||||
} else {
|
||||
return nil, errors.Join(softErr, hardErr)
|
||||
@@ -75,11 +76,11 @@ func parseList[U any](i *TokenIterator, fn func(*TokenIterator) (U, error), mini
|
||||
|
||||
func parseAbstraction(i *TokenIterator) (*Abstraction, error) {
|
||||
return iterator.Do(i, func(i *TokenIterator) (*Abstraction, error) {
|
||||
if _, err := parseToken(i, TokenSlash, true); err != nil {
|
||||
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, TokenDot, true); err != nil {
|
||||
} 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
|
||||
@@ -91,11 +92,11 @@ func parseAbstraction(i *TokenIterator) (*Abstraction, error) {
|
||||
|
||||
func parseApplication(i *TokenIterator) (*Application, error) {
|
||||
return iterator.Do(i, func(i *TokenIterator) (*Application, error) {
|
||||
if _, err := parseToken(i, TokenOpenParen, true); err != nil {
|
||||
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, TokenCloseParen, true); err != nil {
|
||||
} 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
|
||||
@@ -103,11 +104,11 @@ func parseApplication(i *TokenIterator) (*Application, error) {
|
||||
})
|
||||
}
|
||||
|
||||
func parseAtom(i *TokenIterator) (*Variable, error) {
|
||||
if tok, err := parseToken(i, TokenAtom, true); err != 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 NewVariable(tok.Value), nil
|
||||
return NewAtom(tok.Value), nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,7 +133,7 @@ func parseStatements(i *TokenIterator) ([]Statement, error) {
|
||||
|
||||
func parseClause(i *TokenIterator, braces bool) (*Clause, error) {
|
||||
if braces {
|
||||
if _, err := parseToken(i, TokenOpenBrace, true); err != nil {
|
||||
if _, err := parseToken(i, token.OpenBrace, true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -151,7 +152,7 @@ func parseClause(i *TokenIterator, braces bool) (*Clause, error) {
|
||||
}
|
||||
|
||||
if braces {
|
||||
if _, err := parseToken(i, TokenCloseBrace, true); err != nil {
|
||||
if _, err := parseToken(i, token.CloseBrace, true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -164,13 +165,13 @@ func parseExpression(i *TokenIterator) (Expression, error) {
|
||||
passSoftBreaks(i)
|
||||
|
||||
switch peek := i.MustGet(); peek.Type {
|
||||
case TokenOpenParen:
|
||||
case token.OpenParen:
|
||||
return parseApplication(i)
|
||||
case TokenSlash:
|
||||
case token.Slash:
|
||||
return parseAbstraction(i)
|
||||
case TokenAtom:
|
||||
case token.Atom:
|
||||
return parseAtom(i)
|
||||
case TokenOpenBrace:
|
||||
case token.OpenBrace:
|
||||
return parseClause(i, true)
|
||||
default:
|
||||
return nil, fmt.Errorf("expected expression, got '%v' (col %d)", peek.Value, peek.Column)
|
||||
@@ -182,7 +183,7 @@ func parseLet(i *TokenIterator) (*LetStatement, error) {
|
||||
return iterator.Do(i, func(i *TokenIterator) (*LetStatement, error) {
|
||||
if parameters, err := parseList(i, parseString, 1); err != nil {
|
||||
return nil, err
|
||||
} else if _, err := parseToken(i, TokenAssign, true); err != nil {
|
||||
} else if _, err := parseToken(i, token.Assign, true); err != nil {
|
||||
return nil, err
|
||||
} else if body, err := parseExpression(i); err != nil {
|
||||
return nil, err
|
||||
@@ -211,7 +212,7 @@ func parseStatement(i *TokenIterator) (Statement, error) {
|
||||
}
|
||||
|
||||
// Given a list of tokens, attempt to parse it into an syntax tree.
|
||||
func parse(tokens []Token) (Expression, error) {
|
||||
func parse(tokens []token.Token) (Expression, error) {
|
||||
i := iterator.Of(tokens)
|
||||
|
||||
exp, err := parseClause(i, false)
|
||||
@@ -225,13 +226,3 @@ func parse(tokens []Token) (Expression, error) {
|
||||
|
||||
return exp, nil
|
||||
}
|
||||
|
||||
// Convert a piece of valid saccharine code into an expression.
|
||||
func Parse(code string) (Expression, error) {
|
||||
tokens, err := Scan(code)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return parse(tokens)
|
||||
}
|
||||
22
pkg/saccharine/saccharine.go
Normal file
22
pkg/saccharine/saccharine.go
Normal file
@@ -0,0 +1,22 @@
|
||||
// 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,11 +1,7 @@
|
||||
package saccharine
|
||||
|
||||
import (
|
||||
"git.maximhutz.com/max/lambda/pkg/repr"
|
||||
)
|
||||
|
||||
type Statement interface {
|
||||
repr.Repr
|
||||
IsStatement()
|
||||
}
|
||||
|
||||
/** ------------------------------------------------------------------------- */
|
||||
@@ -16,14 +12,17 @@ type LetStatement struct {
|
||||
Body Expression
|
||||
}
|
||||
|
||||
func NewLet(name string, parameters []string, body Expression) *LetStatement {
|
||||
return &LetStatement{Name: name, Parameters: parameters, Body: body}
|
||||
type DeclareStatement struct {
|
||||
Value Expression
|
||||
}
|
||||
|
||||
func (LetStatement) IsStatement() {}
|
||||
func (DeclareStatement) IsStatement() {}
|
||||
|
||||
/** ------------------------------------------------------------------------- */
|
||||
|
||||
type DeclareStatement struct {
|
||||
Value Expression
|
||||
func NewLet(name string, parameters []string, body Expression) *LetStatement {
|
||||
return &LetStatement{Name: name, Parameters: parameters, Body: body}
|
||||
}
|
||||
|
||||
func NewDeclare(value Expression) *DeclareStatement {
|
||||
@@ -5,30 +5,30 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func stringifyAtom(n *Variable) string {
|
||||
func stringifyAtom(n *Atom) string {
|
||||
return n.Name
|
||||
}
|
||||
|
||||
func stringifyAbstraction(n *Abstraction) string {
|
||||
return "\\" + strings.Join(n.Parameters, " ") + "." + Stringify(n.Body)
|
||||
return "\\" + strings.Join(n.Parameters, " ") + "." + stringifyExpression(n.Body)
|
||||
}
|
||||
|
||||
func stringifyApplication(n *Application) string {
|
||||
arguments := []string{Stringify(n.Abstraction)}
|
||||
arguments := []string{stringifyExpression(n.Abstraction)}
|
||||
|
||||
for _, argument := range n.Arguments {
|
||||
arguments = append(arguments, Stringify(argument))
|
||||
arguments = append(arguments, stringifyExpression(argument))
|
||||
}
|
||||
|
||||
return "(" + strings.Join(arguments, " ") + ")"
|
||||
}
|
||||
|
||||
func stringifyLet(s *LetStatement) string {
|
||||
return s.Name + " " + strings.Join(s.Parameters, " ") + " := " + Stringify(s.Body)
|
||||
return s.Name + " " + strings.Join(s.Parameters, " ") + " := " + stringifyExpression(s.Body)
|
||||
}
|
||||
|
||||
func stringifyDeclare(s *DeclareStatement) string {
|
||||
return Stringify(s.Value)
|
||||
return stringifyExpression(s.Value)
|
||||
}
|
||||
|
||||
func stringifyStatement(s Statement) string {
|
||||
@@ -49,13 +49,13 @@ func stringifyClause(n *Clause) string {
|
||||
stmts += stringifyStatement(statement) + "; "
|
||||
}
|
||||
|
||||
return "{ " + stmts + Stringify(n.Returns) + " }"
|
||||
return "{ " + stmts + stringifyExpression(n.Returns) + " }"
|
||||
}
|
||||
|
||||
// Convert an expression back into valid source code.
|
||||
func Stringify(n Expression) string {
|
||||
func stringifyExpression(n Expression) string {
|
||||
switch n := n.(type) {
|
||||
case *Variable:
|
||||
case *Atom:
|
||||
return stringifyAtom(n)
|
||||
case *Abstraction:
|
||||
return stringifyAbstraction(n)
|
||||
@@ -1,4 +1,4 @@
|
||||
package saccharine
|
||||
package token
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@@ -14,7 +14,7 @@ func isVariable(r rune) bool {
|
||||
return unicode.IsLetter(r) || unicode.IsNumber(r)
|
||||
}
|
||||
|
||||
func scanRune(i *iterator.Iterator[rune], expected func(rune) bool) (rune, error) {
|
||||
func parseRune(i *iterator.Iterator[rune], expected func(rune) bool) (rune, error) {
|
||||
i2 := i.Copy()
|
||||
|
||||
if r, err := i2.Next(); err != nil {
|
||||
@@ -27,7 +27,7 @@ func scanRune(i *iterator.Iterator[rune], expected func(rune) bool) (rune, error
|
||||
}
|
||||
}
|
||||
|
||||
func scanCharacter(i *iterator.Iterator[rune], expected rune) (rune, error) {
|
||||
func parseCharacter(i *iterator.Iterator[rune], expected rune) (rune, error) {
|
||||
i2 := i.Copy()
|
||||
|
||||
if r, err := i2.Next(); err != nil {
|
||||
@@ -42,7 +42,7 @@ func scanCharacter(i *iterator.Iterator[rune], expected rune) (rune, error) {
|
||||
|
||||
// Pulls the next token from an iterator over runes. If it cannot, it will
|
||||
// return nil. If an error occurs, it will return that.
|
||||
func scanToken(i *iterator.Iterator[rune]) (*Token, error) {
|
||||
func getToken(i *iterator.Iterator[rune]) (*Token, error) {
|
||||
index := i.Index()
|
||||
|
||||
if i.Done() {
|
||||
@@ -70,7 +70,7 @@ func scanToken(i *iterator.Iterator[rune]) (*Token, error) {
|
||||
case letter == '}':
|
||||
return NewCloseBrace(index), nil
|
||||
case letter == ':':
|
||||
if _, err := scanCharacter(i, '='); err != nil {
|
||||
if _, err := parseCharacter(i, '='); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return NewAssign(index), nil
|
||||
@@ -98,7 +98,7 @@ func scanToken(i *iterator.Iterator[rune]) (*Token, error) {
|
||||
atom := []rune{letter}
|
||||
|
||||
for {
|
||||
if r, err := scanRune(i, isVariable); err != nil {
|
||||
if r, err := parseRune(i, isVariable); err != nil {
|
||||
break
|
||||
} else {
|
||||
atom = append(atom, r)
|
||||
@@ -112,13 +112,13 @@ func scanToken(i *iterator.Iterator[rune]) (*Token, error) {
|
||||
}
|
||||
|
||||
// Parse a string into tokens.
|
||||
func Scan(input string) ([]Token, error) {
|
||||
func Parse(input string) ([]Token, error) {
|
||||
i := iterator.Of([]rune(input))
|
||||
tokens := []Token{}
|
||||
errorList := []error{}
|
||||
|
||||
for !i.Done() {
|
||||
token, err := scanToken(i)
|
||||
token, err := getToken(i)
|
||||
if err != nil {
|
||||
errorList = append(errorList, err)
|
||||
} else if token != nil {
|
||||
91
pkg/saccharine/token/token.go
Normal file
91
pkg/saccharine/token/token.go
Normal file
@@ -0,0 +1,91 @@
|
||||
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)
|
||||
}
|
||||
@@ -4,9 +4,9 @@ import "iter"
|
||||
|
||||
type Set[T comparable] map[T]bool
|
||||
|
||||
func (s Set[T]) Add(items ...T) {
|
||||
func (s *Set[T]) Add(items ...T) {
|
||||
for _, item := range items {
|
||||
s[item] = true
|
||||
(*s)[item] = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,14 +14,14 @@ func (s Set[T]) Has(item T) bool {
|
||||
return s[item]
|
||||
}
|
||||
|
||||
func (s Set[T]) Remove(items ...T) {
|
||||
func (s *Set[T]) Remove(items ...T) {
|
||||
for _, item := range items {
|
||||
delete(s, item)
|
||||
delete(*s, item)
|
||||
}
|
||||
}
|
||||
|
||||
func (s Set[T]) Merge(o Set[T]) {
|
||||
for item := range o {
|
||||
func (s *Set[T]) Merge(o *Set[T]) {
|
||||
for item := range *o {
|
||||
s.Add(item)
|
||||
}
|
||||
}
|
||||
@@ -46,8 +46,8 @@ func (s Set[T]) Items() iter.Seq[T] {
|
||||
}
|
||||
}
|
||||
|
||||
func New[T comparable](items ...T) Set[T] {
|
||||
result := Set[T]{}
|
||||
func New[T comparable](items ...T) *Set[T] {
|
||||
result := &Set[T]{}
|
||||
|
||||
for _, item := range items {
|
||||
result.Add(item)
|
||||
|
||||
1
tests/church_6^6.expected
Normal file
1
tests/church_6^6.expected
Normal file
File diff suppressed because one or more lines are too long
8
tests/church_6^6.test
Normal file
8
tests/church_6^6.test
Normal file
@@ -0,0 +1,8 @@
|
||||
0 := \f.\x.x
|
||||
inc n := \f x.(f (n f x))
|
||||
exp n m := (m n)
|
||||
print n := (n F X)
|
||||
|
||||
N := (inc (inc (inc (inc (inc (inc 0))))))
|
||||
|
||||
(print (exp N N))
|
||||
Reference in New Issue
Block a user