16 Commits

Author SHA1 Message Date
M.V. Hutz
602d8fa074 docs: remove makefile-improvements.md in favor of CLAUDE.md
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-10 17:15:14 -05:00
M.V. Hutz
edfee89bad feat: explicitly set help as default target
Adds .DEFAULT_GOAL := help to make it clear that running 'make'
with no arguments will display the help message.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-10 16:38:37 -05:00
M.V. Hutz
b3db983f62 fix: add lambda binary to .gitignore
Adds the lambda binary to .gitignore to prevent accidentally
committing the build artifact. Previously only *.exe was ignored,
which didn't cover the Unix binary name.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-10 16:38:19 -05:00
M.V. Hutz
997794eaa5 docs: add documentation of Makefile improvements
Documents all issues found and fixes applied to the Makefile,
including both implemented changes and remaining suggestions.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-10 16:36:13 -05:00
M.V. Hutz
8f70bfbbdb refactor: use .SILENT directive instead of @ prefixes
Adds .SILENT directive to suppress command echoing for all targets,
replacing individual @ prefixes. Also moves TEST variable to top with
other variables for better organization.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-10 16:34:07 -05:00
M.V. Hutz
3158c35df2 fix: add profile dependency to graph target
Makes graph target depend on profile to ensure cpu.prof exists
before attempting to generate visualizations.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-10 16:29:25 -05:00
M.V. Hutz
bb48d0777b fix: ensure profile directory exists before writing
Creates profile directory in profile and explain targets to prevent
errors on first run.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-10 16:29:09 -05:00
M.V. Hutz
24fdc1c17c feat: add help target to document available commands
Adds help target that displays all available Make targets and their
descriptions, improving discoverability.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-10 16:28:48 -05:00
M.V. Hutz
7927df4660 feat: add clean target to remove build artifacts
Adds standard clean target to remove binary, output files, and
profile directory.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-10 16:28:29 -05:00
M.V. Hutz
e5ceeb2fcc feat: add .PHONY declarations for all targets
Declares all non-file targets as phony to prevent conflicts with
files of the same name and improve Make's performance.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-10 16:28:08 -05:00
M.V. Hutz
e0b0b92a8a refactor: remove redundant chmod +x command
Go build already sets the executable bit on binaries, making the
explicit chmod +x unnecessary.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-10 16:27:55 -05:00
M.V. Hutz
0d06fac919 fix: remove Windows .exe extension from binary name
Changed BINARY_NAME from lambda.exe to lambda for Unix systems.
The .exe extension is a Windows convention and is inappropriate
for macOS/Linux builds.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-10 16:27:42 -05:00
M.V. Hutz
dc9a1b2b7d feat: add usage instructions to README and improve Makefile 2026-01-10 16:21:22 -05:00
M.V. Hutz
7e59d5cefa style: remove YAML document separator 2026-01-10 16:12:30 -05:00
M.V. Hutz
9f06a5109f style: remove comments from golangci config 2026-01-10 16:11:37 -05:00
M.V. Hutz
b2b2655c1e style: remove decorative comment separators 2026-01-10 16:07:39 -05:00
65 changed files with 345 additions and 1800 deletions

View File

@@ -1,58 +0,0 @@
---
name: "Bug Report"
about: "Report a bug or unexpected behavior in the lambda interpreter."
title: "fix: "
ref: "main"
assignees: []
labels:
- bug
---
## Context
<!--
Describe what you were trying to do when you encountered the bug.
Explain what you expected to happen.
-->
## Current Behavior
<!--
Describe what actually happened.
Be specific about the incorrect behavior or error.
-->
## Steps to Reproduce
<!--
Provide step-by-step instructions to reproduce the issue.
Include any relevant code, commands, or input.
-->
1.
2.
3.
## Expected Behavior
<!--
Describe what should happen instead.
-->
## Environment
<!--
Provide relevant information about your environment.
-->
- Lambda version:
- Go version:
- Operating system:
## Additional Context
<!--
Add any other context about the problem.
Include error messages, logs, or screenshots if applicable.
If none exist, omit this section.
-->

View File

@@ -1,44 +0,0 @@
---
name: "Feature Request"
about: "Suggest a new feature or enhancement for the lambda interpreter."
title: "feat: "
ref: "main"
assignees: []
labels:
- enhancement
---
## Context
<!--
Describe the problem or limitation you're encountering.
Explain why this feature would be valuable.
-->
## Proposed Solution
<!--
Describe your proposed solution or enhancement.
Be specific about what you want to see implemented.
-->
## Alternatives Considered
<!--
List any alternative solutions or approaches you've considered.
If none exist, omit this section.
-->
## Acceptance Criteria
<!--
List clear, testable criteria that define when this feature is complete.
Use bullet points starting with •
-->
## Additional Context
<!--
Add any other context, screenshots, or examples about the feature request.
If none exist, omit this section.
-->

View File

@@ -1,37 +0,0 @@
---
name: "General Issue"
about: "Create an issue that doesn't fit other templates."
title: ""
ref: "main"
assignees: []
labels: []
---
## Context
<!--
Describe the background and context for this issue.
Explain why this issue exists.
-->
## Description
<!--
Provide a detailed description of what needs to be done.
Be clear and specific about the requirements.
-->
## Acceptance Criteria
<!--
List clear, testable criteria that define when this issue is complete.
Use bullet points starting with •
If none exist, omit this section.
-->
## Additional Context
<!--
Add any other relevant information, links, or references.
If none exist, omit this section.
-->

View File

@@ -1,37 +0,0 @@
---
name: "Default Template"
about: "The default template for `lambda`."
title: "<type>: <description>"
ref: "main"
assignees: []
labels: []
---
## Description
<!--
First, describe the context for the PR.
Then, explain why the PR exists.
Finally, in concise, sentence-long bullets, explain each change.
-->
### Decisions
<!--
List any major architectural decisions here.
If none exist, omit this section.
-->
## Benefits
<!--
List any major benefits here.
How would this PR improve the codebase/product?
-->
## Checklist
- [ ] Code follows conventional commit format.
- [ ] Branch follows naming convention (`<type>/<description>`). Always use underscores.
- [ ] Tests pass (if applicable).
- [ ] Documentation updated (if applicable).

7
.gitignore vendored
View File

@@ -3,13 +3,18 @@
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
/lambda
*.exe
*.exe~
*.dll
*.so
*.dylib
# Build artifacts
lambda
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out

View File

@@ -1,230 +1,83 @@
---
# golangci-lint configuration file made by @ccoVeille
# Source: https://github.com/ccoVeille/golangci-lint-config-examples/
# Author: @ccoVeille
# License: MIT
# Variant: 03-safe
# Version: v2.0.0
#
version: "2"
formatters:
enable:
# format the code
- gofmt
# format the block of imports
- gci
settings:
# format the code with Go standard library
gofmt:
# simplify the code
# https://pkg.go.dev/cmd/gofmt#hdr-The_simplify_command
simplify: true
rewrite-rules:
# replace `interface{}` with `any` in the code on format
- pattern: 'interface{}'
replacement: 'any'
# make sure imports are always in a deterministic order
# https://github.com/daixiang0/gci/
gci: # define the section orders for imports
gci:
sections:
# Standard section: captures all standard packages.
- standard
# Default section: catchall that is not standard or custom
- default
# linters that related to local tool, so they should be separated
- localmodule
linters:
exclusions:
# these presets where present in the v1 version of golangci-lint
# it's interesting to keep them when migrating, but removing them should be the goal
presets:
# exclude check on comments format in godoc
# These are common false positives in poor code
# you should not use this on recent code you write from scratch
# More information: https://golangci-lint.run/usage/false-positives/#comments
#
# Please uncomment the following line if your code is not using the godoc format
- comments
# Common false positives
# feel free to remove this if you don't have any false positives
# More information: https://golangci-lint.run/usage/false-positives/#common-false-positives
- common-false-positives
# Legacy preset is not recommended anymore
# More information: https://golangci-lint.run/usage/false-positives/#legacy
- legacy
# std-error-handling is a set of rules that avoid reporting unhandled errors on common functions/methods
# More information: https://golangci-lint.run/usage/false-positives/#std-error-handling
- std-error-handling
# some linters are enabled by default
# https://golangci-lint.run/usage/linters/
#
# enable some extra linters
enable:
# Errcheck is a program for checking for unchecked errors in Go code.
- errcheck
# Vet examines Go source code and reports suspicious constructs.
- govet
# Detects when assignments to existing variables are not used.
- ineffassign
# It's a set of rules from staticcheck. See https://staticcheck.io/
- staticcheck
# Checks Go code for unused constants, variables, functions and types.
- unused
# Fast, configurable, extensible, flexible, and beautiful linter for Go.
# Drop-in replacement of golint.
- revive
# make sure to use t.Helper() when needed
- thelper
# mirror suggests rewrites to avoid unnecessary []byte/string conversion
- mirror
# detect the possibility to use variables/constants from the Go standard library.
- usestdlibvars
# Finds commonly misspelled English words.
- misspell
# Checks for duplicate words in the source code.
- dupword
# linter to detect errors invalid key values count
- loggercheck
# detect when a package or method could be replaced by one from the standard library
- exptostd
# detects nested contexts in loops or function literals
- fatcontext
# Reports uses of functions with replacement inside the testing package.
- usetesting
settings:
revive:
rules:
# these are the default revive rules
# you can remove the whole "rules" node if you want
# BUT
# ! /!\ they all need to be present when you want to add more rules than the default ones
# otherwise, you won't have the default rules, but only the ones you define in the "rules" node
# Blank import should be only in a main or test package, or have a comment justifying it.
- name: blank-imports
# context.Context() should be the first parameter of a function when provided as argument.
- name: context-as-argument
arguments:
- allowTypesBefore: "*testing.T"
# Basic types should not be used as a key in `context.WithValue`
- name: context-keys-type
# Importing with `.` makes the programs much harder to understand
- name: dot-imports
# Empty blocks make code less readable and could be a symptom of a bug or unfinished refactoring.
- name: empty-block
# for better readability, variables of type `error` must be named with the prefix `err`.
- name: error-naming
# for better readability, the errors should be last in the list of returned values by a function.
- name: error-return
# for better readability, error messages should not be capitalized or end with punctuation or a newline.
- name: error-strings
# report when replacing `errors.New(fmt.Sprintf())` with `fmt.Errorf()` is possible
- name: errorf
# check naming and commenting conventions on exported symbols.
- name: exported
arguments:
# make error messages clearer
- "sayRepetitiveInsteadOfStutters"
# incrementing an integer variable by 1 is recommended to be done using the `++` operator
- name: increment-decrement
# highlights redundant else-blocks that can be eliminated from the code
# - name: indent-error-flow
# This rule suggests a shorter way of writing ranges that do not use the second value.
- name: range
# receiver names in a method should reflect the struct name (p for Person, for example)
- name: receiver-naming
# redefining built in names (true, false, append, make) can lead to bugs very difficult to detect.
- name: redefines-builtin-id
# redundant else-blocks that can be eliminated from the code.
# - name: superfluous-else
# prevent confusing name for variables when using `time` package
- name: time-naming
# warns when an exported function or method returns a value of an un-exported type.
- name: unexported-return
# spots and proposes to remove unreachable code. also helps to spot errors
- name: unreachable-code
# Functions or methods with unused parameters can be a symptom of an unfinished refactoring or a bug.
- name: unused-parameter
# report when a variable declaration can be simplified
- name: var-declaration
# warns when initialism, variable or package naming conventions are not followed.
- name: var-naming
misspell:
# Correct spellings using locale preferences for US or UK.
# Setting locale to US will correct the British spelling of 'colour' to 'color'.
# Default ("") is to use a neutral variety of English.
locale: US
# List of words to ignore
# among the one defined in https://github.com/golangci/misspell/blob/master/words.go
ignore-rules: []
# - valor
# - and
# Extra word corrections.
extra-words: []
# - typo: "whattever"
# correction: "whatever"
output:
# Order to use when sorting results.
# Possible values: `file`, `linter`, and `severity`.
#
# If the severity values are inside the following list, they are ordered in this order:
# 1. error
# 2. warning
# 3. high
# 4. medium
# 5. low
# Either they are sorted alphabetically.
#
# Default: ["file"]
sort-order:
- linter
- severity
- file # filepath, line, and column.
- file

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"makefile.configureOnOpen": false
}

105
CLAUDE.md
View File

@@ -1,105 +0,0 @@
# Guide To `lambda`
## Documentation Style
Use full sentences.
Every sentence gets its own line in Markdown.
Every sentence ends in a period.
## Coding Style
### Conventional Commits
Use conventional commit format: `<type>: <description>`.
**Types**: `feat`, `fix`, `docs`, `refactor`, `test`, `chore`, `perf`
**Examples**:
- `feat: add explanation mode flag to CLI`
- `fix: correct variable renaming in nested abstractions`
- `docs: update Makefile documentation`
DO NOT advertise Claude.
### Branch Names
Use format: `<type>/<description>` with kebab-case.
**Types**: Same as commits: `feat`, `fix`, `docs`, `refactor`, `test`, `chore`, `perf`.
**Examples**:
- `feat/explanation-mode`
- `fix/variable-renaming`
- `docs/makefile-improvements`
- `refactor/silent-directive`
DO NOT advertise Claude.
## Issue Management
Use the `tea` CLI (Gitea command-line tool) for issue operations.
**Common commands**:
- `tea issues list` - List all issues.
- `tea issues <number>` - View details of a specific issue.
- `tea issues create --title "<title>" --body "<description>"` - Create a new issue.
- `tea issues close <number>` - Close an issue.
**Reading issues**: Use `tea issues <number>` to read the full details of an issue before starting work.
## Issue Workflow
When working on an issue:
1. Read the issue using `tea issues <number>` to understand requirements.
2. Create a feature branch following the branch naming convention.
3. Make commits following the conventional commit format.
4. Submit a pull request when ready.
**Important**: Never commit directly to `main`.
All work must be done in a feature branch and submitted via pull request.
## Pull Request Management
Use the `tea` CLI (Gitea command-line tool) for PR operations instead of `gh`.
**Common commands**:
- `tea pr create --title "<title>" --description "<body>"` - Create a new pull request.
- `tea pr list` - List pull requests.
- `tea pr checkout <number>` - Check out a PR locally.
- `tea pr close <number>` - Close a pull request.
- `tea pr merge <number>` - Merge a pull request.
**Note**: Use `--description` (not `--body`) for PR body content.
**Creating PRs**: Always create PRs in a branch other than `main`, to the `main` branch unless specified otherwise. ALWAYS FOLLOW THE PR TEMPLATE, EXACTLY.
**Linking issues**: When a PR solves an issue, reference the issue in both the commit message and PR description using `Closes #<number>`.
This automatically links and closes the issue when the PR is merged.
### Updating PRs
When pushing additional changes to an existing PR, add a comment summarizing the new commits.
This keeps reviewers informed of what changed since the initial PR description.
Use the `tea` CLI to add comments to pull requests:
```bash
tea comment <number> "Comment text"
```
#### Examples
```bash
# Add a comment to PR #42
tea comment 42 "Updated implementation based on feedback"
# Add a multi-line comment
tea comment 42 "Summary of changes:
- Fixed bug in reducer
- Added new tests"
```

View File

@@ -1,51 +1,40 @@
BINARY_NAME=lambda
TEST=simple
.PHONY: help build run profile explain graph docs test bench clean
.DEFAULT_GOAL := help
.SILENT:
.PHONY: help it run profile explain graph clean
help:
echo "Available targets:"
echo " build - Build the lambda executable"
echo " run - Build and run the lambda interpreter (use TEST=<name> to specify sample)"
echo " it - Build the lambda binary"
echo " run - Build and run with sample input (default: simple.txt)"
echo " profile - Build and run with CPU profiling enabled"
echo " explain - Build and run with explanation mode and profiling"
echo " graph - Generate and open CPU profile visualization"
echo " docs - Start local godoc server on port 6060"
echo " test - Run tests for all samples"
echo " bench - Run benchmarks for all samples"
echo " clean - Remove all build artifacts"
echo " explain - Run with explanation mode and profiling"
echo " graph - Generate CPU profile visualization"
echo " clean - Remove build artifacts"
echo ""
echo "Usage: make run TEST=<sample-name>"
build:
it:
go build -o ${BINARY_NAME} ./cmd/lambda
chmod +x ${BINARY_NAME}
run: build
./${BINARY_NAME} -s -f ./tests/$(TEST).test -o program.out
run: it
./lambda - < ./samples/$(TEST).txt > program.out
profile: build
./${BINARY_NAME} -p profile/cpu.prof -f ./tests/$(TEST).test -o program.out
profile: it
mkdir -p profile
./lambda -p profile/cpu.prof - < ./samples/$(TEST).txt > program.out
explain: build
./${BINARY_NAME} -x -p profile/cpu.prof -f ./tests/$(TEST).test -o program.out > explain.out
explain: it
mkdir -p profile
./lambda -x -p profile/cpu.prof - < ./samples/$(TEST).txt > program.out
graph:
graph: profile
go tool pprof -raw -output=profile/cpu.raw profile/cpu.prof
go tool pprof -svg profile/cpu.prof > profile/cpu.svg
echo ">>> View at 'file://$(PWD)/profile/cpu.svg'"
docs:
echo ">>> View at 'http://localhost:6060/pkg/git.maximhutz.com/max/lambda/'"
go run golang.org/x/tools/cmd/godoc@latest -http=:6060
test:
go test -v ./cmd/lambda
bench:
go test -bench=. -benchtime=10x -cpu=4 ./cmd/lambda
echo "Profile graph saved to 'file://profile/cpu.svg'"
clean:
rm -f ${BINARY_NAME}
rm -f program.out
rm -f ${BINARY_NAME} program.out
rm -rf profile/

View File

@@ -1,15 +1,17 @@
package main
import (
"fmt"
"os"
"git.maximhutz.com/max/lambda/internal/cli"
"git.maximhutz.com/max/lambda/internal/config"
"git.maximhutz.com/max/lambda/internal/plugins"
"git.maximhutz.com/max/lambda/internal/engine"
"git.maximhutz.com/max/lambda/internal/explanation"
"git.maximhutz.com/max/lambda/internal/performance"
"git.maximhutz.com/max/lambda/internal/statistics"
"git.maximhutz.com/max/lambda/pkg/convert"
"git.maximhutz.com/max/lambda/pkg/debruijn"
"git.maximhutz.com/max/lambda/pkg/lambda"
"git.maximhutz.com/max/lambda/pkg/reducer"
"git.maximhutz.com/max/lambda/pkg/saccharine"
)
@@ -33,53 +35,42 @@ func main() {
// Compile expression to lambda calculus.
compiled := convert.SaccharineToLambda(ast)
logger.Info("compiled λ expression", "tree", compiled.String())
logger.Info("compiled λ expression", "tree", lambda.Stringify(compiled))
// Create reducer based on the selected interpreter.
var red reducer.Reducer
switch options.Interpreter {
case config.DeBruijnInterpreter:
dbExpr := convert.LambdaToDeBruijn(compiled)
logger.Info("converted to De Bruijn", "tree", dbExpr.String())
red = debruijn.NewNormalOrderReducer(&dbExpr)
default:
red = lambda.NewNormalOrderReducer(&compiled)
}
// Create reduction engine.
process := engine.New(options, &compiled)
// If the user selected to track CPU performance, attach a profiler.
// If the user selected to track CPU performance, attach a profiler to the
// process.
if options.Profile != "" {
plugins.NewPerformance(options.Profile, red)
profiler := performance.Track(options.Profile)
process.On("start", profiler.Start)
process.On("end", profiler.End)
}
// If the user selected to produce a step-by-step explanation, attach an
// observer.
// observer here.
if options.Explanation {
plugins.NewExplanation(red)
explanation.Track(process)
}
// If the user opted to track statistics, attach a tracker.
// If the user opted to track statistics, attach a tracker here, too.
if options.Statistics {
plugins.NewStatistics(red)
statistics := statistics.Track()
process.On("start", statistics.Start)
process.On("step", statistics.Step)
process.On("end", statistics.End)
}
// If the user selected for verbose debug logs, attach a reduction tracker.
if options.Verbose {
plugins.NewLogs(logger, red)
process.On("step", func() {
logger.Info("reduction", "tree", lambda.Stringify(compiled))
})
}
// Run reduction.
red.Reduce()
process.Run()
// Return the final reduced result.
// For De Bruijn, convert back to lambda for consistent output.
var result string
if options.Interpreter == config.DeBruijnInterpreter {
dbExpr := red.Expression().(debruijn.Expression)
lambdaExpr := convert.DeBruijnToLambda(dbExpr)
result = lambdaExpr.String()
} else {
result = red.Expression().String()
}
err = options.Destination.Write(result)
cli.HandleError(err)
fmt.Println(lambda.Stringify(compiled))
}

View File

@@ -1,163 +0,0 @@
package main
import (
"os"
"path/filepath"
"strings"
"testing"
"git.maximhutz.com/max/lambda/pkg/convert"
"git.maximhutz.com/max/lambda/pkg/debruijn"
"git.maximhutz.com/max/lambda/pkg/lambda"
"git.maximhutz.com/max/lambda/pkg/saccharine"
"github.com/stretchr/testify/assert"
)
// Helper function to run a single sample through the lambda interpreter.
func runSample(samplePath string) (string, error) {
// Read the sample file.
input, err := os.ReadFile(samplePath)
if err != nil {
return "", err
}
// Parse code into syntax tree.
ast, err := saccharine.Parse(string(input))
if err != nil {
return "", err
}
// Compile expression to lambda calculus.
compiled := convert.SaccharineToLambda(ast)
// Create and run the reducer.
reducer := lambda.NewNormalOrderReducer(&compiled)
reducer.Reduce()
return reducer.Expression().String() + "\n", nil
}
// Helper function to run a single sample through the De Bruijn interpreter.
func runSampleDeBruijn(samplePath string) (string, error) {
// Read the sample file.
input, err := os.ReadFile(samplePath)
if err != nil {
return "", err
}
// Parse code into syntax tree.
ast, err := saccharine.Parse(string(input))
if err != nil {
return "", err
}
// Compile expression to lambda calculus.
compiled := convert.SaccharineToLambda(ast)
// Convert to De Bruijn and run reducer.
dbExpr := convert.LambdaToDeBruijn(compiled)
reducer := debruijn.NewNormalOrderReducer(&dbExpr)
reducer.Reduce()
// Convert back to lambda for output.
result := reducer.Expression().(debruijn.Expression)
lambdaResult := convert.DeBruijnToLambda(result)
return lambdaResult.String() + "\n", nil
}
// Test that all samples produce expected output with lambda interpreter.
func TestSamplesValidity(t *testing.T) {
// Discover all .test files in the tests directory.
testFiles, err := filepath.Glob("../../tests/*.test")
assert.NoError(t, err, "Failed to read tests directory.")
assert.NotEmpty(t, testFiles, "No '*.test' files found in directory.")
for _, testPath := range testFiles {
// Build expected file path.
expectedPath := strings.TrimSuffix(testPath, filepath.Ext(testPath)) + ".expected"
name := strings.TrimSuffix(filepath.Base(testPath), filepath.Ext(testPath))
t.Run(name, func(t *testing.T) {
// Run the sample and capture output.
actual, err := runSample(testPath)
assert.NoError(t, err, "Failed to run sample.")
// Read expected output.
expectedBytes, err := os.ReadFile(expectedPath)
assert.NoError(t, err, "Failed to read expected output.")
expected := string(expectedBytes)
// Compare outputs.
assert.Equal(t, expected, actual, "Output does not match expected.")
})
}
}
// Test that all samples produce expected output with De Bruijn interpreter.
func TestSamplesValidityDeBruijn(t *testing.T) {
// Discover all .test files in the tests directory.
testFiles, err := filepath.Glob("../../tests/*.test")
assert.NoError(t, err, "Failed to read tests directory.")
assert.NotEmpty(t, testFiles, "No '*.test' files found in directory.")
for _, testPath := range testFiles {
// Build expected file path.
expectedPath := strings.TrimSuffix(testPath, filepath.Ext(testPath)) + ".expected"
name := strings.TrimSuffix(filepath.Base(testPath), filepath.Ext(testPath))
t.Run(name, func(t *testing.T) {
// Run the sample and capture output.
actual, err := runSampleDeBruijn(testPath)
assert.NoError(t, err, "Failed to run sample.")
// Read expected output.
expectedBytes, err := os.ReadFile(expectedPath)
assert.NoError(t, err, "Failed to read expected output.")
expected := string(expectedBytes)
// Compare outputs.
assert.Equal(t, expected, actual, "Output does not match expected.")
})
}
}
// Benchmark all samples using sub-benchmarks.
func BenchmarkSamples(b *testing.B) {
// Discover all .test files in the tests directory.
testFiles, err := filepath.Glob("../../tests/*.test")
assert.NoError(b, err, "Failed to read tests directory.")
assert.NotEmpty(b, testFiles, "No '*.test' files found in directory.")
for _, path := range testFiles {
name := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))
b.Run(name, func(b *testing.B) {
for b.Loop() {
_, err := runSample(path)
assert.NoError(b, err, "Failed to run sample.")
}
})
}
}
// Benchmark all samples using De Bruijn interpreter.
func BenchmarkSamplesDeBruijn(b *testing.B) {
// Discover all .test files in the tests directory.
testFiles, err := filepath.Glob("../../tests/*.test")
assert.NoError(b, err, "Failed to read tests directory.")
assert.NotEmpty(b, testFiles, "No '*.test' files found in directory.")
for _, path := range testFiles {
name := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))
b.Run(name, func(b *testing.B) {
for b.Loop() {
_, err := runSampleDeBruijn(path)
assert.NoError(b, err, "Failed to run sample.")
}
})
}
}

8
go.mod
View File

@@ -1,11 +1,3 @@
module git.maximhutz.com/max/lambda
go 1.25.5
require github.com/stretchr/testify v1.11.1
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

9
go.sum
View File

@@ -1,9 +0,0 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -1,21 +1,11 @@
// Package "config" parses ad handles the user settings given to the program.
package config
// Interpreter specifies the reduction engine to use.
type Interpreter string
const (
LambdaInterpreter Interpreter = "lambda"
DeBruijnInterpreter Interpreter = "debruijn"
)
// Configuration settings for the program.
type Config struct {
Source Source // The source code given to the program.
Destination Destination // The destination for the final result.
Verbose bool // Whether or not to print debug logs.
Explanation bool // Whether or not to print an explanation of the reduction.
Profile string // If not nil, print a CPU profile during execution.
Statistics bool // Whether or not to print statistics.
Interpreter Interpreter // The interpreter engine to use.
Source Source // The source code given to the program.
Verbose bool // Whether or not to print debug logs.
Explanation bool // Whether or not to print an explanation of the reduction.
Profile string // If not nil, print a CPU profile during execution.
Statistics bool // Whether or not to print statistics.
}

View File

@@ -1,27 +0,0 @@
package config
import (
"fmt"
"os"
)
// A method of writing output to the user.
type Destination interface {
// Write data to this destination.
Write(data string) error
}
// A destination writing to stdout.
type StdoutDestination struct{}
func (d StdoutDestination) Write(data string) error {
fmt.Println(data)
return nil
}
// A destination writing to a file.
type FileDestination struct{ Path string }
func (d FileDestination) Write(data string) error {
return os.WriteFile(d.Path, []byte(data+"\n"), 0644)
}

View File

@@ -12,58 +12,28 @@ func FromArgs() (*Config, error) {
explanation := flag.Bool("x", false, "Explanation. Whether or not to show all reduction steps.")
statistics := flag.Bool("s", false, "Statistics. If set, the process will print various statistics about the run.")
profile := flag.String("p", "", "CPU profiling. If an output file is defined, the program will profile its execution and dump its results into it.")
file := flag.String("f", "", "File. If set, read source from the specified file.")
output := flag.String("o", "", "Output. If set, write result to the specified file. Use '-' for stdout (default).")
interpreter := flag.String("i", "lambda", "Interpreter. The reduction engine to use: 'lambda' or 'debruijn'.")
flag.Parse()
// Validate interpreter flag.
var interpType Interpreter
switch *interpreter {
case "lambda":
interpType = LambdaInterpreter
case "debruijn":
interpType = DeBruijnInterpreter
default:
return nil, fmt.Errorf("invalid interpreter: %s (must be 'lambda' or 'debruijn')", *interpreter)
// There must only be one input argument.
if flag.NArg() == 0 {
return nil, fmt.Errorf("no input given")
} else if flag.NArg() > 1 {
return nil, fmt.Errorf("more than 1 command-line argument")
}
// Parse source type.
var source Source
if *file != "" {
// File flag takes precedence.
if flag.NArg() > 0 {
return nil, fmt.Errorf("cannot specify both -f flag and positional argument")
}
source = FileSource{Path: *file}
} else if flag.NArg() == 0 {
return nil, fmt.Errorf("no input given")
} else if flag.NArg() > 1 {
return nil, fmt.Errorf("more than 1 command-line argument")
if flag.Arg(0) == "-" {
source = StdinSource{}
} else {
// Positional argument.
if flag.Arg(0) == "-" {
source = StdinSource{}
} else {
source = StringSource{Data: flag.Arg(0)}
}
}
// Parse destination type.
var destination Destination
if *output == "" || *output == "-" {
destination = StdoutDestination{}
} else {
destination = FileDestination{Path: *output}
source = StringSource{Data: flag.Arg(0)}
}
return &Config{
Source: source,
Destination: destination,
Verbose: *verbose,
Explanation: *explanation,
Profile: *profile,
Statistics: *statistics,
Interpreter: interpType,
}, nil
}

View File

@@ -27,15 +27,3 @@ func (s StdinSource) Extract() (string, error) {
return string(data), nil
}
// A source reading from a file.
type FileSource struct{ Path string }
func (s FileSource) Extract() (string, error) {
data, err := os.ReadFile(s.Path)
if err != nil {
return "", err
}
return string(data), nil
}

32
internal/engine/engine.go Normal file
View File

@@ -0,0 +1,32 @@
// Package "engine" provides an extensible interface for users to interfact with
// λ-calculus.
package engine
import (
"git.maximhutz.com/max/lambda/internal/config"
"git.maximhutz.com/max/lambda/pkg/emitter"
"git.maximhutz.com/max/lambda/pkg/lambda"
)
// A process for reducing one λ-expression.
type Engine struct {
Config *config.Config
Expression *lambda.Expression
emitter.Emitter
}
// Create a new engine, given an unreduced λ-expression.
func New(config *config.Config, expression *lambda.Expression) *Engine {
return &Engine{Config: config, Expression: expression}
}
// Begin the reduction process.
func (e Engine) Run() {
e.Emit("start")
for lambda.ReduceOnce(e.Expression) {
e.Emit("step")
}
e.Emit("end")
}

View File

@@ -0,0 +1,32 @@
// Package "explanation" provides a observer to gather the reasoning during the
// reduction, and present a thorough explanation to the user for each step.
package explanation
import (
"fmt"
"git.maximhutz.com/max/lambda/internal/engine"
"git.maximhutz.com/max/lambda/pkg/lambda"
)
// Track the reductions made by a reduction proess.
type Tracker struct {
process *engine.Engine
}
// Attaches a new explanation tracker to a process.
func Track(process *engine.Engine) *Tracker {
tracker := &Tracker{process: process}
process.On("start", tracker.Start)
process.On("step", tracker.Step)
return tracker
}
func (t *Tracker) Start() {
fmt.Println(lambda.Stringify(*t.process.Expression))
}
func (t *Tracker) Step() {
fmt.Println(" =", lambda.Stringify(*t.process.Expression))
}

View File

@@ -1,34 +1,28 @@
// Package "performance" provides a tracker to observer CPU performance during
// execution.
package plugins
package performance
import (
"os"
"path/filepath"
"runtime/pprof"
"git.maximhutz.com/max/lambda/pkg/reducer"
)
// Observes a reduction process, and publishes a CPU performance profile on
// completion.
type Performance struct {
type Tracker struct {
File string
filePointer *os.File
Error error
}
// Create a performance tracker that outputs a profile to "file".
func NewPerformance(file string, process reducer.Reducer) *Performance {
plugin := &Performance{File: file}
process.On(reducer.StartEvent, plugin.Start)
process.On(reducer.StopEvent, plugin.Stop)
return plugin
func Track(file string) *Tracker {
return &Tracker{File: file}
}
// Begin profiling.
func (t *Performance) Start() {
func (t *Tracker) Start() {
var absPath string
absPath, t.Error = filepath.Abs(t.File)
@@ -53,7 +47,7 @@ func (t *Performance) Start() {
}
// Stop profiling.
func (t *Performance) Stop() {
func (t *Tracker) End() {
pprof.StopCPUProfile()
t.filePointer.Close()
}

View File

@@ -1,23 +0,0 @@
package plugins
import (
"log/slog"
"git.maximhutz.com/max/lambda/pkg/reducer"
)
type Logs struct {
logger *slog.Logger
reducer reducer.Reducer
}
func NewLogs(logger *slog.Logger, r reducer.Reducer) *Logs {
plugin := &Logs{logger, r}
r.On(reducer.StepEvent, plugin.Step)
return plugin
}
func (t *Logs) Step() {
t.logger.Info("reduction", "tree", t.reducer.Expression().String())
}

View File

@@ -1,31 +0,0 @@
// Package "explanation" provides an observer to gather the reasoning during the
// reduction, and present a thorough explanation to the user for each step.
package plugins
import (
"fmt"
"git.maximhutz.com/max/lambda/pkg/reducer"
)
// Track the reductions made by a reduction process.
type Explanation struct {
reducer reducer.Reducer
}
// Attaches a new explanation tracker to a reducer.
func NewExplanation(r reducer.Reducer) *Explanation {
plugin := &Explanation{reducer: r}
r.On(reducer.StartEvent, plugin.Start)
r.On(reducer.StepEvent, plugin.Step)
return plugin
}
func (t *Explanation) Start() {
fmt.Println(t.reducer.Expression().String())
}
func (t *Explanation) Step() {
fmt.Println(" =", t.reducer.Expression().String())
}

View File

@@ -1,44 +0,0 @@
package plugins
import (
"fmt"
"os"
"time"
"git.maximhutz.com/max/lambda/internal/statistics"
"git.maximhutz.com/max/lambda/pkg/reducer"
)
// An observer, to track reduction performance.
type Statistics struct {
start time.Time
steps uint64
}
// Create a new reduction performance Statistics.
func NewStatistics(r reducer.Reducer) *Statistics {
plugin := &Statistics{}
r.On(reducer.StartEvent, plugin.Start)
r.On(reducer.StepEvent, plugin.Step)
r.On(reducer.StopEvent, plugin.Stop)
return plugin
}
func (t *Statistics) Start() {
t.start = time.Now()
t.steps = 0
}
func (t *Statistics) Step() {
t.steps++
}
func (t *Statistics) Stop() {
results := statistics.Results{
StepsTaken: t.steps,
TimeElapsed: uint64(time.Since(t.start).Milliseconds()),
}
fmt.Fprint(os.Stderr, results.String())
}

View File

@@ -0,0 +1,36 @@
package statistics
import (
"fmt"
"os"
"time"
)
// An observer, to track reduction performance.
type Tracker struct {
start time.Time
steps uint64
}
// Create a new reduction performance tracker.
func Track() *Tracker {
return &Tracker{}
}
func (t *Tracker) Start() {
t.start = time.Now()
t.steps = 0
}
func (t *Tracker) Step() {
t.steps++
}
func (t *Tracker) End() {
results := Results{
StepsTaken: t.steps,
TimeElapsed: uint64(time.Since(t.start).Milliseconds()),
}
fmt.Fprint(os.Stderr, results.String())
}

View File

@@ -1,82 +0,0 @@
package convert
import (
"fmt"
"git.maximhutz.com/max/lambda/pkg/debruijn"
"git.maximhutz.com/max/lambda/pkg/lambda"
"git.maximhutz.com/max/lambda/pkg/set"
)
// DeBruijnToLambda converts a De Bruijn indexed expression back to named lambda calculus.
func DeBruijnToLambda(expr debruijn.Expression) lambda.Expression {
return deBruijnToLambdaWithContext(expr, []string{})
}
func deBruijnToLambdaWithContext(expr debruijn.Expression, context []string) lambda.Expression {
switch e := expr.(type) {
case *debruijn.Variable:
index := e.Index()
if index < len(context) {
// Bound variable: look up name in context.
name := context[len(context)-1-index]
return lambda.NewVariable(name)
}
// Free variable: use the label if available.
if e.Label() != "" {
return lambda.NewVariable(e.Label())
}
// Generate a name for free variables without labels.
return lambda.NewVariable(fmt.Sprintf("free%d", index))
case *debruijn.Abstraction:
// Generate a fresh parameter name.
used := collectUsedNames(e.Body(), context)
paramName := generateFreshName(used)
newContext := append(context, paramName)
body := deBruijnToLambdaWithContext(e.Body(), newContext)
return lambda.NewAbstraction(paramName, body)
case *debruijn.Application:
abs := deBruijnToLambdaWithContext(e.Abstraction(), context)
arg := deBruijnToLambdaWithContext(e.Argument(), context)
return lambda.NewApplication(abs, arg)
default:
panic("unknown expression type")
}
}
// collectUsedNames gathers all variable labels used in an expression.
func collectUsedNames(expr debruijn.Expression, context []string) *set.Set[string] {
used := set.New[string]()
for _, name := range context {
used.Add(name)
}
collectUsedNamesHelper(expr, used)
return used
}
func collectUsedNamesHelper(expr debruijn.Expression, used *set.Set[string]) {
switch e := expr.(type) {
case *debruijn.Variable:
if e.Label() != "" {
used.Add(e.Label())
}
case *debruijn.Abstraction:
collectUsedNamesHelper(e.Body(), used)
case *debruijn.Application:
collectUsedNamesHelper(e.Abstraction(), used)
collectUsedNamesHelper(e.Argument(), used)
}
}
// generateFreshName creates a fresh variable name not in the used set.
func generateFreshName(used *set.Set[string]) string {
for i := 0; ; i++ {
name := fmt.Sprintf("_%d", i)
if !used.Has(name) {
return name
}
}
}

View File

@@ -1,44 +0,0 @@
package convert
import (
"git.maximhutz.com/max/lambda/pkg/debruijn"
"git.maximhutz.com/max/lambda/pkg/lambda"
)
// LambdaToDeBruijn converts a lambda calculus expression to De Bruijn indexed form.
// The context parameter tracks bound variables from outer abstractions.
func LambdaToDeBruijn(expr lambda.Expression) debruijn.Expression {
return lambdaToDeBruijnWithContext(expr, []string{})
}
func lambdaToDeBruijnWithContext(expr lambda.Expression, context []string) debruijn.Expression {
switch e := expr.(type) {
case *lambda.Variable:
name := e.Value()
// Search for the variable in the context (innermost to outermost).
for i := len(context) - 1; i >= 0; i-- {
if context[i] == name {
index := len(context) - 1 - i
return debruijn.NewVariable(index, name)
}
}
// Free variable: use a negative index to mark it.
// We encode free variables with index = len(context) + position.
// For simplicity, we use a large index that won't conflict.
return debruijn.NewVariable(len(context), name)
case *lambda.Abstraction:
// Add the parameter to the context.
newContext := append(context, e.Parameter())
body := lambdaToDeBruijnWithContext(e.Body(), newContext)
return debruijn.NewAbstraction(body)
case *lambda.Application:
abs := lambdaToDeBruijnWithContext(e.Abstraction(), context)
arg := lambdaToDeBruijnWithContext(e.Argument(), context)
return debruijn.NewApplication(abs, arg)
default:
panic("unknown expression type")
}
}

View File

@@ -1,119 +0,0 @@
// Package debruijn provides De Bruijn indexed lambda calculus expressions.
// De Bruijn indices eliminate the need for variable names by using numeric
// indices to refer to bound variables, avoiding capture issues during substitution.
package debruijn
import "git.maximhutz.com/max/lambda/pkg/expr"
// Expression is the interface for all De Bruijn indexed expression types.
// It embeds the general expr.Expression interface for cross-mode compatibility.
type Expression interface {
expr.Expression
Accept(Visitor)
}
/** ------------------------------------------------------------------------- */
// Abstraction represents a lambda abstraction without a named parameter.
// In De Bruijn notation, the parameter is implicit and referenced by index 0
// within the body.
type Abstraction struct {
body Expression
}
// Body returns the body of the abstraction.
func (a *Abstraction) Body() Expression {
return a.body
}
// Accept implements the Visitor pattern.
func (a *Abstraction) Accept(v Visitor) {
v.VisitAbstraction(a)
}
// String returns the De Bruijn notation string representation.
func (a *Abstraction) String() string {
return Stringify(a)
}
// NewAbstraction creates a new De Bruijn abstraction with the given body.
func NewAbstraction(body Expression) *Abstraction {
return &Abstraction{body: body}
}
/** ------------------------------------------------------------------------- */
// Application represents the application of one expression to another.
type Application struct {
abstraction Expression
argument Expression
}
// Abstraction returns the function expression being applied.
func (a *Application) Abstraction() Expression {
return a.abstraction
}
// Argument returns the argument expression.
func (a *Application) Argument() Expression {
return a.argument
}
// Accept implements the Visitor pattern.
func (a *Application) Accept(v Visitor) {
v.VisitApplication(a)
}
// String returns the De Bruijn notation string representation.
func (a *Application) String() string {
return Stringify(a)
}
// NewApplication creates a new application expression.
func NewApplication(abstraction Expression, argument Expression) *Application {
return &Application{abstraction: abstraction, argument: argument}
}
/** ------------------------------------------------------------------------- */
// Variable represents a De Bruijn indexed variable.
// The index indicates how many binders to skip to find the binding abstraction.
// The label is an optional hint for display purposes.
type Variable struct {
index int
label string
}
// Index returns the De Bruijn index.
func (v *Variable) Index() int {
return v.index
}
// Label returns the optional variable label.
func (v *Variable) Label() string {
return v.label
}
// Accept implements the Visitor pattern.
func (v *Variable) Accept(visitor Visitor) {
visitor.VisitVariable(v)
}
// String returns the De Bruijn notation string representation.
func (v *Variable) String() string {
return Stringify(v)
}
// NewVariable creates a new De Bruijn variable with the given index and label.
func NewVariable(index int, label string) *Variable {
return &Variable{index: index, label: label}
}
/** ------------------------------------------------------------------------- */
// Visitor interface for traversing De Bruijn expressions.
type Visitor interface {
VisitAbstraction(*Abstraction)
VisitApplication(*Application)
VisitVariable(*Variable)
}

View File

@@ -1,76 +0,0 @@
package debruijn
// Iterator provides depth-first traversal of De Bruijn expressions.
type Iterator struct {
trace []*Expression
}
// NewIterator creates a new iterator starting at the given expression.
func NewIterator(expr *Expression) *Iterator {
return &Iterator{[]*Expression{expr}}
}
// Done returns true when the iterator has finished traversal.
func (i *Iterator) Done() bool {
return len(i.trace) == 0
}
// Current returns a pointer to the current expression.
func (i *Iterator) Current() *Expression {
if i.Done() {
return nil
}
return i.trace[len(i.trace)-1]
}
// Parent returns a pointer to the parent expression.
func (i *Iterator) Parent() *Expression {
if len(i.trace) < 2 {
return nil
}
return i.trace[len(i.trace)-2]
}
// Swap replaces the current expression with the given expression.
func (i *Iterator) Swap(with Expression) {
current := i.Current()
if current != nil {
*current = with
}
}
// Back moves the iterator back to the parent expression.
func (i *Iterator) Back() bool {
if i.Done() {
return false
}
i.trace = i.trace[:len(i.trace)-1]
return true
}
// Next advances the iterator to the next expression in leftmost-outermost order.
func (i *Iterator) Next() {
switch typed := (*i.Current()).(type) {
case *Abstraction:
i.trace = append(i.trace, &typed.body)
case *Application:
i.trace = append(i.trace, &typed.abstraction)
case *Variable:
for len(i.trace) > 1 {
if app, ok := (*i.Parent()).(*Application); ok {
if app.abstraction == *i.Current() {
i.Back()
i.trace = append(i.trace, &app.argument)
return
}
}
i.Back()
}
i.trace = []*Expression{}
}
}

View File

@@ -1,66 +0,0 @@
package debruijn
import (
"git.maximhutz.com/max/lambda/pkg/emitter"
"git.maximhutz.com/max/lambda/pkg/expr"
"git.maximhutz.com/max/lambda/pkg/reducer"
)
// NormalOrderReducer implements normal order (leftmost-outermost) reduction
// for De Bruijn indexed lambda calculus expressions.
type NormalOrderReducer struct {
emitter.BaseEmitter[reducer.Event]
expression *Expression
}
// NewNormalOrderReducer creates a new normal order reducer.
func NewNormalOrderReducer(expression *Expression) *NormalOrderReducer {
return &NormalOrderReducer{
BaseEmitter: *emitter.New[reducer.Event](),
expression: expression,
}
}
// Expression returns the current expression state.
func (r *NormalOrderReducer) Expression() expr.Expression {
return *r.expression
}
// isViable checks if an expression is a redex (reducible expression).
// A redex is an application of an abstraction to an argument.
func isViable(e *Expression) (*Abstraction, Expression, bool) {
if e == nil {
return nil, nil, false
} else if app, appOk := (*e).(*Application); !appOk {
return nil, nil, false
} else if fn, fnOk := app.abstraction.(*Abstraction); !fnOk {
return nil, nil, false
} else {
return fn, app.argument, true
}
}
// Reduce performs normal order reduction on a De Bruijn expression.
func (r *NormalOrderReducer) Reduce() {
r.Emit(reducer.StartEvent)
it := NewIterator(r.expression)
for !it.Done() {
if fn, arg, ok := isViable(it.Current()); !ok {
it.Next()
} else {
// Substitute arg for variable 0 in the body.
substituted := Substitute(fn.body, 0, Shift(arg, 1, 0))
// Shift down to account for the removed abstraction.
it.Swap(Shift(substituted, -1, 0))
r.Emit(reducer.StepEvent)
if _, _, ok := isViable(it.Parent()); ok {
it.Back()
}
}
}
r.Emit(reducer.StopEvent)
}

View File

@@ -1,32 +0,0 @@
package debruijn
// Shift increments all free variable indices in an expression by the given amount.
// A variable is free if its index is >= the cutoff (depth of nested abstractions).
// This is necessary when substituting an expression into a different binding context.
func Shift(expr Expression, amount int, cutoff int) Expression {
switch e := expr.(type) {
case *Variable:
if e.index >= cutoff {
return NewVariable(e.index+amount, e.label)
}
return e
case *Abstraction:
newBody := Shift(e.body, amount, cutoff+1)
if newBody == e.body {
return e
}
return NewAbstraction(newBody)
case *Application:
newAbs := Shift(e.abstraction, amount, cutoff)
newArg := Shift(e.argument, amount, cutoff)
if newAbs == e.abstraction && newArg == e.argument {
return e
}
return NewApplication(newAbs, newArg)
default:
return expr
}
}

View File

@@ -1,35 +0,0 @@
package debruijn
import (
"strconv"
"strings"
)
type stringifyVisitor struct {
builder strings.Builder
}
func (v *stringifyVisitor) VisitVariable(a *Variable) {
v.builder.WriteString(strconv.Itoa(a.index))
}
func (v *stringifyVisitor) VisitAbstraction(f *Abstraction) {
v.builder.WriteRune('\\')
v.builder.WriteRune('.')
f.body.Accept(v)
}
func (v *stringifyVisitor) VisitApplication(c *Application) {
v.builder.WriteRune('(')
c.abstraction.Accept(v)
v.builder.WriteRune(' ')
c.argument.Accept(v)
v.builder.WriteRune(')')
}
// Stringify converts a De Bruijn expression to its string representation.
func Stringify(e Expression) string {
b := &stringifyVisitor{builder: strings.Builder{}}
e.Accept(b)
return b.builder.String()
}

View File

@@ -1,34 +0,0 @@
package debruijn
// Substitute replaces the variable at the given index with the replacement expression.
// The replacement is shifted appropriately as we descend into nested abstractions.
func Substitute(expr Expression, index int, replacement Expression) Expression {
switch e := expr.(type) {
case *Variable:
if e.index == index {
return replacement
}
return e
case *Abstraction:
// When entering an abstraction, increment the target index and shift the
// replacement to account for the new binding context.
shiftedReplacement := Shift(replacement, 1, 0)
newBody := Substitute(e.body, index+1, shiftedReplacement)
if newBody == e.body {
return e
}
return NewAbstraction(newBody)
case *Application:
newAbs := Substitute(e.abstraction, index, replacement)
newArg := Substitute(e.argument, index, replacement)
if newAbs == e.abstraction && newArg == e.argument {
return e
}
return NewApplication(newAbs, newArg)
default:
return expr
}
}

View File

@@ -1,7 +1,5 @@
package deltanet
/** ------------------------------------------------------------------------- */
// A connection between exactly two nodes in a graph.
type Edge struct {
A, B Node
@@ -28,8 +26,6 @@ func (e Edge) IsPrincipleEdge() bool {
return e.A.GetMainPort() == e && e.B.GetMainPort() == e
}
/** ------------------------------------------------------------------------- */
type Node interface {
// Returns the principle port that the node is attached to.
GetMainPort() Edge
@@ -42,8 +38,6 @@ type Node interface {
GetLabel() string
}
/** ------------------------------------------------------------------------- */
type EraserNode struct {
Main Edge
}
@@ -52,8 +46,6 @@ func (n EraserNode) GetLabel() string { return "Ⓧ" }
func (n EraserNode) GetMainPort() Edge { return n.Main }
func (n EraserNode) GetAuxPorts() []Edge { return []Edge{} }
/** ------------------------------------------------------------------------- */
type ReplicatorNode struct {
Main Edge
Level uint
@@ -68,8 +60,6 @@ func (n ReplicatorNode) GetAuxPorts() []Edge { return n.Aux }
// Returns the level of the replicator node.
func (n ReplicatorNode) GetLevel() uint { return n.Level }
/** ------------------------------------------------------------------------- */
type FanNode struct {
Label string
Main Edge
@@ -80,8 +70,6 @@ func (n FanNode) GetLabel() string { return n.Label }
func (n FanNode) GetMainPort() Edge { return n.Main }
func (n FanNode) GetAuxPorts() []Edge { return []Edge{n.Left, n.Right} }
/** ------------------------------------------------------------------------- */
type TerminalNode struct {
Label string
Main Edge
@@ -90,5 +78,3 @@ type TerminalNode struct {
func (n TerminalNode) GetLabel() string { return n.Label }
func (n TerminalNode) GetMainPort() Edge { return n.Main }
func (n TerminalNode) GetAuxPorts() []Edge { return []Edge{} }
/** ------------------------------------------------------------------------- */

View File

@@ -2,45 +2,53 @@ package emitter
import "git.maximhutz.com/max/lambda/pkg/set"
type Emitter[E comparable] interface {
On(E, func()) Listener[E]
Off(Listener[E])
Emit(E)
type Observer struct {
fn func()
message string
emitter *Emitter
}
type BaseEmitter[E comparable] struct {
listeners map[E]*set.Set[Listener[E]]
type Emitter struct {
listeners map[string]*set.Set[*Observer]
}
func (e *BaseEmitter[E]) On(kind E, fn func()) Listener[E] {
if e.listeners[kind] == nil {
e.listeners[kind] = set.New[Listener[E]]()
func Ignore[T any](fn func()) func(T) {
return func(T) { fn() }
}
func (e *Emitter) On(message string, fn func()) *Observer {
observer := &Observer{
fn: fn,
message: message,
emitter: e,
}
listener := &BaseListener[E]{kind, fn}
e.listeners[kind].Add(listener)
return listener
}
func (e *BaseEmitter[E]) Off(listener Listener[E]) {
kind := listener.Kind()
if e.listeners[kind] != nil {
e.listeners[kind].Remove(listener)
}
}
func (e *BaseEmitter[E]) Emit(event E) {
if e.listeners[event] == nil {
e.listeners[event] = set.New[Listener[E]]()
if e.listeners == nil {
e.listeners = map[string]*set.Set[*Observer]{}
}
for listener := range e.listeners[event].Items() {
listener.Run()
if e.listeners[message] == nil {
e.listeners[message] = set.New[*Observer]()
}
e.listeners[message].Add(observer)
return observer
}
func New[E comparable]() *BaseEmitter[E] {
return &BaseEmitter[E]{
listeners: map[E]*set.Set[Listener[E]]{},
func (o *Observer) Off() {
if o.emitter.listeners[o.message] == nil {
return
}
o.emitter.listeners[o.message].Remove(o)
}
func (e *Emitter) Emit(message string) {
if e.listeners[message] == nil {
return
}
for listener := range *e.listeners[message] {
listener.fn()
}
}

View File

@@ -1,19 +0,0 @@
package emitter
type Listener[E comparable] interface {
Kind() E
Run()
}
type BaseListener[E comparable] struct {
kind E
fn func()
}
func (l BaseListener[E]) Kind() E {
return l.kind
}
func (l BaseListener[E]) Run() {
l.fn()
}

View File

@@ -1,11 +0,0 @@
// Package expr provides the abstract Expression interface for all evaluatable
// expression types in the lambda interpreter.
package expr
// Expression is the base interface for all evaluatable expression types.
// Different evaluation modes (lambda calculus, SKI combinators, typed lambda
// calculus, etc.) implement this interface with their own concrete types.
type Expression interface {
// String returns a human-readable representation of the expression.
String() string
}

35
pkg/fifo/fifo.go Normal file
View File

@@ -0,0 +1,35 @@
package fifo
import "fmt"
type FIFO[T any] []T
func New[T any](items ...T) *FIFO[T] {
f := FIFO[T](items)
return &f
}
func (f *FIFO[T]) Push(item T) {
*f = append(*f, item)
}
func (f *FIFO[T]) Empty() bool {
return len(*f) == 0
}
func (f *FIFO[T]) MustPop() T {
var item T
*f, item = (*f)[:len(*f)-1], (*f)[len(*f)-1]
return item
}
func (f *FIFO[T]) Pop() (T, error) {
var item T
if f.Empty() {
return item, fmt.Errorf("stack is exhausted")
}
return f.MustPop(), nil
}

View File

@@ -1,92 +1,60 @@
package lambda
import "git.maximhutz.com/max/lambda/pkg/expr"
// 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
Accept(Visitor)
Copy() Expression
}
/** ------------------------------------------------------------------------- */
type Abstraction struct {
parameter string
body Expression
Parameter string
Body Expression
}
func (a *Abstraction) Parameter() string {
return a.parameter
}
func (a *Abstraction) Body() Expression {
return a.body
func (a *Abstraction) Copy() Expression {
return NewAbstraction(a.Parameter, a.Body.Copy())
}
func (a *Abstraction) Accept(v Visitor) {
v.VisitAbstraction(a)
}
func (a *Abstraction) String() string {
return Stringify(a)
}
func NewAbstraction(parameter string, body Expression) *Abstraction {
return &Abstraction{parameter: parameter, body: body}
return &Abstraction{Parameter: parameter, Body: body}
}
/** ------------------------------------------------------------------------- */
type Application struct {
abstraction Expression
argument Expression
Abstraction Expression
Argument Expression
}
func (a *Application) Abstraction() Expression {
return a.abstraction
}
func (a *Application) Argument() Expression {
return a.argument
func (a *Application) Copy() Expression {
return NewApplication(a.Abstraction.Copy(), a.Argument.Copy())
}
func (a *Application) Accept(v Visitor) {
v.VisitApplication(a)
}
func (a *Application) String() string {
return Stringify(a)
func NewApplication(function Expression, argument Expression) *Application {
return &Application{Abstraction: function, Argument: argument}
}
func NewApplication(abstraction Expression, argument Expression) *Application {
return &Application{abstraction: abstraction, argument: argument}
}
/** ------------------------------------------------------------------------- */
type Variable struct {
value string
Value string
}
func (v *Variable) Value() string {
return v.value
func (v *Variable) Copy() Expression {
return NewVariable(v.Value)
}
func (v *Variable) Accept(visitor Visitor) {
visitor.VisitVariable(v)
}
func (v *Variable) String() string {
return Stringify(v)
}
func NewVariable(name string) *Variable {
return &Variable{value: name}
return &Variable{Value: name}
}
/** ------------------------------------------------------------------------- */
type Visitor interface {
VisitAbstraction(*Abstraction)
VisitApplication(*Application)

View File

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

View File

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

View File

@@ -1,68 +0,0 @@
package lambda
type Iterator struct {
trace []*Expression
}
func NewIterator(expr *Expression) *Iterator {
return &Iterator{[]*Expression{expr}}
}
func (i *Iterator) Done() bool {
return len(i.trace) == 0
}
func (i *Iterator) Current() *Expression {
if i.Done() {
return nil
}
return i.trace[len(i.trace)-1]
}
func (i *Iterator) Parent() *Expression {
if len(i.trace) < 2 {
return nil
}
return i.trace[len(i.trace)-2]
}
func (i *Iterator) Swap(with Expression) {
current := i.Current()
if current != nil {
*current = with
}
}
func (i *Iterator) Back() bool {
if i.Done() {
return false
}
i.trace = i.trace[:len(i.trace)-1]
return true
}
func (i *Iterator) Next() {
switch typed := (*i.Current()).(type) {
case *Abstraction:
i.trace = append(i.trace, &typed.body)
case *Application:
i.trace = append(i.trace, &typed.abstraction)
case *Variable:
for len(i.trace) > 1 {
if app, ok := (*i.Parent()).(*Application); ok {
if app.abstraction == *i.Current() {
i.Back()
i.trace = append(i.trace, &app.argument)
return
}
}
i.Back()
}
i.trace = []*Expression{}
}
}

27
pkg/lambda/reduce.go Normal file
View File

@@ -0,0 +1,27 @@
package lambda
import "git.maximhutz.com/max/lambda/pkg/fifo"
func ReduceOnce(e *Expression) bool {
stack := fifo.New(e)
for !stack.Empty() {
top := stack.MustPop()
switch typed := (*top).(type) {
case *Abstraction:
stack.Push(&typed.Body)
case *Application:
if fn, fnOk := typed.Abstraction.(*Abstraction); fnOk {
Substitute(&fn.Body, fn.Parameter, typed.Argument)
*top = fn.Body
return true
}
stack.Push(&typed.Argument)
stack.Push(&typed.Abstraction)
}
}
return false
}

View File

@@ -1,61 +0,0 @@
package lambda
import (
"git.maximhutz.com/max/lambda/pkg/emitter"
"git.maximhutz.com/max/lambda/pkg/expr"
"git.maximhutz.com/max/lambda/pkg/reducer"
)
// NormalOrderReducer implements normal order (leftmost-outermost) reduction
// for lambda calculus expressions.
type NormalOrderReducer struct {
emitter.BaseEmitter[reducer.Event]
expression *Expression
}
// NewNormalOrderReducer creates a new normal order reducer.
func NewNormalOrderReducer(expression *Expression) *NormalOrderReducer {
return &NormalOrderReducer{
BaseEmitter: *emitter.New[reducer.Event](),
expression: expression,
}
}
// Expression returns the current expression state.
func (r *NormalOrderReducer) Expression() expr.Expression {
return *r.expression
}
func isViable(e *Expression) (*Abstraction, Expression, bool) {
if e == nil {
return nil, nil, false
} else if app, appOk := (*e).(*Application); !appOk {
return nil, nil, false
} else if fn, fnOk := app.abstraction.(*Abstraction); !fnOk {
return nil, nil, false
} else {
return fn, app.argument, true
}
}
// Reduce performs normal order reduction on a lambda expression.
// The expression must be a lambda.Expression; other types are returned unchanged.
func (r *NormalOrderReducer) Reduce() {
r.Emit(reducer.StartEvent)
it := NewIterator(r.expression)
for !it.Done() {
if fn, arg, ok := isViable(it.Current()); !ok {
it.Next()
} else {
it.Swap(Substitute(fn.body, fn.parameter, arg))
r.Emit(reducer.StepEvent)
if _, _, ok := isViable(it.Parent()); ok {
it.Back()
}
}
}
r.Emit(reducer.StopEvent)
}

View File

@@ -1,38 +1,19 @@
package lambda
func Rename(expr Expression, target string, newName string) Expression {
switch e := expr.(type) {
func Rename(e Expression, target string, substitute string) {
switch e := e.(type) {
case *Variable:
if e.value == target {
return NewVariable(newName)
if e.Value == target {
e.Value = substitute
}
return e
case *Abstraction:
newParam := e.parameter
if e.parameter == target {
newParam = newName
if e.Parameter == target {
e.Parameter = substitute
}
newBody := Rename(e.body, target, newName)
if newParam == e.parameter && newBody == e.body {
return e
}
return NewAbstraction(newParam, newBody)
Rename(e.Body, target, substitute)
case *Application:
newAbs := Rename(e.abstraction, target, newName)
newArg := Rename(e.argument, target, newName)
if newAbs == e.abstraction && newArg == e.argument {
return e
}
return NewApplication(newAbs, newArg)
default:
return expr
Rename(e.Abstraction, target, substitute)
Rename(e.Argument, target, substitute)
}
}

View File

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

View File

@@ -1,46 +1,27 @@
package lambda
func Substitute(expr Expression, target string, replacement Expression) Expression {
switch e := expr.(type) {
func Substitute(e *Expression, target string, replacement Expression) {
switch typed := (*e).(type) {
case *Variable:
if e.value == target {
return replacement
if typed.Value == target {
*e = replacement.Copy()
}
return e
case *Abstraction:
if e.parameter == target {
return e
if typed.Parameter == target {
return
}
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
if IsFreeVariable(typed.Parameter, replacement) {
replacementFreeVars := GetFreeVariables(replacement)
used := GetFreeVariables(typed.Body)
used.Merge(replacementFreeVars)
freshVar := GenerateFreshName(used)
Rename(typed, typed.Parameter, freshVar)
}
newBody := Substitute(body, target, replacement)
if newBody == body && param == e.parameter {
return e
}
return NewAbstraction(param, newBody)
Substitute(&typed.Body, target, replacement)
case *Application:
newAbs := Substitute(e.abstraction, target, replacement)
newArg := Substitute(e.argument, target, replacement)
if newAbs == e.abstraction && newArg == e.argument {
return e
}
return NewApplication(newAbs, newArg)
default:
return expr
Substitute(&typed.Abstraction, target, replacement)
Substitute(&typed.Argument, target, replacement)
}
}

View File

@@ -1,13 +0,0 @@
package reducer
// Event represents lifecycle events during reduction.
type Event int
const (
// StartEvent is emitted before reduction begins.
StartEvent Event = iota
// StepEvent is emitted after each reduction step.
StepEvent
// StopEvent is emitted after reduction completes.
StopEvent
)

View File

@@ -1,27 +0,0 @@
// Package reducer provides the abstract Reducer interface for all expression
// reduction strategies.
package reducer
import (
"git.maximhutz.com/max/lambda/pkg/emitter"
"git.maximhutz.com/max/lambda/pkg/expr"
)
// Reducer 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
// reduction lifecycle events (Start, Step, Stop).
type Reducer interface {
emitter.Emitter[Event]
// Reduce performs all reduction steps on the expression.
// Emits StartEvent before reduction, StepEvent after each step, and
// StopEvent after completion.
// Returns the final reduced expression.
Reduce()
// Expression returns the current expression state.
Expression() expr.Expression
}

View File

@@ -4,8 +4,6 @@ type Expression interface {
IsExpression()
}
/** ------------------------------------------------------------------------- */
type Abstraction struct {
Parameters []string
Body Expression
@@ -30,8 +28,6 @@ func (Application) IsExpression() {}
func (Atom) IsExpression() {}
func (Clause) IsExpression() {}
/** ------------------------------------------------------------------------- */
func NewAbstraction(parameter []string, body Expression) *Abstraction {
return &Abstraction{Parameters: parameter, Body: body}
}

View File

@@ -4,8 +4,6 @@ type Statement interface {
IsStatement()
}
/** ------------------------------------------------------------------------- */
type LetStatement struct {
Name string
Parameters []string
@@ -19,8 +17,6 @@ type DeclareStatement struct {
func (LetStatement) IsStatement() {}
func (DeclareStatement) IsStatement() {}
/** ------------------------------------------------------------------------- */
func NewLet(name string, parameters []string, body Expression) *LetStatement {
return &LetStatement{Name: name, Parameters: parameters, Body: body}
}

View File

@@ -77,21 +77,6 @@ func getToken(i *iterator.Iterator[rune]) (*Token, error) {
}
case letter == ';':
return NewHardBreak(index), nil
case letter == '#':
// Skip everything until the next newline or EOF.
for !i.Done() {
r, err := i.Next()
if err != nil {
return nil, trace.Wrap(err, "error while parsing comment")
}
if r == '\n' {
// Put the newline back so it can be processed as a soft break.
i.Back()
break
}
}
return nil, nil
case unicode.IsSpace(letter):
return nil, nil
case isVariable(letter):

View File

@@ -1,7 +1,5 @@
package set
import "iter"
type Set[T comparable] map[T]bool
func (s *Set[T]) Add(items ...T) {
@@ -36,16 +34,6 @@ func (s Set[T]) ToList() []T {
return list
}
func (s Set[T]) Items() iter.Seq[T] {
return func(yield func(T) bool) {
for item := range s {
if !yield(item) {
return
}
}
}
}
func New[T comparable](items ...T) *Set[T] {
result := &Set[T]{}

View File

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

16
samples/simple.txt 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
)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,8 +0,0 @@
0 := \f.\x.x
inc n := \f x.(f (n f x))
exp n m := (m n)
print n := (n F X)
N := (inc (inc (inc (inc (inc (inc 0))))))
(print (exp N N))

View File

@@ -1 +0,0 @@
VALUE

View File

@@ -1,17 +0,0 @@
# This is a full-line comment at the start
# The following defines the identity function
identity := \x.x # This is an end-of-line comment
# Define a simple function that applies a function twice
twice := \f.\x.(f
# Comments can be anywhere!
(f x))
# Test that comments don't interfere with expressions
result := (twice identity VALUE) # Should just return VALUE
# Multiple comments in a row
# can appear anywhere
# without breaking the code
result # Final comment at the end

View File

@@ -1 +0,0 @@
(0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (1 END)))))))))))))))))))))))))))))))

View File

@@ -1 +0,0 @@
(0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (1 END)))))))))))))))))))))))))))))))

View File

@@ -1 +0,0 @@
VALUE