feat: add De Bruijn indexed reduction engine

Add a new interpreter option (-i debruijn) that uses De Bruijn indices
for variable representation, eliminating the need for variable renaming
during substitution.

- Add -i flag to select interpreter (lambda or debruijn)
- Create debruijn package with Expression types (Variable with index,
  Abstraction without parameter, Application)
- Implement shift and substitute operations for De Bruijn indices
- Add conversion functions between lambda and De Bruijn representations
- Update CLI to support switching between interpreters
- Add De Bruijn tests to verify all samples pass

Closes #26
This commit is contained in:
2026-01-16 19:36:05 -05:00
parent 1974ad582f
commit 528956b033
12 changed files with 621 additions and 9 deletions

View File

@@ -7,7 +7,9 @@ import (
"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/debruijn"
"git.maximhutz.com/max/lambda/pkg/lambda"
"git.maximhutz.com/max/lambda/pkg/reducer"
"git.maximhutz.com/max/lambda/pkg/saccharine"
)
@@ -33,35 +35,51 @@ func main() {
compiled := convert.SaccharineToLambda(ast)
logger.Info("compiled λ expression", "tree", compiled.String())
// Create reducer with the compiled expression.
reducer := lambda.NewNormalOrderReducer(&compiled)
// Create reducer based on the selected interpreter.
var red reducer.Reducer
switch options.Interpreter {
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 options.Profile != "" {
plugins.NewPerformance(options.Profile, reducer)
plugins.NewPerformance(options.Profile, red)
}
// If the user selected to produce a step-by-step explanation, attach an
// observer.
if options.Explanation {
plugins.NewExplanation(reducer)
plugins.NewExplanation(red)
}
// If the user opted to track statistics, attach a tracker.
if options.Statistics {
plugins.NewStatistics(reducer)
plugins.NewStatistics(red)
}
// If the user selected for verbose debug logs, attach a reduction tracker.
if options.Verbose {
plugins.NewLogs(logger, reducer)
plugins.NewLogs(logger, red)
}
// Run reduction.
reducer.Reduce()
red.Reduce()
// Return the final reduced result.
result := reducer.Expression().String()
// For De Bruijn, convert back to lambda for consistent output.
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)
cli.HandleError(err)
}

View File

@@ -7,6 +7,7 @@ import (
"testing"
"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/saccharine"
"github.com/stretchr/testify/assert"
@@ -36,7 +37,36 @@ func runSample(samplePath string) (string, error) {
return reducer.Expression().String() + "\n", nil
}
// Test that all samples produce expected output.
// Helper function to run a single sample through the De Bruijn interpreter.
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.
ast, err := saccharine.Parse(string(input))
if err != nil {
return "", err
}
// Compile expression to lambda calculus.
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.
func TestSamplesValidity(t *testing.T) {
// Discover all .test files in the tests directory.
testFiles, err := filepath.Glob("../../tests/*.test")
@@ -65,6 +95,35 @@ 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.
func BenchmarkSamples(b *testing.B) {
// Discover all .test files in the tests directory.
@@ -83,3 +142,22 @@ 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.")
}
})
}
}