Compare commits
14 Commits
588f4cd521
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| f2c8d9f7d2 | |||
| 9c7fb8ceba | |||
| e85cf7ceff | |||
| c2aa77cb92 | |||
| 52d40adcc6 | |||
| 1974ad582f | |||
| f8e1223463 | |||
| e0114c736d | |||
| 5c54f4e195 | |||
| 307b7ffd1e | |||
| 335ce95c50 | |||
| 19652563a4 | |||
| dec9af0244 | |||
| dbc3c5a8d4 |
58
.gitea/ISSUE_TEMPLATE/bug.md
Normal file
58
.gitea/ISSUE_TEMPLATE/bug.md
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
---
|
||||||
|
name: "Bug Report"
|
||||||
|
about: "Report a bug or unexpected behavior in the lambda runtime."
|
||||||
|
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.
|
||||||
|
-->
|
||||||
44
.gitea/ISSUE_TEMPLATE/feature.md
Normal file
44
.gitea/ISSUE_TEMPLATE/feature.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
---
|
||||||
|
name: "Feature Request"
|
||||||
|
about: "Suggest a new feature or enhancement for the lambda runtime."
|
||||||
|
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.
|
||||||
|
-->
|
||||||
37
.gitea/ISSUE_TEMPLATE/general.md
Normal file
37
.gitea/ISSUE_TEMPLATE/general.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
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.
|
||||||
|
-->
|
||||||
51
CLAUDE.md
51
CLAUDE.md
@@ -37,6 +37,31 @@ Use format: `<type>/<description>` with kebab-case.
|
|||||||
|
|
||||||
DO NOT advertise Claude.
|
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
|
## Pull Request Management
|
||||||
|
|
||||||
Use the `tea` CLI (Gitea command-line tool) for PR operations instead of `gh`.
|
Use the `tea` CLI (Gitea command-line tool) for PR operations instead of `gh`.
|
||||||
@@ -52,3 +77,29 @@ Use the `tea` CLI (Gitea command-line tool) for PR operations instead of `gh`.
|
|||||||
**Note**: Use `--description` (not `--body`) for PR body content.
|
**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.
|
**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"
|
||||||
|
```
|
||||||
|
|||||||
2
LICENSE
2
LICENSE
@@ -48,7 +48,7 @@ The "source code" for a work means the preferred form of the work for making mod
|
|||||||
|
|
||||||
A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language.
|
A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language.
|
||||||
|
|
||||||
The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it.
|
The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code runtime used to run it.
|
||||||
|
|
||||||
The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those
|
The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those
|
||||||
subprograms and other parts of the work.
|
subprograms and other parts of the work.
|
||||||
|
|||||||
8
Makefile
8
Makefile
@@ -1,18 +1,19 @@
|
|||||||
BINARY_NAME=lambda
|
BINARY_NAME=lambda
|
||||||
TEST=simple
|
TEST=simple
|
||||||
|
|
||||||
.PHONY: help build run profile explain graph docs bench clean
|
.PHONY: help build run profile explain graph docs test bench clean
|
||||||
.DEFAULT_GOAL := help
|
.DEFAULT_GOAL := help
|
||||||
.SILENT:
|
.SILENT:
|
||||||
|
|
||||||
help:
|
help:
|
||||||
echo "Available targets:"
|
echo "Available targets:"
|
||||||
echo " build - Build the lambda executable"
|
echo " build - Build the lambda executable"
|
||||||
echo " run - Build and run the lambda interpreter (use TEST=<name> to specify sample)"
|
echo " run - Build and run the lambda runtime (use TEST=<name> to specify sample)"
|
||||||
echo " profile - Build and run with CPU profiling enabled"
|
echo " profile - Build and run with CPU profiling enabled"
|
||||||
echo " explain - Build and run with explanation mode and profiling"
|
echo " explain - Build and run with explanation mode and profiling"
|
||||||
echo " graph - Generate and open CPU profile visualization"
|
echo " graph - Generate and open CPU profile visualization"
|
||||||
echo " docs - Start local godoc server on port 6060"
|
echo " docs - Start local godoc server on port 6060"
|
||||||
|
echo " test - Run tests for all samples"
|
||||||
echo " bench - Run benchmarks for all samples"
|
echo " bench - Run benchmarks for all samples"
|
||||||
echo " clean - Remove all build artifacts"
|
echo " clean - Remove all build artifacts"
|
||||||
|
|
||||||
@@ -38,6 +39,9 @@ docs:
|
|||||||
echo ">>> View at 'http://localhost:6060/pkg/git.maximhutz.com/max/lambda/'"
|
echo ">>> View at 'http://localhost:6060/pkg/git.maximhutz.com/max/lambda/'"
|
||||||
go run golang.org/x/tools/cmd/godoc@latest -http=:6060
|
go run golang.org/x/tools/cmd/godoc@latest -http=:6060
|
||||||
|
|
||||||
|
test:
|
||||||
|
go test -v ./cmd/lambda
|
||||||
|
|
||||||
bench:
|
bench:
|
||||||
go test -bench=. -benchtime=10x -cpu=4 ./cmd/lambda
|
go test -bench=. -benchtime=10x -cpu=4 ./cmd/lambda
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# lambda
|
# lambda
|
||||||
|
|
||||||
Making a lambda calculus interpreter in Go.
|
Making a lambda calculus runtime in Go.
|
||||||
|
|
||||||
## Things to talk about
|
## Things to talk about
|
||||||
|
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.maximhutz.com/max/lambda/internal/config"
|
|
||||||
"git.maximhutz.com/max/lambda/internal/engine"
|
|
||||||
"git.maximhutz.com/max/lambda/pkg/convert"
|
|
||||||
"git.maximhutz.com/max/lambda/pkg/lambda"
|
|
||||||
"git.maximhutz.com/max/lambda/pkg/saccharine"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Helper function to run a single sample through the lambda interpreter.
|
|
||||||
func runSample(samplePath 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 minimal config for benchmarking.
|
|
||||||
cfg := &config.Config{
|
|
||||||
Source: config.StringSource{Data: ""},
|
|
||||||
Destination: config.StdoutDestination{},
|
|
||||||
Profile: "",
|
|
||||||
Explanation: false,
|
|
||||||
Statistics: false,
|
|
||||||
Verbose: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create and run the engine.
|
|
||||||
process := engine.New(cfg, &compiled)
|
|
||||||
process.Run()
|
|
||||||
|
|
||||||
// Get final result (to ensure it's not optimized away).
|
|
||||||
_ = lambda.Stringify(compiled)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Benchmark all samples using sub-benchmarks.
|
|
||||||
func BenchmarkSamples(b *testing.B) {
|
|
||||||
samples := map[string]string{
|
|
||||||
"Church": "../../tests/church.test",
|
|
||||||
"Fast": "../../tests/fast.test",
|
|
||||||
"Saccharine": "../../tests/saccharine.test",
|
|
||||||
"Simple": "../../tests/simple.test",
|
|
||||||
"Thunk": "../../tests/thunk.test",
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, path := range samples {
|
|
||||||
b.Run(name, func(b *testing.B) {
|
|
||||||
for b.Loop() {
|
|
||||||
if err := runSample(path); err != nil {
|
|
||||||
b.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,12 +5,9 @@ import (
|
|||||||
|
|
||||||
"git.maximhutz.com/max/lambda/internal/cli"
|
"git.maximhutz.com/max/lambda/internal/cli"
|
||||||
"git.maximhutz.com/max/lambda/internal/config"
|
"git.maximhutz.com/max/lambda/internal/config"
|
||||||
"git.maximhutz.com/max/lambda/internal/engine"
|
"git.maximhutz.com/max/lambda/internal/plugins"
|
||||||
"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/convert"
|
||||||
"git.maximhutz.com/max/lambda/pkg/lambda"
|
"git.maximhutz.com/max/lambda/pkg/normalorder"
|
||||||
"git.maximhutz.com/max/lambda/pkg/saccharine"
|
"git.maximhutz.com/max/lambda/pkg/saccharine"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -34,44 +31,37 @@ func main() {
|
|||||||
|
|
||||||
// Compile expression to lambda calculus.
|
// Compile expression to lambda calculus.
|
||||||
compiled := convert.SaccharineToLambda(ast)
|
compiled := convert.SaccharineToLambda(ast)
|
||||||
logger.Info("compiled λ expression", "tree", lambda.Stringify(compiled))
|
logger.Info("compiled λ expression", "tree", compiled.String())
|
||||||
|
|
||||||
// Create reduction engine.
|
// Create reducer with the compiled expression.
|
||||||
process := engine.New(options, &compiled)
|
runtime := normalorder.NewRuntime(compiled)
|
||||||
|
|
||||||
// If the user selected to track CPU performance, attach a profiler to the
|
// If the user selected to track CPU performance, attach a profiler.
|
||||||
// process.
|
|
||||||
if options.Profile != "" {
|
if options.Profile != "" {
|
||||||
profiler := performance.Track(options.Profile)
|
plugins.NewPerformance(options.Profile, runtime)
|
||||||
process.On("start", profiler.Start)
|
|
||||||
process.On("end", profiler.End)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the user selected to produce a step-by-step explanation, attach an
|
// If the user selected to produce a step-by-step explanation, attach an
|
||||||
// observer here.
|
// observer.
|
||||||
if options.Explanation {
|
if options.Explanation {
|
||||||
explanation.Track(process)
|
plugins.NewExplanation(runtime)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the user opted to track statistics, attach a tracker here, too.
|
// If the user opted to track statistics, attach a tracker.
|
||||||
if options.Statistics {
|
if options.Statistics {
|
||||||
statistics := statistics.Track()
|
plugins.NewStatistics(runtime)
|
||||||
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 the user selected for verbose debug logs, attach a reduction tracker.
|
||||||
if options.Verbose {
|
if options.Verbose {
|
||||||
process.On("step", func() {
|
plugins.NewLogs(logger, runtime)
|
||||||
logger.Info("reduction", "tree", lambda.Stringify(compiled))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
process.Run()
|
// Run reduction.
|
||||||
|
runtime.Run()
|
||||||
|
|
||||||
// Return the final reduced result.
|
// Return the final reduced result.
|
||||||
result := lambda.Stringify(compiled)
|
result := runtime.Expression().String()
|
||||||
err = options.Destination.Write(result)
|
err = options.Destination.Write(result)
|
||||||
cli.HandleError(err)
|
cli.HandleError(err)
|
||||||
}
|
}
|
||||||
|
|||||||
85
cmd/lambda/lambda_test.go
Normal file
85
cmd/lambda/lambda_test.go
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/convert"
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/normalorder"
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/saccharine"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Helper function to run a single sample through the lambda runtime.
|
||||||
|
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 := normalorder.NewRuntime(compiled)
|
||||||
|
reducer.Run()
|
||||||
|
|
||||||
|
return reducer.Expression().String() + "\n", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that all samples produce expected output.
|
||||||
|
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.")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
8
go.mod
8
go.mod
@@ -1,3 +1,11 @@
|
|||||||
module git.maximhutz.com/max/lambda
|
module git.maximhutz.com/max/lambda
|
||||||
|
|
||||||
go 1.25.5
|
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
Normal file
9
go.sum
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
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=
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
// 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")
|
|
||||||
|
|
||||||
lambda.ReduceAll(e.Expression, func() {
|
|
||||||
e.Emit("step")
|
|
||||||
})
|
|
||||||
|
|
||||||
e.Emit("end")
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
// 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))
|
|
||||||
}
|
|
||||||
23
internal/plugins/debug.go
Normal file
23
internal/plugins/debug.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package plugins
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Logs struct {
|
||||||
|
logger *slog.Logger
|
||||||
|
reducer runtime.Runtime
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLogs(logger *slog.Logger, r runtime.Runtime) *Logs {
|
||||||
|
plugin := &Logs{logger, r}
|
||||||
|
r.On(runtime.StepEvent, plugin.Step)
|
||||||
|
|
||||||
|
return plugin
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Logs) Step() {
|
||||||
|
t.logger.Info("reduction", "tree", t.reducer.Expression().String())
|
||||||
|
}
|
||||||
31
internal/plugins/explanation.go
Normal file
31
internal/plugins/explanation.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
// 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/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Track the reductions made by a reduction process.
|
||||||
|
type Explanation struct {
|
||||||
|
reducer runtime.Runtime
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attaches a new explanation tracker to a reducer.
|
||||||
|
func NewExplanation(r runtime.Runtime) *Explanation {
|
||||||
|
plugin := &Explanation{reducer: r}
|
||||||
|
r.On(runtime.StartEvent, plugin.Start)
|
||||||
|
r.On(runtime.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())
|
||||||
|
}
|
||||||
@@ -1,28 +1,34 @@
|
|||||||
// Package "performance" provides a tracker to observer CPU performance during
|
// Package "performance" provides a tracker to observer CPU performance during
|
||||||
// execution.
|
// execution.
|
||||||
package performance
|
package plugins
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime/pprof"
|
"runtime/pprof"
|
||||||
|
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/runtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Observes a reduction process, and publishes a CPU performance profile on
|
// Observes a reduction process, and publishes a CPU performance profile on
|
||||||
// completion.
|
// completion.
|
||||||
type Tracker struct {
|
type Performance struct {
|
||||||
File string
|
File string
|
||||||
filePointer *os.File
|
filePointer *os.File
|
||||||
Error error
|
Error error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a performance tracker that outputs a profile to "file".
|
// Create a performance tracker that outputs a profile to "file".
|
||||||
func Track(file string) *Tracker {
|
func NewPerformance(file string, process runtime.Runtime) *Performance {
|
||||||
return &Tracker{File: file}
|
plugin := &Performance{File: file}
|
||||||
|
process.On(runtime.StartEvent, plugin.Start)
|
||||||
|
process.On(runtime.StopEvent, plugin.Stop)
|
||||||
|
|
||||||
|
return plugin
|
||||||
}
|
}
|
||||||
|
|
||||||
// Begin profiling.
|
// Begin profiling.
|
||||||
func (t *Tracker) Start() {
|
func (t *Performance) Start() {
|
||||||
var absPath string
|
var absPath string
|
||||||
|
|
||||||
absPath, t.Error = filepath.Abs(t.File)
|
absPath, t.Error = filepath.Abs(t.File)
|
||||||
@@ -47,7 +53,7 @@ func (t *Tracker) Start() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Stop profiling.
|
// Stop profiling.
|
||||||
func (t *Tracker) End() {
|
func (t *Performance) Stop() {
|
||||||
pprof.StopCPUProfile()
|
pprof.StopCPUProfile()
|
||||||
t.filePointer.Close()
|
t.filePointer.Close()
|
||||||
}
|
}
|
||||||
44
internal/plugins/statistics.go
Normal file
44
internal/plugins/statistics.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package plugins
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.maximhutz.com/max/lambda/internal/statistics"
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// An observer, to track reduction performance.
|
||||||
|
type Statistics struct {
|
||||||
|
start time.Time
|
||||||
|
steps uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new reduction performance Statistics.
|
||||||
|
func NewStatistics(r runtime.Runtime) *Statistics {
|
||||||
|
plugin := &Statistics{}
|
||||||
|
r.On(runtime.StartEvent, plugin.Start)
|
||||||
|
r.On(runtime.StepEvent, plugin.Step)
|
||||||
|
r.On(runtime.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())
|
||||||
|
}
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
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())
|
|
||||||
}
|
|
||||||
@@ -19,7 +19,7 @@ func convertAbstraction(n *saccharine.Abstraction) lambda.Expression {
|
|||||||
// If the function has no parameters, it is a thunk. Lambda calculus still
|
// If the function has no parameters, it is a thunk. Lambda calculus still
|
||||||
// requires _some_ parameter exists, so generate one.
|
// requires _some_ parameter exists, so generate one.
|
||||||
if len(parameters) == 0 {
|
if len(parameters) == 0 {
|
||||||
freeVars := lambda.GetFreeVariables(result)
|
freeVars := result.GetFree()
|
||||||
freshName := lambda.GenerateFreshName(freeVars)
|
freshName := lambda.GenerateFreshName(freeVars)
|
||||||
parameters = append(parameters, freshName)
|
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 {
|
func reduceDeclare(s *saccharine.DeclareStatement, e lambda.Expression) lambda.Expression {
|
||||||
freshVar := lambda.GenerateFreshName(lambda.GetFreeVariables(e))
|
freshVar := lambda.GenerateFreshName(e.GetFree())
|
||||||
|
|
||||||
return lambda.NewApplication(
|
return lambda.NewApplication(
|
||||||
lambda.NewAbstraction(freshVar, e),
|
lambda.NewAbstraction(freshVar, e),
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
// Package "deltanet" is a reduction strategy using ∆-nets.
|
|
||||||
package deltanet
|
|
||||||
|
|
||||||
type Graph struct {
|
|
||||||
Nodes []Node
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
package deltanet
|
|
||||||
|
|
||||||
/** ------------------------------------------------------------------------- */
|
|
||||||
|
|
||||||
// A connection between exactly two nodes in a graph.
|
|
||||||
type Edge struct {
|
|
||||||
A, B Node
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns all nodes the edge is connected to.
|
|
||||||
func (e Edge) GetConnections() []Node { return []Node{e.A, e.B} }
|
|
||||||
|
|
||||||
// Determines if a node is connected via this edge.
|
|
||||||
func (e Edge) IsConnected(n Node) bool { return e.A == n || e.B == n }
|
|
||||||
|
|
||||||
// Swaps an edges connected with one node, for another.
|
|
||||||
func (e *Edge) Swap(from Node, to Node) {
|
|
||||||
if e.A == from {
|
|
||||||
e.A = to
|
|
||||||
}
|
|
||||||
if e.B == from {
|
|
||||||
e.B = to
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns true if the edge is connected to each node via their pricniple ports.
|
|
||||||
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
|
|
||||||
|
|
||||||
// Returns all auxiliary ports that the node has. These ports are guaranteed
|
|
||||||
// to be ordered clockwise, as they would appear graphically.
|
|
||||||
GetAuxPorts() []Edge
|
|
||||||
|
|
||||||
// Returns the label of the node. May be blank.
|
|
||||||
GetLabel() string
|
|
||||||
}
|
|
||||||
|
|
||||||
/** ------------------------------------------------------------------------- */
|
|
||||||
|
|
||||||
type EraserNode struct {
|
|
||||||
Main Edge
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
Aux []Edge
|
|
||||||
Deltas []int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n ReplicatorNode) GetLabel() string { return "" }
|
|
||||||
func (n ReplicatorNode) GetMainPort() Edge { return n.Main }
|
|
||||||
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
|
|
||||||
Left, Right Edge
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n TerminalNode) GetLabel() string { return n.Label }
|
|
||||||
func (n TerminalNode) GetMainPort() Edge { return n.Main }
|
|
||||||
func (n TerminalNode) GetAuxPorts() []Edge { return []Edge{} }
|
|
||||||
|
|
||||||
/** ------------------------------------------------------------------------- */
|
|
||||||
@@ -2,53 +2,45 @@ package emitter
|
|||||||
|
|
||||||
import "git.maximhutz.com/max/lambda/pkg/set"
|
import "git.maximhutz.com/max/lambda/pkg/set"
|
||||||
|
|
||||||
type Observer struct {
|
type Emitter[E comparable] interface {
|
||||||
fn func()
|
On(E, func()) Listener[E]
|
||||||
message string
|
Off(Listener[E])
|
||||||
emitter *Emitter
|
Emit(E)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Emitter struct {
|
type BaseEmitter[E comparable] struct {
|
||||||
listeners map[string]*set.Set[*Observer]
|
listeners map[E]set.Set[Listener[E]]
|
||||||
}
|
}
|
||||||
|
|
||||||
func Ignore[T any](fn func()) func(T) {
|
func (e *BaseEmitter[E]) On(kind E, fn func()) Listener[E] {
|
||||||
return func(T) { fn() }
|
if e.listeners[kind] == nil {
|
||||||
|
e.listeners[kind] = set.New[Listener[E]]()
|
||||||
|
}
|
||||||
|
|
||||||
|
listener := &BaseListener[E]{kind, fn}
|
||||||
|
e.listeners[kind].Add(listener)
|
||||||
|
return listener
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Emitter) On(message string, fn func()) *Observer {
|
func (e *BaseEmitter[E]) Off(listener Listener[E]) {
|
||||||
observer := &Observer{
|
kind := listener.Kind()
|
||||||
fn: fn,
|
if e.listeners[kind] != nil {
|
||||||
message: message,
|
e.listeners[kind].Remove(listener)
|
||||||
emitter: e,
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if e.listeners == nil {
|
func (e *BaseEmitter[E]) Emit(event E) {
|
||||||
e.listeners = map[string]*set.Set[*Observer]{}
|
if e.listeners[event] == nil {
|
||||||
}
|
e.listeners[event] = set.New[Listener[E]]()
|
||||||
|
}
|
||||||
if e.listeners[message] == nil {
|
|
||||||
e.listeners[message] = set.New[*Observer]()
|
for listener := range e.listeners[event].Items() {
|
||||||
}
|
listener.Run()
|
||||||
|
}
|
||||||
e.listeners[message].Add(observer)
|
}
|
||||||
return observer
|
|
||||||
}
|
func New[E comparable]() *BaseEmitter[E] {
|
||||||
|
return &BaseEmitter[E]{
|
||||||
func (o *Observer) Off() {
|
listeners: map[E]set.Set[Listener[E]]{},
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
19
pkg/emitter/listener.go
Normal file
19
pkg/emitter/listener.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
15
pkg/expr/expr.go
Normal file
15
pkg/expr/expr.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
// Package expr provides the abstract Expression interface for all evaluatable
|
||||||
|
// expression types in the lambda runtime.
|
||||||
|
package expr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
// The expression should have a human-readable representation.
|
||||||
|
fmt.Stringer
|
||||||
|
}
|
||||||
@@ -1,7 +1,31 @@
|
|||||||
package lambda
|
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 {
|
type Expression interface {
|
||||||
Accept(Visitor)
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
/** ------------------------------------------------------------------------- */
|
/** ------------------------------------------------------------------------- */
|
||||||
@@ -11,20 +35,22 @@ type Abstraction struct {
|
|||||||
body Expression
|
body Expression
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Abstraction) Parameter() string {
|
var _ Expression = Abstraction{}
|
||||||
|
|
||||||
|
func (a Abstraction) Parameter() string {
|
||||||
return a.parameter
|
return a.parameter
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Abstraction) Body() Expression {
|
func (a Abstraction) Body() Expression {
|
||||||
return a.body
|
return a.body
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Abstraction) Accept(v Visitor) {
|
func (a Abstraction) String() string {
|
||||||
v.VisitAbstraction(a)
|
return "\\" + a.parameter + "." + a.body.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAbstraction(parameter string, body Expression) *Abstraction {
|
func NewAbstraction(parameter string, body Expression) Abstraction {
|
||||||
return &Abstraction{parameter: parameter, body: body}
|
return Abstraction{parameter, body}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** ------------------------------------------------------------------------- */
|
/** ------------------------------------------------------------------------- */
|
||||||
@@ -34,44 +60,40 @@ type Application struct {
|
|||||||
argument Expression
|
argument Expression
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Application) Abstraction() Expression {
|
var _ Expression = Application{}
|
||||||
|
|
||||||
|
func (a Application) Abstraction() Expression {
|
||||||
return a.abstraction
|
return a.abstraction
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Application) Argument() Expression {
|
func (a Application) Argument() Expression {
|
||||||
return a.argument
|
return a.argument
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Application) Accept(v Visitor) {
|
func (a Application) String() string {
|
||||||
v.VisitApplication(a)
|
return "(" + a.abstraction.String() + " " + a.argument.String() + ")"
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewApplication(abstraction Expression, argument Expression) *Application {
|
func NewApplication(abstraction Expression, argument Expression) Application {
|
||||||
return &Application{abstraction: abstraction, argument: argument}
|
return Application{abstraction, argument}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** ------------------------------------------------------------------------- */
|
/** ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
type Variable struct {
|
type Variable struct {
|
||||||
value string
|
name string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *Variable) Value() string {
|
var _ Expression = Variable{}
|
||||||
return v.value
|
|
||||||
|
func (v Variable) Name() string {
|
||||||
|
return v.name
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *Variable) Accept(visitor Visitor) {
|
func (v Variable) String() string {
|
||||||
visitor.VisitVariable(v)
|
return v.name
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewVariable(name string) *Variable {
|
func NewVariable(name string) Variable {
|
||||||
return &Variable{value: name}
|
return Variable{name}
|
||||||
}
|
|
||||||
|
|
||||||
/** ------------------------------------------------------------------------- */
|
|
||||||
|
|
||||||
type Visitor interface {
|
|
||||||
VisitAbstraction(*Abstraction)
|
|
||||||
VisitApplication(*Application)
|
|
||||||
VisitVariable(*Variable)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import (
|
|||||||
"git.maximhutz.com/max/lambda/pkg/set"
|
"git.maximhutz.com/max/lambda/pkg/set"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GenerateFreshName(used *set.Set[string]) string {
|
// 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 {
|
||||||
for i := uint64(0); ; i++ {
|
for i := uint64(0); ; i++ {
|
||||||
attempt := "_" + string(strconv.AppendUint(nil, i, 10))
|
attempt := "_" + string(strconv.AppendUint(nil, i, 10))
|
||||||
|
|
||||||
|
|||||||
@@ -2,19 +2,18 @@ package lambda
|
|||||||
|
|
||||||
import "git.maximhutz.com/max/lambda/pkg/set"
|
import "git.maximhutz.com/max/lambda/pkg/set"
|
||||||
|
|
||||||
func GetFreeVariables(e Expression) *set.Set[string] {
|
func (e Variable) GetFree() set.Set[string] {
|
||||||
switch e := e.(type) {
|
return set.New(e.Name())
|
||||||
case *Variable:
|
}
|
||||||
return set.New(e.value)
|
|
||||||
case *Abstraction:
|
func (e Abstraction) GetFree() set.Set[string] {
|
||||||
vars := GetFreeVariables(e.body)
|
vars := e.Body().GetFree()
|
||||||
vars.Remove(e.parameter)
|
vars.Remove(e.Parameter())
|
||||||
return vars
|
return vars
|
||||||
case *Application:
|
}
|
||||||
vars := GetFreeVariables(e.abstraction)
|
|
||||||
vars.Merge(GetFreeVariables(e.argument))
|
func (e Application) GetFree() set.Set[string] {
|
||||||
return vars
|
vars := e.Abstraction().GetFree()
|
||||||
default:
|
vars.Merge(e.Argument().GetFree())
|
||||||
return nil
|
return vars
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
package lambda
|
package lambda
|
||||||
|
|
||||||
func IsFreeVariable(n string, e Expression) bool {
|
func (e Variable) IsFree(n string) bool {
|
||||||
switch e := e.(type) {
|
return e.Name() == n
|
||||||
case *Variable:
|
}
|
||||||
return e.value == n
|
|
||||||
case *Abstraction:
|
func (e Abstraction) IsFree(n string) bool {
|
||||||
return e.parameter != n && IsFreeVariable(n, e.body)
|
return e.Parameter() != n && e.Body().IsFree(n)
|
||||||
case *Application:
|
}
|
||||||
return IsFreeVariable(n, e.abstraction) || IsFreeVariable(n, e.argument)
|
func (e Application) IsFree(n string) bool {
|
||||||
default:
|
return e.Abstraction().IsFree(n) || e.Argument().IsFree(n)
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package lambda
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ReduceAll(e *Expression, step func()) {
|
|
||||||
it := NewIterator(e)
|
|
||||||
|
|
||||||
for !it.Done() {
|
|
||||||
if fn, arg, ok := IsViable(it.Current()); !ok {
|
|
||||||
it.Next()
|
|
||||||
} else {
|
|
||||||
it.Swap(Substitute(fn.body, fn.parameter, arg))
|
|
||||||
step()
|
|
||||||
|
|
||||||
if _, _, ok := IsViable(it.Parent()); ok {
|
|
||||||
it.Back()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +1,28 @@
|
|||||||
package lambda
|
package lambda
|
||||||
|
|
||||||
func Rename(expr Expression, target string, newName string) Expression {
|
// Rename replaces all occurrences of the target variable name with the new name.
|
||||||
switch e := expr.(type) {
|
func (e Variable) Rename(target string, newName string) Expression {
|
||||||
case *Variable:
|
if e.Name() == target {
|
||||||
if e.value == target {
|
|
||||||
return NewVariable(newName)
|
return NewVariable(newName)
|
||||||
}
|
}
|
||||||
return e
|
|
||||||
|
|
||||||
case *Abstraction:
|
return e
|
||||||
newParam := e.parameter
|
}
|
||||||
if e.parameter == target {
|
|
||||||
|
func (e Abstraction) Rename(target string, newName string) Expression {
|
||||||
|
newParam := e.Parameter()
|
||||||
|
if e.Parameter() == target {
|
||||||
newParam = newName
|
newParam = newName
|
||||||
}
|
}
|
||||||
|
|
||||||
newBody := Rename(e.body, target, newName)
|
newBody := e.Body().Rename(target, newName)
|
||||||
|
|
||||||
if newParam == e.parameter && newBody == e.body {
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
|
|
||||||
return NewAbstraction(newParam, newBody)
|
return NewAbstraction(newParam, newBody)
|
||||||
|
}
|
||||||
|
|
||||||
case *Application:
|
func (e Application) Rename(target string, newName string) Expression {
|
||||||
newAbs := Rename(e.abstraction, target, newName)
|
newAbs := e.Abstraction().Rename(target, newName)
|
||||||
newArg := Rename(e.argument, target, newName)
|
newArg := e.Argument().Rename(target, newName)
|
||||||
|
|
||||||
if newAbs == e.abstraction && newArg == e.argument {
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
|
|
||||||
return NewApplication(newAbs, newArg)
|
return NewApplication(newAbs, newArg)
|
||||||
|
|
||||||
default:
|
|
||||||
return expr
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
package lambda
|
|
||||||
|
|
||||||
import "strings"
|
|
||||||
|
|
||||||
type stringifyVisitor struct {
|
|
||||||
builder strings.Builder
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *stringifyVisitor) VisitVariable(a *Variable) {
|
|
||||||
v.builder.WriteString(a.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *stringifyVisitor) VisitAbstraction(f *Abstraction) {
|
|
||||||
v.builder.WriteRune('\\')
|
|
||||||
v.builder.WriteString(f.parameter)
|
|
||||||
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(')')
|
|
||||||
}
|
|
||||||
|
|
||||||
func Stringify(e Expression) string {
|
|
||||||
b := &stringifyVisitor{builder: strings.Builder{}}
|
|
||||||
e.Accept(b)
|
|
||||||
return b.builder.String()
|
|
||||||
}
|
|
||||||
@@ -1,46 +1,35 @@
|
|||||||
package lambda
|
package lambda
|
||||||
|
|
||||||
func Substitute(expr Expression, target string, replacement Expression) Expression {
|
func (e Variable) Substitute(target string, replacement Expression) Expression {
|
||||||
switch e := expr.(type) {
|
if e.Name() == target {
|
||||||
case *Variable:
|
|
||||||
if e.value == target {
|
|
||||||
return replacement
|
return replacement
|
||||||
}
|
}
|
||||||
return e
|
|
||||||
|
|
||||||
case *Abstraction:
|
return e
|
||||||
if e.parameter == target {
|
}
|
||||||
|
|
||||||
|
func (e Abstraction) Substitute(target string, replacement Expression) Expression {
|
||||||
|
if e.Parameter() == target {
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
body := e.body
|
body := e.Body()
|
||||||
param := e.parameter
|
param := e.Parameter()
|
||||||
if IsFreeVariable(param, replacement) {
|
if replacement.IsFree(param) {
|
||||||
freeVars := GetFreeVariables(replacement)
|
freeVars := replacement.GetFree()
|
||||||
freeVars.Merge(GetFreeVariables(body))
|
freeVars.Merge(body.GetFree())
|
||||||
freshVar := GenerateFreshName(freeVars)
|
freshVar := GenerateFreshName(freeVars)
|
||||||
body = Rename(body, param, freshVar)
|
body = body.Rename(param, freshVar)
|
||||||
param = freshVar
|
param = freshVar
|
||||||
}
|
}
|
||||||
|
|
||||||
newBody := Substitute(body, target, replacement)
|
newBody := body.Substitute(target, replacement)
|
||||||
if newBody == body && param == e.parameter {
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
|
|
||||||
return NewAbstraction(param, newBody)
|
return NewAbstraction(param, newBody)
|
||||||
|
}
|
||||||
case *Application:
|
|
||||||
newAbs := Substitute(e.abstraction, target, replacement)
|
func (e Application) Substitute(target string, replacement Expression) Expression {
|
||||||
newArg := Substitute(e.argument, target, replacement)
|
abs := e.Abstraction().Substitute(target, replacement)
|
||||||
|
arg := e.Argument().Substitute(target, replacement)
|
||||||
if newAbs == e.abstraction && newArg == e.argument {
|
|
||||||
return e
|
return NewApplication(abs, arg)
|
||||||
}
|
|
||||||
|
|
||||||
return NewApplication(newAbs, newArg)
|
|
||||||
|
|
||||||
default:
|
|
||||||
return expr
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
|
}
|
||||||
46
pkg/normalorder/runtime.go
Normal file
46
pkg/normalorder/runtime.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package normalorder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/emitter"
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/expr"
|
||||||
|
"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 Runtime struct {
|
||||||
|
emitter.BaseEmitter[runtime.Event]
|
||||||
|
expression lambda.Expression
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNormalOrderReducer creates a new normal order reducer.
|
||||||
|
func NewRuntime(expression lambda.Expression) *Runtime {
|
||||||
|
return &Runtime{
|
||||||
|
BaseEmitter: *emitter.New[runtime.Event](),
|
||||||
|
expression: expression,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expression returns the current expression state.
|
||||||
|
func (r *Runtime) Expression() expr.Expression {
|
||||||
|
return r.expression
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runtime) Step() bool {
|
||||||
|
result, done := ReduceOnce(r.expression)
|
||||||
|
r.expression = result
|
||||||
|
return !done
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reduce performs normal order reduction on a lambda expression.
|
||||||
|
// The expression must be a lambda.Expression; other types are returned unchanged.
|
||||||
|
func (r *Runtime) Run() {
|
||||||
|
r.Emit(runtime.StartEvent)
|
||||||
|
|
||||||
|
for !r.Step() {
|
||||||
|
r.Emit(runtime.StepEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Emit(runtime.StopEvent)
|
||||||
|
}
|
||||||
13
pkg/runtime/events.go
Normal file
13
pkg/runtime/events.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package runtime
|
||||||
|
|
||||||
|
// Event represents lifecycle events during interpretation.
|
||||||
|
type Event int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// StartEvent is emitted before interpretation begins.
|
||||||
|
StartEvent Event = iota
|
||||||
|
// StepEvent is emitted after each interpretation step.
|
||||||
|
StepEvent
|
||||||
|
// StopEvent is emitted after interpretation completes.
|
||||||
|
StopEvent
|
||||||
|
)
|
||||||
27
pkg/runtime/runtime.go
Normal file
27
pkg/runtime/runtime.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// Package runtime provides the abstract Reducer interface for all expression
|
||||||
|
// reduction strategies.
|
||||||
|
package runtime
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/emitter"
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/expr"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
//
|
||||||
|
// Runtimes also implement the Emitter interface to allow plugins to observe
|
||||||
|
// reduction lifecycle events (Start, Step, Stop).
|
||||||
|
type Runtime interface {
|
||||||
|
emitter.Emitter[Event]
|
||||||
|
|
||||||
|
// Run a single step. Returns whether the runtime is complete or not.
|
||||||
|
Step() bool
|
||||||
|
|
||||||
|
// Run until completion.
|
||||||
|
Run()
|
||||||
|
|
||||||
|
// Copy the state of the runtime.
|
||||||
|
Expression() expr.Expression
|
||||||
|
}
|
||||||
@@ -77,6 +77,21 @@ func getToken(i *iterator.Iterator[rune]) (*Token, error) {
|
|||||||
}
|
}
|
||||||
case letter == ';':
|
case letter == ';':
|
||||||
return NewHardBreak(index), nil
|
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):
|
case unicode.IsSpace(letter):
|
||||||
return nil, nil
|
return nil, nil
|
||||||
case isVariable(letter):
|
case isVariable(letter):
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
package set
|
package set
|
||||||
|
|
||||||
|
import "iter"
|
||||||
|
|
||||||
type Set[T comparable] map[T]bool
|
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 {
|
for _, item := range items {
|
||||||
(*s)[item] = true
|
s[item] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -12,14 +14,14 @@ func (s Set[T]) Has(item T) bool {
|
|||||||
return s[item]
|
return s[item]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Set[T]) Remove(items ...T) {
|
func (s Set[T]) Remove(items ...T) {
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
delete(*s, item)
|
delete(s, item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Set[T]) Merge(o *Set[T]) {
|
func (s Set[T]) Merge(o Set[T]) {
|
||||||
for item := range *o {
|
for item := range o {
|
||||||
s.Add(item)
|
s.Add(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -34,8 +36,18 @@ func (s Set[T]) ToList() []T {
|
|||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
func New[T comparable](items ...T) *Set[T] {
|
func (s Set[T]) Items() iter.Seq[T] {
|
||||||
result := &Set[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]{}
|
||||||
|
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
result.Add(item)
|
result.Add(item)
|
||||||
|
|||||||
1
tests/church_5^5.expected
Normal file
1
tests/church_5^5.expected
Normal file
File diff suppressed because one or more lines are too long
@@ -1,7 +1,8 @@
|
|||||||
0 := \f.\x.x
|
0 := \f.\x.x
|
||||||
inc n := \f x.(f (n f x))
|
inc n := \f x.(f (n f x))
|
||||||
exp n m := (m n)
|
exp n m := (m n)
|
||||||
|
print n := (n F X)
|
||||||
|
|
||||||
N := (inc (inc (inc (inc (inc 0)))))
|
N := (inc (inc (inc (inc (inc 0)))))
|
||||||
|
|
||||||
(exp N N)
|
(print (exp N N))
|
||||||
1
tests/comments.expected
Normal file
1
tests/comments.expected
Normal file
@@ -0,0 +1 @@
|
|||||||
|
VALUE
|
||||||
17
tests/comments.test
Normal file
17
tests/comments.test
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# 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
|
||||||
1
tests/fast_list_2^30.expected
Normal file
1
tests/fast_list_2^30.expected
Normal file
@@ -0,0 +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 (1 END)))))))))))))))))))))))))))))))
|
||||||
1
tests/list_2^30.expected
Normal file
1
tests/list_2^30.expected
Normal file
@@ -0,0 +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 (1 END)))))))))))))))))))))))))))))))
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
(\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.expected
Normal file
1
tests/thunk.expected
Normal file
@@ -0,0 +1 @@
|
|||||||
|
VALUE
|
||||||
Reference in New Issue
Block a user