refactor: rename interpreter to runtime and use receiver methods (#39)
## Description The codebase previously used "interpreter" terminology and standalone functions for expression operations. This PR modernizes the architecture by renaming to "runtime" and converting operations to receiver methods. - Rename `pkg/interpreter` to `pkg/runtime`. - Move `ReduceOnce` to new `pkg/normalorder` package for reduction strategy isolation. - Convert standalone functions (`Substitute`, `Rename`, `GetFree`, `IsFree`) to receiver methods on concrete expression types. - Change `Set` from pointer receivers to value receivers for simpler usage. - Update all references from "interpreter" to "runtime" terminology throughout the codebase. ### Decisions - Operations like `Substitute`, `Rename`, `GetFree`, and `IsFree` are now methods on the `Expression` interface, implemented by each concrete type (`Variable`, `Abstraction`, `Application`). - The `normalorder` package isolates the normal-order reduction strategy, allowing future reduction strategies to be added in separate packages. - `Set` uses value receivers since Go maps are reference types and don't require pointer semantics. ## Benefits - Cleaner API: `expr.Substitute(target, replacement)` instead of `Substitute(expr, target, replacement)`. - Better separation of concerns: reduction strategies are isolated from expression types. - Consistent terminology: "runtime" better reflects the execution model. - Simpler `Set` usage without needing to manage pointers. ## Checklist - [x] Code follows conventional commit format. - [x] Branch follows naming convention (`<type>/<description>`). Always use underscores. - [x] Tests pass (if applicable). - [x] Documentation updated (if applicable). Reviewed-on: #39 Co-authored-by: M.V. Hutz <git@maximhutz.me> Co-committed-by: M.V. Hutz <git@maximhutz.me>
This commit was merged in pull request #39.
This commit is contained in:
@@ -19,7 +19,7 @@ func convertAbstraction(n *saccharine.Abstraction) lambda.Expression {
|
||||
// If the function has no parameters, it is a thunk. Lambda calculus still
|
||||
// requires _some_ parameter exists, so generate one.
|
||||
if len(parameters) == 0 {
|
||||
freeVars := lambda.GetFreeVariables(result)
|
||||
freeVars := result.GetFree()
|
||||
freshName := lambda.GenerateFreshName(freeVars)
|
||||
parameters = append(parameters, freshName)
|
||||
}
|
||||
@@ -63,7 +63,7 @@ func reduceLet(s *saccharine.LetStatement, e lambda.Expression) lambda.Expressio
|
||||
}
|
||||
|
||||
func reduceDeclare(s *saccharine.DeclareStatement, e lambda.Expression) lambda.Expression {
|
||||
freshVar := lambda.GenerateFreshName(lambda.GetFreeVariables(e))
|
||||
freshVar := lambda.GenerateFreshName(e.GetFree())
|
||||
|
||||
return lambda.NewApplication(
|
||||
lambda.NewAbstraction(freshVar, e),
|
||||
|
||||
@@ -9,7 +9,7 @@ type Emitter[E comparable] interface {
|
||||
}
|
||||
|
||||
type BaseEmitter[E comparable] struct {
|
||||
listeners map[E]*set.Set[Listener[E]]
|
||||
listeners map[E]set.Set[Listener[E]]
|
||||
}
|
||||
|
||||
func (e *BaseEmitter[E]) On(kind E, fn func()) Listener[E] {
|
||||
@@ -41,6 +41,6 @@ func (e *BaseEmitter[E]) Emit(event E) {
|
||||
|
||||
func New[E comparable]() *BaseEmitter[E] {
|
||||
return &BaseEmitter[E]{
|
||||
listeners: map[E]*set.Set[Listener[E]]{},
|
||||
listeners: map[E]set.Set[Listener[E]]{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Package expr provides the abstract Expression interface for all evaluatable
|
||||
// expression types in the lambda interpreter.
|
||||
// expression types in the lambda runtime.
|
||||
package expr
|
||||
|
||||
import (
|
||||
|
||||
@@ -2,12 +2,30 @@ package lambda
|
||||
|
||||
import (
|
||||
"git.maximhutz.com/max/lambda/pkg/expr"
|
||||
"git.maximhutz.com/max/lambda/pkg/set"
|
||||
)
|
||||
|
||||
// Expression is the interface for all lambda calculus expression types.
|
||||
// It embeds the general expr.Expression interface for cross-mode compatibility.
|
||||
type Expression interface {
|
||||
expr.Expression
|
||||
|
||||
// Substitute replaces all free occurrences of the target variable with the
|
||||
// replacement expression. Alpha-renaming is performed automatically to
|
||||
// avoid variable capture.
|
||||
Substitute(target string, replacement Expression) Expression
|
||||
|
||||
// GetFree returns the set of all free variable names in the expression.
|
||||
// This function does not mutate the input expression.
|
||||
// The returned set is newly allocated and can be modified by the caller.
|
||||
GetFree() set.Set[string]
|
||||
|
||||
// Rename replaces all occurrences of the target variable name with the new name.
|
||||
Rename(target string, newName string) Expression
|
||||
|
||||
// IsFree returns true if the variable name n occurs free in the expression.
|
||||
// This function does not mutate the input expression.
|
||||
IsFree(n string) bool
|
||||
}
|
||||
|
||||
/** ------------------------------------------------------------------------- */
|
||||
|
||||
@@ -10,7 +10,7 @@ var ticker uint64 = 0
|
||||
|
||||
// GenerateFreshName generates a variable name that is not in the used set.
|
||||
// This function does not mutate the used set.
|
||||
func GenerateFreshName(used *set.Set[string]) string {
|
||||
func GenerateFreshName(used set.Set[string]) string {
|
||||
for i := uint64(0); ; i++ {
|
||||
attempt := "_" + string(strconv.AppendUint(nil, ticker, 10))
|
||||
|
||||
|
||||
@@ -2,22 +2,18 @@ package lambda
|
||||
|
||||
import "git.maximhutz.com/max/lambda/pkg/set"
|
||||
|
||||
// GetFreeVariables returns the set of all free variable names in the expression.
|
||||
// This function does not mutate the input expression.
|
||||
// The returned set is newly allocated and can be modified by the caller.
|
||||
func GetFreeVariables(e Expression) *set.Set[string] {
|
||||
switch e := e.(type) {
|
||||
case Variable:
|
||||
return set.New(e.Name())
|
||||
case Abstraction:
|
||||
vars := GetFreeVariables(e.Body())
|
||||
vars.Remove(e.Parameter())
|
||||
return vars
|
||||
case Application:
|
||||
vars := GetFreeVariables(e.Abstraction())
|
||||
vars.Merge(GetFreeVariables(e.Argument()))
|
||||
return vars
|
||||
default:
|
||||
return set.New[string]()
|
||||
}
|
||||
func (e Variable) GetFree() set.Set[string] {
|
||||
return set.New(e.Name())
|
||||
}
|
||||
|
||||
func (e Abstraction) GetFree() set.Set[string] {
|
||||
vars := e.Body().GetFree()
|
||||
vars.Remove(e.Parameter())
|
||||
return vars
|
||||
}
|
||||
|
||||
func (e Application) GetFree() set.Set[string] {
|
||||
vars := e.Abstraction().GetFree()
|
||||
vars.Merge(e.Argument().GetFree())
|
||||
return vars
|
||||
}
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
package lambda
|
||||
|
||||
// IsFreeVariable returns true if the variable name n occurs free in the expression.
|
||||
// This function does not mutate the input expression.
|
||||
func IsFreeVariable(n string, e Expression) bool {
|
||||
switch e := e.(type) {
|
||||
case Variable:
|
||||
return e.Name() == n
|
||||
case Abstraction:
|
||||
return e.Parameter() != n && IsFreeVariable(n, e.Body())
|
||||
case Application:
|
||||
return IsFreeVariable(n, e.Abstraction()) || IsFreeVariable(n, e.Argument())
|
||||
default:
|
||||
return false
|
||||
}
|
||||
func (e Variable) IsFree(n string) bool {
|
||||
return e.Name() == n
|
||||
}
|
||||
|
||||
func (e Abstraction) IsFree(n string) bool {
|
||||
return e.Parameter() != n && e.Body().IsFree(n)
|
||||
}
|
||||
func (e Application) IsFree(n string) bool {
|
||||
return e.Abstraction().IsFree(n) || e.Argument().IsFree(n)
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
package lambda
|
||||
|
||||
func ReduceOnce(e Expression) (Expression, bool) {
|
||||
switch e := e.(type) {
|
||||
case Abstraction:
|
||||
body, reduced := ReduceOnce(e.Body())
|
||||
if reduced {
|
||||
return NewAbstraction(e.Parameter(), body), true
|
||||
}
|
||||
return e, false
|
||||
|
||||
case Application:
|
||||
if fn, fnOk := e.Abstraction().(Abstraction); fnOk {
|
||||
return Substitute(fn.Body(), fn.Parameter(), e.Argument()), true
|
||||
}
|
||||
|
||||
abs, reduced := ReduceOnce(e.Abstraction())
|
||||
if reduced {
|
||||
return NewApplication(abs, e.Argument()), true
|
||||
}
|
||||
|
||||
arg, reduced := ReduceOnce(e.Argument())
|
||||
if reduced {
|
||||
return NewApplication(e.Abstraction(), arg), true
|
||||
}
|
||||
|
||||
return e, false
|
||||
|
||||
default:
|
||||
return e, false
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,28 @@
|
||||
package lambda
|
||||
|
||||
// Rename replaces all occurrences of the target variable name with the new name.
|
||||
func Rename(expr Expression, target string, newName string) Expression {
|
||||
switch e := expr.(type) {
|
||||
case Variable:
|
||||
if e.Name() == target {
|
||||
return NewVariable(newName)
|
||||
}
|
||||
return e
|
||||
|
||||
case Abstraction:
|
||||
newParam := e.Parameter()
|
||||
if e.Parameter() == target {
|
||||
newParam = newName
|
||||
}
|
||||
|
||||
newBody := Rename(e.Body(), target, newName)
|
||||
|
||||
return NewAbstraction(newParam, newBody)
|
||||
|
||||
case Application:
|
||||
newAbs := Rename(e.Abstraction(), target, newName)
|
||||
newArg := Rename(e.Argument(), target, newName)
|
||||
|
||||
return NewApplication(newAbs, newArg)
|
||||
|
||||
default:
|
||||
return expr
|
||||
func (e Variable) Rename(target string, newName string) Expression {
|
||||
if e.Name() == target {
|
||||
return NewVariable(newName)
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
func (e Abstraction) Rename(target string, newName string) Expression {
|
||||
newParam := e.Parameter()
|
||||
if e.Parameter() == target {
|
||||
newParam = newName
|
||||
}
|
||||
|
||||
newBody := e.Body().Rename(target, newName)
|
||||
|
||||
return NewAbstraction(newParam, newBody)
|
||||
}
|
||||
|
||||
func (e Application) Rename(target string, newName string) Expression {
|
||||
newAbs := e.Abstraction().Rename(target, newName)
|
||||
newArg := e.Argument().Rename(target, newName)
|
||||
|
||||
return NewApplication(newAbs, newArg)
|
||||
}
|
||||
|
||||
@@ -1,41 +1,35 @@
|
||||
package lambda
|
||||
|
||||
// Substitute replaces all free occurrences of the target variable with the replacement expression.
|
||||
// Alpha-renaming is performed automatically to avoid variable capture.
|
||||
func Substitute(expr Expression, target string, replacement Expression) Expression {
|
||||
switch e := expr.(type) {
|
||||
case Variable:
|
||||
if e.Name() == target {
|
||||
return replacement
|
||||
}
|
||||
return e
|
||||
|
||||
case Abstraction:
|
||||
if e.Parameter() == target {
|
||||
return e
|
||||
}
|
||||
|
||||
body := e.Body()
|
||||
param := e.Parameter()
|
||||
if IsFreeVariable(param, replacement) {
|
||||
freeVars := GetFreeVariables(replacement)
|
||||
freeVars.Merge(GetFreeVariables(body))
|
||||
freshVar := GenerateFreshName(freeVars)
|
||||
body = Rename(body, param, freshVar)
|
||||
param = freshVar
|
||||
}
|
||||
|
||||
newBody := Substitute(body, target, replacement)
|
||||
|
||||
return NewAbstraction(param, newBody)
|
||||
|
||||
case Application:
|
||||
newAbs := Substitute(e.Abstraction(), target, replacement)
|
||||
newArg := Substitute(e.Argument(), target, replacement)
|
||||
|
||||
return NewApplication(newAbs, newArg)
|
||||
|
||||
default:
|
||||
return expr
|
||||
func (e Variable) Substitute(target string, replacement Expression) Expression {
|
||||
if e.Name() == target {
|
||||
return replacement
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
func (e Abstraction) Substitute(target string, replacement Expression) Expression {
|
||||
if e.Parameter() == target {
|
||||
return e
|
||||
}
|
||||
|
||||
body := e.Body()
|
||||
param := e.Parameter()
|
||||
if replacement.IsFree(param) {
|
||||
freeVars := replacement.GetFree()
|
||||
freeVars.Merge(body.GetFree())
|
||||
freshVar := GenerateFreshName(freeVars)
|
||||
body = body.Rename(param, freshVar)
|
||||
param = freshVar
|
||||
}
|
||||
|
||||
newBody := body.Substitute(target, replacement)
|
||||
return NewAbstraction(param, newBody)
|
||||
}
|
||||
|
||||
func (e Application) Substitute(target string, replacement Expression) Expression {
|
||||
abs := e.Abstraction().Substitute(target, replacement)
|
||||
arg := e.Argument().Substitute(target, replacement)
|
||||
|
||||
return NewApplication(abs, arg)
|
||||
}
|
||||
|
||||
34
pkg/normalorder/reduce_once.go
Normal file
34
pkg/normalorder/reduce_once.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package normalorder
|
||||
|
||||
import "git.maximhutz.com/max/lambda/pkg/lambda"
|
||||
|
||||
func ReduceOnce(e lambda.Expression) (lambda.Expression, bool) {
|
||||
switch e := e.(type) {
|
||||
case lambda.Abstraction:
|
||||
body, reduced := ReduceOnce(e.Body())
|
||||
if reduced {
|
||||
return lambda.NewAbstraction(e.Parameter(), body), true
|
||||
}
|
||||
return e, false
|
||||
|
||||
case lambda.Application:
|
||||
if fn, fnOk := e.Abstraction().(lambda.Abstraction); fnOk {
|
||||
return fn.Body().Substitute(fn.Parameter(), e.Argument()), true
|
||||
}
|
||||
|
||||
abs, reduced := ReduceOnce(e.Abstraction())
|
||||
if reduced {
|
||||
return lambda.NewApplication(abs, e.Argument()), true
|
||||
}
|
||||
|
||||
arg, reduced := ReduceOnce(e.Argument())
|
||||
if reduced {
|
||||
return lambda.NewApplication(e.Abstraction(), arg), true
|
||||
}
|
||||
|
||||
return e, false
|
||||
|
||||
default:
|
||||
return e, false
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,33 @@
|
||||
package lambda
|
||||
package normalorder
|
||||
|
||||
import (
|
||||
"git.maximhutz.com/max/lambda/pkg/emitter"
|
||||
"git.maximhutz.com/max/lambda/pkg/expr"
|
||||
"git.maximhutz.com/max/lambda/pkg/interpreter"
|
||||
"git.maximhutz.com/max/lambda/pkg/lambda"
|
||||
"git.maximhutz.com/max/lambda/pkg/runtime"
|
||||
)
|
||||
|
||||
// NormalOrderReducer implements normal order (leftmost-outermost) reduction
|
||||
// for lambda calculus expressions.
|
||||
type Interpreter struct {
|
||||
emitter.BaseEmitter[interpreter.Event]
|
||||
expression Expression
|
||||
type Runtime struct {
|
||||
emitter.BaseEmitter[runtime.Event]
|
||||
expression lambda.Expression
|
||||
}
|
||||
|
||||
// NewNormalOrderReducer creates a new normal order reducer.
|
||||
func NewInterpreter(expression Expression) *Interpreter {
|
||||
return &Interpreter{
|
||||
BaseEmitter: *emitter.New[interpreter.Event](),
|
||||
func NewRuntime(expression lambda.Expression) *Runtime {
|
||||
return &Runtime{
|
||||
BaseEmitter: *emitter.New[runtime.Event](),
|
||||
expression: expression,
|
||||
}
|
||||
}
|
||||
|
||||
// Expression returns the current expression state.
|
||||
func (r *Interpreter) Expression() expr.Expression {
|
||||
func (r *Runtime) Expression() expr.Expression {
|
||||
return r.expression
|
||||
}
|
||||
|
||||
func (r *Interpreter) Step() bool {
|
||||
func (r *Runtime) Step() bool {
|
||||
result, done := ReduceOnce(r.expression)
|
||||
r.expression = result
|
||||
return !done
|
||||
@@ -34,12 +35,12 @@ func (r *Interpreter) Step() bool {
|
||||
|
||||
// Reduce performs normal order reduction on a lambda expression.
|
||||
// The expression must be a lambda.Expression; other types are returned unchanged.
|
||||
func (r *Interpreter) Run() {
|
||||
r.Emit(interpreter.StartEvent)
|
||||
func (r *Runtime) Run() {
|
||||
r.Emit(runtime.StartEvent)
|
||||
|
||||
for !r.Step() {
|
||||
r.Emit(interpreter.StepEvent)
|
||||
r.Emit(runtime.StepEvent)
|
||||
}
|
||||
|
||||
r.Emit(interpreter.StopEvent)
|
||||
r.Emit(runtime.StopEvent)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package interpreter
|
||||
package runtime
|
||||
|
||||
// Event represents lifecycle events during interpretation.
|
||||
type Event int
|
||||
@@ -1,27 +1,27 @@
|
||||
// Package interpreter provides the abstract Reducer interface for all expression
|
||||
// Package runtime provides the abstract Reducer interface for all expression
|
||||
// reduction strategies.
|
||||
package interpreter
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"git.maximhutz.com/max/lambda/pkg/emitter"
|
||||
"git.maximhutz.com/max/lambda/pkg/expr"
|
||||
)
|
||||
|
||||
// Reducer defines the interface for expression reduction strategies.
|
||||
// Runtime defines the interface for expression reduction strategies.
|
||||
// Different evaluation modes (normal order, applicative order, SKI combinators,
|
||||
// etc.) implement this interface with their own reduction logic.
|
||||
//
|
||||
// Reducers also implement the Emitter interface to allow plugins to observe
|
||||
// Runtimes also implement the Emitter interface to allow plugins to observe
|
||||
// reduction lifecycle events (Start, Step, Stop).
|
||||
type Interpreter interface {
|
||||
type Runtime interface {
|
||||
emitter.Emitter[Event]
|
||||
|
||||
// Run a single step. Returns whether the interpreter is complete or not.
|
||||
// Run a single step. Returns whether the runtime is complete or not.
|
||||
Step() bool
|
||||
|
||||
// Run until completion.
|
||||
Run()
|
||||
|
||||
// Expression returns the current expression state.
|
||||
// Copy the state of the runtime.
|
||||
Expression() expr.Expression
|
||||
}
|
||||
@@ -4,9 +4,9 @@ import "iter"
|
||||
|
||||
type Set[T comparable] map[T]bool
|
||||
|
||||
func (s *Set[T]) Add(items ...T) {
|
||||
func (s Set[T]) Add(items ...T) {
|
||||
for _, item := range items {
|
||||
(*s)[item] = true
|
||||
s[item] = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,14 +14,14 @@ func (s Set[T]) Has(item T) bool {
|
||||
return s[item]
|
||||
}
|
||||
|
||||
func (s *Set[T]) Remove(items ...T) {
|
||||
func (s Set[T]) Remove(items ...T) {
|
||||
for _, item := range items {
|
||||
delete(*s, item)
|
||||
delete(s, item)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Set[T]) Merge(o *Set[T]) {
|
||||
for item := range *o {
|
||||
func (s Set[T]) Merge(o Set[T]) {
|
||||
for item := range o {
|
||||
s.Add(item)
|
||||
}
|
||||
}
|
||||
@@ -46,8 +46,8 @@ func (s Set[T]) Items() iter.Seq[T] {
|
||||
}
|
||||
}
|
||||
|
||||
func New[T comparable](items ...T) *Set[T] {
|
||||
result := &Set[T]{}
|
||||
func New[T comparable](items ...T) Set[T] {
|
||||
result := Set[T]{}
|
||||
|
||||
for _, item := range items {
|
||||
result.Add(item)
|
||||
|
||||
Reference in New Issue
Block a user