Compare commits
23 Commits
19d0e6d3ea
...
feat/debru
| Author | SHA1 | Date | |
|---|---|---|---|
|
21ae2ca91c
|
|||
|
528956b033
|
|||
| 1974ad582f | |||
| f8e1223463 | |||
| e0114c736d | |||
| 5c54f4e195 | |||
| 307b7ffd1e | |||
| 335ce95c50 | |||
| 19652563a4 | |||
| dec9af0244 | |||
| dbc3c5a8d4 | |||
| 15c904ccc9 | |||
| 609fe05250 | |||
| 0eff85f8fa | |||
| 62699a0e37 | |||
| 90c205db2e | |||
| 72a0afbbc0 | |||
| 22019acbb1 | |||
| d831312dc3 | |||
| c0353c8e1f | |||
| e63c0df410 | |||
| 8b6c632e4d | |||
| 7a5944594f |
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 interpreter."
|
||||||
|
title: "fix: "
|
||||||
|
ref: "main"
|
||||||
|
assignees: []
|
||||||
|
labels:
|
||||||
|
- bug
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Describe what you were trying to do when you encountered the bug.
|
||||||
|
Explain what you expected to happen.
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Current Behavior
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Describe what actually happened.
|
||||||
|
Be specific about the incorrect behavior or error.
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Steps to Reproduce
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Provide step-by-step instructions to reproduce the issue.
|
||||||
|
Include any relevant code, commands, or input.
|
||||||
|
-->
|
||||||
|
|
||||||
|
1.
|
||||||
|
2.
|
||||||
|
3.
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Describe what should happen instead.
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Provide relevant information about your environment.
|
||||||
|
-->
|
||||||
|
|
||||||
|
- Lambda version:
|
||||||
|
- Go version:
|
||||||
|
- Operating system:
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Add any other context about the problem.
|
||||||
|
Include error messages, logs, or screenshots if applicable.
|
||||||
|
If none exist, omit this section.
|
||||||
|
-->
|
||||||
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 interpreter."
|
||||||
|
title: "feat: "
|
||||||
|
ref: "main"
|
||||||
|
assignees: []
|
||||||
|
labels:
|
||||||
|
- enhancement
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Describe the problem or limitation you're encountering.
|
||||||
|
Explain why this feature would be valuable.
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Proposed Solution
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Describe your proposed solution or enhancement.
|
||||||
|
Be specific about what you want to see implemented.
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
<!--
|
||||||
|
List any alternative solutions or approaches you've considered.
|
||||||
|
If none exist, omit this section.
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
<!--
|
||||||
|
List clear, testable criteria that define when this feature is complete.
|
||||||
|
Use bullet points starting with •
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Add any other context, screenshots, or examples about the feature request.
|
||||||
|
If none exist, omit this section.
|
||||||
|
-->
|
||||||
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).
|
||||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -3,18 +3,13 @@
|
|||||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||||
#
|
#
|
||||||
# Binaries for programs and plugins
|
# Binaries for programs and plugins
|
||||||
|
/lambda
|
||||||
*.exe
|
*.exe
|
||||||
*.exe~
|
*.exe~
|
||||||
*.dll
|
*.dll
|
||||||
*.so
|
*.so
|
||||||
*.dylib
|
*.dylib
|
||||||
|
|
||||||
# Build artifacts
|
|
||||||
lambda
|
|
||||||
|
|
||||||
# Test binary, built with `go test -c`
|
|
||||||
*.test
|
|
||||||
|
|
||||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
*.out
|
*.out
|
||||||
|
|
||||||
|
|||||||
151
.golangci.yml
151
.golangci.yml
@@ -1,83 +1,230 @@
|
|||||||
|
---
|
||||||
|
# golangci-lint configuration file made by @ccoVeille
|
||||||
|
# Source: https://github.com/ccoVeille/golangci-lint-config-examples/
|
||||||
|
# Author: @ccoVeille
|
||||||
|
# License: MIT
|
||||||
|
# Variant: 03-safe
|
||||||
|
# Version: v2.0.0
|
||||||
|
#
|
||||||
version: "2"
|
version: "2"
|
||||||
|
|
||||||
formatters:
|
formatters:
|
||||||
enable:
|
enable:
|
||||||
|
# format the code
|
||||||
- gofmt
|
- gofmt
|
||||||
|
# format the block of imports
|
||||||
- gci
|
- gci
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
|
# format the code with Go standard library
|
||||||
gofmt:
|
gofmt:
|
||||||
|
# simplify the code
|
||||||
|
# https://pkg.go.dev/cmd/gofmt#hdr-The_simplify_command
|
||||||
simplify: true
|
simplify: true
|
||||||
rewrite-rules:
|
rewrite-rules:
|
||||||
|
# replace `interface{}` with `any` in the code on format
|
||||||
- pattern: 'interface{}'
|
- pattern: 'interface{}'
|
||||||
replacement: 'any'
|
replacement: 'any'
|
||||||
|
|
||||||
gci:
|
# make sure imports are always in a deterministic order
|
||||||
|
# https://github.com/daixiang0/gci/
|
||||||
|
gci: # define the section orders for imports
|
||||||
sections:
|
sections:
|
||||||
|
# Standard section: captures all standard packages.
|
||||||
- standard
|
- standard
|
||||||
|
# Default section: catchall that is not standard or custom
|
||||||
- default
|
- default
|
||||||
|
# linters that related to local tool, so they should be separated
|
||||||
- localmodule
|
- localmodule
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
exclusions:
|
exclusions:
|
||||||
|
# these presets where present in the v1 version of golangci-lint
|
||||||
|
# it's interesting to keep them when migrating, but removing them should be the goal
|
||||||
presets:
|
presets:
|
||||||
|
# exclude check on comments format in godoc
|
||||||
|
# These are common false positives in poor code
|
||||||
|
# you should not use this on recent code you write from scratch
|
||||||
|
# More information: https://golangci-lint.run/usage/false-positives/#comments
|
||||||
|
#
|
||||||
|
# Please uncomment the following line if your code is not using the godoc format
|
||||||
- comments
|
- comments
|
||||||
|
|
||||||
|
# Common false positives
|
||||||
|
# feel free to remove this if you don't have any false positives
|
||||||
|
# More information: https://golangci-lint.run/usage/false-positives/#common-false-positives
|
||||||
- common-false-positives
|
- common-false-positives
|
||||||
|
|
||||||
|
# Legacy preset is not recommended anymore
|
||||||
|
# More information: https://golangci-lint.run/usage/false-positives/#legacy
|
||||||
- legacy
|
- legacy
|
||||||
|
|
||||||
|
# std-error-handling is a set of rules that avoid reporting unhandled errors on common functions/methods
|
||||||
|
# More information: https://golangci-lint.run/usage/false-positives/#std-error-handling
|
||||||
- std-error-handling
|
- std-error-handling
|
||||||
|
|
||||||
|
# some linters are enabled by default
|
||||||
|
# https://golangci-lint.run/usage/linters/
|
||||||
|
#
|
||||||
|
# enable some extra linters
|
||||||
enable:
|
enable:
|
||||||
|
# Errcheck is a program for checking for unchecked errors in Go code.
|
||||||
- errcheck
|
- errcheck
|
||||||
|
|
||||||
|
# Vet examines Go source code and reports suspicious constructs.
|
||||||
- govet
|
- govet
|
||||||
|
|
||||||
|
# Detects when assignments to existing variables are not used.
|
||||||
- ineffassign
|
- ineffassign
|
||||||
|
|
||||||
|
# It's a set of rules from staticcheck. See https://staticcheck.io/
|
||||||
- staticcheck
|
- staticcheck
|
||||||
|
|
||||||
|
# Checks Go code for unused constants, variables, functions and types.
|
||||||
- unused
|
- unused
|
||||||
|
|
||||||
|
# Fast, configurable, extensible, flexible, and beautiful linter for Go.
|
||||||
|
# Drop-in replacement of golint.
|
||||||
- revive
|
- revive
|
||||||
|
|
||||||
|
# make sure to use t.Helper() when needed
|
||||||
- thelper
|
- thelper
|
||||||
|
|
||||||
|
# mirror suggests rewrites to avoid unnecessary []byte/string conversion
|
||||||
- mirror
|
- mirror
|
||||||
|
|
||||||
|
# detect the possibility to use variables/constants from the Go standard library.
|
||||||
- usestdlibvars
|
- usestdlibvars
|
||||||
|
|
||||||
|
# Finds commonly misspelled English words.
|
||||||
- misspell
|
- misspell
|
||||||
|
|
||||||
|
# Checks for duplicate words in the source code.
|
||||||
- dupword
|
- dupword
|
||||||
|
|
||||||
|
# linter to detect errors invalid key values count
|
||||||
- loggercheck
|
- loggercheck
|
||||||
|
|
||||||
|
# detect when a package or method could be replaced by one from the standard library
|
||||||
- exptostd
|
- exptostd
|
||||||
|
|
||||||
|
# detects nested contexts in loops or function literals
|
||||||
- fatcontext
|
- fatcontext
|
||||||
|
|
||||||
|
# Reports uses of functions with replacement inside the testing package.
|
||||||
- usetesting
|
- usetesting
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
revive:
|
revive:
|
||||||
rules:
|
rules:
|
||||||
|
# these are the default revive rules
|
||||||
|
# you can remove the whole "rules" node if you want
|
||||||
|
# BUT
|
||||||
|
# ! /!\ they all need to be present when you want to add more rules than the default ones
|
||||||
|
# otherwise, you won't have the default rules, but only the ones you define in the "rules" node
|
||||||
|
|
||||||
|
# Blank import should be only in a main or test package, or have a comment justifying it.
|
||||||
- name: blank-imports
|
- name: blank-imports
|
||||||
|
|
||||||
|
# context.Context() should be the first parameter of a function when provided as argument.
|
||||||
- name: context-as-argument
|
- name: context-as-argument
|
||||||
arguments:
|
arguments:
|
||||||
- allowTypesBefore: "*testing.T"
|
- allowTypesBefore: "*testing.T"
|
||||||
|
|
||||||
|
# Basic types should not be used as a key in `context.WithValue`
|
||||||
- name: context-keys-type
|
- name: context-keys-type
|
||||||
|
|
||||||
|
# Importing with `.` makes the programs much harder to understand
|
||||||
- name: dot-imports
|
- name: dot-imports
|
||||||
|
|
||||||
|
# Empty blocks make code less readable and could be a symptom of a bug or unfinished refactoring.
|
||||||
- name: empty-block
|
- name: empty-block
|
||||||
|
|
||||||
|
# for better readability, variables of type `error` must be named with the prefix `err`.
|
||||||
- name: error-naming
|
- name: error-naming
|
||||||
|
|
||||||
|
# for better readability, the errors should be last in the list of returned values by a function.
|
||||||
- name: error-return
|
- name: error-return
|
||||||
|
|
||||||
|
# for better readability, error messages should not be capitalized or end with punctuation or a newline.
|
||||||
- name: error-strings
|
- name: error-strings
|
||||||
|
|
||||||
|
# report when replacing `errors.New(fmt.Sprintf())` with `fmt.Errorf()` is possible
|
||||||
- name: errorf
|
- name: errorf
|
||||||
|
|
||||||
|
# check naming and commenting conventions on exported symbols.
|
||||||
- name: exported
|
- name: exported
|
||||||
arguments:
|
arguments:
|
||||||
|
# make error messages clearer
|
||||||
- "sayRepetitiveInsteadOfStutters"
|
- "sayRepetitiveInsteadOfStutters"
|
||||||
|
|
||||||
|
# incrementing an integer variable by 1 is recommended to be done using the `++` operator
|
||||||
- name: increment-decrement
|
- name: increment-decrement
|
||||||
|
|
||||||
|
# highlights redundant else-blocks that can be eliminated from the code
|
||||||
|
# - name: indent-error-flow
|
||||||
|
|
||||||
|
# This rule suggests a shorter way of writing ranges that do not use the second value.
|
||||||
- name: range
|
- name: range
|
||||||
|
|
||||||
|
# receiver names in a method should reflect the struct name (p for Person, for example)
|
||||||
- name: receiver-naming
|
- name: receiver-naming
|
||||||
|
|
||||||
|
# redefining built in names (true, false, append, make) can lead to bugs very difficult to detect.
|
||||||
- name: redefines-builtin-id
|
- name: redefines-builtin-id
|
||||||
|
|
||||||
|
# redundant else-blocks that can be eliminated from the code.
|
||||||
|
# - name: superfluous-else
|
||||||
|
|
||||||
|
# prevent confusing name for variables when using `time` package
|
||||||
- name: time-naming
|
- name: time-naming
|
||||||
|
|
||||||
|
# warns when an exported function or method returns a value of an un-exported type.
|
||||||
- name: unexported-return
|
- name: unexported-return
|
||||||
|
|
||||||
|
# spots and proposes to remove unreachable code. also helps to spot errors
|
||||||
- name: unreachable-code
|
- name: unreachable-code
|
||||||
|
|
||||||
|
# Functions or methods with unused parameters can be a symptom of an unfinished refactoring or a bug.
|
||||||
- name: unused-parameter
|
- name: unused-parameter
|
||||||
|
|
||||||
|
# report when a variable declaration can be simplified
|
||||||
- name: var-declaration
|
- name: var-declaration
|
||||||
|
|
||||||
|
# warns when initialism, variable or package naming conventions are not followed.
|
||||||
- name: var-naming
|
- name: var-naming
|
||||||
|
|
||||||
misspell:
|
misspell:
|
||||||
|
# Correct spellings using locale preferences for US or UK.
|
||||||
|
# Setting locale to US will correct the British spelling of 'colour' to 'color'.
|
||||||
|
# Default ("") is to use a neutral variety of English.
|
||||||
locale: US
|
locale: US
|
||||||
|
|
||||||
|
# List of words to ignore
|
||||||
|
# among the one defined in https://github.com/golangci/misspell/blob/master/words.go
|
||||||
ignore-rules: []
|
ignore-rules: []
|
||||||
|
# - valor
|
||||||
|
# - and
|
||||||
|
|
||||||
|
# Extra word corrections.
|
||||||
extra-words: []
|
extra-words: []
|
||||||
|
# - typo: "whattever"
|
||||||
|
# correction: "whatever"
|
||||||
|
|
||||||
output:
|
output:
|
||||||
|
# Order to use when sorting results.
|
||||||
|
# Possible values: `file`, `linter`, and `severity`.
|
||||||
|
#
|
||||||
|
# If the severity values are inside the following list, they are ordered in this order:
|
||||||
|
# 1. error
|
||||||
|
# 2. warning
|
||||||
|
# 3. high
|
||||||
|
# 4. medium
|
||||||
|
# 5. low
|
||||||
|
# Either they are sorted alphabetically.
|
||||||
|
#
|
||||||
|
# Default: ["file"]
|
||||||
sort-order:
|
sort-order:
|
||||||
- linter
|
- linter
|
||||||
- severity
|
- severity
|
||||||
- file
|
- file # filepath, line, and column.
|
||||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"makefile.configureOnOpen": false
|
|
||||||
}
|
|
||||||
79
CLAUDE.md
79
CLAUDE.md
@@ -1,6 +1,12 @@
|
|||||||
# Guide To `lambda`
|
# Guide To `lambda`
|
||||||
|
|
||||||
## Coding Styles
|
## Documentation Style
|
||||||
|
|
||||||
|
Use full sentences.
|
||||||
|
Every sentence gets its own line in Markdown.
|
||||||
|
Every sentence ends in a period.
|
||||||
|
|
||||||
|
## Coding Style
|
||||||
|
|
||||||
### Conventional Commits
|
### Conventional Commits
|
||||||
|
|
||||||
@@ -14,11 +20,13 @@ Use conventional commit format: `<type>: <description>`.
|
|||||||
- `fix: correct variable renaming in nested abstractions`
|
- `fix: correct variable renaming in nested abstractions`
|
||||||
- `docs: update Makefile documentation`
|
- `docs: update Makefile documentation`
|
||||||
|
|
||||||
|
DO NOT advertise Claude.
|
||||||
|
|
||||||
### Branch Names
|
### Branch Names
|
||||||
|
|
||||||
Use format: `<type>/<description>` with kebab-case.
|
Use format: `<type>/<description>` with kebab-case.
|
||||||
|
|
||||||
**Types**: Same as commits: `feat`, `fix`, `docs`, `refactor`, `test`, `chore`, `perf`
|
**Types**: Same as commits: `feat`, `fix`, `docs`, `refactor`, `test`, `chore`, `perf`.
|
||||||
|
|
||||||
**Examples**:
|
**Examples**:
|
||||||
|
|
||||||
@@ -27,16 +35,71 @@ Use format: `<type>/<description>` with kebab-case.
|
|||||||
- `docs/makefile-improvements`
|
- `docs/makefile-improvements`
|
||||||
- `refactor/silent-directive`
|
- `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
|
## Pull Request Management
|
||||||
|
|
||||||
Use the `tea` CLI (Gitea command-line tool) for PR operations instead of `gh`.
|
Use the `tea` CLI (Gitea command-line tool) for PR operations instead of `gh`.
|
||||||
|
|
||||||
**Common commands**:
|
**Common commands**:
|
||||||
|
|
||||||
- `tea pr create` - Create a new pull request
|
- `tea pr create --title "<title>" --description "<body>"` - Create a new pull request.
|
||||||
- `tea pr list` - List pull requests
|
- `tea pr list` - List pull requests.
|
||||||
- `tea pr checkout <number>` - Check out a PR locally
|
- `tea pr checkout <number>` - Check out a PR locally.
|
||||||
- `tea pr close <number>` - Close a pull request
|
- `tea pr close <number>` - Close a pull request.
|
||||||
- `tea pr merge <number>` - Merge a pull request
|
- `tea pr merge <number>` - Merge a pull request.
|
||||||
|
|
||||||
**Creating PRs**: Always create PRs to the `main` branch unless specified otherwise
|
**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"
|
||||||
|
```
|
||||||
|
|||||||
51
Makefile
51
Makefile
@@ -1,40 +1,51 @@
|
|||||||
BINARY_NAME=lambda
|
BINARY_NAME=lambda
|
||||||
TEST=simple
|
TEST=simple
|
||||||
|
|
||||||
|
.PHONY: help build run profile explain graph docs test bench clean
|
||||||
.DEFAULT_GOAL := help
|
.DEFAULT_GOAL := help
|
||||||
.SILENT:
|
.SILENT:
|
||||||
.PHONY: help it run profile explain graph clean
|
|
||||||
|
|
||||||
help:
|
help:
|
||||||
echo "Available targets:"
|
echo "Available targets:"
|
||||||
echo " it - Build the lambda binary"
|
echo " build - Build the lambda executable"
|
||||||
echo " run - Build and run with sample input (default: simple.txt)"
|
echo " run - Build and run the lambda interpreter (use TEST=<name> to specify sample)"
|
||||||
echo " profile - Build and run with CPU profiling enabled"
|
echo " profile - Build and run with CPU profiling enabled"
|
||||||
echo " explain - Run with explanation mode and profiling"
|
echo " explain - Build and run with explanation mode and profiling"
|
||||||
echo " graph - Generate CPU profile visualization"
|
echo " graph - Generate and open CPU profile visualization"
|
||||||
echo " clean - Remove build artifacts"
|
echo " docs - Start local godoc server on port 6060"
|
||||||
echo ""
|
echo " test - Run tests for all samples"
|
||||||
echo "Usage: make run TEST=<sample-name>"
|
echo " bench - Run benchmarks for all samples"
|
||||||
|
echo " clean - Remove all build artifacts"
|
||||||
|
|
||||||
it:
|
build:
|
||||||
go build -o ${BINARY_NAME} ./cmd/lambda
|
go build -o ${BINARY_NAME} ./cmd/lambda
|
||||||
|
chmod +x ${BINARY_NAME}
|
||||||
|
|
||||||
run: it
|
run: build
|
||||||
./lambda - < ./samples/$(TEST).txt > program.out
|
./${BINARY_NAME} -s -f ./tests/$(TEST).test -o program.out
|
||||||
|
|
||||||
profile: it
|
profile: build
|
||||||
mkdir -p profile
|
./${BINARY_NAME} -p profile/cpu.prof -f ./tests/$(TEST).test -o program.out
|
||||||
./lambda -p profile/cpu.prof - < ./samples/$(TEST).txt > program.out
|
|
||||||
|
|
||||||
explain: it
|
explain: build
|
||||||
mkdir -p profile
|
./${BINARY_NAME} -x -p profile/cpu.prof -f ./tests/$(TEST).test -o program.out > explain.out
|
||||||
./lambda -x -p profile/cpu.prof - < ./samples/$(TEST).txt > program.out
|
|
||||||
|
|
||||||
graph: profile
|
graph:
|
||||||
go tool pprof -raw -output=profile/cpu.raw profile/cpu.prof
|
go tool pprof -raw -output=profile/cpu.raw profile/cpu.prof
|
||||||
go tool pprof -svg profile/cpu.prof > profile/cpu.svg
|
go tool pprof -svg profile/cpu.prof > profile/cpu.svg
|
||||||
echo "Profile graph saved to 'file://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
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -f ${BINARY_NAME} program.out
|
rm -f ${BINARY_NAME}
|
||||||
|
rm -f program.out
|
||||||
rm -rf profile/
|
rm -rf profile/
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"git.maximhutz.com/max/lambda/internal/cli"
|
"git.maximhutz.com/max/lambda/internal/cli"
|
||||||
"git.maximhutz.com/max/lambda/internal/config"
|
"git.maximhutz.com/max/lambda/internal/config"
|
||||||
"git.maximhutz.com/max/lambda/internal/engine"
|
"git.maximhutz.com/max/lambda/internal/plugins"
|
||||||
"git.maximhutz.com/max/lambda/internal/explanation"
|
|
||||||
"git.maximhutz.com/max/lambda/internal/performance"
|
|
||||||
"git.maximhutz.com/max/lambda/internal/statistics"
|
|
||||||
"git.maximhutz.com/max/lambda/pkg/convert"
|
"git.maximhutz.com/max/lambda/pkg/convert"
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/debruijn"
|
||||||
"git.maximhutz.com/max/lambda/pkg/lambda"
|
"git.maximhutz.com/max/lambda/pkg/lambda"
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/reducer"
|
||||||
"git.maximhutz.com/max/lambda/pkg/saccharine"
|
"git.maximhutz.com/max/lambda/pkg/saccharine"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -35,42 +33,53 @@ func main() {
|
|||||||
|
|
||||||
// Compile expression to lambda calculus.
|
// Compile expression to lambda calculus.
|
||||||
compiled := convert.SaccharineToLambda(ast)
|
compiled := convert.SaccharineToLambda(ast)
|
||||||
logger.Info("compiled λ expression", "tree", lambda.Stringify(compiled))
|
logger.Info("compiled λ expression", "tree", compiled.String())
|
||||||
|
|
||||||
// Create reduction engine.
|
// Create reducer based on the selected interpreter.
|
||||||
process := engine.New(options, &compiled)
|
var red reducer.Reducer
|
||||||
|
switch options.Interpreter {
|
||||||
|
case config.DeBruijnInterpreter:
|
||||||
|
dbExpr := convert.LambdaToDeBruijn(compiled)
|
||||||
|
logger.Info("converted to De Bruijn", "tree", dbExpr.String())
|
||||||
|
red = debruijn.NewNormalOrderReducer(&dbExpr)
|
||||||
|
default:
|
||||||
|
red = lambda.NewNormalOrderReducer(&compiled)
|
||||||
|
}
|
||||||
|
|
||||||
// If the user selected to track CPU performance, attach a profiler to the
|
// If the user selected to track CPU performance, attach a profiler.
|
||||||
// process.
|
|
||||||
if options.Profile != "" {
|
if options.Profile != "" {
|
||||||
profiler := performance.Track(options.Profile)
|
plugins.NewPerformance(options.Profile, red)
|
||||||
process.On("start", profiler.Start)
|
|
||||||
process.On("end", profiler.End)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the user selected to produce a step-by-step explanation, attach an
|
// If the user selected to produce a step-by-step explanation, attach an
|
||||||
// observer here.
|
// observer.
|
||||||
if options.Explanation {
|
if options.Explanation {
|
||||||
explanation.Track(process)
|
plugins.NewExplanation(red)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the user opted to track statistics, attach a tracker here, too.
|
// If the user opted to track statistics, attach a tracker.
|
||||||
if options.Statistics {
|
if options.Statistics {
|
||||||
statistics := statistics.Track()
|
plugins.NewStatistics(red)
|
||||||
process.On("start", statistics.Start)
|
|
||||||
process.On("step", statistics.Step)
|
|
||||||
process.On("end", statistics.End)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the user selected for verbose debug logs, attach a reduction tracker.
|
// If the user selected for verbose debug logs, attach a reduction tracker.
|
||||||
if options.Verbose {
|
if options.Verbose {
|
||||||
process.On("step", func() {
|
plugins.NewLogs(logger, red)
|
||||||
logger.Info("reduction", "tree", lambda.Stringify(compiled))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
process.Run()
|
// Run reduction.
|
||||||
|
red.Reduce()
|
||||||
|
|
||||||
// Return the final reduced result.
|
// Return the final reduced result.
|
||||||
fmt.Println(lambda.Stringify(compiled))
|
// For De Bruijn, convert back to lambda for consistent output.
|
||||||
|
var result string
|
||||||
|
if options.Interpreter == config.DeBruijnInterpreter {
|
||||||
|
dbExpr := red.Expression().(debruijn.Expression)
|
||||||
|
lambdaExpr := convert.DeBruijnToLambda(dbExpr)
|
||||||
|
result = lambdaExpr.String()
|
||||||
|
} else {
|
||||||
|
result = red.Expression().String()
|
||||||
|
}
|
||||||
|
err = options.Destination.Write(result)
|
||||||
|
cli.HandleError(err)
|
||||||
}
|
}
|
||||||
|
|||||||
163
cmd/lambda/lambda_test.go
Normal file
163
cmd/lambda/lambda_test.go
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/convert"
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/debruijn"
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/lambda"
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/saccharine"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Helper function to run a single sample through the lambda interpreter.
|
||||||
|
func runSample(samplePath string) (string, error) {
|
||||||
|
// Read the sample file.
|
||||||
|
input, err := os.ReadFile(samplePath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse code into syntax tree.
|
||||||
|
ast, err := saccharine.Parse(string(input))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile expression to lambda calculus.
|
||||||
|
compiled := convert.SaccharineToLambda(ast)
|
||||||
|
|
||||||
|
// Create and run the reducer.
|
||||||
|
reducer := lambda.NewNormalOrderReducer(&compiled)
|
||||||
|
reducer.Reduce()
|
||||||
|
|
||||||
|
return reducer.Expression().String() + "\n", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to run a single sample through the De Bruijn interpreter.
|
||||||
|
func runSampleDeBruijn(samplePath string) (string, error) {
|
||||||
|
// Read the sample file.
|
||||||
|
input, err := os.ReadFile(samplePath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse code into syntax tree.
|
||||||
|
ast, err := saccharine.Parse(string(input))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile expression to lambda calculus.
|
||||||
|
compiled := convert.SaccharineToLambda(ast)
|
||||||
|
|
||||||
|
// Convert to De Bruijn and run reducer.
|
||||||
|
dbExpr := convert.LambdaToDeBruijn(compiled)
|
||||||
|
reducer := debruijn.NewNormalOrderReducer(&dbExpr)
|
||||||
|
reducer.Reduce()
|
||||||
|
|
||||||
|
// Convert back to lambda for output.
|
||||||
|
result := reducer.Expression().(debruijn.Expression)
|
||||||
|
lambdaResult := convert.DeBruijnToLambda(result)
|
||||||
|
|
||||||
|
return lambdaResult.String() + "\n", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that all samples produce expected output with lambda interpreter.
|
||||||
|
func TestSamplesValidity(t *testing.T) {
|
||||||
|
// Discover all .test files in the tests directory.
|
||||||
|
testFiles, err := filepath.Glob("../../tests/*.test")
|
||||||
|
assert.NoError(t, err, "Failed to read tests directory.")
|
||||||
|
assert.NotEmpty(t, testFiles, "No '*.test' files found in directory.")
|
||||||
|
|
||||||
|
for _, testPath := range testFiles {
|
||||||
|
// Build expected file path.
|
||||||
|
expectedPath := strings.TrimSuffix(testPath, filepath.Ext(testPath)) + ".expected"
|
||||||
|
|
||||||
|
name := strings.TrimSuffix(filepath.Base(testPath), filepath.Ext(testPath))
|
||||||
|
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
// Run the sample and capture output.
|
||||||
|
actual, err := runSample(testPath)
|
||||||
|
assert.NoError(t, err, "Failed to run sample.")
|
||||||
|
|
||||||
|
// Read expected output.
|
||||||
|
expectedBytes, err := os.ReadFile(expectedPath)
|
||||||
|
assert.NoError(t, err, "Failed to read expected output.")
|
||||||
|
expected := string(expectedBytes)
|
||||||
|
|
||||||
|
// Compare outputs.
|
||||||
|
assert.Equal(t, expected, actual, "Output does not match expected.")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that all samples produce expected output with De Bruijn interpreter.
|
||||||
|
func TestSamplesValidityDeBruijn(t *testing.T) {
|
||||||
|
// Discover all .test files in the tests directory.
|
||||||
|
testFiles, err := filepath.Glob("../../tests/*.test")
|
||||||
|
assert.NoError(t, err, "Failed to read tests directory.")
|
||||||
|
assert.NotEmpty(t, testFiles, "No '*.test' files found in directory.")
|
||||||
|
|
||||||
|
for _, testPath := range testFiles {
|
||||||
|
// Build expected file path.
|
||||||
|
expectedPath := strings.TrimSuffix(testPath, filepath.Ext(testPath)) + ".expected"
|
||||||
|
|
||||||
|
name := strings.TrimSuffix(filepath.Base(testPath), filepath.Ext(testPath))
|
||||||
|
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
// Run the sample and capture output.
|
||||||
|
actual, err := runSampleDeBruijn(testPath)
|
||||||
|
assert.NoError(t, err, "Failed to run sample.")
|
||||||
|
|
||||||
|
// Read expected output.
|
||||||
|
expectedBytes, err := os.ReadFile(expectedPath)
|
||||||
|
assert.NoError(t, err, "Failed to read expected output.")
|
||||||
|
expected := string(expectedBytes)
|
||||||
|
|
||||||
|
// Compare outputs.
|
||||||
|
assert.Equal(t, expected, actual, "Output does not match expected.")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Benchmark all samples using sub-benchmarks.
|
||||||
|
func BenchmarkSamples(b *testing.B) {
|
||||||
|
// Discover all .test files in the tests directory.
|
||||||
|
testFiles, err := filepath.Glob("../../tests/*.test")
|
||||||
|
assert.NoError(b, err, "Failed to read tests directory.")
|
||||||
|
assert.NotEmpty(b, testFiles, "No '*.test' files found in directory.")
|
||||||
|
|
||||||
|
for _, path := range testFiles {
|
||||||
|
name := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))
|
||||||
|
|
||||||
|
b.Run(name, func(b *testing.B) {
|
||||||
|
for b.Loop() {
|
||||||
|
_, err := runSample(path)
|
||||||
|
assert.NoError(b, err, "Failed to run sample.")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Benchmark all samples using De Bruijn interpreter.
|
||||||
|
func BenchmarkSamplesDeBruijn(b *testing.B) {
|
||||||
|
// Discover all .test files in the tests directory.
|
||||||
|
testFiles, err := filepath.Glob("../../tests/*.test")
|
||||||
|
assert.NoError(b, err, "Failed to read tests directory.")
|
||||||
|
assert.NotEmpty(b, testFiles, "No '*.test' files found in directory.")
|
||||||
|
|
||||||
|
for _, path := range testFiles {
|
||||||
|
name := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))
|
||||||
|
|
||||||
|
b.Run(name, func(b *testing.B) {
|
||||||
|
for b.Loop() {
|
||||||
|
_, err := runSampleDeBruijn(path)
|
||||||
|
assert.NoError(b, err, "Failed to run sample.")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
8
go.mod
8
go.mod
@@ -1,3 +1,11 @@
|
|||||||
module git.maximhutz.com/max/lambda
|
module git.maximhutz.com/max/lambda
|
||||||
|
|
||||||
go 1.25.5
|
go 1.25.5
|
||||||
|
|
||||||
|
require github.com/stretchr/testify v1.11.1
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
|
|||||||
9
go.sum
Normal file
9
go.sum
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
@@ -1,11 +1,21 @@
|
|||||||
// Package "config" parses ad handles the user settings given to the program.
|
// Package "config" parses ad handles the user settings given to the program.
|
||||||
package config
|
package config
|
||||||
|
|
||||||
|
// Interpreter specifies the reduction engine to use.
|
||||||
|
type Interpreter string
|
||||||
|
|
||||||
|
const (
|
||||||
|
LambdaInterpreter Interpreter = "lambda"
|
||||||
|
DeBruijnInterpreter Interpreter = "debruijn"
|
||||||
|
)
|
||||||
|
|
||||||
// Configuration settings for the program.
|
// Configuration settings for the program.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Source Source // The source code given to the program.
|
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.
|
Verbose bool // Whether or not to print debug logs.
|
||||||
Explanation bool // Whether or not to print an explanation of the reduction.
|
Explanation bool // Whether or not to print an explanation of the reduction.
|
||||||
Profile string // If not nil, print a CPU profile during execution.
|
Profile string // If not nil, print a CPU profile during execution.
|
||||||
Statistics bool // Whether or not to print statistics.
|
Statistics bool // Whether or not to print statistics.
|
||||||
|
Interpreter Interpreter // The interpreter engine to use.
|
||||||
}
|
}
|
||||||
|
|||||||
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)
|
||||||
|
}
|
||||||
@@ -12,28 +12,58 @@ func FromArgs() (*Config, error) {
|
|||||||
explanation := flag.Bool("x", false, "Explanation. Whether or not to show all reduction steps.")
|
explanation := flag.Bool("x", false, "Explanation. Whether or not to show all reduction steps.")
|
||||||
statistics := flag.Bool("s", false, "Statistics. If set, the process will print various statistics about the run.")
|
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.")
|
profile := flag.String("p", "", "CPU profiling. If an output file is defined, the program will profile its execution and dump its results into it.")
|
||||||
|
file := flag.String("f", "", "File. If set, read source from the specified file.")
|
||||||
|
output := flag.String("o", "", "Output. If set, write result to the specified file. Use '-' for stdout (default).")
|
||||||
|
interpreter := flag.String("i", "lambda", "Interpreter. The reduction engine to use: 'lambda' or 'debruijn'.")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
// There must only be one input argument.
|
// Validate interpreter flag.
|
||||||
if flag.NArg() == 0 {
|
var interpType Interpreter
|
||||||
return nil, fmt.Errorf("no input given")
|
switch *interpreter {
|
||||||
} else if flag.NArg() > 1 {
|
case "lambda":
|
||||||
return nil, fmt.Errorf("more than 1 command-line argument")
|
interpType = LambdaInterpreter
|
||||||
|
case "debruijn":
|
||||||
|
interpType = DeBruijnInterpreter
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("invalid interpreter: %s (must be 'lambda' or 'debruijn')", *interpreter)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse source type.
|
// Parse source type.
|
||||||
var source Source
|
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) == "-" {
|
if flag.Arg(0) == "-" {
|
||||||
source = StdinSource{}
|
source = StdinSource{}
|
||||||
} else {
|
} 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{
|
return &Config{
|
||||||
Source: source,
|
Source: source,
|
||||||
|
Destination: destination,
|
||||||
Verbose: *verbose,
|
Verbose: *verbose,
|
||||||
Explanation: *explanation,
|
Explanation: *explanation,
|
||||||
Profile: *profile,
|
Profile: *profile,
|
||||||
Statistics: *statistics,
|
Statistics: *statistics,
|
||||||
|
Interpreter: interpType,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,3 +27,15 @@ func (s StdinSource) Extract() (string, error) {
|
|||||||
|
|
||||||
return string(data), nil
|
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 "engine" provides an extensible interface for users to interfact with
|
|
||||||
// λ-calculus.
|
|
||||||
package engine
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.maximhutz.com/max/lambda/internal/config"
|
|
||||||
"git.maximhutz.com/max/lambda/pkg/emitter"
|
|
||||||
"git.maximhutz.com/max/lambda/pkg/lambda"
|
|
||||||
)
|
|
||||||
|
|
||||||
// A process for reducing one λ-expression.
|
|
||||||
type Engine struct {
|
|
||||||
Config *config.Config
|
|
||||||
Expression *lambda.Expression
|
|
||||||
emitter.Emitter
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new engine, given an unreduced λ-expression.
|
|
||||||
func New(config *config.Config, expression *lambda.Expression) *Engine {
|
|
||||||
return &Engine{Config: config, Expression: expression}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Begin the reduction process.
|
|
||||||
func (e Engine) Run() {
|
|
||||||
e.Emit("start")
|
|
||||||
|
|
||||||
for lambda.ReduceOnce(e.Expression) {
|
|
||||||
e.Emit("step")
|
|
||||||
}
|
|
||||||
|
|
||||||
e.Emit("end")
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
// Package "explanation" provides a observer to gather the reasoning during the
|
|
||||||
// reduction, and present a thorough explanation to the user for each step.
|
|
||||||
package explanation
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.maximhutz.com/max/lambda/internal/engine"
|
|
||||||
"git.maximhutz.com/max/lambda/pkg/lambda"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Track the reductions made by a reduction proess.
|
|
||||||
type Tracker struct {
|
|
||||||
process *engine.Engine
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attaches a new explanation tracker to a process.
|
|
||||||
func Track(process *engine.Engine) *Tracker {
|
|
||||||
tracker := &Tracker{process: process}
|
|
||||||
process.On("start", tracker.Start)
|
|
||||||
process.On("step", tracker.Step)
|
|
||||||
|
|
||||||
return tracker
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Tracker) Start() {
|
|
||||||
fmt.Println(lambda.Stringify(*t.process.Expression))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Tracker) Step() {
|
|
||||||
fmt.Println(" =", lambda.Stringify(*t.process.Expression))
|
|
||||||
}
|
|
||||||
23
internal/plugins/debug.go
Normal file
23
internal/plugins/debug.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package plugins
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/reducer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Logs struct {
|
||||||
|
logger *slog.Logger
|
||||||
|
reducer reducer.Reducer
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLogs(logger *slog.Logger, r reducer.Reducer) *Logs {
|
||||||
|
plugin := &Logs{logger, r}
|
||||||
|
r.On(reducer.StepEvent, plugin.Step)
|
||||||
|
|
||||||
|
return plugin
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Logs) Step() {
|
||||||
|
t.logger.Info("reduction", "tree", t.reducer.Expression().String())
|
||||||
|
}
|
||||||
31
internal/plugins/explanation.go
Normal file
31
internal/plugins/explanation.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
// Package "explanation" provides an observer to gather the reasoning during the
|
||||||
|
// reduction, and present a thorough explanation to the user for each step.
|
||||||
|
package plugins
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/reducer"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Track the reductions made by a reduction process.
|
||||||
|
type Explanation struct {
|
||||||
|
reducer reducer.Reducer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attaches a new explanation tracker to a reducer.
|
||||||
|
func NewExplanation(r reducer.Reducer) *Explanation {
|
||||||
|
plugin := &Explanation{reducer: r}
|
||||||
|
r.On(reducer.StartEvent, plugin.Start)
|
||||||
|
r.On(reducer.StepEvent, plugin.Step)
|
||||||
|
|
||||||
|
return plugin
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Explanation) Start() {
|
||||||
|
fmt.Println(t.reducer.Expression().String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Explanation) Step() {
|
||||||
|
fmt.Println(" =", t.reducer.Expression().String())
|
||||||
|
}
|
||||||
@@ -1,28 +1,34 @@
|
|||||||
// Package "performance" provides a tracker to observer CPU performance during
|
// Package "performance" provides a tracker to observer CPU performance during
|
||||||
// execution.
|
// execution.
|
||||||
package performance
|
package plugins
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime/pprof"
|
"runtime/pprof"
|
||||||
|
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/reducer"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Observes a reduction process, and publishes a CPU performance profile on
|
// Observes a reduction process, and publishes a CPU performance profile on
|
||||||
// completion.
|
// completion.
|
||||||
type Tracker struct {
|
type Performance struct {
|
||||||
File string
|
File string
|
||||||
filePointer *os.File
|
filePointer *os.File
|
||||||
Error error
|
Error error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a performance tracker that outputs a profile to "file".
|
// Create a performance tracker that outputs a profile to "file".
|
||||||
func Track(file string) *Tracker {
|
func NewPerformance(file string, process reducer.Reducer) *Performance {
|
||||||
return &Tracker{File: file}
|
plugin := &Performance{File: file}
|
||||||
|
process.On(reducer.StartEvent, plugin.Start)
|
||||||
|
process.On(reducer.StopEvent, plugin.Stop)
|
||||||
|
|
||||||
|
return plugin
|
||||||
}
|
}
|
||||||
|
|
||||||
// Begin profiling.
|
// Begin profiling.
|
||||||
func (t *Tracker) Start() {
|
func (t *Performance) Start() {
|
||||||
var absPath string
|
var absPath string
|
||||||
|
|
||||||
absPath, t.Error = filepath.Abs(t.File)
|
absPath, t.Error = filepath.Abs(t.File)
|
||||||
@@ -47,7 +53,7 @@ func (t *Tracker) Start() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Stop profiling.
|
// Stop profiling.
|
||||||
func (t *Tracker) End() {
|
func (t *Performance) Stop() {
|
||||||
pprof.StopCPUProfile()
|
pprof.StopCPUProfile()
|
||||||
t.filePointer.Close()
|
t.filePointer.Close()
|
||||||
}
|
}
|
||||||
44
internal/plugins/statistics.go
Normal file
44
internal/plugins/statistics.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package plugins
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.maximhutz.com/max/lambda/internal/statistics"
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/reducer"
|
||||||
|
)
|
||||||
|
|
||||||
|
// An observer, to track reduction performance.
|
||||||
|
type Statistics struct {
|
||||||
|
start time.Time
|
||||||
|
steps uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new reduction performance Statistics.
|
||||||
|
func NewStatistics(r reducer.Reducer) *Statistics {
|
||||||
|
plugin := &Statistics{}
|
||||||
|
r.On(reducer.StartEvent, plugin.Start)
|
||||||
|
r.On(reducer.StepEvent, plugin.Step)
|
||||||
|
r.On(reducer.StopEvent, plugin.Stop)
|
||||||
|
|
||||||
|
return plugin
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Statistics) Start() {
|
||||||
|
t.start = time.Now()
|
||||||
|
t.steps = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Statistics) Step() {
|
||||||
|
t.steps++
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Statistics) Stop() {
|
||||||
|
results := statistics.Results{
|
||||||
|
StepsTaken: t.steps,
|
||||||
|
TimeElapsed: uint64(time.Since(t.start).Milliseconds()),
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprint(os.Stderr, results.String())
|
||||||
|
}
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
package statistics
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// An observer, to track reduction performance.
|
|
||||||
type Tracker struct {
|
|
||||||
start time.Time
|
|
||||||
steps uint64
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new reduction performance tracker.
|
|
||||||
func Track() *Tracker {
|
|
||||||
return &Tracker{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Tracker) Start() {
|
|
||||||
t.start = time.Now()
|
|
||||||
t.steps = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Tracker) Step() {
|
|
||||||
t.steps++
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Tracker) End() {
|
|
||||||
results := Results{
|
|
||||||
StepsTaken: t.steps,
|
|
||||||
TimeElapsed: uint64(time.Since(t.start).Milliseconds()),
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprint(os.Stderr, results.String())
|
|
||||||
}
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
# Makefile Improvements
|
|
||||||
|
|
||||||
This document lists the issues identified in the original Makefile and the improvements that were implemented.
|
|
||||||
|
|
||||||
## Issues Fixed
|
|
||||||
|
|
||||||
### 1. Hardcoded `.exe` extension on Unix
|
|
||||||
**Problem**: `BINARY_NAME=lambda.exe` used a Windows extension on macOS/Linux systems.
|
|
||||||
|
|
||||||
**Solution**: Changed to `BINARY_NAME=lambda` since Unix executables don't use extensions.
|
|
||||||
|
|
||||||
**Commit**: `0d06fac` - fix: remove Windows .exe extension from binary name
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. Redundant `chmod +x`
|
|
||||||
**Problem**: The `chmod +x` command was unnecessary since `go build` already sets the executable bit.
|
|
||||||
|
|
||||||
**Solution**: Removed the redundant `chmod +x ${BINARY_NAME}` line.
|
|
||||||
|
|
||||||
**Commit**: `e0b0b92` - refactor: remove redundant chmod +x command
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3. Missing `.PHONY` declarations
|
|
||||||
**Problem**: Without `.PHONY`, if files named `run`, `graph`, etc. existed, Make would skip those targets.
|
|
||||||
|
|
||||||
**Solution**: Added `.PHONY: help it run profile explain graph clean` declaration.
|
|
||||||
|
|
||||||
**Commit**: `e5ceeb2` - feat: add .PHONY declarations for all targets
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4. No `clean` target
|
|
||||||
**Problem**: No standard way to remove build artifacts.
|
|
||||||
|
|
||||||
**Solution**: Added `clean` target to remove binary, output files, and profile directory:
|
|
||||||
```makefile
|
|
||||||
clean:
|
|
||||||
rm -f ${BINARY_NAME} program.out
|
|
||||||
rm -rf profile/
|
|
||||||
```
|
|
||||||
|
|
||||||
**Commit**: `7927df4` - feat: add clean target to remove build artifacts
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 5. Missing `help` or default target
|
|
||||||
**Problem**: Running `make` with no arguments was unclear about available targets.
|
|
||||||
|
|
||||||
**Solution**: Added `help` target documenting all available commands and their usage.
|
|
||||||
|
|
||||||
**Commit**: `24fdc1c` - feat: add help target to document available commands
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 6. Profile directory not created
|
|
||||||
**Problem**: The `profile` and `explain` targets wrote to `profile/cpu.prof` but never created the directory, causing failures on first run.
|
|
||||||
|
|
||||||
**Solution**: Added `mkdir -p profile` to both `profile` and `explain` targets.
|
|
||||||
|
|
||||||
**Commit**: `bb48d07` - fix: ensure profile directory exists before writing
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 7. No dependency check on `graph`
|
|
||||||
**Problem**: The `graph` target assumed `profile/cpu.prof` exists but didn't depend on `profile`.
|
|
||||||
|
|
||||||
**Solution**: Changed `graph:` to `graph: profile` to ensure profiling runs first.
|
|
||||||
|
|
||||||
**Commit**: `3158c35` - fix: add profile dependency to graph target
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 8. Verbose command prefixes
|
|
||||||
**Problem**: Every command used `@` prefix individually to suppress output, cluttering the file.
|
|
||||||
|
|
||||||
**Solution**: Added `.SILENT:` directive at the top and removed all `@` prefixes. Also moved `TEST` variable to top with other variables.
|
|
||||||
|
|
||||||
**Commit**: `8f70bfb` - refactor: use .SILENT directive instead of @ prefixes
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Additional Suggestions (Not Yet Implemented)
|
|
||||||
|
|
||||||
### 9. Missing `lambda` binary in `.gitignore`
|
|
||||||
**Issue**: The `.gitignore` has `*.exe` but not the actual `lambda` binary name. Since we removed the `.exe` extension, the binary won't be ignored.
|
|
||||||
|
|
||||||
**Recommendation**: Add `lambda` to `.gitignore`:
|
|
||||||
```
|
|
||||||
# Build artifacts
|
|
||||||
lambda
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 10. No explicit default target
|
|
||||||
**Issue**: While `help` is currently the first target (and thus default), it's not explicitly declared.
|
|
||||||
|
|
||||||
**Recommendation**: Add `.DEFAULT_GOAL = help` at the top for clarity:
|
|
||||||
```makefile
|
|
||||||
BINARY_NAME=lambda
|
|
||||||
TEST=simple
|
|
||||||
|
|
||||||
.DEFAULT_GOAL := help
|
|
||||||
.SILENT:
|
|
||||||
.PHONY: help it run profile explain graph clean
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
The Makefile has been significantly improved with better organization, proper dependency management, directory creation, helpful documentation, and cleaner syntax. The remaining suggestions are minor quality-of-life improvements that can be addressed as needed.
|
|
||||||
82
pkg/convert/debruijn_to_lambda.go
Normal file
82
pkg/convert/debruijn_to_lambda.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package convert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/debruijn"
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/lambda"
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/set"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DeBruijnToLambda converts a De Bruijn indexed expression back to named lambda calculus.
|
||||||
|
func DeBruijnToLambda(expr debruijn.Expression) lambda.Expression {
|
||||||
|
return deBruijnToLambdaWithContext(expr, []string{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func deBruijnToLambdaWithContext(expr debruijn.Expression, context []string) lambda.Expression {
|
||||||
|
switch e := expr.(type) {
|
||||||
|
case *debruijn.Variable:
|
||||||
|
index := e.Index()
|
||||||
|
if index < len(context) {
|
||||||
|
// Bound variable: look up name in context.
|
||||||
|
name := context[len(context)-1-index]
|
||||||
|
return lambda.NewVariable(name)
|
||||||
|
}
|
||||||
|
// Free variable: use the label if available.
|
||||||
|
if e.Label() != "" {
|
||||||
|
return lambda.NewVariable(e.Label())
|
||||||
|
}
|
||||||
|
// Generate a name for free variables without labels.
|
||||||
|
return lambda.NewVariable(fmt.Sprintf("free%d", index))
|
||||||
|
|
||||||
|
case *debruijn.Abstraction:
|
||||||
|
// Generate a fresh parameter name.
|
||||||
|
used := collectUsedNames(e.Body(), context)
|
||||||
|
paramName := generateFreshName(used)
|
||||||
|
newContext := append(context, paramName)
|
||||||
|
body := deBruijnToLambdaWithContext(e.Body(), newContext)
|
||||||
|
return lambda.NewAbstraction(paramName, body)
|
||||||
|
|
||||||
|
case *debruijn.Application:
|
||||||
|
abs := deBruijnToLambdaWithContext(e.Abstraction(), context)
|
||||||
|
arg := deBruijnToLambdaWithContext(e.Argument(), context)
|
||||||
|
return lambda.NewApplication(abs, arg)
|
||||||
|
|
||||||
|
default:
|
||||||
|
panic("unknown expression type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// collectUsedNames gathers all variable labels used in an expression.
|
||||||
|
func collectUsedNames(expr debruijn.Expression, context []string) *set.Set[string] {
|
||||||
|
used := set.New[string]()
|
||||||
|
for _, name := range context {
|
||||||
|
used.Add(name)
|
||||||
|
}
|
||||||
|
collectUsedNamesHelper(expr, used)
|
||||||
|
return used
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectUsedNamesHelper(expr debruijn.Expression, used *set.Set[string]) {
|
||||||
|
switch e := expr.(type) {
|
||||||
|
case *debruijn.Variable:
|
||||||
|
if e.Label() != "" {
|
||||||
|
used.Add(e.Label())
|
||||||
|
}
|
||||||
|
case *debruijn.Abstraction:
|
||||||
|
collectUsedNamesHelper(e.Body(), used)
|
||||||
|
case *debruijn.Application:
|
||||||
|
collectUsedNamesHelper(e.Abstraction(), used)
|
||||||
|
collectUsedNamesHelper(e.Argument(), used)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateFreshName creates a fresh variable name not in the used set.
|
||||||
|
func generateFreshName(used *set.Set[string]) string {
|
||||||
|
for i := 0; ; i++ {
|
||||||
|
name := fmt.Sprintf("_%d", i)
|
||||||
|
if !used.Has(name) {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
44
pkg/convert/lambda_to_debruijn.go
Normal file
44
pkg/convert/lambda_to_debruijn.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package convert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/debruijn"
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/lambda"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LambdaToDeBruijn converts a lambda calculus expression to De Bruijn indexed form.
|
||||||
|
// The context parameter tracks bound variables from outer abstractions.
|
||||||
|
func LambdaToDeBruijn(expr lambda.Expression) debruijn.Expression {
|
||||||
|
return lambdaToDeBruijnWithContext(expr, []string{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func lambdaToDeBruijnWithContext(expr lambda.Expression, context []string) debruijn.Expression {
|
||||||
|
switch e := expr.(type) {
|
||||||
|
case *lambda.Variable:
|
||||||
|
name := e.Value()
|
||||||
|
// Search for the variable in the context (innermost to outermost).
|
||||||
|
for i := len(context) - 1; i >= 0; i-- {
|
||||||
|
if context[i] == name {
|
||||||
|
index := len(context) - 1 - i
|
||||||
|
return debruijn.NewVariable(index, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Free variable: use a negative index to mark it.
|
||||||
|
// We encode free variables with index = len(context) + position.
|
||||||
|
// For simplicity, we use a large index that won't conflict.
|
||||||
|
return debruijn.NewVariable(len(context), name)
|
||||||
|
|
||||||
|
case *lambda.Abstraction:
|
||||||
|
// Add the parameter to the context.
|
||||||
|
newContext := append(context, e.Parameter())
|
||||||
|
body := lambdaToDeBruijnWithContext(e.Body(), newContext)
|
||||||
|
return debruijn.NewAbstraction(body)
|
||||||
|
|
||||||
|
case *lambda.Application:
|
||||||
|
abs := lambdaToDeBruijnWithContext(e.Abstraction(), context)
|
||||||
|
arg := lambdaToDeBruijnWithContext(e.Argument(), context)
|
||||||
|
return debruijn.NewApplication(abs, arg)
|
||||||
|
|
||||||
|
default:
|
||||||
|
panic("unknown expression type")
|
||||||
|
}
|
||||||
|
}
|
||||||
119
pkg/debruijn/expression.go
Normal file
119
pkg/debruijn/expression.go
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
// Package debruijn provides De Bruijn indexed lambda calculus expressions.
|
||||||
|
// De Bruijn indices eliminate the need for variable names by using numeric
|
||||||
|
// indices to refer to bound variables, avoiding capture issues during substitution.
|
||||||
|
package debruijn
|
||||||
|
|
||||||
|
import "git.maximhutz.com/max/lambda/pkg/expr"
|
||||||
|
|
||||||
|
// Expression is the interface for all De Bruijn indexed expression types.
|
||||||
|
// It embeds the general expr.Expression interface for cross-mode compatibility.
|
||||||
|
type Expression interface {
|
||||||
|
expr.Expression
|
||||||
|
Accept(Visitor)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
// Abstraction represents a lambda abstraction without a named parameter.
|
||||||
|
// In De Bruijn notation, the parameter is implicit and referenced by index 0
|
||||||
|
// within the body.
|
||||||
|
type Abstraction struct {
|
||||||
|
body Expression
|
||||||
|
}
|
||||||
|
|
||||||
|
// Body returns the body of the abstraction.
|
||||||
|
func (a *Abstraction) Body() Expression {
|
||||||
|
return a.body
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accept implements the Visitor pattern.
|
||||||
|
func (a *Abstraction) Accept(v Visitor) {
|
||||||
|
v.VisitAbstraction(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the De Bruijn notation string representation.
|
||||||
|
func (a *Abstraction) String() string {
|
||||||
|
return Stringify(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAbstraction creates a new De Bruijn abstraction with the given body.
|
||||||
|
func NewAbstraction(body Expression) *Abstraction {
|
||||||
|
return &Abstraction{body: body}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
// Application represents the application of one expression to another.
|
||||||
|
type Application struct {
|
||||||
|
abstraction Expression
|
||||||
|
argument Expression
|
||||||
|
}
|
||||||
|
|
||||||
|
// Abstraction returns the function expression being applied.
|
||||||
|
func (a *Application) Abstraction() Expression {
|
||||||
|
return a.abstraction
|
||||||
|
}
|
||||||
|
|
||||||
|
// Argument returns the argument expression.
|
||||||
|
func (a *Application) Argument() Expression {
|
||||||
|
return a.argument
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accept implements the Visitor pattern.
|
||||||
|
func (a *Application) Accept(v Visitor) {
|
||||||
|
v.VisitApplication(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the De Bruijn notation string representation.
|
||||||
|
func (a *Application) String() string {
|
||||||
|
return Stringify(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewApplication creates a new application expression.
|
||||||
|
func NewApplication(abstraction Expression, argument Expression) *Application {
|
||||||
|
return &Application{abstraction: abstraction, argument: argument}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
// Variable represents a De Bruijn indexed variable.
|
||||||
|
// The index indicates how many binders to skip to find the binding abstraction.
|
||||||
|
// The label is an optional hint for display purposes.
|
||||||
|
type Variable struct {
|
||||||
|
index int
|
||||||
|
label string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index returns the De Bruijn index.
|
||||||
|
func (v *Variable) Index() int {
|
||||||
|
return v.index
|
||||||
|
}
|
||||||
|
|
||||||
|
// Label returns the optional variable label.
|
||||||
|
func (v *Variable) Label() string {
|
||||||
|
return v.label
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accept implements the Visitor pattern.
|
||||||
|
func (v *Variable) Accept(visitor Visitor) {
|
||||||
|
visitor.VisitVariable(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the De Bruijn notation string representation.
|
||||||
|
func (v *Variable) String() string {
|
||||||
|
return Stringify(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewVariable creates a new De Bruijn variable with the given index and label.
|
||||||
|
func NewVariable(index int, label string) *Variable {
|
||||||
|
return &Variable{index: index, label: label}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
// Visitor interface for traversing De Bruijn expressions.
|
||||||
|
type Visitor interface {
|
||||||
|
VisitAbstraction(*Abstraction)
|
||||||
|
VisitApplication(*Application)
|
||||||
|
VisitVariable(*Variable)
|
||||||
|
}
|
||||||
76
pkg/debruijn/iterator.go
Normal file
76
pkg/debruijn/iterator.go
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
package debruijn
|
||||||
|
|
||||||
|
// Iterator provides depth-first traversal of De Bruijn expressions.
|
||||||
|
type Iterator struct {
|
||||||
|
trace []*Expression
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewIterator creates a new iterator starting at the given expression.
|
||||||
|
func NewIterator(expr *Expression) *Iterator {
|
||||||
|
return &Iterator{[]*Expression{expr}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Done returns true when the iterator has finished traversal.
|
||||||
|
func (i *Iterator) Done() bool {
|
||||||
|
return len(i.trace) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Current returns a pointer to the current expression.
|
||||||
|
func (i *Iterator) Current() *Expression {
|
||||||
|
if i.Done() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return i.trace[len(i.trace)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parent returns a pointer to the parent expression.
|
||||||
|
func (i *Iterator) Parent() *Expression {
|
||||||
|
if len(i.trace) < 2 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return i.trace[len(i.trace)-2]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swap replaces the current expression with the given expression.
|
||||||
|
func (i *Iterator) Swap(with Expression) {
|
||||||
|
current := i.Current()
|
||||||
|
if current != nil {
|
||||||
|
*current = with
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Back moves the iterator back to the parent expression.
|
||||||
|
func (i *Iterator) Back() bool {
|
||||||
|
if i.Done() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
i.trace = i.trace[:len(i.trace)-1]
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next advances the iterator to the next expression in leftmost-outermost order.
|
||||||
|
func (i *Iterator) Next() {
|
||||||
|
switch typed := (*i.Current()).(type) {
|
||||||
|
case *Abstraction:
|
||||||
|
i.trace = append(i.trace, &typed.body)
|
||||||
|
case *Application:
|
||||||
|
i.trace = append(i.trace, &typed.abstraction)
|
||||||
|
case *Variable:
|
||||||
|
for len(i.trace) > 1 {
|
||||||
|
if app, ok := (*i.Parent()).(*Application); ok {
|
||||||
|
if app.abstraction == *i.Current() {
|
||||||
|
i.Back()
|
||||||
|
i.trace = append(i.trace, &app.argument)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
i.Back()
|
||||||
|
}
|
||||||
|
|
||||||
|
i.trace = []*Expression{}
|
||||||
|
}
|
||||||
|
}
|
||||||
66
pkg/debruijn/reducer.go
Normal file
66
pkg/debruijn/reducer.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package debruijn
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/emitter"
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/expr"
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/reducer"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NormalOrderReducer implements normal order (leftmost-outermost) reduction
|
||||||
|
// for De Bruijn indexed lambda calculus expressions.
|
||||||
|
type NormalOrderReducer struct {
|
||||||
|
emitter.BaseEmitter[reducer.Event]
|
||||||
|
expression *Expression
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNormalOrderReducer creates a new normal order reducer.
|
||||||
|
func NewNormalOrderReducer(expression *Expression) *NormalOrderReducer {
|
||||||
|
return &NormalOrderReducer{
|
||||||
|
BaseEmitter: *emitter.New[reducer.Event](),
|
||||||
|
expression: expression,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expression returns the current expression state.
|
||||||
|
func (r *NormalOrderReducer) Expression() expr.Expression {
|
||||||
|
return *r.expression
|
||||||
|
}
|
||||||
|
|
||||||
|
// isViable checks if an expression is a redex (reducible expression).
|
||||||
|
// A redex is an application of an abstraction to an argument.
|
||||||
|
func isViable(e *Expression) (*Abstraction, Expression, bool) {
|
||||||
|
if e == nil {
|
||||||
|
return nil, nil, false
|
||||||
|
} else if app, appOk := (*e).(*Application); !appOk {
|
||||||
|
return nil, nil, false
|
||||||
|
} else if fn, fnOk := app.abstraction.(*Abstraction); !fnOk {
|
||||||
|
return nil, nil, false
|
||||||
|
} else {
|
||||||
|
return fn, app.argument, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reduce performs normal order reduction on a De Bruijn expression.
|
||||||
|
func (r *NormalOrderReducer) Reduce() {
|
||||||
|
r.Emit(reducer.StartEvent)
|
||||||
|
it := NewIterator(r.expression)
|
||||||
|
|
||||||
|
for !it.Done() {
|
||||||
|
if fn, arg, ok := isViable(it.Current()); !ok {
|
||||||
|
it.Next()
|
||||||
|
} else {
|
||||||
|
// Substitute arg for variable 0 in the body.
|
||||||
|
substituted := Substitute(fn.body, 0, Shift(arg, 1, 0))
|
||||||
|
// Shift down to account for the removed abstraction.
|
||||||
|
it.Swap(Shift(substituted, -1, 0))
|
||||||
|
|
||||||
|
r.Emit(reducer.StepEvent)
|
||||||
|
|
||||||
|
if _, _, ok := isViable(it.Parent()); ok {
|
||||||
|
it.Back()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Emit(reducer.StopEvent)
|
||||||
|
}
|
||||||
32
pkg/debruijn/shift.go
Normal file
32
pkg/debruijn/shift.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package debruijn
|
||||||
|
|
||||||
|
// Shift increments all free variable indices in an expression by the given amount.
|
||||||
|
// A variable is free if its index is >= the cutoff (depth of nested abstractions).
|
||||||
|
// This is necessary when substituting an expression into a different binding context.
|
||||||
|
func Shift(expr Expression, amount int, cutoff int) Expression {
|
||||||
|
switch e := expr.(type) {
|
||||||
|
case *Variable:
|
||||||
|
if e.index >= cutoff {
|
||||||
|
return NewVariable(e.index+amount, e.label)
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
|
||||||
|
case *Abstraction:
|
||||||
|
newBody := Shift(e.body, amount, cutoff+1)
|
||||||
|
if newBody == e.body {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
return NewAbstraction(newBody)
|
||||||
|
|
||||||
|
case *Application:
|
||||||
|
newAbs := Shift(e.abstraction, amount, cutoff)
|
||||||
|
newArg := Shift(e.argument, amount, cutoff)
|
||||||
|
if newAbs == e.abstraction && newArg == e.argument {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
return NewApplication(newAbs, newArg)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return expr
|
||||||
|
}
|
||||||
|
}
|
||||||
35
pkg/debruijn/stringify.go
Normal file
35
pkg/debruijn/stringify.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package debruijn
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stringifyVisitor struct {
|
||||||
|
builder strings.Builder
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *stringifyVisitor) VisitVariable(a *Variable) {
|
||||||
|
v.builder.WriteString(strconv.Itoa(a.index))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *stringifyVisitor) VisitAbstraction(f *Abstraction) {
|
||||||
|
v.builder.WriteRune('\\')
|
||||||
|
v.builder.WriteRune('.')
|
||||||
|
f.body.Accept(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *stringifyVisitor) VisitApplication(c *Application) {
|
||||||
|
v.builder.WriteRune('(')
|
||||||
|
c.abstraction.Accept(v)
|
||||||
|
v.builder.WriteRune(' ')
|
||||||
|
c.argument.Accept(v)
|
||||||
|
v.builder.WriteRune(')')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stringify converts a De Bruijn expression to its string representation.
|
||||||
|
func Stringify(e Expression) string {
|
||||||
|
b := &stringifyVisitor{builder: strings.Builder{}}
|
||||||
|
e.Accept(b)
|
||||||
|
return b.builder.String()
|
||||||
|
}
|
||||||
34
pkg/debruijn/substitute.go
Normal file
34
pkg/debruijn/substitute.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package debruijn
|
||||||
|
|
||||||
|
// Substitute replaces the variable at the given index with the replacement expression.
|
||||||
|
// The replacement is shifted appropriately as we descend into nested abstractions.
|
||||||
|
func Substitute(expr Expression, index int, replacement Expression) Expression {
|
||||||
|
switch e := expr.(type) {
|
||||||
|
case *Variable:
|
||||||
|
if e.index == index {
|
||||||
|
return replacement
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
|
||||||
|
case *Abstraction:
|
||||||
|
// When entering an abstraction, increment the target index and shift the
|
||||||
|
// replacement to account for the new binding context.
|
||||||
|
shiftedReplacement := Shift(replacement, 1, 0)
|
||||||
|
newBody := Substitute(e.body, index+1, shiftedReplacement)
|
||||||
|
if newBody == e.body {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
return NewAbstraction(newBody)
|
||||||
|
|
||||||
|
case *Application:
|
||||||
|
newAbs := Substitute(e.abstraction, index, replacement)
|
||||||
|
newArg := Substitute(e.argument, index, replacement)
|
||||||
|
if newAbs == e.abstraction && newArg == e.argument {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
return NewApplication(newAbs, newArg)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return expr
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package deltanet
|
package deltanet
|
||||||
|
|
||||||
|
/** ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
// A connection between exactly two nodes in a graph.
|
// A connection between exactly two nodes in a graph.
|
||||||
type Edge struct {
|
type Edge struct {
|
||||||
A, B Node
|
A, B Node
|
||||||
@@ -26,6 +28,8 @@ func (e Edge) IsPrincipleEdge() bool {
|
|||||||
return e.A.GetMainPort() == e && e.B.GetMainPort() == e
|
return e.A.GetMainPort() == e && e.B.GetMainPort() == e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
type Node interface {
|
type Node interface {
|
||||||
// Returns the principle port that the node is attached to.
|
// Returns the principle port that the node is attached to.
|
||||||
GetMainPort() Edge
|
GetMainPort() Edge
|
||||||
@@ -38,6 +42,8 @@ type Node interface {
|
|||||||
GetLabel() string
|
GetLabel() string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
type EraserNode struct {
|
type EraserNode struct {
|
||||||
Main Edge
|
Main Edge
|
||||||
}
|
}
|
||||||
@@ -46,6 +52,8 @@ func (n EraserNode) GetLabel() string { return "Ⓧ" }
|
|||||||
func (n EraserNode) GetMainPort() Edge { return n.Main }
|
func (n EraserNode) GetMainPort() Edge { return n.Main }
|
||||||
func (n EraserNode) GetAuxPorts() []Edge { return []Edge{} }
|
func (n EraserNode) GetAuxPorts() []Edge { return []Edge{} }
|
||||||
|
|
||||||
|
/** ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
type ReplicatorNode struct {
|
type ReplicatorNode struct {
|
||||||
Main Edge
|
Main Edge
|
||||||
Level uint
|
Level uint
|
||||||
@@ -60,6 +68,8 @@ func (n ReplicatorNode) GetAuxPorts() []Edge { return n.Aux }
|
|||||||
// Returns the level of the replicator node.
|
// Returns the level of the replicator node.
|
||||||
func (n ReplicatorNode) GetLevel() uint { return n.Level }
|
func (n ReplicatorNode) GetLevel() uint { return n.Level }
|
||||||
|
|
||||||
|
/** ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
type FanNode struct {
|
type FanNode struct {
|
||||||
Label string
|
Label string
|
||||||
Main Edge
|
Main Edge
|
||||||
@@ -70,6 +80,8 @@ func (n FanNode) GetLabel() string { return n.Label }
|
|||||||
func (n FanNode) GetMainPort() Edge { return n.Main }
|
func (n FanNode) GetMainPort() Edge { return n.Main }
|
||||||
func (n FanNode) GetAuxPorts() []Edge { return []Edge{n.Left, n.Right} }
|
func (n FanNode) GetAuxPorts() []Edge { return []Edge{n.Left, n.Right} }
|
||||||
|
|
||||||
|
/** ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
type TerminalNode struct {
|
type TerminalNode struct {
|
||||||
Label string
|
Label string
|
||||||
Main Edge
|
Main Edge
|
||||||
@@ -78,3 +90,5 @@ type TerminalNode struct {
|
|||||||
func (n TerminalNode) GetLabel() string { return n.Label }
|
func (n TerminalNode) GetLabel() string { return n.Label }
|
||||||
func (n TerminalNode) GetMainPort() Edge { return n.Main }
|
func (n TerminalNode) GetMainPort() Edge { return n.Main }
|
||||||
func (n TerminalNode) GetAuxPorts() []Edge { return []Edge{} }
|
func (n TerminalNode) GetAuxPorts() []Edge { return []Edge{} }
|
||||||
|
|
||||||
|
/** ------------------------------------------------------------------------- */
|
||||||
|
|||||||
@@ -2,53 +2,45 @@ package emitter
|
|||||||
|
|
||||||
import "git.maximhutz.com/max/lambda/pkg/set"
|
import "git.maximhutz.com/max/lambda/pkg/set"
|
||||||
|
|
||||||
type Observer struct {
|
type Emitter[E comparable] interface {
|
||||||
fn func()
|
On(E, func()) Listener[E]
|
||||||
message string
|
Off(Listener[E])
|
||||||
emitter *Emitter
|
Emit(E)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Emitter struct {
|
type BaseEmitter[E comparable] struct {
|
||||||
listeners map[string]*set.Set[*Observer]
|
listeners map[E]*set.Set[Listener[E]]
|
||||||
}
|
}
|
||||||
|
|
||||||
func Ignore[T any](fn func()) func(T) {
|
func (e *BaseEmitter[E]) On(kind E, fn func()) Listener[E] {
|
||||||
return func(T) { fn() }
|
if e.listeners[kind] == nil {
|
||||||
|
e.listeners[kind] = set.New[Listener[E]]()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Emitter) On(message string, fn func()) *Observer {
|
listener := &BaseListener[E]{kind, fn}
|
||||||
observer := &Observer{
|
e.listeners[kind].Add(listener)
|
||||||
fn: fn,
|
return listener
|
||||||
message: message,
|
|
||||||
emitter: e,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if e.listeners == nil {
|
func (e *BaseEmitter[E]) Off(listener Listener[E]) {
|
||||||
e.listeners = map[string]*set.Set[*Observer]{}
|
kind := listener.Kind()
|
||||||
|
if e.listeners[kind] != nil {
|
||||||
|
e.listeners[kind].Remove(listener)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if e.listeners[message] == nil {
|
func (e *BaseEmitter[E]) Emit(event E) {
|
||||||
e.listeners[message] = set.New[*Observer]()
|
if e.listeners[event] == nil {
|
||||||
|
e.listeners[event] = set.New[Listener[E]]()
|
||||||
}
|
}
|
||||||
|
|
||||||
e.listeners[message].Add(observer)
|
for listener := range e.listeners[event].Items() {
|
||||||
return observer
|
listener.Run()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *Observer) Off() {
|
func New[E comparable]() *BaseEmitter[E] {
|
||||||
if o.emitter.listeners[o.message] == nil {
|
return &BaseEmitter[E]{
|
||||||
return
|
listeners: map[E]*set.Set[Listener[E]]{},
|
||||||
}
|
|
||||||
|
|
||||||
o.emitter.listeners[o.message].Remove(o)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Emitter) Emit(message string) {
|
|
||||||
if e.listeners[message] == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for listener := range *e.listeners[message] {
|
|
||||||
listener.fn()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
19
pkg/emitter/listener.go
Normal file
19
pkg/emitter/listener.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package emitter
|
||||||
|
|
||||||
|
type Listener[E comparable] interface {
|
||||||
|
Kind() E
|
||||||
|
Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
type BaseListener[E comparable] struct {
|
||||||
|
kind E
|
||||||
|
fn func()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l BaseListener[E]) Kind() E {
|
||||||
|
return l.kind
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l BaseListener[E]) Run() {
|
||||||
|
l.fn()
|
||||||
|
}
|
||||||
11
pkg/expr/expr.go
Normal file
11
pkg/expr/expr.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
// Package expr provides the abstract Expression interface for all evaluatable
|
||||||
|
// expression types in the lambda interpreter.
|
||||||
|
package expr
|
||||||
|
|
||||||
|
// Expression is the base interface for all evaluatable expression types.
|
||||||
|
// Different evaluation modes (lambda calculus, SKI combinators, typed lambda
|
||||||
|
// calculus, etc.) implement this interface with their own concrete types.
|
||||||
|
type Expression interface {
|
||||||
|
// String returns a human-readable representation of the expression.
|
||||||
|
String() string
|
||||||
|
}
|
||||||
@@ -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,60 +1,92 @@
|
|||||||
package lambda
|
package lambda
|
||||||
|
|
||||||
|
import "git.maximhutz.com/max/lambda/pkg/expr"
|
||||||
|
|
||||||
|
// Expression is the interface for all lambda calculus expression types.
|
||||||
|
// It embeds the general expr.Expression interface for cross-mode compatibility.
|
||||||
type Expression interface {
|
type Expression interface {
|
||||||
|
expr.Expression
|
||||||
Accept(Visitor)
|
Accept(Visitor)
|
||||||
Copy() Expression
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
type Abstraction struct {
|
type Abstraction struct {
|
||||||
Parameter string
|
parameter string
|
||||||
Body Expression
|
body Expression
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Abstraction) Copy() Expression {
|
func (a *Abstraction) Parameter() string {
|
||||||
return NewAbstraction(a.Parameter, a.Body.Copy())
|
return a.parameter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Abstraction) Body() Expression {
|
||||||
|
return a.body
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Abstraction) Accept(v Visitor) {
|
func (a *Abstraction) Accept(v Visitor) {
|
||||||
v.VisitAbstraction(a)
|
v.VisitAbstraction(a)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAbstraction(parameter string, body Expression) *Abstraction {
|
func (a *Abstraction) String() string {
|
||||||
return &Abstraction{Parameter: parameter, Body: body}
|
return Stringify(a)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewAbstraction(parameter string, body Expression) *Abstraction {
|
||||||
|
return &Abstraction{parameter: parameter, body: body}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
type Application struct {
|
type Application struct {
|
||||||
Abstraction Expression
|
abstraction Expression
|
||||||
Argument Expression
|
argument Expression
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Application) Copy() Expression {
|
func (a *Application) Abstraction() Expression {
|
||||||
return NewApplication(a.Abstraction.Copy(), a.Argument.Copy())
|
return a.abstraction
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Application) Argument() Expression {
|
||||||
|
return a.argument
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Application) Accept(v Visitor) {
|
func (a *Application) Accept(v Visitor) {
|
||||||
v.VisitApplication(a)
|
v.VisitApplication(a)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewApplication(function Expression, argument Expression) *Application {
|
func (a *Application) String() string {
|
||||||
return &Application{Abstraction: function, Argument: argument}
|
return Stringify(a)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewApplication(abstraction Expression, argument Expression) *Application {
|
||||||
|
return &Application{abstraction: abstraction, argument: argument}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
type Variable struct {
|
type Variable struct {
|
||||||
Value string
|
value string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *Variable) Copy() Expression {
|
func (v *Variable) Value() string {
|
||||||
return NewVariable(v.Value)
|
return v.value
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *Variable) Accept(visitor Visitor) {
|
func (v *Variable) Accept(visitor Visitor) {
|
||||||
visitor.VisitVariable(v)
|
visitor.VisitVariable(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewVariable(name string) *Variable {
|
func (v *Variable) String() string {
|
||||||
return &Variable{Value: name}
|
return Stringify(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewVariable(name string) *Variable {
|
||||||
|
return &Variable{value: name}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
type Visitor interface {
|
type Visitor interface {
|
||||||
VisitAbstraction(*Abstraction)
|
VisitAbstraction(*Abstraction)
|
||||||
VisitApplication(*Application)
|
VisitApplication(*Application)
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ import "git.maximhutz.com/max/lambda/pkg/set"
|
|||||||
func GetFreeVariables(e Expression) *set.Set[string] {
|
func GetFreeVariables(e Expression) *set.Set[string] {
|
||||||
switch e := e.(type) {
|
switch e := e.(type) {
|
||||||
case *Variable:
|
case *Variable:
|
||||||
return set.New(e.Value)
|
return set.New(e.value)
|
||||||
case *Abstraction:
|
case *Abstraction:
|
||||||
vars := GetFreeVariables(e.Body)
|
vars := GetFreeVariables(e.body)
|
||||||
vars.Remove(e.Parameter)
|
vars.Remove(e.parameter)
|
||||||
return vars
|
return vars
|
||||||
case *Application:
|
case *Application:
|
||||||
vars := GetFreeVariables(e.Abstraction)
|
vars := GetFreeVariables(e.abstraction)
|
||||||
vars.Merge(GetFreeVariables(e.Argument))
|
vars.Merge(GetFreeVariables(e.argument))
|
||||||
return vars
|
return vars
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ package lambda
|
|||||||
func IsFreeVariable(n string, e Expression) bool {
|
func IsFreeVariable(n string, e Expression) bool {
|
||||||
switch e := e.(type) {
|
switch e := e.(type) {
|
||||||
case *Variable:
|
case *Variable:
|
||||||
return e.Value == n
|
return e.value == n
|
||||||
case *Abstraction:
|
case *Abstraction:
|
||||||
return e.Parameter != n && IsFreeVariable(n, e.Body)
|
return e.parameter != n && IsFreeVariable(n, e.body)
|
||||||
case *Application:
|
case *Application:
|
||||||
return IsFreeVariable(n, e.Abstraction) || IsFreeVariable(n, e.Argument)
|
return IsFreeVariable(n, e.abstraction) || IsFreeVariable(n, e.argument)
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
68
pkg/lambda/iterator.go
Normal file
68
pkg/lambda/iterator.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package lambda
|
||||||
|
|
||||||
|
type Iterator struct {
|
||||||
|
trace []*Expression
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIterator(expr *Expression) *Iterator {
|
||||||
|
return &Iterator{[]*Expression{expr}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Iterator) Done() bool {
|
||||||
|
return len(i.trace) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Iterator) Current() *Expression {
|
||||||
|
if i.Done() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return i.trace[len(i.trace)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Iterator) Parent() *Expression {
|
||||||
|
if len(i.trace) < 2 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return i.trace[len(i.trace)-2]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Iterator) Swap(with Expression) {
|
||||||
|
current := i.Current()
|
||||||
|
if current != nil {
|
||||||
|
*current = with
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Iterator) Back() bool {
|
||||||
|
if i.Done() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
i.trace = i.trace[:len(i.trace)-1]
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Iterator) Next() {
|
||||||
|
switch typed := (*i.Current()).(type) {
|
||||||
|
case *Abstraction:
|
||||||
|
i.trace = append(i.trace, &typed.body)
|
||||||
|
case *Application:
|
||||||
|
i.trace = append(i.trace, &typed.abstraction)
|
||||||
|
case *Variable:
|
||||||
|
for len(i.trace) > 1 {
|
||||||
|
if app, ok := (*i.Parent()).(*Application); ok {
|
||||||
|
if app.abstraction == *i.Current() {
|
||||||
|
i.Back()
|
||||||
|
i.trace = append(i.trace, &app.argument)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
i.Back()
|
||||||
|
}
|
||||||
|
|
||||||
|
i.trace = []*Expression{}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,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
|
|
||||||
}
|
|
||||||
61
pkg/lambda/reducer.go
Normal file
61
pkg/lambda/reducer.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package lambda
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/emitter"
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/expr"
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/reducer"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NormalOrderReducer implements normal order (leftmost-outermost) reduction
|
||||||
|
// for lambda calculus expressions.
|
||||||
|
type NormalOrderReducer struct {
|
||||||
|
emitter.BaseEmitter[reducer.Event]
|
||||||
|
expression *Expression
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNormalOrderReducer creates a new normal order reducer.
|
||||||
|
func NewNormalOrderReducer(expression *Expression) *NormalOrderReducer {
|
||||||
|
return &NormalOrderReducer{
|
||||||
|
BaseEmitter: *emitter.New[reducer.Event](),
|
||||||
|
expression: expression,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expression returns the current expression state.
|
||||||
|
func (r *NormalOrderReducer) Expression() expr.Expression {
|
||||||
|
return *r.expression
|
||||||
|
}
|
||||||
|
|
||||||
|
func isViable(e *Expression) (*Abstraction, Expression, bool) {
|
||||||
|
if e == nil {
|
||||||
|
return nil, nil, false
|
||||||
|
} else if app, appOk := (*e).(*Application); !appOk {
|
||||||
|
return nil, nil, false
|
||||||
|
} else if fn, fnOk := app.abstraction.(*Abstraction); !fnOk {
|
||||||
|
return nil, nil, false
|
||||||
|
} else {
|
||||||
|
return fn, app.argument, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reduce performs normal order reduction on a lambda expression.
|
||||||
|
// The expression must be a lambda.Expression; other types are returned unchanged.
|
||||||
|
func (r *NormalOrderReducer) Reduce() {
|
||||||
|
r.Emit(reducer.StartEvent)
|
||||||
|
it := NewIterator(r.expression)
|
||||||
|
|
||||||
|
for !it.Done() {
|
||||||
|
if fn, arg, ok := isViable(it.Current()); !ok {
|
||||||
|
it.Next()
|
||||||
|
} else {
|
||||||
|
it.Swap(Substitute(fn.body, fn.parameter, arg))
|
||||||
|
r.Emit(reducer.StepEvent)
|
||||||
|
|
||||||
|
if _, _, ok := isViable(it.Parent()); ok {
|
||||||
|
it.Back()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Emit(reducer.StopEvent)
|
||||||
|
}
|
||||||
@@ -1,19 +1,38 @@
|
|||||||
package lambda
|
package lambda
|
||||||
|
|
||||||
func Rename(e Expression, target string, substitute string) {
|
func Rename(expr Expression, target string, newName string) Expression {
|
||||||
switch e := e.(type) {
|
switch e := expr.(type) {
|
||||||
case *Variable:
|
case *Variable:
|
||||||
if e.Value == target {
|
if e.value == target {
|
||||||
e.Value = substitute
|
return NewVariable(newName)
|
||||||
}
|
}
|
||||||
|
return e
|
||||||
|
|
||||||
case *Abstraction:
|
case *Abstraction:
|
||||||
if e.Parameter == target {
|
newParam := e.parameter
|
||||||
e.Parameter = substitute
|
if e.parameter == target {
|
||||||
|
newParam = newName
|
||||||
}
|
}
|
||||||
|
|
||||||
Rename(e.Body, target, substitute)
|
newBody := Rename(e.body, target, newName)
|
||||||
|
|
||||||
|
if newParam == e.parameter && newBody == e.body {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewAbstraction(newParam, newBody)
|
||||||
|
|
||||||
case *Application:
|
case *Application:
|
||||||
Rename(e.Abstraction, target, substitute)
|
newAbs := Rename(e.abstraction, target, newName)
|
||||||
Rename(e.Argument, target, substitute)
|
newArg := Rename(e.argument, target, newName)
|
||||||
|
|
||||||
|
if newAbs == e.abstraction && newArg == e.argument {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewApplication(newAbs, newArg)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return expr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,21 +7,21 @@ type stringifyVisitor struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (v *stringifyVisitor) VisitVariable(a *Variable) {
|
func (v *stringifyVisitor) VisitVariable(a *Variable) {
|
||||||
v.builder.WriteString(a.Value)
|
v.builder.WriteString(a.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *stringifyVisitor) VisitAbstraction(f *Abstraction) {
|
func (v *stringifyVisitor) VisitAbstraction(f *Abstraction) {
|
||||||
v.builder.WriteRune('\\')
|
v.builder.WriteRune('\\')
|
||||||
v.builder.WriteString(f.Parameter)
|
v.builder.WriteString(f.parameter)
|
||||||
v.builder.WriteRune('.')
|
v.builder.WriteRune('.')
|
||||||
f.Body.Accept(v)
|
f.body.Accept(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *stringifyVisitor) VisitApplication(c *Application) {
|
func (v *stringifyVisitor) VisitApplication(c *Application) {
|
||||||
v.builder.WriteRune('(')
|
v.builder.WriteRune('(')
|
||||||
c.Abstraction.Accept(v)
|
c.abstraction.Accept(v)
|
||||||
v.builder.WriteRune(' ')
|
v.builder.WriteRune(' ')
|
||||||
c.Argument.Accept(v)
|
c.argument.Accept(v)
|
||||||
v.builder.WriteRune(')')
|
v.builder.WriteRune(')')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,46 @@
|
|||||||
package lambda
|
package lambda
|
||||||
|
|
||||||
func Substitute(e *Expression, target string, replacement Expression) {
|
func Substitute(expr Expression, target string, replacement Expression) Expression {
|
||||||
switch typed := (*e).(type) {
|
switch e := expr.(type) {
|
||||||
case *Variable:
|
case *Variable:
|
||||||
if typed.Value == target {
|
if e.value == target {
|
||||||
*e = replacement.Copy()
|
return replacement
|
||||||
}
|
}
|
||||||
|
return e
|
||||||
|
|
||||||
case *Abstraction:
|
case *Abstraction:
|
||||||
if typed.Parameter == target {
|
if e.parameter == target {
|
||||||
return
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
if IsFreeVariable(typed.Parameter, replacement) {
|
body := e.body
|
||||||
replacementFreeVars := GetFreeVariables(replacement)
|
param := e.parameter
|
||||||
used := GetFreeVariables(typed.Body)
|
if IsFreeVariable(param, replacement) {
|
||||||
used.Merge(replacementFreeVars)
|
freeVars := GetFreeVariables(replacement)
|
||||||
freshVar := GenerateFreshName(used)
|
freeVars.Merge(GetFreeVariables(body))
|
||||||
Rename(typed, typed.Parameter, freshVar)
|
freshVar := GenerateFreshName(freeVars)
|
||||||
|
body = Rename(body, param, freshVar)
|
||||||
|
param = freshVar
|
||||||
}
|
}
|
||||||
|
|
||||||
Substitute(&typed.Body, target, replacement)
|
newBody := Substitute(body, target, replacement)
|
||||||
|
if newBody == body && param == e.parameter {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewAbstraction(param, newBody)
|
||||||
|
|
||||||
case *Application:
|
case *Application:
|
||||||
Substitute(&typed.Abstraction, target, replacement)
|
newAbs := Substitute(e.abstraction, target, replacement)
|
||||||
Substitute(&typed.Argument, target, replacement)
|
newArg := Substitute(e.argument, target, replacement)
|
||||||
|
|
||||||
|
if newAbs == e.abstraction && newArg == e.argument {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewApplication(newAbs, newArg)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return expr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
13
pkg/reducer/events.go
Normal file
13
pkg/reducer/events.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package reducer
|
||||||
|
|
||||||
|
// Event represents lifecycle events during reduction.
|
||||||
|
type Event int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// StartEvent is emitted before reduction begins.
|
||||||
|
StartEvent Event = iota
|
||||||
|
// StepEvent is emitted after each reduction step.
|
||||||
|
StepEvent
|
||||||
|
// StopEvent is emitted after reduction completes.
|
||||||
|
StopEvent
|
||||||
|
)
|
||||||
27
pkg/reducer/reducer.go
Normal file
27
pkg/reducer/reducer.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// Package reducer provides the abstract Reducer interface for all expression
|
||||||
|
// reduction strategies.
|
||||||
|
package reducer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/emitter"
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/expr"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Reducer defines the interface for expression reduction strategies.
|
||||||
|
// Different evaluation modes (normal order, applicative order, SKI combinators,
|
||||||
|
// etc.) implement this interface with their own reduction logic.
|
||||||
|
//
|
||||||
|
// Reducers also implement the Emitter interface to allow plugins to observe
|
||||||
|
// reduction lifecycle events (Start, Step, Stop).
|
||||||
|
type Reducer interface {
|
||||||
|
emitter.Emitter[Event]
|
||||||
|
|
||||||
|
// Reduce performs all reduction steps on the expression.
|
||||||
|
// Emits StartEvent before reduction, StepEvent after each step, and
|
||||||
|
// StopEvent after completion.
|
||||||
|
// Returns the final reduced expression.
|
||||||
|
Reduce()
|
||||||
|
|
||||||
|
// Expression returns the current expression state.
|
||||||
|
Expression() expr.Expression
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@ type Expression interface {
|
|||||||
IsExpression()
|
IsExpression()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
type Abstraction struct {
|
type Abstraction struct {
|
||||||
Parameters []string
|
Parameters []string
|
||||||
Body Expression
|
Body Expression
|
||||||
@@ -28,6 +30,8 @@ func (Application) IsExpression() {}
|
|||||||
func (Atom) IsExpression() {}
|
func (Atom) IsExpression() {}
|
||||||
func (Clause) IsExpression() {}
|
func (Clause) IsExpression() {}
|
||||||
|
|
||||||
|
/** ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
func NewAbstraction(parameter []string, body Expression) *Abstraction {
|
func NewAbstraction(parameter []string, body Expression) *Abstraction {
|
||||||
return &Abstraction{Parameters: parameter, Body: body}
|
return &Abstraction{Parameters: parameter, Body: body}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ type Statement interface {
|
|||||||
IsStatement()
|
IsStatement()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
type LetStatement struct {
|
type LetStatement struct {
|
||||||
Name string
|
Name string
|
||||||
Parameters []string
|
Parameters []string
|
||||||
@@ -17,6 +19,8 @@ type DeclareStatement struct {
|
|||||||
func (LetStatement) IsStatement() {}
|
func (LetStatement) IsStatement() {}
|
||||||
func (DeclareStatement) IsStatement() {}
|
func (DeclareStatement) IsStatement() {}
|
||||||
|
|
||||||
|
/** ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
func NewLet(name string, parameters []string, body Expression) *LetStatement {
|
func NewLet(name string, parameters []string, body Expression) *LetStatement {
|
||||||
return &LetStatement{Name: name, Parameters: parameters, Body: body}
|
return &LetStatement{Name: name, Parameters: parameters, Body: body}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,6 +77,21 @@ func getToken(i *iterator.Iterator[rune]) (*Token, error) {
|
|||||||
}
|
}
|
||||||
case letter == ';':
|
case letter == ';':
|
||||||
return NewHardBreak(index), nil
|
return NewHardBreak(index), nil
|
||||||
|
case letter == '#':
|
||||||
|
// Skip everything until the next newline or EOF.
|
||||||
|
for !i.Done() {
|
||||||
|
r, err := i.Next()
|
||||||
|
if err != nil {
|
||||||
|
return nil, trace.Wrap(err, "error while parsing comment")
|
||||||
|
}
|
||||||
|
|
||||||
|
if r == '\n' {
|
||||||
|
// Put the newline back so it can be processed as a soft break.
|
||||||
|
i.Back()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
case unicode.IsSpace(letter):
|
case unicode.IsSpace(letter):
|
||||||
return nil, nil
|
return nil, nil
|
||||||
case isVariable(letter):
|
case isVariable(letter):
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package set
|
package set
|
||||||
|
|
||||||
|
import "iter"
|
||||||
|
|
||||||
type Set[T comparable] map[T]bool
|
type Set[T comparable] map[T]bool
|
||||||
|
|
||||||
func (s *Set[T]) Add(items ...T) {
|
func (s *Set[T]) Add(items ...T) {
|
||||||
@@ -34,6 +36,16 @@ func (s Set[T]) ToList() []T {
|
|||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s Set[T]) Items() iter.Seq[T] {
|
||||||
|
return func(yield func(T) bool) {
|
||||||
|
for item := range s {
|
||||||
|
if !yield(item) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func New[T comparable](items ...T) *Set[T] {
|
func New[T comparable](items ...T) *Set[T] {
|
||||||
result := &Set[T]{}
|
result := &Set[T]{}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
0 := \f.\x.x
|
||||||
inc n := \f x.(f (n f x))
|
inc n := \f x.(f (n f x))
|
||||||
exp n m := (m n)
|
exp n m := (m n)
|
||||||
|
print n := (n F X)
|
||||||
|
|
||||||
N := (inc (inc (inc (inc (inc 0)))))
|
N := (inc (inc (inc (inc (inc 0)))))
|
||||||
|
|
||||||
(exp N N)
|
(print (exp N N))
|
||||||
1
tests/church_6^6.expected
Normal file
1
tests/church_6^6.expected
Normal file
File diff suppressed because one or more lines are too long
8
tests/church_6^6.test
Normal file
8
tests/church_6^6.test
Normal file
@@ -0,0 +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 (inc 0))))))
|
||||||
|
|
||||||
|
(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