Compare commits
1 Commits
528956b033
...
f3b9137d75
| Author | SHA1 | Date | |
|---|---|---|---|
|
f3b9137d75
|
23
CLAUDE.md
23
CLAUDE.md
@@ -80,26 +80,3 @@ Use the `tea` CLI (Gitea command-line tool) for PR operations instead of `gh`.
|
|||||||
|
|
||||||
**Linking issues**: When a PR solves an issue, reference the issue in both the commit message and PR description using `Closes #<number>`.
|
**Linking issues**: When a PR solves an issue, reference the issue in both the commit message and PR description using `Closes #<number>`.
|
||||||
This automatically links and closes the issue when the PR is merged.
|
This automatically links and closes the issue when the PR is merged.
|
||||||
|
|
||||||
### Updating PRs
|
|
||||||
|
|
||||||
When pushing additional changes to an existing PR, add a comment summarizing the new commits.
|
|
||||||
This keeps reviewers informed of what changed since the initial PR description.
|
|
||||||
|
|
||||||
Use the `tea` CLI to add comments to pull requests:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
tea comment <number> "Comment text"
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Examples
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Add a comment to PR #42
|
|
||||||
tea comment 42 "Updated implementation based on feedback"
|
|
||||||
|
|
||||||
# Add a multi-line comment
|
|
||||||
tea comment 42 "Summary of changes:
|
|
||||||
- Fixed bug in reducer
|
|
||||||
- Added new tests"
|
|
||||||
```
|
|
||||||
|
|||||||
98
cmd/lambda/engine_test.go
Normal file
98
cmd/lambda/engine_test.go
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.maximhutz.com/max/lambda/internal/config"
|
||||||
|
"git.maximhutz.com/max/lambda/internal/engine"
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/convert"
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/saccharine"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEngineEquivalence(t *testing.T) {
|
||||||
|
testsDir := "../../tests"
|
||||||
|
files, err := os.ReadDir(testsDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to read tests directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
if !strings.HasSuffix(file.Name(), ".test") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
testName := strings.TrimSuffix(file.Name(), ".test")
|
||||||
|
t.Run(testName, func(t *testing.T) {
|
||||||
|
// Read test input
|
||||||
|
inputPath := filepath.Join(testsDir, file.Name())
|
||||||
|
input, err := os.ReadFile(inputPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to read test file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse syntax tree
|
||||||
|
ast, err := saccharine.Parse(string(input))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to parse input: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test lambda engine
|
||||||
|
lambdaExpr := convert.SaccharineToLambda(ast)
|
||||||
|
lambdaCfg := &config.Config{Interpreter: "lambda"}
|
||||||
|
lambdaEngine := engine.NewLambdaEngine(lambdaCfg, &lambdaExpr)
|
||||||
|
lambdaEngine.Run()
|
||||||
|
lambdaResult := lambdaEngine.GetResult()
|
||||||
|
|
||||||
|
// Test De Bruijn engine
|
||||||
|
debruijnExpr := convert.SaccharineToDeBruijn(ast)
|
||||||
|
debruijnCfg := &config.Config{Interpreter: "debruijn"}
|
||||||
|
debruijnEngine := engine.NewDeBruijnEngine(debruijnCfg, &debruijnExpr)
|
||||||
|
debruijnEngine.Run()
|
||||||
|
debruijnResult := debruijnEngine.GetResult()
|
||||||
|
|
||||||
|
// Convert De Bruijn result back to lambda for comparison
|
||||||
|
debruijnConverted := convert.DeBruijnToLambda(*debruijnEngine.Expression)
|
||||||
|
debruijnConvertedStr := convert.DeBruijnToLambda(*debruijnEngine.Expression)
|
||||||
|
|
||||||
|
// Check if expected file exists
|
||||||
|
expectedPath := filepath.Join(testsDir, testName+".expected")
|
||||||
|
if expectedBytes, err := os.ReadFile(expectedPath); err == nil {
|
||||||
|
expected := strings.TrimSpace(string(expectedBytes))
|
||||||
|
|
||||||
|
if lambdaResult != expected {
|
||||||
|
t.Errorf("Lambda engine result mismatch:\nExpected: %s\nGot: %s", expected, lambdaResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
// De Bruijn result will have different variable names, so we just check it runs
|
||||||
|
if debruijnResult == "" {
|
||||||
|
t.Errorf("De Bruijn engine produced empty result")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log results for comparison
|
||||||
|
t.Logf("Lambda result: %s", lambdaResult)
|
||||||
|
t.Logf("De Bruijn result: %s", debruijnResult)
|
||||||
|
t.Logf("De Bruijn converted: %v", debruijnConvertedStr)
|
||||||
|
_ = debruijnConverted // Suppress unused warning
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInvalidInterpreterFlag(t *testing.T) {
|
||||||
|
// This would be tested at the config level
|
||||||
|
cfg := &config.Config{Interpreter: "invalid"}
|
||||||
|
|
||||||
|
// The validation happens in FromArgs, but we can test the engine creation
|
||||||
|
// doesn't panic with invalid values
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Errorf("Engine creation panicked with invalid interpreter: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Just check that default behavior works
|
||||||
|
_ = cfg
|
||||||
|
}
|
||||||
@@ -1,15 +1,17 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"git.maximhutz.com/max/lambda/internal/cli"
|
"git.maximhutz.com/max/lambda/internal/cli"
|
||||||
"git.maximhutz.com/max/lambda/internal/config"
|
"git.maximhutz.com/max/lambda/internal/config"
|
||||||
"git.maximhutz.com/max/lambda/internal/plugins"
|
"git.maximhutz.com/max/lambda/internal/engine"
|
||||||
|
"git.maximhutz.com/max/lambda/internal/performance"
|
||||||
|
"git.maximhutz.com/max/lambda/internal/statistics"
|
||||||
"git.maximhutz.com/max/lambda/pkg/convert"
|
"git.maximhutz.com/max/lambda/pkg/convert"
|
||||||
"git.maximhutz.com/max/lambda/pkg/debruijn"
|
"git.maximhutz.com/max/lambda/pkg/debruijn"
|
||||||
"git.maximhutz.com/max/lambda/pkg/lambda"
|
"git.maximhutz.com/max/lambda/pkg/lambda"
|
||||||
"git.maximhutz.com/max/lambda/pkg/reducer"
|
|
||||||
"git.maximhutz.com/max/lambda/pkg/saccharine"
|
"git.maximhutz.com/max/lambda/pkg/saccharine"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -31,55 +33,92 @@ func main() {
|
|||||||
cli.HandleError(err)
|
cli.HandleError(err)
|
||||||
logger.Info("parsed syntax tree", "tree", ast)
|
logger.Info("parsed syntax tree", "tree", ast)
|
||||||
|
|
||||||
// Compile expression to lambda calculus.
|
// Create reduction engine based on interpreter type.
|
||||||
compiled := convert.SaccharineToLambda(ast)
|
var process engine.Engine
|
||||||
logger.Info("compiled λ expression", "tree", compiled.String())
|
if options.Interpreter == "debruijn" {
|
||||||
|
// Compile expression to De Bruijn indices.
|
||||||
// Create reducer based on the selected interpreter.
|
compiled := convert.SaccharineToDeBruijn(ast)
|
||||||
var red reducer.Reducer
|
logger.Info("compiled De Bruijn expression", "tree", debruijn.Stringify(compiled))
|
||||||
switch options.Interpreter {
|
dbEngine := engine.NewDeBruijnEngine(options, &compiled)
|
||||||
case config.DeBruijnInterpreter:
|
|
||||||
dbExpr := convert.LambdaToDeBruijn(compiled)
|
|
||||||
logger.Info("converted to De Bruijn", "tree", dbExpr.String())
|
|
||||||
red = debruijn.NewNormalOrderReducer(&dbExpr)
|
|
||||||
default:
|
|
||||||
red = lambda.NewNormalOrderReducer(&compiled)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the user selected to track CPU performance, attach a profiler.
|
// If the user selected to track CPU performance, attach a profiler.
|
||||||
if options.Profile != "" {
|
if options.Profile != "" {
|
||||||
plugins.NewPerformance(options.Profile, red)
|
profiler := performance.Track(options.Profile)
|
||||||
|
dbEngine.On("start", profiler.Start)
|
||||||
|
dbEngine.On("end", profiler.End)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the user selected to produce a step-by-step explanation, attach an
|
// If the user selected to produce a step-by-step explanation, print steps.
|
||||||
// observer.
|
|
||||||
if options.Explanation {
|
if options.Explanation {
|
||||||
plugins.NewExplanation(red)
|
dbEngine.On("start", func() {
|
||||||
|
fmt.Println(debruijn.Stringify(*dbEngine.Expression))
|
||||||
|
})
|
||||||
|
dbEngine.On("step", func() {
|
||||||
|
fmt.Println(" =", debruijn.Stringify(*dbEngine.Expression))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the user opted to track statistics, attach a tracker.
|
// If the user opted to track statistics, attach a tracker.
|
||||||
if options.Statistics {
|
if options.Statistics {
|
||||||
plugins.NewStatistics(red)
|
statistics := statistics.Track()
|
||||||
|
dbEngine.On("start", statistics.Start)
|
||||||
|
dbEngine.On("step", statistics.Step)
|
||||||
|
dbEngine.On("end", statistics.End)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the user selected for verbose debug logs, attach a reduction tracker.
|
// If the user selected for verbose debug logs, attach a reduction tracker.
|
||||||
if options.Verbose {
|
if options.Verbose {
|
||||||
plugins.NewLogs(logger, red)
|
dbEngine.On("step", func() {
|
||||||
|
logger.Info("reduction", "tree", debruijn.Stringify(*dbEngine.Expression))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run reduction.
|
process = dbEngine
|
||||||
red.Reduce()
|
} else {
|
||||||
|
// Compile expression to lambda calculus.
|
||||||
|
compiled := convert.SaccharineToLambda(ast)
|
||||||
|
logger.Info("compiled λ expression", "tree", lambda.Stringify(compiled))
|
||||||
|
lambdaEngine := engine.NewLambdaEngine(options, &compiled)
|
||||||
|
|
||||||
|
// If the user selected to track CPU performance, attach a profiler.
|
||||||
|
if options.Profile != "" {
|
||||||
|
profiler := performance.Track(options.Profile)
|
||||||
|
lambdaEngine.On("start", profiler.Start)
|
||||||
|
lambdaEngine.On("end", profiler.End)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the user selected to produce a step-by-step explanation, print steps.
|
||||||
|
if options.Explanation {
|
||||||
|
lambdaEngine.On("start", func() {
|
||||||
|
fmt.Println(lambda.Stringify(*lambdaEngine.Expression))
|
||||||
|
})
|
||||||
|
lambdaEngine.On("step", func() {
|
||||||
|
fmt.Println(" =", lambda.Stringify(*lambdaEngine.Expression))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the user opted to track statistics, attach a tracker.
|
||||||
|
if options.Statistics {
|
||||||
|
statistics := statistics.Track()
|
||||||
|
lambdaEngine.On("start", statistics.Start)
|
||||||
|
lambdaEngine.On("step", statistics.Step)
|
||||||
|
lambdaEngine.On("end", statistics.End)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the user selected for verbose debug logs, attach a reduction tracker.
|
||||||
|
if options.Verbose {
|
||||||
|
lambdaEngine.On("step", func() {
|
||||||
|
logger.Info("reduction", "tree", lambda.Stringify(*lambdaEngine.Expression))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
process = lambdaEngine
|
||||||
|
}
|
||||||
|
|
||||||
|
process.Run()
|
||||||
|
|
||||||
// Return the final reduced result.
|
// Return the final reduced result.
|
||||||
// For De Bruijn, convert back to lambda for consistent output.
|
result := process.GetResult()
|
||||||
var result string
|
|
||||||
if options.Interpreter == config.DeBruijnInterpreter {
|
|
||||||
dbExpr := red.Expression().(debruijn.Expression)
|
|
||||||
lambdaExpr := convert.DeBruijnToLambda(dbExpr)
|
|
||||||
result = lambdaExpr.String()
|
|
||||||
} else {
|
|
||||||
result = red.Expression().String()
|
|
||||||
}
|
|
||||||
err = options.Destination.Write(result)
|
err = options.Destination.Write(result)
|
||||||
cli.HandleError(err)
|
cli.HandleError(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"git.maximhutz.com/max/lambda/internal/config"
|
||||||
|
"git.maximhutz.com/max/lambda/internal/engine"
|
||||||
"git.maximhutz.com/max/lambda/pkg/convert"
|
"git.maximhutz.com/max/lambda/pkg/convert"
|
||||||
"git.maximhutz.com/max/lambda/pkg/debruijn"
|
|
||||||
"git.maximhutz.com/max/lambda/pkg/lambda"
|
"git.maximhutz.com/max/lambda/pkg/lambda"
|
||||||
"git.maximhutz.com/max/lambda/pkg/saccharine"
|
"git.maximhutz.com/max/lambda/pkg/saccharine"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -30,43 +31,24 @@ func runSample(samplePath string) (string, error) {
|
|||||||
// Compile expression to lambda calculus.
|
// Compile expression to lambda calculus.
|
||||||
compiled := convert.SaccharineToLambda(ast)
|
compiled := convert.SaccharineToLambda(ast)
|
||||||
|
|
||||||
// Create and run the reducer.
|
// Create minimal config for benchmarking.
|
||||||
reducer := lambda.NewNormalOrderReducer(&compiled)
|
cfg := &config.Config{
|
||||||
reducer.Reduce()
|
Source: config.StringSource{Data: ""},
|
||||||
|
Destination: config.StdoutDestination{},
|
||||||
return reducer.Expression().String() + "\n", nil
|
Profile: "",
|
||||||
}
|
Explanation: false,
|
||||||
|
Statistics: false,
|
||||||
// Helper function to run a single sample through the De Bruijn interpreter.
|
Verbose: false,
|
||||||
func runSampleDeBruijn(samplePath string) (string, error) {
|
|
||||||
// Read the sample file.
|
|
||||||
input, err := os.ReadFile(samplePath)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse code into syntax tree.
|
// Create and run the engine.
|
||||||
ast, err := saccharine.Parse(string(input))
|
process := engine.New(cfg, &compiled)
|
||||||
if err != nil {
|
process.Run()
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compile expression to lambda calculus.
|
return lambda.Stringify(compiled) + "\n", nil
|
||||||
compiled := convert.SaccharineToLambda(ast)
|
|
||||||
|
|
||||||
// Convert to De Bruijn and run reducer.
|
|
||||||
dbExpr := convert.LambdaToDeBruijn(compiled)
|
|
||||||
reducer := debruijn.NewNormalOrderReducer(&dbExpr)
|
|
||||||
reducer.Reduce()
|
|
||||||
|
|
||||||
// Convert back to lambda for output.
|
|
||||||
result := reducer.Expression().(debruijn.Expression)
|
|
||||||
lambdaResult := convert.DeBruijnToLambda(result)
|
|
||||||
|
|
||||||
return lambdaResult.String() + "\n", nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test that all samples produce expected output with lambda interpreter.
|
// Test that all samples produce expected output.
|
||||||
func TestSamplesValidity(t *testing.T) {
|
func TestSamplesValidity(t *testing.T) {
|
||||||
// Discover all .test files in the tests directory.
|
// Discover all .test files in the tests directory.
|
||||||
testFiles, err := filepath.Glob("../../tests/*.test")
|
testFiles, err := filepath.Glob("../../tests/*.test")
|
||||||
@@ -95,35 +77,6 @@ func TestSamplesValidity(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test that all samples produce expected output with De Bruijn interpreter.
|
|
||||||
func TestSamplesValidityDeBruijn(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 := runSampleDeBruijn(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.
|
// Benchmark all samples using sub-benchmarks.
|
||||||
func BenchmarkSamples(b *testing.B) {
|
func BenchmarkSamples(b *testing.B) {
|
||||||
// Discover all .test files in the tests directory.
|
// Discover all .test files in the tests directory.
|
||||||
@@ -142,22 +95,3 @@ func BenchmarkSamples(b *testing.B) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Benchmark all samples using De Bruijn interpreter.
|
|
||||||
func BenchmarkSamplesDeBruijn(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 := runSampleDeBruijn(path)
|
|
||||||
assert.NoError(b, err, "Failed to run sample.")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,14 +1,6 @@
|
|||||||
// Package "config" parses ad handles the user settings given to the program.
|
// Package "config" parses ad handles the user settings given to the program.
|
||||||
package config
|
package config
|
||||||
|
|
||||||
// Interpreter specifies the reduction engine to use.
|
|
||||||
type Interpreter string
|
|
||||||
|
|
||||||
const (
|
|
||||||
LambdaInterpreter Interpreter = "lambda"
|
|
||||||
DeBruijnInterpreter Interpreter = "debruijn"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Configuration settings for the program.
|
// Configuration settings for the program.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Source Source // The source code given to the program.
|
Source Source // The source code given to the program.
|
||||||
@@ -17,5 +9,5 @@ type Config struct {
|
|||||||
Explanation bool // Whether or not to print an explanation of the reduction.
|
Explanation bool // Whether or not to print an explanation of the reduction.
|
||||||
Profile string // If not nil, print a CPU profile during execution.
|
Profile string // If not nil, print a CPU profile during execution.
|
||||||
Statistics bool // Whether or not to print statistics.
|
Statistics bool // Whether or not to print statistics.
|
||||||
Interpreter Interpreter // The interpreter engine to use.
|
Interpreter string // The interpreter to use: "lambda" or "debruijn".
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,20 +14,9 @@ func FromArgs() (*Config, error) {
|
|||||||
profile := flag.String("p", "", "CPU profiling. If an output file is defined, the program will profile its execution and dump its results into it.")
|
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.")
|
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).")
|
output := flag.String("o", "", "Output. If set, write result to the specified file. Use '-' for stdout (default).")
|
||||||
interpreter := flag.String("i", "lambda", "Interpreter. The reduction engine to use: 'lambda' or 'debruijn'.")
|
interpreter := flag.String("i", "lambda", "Interpreter. Choose 'lambda' or 'debruijn' reduction engine (default: lambda).")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
// Validate interpreter flag.
|
|
||||||
var interpType Interpreter
|
|
||||||
switch *interpreter {
|
|
||||||
case "lambda":
|
|
||||||
interpType = LambdaInterpreter
|
|
||||||
case "debruijn":
|
|
||||||
interpType = DeBruijnInterpreter
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("invalid interpreter: %s (must be 'lambda' or 'debruijn')", *interpreter)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse source type.
|
// Parse source type.
|
||||||
var source Source
|
var source Source
|
||||||
if *file != "" {
|
if *file != "" {
|
||||||
@@ -57,6 +46,11 @@ func FromArgs() (*Config, error) {
|
|||||||
destination = FileDestination{Path: *output}
|
destination = FileDestination{Path: *output}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate interpreter flag.
|
||||||
|
if *interpreter != "lambda" && *interpreter != "debruijn" {
|
||||||
|
return nil, fmt.Errorf("invalid interpreter: %s (must be 'lambda' or 'debruijn')", *interpreter)
|
||||||
|
}
|
||||||
|
|
||||||
return &Config{
|
return &Config{
|
||||||
Source: source,
|
Source: source,
|
||||||
Destination: destination,
|
Destination: destination,
|
||||||
@@ -64,6 +58,6 @@ func FromArgs() (*Config, error) {
|
|||||||
Explanation: *explanation,
|
Explanation: *explanation,
|
||||||
Profile: *profile,
|
Profile: *profile,
|
||||||
Statistics: *statistics,
|
Statistics: *statistics,
|
||||||
Interpreter: interpType,
|
Interpreter: *interpreter,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
36
internal/engine/debruijn_engine.go
Normal file
36
internal/engine/debruijn_engine.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package engine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.maximhutz.com/max/lambda/internal/config"
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/debruijn"
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/emitter"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A process for reducing one λ-expression using De Bruijn indices.
|
||||||
|
type DeBruijnEngine struct {
|
||||||
|
Config *config.Config
|
||||||
|
Expression *debruijn.Expression
|
||||||
|
emitter.Emitter
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDeBruijnEngine creates a new De Bruijn engine.
|
||||||
|
func NewDeBruijnEngine(config *config.Config, expression interface{}) *DeBruijnEngine {
|
||||||
|
expr := expression.(*debruijn.Expression)
|
||||||
|
return &DeBruijnEngine{Config: config, Expression: expr}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run begins the reduction process.
|
||||||
|
func (e *DeBruijnEngine) Run() {
|
||||||
|
e.Emit("start")
|
||||||
|
|
||||||
|
debruijn.ReduceAll(e.Expression, func() {
|
||||||
|
e.Emit("step")
|
||||||
|
})
|
||||||
|
|
||||||
|
e.Emit("end")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetResult returns the stringified result.
|
||||||
|
func (e *DeBruijnEngine) GetResult() string {
|
||||||
|
return debruijn.Stringify(*e.Expression)
|
||||||
|
}
|
||||||
24
internal/engine/interface.go
Normal file
24
internal/engine/interface.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// Package "engine" provides an extensible interface for users to interfact with
|
||||||
|
// λ-calculus.
|
||||||
|
package engine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.maximhutz.com/max/lambda/internal/config"
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/emitter"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Engine is an interface for reduction engines.
|
||||||
|
type Engine interface {
|
||||||
|
Run()
|
||||||
|
GetResult() string
|
||||||
|
On(message string, fn func()) *emitter.Observer
|
||||||
|
Emit(message string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates the appropriate engine based on the config.
|
||||||
|
func New(cfg *config.Config, input interface{}) Engine {
|
||||||
|
if cfg.Interpreter == "debruijn" {
|
||||||
|
return NewDeBruijnEngine(cfg, input)
|
||||||
|
}
|
||||||
|
return NewLambdaEngine(cfg, input)
|
||||||
|
}
|
||||||
36
internal/engine/lambda_engine.go
Normal file
36
internal/engine/lambda_engine.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package engine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.maximhutz.com/max/lambda/internal/config"
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/emitter"
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/lambda"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A process for reducing one λ-expression using named variables.
|
||||||
|
type LambdaEngine struct {
|
||||||
|
Config *config.Config
|
||||||
|
Expression *lambda.Expression
|
||||||
|
emitter.Emitter
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLambdaEngine creates a new lambda engine.
|
||||||
|
func NewLambdaEngine(config *config.Config, expression interface{}) *LambdaEngine {
|
||||||
|
expr := expression.(*lambda.Expression)
|
||||||
|
return &LambdaEngine{Config: config, Expression: expr}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run begins the reduction process.
|
||||||
|
func (e *LambdaEngine) Run() {
|
||||||
|
e.Emit("start")
|
||||||
|
|
||||||
|
lambda.ReduceAll(e.Expression, func() {
|
||||||
|
e.Emit("step")
|
||||||
|
})
|
||||||
|
|
||||||
|
e.Emit("end")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetResult returns the stringified result.
|
||||||
|
func (e *LambdaEngine) GetResult() string {
|
||||||
|
return lambda.Stringify(*e.Expression)
|
||||||
|
}
|
||||||
32
internal/explanation/tracker.go
Normal file
32
internal/explanation/tracker.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// Package "explanation" provides a observer to gather the reasoning during the
|
||||||
|
// reduction, and present a thorough explanation to the user for each step.
|
||||||
|
package explanation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.maximhutz.com/max/lambda/internal/engine"
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/lambda"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Track the reductions made by a reduction proess.
|
||||||
|
type Tracker struct {
|
||||||
|
process *engine.Engine
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attaches a new explanation tracker to a process.
|
||||||
|
func Track(process *engine.Engine) *Tracker {
|
||||||
|
tracker := &Tracker{process: process}
|
||||||
|
process.On("start", tracker.Start)
|
||||||
|
process.On("step", tracker.Step)
|
||||||
|
|
||||||
|
return tracker
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tracker) Start() {
|
||||||
|
fmt.Println(lambda.Stringify(*t.process.Expression))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tracker) Step() {
|
||||||
|
fmt.Println(" =", lambda.Stringify(*t.process.Expression))
|
||||||
|
}
|
||||||
@@ -1,34 +1,28 @@
|
|||||||
// Package "performance" provides a tracker to observer CPU performance during
|
// Package "performance" provides a tracker to observer CPU performance during
|
||||||
// execution.
|
// execution.
|
||||||
package plugins
|
package performance
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime/pprof"
|
"runtime/pprof"
|
||||||
|
|
||||||
"git.maximhutz.com/max/lambda/pkg/reducer"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Observes a reduction process, and publishes a CPU performance profile on
|
// Observes a reduction process, and publishes a CPU performance profile on
|
||||||
// completion.
|
// completion.
|
||||||
type Performance struct {
|
type Tracker struct {
|
||||||
File string
|
File string
|
||||||
filePointer *os.File
|
filePointer *os.File
|
||||||
Error error
|
Error error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a performance tracker that outputs a profile to "file".
|
// Create a performance tracker that outputs a profile to "file".
|
||||||
func NewPerformance(file string, process reducer.Reducer) *Performance {
|
func Track(file string) *Tracker {
|
||||||
plugin := &Performance{File: file}
|
return &Tracker{File: file}
|
||||||
process.On(reducer.StartEvent, plugin.Start)
|
|
||||||
process.On(reducer.StopEvent, plugin.Stop)
|
|
||||||
|
|
||||||
return plugin
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Begin profiling.
|
// Begin profiling.
|
||||||
func (t *Performance) Start() {
|
func (t *Tracker) Start() {
|
||||||
var absPath string
|
var absPath string
|
||||||
|
|
||||||
absPath, t.Error = filepath.Abs(t.File)
|
absPath, t.Error = filepath.Abs(t.File)
|
||||||
@@ -53,7 +47,7 @@ func (t *Performance) Start() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Stop profiling.
|
// Stop profiling.
|
||||||
func (t *Performance) Stop() {
|
func (t *Tracker) End() {
|
||||||
pprof.StopCPUProfile()
|
pprof.StopCPUProfile()
|
||||||
t.filePointer.Close()
|
t.filePointer.Close()
|
||||||
}
|
}
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
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())
|
|
||||||
}
|
|
||||||
@@ -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/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())
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
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())
|
|
||||||
}
|
|
||||||
36
internal/statistics/tracker.go
Normal file
36
internal/statistics/tracker.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package statistics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// An observer, to track reduction performance.
|
||||||
|
type Tracker struct {
|
||||||
|
start time.Time
|
||||||
|
steps uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new reduction performance tracker.
|
||||||
|
func Track() *Tracker {
|
||||||
|
return &Tracker{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tracker) Start() {
|
||||||
|
t.start = time.Now()
|
||||||
|
t.steps = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tracker) Step() {
|
||||||
|
t.steps++
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tracker) End() {
|
||||||
|
results := Results{
|
||||||
|
StepsTaken: t.steps,
|
||||||
|
TimeElapsed: uint64(time.Since(t.start).Milliseconds()),
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprint(os.Stderr, results.String())
|
||||||
|
}
|
||||||
@@ -5,77 +5,58 @@ import (
|
|||||||
|
|
||||||
"git.maximhutz.com/max/lambda/pkg/debruijn"
|
"git.maximhutz.com/max/lambda/pkg/debruijn"
|
||||||
"git.maximhutz.com/max/lambda/pkg/lambda"
|
"git.maximhutz.com/max/lambda/pkg/lambda"
|
||||||
"git.maximhutz.com/max/lambda/pkg/set"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// DeBruijnToLambda converts a De Bruijn indexed expression back to named lambda calculus.
|
// DeBruijnToLambda converts a De Bruijn expression back to named lambda calculus.
|
||||||
func DeBruijnToLambda(expr debruijn.Expression) lambda.Expression {
|
func DeBruijnToLambda(expr debruijn.Expression) lambda.Expression {
|
||||||
return deBruijnToLambdaWithContext(expr, []string{})
|
return deBruijnToLambda(expr, []string{})
|
||||||
}
|
}
|
||||||
|
|
||||||
func deBruijnToLambdaWithContext(expr debruijn.Expression, context []string) lambda.Expression {
|
func deBruijnToLambda(expr debruijn.Expression, context []string) lambda.Expression {
|
||||||
switch e := expr.(type) {
|
switch e := expr.(type) {
|
||||||
case *debruijn.Variable:
|
case *debruijn.Variable:
|
||||||
index := e.Index()
|
if e.Index() >= 0 && e.Index() < len(context) {
|
||||||
if index < len(context) {
|
return lambda.NewVariable(context[e.Index()])
|
||||||
// Bound variable: look up name in context.
|
|
||||||
name := context[len(context)-1-index]
|
|
||||||
return lambda.NewVariable(name)
|
|
||||||
}
|
}
|
||||||
// Free variable: use the label if available.
|
|
||||||
if e.Label() != "" {
|
if e.Label() != "" {
|
||||||
return lambda.NewVariable(e.Label())
|
return lambda.NewVariable(e.Label())
|
||||||
}
|
}
|
||||||
// Generate a name for free variables without labels.
|
return lambda.NewVariable(fmt.Sprintf("free_%d", e.Index()))
|
||||||
return lambda.NewVariable(fmt.Sprintf("free%d", index))
|
|
||||||
|
|
||||||
case *debruijn.Abstraction:
|
case *debruijn.Abstraction:
|
||||||
// Generate a fresh parameter name.
|
paramName := generateParamName(context)
|
||||||
used := collectUsedNames(e.Body(), context)
|
newContext := append([]string{paramName}, context...)
|
||||||
paramName := generateFreshName(used)
|
body := deBruijnToLambda(e.Body(), newContext)
|
||||||
newContext := append(context, paramName)
|
|
||||||
body := deBruijnToLambdaWithContext(e.Body(), newContext)
|
|
||||||
return lambda.NewAbstraction(paramName, body)
|
return lambda.NewAbstraction(paramName, body)
|
||||||
|
|
||||||
case *debruijn.Application:
|
case *debruijn.Application:
|
||||||
abs := deBruijnToLambdaWithContext(e.Abstraction(), context)
|
abs := deBruijnToLambda(e.Abstraction(), context)
|
||||||
arg := deBruijnToLambdaWithContext(e.Argument(), context)
|
arg := deBruijnToLambda(e.Argument(), context)
|
||||||
return lambda.NewApplication(abs, arg)
|
return lambda.NewApplication(abs, arg)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
panic("unknown expression type")
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// collectUsedNames gathers all variable labels used in an expression.
|
// generateParamName generates a fresh parameter name that doesn't conflict with context.
|
||||||
func collectUsedNames(expr debruijn.Expression, context []string) *set.Set[string] {
|
func generateParamName(context []string) string {
|
||||||
used := set.New[string]()
|
base := 'a'
|
||||||
for _, name := range context {
|
|
||||||
used.Add(name)
|
|
||||||
}
|
|
||||||
collectUsedNamesHelper(expr, used)
|
|
||||||
return used
|
|
||||||
}
|
|
||||||
|
|
||||||
func collectUsedNamesHelper(expr debruijn.Expression, used *set.Set[string]) {
|
|
||||||
switch e := expr.(type) {
|
|
||||||
case *debruijn.Variable:
|
|
||||||
if e.Label() != "" {
|
|
||||||
used.Add(e.Label())
|
|
||||||
}
|
|
||||||
case *debruijn.Abstraction:
|
|
||||||
collectUsedNamesHelper(e.Body(), used)
|
|
||||||
case *debruijn.Application:
|
|
||||||
collectUsedNamesHelper(e.Abstraction(), used)
|
|
||||||
collectUsedNamesHelper(e.Argument(), used)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateFreshName creates a fresh variable name not in the used set.
|
|
||||||
func generateFreshName(used *set.Set[string]) string {
|
|
||||||
for i := 0; ; i++ {
|
for i := 0; ; i++ {
|
||||||
name := fmt.Sprintf("_%d", i)
|
name := string(rune(base + rune(i%26)))
|
||||||
if !used.Has(name) {
|
if i >= 26 {
|
||||||
|
name = fmt.Sprintf("%s%d", name, i/26)
|
||||||
|
}
|
||||||
|
|
||||||
|
conflict := false
|
||||||
|
for _, existing := range context {
|
||||||
|
if existing == name {
|
||||||
|
conflict = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !conflict {
|
||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,40 +5,39 @@ import (
|
|||||||
"git.maximhutz.com/max/lambda/pkg/lambda"
|
"git.maximhutz.com/max/lambda/pkg/lambda"
|
||||||
)
|
)
|
||||||
|
|
||||||
// LambdaToDeBruijn converts a lambda calculus expression to De Bruijn indexed form.
|
// LambdaToDeBruijn converts a lambda expression to De Bruijn index representation.
|
||||||
// The context parameter tracks bound variables from outer abstractions.
|
|
||||||
func LambdaToDeBruijn(expr lambda.Expression) debruijn.Expression {
|
func LambdaToDeBruijn(expr lambda.Expression) debruijn.Expression {
|
||||||
return lambdaToDeBruijnWithContext(expr, []string{})
|
return lambdaToDeBruijn(expr, []string{})
|
||||||
}
|
}
|
||||||
|
|
||||||
func lambdaToDeBruijnWithContext(expr lambda.Expression, context []string) debruijn.Expression {
|
func lambdaToDeBruijn(expr lambda.Expression, context []string) debruijn.Expression {
|
||||||
switch e := expr.(type) {
|
switch e := expr.(type) {
|
||||||
case *lambda.Variable:
|
case *lambda.Variable:
|
||||||
name := e.Value()
|
index := findIndex(e.Value(), context)
|
||||||
// Search for the variable in the context (innermost to outermost).
|
return debruijn.NewVariable(index, e.Value())
|
||||||
for i := len(context) - 1; i >= 0; i-- {
|
|
||||||
if context[i] == name {
|
|
||||||
index := len(context) - 1 - i
|
|
||||||
return debruijn.NewVariable(index, name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Free variable: use a negative index to mark it.
|
|
||||||
// We encode free variables with index = len(context) + position.
|
|
||||||
// For simplicity, we use a large index that won't conflict.
|
|
||||||
return debruijn.NewVariable(len(context), name)
|
|
||||||
|
|
||||||
case *lambda.Abstraction:
|
case *lambda.Abstraction:
|
||||||
// Add the parameter to the context.
|
newContext := append([]string{e.Parameter()}, context...)
|
||||||
newContext := append(context, e.Parameter())
|
body := lambdaToDeBruijn(e.Body(), newContext)
|
||||||
body := lambdaToDeBruijnWithContext(e.Body(), newContext)
|
|
||||||
return debruijn.NewAbstraction(body)
|
return debruijn.NewAbstraction(body)
|
||||||
|
|
||||||
case *lambda.Application:
|
case *lambda.Application:
|
||||||
abs := lambdaToDeBruijnWithContext(e.Abstraction(), context)
|
abs := lambdaToDeBruijn(e.Abstraction(), context)
|
||||||
arg := lambdaToDeBruijnWithContext(e.Argument(), context)
|
arg := lambdaToDeBruijn(e.Argument(), context)
|
||||||
return debruijn.NewApplication(abs, arg)
|
return debruijn.NewApplication(abs, arg)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
panic("unknown expression type")
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// findIndex returns the De Bruijn index for a variable name in the context.
|
||||||
|
// Returns the index if found, or -1 if the variable is free.
|
||||||
|
func findIndex(name string, context []string) int {
|
||||||
|
for i, v := range context {
|
||||||
|
if v == name {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|||||||
12
pkg/convert/saccharine_to_debruijn.go
Normal file
12
pkg/convert/saccharine_to_debruijn.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package convert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/debruijn"
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/saccharine"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SaccharineToDeBruijn converts a saccharine expression directly to De Bruijn indices.
|
||||||
|
func SaccharineToDeBruijn(expr saccharine.Expression) debruijn.Expression {
|
||||||
|
lambdaExpr := SaccharineToLambda(expr)
|
||||||
|
return LambdaToDeBruijn(lambdaExpr)
|
||||||
|
}
|
||||||
@@ -1,117 +1,75 @@
|
|||||||
// Package debruijn provides De Bruijn indexed lambda calculus expressions.
|
|
||||||
// De Bruijn indices eliminate the need for variable names by using numeric
|
|
||||||
// indices to refer to bound variables, avoiding capture issues during substitution.
|
|
||||||
package debruijn
|
package debruijn
|
||||||
|
|
||||||
import "git.maximhutz.com/max/lambda/pkg/expr"
|
|
||||||
|
|
||||||
// Expression is the interface for all De Bruijn indexed expression types.
|
|
||||||
// It embeds the general expr.Expression interface for cross-mode compatibility.
|
|
||||||
type Expression interface {
|
type Expression interface {
|
||||||
expr.Expression
|
|
||||||
Accept(Visitor)
|
Accept(Visitor)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** ------------------------------------------------------------------------- */
|
/** ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
// Abstraction represents a lambda abstraction without a named parameter.
|
|
||||||
// In De Bruijn notation, the parameter is implicit and referenced by index 0
|
|
||||||
// within the body.
|
|
||||||
type Abstraction struct {
|
type Abstraction struct {
|
||||||
body Expression
|
body Expression
|
||||||
}
|
}
|
||||||
|
|
||||||
// Body returns the body of the abstraction.
|
|
||||||
func (a *Abstraction) Body() Expression {
|
func (a *Abstraction) Body() Expression {
|
||||||
return a.body
|
return a.body
|
||||||
}
|
}
|
||||||
|
|
||||||
// Accept implements the Visitor pattern.
|
|
||||||
func (a *Abstraction) Accept(v Visitor) {
|
func (a *Abstraction) Accept(v Visitor) {
|
||||||
v.VisitAbstraction(a)
|
v.VisitAbstraction(a)
|
||||||
}
|
}
|
||||||
|
|
||||||
// String returns the De Bruijn notation string representation.
|
|
||||||
func (a *Abstraction) String() string {
|
|
||||||
return Stringify(a)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewAbstraction creates a new De Bruijn abstraction with the given body.
|
|
||||||
func NewAbstraction(body Expression) *Abstraction {
|
func NewAbstraction(body Expression) *Abstraction {
|
||||||
return &Abstraction{body: body}
|
return &Abstraction{body: body}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** ------------------------------------------------------------------------- */
|
/** ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
// Application represents the application of one expression to another.
|
|
||||||
type Application struct {
|
type Application struct {
|
||||||
abstraction Expression
|
abstraction Expression
|
||||||
argument Expression
|
argument Expression
|
||||||
}
|
}
|
||||||
|
|
||||||
// Abstraction returns the function expression being applied.
|
|
||||||
func (a *Application) Abstraction() Expression {
|
func (a *Application) Abstraction() Expression {
|
||||||
return a.abstraction
|
return a.abstraction
|
||||||
}
|
}
|
||||||
|
|
||||||
// Argument returns the argument expression.
|
|
||||||
func (a *Application) Argument() Expression {
|
func (a *Application) Argument() Expression {
|
||||||
return a.argument
|
return a.argument
|
||||||
}
|
}
|
||||||
|
|
||||||
// Accept implements the Visitor pattern.
|
|
||||||
func (a *Application) Accept(v Visitor) {
|
func (a *Application) Accept(v Visitor) {
|
||||||
v.VisitApplication(a)
|
v.VisitApplication(a)
|
||||||
}
|
}
|
||||||
|
|
||||||
// String returns the De Bruijn notation string representation.
|
|
||||||
func (a *Application) String() string {
|
|
||||||
return Stringify(a)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewApplication creates a new application expression.
|
|
||||||
func NewApplication(abstraction Expression, argument Expression) *Application {
|
func NewApplication(abstraction Expression, argument Expression) *Application {
|
||||||
return &Application{abstraction: abstraction, argument: argument}
|
return &Application{abstraction: abstraction, argument: argument}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** ------------------------------------------------------------------------- */
|
/** ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
// Variable represents a De Bruijn indexed variable.
|
|
||||||
// The index indicates how many binders to skip to find the binding abstraction.
|
|
||||||
// The label is an optional hint for display purposes.
|
|
||||||
type Variable struct {
|
type Variable struct {
|
||||||
index int
|
index int
|
||||||
label string
|
label string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Index returns the De Bruijn index.
|
|
||||||
func (v *Variable) Index() int {
|
func (v *Variable) Index() int {
|
||||||
return v.index
|
return v.index
|
||||||
}
|
}
|
||||||
|
|
||||||
// Label returns the optional variable label.
|
|
||||||
func (v *Variable) Label() string {
|
func (v *Variable) Label() string {
|
||||||
return v.label
|
return v.label
|
||||||
}
|
}
|
||||||
|
|
||||||
// Accept implements the Visitor pattern.
|
|
||||||
func (v *Variable) Accept(visitor Visitor) {
|
func (v *Variable) Accept(visitor Visitor) {
|
||||||
visitor.VisitVariable(v)
|
visitor.VisitVariable(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
// String returns the De Bruijn notation string representation.
|
|
||||||
func (v *Variable) String() string {
|
|
||||||
return Stringify(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewVariable creates a new De Bruijn variable with the given index and label.
|
|
||||||
func NewVariable(index int, label string) *Variable {
|
func NewVariable(index int, label string) *Variable {
|
||||||
return &Variable{index: index, label: label}
|
return &Variable{index: index, label: label}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** ------------------------------------------------------------------------- */
|
/** ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
// Visitor interface for traversing De Bruijn expressions.
|
|
||||||
type Visitor interface {
|
type Visitor interface {
|
||||||
VisitAbstraction(*Abstraction)
|
VisitAbstraction(*Abstraction)
|
||||||
VisitApplication(*Application)
|
VisitApplication(*Application)
|
||||||
|
|||||||
@@ -1,21 +1,17 @@
|
|||||||
package debruijn
|
package debruijn
|
||||||
|
|
||||||
// Iterator provides depth-first traversal of De Bruijn expressions.
|
|
||||||
type Iterator struct {
|
type Iterator struct {
|
||||||
trace []*Expression
|
trace []*Expression
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewIterator creates a new iterator starting at the given expression.
|
|
||||||
func NewIterator(expr *Expression) *Iterator {
|
func NewIterator(expr *Expression) *Iterator {
|
||||||
return &Iterator{[]*Expression{expr}}
|
return &Iterator{[]*Expression{expr}}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Done returns true when the iterator has finished traversal.
|
|
||||||
func (i *Iterator) Done() bool {
|
func (i *Iterator) Done() bool {
|
||||||
return len(i.trace) == 0
|
return len(i.trace) == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Current returns a pointer to the current expression.
|
|
||||||
func (i *Iterator) Current() *Expression {
|
func (i *Iterator) Current() *Expression {
|
||||||
if i.Done() {
|
if i.Done() {
|
||||||
return nil
|
return nil
|
||||||
@@ -24,7 +20,6 @@ func (i *Iterator) Current() *Expression {
|
|||||||
return i.trace[len(i.trace)-1]
|
return i.trace[len(i.trace)-1]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parent returns a pointer to the parent expression.
|
|
||||||
func (i *Iterator) Parent() *Expression {
|
func (i *Iterator) Parent() *Expression {
|
||||||
if len(i.trace) < 2 {
|
if len(i.trace) < 2 {
|
||||||
return nil
|
return nil
|
||||||
@@ -33,7 +28,6 @@ func (i *Iterator) Parent() *Expression {
|
|||||||
return i.trace[len(i.trace)-2]
|
return i.trace[len(i.trace)-2]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Swap replaces the current expression with the given expression.
|
|
||||||
func (i *Iterator) Swap(with Expression) {
|
func (i *Iterator) Swap(with Expression) {
|
||||||
current := i.Current()
|
current := i.Current()
|
||||||
if current != nil {
|
if current != nil {
|
||||||
@@ -41,7 +35,6 @@ func (i *Iterator) Swap(with Expression) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Back moves the iterator back to the parent expression.
|
|
||||||
func (i *Iterator) Back() bool {
|
func (i *Iterator) Back() bool {
|
||||||
if i.Done() {
|
if i.Done() {
|
||||||
return false
|
return false
|
||||||
@@ -51,7 +44,6 @@ func (i *Iterator) Back() bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Next advances the iterator to the next expression in leftmost-outermost order.
|
|
||||||
func (i *Iterator) Next() {
|
func (i *Iterator) Next() {
|
||||||
switch typed := (*i.Current()).(type) {
|
switch typed := (*i.Current()).(type) {
|
||||||
case *Abstraction:
|
case *Abstraction:
|
||||||
|
|||||||
30
pkg/debruijn/reduce.go
Normal file
30
pkg/debruijn/reduce.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package debruijn
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReduceAll(e *Expression, step func()) {
|
||||||
|
it := NewIterator(e)
|
||||||
|
|
||||||
|
for !it.Done() {
|
||||||
|
if fn, arg, ok := IsViable(it.Current()); !ok {
|
||||||
|
it.Next()
|
||||||
|
} else {
|
||||||
|
it.Swap(Substitute(fn.body, arg))
|
||||||
|
step()
|
||||||
|
|
||||||
|
if _, _, ok := IsViable(it.Parent()); ok {
|
||||||
|
it.Back()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
package debruijn
|
|
||||||
|
|
||||||
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 De Bruijn indexed 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// isViable checks if an expression is a redex (reducible expression).
|
|
||||||
// A redex is an application of an abstraction to an argument.
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// betaReduce performs a single beta reduction step.
|
|
||||||
// Given (\. body) arg, it substitutes arg for index 0 in body,
|
|
||||||
// then shifts the result down to account for the removed abstraction.
|
|
||||||
func betaReduce(fn *Abstraction, arg Expression) Expression {
|
|
||||||
// Substitute arg for variable 0 in the body.
|
|
||||||
substituted := Substitute(fn.body, 0, Shift(arg, 1, 0))
|
|
||||||
// Shift down to account for the removed abstraction.
|
|
||||||
return Shift(substituted, -1, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reduce performs normal order reduction on a De Bruijn expression.
|
|
||||||
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(betaReduce(fn, arg))
|
|
||||||
r.Emit(reducer.StepEvent)
|
|
||||||
|
|
||||||
if _, _, ok := isViable(it.Parent()); ok {
|
|
||||||
it.Back()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
r.Emit(reducer.StopEvent)
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
package debruijn
|
|
||||||
|
|
||||||
// Shift increments all free variable indices in an expression by the given amount.
|
|
||||||
// A variable is free if its index is >= the cutoff (depth of nested abstractions).
|
|
||||||
// This is necessary when substituting an expression into a different binding context.
|
|
||||||
func Shift(expr Expression, amount int, cutoff int) Expression {
|
|
||||||
switch e := expr.(type) {
|
|
||||||
case *Variable:
|
|
||||||
if e.index >= cutoff {
|
|
||||||
return NewVariable(e.index+amount, e.label)
|
|
||||||
}
|
|
||||||
return e
|
|
||||||
|
|
||||||
case *Abstraction:
|
|
||||||
newBody := Shift(e.body, amount, cutoff+1)
|
|
||||||
if newBody == e.body {
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
return NewAbstraction(newBody)
|
|
||||||
|
|
||||||
case *Application:
|
|
||||||
newAbs := Shift(e.abstraction, amount, cutoff)
|
|
||||||
newArg := Shift(e.argument, amount, cutoff)
|
|
||||||
if newAbs == e.abstraction && newArg == e.argument {
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
return NewApplication(newAbs, newArg)
|
|
||||||
|
|
||||||
default:
|
|
||||||
return expr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package debruijn
|
package debruijn
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strconv"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -10,7 +10,11 @@ type stringifyVisitor struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (v *stringifyVisitor) VisitVariable(a *Variable) {
|
func (v *stringifyVisitor) VisitVariable(a *Variable) {
|
||||||
v.builder.WriteString(strconv.Itoa(a.index))
|
if a.label != "" {
|
||||||
|
v.builder.WriteString(a.label)
|
||||||
|
} else {
|
||||||
|
v.builder.WriteString(fmt.Sprintf("%d", a.index))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *stringifyVisitor) VisitAbstraction(f *Abstraction) {
|
func (v *stringifyVisitor) VisitAbstraction(f *Abstraction) {
|
||||||
@@ -27,7 +31,6 @@ func (v *stringifyVisitor) VisitApplication(c *Application) {
|
|||||||
v.builder.WriteRune(')')
|
v.builder.WriteRune(')')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stringify converts a De Bruijn expression to its string representation.
|
|
||||||
func Stringify(e Expression) string {
|
func Stringify(e Expression) string {
|
||||||
b := &stringifyVisitor{builder: strings.Builder{}}
|
b := &stringifyVisitor{builder: strings.Builder{}}
|
||||||
e.Accept(b)
|
e.Accept(b)
|
||||||
|
|||||||
@@ -1,28 +1,62 @@
|
|||||||
package debruijn
|
package debruijn
|
||||||
|
|
||||||
// Substitute replaces the variable at the given index with the replacement expression.
|
// Shift increments all free variable indices by delta when crossing depth abstractions.
|
||||||
// The replacement is shifted appropriately as we descend into nested abstractions.
|
func Shift(expr Expression, delta int, depth int) Expression {
|
||||||
func Substitute(expr Expression, index int, replacement Expression) Expression {
|
|
||||||
switch e := expr.(type) {
|
switch e := expr.(type) {
|
||||||
case *Variable:
|
case *Variable:
|
||||||
if e.index == index {
|
if e.index >= depth {
|
||||||
return replacement
|
return NewVariable(e.index+delta, e.label)
|
||||||
}
|
}
|
||||||
return e
|
return e
|
||||||
|
|
||||||
case *Abstraction:
|
case *Abstraction:
|
||||||
// When entering an abstraction, increment the target index and shift the
|
newBody := Shift(e.body, delta, depth+1)
|
||||||
// replacement to account for the new binding context.
|
|
||||||
shiftedReplacement := Shift(replacement, 1, 0)
|
|
||||||
newBody := Substitute(e.body, index+1, shiftedReplacement)
|
|
||||||
if newBody == e.body {
|
if newBody == e.body {
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
return NewAbstraction(newBody)
|
return NewAbstraction(newBody)
|
||||||
|
|
||||||
case *Application:
|
case *Application:
|
||||||
newAbs := Substitute(e.abstraction, index, replacement)
|
newAbs := Shift(e.abstraction, delta, depth)
|
||||||
newArg := Substitute(e.argument, index, replacement)
|
newArg := Shift(e.argument, delta, depth)
|
||||||
|
if newAbs == e.abstraction && newArg == e.argument {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
return NewApplication(newAbs, newArg)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return expr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Substitute replaces variable at index 0 with replacement in expr.
|
||||||
|
// This assumes expr is the body of an abstraction being applied.
|
||||||
|
func Substitute(expr Expression, replacement Expression) Expression {
|
||||||
|
return substitute(expr, 0, replacement)
|
||||||
|
}
|
||||||
|
|
||||||
|
// substitute replaces variable at targetIndex with replacement, adjusting indices as needed.
|
||||||
|
func substitute(expr Expression, targetIndex int, replacement Expression) Expression {
|
||||||
|
switch e := expr.(type) {
|
||||||
|
case *Variable:
|
||||||
|
if e.index == targetIndex {
|
||||||
|
return Shift(replacement, targetIndex, 0)
|
||||||
|
}
|
||||||
|
if e.index > targetIndex {
|
||||||
|
return NewVariable(e.index-1, e.label)
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
|
||||||
|
case *Abstraction:
|
||||||
|
newBody := substitute(e.body, targetIndex+1, replacement)
|
||||||
|
if newBody == e.body {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
return NewAbstraction(newBody)
|
||||||
|
|
||||||
|
case *Application:
|
||||||
|
newAbs := substitute(e.abstraction, targetIndex, replacement)
|
||||||
|
newArg := substitute(e.argument, targetIndex, replacement)
|
||||||
if newAbs == e.abstraction && newArg == e.argument {
|
if newAbs == e.abstraction && newArg == e.argument {
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,45 +2,53 @@ package emitter
|
|||||||
|
|
||||||
import "git.maximhutz.com/max/lambda/pkg/set"
|
import "git.maximhutz.com/max/lambda/pkg/set"
|
||||||
|
|
||||||
type Emitter[E comparable] interface {
|
type Observer struct {
|
||||||
On(E, func()) Listener[E]
|
fn func()
|
||||||
Off(Listener[E])
|
message string
|
||||||
Emit(E)
|
emitter *Emitter
|
||||||
}
|
}
|
||||||
|
|
||||||
type BaseEmitter[E comparable] struct {
|
type Emitter struct {
|
||||||
listeners map[E]*set.Set[Listener[E]]
|
listeners map[string]*set.Set[*Observer]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *BaseEmitter[E]) On(kind E, fn func()) Listener[E] {
|
func Ignore[T any](fn func()) func(T) {
|
||||||
if e.listeners[kind] == nil {
|
return func(T) { fn() }
|
||||||
e.listeners[kind] = set.New[Listener[E]]()
|
}
|
||||||
|
|
||||||
|
func (e *Emitter) On(message string, fn func()) *Observer {
|
||||||
|
observer := &Observer{
|
||||||
|
fn: fn,
|
||||||
|
message: message,
|
||||||
|
emitter: e,
|
||||||
}
|
}
|
||||||
|
|
||||||
listener := &BaseListener[E]{kind, fn}
|
if e.listeners == nil {
|
||||||
e.listeners[kind].Add(listener)
|
e.listeners = map[string]*set.Set[*Observer]{}
|
||||||
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() {
|
if e.listeners[message] == nil {
|
||||||
listener.Run()
|
e.listeners[message] = set.New[*Observer]()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
e.listeners[message].Add(observer)
|
||||||
|
return observer
|
||||||
}
|
}
|
||||||
|
|
||||||
func New[E comparable]() *BaseEmitter[E] {
|
func (o *Observer) Off() {
|
||||||
return &BaseEmitter[E]{
|
if o.emitter.listeners[o.message] == nil {
|
||||||
listeners: map[E]*set.Set[Listener[E]]{},
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
o.emitter.listeners[o.message].Remove(o)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Emitter) Emit(message string) {
|
||||||
|
if e.listeners[message] == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for listener := range *e.listeners[message] {
|
||||||
|
listener.fn()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
// Package expr provides the abstract Expression interface for all evaluatable
|
|
||||||
// expression types in the lambda interpreter.
|
|
||||||
package expr
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
// String returns a human-readable representation of the expression.
|
|
||||||
String() string
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,6 @@
|
|||||||
package lambda
|
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 {
|
type Expression interface {
|
||||||
expr.Expression
|
|
||||||
Accept(Visitor)
|
Accept(Visitor)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,10 +23,6 @@ func (a *Abstraction) Accept(v Visitor) {
|
|||||||
v.VisitAbstraction(a)
|
v.VisitAbstraction(a)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Abstraction) String() string {
|
|
||||||
return Stringify(a)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewAbstraction(parameter string, body Expression) *Abstraction {
|
func NewAbstraction(parameter string, body Expression) *Abstraction {
|
||||||
return &Abstraction{parameter: parameter, body: body}
|
return &Abstraction{parameter: parameter, body: body}
|
||||||
}
|
}
|
||||||
@@ -55,10 +46,6 @@ func (a *Application) Accept(v Visitor) {
|
|||||||
v.VisitApplication(a)
|
v.VisitApplication(a)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Application) String() string {
|
|
||||||
return Stringify(a)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewApplication(abstraction Expression, argument Expression) *Application {
|
func NewApplication(abstraction Expression, argument Expression) *Application {
|
||||||
return &Application{abstraction: abstraction, argument: argument}
|
return &Application{abstraction: abstraction, argument: argument}
|
||||||
}
|
}
|
||||||
@@ -77,10 +64,6 @@ func (v *Variable) Accept(visitor Visitor) {
|
|||||||
visitor.VisitVariable(v)
|
visitor.VisitVariable(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *Variable) String() string {
|
|
||||||
return Stringify(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewVariable(name string) *Variable {
|
func NewVariable(name string) *Variable {
|
||||||
return &Variable{value: name}
|
return &Variable{value: name}
|
||||||
}
|
}
|
||||||
|
|||||||
30
pkg/lambda/reduce.go
Normal file
30
pkg/lambda/reduce.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package lambda
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReduceAll(e *Expression, step func()) {
|
||||||
|
it := NewIterator(e)
|
||||||
|
|
||||||
|
for !it.Done() {
|
||||||
|
if fn, arg, ok := IsViable(it.Current()); !ok {
|
||||||
|
it.Next()
|
||||||
|
} else {
|
||||||
|
it.Swap(Substitute(fn.body, fn.parameter, arg))
|
||||||
|
step()
|
||||||
|
|
||||||
|
if _, _, ok := IsViable(it.Parent()); ok {
|
||||||
|
it.Back()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
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
|
|
||||||
)
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
// 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,7 +1,5 @@
|
|||||||
package set
|
package set
|
||||||
|
|
||||||
import "iter"
|
|
||||||
|
|
||||||
type Set[T comparable] map[T]bool
|
type Set[T comparable] map[T]bool
|
||||||
|
|
||||||
func (s *Set[T]) Add(items ...T) {
|
func (s *Set[T]) Add(items ...T) {
|
||||||
@@ -36,16 +34,6 @@ func (s Set[T]) ToList() []T {
|
|||||||
return list
|
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] {
|
func New[T comparable](items ...T) *Set[T] {
|
||||||
result := &Set[T]{}
|
result := &Set[T]{}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user