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:
2026-01-18 20:52:34 +00:00
committed by Maxim Hutz
parent e85cf7ceff
commit 9c7fb8ceba
26 changed files with 200 additions and 196 deletions

View File

@@ -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),

View File

@@ -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]]{},
}
}

View File

@@ -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 (

View File

@@ -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
}
/** ------------------------------------------------------------------------- */

View File

@@ -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))

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}
}

View File

@@ -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)
}

View File

@@ -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)
}

View 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
}
}

View File

@@ -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)
}

View File

@@ -1,4 +1,4 @@
package interpreter
package runtime
// Event represents lifecycle events during interpretation.
type Event int

View File

@@ -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
}

View File

@@ -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)