Compare commits
1 Commits
main
...
f3b9137d75
| Author | SHA1 | Date | |
|---|---|---|---|
|
f3b9137d75
|
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,16 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"git.maximhutz.com/max/lambda/internal/cli"
|
||||
"git.maximhutz.com/max/lambda/internal/config"
|
||||
"git.maximhutz.com/max/lambda/internal/engine"
|
||||
"git.maximhutz.com/max/lambda/internal/explanation"
|
||||
"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/debruijn"
|
||||
"git.maximhutz.com/max/lambda/pkg/lambda"
|
||||
"git.maximhutz.com/max/lambda/pkg/saccharine"
|
||||
)
|
||||
@@ -32,46 +33,92 @@ func main() {
|
||||
cli.HandleError(err)
|
||||
logger.Info("parsed syntax tree", "tree", ast)
|
||||
|
||||
// Compile expression to lambda calculus.
|
||||
compiled := convert.SaccharineToLambda(ast)
|
||||
logger.Info("compiled λ expression", "tree", lambda.Stringify(compiled))
|
||||
// Create reduction engine based on interpreter type.
|
||||
var process engine.Engine
|
||||
if options.Interpreter == "debruijn" {
|
||||
// Compile expression to De Bruijn indices.
|
||||
compiled := convert.SaccharineToDeBruijn(ast)
|
||||
logger.Info("compiled De Bruijn expression", "tree", debruijn.Stringify(compiled))
|
||||
dbEngine := engine.NewDeBruijnEngine(options, &compiled)
|
||||
|
||||
// Create reduction engine.
|
||||
process := engine.New(options, &compiled)
|
||||
|
||||
// If the user selected to track CPU performance, attach a profiler to the
|
||||
// process.
|
||||
// If the user selected to track CPU performance, attach a profiler.
|
||||
if options.Profile != "" {
|
||||
profiler := performance.Track(options.Profile)
|
||||
process.On("start", profiler.Start)
|
||||
process.On("end", profiler.End)
|
||||
dbEngine.On("start", profiler.Start)
|
||||
dbEngine.On("end", profiler.End)
|
||||
}
|
||||
|
||||
// If the user selected to produce a step-by-step explanation, attach an
|
||||
// observer here.
|
||||
// If the user selected to produce a step-by-step explanation, print steps.
|
||||
if options.Explanation {
|
||||
explanation.Track(process)
|
||||
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 here, too.
|
||||
// If the user opted to track statistics, attach a tracker.
|
||||
if options.Statistics {
|
||||
statistics := statistics.Track()
|
||||
process.On("start", statistics.Start)
|
||||
process.On("step", statistics.Step)
|
||||
process.On("end", statistics.End)
|
||||
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 options.Verbose {
|
||||
process.On("step", func() {
|
||||
logger.Info("reduction", "tree", lambda.Stringify(compiled))
|
||||
dbEngine.On("step", func() {
|
||||
logger.Info("reduction", "tree", debruijn.Stringify(*dbEngine.Expression))
|
||||
})
|
||||
}
|
||||
|
||||
process = dbEngine
|
||||
} 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.
|
||||
result := lambda.Stringify(compiled)
|
||||
result := process.GetResult()
|
||||
err = options.Destination.Write(result)
|
||||
cli.HandleError(err)
|
||||
}
|
||||
|
||||
@@ -9,4 +9,5 @@ type Config struct {
|
||||
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.
|
||||
Interpreter string // The interpreter to use: "lambda" or "debruijn".
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ 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.")
|
||||
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).")
|
||||
interpreter := flag.String("i", "lambda", "Interpreter. Choose 'lambda' or 'debruijn' reduction engine (default: lambda).")
|
||||
flag.Parse()
|
||||
|
||||
// Parse source type.
|
||||
@@ -45,6 +46,11 @@ func FromArgs() (*Config, error) {
|
||||
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{
|
||||
Source: source,
|
||||
Destination: destination,
|
||||
@@ -52,5 +58,6 @@ func FromArgs() (*Config, error) {
|
||||
Explanation: *explanation,
|
||||
Profile: *profile,
|
||||
Statistics: *statistics,
|
||||
Interpreter: *interpreter,
|
||||
}, 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)
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
// 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"
|
||||
"git.maximhutz.com/max/lambda/pkg/lambda"
|
||||
)
|
||||
|
||||
// A process for reducing one λ-expression.
|
||||
type Engine struct {
|
||||
Config *config.Config
|
||||
Expression *lambda.Expression
|
||||
emitter.Emitter
|
||||
}
|
||||
|
||||
// Create a new engine, given an unreduced λ-expression.
|
||||
func New(config *config.Config, expression *lambda.Expression) *Engine {
|
||||
return &Engine{Config: config, Expression: expression}
|
||||
}
|
||||
|
||||
// Begin the reduction process.
|
||||
func (e Engine) Run() {
|
||||
e.Emit("start")
|
||||
|
||||
lambda.ReduceAll(e.Expression, func() {
|
||||
e.Emit("step")
|
||||
})
|
||||
|
||||
e.Emit("end")
|
||||
}
|
||||
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)
|
||||
}
|
||||
63
pkg/convert/debruijn_to_lambda.go
Normal file
63
pkg/convert/debruijn_to_lambda.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.maximhutz.com/max/lambda/pkg/debruijn"
|
||||
"git.maximhutz.com/max/lambda/pkg/lambda"
|
||||
)
|
||||
|
||||
// DeBruijnToLambda converts a De Bruijn expression back to named lambda calculus.
|
||||
func DeBruijnToLambda(expr debruijn.Expression) lambda.Expression {
|
||||
return deBruijnToLambda(expr, []string{})
|
||||
}
|
||||
|
||||
func deBruijnToLambda(expr debruijn.Expression, context []string) lambda.Expression {
|
||||
switch e := expr.(type) {
|
||||
case *debruijn.Variable:
|
||||
if e.Index() >= 0 && e.Index() < len(context) {
|
||||
return lambda.NewVariable(context[e.Index()])
|
||||
}
|
||||
if e.Label() != "" {
|
||||
return lambda.NewVariable(e.Label())
|
||||
}
|
||||
return lambda.NewVariable(fmt.Sprintf("free_%d", e.Index()))
|
||||
|
||||
case *debruijn.Abstraction:
|
||||
paramName := generateParamName(context)
|
||||
newContext := append([]string{paramName}, context...)
|
||||
body := deBruijnToLambda(e.Body(), newContext)
|
||||
return lambda.NewAbstraction(paramName, body)
|
||||
|
||||
case *debruijn.Application:
|
||||
abs := deBruijnToLambda(e.Abstraction(), context)
|
||||
arg := deBruijnToLambda(e.Argument(), context)
|
||||
return lambda.NewApplication(abs, arg)
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// generateParamName generates a fresh parameter name that doesn't conflict with context.
|
||||
func generateParamName(context []string) string {
|
||||
base := 'a'
|
||||
for i := 0; ; i++ {
|
||||
name := string(rune(base + rune(i%26)))
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
43
pkg/convert/lambda_to_debruijn.go
Normal file
43
pkg/convert/lambda_to_debruijn.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"git.maximhutz.com/max/lambda/pkg/debruijn"
|
||||
"git.maximhutz.com/max/lambda/pkg/lambda"
|
||||
)
|
||||
|
||||
// LambdaToDeBruijn converts a lambda expression to De Bruijn index representation.
|
||||
func LambdaToDeBruijn(expr lambda.Expression) debruijn.Expression {
|
||||
return lambdaToDeBruijn(expr, []string{})
|
||||
}
|
||||
|
||||
func lambdaToDeBruijn(expr lambda.Expression, context []string) debruijn.Expression {
|
||||
switch e := expr.(type) {
|
||||
case *lambda.Variable:
|
||||
index := findIndex(e.Value(), context)
|
||||
return debruijn.NewVariable(index, e.Value())
|
||||
|
||||
case *lambda.Abstraction:
|
||||
newContext := append([]string{e.Parameter()}, context...)
|
||||
body := lambdaToDeBruijn(e.Body(), newContext)
|
||||
return debruijn.NewAbstraction(body)
|
||||
|
||||
case *lambda.Application:
|
||||
abs := lambdaToDeBruijn(e.Abstraction(), context)
|
||||
arg := lambdaToDeBruijn(e.Argument(), context)
|
||||
return debruijn.NewApplication(abs, arg)
|
||||
|
||||
default:
|
||||
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)
|
||||
}
|
||||
77
pkg/debruijn/expression.go
Normal file
77
pkg/debruijn/expression.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package debruijn
|
||||
|
||||
type Expression interface {
|
||||
Accept(Visitor)
|
||||
}
|
||||
|
||||
/** ------------------------------------------------------------------------- */
|
||||
|
||||
type Abstraction struct {
|
||||
body Expression
|
||||
}
|
||||
|
||||
func (a *Abstraction) Body() Expression {
|
||||
return a.body
|
||||
}
|
||||
|
||||
func (a *Abstraction) Accept(v Visitor) {
|
||||
v.VisitAbstraction(a)
|
||||
}
|
||||
|
||||
func NewAbstraction(body Expression) *Abstraction {
|
||||
return &Abstraction{body: 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 (a *Application) Accept(v Visitor) {
|
||||
v.VisitApplication(a)
|
||||
}
|
||||
|
||||
func NewApplication(abstraction Expression, argument Expression) *Application {
|
||||
return &Application{abstraction: abstraction, argument: argument}
|
||||
}
|
||||
|
||||
/** ------------------------------------------------------------------------- */
|
||||
|
||||
type Variable struct {
|
||||
index int
|
||||
label string
|
||||
}
|
||||
|
||||
func (v *Variable) Index() int {
|
||||
return v.index
|
||||
}
|
||||
|
||||
func (v *Variable) Label() string {
|
||||
return v.label
|
||||
}
|
||||
|
||||
func (v *Variable) Accept(visitor Visitor) {
|
||||
visitor.VisitVariable(v)
|
||||
}
|
||||
|
||||
func NewVariable(index int, label string) *Variable {
|
||||
return &Variable{index: index, label: label}
|
||||
}
|
||||
|
||||
/** ------------------------------------------------------------------------- */
|
||||
|
||||
type Visitor interface {
|
||||
VisitAbstraction(*Abstraction)
|
||||
VisitApplication(*Application)
|
||||
VisitVariable(*Variable)
|
||||
}
|
||||
68
pkg/debruijn/iterator.go
Normal file
68
pkg/debruijn/iterator.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package debruijn
|
||||
|
||||
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{}
|
||||
}
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
38
pkg/debruijn/stringify.go
Normal file
38
pkg/debruijn/stringify.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package debruijn
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type stringifyVisitor struct {
|
||||
builder strings.Builder
|
||||
}
|
||||
|
||||
func (v *stringifyVisitor) VisitVariable(a *Variable) {
|
||||
if a.label != "" {
|
||||
v.builder.WriteString(a.label)
|
||||
} else {
|
||||
v.builder.WriteString(fmt.Sprintf("%d", a.index))
|
||||
}
|
||||
}
|
||||
|
||||
func (v *stringifyVisitor) VisitAbstraction(f *Abstraction) {
|
||||
v.builder.WriteRune('\\')
|
||||
v.builder.WriteRune('.')
|
||||
f.body.Accept(v)
|
||||
}
|
||||
|
||||
func (v *stringifyVisitor) VisitApplication(c *Application) {
|
||||
v.builder.WriteRune('(')
|
||||
c.abstraction.Accept(v)
|
||||
v.builder.WriteRune(' ')
|
||||
c.argument.Accept(v)
|
||||
v.builder.WriteRune(')')
|
||||
}
|
||||
|
||||
func Stringify(e Expression) string {
|
||||
b := &stringifyVisitor{builder: strings.Builder{}}
|
||||
e.Accept(b)
|
||||
return b.builder.String()
|
||||
}
|
||||
68
pkg/debruijn/substitute.go
Normal file
68
pkg/debruijn/substitute.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package debruijn
|
||||
|
||||
// Shift increments all free variable indices by delta when crossing depth abstractions.
|
||||
func Shift(expr Expression, delta int, depth int) Expression {
|
||||
switch e := expr.(type) {
|
||||
case *Variable:
|
||||
if e.index >= depth {
|
||||
return NewVariable(e.index+delta, e.label)
|
||||
}
|
||||
return e
|
||||
|
||||
case *Abstraction:
|
||||
newBody := Shift(e.body, delta, depth+1)
|
||||
if newBody == e.body {
|
||||
return e
|
||||
}
|
||||
return NewAbstraction(newBody)
|
||||
|
||||
case *Application:
|
||||
newAbs := Shift(e.abstraction, delta, depth)
|
||||
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 {
|
||||
return e
|
||||
}
|
||||
return NewApplication(newAbs, newArg)
|
||||
|
||||
default:
|
||||
return expr
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user