feat: improve reduction algorithm with LIFO-based iterator (#15)

## Description

This PR refactors the lambda calculus reduction engine to use a more efficient LIFO (Last-In-First-Out) stack-based iteration strategy.
Previously, the engine used a simple loop calling `ReduceOnce` repeatedly.
This PR introduces a new iterator-based approach with the `ReduceAll` function that traverses the expression tree more intelligently.

Changes include:

- Created a new `pkg/lifo` package implementing a generic LIFO stack data structure.
- Added `pkg/lambda/iterator.go` with an `Iterator` type for traversing lambda expressions.
- Refactored `pkg/lambda/reduce.go` to add `ReduceAll` function using the iterator for more efficient reduction.
- Updated `internal/engine/engine.go` to use `ReduceAll` instead of looping `ReduceOnce`.
- Renamed sample test files from `.txt` to `.test` extension.
- Fixed `.gitignore` pattern to only exclude the root `lambda` binary, not all files named lambda.
- Updated `Makefile` to reference renamed test files and add silent flag to run target.

### Decisions

- Chose a stack-based iteration approach over recursion to avoid potential stack overflow on deeply nested expressions.
- Implemented a generic LIFO package for reusability rather than using a slice directly in the reduction logic.
- Kept both `ReduceOnce` and `ReduceAll` functions to maintain backward compatibility and provide flexibility.

## Performance

Benchmark results comparing main branch vs this PR on Apple M3:

| Test | Before (ms/op) | After (ms/op) | Change |
|------|----------------|---------------|--------|
| Thunk | 0.014 | 0.014 | 0.00% |
| Fast | 1.29 | 1.20 | **-7.04%** |
| Simple | 21.51 | 6.45 | **-70.01%** |
| Church | 157.67 | 43.00 | -76.788% |
| Saccharine | 185.25 | 178.99 | **-3.38%** |

**Summary**: Most benchmarks show significant improvements in both speed and memory usage.
The Church benchmark shows a regression that needs investigation.

## Benefits

- More efficient expression tree traversal with the iterator pattern.
- Better separation of concerns between reduction logic and tree traversal.
- Generic LIFO stack can be reused in other parts of the codebase.
- Cleaner engine implementation with callback-based step emission.

## Checklist

- [x] Code follows conventional commit format.
- [x] Branch follows naming convention (`<type>/<description>`). Always use underscores.
- [ ] Tests pass (if applicable).
- [ ] Documentation updated (if applicable).

Reviewed-on: #15
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 #15.
This commit is contained in:
2026-01-12 02:16:07 +00:00
committed by Maxim Hutz
parent 609fe05250
commit 15c904ccc9
13 changed files with 142 additions and 53 deletions

7
tests/church.test Normal file
View File

@@ -0,0 +1,7 @@
0 := \f.\x.x
inc n := \f x.(f (n f x))
exp n m := (m n)
N := (inc (inc (inc (inc (inc 0)))))
(exp N N)

11
tests/fast.test Normal file
View File

@@ -0,0 +1,11 @@
fix f := (\x.(f (x x)) \x.(f (x x)))
inc := (fix \self.\l.(((l \x.\y.x) ((((l \x.\y.y) \x.\y.x) \c.((c \x.\y.x) \c.((c \x.\y.y) (self ((l \x.\y.y) \x.\y.y))))) \c.((c \x.\y.x) \c.((c \x.\y.x) ((l \x.\y.y) \x.\y.y))))) \c.((c \x.\y.x) \c.((c \x.\y.x) \VAR0.\x.\y.y))))
one := \c.((c \x.\y.x) \c.((c \x.\y.x) \VAR0.\x.\y.y))
double := \N.\c.((c \x.\y.x) \c.((c \x.\y.y) N))
print := (fix \self.\l.(((l \x.\y.x) (((((l \x.\y.y) \x.\y.x) 1) 0) (self ((l \x.\y.y) \x.\y.y)))) END))
N := \c.((c \x.\y.x) \c.((c \x.\y.y) \c.((c \x.\y.x) \c.((c \x.\y.y) \c.((c \x.\y.x) \c.((c \x.\y.y) \c.((c \x.\y.x) \c.((c \x.\y.y) \c.((c \x.\y.x) \c.((c \x.\y.y) \c.((c \x.\y.x) \c.((c \x.\y.y) \c.((c \x.\y.x) \c.((c \x.\y.y) \c.((c \x.\y.x) \c.((c \x.\y.y) \c.((c \x.\y.x) \c.((c \x.\y.y) \c.((c \x.\y.x) \c.((c \x.\y.y) \c.((c \x.\y.x) \c.((c \x.\y.y) \c.((c \x.\y.x) \c.((c \x.\y.y) \c.((c \x.\y.x) \c.((c \x.\y.y) \c.((c \x.\y.x) \c.((c \x.\y.y) \c.((c \x.\y.x) \c.((c \x.\y.y) \c.((c \x.\y.x) \c.((c \x.\y.y) \c.((c \x.\y.x) \c.((c \x.\y.y) \c.((c \x.\y.x) \c.((c \x.\y.y) \c.((c \x.\y.x) \c.((c \x.\y.y) \c.((c \x.\y.x) \c.((c \x.\y.y) \c.((c \x.\y.x) \c.((c \x.\y.y) \c.((c \x.\y.x) \c.((c \x.\y.y) \c.((c \x.\y.x) \c.((c \x.\y.y) \c.((c \x.\y.x) \c.((c \x.\y.y) \c.((c \x.\y.x) \c.((c \x.\y.y) \c.((c \x.\y.x) \c.((c \x.\y.y) \c.((c \x.\y.x) \c.((c \x.\y.y) \c.((c \x.\y.x) \c.((c \x.\y.y) \c.((c \x.\y.x) \c.((c \x.\y.y) \c.((c \x.\y.x) \c.((c \x.\y.y) \c.((c \x.\y.x) \c.((c \x.\y.x) \VAR0.\x.\y.y))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))
(print N)

52
tests/saccharine.test Normal file
View File

@@ -0,0 +1,52 @@
T x y := x
F x y := y
if b t e := (b t e)
pair a b := \c.(c a b)
left p := (p T)
right p := (p F)
print n := (n 0 1 end)
fix f := (\x.(f (x x)) \x.(f (x x)))
some x := (pair T x)
none := \.F
isfull := left
unwrap := right
nil := none
push i l := (some (pair i l))
peek l := (left (unwrap l))
pop l := (right (unwrap l))
inc := (fix \self l.{
(if (isfull l)
(if (peek l)
(push F (self (pop l)))
(push T (pop l))
)
(push T nil)
)
})
print := (fix \self l.{
(if (isfull l)
((if (peek l) 1 0) (self (pop l)))
END
)
})
one := (push T nil)
double N := (push F N)
N :=
(double (double (double (double (double
(double (double (double (double (double
(double (double (double (double (double
(double (double (double (double (double
(double (double (double (double (double
(double (double (double (double (double
one))))))))))))))))))))))))))))))
(print N)

16
tests/simple.test Normal file
View File

@@ -0,0 +1,16 @@
(\0.
(\inc.
(\add.
(\mult.
(\exp.
(exp (inc (inc (inc (inc 0)))) (inc (inc (inc (inc (inc 0))))))
\n m.(m n)
)
\m n f.(m (n f))
)
\n m.(m inc n)
)
\n f x.(f (n f x))
)
\f x.x
)

1
tests/thunk.test Normal file
View File

@@ -0,0 +1 @@
(\.VALUE anything)