perf: implement structural sharing for expression trees #10

Merged
mvhutz merged 5 commits from perf/structural-sharing into main 2026-01-11 02:15:38 +00:00
7 changed files with 135 additions and 72 deletions
Showing only changes of commit b52a7596ef - Show all commits

View File

@@ -1,19 +1,29 @@
package lambda package lambda
// Expression represents an immutable lambda calculus expression.
// All expression types use structural sharing: operations return
// the same expression pointer when unchanged, reducing allocations.
type Expression interface { type Expression interface {
Accept(Visitor) Accept(Visitor)
Copy() Expression Substitute(target string, replacement Expression) Expression
Rename(target string, newName string) Expression
} }
/** ------------------------------------------------------------------------- */ /** ------------------------------------------------------------------------- */
// Abstraction represents a lambda abstraction (λx. body).
// Fields are unexported to enforce immutability.
type Abstraction struct { type Abstraction struct {
Parameter string parameter string
Body Expression body Expression
} }
func (a *Abstraction) Copy() Expression { func (a *Abstraction) Parameter() string {
return NewAbstraction(a.Parameter, a.Body.Copy()) return a.parameter
}
func (a *Abstraction) Body() Expression {
return a.body
} }
func (a *Abstraction) Accept(v Visitor) { func (a *Abstraction) Accept(v Visitor) {
@@ -21,18 +31,24 @@ func (a *Abstraction) Accept(v Visitor) {
} }
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}
} }
/** ------------------------------------------------------------------------- */ /** ------------------------------------------------------------------------- */
// Application represents function application (f arg).
// Fields are unexported to enforce immutability.
type Application struct { type Application struct {
Abstraction Expression function Expression
Argument Expression argument Expression
} }
func (a *Application) Copy() Expression { func (a *Application) Function() Expression {
return NewApplication(a.Abstraction.Copy(), a.Argument.Copy()) return a.function
}
func (a *Application) Argument() Expression {
return a.argument
} }
func (a *Application) Accept(v Visitor) { func (a *Application) Accept(v Visitor) {
@@ -40,17 +56,19 @@ func (a *Application) Accept(v Visitor) {
} }
func NewApplication(function Expression, argument Expression) *Application { func NewApplication(function Expression, argument Expression) *Application {
return &Application{Abstraction: function, Argument: argument} return &Application{function: function, argument: argument}
} }
/** ------------------------------------------------------------------------- */ /** ------------------------------------------------------------------------- */
// Variable represents a variable reference.
// Fields are unexported to enforce immutability.
type Variable struct { type Variable struct {
Value string value string
} }
func (v *Variable) Copy() Expression { func (v *Variable) Value() string {
return NewVariable(v.Value) return v.value
} }
func (v *Variable) Accept(visitor Visitor) { func (v *Variable) Accept(visitor Visitor) {
@@ -58,7 +76,7 @@ func (v *Variable) Accept(visitor Visitor) {
} }
func NewVariable(name string) *Variable { func NewVariable(name string) *Variable {
return &Variable{Value: name} return &Variable{value: name}
} }
/** ------------------------------------------------------------------------- */ /** ------------------------------------------------------------------------- */

View File

@@ -5,14 +5,14 @@ import "git.maximhutz.com/max/lambda/pkg/set"
func GetFreeVariables(e Expression) *set.Set[string] { func GetFreeVariables(e Expression) *set.Set[string] {
switch e := e.(type) { switch e := e.(type) {
case *Variable: case *Variable:
return set.New(e.Value) return set.New(e.value)
case *Abstraction: case *Abstraction:
vars := GetFreeVariables(e.Body) vars := GetFreeVariables(e.body)
vars.Remove(e.Parameter) vars.Remove(e.parameter)
return vars return vars
case *Application: case *Application:
vars := GetFreeVariables(e.Abstraction) vars := GetFreeVariables(e.function)
vars.Merge(GetFreeVariables(e.Argument)) vars.Merge(GetFreeVariables(e.argument))
return vars return vars
default: default:
return nil return nil

View File

@@ -3,11 +3,11 @@ package lambda
func IsFreeVariable(n string, e Expression) bool { func IsFreeVariable(n string, e Expression) bool {
switch e := e.(type) { switch e := e.(type) {
case *Variable: case *Variable:
return e.Value == n return e.value == n
case *Abstraction: case *Abstraction:
return e.Parameter != n && IsFreeVariable(n, e.Body) return e.parameter != n && IsFreeVariable(n, e.body)
case *Application: case *Application:
return IsFreeVariable(n, e.Abstraction) || IsFreeVariable(n, e.Argument) return IsFreeVariable(n, e.function) || IsFreeVariable(n, e.argument)
default: default:
return false return false
} }

View File

@@ -10,16 +10,17 @@ func ReduceOnce(e *Expression) bool {
switch typed := (*top).(type) { switch typed := (*top).(type) {
case *Abstraction: case *Abstraction:
stack.Push(&typed.Body) stack.Push(&typed.body)
case *Application: case *Application:
if fn, fnOk := typed.Abstraction.(*Abstraction); fnOk { if fn, fnOk := typed.function.(*Abstraction); fnOk {
Substitute(&fn.Body, fn.Parameter, typed.Argument) // Perform beta reduction with structural sharing
*top = fn.Body reduced := fn.body.Substitute(fn.parameter, typed.argument)
*top = reduced
return true return true
} }
stack.Push(&typed.Argument) stack.Push(&typed.argument)
stack.Push(&typed.Abstraction) stack.Push(&typed.function)
} }
} }

View File

@@ -1,19 +1,40 @@
package lambda package lambda
func Rename(e Expression, target string, substitute string) { // Rename replaces all occurrences of target variable with newName.
switch e := e.(type) { // Uses structural sharing: returns the same expression pointer when unchanged.
case *Variable: func (v *Variable) Rename(target string, newName string) Expression {
if e.Value == target { if v.value == target {
e.Value = substitute return NewVariable(newName)
} }
case *Abstraction: return v // unchanged
if e.Parameter == target {
e.Parameter = substitute
} }
Rename(e.Body, target, substitute) // Rename replaces all occurrences of target variable with newName.
case *Application: // Uses structural sharing: only allocates when something changes.
Rename(e.Abstraction, target, substitute) func (a *Abstraction) Rename(target string, newName string) Expression {
Rename(e.Argument, target, substitute) newParam := a.parameter
if a.parameter == target {
newParam = newName
} }
newBody := a.body.Rename(target, newName)
if newParam == a.parameter && newBody == a.body {
return a // unchanged
}
return NewAbstraction(newParam, newBody)
}
// Rename replaces all occurrences of target variable with newName.
// Uses structural sharing: only allocates when something changes.
func (a *Application) Rename(target string, newName string) Expression {
newFunc := a.function.Rename(target, newName)
newArg := a.argument.Rename(target, newName)
if newFunc == a.function && newArg == a.argument {
return a // unchanged
}
return NewApplication(newFunc, newArg)
} }

View File

@@ -7,21 +7,21 @@ type stringifyVisitor struct {
} }
func (v *stringifyVisitor) VisitVariable(a *Variable) { func (v *stringifyVisitor) VisitVariable(a *Variable) {
v.builder.WriteString(a.Value) v.builder.WriteString(a.value)
} }
func (v *stringifyVisitor) VisitAbstraction(f *Abstraction) { func (v *stringifyVisitor) VisitAbstraction(f *Abstraction) {
v.builder.WriteRune('\\') v.builder.WriteRune('\\')
v.builder.WriteString(f.Parameter) v.builder.WriteString(f.parameter)
v.builder.WriteRune('.') v.builder.WriteRune('.')
f.Body.Accept(v) f.body.Accept(v)
} }
func (v *stringifyVisitor) VisitApplication(c *Application) { func (v *stringifyVisitor) VisitApplication(c *Application) {
v.builder.WriteRune('(') v.builder.WriteRune('(')
c.Abstraction.Accept(v) c.function.Accept(v)
v.builder.WriteRune(' ') v.builder.WriteRune(' ')
c.Argument.Accept(v) c.argument.Accept(v)
v.builder.WriteRune(')') v.builder.WriteRune(')')
} }

View File

@@ -1,27 +1,50 @@
package lambda package lambda
func Substitute(e *Expression, target string, replacement Expression) { // Substitute replaces all free occurrences of target with replacement.
switch typed := (*e).(type) { // Uses structural sharing: returns the same expression pointer when unchanged.
case *Variable: func (v *Variable) Substitute(target string, replacement Expression) Expression {
if typed.Value == target { if v.value == target {
*e = replacement.Copy() return replacement // NO COPY - share directly!
} }
case *Abstraction: return v // unchanged, return self
if typed.Parameter == target {
return
} }
if IsFreeVariable(typed.Parameter, replacement) { // Substitute replaces all free occurrences of target with replacement.
replacementFreeVars := GetFreeVariables(replacement) // Uses structural sharing and performs alpha conversion to avoid variable capture.
used := GetFreeVariables(typed.Body) func (a *Abstraction) Substitute(target string, replacement Expression) Expression {
used.Merge(replacementFreeVars) if a.parameter == target {
freshVar := GenerateFreshName(used) return a // shadowed, return unchanged
Rename(typed, typed.Parameter, freshVar)
} }
Substitute(&typed.Body, target, replacement) // Handle alpha conversion if needed to avoid variable capture
case *Application: body := a.body
Substitute(&typed.Abstraction, target, replacement) param := a.parameter
Substitute(&typed.Argument, target, replacement) if IsFreeVariable(param, replacement) {
// Need to rename to avoid capture
freeVars := GetFreeVariables(replacement)
freeVars.Merge(GetFreeVariables(body))
freshVar := GenerateFreshName(freeVars)
body = body.Rename(param, freshVar)
param = freshVar
} }
newBody := body.Substitute(target, replacement)
if newBody == body && param == a.parameter {
return a // STRUCTURAL SHARING: nothing changed
}
return NewAbstraction(param, newBody)
}
// Substitute replaces all free occurrences of target with replacement.
// Uses structural sharing: only allocates when something actually changes.
func (a *Application) Substitute(target string, replacement Expression) Expression {
newFunc := a.function.Substitute(target, replacement)
newArg := a.argument.Substitute(target, replacement)
if newFunc == a.function && newArg == a.argument {
return a // STRUCTURAL SHARING: nothing changed
}
return NewApplication(newFunc, newArg)
} }