Compare commits
39 Commits
1d981ecce3
...
feat/updat
| Author | SHA1 | Date | |
|---|---|---|---|
|
31924237b2
|
|||
|
68cc1624c7
|
|||
|
0cdce0e42c
|
|||
|
0ec52008bb
|
|||
| f2c8d9f7d2 | |||
| 9c7fb8ceba | |||
| e85cf7ceff | |||
| c2aa77cb92 | |||
| 52d40adcc6 | |||
| 1974ad582f | |||
| f8e1223463 | |||
| e0114c736d | |||
| 5c54f4e195 | |||
| 307b7ffd1e | |||
| 335ce95c50 | |||
| 19652563a4 | |||
| dec9af0244 | |||
| dbc3c5a8d4 | |||
| 15c904ccc9 | |||
| 609fe05250 | |||
| 0eff85f8fa | |||
| 62699a0e37 | |||
| 90c205db2e | |||
| 72a0afbbc0 | |||
| 22019acbb1 | |||
| d831312dc3 | |||
| c0353c8e1f | |||
| e63c0df410 | |||
| 8b6c632e4d | |||
| 7a5944594f | |||
|
242fda3b4a
|
|||
|
2499921679
|
|||
|
412d3924eb
|
|||
|
05cd8bc4f3
|
|||
|
aabe92f2dc
|
|||
|
13989e4c61
|
|||
|
529abb7c26
|
|||
|
351faa7e08
|
|||
|
3f9f3a603f
|
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.
|
||||
-->
|
||||
37
.gitea/PULL_REQUEST_TEMPLATE.md
Normal file
37
.gitea/PULL_REQUEST_TEMPLATE.md
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
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).
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -3,15 +3,13 @@
|
||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||
#
|
||||
# Binaries for programs and plugins
|
||||
/lambda
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ linters:
|
||||
# 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
|
||||
# - comments
|
||||
|
||||
# Common false positives
|
||||
# feel free to remove this if you don't have any false positives
|
||||
@@ -126,6 +126,9 @@ linters:
|
||||
# Blank import should be only in a main or test package, or have a comment justifying it.
|
||||
- name: blank-imports
|
||||
|
||||
# Packages should have comments of the form "Package x ...".
|
||||
- name: package-comments
|
||||
|
||||
# context.Context() should be the first parameter of a function when provided as argument.
|
||||
- name: context-as-argument
|
||||
arguments:
|
||||
|
||||
105
CLAUDE.md
Normal file
105
CLAUDE.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# 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"
|
||||
```
|
||||
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.
|
||||
|
||||
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
|
||||
subprograms and other parts of the work.
|
||||
|
||||
64
Makefile
64
Makefile
@@ -1,21 +1,55 @@
|
||||
BINARY_NAME=lambda.exe
|
||||
|
||||
it:
|
||||
@ go build -o ${BINARY_NAME} ./cmd/lambda
|
||||
@ chmod +x ${BINARY_NAME}
|
||||
|
||||
BINARY_NAME=lambda
|
||||
TEST=simple
|
||||
|
||||
run: it
|
||||
@ ./lambda.exe - < ./samples/$(TEST).txt > program.out
|
||||
.PHONY: help build run profile explain graph docs test bench lint clean
|
||||
.DEFAULT_GOAL := help
|
||||
.SILENT:
|
||||
|
||||
profile: it
|
||||
@ ./lambda.exe -p profile/cpu.prof - < ./samples/$(TEST).txt > program.out
|
||||
help:
|
||||
echo "Available targets:"
|
||||
echo " build - Build the lambda executable"
|
||||
echo " run - Build and run the lambda runtime (use TEST=<name> to specify sample)"
|
||||
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 " lint - Run golangci-lint on all packages"
|
||||
echo " clean - Remove all build artifacts"
|
||||
|
||||
explain: it
|
||||
@ ./lambda.exe -x -p profile/cpu.prof - < ./samples/$(TEST).txt > program.out
|
||||
build:
|
||||
go build -o ${BINARY_NAME} ./cmd/lambda
|
||||
chmod +x ${BINARY_NAME}
|
||||
|
||||
run: build
|
||||
./${BINARY_NAME} -s -f ./tests/$(TEST).test -o program.out
|
||||
|
||||
profile: build
|
||||
./${BINARY_NAME} -p profile/cpu.prof -f ./tests/$(TEST).test -o program.out
|
||||
|
||||
explain: build
|
||||
./${BINARY_NAME} -x -p profile/cpu.prof -f ./tests/$(TEST).test -o program.out > explain.out
|
||||
|
||||
graph:
|
||||
@ go tool pprof -raw -output=profile/cpu.raw profile/cpu.prof
|
||||
@ go tool pprof -svg profile/cpu.prof > profile/cpu.svg
|
||||
@ open profile/cpu.svg
|
||||
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
|
||||
|
||||
lint:
|
||||
go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest run ./...
|
||||
|
||||
clean:
|
||||
rm -f ${BINARY_NAME}
|
||||
rm -f program.out
|
||||
rm -rf profile/
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
# lambda
|
||||
|
||||
Making a lambda calculus interpreter in Go.
|
||||
Making a lambda calculus runtime in Go.
|
||||
|
||||
## Things to talk about
|
||||
|
||||
- Exhaustive sum types.
|
||||
- Recursive descent and left-recursion.
|
||||
- Observer pattern, event emission.
|
||||
|
||||
## Links
|
||||
|
||||
<https://zicklag.katharos.group/blog/interaction-nets-combinators-calculus/>
|
||||
<https://arxiv.org/pdf/2505.20314>
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
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/executer"
|
||||
"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/internal/plugins"
|
||||
"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"
|
||||
)
|
||||
|
||||
// Run main application.
|
||||
func main() {
|
||||
// Parse CLI arguments.
|
||||
options, err := config.FromArgs()
|
||||
cli.HandleError(err)
|
||||
|
||||
@@ -24,44 +20,48 @@ func main() {
|
||||
logger.Info("using program arguments", "args", os.Args)
|
||||
logger.Info("parsed CLI options", "options", options)
|
||||
|
||||
input, err := options.Source.Pull()
|
||||
// Get input.
|
||||
input, err := options.Source.Extract()
|
||||
cli.HandleError(err)
|
||||
|
||||
// Parse tokens.
|
||||
tokens, err := saccharine.GetTokens(input)
|
||||
// Parse code into syntax tree.
|
||||
ast, err := saccharine.Parse(input)
|
||||
cli.HandleError(err)
|
||||
logger.Info("parsed tokens", "tokens", tokens)
|
||||
logger.Info("parsed syntax tree", "tree", ast)
|
||||
|
||||
// Turn tokens into syntax tree.
|
||||
expression, err := saccharine.Parse(tokens)
|
||||
cli.HandleError(err)
|
||||
if options.Verbose {
|
||||
logger.Info("parsed syntax tree", "tree", saccharine.Stringify(expression))
|
||||
}
|
||||
// Compile expression to lambda calculus.
|
||||
compiled := convert.SaccharineToLambda(ast)
|
||||
logger.Info("compiled λ expression", "tree", compiled.String())
|
||||
|
||||
compiled := convert.SaccharineToLambda(expression)
|
||||
if options.Verbose {
|
||||
logger.Info("compiled lambda expression", "tree", lambda.Stringify(compiled))
|
||||
}
|
||||
// Create reducer with the compiled expression.
|
||||
runtime := normalorder.NewRuntime(compiled)
|
||||
|
||||
process := executer.New(options, &compiled)
|
||||
// If the user selected to track CPU performance, attach a profiler.
|
||||
if options.Profile != "" {
|
||||
profiler := performance.Track(options.Profile)
|
||||
process.On("start", profiler.Start)
|
||||
process.On("end", profiler.End)
|
||||
plugins.NewPerformance(options.Profile, runtime)
|
||||
}
|
||||
|
||||
// If the user selected to produce a step-by-step explanation, attach an
|
||||
// observer.
|
||||
if options.Explanation {
|
||||
explanation.Track(process)
|
||||
plugins.NewExplanation(runtime)
|
||||
}
|
||||
|
||||
statistics := statistics.Track()
|
||||
process.On("start", statistics.Start)
|
||||
process.On("step", statistics.Step)
|
||||
process.On("end", statistics.End)
|
||||
// If the user opted to track statistics, attach a tracker.
|
||||
if options.Statistics {
|
||||
plugins.NewStatistics(runtime)
|
||||
}
|
||||
|
||||
process.Run()
|
||||
// If the user selected for verbose debug logs, attach a reduction tracker.
|
||||
if options.Verbose {
|
||||
plugins.NewLogs(logger, runtime)
|
||||
}
|
||||
|
||||
fmt.Println(lambda.Stringify(compiled))
|
||||
fmt.Fprint(os.Stderr, statistics.Results.String())
|
||||
// Run reduction.
|
||||
runtime.Run()
|
||||
|
||||
// Return the final reduced result.
|
||||
result := runtime.Expression().String()
|
||||
err = options.Destination.Write(result)
|
||||
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.")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
26
cmd/lambda/registry.go
Normal file
26
cmd/lambda/registry.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"git.maximhutz.com/max/lambda/internal/cli"
|
||||
"git.maximhutz.com/max/lambda/internal/registry"
|
||||
"git.maximhutz.com/max/lambda/pkg/convert"
|
||||
"git.maximhutz.com/max/lambda/pkg/engine/normalorder"
|
||||
"git.maximhutz.com/max/lambda/pkg/lambda"
|
||||
"git.maximhutz.com/max/lambda/pkg/saccharine"
|
||||
)
|
||||
|
||||
func MakeRegistry() *registry.Registry {
|
||||
r := registry.New()
|
||||
|
||||
// Codecs
|
||||
r.AddCodec(cli.ConvertCodec(convert.Saccharine2Lambda{}, "saccharine", "lambda"))
|
||||
|
||||
// Engines
|
||||
r.AddEngine(cli.ConvertEngine(normalorder.Engine{}, "normalorder", "lambda"))
|
||||
|
||||
// Marshalers
|
||||
r.AddMarshaler(cli.ConvertMarshaler(lambda.Marshaler{}, "lambda"))
|
||||
r.AddMarshaler(cli.ConvertMarshaler(saccharine.Marshaler{}, "saccharine"))
|
||||
|
||||
return r
|
||||
}
|
||||
Binary file not shown.
8
go.mod
8
go.mod
@@ -1,3 +1,11 @@
|
||||
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
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=
|
||||
55
internal/cli/codec.go
Normal file
55
internal/cli/codec.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.maximhutz.com/max/lambda/pkg/codec"
|
||||
)
|
||||
|
||||
type Codec interface {
|
||||
codec.Codec[Repr, Repr]
|
||||
|
||||
InType() string
|
||||
OutType() string
|
||||
}
|
||||
|
||||
type convertedCodec[T, U any] struct {
|
||||
codec codec.Codec[T, U]
|
||||
inType, outType string
|
||||
}
|
||||
|
||||
func (c convertedCodec[T, U]) Decode(r Repr) (Repr, error) {
|
||||
u, ok := r.Data().(U)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("could not parse '%v' as '%s'", r, c.inType)
|
||||
}
|
||||
|
||||
t, err := c.codec.Decode(u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewRepr(c.outType, t), nil
|
||||
}
|
||||
|
||||
func (c convertedCodec[T, U]) Encode(r Repr) (Repr, error) {
|
||||
t, ok := r.Data().(T)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("could not parse '%v' as '%s'", t, c.outType)
|
||||
}
|
||||
|
||||
u, err := c.codec.Encode(t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewRepr(c.inType, u), nil
|
||||
}
|
||||
|
||||
func (c convertedCodec[T, U]) InType() string { return c.inType }
|
||||
|
||||
func (c convertedCodec[T, U]) OutType() string { return c.outType }
|
||||
|
||||
func ConvertCodec[T, U any](e codec.Codec[T, U], inType, outType string) Codec {
|
||||
return convertedCodec[T, U]{e, inType, outType}
|
||||
}
|
||||
49
internal/cli/engine.go
Normal file
49
internal/cli/engine.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.maximhutz.com/max/lambda/pkg/engine"
|
||||
)
|
||||
|
||||
type Engine interface {
|
||||
engine.Engine[Repr]
|
||||
|
||||
Name() string
|
||||
InType() string
|
||||
}
|
||||
|
||||
type convertedEngine[T any] struct {
|
||||
engine engine.Engine[T]
|
||||
name string
|
||||
inType string
|
||||
}
|
||||
|
||||
func (b convertedEngine[T]) InType() string { return b.inType }
|
||||
|
||||
func (b convertedEngine[T]) Name() string { return b.name }
|
||||
|
||||
func (b convertedEngine[T]) Get() (Repr, error) {
|
||||
s, err := b.engine.Get()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewRepr(b.inType, s), nil
|
||||
}
|
||||
|
||||
func (b convertedEngine[T]) Set(r Repr) error {
|
||||
if t, ok := r.Data().(T); ok {
|
||||
return b.engine.Set(t)
|
||||
}
|
||||
|
||||
return fmt.Errorf("Incorrent format '%s' for engine '%s'.", r.Id(), b.inType)
|
||||
}
|
||||
|
||||
func (b convertedEngine[T]) Step(i int) bool {
|
||||
return b.engine.Step(i)
|
||||
}
|
||||
|
||||
func ConvertEngine[T any](e engine.Engine[T], name string, inType string) Engine {
|
||||
return convertedEngine[T]{e, name, inType}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
// Package "cli" provides miscellaneous helper functions.
|
||||
package cli
|
||||
|
||||
import (
|
||||
@@ -12,6 +13,6 @@ func HandleError(err error) {
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
fmt.Fprintln(os.Stderr, "ERROR:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
42
internal/cli/marshaler.go
Normal file
42
internal/cli/marshaler.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.maximhutz.com/max/lambda/pkg/codec"
|
||||
)
|
||||
|
||||
type Marshaler interface {
|
||||
codec.Marshaler[Repr]
|
||||
|
||||
InType() string
|
||||
}
|
||||
|
||||
type convertedMarshaler[T any] struct {
|
||||
codec codec.Marshaler[T]
|
||||
inType string
|
||||
}
|
||||
|
||||
func (c convertedMarshaler[T]) Decode(s string) (Repr, error) {
|
||||
t, err := c.codec.Decode(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewRepr(c.inType, t), nil
|
||||
}
|
||||
|
||||
func (c convertedMarshaler[T]) Encode(r Repr) (string, error) {
|
||||
t, ok := r.Data().(T)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("could not parse '%v' as 'string'", t)
|
||||
}
|
||||
|
||||
return c.codec.Encode(t)
|
||||
}
|
||||
|
||||
func (c convertedMarshaler[T]) InType() string { return c.inType }
|
||||
|
||||
func ConvertMarshaler[T any](e codec.Marshaler[T], inType string) Marshaler {
|
||||
return convertedMarshaler[T]{e, inType}
|
||||
}
|
||||
21
internal/cli/repr.go
Normal file
21
internal/cli/repr.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package cli
|
||||
|
||||
type Repr interface {
|
||||
// Id returns to name of the objects underlying representation. If is
|
||||
// assumed that if two Repr objects have the same Id(), they share the same
|
||||
// representation.
|
||||
Id() string
|
||||
|
||||
Data() any
|
||||
}
|
||||
|
||||
type baseRepr struct {
|
||||
id string
|
||||
data any
|
||||
}
|
||||
|
||||
func (r baseRepr) Id() string { return r.id }
|
||||
|
||||
func (r baseRepr) Data() any { return r.data }
|
||||
|
||||
func NewRepr(id string, data any) Repr { return baseRepr{id, data} }
|
||||
@@ -1,9 +1,12 @@
|
||||
// Package "config" parses ad handles the user settings given to the program.
|
||||
package config
|
||||
|
||||
// Arguments given to program.
|
||||
// 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.
|
||||
}
|
||||
|
||||
27
internal/config/destination.go
Normal file
27
internal/config/destination.go
Normal file
@@ -0,0 +1,27 @@
|
||||
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)
|
||||
}
|
||||
@@ -5,13 +5,14 @@ import (
|
||||
"os"
|
||||
)
|
||||
|
||||
// Define the correct logger for the program to use.
|
||||
// Returns a structured logger with the appropriate configurations.
|
||||
func (c Config) GetLogger() *slog.Logger {
|
||||
var level slog.Level
|
||||
// By default, only print out errors.
|
||||
level := slog.LevelError
|
||||
|
||||
// If the user set the output to be "VERBOSE", return the debug logs.
|
||||
if c.Verbose {
|
||||
level = slog.LevelInfo
|
||||
} else {
|
||||
level = slog.LevelError
|
||||
}
|
||||
|
||||
return slog.New(
|
||||
|
||||
@@ -7,31 +7,50 @@ import (
|
||||
|
||||
// Extract the program configuration from the command-line arguments.
|
||||
func FromArgs() (*Config, error) {
|
||||
// Parse flags.
|
||||
// Relevant flags.
|
||||
verbose := flag.Bool("v", false, "Verbosity. If set, the program will print logs.")
|
||||
explanation := flag.Bool("x", false, "Explanation. Whether or not to show all reduction steps.")
|
||||
profile := flag.String("p", "", "CPU profiling. If set, the program will run a performance profile during execution.")
|
||||
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).")
|
||||
flag.Parse()
|
||||
|
||||
// Parse non-flag arguments.
|
||||
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")
|
||||
} else {
|
||||
// Positional argument.
|
||||
if flag.Arg(0) == "-" {
|
||||
source = StdinSource{}
|
||||
} else {
|
||||
source = StringSource{data: flag.Arg(0)}
|
||||
source = StringSource{Data: flag.Arg(0)}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse destination type.
|
||||
var destination Destination
|
||||
if *output == "" || *output == "-" {
|
||||
destination = StdoutDestination{}
|
||||
} else {
|
||||
destination = FileDestination{Path: *output}
|
||||
}
|
||||
|
||||
return &Config{
|
||||
Source: source,
|
||||
Destination: destination,
|
||||
Verbose: *verbose,
|
||||
Explanation: *explanation,
|
||||
Profile: *profile,
|
||||
Statistics: *statistics,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -5,21 +5,21 @@ import (
|
||||
"os"
|
||||
)
|
||||
|
||||
// Defines the consumption of different types of input sources.
|
||||
// A method of extracting input from the user.
|
||||
type Source interface {
|
||||
// Get the data.
|
||||
Pull() (string, error)
|
||||
// Fetch data from this source.
|
||||
Extract() (string, error)
|
||||
}
|
||||
|
||||
// A source coming from a string.
|
||||
type StringSource struct{ data string }
|
||||
// A source defined by a string.
|
||||
type StringSource struct{ Data string }
|
||||
|
||||
func (s StringSource) Pull() (string, error) { return s.data, nil }
|
||||
func (s StringSource) Extract() (string, error) { return s.Data, nil }
|
||||
|
||||
// A source coming from standard input.
|
||||
// A source pulling from standard input.
|
||||
type StdinSource struct{}
|
||||
|
||||
func (s StdinSource) Pull() (string, error) {
|
||||
func (s StdinSource) Extract() (string, error) {
|
||||
data, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -27,3 +27,15 @@ func (s StdinSource) Pull() (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
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
package executer
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
|
||||
"git.maximhutz.com/max/lambda/internal/config"
|
||||
"git.maximhutz.com/max/lambda/pkg/emitter"
|
||||
"git.maximhutz.com/max/lambda/pkg/lambda"
|
||||
)
|
||||
|
||||
type Executor struct {
|
||||
Config *config.Config
|
||||
Expression *lambda.Expression
|
||||
emitter.Emitter
|
||||
}
|
||||
|
||||
func New(config *config.Config, expression *lambda.Expression) *Executor {
|
||||
return &Executor{Config: config, Expression: expression}
|
||||
}
|
||||
|
||||
func (e Executor) Run() {
|
||||
e.Emit("start")
|
||||
|
||||
for lambda.ReduceOnce(e.Expression) {
|
||||
e.Emit("step")
|
||||
if e.Config.Verbose {
|
||||
slog.Info("reduction", "tree", lambda.Stringify(*e.Expression))
|
||||
}
|
||||
}
|
||||
|
||||
e.Emit("end")
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package explanation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.maximhutz.com/max/lambda/internal/executer"
|
||||
"git.maximhutz.com/max/lambda/pkg/lambda"
|
||||
)
|
||||
|
||||
type Tracker struct {
|
||||
process *executer.Executor
|
||||
}
|
||||
|
||||
func Track(process *executer.Executor) *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))
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package performance
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime/pprof"
|
||||
)
|
||||
|
||||
type Tracker struct {
|
||||
File string
|
||||
filePointer *os.File
|
||||
Error error
|
||||
}
|
||||
|
||||
func Track(file string) *Tracker {
|
||||
return &Tracker{File: file}
|
||||
}
|
||||
|
||||
func (t *Tracker) Start() {
|
||||
var absPath string
|
||||
|
||||
absPath, t.Error = filepath.Abs(t.File)
|
||||
if t.Error != nil {
|
||||
return
|
||||
}
|
||||
|
||||
t.Error = os.MkdirAll(filepath.Dir(absPath), 0777)
|
||||
if t.Error != nil {
|
||||
return
|
||||
}
|
||||
|
||||
t.filePointer, t.Error = os.Create(absPath)
|
||||
if t.Error != nil {
|
||||
return
|
||||
}
|
||||
|
||||
t.Error = pprof.StartCPUProfile(t.filePointer)
|
||||
if t.Error != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tracker) End() {
|
||||
pprof.StopCPUProfile()
|
||||
t.filePointer.Close()
|
||||
}
|
||||
75
internal/registry/registry.go
Normal file
75
internal/registry/registry.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package registry
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.maximhutz.com/max/lambda/internal/cli"
|
||||
)
|
||||
|
||||
type Registry struct {
|
||||
marshalers map[string]cli.Marshaler
|
||||
codecs []cli.Codec
|
||||
engines map[string]cli.Engine
|
||||
}
|
||||
|
||||
func New() *Registry {
|
||||
return &Registry{
|
||||
marshalers: map[string]cli.Marshaler{},
|
||||
codecs: []cli.Codec{},
|
||||
engines: map[string]cli.Engine{},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Registry) AddCodec(c cli.Codec) error {
|
||||
r.codecs = append(r.codecs, c)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Registry) AddMarshaler(c cli.Marshaler) error {
|
||||
if _, ok := r.marshalers[c.InType()]; ok {
|
||||
return fmt.Errorf("marshaler for '%s' already registered", c.InType())
|
||||
}
|
||||
|
||||
r.marshalers[c.InType()] = c
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Registry) AddEngine(e cli.Engine) error {
|
||||
if _, ok := r.engines[e.Name()]; ok {
|
||||
return fmt.Errorf("engine '%s' already registered", e.Name())
|
||||
}
|
||||
|
||||
r.engines[e.Name()] = e
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Registry) GetEngine(name string) (cli.Engine, error) {
|
||||
e, ok := r.engines[name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("engine '%s' not found", name)
|
||||
}
|
||||
|
||||
return e, nil
|
||||
}
|
||||
|
||||
func (r *Registry) ConvertTo(repr cli.Repr, outType string) (cli.Repr, error) {
|
||||
panic("")
|
||||
}
|
||||
|
||||
func (r *Registry) Marshal(repr cli.Repr) (string, error) {
|
||||
m, ok := r.marshalers[repr.Id()]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("no marshaler for '%s'", repr.Id())
|
||||
}
|
||||
|
||||
return m.Encode(repr)
|
||||
}
|
||||
|
||||
func (r *Registry) Unmarshal(s string, outType string) (cli.Repr, error) {
|
||||
m, ok := r.marshalers[outType]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no marshaler for '%s'", outType)
|
||||
}
|
||||
|
||||
return m.Decode(s)
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package statistics
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Results struct {
|
||||
StepsTaken uint64 // Number of steps taken during execution.
|
||||
TimeElapsed uint64 // The time (ms) taken for execution to complete.
|
||||
}
|
||||
|
||||
func (r Results) OpsPerSecond() float32 {
|
||||
return float32(r.StepsTaken) / (float32(r.TimeElapsed) / 1000)
|
||||
}
|
||||
|
||||
func (r Results) String() string {
|
||||
builder := strings.Builder{}
|
||||
fmt.Fprintln(&builder, "Time Spent:", r.TimeElapsed, "ms")
|
||||
fmt.Fprintln(&builder, "Steps:", r.StepsTaken)
|
||||
fmt.Fprintln(&builder, "Speed:", r.OpsPerSecond(), "ops")
|
||||
return builder.String()
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package statistics
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type Tracker struct {
|
||||
start time.Time
|
||||
steps uint64
|
||||
Results *Results
|
||||
}
|
||||
|
||||
func Track() *Tracker {
|
||||
return &Tracker{}
|
||||
}
|
||||
|
||||
func (t *Tracker) Start() {
|
||||
t.start = time.Now()
|
||||
t.steps = 0
|
||||
t.Results = nil
|
||||
}
|
||||
|
||||
func (t *Tracker) Step() {
|
||||
t.steps++
|
||||
}
|
||||
|
||||
func (t *Tracker) End() {
|
||||
t.Results = &Results{
|
||||
StepsTaken: t.steps,
|
||||
TimeElapsed: uint64(time.Since(t.start).Milliseconds()),
|
||||
}
|
||||
}
|
||||
8
pkg/codec/codec.go
Normal file
8
pkg/codec/codec.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package codec
|
||||
|
||||
type Codec[T, U any] interface {
|
||||
Encode(T) (U, error)
|
||||
Decode(U) (T, error)
|
||||
}
|
||||
|
||||
type Marshaler[T any] = Codec[T, string]
|
||||
@@ -3,23 +3,24 @@ package convert
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.maximhutz.com/max/lambda/pkg/codec"
|
||||
"git.maximhutz.com/max/lambda/pkg/lambda"
|
||||
"git.maximhutz.com/max/lambda/pkg/saccharine/ast"
|
||||
"git.maximhutz.com/max/lambda/pkg/saccharine"
|
||||
)
|
||||
|
||||
func convertAtom(n *ast.Atom) lambda.Expression {
|
||||
func encodeAtom(n *saccharine.Atom) lambda.Expression {
|
||||
return lambda.NewVariable(n.Name)
|
||||
}
|
||||
|
||||
func convertAbstraction(n *ast.Abstraction) lambda.Expression {
|
||||
result := SaccharineToLambda(n.Body)
|
||||
func encodeAbstraction(n *saccharine.Abstraction) lambda.Expression {
|
||||
result := encodeExpression(n.Body)
|
||||
|
||||
parameters := n.Parameters
|
||||
|
||||
// If the function has no parameters, it is a thunk. Lambda calculus still
|
||||
// requires _some_ parameter exists, so generate one.
|
||||
if len(parameters) == 0 {
|
||||
freeVars := lambda.GetFreeVariables(result)
|
||||
freeVars := result.GetFree()
|
||||
freshName := lambda.GenerateFreshName(freeVars)
|
||||
parameters = append(parameters, freshName)
|
||||
}
|
||||
@@ -31,13 +32,13 @@ func convertAbstraction(n *ast.Abstraction) lambda.Expression {
|
||||
return result
|
||||
}
|
||||
|
||||
func convertApplication(n *ast.Application) lambda.Expression {
|
||||
result := SaccharineToLambda(n.Abstraction)
|
||||
func encodeApplication(n *saccharine.Application) lambda.Expression {
|
||||
result := encodeExpression(n.Abstraction)
|
||||
|
||||
arguments := []lambda.Expression{}
|
||||
for _, argument := range n.Arguments {
|
||||
convertedArgument := SaccharineToLambda(argument)
|
||||
arguments = append(arguments, convertedArgument)
|
||||
encodeedArgument := encodeExpression(argument)
|
||||
arguments = append(arguments, encodeedArgument)
|
||||
}
|
||||
|
||||
for _, argument := range arguments {
|
||||
@@ -47,13 +48,13 @@ func convertApplication(n *ast.Application) lambda.Expression {
|
||||
return result
|
||||
}
|
||||
|
||||
func reduceLet(s *ast.LetStatement, e lambda.Expression) lambda.Expression {
|
||||
func reduceLet(s *saccharine.LetStatement, e lambda.Expression) lambda.Expression {
|
||||
var value lambda.Expression
|
||||
|
||||
if len(s.Parameters) == 0 {
|
||||
value = SaccharineToLambda(s.Body)
|
||||
value = encodeExpression(s.Body)
|
||||
} else {
|
||||
value = convertAbstraction(ast.NewAbstraction(s.Parameters, s.Body))
|
||||
value = encodeAbstraction(saccharine.NewAbstraction(s.Parameters, s.Body))
|
||||
}
|
||||
|
||||
return lambda.NewApplication(
|
||||
@@ -62,28 +63,28 @@ func reduceLet(s *ast.LetStatement, e lambda.Expression) lambda.Expression {
|
||||
)
|
||||
}
|
||||
|
||||
func reduceDeclare(s *ast.DeclareStatement, e lambda.Expression) lambda.Expression {
|
||||
freshVar := lambda.GenerateFreshName(lambda.GetFreeVariables(e))
|
||||
func reduceDeclare(s *saccharine.DeclareStatement, e lambda.Expression) lambda.Expression {
|
||||
freshVar := lambda.GenerateFreshName(e.GetFree())
|
||||
|
||||
return lambda.NewApplication(
|
||||
lambda.NewAbstraction(freshVar, e),
|
||||
SaccharineToLambda(s.Value),
|
||||
encodeExpression(s.Value),
|
||||
)
|
||||
}
|
||||
|
||||
func reduceStatement(s ast.Statement, e lambda.Expression) lambda.Expression {
|
||||
func reduceStatement(s saccharine.Statement, e lambda.Expression) lambda.Expression {
|
||||
switch s := s.(type) {
|
||||
case *ast.DeclareStatement:
|
||||
case *saccharine.DeclareStatement:
|
||||
return reduceDeclare(s, e)
|
||||
case *ast.LetStatement:
|
||||
case *saccharine.LetStatement:
|
||||
return reduceLet(s, e)
|
||||
default:
|
||||
panic(fmt.Errorf("unknown statement type: %v", s))
|
||||
}
|
||||
}
|
||||
|
||||
func convertClause(n *ast.Clause) lambda.Expression {
|
||||
result := SaccharineToLambda(n.Returns)
|
||||
func encodeClause(n *saccharine.Clause) lambda.Expression {
|
||||
result := encodeExpression(n.Returns)
|
||||
|
||||
for i := len(n.Statements) - 1; i >= 0; i-- {
|
||||
result = reduceStatement(n.Statements[i], result)
|
||||
@@ -92,17 +93,46 @@ func convertClause(n *ast.Clause) lambda.Expression {
|
||||
return result
|
||||
}
|
||||
|
||||
func SaccharineToLambda(n ast.Expression) lambda.Expression {
|
||||
switch n := n.(type) {
|
||||
case *ast.Atom:
|
||||
return convertAtom(n)
|
||||
case *ast.Abstraction:
|
||||
return convertAbstraction(n)
|
||||
case *ast.Application:
|
||||
return convertApplication(n)
|
||||
case *ast.Clause:
|
||||
return convertClause(n)
|
||||
func encodeExpression(s saccharine.Expression) lambda.Expression {
|
||||
switch s := s.(type) {
|
||||
case *saccharine.Atom:
|
||||
return encodeAtom(s)
|
||||
case *saccharine.Abstraction:
|
||||
return encodeAbstraction(s)
|
||||
case *saccharine.Application:
|
||||
return encodeApplication(s)
|
||||
case *saccharine.Clause:
|
||||
return encodeClause(s)
|
||||
default:
|
||||
panic(fmt.Errorf("unknown expression type: %T", n))
|
||||
panic(fmt.Errorf("unknown expression type: %T", s))
|
||||
}
|
||||
}
|
||||
|
||||
func decodeExression(l lambda.Expression) saccharine.Expression {
|
||||
switch l := l.(type) {
|
||||
case lambda.Variable:
|
||||
return saccharine.NewAtom(l.Name())
|
||||
case lambda.Abstraction:
|
||||
return saccharine.NewAbstraction(
|
||||
[]string{l.Parameter()},
|
||||
decodeExression(l.Body()))
|
||||
case lambda.Application:
|
||||
return saccharine.NewApplication(
|
||||
decodeExression(l.Abstraction()),
|
||||
[]saccharine.Expression{decodeExression(l.Argument())})
|
||||
default:
|
||||
panic(fmt.Errorf("unknown expression type: %T", l))
|
||||
}
|
||||
}
|
||||
|
||||
type Saccharine2Lambda struct{}
|
||||
|
||||
func (c Saccharine2Lambda) Decode(l lambda.Expression) (saccharine.Expression, error) {
|
||||
return decodeExression(l), nil
|
||||
}
|
||||
|
||||
func (c Saccharine2Lambda) Encode(s saccharine.Expression) (lambda.Expression, error) {
|
||||
return encodeExpression(s), nil
|
||||
}
|
||||
|
||||
var _ codec.Codec[saccharine.Expression, lambda.Expression] = (*Saccharine2Lambda)(nil)
|
||||
|
||||
@@ -2,53 +2,45 @@ package emitter
|
||||
|
||||
import "git.maximhutz.com/max/lambda/pkg/set"
|
||||
|
||||
type Observer struct {
|
||||
fn func()
|
||||
message string
|
||||
emitter *Emitter
|
||||
type Emitter[E comparable] interface {
|
||||
On(E, func()) Listener[E]
|
||||
Off(Listener[E])
|
||||
Emit(E)
|
||||
}
|
||||
|
||||
type Emitter struct {
|
||||
listeners map[string]*set.Set[*Observer]
|
||||
type BaseEmitter[E comparable] struct {
|
||||
listeners map[E]set.Set[Listener[E]]
|
||||
}
|
||||
|
||||
func Ignore[T any](fn func()) func(T) {
|
||||
return func(T) { fn() }
|
||||
func (e *BaseEmitter[E]) On(kind E, fn func()) Listener[E] {
|
||||
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 {
|
||||
observer := &Observer{
|
||||
fn: fn,
|
||||
message: message,
|
||||
emitter: e,
|
||||
}
|
||||
|
||||
if e.listeners == nil {
|
||||
e.listeners = map[string]*set.Set[*Observer]{}
|
||||
}
|
||||
|
||||
if e.listeners[message] == nil {
|
||||
e.listeners[message] = set.New[*Observer]()
|
||||
}
|
||||
|
||||
e.listeners[message].Add(observer)
|
||||
return observer
|
||||
}
|
||||
|
||||
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()
|
||||
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]]()
|
||||
}
|
||||
|
||||
for listener := range e.listeners[event].Items() {
|
||||
listener.Run()
|
||||
}
|
||||
}
|
||||
|
||||
func New[E comparable]() *BaseEmitter[E] {
|
||||
return &BaseEmitter[E]{
|
||||
listeners: map[E]set.Set[Listener[E]]{},
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
7
pkg/engine/engine.go
Normal file
7
pkg/engine/engine.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package engine
|
||||
|
||||
type Engine[T any] interface {
|
||||
Get() (T, error)
|
||||
Set(T) error
|
||||
Step(int) bool
|
||||
}
|
||||
34
pkg/engine/normalorder/engine.go
Normal file
34
pkg/engine/normalorder/engine.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package normalorder
|
||||
|
||||
import (
|
||||
"git.maximhutz.com/max/lambda/pkg/engine"
|
||||
"git.maximhutz.com/max/lambda/pkg/lambda"
|
||||
)
|
||||
|
||||
type Engine struct {
|
||||
expr lambda.Expression
|
||||
}
|
||||
|
||||
func (e Engine) Get() (lambda.Expression, error) {
|
||||
return e.expr, nil
|
||||
}
|
||||
|
||||
func (e Engine) Set(l lambda.Expression) error {
|
||||
e.expr = l
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e Engine) Step(i int) bool {
|
||||
var reduced bool
|
||||
|
||||
for range i {
|
||||
e.expr, reduced = ReduceOnce(e.expr)
|
||||
if !reduced {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
var _ engine.Engine[lambda.Expression] = (*Engine)(nil)
|
||||
34
pkg/engine/normalorder/reduce_one.go
Normal file
34
pkg/engine/normalorder/reduce_one.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package normalorder
|
||||
|
||||
import "git.maximhutz.com/max/lambda/pkg/lambda"
|
||||
|
||||
func ReduceOnce(e lambda.Expression) (lambda.Expression, bool) {
|
||||
switch e := e.(type) {
|
||||
case lambda.Abstraction:
|
||||
body, reduced := ReduceOnce(e.Body())
|
||||
if reduced {
|
||||
return lambda.NewAbstraction(e.Parameter(), body), true
|
||||
}
|
||||
return e, false
|
||||
|
||||
case lambda.Application:
|
||||
if fn, fnOk := e.Abstraction().(lambda.Abstraction); fnOk {
|
||||
return fn.Body().Substitute(fn.Parameter(), e.Argument()), true
|
||||
}
|
||||
|
||||
abs, reduced := ReduceOnce(e.Abstraction())
|
||||
if reduced {
|
||||
return lambda.NewApplication(abs, e.Argument()), true
|
||||
}
|
||||
|
||||
arg, reduced := ReduceOnce(e.Argument())
|
||||
if reduced {
|
||||
return lambda.NewApplication(e.Abstraction(), arg), true
|
||||
}
|
||||
|
||||
return e, false
|
||||
|
||||
default:
|
||||
return e, false
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,70 +1,100 @@
|
||||
package lambda
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"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 {
|
||||
Accept(Visitor)
|
||||
Copy() Expression
|
||||
fmt.Stringer
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
/** ------------------------------------------------------------------------- */
|
||||
|
||||
type Abstraction struct {
|
||||
Parameter string
|
||||
Body Expression
|
||||
parameter string
|
||||
body Expression
|
||||
}
|
||||
|
||||
func (a *Abstraction) Copy() Expression {
|
||||
return NewAbstraction(a.Parameter, a.Body.Copy())
|
||||
var _ Expression = Abstraction{}
|
||||
|
||||
func (a Abstraction) Parameter() string {
|
||||
return a.parameter
|
||||
}
|
||||
|
||||
func (a *Abstraction) Accept(v Visitor) {
|
||||
v.VisitAbstraction(a)
|
||||
func (a Abstraction) Body() Expression {
|
||||
return a.body
|
||||
}
|
||||
|
||||
func NewAbstraction(parameter string, body Expression) *Abstraction {
|
||||
return &Abstraction{Parameter: parameter, Body: body}
|
||||
func (a Abstraction) String() string {
|
||||
return "\\" + a.parameter + "." + a.body.String()
|
||||
}
|
||||
|
||||
func NewAbstraction(parameter string, body Expression) Abstraction {
|
||||
return Abstraction{parameter, body}
|
||||
}
|
||||
|
||||
/** ------------------------------------------------------------------------- */
|
||||
|
||||
type Application struct {
|
||||
Abstraction Expression
|
||||
Argument Expression
|
||||
abstraction Expression
|
||||
argument Expression
|
||||
}
|
||||
|
||||
func (a *Application) Copy() Expression {
|
||||
return NewApplication(a.Abstraction.Copy(), a.Argument.Copy())
|
||||
var _ Expression = Application{}
|
||||
|
||||
func (a Application) Abstraction() Expression {
|
||||
return a.abstraction
|
||||
}
|
||||
|
||||
func (a *Application) Accept(v Visitor) {
|
||||
v.VisitApplication(a)
|
||||
func (a Application) Argument() Expression {
|
||||
return a.argument
|
||||
}
|
||||
|
||||
func NewApplication(function Expression, argument Expression) *Application {
|
||||
return &Application{Abstraction: function, Argument: argument}
|
||||
func (a Application) String() string {
|
||||
return "(" + a.abstraction.String() + " " + a.argument.String() + ")"
|
||||
}
|
||||
|
||||
func NewApplication(abstraction Expression, argument Expression) Application {
|
||||
return Application{abstraction, argument}
|
||||
}
|
||||
|
||||
/** ------------------------------------------------------------------------- */
|
||||
|
||||
type Variable struct {
|
||||
Value string
|
||||
name string
|
||||
}
|
||||
|
||||
func (v *Variable) Copy() Expression {
|
||||
return NewVariable(v.Value)
|
||||
var _ Expression = Variable{}
|
||||
|
||||
func (v Variable) Name() string {
|
||||
return v.name
|
||||
}
|
||||
|
||||
func (v *Variable) Accept(visitor Visitor) {
|
||||
visitor.VisitVariable(v)
|
||||
func (v Variable) String() string {
|
||||
return v.name
|
||||
}
|
||||
|
||||
func NewVariable(name string) *Variable {
|
||||
return &Variable{Value: name}
|
||||
}
|
||||
|
||||
/** ------------------------------------------------------------------------- */
|
||||
|
||||
type Visitor interface {
|
||||
VisitAbstraction(*Abstraction)
|
||||
VisitApplication(*Application)
|
||||
VisitVariable(*Variable)
|
||||
func NewVariable(name string) Variable {
|
||||
return Variable{name}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,9 @@ import (
|
||||
"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++ {
|
||||
attempt := "_" + string(strconv.AppendUint(nil, i, 10))
|
||||
|
||||
|
||||
@@ -2,19 +2,18 @@ package lambda
|
||||
|
||||
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)
|
||||
case *Abstraction:
|
||||
vars := GetFreeVariables(e.Body)
|
||||
vars.Remove(e.Parameter)
|
||||
return vars
|
||||
case *Application:
|
||||
vars := GetFreeVariables(e.Abstraction)
|
||||
vars.Merge(GetFreeVariables(e.Argument))
|
||||
return vars
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
func (e Variable) GetFree() set.Set[string] {
|
||||
return set.New(e.Name())
|
||||
}
|
||||
|
||||
func (e Abstraction) GetFree() set.Set[string] {
|
||||
vars := e.Body().GetFree()
|
||||
vars.Remove(e.Parameter())
|
||||
return vars
|
||||
}
|
||||
|
||||
func (e Application) GetFree() set.Set[string] {
|
||||
vars := e.Abstraction().GetFree()
|
||||
vars.Merge(e.Argument().GetFree())
|
||||
return vars
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
package lambda
|
||||
|
||||
func IsFreeVariable(n string, e Expression) bool {
|
||||
switch e := e.(type) {
|
||||
case *Variable:
|
||||
return e.Value == n
|
||||
case *Abstraction:
|
||||
return e.Parameter != n && IsFreeVariable(n, e.Body)
|
||||
case *Application:
|
||||
return IsFreeVariable(n, e.Abstraction) || IsFreeVariable(n, e.Argument)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
func (e Variable) IsFree(n string) bool {
|
||||
return e.Name() == n
|
||||
}
|
||||
|
||||
func (e Abstraction) IsFree(n string) bool {
|
||||
return e.Parameter() != n && e.Body().IsFree(n)
|
||||
}
|
||||
func (e Application) IsFree(n string) bool {
|
||||
return e.Abstraction().IsFree(n) || e.Argument().IsFree(n)
|
||||
}
|
||||
|
||||
19
pkg/lambda/marshaler.go
Normal file
19
pkg/lambda/marshaler.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package lambda
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.maximhutz.com/max/lambda/pkg/codec"
|
||||
)
|
||||
|
||||
type Marshaler struct{}
|
||||
|
||||
func (m Marshaler) Decode(string) (Expression, error) {
|
||||
return nil, fmt.Errorf("unimplemented")
|
||||
}
|
||||
|
||||
func (m Marshaler) Encode(e Expression) (string, error) {
|
||||
return e.String(), nil
|
||||
}
|
||||
|
||||
var _ codec.Marshaler[Expression] = (*Marshaler)(nil)
|
||||
@@ -1,27 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,19 +1,28 @@
|
||||
package lambda
|
||||
|
||||
func Rename(e Expression, target string, substitute string) {
|
||||
switch e := e.(type) {
|
||||
case *Variable:
|
||||
if e.Value == target {
|
||||
e.Value = substitute
|
||||
}
|
||||
case *Abstraction:
|
||||
if e.Parameter == target {
|
||||
e.Parameter = substitute
|
||||
// Rename replaces all occurrences of the target variable name with the new name.
|
||||
func (e Variable) Rename(target string, newName string) Expression {
|
||||
if e.Name() == target {
|
||||
return NewVariable(newName)
|
||||
}
|
||||
|
||||
Rename(e.Body, target, substitute)
|
||||
case *Application:
|
||||
Rename(e.Abstraction, target, substitute)
|
||||
Rename(e.Argument, target, substitute)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
func (e Abstraction) Rename(target string, newName string) Expression {
|
||||
newParam := e.Parameter()
|
||||
if e.Parameter() == target {
|
||||
newParam = newName
|
||||
}
|
||||
|
||||
newBody := e.Body().Rename(target, newName)
|
||||
|
||||
return NewAbstraction(newParam, newBody)
|
||||
}
|
||||
|
||||
func (e Application) Rename(target string, newName string) Expression {
|
||||
newAbs := e.Abstraction().Rename(target, newName)
|
||||
newArg := e.Argument().Rename(target, newName)
|
||||
|
||||
return NewApplication(newAbs, newArg)
|
||||
}
|
||||
|
||||
@@ -1,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,27 +1,35 @@
|
||||
package lambda
|
||||
|
||||
func Substitute(e *Expression, target string, replacement Expression) {
|
||||
switch typed := (*e).(type) {
|
||||
case *Variable:
|
||||
if typed.Value == target {
|
||||
*e = replacement.Copy()
|
||||
}
|
||||
case *Abstraction:
|
||||
if typed.Parameter == target {
|
||||
return
|
||||
func (e Variable) Substitute(target string, replacement Expression) Expression {
|
||||
if e.Name() == target {
|
||||
return replacement
|
||||
}
|
||||
|
||||
if IsFreeVariable(typed.Parameter, replacement) {
|
||||
replacementFreeVars := GetFreeVariables(replacement)
|
||||
used := GetFreeVariables(typed.Body)
|
||||
used.Merge(replacementFreeVars)
|
||||
freshVar := GenerateFreshName(used)
|
||||
Rename(typed, typed.Parameter, freshVar)
|
||||
}
|
||||
|
||||
Substitute(&typed.Body, target, replacement)
|
||||
case *Application:
|
||||
Substitute(&typed.Abstraction, target, replacement)
|
||||
Substitute(&typed.Argument, target, replacement)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
func (e Abstraction) Substitute(target string, replacement Expression) Expression {
|
||||
if e.Parameter() == target {
|
||||
return e
|
||||
}
|
||||
|
||||
body := e.Body()
|
||||
param := e.Parameter()
|
||||
if replacement.IsFree(param) {
|
||||
freeVars := replacement.GetFree()
|
||||
freeVars.Merge(body.GetFree())
|
||||
freshVar := GenerateFreshName(freeVars)
|
||||
body = body.Rename(param, freshVar)
|
||||
param = freshVar
|
||||
}
|
||||
|
||||
newBody := body.Substitute(target, replacement)
|
||||
return NewAbstraction(param, newBody)
|
||||
}
|
||||
|
||||
func (e Application) Substitute(target string, replacement Expression) Expression {
|
||||
abs := e.Abstraction().Substitute(target, replacement)
|
||||
arg := e.Argument().Substitute(target, replacement)
|
||||
|
||||
return NewApplication(abs, arg)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package ast
|
||||
package saccharine
|
||||
|
||||
type Expression interface {
|
||||
IsExpression()
|
||||
24
pkg/saccharine/marshaler.go
Normal file
24
pkg/saccharine/marshaler.go
Normal file
@@ -0,0 +1,24 @@
|
||||
// Package "saccharine" provides a simple language built on top of λ-calculus,
|
||||
// to facilitate productive coding using it.
|
||||
package saccharine
|
||||
|
||||
import (
|
||||
"git.maximhutz.com/max/lambda/pkg/codec"
|
||||
)
|
||||
|
||||
type Marshaler struct{}
|
||||
|
||||
func (m Marshaler) Decode(s string) (Expression, error) {
|
||||
tokens, err := scan(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return parse(tokens)
|
||||
}
|
||||
|
||||
func (m Marshaler) Encode(e Expression) (string, error) {
|
||||
return stringifyExpression(e), nil
|
||||
}
|
||||
|
||||
var _ codec.Marshaler[Expression] = (*Marshaler)(nil)
|
||||
@@ -5,19 +5,17 @@ import (
|
||||
"fmt"
|
||||
|
||||
"git.maximhutz.com/max/lambda/pkg/iterator"
|
||||
"git.maximhutz.com/max/lambda/pkg/saccharine/ast"
|
||||
"git.maximhutz.com/max/lambda/pkg/saccharine/token"
|
||||
"git.maximhutz.com/max/lambda/pkg/trace"
|
||||
)
|
||||
|
||||
type TokenIterator = iterator.Iterator[token.Token]
|
||||
type TokenIterator = iterator.Iterator[Token]
|
||||
|
||||
func parseRawToken(i *TokenIterator, expected token.Type) (*token.Token, error) {
|
||||
return iterator.Do(i, func(i *TokenIterator) (*token.Token, error) {
|
||||
func parseRawToken(i *TokenIterator, expected TokenType) (*Token, error) {
|
||||
return iterator.Do(i, func(i *TokenIterator) (*Token, error) {
|
||||
if tok, err := i.Next(); err != nil {
|
||||
return nil, err
|
||||
} else if tok.Type != expected {
|
||||
return nil, fmt.Errorf("expected token %v, got %v'", token.Name(expected), tok.Value)
|
||||
return nil, fmt.Errorf("expected token %v, got %v'", expected.Name(), tok.Value)
|
||||
} else {
|
||||
return &tok, nil
|
||||
}
|
||||
@@ -26,14 +24,14 @@ func parseRawToken(i *TokenIterator, expected token.Type) (*token.Token, error)
|
||||
|
||||
func passSoftBreaks(i *TokenIterator) {
|
||||
for {
|
||||
if _, err := parseRawToken(i, token.SoftBreak); err != nil {
|
||||
if _, err := parseRawToken(i, TokenSoftBreak); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func parseToken(i *TokenIterator, expected token.Type, ignoreSoftBreaks bool) (*token.Token, error) {
|
||||
return iterator.Do(i, func(i *TokenIterator) (*token.Token, error) {
|
||||
func parseToken(i *TokenIterator, expected TokenType, ignoreSoftBreaks bool) (*Token, error) {
|
||||
return iterator.Do(i, func(i *TokenIterator) (*Token, error) {
|
||||
if ignoreSoftBreaks {
|
||||
passSoftBreaks(i)
|
||||
}
|
||||
@@ -43,17 +41,17 @@ func parseToken(i *TokenIterator, expected token.Type, ignoreSoftBreaks bool) (*
|
||||
}
|
||||
|
||||
func parseString(i *TokenIterator) (string, error) {
|
||||
if tok, err := parseToken(i, token.Atom, true); err != nil {
|
||||
if tok, err := parseToken(i, TokenAtom, true); err != nil {
|
||||
return "", trace.Wrap(err, "no variable (col %d)", i.Index())
|
||||
} else {
|
||||
return tok.Value, nil
|
||||
}
|
||||
}
|
||||
|
||||
func parseBreak(i *TokenIterator) (*token.Token, error) {
|
||||
if tok, softErr := parseRawToken(i, token.SoftBreak); softErr == nil {
|
||||
func parseBreak(i *TokenIterator) (*Token, error) {
|
||||
if tok, softErr := parseRawToken(i, TokenSoftBreak); softErr == nil {
|
||||
return tok, nil
|
||||
} else if tok, hardErr := parseRawToken(i, token.HardBreak); hardErr == nil {
|
||||
} else if tok, hardErr := parseRawToken(i, TokenHardBreak); hardErr == nil {
|
||||
return tok, nil
|
||||
} else {
|
||||
return nil, errors.Join(softErr, hardErr)
|
||||
@@ -75,46 +73,46 @@ func parseList[U any](i *TokenIterator, fn func(*TokenIterator) (U, error), mini
|
||||
}
|
||||
}
|
||||
|
||||
func parseAbstraction(i *TokenIterator) (*ast.Abstraction, error) {
|
||||
return iterator.Do(i, func(i *TokenIterator) (*ast.Abstraction, error) {
|
||||
if _, err := parseToken(i, token.Slash, true); err != nil {
|
||||
func parseAbstraction(i *TokenIterator) (*Abstraction, error) {
|
||||
return iterator.Do(i, func(i *TokenIterator) (*Abstraction, error) {
|
||||
if _, err := parseToken(i, TokenSlash, true); err != nil {
|
||||
return nil, trace.Wrap(err, "no function slash (col %d)", i.MustGet().Column)
|
||||
} else if parameters, err := parseList(i, parseString, 0); err != nil {
|
||||
return nil, err
|
||||
} else if _, err = parseToken(i, token.Dot, true); err != nil {
|
||||
} else if _, err = parseToken(i, TokenDot, true); err != nil {
|
||||
return nil, trace.Wrap(err, "no function dot (col %d)", i.MustGet().Column)
|
||||
} else if body, err := parseExpression(i); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return ast.NewAbstraction(parameters, body), nil
|
||||
return NewAbstraction(parameters, body), nil
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func parseApplication(i *TokenIterator) (*ast.Application, error) {
|
||||
return iterator.Do(i, func(i *TokenIterator) (*ast.Application, error) {
|
||||
if _, err := parseToken(i, token.OpenParen, true); err != nil {
|
||||
func parseApplication(i *TokenIterator) (*Application, error) {
|
||||
return iterator.Do(i, func(i *TokenIterator) (*Application, error) {
|
||||
if _, err := parseToken(i, TokenOpenParen, true); err != nil {
|
||||
return nil, trace.Wrap(err, "no openning brackets (col %d)", i.MustGet().Column)
|
||||
} else if expressions, err := parseList(i, parseExpression, 1); err != nil {
|
||||
return nil, err
|
||||
} else if _, err := parseToken(i, token.CloseParen, true); err != nil {
|
||||
} else if _, err := parseToken(i, TokenCloseParen, true); err != nil {
|
||||
return nil, trace.Wrap(err, "no closing brackets (col %d)", i.MustGet().Column)
|
||||
} else {
|
||||
return ast.NewApplication(expressions[0], expressions[1:]), nil
|
||||
return NewApplication(expressions[0], expressions[1:]), nil
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func parseAtom(i *TokenIterator) (*ast.Atom, error) {
|
||||
if tok, err := parseToken(i, token.Atom, true); err != nil {
|
||||
func parseAtom(i *TokenIterator) (*Atom, error) {
|
||||
if tok, err := parseToken(i, TokenAtom, true); err != nil {
|
||||
return nil, trace.Wrap(err, "no variable (col %d)", i.Index())
|
||||
} else {
|
||||
return ast.NewAtom(tok.Value), nil
|
||||
return NewAtom(tok.Value), nil
|
||||
}
|
||||
}
|
||||
|
||||
func parseStatements(i *TokenIterator) ([]ast.Statement, error) {
|
||||
statements := []ast.Statement{}
|
||||
func parseStatements(i *TokenIterator) ([]Statement, error) {
|
||||
statements := []Statement{}
|
||||
|
||||
//nolint:errcheck
|
||||
parseList(i, parseBreak, 0)
|
||||
@@ -122,7 +120,7 @@ func parseStatements(i *TokenIterator) ([]ast.Statement, error) {
|
||||
for {
|
||||
if statement, err := parseStatement(i); err != nil {
|
||||
break
|
||||
} else if _, err := parseList(i, parseBreak, 1); err != nil {
|
||||
} else if _, err := parseList(i, parseBreak, 1); err != nil && !i.Done() {
|
||||
break
|
||||
} else {
|
||||
statements = append(statements, statement)
|
||||
@@ -132,15 +130,15 @@ func parseStatements(i *TokenIterator) ([]ast.Statement, error) {
|
||||
return statements, nil
|
||||
}
|
||||
|
||||
func parseClause(i *TokenIterator, braces bool) (*ast.Clause, error) {
|
||||
func parseClause(i *TokenIterator, braces bool) (*Clause, error) {
|
||||
if braces {
|
||||
if _, err := parseToken(i, token.OpenBrace, true); err != nil {
|
||||
if _, err := parseToken(i, TokenOpenBrace, true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var stmts []ast.Statement
|
||||
var last *ast.DeclareStatement
|
||||
var stmts []Statement
|
||||
var last *DeclareStatement
|
||||
var err error
|
||||
var ok bool
|
||||
|
||||
@@ -148,31 +146,31 @@ func parseClause(i *TokenIterator, braces bool) (*ast.Clause, error) {
|
||||
return nil, err
|
||||
} else if len(stmts) == 0 {
|
||||
return nil, fmt.Errorf("no statements in clause")
|
||||
} else if last, ok = stmts[len(stmts)-1].(*ast.DeclareStatement); !ok {
|
||||
} else if last, ok = stmts[len(stmts)-1].(*DeclareStatement); !ok {
|
||||
return nil, fmt.Errorf("this clause contains no final return value (col %d)", i.MustGet().Column)
|
||||
}
|
||||
|
||||
if braces {
|
||||
if _, err := parseToken(i, token.CloseBrace, true); err != nil {
|
||||
if _, err := parseToken(i, TokenCloseBrace, true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return ast.NewClause(stmts[:len(stmts)-1], last.Value), nil
|
||||
return NewClause(stmts[:len(stmts)-1], last.Value), nil
|
||||
}
|
||||
|
||||
func parseExpression(i *TokenIterator) (ast.Expression, error) {
|
||||
return iterator.Do(i, func(i *TokenIterator) (ast.Expression, error) {
|
||||
func parseExpression(i *TokenIterator) (Expression, error) {
|
||||
return iterator.Do(i, func(i *TokenIterator) (Expression, error) {
|
||||
passSoftBreaks(i)
|
||||
|
||||
switch peek := i.MustGet(); peek.Type {
|
||||
case token.OpenParen:
|
||||
case TokenOpenParen:
|
||||
return parseApplication(i)
|
||||
case token.Slash:
|
||||
case TokenSlash:
|
||||
return parseAbstraction(i)
|
||||
case token.Atom:
|
||||
case TokenAtom:
|
||||
return parseAtom(i)
|
||||
case token.OpenBrace:
|
||||
case TokenOpenBrace:
|
||||
return parseClause(i, true)
|
||||
default:
|
||||
return nil, fmt.Errorf("expected expression, got '%v' (col %d)", peek.Value, peek.Column)
|
||||
@@ -180,29 +178,29 @@ func parseExpression(i *TokenIterator) (ast.Expression, error) {
|
||||
})
|
||||
}
|
||||
|
||||
func parseLet(i *TokenIterator) (*ast.LetStatement, error) {
|
||||
return iterator.Do(i, func(i *TokenIterator) (*ast.LetStatement, error) {
|
||||
func parseLet(i *TokenIterator) (*LetStatement, error) {
|
||||
return iterator.Do(i, func(i *TokenIterator) (*LetStatement, error) {
|
||||
if parameters, err := parseList(i, parseString, 1); err != nil {
|
||||
return nil, err
|
||||
} else if _, err := parseToken(i, token.Assign, true); err != nil {
|
||||
} else if _, err := parseToken(i, TokenAssign, true); err != nil {
|
||||
return nil, err
|
||||
} else if body, err := parseExpression(i); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return ast.NewLet(parameters[0], parameters[1:], body), nil
|
||||
return NewLet(parameters[0], parameters[1:], body), nil
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func parseDeclare(i *TokenIterator) (*ast.DeclareStatement, error) {
|
||||
func parseDeclare(i *TokenIterator) (*DeclareStatement, error) {
|
||||
if value, err := parseExpression(i); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return ast.NewDeclare(value), nil
|
||||
return NewDeclare(value), nil
|
||||
}
|
||||
}
|
||||
|
||||
func parseStatement(i *TokenIterator) (ast.Statement, error) {
|
||||
func parseStatement(i *TokenIterator) (Statement, error) {
|
||||
if let, letErr := parseLet(i); letErr == nil {
|
||||
return let, nil
|
||||
} else if declare, declErr := parseDeclare(i); declErr == nil {
|
||||
@@ -212,7 +210,8 @@ func parseStatement(i *TokenIterator) (ast.Statement, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func Parse(tokens []token.Token) (ast.Expression, error) {
|
||||
// Given a list of tokens, attempt to parse it into an syntax tree.
|
||||
func parse(tokens []Token) (Expression, error) {
|
||||
i := iterator.Of(tokens)
|
||||
|
||||
exp, err := parseClause(i, false)
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"unicode"
|
||||
|
||||
"git.maximhutz.com/max/lambda/pkg/iterator"
|
||||
"git.maximhutz.com/max/lambda/pkg/saccharine/token"
|
||||
"git.maximhutz.com/max/lambda/pkg/trace"
|
||||
)
|
||||
|
||||
@@ -15,7 +14,7 @@ func isVariable(r rune) bool {
|
||||
return unicode.IsLetter(r) || unicode.IsNumber(r)
|
||||
}
|
||||
|
||||
func parseRune(i *iterator.Iterator[rune], expected func(rune) bool) (rune, error) {
|
||||
func scanRune(i *iterator.Iterator[rune], expected func(rune) bool) (rune, error) {
|
||||
i2 := i.Copy()
|
||||
|
||||
if r, err := i2.Next(); err != nil {
|
||||
@@ -28,7 +27,7 @@ func parseRune(i *iterator.Iterator[rune], expected func(rune) bool) (rune, erro
|
||||
}
|
||||
}
|
||||
|
||||
func parseCharacter(i *iterator.Iterator[rune], expected rune) (rune, error) {
|
||||
func scanCharacter(i *iterator.Iterator[rune], expected rune) (rune, error) {
|
||||
i2 := i.Copy()
|
||||
|
||||
if r, err := i2.Next(); err != nil {
|
||||
@@ -43,7 +42,7 @@ func parseCharacter(i *iterator.Iterator[rune], expected rune) (rune, error) {
|
||||
|
||||
// Pulls the next token from an iterator over runes. If it cannot, it will
|
||||
// return nil. If an error occurs, it will return that.
|
||||
func getToken(i *iterator.Iterator[rune]) (*token.Token, error) {
|
||||
func scanToken(i *iterator.Iterator[rune]) (*Token, error) {
|
||||
index := i.Index()
|
||||
|
||||
if i.Done() {
|
||||
@@ -57,54 +56,69 @@ func getToken(i *iterator.Iterator[rune]) (*token.Token, error) {
|
||||
|
||||
switch {
|
||||
case letter == '(':
|
||||
return token.NewOpenParen(index), nil
|
||||
return NewTokenOpenParen(index), nil
|
||||
case letter == ')':
|
||||
return token.NewCloseParen(index), nil
|
||||
return NewTokenCloseParen(index), nil
|
||||
case letter == '.':
|
||||
return token.NewDot(index), nil
|
||||
return NewTokenDot(index), nil
|
||||
case letter == '\\':
|
||||
return token.NewSlash(index), nil
|
||||
return NewTokenSlash(index), nil
|
||||
case letter == '\n':
|
||||
return token.NewSoftBreak(index), nil
|
||||
return NewTokenSoftBreak(index), nil
|
||||
case letter == '{':
|
||||
return token.NewOpenBrace(index), nil
|
||||
return NewTokenOpenBrace(index), nil
|
||||
case letter == '}':
|
||||
return token.NewCloseBrace(index), nil
|
||||
return NewTokenCloseBrace(index), nil
|
||||
case letter == ':':
|
||||
if _, err := parseCharacter(i, '='); err != nil {
|
||||
if _, err := scanCharacter(i, '='); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return token.NewAssign(index), nil
|
||||
return NewTokenAssign(index), nil
|
||||
}
|
||||
case letter == ';':
|
||||
return token.NewHardBreak(index), nil
|
||||
return NewTokenHardBreak(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):
|
||||
atom := []rune{letter}
|
||||
|
||||
for {
|
||||
if r, err := parseRune(i, isVariable); err != nil {
|
||||
if r, err := scanRune(i, isVariable); err != nil {
|
||||
break
|
||||
} else {
|
||||
atom = append(atom, r)
|
||||
}
|
||||
}
|
||||
|
||||
return token.NewAtom(string(atom), index), nil
|
||||
return NewTokenAtom(string(atom), index), nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unknown character '%v'", string(letter))
|
||||
}
|
||||
|
||||
// Parses a list of runes into tokens. All error encountered are returned, as well.
|
||||
func GetTokens(input string) ([]token.Token, error) {
|
||||
// scan a string into tokens.
|
||||
func scan(input string) ([]Token, error) {
|
||||
i := iterator.Of([]rune(input))
|
||||
tokens := []token.Token{}
|
||||
tokens := []Token{}
|
||||
errorList := []error{}
|
||||
|
||||
for !i.Done() {
|
||||
token, err := getToken(i)
|
||||
token, err := scanToken(i)
|
||||
if err != nil {
|
||||
errorList = append(errorList, err)
|
||||
} else if token != nil {
|
||||
@@ -1,4 +1,4 @@
|
||||
package ast
|
||||
package saccharine
|
||||
|
||||
type Statement interface {
|
||||
IsStatement()
|
||||
@@ -3,66 +3,65 @@ package saccharine
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.maximhutz.com/max/lambda/pkg/saccharine/ast"
|
||||
)
|
||||
|
||||
func stringifyAtom(n *ast.Atom) string {
|
||||
func stringifyAtom(n *Atom) string {
|
||||
return n.Name
|
||||
}
|
||||
|
||||
func stringifyAbstraction(n *ast.Abstraction) string {
|
||||
return "\\" + strings.Join(n.Parameters, " ") + "." + Stringify(n.Body)
|
||||
func stringifyAbstraction(n *Abstraction) string {
|
||||
return "\\" + strings.Join(n.Parameters, " ") + "." + stringifyExpression(n.Body)
|
||||
}
|
||||
|
||||
func stringifyApplication(n *ast.Application) string {
|
||||
arguments := []string{Stringify(n.Abstraction)}
|
||||
func stringifyApplication(n *Application) string {
|
||||
arguments := []string{stringifyExpression(n.Abstraction)}
|
||||
|
||||
for _, argument := range n.Arguments {
|
||||
arguments = append(arguments, Stringify(argument))
|
||||
arguments = append(arguments, stringifyExpression(argument))
|
||||
}
|
||||
|
||||
return "(" + strings.Join(arguments, " ") + ")"
|
||||
}
|
||||
|
||||
func stringifyLet(s *ast.LetStatement) string {
|
||||
return s.Name + " " + strings.Join(s.Parameters, " ") + " := " + Stringify(s.Body)
|
||||
func stringifyLet(s *LetStatement) string {
|
||||
return s.Name + " " + strings.Join(s.Parameters, " ") + " := " + stringifyExpression(s.Body)
|
||||
}
|
||||
|
||||
func stringifyDeclare(s *ast.DeclareStatement) string {
|
||||
return Stringify(s.Value)
|
||||
func stringifyDeclare(s *DeclareStatement) string {
|
||||
return stringifyExpression(s.Value)
|
||||
}
|
||||
|
||||
func stringifyStatement(s ast.Statement) string {
|
||||
func stringifyStatement(s Statement) string {
|
||||
switch s := s.(type) {
|
||||
case *ast.DeclareStatement:
|
||||
case *DeclareStatement:
|
||||
return stringifyDeclare(s)
|
||||
case *ast.LetStatement:
|
||||
case *LetStatement:
|
||||
return stringifyLet(s)
|
||||
default:
|
||||
panic(fmt.Errorf("unknown statement type: %v", s))
|
||||
}
|
||||
}
|
||||
|
||||
func stringifyClause(n *ast.Clause) string {
|
||||
func stringifyClause(n *Clause) string {
|
||||
stmts := ""
|
||||
|
||||
for _, statement := range n.Statements {
|
||||
stmts += stringifyStatement(statement) + "; "
|
||||
}
|
||||
|
||||
return "{ " + stmts + Stringify(n.Returns) + " }"
|
||||
return "{ " + stmts + stringifyExpression(n.Returns) + " }"
|
||||
}
|
||||
|
||||
func Stringify(n ast.Expression) string {
|
||||
// Convert an expression back into valid source code.
|
||||
func stringifyExpression(n Expression) string {
|
||||
switch n := n.(type) {
|
||||
case *ast.Atom:
|
||||
case *Atom:
|
||||
return stringifyAtom(n)
|
||||
case *ast.Abstraction:
|
||||
case *Abstraction:
|
||||
return stringifyAbstraction(n)
|
||||
case *ast.Application:
|
||||
case *Application:
|
||||
return stringifyApplication(n)
|
||||
case *ast.Clause:
|
||||
case *Clause:
|
||||
return stringifyClause(n)
|
||||
default:
|
||||
panic(fmt.Errorf("unknown expression type: %T", n))
|
||||
|
||||
91
pkg/saccharine/token.go
Normal file
91
pkg/saccharine/token.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package saccharine
|
||||
|
||||
import "fmt"
|
||||
|
||||
// All tokens in the pseudo-lambda language.
|
||||
type TokenType int
|
||||
|
||||
const (
|
||||
TokenOpenParen TokenType = iota // Denotes the '(' token.
|
||||
TokenCloseParen // Denotes the ')' token.
|
||||
TokenOpenBrace // Denotes the '{' token.
|
||||
TokenCloseBrace // Denotes the '}' token.
|
||||
TokenHardBreak // Denotes the ';' token.
|
||||
TokenAssign // Denotes the ':=' token.
|
||||
TokenAtom // Denotes an alpha-numeric variable.
|
||||
TokenSlash // Denotes the '/' token.
|
||||
TokenDot // Denotes the '.' token.
|
||||
TokenSoftBreak // Denotes a new-line.
|
||||
)
|
||||
|
||||
// A representation of a token in source code.
|
||||
type Token struct {
|
||||
Column int // Where the token begins in the source text.
|
||||
Type TokenType // What type the token is.
|
||||
Value string // The value of the token.
|
||||
}
|
||||
|
||||
func NewTokenOpenParen(column int) *Token {
|
||||
return &Token{Type: TokenOpenParen, Column: column, Value: "("}
|
||||
}
|
||||
|
||||
func NewTokenCloseParen(column int) *Token {
|
||||
return &Token{Type: TokenCloseParen, Column: column, Value: ")"}
|
||||
}
|
||||
|
||||
func NewTokenOpenBrace(column int) *Token {
|
||||
return &Token{Type: TokenOpenBrace, Column: column, Value: "{"}
|
||||
}
|
||||
|
||||
func NewTokenCloseBrace(column int) *Token {
|
||||
return &Token{Type: TokenCloseBrace, Column: column, Value: "}"}
|
||||
}
|
||||
|
||||
func NewTokenDot(column int) *Token {
|
||||
return &Token{Type: TokenDot, Column: column, Value: "."}
|
||||
}
|
||||
|
||||
func NewTokenHardBreak(column int) *Token {
|
||||
return &Token{Type: TokenHardBreak, Column: column, Value: ";"}
|
||||
}
|
||||
|
||||
func NewTokenAssign(column int) *Token {
|
||||
return &Token{Type: TokenAssign, Column: column, Value: ":="}
|
||||
}
|
||||
|
||||
func NewTokenSlash(column int) *Token {
|
||||
return &Token{Type: TokenSlash, Column: column, Value: "\\"}
|
||||
}
|
||||
|
||||
func NewTokenAtom(name string, column int) *Token {
|
||||
return &Token{Type: TokenAtom, Column: column, Value: name}
|
||||
}
|
||||
|
||||
func NewTokenSoftBreak(column int) *Token {
|
||||
return &Token{Type: TokenSoftBreak, Column: column, Value: "\\n"}
|
||||
}
|
||||
|
||||
func (t TokenType) Name() string {
|
||||
switch t {
|
||||
case TokenOpenParen:
|
||||
return "("
|
||||
case TokenCloseParen:
|
||||
return ")"
|
||||
case TokenSlash:
|
||||
return "\\"
|
||||
case TokenDot:
|
||||
return "."
|
||||
case TokenAtom:
|
||||
return "ATOM"
|
||||
case TokenSoftBreak:
|
||||
return "\\n"
|
||||
case TokenHardBreak:
|
||||
return ";"
|
||||
default:
|
||||
panic(fmt.Errorf("unknown token type %v", t))
|
||||
}
|
||||
}
|
||||
|
||||
func (t Token) Name() string {
|
||||
return t.Type.Name()
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
package token
|
||||
|
||||
import "fmt"
|
||||
|
||||
// All tokens in the pseudo-lambda language.
|
||||
type Type int
|
||||
|
||||
const (
|
||||
OpenParen Type = iota // Denotes the '(' token.
|
||||
CloseParen // Denotes the ')' token.
|
||||
OpenBrace // Denotes the '{' token.
|
||||
CloseBrace // Denotes the '}' token.
|
||||
HardBreak // Denotes the ';' token.
|
||||
Assign // Denotes the ':=' token.
|
||||
Atom // Denotes an alpha-numeric variable.
|
||||
Slash // Denotes the '/' token.
|
||||
Dot // Denotes the '.' token.
|
||||
SoftBreak // Denotes a new-line.
|
||||
)
|
||||
|
||||
// A representation of a token in source code.
|
||||
type Token struct {
|
||||
Column int // Where the token begins in the source text.
|
||||
Type Type // What type the token is.
|
||||
Value string // The value of the token.
|
||||
}
|
||||
|
||||
func NewOpenParen(column int) *Token {
|
||||
return &Token{Type: OpenParen, Column: column, Value: "("}
|
||||
}
|
||||
|
||||
func NewCloseParen(column int) *Token {
|
||||
return &Token{Type: CloseParen, Column: column, Value: ")"}
|
||||
}
|
||||
|
||||
func NewOpenBrace(column int) *Token {
|
||||
return &Token{Type: OpenBrace, Column: column, Value: "{"}
|
||||
}
|
||||
|
||||
func NewCloseBrace(column int) *Token {
|
||||
return &Token{Type: CloseBrace, Column: column, Value: "}"}
|
||||
}
|
||||
|
||||
func NewDot(column int) *Token {
|
||||
return &Token{Type: Dot, Column: column, Value: "."}
|
||||
}
|
||||
|
||||
func NewHardBreak(column int) *Token {
|
||||
return &Token{Type: HardBreak, Column: column, Value: ";"}
|
||||
}
|
||||
|
||||
func NewAssign(column int) *Token {
|
||||
return &Token{Type: Assign, Column: column, Value: ":="}
|
||||
}
|
||||
|
||||
func NewSlash(column int) *Token {
|
||||
return &Token{Type: Slash, Column: column, Value: "\\"}
|
||||
}
|
||||
|
||||
func NewAtom(name string, column int) *Token {
|
||||
return &Token{Type: Atom, Column: column, Value: name}
|
||||
}
|
||||
|
||||
func NewSoftBreak(column int) *Token {
|
||||
return &Token{Type: SoftBreak, Column: column, Value: "\\n"}
|
||||
}
|
||||
|
||||
func Name(typ Type) string {
|
||||
switch typ {
|
||||
case OpenParen:
|
||||
return "("
|
||||
case CloseParen:
|
||||
return ")"
|
||||
case Slash:
|
||||
return "\\"
|
||||
case Dot:
|
||||
return "."
|
||||
case Atom:
|
||||
return "ATOM"
|
||||
case SoftBreak:
|
||||
return "\\n"
|
||||
case HardBreak:
|
||||
return ";"
|
||||
default:
|
||||
panic(fmt.Errorf("unknown token type %v", typ))
|
||||
}
|
||||
}
|
||||
|
||||
func (t Token) Name() string {
|
||||
return Name(t.Type)
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
package set
|
||||
|
||||
import "iter"
|
||||
|
||||
type Set[T comparable] map[T]bool
|
||||
|
||||
func (s *Set[T]) Add(items ...T) {
|
||||
func (s Set[T]) Add(items ...T) {
|
||||
for _, item := range items {
|
||||
(*s)[item] = true
|
||||
s[item] = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,14 +14,14 @@ func (s Set[T]) Has(item T) bool {
|
||||
return s[item]
|
||||
}
|
||||
|
||||
func (s *Set[T]) Remove(items ...T) {
|
||||
func (s Set[T]) Remove(items ...T) {
|
||||
for _, item := range items {
|
||||
delete(*s, item)
|
||||
delete(s, item)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Set[T]) Merge(o *Set[T]) {
|
||||
for item := range *o {
|
||||
func (s Set[T]) Merge(o Set[T]) {
|
||||
for item := range o {
|
||||
s.Add(item)
|
||||
}
|
||||
}
|
||||
@@ -34,8 +36,18 @@ func (s Set[T]) ToList() []T {
|
||||
return list
|
||||
}
|
||||
|
||||
func New[T comparable](items ...T) *Set[T] {
|
||||
result := &Set[T]{}
|
||||
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]{}
|
||||
|
||||
for _, item := range items {
|
||||
result.Add(item)
|
||||
|
||||
@@ -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/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
|
||||
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)))))
|
||||
|
||||
(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
tests/thunk.expected
Normal file
1
tests/thunk.expected
Normal file
@@ -0,0 +1 @@
|
||||
VALUE
|
||||
Reference in New Issue
Block a user