perf: implement structural sharing for expression trees #10
@@ -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}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** ------------------------------------------------------------------------- */
|
/** ------------------------------------------------------------------------- */
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
|
||||||
if e.Parameter == target {
|
|
||||||
e.Parameter = substitute
|
|
||||||
}
|
|
||||||
|
|
||||||
Rename(e.Body, target, substitute)
|
|
||||||
case *Application:
|
|
||||||
Rename(e.Abstraction, target, substitute)
|
|
||||||
Rename(e.Argument, target, substitute)
|
|
||||||
}
|
}
|
||||||
|
return v // unchanged
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rename replaces all occurrences of target variable with newName.
|
||||||
|
// Uses structural sharing: only allocates when something changes.
|
||||||
|
func (a *Abstraction) Rename(target string, newName string) Expression {
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(')')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
|
||||||
if typed.Parameter == target {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if IsFreeVariable(typed.Parameter, replacement) {
|
|
||||||
replacementFreeVars := GetFreeVariables(replacement)
|
|
||||||
used := GetFreeVariables(typed.Body)
|
|
||||||
used.Merge(replacementFreeVars)
|
|
||||||
freshVar := GenerateFreshName(used)
|
|
||||||
Rename(typed, typed.Parameter, freshVar)
|
|
||||||
}
|
|
||||||
|
|
||||||
Substitute(&typed.Body, target, replacement)
|
|
||||||
case *Application:
|
|
||||||
Substitute(&typed.Abstraction, target, replacement)
|
|
||||||
Substitute(&typed.Argument, target, replacement)
|
|
||||||
}
|
}
|
||||||
|
return v // unchanged, return self
|
||||||
|
}
|
||||||
|
|
||||||
|
// Substitute replaces all free occurrences of target with replacement.
|
||||||
|
// Uses structural sharing and performs alpha conversion to avoid variable capture.
|
||||||
|
func (a *Abstraction) Substitute(target string, replacement Expression) Expression {
|
||||||
|
if a.parameter == target {
|
||||||
|
return a // shadowed, return unchanged
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle alpha conversion if needed to avoid variable capture
|
||||||
|
body := a.body
|
||||||
|
param := a.parameter
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user