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
164 lines
4.8 KiB
Go
164 lines
4.8 KiB
Go
package main
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"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"
|
|
)
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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")
|
|
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.")
|
|
})
|
|
}
|
|
}
|
|
|
|
// 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.
|
|
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.")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// 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.")
|
|
}
|
|
})
|
|
}
|
|
}
|